0%

跟着OpenCode学智能体设计和开发3:会话管理系统

会话生命周期:创建、压缩与持久化

OpenCode 中的会话生命周期负责编排用户与 AI Agent 之间的对话上下文的创建、维护和演进。这个综合系统通过层级关系管理会话初始化,实施智能压缩以保持上下文效率,并提供带有自动迁移功能的强大持久化机制。

会话创建与层级结构

会话创建会建立一个带有元数据跟踪、可选父子关系和可配置自动共享的对话上下文。该系统支持独立会话以及从现有对话派生出的分支会话。

核心创建过程通过 Session.create() 进行,该方法会委托给 Session.createNext()

来源:packages/opencode/src/session/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export async function createNext(input: {
id?: string
title?: string
parentID?: string
directory: string
permission?: PermissionNext.Ruleset
}) {
const result: Info = {
id: Identifier.descending("session", input.id),
version: Installation.VERSION,
projectID: Instance.project.id,
directory: input.directory,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
permission: input.permission,
time: {
created: Date.now(),
updated: Date.now(),
},
}
}

会话会被分配唯一的降序标识符,并自动接收时间戳。标题生成功能区分父会话 (“New session - “) 和子会话 (“Child session - “),并附上 ISO 时间戳后缀。

来源:packages/opencode/src/session/index.ts

创建后,会话通过存储层进行持久化,并发布到事件总线以进行实时更新:

来源:packages/opencode/src/session/index.ts

当通过 Flag.OPENCODE_AUTO_SHAREConfig.share === "auto" 进行配置时,父会话会自动启动自动共享:

来源:packages/opencode/src/session/index.ts

会话分支

Session.fork() 函数通过将现有会话中的消息和部分克隆到指定的消息边界来创建子会话。这使得能够在保留共享上下文的同时实现对话分支:

来源:packages/opencode/src/session/index.ts

分支通过将原始消息 ID 映射到新的升序标识符来维护消息谱系,确保部分在克隆的会话中引用正确的父消息。

会话信息模式

每个会话维护一个全面的 Info 结构:

来源:packages/opencode/src/session/index.ts

字段 类型 描述
id string 唯一会话标识符
projectID string 关联的项目标识符
directory string 工作目录路径
parentID string 分支会话的可选父会话
summary object 聚合统计信息(新增、删除、文件、差异)
share object 可选的共享 URL 和密钥
title string 会话显示标题
version string OpenCode 安装版本
time object 时间戳(创建、更新、压缩、归档)
permission object 会话的权限规则集
revert object 回滚状态(messageID、partID、快照、差异)

存储架构

持久化层使用基于文件的 JSON 存储系统,具有项目级隔离和自动迁移支持。所有会话数据都驻留在全局数据目录下的有组织的子目录中。

存储操作

Storage 命名空间提供带有文件锁定和自动错误处理的原子读/写/更新操作:

来源:packages/opencode/src/storage/storage.ts

1
2
3
4
5
6
7
8
export async function write<T>(key: string[], content: T) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write(target)
await Bun.write(target, JSON.stringify(content, null, 2))
})
}

存储键遵循层级模式:

  • 会话:["session", projectID, sessionID]
  • 消息:["message", sessionID, messageID]
  • 部分:["part", messageID, partID]
  • 会话差异:["session_diff", sessionID]
  • 共享信息:["share", sessionID]

文件锁定

所有写操作通过 Lock.write() 获取排他锁,而读操作通过 Lock.read() 使用共享锁。这可以防止并发修改,并确保多个 OpenCode 进程之间的数据一致性。

来源:packages/opencode/src/storage/storage.ts

迁移系统

存储层通过版本化函数实现自动迁移:

来源:packages/opencode/src/storage/storage.ts

迁移处理遗留数据格式转换,包括:

  • 项目 ID 从基于目录到基于 Git commit 的标识迁移
  • 用于差异摘要分离的会话结构更新
  • 消息和部分位置重组

迁移状态保存在 migration 文件中,跟踪已应用的迁移。系统仅在初始化时运行待处理的迁移。

会话压缩

压缩通过移除过期的工具输出并生成摘要提示词以继续对话,来管理上下文窗口限制。这可以防止上下文溢出,同时为正在进行的工作保留必要信息。

溢出检测

SessionCompaction.isOverflow() 函数评估当前 Token 使用量是否超过可用上下文窗口:

来源:packages/opencode/src/session/compaction.ts

1
2
3
4
5
6
7
8
9
10
export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
const config = await Config.get()
if (config.compaction?.auto === false) return false
const context = input.model.limit.context
if (context === 0) return false
const count = input.tokens.input + input.tokens.cache.read + input.tokens.output
const output = Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) || SessionPrompt.OUTPUT_TOKEN_MAX
const usable = context - output
return count > usable
}

该计算为最大输出 Token 预留空间,并与模型的上下文限制进行比较。可以通过 config.compaction.autoFlag.OPENCODE_DISABLE_AUTOCOMPACT 标志禁用自动压缩。

来源:packages/opencode/src/config/config.ts

工具输出修剪

SessionCompaction.prune() 函数移除旧的工具结果,同时保护最近的上下文:

来源:packages/opencode/src/session/compaction.ts

阈值 Tokens 用途
PRUNE_MINIMUM 20,000 尝试修剪的最小 Token 数
PRUNE_PROTECT 40,000 最近上下文的保护区

修剪按相反的时间顺序遍历消息,跳过:

  1. 最后两次对话轮次
  2. 来自受保护工具类型(目前为 “skill”)的工具输出
  3. 已压缩的输出
  4. 摘要标记之后的消息

当工具输出被修剪时,会设置其 time.compacted 时间戳,将其标记为从未来的上下文窗口中排除。

被修剪的工具输出仍持久存储在存储中,并设置了 time.compacted,允许在需要时进行恢复。压缩提示词会明确提到工具输出已被清除。

压缩处理

SessionCompaction.process() 函数在需要压缩时生成继续对话的提示词:

来源:packages/opencode/src/session/compaction.ts

该过程:

  1. 检索触发溢出的用户消息
  2. 以 “compaction” 模式创建 Assistant 消息
  3. 使用默认提示词调用 LLM:“Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we’re doing, which files we’re working on, and what we’re going to do next considering new session will not have access to our conversation.”
  4. 如果配置了,可选择插入合成的“如果你有下一步,请继续”用户消息
  5. 发布 SessionCompaction.Event.Compacted 事件

插件可以通过 experimental.session.compacting 钩子注入自定义上下文或替换压缩提示词。

来源:packages/opencode/src/session/compaction.ts

SessionProcessor 集成

会话处理器将压缩检查集成到主处理循环中:

来源:packages/opencode/src/session/processor.ts

1
2
3
if (await SessionCompaction.isOverflow({ tokens: usage.tokens, model: input.model })) {
needsCompaction = true
}

当设置 needsCompaction 时,处理器在完成当前步骤后返回 “compact”,在继续之前触发压缩工作流。

来源:packages/opencode/src/session/processor.ts

消息生命周期和持久化

消息及其组成部分存储为单独的 JSON 文件,从而实现对话历史的高效加载和流式传输。

消息结构

消息遵循 V2 格式,并针对用户和 Assistant 角色使用可区分联合:

来源:packages/opencode/src/session/message-v2.ts

用户消息包含

  • 角色:”user”
  • 创建时间
  • 可选摘要(标题、正文、差异)
  • Agent 标识符
  • 模型选择(providerID、modelID)
  • 可选的系统提示词覆盖
  • 可选的已启用工具映射
  • 可选的变体标识符

Assistant 消息包含

  • 角色:”assistant”
  • 创建和完成时间(可选)
  • 可选的错误信息
  • 父消息 ID
  • 模型和提供者标识符
  • Agent 标识符
  • 工作路径(cwd、root)
  • 摘要标记
  • 总成本和 Token 使用量
  • 可选的完成原因

消息部分

每条消息包含多个代表不同内容类型的部分:

来源:packages/opencode/src/session/message-v2.ts

部分类型 用途
TextPart 用户或 Assistant 文本内容
ReasoningPart LLM 推理链
ToolPart 带有状态的工具调用记录
FilePart 带有元数据的文件附件
SnapshotPart 项目快照引用
PatchPart 文件修改跟踪
AgentPart Agent 特定信息
SubtaskPart 子任务委托记录
RetryPart 重试尝试信息
CompactionPart 压缩触发标记
StepStartPart 步骤执行开始标记
StepFinishPart 带有指标的步骤完成

工具部分维护状态转换:pending → running → completederror。完成的工具部分跟踪计时信息和可选的压缩时间戳。

来源:packages/opencode/src/session/message-v2.ts

消息流式传输

MessageV2.stream() 函数提供一个异步生成器,用于按时间顺序读取消息:

来源:packages/opencode/src/session/message-v2.ts

1
2
3
4
5
6
7
8
9
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
for (let i = list.length - 1; i >= 0; i--) {
yield await get({
sessionID,
messageID: list[i][2],
})
}
})

该函数检索按存储字母顺序排序的消息 ID,然后以相反顺序生成以产生时间序列。

FilterCompacted

MessageV2.filterCompacted() 函数过滤消息以排除最后一个压缩点之前的历史记录:

来源:packages/opencode/src/session/message-v2.ts

这通过查找带有 summary: trueAssistant 消息和随后包含 CompactionPart 的用户消息来识别边界。

会话删除和清理

会话删除会级联处理所有相关数据,包括子会话、共享、消息和部分:

来源:packages/opencode/src/session/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
const project = Instance.project
try {
const session = await get(sessionID)
for (const child of await children(sessionID)) {
await remove(child.id)
}
await unshare(sessionID).catch(() => {})
for (const msg of await Storage.list(["message", sessionID])) {
for (const part of await Storage.list(["part", msg.at(-1)!])) {
await Storage.remove(part)
}
await Storage.remove(msg)
}
await Storage.remove(["session", project.id, sessionID])
Bus.publish(Event.Deleted, {
info: session,
})
} catch (e) {
log.error(e)
}
})

删除过程:

  1. 递归删除所有子会话
  2. 删除共享元数据(静默忽略错误)
  3. 遍历所有消息及其部分
  4. 删除消息和部分 JSON 文件
  5. 删除会话元数据文件
  6. 发布删除事件

配置和控制

会话生命周期行为可通过多个层进行配置:

配置优先级

设置按优先级递增的顺序从多个来源合并:

来源:packages/opencode/src/config/config.ts

  1. 远程/知名配置(组织默认值)
  2. 全局用户配置 (~/.opencode/opencode.json)
  3. 自定义配置路径 (Flag.OPENCODE_CONFIG)
  4. 项目配置(从目录向上搜索的 opencode.json 或 opencode.jsonc)
  5. 内联配置内容 (Flag.OPENCODE_CONFIG_CONTENT)

压缩设置

压缩行为的配置选项:

设置 类型 默认值 描述
compaction.auto boolean true 在溢出时启用自动压缩
compaction.prune boolean true 启用工具输出修剪
share string/boolean - 自动共享配置(”auto”、”disabled”或 true 以保持旧版兼容性)

