大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

バックグラウンドタスク

バックグラウンドタスクは、エージェントの実行基盤が分離したシェルで動かすコマンドです。呼び出しは先にハンドルを返し、プロセスが動く間にエージェントは別の仕事を進めます。完了通知がコンテキストに入るのは次のターン境界で、生成中の応答には割り込みません。

本レッスンの直列に動くエージェントは、通常の呼び出しではコマンドを送り、結果を待ってから読みます。40 秒のテストを待つ間、他の仕事は進みません。バックグラウンド化してもコマンド自体は速くなりませんが、その 40 秒を別の仕事と重ねられます。ただし、重ねる仕事はコマンドの結果に依存してはいけません。

バックグラウンドタスクの仕組み

起動はハンドルを返します。 Bash(..., run_in_background=True) はシェルを起動し、約一秒で ID を返します。本例の ID は bash_1 です。その後、エージェントは次のターンへ進めます。シェルの現在の状態が、そのターンのコンテキストへ自動的に見えるわけではありません。

出力はバッファーにたまります。 分離プロセスの標準出力を蓄積し、BashOutput(bash_1) は前回の読み取り以降の内容を返します。画面を見ていなくても、テストの失敗情報は失われません。ただし、まだ何も出力していなければ空のバッファーを返し、出力が生じるまで待機はしません。

完了通知はターン境界で届きます。 プロセスの終了後、実行基盤が通知を用意します。本レッスンでは、モデルは一つの会話を入力として応答を生成し、その途中で使用中のコンテキストへメッセージを追加できません。現在のターンが終わり、次のコンテキストを組み立てる時点まで通知が待ちます。エージェントがアイドルなら終了時点で起こせますが、ターンの途中なら、終了を知るまで数秒空くことがあります。

ブロッキング呼び出しはコマンドの実行時間を占有します。バックグラウンド呼び出しは起動と結果を読む手間を加え、その間に独立した仕事を進めます。効果は、この二つの差で決まります。

ステップごとの解説

既定の仕事は、40 秒かかる pytest -q、テストが読まないファイルへの三つの CHANGELOG 形式の編集、最後の報告です。画面ではモデルの一ターンを 8 秒、編集を 5 秒、起動を 1 秒、読み取りを 1 秒に固定しています。

  • t = 0–8 秒:計画。 40 秒のコマンドと、結果に依存しない三つの編集を見て、分離実行を選びます。
  • t = 8–9 秒:起動。 run_in_background が bash_1 を返します。シェルは 9 秒から 49 秒まで動きます。
  • t = 9–22 秒:最初の編集。 新しいターンに 8 秒、CHANGELOG.md の編集に 5 秒かかります。エージェントとシェルの二つの時間線が同時に進み、この重なりが節約を生みます。
  • t = 22–35、35–48 秒:残る二つの編集。 同じ流れを繰り返します。48 秒には最終報告だけが残り、テストはあと一秒です。
  • t = 49 秒:プロセス終了。 bash_1 が終了コード 0 で終わり、42 行をバッファーに残します。エージェントは 48 秒から始めたターンの一秒目で、生成途中に通知を受け取れません。シェルの上に通知カードが現れ、右の破線が「プロセスは終了したが、まだ知らない」時間を示します。
  • t = 56 秒:通知の配信。 ターンが終わり、次のコンテキストに通知が入ります。プロセスは七秒前に終わっています。この差は遅いキューではなく、ターン境界によるものです。
  • t = 56–65 秒:読み取り。 一ターンの 8 秒と BashOutput(bash_1) の 1 秒で、42 行を会話へ取り込みます。
  • t = 65–73 秒:回答。 合計は 73 秒です。

派遣方法だけをブロッキングに変えると、同じ 40 秒のテストが最初のターン内で実行され、エージェントは 48 秒まで待ちます。その後、独立した三つの編集を一ターンずつ進め、95 秒で終わります。画面の比較線に対して、分離実行は 22 秒早く終了します。

どれだけ時間を節約できるか

節約は最大で min(command runtime, independent work available) − overhead です。重ねられる時間は、コマンドの実行時間と独立した仕事量の小さい方に制限され、そこからオーバーヘッドを引きます。終了がターン境界をわずかに逃すと読み取りがもう一ターン待つため、既定の実行は 29 秒ではなく 22 秒の節約です。

