型契約
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の 4 引数を取る。packages/agent/src/types.ts:332-355参照。 - イベント共用体:
AgentEventは 9 種のイベントの discriminated union で、agent/turn/message/tool execution の 4 層ライフサイクルで組織される。packages/agent/src/types.ts:374-389参照。
設計動機
なぜ AgentMessage が Message | 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 に変換されるが、ループ自身のコールバックは救えない——これが型契約のハード制約だ。
なぜ BeforeToolCallResult は block/reason の 2 つの optional フィールドだけで完全な結果ではないのか? before 段階は「走らせるかどうか」だけで「結果は何か」は決めないからだ。block されたツールはループが error result を構築し、reason が content に書かれる。これで before フックはツール固有の result の形を知らなくて済む。一方 AfterToolCallResult は result が既に存在する時点なので content/details/isError/terminate のフィールド単位の上書きを許す。
なぜ 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 は深マージしない:
content/detailsのフィールド上書きは「全部置き換え」で深くマージしない。isError/terminateは単独で置き換える。ドキュメントには「No deep merge is performed」と明記されている。packages/agent/src/types.ts:52-73参照。 - AgentContext はスナップショット:
AgentはcreateContextSnapshotでmessages.slice()/tools.slice()してからループに渡す。ループ内部がこのコピーを mutate しても (partial を push、toolResult を push)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 参照。