Agent 管理层精读 — agent.tsAgent Management Layer Deep Read — agent.ts

源文件:packages/agent/src/agent.ts  |  543 行 Source: packages/agent/src/agent.ts  |  543 lines

这个文件是什么:上次读的 agent-loop.ts 是"发动机"——它只管跑循环,自己不记东西。这个文件是"整车"——它记住对话管理启动和停止运行过程中往外通报进度。它内部调用上次读过的引擎来干活,但在调用前后做大量管理工作。

What this file is: The agent-loop.ts we read last time is the "engine" — it only runs the loop and remembers nothing itself. This file is the "full vehicle" — it remembers conversations, manages start and stop, and broadcasts progress during execution. Internally it calls the engine we already read, but performs extensive management work before and after each call.

为什么要分两层:引擎是纯逻辑,不绑定任何产品形态。Agent 类是产品化的壳——不同产品可以用同一个引擎,但用不同的 Agent 配置。

Why two layers: The engine is pure logic, not tied to any product form. The Agent class is the productized shell — different products can use the same engine with different Agent configurations.

全局定位:agent.ts 在整个架构里的位置Global Position: Where agent.ts Sits in the Architecture

知识全景地图里的 ① 核心循环,展开看其实分两层。外层是 agent.ts(管理层,本文),内层是 agent-loop.ts(引擎,上次已读)。下图标注了本文 9 个区块各自负责的角色,以及它们连接的外部模块:

The ① Core Loop in our knowledge map actually splits into two layers when expanded. The outer layer is agent.ts (management layer, this article), the inner layer is agent-loop.ts (engine, previously read). The diagram below labels the roles of all 9 blocks in this article and the external modules they connect to:

外部世界(UI / 产品代码 / 持久化层) │ 发指令 ▲ 收通知 ▼ │ ┌──────────────────────────────────────────────────────┐ │ agent.ts 管理层(本文精读) │ │ │ │ ┌ A 配置层 ─────────────────────┐ │ │ │ 对话记录 / 工具清单 / 转换函数 │──→ ② 上下文工程 │ │ │ 区块 1, 2, 3 │──→ ③ 工具系统 │ │ └───────────────────────────────┘ │ │ │ │ ┌ B 控制层 ─────────────────────┐ │ │ │ 启动 / 停止 / 插话 / 等待 │←──→ 外部世界 │ │ │ 区块 4, 6, 7 │ │ │ └──────────────┬────────────────┘ │ │ │ 打包配置交给引擎 │ │ ┌ C 引擎对接 ──┼────────────────┐ │ │ │ ▼ │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ agent-loop.ts 引擎 │ │ │ │ │ │ 调 LLM → 工具 → 喂回 │ │ │ │ │ │ (上次已精读) │ │ │ │ │ └────────────────────────┘ │ │ │ │ 区块 5, 8 │ │ │ └──────────────┬────────────────┘ │ │ │ 引擎发进度通知 │ │ ┌ D 事件处理 ──┼────────────────┐ │ │ │ ▼ │ │ │ │ 更新内部状态 → 转发给订阅者 │──→ 外部订阅者 │ │ │ 区块 9 │──→ ⑨ 持久化 │ │ └───────────────────────────────┘ (Session Log) │ │ │ └──────────────────────────────────────────────────────┘ │ ▼ LLM API (Claude)
External World (UI / Product Code / Persistence Layer) │ Send commands ▲ Receive notifications ▼ │ ┌──────────────────────────────────────────────────────────┐ │ agent.ts Management Layer (this article) │ │ │ │ ┌ A Config Layer ───────────────────┐ │ │ │ Message history / Tool list / │──→ ② Context Eng │ │ │ Transform functions │──→ ③ Tool System │ │ │ Blocks 1, 2, 3 │ │ │ └───────────────────────────────────┘ │ │ │ │ ┌ B Control Layer ──────────────────┐ │ │ │ Start / Stop / Steer / Wait │←──→ External │ │ │ Blocks 4, 6, 7 │ │ │ └──────────────┬────────────────────┘ │ │ │ Package config for engine │ │ ┌ C Engine Bridge ─┼───────────────┐ │ │ │ ▼ │ │ │ │ ┌────────────────────────────┐ │ │ │ │ │ agent-loop.ts Engine │ │ │ │ │ │ Call LLM → Tools → Feed │ │ │ │ │ │ (previously read) │ │ │ │ │ └────────────────────────────┘ │ │ │ │ Blocks 5, 8 │ │ │ └──────────────┬────────────────────┘ │ │ │ Engine sends progress events │ │ ┌ D Event Handling ┼───────────────┐ │ │ │ ▼ │ │ │ │ Update state → Forward to subs │──→ Subscribers │ │ │ Block 9 │──→ ⑨ Persistence │ │ └───────────────────────────────────┘ (Session Log) │ │ │ └──────────────────────────────────────────────────────────┘ │ ▼ LLM API (Claude)

