Refine course progression and runtime safety

This commit is contained in:
Haoran
2026-08-11 15:13:13 +08:00
parent b36dbcd84f
commit ab35e59672
83 changed files with 5291 additions and 2267 deletions

View File

@@ -24,31 +24,43 @@ Agent は作業を開始する。3 つのファイルをリネーム、テスト
![Todo Overview](images/todo-overview.ja.svg)
前章の最小フック構造を保持し、本章では新規の `todo_write` ツールとリマインダー機構に注目する。`todo_write`実際の作業を何もしない。ファイルを読めない、コマンドを実行できない。Agent が手を動かす前に思考を整理できるようにするだけ
S05 は S04 のツールディスパッチ、権限チェック、Hooks を保持し、`todo_write` とリマインダーカウンターを追加する。`todo_write`計画状態だけを更新し、実際の作業は既存のツールが行う
ディスパッチ機構は変わらず、新ツールも `TOOL_HANDLERS[block.name]` を経由する。ただし、todo リマインダーのデモのため、ループにカウンターを追加した:連続 3 ラウンド `todo_write` を呼び出さないとリマインダーが注入される。
しいツールも `TOOL_HANDLERS[block.name]` を経由する。3 回連続のツール使用ラウンド `todo_write` が呼ばれなければ、Harness は 3 回目のツール結果にリマインダーを追加する。
---
## 仕組み
**todo_write ツール**は、ステータス付きのリストを受け取り、現在のプロセスメモリに保持し、端末に進捗を表示する:
**TodoManager** はメモリ上のタスクリストを保持し、更新を検証して、描画結果をモデルへ返す。`run_todo_write` は同じ状態を端末にも表示する:
```python
CURRENT_TODOS: list[dict] = []
class TodoManager:
def __init__(self):
self.items = []
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
CURRENT_TODOS = todos
def update(self, todos: list | str) -> str:
# Parse and validate before replacing the current list.
validated = []
...
self.items = validated
return self.render()
lines = ["\n## Current Tasks"]
for t in CURRENT_TODOS:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(CURRENT_TODOS)} tasks"
def render(self) -> str:
# [ ] pending, [>] in progress, [x] completed
...
TODO = TodoManager()
def run_todo_write(todos: list | str) -> str:
output = TODO.update(todos)
print(output)
return output
```
1 回の更新は最大 20 項目で、各項目には空でない `content` が必要となり、`in_progress` にできる項目は同時に 1 つだけ。文字列入力は JSON または Python のリスト表現として、`eval` を使わずに解析する。
ツール定義は他の 5 つと一緒にディスパッチマップに追加される:
```python
@@ -81,18 +93,19 @@ TOOLS = [
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag リマインダー**モデルが 3 ラウンド連続`todo_write` を呼び出さなかった場合、リマインダーが自動的に注入される:
**リマインダー**3 回連続のツール使用ラウンドで `todo_write` が呼ばれなければ、リマインダーを 3 回目の結果に追加し、カウンターをリセットする:
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
results.append({
"type": "text",
"text": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Agent がタスクを受け取った後の典型的な流れ:まず `todo_write` を呼び出して全手順を列挙(全て `pending`)→ 一つの手順に取り掛かり、`in_progress` に変更 → 完了したら `completed` に変更 → 次の `pending` を見る → 続行。3 ラウンド `todo_write` がない場合、次の LLM 呼び出し前にリマインダーが追加される。
Agent がタスクを受け取った後の典型的な流れ:まず `todo_write` を呼び出して全手順を列挙(全て `pending`)→ 一つの手順に取り掛かり、`in_progress` に変更 → 完了したら `completed` に変更 → 次の `pending` を見る → 続行。
**重要な洞察**todo_write は Agent に**実行能力**を何も追加しない。追加するのは**計画能力**だ。
@@ -103,9 +116,9 @@ Agent がタスクを受け取った後の典型的な流れ:まず `todo_writ
| コンポーネント | 変更前 (s04) | 変更後 (s05) |
|--------------|-------------|-------------|
| ツール数 | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| 計画能力 | なし | ステータス付き TODO リスト + Nag リマインダー |
| 計画能力 | なし | ステータス付き TODO リスト + リマインダー |
| SYSTEM プロンプト | 汎用プロンプト | 「先に計画してから実行」のガイダンスを追加 |
| ループ | 不変 | ディスパッチは不変、rounds_since_todo カウンターとリマインダー注入を追加 |
| ループ | ツールディスパッチと Hooks | 同じ分配経路に rounds_since_todo とリマインダー注入を追加 |
---

