一个能够调用工具的 Agent,最小实现并不复杂。模型接收消息,返回文本或 Tool Call;程序执行工具,把 Tool Result 追加到消息列表,再继续请求模型。Vercel AI SDK 的 ToolLoopAgent 封装的就是这一类多 Step 执行:模型、Instructions、Tools 和停止条件集中在 Agent 定义中,由 SDK 驱动后续循环。[1]
把这层抽象还原以后,核心逻辑接近下面这段代码:
这已经是一个完整的 Tool Loop。模型可以根据工具结果继续决策,一次用户输入也可以触发多次模型调用。
它同时存在一个很明确的边界:整个执行生命周期仍然被当前函数调用包住。
messages 存在于内存中,正在执行哪个 Tool 可以由局部变量表达,取消可以依赖当前调用的 AbortSignal,函数返回以后,本次执行随之结束。如果调用方不要求暂停、恢复、中途输入和跨连接继续执行,这样的结构已经足够。
Agent Runtime 开始成为独立问题,通常发生在执行生命周期超出这次函数调用之后。
这个边界比 Tool Calling、Memory 等能力清单更适合判断一个系统是否真正需要 Runtime。
Tool Loop 能管理什么
先限制问题范围。Tool Loop 负责的是一条局部执行链:
User Input
↓
Model Request
↓
Assistant Message
↓
Tool Call
↓
Tool Execution
↓
Tool Result
↓
Next Model Request
它主要处理三件事:
- 什么时候请求模型;
- 模型要求调用工具时如何执行;
- Tool Result 如何回到下一次模型请求。
这三件事都发生在一次连续执行内部,因此局部变量和函数栈天然就是状态容器。
例如下面几个状态都可以直接保存在当前调用中:
此时还没有必要引入更复杂的对象模型。所谓 Runtime,如果只是把这些局部变量挪进一个 Class,并不会自动获得新的架构价值。
真正的变化来自状态开始拥有不同的生命周期。
第一个变化:执行状态不能再依附请求
Web 应用最容易暴露这个问题。
假设浏览器发起请求,后端开始运行 Agent:
Browser Request
│
▼
Agent Loop
│
├── Model
├── Tool
├── Model
└── Tool
如果整个执行必须依附 HTTP/SSE 连接,那么连接断开通常意味着调用链被释放。对于普通请求这是合理的:客户端离开,请求结束。
Agent 的执行经常需要另一种语义:连接只负责传输,任务本身仍由后端继续管理。客户端稍后重新连接时,应当读取当前状态并继续接收结果。
此时至少出现两个生命周期:
Connection
──────────────>
Execution
──────────────────────────────>
连接不再拥有执行。
一旦做出这个拆分,下面这些状态就必须有独立于网络请求的所有者:
这些状态已经不能可靠地保存在一次 Controller 调用或 SSE handler 中。
第二个变化:执行期间仍然会有新输入
普通函数调用有一个稳定前提:调用开始时参数已经确定。
Agent 不一定满足这个前提。当前 Tool 尚未执行完时,用户可能补充要求;插件可能注入新的环境信息;调度器可能发送后续任务;其他 Agent 也可能向当前 Agent 传递结果。
如果 Agent 正在运行时再次直接调用:
系统必须回答一个很具体的问题:这条消息属于哪一段执行?
它可能有几种完全不同的语义:
这些差异已经无法通过一个 messages.push() 表达。
输入需要进入某个受控的调度结构,由 Runtime 在稳定边界消费。Pi 的 Steering / Follow-up Queue,以及 DeepSeek Harness 的 Inbox,处理的就是这一层问题。[2][3]
因此,Runtime 除了执行 Model 和 Tool,还开始承担输入交付语义。
第三个变化:长期历史与当前执行状态分离
Tool Loop 中的 messages 通常同时扮演两种角色:
历史记录
+
下一次模型请求的输入
在简单实现里这没有问题。随着会话变长,两者会逐渐分离。
长期 Session 可能保留完整历史;模型当前能够看到的内容则受到窗口大小、Compaction、Memory、分支选择和 Agent Policy 影响。
因此更准确的数据关系是:
Session 保存的是长期事实,Context 描述当前一次模型计算可见的信息。
这两个概念如果继续共用同一个 messages[],后续会出现几个典型问题:
- Compaction 以后是否覆盖原始历史;
- UI 是否还能展示被压缩掉的消息;
- 同一个 Session 交给不同 Agent 时是否能够使用不同 Context Policy;
- Tool 执行中的临时状态是否应该进入长期历史;
- Provider Message 与应用内部 Message 是否必须使用相同结构。
Runtime 因此还需要负责从长期状态构造当前计算视图。
Runtime 的核心是状态所有权
到这里可以重新看最开始的 Tool Loop。
原先所有状态都隐含地由当前函数拥有:
function call
├── messages
├── tool state
├── abort signal
└── control flow
生命周期拆开以后,状态需要重新分配所有权:
Runtime 的价值由此变得具体:它为跨越单次函数调用的执行状态提供稳定所有者,并负责这些状态之间的推进。
一个最小结构可以表示为:
这张图里,Loop 只是 Runtime 的一个组成部分。Runtime 还需要维护 Loop 外部的状态和控制关系。
Runtime 还需要区分配置、活动状态与持久事实
把执行状态从函数调用中抽出来以后,另一个问题随之出现:哪些内容应该长期存在,哪些内容只在当前活动期间有效。
可以把 Runtime 周围的数据分成三类:
三类数据经常被放在同一个 Agent 对象里,但它们的生命周期不同。
AgentDefinition 可以被多个 Session 复用,也可能通过版本号长期存在。它描述“这个 Agent 具备什么能力”。
Live Runtime State 只描述某个进程当前正在发生的事情。AbortController、active promise、网络 stream 句柄都属于这一层。进程结束以后,这些对象没有直接恢复价值。
Durable Session State 需要跨进程保存。它应当记录足够的事实,使新 Runtime 能够重新建立当前工作视图。
因此恢复 Agent 时,更稳定的路径是根据持久事实创建新的 Runtime,而非直接反序列化旧的进程内对象:
Durable Session Facts
+
Agent Definition / Version
↓
create new Runtime
↓
rebuild Context / pending work
↓
continue
这条边界会直接影响持久化设计。把所有内存字段都序列化,往往只能得到一份难以升级、也难以跨进程恢复的 Runtime Snapshot。更稳定的做法是保存能够重建运行状态的事实,再由新 Runtime 创建进程内对象。
从这个角度看,Runtime 与 Persistence 是相邻层次:Runtime 管理活动状态,Persistence 负责保存恢复所需事实。两者可以紧密协作,但不需要共用同一套对象。
Runtime 更接近状态机,而非一组工具函数
当 Runtime 拥有 idle / running / waiting / cancelled 等状态以后,很多 API 的合法性也由当前状态决定。
例如 Pi 在 active 状态下禁止再次 prompt(),允许 steer()、followUp() 和 abort();idle 状态下则可以开始新的 Prompt。
可以抽象成:
Runtime 因此不仅保存数据,还约束哪些状态转换是合法的。
这一点对 API 设计很重要。若外部接口只是若干无状态函数,调用方必须自己理解当前执行是否存在、某条输入应该进入哪里、什么时候可以取消。把这些约束收进 Runtime,可以让错误状态更早暴露,也让上层产品获得更稳定的行为契约。
Pi 为什么适合作为 Runtime 样本
Pi 的 Agent 与底层 agent-loop.ts 分得比较清楚。
低层 Loop 负责模型请求、Tool Call、Tool Result、Steering 和 Follow-up 的执行推进;外层 Agent 被源码描述为 low-level agent loop 的 stateful wrapper。[2]
Agent 持有的状态大致包括:
systemPrompt
model
thinkingLevel
tools
messages
isStreaming
streamingMessage
pendingToolCalls
errorMessage
这些字段形成了一个明确的分层:
messages
已经完成的稳定 transcript
streamingMessage
当前正在生成、仍会变化的 Assistant Message
pendingToolCalls
当前执行中的 Tool 状态
这些状态并没有全部塞进 messages。
Pi 还维护 Steering Queue 和 Follow-up Queue。Agent 活动期间禁止再次直接 prompt() 创建第二条主动执行链,新消息需要通过明确的输入控制 API 交给当前 Runtime。[2]
这已经符合前面推导出的 Runtime 特征:持续状态、单一活动执行链、输入调度、取消和生命周期事件具有明确所有者。
DeepSeek Harness 把 Runtime 再向外抽象一层
DeepSeek Harness 对 Agent 的抽象更偏 Harness。
公开 Agent handle 暴露:
session
inbox
status
ctx
cancel()
whenIdle()
send()
steer()
followup()
inject()
具体 Agent Loop 则由插件提供。[3]
这里出现了另一个重要变化:应用与插件依赖稳定 Agent 接口,具体 Loop 实现可以替换。
这意味着 Runtime 的“执行机制”本身也可以替换。Agent Loop、Session、Tool Registry、Model Adapter 不必全部固化在一个核心类里。这个方向最终会进入 Harness 和 Plugin Runtime,本系列后面的 Cordis / DeepSeek Harness 单元会单独处理。
在第一篇里只需要保留一个判断:
Runtime 解决执行状态的生命周期和推进问题;Harness 进一步解决 Runtime 能力如何被组合、替换和扩展。
两层关注点不同。
Vercel AI SDK 提供的是另一侧参照
Vercel AI SDK 的价值在于它把 Web 应用中几个经常混在一起的层次拆开。
ToolLoopAgent 负责多 Step Agent 执行;AI SDK UI 则提供 useChat、Transport、UI Message、消息持久化和 Stream Resume 等能力。[1][6]
因此可以把它放到下面这张图中理解:
Browser UI
│
▼
Transport
│
▼
Application / Agent Execution
│
▼
Model + Tools
AI SDK 并不要求所有产品都采用 Pi 或 DSH 那种长生命周期 Agent 对象,但它提供了一个很清楚的参照:UI 状态、网络传输、Agent 执行和模型消息可以是不同层次。
这也是后续讨论 Web Agent Runtime 时的重要基础。
Runtime 和 Agent Framework 的边界也需要控制
Runtime 独立以后,很容易继续把所有 Agent 能力都归入这一层。这样会重新形成一个不断膨胀的核心。
可以用“是否直接参与当前执行推进”作为初步边界。
下面这些能力通常与 Runtime 强相关:
Context preparation
Model invocation
Tool execution
Input delivery
Cancellation
Lifecycle state
它们会直接影响当前 Loop 下一步如何运行。
而下面这些能力更可能位于 Harness 或 Application 层:
Plugin discovery
Skill marketplace
Project management
Workflow definition
UI layout
Billing
Tenant permission
这些能力可以向 Runtime 注册 Tool、Hook 或 Policy,但没有必要成为低层执行对象的固有字段。
Pi 与 DSH 可以作为两种扩展边界的对照。Pi 倾向保留一个较小的 Agent Core,再通过 Extension 扩展行为;DSH 进一步让 Agent Loop、Session、Tool Registry 等能力本身也进入 Plugin Runtime。两种方案的扩展边界不同,但都在处理同一个问题:如何避免 Agent Core 随产品能力增长而持续膨胀。
因此 Runtime 的抽象也应保持克制。它需要足够完整地拥有执行生命周期,同时给外层 Harness 留出组合空间。
一个实用的状态归属检查方法
设计新字段或新能力时,可以连续检查四个问题:
例如 pendingToolCalls 只在当前执行期间有效,又直接影响 UI 和取消控制,适合由 Runtime 持有。
Session Branch 需要跨进程恢复,对当前一次 Tool 调用没有直接控制作用,更适合归入 Session Persistence。
Run Status 需要被产品独立查询和恢复,可以放在 Application Runtime。
这种检查方法比按“Agent 相关功能都放进 Agent 类”更容易维持清晰边界。
Runtime 中为什么还会出现 Session、Run、Turn、Step
当执行生命周期被独立出来以后,仍然存在多个不同时间尺度:
它们分别解决不同范围的问题。
Session 管长期会话;Run 常用于应用层的一次持久执行;Turn 表示一段连续 Agent 工作;Step 接近一次模型决策和由它直接产生的 Tool 工作。
这些对象并非为了把架构画得更复杂。每增加一个生命周期,通常都是因为上一层无法稳定表达某种状态。
例如:
第二篇会沿这条推导继续,把 Session、Run、Turn、Step 的边界建立起来。
Context 为什么属于 Runtime 链路
Pi 在开始一次 Loop 前调用 createContextSnapshot():
随后将这份 AgentContext 交给 runAgentLoop()。[5]
真正进入 Provider 前,Context 还可以经过 transformContext 和 convertToLlm。
这段实现说明了一个容易被忽略的边界:
Long-lived Agent State
│
▼
Context Snapshot
│
▼
Loop Local Context
│
▼
Provider Context
模型输入是 Runtime 在某个时间点构造出的计算视图。
这也是为什么后续做 Session 持久化时,不能简单把“模型当前看到什么”与“系统长期保存什么”视为同一个问题。
如何判断一个系统是否真的需要 Agent Runtime
并非所有 Agent 应用都需要复杂 Runtime。
如果系统满足下面这些条件:
一次请求内完成
没有中途输入
没有暂停和恢复
不要求跨连接继续执行
消息列表直接作为模型上下文
Tool 状态不需要独立暴露
一个 Tool Loop 加少量状态管理通常已经足够。
当需求逐渐变成:
Runtime 才具有明确的独立价值。
判断依据应放在状态生命周期是否已经分裂,框架选型本身不能替代这个判断。
本系列采用的边界
后续文章统一使用下面这组关系:
其中:
- AgentDefinition 描述 Agent 的配置和能力集合;
- Session 保存长期会话事实;
- Runtime 管理当前执行状态并推进状态变化;
- Run 提供一次应用层执行的管理边界;
- Turn 和 Step 描述 Loop 内部的连续工作与模型决策边界;
- Context 在模型请求前由当前状态构造。
这套模型用于后续比较不同框架的实现边界,并不要求它们采用相同 API。
第一篇最终只需要留下一个判断:
当 Agent 的执行生命周期开始脱离一次函数调用或一次网络请求,状态所有权就必须重新划分。Tool Loop 仍然是执行内核,但 Runtime 开始成为独立的系统层。
下一篇继续处理这些状态的时间尺度,说明 Session、Run、Turn、Step 为什么需要分别存在,以及哪些边界应当留在 Agent Core,哪些更适合由应用层承担。
参考资料
[1] Vercel AI SDK, ToolLoopAgent: https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent
[2] Pi Agent source: https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[3] DeepSeek Harness core subsystem: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/core.md
[4] DeepSeek Harness agent lifecycle: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/agent-lifecycle.md
[5] Pi createContextSnapshot(): https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[6] Vercel AI SDK UI: https://ai-sdk.dev/docs/ai-sdk-ui
[7] Pi Coding Agent README: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md
[8] DeepSeek Harness architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md