Follow up PR #265: refine chapters, diagrams, and add S20 (#283)

* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
gui-yue
2026-05-20 21:45:38 +08:00
committed by GitHub
parent c354cf7721
commit 1baf1aca5a
174 changed files with 35833 additions and 353 deletions

View File

@@ -0,0 +1,261 @@
# s13: Background Tasks — Slow Operations Go to the Background
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s11 → s12 → `s13` → [s14](../s14_cron_scheduler/) → s15 → ... → s20
> *"Slow operations go to the background, agent continues processing"* — Background threads run commands, inject notifications when done.
>
> **Harness Layer**: Background — Async execution, doesn't block the main loop.
---
## The Problem
Ever used a washing machine? Throw clothes in, press start, then go do other things — cook, reply to messages, read papers. 30 minutes later the machine beeps: done. You don't stand there waiting for 30 minutes.
The agent's bash tool is the same. `pip install torch` takes 10 minutes, `npm run build` takes 3 minutes. While these commands run, the agent waits for bash to return, unable to use that time to process other tasks.
Reading files is milliseconds, no wait. `git status` returns in under a second, no wait. But `npm install`? Minutes. The agent waits 10 minutes doing nothing, and LLM calls are billed by token — idle time is waste.
---
## The Solution
![Background Tasks Overview](images/background-tasks-overview.en.svg)
Teaching code carries forward S12's simplified task system and prompt assembly; to stay focused on background tasks, it omits full error recovery, memory, and skill systems. The only change: slow operations go to background threads, the agent continues running the loop, and background results are injected as notifications.
Sync vs Background:
| | Sync (s12) | Background (s13) |
|---|---|---|
| Slow operations | Agent waits | Background thread executes |
| Agent idle | Yes | No, continues processing |
| Result | Immediate return | Notification injected next turn |
| Decision criteria | — | `run_in_background` param (model explicit request), heuristic fallback |
---
## How It Works
### should_run_background: Explicit Request First, Heuristic Fallback
The model explicitly requests background execution via the bash tool's `run_in_background` parameter. If the model doesn't specify, the teaching version falls back to keyword heuristics:
```python
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
if tool_input.get("run_in_background"):
return True
return is_slow_operation(tool_name, tool_input)
```
CC's bash tool schema has a `run_in_background: boolean` parameter (`BashTool.tsx:241`). The model decides which commands go to background, no keyword guessing. The teaching version keeps heuristics as fallback, but the primary path is explicit model request.
### start_background_task: Background Execution and Lifecycle
Wraps the tool call in a worker function, dispatches to a daemon thread. Each background task gets a unique ID, with state tracked in the `background_tasks` dict:
```python
_bg_counter = 0
background_tasks: dict[str, dict] = {} # bg_id → {tool_use_id, command, status}
background_results: dict[str, str] = {} # bg_id → output
background_lock = threading.Lock()
def start_background_task(block) -> str:
"""Run tool in a daemon thread. Returns background task ID."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
def worker():
result = execute_tool(block)
with background_lock:
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": block.input.get("command", ""),
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
return bg_id
```
Returns `bg_id` instead of just `[Running in background...]`. `daemon=True` ensures threads exit when the agent process exits. The teaching version uses in-memory dicts for tracking; real CC has `LocalShellTaskState`, output redirected to files, with full lifecycle including stopping tasks and reading subsequent output.
### collect_background_results: Notification Collection
When background tasks complete, results are collected and formatted as `<task_notification>` messages:
```python
def collect_background_results() -> list[str]:
"""Collect completed results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] == "completed"]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>completed</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{output[:200]}</summary>\n"
f"</task_notification>")
return notifications
```
Notifications don't reuse the original `tool_use_id`. The original tool call was already answered with a placeholder `tool_result`; background completion is an independent event, injected in `task_notification` format. This respects Messages API tool pairing: one `tool_use` gets exactly one `tool_result`.
### Loop Integration
In the agent loop, tool execution splits into two paths. Notifications and results merge into a single user message:
```python
results = []
for block in response.content:
if block.type != "tool_use":
continue
if should_run_background(block.name, block.input):
bg_id = start_background_task(block)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"[Background task {bg_id} started] "
f"Result will be available when complete."})
else:
output = execute_tool(block)
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
# Merge notifications and tool results into one user message
user_content = []
bg_notifications = collect_background_results()
if bg_notifications:
for notif in bg_notifications:
user_content.append({"type": "text", "text": notif})
user_content.extend(results)
messages.append({"role": "user", "content": user_content})
```
Slow operations get a placeholder tool_result with `bg_id`, so the LLM knows this command is still running and can do other things first. When background completes, the notification is injected as an independent text block alongside the current turn's tool_results in one user message.
The teaching version polls background results while the agent loop continues running. Real CC uses a notification queue (`messageQueueManager.ts`) to deliver background completion events to subsequent turns, without waiting for the tool loop.
### Putting It Together
```
Turn 1:
LLM → bash "npm install" (run_in_background=true)
→ start_background_task → bg_0001
→ tool_result: "[Background task bg_0001 started]..."
→ LLM: "OK, I'll check later. Let me also read the config."
Turn 2:
LLM → read_file "package.json" (fast, sync)
→ tool_result: file content
→ collect: bg_0001 done! inject <task_notification>
→ LLM sees: config file + install notification in one message
```
The agent didn't wait — while npm install ran in the background, it read the config file.
---
## Changes from s12
| Component | Before (s12) | After (s13) |
|-----------|-------------|-------------|
| Execution model | All synchronous | Slow ops to background thread + notification injection |
| bash schema | `command` | `command` + `run_in_background` |
| New functions | — | `should_run_background`, `is_slow_operation`, `start_background_task`, `collect_background_results` |
| New types | — | `background_tasks: dict`, `background_results: dict`, `background_lock: Lock` |
| Notification format | — | `<task_notification>` (doesn't reuse tool_use_id) |
| Loop behavior | Tools execute serially | Slow ops async, fast ops sync, notifications collected each turn |
| Tools | 8 (s12) | 8 (unchanged, execution strategy changed) |
---
## Try It
```sh
cd learn-claude-code
python s13_background_tasks/code.py
```
Try these prompts:
1. `Run pip list in the background and find all Python files in this directory`
2. `Run npm install (use run_in_background) and while waiting, read package.json`
3. `Create a task to setup the project, then run pip list in the background`
What to observe: Are slow operations dispatched to background? Is a `bg_id` returned? Are background notifications injected in `<task_notification>` format?
---
## What's Next
Background tasks solved "slow operations don't block." But what if you want to do something on a schedule? Like "run tests every morning at 9am" or "check server status every 5 minutes."
s14 Cron Scheduler → Give the agent an alarm clock.
<details>
<summary>Deep Dive into CC Source</summary>
> The following is a complete analysis based on CC source code `query.ts` (lines 211, 1054-1060, 1411-1482), `services/toolUseSummary/toolUseSummaryGenerator.ts` (L15 prompt text), `LocalShellTask.tsx` (L24-25 constants, L59-98 watchdog logic), `messageQueueManager.ts` (notification queue), `utils/task/framework.ts` (L267 `enqueueTaskNotification`).
### 1. pendingToolUseSummary: Haiku Background Generation
CC starts a Haiku side-query after each batch of tool executions to generate a tool use summary. Initiated at `query.ts:1411-1482`, prompt text defined at `services/toolUseSummary/toolUseSummaryGenerator.ts:15` (variable `TOOL_USE_SUMMARY_SYSTEM_PROMPT`). The prompt is "Write a short summary label... think git-commit-subject, not sentence", past tense, ~30 characters.
Haiku summary (~1s) completes during the main model's streaming output (5-30s). Before the next turn starts, the summary is yielded. SDK consumers use these summaries for mobile progress display.
### 2. Thread Model: No Real Threads
CC runs on Node.js/Bun's single-threaded event loop. "Background" just means "don't await". `ShellCommand.background(taskId)` redirects stdout/stderr to files, letting the process run independently.
### 3. Seven Background Task Types
CC defines 7 background task types (`Task.ts:7-13`): `local_bash`, `local_agent`, `remote_agent`, `in_process_teammate`, `local_workflow`, `monitor_mcp`, `dream`. Each has its own registration, lifecycle, and notification mechanism.
### 4. Notification Injection: Command Queue
When a background task completes, it's enqueued via `enqueueTaskNotification` (`utils/task/framework.ts:267`) or `enqueuePendingNotification` (`messageQueueManager.ts`) into a shared command queue. The notification format is structured XML:
```xml
<task_notification>
<status>completed</status>
<summary>Background command "npm test" completed (exit code 0)</summary>
</task_notification>
```
Priority is `next` > `later` (`messageQueueManager.ts`). Background tasks default to `later` (don't block user input). Consumption point at `query.ts:1566-1593`.
### 5. Stall Watchdog
Background bash tasks have a watchdog (`LocalShellTask.tsx` L24-25 constants, L59-98 logic) that periodically checks if output has stalled. After 45 seconds with no growth, it detects interactive prompts (`(y/n)` etc.), preventing background tasks from getting stuck on unanswered interactive dialogs.
### 6. Concurrency Limits
Foreground tool calls: `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` (default 10 concurrent safe tools). Background bash tasks: no hard limit, they're independent subprocesses.
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -0,0 +1,261 @@
# s13: Background Tasks — 遅い操作はバックグラウンドへ
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s11 → s12 → `s13` → [s14](../s14_cron_scheduler/) → s15 → ... → s20
> *"遅い操作はバックグラウンドへ、agent は処理を継続"* — バックグラウンドスレッドでコマンドを実行、完了時に通知を注入。
>
> **Harness 層**: バックグラウンド — 非同期実行、メインループをブロックしない。
---
## 課題
洗濯機を使ったことがあるか衣類を入れ、スタートを押し、他のことをする——料理、メッセージ返信、論文読み。30 分後に洗濯機が「ピッピッ」と知らせる完了。30 分間立って待つ人はいない。
Agent の bash ツールも同じ。`pip install torch` は 10 分、`npm run build` は 3 分かかる。これらのコマンドが実行中、Agent は bash の戻りを待ち、その時間を他のタスクの処理に使えない。
ファイル読み込みはミリ秒、待たない。`git status` は 1 秒以内に戻る、待たない。しかし `npm install`分単位。Agent は 10 分間何もせず待ち、LLM 呼び出しはトークン課金、アイドル時間は無駄。
---
## ソリューション
![Background Tasks Overview](images/background-tasks-overview.ja.svg)
教学版は S12 の簡易タスクシステムとプロンプト組み立てを踏襲。バックグラウンドタスクに集中するため、完全なエラーリカバリ、メモリ、スキルシステムは省略。唯一の変更遅い操作をバックグラウンドスレッドに投げ、Agent はループを継続、バックグラウンド完了時に通知を注入。
同期 vs バックグラウンド:
| | 同期 (s12) | バックグラウンド (s13) |
|---|---|---|
| 遅い操作 | Agent が待機 | バックグラウンドスレッドで実行 |
| Agent アイドル | はい | いいえ、処理を継続 |
| 結果 | 即時返却 | 次ターンで通知を注入 |
| 判断基準 | — | `run_in_background` パラメータ(モデル明示的リクエスト)、ヒューリスティックフォールバック |
---
## 仕組み
### should_run_background: 明示的リクエスト優先、ヒューリスティックフォールバック
モデルは bash ツールの `run_in_background` パラメータで明示的にバックグラウンド実行をリクエストする。モデルが指定しない場合、教学版はキーワードヒューリスティックにフォールバック:
```python
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
if tool_input.get("run_in_background"):
return True
return is_slow_operation(tool_name, tool_input)
```
CC の bash ツールスキーマには `run_in_background: boolean` パラメータがある(`BashTool.tsx:241`)。モデルがどのコマンドをバックグラウンドにするかを決定、キーワード推測ではない。教学版はヒューリスティックをフォールバックとして残すが、主パスはモデルの明示的リクエスト。
### start_background_task: バックグラウンド実行とライフサイクル
ツール呼び出しをワーカー関数にラップし、daemon スレッドにディスパッチ。各バックグラウンドタスクは一意 ID を持ち、`background_tasks` 辞書で状態を追跡:
```python
_bg_counter = 0
background_tasks: dict[str, dict] = {} # bg_id → {tool_use_id, command, status}
background_results: dict[str, str] = {} # bg_id → output
background_lock = threading.Lock()
def start_background_task(block) -> str:
"""Run tool in a daemon thread. Returns background task ID."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
def worker():
result = execute_tool(block)
with background_lock:
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": block.input.get("command", ""),
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
return bg_id
```
`[Running in background...]` ではなく `bg_id` を返す。`daemon=True` で Agent プロセス終了時にスレッドも終了。教学版はメモリ内辞書で追跡。実際の CC は `LocalShellTaskState` を持ち、出力をファイルにリダイレクト、タスク停止や継続出力読み取りを含む完全なライフサイクルを備える。
### collect_background_results: 通知収集
バックグラウンドタスク完了時、結果を収集して `<task_notification>` メッセージとしてフォーマット:
```python
def collect_background_results() -> list[str]:
"""Collect completed results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] == "completed"]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>completed</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{output[:200]}</summary>\n"
f"</task_notification>")
return notifications
```
通知は元の `tool_use_id` を再利用しない。元のツール呼び出しはプレースホルダー `tool_result` で応答済み。バックグラウンド完了は独立したイベントで、`task_notification` 形式で注入する。これは Messages API のツールペアリングに従う1 つの `tool_use` に対して正確に 1 つの `tool_result`
### ループ統合
agent_loop でツール実行は 2 つのパスに分かれる。通知と結果は 1 つの user メッセージに統合:
```python
results = []
for block in response.content:
if block.type != "tool_use":
continue
if should_run_background(block.name, block.input):
bg_id = start_background_task(block)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"[Background task {bg_id} started] "
f"Result will be available when complete."})
else:
output = execute_tool(block)
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
# 通知とツール結果を 1 つの user メッセージに統合
user_content = []
bg_notifications = collect_background_results()
if bg_notifications:
for notif in bg_notifications:
user_content.append({"type": "text", "text": notif})
user_content.extend(results)
messages.append({"role": "user", "content": user_content})
```
遅い操作は `bg_id` 付きプレースホルダー tool_result を返し、LLM はコマンドがまだ実行中だと知り、先に他のことをできる。バックグラウンド完了時、通知は独立した text block として現在のターンの tool_result と一緒に 1 つの user メッセージを構成する。
教学版は agent loop が継続実行中にバックグラウンド結果をポーリングする。実際の CC は通知キュー(`messageQueueManager.ts`)でバックグラウンド完了イベントを後続ターンに配信、ツールループを待つ必要はない。
### 組み合わせて実行
```
Turn 1:
LLM → bash "npm install" (run_in_background=true)
→ start_background_task → bg_0001
→ tool_result: "[Background task bg_0001 started]..."
→ LLM: "OK, I'll check later. Let me also read the config."
Turn 2:
LLM → read_file "package.json" (fast, sync)
→ tool_result: file content
→ collect: bg_0001 done! inject <task_notification>
→ LLM sees: config file + install notification in one message
```
Agent は待たなかった。npm install がバックグラウンドで実行中に、設定ファイルを読んだ。
---
## s12 からの変更
| コンポーネント | 変更前 (s12) | 変更後 (s13) |
|--------------|------------|------------|
| 実行モデル | すべて同期 | 遅い操作はバックグラウンドスレッド + 通知注入 |
| bash スキーマ | `command` | `command` + `run_in_background` |
| 新規関数 | — | `should_run_background`, `is_slow_operation`, `start_background_task`, `collect_background_results` |
| 新規型 | — | `background_tasks: dict`, `background_results: dict`, `background_lock: Lock` |
| 通知形式 | — | `<task_notification>`tool_use_id を再利用しない) |
| ループ動作 | ツール直列実行 | 遅い操作は非同期、速い操作は同期、通知は毎ターン収集 |
| ツール | 8 (s12) | 8変更なし、実行戦略が変更 |
---
## 試してみる
```sh
cd learn-claude-code
python s13_background_tasks/code.py
```
以下のプロンプトを試してください:
1. `Run pip list in the background and find all Python files in this directory`
2. `Run npm install (use run_in_background) and while waiting, read package.json`
3. `Create a task to setup the project, then run pip list in the background`
観察ポイント:遅い操作はバックグラウンドにディスパッチされているか?`bg_id` は返されているか?バックグラウンド通知は `<task_notification>` 形式で注入されているか?
---
## 次の章
バックグラウンドタスクは「遅い操作がブロックしない」を解決した。しかし、定期的に何かをしたい場合は?例えば「毎朝 9 時にテストを実行」「5 分ごとにサーバーステータスを確認」。
s14 Cron Scheduler → Agent にアラームクロックを付ける。
<details>
<summary>CC ソースコード深掘り</summary>
> 以下は CC ソースコード `query.ts`211, 1054-1060, 1411-1482 行)、`services/toolUseSummary/toolUseSummaryGenerator.ts`L15 プロンプトテキスト)、`LocalShellTask.tsx`L24-25 定数, L59-98 ウォッチドッグロジック)、`messageQueueManager.ts`(通知キュー)、`utils/task/framework.ts`L267 `enqueueTaskNotification`)の完全分析に基づく。
### 一、pendingToolUseSummaryHaiku バックグラウンド生成
CC は各ツール実行バッチの後、Haiku サイドクエリを開始してツール使用サマリを生成。開始コードは `query.ts:1411-1482`、プロンプトテキストは `services/toolUseSummary/toolUseSummaryGenerator.ts:15`(変数 `TOOL_USE_SUMMARY_SYSTEM_PROMPT`)。プロンプトは "Write a short summary label... think git-commit-subject, not sentence"、過去形、約 30 文字。
Haiku サマリ(~1sはメインモデルのストリーミング出力5-30s中に完了。次のターン開始前にサマリを yield。SDK コンシューマーはこれらのサマリをモバイル進捗表示に使用。
### 二、スレッドモデル:本当のスレッドはない
CC は Node.js/Bun のシングルスレッドイベントループで動作。「バックグラウンド」は単に「await しない」こと。`ShellCommand.background(taskId)` は stdout/stderr をファイルにリダイレクトし、プロセスを独立実行。
### 三、7 種のバックグラウンドタスク型
CC は 7 種のバックグラウンドタスク型を定義(`Task.ts:7-13``local_bash``local_agent``remote_agent``in_process_teammate``local_workflow``monitor_mcp``dream`。それぞれ独自の登録、ライフサイクル、通知メカニズムを持つ。
### 四、通知注入:コマンドキュー
バックグラウンドタスク完了時、`enqueueTaskNotification``utils/task/framework.ts:267`)または `enqueuePendingNotification``messageQueueManager.ts`)で共有コマンドキューにエンキュー。通知形式は構造化 XML
```xml
<task_notification>
<status>completed</status>
<summary>Background command "npm test" completed (exit code 0)</summary>
</task_notification>
```
優先度は `next` > `later``messageQueueManager.ts`)。バックグラウンドタスクはデフォルト `later`(ユーザー入力をブロックしない)。消費点は `query.ts:1566-1593`
### 五、停滞ウォッチドッグ
バックグラウンド bash タスクにはウォッチドッグがある(`LocalShellTask.tsx` L24-25 定数, L59-98 ロジック。出力の停滞を定期チェックし、45 秒間増加がない場合にインタラクティブプロンプト(`(y/n)` 等)を検出、バックグラウンドタスクが無応答のインタラクティブダイアログでスタックするのを防ぐ。
### 六、同時実行制限
フォアグラウンドツール呼び出し:`CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`(デフォルト 10 同時実行安全ツール)。バックグラウンド bash タスク:ハードリミットなし、独立したサブプロセス。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -0,0 +1,261 @@
# s13: Background Tasks — 慢操作放后台
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s11 → s12 → `s13` → [s14](../s14_cron_scheduler/) → s15 → ... → s20
> *"慢操作丢后台, agent 继续处理"* — 后台线程跑命令, 完成后注入通知。
>
> **Harness 层**: 后台 — 异步执行, 不阻塞主循环。
---
## 问题
你用过洗衣机吗把衣服扔进去按下启动然后去干别的——做饭、回消息、看论文。30 分钟后洗衣机"滴滴滴"提醒你:好了。你不会站在洗衣机前面干等 30 分钟。
Agent 的 bash 工具也一样。`pip install torch` 要 10 分钟,`npm run build` 要 3 分钟。这些命令一跑Agent 就在等 bash 工具返回,没法利用这段时间处理别的任务。
读文件是毫秒级,不等。`git status` 一秒内返回,不等。但 `npm install`分钟级。Agent 等 10 分钟什么都不做,而 LLM 按 token 计费,空转就是浪费。
---
## 解决方案
![Background Tasks Overview](images/background-tasks-overview.svg)
教学代码沿用 S12 的简化任务系统和 prompt 组装为了聚焦后台任务省略完整错误恢复、记忆和技能系统。唯一的变动慢操作扔到后台线程Agent 继续跑循环,后台完成后把通知注入到对话里。
同步 vs 后台:
| | 同步 (s12) | 后台 (s13) |
|---|---|---|
| 慢操作 | Agent 干等 | 后台线程执行 |
| Agent 空闲 | 是 | 否,继续处理 |
| 结果 | 立即返回 | 下轮注入通知 |
| 判断标准 | — | `run_in_background` 参数(模型显式请求),启发式兜底 |
---
## 工作原理
### should_run_background: 显式请求优先,启发式兜底
模型通过 bash 工具的 `run_in_background` 参数显式请求后台执行。如果模型没指定,教学版用关键词启发式兜底:
```python
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
if tool_input.get("run_in_background"):
return True
return is_slow_operation(tool_name, tool_input)
```
CC 的 bash 工具 schema 里有 `run_in_background: boolean` 参数(`BashTool.tsx:241`)。模型自己决定哪些命令丢后台,不靠关键词猜。教学版保留启发式作为兜底,但主路径是模型显式请求。
### start_background_task: 后台执行与生命周期
把工具调用包装成 worker 函数,扔到 daemon 线程里执行。每个后台任务有唯一 ID状态存在 `background_tasks` 字典里:
```python
_bg_counter = 0
background_tasks: dict[str, dict] = {} # bg_id → {tool_use_id, command, status}
background_results: dict[str, str] = {} # bg_id → output
background_lock = threading.Lock()
def start_background_task(block) -> str:
"""Run tool in a daemon thread. Returns background task ID."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
def worker():
result = execute_tool(block)
with background_lock:
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": block.input.get("command", ""),
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
return bg_id
```
返回 `bg_id` 而不是只返回 `[Running in background...]``daemon=True` 确保 Agent 进程退出时线程跟着退出。教学版用内存字典追踪状态;真实 CC 有 `LocalShellTaskState`,输出重定向到文件,支持停止任务、读取后续输出等完整生命周期。
### collect_background_results: 通知收集
后台任务完成后,收集结果并格式化为 `<task_notification>` 通知:
```python
def collect_background_results() -> list[str]:
"""Collect completed results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] == "completed"]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>completed</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{output[:200]}</summary>\n"
f"</task_notification>")
return notifications
```
通知不复用原始 `tool_use_id`。原始 tool call 已经用占位 `tool_result` 回复了,后台完成是独立事件,用 `task_notification` 格式注入。这符合 Messages API 的工具配对语义:一个 `tool_use` 只对应一个 `tool_result`
### 循环中的集成
agent_loop 里,工具执行分两条路,通知和结果合并为一条 user 消息:
```python
results = []
for block in response.content:
if block.type != "tool_use":
continue
if should_run_background(block.name, block.input):
bg_id = start_background_task(block)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"[Background task {bg_id} started] "
f"Result will be available when complete."})
else:
output = execute_tool(block)
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
# 通知和工具结果合入同一条 user 消息
user_content = []
bg_notifications = collect_background_results()
if bg_notifications:
for notif in bg_notifications:
user_content.append({"type": "text", "text": notif})
user_content.extend(results)
messages.append({"role": "user", "content": user_content})
```
慢操作先回一个带 `bg_id` 的占位 tool_resultLLM 知道这个命令还在跑,可以先做别的事。后台完成后,通知作为独立 text block 和当前轮的 tool_result 一起组成 user 消息。
教学版在 agent loop 继续运行时轮询后台结果。真实 CC 通过通知队列(`messageQueueManager.ts`)把后台完成事件送入后续 turn不需要等工具循环。
### 合起来跑
```
Turn 1:
LLM → bash "npm install" (run_in_background=true)
→ start_background_task → bg_0001
→ tool_result: "[Background task bg_0001 started]..."
→ LLM: "OK, I'll check later. Let me also read the config."
Turn 2:
LLM → read_file "package.json" (fast, sync)
→ tool_result: file content
→ collect: bg_0001 done! inject <task_notification>
→ LLM sees: config file + install notification in one message
```
Agent 没干等npm install 跑后台的时候,它去读了配置文件。
---
## 相对 s12 的变更
| 组件 | 之前 (s12) | 之后 (s13) |
|------|-----------|-----------|
| 执行模型 | 全部同步 | 慢操作后台线程 + 通知注入 |
| bash schema | `command` | `command` + `run_in_background` |
| 新函数 | — | `should_run_background`, `is_slow_operation`, `start_background_task`, `collect_background_results` |
| 新类型 | — | `background_tasks: dict`, `background_results: dict`, `background_lock: Lock` |
| 通知格式 | — | `<task_notification>`(不复用 tool_use_id |
| 循环行为 | 工具串行执行 | 慢操作异步,快操作同步,通知每轮收集 |
| 工具 | 8 (s12) | 8不变执行策略变了 |
---
## 试一下
```sh
cd learn-claude-code
python s13_background_tasks/code.py
```
试试这些 prompt
1. `Run pip list in the background and find all Python files in this directory`
2. `Run npm install (use run_in_background) and while waiting, read package.json`
3. `Create a task to setup the project, then run pip list in the background`
观察重点:慢操作有没有被送到后台?`bg_id` 是否返回?后台通知有没有以 `<task_notification>` 格式注入?
---
## 接下来
后台任务解决了"慢操作不阻塞"。但如果想定时做某件事呢?比如"每天早上 9 点跑测试"、"每 5 分钟检查一次服务器状态"。
s14 Cron Scheduler → 给 Agent 装一个闹钟。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `query.ts`211, 1054-1060, 1411-1482 行)、`services/toolUseSummary/toolUseSummaryGenerator.ts`L15 prompt 文本)、`LocalShellTask.tsx`L24-25 常量, L59-98 看门狗逻辑)、`messageQueueManager.ts`(通知队列)、`utils/task/framework.ts`L267 `enqueueTaskNotification`)的完整分析。
### 一、pendingToolUseSummaryHaiku 后台生成
CC 在每批工具执行完后,启动一个 Haiku side-query 生成工具使用摘要。发起代码在 `query.ts:1411-1482`prompt 文本定义在 `services/toolUseSummary/toolUseSummaryGenerator.ts:15`(变量名 `TOOL_USE_SUMMARY_SYSTEM_PROMPT`)。提示是 "Write a short summary label... think git-commit-subject, not sentence",过去时态,约 30 字符。
Haiku 摘要(~1s在主模型流式生成5-30s期间完成。下一轮开始前把摘要 yield 出去。SDK 消费这些摘要做移动端进度展示。
### 二、线程模型:没有真正的线程
CC 运行在 Node.js/Bun 单线程事件循环中。"后台"只是 "不 await"。`ShellCommand.background(taskId)` 把 stdout/stderr 重定向到文件,让进程独立运行。
### 三、七种后台任务类型
CC 定义了 7 种后台任务(`Task.ts:7-13``local_bash``local_agent``remote_agent``in_process_teammate``local_workflow``monitor_mcp``dream`。每种有自己的注册、生命周期和通知机制。
### 四、通知注入:命令队列
后台任务完成后通过 `enqueueTaskNotification``utils/task/framework.ts:267`)或 `enqueuePendingNotification``messageQueueManager.ts`)入队到共享命令队列。通知格式是结构化的 XML
```xml
<task_notification>
<status>completed</status>
<summary>Background command "npm test" completed (exit code 0)</summary>
</task_notification>
```
优先级分 `next` > `later``messageQueueManager.ts`)。后台任务默认 `later`(不阻塞用户输入)。消费点在 `query.ts:1566-1593`
### 五、停滞看门狗
后台 bash 任务有一个看门狗(`LocalShellTask.tsx` L24-25 常量, L59-98 逻辑定期检查输出是否停滞45 秒无增长后检测交互式提示(`(y/n)` 等),防止后台任务卡在无人响应的交互式对话框。
### 六、并发限制
前台工具调用:`CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`(默认 10 个并发安全工具)。后台 bash 任务:没有硬性限制,它们是独立的子进程。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -0,0 +1,479 @@
#!/usr/bin/env python3
"""
s13: Background Tasks — thread-based async execution + notification injection.
Run: python s13_background_tasks/code.py
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
Changes from s12:
- threading.Thread for background execution
- background_tasks dict for lifecycle tracking (bg_id, command, status)
- background_results dict + threading.Lock for thread-safe storage
- should_run_background: model explicit request via run_in_background param
- is_slow_operation: fallback heuristic when model doesn't specify
- start_background_task: dispatch to daemon thread, return bg task id
- collect_background_results: gather completed, return as notifications
- agent_loop: slow ops → background + placeholder, inject notifications
- Notifications use <task_notification> format, not reused tool_use_id
Note: Teaching code keeps a basic agent loop to stay focused on background
tasks. S11's full error recovery (RecoveryState, backoff, escalation,
reactive compact, fallback model) is omitted.
"""
import os, subprocess, json, time, random, threading
from pathlib import Path
from dataclasses import dataclass, asdict
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
MEMORY_DIR = WORKDIR / ".memory"
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# ── Task System (from s12, synced) ──
TASKS_DIR = WORKDIR / ".tasks"
TASKS_DIR.mkdir(exist_ok=True)
@dataclass
class Task:
id: str
subject: str
description: str
status: str # pending | in_progress | completed
owner: str | None
blockedBy: list[str]
def _task_path(task_id: str) -> Path:
return TASKS_DIR / f"{task_id}.json"
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
task = Task(
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
subject=subject, description=description,
status="pending", owner=None,
blockedBy=blockedBy or [],
)
save_task(task)
return task
def save_task(task: Task):
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
def load_task(task_id: str) -> Task:
return Task(**json.loads(_task_path(task_id).read_text()))
def list_tasks() -> list[Task]:
return [Task(**json.loads(p.read_text()))
for p in sorted(TASKS_DIR.glob("task_*.json"))]
def get_task(task_id: str) -> str:
"""Return full task details as JSON."""
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
def can_start(task_id: str) -> bool:
"""Check if all blockedBy dependencies are completed.
Missing dependencies are treated as blocked."""
task = load_task(task_id)
for dep_id in task.blockedBy:
if not _task_path(dep_id).exists():
return False
if load_task(dep_id).status != "completed":
return False
return True
def claim_task(task_id: str, owner: str = "agent") -> str:
task = load_task(task_id)
if task.status != "pending":
return f"Task {task_id} is {task.status}, cannot claim"
if not can_start(task_id):
deps = [d for d in task.blockedBy
if not _task_path(d).exists() or load_task(d).status != "completed"]
return f"Blocked by: {deps}"
task.owner = owner
task.status = "in_progress"
save_task(task)
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
return f"Claimed {task.id} ({task.subject})"
def complete_task(task_id: str) -> str:
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
task.status = "completed"
save_task(task)
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy and can_start(t.id)]
print(f" \033[32m[complete] {task.subject}\033[0m")
msg = f"Completed {task.id} ({task.subject})"
if unblocked:
msg += f"\nUnblocked: {', '.join(unblocked)}"
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
return msg
# ── Prompt Assembly (from s10, synced) ──
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, "
"create_task, list_tasks, get_task, claim_task, complete_task.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
def assemble_system_prompt(context: dict) -> str:
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["workspace"]]
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
_last_context_key, _last_prompt = None, None
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
# ── Tools ──
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str, run_in_background: bool = False) -> str:
# run_in_background is handled by agent_loop dispatch, not here
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
# Task tools
def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
return f"Created {task.id}: {task.subject}{deps}"
def run_list_tasks() -> str:
tasks = list_tasks()
if not tasks:
return "No tasks. Use create_task to add some."
lines = []
for t in tasks:
icon = {"pending": "", "in_progress": "",
"completed": ""}.get(t.status, "?")
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else ""
owner = f" [{t.owner}]" if t.owner else ""
lines.append(f" {icon} {t.id}: {t.subject} "
f"[{t.status}]{owner}{deps}")
return "\n".join(lines)
def run_get_task(task_id: str) -> str:
try:
return get_task(task_id)
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def run_claim_task(task_id: str) -> str:
return claim_task(task_id, owner="agent")
def run_complete_task(task_id: str) -> str:
return complete_task(task_id)
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object",
"properties": {
"command": {"type": "string"},
"run_in_background": {"type": "boolean"}},
"required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"limit": {"type": "integer"}},
"required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"content": {"type": "string"}},
"required": ["path", "content"]}},
{"name": "create_task",
"description": "Create a new task with optional blockedBy dependencies.",
"input_schema": {"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
{"name": "list_tasks",
"description": "List all tasks with status, owner, and dependencies.",
"input_schema": {"type": "object", "properties": {},
"required": []}},
{"name": "get_task",
"description": "Get full details of a specific task by ID.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "claim_task",
"description": "Claim a pending task. Sets owner, changes status to in_progress.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "complete_task",
"description": "Complete an in-progress task. Reports unblocked downstream tasks.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
]
TOOL_HANDLERS = {
"bash": run_bash, "read_file": run_read, "write_file": run_write,
"create_task": run_create_task, "list_tasks": run_list_tasks,
"get_task": run_get_task, "claim_task": run_claim_task,
"complete_task": run_complete_task,
}
# ── Background Tasks (s13 new) ──
_bg_counter = 0
background_tasks: dict[str, dict] = {} # bg_id → {tool_use_id, command, status}
background_results: dict[str, str] = {} # bg_id → output
background_lock = threading.Lock()
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
if tool_input.get("run_in_background"):
return True
return is_slow_operation(tool_name, tool_input)
def execute_tool(block) -> str:
"""Execute a tool call block, return output."""
handler = TOOL_HANDLERS.get(block.name)
if handler:
return handler(**block.input)
return f"Unknown tool: {block.name}"
def start_background_task(block) -> str:
"""Run tool in a daemon thread. Returns background task ID."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
cmd = block.input.get("command", block.name)
def worker():
result = execute_tool(block)
with background_lock:
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": cmd,
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
print(f" \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
return bg_id
def collect_background_results() -> list[str]:
"""Collect completed background results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] == "completed"]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
summary = output[:200] if len(output) > 200 else output
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>completed</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{summary}</summary>\n"
f"</task_notification>")
print(f" \033[32m[background done] {bg_id}: "
f"{task['command'][:40]} ({len(output)} chars)\033[0m")
return notifications
# ── Context ──
def update_context(context: dict, messages: list) -> dict:
"""Derive context from real state."""
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
# ── Agent Loop (simplified, focused on background tasks) ──
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
except Exception as e:
messages.append({"role": "assistant", "content": [
{"type": "text",
"text": f"[Error] {type(e).__name__}: {e}"}]})
return
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"\033[36m> {block.name}\033[0m")
if should_run_background(block.name, block.input):
bg_id = start_background_task(block)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"[Background task {bg_id} started] "
f"Command: {block.input.get('command', '')}. "
f"Result will be available when complete."})
else:
output = execute_tool(block)
print(str(output)[:300])
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": output})
# Inject background notifications + tool results in one user message
user_content = []
bg_notifications = collect_background_results()
if bg_notifications:
for notif in bg_notifications:
user_content.append({"type": "text", "text": notif})
print(f" \033[32m[inject] {len(bg_notifications)} background "
f"notification(s)\033[0m")
user_content.extend(results)
messages.append({"role": "user", "content": user_content})
context = update_context(context, messages)
system = get_system_prompt(context)
if __name__ == "__main__":
print("s13: background tasks")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
context = update_context({}, [])
while True:
try:
query = input("\033[36ms13 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
agent_loop(history, context)
context = update_context(context, history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
print()

View File

@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 440" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#ea580c"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-orange" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ea580c"/>
</marker>
</defs>
<rect width="760" height="440" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Background Tasks — Slow ops to background, Agent keeps thinking</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s12 retained</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="158" y="66" fill="#ea580c" font-size="10" font-weight="600">s13 new</text>
<!-- ===== Top: s12 loop (compact) ===== -->
<rect x="30" y="86" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="110" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="106" x2="128" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="80" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</text>
<text x="191" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s12)</text>
<line x1="251" y1="106" x2="269" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="80" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</text>
<text x="322" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s11 retry)</text>
<line x1="372" y1="106" x2="390" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- TOOL DISPATCH (expanded) -->
<rect x="393" y="76" width="210" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="94" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
<text x="408" y="110" fill="#2563eb" font-size="9">fast? → sync execute (s12)</text>
<text x="408" y="124" fill="#ea580c" font-size="9" font-weight="600">slow? → run_in_background ★</text>
<!-- Loop back -->
<path d="M 603 106 L 640 106 L 640 148 L 70 148 L 70 126" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== Background execution (orange) ===== -->
<rect x="40" y="172" width="310" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="195" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">Background thread execution</text>
<text x="60" y="212" fill="#ea580c" font-size="9">run_in_background(tool_use_id, fn, *args)</text>
<text x="60" y="226" fill="#6b7280" font-size="8">threading.Thread(target=worker, daemon=True)</text>
<text x="60" y="240" fill="#6b7280" font-size="8">result → background_results[id] (threading.Lock protected)</text>
<!-- Arrow: dispatch → background -->
<path d="M 440 136 L 440 158 L 250 158 L 250 172" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<text x="320" y="170" fill="#ea580c" font-size="9">slow op</text>
<!-- ===== Notification injection (orange) ===== -->
<rect x="390" y="172" width="330" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="555" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">Notification injection</text>
<text x="408" y="212" fill="#ea580c" font-size="9">collect_background_results() check each turn</text>
<text x="408" y="226" fill="#6b7280" font-size="8">completed → tool_result inject into messages</text>
<text x="408" y="240" fill="#6b7280" font-size="8">pending → "[Running in background...]" placeholder</text>
<!-- Arrow: background → notification -->
<path d="M 350 220 L 390 220" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<!-- ===== Slow operation heuristic ===== -->
<rect x="40" y="274" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="296" fill="#1e3a5f" font-size="11" font-weight="600">Heuristic:</text>
<rect x="155" y="284" width="56" height="18" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="183" y="297" fill="#16a34a" font-size="9" text-anchor="middle">fast</text>
<text x="218" y="297" fill="#475569" font-size="9">read_file · git status · glob</text>
<rect x="360" y="284" width="56" height="18" rx="4" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="388" y="297" fill="#ea580c" font-size="9" text-anchor="middle">slow</text>
<text x="423" y="297" fill="#475569" font-size="9">npm install · pip install · pytest (timeout > 30s)</text>
<!-- ===== Timeline comparison ===== -->
<rect x="40" y="340" width="330" height="84" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="205" y="360" fill="#991b1b" font-size="10" font-weight="700" text-anchor="middle">s12 sync blocking</text>
<rect x="60" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="381" fill="#1e40af" font-size="8" text-anchor="middle">think</text>
<rect x="145" y="370" width="160" height="14" rx="3" fill="#fecaca" stroke="#dc2626" stroke-width="1"/>
<text x="225" y="381" fill="#991b1b" font-size="8" text-anchor="middle">waiting for bash 3min...</text>
<rect x="310" y="370" width="40" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="330" y="381" fill="#1e40af" font-size="7" text-anchor="middle">continue</text>
<text x="60" y="410" fill="#991b1b" font-size="9">Total ~3min, Agent idled for 3 minutes</text>
<rect x="390" y="340" width="330" height="84" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="555" y="360" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">s13 background execution</text>
<rect x="410" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="450" y="381" fill="#1e40af" font-size="8" text-anchor="middle">think</text>
<rect x="495" y="370" width="100" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="545" y="381" fill="#166534" font-size="8" text-anchor="middle">keep doing other work</text>
<rect x="600" y="370" width="100" height="14" rx="3" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="650" y="381" fill="#ea580c" font-size="8" text-anchor="middle">notification: result ready</text>
<text x="410" y="410" fill="#166534" font-size="9">Total ~3min, but Agent wasn't idle</text>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 440" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#ea580c"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-orange" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ea580c"/>
</marker>
</defs>
<rect width="760" height="440" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Background Tasks — 遅い操作はバックグラウンドへ、Agent は考え続ける</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s12 維持</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="158" y="66" fill="#ea580c" font-size="10" font-weight="600">s13 新規</text>
<!-- ===== Top: s12 loop (compact) ===== -->
<rect x="30" y="86" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="110" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="106" x2="128" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="80" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</text>
<text x="191" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s12)</text>
<line x1="251" y1="106" x2="269" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="80" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</text>
<text x="322" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s11 retry)</text>
<line x1="372" y1="106" x2="390" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- TOOL DISPATCH (expanded) -->
<rect x="393" y="76" width="210" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="94" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
<text x="408" y="110" fill="#2563eb" font-size="9">fast? → 同期実行 (s12)</text>
<text x="408" y="124" fill="#ea580c" font-size="9" font-weight="600">slow? → run_in_background ★</text>
<!-- Loop back -->
<path d="M 603 106 L 640 106 L 640 148 L 70 148 L 70 126" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== Background execution (orange) ===== -->
<rect x="40" y="172" width="310" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="195" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">バックグラウンドスレッド実行</text>
<text x="60" y="212" fill="#ea580c" font-size="9">run_in_background(tool_use_id, fn, *args)</text>
<text x="60" y="226" fill="#6b7280" font-size="8">threading.Thread(target=worker, daemon=True)</text>
<text x="60" y="240" fill="#6b7280" font-size="8">結果 → background_results[id] (threading.Lock で保護)</text>
<!-- Arrow: dispatch → background -->
<path d="M 440 136 L 440 158 L 250 158 L 250 172" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<text x="320" y="170" fill="#ea580c" font-size="9">slow op</text>
<!-- ===== Notification injection (orange) ===== -->
<rect x="390" y="172" width="330" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="555" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">通知注入</text>
<text x="408" y="212" fill="#ea580c" font-size="9">collect_background_results() 毎ターン確認</text>
<text x="408" y="226" fill="#6b7280" font-size="8">完了 → tool_result を messages に注入</text>
<text x="408" y="240" fill="#6b7280" font-size="8">未完了 → "[Running in background...]" プレースホルダー</text>
<!-- Arrow: background → notification -->
<path d="M 350 220 L 390 220" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<!-- ===== Slow operation heuristic ===== -->
<rect x="40" y="274" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="296" fill="#1e3a5f" font-size="11" font-weight="600">ヒューリスティック判定:</text>
<rect x="155" y="284" width="56" height="18" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="183" y="297" fill="#16a34a" font-size="9" text-anchor="middle">fast</text>
<text x="218" y="297" fill="#475569" font-size="9">read_file · git status · glob</text>
<rect x="360" y="284" width="56" height="18" rx="4" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="388" y="297" fill="#ea580c" font-size="9" text-anchor="middle">slow</text>
<text x="423" y="297" fill="#475569" font-size="9">npm install · pip install · pytest (timeout > 30s)</text>
<!-- ===== Timeline comparison ===== -->
<rect x="40" y="340" width="330" height="84" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="205" y="360" fill="#991b1b" font-size="10" font-weight="700" text-anchor="middle">s12 同期ブロッキング</text>
<rect x="60" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="381" fill="#1e40af" font-size="8" text-anchor="middle">思考</text>
<rect x="145" y="370" width="160" height="14" rx="3" fill="#fecaca" stroke="#dc2626" stroke-width="1"/>
<text x="225" y="381" fill="#991b1b" font-size="8" text-anchor="middle">bash 待ち 3分...</text>
<rect x="310" y="370" width="40" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="330" y="381" fill="#1e40af" font-size="7" text-anchor="middle">継続</text>
<text x="60" y="410" fill="#991b1b" font-size="9">合計 ~3分、Agent は3分間待機</text>
<rect x="390" y="340" width="330" height="84" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="555" y="360" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">s13 バックグラウンド実行</text>
<rect x="410" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="450" y="381" fill="#1e40af" font-size="8" text-anchor="middle">思考</text>
<rect x="495" y="370" width="100" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="545" y="381" fill="#166534" font-size="8" text-anchor="middle">別の作業を継続</text>
<rect x="600" y="370" width="100" height="14" rx="3" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="650" y="381" fill="#ea580c" font-size="8" text-anchor="middle">通知: 結果完了</text>
<text x="410" y="410" fill="#166534" font-size="9">合計 ~3分、Agent は遊ばず</text>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 440" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#ea580c"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-orange" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ea580c"/>
</marker>
</defs>
<rect width="760" height="440" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Background Tasks — 慢操作丢后台Agent 继续思考</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s12 保留</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="158" y="66" fill="#ea580c" font-size="10" font-weight="600">s13 新增</text>
<!-- ===== Top: s12 loop (compact) ===== -->
<rect x="30" y="86" width="80" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="70" y="110" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="110" y1="106" x2="128" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="131" y="80" width="120" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="191" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</text>
<text x="191" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s10-s12)</text>
<line x1="251" y1="106" x2="269" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="272" y="80" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="322" y="102" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</text>
<text x="322" y="116" fill="#94a3b8" font-size="8" text-anchor="middle">(s11 retry)</text>
<line x1="372" y1="106" x2="390" y2="106" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- TOOL DISPATCH (expanded) -->
<rect x="393" y="76" width="210" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="498" y="94" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
<text x="408" y="110" fill="#2563eb" font-size="9">fast? → 同步执行 (s12)</text>
<text x="408" y="124" fill="#ea580c" font-size="9" font-weight="600">slow? → run_in_background ★</text>
<!-- Loop back -->
<path d="M 603 106 L 640 106 L 640 148 L 70 148 L 70 126" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<!-- ===== Background execution (orange) ===== -->
<rect x="40" y="172" width="310" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="195" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">后台线程执行</text>
<text x="60" y="212" fill="#ea580c" font-size="9">run_in_background(tool_use_id, fn, *args)</text>
<text x="60" y="226" fill="#6b7280" font-size="8">threading.Thread(target=worker, daemon=True)</text>
<text x="60" y="240" fill="#6b7280" font-size="8">结果 → background_results[id] (threading.Lock 保护)</text>
<!-- Arrow: dispatch → background -->
<path d="M 440 136 L 440 158 L 250 158 L 250 172" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<text x="320" y="170" fill="#ea580c" font-size="9">slow op</text>
<!-- ===== Notification injection (orange) ===== -->
<rect x="390" y="172" width="330" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2"/>
<text x="555" y="194" fill="#9a3412" font-size="11" font-weight="700" text-anchor="middle">通知注入</text>
<text x="408" y="212" fill="#ea580c" font-size="9">collect_background_results() 每轮检查</text>
<text x="408" y="226" fill="#6b7280" font-size="8">已完成 → tool_result 注入 messages</text>
<text x="408" y="240" fill="#6b7280" font-size="8">未完成 → "[Running in background...]" 占位</text>
<!-- Arrow: background → notification -->
<path d="M 350 220 L 390 220" fill="none" stroke="#ea580c" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
<!-- ===== Slow operation heuristic ===== -->
<rect x="40" y="274" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="296" fill="#1e3a5f" font-size="11" font-weight="600">启发式判断:</text>
<rect x="155" y="284" width="56" height="18" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="183" y="297" fill="#16a34a" font-size="9" text-anchor="middle">fast</text>
<text x="218" y="297" fill="#475569" font-size="9">read_file · git status · glob</text>
<rect x="360" y="284" width="56" height="18" rx="4" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="388" y="297" fill="#ea580c" font-size="9" text-anchor="middle">slow</text>
<text x="423" y="297" fill="#475569" font-size="9">npm install · pip install · pytest (timeout > 30s)</text>
<!-- ===== Timeline comparison ===== -->
<rect x="40" y="340" width="330" height="84" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="205" y="360" fill="#991b1b" font-size="10" font-weight="700" text-anchor="middle">s12 同步阻塞</text>
<rect x="60" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="381" fill="#1e40af" font-size="8" text-anchor="middle">思考</text>
<rect x="145" y="370" width="160" height="14" rx="3" fill="#fecaca" stroke="#dc2626" stroke-width="1"/>
<text x="225" y="381" fill="#991b1b" font-size="8" text-anchor="middle">等 bash 3 分钟...</text>
<rect x="310" y="370" width="40" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="330" y="381" fill="#1e40af" font-size="7" text-anchor="middle">继续</text>
<text x="60" y="410" fill="#991b1b" font-size="9">总耗时 ~3minAgent 空 etc. 等了 3 分钟</text>
<rect x="390" y="340" width="330" height="84" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="555" y="360" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">s13 后台执行</text>
<rect x="410" y="370" width="80" height="14" rx="3" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="450" y="381" fill="#1e40af" font-size="8" text-anchor="middle">思考</text>
<rect x="495" y="370" width="100" height="14" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="545" y="381" fill="#166534" font-size="8" text-anchor="middle">继续做别的事</text>
<rect x="600" y="370" width="100" height="14" rx="3" fill="#fff7ed" stroke="#ea580c" stroke-width="1"/>
<text x="650" y="381" fill="#ea580c" font-size="8" text-anchor="middle">通知: 结果来了</text>
<text x="410" y="410" fill="#166534" font-size="9">总耗时 ~3min但 Agent 没闲着</text>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB