feat(agent): mode-scoped agent profiles with cross-mode handoff
没有 composer 声明时回落到 Durable Object 里的模式;显式 none 才会清除它。 toStoredMode 会把 design preset 去掉,只保存“design 模式”本身,避免下一轮误绑定到声明轮预建的空白页。这个函数把最容易被散落在请求处理器里的语义压缩成可单测的状态边界
source_automation: “voyager-merged-pr” run_date: “2026-07-15” anchor_pr_number: 4637 pr_number: 4637 pr_title: “feat(agent): mode-scoped agent profiles with cross-mode handoff” pr_url: “https://github.com/adastralab-ai/voyager/pull/4637 ↗” author: “Horcrux / magicismight” merged_at: “2026-07-15T02:39:07Z” modules: [“backend/workers/agent”,“agent-eval”,“backend/workers/task”,“packages/common/src/agentChat”,“packages/site/src/app/(main)/_agent”] files_changed: 43 learning_tags: [“agent-runtime”,“mode-profile”,“prompt-caching”,“cross-mode-handoff”,“state-machine”,“tool-protocol”,“durable-chat-state”,“agent-eval”,“image-routing”] business_line: null related_prs: [4389,4580,4720,4598] line_stage: null open_questions: [] feishu_doc_url: “https://my.feishu.cn/docx/JIkxdKv0eoUWkAxl6RdcjFnWnCc ↗”#
正文来源:飞书学习文档 ↗。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。
今日选择#
- PR:feat(agent): mode-scoped agent profiles with cross-mode handoff(#4637)
- 作者:Horcrux(magicismight)
- Merge 时间:2026-07-15 02:39:07 UTC(Melbourne:2026-07-15 12:39:07 AEST)
- 链接:GitHub PR #4637 ↗
- 合并提交:
760c530f7fbe287c8c2feb908d390195571f57f1,已验证可达origin/main - 文件:43 个;改动:+2201/-653
- 模块:
backend/workers/agent、agent-eval、backend/workers/task、packages/common/src/agentChat、packages/site/src/app/(main)/_agent
为什么值得学#
- 它修的不是某个 prompt 文案,而是 agent 的路由模型:把原先全局 system prompt 加一次性 creation directive,改成 router、design、image、document 四类 profile,避免 design 路由规则泄漏到图片和文档请求。
- 跨模式切换被建模为真实工具动作
switch_creation_mode,同轮立即生效并持久化;模型不能只在文本里说“切换了”,而必须经过可观测的工具轨迹。 - 它把 durable state 和 turn-local state 分开:模式可以粘住会话,design 的 preset 绑定只服务声明轮,image 的比例/分辨率则作为持续偏好保留。这直接处理了重连、regenerate、后续轮次和 composer 取消选择。
- 验证面不是只测函数返回值,而是测模型轨迹:路由顺序、页面内编辑不切模式、模式粘性、图片交付形态、历史图片引用,以及同轮多次 handoff。
关键代码#
1. 用纯状态转移处理声明、持久模式和显式取消#
export function resolveCreationMode(bodyCreation, stored) {
if (!bodyCreation) {
return { creation: stored, declared: false, nextStored: stored };
}
const creation = bodyCreation.kind === "none" ? undefined : bodyCreation;
return {
creation,
declared: creation !== undefined,
nextStored: creation && toStoredMode(creation),
};
}plaintext没有 composer 声明时回落到 Durable Object 里的模式;显式 none 才会清除它。toStoredMode 会把 design preset 去掉,只保存“design 模式”本身,避免下一轮误绑定到声明轮预建的空白页。这个函数把最容易被散落在请求处理器里的语义压缩成可单测的状态边界。
2. 把跨模式 handoff 变成同轮的工具协议#
switchCreationMode: async (kind) => {
if ((turnProfileSwitch ?? creation)?.kind === kind) {
return { switched: true, kind };
}
if (kind === "document" && !skills.some((skill) => skill.data.name === "document")) {
return { switched: false, reason: "Document pages are unavailable" };
}
turnProfileSwitch = creation?.kind === kind ? creation : { kind };
await this.resolveCreation(turnProfileSwitch);
return { switched: true, kind };
}plaintext同模式切换幂等,文档能力不可用时返回结构化失败,其他切换先更新本轮 profile 再沿用 composer 的持久化语义。这样“先切换、再建造”的顺序能被工具轨迹和后续 prepareStep 同时观察到。
3. 把 prompt cache 边界和模式边界对齐#
const baseSystemMessage = {
role: "system",
content: buildBasePrompt(skills),
};
const profileSystemMessage = {
role: "system",
content: profile.profilePrompt,
};
messages: [
baseSystemMessage,
profileSystemMessage,
userContextMessage,
...(creationDirectiveMessage ? [creationDirectiveMessage] : []),
]plaintextbase prompt 包含跨用户、跨模式都稳定的产品规则和通用 skills;profile 只承载当前模式的路由/手艺;用户上下文和一次性 directive 放在后面。随后 prepareStep 在 handoff 时替换第二个 system message,并移除过期 directive。设计上的收益是工具 schema 前缀保持稳定,模式切换只让 profile 段失效,不把所有历史 prompt cache 一起打散。
4. 用轨迹断言锁住路由顺序,而不是只断言最终文本#
- description: "router: an explicit standalone image ask switches to image"
assert:
- type: trajectory:tool-args-match
value:
name: switch_creation_mode
args:
kind: image
- type: trajectory:tool-used
value: image_generation
- type: not-trajectory:tool-used
value: load_creation_targetplaintext路由 case 要求 standalone image 先 switch 到 image,再生成,并明确不能走 design target。另一组 sticky case 用历史和 page mock 验证后续编辑留在原模式;image-mode case 还检查一次生成、原话 prompt、引用 asset,以及 image-only page 交付。评估 harness 让这些判断复用真实 profile、tool schema、skill 和 sandbox 路径。
和最近学习记录的关系#
有直接关系,而且是同一条 agent runtime 演进链:
- #4389 先把 home composer 的 creation type 和 image model 路由带进 agent 协议;#4637 把“一次性 directive”升级为可持久化、可 handoff 的 mode profile。
- #4580 把高级图片模型升级做成用户确认协议;#4637 将这类策略放进 mode-specific prompt,并保留 image tool 对高分辨率的机械拒绝。
- #4720 昨天把 assetId 作为 durable transcript 状态、签名 URL 作为每轮 materialization;#4637 采用相同边界意识:持久化模式不携带声明轮的 preset,profile 切换不改变稳定的工具 schema。
- #4598 的 CODE inline editing 与本 PR 没有直接代码依赖,但都把短生命周期的交互/运行时表面和可恢复的语义状态拆开,方法论相近。
我会怎么吸收#
- 遇到“状态会跨轮保留,但某些参数只对当前动作有效”的功能,先写一个纯状态转移函数,明确 absent、explicit clear、stored fallback 和 declaration-scoped 字段,再接入存储。
- 当模型需要改变行为时,优先增加一个可审计的动作/状态边界,并在执行层做机械 gate;不要把关键约束全部寄托在 prompt 叙述上。
- 对 agent 功能测试工具轨迹和顺序:验证先后、次数、禁止动作、历史回放和最终交付形态,而不是只看最后一段自然语言。
边界与风险#
- PR 自身列出一个未收口的产品冲突:自由聊天里的独立图片请求切入 image 模式后会继承“直选模型”语义,而旧自由态仍是“先确认高级模型”;需要产品拍板后再收敛对应 profile/eval。
- design target 表仍保留
imagetarget,因此已锚定 design 的会话里,独立图片可能继续走 design 流的 image target,而不是切到 image profile。 - composer 对 stored mode 的回显、取消入口和 switch card 依赖后续 #4758 二期;PR body 明确写了 handoff 在二期前主要是 dev 验证范围。
- image mode 的粘性会改变后续请求的默认解释:独立图片之后再说“做海报”,如果没有明确 deck/多页等 design 信号,可能仍被当成 flat image。模式粘性是有意设计,但需要持续观察误切和 ping-pong。
候选说明#
按 Melbourne 本地日界线统计,今天有 6 个已经进入 origin/main 且未被学习日志记录的 merged PR;昨天有 14 个候选,其中 #4720 已记录并跳过。今天的候选还包括 #4706(CODE PNG export)、#4751(Admin workspaces)、#4773(prompt composer 粘贴图片)、#4614(CODE typography)和 #4696(mobile polish)。最终选 #4637,是因为它同时触及 agent runtime、prompt cache、tool protocol、持久化状态和 eval 轨迹,并且直接接续最近几天的 #4389、#4580、#4720;不是单纯按 diff 行数选择。
飞书文档#
本页即为本次学习文档。PR 链接、作者、merge 时间、模块、文件数和学习标签已记录在上方。
学习标签:agent-runtime、mode-profile、prompt-caching、cross-mode-handoff、state-machine、tool-protocol、durable-chat-state、agent-eval、image-routing