注意箭头含义:图中 ──→ ② 上下文工程──→ ③ 工具系统 表示"连接到 / 持有引用",不是"包含"。上下文压缩函数、工具定义等实际代码在其他文件里。agent.ts 像项目经理——手上有通讯录(知道谁负责什么),自己不干活,把引用打包交给引擎去调用。

Note on arrows: The arrows like ──→ ② Context Eng and ──→ ③ Tool System mean "connects to / holds a reference", not "contains". The actual code for context compression, tool definitions, etc. lives in other files. agent.ts is like a project manager — it has the contact list (knows who does what) but doesn't do the work itself; it packages references and hands them to the engine.

前面章节提到的 JSONL 文件(Session Log)就在右下角的 ⑨ 持久化——它是一个"订阅者",挂在 agent.ts 上。agent.ts 每收到引擎的通知,先更新自己的内存记录,然后转发给所有订阅者——其中一个订阅者负责把记录写成 JSONL 文件存到磁盘。

The JSONL file (Session Log) mentioned in earlier chapters sits at ⑨ Persistence in the bottom right — it is a "subscriber" attached to agent.ts. Each time agent.ts receives a notification from the engine, it first updates its own in-memory records, then forwards to all subscribers — one of which writes records as a JSONL file to disk.

A 配置层A Config B 控制层B Control C 引擎对接C Engine Bridge D 事件处理D Event Handling

全文结构地图(9 个区块,角色对应上图 A-D)Article Structure Map (9 Blocks, roles mapped to A-D above)

┌─ 区块 1 ────────────────────────── A 配置层 ────── [快读] ──┐ │ 消息筛选函数 + 默认值 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 2 ────────────────────────── A 配置层 ────── [快读] ──┐ │ Agent 内部状态的保护机制 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 3 ────────────────────────── A 配置层 ────── [快读] ──┐ │ 出厂配置清单 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 4 ────────────────────────── B 控制层 ────── [精读] ──┐ │ 消息排队桶 — "运行中插话"的实现 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 5 ────────────────────────── C 引擎对接 ──── [精读] ──┐ │ Agent 的零件清单 + 出厂组装 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 6 ────────────────────────── B 控制层 ────── [精读] ──┐ │ 对外操作面板 — 订阅/插话/取消/等待/重置 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 7 ────────────────────────── B 控制层 ────── [精读] ──┐ │ 两种启动方式 — 发新消息 vs 从断点恢复 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 8 ────────────────────────── C 引擎对接 ──── [精读] ──┐ │ 启动前准备 + 引擎对接 + 收工清理 │ └──────────────────────────────────────────────────────────────┘ ↓ ┌─ 区块 9 ────────────────────────── D 事件处理 ──── [精读] ──┐ │ 收到引擎通知后怎么处理 │ └──────────────────────────────────────────────────────────────┘
┌─ Block 1 ───────────────────────── A Config ─────── [Skim] ──┐ │ Message filter function + defaults │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 2 ───────────────────────── A Config ─────── [Skim] ──┐ │ Agent internal state protection │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 3 ───────────────────────── A Config ─────── [Skim] ──┐ │ Factory configuration checklist │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 4 ───────────────────────── B Control ────── [Deep] ──┐ │ Message queue — implementing "mid-run injection" │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 5 ───────────────────────── C Engine ─────── [Deep] ──┐ │ Agent's parts list + factory assembly │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 6 ───────────────────────── B Control ────── [Deep] ──┐ │ External control panel — subscribe/steer/cancel/wait/reset │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 7 ───────────────────────── B Control ────── [Deep] ──┐ │ Two ways to start — new message vs resume from checkpoint │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 8 ───────────────────────── C Engine ─────── [Deep] ──┐ │ Pre-launch prep + engine bridge + post-run cleanup │ └───────────────────────────────────────────────────────────────┘ ↓ ┌─ Block 9 ───────────────────────── D Events ─────── [Deep] ──┐ │ Processing engine notifications │ └───────────────────────────────────────────────────────────────┘

区块 1:消息筛选函数 + 默认值Block 1: Message Filter Function + Defaults

行 27-53Lines 27-53 A 配置层A Config
27function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
28  return messages.filter(
29    (message) => message.role === "user"
30      || message.role === "assistant"
31      || message.role === "toolResult",
32  );
33}
34
35const EMPTY_USAGE = { input: 0, output: 0, ... };
42const DEFAULT_MODEL = { id: "unknown", ... };

