OpenCode 的核心不是把用户文本转发给模型,而是一个以 Session 为边界的本地 Agent Runtime。它在本地保存任务事实,构造每轮上下文,限制模型可用工具,执行真实动作,再依据结果决定继续还是结束。
本文按运行时依赖方向,从底向上 讲:上一层只建立在下一层已经提供的能力之上。packages/opencode/src/session 承载当前 Loop 主线;packages/core/src/session 是正在演进的 V2 Session 持久化实现,文中单独标注。
OpenCode 六层主栈图 :。
六层主栈与代码锚点
| 自底向上 | 一句话说明 | 主要代码 |
|---|---|---|
| 1. 本地状态、Memory 与执行 | 把可恢复的会话事实和可实际操作的工作区留在本地。 | src/storage/、src/git/、shell/PTY 工具、packages/core/src/database/ |
| 2. 模型与外部能力接入 | 把不同模型厂商和 MCP 都收敛成统一的模型流与工具能力。 | src/session/llm/、src/mcp/、request.ts |
| 3. 工具与权限 | 把模型给出的 JSON 动作转换为受控的真实执行。 | tools.ts、src/tool/、permission |
| 4. Session 与本轮上下文 | 把历史、规则和工具重新编译为这一轮模型真正能读的输入。 | session.ts、processor.ts、message-v2.ts |
| 5. Agent Loop | 决定何时调模型、何时继续、何时压缩/等子任务,以及何时结束。 | prompt.ts、run-state.ts |
| 6. Runtime 接入与运行控制 | 把同一套 Runtime 提供给 CLI、UI、SDK,并发布过程事件。 | server.ts、agent.ts、prompt.ts |
如何把分层与流程对应起来
房子图解释"能力放在哪里";主流程解释"一条输入如何跑完"。两者通过 Session 汇合:每个模块都向 Session 读写事实,Agent Loop 每次回环都重新从它取数。
后文每一层都会附"最小代码骨架":保留真实源码的入口、核心数据和下一跳;/* ... */ 代表为阅读而省略的字段或分支,示例用于对照流程,不是可直接粘贴运行的完整实现。
text
用户输入
→ Session 写入 User Message
→ 读取状态与记忆:历史、摘要、Todo、规则、工作区事实
→ 编译 system / messages / tools / params
→ 模型流
├─ 文本 / 推理 → 写入 Assistant Part
└─ tool-call → 权限 → 本地或 MCP 工具 → ToolResult → 写入 tool Part
→ Loop 重读 Session
→ 继续下一模型回合,或满足终态后 idle
贯穿主栈的状态与 Memory
这里的 Memory 不是单独的向量库层,而是四类状态的组合:
| 类型 | owner | 下一轮是否直接送模型 |
|---|---|---|
| 会话记忆 | SQLite 中的 Session、Message、Part、Todo | 是,重放后进入 messages |
| 压缩记忆 | compaction 生成的摘要 Message 与保留边界 | 是,以摘要替代旧历史 |
| 外部事实 | 工作区文件、Git、快照、终端输出 | 否;模型通过工具再次读取 |
| 进程运行态 | runner、AbortController、MCP client、流式拼接缓冲 | 否;进程重启即丢失 |
因此,Session 是 Agent 的主要持久记忆;工作区是外部事实源;进程内 Map 只是暂态。当前源码没有"自动抽取偏好并做 embedding / 向量检索、跨 Session 注入"的内建长期记忆链路。MemoryState 仅是流式事件处理期间的内存消息投影,不是长期记忆库。SQLite 表结构;内存投影
1. 本地状态、Memory 与执行
这一层提供两类本地能力:一类是工具实际操作工作区所需的 filesystem、process/PTY、Git、snapshot;另一类是会话事实的 SQLite 持久化。
一句话:它既保存"这个 Agent 已经知道和做过什么",也提供"它现在能在电脑上实际做什么"。
默认数据库在 Global.Path.data/opencode.db;OPENCODE_DB 可改为其他绝对路径,也可设为 :memory:。数据库路径
持久化不是一个大 JSON,而是三层关系:
text
Session
├─ Message[] 用户 / assistant 的一轮消息
│ └─ Part[] text / reasoning / tool / 文件 / step
├─ Todo
└─ 元数据:目录、Agent、模型、权限、token/cost、摘要、revert
session 表保存会话元数据;message 表按 session_id 保存轮次;part 表按 message_id 保存文本、工具调用和工具结果,主体字段以 JSON 保存。表结构
流式中,Text.Delta 是即时事件,供订阅者增量渲染;完整文本或工具结果结束后变成 PartUpdated,由 projector upsert 到 SQLite 的 part 表。模型事件发布;Part 投影
因此,重启后可以重读历史 Session;但模型连接、AbortController、MCP client、未完成流式缓冲和当前 loop 协程是进程内运行态,不能原地续跑。每 Session 串行协调器
核心流程
这一层把模型产生的 tool-call(name, args) 翻译为一次可审批、可观测、可回放的本机动作:
text
tool-call(name, args)
→ 解析输入:command/workdir 或 filePath/content/patch
→ 解析本地目标:cwd、绝对路径、worktree 边界
→ 权限:read / edit / bash;越界时 external_directory
→ 执行器:ChildProcessSpawner.spawn(...) 或 FileSystem API
→ 收集结果:输出流、exit code、diff、diagnostic、截断信息
→ 发布 FileSystem / Watcher / LSP 事件
→ ToolResult
→ 上层写为 Session 的 tool Part,并成为下一轮上下文
最小代码骨架
下面按 shell 工具执行器 删减;省略输出截断、超时、取消和审批细节:
ts
// tool-call 的 args 已在上层转换为 command / cwd / timeout
const handle = yield* spawner.spawn(cmd(shell, command, cwd, env))
// 进程 stdout / stderr 持续更新本次工具调用的运行 metadata
yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
ctx.metadata({ metadata: { output: preview(chunk) } }),
)
const exit = yield* handle.exitCode
return {
title: command,
metadata: { exit, truncated: false },
output: collectedOutput,
} satisfies ToolResult
shell 的执行器是 OS 子进程;read、write、apply_patch 直接调用文件系统 API。它们共享同一工具协议:参数 Schema、Tool.Context(Session、abort、审批)、输出截断和 ToolResult 形状。工具协议;shell 启动进程;批量 patch 的审批后落盘
目录边界、动作权限和执行后事件是三件不同的事:前者限制目标位置;中者决定本次 read/edit/bash 是否可执行;后者通知文件观察者/LSP。它们都发生在本地工具层,但 Session 持久化只消费最终的 ToolResult,而不替代文件事件。
2. 模型与外部能力接入
这里合并原先的"外部网络接入"和"模型接入":模型 Provider 负责请求/流式返回,MCP 负责把外部能力接成工具;模型回合编译负责将两者接到 Session。
一句话:无论底下是 OpenAI、Claude、Azure,还是一个远程 MCP,上一层只看到"模型能流式回答、工具能被调用"。
Provider 与 MCP 通道
核心流程
text
Provider:本轮模型请求 → Provider adapter / AI SDK → 供应商 HTTP 流 → 统一 LLMEvent → Session
MCP :stdio | Streamable HTTP | SSE → MCP Client → 工具发现 → client.callTool → ToolResult → Session
最小代码骨架
网络层只保留"建立外部通道"和"交给统一接口"两件事;Provider 与 MCP 的真实协议细节留在 adapter / client 内部。Provider 流入口;MCP 建连与发现
ts
// Provider:OpenCode 不直接拼各厂商 HTTP body
const result = streamText({ model, messages, tools, headers, providerOptions })
for await (const chunk of result.fullStream) {
yield* LLMAISDK.toLLMEvents(state, chunk) // 统一为 LLMEvent
}
// MCP:先按 stdio / HTTP / SSE 建连,再发现工具定义
const client = yield* connectTransport(transport, timeout)
const defs = yield* McpCatalog.defs(client, timeout)
// defs 后续会转换为普通 Tool 的 Schema + execute
Provider:统一的是模型接口,不是 HTTP body
Provider 层保存模型目录、认证、variant、headers、provider options 等全局接入配置;一次 Session 调用只携带"本轮模型、上下文、工具、采样参数"。LLMRequestPrep.prepare() 先把它们整理为 { system, messages, tools, params, headers },再交给 streamText(...)。请求准备;调用
text
Session 本轮上下文
→ LLMRequestPrep.prepare()
→ AI SDK streamText({ model, messages, tools, headers, providerOptions, ... })
→ 指定 Provider adapter
→ 实际 URL / 鉴权 / HTTP body / 流协议
→ result.fullStream(异步事件序列)
AI SDK 是这里的适配层:它替 OpenCode 对接不同模型供应商。OpenCode 因而不维护一份跨供应商通用 HTTP JSON;实际 wire-format 由具体 adapter 决定。很多 Provider 使用 SSE,但 OpenCode 消费的是 AI SDK 暴露的 fullStream,不能把 SSE 当作所有 Provider 的固定实现。
fullStream 的每个 chunk 会被 LLMAISDK.toLLMEvents 转成统一 LLMEvent,再进入 Session 处理器。AI SDK 事件适配;流消费
text
Provider 流事件
→ text / reasoning / tool-input / tool-call / tool-result / step-finish
→ LLMEvent
→ SessionProcessor.handleEvent
→ text、reasoning、tool、usage、finish 等 Session Part / Message 更新
→ 订阅者实时渲染;完成态持久化
这解释了"模型回调后做什么":不是直接通知 UI,而是先绑定到当前 Session 的 message/part;UI 只是 Session 事件的订阅者。text-delta 形成文本增量,tool-input-* 形成 pending 参数,完整 tool-call 才交给工具层,step-finish 写 token、成本、finish 等轮次状态。处理器
MCP:三种 transport 收敛为同一个工具调用
MCP 的本地模式用子进程 stdio;远程模式优先 Streamable HTTP,必要时退回 SSE。三者都先建立 MCP client,随后发现 server 的工具定义和 instruction;连接对象、工具目录、instruction 缓存在 Instance/MCP 运行态,而不是复制进每个 Session。本地连接;远程连接;发现与缓存
text
local cmd / Streamable HTTP / SSE
→ MCP Client
→ getServerCapabilities().tools
→ MCP definition:name + inputSchema
→ McpCatalog.convertTool(...)
→ AI SDK dynamicTool({ inputSchema, execute })
→ client.callTool({ name, arguments })
因此模型看到的 MCP 工具和内建工具没有本质差别:都是 description + JSON Schema + execute。模型不需要知道某个工具位于哪个 server,也不需要自己拼 transport 请求。
"三种 MCP 回复在哪里统一处理"分两步:
- catalog.ts 的
convertTool用统一client.callTool(...)接收CallToolResult,处理协议级isError。 - session/tools.ts 的执行包装把 MCP content 统一成 OpenCode
ToolResult:文本、图片、resource/blob 附件都会被规范化;随后进入当前 assistant message 的tool Part。
text
MCP CallToolResult
→ OpenCode ToolResult / attachments
→ Session tool Part(completed 或 error)
→ 下一轮模型历史
这也是 Provider 与 MCP 的共同边界:连接配置和 client 是运行时全局状态;一次模型输出或一次 MCP 工具结果才是当前 Session 的事实,并在完成后被持久化。
Provider / MCP 通道负责连接、transport 和结果回流边界;本层接着讨论 OpenCode 如何把这些能力组织成统一模型回合。
模型回合:统一请求和统一事件
上一层完成 Provider 网络连接;本层不关心 URL 或 SSE 细节,只定义 OpenCode 的"标准模型回合"如何变成 AI SDK 调用,以及如何把返回流统一为 LLMEvent。
核心流程
text
Session 历史、system、候选工具和模型选择
→ StreamInput(OpenCode 的标准模型回合)
→ prepare() 编译 system / messages / tools / params / headers
→ run() 选择 native runtime 或 AI SDK runtime
→ Provider 发起流式模型调用
→ 原始流事件转换为 LLMEvent
→ SessionProcessor 消费事件,更新 Message / Part
最小代码骨架
这是 请求编译、默认 runtime 与 流事件适配 的主干;省略 Provider 兼容分支:
ts
const prepared = yield* LLMRequestPrep.prepare(input)
const result = streamText({
model: language, // 已按 Provider / 模型配置解析的 AI SDK LanguageModel
messages: prepared.messages,
tools: prepared.tools,
headers: prepared.headers,
providerOptions: ProviderTransform.providerOptions(model, prepared.params.options),
})
const state = LLMAISDK.adapterState()
for await (const raw of result.fullStream) {
for (const event of LLMAISDK.toLLMEvents(state, raw)) {
yield* processor.handleEvent(event)
}
}
通用模型回合
llm.ts 的 StreamInput 是 Host 侧模型请求,不是 Provider HTTP body:
ts
{
user, sessionID, parentSessionID?,
model, agent, permission?,
system, messages, tools,
retries?, toolChoice?
}
| 数据组 | 字段 | 用途 |
|---|---|---|
| 本轮身份 | user、sessionID、parentSessionID |
当前任务、会话归属、子 Session 关联;还会进入 header、日志、trace。 |
| 模型选择 | model、agent |
确定 Provider 模型、Agent prompt、采样参数和默认权限。 |
| 上下文 | system、messages |
系统规则和由 Session 历史转换出的 ModelMessage[]。 |
| 能力 | tools、permission、toolChoice |
候选工具、可见性规则,以及 auto/required/none 调用策略。 |
| 控制 | retries |
重试、取消、遥测等 Host 控制。 |
其中 sessionID、agent、permission 并不原样发给模型:它们分别影响请求 header/观测、system/options、工具过滤。真正交给 AI SDK 的通用数据是 model + messages + tools + generation params + providerOptions + headers。
prepare():把抽象模型回合编译为本次真实请求
request.ts 的 LLMRequestPrep.prepare() 只负责准备请求:不发网络请求、不写 Session、不控制 loop。
text
Host 侧标准回合 + Agent / Provider / Session / Plugin 配置
│
▼
prepare()
│
▼
本轮最终请求:system · messages · tools · params · headers
输入来自不同 owner:Agent 提供 prompt、采样参数和默认权限;Session 提供环境、instruction 与历史;User 提供本轮 system、variant、工具开关;Model/Provider 提供默认 options、headers 与能力约束。
输出 Prepared Request 的结构是:
ts
{
system,
messages,
tools,
params: { temperature, topP, topK, maxOutputTokens, options },
messageTransformOptions,
headers,
}
| 输出字段 | 编译规则 | 这一步解决什么 |
|---|---|---|
system |
Agent prompt / 模型默认 prompt + input.system + user.system |
得到模型本轮最高优先级规则。 |
messages |
通常为 system message + Session history |
得到模型实际看到的历史;特殊 Provider 可改用专用 instructions。 |
tools |
候选工具 − Agent/Session 禁用项 − user 显式关闭项 | 得到模型本轮真正可调用的能力。 |
params |
Provider base → model.options → agent.options → user variant | 得到最终温度、token 上限和 Provider options;右侧覆盖左侧。 |
headers |
模型 headers + Plugin headers + Session 标识 | 将会话关联、客户端标识和 Provider 接入信息带到请求边界。 |
Plugin 的改写点也是分开的,而不是任意改整个请求:
| Hook | 允许改写 |
|---|---|
experimental.chat.system.transform |
system |
chat.params |
采样参数、token 上限、Provider options |
chat.headers |
headers |
兼容分支的含义是:
| 分支 | 为什么存在 |
|---|---|
OpenAI OAuth:system → instructions |
该接口路径使用专用 instructions 字段,而不是普通 system message。 |
| Azure completion URL:删除字段 | 这类 endpoint 不接受 reasoningSummary、include。 |
OpenAI / Azure / Bedrock:strict: false |
动态/MCP Schema 可能不符合 Provider 的严格 structured-output 约束;OpenCode 仍在工具执行边界校验参数。 |
Copilot:补 _noop |
历史含 tool call 但本轮无可用工具时,该 Provider 仍要求 tools 字段存在。 |
这些是 Provider API 差异的隔离层,不是 Agent Loop 的分支。
run():接入依赖、选择 runtime、发起流
run() 是模型调用的执行入口:它拿到 prepare() 的结果后,选择可用 runtime,并返回模型流;不写 Session,也不判断 loop 是否结束。
text
LLM.stream(input)
→ 创建本轮 AbortController
→ run(input, abort)
→ 取得 model adapter、Provider、Auth、Config
→ prepare(input)
→ 尝试 Native runtime
→ 否则 AI SDK streamText(...)
→ Native stream 或 AI SDK fullStream
→ 统一为 Stream<LLMEvent>
真正进入 Provider 网络层的边界是 streamText(...):run
streamText 字段组 |
字段 | 用途 |
|---|---|---|
| 模型与上下文 | model、messages |
选择具体 adapter;提供本轮推理上下文。 |
| 工具能力 | tools、activeTools、toolChoice |
工具完整定义、启用工具名、auto/required/none 调用策略。 |
| 生成控制 | temperature、topP、topK、maxOutputTokens、maxRetries |
采样、输出上限、重试。 |
| Provider / Host 控制 | providerOptions、headers、abortSignal |
供应商特有参数、请求头、取消信号。 |
transformParams 在最后发送前,把通用 ModelMessage[] 改写成当前 Provider 所需的消息格式。
LLM.Service 依赖的 Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service | EventV2Bridge.Service | LLMClientService 是 Effect 的依赖声明,不是发给 Provider 的请求字段。
| 依赖 | 本层用途 |
|---|---|
| Auth / Config | 凭证、全局配置、遥测开关。 |
| Provider | 根据 model 找语言模型 adapter、Provider 配置。 |
| Plugin / Permission | 改写请求,过滤模型可见工具。 |
| EventV2Bridge | 事件体系桥接;不属于 Provider 网络请求。 |
| LLMClient | 给实验 Native LLM runtime 使用的底层客户端。 |
工具调用在流中发生
模型不必等整条回答结束才调用工具:
text
text/reasoning delta
→ tool-input-delta(参数 JSON 尚未完整)
→ tool-call(工具名和参数完整)
→ AI SDK 调用 tools[name].execute(args)
→ 本地工具或 MCP 工具返回
→ tool-result
→ 模型基于结果继续下一个推理 step / 后续模型回合
对模型语义来说,工具结果后的推理就是"下一次回答";对 Runtime 来说,它可能是同一流的后续 step,也可能由外层 Loop 发起下一次模型请求。共同点是工具结果先写入 Session,下一次模型看到的是这份真实历史。
| 流事件字段 | 含义 |
|---|---|
tool-input-{start,delta,end} |
{ id, name, text };text 是尚未完成的 JSON 参数片段。 |
tool-call |
{ id, name, input };input 已完整,可执行。 |
tool-result |
{ id, name, result };与同一 tool-call.id 对应。 |
providerExecuted / providerMetadata |
标识调用是否由 Provider 托管执行,以及 Provider 附带元数据。 |
流事件适配:分发 + 拼接
Stream<LLMEvent> 只是"异步依次产出标准事件"的类型名。实现流程只有一条:
text
AI SDK fullStream 的一个原始 event
→ 按 event.type 进入 switch 分支
→ 必要时读取/更新 adapterState
→ 输出 0、1 或多个标准 LLMEvent
→ 下一个原始 event
text
native.stream 已经是 LLMEvent
AI SDK fullStream → toLLMEvents(state, event) → LLMEvent
↑
只用于拼接 text / reasoning / tool 分片
| 原始 AI SDK event | 适配动作 | 输出 |
|---|---|---|
text-*、reasoning-* |
用当前 block ID 关联前后分片 | 对应 text-*、reasoning-*。 |
tool-input-*、tool-call、tool-result/error |
记录或查询 toolCallId → toolName |
对应工具事件。 |
finish-step、finish |
记录 step 序号、读取 usage/finish reason,结束时 reset | step-finish、finish。 |
start、raw、source、file 等 |
不需要交给 SessionProcessor,或只提取内部 metadata | 不输出事件。 |
adapterState 只保存拼接所需的临时索引:当前 text/reasoning block ID、toolCallId → toolName 和 step 序号。连续 text-delta 要归到同一个 text Part;工具结果可能只带 call ID,需靠映射找回工具名。finish 后立刻 reset,不跨模型流保存业务状态。
LLMEvent |
表示什么 | SessionProcessor 的落点 |
|---|---|---|
text-start/delta/end |
普通文本片段 | assistant text part;delta 可即时展示。 |
reasoning-* |
推理片段 | reasoning part。 |
tool-input-* |
工具参数 JSON 正在生成 | pending tool part。 |
tool-call |
工具名和完整参数已就绪 | 工具层开始执行。 |
tool-result/tool-error |
工具结束 | completed/error tool part。 |
step-finish/finish |
一个模型步骤或整条流结束 | finish、token、成本、快照。 |
模型层到此只输出事件,不写 SQLite、不渲染 UI、不决定是否继续 loop;SessionProcessor 消费事件,Agent Loop 再据此判断继续或结束。
3. 工具与权限:从 JSON Schema 到真实动作
工具层不负责决定"下一步做什么";它负责把模型给出的 tool-call(name, args) 变成一次受控的真实动作,并把结果写回本轮 Session。这里有三个不能混淆的动作:
一句话:模型只能提出"调用什么、参数是什么";这一层负责决定能不能做、怎么做、结果如何留下来。
text
注册(全局有哪些工具)
≠ resolve(当前这轮允许模型看见哪些工具)
≠ execute(模型已调用某工具后,真正执行动作)
核心流程
text
1. 启动时注册:ToolRegistry 注册内建 / Plugin 工具;MCP 连接后发现远端工具。
2. 每个模型回合前:SessionTools.resolve() 根据当前 Agent、Session、模型能力和 MCP 状态生成候选工具表。
3. resolve 过滤掉当前 Agent 或 Session 禁用、以及静态规则完全 deny 的工具;其余工具转为 AI SDK schema。
4. 同时为每个工具生成 execute 闭包,闭包绑定 sessionID、messageID、toolCallID、AbortSignal 和权限上下文。
5. 模型流产生完整 tool-call(name, args) 后,AI SDK 才调用这个 execute 闭包。
6. 闭包构造 Tool.Context,进入 Plugin hook、具体本地/MCP 工具的执行包装和细粒度权限判断。
7. 工具返回 ToolResult;模型流产生 tool-result/tool-error,SessionProcessor 将其写成当前 assistant message 的 tool Part。
8. 结果进入下一模型 step 或下一轮上下文;是否继续由 Agent Loop 判断。
最小代码骨架
下面是 SessionTools.resolve() 中最重要的"本轮工具表 + execute 闭包"结构;省略 Plugin hook、附件处理和特殊 MCP resource 工具:
ts
const tools: Record<string, AITool> = {}
for (const item of yield* registry.tools({ agent, modelID, providerID, permission })) {
tools[item.id] = tool({
description: item.description,
inputSchema: jsonSchema(ProviderTransform.schema(model, ToolJsonSchema.fromTool(item))),
execute(args, options) {
return run.promise(Effect.gen(function* () {
const ctx = context(args, options) // 绑定 session / message / call / abort / ask
return yield* item.execute(args, ctx)
}))
},
})
}
MCP 的连接、发现、协议结果归一化已在上一层完成;到这一步,它已被转换为同样的工具定义,和本地工具共享同一工具表、权限边界和 ToolResult 回写路径。
1. resolve():为本轮编译工具表
SessionTools.resolve() 不是注册工具,也不在此时实际调用工具。它为这一轮模型请求组装 AI SDK 能消费的映射:
ts
Record<toolName, {
description: string
inputSchema: JSONSchema
execute(args, options): Promise<ToolResult>
}>
其中 description 和 inputSchema 会随模型请求发出,使模型知道可调用什么、参数如何组织;execute 不会发给模型,而是留在 OpenCode 进程中,等待模型真的发出同名 tool-call 时被 AI SDK 调用。
resolve() 读取的运行时输入及其用途如下:
| 输入 | 在本轮工具表中的作用 |
|---|---|
| 当前 Agent | 根据 Agent 的 tool / permission 配置筛掉不可用工具。 |
| Session 与当前 message | 把工具执行结果归属到正确的会话和 assistant message。 |
| 当前模型 | 按 Provider / 模型能力把输入 schema 转成兼容格式。 |
| MCP client | 把已发现的远端工具加入同一候选集合。 |
processor、AbortSignal |
让工具能够发布执行进度、响应取消并回写本轮事件。 |
2. 模型调用后:执行包装如何落到真实工具
工具参数并不是一次性凭空拿到的。流中先有 tool-input-delta,表示参数 JSON 仍在增量生成;完整 tool-call 到达后,AI SDK 才执行第 4 步创建的闭包:
text
model tool-call
→ tools[name].execute(args, options)
→ 由闭包构造 Tool.Context
→ plugin before hook
→ 本地工具 item.execute(args, context) 或 MCP client.callTool(...)
→ plugin after hook
→ ToolResult
→ tool-result / tool-error 事件
→ Session 的 tool Part
Tool.Context 是真实执行的运行时边界,包含 sessionID、messageID、callID、agent、abort、历史消息,以及两个回调:metadata() 用于更新执行中的工具 Part,ask() 用于在需要时发起权限审批。也就是说,resolve() 创建的是"带会话身份的执行入口";第 5 步的模型调用才会触发真正的 shell、读写文件或 MCP RPC。
3. 权限有两道边界
| 时机 | 规则 | 结果 |
|---|---|---|
resolve() |
工具被规则完全 deny |
不进入本轮 schema,模型根本看不见。 |
execute() |
对具体命令、文件路径或参数判断 allow / ask / deny |
allow 直接执行;deny 拒绝;ask 发布审批事件并等待 UI/SDK 回复。 |
前一道是在降低模型可见的能力面;后一道面对的是实际参数,因此能精确限制"哪条 shell 命令""哪个文件路径"。ask 不是 UI 线程阻塞:权限服务发布请求事件后等待一个 Deferred 结果,收到用户的 allow/deny 回复再恢复这次工具调用。权限判断
4. 结果如何回到模型与 Session
工具的统一返回形状包含 title、metadata、文本 output 和可选 attachments。它不是由工具直接"回调模型";先进入 AI SDK 的流,再成为 Session 的可持久化事实,最后由下一次模型请求重放。
text
1. 模型输出 tool-call(name, args)
2. 找到本轮 resolve() 预先包装好的 execute 闭包
3. 闭包绑定 sessionID / assistant messageID / toolCallID / 权限上下文
4. 执行真实工具
5. 工具结果包装为 ToolResult
6. ToolResult → tool-result 事件 → 写入原 assistant message 下的 tool Part
7. Loop 重新读取 Session,把该 tool Part 编译进下一次模型请求
8. 模型根据工具结果:
- 再发 tool-call:重复 2--7
- 输出正常文本并 stop:Loop 结束
其中第 2--6 步是一次工具调用的执行链;第 7--8 步将工具结果送回模型,构成"模型 → 工具 → Session → 模型"的外层循环。
text
真实工具执行
→ ToolResult
→ AI SDK fullStream: tool-result / tool-error
→ OpenCode LLMEvent
→ SessionProcessor 更新 tool Part
→ Agent Loop 重新读取 Session
→ tool Part 编译为 ModelMessage
→ 下一次模型请求
工具执行包装已经持有三类关联键:sessionID 指明会话,messageID 指明这次 assistant 回答,toolCallID 对应模型发出的具体调用。SessionProcessor 用 toolCallID 找到此前由 tool-input-* 创建的 pending Part;收到 tool-call 后将其更新为 running,收到 tool-result 后将其更新为 completed,收到 tool-error 则标记 error。
text
tool-input-start → pending
tool-call → running
tool-result → completed
tool-error → error
完成后的事实形状可理解为:
ts
{
type: "tool",
tool: "read",
callID: "call_xxx",
state: {
status: "completed",
input: { filePath: "src/a.ts" },
output: "...文件内容...",
attachments: []
}
}
这份 tool Part 挂在当前 assistant message 下,因此 UI/TUI 可以显示运行中状态和最终结果;更重要的是,外层 runLoop() 不会仅因 Provider 返回 stop 就退出。它重新读取 Session:只要还发现工具调用,就继续编译历史。继续条件
MessageV2.toModelMessagesEffect() 会把 completed tool Part 转成模型通用的工具输出块:
ts
{
type: "tool-read",
state: "output-available",
toolCallId: "call_xxx",
input: { filePath: "src/a.ts" },
output: "...文件内容..."
}
Provider adapter 再将这个通用块转换成对应厂商的 tool-result 协议。因此模型在下一次 请求中看到的是已绑定、已完成的工具结果;它可以再发 tool-call,流程从执行包装再次开始,也可以输出普通文本并终止 Loop。工具调用形成的是"模型 → 工具 → Session → 模型"的外层循环,而不是工具和模型之间的一次直接回调。
工具主干的代码锚点为:全局注册、每轮 resolve 与执行闭包、工具 ABI、流事件转换、Session 回写、历史重放、权限服务。
代码定位也属于这一层的工具反馈回路:
text
glob:按文件名模式找候选路径
grep:按内容返回真实路径、行号、命中片段
read:读取已定位文件或目录
相对路径由后端锚定到 instance 工作目录。模型依靠工作目录、搜索结果和失败反馈收敛到正确文件,而不是天然记住仓库每个路径。read、grep、glob
4. Session 与本轮上下文:事实中心和 prompt 编译器
Session 既是持久化事实中心,也是每轮模型输入的来源。它不会把全部本地状态直接送给模型,而是从历史和配置中选择、转换并编译出一轮上下文。组装位置
一句话:它把散落的历史、规则、工具结果整理成模型此刻真正看得懂的一包输入。
核心流程
text
1. 用户输入先写成 Session 中最新的 User Message(U1)。
2. Agent Loop 重新读取该 Session 的 Message + Part;已被 compaction 摘要替代的旧历史被过滤。
3. 从 U1 取得本轮 Agent、模型和 Session 权限,并创建空的 Assistant Message(A1)作为本次模型流的输出容器。
4. MessageV2.toModelMessagesEffect() 将已持久化的历史转为 ModelMessage[]:文本、推理、历史 tool-call 和 tool-result 都在这里重放。
5. 同时构建本轮运行上下文:环境、项目 instruction、MCP instruction、Skill 引导,并 resolve 可用工具表。
6. 将 system + ModelMessage[] + tools + model / agent 等输入交给 handle.process();随后 prepare() 编译成真实 Provider 请求。
7. 模型流的 text / reasoning / tool 事件持续写入 A1 下的 Part;工具结果也写回 A1 的 tool Part。
8. 若模型结束且无工具待继续,A1 成为终态回答;若仍需工具结果继续推理,Loop 回到第 2 步,重放 U1 + A1,并创建 A2 接收下一模型回合的输出。
最小代码骨架
以下对应 prompt.ts 的每轮编译,省略 Agent 查找、Plugin 注入和结构化输出分支:
ts
const msgs = yield* MessageV2.filterCompactedEffect(sessionID)
const lastUser = MessageV2.latest(msgs).user!
const msg = {
id: MessageID.ascending(),
parentID: lastUser.id,
role: "assistant",
agent: agent.name,
mode: agent.name,
sessionID,
modelID: model.id,
providerID: model.providerID,
time: { created: Date.now() },
// path / tokens / cost 等初始字段省略
}
yield* sessions.updateMessage(msg) // 先创建本轮输出容器 A1
const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([
sys.skills(agent),
sys.environment(model),
instruction.system(),
sys.mcp(agent, session.permission),
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(mcpInstructions ? [mcpInstructions] : []), ...(skills ? [skills] : [])]
const tools = yield* SessionTools.resolve({ agent, session, model, processor: handle, messages: msgs })
yield* handle.process({ user: lastUser, system, messages: modelMsgs, tools, model })
每次实际发给模型的核心输入可简化为:
text
system = Agent 规则 + 环境 + 项目 instruction + MCP / Skill 引导
messages = Session 历史重放:用户消息、assistant 消息、已完成工具结果
tools = 本轮允许模型调用的工具 description + JSON Schema
params = model、toolChoice、temperature、maxOutputTokens 等控制参数
每个模型回合先创建输出容器
模型流不是结束后才一次性写入 Session。每次发起模型调用前,Loop 会先创建一条空的 assistant message,并将它交给 SessionProcessor;随后到达的 text-delta、reasoning、tool-call、tool-result 都按 messageID 写入它下面的对应 Part。创建容器;流事件回写
text
U1:请读 a.ts 并解释
│
├─ A1:本次模型回合预先创建的空 assistant message
│ ├─ text Part:我先读取文件
│ └─ tool Part:read(a.ts) → 文件内容
│
└─ A2:工具结果需要继续推理时,下一模型回合创建的空 assistant message
└─ text Part:基于 a.ts 的分析与最终回答
因此"创建 assistant message"不是提前生成下一条用户输入;它是为当前模型流 预留的输出归属。A1 的 tool Part 完成后,外层 Loop 从 Session 重放 U1 + A1,并在下一次模型调用前创建 A2 接收继续输出。这样每个流式事件都能确定写到哪一个 assistant message、哪一个 Part;工具结果也成为下一回合可恢复、可重放的历史事实。
Session 存储模型与模型消息之间的重放
模型不能直接理解 OpenCode 的数据库对象,例如 SessionV1.ToolPart。Session 的 Message + Part 是面向持久化、事件订阅和 UI 展示的内部存储模型;Provider 需要的是面向对话协议的 ModelMessage[]。MessageV2.toModelMessagesEffect() 是二者之间的翻译层:每轮都从持久化的 Session 历史重新生成模型可读消息,而不是依赖上一轮请求的内存。消息重放
text
Session 存储模型 通用模型消息模型
──────────────────────────────────────────────────────────────
User Message + text Part → role=user 的 text content
Assistant Message + text Part → role=assistant 的 text content
Assistant Message + reasoning Part → role=assistant 的 reasoning content
Assistant Message + completed tool → tool-<name> / output-available
File Part / tool attachment → 可被模型支持的 media content
例如 Session 中的一条 read 工具事实:
ts
// 持久化 / UI 使用的 ToolPart
{
type: "tool",
tool: "read",
callID: "call_xxx",
state: {
status: "completed",
input: { filePath: "src/a.ts" },
output: "...文件内容..."
}
}
重放后变成模型通用的工具输出块:
ts
// 发送给 Provider adapter 前的 ModelMessage content
{
type: "tool-read",
state: "output-available",
toolCallId: "call_xxx",
input: { filePath: "src/a.ts" },
output: "...文件内容..."
}
Provider adapter 再将这个通用形状转换为 OpenAI、Anthropic、Gemini 等各自的实际协议。这里的"重放"只是在重建模型上下文,不会再次执行工具;它保证重试、断开恢复、压缩或下一模型回合仍能依据同一份工具结果继续推理。
text
Agent 系统规则
+ 环境:工作目录、workspace root、仓库、平台、日期
+ 项目 instruction:AGENTS.md / CLAUDE.md / 配置 instruction
+ MCP instruction 与可发现的 Skill
+ 当前 Session 历史与已完成工具结果
+ 本轮获准工具的 description + JSON Schema
+ 最新用户目标
Skill 正文默认不会全量进入 system prompt;模型通常先知道有哪些 Skill 及如何加载,需要时再读取。Skill system prompt
"写 Session"也不是直接调用 UI 的 appendText。模型/工具的事件先变成 Session 的 message/part 更新;UI、TUI 和 SDK 都订阅这些事件。已完成的工具输出属于当前 message 的 tool Part;Provider 配置、模型目录、MCP client 和工具注册表则是全局运行配置,不会为每个 Session 复制一份。
5. Agent Loop:唯一的编排和终态判断点
一句话:它是总调度员,反复驱动"读状态 → 调模型/工具 → 写结果",直到这一轮任务真的结束。
核心流程
SessionPrompt.prompt() 写入用户消息、取得该 Session 的串行 runner 后进入主循环。入口;循环
text
用户输入
→ 写 user message
→ 读取 Session 历史和任务状态
→ 优先处理 subtask / compaction / revert
→ 编译本轮上下文与工具表
→ 调模型并消费 LLMEvent
→ 模型 / 工具结果写回 Session
→ 是否有未完成工具或需要 continue?是则回到"读取 Session"
→ 否则 Session idle
最小代码骨架
以下是 runLoop() 的循环骨架;真正代码还包含标题生成、提醒注入、重试和错误处理:
ts
while (true) {
const msgs = yield* MessageV2.filterCompactedEffect(sessionID)
const { user: lastUser, assistant: lastAssistant, tasks } = MessageV2.latest(msgs)
const hasToolCalls = /* 当前 assistant 是否仍有需回传模型的 tool Part */
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastAssistant.parentID === lastUser.id
) break
const task = tasks.pop()
if (task?.type === "subtask") { yield* handleSubtask({ task, msgs }); continue }
if (task?.type === "compaction") { yield* compaction.process({ msgs, sessionID }); continue }
// 以下进入第 5 节:创建 assistant 容器 → 编译上下文 / 工具 → handle.process()
}
形式上是 while (true),但每轮都重新从 Session 读取历史。因此工具结果、子任务结果、压缩结果都会成为下一轮真实模型上下文。
结束条件不只是 Provider 返回 stop:还要确认最近 assistant 是否对应最新 user message、是否已经终态、是否带 tool-calls,以及是否存在 pending/running tool part。只有"正常结束 + 无待处理工具 + 对应当前用户"才 idle。
Loop 状态机
| 状态载体 | 关键状态 | 职责 |
|---|---|---|
| Session runner | idle、busy、retry |
同一 Session 串行、取消和运行态展示。 |
| Assistant message | finish=stop/tool-calls/error |
判断模型轮次是否可终止。 |
| Tool part | pending、running、completed、error |
判断真实动作是否仍在执行。 |
| SessionProcessor 返回值 | continue、compact、stop |
消费完一次模型流后,建议外层 Loop 继续、先压缩或立即停止。 |
text
空闲(`idle`) → 接收用户输入 → 运行(`busy`)
运行 → 优先处理子任务 / 上下文压缩 → 重新读取会话
运行 → 组装本轮模型输入 → 调用模型与执行工具 → 终态判断
终态判断 → 继续运行(仍有工具或需要继续)| 空闲(满足 `stop` 条件)
OpenCode Agent Loop 状态机 。
图中的"终态判断 → 本次结束"是唯一的 stop 出口。它不是"模型供应商返回了 stop"这一条信号,而是必须同时满足:assistant 已有 finish、finish 不是 tool-calls、没有仍需回传模型的工具 Part,且该 assistant 属于最新 user message。任一条件不满足都沿"继续"回到"读取当前会话",由下一回合重新编译上下文。
task、控制结果与终态的边界
Loop 从 MessageV2.latest(msgs) 取出的 task,不是泛指"模型做的任务",而是 Session 中待处理的控制 Part:subtask 表示先执行一条子 Agent 委派,compaction 表示先压缩上下文。它们是持久化历史的一部分,Loop 每次重读 Session 都会优先调度它们。任务提取;调度分支
不要把 continue / compact / stop 与模型事件混为一谈。text-delta、tool-call、finish 是模型流事件;pending/running/completed/error 是工具 Part 状态;而 continue/compact/stop 是 SessionProcessor.process() 消费完一次模型流 后返回给外层 Loop 的控制结果。它分别由"正常完成""需要压缩""被阻断或模型错误"等处理状态导出。处理器结果
一次外层终止表示"当前 Session 的这一次 prompt run 完成,runner 回到 idle",并不表示 Agent 模板生命周期结束;用户下一条输入仍可用同一 Agent、同一 Session 重新启动 Loop。子 Agent 也是如此:子 Session 先终止并将结果写回父 Session 的 task tool Part,父 Loop 是否终止仍要由父会话自己的终态条件决定。
因此,正常情况下"一次 Loop 迭代约等于一次父 Agent 的模型调用",但并不绝对:遇到 subtask 或 compaction 时,这一轮只处理控制任务,不发父 Agent 的正常模型请求。OpenCode 具备 Session 持久化和循环恢复基础,但没有固定的 Planner、统一 Evaluator 或严格确定性重放机制;它是 Agent Runtime,而不是完整的声明式工作流引擎。
6. Runtime 接入与运行控制
这一章有两个方向:接入 回答"用户和 UI 怎样进入、怎样看到 Runtime 的变化";运行控制 回答"同一次 Session run 依据什么策略选择模型、规则、工具和权限"。二者最终都汇入 SessionPrompt → Agent Loop。
一句话:它把同一个 Agent Runtime 交给 CLI、Web、Desktop、SDK 使用,并让它们看到同一份过程状态。
本节总结
text
1. 同步 prompt 和异步 prompt 启动的是同一个 Agent Loop。
区别只是:同步请求等本次 run 完成;异步请求立即返回,随后靠事件看进度和结果。
2. 模型和工具不直接更新 UI。
它们先更新 Session 的 Message / Part;Runtime 发布变化事件;UI 订阅这些变化后再渲染。
3. Provider、MCP、工具、项目规则等基础配置先在初始化阶段建立 Runtime 能力。
每一轮再按当前 Agent、Session 权限和模型筛选出本轮有效策略;最后才编译为模型输入。
核心流程
text
CLI / TUI / Web / Desktop / SDK
→ 进程内 Server 或 HTTP Server
→ prompt、取消、审批、查询等 Session 服务
→ SessionPrompt / Agent Loop
→ Message / Part 更新并发布事件
→ 进程内订阅或 SSE 订阅
→ UI 渲染同一份 Session 状态
最小代码骨架
同步与异步入口共享同一个 promptSvc.prompt();区别只有 HTTP handler 是否等待。事件端则把 Runtime 事件队列编码为 SSE。prompt handler;事件 handler
ts
// 同步:本 HTTP 请求等待 Session Loop 完成
const message = yield* promptSvc.prompt({ ...payload, sessionID })
return HttpServerResponse.stream(Stream.make(JSON.stringify(message)).pipe(Stream.encodeText))
// 异步:同一调用在后台运行,立即返回
yield* promptSvc.prompt({ ...payload, sessionID }).pipe(Effect.forkIn(scope))
return HttpApiSchema.NoContent.make()
// UI 订阅:事件 → 队列 → SSE;UI 再按 Message / Part 重新渲染
const queue = yield* Queue.unbounded<EventV2.Payload>()
const unsubscribe = yield* events.listen((event) => Queue.offerUnsafe(queue, event))
return HttpServerResponse.stream(Stream.fromQueue(queue).pipe(Stream.pipeThroughChannel(Sse.encode())))
请求进入与事件返回
Server 的 HTTP handler 将 prompt payload 直接交给 SessionPrompt.prompt();SDK/CLI 也可调用进程内 Server.Default.fetch(),不需要监听 HTTP 端口。进程内 Server;prompt handler
| 调用方式 | Server 行为 | 客户端如何取得结果 |
|---|---|---|
| 同步 prompt | 等待 promptSvc.prompt() 完成本次 Session run,再返回最终 message。 |
HTTP 响应拿最终结果;也可同时订阅事件展示过程。 |
| 异步 prompt | 将同一 promptSvc.prompt() 放到后台执行,HTTP 立即返回。 |
订阅事件或后续查询 Session。 |
同步与异步不代表两套 Agent 执行逻辑;它们启动的是同一个 Session、同一个 Agent Loop、同一套模型/工具/权限路径。异步入口
模型和工具并不直接操作 UI。它们先更新 Session 的 Message / Part,Runtime 再发布事件;进程内客户端可直接监听,Web 等远程客户端由 Server 将同一事件流编码为 SSE 后订阅。事件 handler 在注册监听后建立队列、按 workspace 过滤事件,并发送连接事件与心跳。SSE 事件桥接
text
模型 / 工具
→ Session Message / Part 更新
→ Runtime 事件
→ 进程内订阅 或 SSE
→ TUI / Web / Desktop 渲染
权限审批也走同一条返回路径:工具发起 ask,UI 订阅到审批事件;用户允许/拒绝后调用 permissionRespond,Server 再调用 Permission.reply() 解除或拒绝被暂停的工具执行。审批回复
运行控制:初始化能力 → 本轮策略 → 模型请求
"配置、Agent、项目规则、Plugin、Skill、MCP、Provider、工作区"不是一个会原样发送给模型的大对象,而是三层来源:
text
1. 初始化能力:系统有什么能力
Provider / 账户、MCP client、工具注册表、数据库、Server、工作区
2. 本轮有效策略:当前这次允许用什么
当前 Agent + Session 权限 + 项目规则 + 当前模型
3. 模型请求包:真正给模型的内容
system + messages + tools + params
初始化阶段加载并合并基础配置,建立长期运行的 Provider、MCP、工具注册和工作区能力;它们不会整体进入 prompt。Agent 服务再合并默认值与用户配置,得到模型、prompt、采样参数、mode、steps、options、permission 等策略。配置合并;Agent 合并
每次 Loop 运行时,才依据当前 Agent、当前 Session、当前模型再次筛选和编译:项目 instruction、Skill、MCP instruction、Session 历史与工具表汇入模型输入。也就是说,初始化决定"有什么能力",每轮编译决定"当前能启用什么",最终只有编译产物进入模型。每轮汇合点
| 控制来源 | 本轮的实际影响 |
|---|---|
| Agent | 选择模型、Agent prompt、采样参数、最大 steps、默认权限。 |
| Session 权限 | 与 Agent 权限合并,筛选工具并约束真实执行。 |
| 项目规则 / Skill / MCP | 形成 system 引导;MCP 已发现工具可加入候选工具表。 |
| Plugin | 改写 messages、system、params、headers,或包裹工具执行。 |
| Provider / 账户 | 提供模型 adapter、认证、headers、provider options。 |
| 工作区 | 提供 cwd、worktree、Git、目录边界和环境提示。 |
子 Agent 与后台任务
Agent 是模板;Session 是一次具体运行。开子 Agent 不只是把父会话加一个角色,而是创建带 parentID 的独立子 Session,并运行自己的 Loop:
text
父 Session S0 → task(subagent_type, prompt)
→ 子 Session S1(parentID=S0,agent=指定 Agent)
→ S1 的 模型 → 工具 → 模型 loop
→ 最终 text → 父任务的 task tool result
→ 父 Session 下一轮模型调用
TaskTool.execute() 检查委派权限和深度、选择子 Agent 与模型、创建子 Session 并调用它的 prompt()。TaskTool
子 Session 的直接背景来自 task.prompt,不复制父 Session 全量历史;它使用自己的权限,并承接父任务的 deny 与外部目录限制,默认限制再次开 task,避免无限递归。子 Agent 权限
| 模式 | 父任务行为 | 结果回传 |
|---|---|---|
| 前台(默认) | task 等子 Session job 完成 |
作为当前 tool result,父 loop 再调模型。 |
| 后台(实验功能) | task 立即返回 running |
job 完成后写 synthetic user message 到父 Session,再唤醒父 loop。 |
补充:长任务支撑机制(Agent Loop 的扩展)
这不是第七层;它是第 5 层为了处理长上下文、回退和后台工作而使用的持久化辅助机制。
一句话:任务太长时,不让 Loop 丢失现场,而是把"下一步要恢复什么"也写进 Session。
核心流程
text
正常 Session Loop
→ 发现上下文溢出、用户 revert、需要标题/摘要等条件
→ 创建或触发对应的持久化辅助任务 / 快照操作
→ 下一次 Loop 优先处理 compaction、subtask、revert 等状态
→ 更新 Session 历史或工作区事实
→ 回到正常模型 → 工具 → Session 循环
最小代码骨架
压缩不是在 Loop 外另开一个不可见任务;它先写成持久化的 compaction Part,随后由 Loop 优先处理。创建压缩任务;Loop 分支
ts
// 1. 先把"需要压缩"写入 Session,重启后仍能发现
const message = yield* session.updateMessage({ role: "user", sessionID, agent, model })
yield* session.updatePart({
messageID: message.id,
sessionID,
type: "compaction",
auto,
})
// 2. 下一次 Loop 读取 task 后优先处理
if (task?.type === "compaction") {
const result = yield* compaction.process({ messages: msgs, sessionID, parentID: lastUser.id })
if (result === "stop") break
continue
}
上下文压缩:生成可重放的摘要历史
压缩本身是一次特殊模型调用,不是直接删除或截断 Session 历史。它先把旧历史划分为需要总结的 head 和原样保留的近期 tail;tail 会按 token 预算和 tail_turns 配置从最新回合向前选择。head / tail 选择
text
原历史:U1 → A1 → U2 → A2 → U3 → A3 → U4 → A4
1. Loop 写入 C1:系统生成的 compaction pseudo-user message + Compaction Part。
2. Loop 优先调度 C1;compaction Agent 将旧段 head 连同压缩提示交给模型。
3. 模型生成 S1:agent=compaction、summary=true 的 assistant 摘要消息。
4. C1 的 Compaction Part 记录 tail_start_id,指出 U4 / A4 等近期事实从哪里开始保留。
5. 下一次普通模型调用重放:C1 + S1 + tail + 后续用户消息,而不再发送完整旧历史。
因此压缩后的下一轮模型上下文可理解为"系统生成的压缩任务 + 模型生成的摘要 + 最近原始历史 "。C1 不是用户真实输入;它是 Runtime 写入的持久化控制任务。自动压缩成功后,Runtime 还可能写入一条 synthetic "继续执行"用户消息来唤醒正常 Loop。
text
旧历史 head 仍保留在数据库,用于审计和追溯;
filterCompacted() 决定下次模型请求不再重放它,而是发送 summary + tail。
这与展示用途的标题、文件 diff 摘要不同:compaction summary 会改变后续模型看到的上下文;标题和文件 diff 仅用于展示、检索或审计。历史重放过滤
| 机制 | 作用 | 代码 |
|---|---|---|
| Revert | 恢复文件快照,并清理被放弃分支后续历史,保证文件与 prompt 历史一致。 | revert.ts |
| Compaction | 接近上下文窗口时写入 compaction pseudo-user task,优先压缩后继续。 | compaction.ts |
| 标题与摘要 | 后台生成会话标题、文件差异摘要,供展示、恢复与审计。 | summary.ts |
| 消息转换 | 将持久化文本、附件、工具调用/结果转为 provider-ready messages。 | message-v2.ts |
推荐阅读路径
- 本地数据库、Session 表:先明确本地事实边界。
- MCP、LLM 请求:外部通道。
- 工具桥接、权限:动作如何落地。
- 上下文、消息转换:模型实际看到什么。
- 主 loop、子 Agent:最后理解编排。
范围
基于静态源码 2859603;未执行真实 Provider/MCP 调用。Loop、工具和子 Agent 主线以 packages/opencode/src/session 的现有 runtime 为准;Session 本地持久化补充 packages/core 的 V2 实现。不同 Provider 的实际 wire-format、不同 MCP server 的行为仍应以 adapter 实现或真实运行证据为准。

