型別契約
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。