news 2026/9/7 17:38:34

opencode v2 会话消息形态设计:User/Assistant 存储模型、PromptMessage 与 Prompt Mutators 三种方案对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
opencode v2 会话消息形态设计:User/Assistant 存储模型、PromptMessage 与 Prompt Mutators 三种方案对比

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 会话存储的两个根本矛盾:

  1. 存储的消息需要足够的数据,以便日后重放和恢复一个会话(stored messages need enough data to replay and resume a session later);
  2. 提示词钩子(prompt hooks)往往只是想追加一条合成的 user/assistant 消息(prompt hooks often just want to append a synthetic user/assistant message);
  3. 现状是:这就意味着要伪造(fake)ID、时间戳和请求元数据

第 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消息则同时承载了运行上下文(agentmodel: Model.Ref)、结果(finisherror)、用量(costtokens)与快照(snapshot)等大量执行侧数据,使得"纯对话内容"与"执行元数据"混居在一条消息里。

设计文档的目标就是解决这一矛盾:让存储历史更干净,让提示词钩子更轻。以下三个方案按递进关系给出。

方案一:两种消息形态(Two Message Shapes)

方案一的核心思路是:User/Assistant继续作为存储历史的模型,但要把它们"清理干净";同时新增一个独立的、瞬态(transient)的PromptMessage专供提示词改写使用。

存储形态:精简后的 User / Assistant

type User = { role: "user" time: { created: number } request: { agent: string model: ModelRef variant?: string variant?: string format?: OutputFormat system?: string tools?: Record<string, 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之外的重复字段,Userrequest字段实际包含agent / model / variant? / format? / system? / tools?。)

与现状相比可以注意到几个关键变化:

  • 执行设置收敛进request子对象agentmodelvariantformatsystemtools归组为一次请求的完整描述。对照 session-message.ts 中的现状,Assistant目前把agentmodelfinishcosttokenserror全部平铺在消息顶层,而方案一将其重组为run(运行时上下文:agent、model、工作目录路径)与usage/result(用量与结果)两个语义明确的块。

  • model: ModelRef的落点有现成依据:v2 模型层已经定义了对应的引用结构,即 model.ts 中的Model.Ref

    export const Ref = Schema.Struct({ id: ID, providerID: Provider.ID, variant: VariantID.pipe(optional), })

    规格中ModelRef{ providerID, modelID }风格(见方案三示例)与该Ref结构语义一致,说明"模型以引用而非内联对象进入消息"是 v2 既定的方向。

  • Assistantkind: "reply" | "summary"区分了普通回复与压缩摘要类消息,对应现状中 session-message.ts 里独立存在的Compaction消息类型——方案一将其内化为结果的一个维度。

瞬态形态:PromptMessage

type 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.md):

type 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?: Record<string, 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消息被压缩到了极致——roleturnIDtime.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联合类型已经包含AgentSwitchedModelSwitchedUserSyntheticSystemShellAssistantCompaction八种类型,说明"存储形态"正在向按事件/状态细粒度拆分的方向演进;规格文档讨论的三个方案,是在这一基础上继续回答"对话内容、执行配置、提示词改写三者如何分层"的问题。v2 API 侧的整体调用形态可参考同目录的 api.ts(session.createsession.promptsession.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 17:37:27

MacBook终端效率革命:Oh My Zsh安装配置与实用插件全指南

1. 为什么每个 MacBook 用户都该装一套 Oh My Zsh我大概五年前第一次在 MacBook 上敲开终端&#xff0c;那时候还是满屏的 bash 默认提示符&#xff0c;长出一口气都觉得费劲。后来接触了 zsh&#xff0c;再后来装上 Oh My Zsh&#xff0c;整个终端体验直接上了一个台阶。说句实…

作者头像 李华
网站建设 2026/9/7 17:33:52

KUKA机器人工具坐标系标定:XYZ四点示教法详解与实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:32:45

装载机安全驾驶与维护保养全攻略:从操作规范到事故预防

1. 装载机安全驾驶的核心逻辑与事故共性拆解装载机这设备&#xff0c;说简单也简单&#xff0c;一个方向盘、两个操纵杆、几个踏板&#xff0c;学起来两三天就能上手。但要说把它开好、开安全、开得长久不坏&#xff0c;这里面的门道远不是“会开”两个字能概括的。很多工地上的…

作者头像 李华
网站建设 2026/9/7 17:28:42

基于AD9910的DDS波形发生器硬件设计与扫频实现

简介&#xff1a;基于AD9910的波形发生器工程包&#xff0c;面向学习STM32与DDS技术的嵌入式开发者和电子竞赛选手&#xff0c;覆盖1Hz-400MHz正弦波输出、1mV-650mV幅度调节&#xff08;初始化后为500mV&#xff09;、上下限频率与步进可调的扫频模式&#xff0c;以及通过RAM调…

作者头像 李华
网站建设 2026/9/7 17:27:37

Linux sort命令实战详解:参数用法与日志处理技巧

1. sort命令到底在干什么1.1 一句话理解sortsort是Linux下最基础也最强大的文本排序工具。它的核心功能就是把输入的行按照指定规则重新排列&#xff0c;听起来简单&#xff0c;但实际用起来门道不少。我这些年处理日志、统计访问量、清理重复数据&#xff0c;几乎每次都离不开…

作者头像 李华