View File

@@ -24,31 +24,43 @@ The longer the conversation, the worse it gets: tool results keep filling the co
![Todo Overview](images/todo-overview.en.svg)
The minimal hook structure from the previous chapter is preserved, focusing on the new `todo_write` tool and reminder mechanism. `todo_write` does no actual work, can't read files or run commands, it simply lets the Agent organize its thoughts before diving in.
S05 keeps the tool dispatch, permissions, and hooks from S04, then adds `todo_write` and a reminder counter. `todo_write` only updates planning state; the existing tools still perform the work.
The dispatch mechanism is unchanged; the new tool is still routed through `TOOL_HANDLERS[block.name]`. However, to demonstrate the todo reminder, a counter was added to the loop: after 3 consecutive rounds without calling `todo_write`, a reminder is injected.
The new tool uses the same `TOOL_HANDLERS[block.name]` dispatch path. After three consecutive tool-use rounds without `todo_write`, the harness adds a reminder to that round's tool results.
---
## How It Works
**The todo_write tool** accepts a list with statuses, keeps it in the current process memory, and displays progress in the terminal:
**TodoManager** owns the in-memory list, validates updates, and renders the state returned to the model. `run_todo_write` also prints that state in the terminal:
```python
CURRENT_TODOS: list[dict] = []
class TodoManager:
def __init__(self):
self.items = []
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
CURRENT_TODOS = todos
def update(self, todos: list | str) -> str:
# Parse and validate before replacing the current list.
validated = []
...
self.items = validated
return self.render()
lines = ["\n## Current Tasks"]
for t in CURRENT_TODOS:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(CURRENT_TODOS)} tasks"
def render(self) -> str:
# [ ] pending, [>] in progress, [x] completed
...
TODO = TodoManager()
def run_todo_write(todos: list | str) -> str:
output = TODO.update(todos)
print(output)
return output
```
An update may contain at most 20 items, each item needs non-empty `content`, and only one item may be `in_progress`. The string input path accepts JSON or a Python list representation without using `eval`.
The tool definition joins the other 5 in the dispatch map:
```python
@@ -81,18 +93,19 @@ TOOLS = [
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag reminder**: when the model has not called `todo_write` for 3 consecutive rounds, a reminder is automatically injected:
**Reminder**: after three tool-use rounds without `todo_write`, the reminder is appended to the third round's results and the counter resets:
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
results.append({
"type": "text",
"text": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Typical flow when the Agent receives a task: first call `todo_write` to list all steps (all `pending`) → pick one step, set it to `in_progress` → complete it, set to `completed` → look at the next `pending` → continue. After 3 rounds without `todo_write`, the loop appends a reminder before the next LLM call.
Typical flow when the Agent receives a task: first call `todo_write` to list all steps (all `pending`) → pick one step, set it to `in_progress` → complete it, set to `completed` → look at the next `pending` → continue.
**Key insight**: todo_write doesn't give the Agent any additional **execution capability**. What it adds is **planning capability**.
@@ -103,9 +116,9 @@ Typical flow when the Agent receives a task: first call `todo_write` to list all
| Component | Before (s04) | After (s05) |
|-----------|-------------|-------------|
| Tool count | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| Planning | None | Stateful TODO list + nag reminder |
| Planning | None | Stateful TODO list + reminder |
| SYSTEM prompt | Generic prompt | Added "plan before executing" guidance |
| Loop | Unchanged | Dispatch unchanged, added rounds_since_todo counter and reminder injection |
| Loop | Tool dispatch and hooks | Same dispatch path, plus rounds_since_todo and reminder injection |
---

View File

@@ -24,31 +24,43 @@ Agent 开始干活,改了 3 个文件,跑了个测试,发现 2 个失败
![Todo Overview](images/todo-overview.svg)
保留上一章的最小 hook 结构,重点看新增的 `todo_write` 工具和 reminder 机制`todo_write` 本身不做任何实际工作,不能读文件、不能跑命令,只是让 Agent 在动手之前先理清思路
S05 保留 S04 的工具分发、权限检查和 Hooks再加入 `todo_write` reminder 计数器`todo_write` 只更新计划状态,实际工作仍由原有工具完成
dispatch 机制不变,新工具仍然走 `TOOL_HANDLERS[block.name]` 分发。但为了演示 todo reminder循环里加了一个计数器连续 3 轮没调 `todo_write` 就注入一条提醒
新工具仍通过 `TOOL_HANDLERS[block.name]` 分发。连续三个工具调用轮次没有使用 `todo_write`Harness 会把 reminder 追加到第三轮的工具结果中
---
## 工作原理
**todo_write 工具**,接收一个带状态的列表,保存在当前进程内存中,同时在终端显示进度
**TodoManager** 持有内存中的任务列表,负责校验更新,并把渲染结果返回给模型。`run_todo_write` 同时把这份状态打印到终端
```python
CURRENT_TODOS: list[dict] = []
class TodoManager:
def __init__(self):
self.items = []
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
CURRENT_TODOS = todos
def update(self, todos: list | str) -> str:
# Parse and validate before replacing the current list.
validated = []
...
self.items = validated
return self.render()
lines = ["\n## Current Tasks"]
for t in CURRENT_TODOS:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(CURRENT_TODOS)} tasks"
def render(self) -> str:
# [ ] pending, [>] in progress, [x] completed
...
TODO = TodoManager()
def run_todo_write(todos: list | str) -> str:
output = TODO.update(todos)
print(output)
return output
```
一次更新最多包含 20 项;每项都必须有非空的 `content`;同一时间只能有一个 `in_progress`。字符串输入可以是 JSON也可以是 Python 列表表示,解析过程不使用 `eval`
工具定义和其他 5 个工具一起加入 dispatch map
```python
@@ -81,18 +93,19 @@ TOOLS = [
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag reminder**模型连续 3 轮未调`todo_write` 时,自动注入提醒
**Reminder**连续三个工具调用轮次没有使`todo_write` 时,reminder 会追加到第三轮的结果中,随后计数器清零
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
results.append({
"type": "text",
"text": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Agent 收到任务后的典型流程:先调 `todo_write` 列出所有步骤(全 `pending`)→ 做一个步骤,改成 `in_progress` → 做完改成 `completed` → 看下一个 `pending` → 继续。连续 3 轮没有调用 `todo_write` 时,循环会在下一次 LLM 调用前追加一条 reminder。
Agent 收到任务后的典型流程:先调 `todo_write` 列出所有步骤(全 `pending`)→ 做一个步骤,改成 `in_progress` → 做完改成 `completed` → 看下一个 `pending` → 继续。
**关键洞察**todo_write 不给 Agent 增加任何**执行能力**。它增加的是**规划能力**。
@@ -103,9 +116,9 @@ Agent 收到任务后的典型流程:先调 `todo_write` 列出所有步骤(
| 组件 | 之前 (s04) | 之后 (s05) |
|------|-----------|-----------|
| 工具数量 | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| 规划能力 | 无 | 带状态的 TODO 列表 + nag reminder |
| 规划能力 | 无 | 带状态的 TODO 列表 + reminder |
| SYSTEM 提示 | 通用提示 | 加入 "先计划再执行" 引导 |
| 循环 | 不变 | dispatch 不变,新增 rounds_since_todo 计数器和 reminder 注入 |
| 循环 | 工具分发与 Hooks | 保留分发路径,加入 rounds_since_todo 和 reminder 注入 |
---

View File

@@ -1,34 +1,31 @@
#!/usr/bin/env python3
"""
s05: TodoWrite — add a planning tool on top of s04 hooks.
s05_todo_write.py - TodoWrite
+---------+ +-------+ +------------------+
| User | ---> | LLM | ---> | TOOL_HANDLERS |
| prompt | | | | bash |
+---------+ +---+---+ | read_file |
^ | write_file |
| result | edit_file |
+---------+ glob |
todo_write ← NEW
+------------------+
|
in-memory current_todos
|
if rounds_since_todo >= 3:
inject <reminder>
The model tracks its progress through a TodoManager. After three rounds
without an update, the harness adds a reminder alongside the tool results.
Changes from s04:
+ todo_write tool + run_todo_write() implementation
+ Nag reminder (inject reminder after 3 rounds without todo update)
+ SYSTEM prompt includes "plan before execute" guidance
+ rounds_since_todo counter in agent_loop
Loop unchanged: new tool auto-dispatches via TOOL_HANDLERS.
+----------+ +-------+ +--------------+
| User | ---> | LLM | ---> | Tools |
| prompt | | | | + todo_write |
+----------+ +---^---+ +------+-------+
| | update
| +------v----------+
| | TodoManager |
| | [ ] pending |
| | [>] in progress |
| | [x] completed |
| +------+----------+
| tool_result |
+-----------------+
Run: python s05_todo_write/code.py
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
rounds_since_todo >= 3 -> add <reminder>
"""
import ast, json, os, subprocess
import ast
import json
import os
import subprocess
from pathlib import Path
try:
@@ -47,7 +44,6 @@ if os.getenv("ANTHROPIC_BASE_URL"):
WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
CURRENT_TODOS: list[dict] = []
# s05 change: SYSTEM prompt adds planning guidance
SYSTEM = (
@@ -57,15 +53,7 @@ SYSTEM = (
)
# ═══════════════════════════════════════════════════════════
# FROM s02-s04 (unchanged): Tool Implementations
# ═══════════════════════════════════════════════════════════
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
# -- Tool implementations from s02-s04 --
def run_bash(command: str) -> str:
try:
@@ -78,7 +66,7 @@ def run_bash(command: str) -> str:
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
lines = (WORKDIR / path).resolve().read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
@@ -87,7 +75,7 @@ def run_read(path: str, limit: int | None = None) -> str:
def run_write(path: str, content: str) -> str:
try:
file_path = safe_path(path)
file_path = (WORKDIR / path).resolve()
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
@@ -96,7 +84,7 @@ def run_write(path: str, content: str) -> str:
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = safe_path(path)
file_path = (WORKDIR / path).resolve()
text = file_path.read_text()
if old_text not in text:
return f"Error: text not found in {path}"
@@ -117,42 +105,77 @@ def run_glob(pattern: str) -> str:
return f"Error: {e}"
# ═══════════════════════════════════════════════════════════
# NEW in s05: todo_write tool — plan only, no execution
# ═══════════════════════════════════════════════════════════
# -- New in s05: structured state the model updates --
def _normalize_todos(todos):
if isinstance(todos, str):
try:
todos = json.loads(todos)
except json.JSONDecodeError:
class TodoManager:
def __init__(self):
self.items: list[dict] = []
def update(self, todos: list | str) -> str:
if isinstance(todos, str):
try:
todos = ast.literal_eval(todos)
except (SyntaxError, ValueError):
return None, "Error: todos must be a list or JSON array string"
if not isinstance(todos, list):
return None, "Error: todos must be a list"
for i, t in enumerate(todos):
if not isinstance(t, dict):
return None, f"Error: todos[{i}] must be an object"
if "content" not in t or "status" not in t:
return None, f"Error: todos[{i}] missing 'content' or 'status'"
if t["status"] not in ("pending", "in_progress", "completed"):
return None, f"Error: todos[{i}] has invalid status '{t['status']}'"
return todos, None
todos = json.loads(todos)
except json.JSONDecodeError:
try:
todos = ast.literal_eval(todos)
except (SyntaxError, ValueError) as e:
raise ValueError("todos must be a list or JSON array string") from e
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
todos, error = _normalize_todos(todos)
if error:
return error
CURRENT_TODOS = todos
lines = ["\n\033[33m## Current Tasks\033[0m"]
for t in CURRENT_TODOS:
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(CURRENT_TODOS)} tasks"
if not isinstance(todos, list):
raise ValueError("todos must be a list")
if len(todos) > 20:
raise ValueError("Max 20 todos allowed")
validated = []
in_progress_count = 0
for index, todo in enumerate(todos):
if not isinstance(todo, dict):
raise ValueError(f"todos[{index}] must be an object")
content = str(todo.get("content", "")).strip()
status = str(todo.get("status", "pending")).lower()
if not content:
raise ValueError(f"todos[{index}] requires content")
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"todos[{index}] has invalid status '{status}'")
if status == "in_progress":
in_progress_count += 1
validated.append({"content": content, "status": status})
if in_progress_count > 1:
raise ValueError("Only one todo can be in_progress at a time")
self.items = validated
return self.render()
def render(self) -> str:
if not self.items:
return "No todos."
lines = []
for todo in self.items:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]",
}[todo["status"]]
lines.append(f"{marker} {todo['content']}")
done = sum(todo["status"] == "completed" for todo in self.items)
lines.append(f"\n({done}/{len(self.items)} completed)")
return "\n".join(lines)
TODO = TodoManager()
def run_todo_write(todos: list | str) -> str:
try:
output = TODO.update(todos)
except ValueError as e:
return f"Error: {e}"
print(f"\n\033[33m## Current Tasks\033[0m\n{output}")
return output
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
@@ -167,7 +190,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
# s05: new tool
{"name": "todo_write", "description": "Create and manage a task list for your current coding session.",
"input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
"input_schema": {"type": "object", "properties": {"todos": {"type": "array", "maxItems": 20, "items": {"type": "object", "properties": {"content": {"type": "string", "minLength": 1}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
]
TOOL_HANDLERS = {
@@ -176,9 +199,7 @@ TOOL_HANDLERS = {
}
# ═══════════════════════════════════════════════════════════
# FROM s04 (unchanged): Hook System
# ═══════════════════════════════════════════════════════════
# -- Hook system from s04 --
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
@@ -192,21 +213,44 @@ def trigger_hooks(event: str, *args):
return result
return None
# s04 hooks preserved
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def permission_hook(block):
"""PreToolUse: deny list check."""
"""PreToolUse: s03 permission logic, registered as an s04 hook."""
if block.name == "bash":
for p in DENY_LIST:
if p in block.input.get("command", ""):
print(f"\n\033[31m⛔ Blocked: '{p}'\033[0m")
return "Permission denied"
command = block.input.get("command", "")
for pattern in DENY_LIST:
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list"
for keyword in DESTRUCTIVE:
if keyword in command:
print(f"\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
print(f"\n\033[33m[permission] Access outside workspace\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
return None
def log_hook(block):
"""PreToolUse: log tool calls."""
print(f"\033[90m[HOOK] {block.name}\033[0m")
"""PreToolUse: log every tool call."""
args_preview = str(list(block.input.values())[:2])[:60]
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
return None
def large_output_hook(block, output):
"""PostToolUse: warn on large output."""
if len(str(output)) > 100000:
print(f"\033[33m[HOOK] Large output from {block.name}: {len(str(output))} chars\033[0m")
return None
def context_inject_hook(query: str):
@@ -225,22 +269,15 @@ def summary_hook(messages: list):
register_hook("UserPromptSubmit", context_inject_hook)
register_hook("PreToolUse", permission_hook)
register_hook("PreToolUse", log_hook)
register_hook("PostToolUse", large_output_hook)
register_hook("Stop", summary_hook)
# ═══════════════════════════════════════════════════════════
# agent_loop — same as s04 + nag reminder counter
# ═══════════════════════════════════════════════════════════
# -- Agent loop with the reminder counter --
def agent_loop(messages: list):
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:
messages.append({"role": "user",
"content": "<reminder>Update your todos.</reminder>"})
rounds_since_todo = 0
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
@@ -254,8 +291,8 @@ def agent_loop(messages: list):
continue
return
rounds_since_todo += 1
results = []
used_todo = False
for block in response.content:
if block.type != "tool_use":
continue
@@ -267,23 +304,31 @@ def agent_loop(messages: list):
continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
try:
output = handler(**block.input) if handler else f"Unknown: {block.name}"
except Exception as e:
output = f"Error: {e}"
trigger_hooks("PostToolUse", block, output)
# s05: reset nag counter when todo_write is called
if block.name == "todo_write":
rounds_since_todo = 0
used_todo = True
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": output})
"content": str(output)})
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
results.append({"type": "text",
"text": "<reminder>Update your todos.</reminder>"})
rounds_since_todo = 0
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("s05: TodoWrite plan before execute, nag if you forget")
print("Type a question, press Enter. Type q to quit.\n")
print("s05: TodoWrite - plan before execution")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
while True: