処理を自走させて agent をブロックしない
「Fire and forget — the agent doesn't block while the command runs.」
同期呼び出しの辛さ
S02 の bash ツールは同期だ:subprocess.run(..., timeout=120) で、npm install のような90秒かかるコマンドを実行すると、agent loop 全体が90秒間ブロックされる。ユーザーはターミナルを見つめ、ハングしているのか作業中なのか判断できない。
s08 の解決策:agent に background_run ツールを与える。これは即座に task_id を返し、コマンドは別のスレッドで実行される。agent はループを続けて他の作業をこなし、bg タスクが完了したら通知キューに結果を push する。
def run(self, command: str) -> str: task_id = str(uuid.uuid4())[:8] self.tasks[task_id] = {"status":"running", ...} thread = threading.Thread(target=self._execute, args=(task_id, command), daemon=True) thread.start() return f"Background task {task_id} started" # 即座に返る
結果はどうやって agent に届くのか
鍵はスレッドセーフなキューだ:bg スレッドが完了したらキューに append し、メインスレッドは毎回の LLM 呼び出し前にキューを drain して、完了通知を user メッセージとして messages に追加する。
def agent_loop(messages): while True: # Drain bg notifications before each LLM call notifs = BG.drain_notifications() if notifs: messages.append({ "role": "user", "content": f"<background-results>{notif_text}</background-results>", }) response = client.messages.create(...) ...
こうして agent が第 N ターンに bg タスクを spawn し、第 N+3 ターンでそのタスクが完了すると、次の LLM 呼び出しには自動的に結果が含まれる——モデルは <background-results> ブロックを見て「あのタスクが終わった、続けよう」と認識する。
タイムラインのデモ
以下の widget でシミュレートしよう:メインスレッドが毎秒 tick する(agent ループのリズムを模擬)。任意のタイミングで bg タスクを spawn できる。2つの流れが「drain ポイント」でどう合流するかを確認しよう。
どのコマンドをバックグラウンドに回すべきか
すべてのコマンドをバックグラウンドに送るべきではない。判断基準は2つ:
- 実行時間:数秒以内なら同期で実行した方がシンプルで、キューを管理するオーバーヘッドがない。
- 結果の即時性:次のステップがすぐにその結果を必要とする場合(例:
cat file.txtの直後にgrep)、バックグラウンドに意味はない——どうせ待つことになる。