从零构建 Agent 框架(三)让子 Agent 隔离上下文并完成委派

本文承接系列(一)系列(二)。上一篇收尾时我计划先接入 SQLite,但写到这里时发现,持久化之前还要先分清一件事:哪些历史应该留下,哪些只是一次查找过程。子 Agent 恰好把这个边界讲清楚。下一篇会先实现逐条追加的会话存储,再讨论何时需要 SQLite。

子 Agent 解决的不是“更聪明”

假设用户问:“这个项目使用什么测试框架?”主 Agent 为了回答,可能会读取依赖文件、测试目录和 CI 配置。答案也许只有一句“使用 pytest”,但读取过程会留下大段文件内容和工具输出。

这些过程在查完之后通常没有价值,却会一直占用主对话的上下文。子 Agent 的作用不是提高模型能力,而是把这类独立、输出很多的工作放到另一份历史里完成,再把必要结论交回主 Agent。

这篇实现的是一种最容易理解的模式:fresh-context 子 Agent。它不继承父 Agent 的完整对话历史,只接收委派任务;完成后,父 Agent 只保留摘要,不保存子 Agent 的中间过程。

Claude Code 的当前实现比这个更丰富。命名子 Agent 使用自己的上下文和工具配置,fork 则会继承父对话以复用缓存。本文不试图复刻全部机制,只先实现前一种模式。Claude Code 子 Agent 文档

什么时候值得委派

适合交给子 Agent 的任务通常有两个特征:它能独立完成,并且过程输出很多、最终结论相对短。例如:

  • 在代码库中查找某个调用链,只返回涉及的文件和结论。
  • 运行测试并汇总失败项。
  • 阅读多个配置文件,确认项目使用的框架或构建方式。

下面这些任务则先留在主 Agent 更合适:

  • 用户正在持续补充要求,需要频繁来回沟通。
  • 规划、实现和验证必须反复共享大量细节。
  • 只读一个文件或只改一行代码。
  • 用户希望立刻看到结果,启动一个新 Agent 的额外延迟反而不划算。

这也是 Claude Code 文档给出的取舍:子 Agent 适合隔离高输出的自包含任务,但不适合所有工作。官方使用建议

本篇的约束

为了和前两篇保持一致,子 Agent 不新增任意命令执行能力。更重要的是,本篇的默认子 Agent 是研究型、只读角色:它只能使用 list_filesread_file。主 Agent 若要写文件或运行测试,仍要自己承担这个副作用,等后续有审批与隔离后再定义专门的执行型子 Agent:

  • 读取路径仍限制在 workspace/ 内。
  • 子 Agent 看不到 write_filerun_tests,也不能执行任意命令。
  • 子 Agent 没有 task 工具,因此不能递归委派。
  • 父 Agent 与子 Agent 各自有回合预算。

这里故意不实现并行、后台运行、取消、工作树隔离和跨会话恢复。这些能力需要独立的运行时和状态设计,留到后续章节再做。尤其要注意:父 Agent 的 10 轮加上每个子 Agent 的 6 轮,并不是整个任务的总上限;一次父任务可以连续派出多个子任务。真正放开委派前,还要加任务级的总轮数/费用、最长墙钟时间、最大委派数和取消传播预算。

给父 Agent 增加 task 工具

在上一篇的 agent.py 中保留原来的 tool_schemas(),再加一个只给父 Agent 使用的 schema。委派时不要只传一句模糊的“去看看”,至少写清任务和期望回传格式。

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
TASK_SCHEMA = {
"type": "function",
"function": {
"name": "task",
"description": (
"委派一个独立、高输出的子任务。子 Agent 使用独立历史,"
"回传状态、结论、证据、限制和已使用轮数。"
),
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "需要完成的独立任务,包含必要的文件或范围信息",
},
"expected_output": {
"type": "string",
"description": "父 Agent 希望拿回的简短结果,例如结论、证据和测试状态",
},
},
"required": ["task", "expected_output"],
"additionalProperties": False,
},
},
}


def parent_tool_schemas():
return tool_schemas() + [TASK_SCHEMA]

task 的参数本身就是主 Agent 对子 Agent 的工作说明。因为子 Agent 没有父对话的上下文,委派时必须把范围说完整。这个限制看似麻烦,却能迫使主 Agent 把目标讲清楚。

复用上一章的循环

先说清这次重构改了什么,不然下面一大块代码容易让人望而生畏。上一篇的 run_agent() 里,有四样东西是写死的:系统提示词 SYSTEM_PROMPT、工具 schema、执行函数 execute_tool、回合上限 MAX_TURNS。这一篇把这四样全抽成参数,得到一个通用的 run_loop()

写死在上一篇 这一篇抽成参数 父 Agent 传入 子 Agent 传入
SYSTEM_PROMPT system_prompt PARENT_SYSTEM_PROMPT SUBAGENT_SYSTEM_PROMPT
tool_schemas() schemas taskparent_tool_schemas() list_files / read_filereadonly_tool_schemas()
execute_tool execute execute_parent_tool execute_readonly_tool
MAX_TURNS = 10 max_turns PARENT_MAX_TURNS = 10 SUBAGENT_MAX_TURNS = 6

这就是为什么父子 Agent 没有分裂成两套循环:它们跑的是同一个 run_loop(),只是喂进去的这四个参数不同。请特别留意 schemasexecute 这两列父子的差异,后面「为什么子 Agent 不能再委派」一节会看到它们正是关闭递归委派的两道锁。

assistant_message_for_history()build_api_messages() 和上一篇几乎相同,唯一变化是 build_api_messages() 现在也把系统提示词当参数收,以便父子用不同提示词。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# agent.py
import json
from dataclasses import dataclass
from typing import Awaitable, Callable, Literal

from llm import call_llm
from tools import (
execute_readonly_tool,
execute_tool,
readonly_tool_schemas,
tool_schemas,
)


PARENT_MAX_TURNS = 10
SUBAGENT_MAX_TURNS = 6

PARENT_SYSTEM_PROMPT = (
"你是编程助手。只能使用提供的工具处理 workspace 内的文件。"
"遇到高输出且独立的子任务时,可以使用 task 委派。"
"修改前先阅读相关文件;测试失败时如实说明结果。"
"子 Agent 不是 completed 时,必须把结果标为未完成线索,不能当作已验证事实。"
)
SUBAGENT_SYSTEM_PROMPT = (
"你是一个子 Agent。只完成当前委派的任务,不要尝试委派新的子任务。"
"只能使用提供的只读 workspace 工具。最后只返回 JSON 对象,包含 summary、"
"evidence(含 path 和 reason 的列表)与 warnings(限制或未验证项的列表)。"
)
API_FIELDS = ("role", "content", "tool_calls", "tool_call_id", "name")

# 与前面「给父 Agent 增加 task 工具」一节展示的是同一份 schema,
# 这里连同完整参数描述一并列出,方便对照整段文件。
TASK_SCHEMA = {
"type": "function",
"function": {
"name": "task",
"description": (
"委派一个独立、高输出的子任务。子 Agent 使用独立历史,"
"回传状态、结论、证据、限制和已使用轮数。"
),
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "需要完成的独立任务,包含必要的文件或范围信息",
},
"expected_output": {
"type": "string",
"description": "父 Agent 希望拿回的简短结果,例如结论、证据和测试状态",
},
},
"required": ["task", "expected_output"],
"additionalProperties": False,
},
},
}


@dataclass
class AgentResult:
status: Literal["completed", "budget_exhausted"]
reply: str
turns: int
messages: list[dict]


def assistant_message_for_history(message) -> dict:
item = {"role": "assistant", "content": message.content}
tool_calls = []

for tool_call in message.tool_calls or []:
tool_calls.append({
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
},
})

if tool_calls:
item["tool_calls"] = tool_calls
return item


def build_api_messages(messages: list[dict], system_prompt: str) -> list[dict]:
api_messages = [{"role": "system", "content": system_prompt}]
for message in messages:
api_messages.append({
key: message[key]
for key in API_FIELDS
if key in message and message[key] is not None
})
return api_messages


