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