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 SimpleStreamOptionsmodel/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 の 4 引数を取る。packages/agent/src/types.ts:332-355 参照。
  6. イベント共用体:AgentEvent は 9 種のイベントの discriminated union で、agent/turn/message/tool execution の 4 層ライフサイクルで組織される。packages/agent/src/types.ts:374-389 参照。

設計動機

なぜ AgentMessageMessage | CustomAgentMessages[keyof CustomAgentMessages] なのか? コーディングアシスタントはトランスクリプトに「UI 専用メッセージ」(artifact、notification、思考要約) を混ぜたいが、これらは LLM に送るべきでないからだ。CustomAgentMessages はデフォルトで空の interface で、アプリ層が declaration merging で独自 role を詰め込み、convertToLlm がフィルタするか user/assistant/toolResult に変換する。こうすることでトランスクリプト型はコンパイル時に拡張され、any に退化しない。

なぜフック契約が「throw してはいけない」を繰り返すのか? runLoop はシングルスレッドのイベントストリームで、どのコールバックが throw してもループが中断され、正常な agent_end イベント列が生まれず、UI は isStreaming=true でスタックする。だから convertToLlm/transformContext/getApiKey/shouldStopAfterTurn/getSteeringMessages/getFollowUpMessages のドキュメントはすべて「throw/reject してはいけない、fallback を返す」と明記する。フックの throw は呼び出し点の try/catch で error result に変換されるが、ループ自身のコールバックは救えない——これが型契約のハード制約だ。

なぜ BeforeToolCallResultblock/reason の 2 つの optional フィールドだけで完全な結果ではないのか? before 段階は「走らせるかどうか」だけで「結果は何か」は決めないからだ。block されたツールはループが error result を構築し、reason が content に書かれる。これで before フックはツール固有の result の形を知らなくて済む。一方 AfterToolCallResult は result が既に存在する時点なので content/details/isError/terminate のフィールド単位の上書きを許す。

なぜ AgentToolexecute がジェネリック TParameters/TDetails なのか? TParametersTSchema (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 は深マージしない:content/details のフィールド上書きは「全部置き換え」で深くマージしない。isError/terminate は単独で置き換える。ドキュメントには「No deep merge is performed」と明記されている。packages/agent/src/types.ts:52-73 参照。
  • AgentContext はスナップショット:AgentcreateContextSnapshotmessages.slice()/tools.slice() してからループに渡す。ループ内部がこのコピーを mutate しても (partial を push、toolResult を push) 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 参照。