跳到主要内容

Agent 主机层:拼提示 · 管上下文 · 控权限 · 派子agent

30 秒导读: 上一章 讲的 loop 是一台无状态的"发一次请求、跑一批工具、判断要不要再来一轮"的机器。它自己不记历史、不知道系统提示、不管权限。本章讲的 Agent 类,就是把这台机器装配成一个能真正用起来的编码 agent 的主机:它准备好 loop 每一步要的原料(系统提示、消息历史、工具表),在 loop 的每个钩子上插入自己的逻辑(压缩、权限、去重、记账),并把结果记进可回放的日志。

本章范围锁定 packages/agent-core/src/agent/。不深入具体工具怎么实现(留给 03-tools),不讲 provider 协议细节(留给 04-providers)。


1. 这是什么(零基础也能懂)

一句话定义: Agent 是一个长期存活的对象,它握着一次会话的全部可变状态(消息历史、配置、权限模式、计划/目标状态),并在每次该请求模型时,把这些状态渲染成无状态 loop 需要的输入。

loop 和 Agent 的分工,用一个类比:

  • loop = 一台榨汁机。 你塞进去水果(消息 + 工具表),它吐出果汁(模型回复 + 工具结果),它不关心水果哪来的、果汁往哪去。
  • Agent = 厨房。 它负责买菜、洗菜、切好(拼系统提示、投影历史)、决定这次榨什么、榨完把渣清理掉(压缩)、把成品装盘记账(records)。榨汁机可以换,厨房的流程不变。

为什么需要这一层? 因为一个无状态循环缺了三样东西才能变成 agent:

缺的东西Agent 怎么补在哪
它是谁、能干什么系统提示 + 画像(profile)装配profile/services/prompt/
它记得什么上下文记忆 + 投影 + 压缩agent/context/agent/compaction/
它被允许做什么工具执行前的权限门控agent/permission/

一个关键设计约束(全书都要记住): Agent 必须能独立使用——它的构造函数不强迫调用方先造一个 Session,不要求 agentIdsession。它可以接一个可选的 sessionId 作为请求配置的提示(比如映射到 provider 的 prompt_cache_key),但实例本身不持有 sessionId,也不依赖 Session 的生命周期、元数据或父子关系(依据:仓库根 CLAUDE.md "General Coding Rules";以 agent/index.ts 构造函数为准 packages/agent-core/src/agent/index.ts:176-234)。

这条约束贯穿本章:凡是需要"多个 agent 协作/父子关系"的东西(比如子 agent 编排),都不在 agent/ 里,而在 session/ 里。agent/ 只知道"我是一个 agent",不知道"我是谁的孩子"。


2. 顶层全景(它大概怎么转)

2.1 Agent 类是一堆子系统的装配台

Agent 的构造函数本身几乎不含逻辑——它就是一条装配线:接住选项,然后把十几个子系统 new 出来,每个都把 this(Agent 自己)传进去,于是子系统之间能通过 this.agent.xxx 互相拿到对方(packages/agent-core/src/agent/index.ts:203-233)。

┌─────────────────────────────┐
AgentOptions ───────▶ │ Agent (主机) │
(kaos, config, │ 一次会话的全部可变状态 │
modelProvider, └───────────────┬─────────────┘
subagentHost, ...) │ 构造时装配(每个子系统持有 this)

┌──────────┬───────────┬───────────┬───────────┬───────────┬──────────┐
│ config │ context │ compaction│ permission│ turn │ tools │
│ 模型/画像 │ 记忆+投影 │ 压缩 │ 权限门控 │ 回合驱动 │ 工具表 │
├──────────┼───────────┼───────────┼───────────┼───────────┼──────────┤
│ planMode │ goal │ swarmMode │ injection │ background│ cron │
│ 计划模式 │ 目标模式 │ 群体模式 │ 动态注入 │ 背景任务 │ 定时 │
├──────────┴───────────┴───────────┴───────────┴───────────┴──────────┤
│ records(写 wire 日志) replayBuilder(攒回放) usage(记账) │
└─────────────────────────────────────────────────────────────────────┘

2.2 各部件一句话职责

部件(字段)干什么起点文件
config存当前模型别名、画像名、思考档位、系统提示;算出 provider 与能力agent/config/index.ts ConfigState
context存消息历史;把历史投影成 provider 能收的合法请求agent/context/index.ts ContextMemory
fullCompaction / microCompaction上下文快满时压缩历史agent/compaction/full.tsmicro.ts
permission工具执行前跑一串策略,决定放行/拒绝/问用户agent/permission/index.ts PermissionManager
turn把 kosong 适配成 loop 的 LLM,驱动一个个回合,并把上面这些挂进 loop 钩子agent/turn/index.ts TurnFlow
tools维护当前可用工具表(内建 + 用户 + MCP + 动态)agent/tool/index.ts ToolManager
planMode / goal / swarmMode三种"跑法":只读计划、自主追目标、群体协作agent/plan/goal/swarm/
injection每步/边界处往历史尾部追加系统提醒(待办、权限模式、目标)agent/injection/manager.ts
background / cron背景 bash / 背景子 agent;定时任务agent/background/agent/cron/
records / replayBuilder把每个状态变更写成 wire 记录,支持 resume 精确重建agent/records/agent/replay/

2.3 主线走一遍(高层,不进代码)

从"用户敲一句话"到"模型开始生成",Agent 内部大致这样流转:

用户输入


turn.prompt() ──写 turn.prompt 记录──▶ records


context.appendUserMessage() 把话进历史


turn 启动一个回合,调 runTurn(loop) ←── 把 Agent 的子系统挂成 loop 的钩子:
│ beforeStep → 压缩 + 注入
│ buildMessages → context.messages(投影)
│ authorizeToolExecution → permission
│ finalizeToolResult → 去重 + 预算 + 钩子

loop 向 KosongLLM.chat() 要一次生成


context.messages 把历史投影成合法 wire ──▶ kosong.generate() ──▶ provider

关键洞察:loop 不主动去 Agent 里拿东西,是 Agent 把自己"喂"给 loop。 turn 在调 runTurn 时,把 buildMessagesbuildTools、以及一堆 hooks 作为回调传进去(packages/agent-core/src/agent/turn/index.ts:783-958)。loop 需要原料时回调这些函数,Agent 就在回调里现算。这样 loop 保持无状态,Agent 保持"活"的状态。


3. 系统提示与画像装配

它要解决的小问题: 模型不知道自己是谁、在哪、有哪些工具、项目有什么约定。系统提示就是每次请求最前面那段"人设 + 环境说明",得在运行时拼出来。

3.1 画像(profile)= 模板 + 变量 + 工具集

一个"画像"就是一种 agent 人设。默认有四个:agent(主 agent)、coderexploreplan,用 YAML 定义,靠 extends 继承(packages/agent-core/src/profile/default/)。例如 coder.yaml 继承 agent,只覆盖 roleAdditional(告诉它"你现在是子 agent")和工具白名单。

画像加载后不是一段死文本,而是一个渲染器函数:它闭包住合并后的模板和变量,等到真要生成提示时,才用运行时上下文把模板变量填进去(packages/agent-core/src/profile/resolve.ts:124-167 createSystemPromptRenderer)。为什么要延迟?因为 cwd 目录列表、AGENTS.md、技能清单这些只有运行时才知道。

3.2 运行时上下文哪来的

prepareSystemPromptContext 并行采集三样运行时信息(packages/agent-core/src/profile/context.ts:29-46):

变量内容采集方式
KIMI_WORK_DIR_LS当前目录列表listDirectory(kaos)
KIMI_AGENTS_MD项目/用户级 AGENTS.md 拼接从用户目录到项目叶子逐级收集
KIMI_ADDITIONAL_DIRS_INFO额外目录的列表逐个列目录

AGENTS.md 的收集顺序是"用户级在前、项目级在后",于是项目级能覆盖用户级(packages/agent-core/src/profile/context.ts:76-104)。超过 32 KB 不截断,只发一条 warning 提醒用户瘦身(AGENTS_MD_RECOMMENDED_MAX_BYTES context.ts:15)。

注意:这些 AGENTS.md 对 Agent 而言是被采集进提示的数据,不是给写文档的人的指令——本章按开源仓库的真实源码事实来写。

3.3 装配的三步

useProfile 是把画像挂上 Agent 的入口,做三件事(packages/agent-core/src/agent/index.ts:436-444):

useProfile(profile, context)

├─▶ setActiveProfile 记住当前画像(供压缩后重渲染用)
├─▶ updateSystemPromptFromProfile 用 context 渲染出系统提示 → config.update()
└─▶ tools.setActiveTools(profile.tools) 按画像白名单裁工具

压缩之后为什么要重来一遍?因为压缩会把老历史折叠掉,但系统提示里的 cwd 列表、技能清单是会话启动时的旧快照。refreshSystemPrompt 在压缩后重新采集运行时上下文再渲染一次,让压缩后的回合看到新鲜环境(代价是让 prompt cache 的前缀失效,这是有意的 packages/agent-core/src/agent/index.ts:457-465)。


4. 上下文记忆与投影

这是本章工程含量最高的一支。ContextMemory 存的历史和最终发给 provider 的消息,几乎从不一字不差——中间隔着一层叫"投影(projection)"的翻译。

4.1 为什么"存的"和"发的"要分开

ContextMemory._history 存的是事实:每一步的助手内容、工具调用、工具结果原文、以及结构化的 isError / note 字段(packages/agent-core/src/agent/context/index.ts:52-63)。它不为任何 provider 而优化,它只忠实记录发生了什么。

但严格 provider(如 Anthropic)对请求体有一堆硬规矩:每个 tool_use 后面必须紧跟对应的 tool_result;第一条消息必须是 user;不能有空文本块;不能有连续同角色。真实历史因为背景任务通知、steer 插入、中断等原因,经常违反这些规矩。

投影就是那层"临时修复":读侧变换,把历史整成合法 wire,但不动底层历史(packages/agent-core/src/agent/context/projector.ts:102-118 project)。

4.2 一次投影都修了啥

project() 是一条流水线,按顺序跑若干修复,每个修复都会通过 onAnomaly 汇报,好让"被悄悄修过的历史"留下痕迹而不是被掩盖:

history(事实)

├─ 合并相邻 user 消息 / 丢空白文本块
├─ (严格重发才开) 去重重复的 tool_use id
├─ 修复工具交换的相邻性 ◀── 核心:把错位的 tool_result 挪回它的 tool_use 后面;
│ 中途缺结果的调用合成一个占位结果关闭它
├─ (严格重发才开) 合并连续 assistant
├─ 丢掉没有对应调用的孤儿 tool_result
└─ (严格重发才开) 丢掉开头的非 user 消息

Message[](合法 wire)

"修复工具交换相邻性"最关键(packages/agent-core/src/agent/context/projector.ts:150-210)。它的巧处在于区分"尾部"和"中段"的缺失结果:

  • 中段某个 tool_use 没结果 → 后面已经有新回合了,证明它不可能还在飞,合成占位结果关闭它。
  • 尾部那个 tool_use 没结果 → 可能真的还在执行中,默认不动它(留给别的机制处理)。

于是历史保持忠实,模型永远收到合法的工具交换。

4.3 三种投影视图

ContextMemory 暴露几个只读 getter,对应不同场景(packages/agent-core/src/agent/context/index.ts:539-585):

视图用途额外做的事
messages正常每回合请求丢孤儿结果
strictMessagesprovider 报了 400 之后的兜底重发全套严格修复(去重、丢开头非 user、合并 assistant…)
mediaDegradedMessagesprovider 报 413(体积过大)后重发除最近 2 个外的媒体换成文字标记

这三个视图直接被 turn 当作 buildMessages / buildMessagesStrict / buildMessagesMediaDegraded 传给 loop——loop 报错时按需切换到更狠的投影重发。

4.4 历史是怎么长出来的:appendLoopEvent

历史不是直接 push 的,而是由 loop 事件驱动的状态机(packages/agent-core/src/agent/context/index.ts:644-755)。loop 每产出一个事件,appendLoopEvent 就先写进 records,再改内存:

step.begin → 新建一条空 assistant 消息,登记为 openStep
content.part → 往当前 openStep 追加内容块
tool.call → 往 openStep 追加工具调用,把 id 记入"待结果集合"
tool.result → 建 tool 消息,从"待结果集合"移除该 id
step.end → 结算 token,若工具交换已闭合则冲刷被延迟的消息

一个不变量保证正确:历史里除了尾部,不能有未闭合的工具交换。所以当有工具调用还没结果时,后来的用户消息(比如背景通知)会先被塞进 deferredMessages 暂存,等交换闭合了再冲刷进历史(appendMessage + flushDeferredMessagesIfToolExchangeClosed)。这避免了"用户消息插在 tool_use 和 tool_result 中间"这种非法结构。

4.5 工具结果的渲染只发生一次

历史里 tool 消息存的是原始输出 + isError + note。模型看到的那份"带 <system>ERROR: 前缀的、空输出补占位符的、note 拼在后面的"文本,是在投影边界恰好渲染一次的(packages/agent-core/src/agent/context/tool-result-render.ts:53-91 renderToolResultForModel)。好处:每一段系统生成的文字都带同样的 <system> 标记,模型永远能分清"这是工具产出"还是"这是主机在说话";而 UI 拿原始输出、靠 isError 自己上色,看不到这些标记。

4.6 动态工具与通知的投影

两件与投影相关的收尾:

  • 动态工具上下文:开了 select_tools 渐进披露时,历史里会有"工具 schema 消息"和"可加载工具公告"。给一个没有该能力的模型发请求时,stripDynamicToolContext 会在投影之前把这些剥掉(因为投影会抹掉 origin 锚点 packages/agent-core/src/agent/context/dynamic-tools.ts:52-71)。
  • 通知 XML:背景任务/子 agent 完成的通知,渲染成 <notification …> 结构块注入历史,子 agent 类型的通知还会带 agent_id 属性,方便模型直接拿去 Agent(resume=…)(packages/agent-core/src/agent/context/notification-xml.ts:26-49)。

5. 上下文压缩(解决上下文窗口溢出)

它要解决的小问题: 会话越来越长,迟早撑爆模型的上下文窗口。压缩就是"在快满时,把老对话换成一段摘要,腾出空间接着聊"。

5.1 何时触发:strategy

策略层很薄:默认在上下文用量达到窗口的 85% 时触发压缩,并且 blockRatio 也是 85%,意味着压缩是同步阻塞的(没有后台压缩)。另外还预留 50000 token 的输出空间,快踩到预留线时也提前压(packages/agent-core/src/agent/compaction/strategy.ts:25-31 DEFAULT_COMPACTION_CONFIG)。

turn 在每步之前调 fullCompaction.beforeStep:先看要不要压,该阻塞就等压缩跑完再继续(packages/agent-core/src/agent/compaction/full.ts:269-274)。

5.2 全量压缩(full.ts):把整段历史换成一封"给自己的信"

FullCompaction.compactionRound 是主流程(packages/agent-core/src/agent/compaction/full.ts:399-668):

  1. 快照当前历史,估算压缩前 token。
  2. 把历史(剥掉动态工具协议上下文)投影成合法请求,末尾接一条压缩指令,请模型写摘要。
  3. 拿到摘要后做一次竞态检查:如果压缩期间历史的前缀变了(比如用户撤销),或者尾部长出了会被压缩丢掉的非用户消息,就放弃这次压缩(避免悄悄吞掉内容)。
  4. context.applyCompaction,把历史重建成 [保留的用户消息, 摘要]

这封"摘要"很讲究:压缩指令让模型用第一人称、现在时给未来的自己写一封续写笔记,而不是写第三方报告,并且要用对话本来的语言写(packages/agent-core/src/agent/compaction/compaction-instruction.md:1-25)。目的是让压缩后的下一回合能无缝接上。

applyCompaction 保留用户消息时不是简单留尾巴,而是头 + 尾都留、中间用省略标记:在 token 预算内保留最老的头几条用户输入和最近的尾部输入,中间放一个 elision 标记说"这里省略了 N token"(packages/agent-core/src/agent/context/index.ts:313-429)。头也留,是因为最初的任务描述往往最重要。

5.3 溢出时的多级降级

如果连"发压缩请求"本身都塞不下,compactionRound 有一串兜底(同一个 while 循环里):

压缩请求被拒
├─ 413/图片格式错 且还没试过 → 把媒体换成文字标记,重试
├─ 上下文溢出 / 请求过大 → 按 0.7/0.5/0.35 比例砍掉最老消息,重试(最多 3 次)
├─ 空响应 / 被截断 → 丢最老一条消息重试(受重试预算封顶)
└─ 其它可重试错误 → 退避重试

被砍掉的老消息不在摘要覆盖范围内,droppedCount 会如实报告这个盲区(full.ts:452-577)。

5.4 微压缩(micro.ts):当前已禁用

诚实说明:micro.ts 里的微压缩(把老的大工具结果就地截断成一个标记、保留最近若干条)在本 commit 是关闭的——它依赖的 micro_compaction 实验开关已从注册表移除,detect()compact() 都直接返回、原实现整段注释掉(packages/agent-core/src/agent/compaction/micro.ts:50-138)。context.project 里仍会调 microCompaction.compact(shaped),但当前是恒等变换。所以现在实际生效的只有 §5.2 的全量压缩。


6. 权限门控(工具执行前 gate)

它要解决的小问题: 模型想写文件、跑命令、删东西——哪些直接放行、哪些得先问用户、哪些直接拒绝?这个判断必须在工具真正执行之前做。

6.1 挂载点:loop 的 authorizeToolExecution 钩子

turn 把权限管理器挂在 loop 的授权钩子上(packages/agent-core/src/agent/turn/index.ts:924-926):

loop 准备执行工具


authorizeToolExecution(ctx) ──▶ agent.permission.beforeToolCall(ctx)
│ 跑一串策略,第一个出结果的赢

返回 undefined = 放行 / {block:true} = 拒绝 / 触发审批请求 = 问用户