async def run_loop(
messages: list[dict],
*,
system_prompt: str,
schemas: list[dict],
execute: Callable[[str, str], Awaitable[str]],
max_turns: int,
log_prefix: str = "",
) -> AgentResult:
for turn in range(1, max_turns + 1):
response = await call_llm(
build_api_messages(messages, system_prompt),
tools=schemas,
)
messages.append(assistant_message_for_history(response))

tool_calls = response.tool_calls or []
if not tool_calls:
return AgentResult(
status="completed",
reply=response.content or "模型没有返回文本回复。",
turns=turn,
messages=messages,
)

for tool_call in tool_calls:
print(f"{log_prefix}调用工具: {tool_call.function.name}")
output = await execute(
tool_call.function.name,
tool_call.function.arguments,
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": output,
})

return AgentResult(
status="budget_exhausted",
reply=f"达到 {max_turns} 轮预算,已停止继续调用工具。",
turns=max_turns,
messages=messages,
)

两个可能卡住的语法先解释掉:

  • execute: Callable[[str, str], Awaitable[str]] 只是说明「execute 是一个函数参数」:它接收两个字符串(工具名、参数 JSON 串),返回一个 await 后得到字符串的协程。父 Agent 会传 execute_parent_tool,子 Agent 会传 execute_tool,稍后就能看到。
  • log_prefix 是这一篇新加的,用来在终端上区分「谁在调用工具」:父 Agent 用默认空前缀,打印成 调用工具: task;子 Agent 传一个缩进前缀,打印成 [子] 调用工具: read_file。有了它,子 Agent 在自己上下文里干的活才看得见,否则它会全程静默到只剩一句摘要。

循环没有因为加入子 Agent 而分叉成两套。不同点只在于父 Agent 的 schema 多了 task,以及它遇到 task 时需要启动一条新的循环。

启动子 Agent 并回传摘要

接着补上只读能力集和两个执行函数。先在 tools.py 明确收窄子 Agent 的能力;只过滤 schema 不够,执行层也必须拒绝不在白名单内的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# tools.py
READONLY_SUBAGENT_TOOLS = {"list_files", "read_file"}


def readonly_tool_schemas() -> list[dict]:
return [
schema
for schema, _ in TOOLS.values()
if schema["function"]["name"] in READONLY_SUBAGENT_TOOLS
]


async def execute_readonly_tool(name: str, raw_arguments: str) -> str:
if name not in READONLY_SUBAGENT_TOOLS:
return f"子 Agent 无权调用工具: {name}"
return await execute_tool(name, raw_arguments)

这里正是 fresh-context 的落点:注意 run_subagent() 里的 sub_messages 开局就只有一条 user 消息,装的是委派任务本身,没有一个字来自父 Agent 的历史。所谓「fresh」不是「没有消息」,而是「从这条委派任务从头开始,一张白纸」。父 Agent 聊了多久、读过什么,子 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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def parent_tool_schemas() -> list[dict]:
return tool_schemas() + [TASK_SCHEMA]


async def run_subagent(task: str, expected_output: str) -> AgentResult:
sub_messages = [{
"role": "user",
"content": (
f"任务:{task}\n\n"
f"期望输出:{expected_output}\n\n"
"不要展开无关过程。完成后直接给出可供父 Agent 使用的结论。"
),
}]
return await run_loop(
sub_messages,
system_prompt=SUBAGENT_SYSTEM_PROMPT,
schemas=readonly_tool_schemas(), # ← 不含 task、写入和测试工具
execute=execute_readonly_tool, # ← 执行层同样拒绝越权调用
max_turns=SUBAGENT_MAX_TURNS,
log_prefix=" [子] ",
)


async def execute_parent_tool(name: str, raw_arguments: str) -> str:
if name != "task":
return await execute_tool(name, raw_arguments)

try:
arguments = json.loads(raw_arguments or "{}")
task = arguments["task"].strip()
expected_output = arguments["expected_output"].strip()
except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as error:
return f"task 参数无效: {type(error).__name__}: {error}"

if not task or not expected_output:
return "task 和 expected_output 都不能为空"