标志覆盖配置:

  • Flag.OPENCODE_DISABLE_AUTOCOMPACT:禁用自动压缩
  • Flag.OPENCODE_DISABLE_PRUNE:禁用工具输出修剪
  • Flag.OPENCODE_AUTO_SHARE:强制新会话自动共享

来源:packages/opencode/src/config/config.ts

事件总线集成

会话生命周期事件发布到全局事件总线,以实现实时 UI 更新和插件集成:

来源:packages/opencode/src/session/index.ts

事件 负载 触发器
session.created { info: Session.Info } 创建新会话
session.updated { info: Session.Info } 修改会话元数据
session.deleted { info: Session.Info } 删除会话
session.diff { sessionID, diff: Snapshot.FileDiff[] } 计算文件差异
session.compacted { sessionID } 压缩完成
session.error { sessionID?, error } 发生错误

消息和部分事件:

  • message.updated:消息元数据已更改
  • message.removed:消息已删除
  • message.part.updated:部分已添加或修改(带有可选增量)
  • message.part.removed:部分已删除

来源:packages/opencode/src/session/message-v2.ts


总结

  1. 会话生命周期核心是层级化创建、智能压缩、持久化存储三大模块,支撑对话上下文的高效管理。
  2. 存储采用基于文件的 JSON 格式,配合文件锁保证数据一致性,自动迁移处理遗留格式。
  3. 会话压缩通过溢出检测、工具输出修剪、摘要生成,在防止上下文溢出的同时保留关键工作信息。
  4. 消息以 V2 格式存储,通过多部分结构支持丰富内容类型,流式传输实现高效历史加载。
  5. 配置支持多来源优先级合并,事件总线提供实时集成能力,删除操作实现级联清理保证数据整洁。

消息处理:V2 消息格式与处理机制

V2 消息格式是 OpenCode 管理对话状态、工具交互和 Agent 响应的核心架构演进,它提供了一个精细、类型安全的多部分消息表示体系,具备流式传输、持久化存储和复杂错误处理的完整能力,为对话系统的高效运行奠定了基础。

一、V2 消息架构概述

V2 消息格式采用基于部分的架构,每条消息由「标准化元数据」和「有序的类型化部分集合」组成。这种设计实现了不同内容类型的独立实时流式传输,支持丰富的工具交互,同时能对执行状态进行精确跟踪。

核心类型定义在 packages/opencode/src/session/message-v2.ts 中,通过可辨识联合实现编译时类型安全,并借助 Zod schemas 完成运行时数据验证,兼顾开发效率和运行稳定性。

来源:packages/opencode/src/session/message-v2.ts

二、消息元数据结构

V2 格式中每条消息都携带标准化元数据,用于跟踪执行上下文、计时信息和资源利用率,且针对「用户消息」和「Assistant 消息」设计了差异化的元数据结构。

1. 用户消息元数据

用户消息捕获每个对话轮次的启动上下文,核心字段包括:

  • 角色标识:固定为 "user",用于区分消息发起方
  • 时间跟踪:创建时间戳,用于消息排序和对话分析
  • Agent 分配:指定处理该消息的目标 Agent
  • 模型配置:LLM 提供者(如 anthropic)和模型 ID(如 claude-sonnet-4-20250514),支持可选变体
  • 系统指令:针对该特定轮次的可选系统级提示词覆盖
  • 工具权限:精细的工具启用/禁用标志,控制 Agent 可调用的工具范围
  • 摘要集成:可选的差异数据和正文内容,用于保证上下文连续性

来源:packages/opencode/src/session/message-v2.ts

2. Assistant 消息元数据

Assistant 消息跟踪执行结果和资源消耗,核心字段包括:

  • 父引用:链接到对应的响应用户消息,维护对话上下文关联
  • 模型信息:实际用于生成响应的 LLM 提供者和模型(可能与用户配置有差异)
  • Agent 身份:确认实际处理请求的 Agent 实例
  • 路径上下文:当前工作目录(cwd)和项目根目录,记录执行环境
  • 资源指标:Token 计数、缓存使用量、推理 Token 消耗,用于成本和性能统计
  • 成本跟踪:响应生成的累计成本
  • 错误状态:包含重试元数据的全面错误信息,支持故障排查
  • 完成状态:响应完成时的时间戳
  • 结束原因:LLM 提供者停止生成响应的原因(如正常完成、Token 耗尽)

来源:packages/opencode/src/session/message-v2.ts

三、消息部分系统

部分系统是 V2 消息格式的核心,为表示不同类型的内容和操作提供了丰富、可扩展的词汇表。每个部分包含基本标识符和类型特定数据,主要分为三大类:内容部分、工具执行部分、控制和元数据部分。

1. 内容部分

用于承载对话中的核心内容数据,支持流式传输和元数据跟踪。

  • TextPart:表示叙述性内容(用户输入、Agent 最终回复)

    1
    2
    3
    4
    5
    6
    7
    8
    {
    type: "text",
    text: string,
    synthetic?: boolean, // 标记是否为系统生成(非用户手动提供)
    ignored?: boolean, // 标记是否从 LLM 上下文中排除
    time?: { start: number, end?: number }, // 流式传输计时
    metadata?: Record<string, any>
    }
  • ReasoningPart:捕获 LLM 的推理链,仅用于具备思考能力的模型,方便调试和理解 Agent 决策过程

    1
    2
    3
    4
    5
    6
    {
    type: "reasoning",
    text: string,
    time: { start: number, end?: number },
    metadata?: Record<string, any>
    }
  • FilePart:封装二进制和多文件内容,支持 MIME 类型分类,关联文件源信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    type: "file",
    mime: string,
    filename?: string,
    url: string,
    source?: {
    type: "file" | "symbol" | "resource",
    path?: string,
    text: { value: string, start: number, end: number },
    range?: LSP.Range, // 针对符号源的位置信息
    name?: string, // 针对符号源的名称
    kind?: number, // LSP 符号类型
    clientName?: string, // 针对 MCP 资源
    uri?: string // 针对 MCP 资源
    }
    }