上限は独立した仕事量です。 起動後に三ターンと三回の編集、計 39 秒の独立した仕事があります。テストを 50 秒へ延ばすと節約は 29 秒、つまり 39 秒から 10 秒のオーバーヘッドを引いた値に達し、120 秒にしても 29 秒のままです。編集を終えたエージェントは待つしかありません。灰色の区間と「エージェントの待機時間」がその長さを示します。二時間のコマンドを分離しても、二時間分の利益が得られるわけではありません。

下限はオーバーヘッドで決まります。 起動に 1 秒、読み取りに一ターンと一回の呼び出しで 8 + 1 秒、合計 10 秒かかります。10 秒未満のコマンドでは、重なりがこの費用を補えません。10 秒なら両方式は同じで、5 秒ならバックグラウンド化が 5 秒遅くなります。

配信の時点も合計に影響します。 コマンドを 35 秒から 40 秒へ延ばすと、節約は 25 秒から 22 秒へ減ります。終了がターン境界を一秒越え、読み取りが一ターン後になるためです。この落ち込みを除けば、節約はコマンドの一秒ごとに一秒ずつ増え、編集を使い切る 50 秒以降は一定です。

境界ケース

  • 後続の仕事が出力を必要とする。 三つの編集がテストの失敗を直す仕事なら、重ねる独立作業はありません。起動後に依存で止まり、一ターン使って空の出力を早期に読み、その後で終了まで待ちます。同じ仕事でもバックグラウンドは 105 秒、ブロッキングは 95 秒です。差の 10 秒は起動と無駄な読み取りの一ターンです。10 秒のコマンドでは 17 秒の損になります。終了が読み取りより一秒遅く、読み取りが次のターン境界まで待つためです。
  • 終了時に既にアイドル。 実行基盤はその時点で起こせるため、通知の遅れはありません。これは長いコマンドの場合です。エージェントが先に仕事を終えたので節約はすでに上限にあり、余った実行時間は眠って過ごします。
  • 出力前に読む。 BashOutput はブロックせず、空の内容と実行中の状態を返します。その情報を得るにも一ターンかかります。
  • プロセスが終了しない。 サーバー、監視処理、停止したままのコマンドには完了通知が来ません。状態を知るには自分で読み取るかポーリングし、毎回一ターンを使います。長時間動くプロセスは、その寿命を意識して起動します。
  • 複数のシェル。 各コマンドには独自の時間線と通知があり、同じターン境界で配信を待ちます。エージェントの判断は引き続き直列なので、シェルを増やす効果も独立した仕事量に依存します。

よくある間違い

  • 非同期ならコマンドが速くなると思う。 変わるのは待ち方です。同時に進める仕事がなければ、合計時間は同じか長くなります。
  • 空の出力を成功とみなす。 実行中のプロセスは、まだ何も出力していないだけかもしれません。空のバッファーでは問題がないと判断できず、実行状態と終了結果を確認します。
  • 繰り返しポーリングする。 少量の状態を得るために、毎回一ターンのモデル遅延を払います。二回のポーリングが短いコマンドより高くつくこともあるため、完了通知を使います。
  • 通知が生成を中断すると期待する。 本レッスンの仕組みはターン境界で反応します。終了の瞬間に行動する保証はありません。
  • 読み取りのターンを忘れる。 通知が伝えるのは終了で、出力全体ではありません。分離実行を選ぶときは読み取りの費用も含めます。

このレッスンでの簡略化

これは特定の実行基盤のスケジューラーを再現したものではありません。所要時間は説明用の定数です。実際のモデルの一ターンは一秒未満から数十秒以上まで変わり、ツールの費用も異なります。出力も 42 行が一度に現れるとは限らず、ストリームで届きます。本例は一つのバックグラウンドシェル、一人のエージェントを使い、ユーザーのターンを挟みません。実際の会話ではユーザーのターンが通知の配信境界になることもあります。

保っている仕組みは、分離呼び出しが先にハンドルを返すこと、出力を読むまでバッファーに置くこと、完了メッセージをターン境界で渡すこと、節約が独立した仕事量に制限されることです。

エージェントループはターンの構造、タスクシステムは順序付けとクラッシュ後の復旧を扱います。サブエージェントがコンテキストで仕事を分けるのに対し、本レッスンは時間の重なりを扱います。プロセススケジューリングにも同じ考えがあります。一つの CPU が待ち状態になったとき、他に実行可能な仕事があれば切り替えが役立ちます。