result = await run_subagent(task, expected_output)
report = {
"summary": result.reply,
"evidence": [],
"warnings": [],
}
try:
candidate = json.loads(result.reply)
if isinstance(candidate, dict):
report["summary"] = str(candidate.get("summary", result.reply))
if isinstance(candidate.get("evidence"), list):
report["evidence"] = candidate["evidence"]
if isinstance(candidate.get("warnings"), list):
report["warnings"] = candidate["warnings"]
except json.JSONDecodeError:
report["warnings"].append("子 Agent 没有返回可解析的结构化报告")

if result.status != "completed":
report["warnings"].append("子 Agent 未确认完成;结论只能视为未完成线索")

return json.dumps(
{
"status": result.status,
**report,
"turns": result.turns,
},
ensure_ascii=False,
)


async def run_agent(messages: list[dict], user_input: str) -> AgentResult:
messages.append({"role": "user", "content": user_input})
return await run_loop(
messages,
system_prompt=PARENT_SYSTEM_PROMPT,
schemas=parent_tool_schemas(),
execute=execute_parent_tool,
max_turns=PARENT_MAX_TURNS,
)

父 Agent 的历史中只会新增一条 task 的工具结果,例如:

1
2
3
4
5
6
7
8
9
10
{
"status": "completed",
"summary": "项目使用 pytest。",
"evidence": [
{"path": "pyproject.toml", "reason": "含 pytest 配置"},
{"path": "tests/", "reason": "包含 pytest 测试文件"}
],
"warnings": [],
"turns": 3
}

status 必须由运行时生成,不能相信子 Agent 在文本里自称“完成”。父 Agent 收到非 completed 结果时,应如实报告未完成、缩小任务或请用户决定是否继续,而不能把它包装成已验证的事实。子 Agent 读过的文件、每次工具调用和它自己的交互历史,都保留在 result.messages 里,但没有被复制进父 Agent 的 messages。以后接入会话存储时,可以决定是否把这些调试记录另存为 trace,而不是混进主会话。

为并发预留一条顺序规则

本文的 run_loop() 故意按顺序执行工具,便于先看清父子边界;它还没有实现并发工具调用。以后若把互不冲突的只读调用改成并发,不能简单地“谁先完成就先 append 谁”。

模型一次回复中给出的 tool_calls 本身有顺序;工具 A、B 可以在执行层并发,B 也可能更早完成,但写回 transcript 时仍应按原始 call 顺序追加各自的结果。这样下一次模型请求、会话恢复和测试看到的是稳定的协议顺序,而不是受磁盘、网络或调度抖动影响的偶然顺序。

同样地,用户的 steering 输入不应夹在一组工具结果中间。更稳妥的边界是:先收齐这批调用及其结果,再把新输入作为下一段完整交互加入历史。课程在“有状态 Agent”一章将这一点写成可执行契约:工具可以反序完成,但 transcript 按 call 顺序落盘;运行中插入的指令只在完整工具批次或文本终态后生效。它是未来做异步桥接时必须补上的一致性规则,不是本篇已经实现的能力。

为什么子 Agent 不能再委派

这是本篇最值得停下来看的一处设计。子 Agent 不能递归地再派出孙 Agent,靠的不是某句 if 判断,而是两道彼此独立的锁,回头看 run_subagent() 里那两行带注释的参数:

  1. schema 层:子 Agent 根本看不到 task,也看不到写入和测试工具。父 Agent 用 parent_tool_schemas()tool_schemas() + TASK_SCHEMA),子 Agent 只用 readonly_tool_schemas()。模型只能调用它在 schema 里见过的工具,子 Agent 的工具清单里压根没有 task 这一项,自然无从委派。
  2. 执行层:即便硬调 task 也没人执行。父 Agent 的 executeexecute_parent_tool(认识 task、会启动子循环);子 Agent 的 executeexecute_readonly_tool。它只放行白名单中的两个读取工具;就算子 Agent 因为幻觉硬吐出 taskwrite_file,也只会收到越权错误,不会真的打开另一层委派或修改文件。

两道锁只要一道生效就够了,这里两道都上,这正是上一篇「模型能请求什么」和「程序允许什么」是两条线那个原则的延续。递归委派不是被「禁止」的,而是从能力上就不可能发生。想让子 Agent 能再委派,你得同时把这两样换成父 Agent 的版本;忘掉任何一个,它都动不了。

