类型契约
packages/agent/src/types.ts 是 pi-agent-core 对外暴露的全部类型契约。它定义了循环入口的 StreamFn、工具调度的 ToolExecutionMode、装配循环的 AgentLoopConfig、运行时状态的 AgentState、工具协议的 AgentTool、上下文快照的 AgentContext、事件流的 AgentEvent、可扩展消息联合 AgentMessage。这一层故意全是 interface/type 没有运行时代码,让 AgentSession/Agent/runLoop 都依赖抽象而非具体。
职责
- 流函数契约:
StreamFn直接复用streamSimple的签名,要求实现不能在请求/模型失败时 throw,要把失败编码进返回流里的error事件和stopReason: "error",见packages/agent/src/types.ts:15-26。 - 工具执行模式:
ToolExecutionMode = "sequential" | "parallel",既出现在AgentLoopConfig.toolExecution也出现在AgentTool.executionMode(每工具覆盖),见packages/agent/src/types.ts:28-36。 - 循环配置:
AgentLoopConfig extends SimpleStreamOptions,包含model/convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages/toolExecution/beforeToolCall/afterToolCall。见packages/agent/src/types.ts:115-248。 - 运行时状态:
AgentState用 setter/getter 形式声明tools和messages,允许实现拷贝顶层数组;isStreaming/streamingMessage/pendingToolCalls/errorMessage全是 readonly。见packages/agent/src/types.ts:288-313。 - 工具协议:
AgentTool<TParameters, TDetails>继承 pi-ai 的Tool,加label/prepareArguments/execute/executionMode;execute收toolCallId/params/signal/onUpdate四个参数。见packages/agent/src/types.ts:332-355。 - 事件联合:
AgentEvent是 9 种事件的 discriminated union,按 agent/turn/message/tool execution 四层生命周期组织。见packages/agent/src/types.ts:374-389。
设计动机
为什么 AgentMessage 是 Message | 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 已存在。
为什么 AgentTool 的 execute 用泛型 TParameters/TDetails?TParameters 约束为 TSchema(typebox),Static<TParameters> 是编译期推导出的参数类型;TDetails 是工具自己定义的 details 形状。这样工具实现里拿到的 params 是强类型,而循环侧用 AgentTool<any> 统一存储,既类型安全又能在 heterogeneous 列表里用。
关键文件
packages/agent/src/types.ts:15-26—StreamFn:签名复用streamSimple,契约要求失败编码进流而非 throw。packages/agent/src/types.ts:28-36—ToolExecutionMode注释,讲清 sequential/parallel 在事件顺序上的差异。packages/agent/src/types.ts:47-73—BeforeToolCallResult/AfterToolCallResult:block+reason vs 字段级覆盖。packages/agent/src/types.ts:76-113—BeforeToolCallContext/AfterToolCallContext/ShouldStopAfterTurnContext,钩子入参形状。packages/agent/src/types.ts:115-248—AgentLoopConfig全量字段,每个都带 JSDoc 契约说明。packages/agent/src/types.ts:257-280—CustomAgentMessages+AgentMessage联合,declaration merging 扩展点。packages/agent/src/types.ts:288-313—AgentState:setter/getter 形式声明可拷贝字段,运行时态全 readonly。packages/agent/src/types.ts:316-355—AgentToolResult/AgentToolUpdateCallback/AgentTool,工具协议。packages/agent/src/types.ts:358-365—AgentContext:循环入参快照,只含 systemPrompt/messages/tools。packages/agent/src/types.ts:367-389—AgentEvent:9 种事件的 discriminated union。
AgentLoopConfig 的核心字段,convertToLlm 是必填,其余都可选:
// 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 联合:
// 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,加执行协议:
// 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,否则
runLoop的for 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 是快照:
Agent在createContextSnapshot里messages.slice()/tools.slice()后传给循环,循环内部 mutate 这份拷贝(pushpartial、push toolResult)不影响Agent._state,见packages/agent/src/agent.ts:402-408。
小结
types.ts 是 pi-agent-core 的契约层:StreamFn 钉死流函数形状、AgentLoopConfig 装配循环、AgentState 暴露只读运行时态、AgentTool 定义工具协议、AgentMessage 通过 declaration merging 扩展、AgentEvent 是 9 种事件的联合。契约反复强调「不能 throw」是为了保护 runLoop 的事件序列完整性。这些类型如何被消费,看 Agent 类与生命周期 和 双层 while 主循环;钩子在工具执行里的具体调用,看 工具执行 sequential/parallel。