Skip to content

类型契约

源码版本v0.73.1

packages/agent/src/types.tspi-agent-core 对外暴露的全部类型契约。它定义了循环入口的 StreamFn、工具调度的 ToolExecutionMode、装配循环的 AgentLoopConfig、运行时状态的 AgentState、工具协议的 AgentTool、上下文快照的 AgentContext、事件流的 AgentEvent、可扩展消息联合 AgentMessage。这一层故意全是 interface/type 没有运行时代码,让 AgentSession/Agent/runLoop 都依赖抽象而非具体。

职责

  1. 流函数契约:StreamFn 直接复用 streamSimple 的签名,要求实现不能在请求/模型失败时 throw,要把失败编码进返回流里的 error 事件和 stopReason: "error",见 packages/agent/src/types.ts:15-26
  2. 工具执行模式:ToolExecutionMode = "sequential" | "parallel",既出现在 AgentLoopConfig.toolExecution 也出现在 AgentTool.executionMode(每工具覆盖),见 packages/agent/src/types.ts:28-36
  3. 循环配置:AgentLoopConfig extends SimpleStreamOptions,包含 model/convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages/toolExecution/beforeToolCall/afterToolCall。见 packages/agent/src/types.ts:115-248
  4. 运行时状态:AgentState 用 setter/getter 形式声明 toolsmessages,允许实现拷贝顶层数组;isStreaming/streamingMessage/pendingToolCalls/errorMessage 全是 readonly。见 packages/agent/src/types.ts:288-313
  5. 工具协议:AgentTool<TParameters, TDetails> 继承 pi-ai 的 Tool,加 label/prepareArguments/execute/executionMode;executetoolCallId/params/signal/onUpdate 四个参数。见 packages/agent/src/types.ts:332-355
  6. 事件联合:AgentEvent 是 9 种事件的 discriminated union,按 agent/turn/message/tool execution 四层生命周期组织。见 packages/agent/src/types.ts:374-389

设计动机

为什么 AgentMessageMessage | CustomAgentMessages[keyof CustomAgentMessages]?因为编码助手要在 transcript 里混入「UI 专用消息」(artifact、notification、思考摘要),这些不该发给 LLM。CustomAgentMessages 默认是空 interface,应用层通过 declaration merging 往里塞自定义 role,convertToLlm 负责把它们过滤或转成 LLM 能理解的 user/assistant/toolResult。这种设计让 transcript 类型在编译期就扩展,而不是退化成 any

为什么钩子契约反复强调「不能 throw」?因为 runLoop 是单线程事件流,任何一个回调 throw 都会中断循环且不产生正常 agent_end 事件序列,UI 会卡在 isStreaming=true。所以 convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages 的文档都明说「must not throw or reject,返回 fallback」。钩子抛错由调用点 try/catch 兜底转成 error result,但循环本身的回调没法兜——这是类型契约的硬约束。

为什么 BeforeToolCallResult 只有 block/reason 两个可选字段而不是完整结果?因为 before 阶段只决定「让不让它跑」,不决定「结果是什么」。被 block 的工具由循环构造一个 error result,reason 写进 content。这样 before 钩子不用关心工具的具体 result 形状,而 after 钩子(AfterToolCallResult)才允许字段级覆盖 content/details/isError/terminate,因为此时 result 已存在。

为什么 AgentToolexecute 用泛型 TParameters/TDetails?TParameters 约束为 TSchema(typebox),Static<TParameters> 是编译期推导出的参数类型;TDetails 是工具自己定义的 details 形状。这样工具实现里拿到的 params 是强类型,而循环侧用 AgentTool<any> 统一存储,既类型安全又能在 heterogeneous 列表里用。

关键文件

AgentLoopConfig 的核心字段,convertToLlm 是必填,其余都可选:

typescript
// packages/agent/src/types.ts:115-144
export interface AgentLoopConfig extends SimpleStreamOptions {
  model: Model<any>;
  /** Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. */
  convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
  // ...
}

AgentMessage 通过 declaration merging 扩展,默认就是 pi-ai 的 Message 联合:

typescript
// packages/agent/src/types.ts:271-280
export interface CustomAgentMessages {
  // Empty by default - apps extend via declaration merging
}

export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];

AgentTool 继承 pi-ai 的 Tool,加执行协议:

typescript
// packages/agent/src/types.ts:332-346
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {
  label: string;
  prepareArguments?: (args: unknown) => Static<TParameters>;
  execute: (
    toolCallId: string,
    params: Static<TParameters>,
    signal?: AbortSignal,
    onUpdate?: AgentToolUpdateCallback<TDetails>,
  ) => Promise<AgentToolResult<TDetails>>;
  // ...
}

数据流

类型在不同层的流转:

边界与失败

  • StreamFn 失败契约:请求/模型失败必须编码进流,不能 throw,否则 runLoopfor await 会中断且无 agent_end,见 packages/agent/src/types.ts:16-23
  • convertToLlm 失败契约:不能 throw,要返回 fallback Message[];类型注释里写明 throwing 会无事件序列地中断循环,见 packages/agent/src/types.ts:119-127
  • AgentState setter 拷贝:tools/messages 是 setter/getter,实现可以拷贝顶层数组(默认实现 createMutableAgentState 就这么做),避免外部直接 mutate,见 packages/agent/src/types.ts:295-300
  • afterToolCall 无深 merge:content/details 字段级覆盖是「全替换」,不深合并;isError/terminate 单独替换。文档里明确「No deep merge is performed」,见 packages/agent/src/types.ts:52-73
  • AgentContext 是快照:AgentcreateContextSnapshotmessages.slice()/tools.slice() 后传给循环,循环内部 mutate 这份拷贝(push partial、push toolResult)不影响 Agent._state,见 packages/agent/src/agent.ts:402-408

小结

types.tspi-agent-core 的契约层:StreamFn 钉死流函数形状、AgentLoopConfig 装配循环、AgentState 暴露只读运行时态、AgentTool 定义工具协议、AgentMessage 通过 declaration merging 扩展、AgentEvent 是 9 种事件的联合。契约反复强调「不能 throw」是为了保护 runLoop 的事件序列完整性。这些类型如何被消费,看 Agent 类与生命周期双层 while 主循环;钩子在工具执行里的具体调用,看 工具执行 sequential/parallel