上次读过的"变换管道"的最后一步The final step of the "transform pipeline" we read last time

Agent 内部记录了很多种消息,但发给 AI 模型时只需要三种:用户说的(user)、AI 说的(assistant)、工具结果(toolResult)。

The Agent internally records many types of messages, but only three kinds need to be sent to the AI model: what the user said (user), what the AI said (assistant), and tool results (toolResult).

.filter() 就是过一遍列表,只留符合条件的——这里就是只留这三种角色的消息,其他的丢掉。

.filter() iterates through the list and keeps only items matching the condition — here it keeps only these three role types and discards the rest.

Pi-mono 的转换极其简单,因为它内部格式本来就和 AI 的格式很像。Claude Code 的就复杂得多。

Pi-mono's conversion is extremely simple because its internal format already closely resembles the AI's format. Claude Code's conversion is far more complex.

灰色两个常量:出错时用的空白填充,不影响理解。

The two grayed-out constants: Blank placeholders used when errors occur — not important for understanding.

区块 2:Agent 内部状态的保护机制Block 2: Agent Internal State Protection

行 55-91Lines 55-91 A 配置层A Config
55type QueueMode = "all" | "one-at-a-time";
57type MutableAgentState = Omit<AgentState,
58  "isStreaming" | "streamingMessage"
59  | "pendingToolCalls" | "errorMessage"
60> & { ... };
66
67function createMutableAgentState(initialState?) {
70  let tools = initialState?.tools?.slice() ?? [];
71  let messages = initialState?.messages?.slice() ?? [];
73  return {
74    systemPrompt: initialState?.systemPrompt ?? "",
75    model: initialState?.model ?? DEFAULT_MODEL,
77    get tools() { return tools; },
80    set tools(next) { tools = next.slice(); },
83    get messages() { return messages; },
86    set messages(next) { messages = next.slice(); },
89    isStreaming: false,
91    pendingToolCalls: new Set(),
93  };
94}

一句话:外部只能看,内部才能改In a nutshell: outsiders can look, only internals can modify

Agent 运行时需要记住的东西:对话记录(messages)、可用工具列表(tools)、是否正在运行(isStreaming)、出错信息等。

Things the Agent needs to remember at runtime: message history (messages), available tool list (tools), whether it's currently running (isStreaming), error info, etc.

设计者做了一层防护:对外展示的版本是"只能看不能改"的,内部用的版本可以改。

The designers added a layer of protection: the externally exposed version is "read-only", while the internal version is mutable.

绿色行是防护的具体做法:当外部想替换整个工具列表时,代码会先把新列表复印一份.slice() 就是复印)再存,这样外部后续怎么改自己手上的列表,都不影响 Agent 内部的存档。

The green lines show the protection mechanism: when external code tries to replace the entire tool list, the code first makes a copy (.slice() means copy) before storing it. This way, whatever the external code does to its own list afterwards won't affect the Agent's internal copy.

这个区块的实现细节不重要,只需要知道:Agent 的内部状态有保护,外部代码不能随意篡改。

The implementation details of this block aren't important — just know that the Agent's internal state is protected, and external code cannot tamper with it.

区块 3:创建 Agent 时的配置清单Block 3: Configuration Checklist for Creating an Agent

行 93-111Lines 93-111 A 配置层A Config
 93export interface AgentOptions {
 94  initialState?: ...;
 96  convertToLlm?: ...;      // 消息筛选message filter
 98  transformContext?: ...;   // 上下文压缩context compression
100  streamFn?: StreamFn;      // 调 AI 的函数function to call AI
102  getApiKey?: ...;
104  onPayload?: ...;
105  onResponse?: ...;
107  beforeToolCall?: ...;    // 工具执行前拦截pre-tool-call hook
110  afterToolCall?: ...;     // 工具执行后处理post-tool-call hook
114  steeringMode?: QueueMode; // 方向盘队列模式steering queue mode
115  followUpMode?: QueueMode; // 追加任务队列模式follow-up queue mode
117  sessionId?: string;
118  thinkingBudgets?: ...;
119  transport?: Transport;
120  maxRetryDelayMs?: number;
121  toolExecution?: ToolExecutionMode;
122}

一张"出厂定制单"A "factory customization form"

这是一个 interface——可以理解为一张表格模板,规定了创建 Agent 时可以填哪些配置项。所有项都是可选的(带 ?),不填就用默认值。

This is an interface — think of it as a form template that specifies what configuration options are available when creating an Agent. All fields are optional (marked with ?); if left blank, defaults are used.

绿色行是上次读引擎时见过的三个消息处理函数。

Green lines are the three message-processing functions we saw when reading the engine last time.

黄色行是上次在引擎的 executeToolCalls 里见过的两个拦截点——Claude Code 的 "Allow?" 弹窗就是通过 beforeToolCall 实现的。

Yellow lines are the two intercept points we saw in the engine's executeToolCalls — Claude Code's "Allow?" dialog is implemented via beforeToolCall.

绿色 steeringMode / followUpMode:控制下面区块 4 要讲的两个队列的行为。

Green steeringMode / followUpMode: Control the behavior of the two queues covered in Block 4 below.

这张表是引擎配置(AgentLoopConfig)的"用户友好版"。Agent 类在区块 8 的 createLoopConfig() 里会把它翻译成引擎需要的格式。This form is the "user-friendly version" of the engine config (AgentLoopConfig). The Agent class translates it into the engine's format in Block 8's createLoopConfig().

区块 4:消息排队桶 — "运行中插话"的实现Block 4: Message Queue — Implementing "Mid-Run Injection"

行 113-144Lines 113-144 B 控制层B Control
113class PendingMessageQueue {
114  private messages: AgentMessage[] = [];
115
116  constructor(public mode: QueueMode) {}
117
118  enqueue(message: AgentMessage): void {
119    this.messages.push(message);
120  }
121
122  hasItems(): boolean {
123    return this.messages.length > 0;
124  }
125
126  drain(): AgentMessage[] {
127    if (this.mode === "all") {
128      const drained = this.messages.slice();
129      this.messages = [];
130      return drained;
131    }
132    // one-at-a-time: 每次只取第一条take only the first message each time
133    const first = this.messages[0];
134    if (!first) return [];
135    this.messages = this.messages.slice(1);
136    return [first];
137  }
138
139  clear(): void {
140    this.messages = [];
141  }
142}

一个装消息的桶,有两种倒法A message bucket with two ways to pour

Agent 运行时,外部可以往这个桶里塞消息。引擎在合适的时机来取。

While the Agent is running, external code can push messages into this bucket. The engine fetches them at the right moment.

四个操作:

Four operations:

  • enqueue(排队)— 往桶里放一条消息
  • hasItems — 桶里还有东西吗?
  • drain(倒出来)— 核心方法,见下
  • clear — 把桶清空
  • enqueue — push one message into the bucket
  • hasItems — is anything in the bucket?
  • drain — the core method, see below
  • clear — empty the bucket

drain() 的两种倒法(绿色高亮)Two drain modes (green highlight)

"all" 模式:一次把桶里所有消息全部倒出来,桶清空。适合批量场景——比如用户快速连发了 3 条修改指令,全部一次性交给 AI。

"all" mode: Pour out every message at once, emptying the bucket. Good for batch scenarios — e.g. the user fires off 3 edit instructions in quick succession; all are handed to the AI at once.

"one-at-a-time" 模式(默认值):每次只从桶里取第一条,剩下的留着下次取。这样每条消息都会触发一轮完整的 AI 回复,确保每条都被认真处理。

"one-at-a-time" mode (the default): Take only the first message each time; the rest stay for next time. This way, each message triggers a full AI response cycle, ensuring each one is properly handled.

Pi-mono 特有:这种"运行中插话"的双队列设计是 Pi-mono 独有的。Claude Code 和 Hermes 都没有这个机制,它们处理"用户改主意"的方式完全不同,到时候对比。Pi-Specific: This dual-queue "mid-run injection" design is unique to Pi-mono. Claude Code and Hermes lack this mechanism entirely — they handle "user changes their mind" in completely different ways. We'll compare when we get there.

区块 5:Agent 的零件清单 + 出厂组装Block 5: Agent's Parts List + Factory Assembly

