opencode v2 会话消息形态设计:User/Assistant 存储模型、PromptMessage 与 Prompt Mutators 三种方案对比 📅 发布时间:2026/9/7 17:38:35 👁 浏览次数: opencode v2 会话消息形态设计User/Assistant 存储模型、PromptMessage 与 Prompt Mutators 三种方案对比【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode本文基于 opencode 仓库中的 v2 设计规范文档 message-shape.md深入解析 v2 会话存储层消息形态Message Shape的设计动机、三种候选方案的完整类型定义与权衡取舍并结合 packages/schema/src/session-message.ts 中的现有实现源码说明每种方案相对现状的具体改进点。读完本文你将理解为什么持久化历史与提示词改写prompt surgery应当分离以及 v2 消息结构如何以更小的存储体积支撑会话的重放replay与恢复resume。问题陈述存储消息与提示词改写的需求冲突v2 设计文档开篇给出的问题定义只有三条但精准命中了编码 Agent 会话存储的两个根本矛盾存储的消息需要足够的数据以便日后重放和恢复一个会话stored messages need enough data to replay and resume a session later提示词钩子prompt hooks往往只是想追加一条合成的 user/assistant 消息prompt hooks often just want to append a synthetic user/assistant message现状是这就意味着要伪造fakeID、时间戳和请求元数据。第 3 条是当前实现的痛点所在。从源码结构看这一痛点有直接证据当前 v2 的会话消息统一由 session-message.ts 中的Session.Message联合类型承载所有消息类型共享同一个Base结构const Base { id: ID, // msg_ 前缀的品牌化 ID metadata: ..., // 任意元数据 time: { created }, // 创建时间 }其中就包含专门用于注入合成文本的Synthetic消息类型export const Synthetic Schema.Struct({ ...Base, // 必须携带 id、time.created 等完整真实消息字段 sessionID: SessionID, text: Schema.String, type: Schema.Literal(synthetic), })也就是说一个插件钩子如果只是想往提示词里插一句总结上面工具的输出并继续就必须构造一条带有 ID、时间戳、会话 ID 的完整持久化消息——这正是文档所说的faking ids, timestamps。而Assistant消息则同时承载了运行上下文agent、model: Model.Ref、结果finish、error、用量cost、tokens与快照snapshot等大量执行侧数据使得纯对话内容与执行元数据混居在一条消息里。设计文档的目标就是解决这一矛盾让存储历史更干净让提示词钩子更轻。以下三个方案按递进关系给出。方案一两种消息形态Two Message Shapes方案一的核心思路是User/Assistant继续作为存储历史的模型但要把它们清理干净同时新增一个独立的、瞬态transient的PromptMessage专供提示词改写使用。存储形态精简后的 User / Assistanttype User { role: user time: { created: number } request: { agent: string model: ModelRef variant?: string variant?: string format?: OutputFormat system?: string tools?: Recordstring, boolean } } type Assistant { role: assistant run: { agent: string; model: ModelRef; path: { cwd: string; root: string } } usage: { cost: number; tokens: Tokens } result: { finish?: string; error?: Error; structured?: unknown; kind: reply | summary } }注以上代码完整继承自 message-shape.md 原文User.request中未含variant之外的重复字段User的request字段实际包含agent / model / variant? / format? / system? / tools?。与现状相比可以注意到几个关键变化执行设置收敛进request子对象agent、model、variant、format、system、tools归组为一次请求的完整描述。对照 session-message.ts 中的现状Assistant目前把agent、model、finish、cost、tokens、error全部平铺在消息顶层而方案一将其重组为run运行时上下文agent、model、工作目录路径与usage/result用量与结果两个语义明确的块。model: ModelRef的落点有现成依据v2 模型层已经定义了对应的引用结构即 model.ts 中的Model.Refexport const Ref Schema.Struct({ id: ID, providerID: Provider.ID, variant: VariantID.pipe(optional), })规格中ModelRef的{ providerID, modelID }风格见方案三示例与该Ref结构语义一致说明模型以引用而非内联对象进入消息是 v2 既定的方向。Assistant的kind: reply | summary区分了普通回复与压缩摘要类消息对应现状中 session-message.ts 里独立存在的Compaction消息类型——方案一将其内化为结果的一个维度。瞬态形态PromptMessagetype PromptMessage { role: user | assistant parts: PromptPart[] }PromptMessage刻意没有 ID、没有时间戳、没有请求元数据——它只在提示词构造管道中存活不进入存储历史。插件钩子的用法因此变得极其简单prompt.push({ role: user, parts: [{ type: text, text: Summarize the tool output above and continue. }], })权衡原文 Tradeoff提示词钩子获得了轻量的消息形态但代价是系统中从此存在两种消息形态类型系统与序列化边界都要为这条分界线负责。方案二提示词修改器Prompt Mutators方案二选择不引入第二种完整消息类型User/Assistant仍是唯一的存储历史模型提示词钩子不直接构造消息而是由运行时提供一组提示词修改器prompt mutators由运行时把修改意图落到正确的消息上。PromptEditor的完整 API 如下继承自 message-shape.mdtype PromptEditor { append(input: { role: user | assistant; parts: PromptPart[] }): void prepend(input: { role: user | assistant; parts: PromptPart[] }): void appendTo(target: last-user | last-assistant, parts: PromptPart[]): void insertAfter(messageID: string, input: { role: user | assistant; parts: PromptPart[] }): void insertBefore(messageID: string, input: { role: user | assistant; parts: PromptPart[] }): void }五个方法覆盖了提示词改写的主要意图空间方法语义典型用途append在末尾追加一条消息在工具执行后追加合成的 user 指令prepend在开头插入一条消息前置全局约束/上下文appendTo向最后一条 user/assistant 消息追加 parts向当前用户输入追加一段固定文本insertAfter在指定消息 ID 之后插入针对历史中特定轮次的定点注入insertBefore在指定消息 ID 之前插入同上方向相反插件钩子示例文档原文的两个用例prompt.append({ role: user, parts: [{ type: text, text: Summarize the tool output above and continue. }], })prompt.appendTo(last-user, [{ type: text, text: BUILD_SWITCH }])第二个示例揭示了该方案的另一个价值点向最后一条用户消息追加 parts 时钩子不需要知道这条消息的 ID、时间戳甚至不需要复制其内容——运行时通过messageID/ 目标选择器把 parts 合并进既有的存储消息彻底回避了伪造 ID 与时间戳的问题。权衡原文 Tradeoff避免了第二种完整消息类型也避免了伪造 id/timestamp但代价是把更多魔法magic挪进了钩子 API——钩子不再操作显式数据而是操作一组有隐式定位语义的操作。方案三独立的 Turn 状态Separate Turn State方案三更进一步把执行设置整体移出User消息放进独立的 turn/request 对象。消息只保留对话内容 归属哪个 turn执行配置作为一等公民单独建模。type Turn { id: string request: { agent: string model: ModelRef variant?: string format?: OutputFormat system?: string tools?: Recordstring, boolean } } type User { role: user turnID: string time: { created: number } } type Assistant { role: assistant turnID: string usage: { cost: number; tokens: Tokens } result: { finish?: string; error?: Error; structured?: unknown; kind: reply | summary } }文档给出的两个构造示例const turn { request: { agent: build, model: { providerID: openai, modelID: gpt-5 }, }, }const msg { role: user, turnID: turn.id, parts: [{ type: text, text: Summarize the tool output above and continue. }], }与方案一相比方案三中User消息被压缩到了极致——role、turnID、time.created三个字段存储体积与序列化成本都显著下降而request块agent/model/variant/format/system/tools完整移入Turn成为可独立查询、独立变更的执行配置。权衡原文 Tradeoff存储消息变得非常小且干净但重放replay时必须把消息与 turn 状态做 join而且提示词钩子仍然需要一个机制来声明我追加的内容归属于哪个 turn。三方案对比与源码佐证把三个方案并置可以得到如下对比痛点列引用文档原文 Tradeoff现状对照列基于当前仓库源码方案核心思想存储消息体积钩子复杂度文档指出的代价现状对照源码证据方案一存储形态 瞬态PromptMessage双形态中request 归组进 User低直接 push 轻量消息系统中存在两种消息形态现状的Synthetic类型即带完整 Base 的注入消息PromptMessage是其去 ID 化版本方案二单一存储形态 PromptEditor修改器中不变中API 有隐式定位语义更多魔法进入钩子 API对应现状中AgentSwitched/ModelSwitched等只改状态不发消息的先例方案三执行配置抽离为Turn消息仅持turnID最小需指定 turn 归属replay 需要 join 消息与 turn 状态现状Assistant顶层平铺的agent/model/cost/tokens正是被抽离的对象从 session-message.ts 的整体结构可以进一步看出规格与实现的衔接关系当前Session.Message联合类型已经包含AgentSwitched、ModelSwitched、User、Synthetic、System、Shell、Assistant、Compaction八种类型说明存储形态正在向按事件/状态细粒度拆分的方向演进规格文档讨论的三个方案是在这一基础上继续回答对话内容、执行配置、提示词改写三者如何分层的问题。v2 API 侧的整体调用形态可参考同目录的 api.tssession.create→session.prompt→session.messages消息形态最终就是session.messages返回的持久化结构。小结这篇 v2 规格的价值不在于给出唯一答案而在于把编码 Agent 会话存储中一条清晰的架构分界线摆了出来持久化历史需要 ID、时间戳、用量、结果供 replay/resume与提示词改写只需要 role parts供当轮提示词构造不应共用同一个消息类型方案一用类型分离解决方案二用API 抽象解决方案三用状态外置解决分别对应类型系统、运行时 API、存储模型三个层面的取舍。对阅读 opencode v2 代码的开发者而言理解这条分界线是读懂 packages/schema/src/session-message.ts 中消息模型演进、以及后续 prompt 钩子 API 设计的前提。【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考