# Asynchronous Agent with Parallel Execution and Interruption / 带并行执行和打断能力的异步 Agent > Companion code for *AI Agents in Depth*, Chapter 6 — **Experiment 6-2 ★★★**. Event-driven async Agent framework (Flux): parallel tools, interrupt/cancel, state checkpoints. > 配套《深入理解 AI Agent》第 4 章 **实验 6-2 ★★★**。事件驱动异步 Agent 框架(Flux):并行工具、打断取消、状态检查点。 ← [Chapter 4 index / 返回第 4 章目录](../README.md) --- ## English This directory is the runnable code for Experiment 6-2. It implements the core of the event-driven asynchronous Agent framework (Flux) described in [`agent_framework_design.md`](./agent_framework_design.md). Building on the simple event queue of 4-5, this experiment goes deeper into async Agents and focuses on four things: **async tool execution, event queues and batching, interruption, and cancel/status query for parallel tools**. The Agent must manage concurrent tasks, handle interrupt and recovery, and decide from real-time state. Two usage paths: - **Offline demos (recommended first; zero deps, no API key)**: three measurable capabilities—**parallel vs serial wall-clock, interrupt/cancel then recover, checkpoint persist and restore**. No network, no LLM, no need for `openai`; `python demo.py` runs as-is. - **LLM scenarios (four book verification scenes)**: decisions by a real LLM (default OpenAI `gpt-5.6-luna`, function calling); API key required. Both paths share the same async runtime. Long-running work uses **real, allowlisted Python subprocesses**. No shell is opened: progress is parsed from child stdout, completion includes observed hashes/return codes, and cancellation terminates the child PID. ## Code map - **Run first:** python demo.py (offline, no API key). - **Start here:** runtime.py::AgentRuntime - **Core behavior:** runtime.py::_dispatcher routes urgency; _handle_interrupt cancels at a safe point; run_llm_turn advances the trajectory. - **State / protocol:** Event, inbox, pending batch and checkpoint files. - **Verifier:** offline demo assertions and run_real_experiment.py evidence gates. - **Experiment variable:** serial vs parallel tools, interrupt timing and checkpoint restore. - **Skip on first pass:** provider clients and the frontend/demo formatting. ### Architecture Corresponds to section 5 of the design doc—all single-threaded `asyncio`: ``` ┌──────────────┐ user msg / interrupt ──▶ │ inbox │ all inbound raw events async task completion ──▶ │ (asyncio.Q) │ └──────┬───────┘ │ ┌──────────▼───────────┐ classify_urgency() │ _dispatcher │──▶ interrupt / immediate / deferred └──────────┬───────────┘ ┌────────────────┼───────────────────┐ INTERRUPT │ IMMEDIATE│ DEFERRED│ cancel current turn+async direct to work pending buffer; tools; leave trace batch when async.result arrives ┌──────────▼───────────┐ │ work │ event batches to process └──────────┬───────────┘ ┌──────────▼───────────┐ │ _worker │ per batch: append trajectory -> run_llm_turn() │ turn_task cancelable│ (on interrupt, cancel this child task) └──────────────────────┘ TaskManager: bounded real subprocesses (start / query / cancel / cancel_all) natural completion -> inject as new event (async.result) into inbox ``` Code files: | File | Role | |------|------| | `events.py` | `Event` model (checkpoint `to_dict`/`from_dict`), event types, **urgency** `classify_urgency()` | | `tasks.py` | Allowlisted subprocess `TaskManager` (stdout progress, OS cancel/query by id, executable receipts, `snapshot`/`restore`) | | `analysis_worker.py` | Real executable that hashes and analyzes `book/chapter4.md` while emitting progress | | `run_real_experiment.py` | Durable, model-independent acceptance campaign for all four manuscript scenarios | | `runtime.py` | `AgentRuntime`: event loop, two processing modes, LLM function calling, tool exec, `save_checkpoint`/`load_checkpoint` | | `async_demos.py` | Three **offline demos** (no API key): parallel wall-clock, interrupt/recover, state checkpoint | | `demo.py` | Unified CLI (argparse subcommands): offline demos + four LLM scenarios | #### Two event-processing mechanisms (design doc 5.1) - **Cancellation-based**: urgent events (user “cancel/stop”) immediately cancel the in-flight LLM turn and all background async tools; write interrupt event + cancel receipts into the trajectory. - **Queued**: non-urgent events (supplementary instructions) go to a `pending` buffer without interrupting work; when an async tool finishes and emits `async.result`, pending events are batch-appended to the trajectory, then one LLM turn runs. Urgency rules (simple and explainable): 1. Interrupt keywords (cancel/stop/停止…) → `INTERRUPT` (cancellation-based) 2. A question (question mark or interrogative, e.g. “what time is it?”) → `IMMEDIATE` (reply now, **do not** cancel background tasks) 3. Other supplementary instructions (e.g. “reply in Japanese”) → `DEFERRED` (queue, batch) #### Async tools `run_terminal_command` is **async**: it returns a `task_id` placeholder immediately, then starts an allowlisted child process with `asyncio.create_subprocess_exec` and `shell=False`. Progress comes from the process's stdout. On completion, file metrics, input/stdout hashes, PID, return code, and duration are injected as a **new event** (`async.result`). `cancel_task` sends termination to that PID. Also: `query_task` by id and `get_current_time` for immediate questions. **Timeline acceleration**: one logical progress tick maps to `0.4` wall-clock seconds by default (`FLUX_TICK_REAL` tunable). The real executables retain the **3% / 2% / 1% per tick** rates and **50%** threshold. ### How to run CLI entry is `demo.py` with argparse subcommands; `python demo.py --help` for full usage. For canonical, machine-readable evidence across all four scenarios: ```bash python run_real_experiment.py --tick-real 0.15 pytest -q test_tasks_env.py test_real_tasks.py ``` The campaign takes about 20 seconds and writes per-scenario receipts, Japanese HTML, the integrated report, acceptance gates, and a hash manifest under `validation/experiment_6_2/`. #### Offline demos (no API key, out of the box) ```bash cd chapter4/async-agent python demo.py # default: run all three offline demos in order python demo.py offline # same: explicit sequential offline demos python demo.py parallel # capability 1: parallel vs serial wall-clock (prints speedup) python demo.py interrupt # capability 2: interrupt/cancel mid-task, then recover python demo.py state # capability 3: checkpoint persist + cross-session restore + verify ``` These demos do not network, call LLMs, or require `openai`—pure `asyncio` measures parallel speedup, frozen state after interrupt, and checkpoint save/restore. #### Tests (offline) The automated regression tests live in `tests/` and do not require an API key. ```bash # From the repository root, include the dev extra for pytest: uv sync --locked --python 3.12 --extra ch4 --extra dev # pip testing fallback: # python -m pip install -e ".[ch4,dev]" cd chapter4/async-agent python -m pytest tests ``` #### LLM verification scenarios (four book scenes; API key required) ```bash # From the repository root: use the shared Chapter 4 environment uv sync --locked --python 3.12 --extra ch4 # Activate it before changing directories: # macOS/Linux: source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1 # Windows cmd: .venv\Scripts\activate.bat # pip fallback when uv is not installed: # python -m pip install -e ".[ch4]" cd chapter4/async-agent # Single-project compatibility path, still supported during migration: # python -m pip install -r requirements.txt cp env.example .env # set OPENAI_API_KEY python demo.py scenarios # all four scenarios python demo.py scenarios --scenario 1 # scenario 1 only (async exec + immediate question) python demo.py scenarios --scenario 3 # scenario 3 only (interrupt) ``` Default OpenAI `gpt-5.6-luna`. Other OpenAI-compatible providers: ```bash # Moonshot (default model: reasoning model kimi-k3) LLM_PROVIDER=moonshot python demo.py scenarios --scenario 1 # Volcengine ARK (LLM_MODEL = inference endpoint id) LLM_PROVIDER=ark LLM_MODEL=ep-xxxx python demo.py scenarios --scenario 1 # Alibaba Cloud Model Studio / Bailian (Qwen) LLM_PROVIDER=dashscope DASHSCOPE_API_KEY=your-key LLM_MODEL=qwen3.7-plus python demo.py scenarios --scenario 1 ``` > **Universal OpenRouter fallback**: if `OPENAI_API_KEY` is unset (and not moonshot/ark), with `OPENROUTER_API_KEY` set, `demo.py` routes via OpenRouter and maps model ids to `provider/model` (`gpt-*` → `openai/…`, `claude-*` → `anthropic/claude-opus-4.8`, ids with `/` pass through). Or set `LLM_PROVIDER=openrouter` explicitly. Example: > `OPENROUTER_API_KEY=your-openrouter-api-key LLM_MODEL=openai/gpt-5.6-luna python demo.py scenarios --scenario 1` > Moonshot defaults to **reasoning model `kimi-k3`** (older `kimi-k2-*-preview` / `moonshot-v1-*` are outdated/retired). Reasoning models need `temperature=1` and `max_tokens>=2048`; `demo.py` applies these automatically by model. > Legacy: `python demo.py --scenario N` is equivalent to `scenarios --scenario N`. Log sources are color-coded: `USER`, `AGENT`, `TOOL`, `TASK`, `TRAJ`, `STATE`, `SYSTEM`. ### Three offline capabilities (real measured output) The following are excerpts from **real runs** (no API key). #### Capability 1: parallel vs serial tools (`python demo.py parallel`) Four independent read-only perception-style tools (read file / search / DB / vector retrieval): serial `await` vs parallel `asyncio.gather`: ``` ── 结果对比 ───────────────────────────────────────────── 串行总耗时(Σ 各工具) 4.51s 并行总耗时(gather) 1.50s 并行理论下界(最慢单个) 1.50s 加速比 = 串行 / 并行 3.00x ───────────────────────────────────────────────────────── ``` Wall-clock drops from sum-of-tools to max-single—quantifying “read-only perception tools naturally parallelize.” #### Capability 2: interrupt / cancel / recover (`python demo.py interrupt`) Three parallel background tasks; user asks a question first (does not block tasks), then sends “cancel”: ``` [ 1.00s] USER | (即时提问)现在几点了? [ 1.00s] AGENT | 现在 00:14:26。三个后台任务仍在并行推进,未被这次提问阻塞。 [ 2.00s] USER | (打断)取消 [ 2.00s] TASK | T1 已被取消 🛑(进度停在 39%) [ 2.00s] TASK | T2 已被取消 🛑(进度停在 26%) [ 2.00s] TASK | T3 已被取消 🛑(进度停在 13%) ── 打断后各任务状态(进度冻结在中途)─────────────────── task_id 命令 状态 进度 T1 python analyze_fast.py cancelled 39% T2 python analyze_mid.py cancelled 26% T3 python analyze_slow.py cancelled 13% ───────────────────────────────────────────────────────── [ 2.05s] SYSTEM | 打断处理完毕,系统恢复空闲,可继续接受新任务…… [ 5.52s] TASK | T4 完成 ✅ [ 5.52s] AGENT | 已从打断中恢复,新任务 T4 正常完成:…… ``` Interrupt freezes cancelled tasks’ progress; the runtime itself stays healthy and can finish new work immediately. #### Capability 3: state checkpoint persist and restore (`python demo.py state`) Session A produces a trajectory + two running background tasks, saved to `checkpoints/agent_state.json`; session B restores with a fresh runtime and verifies: ``` ── 恢复校验 ───────────────────────────────────────────── 轨迹事件数 保存前 3 -> 恢复后 3 [一致 ✓] 可重建 LLM 上下文消息 4 条(system + 轨迹回放) task_id 命令 保存前进度 恢复后状态 进度 T1 python analyze_fast.py 21% suspended 21% T2 python analyze_slow.py 7% suspended 7% ───────────────────────────────────────────────────────── ``` Trajectory and task progress fully persist across sessions; running tasks restore as `suspended` with last known progress for upper layers to “re-run” or “continue from progress.” ### Four LLM verification scenarios #### Scenario 1: async tool execution Agent runs a long terminal command; user inserts “what time is it?”. Because the long command is async and non-blocking, the Agent answers immediately with `get_current_time`, then presents analysis when the background task finishes. #### Scenario 2: event queue and batching During a long task, user sends “reply in Japanese” then “format as a webpage.” These non-urgent instructions queue; on task completion the framework **batch-appends** them; the Agent outputs Japanese HTML. #### Scenario 3: interruption During a long task, user says “cancel.” The framework cancels the current turn and background async tools, recording `user.interrupt` and a `system.note` with cancelled task ids. #### Scenario 4: parallel cancel and status query User: “run these three scripts at once; when the first finishes, query the others’ progress; cancel any under 50%.” Speeds 3% / 2% / 1% per second. Agent starts three async tasks; after the fastest finishes, queries the others (~66% and ~33%), cancels the under-50% one, and reports when the rest complete. ### Real LLM scenario output (key fragments) > Real calls to `gpt-5.6-luna` (OpenAI-compatible); timestamps are real seconds; need API key to reproduce. **Scenario 1 (async + immediate question)** ``` [ 3.97s] AGENT | 任务已在后台启动(task_id:T1)。完成后我会根据日志分析结果给出结论。 [ 4.96s] TASK | T1 `python analyze_logs.py` 进度 22% ← 任务仍在后台跑 [ 5.19s] TOOL | get_current_time -> 2026-07-18 13:43:30 ← 即时提问先回应 [ 6.91s] AGENT | 现在是 2026 年 7 月 18 日 13:43:30。 [12.19s] TRAJ | + async.result 异步完成 T1 ← 真实结果作为新事件注入 [16.67s] AGENT | 日志分析已完成,结论如下:共扫描 12,840 条记录… ← 再呈现分析 ``` **Scenario 2 (batching)** ``` [ 1.50s] SYSTEM | 事件进入排队缓冲(当前积压 1 条) [ 1.90s] SYSTEM | 事件进入排队缓冲(当前积压 2 条) [12.05s] TASK | T1 完成 ✅ [12.05s] SYSTEM | 异步结果到达,批量处理 2 条积压的非紧急事件 [12.06s] TRAJ | + async.result 异步完成 T1 [12.06s] TRAJ | + user.input 记得最后用日语回复 [12.06s] TRAJ | + user.input 把结果整理成一个网页(HTML) ... [22.38s] AGENT | …