第4課:ツールと実行:エージェントに本当に手を動かさせる
一言でいうと:エージェントは「考える」だけではなく「やる」こともできなければならない——DSHではモデルが「どのツールをどんな引数で呼び出すか」の宣言だけを担当し、ツールレジストリ
ctx.toolsがディスパッチを担当し、bash・pty・subprocessといった差し替え可能な実行バックエンドが実際に手を動かし、結果がモデルのコンテキストに戻って次の思考ラウンドが始まります。
1. ユーザーストーリー:1本のBashから複数ステップにまたがるターミナルセッションへ
概念はいったん脇に置いて、実際のタスクを見てみましょう:「このプロジェクトの最近の変更を確認して、テストを一通り実行して。」
第1ラウンド:ごく普通のコマンド
モデルは「思考」した末、自分では手を動かさず、ツール呼び出しを宣言することを決めました:
{
"name": "bash",
"arguments": {
"command": "git log --oneline -3",
"description": "Show last 3 commits"
}
}
注意点:モデルは実際にキーボードを叩いたわけではありません。「bashを呼び出す、引数はこうだ」と言っているだけです。フレームワークはこの宣言を受け取ると:
- ツールレジストリ
ctx.toolsが引数を検証する; - 呼び出しを実行パイプラインに送り込む;
- bashエグゼキューターが実際に
bash -c "git log --oneline -3"を実行する; - 結果がテキストにまとめられてモデルのコンテキストに戻り、末尾には
[exit code: 0]というマーカーが付きます。
モデルは結果を見て「思考」を続けます——コミットを要約するかもしれませんし、次の呼び出しを発行するかもしれません。
第2ラウンド:時間のかかるタスク
コマンドの実行に時間がかかる場合(たとえば「テストを全部実行して」)、モデルは run_in_background: true という引数を追加できます。この呼び出しはブロックして待つことなく、すぐに返ります:
started background job <id>
コマンドはバックグラウンドで動き続けます。モデルは先に別のことをして、あとで job_output で出力を読み、job_list でどんなタスクがあるかを確認し、job_kill で不要になったタスクを止めます。DSHではバックグラウンドタスクは汎用のバックグラウンドジョブランタイム ctx.jobs に登録され、所有者とクリーンアップの記録が残ります——誰にも管理されない「野良プロセス」にはなりません。
第3ラウンド:「現場感」が必要なタスク
「依存関係をインストールして、コンパイルして、それから単体テストを実行する」——この3つのステップには順序があり、しかも同じ作業現場を共有したいのです:前のステップのカレントディレクトリ、環境変数、さらには対話的な入力まで、次のステップでもそのまま残っていてほしい。
通常のbash呼び出しは毎回まっさらです:毎回新しいシェルで実行され、呼び出し間で状態は保持されません。そこでモデルは別のツールに切り替えます——永続ターミナルを開くのです:
terminal_open:ターミナルセッションを開き、session idを取得する;terminal_send:そこにコマンドを送る(たとえばnpm install);terminal_read:ターミナルの出力を読み戻す;- 何ステップか経ったら
terminal_close:使い終わったら閉じる。
セッションが開いている限り、ステップ間の状態はずっと保持されます——これが「複数ステップにまたがってセッションを保持する」ということです。
💡 3つのシナリオに共通する点:モデルは終始「何がほしいかを言う」だけで、実際に手を動かすのはバックエンドです。ステップをまたぐ状態は永続ターミナルのようなバックエンドが保存を担当します。
2. ツールレジストリ ctx.tools:モデルの「取扱説明書」と実行パイプライン
モデルはどうやって世の中にどんなツールがあり、それぞれをどう使うかを知るのでしょうか?答えは:ツールレジストリが各ツールを1枚の「取扱説明書」に翻訳する——JSON Schemaでツールの名前・用途・引数を記述します。モデルは説明書を見れば「ああ、bashというツールがあって、commandとdescriptionを渡す必要があるんだな」と分かります。
2.1 モデルが見る説明書:bashツールの実際のschema
これはDSHリポジトリにあるbashツールの実際のschemaです(モデル側から見える完全な形):
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute."
},
"description": {
"type": "string",
"description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
},
"timeoutMs": {
"type": "number",
"description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
},
"workdir": {
"type": "string",
"description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
}
},
"required": [
"command",
"description"
]
}
(出典:docs/tool-catalog.zh.md のbashツールの章。schemaは packages/shell/tool-bash/src/index.ts から生成)
required には command と description しかないことに注目してください——モデルは少なくとも「何を実行するか」と「これが何をするものかの一言説明」を明確に言う必要があり、残りの引数(タイムアウト、作業ディレクトリ、バックグラウンド実行)はすべて任意です。
2.2 レジストリそのもの:ctx.tools
DSHでは、レジストリはコンテキスト内の ctx.tools サービスで、いくつかの重要な操作を提供します:
ctx.tools.register(definition):ツールを登録する——「説明書」(schema)と「エグゼキューター」(execute関数)を結び付けます;ctx.tools.schemas(scope):現在のスコープで見えるすべてのschemaを返す——これがモデルが毎回のリクエストで目にする「説明書の束」です;ctx.tools.guard(guard):ガードを登録する——呼び出しが実際に実行される前に許可/拒否の判定を行います。
ツールプラグインが登録されると、そのschemaは自動的にシステムプロンプトの組み立てに流れ込み、モデルは次のリクエストラウンドでそれを見て呼び出せるようになります。
2.3 実行パイプライン:1回の呼び出しの一生
ツール呼び出しは「そのまま実行する」ほど単純ではなく、一本のパイプライン全体を通り抜けます。リポジトリのドキュメントの原文:
「ツールプラグインはそれぞれのschemaとエグゼキューターを登録する;agent loop(エージェントループ)は各呼び出しを順に
tools/pre-execute(拡張可能な許可/拒否ゲート)→ 登録済みの単調ガード →tools/execute(タイムアウト/リトライ/メトリクスプラグインのためのラッピングディスパッチ層)→tools/post-execute(結果の検査/置換、コンテキストの付加)→ 定義が所有するfinalizeContent境界 → 観測専用のtools/result通知に通す。」—— 出典:
packages/core/tools/README.zh.md
平易に言い換えると:
| 段階 | 何をするか | 日常のたとえ |
|---|---|---|
tools/pre-execute | 許可/拒否/確認のゲート(権限、承認、サンドボックスフックはすべてここにぶら下がる) | 入場前のセキュリティチェック |
| 単調ガード | ツールの所有者が定めた拒否ポリシー。一度拒否されると後続の段階で覆せない | 店主の「お断り」 |
tools/execute | ラッピングディスパッチ層:タイムアウト、リトライ、メトリクスはすべてこの層にある | レジ脇の「時間切れのお知らせ」 |
tools/post-execute | 結果の検査/置換、ブロック、追加コンテキストの付加 | 梱包時に中身が合っているか確認する |
finalizeContent | ツール定義が所有する最後のコンテンツ加工。最終コンテンツの置換のみ可能 | 最後のラベルを貼る |
tools/result | 観測専用の最終結果通知 | 出入口の監視カメラの記録 |
重要な点:パイプラインは「シーム(継ぎ目)」設計——権限、承認、タイムアウト、リトライといった横断的関心事はすべて固定のイベントにぶら下がっており、ツール本体はそれらを気にする必要がありません。どの段階も差し替え・拡張が可能で、ツール自身のexecute関数は1行も変えずに済みます。
模型只声明要什么工具,注册表调度,后端执行——每个环节都可替换
3. 実行バックエンド:すべてが差し替え可能な「シーム」
レジストリは「ディスパッチ」を担当しますが、実際に手を動かすのは実行バックエンドです。DSHは最も一般的な3種類の実行バックエンドをすべて差し替え可能なシームとして作りました——モデル側から見えるツールインターフェースは変わらず、下の実装は自由に切り替えられます。
3.1 bash:フォアグラウンドとバックグラウンド
ctx.shell はbashエグゼキューターのシーム(継ぎ目)の標準仕様で、モデル側の bash ツールはこのシームに登録されています:
- フォアグラウンド:コマンドが終わるのを待ち、stdout/stderrと終了コードを受け取ります。bashツールの規約は「各呼び出しは新しいシェルで実行される:呼び出し間で状態(cwd、変数、関数)は一切保持されない。
workdirを渡し、cdは使わないこと」(出典:docs/tool-catalog.zh.md); - バックグラウンド:
run_in_background: trueで、すぐにjob idを返し、汎用タスクランタイムctx.jobsが引き継ぎます。
エグゼキューターは誰か?それはデプロイ設定次第です:dsh-bash-local はローカルのsubprocessで実行し、dsh-bash-sandbox は先にサンドボックスを一枚かぶせてから実行し、pwsh-local はPowerShellのセマンティクスで実行します。エグゼキューターを変えてもモデル側は何も変更不要です。
3.2 pty:所有者でスコープされた永続ターミナル
ctx.terminals は永続的で所有者スコープに限定されたターミナルセッションを提供します。リポジトリのドキュメントの原文:
「PTYはPseudo-Terminal(疑似端末)の略です。この機能は永続的で所有者スコープに限定されたターミナルセッションを提供し、ツール呼び出しをまたいで状態を保持する必要があるワークフローや、対話的なstdinを使うワークフローに適しています。」
—— 出典:
packages/terminal/README.zh.md
モデルに公開されるツールは6つ:terminal_open、terminal_send、terminal_read、terminal_signal、terminal_close、terminal_list。特に注意すべきは所有権の分離です:すべての操作で、開始したAgent(エージェント)が完全に同一であることが求められます——モデルが別のagentのターミナルidを知ったとしても、そのターミナルを操作することはできません。
PTYは単発のbashやファイルシステムツールの補完であり、後者のより厳格な操作ごとの規約を置き換えるものではありません:一回きりの小さな操作にはbashを使い、永続的な現場が必要なワークフローにだけターミナルを開きます。
3.3 subprocess:管理されたプロセスツリー
ctx.subprocess はより低レイヤーの共有プロセス基盤です:実行ファイルの探索、明確な仕様を持つ管理された子プロセスツリー、そしてPTY割り当てとフォアグラウンドプロセスグループを担当する低レイヤーのターミナルプロセスプリミティブ。bashエグゼキューターもPTYシェルバックエンドも、その上に構築されています。
「管理された」とはどういう意味か?プロセスのライフサイクルはサービスが管理します——spawnされたプロセスツリー、ハンドルのライフタイム、シグナル送信、終了させてから待つリソース解放まで、すべてに明確な規約があります。利用側は「プロセスの意味」(たとえば「1本のbashコマンド」)を定義するだけで、車輪の再発明をする必要はありません。
3.4 なぜ「シーム」と呼ぶのか
第2課の視点に戻りましょう:DSHは「できる」ことを3つの層に分けました——モデルの宣言、レジストリのディスパッチ、バックエンドの実行。各層の間のインターフェースは固定(schema+パイプラインイベント)で、実装は差し替え可能です。これがエンジニアリングにおける「シーム」です:サンドボックスを変えたい、エグゼキューターを変えたい、タイムアウトポリシーを追加したい——どれもシームの片側だけをいじればよく、もう片側には影響しません。
4. 結果がコンテキストに戻る:次のループの始まり
ツールの実行が終わっても、物語はまだ終わりません——結果はモデルのコンテキストに戻らなければならない。さもないとモデルは「目隠しをした状態」になってしまいます。
- 呼び出しが発行されると、セッションに
tool/callイベントが1件記録されます(実行前に記録); - 結果が確定すると、
tool/resultイベントが追記されます——これがモデルに見える唯一の結果です; - 結果はテキストとしてモデルのコンテキストに入ります:コマンド出力、
[exit code: N]マーカー、起こりうる切り詰めやエラーメッセージ; - モデルは結果を読み終えると新しい「思考」のラウンドを始めます——要約するかもしれませんし、再びツール呼び出しを発行するかもしれません。
第2課のステップ構造を覚えていますか?思考 → 行動 → 観察 → 再び思考。ツール呼び出しとは「行動+観察」という一対の動作がフレームワークに落ちた姿です:行動=レジストリが呼び出しを実行バックエンドに振り分けること、観察=結果がコンテキストに戻ること。1回のループが終わり、次のループが始まる——複数ステップのタスクはこうして一歩ずつ完了していきます。
だからこそ、毎ラウンドが安全で制御可能でなければなりません:誰がどのツールを呼び出せるのか、コマンドがサンドボックス外のファイルに触れてよいのか、先にユーザーに確認すべきか……これらこそが、次の「サンドボックスとセキュリティ」の課で扱う問題です。
重要ポイントの振り返り
- モデルは宣言するだけ、フレームワークが手を動かす:モデルはツール呼び出しの宣言(ツール名+引数)を発行し、レジストリがディスパッチし、実行バックエンドが実際に実行します。
- レジストリ ctx.tools:各ツールをJSON Schemaの「説明書」に翻訳します。registerで登録、schemasでモデルに提供、guardでガードを設定します。
- 実行パイプライン:
tools/pre-execute→ ガード →tools/execute→tools/post-execute→finalizeContent→tools/result。権限、承認、タイムアウト、リトライはすべて固定のシームにぶら下がります。 - バックエンドはすべて差し替え可能なシーム:bash(フォアグラウンド/バックグラウンド)、pty(所有者でスコープされた永続ターミナル)、subprocess(管理されたプロセスツリー)。実装を変えてもモデル側には影響しません。
- 結果はコンテキストに戻る:
tool/resultがモデルに見える唯一の結果となり、次の「思考 → 行動 → 観察」のラウンドを引き起こし、複数ステップのタスクはこうして完了します。
🚀 次の課(第4課)では「サンドボックスとセキュリティ」を扱います:コマンドがどのファイルに触れられるのか、どんなときにユーザーに許可を求める必要があるのか——エージェントに「手を動かせる」だけでなく「むやみに動かさない」ようにする話です。
確認テスト · ツールと実行
回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。