来源:packages/opencode/src/session/message-v2.ts

2. 工具执行部分(ToolPart)

跟踪完整的工具执行生命周期,维护 LLM 工具调用 ID 与内部部分 ID 的双向映射,支持四种状态流转,是 Agent 与外部工具交互的核心载体。

状态 目的 关键字段
pending 工具已调用,正在解析输入 input, raw
running 工具正在执行中 input, time.start, metadata
completed 工具成功完成 output, title, attachments, time
error 工具执行失败 error, metadata

核心结构定义:

1
2
3
4
5
6
7
{
type: "tool",
callID: string, // LLM 的工具调用标识符
tool: string, // 工具名称(如 bash、read)
state: ToolState, // 四种生命周期状态之一
metadata?: Record<string, any>
}

处理器在消息生成期间,会在运行时映射中跟踪活动的工具调用,实现流式更新与最终存储的同步。

来源:packages/opencode/src/session/message-v2.ts

3. 控制和元数据部分

用于标记对话流程中的关键节点、记录系统状态,支撑压缩、回滚、分步执行等高级功能:

  • StepStartPart/StepFinishPart:标记 Assistant 消息中的离散推理步骤,实现步骤间快照跟踪
  • SnapshotPart:记录特定时间点的文件系统状态,支持对话回滚和文件差异跟踪
  • PatchPart:表示一组文件修改,基于哈希实现版本控制
  • CompactionPart:标记由上下文压缩产生的消息,区分原始对话和压缩对话
  • SubtaskPart:表示委托给其他 Agent 或工具系统的子工作任务
  • RetryPart:记录失败操作的重试尝试,包含详细错误信息

来源:packages/opencode/src/session/message-v2.ts

四、存储架构

V2 消息实现分层存储模型,将消息元数据与部分数据分离存储,针对不同访问模式进行优化,提升数据读写效率。

1. 存储层次结构

1
2
3
4
storage/
├── session/{projectID}/{sessionID}.json # 会话元数据
├── message/{sessionID}/{messageID}.json # 仅消息元数据(无部分内容)
└── part/{messageID}/{partID}.json # 单个消息部分的独立数据

这种分离存储带来四大优势:

  1. 高效流式传输:部分可以独立写入和读取,无需等待整个消息完成
  2. 定向查询:访问消息元数据时无需加载所有部分,提升查询速度
  3. 选择性更新:修改单个部分仅需重写对应文件,无需重写整个消息
  4. 空间效率:删除部分仅需删除单个文件,清理更轻便,无冗余数据

来源:packages/opencode/src/storage/storage.ts

2. 消息检索模式

存储系统支持两种核心检索模式:流式传输所有消息、定向检索特定消息的所有部分,且保证返回结果的有序性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模式1:按逆时间顺序流式传输某个会话的所有消息
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
for (let i = list.length - 1; i >= 0; i--) {
yield await get({
sessionID,
messageID: list[i][2],
})
}
})

// 模式2:检索特定消息的所有部分,并按创建顺序排序
export const parts = fn(Identifier.schema("message"), async (messageID) => {
const result = [] as MessageV2.Part[]
for (const item of await Storage.list(["part", messageID])) {
const read = await Storage.read<MessageV2.Part>(item)
result.push(read)
}
result.sort((a, b) => (a.id > b.id ? 1 : -1))
return result
})

来源:packages/opencode/src/session/message-v2.ts

五、消息处理流水线

处理器负责协调 Assistant 消息生成的完整生命周期,处理来自 LLM 提供者的流式响应,管理工具执行,同时实现异常检测和容错。

1. 流式事件处理

处理器为每个 LLM 流式事件配置专用处理程序,实现「事件触发-业务处理-存储更新」的闭环,保证消息状态与执行进度同步。

事件类型 处理程序操作 存储操作
start 将会话状态设置为忙碌
reasoning-start 创建推理部分 写入新部分
reasoning-delta 追加到推理文本 使用增量更新部分
reasoning-end 完成推理元数据 更新部分
tool-input-start 创建待处理工具部分 写入新部分
tool-call 转换工具状态为运行中 更新部分状态
tool-result 标记工具状态为已完成 使用输出更新部分
tool-error 标记工具状态为错误 使用错误更新部分
text-start 创建文本部分 写入新部分
text-delta 追加文本内容 使用增量更新部分
text-end 完成文本元数据 更新部分

来源:packages/opencode/src/session/processor.ts

2. Doom Loop 检测

处理器实现复杂的重复工具调用检测系统,防止无限循环(Doom Loop),提升系统稳定性。

核心逻辑:当同一工具使用完全相同的输入被连续调用 3 次(DOOM_LOOP_THRESHOLD = 3)时,系统会请求用户许可后再继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)

if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
(p) =>
p.type === "tool" &&
p.tool === value.toolName &&
p.state.status !== "pending" &&
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await PermissionNext.ask({
permission: "doom_loop",
patterns: [value.toolName],
sessionID: input.assistantMessage.sessionID,
metadata: { tool: value.toolName, input: value.input },
always: [value.toolName],
ruleset: agent.permission,
})
}

