任务交接与迁移
当对话的上下文接近模型窗口上限时,继续对话会导致信息被截断或请求失败。这里有两个相关但不同的概念:
| 概念 | 做什么 | 命令 |
|---|---|---|
| 交接(handoff) | 停下手头工作,写出一份交接文档。会话照旧运行。 | /handoff |
| 迁移(migrate) | 交接 加上 清空历史、以该文档开启新会话。 | /migrate |
所以「迁移」= 「交接 + 新 agent 上岗」:原 agent 交班,新会话从文档接续。
交接文档
每一次交接(手动或自动)都会产出一个 Markdown 文件:
<项目>/.ai-zen/handoffs/<YYYYMMDD-HHmmss>-<会话id前8位>.md- 目录在项目内,便于随代码一起审阅、提交;
- 文件名确定性可预期,并带原始会话 id,方便回溯;
- 注入新会话的断点消息只包含路径——接手的 agent 在真正需要细节时才
read它。
模板来自 Skill,不在代码里
文档结构与书写规则由 handoff Skill 定义:
~/.ai-zen/skills/handoff/SKILL.md它由 SDK 首次 bootstrap 时释放,并在后续版本自动同步:
- 未被改动(frontmatter 没有
metadata.custom: "true")→ 覆盖为最新版本,模板改进能到达老用户; - 标了
custom: "true"→ 视为用户自有,永不覆盖。
也可以在项目里放一份 ./.ai-zen/skills/handoff/SKILL.md 覆盖(遵循既有的 Skill 目录优先级)。
代码里不再保留第二份章节清单。它只负责四件事:读 Skill 正文 → 注入 → 校验文件确实写出来了 → 读不到 Skill 时使用内置的最小兜底模板。
默认模板包含的章节:
- 💬 对话断点 —— 用户最后的要求、你最后交付了什么;
- 🎯 任务目标 —— 用户到底要什么(意图与验收标准);
- 📊 当前状态 —— 分支/commit、未提交改动、测试是否通过、有无进程/端口在跑;
- ✅ 已完成 —— 每项附产出路径与验证方式;
- 📋 未完成 —— 按优先级,每项给「下一步最小动作」;
- 🧠 关键决策 —— 取舍理由,以及走过的死路;
- ⚠️ 约束与偏好 —— 用户强调的规矩、项目约定、不可动的部分;
- 📁 文件索引 —— 路径 + 作用 + 本次是否改动;
- 🔔 接手第一步 —— 3 条以内可直接执行的命令。
两种执行者
| 执行者 | 谁来写 | 优势 | 代价 |
|---|---|---|---|
self(默认) | 原 agent 停手后在自己的会话里写 | 上下文完整,可用工具核实真实状态 | 多 1~2 次全量请求(输入侧) |
summary | 一次独立模型调用,基于序列化历史摘要 | 便宜、确定 | 无工具、无原会话上下文,保真度低 |
self 之所以是默认,是因为「保真」正是交接的意义。summary 是逃生口,也是单向降级目标:self 失败(没有 write 工具、文件缺失或过小且重试一次仍不行、工具/护栏报错)时自动降级并告知用户。反向绝不发生——那会在最危险的时刻再加一次全量请求。
self 为什么可靠
- 「停手」由代码兜底,不只是提示词:交接回合会临时启用一次性
HandoffGuardPlugin,拒绝edit/batch_edit/rm/rename/mkdir/copy,write仅在目标路径归一后等于交接文档时放行。拒绝是软拒绝——原因作为工具结果回给模型,它会转回正题,而不是把整轮交接打断。 - 边界:挡不住
exec(echo > file总能绕过)。它降低的是误操作概率,不是沙箱。 - 护栏是放宽而非消失:
service.busy期间ContextGuardPlugin从maxTokens × 1.5放宽到× 2——交接恰好发生在上下文最满的时刻,但仍保留熔断。 - 不会递归:交接回合本身也是一次
send(),AutoMigratePlugin在service.busy时让行,因此「交接触发迁移、迁移再触发交接」不可能发生。
触发方式
手动
/handoff # 只写交接文档,保持当前会话
/handoff summary # 强制使用独立摘要执行者
/migrate # 交接 + 开启新会话(别名 /migration)
/migrate self # 显式指定执行者/handoff 不弹确认(非破坏性、可反复执行);而且交接回合会留在 agent.messages 里,你可以接着说「第 3 节写细一点」「补上端口占用情况」,agent 会重写同一个文件。/migrate 会丢历史,因此要确认。
自动(上下文超限)
当 usage.prompt_tokens 超过模型的 maxContextTokens,SDK 的 AutoMigratePlugin 触发迁移。交互式终端里 CLI 弹出三选一选择器:
原 agent 自交接(默认高亮)—— 保真度最高;独立摘要—— 便宜;Esc—— 跳过本次。历史不受影响;只要用量仍超阈值,下次回复后还会再问。
非交互环境(管道、重定向、e2e 脚本)没有选择通道:按默认执行者迁移,不阻塞等待输入、不额外输出。
迁移链路
onBeforeHandoff—— 进度提示(正在编写任务交接文档…);- 交接回合 —— 中止在途请求,执行
self回合(或summary调用),校验文件(存在 + ≥ 400 字节,失败重试一次,再失败降级); onHandoff—— 提示文档路径;若是迁移,此刻保存旧会话(已包含交接回合,归档可审计);migrate的prune策略 —— 物理剔除历史,只保留definition.messages+ 一条只含路径的断点消息;onMigrated—— 清空agent.lastUsage(否则护栏会拿陈旧用量误杀新会话的第一条消息),分配新会话 id、落盘、更新「上一轮会话」指针。
旧会话一定在剔除前保存,因此即使交接失败也不会丢历史:可以重试,也可以继续当前对话。
会话 id 只有一个真相源。
onMigrated写入的新 id 必须立刻对会话层的落盘逻辑可见(CLI 中ChatSession.currentId直接代理ConversationContext.currentId,而不是各存一份)。否则下一轮自动落盘仍按旧 id 写盘,会把prune后的空壳(系统提示 + 断点)覆盖到旧会话文件上,表现为「交接之后历史全没了,只剩交接文档」。
成本说明
- 每次交接文档只写一次,代码不会重新生成或再次摘要;
- 断点消息只带路径,
read的成本由真正需要细节的一方支付; self相比summary多花的钱几乎全在输入侧(多 1~2 次全量历史),输出侧基本相同(文档正文 + ≤40 字确认)。