从 Hooks 到 TodoWrite 再到 Subagent:一个 Claude Code 风格 Harness 的构造过程

一、什么是 Harness?

在 Agent 系统里,LLM 本身只负责“思考”和“决定下一步”。但真正让它能稳定工作的,不只是模型,而是模型外面那层控制结构,也就是 harness

可以把 harness 理解成:

包住 Agent 的运行外壳,负责接收用户输入、调用模型、分发工具、拦截危险操作、记录状态、控制循环退出,以及在复杂任务中帮 Agent 维持方向。

这三个文件刚好按顺序展示了一个 harness 的逐步增强过程:

  1. s04:加入 Hooks,让循环可扩展;
  2. s05:加入 TodoWrite,让 Agent 先规划再执行;
  3. s06:加入 Subagent,让复杂任务拆出去,用干净上下文处理。

第一部分:s04 Hooks —— 不要把扩展逻辑写死在循环里

1. 问题:agent_loop 会越来越臃肿

在 s03 里,Agent 已经能调用工具,也能做权限检查。

但是问题来了:如果后面还想加这些功能:

  • 每次 bash 调用都写日志;
  • 写文件后自动 git add;
  • 工具输出太大时提醒;
  • 执行危险命令前拦截;
  • Agent 结束前做清理;

如果每加一个功能,就往 agent_loop 里面塞一行代码,那么核心循环会越来越乱。

原本循环应该只负责:

1
调用模型 → 解析 tool_use → 执行工具 → 回填结果 → 继续循环

但如果扩展逻辑都写进去,就会变成:

1
2
3
4
5
log_to_file(block)
check_permission(block)
notify_slack(block)
execute(block)
auto_git_add(block)

这就破坏了 harness 的核心原则:

agent_loop 应该稳定,扩展逻辑应该挂在循环外面。

所以 s04 引入了 Hooks。


2. 核心设计:HOOKS 注册表

s04 的核心代码是一个 Hook 注册表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HOOKS = {
"UserPromptSubmit": [],
"PreToolUse": [],
"PostToolUse": [],
"Stop": [],
}

def register_hook(event: str, callback):
HOOKS[event].append(callback)

def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None

这个结构很像 Spring 里的拦截器、过滤器,也像前端里的生命周期钩子。

它的意思是:

  • register_hook():注册一个扩展函数;
  • trigger_hooks():在某个时机触发对应函数;
  • 如果 hook 返回 None,说明继续执行;
  • 如果 hook 返回非 None,说明要中断或者改变流程。

3. 四个关键 Hook 时机

s04 只实现了四类事件,但已经覆盖了 Agent 一轮运行的关键节点。

1)UserPromptSubmit

触发位置:用户输入之后,进入 LLM 之前。

用途:

  • 打印当前工作目录;
  • 注入上下文;
  • 校验用户输入;
  • 修改用户 prompt。

示例:

1
2
3
4
5
def context_inject_hook(query: str) -> str | None:
print(f"[HOOK] UserPromptSubmit: working in {WORKDIR}")
return None

register_hook("UserPromptSubmit", context_inject_hook)

主流程中这样触发:

1
2
3
4
query = input("s04 >> ")
trigger_hooks("UserPromptSubmit", query)
history.append({"role": "user", "content": query})
agent_loop(history)

也就是说,用户输入不会直接进入 Agent,而是先经过 Hook 层。


2)PreToolUse

触发位置:工具执行前。

用途:

  • 权限检查;
  • 日志记录;
  • 拦截危险命令;
  • 判断是否允许写文件。

例如权限检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def permission_hook(block):
if block.name == "bash":
for pattern in DENY_LIST:
if pattern in block.input.get("command", ""):
return "Permission denied by deny list"

if block.name in ("write_file", "edit_file"):
path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
choice = input("Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"

return None

这里的重点是:

权限检查不再写死在 agent_loop 中,而是变成了一个 PreToolUse hook。

执行工具前,循环只需要这样写:

1
2
3
4
5
6
7
8
9
blocked = trigger_hooks("PreToolUse", block)

if blocked:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(blocked)
})
continue

如果 hook 返回了拦截信息,这次工具调用就不会执行,而是把“被拦截的原因”作为 tool_result 返回给模型。

这非常关键,因为模型可以看到失败原因,然后自己调整下一步。


3)PostToolUse

触发位置:工具执行后。

用途:

  • 记录工具结果;
  • 检查输出是否过大;
  • 自动执行副作用;
  • 做工具结果审计。

例如:

1
2
3
4
5
def large_output_hook(block, output):
if len(str(output)) > 100000:
print(f"[HOOK] Large output from {block.name}")

register_hook("PostToolUse", large_output_hook)

工具真正执行后:

1
2
3
4
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"

trigger_hooks("PostToolUse", block, output)

这说明 PostToolUse 不负责执行工具,它只是在工具执行完成后“顺手检查一下”。


4)Stop

触发位置:模型不再请求工具,即 Agent 准备退出时。

用途:

  • 打印总结;
  • 清理资源;
  • 决定是否强制继续;
  • 做最后检查。

示例:

1
2
3
4
5
6
7
8
9
10
11
def summary_hook(messages: list) -> str | None:
tool_count = sum(
1
for m in messages
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
if isinstance(b, dict) and b.get("type") == "tool_result"
)
print(f"[HOOK] Stop: session used {tool_count} tool calls")
return None

register_hook("Stop", summary_hook)

循环退出前:

1
2
3
4
5
6
7
8
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)

if force:
messages.append({"role": "user", "content": force})
continue

return

这里有一个小细节:如果 Stop hook 返回了内容,就会把它重新塞回 messages,让 Agent 继续跑。

这就给 harness 提供了“临门一脚”的控制能力。


4. s04 的核心价值

s04 的核心不是增加了某个具体功能,而是改变了架构方式。

以前是:

1
agent_loop 里面写权限、日志、提醒、清理

现在是:

1
2
agent_loop 只触发 hook
具体逻辑挂到 HOOKS 里

所以 s04 的本质是:

把 Agent 循环变成稳定内核,把扩展能力放到外部插件系统里。

这一步非常重要。因为后面的 TodoWrite 和 Subagent 都是在这个稳定循环上继续加能力。


第二部分:s05 TodoWrite —— 让 Agent 先列计划,再开始干活

1. 问题:Agent 容易在长任务中跑偏

有了 Hooks 后,Agent 可以安全地执行工具了。

但新的问题出现了:复杂任务中,Agent 很容易忘记原始目标。

比如用户说:

把所有 Python 文件改成 snake_case,运行测试,并修复失败。

Agent 可能一开始确实去改文件名,但跑测试发现失败后,它的注意力就被测试错误吸走了。修着修着,它可能忘了最开始还有“统一 snake_case”这个目标。

原因是:

  • 对话轮数越来越多;
  • 工具结果越来越多;
  • messages 里塞满了中间过程;
  • 原始目标在上下文中被稀释。

所以 s05 加了一个新的工具:todo_write


2. TodoWrite 的核心作用

todo_write 不执行真实任务。

它不会读文件,不会写文件,也不会运行命令。

它只做一件事:

让 Agent 把任务拆成清单,并维护每一步的状态。

这其实就是给 Agent 加了一个“任务白板”。


3. CURRENT_TODOS:在进程内保存任务列表

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CURRENT_TODOS: list[dict] = []

def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
CURRENT_TODOS = todos

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"

这个函数做了几件事:

  1. 接收模型传来的 todos;
  2. 把 todos 存到全局变量 CURRENT_TODOS
  3. 根据状态显示不同图标;
  4. 在终端打印当前任务列表;
  5. 返回更新结果给模型。

每个 todo 大概长这样:

1
2
3
4
{
"content": "Read project files",
"status": "pending"
}

状态只有三种:

1
2
3
pending
in_progress
completed

也就是:

  • pending:还没做;
  • in_progress:正在做;
  • completed:已完成。

4. 把 todo_write 加入工具系统

s05 没有改变工具分发机制。

它只是把 todo_write 加入 TOOLSTOOL_HANDLERS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
{"name": "glob", ...},
{
"name": "todo_write",
"description": "Create and manage a task list ...",
"input_schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {
"type": "string",
"enum": [
"pending",
"in_progress",
"completed"
]
},
},
},
},
},
},
},
]

TOOL_HANDLERS["todo_write"] = run_todo_write

这里有一个非常关键的设计点:

todo_write 也是普通工具,仍然通过 TOOL_HANDLERS[block.name] 分发。

也就是说,harness 的工具执行模型没有变,只是多挂了一个工具。


5. Reminder:防止 Agent 忘记更新 TODO

s05 还加了一个提醒机制。

如果 Agent 连续 3 轮没有调用 todo_write,harness 就自动给 messages 里追加一条提醒:

1
2
3
4
5
6
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0

这条 reminder 的作用不是给用户看,而是给模型看。

它相当于在提醒模型:

你该更新一下任务进度了。

于是典型流程变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户提出复杂任务

Agent 首先调用 todo_write,列出所有步骤

把第一个任务设为 in_progress

执行工具

完成后改成 completed

继续下一个 pending

如果太久没更新,harness 自动提醒

6. s05 的核心价值

s05 的重点不是让 Agent 多了一个执行能力,而是让它多了一个规划能力。

以前 Agent 是:

1
想到哪做到哪

现在变成:

1
先列计划,再按计划推进

所以 TodoWrite 的价值是:

它不增强 Agent 的手脚,而是增强 Agent 的方向感。

对于复杂任务来说,这很关键。


第三部分:s06 Subagent —— 大任务拆出去,用干净上下文处理

1. 问题:TODO 解决不了上下文污染

TodoWrite 可以让 Agent 不容易跑偏,但它仍然解决不了一个更大的问题:

复杂任务会污染主上下文。

比如 Agent 要修一个 bug,它可能需要:

  • 查 30 个文件;
  • 跟踪调用链;
  • 运行测试;
  • 分析日志;
  • 尝试多个修复方向。

这些中间过程都会进入主 Agent 的 messages[]

当 messages 越来越长,主 Agent 的注意力就会被大量细节占满。最后它可能连原本要修什么 bug 都忘了。

这时候,光有 TODO 不够。

所以 s06 加了 Subagent。


2. Subagent 的核心思想

Subagent 的思路很像我们自己写代码时的习惯:

遇到复杂问题,单独开一个终端去查。查完后,只把结论带回来,不把所有中间过程都复制回来。

在 Agent 中,就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
主 Agent 遇到复杂子任务

调用 task 工具

task 创建一个子 Agent

子 Agent 使用新的 messages[]

子 Agent 自己读文件、运行命令、分析问题

最后只返回总结

主 Agent 继续处理主任务

这样主 Agent 的上下文不会被污染。


3. spawn_subagent:创建一个独立 Agent

核心函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def spawn_subagent(description: str) -> str:
sub_tools = [...]
messages = [
{
"role": "user",
"content": description
}
]

for _ in range(30):
response = client.messages.create(
model=MODEL,
system=SUB_SYSTEM,
messages=messages,
tools=sub_tools,
max_tokens=8000,
)

messages.append({
"role": "assistant",
"content": response.content
})

if response.stop_reason != "tool_use":
break

results = []

for block in response.content:
if block.type == "tool_use":
blocked = trigger_hooks("PreToolUse", block)

if blocked:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(blocked)
})
continue

handler = SUB_HANDLERS.get(block.name)
output = handler(**block.input) if handler else "Unknown"

trigger_hooks("PostToolUse", block, output)

results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})

messages.append({
"role": "user",
"content": results
})

return extract_text(messages[-1]["content"])

这个函数里有几个关键点。


4. 关键点一:子 Agent 使用新的 messages[]

1
2
3
4
5
6
messages = [
{
"role": "user",
"content": description
}
]

这就是上下文隔离的核心。

子 Agent 不继承主 Agent 的完整聊天历史,只拿到一个任务描述。

这意味着:

  • 子 Agent 可以专心做一个子任务;
  • 它的中间工具调用不会塞进主 Agent 的 messages;
  • 它查文件、跑命令、分析失败的过程都会被丢弃;
  • 主 Agent 最后只拿到一个总结。

5. 关键点二:子 Agent 也有自己的循环

子 Agent 不是简单调用一次模型,而是自己跑一个小型 agent_loop。

它也会:

1
2
3
4
5
6
7
8
9
调用模型

判断是否 tool_use

执行工具

回填 tool_result

继续下一轮

所以 Subagent 本质上是:

在主 Agent 的工具调用里,又启动了一个小 Agent。

但是为了防止无限跑,代码加了安全上限:

1
for _ in range(30):

也就是最多跑 30 轮。


6. 关键点三:子 Agent 没有 task 工具

1
sub_tools = [...]

文件中说明,子 Agent 有基础工具:

  • bash;
  • read_file;
  • write_file;
  • edit_file;
  • glob。

但是没有 task

原因很简单:

防止子 Agent 再创建子 Agent,导致递归失控。

如果允许递归,可能出现:

1
2
3
4
5
主 Agent
└── 子 Agent
└── 子 Agent
└── 子 Agent
└── ...

所以教学版直接把 task 从子 Agent 工具列表里拿掉。


7. 关键点四:权限 Hook 仍然生效

子 Agent 虽然有独立上下文,但不代表它可以绕过安全机制。

它执行工具前仍然会走:

1
blocked = trigger_hooks("PreToolUse", block)

工具执行后仍然会走:

1
trigger_hooks("PostToolUse", block, output)

这说明:

上下文可以隔离,但权限不能隔离。

子 Agent 做危险操作时,依然会被主 harness 的 hook 拦截。


8. 把 task 加入主 Agent 工具系统

主 Agent 把 task 当作普通工具使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
{"name": "glob", ...},
{"name": "todo_write", ...},
{
"name": "task",
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
"input_schema": {
"type": "object",
"properties": {
"description": {
"type": "string"
}
},
"required": ["description"]
}
},
]

TOOL_HANDLERS["task"] = spawn_subagent

这点很优雅:

Subagent 没有破坏原来的工具分发系统,它只是新增了一个工具 handler。

主 Agent 看到复杂任务时,会调用:

1
task(description="Find what testing framework this project uses")

然后 harness 实际执行的是:

1
spawn_subagent(description)

最后返回一段总结文本给主 Agent。


第四部分:完整 Harness 构造过程

把 s04、s05、s06 串起来,一个完整 harness 的构造过程大概是这样的。


第一步:准备基础工具

最开始要有基础工具能力:

1
2
3
4
5
6
7
TOOLS = [
bash,
read_file,
write_file,
edit_file,
glob,
]

同时准备工具执行映射:

1
2
3
4
5
6
7
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": read_file,
"write_file": write_file,
"edit_file": edit_file,
"glob": glob_files,
}

这个映射表是工具分发的核心。

模型不会直接调用 Python 函数,它只会返回:

1
2
3
4
5
6
tool_use: {
name: "read_file",
input: {
path: "main.py"
}
}

harness 拿到 name 后,再从 TOOL_HANDLERS 里找到对应函数:

1
2
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)

这就是工具调用真正发生的位置。


第二步:构造 Agent 主循环

主循环负责:

  1. 调用 LLM;
  2. 保存 assistant 回复;
  3. 判断是否有工具调用;
  4. 执行工具;
  5. 把工具结果塞回 messages;
  6. 继续下一轮。

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def agent_loop(messages):
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)

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

handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)

results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})

messages.append({
"role": "user",
"content": results
})

这是 harness 的骨架。

后面所有能力都围绕它增强。


第三步:加入 Hooks,让循环可扩展

定义 Hook 注册表:

1
2
3
4
5
6
HOOKS = {
"UserPromptSubmit": [],
"PreToolUse": [],
"PostToolUse": [],
"Stop": [],
}

提供注册和触发函数:

1
2
3
4
5
6
7
8
9
def register_hook(event: str, callback):
HOOKS[event].append(callback)

def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None

然后在关键位置挂 Hook:

用户输入后:

1
trigger_hooks("UserPromptSubmit", query)

工具执行前:

1
blocked = trigger_hooks("PreToolUse", block)

工具执行后:

1
trigger_hooks("PostToolUse", block, output)

循环退出前:

1
force = trigger_hooks("Stop", messages)

这一步完成后,harness 就不再需要把权限、日志、清理逻辑写死在 loop 里了。


第四步:把权限检查改成 PreToolUse Hook

权限检查函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def permission_hook(block):
if block.name == "bash":
for pattern in DENY_LIST:
if pattern in block.input.get("command", ""):
return "Permission denied by deny list"

if block.name in ("write_file", "edit_file"):
path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
choice = input("Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"

return None

注册:

1
register_hook("PreToolUse", permission_hook)

工具执行处变成:

1
2
3
4
5
6
7
8
9
blocked = trigger_hooks("PreToolUse", block)

if blocked:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(blocked)
})
continue

这一步让安全逻辑和主循环解耦。


第五步:加入 TodoWrite 工具

定义状态:

1
CURRENT_TODOS: list[dict] = []

定义工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def run_todo_write(todos: list) -> str:
global CURRENT_TODOS
CURRENT_TODOS = todos

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"

加入工具列表:

1
2
3
4
5
TOOLS.append({
"name": "todo_write",
"description": "Create and manage a task list ...",
"input_schema": ...
})

加入 handler:

1
TOOL_HANDLERS["todo_write"] = run_todo_write

从这一步开始,Agent 就能主动维护任务清单了。


第六步:加入 Todo Reminder

定义计数器:

1
rounds_since_todo = 0

如果多轮没有调用 todo_write,自动提醒:

1
2
3
4
5
6
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0

每次执行工具时,如果发现工具名是 todo_write,就重置计数器。

这样 harness 不只是被动执行工具,还会主动提醒 Agent 保持计划更新。