来源:packages/opencode/src/session/processor.ts

3. 模型消息转换

toModelMessage 函数将 V2 消息转换为 LLM 提供者期望的格式,同时应用一系列过滤和转换规则,保证 LLM 输入的有效性和简洁性:

  • 过滤空消息(无部分的消息)
  • 排除标记为 ignored: true 的文本部分
  • 转换文件类型(纯文本/目录转为文本部分,其他 MIME 类型保留为文件部分)
  • 注入压缩提示词、子任务提示词
  • 处理工具附件和错误消息

来源:packages/opencode/src/session/message-v2.ts

六、错误处理系统

V2 消息包含全面的错误分类和标准化处理逻辑,带有结构化元数据,方便调试和重试逻辑实现。

1. 核心错误类型

错误类型 用例 核心元数据
OutputLengthError 响应超过 Token 限制
AbortedError 用户中止生成响应 message
AuthError 提供者身份验证失败 providerID, message
APIError 提供者 API 故障 message, statusCode, isRetryable
Unknown 捕获所有其他错误 message

2. 错误规范化转换

fromError 函数将不同来源的错误规范化为 V2 错误格式,实现跨 LLM 提供者的一致错误处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function fromError(e: unknown, ctx: { providerID: string }) {
switch (true) {
case e instanceof DOMException && e.name === "AbortError":
return new MessageV2.AbortedError({ message: e.message }, { cause: e }).toObject()

case LoadAPIKeyError.isInstance(e):
return new MessageV2.AuthError({
providerID: ctx.providerID,
message: e.message,
}, { cause: e }).toObject()

case APICallError.isInstance(e):
const message = extractErrorMessage(e)
return new MessageV2.APIError({
message,
statusCode: e.statusCode,
isRetryable: e.isRetryable,
}, { cause: e }).toObject()

default:
return new NamedError.Unknown({ message: String(e) }, { cause: e }).toObject()
}
}

来源:packages/opencode/src/session/message-v2.ts

七、事件系统集成

消息更新会发布到全局事件总线,实现实时 UI 更新和跨组件通信,提升用户体验和系统可扩展性。

核心事件类型:

  1. Message.Updated:消息元数据更改时发布
  2. Message.Removed:删除消息时发布
  3. Message.PartUpdated:修改部分时发布(包含增量数据,提升流式更新效率)
  4. Message.PartRemoved:删除部分时发布

其中 PartUpdated 中的 delta 字段仅发送增量更改,而非完整部分内容,大幅减少数据传输开销。

来源:packages/opencode/src/session/message-v2.ts

八、压缩集成

V2 消息与上下文压缩系统无缝集成,用于管理长对话中的 Token 限制,防止上下文溢出。

  1. 溢出检测:压缩系统在每次 Assistant 响应后监控 Token 使用量,计算可用上下文空间,判断是否需要压缩
  2. 压缩过程:创建标记为 summary: true 的特殊 Assistant 消息,通过压缩 Agent 处理对话历史,生成精简摘要,同时注入压缩标记区分原始内容和压缩内容
  3. 工具结果修剪:对旧消息的工具输出标记 time.compacted 时间戳,用占位符替换大输出,节省 Token 同时保留执行记录

来源:packages/opencode/src/session/compaction.ts

九、V2 与 V1 格式核心对比

V2 格式相对于 V1 格式实现了重大架构改进,核心差异如下:

方面 V1 格式 V2 格式
结构 消息元数据中的平铺部分数组 分离的消息信息和部分存储
部分类型 仅限于文本、工具、推理 11+ 种部分类型,带有丰富元数据
存储 每条消息单个 JSON 文件 分层存储(消息 + N 个独立部分文件)
流式传输 有限的增量支持 精细的每部分独立流式传输
工具跟踪 简单状态机 四状态生命周期 + 完整元数据跟踪
错误处理 基本错误字符串 结构化错误 + 重试元数据
压缩支持 仅消息级别压缩 部分级别修剪 + 消息级摘要压缩

来源:packages/opencode/src/session/message.ts, packages/opencode/src/session/message-v2.ts

十、核心使用模式

1. 创建用户消息

1
2
3
4
5
6
7
8
9
const userMessage = await Session.updateMessage({
id: Identifier.ascending("message"),
role: "user",
sessionID,
time: { created: Date.now() },
agent: "builder",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
tools: { bash: true, edit: true, read: true },
})

2. 流式传输文本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let currentText: MessageV2.TextPart | undefined

// 文本开始事件
currentText = {
id: Identifier.ascending("part"),
messageID,
sessionID,
type: "text",
text: "",
time: { start: Date.now() },
}

// 文本增量事件
currentText.text += delta
await Session.updatePart({ part: currentText, delta })

// 文本结束事件
currentText.text = currentText.text.trimEnd()
await Session.updatePart(currentText)

3. 处理工具执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接收 LLM 工具调用,创建运行中的工具部分
const toolPart = await Session.updatePart({
id: Identifier.ascending("part"),
messageID,
sessionID,
type: "tool",
callID: toolCallId,
tool: toolName,
state: { status: "running", input, time: { start: Date.now() } },
})

// 工具执行完成,更新为已完成状态
await Session.updatePart({
...toolPart,
state: {
status: "completed",
input,
output,
title,
metadata,
time: { start: toolPart.state.time.start, end: Date.now() },
attachments,
},
})

