ツールを呼び出せるAgentなら、最小限の実装はそこまで複雑ではない。モデルはメッセージを受け取り、テキストまたはTool Callを返す。プログラムがツールを実行し、Tool Resultをメッセージリストへ追加して、続けてモデルをリクエストする。Vercel AI SDKのToolLoopAgentがラップしているのは、この種の複数ステップ実行だ。モデル、Instructions、Tools、停止条件はAgent定義にまとめられ、SDKが以降のループを駆動する。[1]
この抽象化を外して看看吧。中心となるロジックは、次のコードに近くなる。
これで完全なTool Loopになる。モデルはツールの結果を使って次の判断を続け、1回のユーザー入力から複数回のモデル呼び出しが発生することもある。
ただし、境界も明確だ。実行ライフサイクル全体が、いまだに現在の関数呼び出しの中に収まっている。
messagesはメモリ上にあり、現在どのToolを実行中かはローカル変数で表せる。キャンセルは現在の呼び出しのAbortSignalに依存できる。関数が返れば、この実行も終わる。呼び出し元が停止、再開、実行中の入力、接続をまたぐ継続実行を求めないなら、この構造で十分だ。
Agent Runtimeが独立した問題になるのは、通常、実行ライフサイクルがこの関数呼び出しを超えてからになる。
Tool CallingやMemoryなどの機能リストよりも、この境界のほうが、システムが本当にRuntimeを必要としているかを判断しやすい。
Tool Loopが管理できること
まず問題を限定する。Tool Loopが担当するのは、ローカルな1本の実行チェーンだ。
User Input
↓
Model Request
↓
Assistant Message
↓
Tool Call
↓
Tool Execution
↓
Tool Result
↓
Next Model Request
主に次の3つを扱う。
- いつモデルをリクエストするか
- モデルがツールの実行を要求した場合の実行方法
- Tool Resultを次のモデルリクエストへ渡す方法
この3つはすべて連続した実行の中で起こる。そのため、ローカル変数と関数スタックがそのまま状態コンテナになる。
たとえば、次の状態は現在の呼び出し内に直接保持できる。
この段階なら、より複雑なオブジェクトモデルを導入する必要はない。Runtimeとは、単にこれらのローカル変数をクラスへ移しただけでは、新しいアーキテクチャ上の価値が自動的に生じるわけではない。
本当の変化は、状態が異なるライフサイクルを持ち始めたときに現れる。
最初の変化:実行状態をリクエストから切り離す
Webアプリケーションでは、この問題が表面化しやすい。
ブラウザがリクエストを送り、バックエンドがAgentの実行を開始すると仮定しよう。
Browser Request
│
▼
Agent Loop
│
├── Model
├── Tool
├── Model
└── Tool
実行全体がHTTP/SSE接続に依存していなければならないなら、接続が切れた時点で通常はコールチェーンが解放される。通常のリクエストならこれは当然だ。クライアントが離脱すれば、リクエストは終わる。
Agentの実行では別の意味合いが必要になる。接続は転送だけを担当し、タスク自体はバックエンドで管理し続ける。クライアントが再接続したら、現在の状態を読み取って結果の受信を再開できる。
この段階では、少なくとも2つのライフサイクルが現れる。
Connection
──────────────>
Execution
──────────────────────────────>
接続は実行を所有しなくなった。
この分離によって、次のような状態にはネットワークリクエストとは独立した所有者が必要になる。
こうした状態は、1回のController呼び出しやSSE handlerに信頼して保持することはもうできない。
2つ目:実行中にも新しい入力が来る
通常の関数呼び出しには、呼び出し開始時点ですでに引数が確定している、という安定した前提がある。
Agentなら必ずしもそうはならない。現在のToolの実行中に、ユーザーが追加の要求を出すことがある。プラグインが新しい環境情報を注入することもある。スケジューラが後続のタスクを送ることもある。他のAgentが現在のAgentに結果を渡すこともある。
Agentの実行中に、もう一度直接呼び出した場合。
システムは具体,面に答えなければならない。このメッセージはどの実行に属するのか。
何通りか、意味がまったく異なる。
この違いはmessages.push()だけでは表現できない。
入力は制御されたスケジューリング構造に入り、Runtimeが安定した境界で消費する必要がある。PiのSteering / Follow-up QueueやDeepSeek HarnessのInboxが扱うのは、この層の問題だ。[2][3]
そのためRuntimeはModelとToolを実行するだけでなく、入力をどう渡すかという責務も担うようになった。
3つ目:長期履歴と現在の実行状態を分離する
Tool Loopのmessagesは通常、2つの役割を同時に担う。
历史记录
+
下一次模型请求的输入
単純な実装ではこれは問題ない。会話が長くなるにつれ、2つの役割は徐々に分離する。
長期Sessionは完全な履歴を保持する。モデルが現在見ている内容は、ウィンドウサイズ、Compaction、Memory、分岐選択、Agent Policyによって左右される。
より正確なデータ関係は次のようになる。
Sessionには長期的な事実が保存される。Contextは、現在の1回のモデル計算で見える情報を表す。
この2つの概念が同じmessages[]を使い続けると、典型的な問題が後で出る。
- Compaction後、元の履歴を上書きするのか
- 圧縮で消えたメッセージもUIに表示できるのか
- 同じSessionを異なるAgentに渡したとき、異なるContext Policyを使えるのか
- Tool実行中の一時的な状態を長期履歴に含めるべきか
- Provider Messageとアプリ内部のMessageに同じ構造が必須か
そのためRuntimeは、長期状態から現在の計算ビューを組み立てる役割も担う。
Runtimeの中核は状態の所有権にある
ここまで来ると、最初に見たTool Loopをもう一度見直せる。
当初、すべての状態は現在の関数が暗黙的に所有していた。
function call
├── messages
├── tool state
├── abort signal
└── control flow
ライフサイクルを分けた後は、状態の所有権を再配分する必要がある。
Runtimeの価値が具体的になる。単一の関数呼び出しをまたぐ実行状態に安定した所有者を用意し、状態の変化を進める役割を担う。
最小の構造は、次のように表せる。
この図でLoopはRuntimeの部品の一つにすぎない。Loopの外側にある状態と制御関係も、Runtimeが維持しなければならない。
Runtimeが設定、稼働状態、持続的事実を区別する必要もある
実行状態を関数呼び出しから切り離すと、別の問いが現れる。何を長期保存し、何を現在の活動期間中だけ有効にするべきか。
Runtimeの周囲にあるデータは、3種類に分けられる。
3種類のデータはしばしば同じAgentオブジェクトにまとめられる。しかし、ライフサイクルは異なる。
AgentDefinitionは複数のSessionで再利用でき、バージョン番号とともに長期保存されることもある。「このAgentには何ができるか」を表す。
Live Runtime Stateは、特定のプロセスで今起きていることだけを表す。AbortController、active promise、ネットワークstreamのハンドルなどがこの層に属する。プロセスが終了すれば、これらのオブジェクトを直接復元する価値はない。
Durable Session Stateはプロセスをまたいで保持しなければならない。新しいRuntimeが現在の作業ビューを再構築できるだけの事実を記録する。
したがってAgentを復元するとき、古いプロセス内オブジェクトを直接デシリアライズするより、持続的事実から新しいRuntimeを作る方が安定している。
Durable Session Facts
+
Agent Definition / Version
↓
create new Runtime
↓
rebuild Context / pending work
↓
continue
この境界は永続化の設計に直接影響する。メモリ上のフィールドをすべてシリアライズしても、得られるのは、アップグレードもプロセス間復元も難しいRuntime Snapshotが多い。より安定した方法は、実行状態を再構築できる事実を保存し、新しいRuntimeがプロセス内オブジェクトを作ることだ。
この視点に立つと、RuntimeとPersistenceは隣り合う層になる。Runtimeは活動状態を管理し、Persistenceは復元に必要な事実を保存する。密密に連携できるが、同じオブジェクトを共有する必要はない。
Runtimeはツール関数の集合というより状態機械に近い
Runtimeがidle / running / waiting / cancelledなどの状態を持つようになると、多くのAPIの呼出し可否が現在の状態で決まる。
たとえばPiではactive状態のときに再度prompt()することは禁止されている。steer()、followUp()、abort()なら許される。idle状態なら新しいPromptを始められる。
抽象化すると次のようになる。
Runtimeはデータを保存するだけでなく、どの状態遷移が合法かを制限する。
これがAPI設計にとって重要なのは、外部インターフェースが単なるステートレス関数の集合なら、呼び出し側が、現在の実行が存在するか、入力はどこへ送られるべきか、いつキャンセルできるかを自分で理解しなければならないからだ。これらの制約をRuntimeにまとめれば、不正な状態をより早く表面化でき、上位製品にはより安定した振る舞い契約を提供できる。
PiがRuntimeのサンプルとして適した理由
PiのAgentは、低層のagent-loop.tsと比較的明確に分かれている。
低層のLoopはモデルリクエスト、Tool Call、Tool Result、Steering、Follow-upの実行を進める。外層のAgentは、ソースコードでは low-level agent loop の stateful wrapper と説明されている。[2]
Agentが保持する状態はおおよそ次のとおり。
これらのフィールドは、明確な階層をなす。
messages
已经完成的稳定 transcript
streamingMessage
当前正在生成、仍会变化的 Assistant Message
pendingToolCalls
当前执行中的 Tool 状态
これらの状態をすべてmessagesへ押し込んではいない。
PiはSteering QueueとFollow-up Queueも維持している。Agentが活動中は、再度prompt()を直接呼び出して2本目の実行チェーンを作ることはできない。新しいメッセージは、明示的な入力制御APIを通して現在のRuntimeへ渡す。[2]
ここまでで導いたRuntimeの特徴に合っている。持続状態、単一の活動実行チェーン、入力スケジューリング、キャンセル、ライフサイクルイベントに、明確な所有者がいる。
DeepSeek HarnessはRuntimeをさらに外側へ抽象化する
DeepSeek HarnessのAgent抽象は、Harness寄りだ。
公開されているAgent handleは次のインターフェースを持つ。
session
inbox
status
ctx
cancel()
whenIdle()
send()
steer()
followup()
inject()
具体的なAgent Loopはプラグインが提供する。[3]
ここでもう1つ重要な変化が現れる。アプリケーションとプラグインは安定したAgentインターフェースに依存し、具体的なLoop実装は差し替え可能だ。
つまり、Runtimeの「実行機構」自体も差し替え可能になる。Agent Loop、Session、Tool Registry、Model Adapterをすべて1つのコアクラスに固定する必要はない。この方向は最終的にHarnessとPlugin Runtimeにつながる。本シリーズ後段のCordis / DeepSeek Harnessの章で別途扱う。
第1回では、判断を1つだけ残しておこう。
Runtimeは実行状態のライフサイクルと進行を解決する。Harnessはさらに、Runtimeの機能をどのように組み合わせ、置き換え、拡張するかを解決する。
2つの層が注目する領域は異なる。
Vercel AI SDKは別の角度からの参照点になる
その価値は、Webアプリケーションでしばしば混同される複数の層を分けている点にある。
ToolLoopAgentは複数ステップのAgent実行を担当する。AI SDK UIは、useChat、Transport、UI Message、メッセージの永続化、Stream Resumeなどを提供する。[1][6]
したがって、次のように理解できる。
Browser UI
│
▼
Transport
│
▼
Application / Agent Execution
│
▼
Model + Tools
AI SDKは、すべての製品にPiやDSHのような長寿命のAgentオブジェクトを要求するわけではない。ただし、UI状態、ネットワーク転送、Agent実行、モデルメッセージを別々の層にできることを、はっきりした参照として示している。
これは、後でWeb Agent Runtimeを議論するときの重要な土台にもなる。
RuntimeとAgent Frameworkの境界も設計しておく必要がある
Runtimeが独立すると、今度はすべてのAgent能力をこの層へまとめredditは容易だ。主要部 需要重新设计。
容易起来。続けて膨張为核心が再構成される。
「現在の実行の進行に直接関与するかどうか」を、おおよその境界として使える。
次のような機能は、通常、Runtimeと強く関係する。
Context preparation
Model invocation
Tool execution
Input delivery
Cancellation
Lifecycle state
これらは、現在のLoopが次にどう進むかに直接影響する。
一方、次のような機能はHarnessやApplication layerにある可能性が高い。
Plugin discovery
Skill marketplace
Project management
Workflow definition
UI layout
Billing
Tenant permission
これらはRuntimeにTool、Hook、Policyを登録できるが、低層の実行オブジェクトの固有フィールドにする必要はない。
PiとDSHは、2つの異なる拡張境界として対比できる。Piは比較的小さなAgent Coreを保ち、Extensionで動作を拡張する。DSHはさらに、Agent Loop、Session、Tool Registryなどの能力自体もPlugin Runtimeに組み込む。拡張境界は異なるが、どちらも同じ問題に取り組んでいる。Agent Coreが製品機能の増加に合わせて膨張しないようにすることだ。
したがってRuntimeの抽象もほどよい範囲に留める必要がある。実行ライフサイクルを必要な粒度で完全に保有しながら、外側のHarnessに組み合わせの余地を残す。
実践的な状態所有チェック方法
新しいフィールドや機能を設計するとき、次の4つを順番に確認すればよい。
たとえばpendingToolCallsは現在の実行期間中にしか有効でなく、UIとキャンセル制御に直接影響するので、Runtimeが保持するのに適している。
Session Branchはプロセスをまたいで復元する必要があり、現在1回のTool呼び出しを直接制御する役割はないため、Session Persistenceに入れる方がよい。
Run Statusは製品から独立して照会・復元する必要があるので、Application Runtimeに置ける。
この確認方法なら、「Agentに関連する機能はすべてAgentクラスに入れる」という方式より、境界を明確に保つやすい。
RuntimeにSession、Run、Turn、Stepが現れる理由
実行ライフサイクルを独立させると、依然として複数の異なる時間スケールが存在する。
それぞれが異なる範囲の問題を解決する。
Sessionは長期的な会話を管理する。Runはアプリケーション層での持続実行1回を指すことが多い。Turnは連続したAgent作業の1区間を表す。Stepは、1回のモデル判断とそこから直接生じるTool作業に近い。
これらのオブジェクトは、アーキテクチャを複雑にするためにあるのではない。ライフサイクルを1つ増やすのは、通常、上位の層がある状態を安定して表現できないためだ。
たとえば。
第2回ではこの導出をさらに進め、Session、Run、Turn、Stepの境界を定める。
ContextがRuntimeのチェーンに属する理由
PiはLoopを開始する前にcreateContextSnapshot()を呼び出す。
その後、このAgentContextをrunAgentLoop()に渡す。[5]
Providerへ実際に到達する前なら、ContextはtransformContextやconvertToLlmで変換されることもある。
この実装は、しばしば見落とされる境界を示している。
Long-lived Agent State
│
▼
Context Snapshot
│
▼
Loop Local Context
│
▼
Provider Context
モデルへの入力は、Runtimeが特定の時点に組み立てた計算ビューだ。
だからこそ、後でSessionの永続化を実装するとき、「モデルが今見ているもの」と「システムが長期保存するもの」を同じ問題として扱ってはいけない。
本シリーズで採用する境界
以降のentriesでは、この関係を一貫して使う。
各項目の意味は次のとおり。
- AgentDefinitionはAgentの設定と機能集合を表す。
- Sessionは長期的な会話の事実を保存する。
- Runtimeは現在の実行状態を管理し、状態の変化を進める。
- Runはアプリケーション層で1回の実行を管理する境界。
- TurnとStepはLoop内部の連続した作業とモデル判断の境界。
- Contextはモデルリクエストの前に現在の状態から構築される。
このモデルは後続の記事で異なるフレームワークの実装境界を比較するために使う。各フレームワークが同じAPIを採用する必要はない。
第1回で最後に残す判断は1つだけでいい。
Agentの実行ライフサイクルが1回の関数呼び出しやネットワークリクエストから分離し始めたら、状態の所有権も划分し直さなければならない。Tool Loopは引き続き実行カーネルだが、Runtimeは独立したシステムレイヤーになり始める。
次のselectionは、これらの状態の時間スケールを扱い、Session、Run、Turn、Stepを分ける必要性の理由と、Agent Coreに残すべき境界やアプリケーション層に任せるべき境界を説明する。
参考文献
[1] Vercel AI SDK, ToolLoopAgent: https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent
[2] Pi Agentソース: https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[3] DeepSeek Harnessコアサブシステム: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/core.md
[4] DeepSeek Harnessエージェントライフサイクル: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/agent-lifecycle.md
[5] Pi createContextSnapshot(): https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[6] Vercel AI SDK UI: https://ai-sdk.dev/docs/ai-sdk-ui
[7] Pi Coding Agent README: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md
[8] DeepSeek Harnessアーキテクチャ: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md