第七步:加入 task 工具

定义主 Agent 的新工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "task",
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
"input_schema": {
"type": "object",
"properties": {
"description": {
"type": "string"
}
},
"required": ["description"]
}
}

注册 handler:

1
TOOL_HANDLERS["task"] = spawn_subagent

从主 Agent 角度看,task 只是一个普通工具。

但是从 harness 角度看,它会启动一个新的 Agent 循环。


第八步:实现 spawn_subagent

子 Agent 的核心构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def spawn_subagent(description: str) -> str:
sub_tools = [...]
messages = [
{
"role": "user",
"content": description
}
]

for _ in range(30):
response = client.messages.create(
model=MODEL,
system=SUB_SYSTEM,
messages=messages,
tools=sub_tools,
max_tokens=8000,
)

messages.append({
"role": "assistant",
"content": response.content
})

if response.stop_reason != "tool_use":
break

results = []

for block in response.content:
if block.type != "tool_use":
continue

blocked = trigger_hooks("PreToolUse", block)

if blocked:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(blocked)
})
continue

handler = SUB_HANDLERS.get(block.name)
output = handler(**block.input) if handler else "Unknown"

trigger_hooks("PostToolUse", block, output)

results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})

messages.append({
"role": "user",
"content": results
})

return extract_text(messages[-1]["content"])

它的完整流程是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
收到 description

创建新的 messages[]

用 SUB_SYSTEM 调用模型

子 Agent 自己执行工具

工具执行仍走 Hooks

最多跑 30 轮

提取最后结论

返回给主 Agent

这一步让 harness 具备了拆任务能力。


第五部分:三层 Harness 能力的关系

这三个文件不是孤立的,而是一层一层往上加能力。

s04:Hooks

解决的是:

怎么在不污染 agent_loop 的情况下扩展 Agent 行为?

核心能力:

1
2
3
4
输入前拦截
工具前拦截
工具后处理
退出前处理

它让 harness 从“硬编码循环”变成“可扩展循环”。


s05:TodoWrite

解决的是:

怎么让 Agent 在复杂任务中不忘记计划?

核心能力:

1
2
3
列任务
标记进度
自动提醒更新 todo

它让 harness 从“只会执行”变成“会督促 Agent 规划”。


s06:Subagent

解决的是:

怎么避免大任务污染主上下文?

核心能力:

1
2
3
4
5
6
创建子 Agent
使用独立 messages[]
只返回最终总结
不返回中间过程
子 Agent 不允许递归创建 task
权限 Hook 仍然生效

它让 harness 从“单一上下文执行”变成“可拆分、可隔离的多 Agent 执行”。


第六部分:最终形成的 Harness 运行流程

完整流程可以这样理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
用户输入

UserPromptSubmit Hook

写入主 messages

进入 agent_loop

调用 LLM

LLM 返回普通文本?
是 → 触发 Stop Hook → 退出或继续
否 → 返回 tool_use

遍历 tool_use

PreToolUse Hook
如果被拦截 → 返回拦截结果给模型
如果通过 → 执行工具

根据工具名分发:
bash/read/write/edit/glob → 执行基础工具
todo_write → 更新任务清单
task → 启动子 Agent

PostToolUse Hook

收集 tool_result

把 tool_result 作为 user message 放回 messages

继续下一轮

如果工具是 task,则进入子流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
task(description)

spawn_subagent(description)

创建新的 messages[]

子 Agent 调用 LLM

子 Agent 执行工具

子 Agent 工具调用仍经过 Hooks

子 Agent 完成

只提取最终文本总结

返回给主 Agent

总结

这三个章节其实在讲一个非常清晰的 Agent harness 演进路线。

最开始,Agent 只有一个简单循环:

1
模型 → 工具 → 结果 → 模型

s04 加入 Hooks,让循环可以被扩展,但不被污染。

s05 加入 TodoWrite,让 Agent 在复杂任务中先规划、再执行、持续更新状态。

s06 加入 Subagent,让大任务可以拆给独立上下文处理,主 Agent 只接收最终结论。

所以最终的 harness 不只是“调用大模型的代码”,而是一套控制系统:

  • Hooks 负责扩展;
  • TodoWrite 负责规划;
  • Subagent 负责隔离;
  • TOOL_HANDLERS 负责工具分发;
  • messages 负责上下文流转;
  • agent_loop 负责整体调度。

一句话概括:

一个可靠的 Agent,不是靠模型自己一直聪明,而是靠 harness 在模型外面帮它守住流程、计划和上下文边界。