总结

  1. V2 消息格式的核心是「元数据+多类型部分」的分层架构,兼顾类型安全和可扩展性。
  2. 分离存储和增量流式传输是 V2 格式的性能核心,大幅提升长对话和复杂工具交互的效率。
  3. 完整的生命周期跟踪(工具执行、步骤流转、错误处理)和压缩集成,让 V2 格式能够支撑复杂的 Agent 对话场景。
  4. 标准化的错误处理和事件系统,为系统的可维护性和跨组件集成提供了保障。

上下文管理:Token 限制与截断策略

有效的上下文管理是 OpenCode 保障对话在模型 Token 限制内运行、同时保留关键信息的核心能力。该系统通过Token 估算与限制、工具输出截断、会话压缩、主动修剪四大核心策略,实现了对长对话和大输出场景的高效管控。

一、Token 估算与限制

由于不同 LLM 提供商的 Token 化规则存在差异,且无法通过 SDK 直接获取精确 Token 数,OpenCode 采用基于字符的近似估算方法

核心换算标准:4 个字符 ≈ 1 个 Token

1. Token 分类跟踪

每条消息会在以下 5 个类别中,跟踪详细的 Token 使用情况,确保对上下文消耗的精细化管控:

Token 类别 含义
Input tokens 发送给模型的 Token,包含提示词、历史消息、工具输入
Output tokens 模型生成的 Token,包含响应文本、推理内容
Reasoning tokens 模型用于扩展思考的 Token(仅支持该能力的模型有效)
Cache read/write tokens 来自提示词缓存读写操作的 Token
Total context 上下文总 Token 数 = Input + Cache.read + Output

2. 超限处理

当消息 Token 总数超过模型上下文限制时,系统会触发 MessageV2.OutputLengthError 错误,同时自动启动截断压缩流程,避免对话中断。

二、工具输出截断

当工具生成大量输出(如读取大文件、批量命令执行结果)时,OpenCode 会自动触发截断机制,在保留关键上下文的同时,防止单次工具输出占用过多 Token。

1. 截断限制参数

截断行为由两个核心参数控制,且支持自定义配置:

参数 默认值 作用
最大行数 2,000 行 限制工具输出的最大行数
最大字节数 50 KB 限制工具输出的最大体积
截断方向 支持 head(从开头截断)/tail(从末尾截断) 灵活适配不同场景的内容保留需求

2. 截断完整流程

  1. 工具执行产生大体积输出,触发截断条件;
  2. 系统将完整输出内容持久化到磁盘的 tool-output 目录,保留期限为 7 天
  3. 对返回给 Agent 的内容进行截断,并附加引导性提示,分两种情况:
    • 有 Task 工具权限使用 Task 工具让子 agent 使用 Grep 和 Read 处理此文件。不要自己读取完整文件——委托处理以保存上下文。
    • 无 Task 工具权限使用 Grep 搜索完整内容,或使用带 offset/limit 的 Read 查看特定部分。

这种分层提示的设计,既避免了当前 Agent 上下文溢出,又提供了获取完整内容的可行路径。

三、会话压缩策略

会话压缩针对长时间对话的上下文膨胀问题,采用 溢出检测+自动压缩+主动修剪 三种互补策略,确保对话始终运行在模型限制内。

1. 溢出检测

在每次 Assistant 生成响应前,系统会通过 SessionCompaction.isOverflow 函数,判断当前对话是否超出可用上下文容量,核心计算逻辑如下:

1
2
3
4
5
const context = model.limit.context  // 模型总上下文窗口大小
const output = model.limit.output || OUTPUT_TOKEN_MAX // 预留响应所需 Token
const usable = context - output // 实际可用于输入的 Token 空间
const total = tokens.input + tokens.cache.read + tokens.output // 当前已用 Token 总数
const isOverflow = total > usable // 是否溢出

其中 OUTPUT_TOKEN_MAX 默认为 32,000 Token,可通过 OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX 标志修改。

2. 自动压缩

当检测到上下文溢出时,系统会自动创建压缩任务,通过专门的「压缩 Agent」处理对话历史,核心步骤如下:

  1. 在消息流中插入 CompactionPart,标记压缩触发点;
  2. 压缩 Agent 接收专用提示词,分析对话历史:

    提供一个详细的提示词,以继续我们上面的对话。重点关注对继续对话有帮助的信息,包括我们做了什么、我们正在做什么、我们正在处理哪些文件,以及考虑到新会话无法访问我们的对话,我们下一步打算做什么。

  3. 生成对话核心内容的简洁摘要,识别关键上下文(编辑中的文件、当前任务、后续步骤);
  4. 清除上一次压缩点之前的历史消息,仅保留摘要和最新对话;
  5. 对话从压缩后的摘要点恢复,实现上下文「重置」但关键信息不丢失。

3. 主动修剪旧工具输出

主动修剪是在不触发全量压缩的情况下,减少 Token 消耗的轻量化策略,由 SessionCompaction.prune 函数执行,核心规则如下:

修剪规则 具体说明
保护区域 最近 40,000 Token 的工具输出不会被修剪,确保当前工作的上下文完整
最小阈值 只有修剪操作能回收至少 20,000 Token 时,才会执行修剪,避免无效操作
受保护工具 特定工具(如 "skill" 类工具)的输出免于修剪
修剪范围 仅处理至少 2 轮对话之前的、状态为「已完成」的工具调用输出

修剪执行逻辑

  1. 从对话末尾反向遍历消息列表;
  2. 跳过受保护区域内的消息、受保护工具的输出、已压缩的内容;
  3. 对符合条件的工具输出,设置 compacted 时间戳(不删除原始内容);
  4. 在将消息转换为模型输入时,被修剪的工具输出会被替换为占位文本:[Old tool result content cleared]