6.2 策略链:一串"看情况说话"的判官

PermissionManager.beforeToolCall 按顺序问每个策略,第一个给出非 undefined 结果的胜出(packages/agent-core/src/agent/permission/index.ts:96-114 + evaluatePolicies)。策略的顺序本身就是优先级,读一遍这个顺序基本就懂了整套权限模型(packages/agent-core/src/agent/permission/policies/index.ts:28-71)。挑关键几条:

顺序(靠前=优先)策略决定
1PreToolUse 钩子返回 block拒绝
2AgentSwarm 必须独占一批拒绝(与模式无关)
4plan 模式下写计划文件外的文件拒绝
5用户配置的 deny 规则命中拒绝
6auto 模式放行(能拦的 deny 都在它上面)
7本会话已"批准整场"的记忆命中放行
8 / 9用户配置的 ask / allow 规则问 / 放行
倒数几条碰到敏感文件(.env、SSH key)
倒数第 4yolo 模式放行
倒数第 3默认放行表里的只读工具放行
最后什么都没命中问用户(fallback)

这个"deny 全在 approve 前面、fallback 是 ask"的排布,保证了默认安全:没被任何规则明确放行的操作,兜底都会去问人。

6.3 规则怎么匹配:一个小 DSL

用户配的权限规则是形如 Read(/etc/**)Bash(!rm *)mcp__github__* 的字符串。parsePattern 把它拆成"工具名 + 可选的参数模式",工具名用 picomatch 做 glob 匹配,参数模式则交给工具自带的 matchesRule 匹配器判断(packages/agent-core/src/agent/permission/matches-rule.ts:47-98)。工具名与参数分离,是因为二者语义不同——工具名是主机认识的,参数怎么算命中只有工具自己知道。

6.4 审批请求与"记住这次"

若策略判为 ask,requestToolApproval 通过 RPC 向前端要审批,顺便触发 PermissionRequest/PermissionResult 钩子。如果用户选了"本会话都批准",就把该规则模式记进 localSessionApprovalRulePatterns,下次同类调用直接被 §6.2 第 7 条放行(packages/agent-core/src/agent/permission/index.ts:116-25072-94)。

子 agent 被拒时,拒绝话术会不一样:告诉它"别重试同一个调用、别想绕过限制"(因为子 agent 不能直接问最终用户 permission/index.ts:304-311)。


7. 三种模式:plan · goal · swarm

三种模式改变的是"这个 agent 这一段怎么跑",都是挂在 Agent 上的独立子系统。

7.1 计划模式(plan):只读地想清楚再动手

PlanMode 维护一个"是否在计划中 + 计划文件路径"的状态(packages/agent-core/src/agent/plan/index.ts:14-140)。进入后,权限策略里的 PlanModeGuardDeny 会拒绝对计划文件之外的写/编辑,PlanModeToolApprove 只放行读、进计划、写计划文件本身——于是模型被逼着"只读地想、把方案写进计划文件",直到用户 ExitPlanMode 审核通过。计划内容存成一个 .md 文件(homedir/plans/<id>.md 或 cwd 下)。

7.2 目标模式(goal):自主追一个目标直到完成/受阻

GoalMode 是三者里最厚的(packages/agent-core/src/agent/goal/index.ts:227-718),因为它管一个持久的、有生命周期的目标:

active ──pause──▶ paused ──resume──▶ active
│ \ ▲
│ \──markBlocked──▶ blocked ───────┘

└──markComplete──▶ complete(瞬态:宣告成功后立刻清空,从不落盘)

状态被刻意压到最少:落盘的只有 active/paused/blocked,complete 是"宣告即清除"的瞬态。pausedblocked 本质相同(都是"驱动不再跑、但目标完好可 /goal resume"),只差在停的(用户 vs 系统)和 terminalReason。没有单独的 impossible/error/cancelled 状态——不可达就是 blocked、运行失败就是 paused、取消就直接删记录。

目标还带预算(token / 回合数 / 墙钟时间),turn 每步把 token 记进目标,超预算就在回合边界把目标标为 blocked(packages/agent-core/src/agent/turn/index.ts:799-806843-845)。每个目标回合其实是一个普通回合,由 turn 里的 goal 驱动决定要不要再跑一轮,续跑用的是一段合成的 "continue working toward the goal" 提示(turn/index.ts:92-121)。

7.3 群体模式(swarm):一批子 agent 并行

SwarmMode 只是个开关 + 提醒(packages/agent-core/src/agent/swarm/index.ts:13-60):进入时往历史注入一段"群体模式"提醒,退出时撤掉或补一段退出提醒。真正的并行编排在别处(见 §8 的 AgentSwarmsubagent-batch)——这符合本章的分层:agent/ 只管"我处于群体模式",多 agent 的调度归 session/


8. TurnFlow:把 kosong 适配成 loop、并把子系统挂进 loop

TurnFlow 是 Agent 和 loop 之间的变速箱。它做两件事:提供一个 loop 能用的 LLM;在 loop 的每个钩子上插入 Agent 的逻辑。

8.1 把 kosong 适配成 loop 的 LLM:KosongLLM

loop 只认一个抽象的 LLM.chat() 接口,不认 kosong。KosongLLM 就是把 kosong 的流式 generate() 桥接成这个接口(packages/agent-core/src/agent/turn/kosong-llm.ts:69-170):

  • kosong 的逐块 onMessagePart 回调 → 转成 loop 的逐 delta 回调(文本/思考/工具调用)。
  • 流结束后再把合并好的完整内容块回放给 loop 的逐块回调,保证顺序、避免半截内容落地。
  • 按需给每次请求算并套上"补全预算"(留多少输出空间),还会把当前模型不支持的媒体(图/音/视频)降级成文字占位(downgradeUnsupportedMedia)。

Agent.llm getter 每次都新建一个 KosongLLM,喂进当前 provider、系统提示、能力(packages/agent-core/src/agent/index.ts:417-434)。

8.2 把子系统挂进 loop 钩子

这是整章的枢纽turnrunTurn 时,传进一组 hooks,把 Agent 的各个子系统精确地插到 loop 的时序里(packages/agent-core/src/agent/turn/index.ts:783-958):

loop 钩子Agent 插进去的逻辑
buildMessagescontext.messages(投影)
beforeStep全量压缩检查 + 冲刷 steer 缓冲 + 动态注入(待办/权限模式/计划)
afterStep记 usage + 压缩后处理 + 去重器结算本步
prepareToolExecution同步去重:同一步里重复的 (工具,参数) 复用首个结果
authorizeToolExecution权限门控(§6)
finalizeToolResult跨步去重收尾 + 触发 PostToolUse 钩子 + 大结果落盘
shouldContinueAfterStop决定回合要不要因 steer/goal/Stop 钩子而续跑

8.3 工具去重:别让模型原地打转

ToolCallDeduplicator 处理两种重复(packages/agent-core/src/agent/turn/tool-dedup.ts:116-262):

  • 同步内去重:同一个 LLM 步里发了两个一模一样的调用,第二个不真跑,直接复用第一个的结果。
  • 跨步去重:连续多步发同一个调用,给结果追加升级式的系统提醒——连了 3 次给温和提醒,5 次给"三选一决策菜单",8 次给"现在就写最终答复",到 12 次直接 stopTurn 强制结束这一回合。这套阶梯专治模型卡死循环。

8.4 大工具结果落盘:别撑爆上下文

budgetToolResultForModel:工具输出超过 5 万字符就写到 homedir/tool-results/*.txt,只给模型留一段 2000 字的预览 + 文件路径 + "用 Read 分页看全文"的提示(packages/agent-core/src/agent/turn/tool-result-budget.ts:19-83)。一次巨大的 grep 结果不会一口气塞满上下文。


9. 子 agent 编排(为什么在 session/ 而不在 agent/)

关键分层: 派子 agent 需要"父子关系、共享一个会话、并行调度"——这些恰恰是 §1 那条约束禁止 Agent 依赖的东西。所以子 agent 编排住在 session/,而不是 agent/Agent 只提供"能被当子 agent 用"的能力,不知道自己是谁的孩子。

9.1 一个子 agent 就是另一个 Agent 实例

SessionSubagentHost.spawn 的流程(packages/agent-core/src/session/subagent-host.ts:152-177):

父 agent 想派活

├─ session.createAgent({type:'sub'}) 造一个全新的 Agent 实例(子)
├─ configureChild() 子继承父的模型/cwd/思考档;按子画像装配系统提示与工具
├─ child.turn.prompt(任务) 子自己跑一个完整回合(有自己的 context/permission)
└─ waitForChildCompletion() 等子跑完,取它最后一条 assistant 文本当交接

子 agent 是独立的 Agent:自己的历史、自己的权限管理器(带 parent 指针继承规则)、自己的压缩。父 agent 看不到子的上下文,只看到子的最后一条消息——所以子画像(coder.yaml)的系统提示反复强调"你的最终消息就是全部交接,要技术上完整"(packages/agent-core/src/profile/default/coder.yaml:4-7)。太短(<200 字符)还会被要求扩写一轮(subagent-host.ts:378-386)。

9.2 前台/后台与 Agent 工具

模型侧通过 Agent 工具派活(packages/agent-core/src/tools/builtin/collaboration/agent.ts:107-312)。它是"协作工具",构造时直接注入 SessionSubagentHostBackgroundManager,而不走普通工具的 Runtime。前台调用等子跑完再返回;run_in_background=true 则立刻返回一个 task_id,完成通知在之后某个回合自动到达。工具结果里连 agent_idresume_hint 一起给,方便模型日后 Agent(resume="…") 续跑同一个子。

side-channel(btw)是特例:startBtw 造一个内存态子 agent,把父的历史投影过去,注入"这是旁路问答、禁用所有工具"的提醒,并在权限链最前面塞一个 DenyAll 策略(subagent-host.ts:251-275)。


10. 背景任务、cron、记录与回放

10.1 背景任务与 cron

  • BackgroundManager 管两类背景活:背景 bash 和背景子 agent。每个任务有唯一 id、环形缓冲收 stdout/stderr、能查状态/取输出/停止。任务终结时把结果渲染成 <notification> 注入历史,让模型在后续回合反应(packages/agent-core/src/agent/background/index.ts)。
  • CronManager 是 agent 侧的定时门面:把定时任务存进 SessionCronStore,每个 tick 检查,只在 agent.turn.hasActiveTurn 为假(空闲)时触发,触发时用一个带 CronJobOriginsteer(...) 把提醒喂进去(packages/agent-core/src/agent/cron/manager.ts)。子 agent 从不排程,所以子 agent 的 cronnull(agent/index.ts:231)。

10.2 记录与回放:每个状态变更都能精确重建

Agent 的每一次状态变更都写成一条 wire 记录(records.logRecord)。resume 时,restoreAgentRecord 按记录类型回放——关键契约是:尽量调用当初写这条记录的同一个方法来重建,让"活着跑"和"恢复"共用同一条状态变更路径(packages/agent-core/src/agent/records/index.ts:22-120)。

turn.prompt → 回放 turn.restorePrompt()
permission.set_mode→ 回放 permission.setMode()
context.append_* → 回放 context.appendMessage/appendLoopEvent()
apply_compaction → 回放 context.applyCompaction()

回放期间只重建内存:不发 UI 事件、不调 LLM、不跑工具、不碰会触发外部副作用的文件(records/index.ts:22-31 的契约注释)。ReplayBuilder 则在恢复时顺带攒出一份"给前端重放用"的记录序列,支持撤销边界、范围切片(packages/agent-core/src/agent/replay/index.ts:16-104)。

Agent.resume 把这些串起来:回放记录 → 归一化目标状态 → 从盘加载背景任务/cron → 收尾上下文与回合的恢复(packages/agent-core/src/agent/index.ts:482-497)。


11. 巧妙之处(可借鉴的技术)

  • "存事实、发投影"的双层历史。 底层历史只忠实记录发生了什么,合法性修复全在只读投影层做,永不回写历史。于是回放、导出、调试看到的都是真相,而每个 provider 都收到为它整过的合法请求(context/projector.ts:102-118)。
  • 区分"中段缺结果"与"尾部缺结果"。 同样是"工具调用没有结果",中段的一定不是在飞(后面有新回合作证),尾部的可能还在飞——据此决定合成占位还是留着(context/projector.ts:150-210)。
  • 策略链即权限模型。 权限不是一个大 if,而是一串有序策略,"第一个出声的赢",deny 全排在 approve 前、fallback 是 ask,读顺序就读懂了默认安全(permission/policies/index.ts:28-71)。
  • 压缩摘要是"给自己的第一人称续写笔记"。 不是第三方报告,用对话原语言写,目标是下一回合无缝接上;保留用户消息还"留头留尾中间省略",因为最初的任务往往最重要(compaction/compaction-instruction.mdcontext/index.ts:313-429)。
  • 去重的升级式提醒 + 强制停。 连续重复同一调用时,提醒一级级加码到最后强制结束回合,专治模型卡死(turn/tool-dedup.ts:116-262)。
  • 把状态压到最少。 目标只有 3 个落盘状态,complete 是瞬态;pausedblocked 只差"谁停的",没有 impossible/error/cancelled 冗余态(goal/index.ts:69-104)。
  • 回放走写入时的同一方法。 恢复不是另写一套"设值"逻辑,而是复用当初的状态变更方法,天然避免"活路径与恢复路径漂移"(records/index.ts:22-31)。

12. 边界与局限(诚实)

  • 微压缩当前禁用。 micro.ts 整段实现被注释、开关已移除,现在只有全量压缩生效(compaction/micro.ts:50-138)。
  • 子 agent 编排不在本层。 agent/ 刻意不含父子/多 agent 调度;那些在 session/subagent-host.tssubagent-batch.ts。想读并行群体调度得去 session/
  • 压缩会留盲区。 当压缩请求本身都塞不下、走到"砍最老消息"这一步时,被砍的消息不在摘要覆盖内,只能靠 droppedCount 如实报告丢了多少(compaction/full.ts:452-577)。
  • 压缩是同步阻塞的。 默认 blockRatio == triggerRatio == 0.85,没有后台压缩,压缩期间回合会等(compaction/strategy.ts:25-31)。
  • 具体工具实现不在此章。 工具怎么落到真实文件/GUI/浏览器,见 03-tools;provider 协议细节见 04-providers

13. 代码地图(导航索引)

主题文件路径符号名
Agent 装配台 / 构造packages/agent-core/src/agent/index.tsAgentAgentOptions
LLM / 生成入口packages/agent-core/src/agent/index.tsget llmget generate
画像挂载 / 提示重渲染packages/agent-core/src/agent/index.tsuseProfilerefreshSystemPrompt
恢复入口packages/agent-core/src/agent/index.tsresume
画像解析(extends 继承)packages/agent-core/src/profile/resolve.tsresolveAgentProfilescreateSystemPromptRenderer
运行时提示上下文采集packages/agent-core/src/profile/context.tsprepareSystemPromptContextloadAgentsMdForRoots
画像加载(YAML→raw)packages/agent-core/src/profile/load.tsloadAgentProfilesFromSources
上下文记忆packages/agent-core/src/agent/context/index.tsContextMemoryappendLoopEventapplyCompaction
投影(合法化历史)packages/agent-core/src/agent/context/projector.tsprojectrepairToolExchangeAdjacency
工具结果渲染packages/agent-core/src/agent/context/tool-result-render.tsrenderToolResultForModel
动态工具剥离packages/agent-core/src/agent/context/dynamic-tools.tsstripDynamicToolContextfoldAnnouncedToolNames
通知 XMLpackages/agent-core/src/agent/context/notification-xml.tsrenderNotificationXml
全量压缩packages/agent-core/src/agent/compaction/full.tsFullCompactioncompactionRound
微压缩(已禁用)packages/agent-core/src/agent/compaction/micro.tsMicroCompaction
压缩策略/阈值packages/agent-core/src/agent/compaction/strategy.tsDefaultCompactionStrategyDEFAULT_COMPACTION_CONFIG
压缩指令模板packages/agent-core/src/agent/compaction/compaction-instruction.md(提示文本)
权限管理器packages/agent-core/src/agent/permission/index.tsPermissionManagerbeforeToolCall
权限策略链packages/agent-core/src/agent/permission/policies/index.tscreatePermissionDecisionPolicies
权限规则匹配 DSLpackages/agent-core/src/agent/permission/matches-rule.tsparsePatternmatchPermissionRule
计划模式packages/agent-core/src/agent/plan/index.tsPlanMode
目标模式packages/agent-core/src/agent/goal/index.tsGoalModeGoalStatus
群体模式packages/agent-core/src/agent/swarm/index.tsSwarmMode
回合驱动 / loop 钩子装配packages/agent-core/src/agent/turn/index.tsTurnFlowrunOneTurn
kosong→loop 适配packages/agent-core/src/agent/turn/kosong-llm.tsKosongLLMdowngradeUnsupportedMedia
工具去重packages/agent-core/src/agent/turn/tool-dedup.tsToolCallDeduplicator
大工具结果落盘packages/agent-core/src/agent/turn/tool-result-budget.tsbudgetToolResultForModel
动态注入packages/agent-core/src/agent/injection/manager.tsInjectionManager
子 agent 编排(在 session/)packages/agent-core/src/session/subagent-host.tsSessionSubagentHostspawn
子 agent 批调度packages/agent-core/src/session/subagent-batch.tsSubagentBatch
Agent 工具(模型派活)packages/agent-core/src/tools/builtin/collaboration/agent.tsAgentTool
背景任务packages/agent-core/src/agent/background/index.tsBackgroundManager
定时任务packages/agent-core/src/agent/cron/manager.tsCronManager
记录 / 回放重建packages/agent-core/src/agent/records/index.tsAgentRecordsrestoreAgentRecord
回放序列构建packages/agent-core/src/agent/replay/index.tsReplayBuilder
配置状态packages/agent-core/src/agent/config/index.tsConfigState