运行与观察

入口仍与上一篇相同:保留一个父 Agent 的 messages 列表,再调用 run_agent()

1
2
3
4
5
result = await run_agent(
messages,
"查清项目使用的测试框架。若需要读取多个文件,请委派子 Agent,最后用两句话说明依据。",
)
print(result.reply)

因为加上了 log_prefix,终端会把父级和子级的工具调用分层打印出来,一次典型运行大致长这样:

1
2
3
4
5
调用工具: task
[子] 调用工具: list_files
[子] 调用工具: read_file
[子] 调用工具: read_file
助手: 项目使用 pytest。依据是 pyproject.toml 里的 [tool.pytest.ini_options] 配置,以及 tests/ 目录下的测试文件。

对着这段回显看,三件事就都看得见了:

  1. 父 Agent 只调了一次 task,就把这件「要翻好几个文件」的活整体委派了出去,它自己没去逐个读文件。
  2. [子] 缩进的三次调用发生在子 Agent 的独立历史里,父 Agent 的 messages 完全没沾到这些文件内容。
  3. 子 Agent 干完后,父 Agent 只拿到一条 status/summary/turns 的摘要,据此给出最后那句总结。

如果模型没有委派,而是自己完成了任务,也不算错误。task 是模型可以选择的工具,不是每个请求都必须走的分支。为了便于测试,你可以在用户输入中明确要求“请使用子 Agent”。

到底省下了多少上下文

前面反复说子 Agent「省下主 Agent 的上下文」,这里把两种做法的父 Agent 历史摆在一起,差别就一目了然了。

先看不委派的情况。父 Agent 亲自读三个文件,每个文件的完整内容都会以 tool 消息落进它自己的历史:

1
2
3
4
5
6
7
8
9
10
11
12
# 父 Agent 的 messages(不委派)
[
{"role": "system", ...},
{"role": "user", "content": "查清项目使用的测试框架……"},
{"role": "assistant", "tool_calls": [list_files, read_file×3]},
{"role": "tool", "name": "list_files", "content": "pyproject.toml\ntests/..."},
{"role": "tool", "name": "read_file", "content": "<pyproject.toml 全文,几百行>"},
{"role": "tool", "name": "read_file", "content": "<tests/conftest.py 全文>"},
{"role": "tool", "name": "read_file", "content": "<.github/workflows/ci.yml 全文>"},
{"role": "assistant", "content": "项目使用 pytest……"},
]
# ↑ 几百上千行文件内容,永久留在主对话里,后面每一轮都要重发

再看委派的情况。同样的活交给子 Agent,父 Agent 历史里只多了一条摘要:

1
2
3
4
5
6
7
8
9
10
# 父 Agent 的 messages(委派给子 Agent)
[
{"role": "system", ...},
{"role": "user", "content": "查清项目使用的测试框架……"},
{"role": "assistant", "tool_calls": [task]},
{"role": "tool", "name": "task", "content":
'{"status": "completed", "summary": "项目使用 pytest。证据是……", "turns": 3}'},
{"role": "assistant", "content": "项目使用 pytest……"},
]
# ↑ 那几百行文件内容去哪了?留在子 Agent 的 result.messages 里,没进主对话

两份历史给出的最终答案一样,但前一种把几百行文件内容永久压在了主对话上,而这些内容查完之后基本再也用不到,却要在后续每一轮请求里重复发送、重复付费、并不断挤占上下文窗口。后一种则把这段「查找过程」隔离在了子 Agent 那次性的历史里。这就是子 Agent 省下的东西:不是让模型更聪明,而是让主对话只留结论、不留翻找的草稿。

这一步省下什么,又付出什么

子 Agent 减少的是主 Agent 的上下文负担,不是总成本。子 Agent 仍然会请求模型、读取文件、执行测试。它还会带来额外延迟,并且摘要会丢失一部分细节。

因此委派前可以问自己两个问题:这件事是否能独立完成?它的中间输出在主对话中是否大多用不上?两个答案都是“是”时,子 Agent 才更可能划算。