注意:修剪的 Token 计算基于「4 字符≈1 Token」的近似值,实际回收量可能存在小幅偏差。

四、配置与控制

上下文管理的行为可以通过配置文件系统标志灵活自定义,且标志的优先级高于配置文件。

1. 配置文件设置(opencode.json)

设置项 类型 默认值 描述
compaction.auto boolean true 上下文溢出时,是否启用自动压缩
compaction.prune boolean true 是否启用旧工具输出的主动修剪

配置示例

1
2
3
4
5
6
{
"compaction": {
"auto": true,
"prune": true
}
}

2. 系统标志覆盖

标志 作用
OPENCODE_DISABLE_AUTOCOMPACT 强制禁用自动压缩,忽略 compaction.auto 配置
OPENCODE_DISABLE_PRUNE 强制禁用工具输出修剪,忽略 compaction.prune 配置
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX 自定义最大输出 Token 限制(默认 32,000)

五、与会话处理的集成

上下文管理功能深度嵌入 SessionPrompt.loop 的会话处理主循环,实现自动化、无感知的上下文管控:

  1. 每次 Assistant 响应完成后:处理器自动检查当前 Token 使用量是否溢出;
  2. 溢出触发后:立即创建压缩任务,在下一次循环迭代中优先执行压缩;
  3. 对话完成后:自动运行修剪流程,清理无效的旧工具输出;
  4. 手动触发方式:可通过 session_compact 快捷键,或让处理器返回 "compact" 指令,手动启动压缩。

这种集成方式既保证了对话的连续性,又最大限度减少了对用户工作流的干扰。

总结

OpenCode 上下文管理的核心优势在于 分层治理、智能决策

  1. 字符近似法解决了跨平台 Token 估算的一致性问题;
  2. 工具输出截断处理单次大输出,用会话压缩+主动修剪解决长对话膨胀,形成互补策略;
  3. 压缩和修剪过程保留完整原始数据,仅在模型输入层做精简,兼顾 Token 效率和数据可追溯性;
  4. 支持配置+标志的双层自定义,适配不同场景的需求。

会话回退与回滚机制

OpenCode 提供的精密版本回退和回滚系统,支持撤销对话进度、恢复文件系统状态,既能实现错误恢复与实验性操作,又能保留探索替代方案的能力,其核心是两阶段运作模型Git 快照驱动的文件恢复机制

一、回退架构核心设计

回退系统采用三阶段生命周期的设计,允许用户先尝试回退操作,再决定是确认提交还是撤销回退,兼顾灵活性与安全性。

  1. 回退阶段:标记回滚点,恢复文件系统到目标状态,此时会话消息仍保留,操作可逆
  2. 取消回退阶段:放弃回退操作,将文件系统和会话状态恢复到回退前的样子
  3. 清理阶段:永久删除回退点之后的消息和部分,完成回退操作,状态不可逆

这种设计的核心优势是:回退操作不立即删除数据,而是通过标记实现状态管理,给用户留出决策缓冲期。

二、回退点指定规则

启动回退时,需要在对话链中指定目标点,系统支持两种指定方式,会智能识别并定位实际回退节点:

指定方式 作用范围 系统行为
仅指定 Message ID 整条目标消息 1. 若目标是助手消息:回退到该消息之前的最后一条用户消息
2. 若目标是用户消息:直接回退到该用户消息节点
最终确保回退到可输入新指令的用户节点
Message ID + Part ID 目标消息内的特定部分 回退到该部分的位置,且保留同一消息中该部分之前的有效内容(文本/工具类型部分)

系统通过分析对话链的父子关系和消息类型,自动校准回退点,保证回退后的会话处于可继续操作的状态。
来源:revert.ts

三、回滚状态模型

触发回退时,系统会在会话元数据中记录完整的回滚状态信息,用于跨操作跟踪和状态恢复,核心结构如下:

1
2
3
4
5
6
revert: {
messageID: string, // 目标回退消息的 ID(必需)
partID?: string, // 可选:目标部分 ID,用于细粒度回退
snapshot?: string, // 文件系统状态的 Git 树哈希,用于恢复文件
diff?: string // 当前状态与快照状态的差异记录
}

该元数据存储在会话的 Info 结构中,是实现回退、取消回退、清理操作的核心依据。
来源:index.ts

四、文件系统恢复机制

回退系统的文件恢复能力基于 Git 内部操作实现,通过快照跟踪和补丁回退,确保文件系统精准恢复到目标状态,且不影响无关文件。

1. 快照跟踪原理

在执行文件回退前,系统会创建 Git 树快照来捕获当前文件系统状态,快照与项目主 Git 仓库完全隔离,存储在独立工作树目录中。

快照创建流程

  1. 检查受管位置是否已初始化 Git 仓库,未初始化则自动初始化
  2. 将当前项目的所有文件添加到 Git 索引
  3. 创建代表当前文件系统状态的 Git 树对象
  4. 返回树哈希值,存储到回滚状态的 snapshot 字段中

快照的核心作用是:保存文件系统的基准状态,为后续的恢复或取消回退提供数据支撑。
来源:snapshot/index.ts

2. 补丁收集与文件回退

系统在回退过程中,会识别所有由 edit/write/multiedit 等工具执行的补丁操作(即文件修改记录),并针对性地恢复文件,具体处理逻辑分两种场景:

文件场景 执行的 Git 命令 最终结果
文件存在于快照中 git checkout <快照哈希> -- <文件路径> 将文件内容精准恢复到快照时的状态
文件不存在于快照中 直接删除该文件 移除回退点之后新建的文件

