From 4faf3e4c52d514b259052b266feec503d487f42c Mon Sep 17 00:00:00 2001 From: ggttyy1 Date: Sat, 11 Jul 2026 18:36:19 +0800 Subject: [PATCH 1/6] fix: reset rounds_since_todo on each session --- s05_todo_write/code.py | 1 + s06_subagent/code.py | 1 + s07_skill_loading/code.py | 1 + 3 files changed, 3 insertions(+) diff --git a/s05_todo_write/code.py b/s05_todo_write/code.py index 9c88de35..33fda525 100644 --- a/s05_todo_write/code.py +++ b/s05_todo_write/code.py @@ -236,6 +236,7 @@ rounds_since_todo = 0 def agent_loop(messages: list): global rounds_since_todo + rounds_since_todo = 0 # 每轮新对话从0开始计数 while True: # s05: nag reminder — inject if model hasn't updated todos for 3 rounds if rounds_since_todo >= 3 and messages: diff --git a/s06_subagent/code.py b/s06_subagent/code.py index 2732246b..f4299171 100644 --- a/s06_subagent/code.py +++ b/s06_subagent/code.py @@ -316,6 +316,7 @@ rounds_since_todo = 0 def agent_loop(messages: list): global rounds_since_todo + rounds_since_todo = 0 # 每轮新对话从0开始计数 while True: # s05: nag reminder if rounds_since_todo >= 3 and messages: diff --git a/s07_skill_loading/code.py b/s07_skill_loading/code.py index b4506fc5..20c3b196 100644 --- a/s07_skill_loading/code.py +++ b/s07_skill_loading/code.py @@ -360,6 +360,7 @@ rounds_since_todo = 0 def agent_loop(messages: list): global rounds_since_todo + rounds_since_todo = 0 # 每轮新对话从0开始计数 while True: if rounds_since_todo >= 3 and messages: messages.append({"role": "user", From ffe1fef0b5e27a56164afb86a5c433b691cd6131 Mon Sep 17 00:00:00 2001 From: ggttyy1 Date: Sat, 11 Jul 2026 19:47:55 +0800 Subject: [PATCH 2/6] fix: make rounds_since_todo a local variable --- s05_todo_write/code.py | 5 +---- s06_subagent/code.py | 5 +---- s07_skill_loading/code.py | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/s05_todo_write/code.py b/s05_todo_write/code.py index 33fda525..0529cba3 100644 --- a/s05_todo_write/code.py +++ b/s05_todo_write/code.py @@ -232,11 +232,8 @@ register_hook("Stop", summary_hook) # agent_loop — same as s04 + nag reminder counter # ═══════════════════════════════════════════════════════════ -rounds_since_todo = 0 - def agent_loop(messages: list): - global rounds_since_todo - rounds_since_todo = 0 # 每轮新对话从0开始计数 + rounds_since_todo = 0 while True: # s05: nag reminder — inject if model hasn't updated todos for 3 rounds if rounds_since_todo >= 3 and messages: diff --git a/s06_subagent/code.py b/s06_subagent/code.py index f4299171..901a6368 100644 --- a/s06_subagent/code.py +++ b/s06_subagent/code.py @@ -312,11 +312,8 @@ register_hook("Stop", summary_hook) # agent_loop — same as s05 + nag reminder, task auto-dispatches # ═══════════════════════════════════════════════════════════ -rounds_since_todo = 0 - def agent_loop(messages: list): - global rounds_since_todo - rounds_since_todo = 0 # 每轮新对话从0开始计数 + rounds_since_todo = 0 while True: # s05: nag reminder if rounds_since_todo >= 3 and messages: diff --git a/s07_skill_loading/code.py b/s07_skill_loading/code.py index 20c3b196..45c04313 100644 --- a/s07_skill_loading/code.py +++ b/s07_skill_loading/code.py @@ -356,11 +356,8 @@ register_hook("Stop", summary_hook) # agent_loop — same as s05-s06 + nag reminder # ═══════════════════════════════════════════════════════════ -rounds_since_todo = 0 - def agent_loop(messages: list): - global rounds_since_todo - rounds_since_todo = 0 # 每轮新对话从0开始计数 + rounds_since_todo = 0 while True: if rounds_since_todo >= 3 and messages: messages.append({"role": "user", From fbd40c453972c41600619c8f77a727f029a38e7b Mon Sep 17 00:00:00 2001 From: ggttyy1 Date: Sat, 11 Jul 2026 20:18:19 +0800 Subject: [PATCH 3/6] fix: resolve lambda default param capturing current_model --- s11_error_recovery/code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/s11_error_recovery/code.py b/s11_error_recovery/code.py index 8a4862b8..f54fe28c 100644 --- a/s11_error_recovery/code.py +++ b/s11_error_recovery/code.py @@ -272,10 +272,10 @@ def agent_loop(messages: list, context: dict): # ── LLM call: with_retry handles 429/529, outer handles rest ── try: response = with_retry( - lambda mt=max_tokens, mdl=state.current_model: - client.messages.create( - model=mdl, system=system, messages=messages, - tools=TOOLS, max_tokens=mt), + lambda: client.messages.create( + model=state.current_model, system=system, + messages=messages, tools=TOOLS, + max_tokens=max_tokens), state) except Exception as e: # Path 2: prompt_too_long -> reactive compact (once) From fc114def7a8beba4137c06e51a8bd98f7766cf7c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Jul 2026 21:29:53 +0800 Subject: [PATCH 4/6] docs(s17): fix incorrect s16 comparison in changes-from-s16 table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table had several inaccurate entries for the s16 column. s16 already had a WORK→IDLE→SHUTDOWN state machine, handled shutdown_request during IDLE, and used consume_lead_inbox for protocol routing and history injection — but the table described s16 as having none of these. This updates the table so the s16 column reflects what s16 actually does, verified against s16_team_protocols/code.py. All three README languages are synchronized. Closes #480 --- s17_autonomous_agents/README.en.md | 12 ++++++------ s17_autonomous_agents/README.ja.md | 12 ++++++------ s17_autonomous_agents/README.md | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/s17_autonomous_agents/README.en.md b/s17_autonomous_agents/README.en.md index 2d676faf..d229194b 100644 --- a/s17_autonomous_agents/README.en.md +++ b/s17_autonomous_agents/README.en.md @@ -195,15 +195,15 @@ Two teammates claim and work in parallel. Lead only creates tasks and spawns tea | Component | Before (s16) | After (s17) | |-----------|-------------|-------------| | Task assignment | Lead manually assigns | Teammates auto-claim (can_start checks deps) | -| Teammate state | WORK or exit | WORK → IDLE (60s poll) → SHUTDOWN | +| Teammate state | WORK → IDLE (1s inbox poll) → WORK / SHUTDOWN | WORK → IDLE (5s inbox + task board poll, 60s timeout) → WORK / SHUTDOWN | | claim_task | No owner check | Rejects tasks that already have an owner | -| IDLE phase shutdown | Doesn't handle shutdown_request | Dispatches shutdown immediately and exits | -| Lead inbox | Prints only, not in context | consume_lead_inbox injects into history | -| New functions | — | idle_poll, scan_unclaimed_tasks, consume_lead_inbox | +| IDLE phase shutdown | Exits after receiving shutdown_request | Dispatches shutdown immediately and exits | +| Lead inbox | consume_lead_inbox routes protocol responses and injects into context | Reuses consume_lead_inbox mechanism | +| New functions | consume_lead_inbox already exists | idle_poll, scan_unclaimed_tasks (reuses consume_lead_inbox) | | Identity persistence | System prompt only | Auto re-inject after compression | -| Lead tools | 14 (s16) | 14 (unchanged) | +| Lead tools | 14 | 14 (unchanged) | | Teammate tools | 5 | 8 (+ list_tasks, claim_task, complete_task) | -| Teammate exit | Exit after task done | Exit only after 60s idle timeout | +| Teammate exit | WORK ends → enters IDLE, waits for shutdown_request (no timeout) | Exits after 60s idle timeout or receiving shutdown_request | --- diff --git a/s17_autonomous_agents/README.ja.md b/s17_autonomous_agents/README.ja.md index a88642d8..14177872 100644 --- a/s17_autonomous_agents/README.ja.md +++ b/s17_autonomous_agents/README.ja.md @@ -195,15 +195,15 @@ if len(messages) <= 3: | コンポーネント | 変更前 (s16) | 変更後 (s17) | |--------------|------------|------------| | タスク割り当て | Lead が手動 assign | チームメイトが自動認領(can_start で依存確認) | -| チームメイト状態 | WORK または終了 | WORK → IDLE(60s ポーリング) → SHUTDOWN | +| チームメイト状態 | WORK → IDLE(1s 間隔で inbox をポーリング)→ WORK / SHUTDOWN | WORK → IDLE(5s 間隔で inbox + タスクボードをポーリング、60s タイムアウト)→ WORK / SHUTDOWN | | claim_task | owner チェックなし | 既に owner があるタスクを拒否 | -| IDLE フェーズシャットダウン | shutdown_request を処理しない | 即座にシャットダウンをディスパッチして終了 | -| Lead inbox | 印刷のみ、コンテキストに入らない | consume_lead_inbox で history に注入 | -| 新規関数 | — | idle_poll, scan_unclaimed_tasks, consume_lead_inbox | +| IDLE フェーズシャットダウン | shutdown_request を受信後に終了 | 即座にシャットダウンをディスパッチして終了 | +| Lead inbox | consume_lead_inbox がプロトコル応答をルーティングしコンテキストに注入 | consume_lead_inbox 機構を踏襲 | +| 新規関数 | consume_lead_inbox は既存 | idle_poll, scan_unclaimed_tasks(consume_lead_inbox を踏襲) | | 身份保持 | system prompt のみ | 圧縮後に自動再注入 | -| Lead ツール | 14 (s16) | 14(変更なし) | +| Lead ツール | 14 | 14(変更なし) | | チームメイトツール | 5 | 8(+ list_tasks, claim_task, complete_task) | -| チームメイト終了条件 | タスク完了後即終了 | 60s アイドルタイムアウト後のみ終了 | +| チームメイト終了条件 | WORK 完了後 IDLE に入り、shutdown_request を待って終了(タイムアウトなし) | 60s アイドルタイムアウトまたは shutdown_request 受信で終了 | --- diff --git a/s17_autonomous_agents/README.md b/s17_autonomous_agents/README.md index b3192c2d..0370cdbc 100644 --- a/s17_autonomous_agents/README.md +++ b/s17_autonomous_agents/README.md @@ -195,15 +195,15 @@ if len(messages) <= 3: | 组件 | 之前 (s16) | 之后 (s17) | |------|-----------|-----------| | 任务分配 | Lead 手动 assign | 队友自动认领(can_start 检查依赖) | -| 队友状态 | WORK 或退出 | WORK → IDLE(轮询 60s) → SHUTDOWN | +| 队友状态 | WORK → IDLE(每 1s 轮询 inbox)→ WORK / SHUTDOWN | WORK → IDLE(每 5s 轮询 inbox + 任务板,60s 超时)→ WORK / SHUTDOWN | | claim_task | 无 owner 检查 | 拒绝已有 owner 的任务 | -| IDLE 阶段关机 | 不处理 shutdown_request | 直接 dispatch shutdown 并退出 | -| Lead inbox | 只打印,不进上下文 | consume_lead_inbox 统一注入 history | -| 新函数 | — | idle_poll, scan_unclaimed_tasks, consume_lead_inbox | +| IDLE 阶段关机 | 收到 shutdown_request 后退出 | 直接 dispatch shutdown 并退出 | +| Lead inbox | consume_lead_inbox 路由协议响应并注入上下文 | 沿用 consume_lead_inbox 机制 | +| 新函数 | 已有 consume_lead_inbox | idle_poll, scan_unclaimed_tasks(沿用 consume_lead_inbox) | | 身份保持 | 仅 system prompt | 压缩后自动重注入 | -| Lead 工具 | 14 (s16) | 14(不变) | +| Lead 工具 | 14 | 14(不变) | | 队友工具 | 5 | 8(+ list_tasks, claim_task, complete_task) | -| 队友退出条件 | 完成任务即退出 | 60s 无新任务才退出 | +| 队友退出条件 | WORK 完进入 IDLE,等待 shutdown_request 后退出(无超时) | 60s 无新任务或收到 shutdown_request 后退出 | --- From b6eb5c0f3ba771a117ab088addd78d72f9a2a25d Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Jul 2026 22:17:25 +0800 Subject: [PATCH 5/6] docs(s08): clarify reactive_compact aggressive wording refers to trigger, not compression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original phrasing "more aggressive than compact_history" was ambiguous: the code shows reactive_compact is actually more conservative in compression (keeps ~5 recent messages vs replacing everything with a summary). The real difference is the trigger mechanism — reactive_compact is an emergency response to a 413 error, not a proactive threshold-based trigger. Clarify in all three README languages that the aggressiveness is about the trigger, not the compression strategy. Closes #467 --- s08_context_compact/README.en.md | 2 +- s08_context_compact/README.ja.md | 2 +- s08_context_compact/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/s08_context_compact/README.en.md b/s08_context_compact/README.en.md index 15199c37..b35b7125 100644 --- a/s08_context_compact/README.en.md +++ b/s08_context_compact/README.en.md @@ -136,7 +136,7 @@ def compact_history(messages): Sometimes the API still returns `prompt_too_long` (413) — when context grows faster than compression triggers. -This triggers **reactive_compact**: more aggressive than compact_history, it retreats from the tail, but still avoids leaving an orphaned `tool_result`. +This triggers **reactive_compact**: more aggressive than compact_history in trigger (emergency response to a 413 error), but more conservative in what it removes, keeping ~5 recent messages and only summarizing earlier history. Still avoids an orphaned `tool_result`. ```python def reactive_compact(messages): diff --git a/s08_context_compact/README.ja.md b/s08_context_compact/README.ja.md index da174d0a..9d8ba810 100644 --- a/s08_context_compact/README.ja.md +++ b/s08_context_compact/README.ja.md @@ -136,7 +136,7 @@ def compact_history(messages): API がまだ `prompt_too_long`(413)を返すことがある。コンテキストの増加速度が圧縮のトリガー速度を上回る場合。 -この時 **reactive_compact** がトリガーされる:compact_history よりもさらに積極的だが、末尾を残す際も孤立した `tool_result` を残さないようにする。 +この時 **reactive_compact** がトリガーされる。トリガー方式は compact_history より積極的(413 エラー後の緊急対応)だが、圧縮方針はより温和で、末尾約 5 件のメッセージを保持し、早期の履歴だけを要約する。孤立した `tool_result` を残さないよう配慮する。 ```python def reactive_compact(messages): diff --git a/s08_context_compact/README.md b/s08_context_compact/README.md index ab5dae54..7f63b49f 100644 --- a/s08_context_compact/README.md +++ b/s08_context_compact/README.md @@ -136,7 +136,7 @@ def compact_history(messages): 有时候 API 还是返回 `prompt_too_long`(413),上下文增长速度快于压缩触发速度时。 -这时触发 **reactive_compact**:比 compact_history 更激进,从尾部回退,但仍要避免留下孤立 `tool_result`。 +这时触发 **reactive_compact**:触发方式比 compact_history 更激进(API 报错后的应急手段),但压缩策略更温和,保留最近约 5 条原始消息,只总结较早历史。同样避免留下孤立 `tool_result`。 ```python def reactive_compact(messages): From b4e7269d764218a9dbbe17f58559e5cbbadf2b13 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Jul 2026 22:27:58 +0800 Subject: [PATCH 6/6] fix(s10): assemble system prompt from dynamic context instead of static strings PROMPT_SECTIONS["memory"] was defined but never referenced, and update_context() collected enabled_tools and workspace that assemble_system_prompt() ignored, using hardcoded strings instead. Now assemble_system_prompt reads tools and workspace from the context dict, and PROMPT_SECTIONS only holds the identity line. All three README code snippets are synchronized. Closes #471 --- s10_system_prompt/README.en.md | 11 ++++++----- s10_system_prompt/README.ja.md | 11 ++++++----- s10_system_prompt/README.md | 11 ++++++----- s10_system_prompt/code.py | 13 +++++++------ 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/s10_system_prompt/README.en.md b/s10_system_prompt/README.en.md index fcaa99c9..bcf1811a 100644 --- a/s10_system_prompt/README.en.md +++ b/s10_system_prompt/README.en.md @@ -68,9 +68,6 @@ Split the monolithic string into a dictionary, each key is a topic: ```python PROMPT_SECTIONS = { "identity": "You are a coding agent. Act, don't explain.", - "tools": "Available tools: bash, read_file, write_file.", - "workspace": f"Working directory: {WORKDIR}", - "memory": "Relevant memories are injected below when available.", } ``` @@ -86,8 +83,12 @@ def assemble_system_prompt(context: dict) -> str: # Always loaded sections.append(PROMPT_SECTIONS["identity"]) - sections.append(PROMPT_SECTIONS["tools"]) - sections.append(PROMPT_SECTIONS["workspace"]) + + # Dynamic — tools and workspace from context + tools = ", ".join(context.get("enabled_tools", [])) + if tools: + sections.append(f"Available tools: {tools}.") + sections.append(f"Working directory: {context.get("workspace", WORKDIR)}") # On-demand — based on real state, not keywords memories = context.get("memories", "") diff --git a/s10_system_prompt/README.ja.md b/s10_system_prompt/README.ja.md index 50f8f95f..cfe8f95b 100644 --- a/s10_system_prompt/README.ja.md +++ b/s10_system_prompt/README.ja.md @@ -68,9 +68,6 @@ s10 は prompt アセンブリ機構に焦点を当てる。s08-s09 の能力を ```python PROMPT_SECTIONS = { "identity": "You are a coding agent. Act, don't explain.", - "tools": "Available tools: bash, read_file, write_file.", - "workspace": f"Working directory: {WORKDIR}", - "memory": "Relevant memories are injected below when available.", } ``` @@ -86,8 +83,12 @@ def assemble_system_prompt(context: dict) -> str: # 常にロード sections.append(PROMPT_SECTIONS["identity"]) - sections.append(PROMPT_SECTIONS["tools"]) - sections.append(PROMPT_SECTIONS["workspace"]) + + # context から動的に tools と workspace を取得 + tools = ", ".join(context.get("enabled_tools", [])) + if tools: + sections.append(f"Available tools: {tools}.") + sections.append(f"Working directory: {context.get("workspace", WORKDIR)}") # オンデマンド — 実際の状態に基づく、キーワードではない memories = context.get("memories", "") diff --git a/s10_system_prompt/README.md b/s10_system_prompt/README.md index 2ed0d6a5..0cd95930 100644 --- a/s10_system_prompt/README.md +++ b/s10_system_prompt/README.md @@ -68,9 +68,6 @@ s10 聚焦 prompt 组装机制。以 s08-s09 的能力为背景,但不重复 ```python PROMPT_SECTIONS = { "identity": "You are a coding agent. Act, don't explain.", - "tools": "Available tools: bash, read_file, write_file.", - "workspace": f"Working directory: {WORKDIR}", - "memory": "Relevant memories are injected below when available.", } ``` @@ -86,8 +83,12 @@ def assemble_system_prompt(context: dict) -> str: # 始终加载 sections.append(PROMPT_SECTIONS["identity"]) - sections.append(PROMPT_SECTIONS["tools"]) - sections.append(PROMPT_SECTIONS["workspace"]) + + # 从 context 动态获取 tools 和 workspace + tools = ", ".join(context.get("enabled_tools", [])) + if tools: + sections.append(f"Available tools: {tools}.") + sections.append(f"Working directory: {context.get("workspace", WORKDIR)}") # 按需加载 — 基于真实状态,不是关键词 memories = context.get("memories", "") diff --git a/s10_system_prompt/code.py b/s10_system_prompt/code.py index 723adc6d..be1d5f3d 100644 --- a/s10_system_prompt/code.py +++ b/s10_system_prompt/code.py @@ -41,9 +41,6 @@ MODEL = os.environ["MODEL_ID"] PROMPT_SECTIONS = { "identity": "You are a coding agent. Act, don't explain.", - "tools": "Available tools: bash, read_file, write_file.", - "workspace": f"Working directory: {WORKDIR}", - "memory": "Relevant memories are injected below when available.", } @@ -51,10 +48,14 @@ def assemble_system_prompt(context: dict) -> str: """Select and join prompt sections based on current context.""" sections = [] - # Always loaded — identity, tools, workspace + # Always loaded — identity sections.append(PROMPT_SECTIONS["identity"]) - sections.append(PROMPT_SECTIONS["tools"]) - sections.append(PROMPT_SECTIONS["workspace"]) + + # Dynamic — tools and workspace from context + tools = ", ".join(context.get("enabled_tools", [])) + if tools: + sections.append(f"Available tools: {tools}.") + sections.append(f"Working directory: {context.get("workspace", WORKDIR)}") # Conditional — memory loaded when MEMORY.md exists and has content memories = context.get("memories", "")