行 146-207Lines 146-207 C 引擎对接C Engine Bridge
146type ActiveRun = {
147  promise: Promise<void>;
148  resolve: () => void;
149  abortController: AbortController;
150};
151
158export class Agent {
159  private _state: MutableAgentState;
160  private readonly listeners = new Set<...>();
161  private readonly steeringQueue: PendingMessageQueue;
162  private readonly followUpQueue: PendingMessageQueue;
163
164  public convertToLlm: ...;
165  public transformContext?: ...;
166  public streamFn: StreamFn;
170  public beforeToolCall?: ...;
171  public afterToolCall?: ...;
172  private activeRun?: ActiveRun;
177  public toolExecution: ToolExecutionMode;
178
190  constructor(options: AgentOptions = {}) {
191    this._state = createMutableAgentState(options.initialState);
192    this.convertToLlm = options.convertToLlm
193      ?? defaultConvertToLlm;
195    this.streamFn = options.streamFn ?? streamSimple;
200    this.steeringQueue = new PendingMessageQueue(
201      options.steeringMode ?? "one-at-a-time");
202    this.followUpQueue = new PendingMessageQueue(
203      options.followUpMode ?? "one-at-a-time");
206    this.toolExecution = options.toolExecution
207      ?? "parallel";
208  }

Agent 拥有哪些零件What parts does an Agent have

class(类)就是一张设计图纸。这里定义了"一个 Agent 由哪些零件组成"。

A class is a blueprint. This defines "what parts make up an Agent".

绿色行 159-162 — 四个核心零件

Green lines 159-162 — four core parts:

  • _state — 记事本,记住所有对话内容和运行状态
  • listeners — 订阅者名单,谁在关注 Agent 的进度
  • steeringQueue / followUpQueue — 上面读过的两个消息排队桶
  • _state — the notebook, remembering all conversation content and runtime status
  • listeners — the subscriber roster: who's watching the Agent's progress
  • steeringQueue / followUpQueue — the two message queues we read above

标了 private 的是内部零件,外部摸不到;标 public 的外部可以直接使用或替换。

Parts marked private are internal — outsiders can't touch them; those marked public can be accessed or replaced by external code.

黄色行 172 — activeRun:记录"当前正在跑的任务"。同一时间只能有一个任务在跑,有 activeRun 时再调 prompt() 会直接拒绝。它里面有三样东西:一个"等待完成"的凭证(promise)、一个"通知完成"的开关(resolve)、一个取消按钮(abortController)。

Yellow line 172 — activeRun: Tracks the "currently running task". Only one task can run at a time — calling prompt() when activeRun exists is immediately rejected. It contains three things: a "wait for completion" token (promise), a "notify completion" switch (resolve), and a cancel button (abortController).

constructor — 出厂组装constructor — Factory assembly

constructor 是"出厂组装步骤"——把配置清单(区块 3 的 AgentOptions)里的每一项取出来,安装到对应的零件上。?? 的意思是"如果用户没提供,就用默认值"。

The constructor is the "factory assembly step" — it takes each item from the configuration checklist (Block 3's AgentOptions) and installs it onto the corresponding part. ?? means "if the user didn't provide one, use the default".

通用模式:Agent = 记事本 + 进度通知 + 启停控制。它自己不跑循环,而是把零件组装好后交给引擎。Universal Pattern: Agent = notebook + progress notification + start/stop control. It doesn't run the loop itself — it assembles the parts and hands them to the engine.

区块 6:对外操作面板Block 6: External Control Panel

行 219-313Lines 219-313 B 控制层B Control
219subscribe(listener): () => void {
222  this.listeners.add(listener);
223  return () => this.listeners.delete(listener);
224}
225
229get state(): AgentState {
230  return this._state;
231}
232
252steer(message): void {
253  this.steeringQueue.enqueue(message);
254}
255
258followUp(message): void {
259  this.followUpQueue.enqueue(message);
260}
261
282hasQueuedMessages(): boolean {
283  return this.steeringQueue.hasItems()
284    || this.followUpQueue.hasItems();
285}
286
292abort(): void {
293  this.activeRun?.abortController.abort();
294}
295
300waitForIdle(): Promise<void> {
301  return this.activeRun?.promise
302    ?? Promise.resolve();
303}
304
305reset(): void {
306  this._state.messages = [];
307  this._state.isStreaming = false;
311  this.clearFollowUpQueue();
312  this.clearSteeringQueue();
313}

subscribe() — 订阅进度通知subscribe() — Subscribe to progress notifications

外部代码想知道 Agent 的进展(AI 开始说话了、工具在执行…),就调 subscribe(),传入一个"收到通知时要执行的函数"。这种"你把一个函数交给别人,别人在合适的时机来调用它"的模式叫回调(callback)。

When external code wants to know the Agent's progress (AI started speaking, a tool is executing...), it calls subscribe(), passing in a "function to run when a notification arrives". This pattern of "you hand a function to someone else, and they call it at the right moment" is called a callback.

subscribe 返回的也是一个函数——调用它就取消订阅,不再收通知。

subscribe returns a function too — calling it unsubscribes, so you stop receiving notifications.

steer() 和 followUp()(黄色行)steer() and followUp() (yellow lines)

往上面读过的两个排队桶里塞消息。引擎在合适的时机来取:每轮结束后取 steering,准备收工时取 followUp。

Push messages into the two queues we read above. The engine fetches them at the right moments: steering messages after each turn ends, follow-up messages when the agent is about to wrap up.

abort() 和 waitForIdle()(绿色行)abort() and waitForIdle() (green lines)

abort():按下取消按钮,发出"请停下"的信号。引擎收到后在下一个安全时机停下来。就像你在 Claude Code 里按 Escape。

abort(): Press the cancel button, sending a "please stop" signal. The engine stops at the next safe point. Like pressing Escape in Claude Code.

waitForIdle():返回 activeRun 里的那个"等待完成凭证"。外部可以拿着它等——Agent 跑完后凭证自动兑现。如果 Agent 没在忙,立即通过。

waitForIdle(): Returns the "wait-for-completion" token from activeRun. External code can hold onto it and wait — it auto-resolves when the Agent finishes. If the Agent isn't busy, it resolves immediately.

reset()reset()

清空一切:对话记录、运行状态、两个队列。相当于"开新对话"。

Clear everything: message history, runtime state, both queues. Equivalent to "start a new conversation".

通用模式:任何 agent 框架都需要这几种操作——订阅进度、运行中插话、取消、等待完成、重置。名字不同,概念相同。Universal Pattern: Every agent framework needs these operations — subscribe to progress, mid-run message injection, cancel, wait for completion, reset. Names differ, concepts are the same.

区块 7:两种启动方式Block 7: Two Ways to Start

行 312-372Lines 312-372 B 控制层B Control
313async prompt(message: AgentMessage | AgentMessage[]): Promise<void>;
314async prompt(input: string, images?: ImageContent[]): Promise<void>;
315async prompt(input, images?): Promise<void> {
317  if (this.activeRun) {
318    throw new Error(
319      "Agent is already processing.
320       Use steer() or followUp()");
322  }
323  const messages = this.normalizePromptInput(input, images);
324  await this.runPromptMessages(messages);
325}
326
327async continue(): Promise<void> {
328  if (this.activeRun) { throw new Error(...); }
330  const lastMessage = this._state.messages.at(-1);
331  if (!lastMessage) throw new Error("No messages");
332
333  if (lastMessage.role === "assistant") {
335    const queued = this.steeringQueue.drain();
336    if (queued.length > 0) {
337      await this.runPromptMessages(queued, ...);
339      return;
340    }
341    const followUps = this.followUpQueue.drain();
342    if (followUps.length > 0) {
343      await this.runPromptMessages(followUps);
345      return;
346    }
347    throw new Error("Cannot continue: nothing to do");
348  }
350  await this.runContinuation();
351}

prompt() — 发新消息启动prompt() — Start with a new message

灰色前两行是"重载"——同一个函数名可以接受不同类型的输入(一句话、一条消息、一批消息),内部自动识别格式。

The two grayed-out lines are "overloads" — the same function name accepts different input types (a string, a message, or a batch of messages), and internally auto-detects the format.

黄色行 317-322 是"门卫":如果 Agent 已经在忙(activeRun 存在),直接拒绝并报错——throw new Error 就是"拉响警报,停下来"。报错信息还引导你改用 steer/followUp。

Yellow lines 317-322 are the "gatekeeper": If the Agent is already busy (activeRun exists), it immediately rejects with an error — throw new Error means "sound the alarm, stop". The error message even guides you to use steer()/followUp() instead.

continue() — 让 Agent 接着干continue() — Let the Agent carry on

黄色高亮是核心逻辑,分两种情况:

The yellow highlight is the core logic, with two cases:

情况 A:上次最后说话的是 AI(assistant)

Case A: The last speaker was the AI (assistant)

AI 说完了想让它继续,但继续什么?代码去两个桶里找:先倒方向盘桶 → 有就用;没有就倒追加任务桶 → 有就用;都空 → 报错。

The AI finished speaking and you want it to continue — but continue with what? The code checks the two buckets: drain the steering bucket first — if items found, use them; if empty, drain the follow-up bucket — if items found, use them; both empty — throw an error.

情况 B:上次最后说话的是用户或工具

Case B: The last speaker was the user or a tool

说明 AI 还没来得及回就被中断了,直接从断点恢复。

This means the AI was interrupted before it could respond — resume directly from the checkpoint.

prompt() 最终调引擎的 runAgentLoop()(开新循环),continue()runAgentLoopContinue()(接着跑)。这两个函数上次精读过。prompt() ultimately calls the engine's runAgentLoop() (start a new loop), while continue() calls runAgentLoopContinue() (resume the loop). We read both functions in detail last time.

区块 8:启动前准备 + 引擎对接 + 收工清理Block 8: Pre-Launch Prep + Engine Bridge + Post-Run Cleanup

行 374-486Lines 374-486 C 引擎对接C Engine Bridge
374private async runPromptMessages(messages, options) {
377  await this.runWithLifecycle(async (signal) => {
378    await runAgentLoop(
380      this.createContextSnapshot(),
381      this.createLoopConfig(options),
382      (event) => this.processEvents(event),
385    );
387}
388
402private createContextSnapshot() {
405    messages: this._state.messages.slice(),
406    tools: this._state.tools.slice(),
408}
409
410private createLoopConfig(options) {
412  return {
417    beforeToolCall: this.beforeToolCall,
419    convertToLlm: this.convertToLlm,
420    transformContext: this.transformContext,
422    getSteeringMessages: async () => {
427      return this.steeringQueue.drain();
428    },
429    getFollowUpMessages: async () =>
430      this.followUpQueue.drain(),
431  };
432}
433
438private async runWithLifecycle(executor) {
441  if (this.activeRun) throw new Error(...);
443  const abortController = new AbortController();
445  const promise = new Promise((resolve) => ...);
447  this.activeRun = { promise, resolve, abortController };
450  this._state.isStreaming = true;
451
454  try {
455    await executor(abortController.signal);
456  } catch (error) {
457    await this.handleRunFailure(error, ...);
458  } finally {
459    this.finishRun();
460  }
461}
462
463private async handleRunFailure(error, aborted) {
464  // 构造一条带错误信息的 assistant 消息build an assistant message with the error info
465  // 追加到对话记录里append it to the conversation history
466  // 发 agent_end 通知订阅者emit agent_end to notify subscribers
467}
468
480private finishRun(): void {
481  this._state.isStreaming = false;
484  this.activeRun?.resolve();
485  this.activeRun = undefined;
486}

createContextSnapshot() — 复印一份再交出去createContextSnapshot() — Make a copy before handing it over

绿色行:传给引擎的是对话记录和工具清单的复印件。引擎拿着复印件干活,怎么改都不影响 Agent 的"正本"。

Green lines: What gets passed to the engine is a copy of the message history and tool list. The engine works with the copy — any modifications won't affect the Agent's "master copy".

createLoopConfig() — Agent 和引擎之间的桥createLoopConfig() — The bridge between Agent and engine

黄色行:把 Agent 的配置和函数打包成引擎认识的格式。

Yellow line: Package the Agent's config and functions into the format the engine expects.

绿色行 422-430 是关键——引擎不直接摸 Agent 的队列,而是通过这两个回调来取消息。引擎每轮结束后调 getSteeringMessages 问"有方向调整吗?",准备收工时调 getFollowUpMessages 问"有追加任务吗?"

Green lines 422-430 are the key — the engine doesn't directly touch the Agent's queues. Instead, it fetches messages through these two callbacks. After each turn, the engine calls getSteeringMessages asking "any course corrections?", and when wrapping up, calls getFollowUpMessages asking "any follow-up tasks?"

runWithLifecycle() — 每次启动的标准流程runWithLifecycle() — The standard flow for every launch

所有 prompt 和 continue 都走这套固定流程,绿色行 454-460 是骨架:

Every prompt and continue goes through this fixed flow. Green lines 454-460 are the skeleton:

  • try:尝试把活儿交给引擎跑
  • catch:如果出错了,记录错误到对话历史,通知订阅者
  • finally不管成败都要做的收工清理——标记"不忙了"、通知等待的人、清掉当前任务
  • try: Attempt to hand the work to the engine
  • catch: If an error occurs, record it to conversation history and notify subscribers
  • finally: Cleanup that runs regardless of success or failure — mark "no longer busy", notify waiters, clear the current task

灰色的 handleRunFailure 和 finishRun 是细节——出错了也要往对话记录里追加一条(这样翻记录能看到"这里出过错"),finishRun 做收工清理。

The grayed-out handleRunFailure and finishRun are details — even on error, a record is appended to conversation history (so you can see "an error happened here" when reviewing), and finishRun handles the post-run cleanup.

通用模式:准备 → 干活 → 清理(不管成败都要清理)。所有 agent 框架都有这个结构。Universal Pattern: Prepare → Execute → Cleanup (cleanup runs regardless of outcome). Every agent framework has this structure.

区块 9:收到引擎通知后怎么处理Block 9: Processing Engine Notifications

行 495-542Lines 495-542 D 事件处理D Event Handling
495private async processEvents(event) {
496  switch (event.type) {
497
498    case "message_start":  
499      this._state.streamingMessage = event.message;
500      break;
501
502    case "message_update":
503      this._state.streamingMessage = event.message;
504      break;
505
506    case "message_end":
507      this._state.streamingMessage = undefined;
508      this._state.messages.push(event.message);
509      break;
510
511    case "tool_execution_start": {
512      const pending = new Set(this._state.pendingToolCalls);
513      pending.add(event.toolCallId);
514      this._state.pendingToolCalls = pending;
515      break;
516    }
517
518    case "tool_execution_end": {
519      const pending = new Set(this._state.pendingToolCalls);
520      pending.delete(event.toolCallId);
521      this._state.pendingToolCalls = pending;
522      break;
523    }
524
525    case "turn_end":
526      if (event.message.role === "assistant"
527        && event.message.errorMessage) {
528        this._state.errorMessage = event.message.errorMessage;
529      }
530      break;
531
532    case "agent_end":
533      this._state.streamingMessage = undefined;
534      break;
535  }
536
539  for (const listener of this.listeners) {
540    await listener(event, signal);
541  }
542}

每次收到引擎的通知,做两步Two steps every time an engine notification arrives

第一步:更新自己的记事本(行 496-535)

Step 1: Update its own notebook (lines 496-535)

引擎跑循环时,每到一个节点就发一条通知过来。这段代码根据通知类型做不同处理(switch 就是"看是哪种情况,走对应分支"):

As the engine runs its loop, it sends a notification at each milestone. This code handles each notification type differently (switch means "check which case it is and go to the matching branch"):

  • message_start(黄色)— AI 开始说话了 → 记下"正在说的消息"(UI 可以用来显示"正在输入…")
  • message_update — AI 还在说,逐段推送 → 更新"正在说的消息"内容
  • message_end(绿色)— AI 说完了一条完整消息 → 存入对话记录。这是最关键的时刻——从"正在说"变成"说完了,存档"
  • tool_execution_start / end — 工具开始/完成执行 → 更新"正在执行的工具"名单
  • turn_end — 一轮结束 → 如有错误就记下来
  • agent_end — 整个任务结束 → 清理临时状态
  • message_start (yellow) — AI started speaking → record the "message in progress" (UI can use this to show "typing...")
  • message_update — AI is still speaking, pushing content in chunks → update the "message in progress"
  • message_end (green) — AI finished one complete message → commit to conversation history. This is the most critical moment — transitioning from "in progress" to "done, archived"
  • tool_execution_start / end — A tool started/finished executing → update the "currently executing tools" roster
  • turn_end — A turn ended → record the error if any
  • agent_end — The entire task finished → clean up temporary state

第二步:转发给所有订阅者(绿色行 539-541)

Step 2: Forward to all subscribers (green lines 539-541)

遍历订阅者名单,把通知挨个转发。await 意味着每个订阅者处理完了才通知下一个——保证顺序,但也意味着一个处理很慢的订阅者会拖慢整个 Agent。

Iterate through the subscriber roster and forward the notification one by one. await means each subscriber must finish processing before the next one is notified — this guarantees order, but also means a slow subscriber can drag down the entire Agent.

通用模式:先更新自己的状态,再通知外部——这个顺序很重要。如果反过来,外部去查状态时会看到过时的数据。Universal Pattern: Update your own state first, then notify the outside world — this order matters. If reversed, external code would see stale data when querying state.

全文总结:agent.ts 做了什么Full Summary: What agent.ts Does

你(外部调用方) Agent 类(管理层) 引擎(agent-loop.ts) ────────────── ────────────── ────────────────── agent.prompt("你好") ────→ 门卫检查 → 复印对话记录 打包配置 → 交给引擎 ─────────→ 开始跑循环 │ agent.steer("改方向") ──→ 塞进方向盘桶 引擎来取 → │ 收到通知 ←── 进度通知 ────────────┘ ├─ 更新记事本 └─ 转发给订阅者 ──→ UI 等外部代码 │ 收工清理 ←── 循环结束 ────────────┘ agent.abort() ──→ 发取消信号 ──→ 引擎停下来 agent.waitForIdle() ──→ 等到收工清理完成
You (external caller) Agent class (mgmt layer) Engine (agent-loop.ts) ───────────────── ──────────────── ────────────────── agent.prompt("hello") ────→ Gatekeeper check → Copy history Package config → Hand to engine ─→ Start loop │ agent.steer("change") ───→ Push to steering queue Engine drains → │ Receive notification ←── Progress ───┘ ├─ Update notebook └─ Forward to subscribers ──→ UI / external code │ Post-run cleanup ←── Loop ends ──────┘ agent.abort() ──→ Send cancel signal ──→ Engine stops agent.waitForIdle() ──→ Wait for cleanup to complete

一句话:Agent 类是引擎和产品之间的管理层。它管四件事——包装引擎运行中插话进度通知启停善后。引擎只管跑循环。

In one sentence: The Agent class is the management layer between the engine and the product. It handles four things — wrapping the engine, mid-run message injection, progress notification, and start/stop lifecycle management. The engine just runs the loop.