这种精细化处理的优势是:仅修改回退点之后变动的文件,回退点之前的无关更改会被完整保留,避免“一刀切”式的恢复。
来源:snapshot/index.ts, revert.ts

五、回退生命周期操作详解

回退系统的三个阶段对应三种核心操作,每个操作都有明确的执行步骤和状态影响。

1. 回退操作(触发回滚)

这是启动回退的核心操作,执行步骤如下:

  1. 状态验证:检查会话是否处于非繁忙状态(未处理命令/提示词),防止冲突
  2. 遍历消息链:按时间顺序遍历会话消息,定位目标 messageID/partID
  3. 识别回退点:根据指定方式,校准到实际的回退节点(用户消息优先)
  4. 收集补丁:提取回退点之后所有工具产生的文件修改补丁
  5. 创建快照:若当前无回退状态,创建当前文件系统的 Git 快照;若已有快照则复用
  6. 恢复文件:执行 Git checkout 操作,将文件系统恢复到快照状态
  7. 存储状态:将回滚信息(messageID/partID/snapshot 等)写入会话元数据
  8. 生成差异:计算当前状态与快照状态的差异,保存到 diff 字段

来源:revert.ts

2. 取消回退操作(撤销回滚)

当对回退结果不满意时,可执行取消回退,恢复到回退前的状态,步骤如下:

  1. 状态验证:确认会话非繁忙,且存在活跃的回退状态
  2. 恢复快照:如果存在快照哈希,通过 git read-treecheckout 恢复文件系统到回退前状态
  3. 清除状态:从会话元数据中删除 revert 字段,重置回退状态

3. 清理操作(完成回滚)

清理操作会永久删除回退点之后的消息和部分,完成不可逆的回退,步骤如下:

  1. 检查状态:确认存在活跃的回退状态
  2. 拆分消息链:根据 messageID 拆分消息列表,区分回退点前后的内容
  3. 删除数据
    • 全消息回退:删除回退点之后的所有消息及其部分
    • 部分回退:仅删除目标消息中指定 partID 之后的部分
  4. 发布事件:发送 message.removed/message.part.removed 事件,触发 UI 更新
  5. 清除状态:删除会话元数据中的 revert 字段,完成回退

关键特性:清理操作会在会话压缩前自动执行,确保压缩摘要基于干净的消息链生成。
来源:revert.ts

六、与会话压缩的深度集成

回退系统与上下文压缩工作流紧密联动,确保状态一致性:

  1. 压缩前置清理:当请求会话压缩/摘要时,系统会先自动清理所有活跃的回退状态,删除已回退的消息
  2. 避免摘要污染:防止已回退的无效对话内容被纳入压缩摘要,保证摘要的准确性
  3. 状态同步:压缩完成后,文件系统状态与消息历史完全匹配,无残留的回退标记

这种集成让回退操作无需用户手动清理,降低了使用成本。
来源:server.ts, compaction.ts

七、API 端点与事件系统

1. 核心 API 端点

系统提供三个专用 API 端点,支持程序化操作回退功能:

端点 请求方法 作用 请求体参数 响应内容
/session/:sessionID/revert POST 启动回退到指定消息/部分 messageID(必需)、partID(可选) 更新后的会话信息(含回退状态)
/session/:sessionID/unrevert POST 取消活跃的回退操作 清除回退状态的会话信息
/session/:sessionID/summarize POST 启动会话压缩(自动清理回退) providerIDmodelIDauto(可选) 压缩是否成功的布尔值

来源:server.ts

2. 状态事件通知

回退操作的状态变化会发布到全局事件总线,用于 UI 实时更新和跨组件通信:

事件类型 触发条件 负载内容
message.removed 清理操作删除消息 { sessionID, messageID }
message.part.removed 清理操作删除部分 { sessionID, messageID, partID }
session.updated 回退状态被修改 更新后的完整会话信息

来源:revert.ts, message-v2.ts

八、配置与限制

1. 依赖与限制

  • Git 依赖:回退系统需要项目目录初始化 Git 仓库,若无 Git,快照机制失效,无法执行文件回退
  • 繁忙状态保护:会话处于繁忙状态(处理命令/提示词)时,禁止执行回退/取消回退操作,防止状态冲突
  • 受保护工具skill 类工具的输出在压缩修剪时不会被清除,确保重要上下文在回退/压缩后仍保留

来源:compaction.ts, revert.ts

2. 测试覆盖

系统包含全面的测试套件,覆盖核心场景:

  • 消息/部分级别的回退工作流
  • 压缩前的自动清理集成
  • 消息/部分移除事件触发
  • Git 快照恢复准确性
  • 部分回退的边缘场景
    来源:revert-compact.test.ts

九、最佳实践

1. 回退 vs 分支:场景选择

操作 适用场景 对状态的影响
回退 尝试不同操作方向,需保留“反悔”余地 修改当前会话,清理前可逆,适合短期实验
分支 探索替代方案,同时保留原始对话路径 创建新的子会话,原会话状态完全不变,适合长期多方案对比

简单总结:短期试错用回退,长期分支用 fork

2. 回退操作时机

  • 最佳时机:在执行新操作前启动回退,避免新修改与回退操作冲突
  • 安全原则:等待当前会话的所有操作完成(非繁忙状态)后再执行回退,不强行操作

3. 清理注意事项

  • 未清理的回退会保留消息数据,频繁回退且不清理可能导致存储膨胀
  • 压缩操作会自动触发清理,无需手动干预;若需立即释放空间,可手动调用清理 API
  • 清理操作不可逆,执行前请确认回退结果符合预期

来源:index.ts