另外,父子 Agent 共享同一个 workspace/。上下文隔离不等于文件系统隔离。未来即使只让多个子 Agent 并行读取,也要保留它们原始 tool_call_id 的结果顺序;若放开写入,则必须串行执行或使用 worktree 等独立工作区,避免它们修改同一文件。

跑不通时,先查这几处

父 Agent 从不调用 task,总是自己动手。先确认 parent_tool_schemas() 真的把 TASK_SCHEMA 拼进去了、且 run_agent 传的是它而不是 tool_schemas()。schema 没进去,模型就看不到这个工具。其次是提示词:PARENT_SYSTEM_PROMPT 要明确「遇到高输出且独立的子任务时用 task 委派」。测试阶段可以在用户输入里直接写「请使用子 Agent」逼它走这条路。

子 Agent 反过来调用 task,或意外能写文件。说明你给 run_subagent 传错了 schemasexecute,把它们错传成了父 Agent 的版本。对照「为什么子 Agent 不能再委派」一节,子 Agent 必须用 readonly_tool_schemas() + execute_readonly_tool

task 参数无效task 和 expected_output 都不能为空。模型吐的 task 参数 JSON 不合法或缺字段。execute_parent_tool 已经把这些兜成一条文字结果回传给父 Agent,父 Agent 通常会自己重填重试;若反复失败,多半是 TASK_SCHEMA 的参数 description 写得太含糊,模型不知道该填什么。

终端看不到子 Agent 在做什么。检查 run_loop 里那行 print(f"{log_prefix}调用工具: ...") 是否还在,以及 run_subagent 是否传了 log_prefix=" [子] "。这行日志是你唯一能观察子 Agent 内部行为的窗口。

通用手段依旧是打印 AgentResult。子 Agent 返回的 result.messages 里有它读过的每一个文件、每一次工具调用,父对话看不到的中间过程全都在这里,排查子 Agent 为什么给错结论时就靠它。

动手改改看

  1. 做一个「执行型子 Agent」。只给它增加一个经过审批的 run_tests,并保持 write_file 不可用。对照研究型子 Agent,想一想为什么能力集应由任务角色决定,而不是图省事地共用父 Agent 的全部工具。
  2. run_loop 里的工具参数也打印出来(现在只打印了工具名)。观察父级 task 调用的参数,看看模型给子 Agent 写的任务描述够不够完整,这能直观感受 expected_output 的价值。
  3. 给父任务加 max_delegations 和总墙钟时间;子 Agent 返回 budget_exhausted 时,确保父 Agent 的最终答复包含 warnings,而不是把半截结论当成功。

安全边界没有消失

子 Agent 只是另一条循环,不是安全沙箱。本文仍依赖上一篇的工具限制:

  • workspace_path() 拒绝离开 workspace/ 的路径。
  • 子 Agent 的 schema 与执行器只放行 list_files / read_file
  • task 不会被加入子 Agent 的 schema,递归委派被关闭。

父 Agent 一旦执行测试,仍会运行工作目录中的测试代码。不要把不可信代码直接交给本地 Agent;需要处理时,应放到容器或其他隔离环境,并在增加删除、联网、部署等高影响工具前加入审批。

小结

  • 子 Agent 适合处理独立且高输出的任务,帮助主 Agent 保留用户目标和关键结论。
  • fresh-context 不等于没有消息,而是不继承父 Agent 的完整历史,只从委派任务开始。
  • 父 Agent 增加 task,研究型子 Agent 默认只使用两个读取工具,因此不能递归委派或写入文件。
  • 子 Agent 的结果以运行时生成的 statussummaryevidencewarningsturns 回传;父 Agent 不保存完整中间过程,也不能把未完成结果当作事实。
  • 这不是 Claude Code 的全部子 Agent 机制,也不是并发或文件隔离方案。它是后续实现会话存储、权限和多 Agent 协作前的一块基础。

下一篇会让主会话在重启后仍能恢复:每个模型回复和工具结果都作为事件逐条保存,子 Agent 的完整过程则作为可选 trace 单独关联。先用 JSONL 把数据流看清,再决定何时需要 SQLite 的查询与并发能力。


参考资料