OpenCode 源码解析

138 阅读44分钟

OpenCode 源码解析

OpenCode 的核心不是把用户文本转发给模型,而是一个以 Session 为边界的本地 Agent Runtime。它在本地保存任务事实,构造每轮上下文,限制模型可用工具,执行真实动作,再依据结果决定继续还是结束。

本文按运行时依赖方向,从底向上讲:上一层只建立在下一层已经提供的能力之上。packages/opencode/src/session 承载当前 Loop 主线;packages/core/src/session 是正在演进的 V2 Session 持久化实现,文中单独标注。

图 1:OpenCode 六层运行时架构

第一部分:整体架构——六层主栈

自底向上一句话说明主要代码
1. 本地操作与会话存档真正读写文件、运行命令、操作 Git;同时保存会话与工具结果。src/storage/src/git/、shell/PTY 工具、packages/core/src/database/
2. 模型和外部服务连接连接不同模型厂商和 MCP 等外部服务。src/session/llm/src/mcp/request.ts
3. 工具准备与安全校验决定模型能用什么工具,并在执行前做权限校验。tools.tssrc/tool/permission
4. 会话整理与本轮输入准备整理历史、规则与工具结果,形成模型输入;也接收模型输出。session.tsprocessor.tsmessage-v2.ts
5. 任务循环与下一步判断决定调模型、执行工具、压缩、启动子任务或停止。prompt.tsrun-state.ts
6. 用户入口与界面同步接收 CLI、UI、SDK 输入,并把过程状态同步给界面。server.tsagent.tsprompt.ts

第二部分:端到端流程——一次任务如何运行

房子图解释“能力放在哪里”;主流程解释“一条输入如何跑完”。两者通过 Session 汇合:每个模块都向 Session 读写事实,Agent Loop 每次回环都重新从它取数。

后文每一层都会附“最小代码骨架”:保留真实源码的入口、核心数据和下一跳;/* ... */ 代表为阅读而省略的字段或分支,示例用于对照流程,不是可直接粘贴运行的完整实现。

1. 六阶段主流程

以“读取 src/a.ts 并解释”为例:

  1. 用户把任务交进来:L6 接收 CLI、Web 或 Desktop 输入,并交给当前 Session 的 Agent。
  2. 先把用户要求记下来:L5 启动本次运行;L1 将“用户要读取什么”保存为 Session 事实,重启后仍能恢复。
  3. 整理本轮模型输入:L5 触发 L4 读取历史、摘要、规则和已有工具结果;L3 筛选当前允许的工具;L2 接收编译后的模型请求。
  4. 模型开始生成:L2 返回文字、推理或工具请求;L4 将这些过程写入当前会话,L1 持久化,界面因此可以实时显示。
  5. 若模型需要工具,就执行后回到模型:L3 校验权限;L1 真实读取文件、运行命令或调用 MCP,并保存结果;L5 发现任务未完成,回到第 3 步重新调用模型。
  6. 模型给出最终答案,结束本次运行:L5 确认没有待执行工具、压缩或子任务后停止;L6 将 Session 的最终变化同步到 CLI、Web、Desktop。

第 3~5 步构成主循环。L1 也不是只在流程底部经过一次:每当用户输入、模型输出、工具状态或结果变化时,它都会作为会话事实的存档点。

写入会话事实(对应第 2 步)

这一阶段不调用模型。它做的是把“读取 src/a.ts 并解释”放进已有的 Session,而不是把这句话只留在当前 HTTP 请求或界面内存里。

写入后的内容可以简单理解为三部分:

  • 用户这次输入的文字;
  • 随输入带来的图片、文件或其他内容;
  • 本次选择的 Agent、模型以及临时工具开关。

其中,文字和附件是这条用户消息的内容;Agent、模型、权限是后续运行这条消息时要采用的条件。它们都以 Session 为归属:刷新页面、退出再打开后,系统仍知道用户问了什么、上一次走到了哪里。

写完才开始本次运行。若这是“只记入会话、不要求立即回复”的输入,就到这里为止;正常输入才进入下一步的模型准备。

编译模型工作包(对应第 3 步)

这一阶段的目标不是“把全部数据库记录原样发给模型”,而是生成本轮真正要使用的上下文包。它由 L5 发起,L4、L3、L2 分别补齐不同部分:

  • L4 会话整理:取这条用户要求、相关历史、已有摘要、项目规则和已经拿到的工具结果;过长的旧历史不会无条件全部带上。
  • L3 工具准备:从全部工具中筛出当前 Agent 和当前权限允许使用的工具,并附上每个工具的名称、用途和参数说明。
  • L2 模型连接:将整理后的系统规则、对话消息、工具说明、模型参数交给所选 Provider,由它适配具体模型厂商的协议并发起请求。

所以模型看到的不是 OpenCode 的原始 Session 数据库对象,而是一份“本轮工作包”:该做什么、此前已经知道什么、可以调用什么、应遵守什么规则。第 4 步开始的流式输出,也会反过来继续补充这份 Session 事实。

2. 三条特殊分支

这三件事不是另一套主流程,而是在不同阶段暂时改道;最终仍回到 Session,再由 L5 决定是否进入下一轮。

分支从哪里分出这条支路做什么怎样回到主流程
上下文压缩第 3 步准备输入时发现历史过长,或第 4 步模型返回“上下文超限”暂停普通模型请求,使用专门的压缩 Agent 把较早历史整理成摘要,同时保留最近几轮和必要的工具调用摘要与压缩标记写回同一 Session;回到第 3 步,重新生成“摘要 + 最近历史”的工作包
子 Agent第 4 步模型输出 task 工具请求,随后进入第 5 步工具执行建立一个带父 Session 关系的子 Session;子 Agent 用自己的任务输入跑完整的小循环,而不是共享父会话的全部历史前台子 Agent 完成后作为父工具结果,回到父任务第 3 步;后台子 Agent 结束后把结果写成父会话的一条系统生成输入,再启动父会话的新一轮
revert(回退)用户在 L6 主动要求回退;它不是模型正常推理中自动触发的分支在没有运行中的任务时,恢复对应的工作区快照,并标记 Session 应回到的消息或消息片段边界下一次用户输入进入第 2 步时,先清除被放弃分支的消息和 Part,再以回退后的事实进入第 3 步

可以把三者的差别记成一句话:压缩是“历史太长,先换一种历史表示”;子 Agent 是“任务太大,另开一个独立会话完成子问题”;revert 是“用户改变历史,丢弃后续事实后再继续”。

其中,压缩和前台子 Agent 都会让当前任务暂时等待;后台子 Agent 不等待父任务当前模型回合,所以它完成后不能硬插回已经结束的模型流,只能以一条新的会话输入重新唤醒父 Loop。revert 则要求当前 Session 不处于运行中,避免文件快照和会话历史落在不一致的中间状态。

图 2:OpenCode 六层主数据流程

3. 贯穿六层的状态与 Memory

这里的 Memory 不是单独的向量库层,而是四类状态的组合:

类型owner下一轮是否直接送模型
会话记忆SQLite 中的 Session、Message、Part、Todo是,重放后进入 messages
压缩记忆compaction 生成的摘要 Message 与保留边界是,以摘要替代旧历史
外部事实工作区文件、Git、快照、终端输出否;模型通过工具再次读取
进程运行态runner、AbortController、MCP client、流式拼接缓冲否;进程重启即丢失

因此,Session 是 Agent 的主要持久记忆;工作区是外部事实源;进程内 Map 只是暂态。当前源码没有“自动抽取偏好并做 embedding / 向量检索、跨 Session 注入”的内建长期记忆链路。MemoryState 仅是流式事件处理期间的内存消息投影,不是长期记忆库。SQLite 表结构内存投影

第三部分:六层实现与代码

第 1 层:本地操作与会话存档(文件、进程、SQLite)

这一层提供两类本地能力:一类是工具实际操作工作区所需的 filesystem、process/PTY、Git、snapshot;另一类是会话事实的 SQLite 持久化。

一句话:它既保存“这个 Agent 已经知道和做过什么”,也提供“它现在能在电脑上实际做什么”。

默认数据库在 Global.Path.data/opencode.dbOPENCODE_DB 可改为其他绝对路径,也可设为 :memory:数据库路径

持久化不是一个大 JSON,而是三层关系:

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 持久化:在什么时候写、写什么、下一轮怎样用

持久化的时机是每个可恢复的状态变化点,而不是等整次 Agent 回答结束后统一保存。一次普通任务可以按下面理解:

创建 Session
→ 收到用户输入:写 U1(user Message + Part)
→ 调模型前:先写空的 A1(assistant Message,parentID=U1)
→ 模型流:更新 A1 下的 text / reasoning / tool Part
→ 工具完成:更新同一个 tool Part 的结果
→ 本轮结束:更新 A1 的 finish、error、token、cost、完成时间
→ 下一轮:重读 U1 + A1,必要时再创建 A2

因此,一轮用户输入或一次模型回合创建一条 Message;流式文字、推理和工具结果补在该 Message 的 Part 中。工具结果不另起 user Message:模型先在 A1 下发起 read,执行后仍把 input / output / state 写回 A1 的同一个 tool Part;外层 Loop 重读 Session 后,才创建 A2 让模型解释该结果。

写入阶段主要内容后续用途
建 Session目录、工作区、Agent、模型、权限、父 Session、统计信息约束运行环境,并关联子 Agent。
收用户输入user Message、文本/附件/命令等 Part;V2 还记录输入顺序latest() 找到本轮目标用户消息。
创建 assistant 容器assistant Message,parentID 指向当前 user Message为后续所有流式 Part 提供确定归属。
流与工具text、reasoning、tool 的输入、状态、输出、附件、错误UI 实时展示;Loop 判断是否仍有待处理工具。
完成与控制任务finish、usage、cost、压缩任务、摘要、subtask、revert决定继续/停止;压缩和子任务可在重启后继续被调度。

下一次模型调用并不是从内存接着上一次字符串继续,而是 runLoop() 每轮从数据库读取 Message + Part,先由 filterCompactedEffect() 去掉已被摘要替代的旧历史,再由 toModelMessagesEffect() 翻译成 Provider 可理解的 messages。这就是 Session 同时承担“可恢复存储”和“下一轮 prompt 的事实来源”的原因。Loop 重读 Session压缩历史过滤

真实文件和代码改动不复制进 SQLite:它们留在工作区、Git 与 snapshot;数据库保存的是“调用了哪个工具、输入是什么、得到了什么结果、当时处于什么状态”。模型连接、AbortController、MCP client 和未完成的流式缓冲则只存在进程内。

因此,重启后可以重读历史 Session;但模型连接、AbortController、MCP client、未完成流式缓冲和当前 loop 协程是进程内运行态,不能原地续跑。每 Session 串行协调器

核心流程

这一层把模型产生的 tool-call(name, args) 翻译为一次可审批、可观测、可回放的本机动作:

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 工具执行器 删减;省略输出截断、超时、取消和审批细节:

// 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 子进程;readwriteapply_patch 直接调用文件系统 API。它们共享同一工具协议:参数 Schema、Tool.Context(Session、abort、审批)、输出截断和 ToolResult 形状。工具协议shell 启动进程批量 patch 的审批后落盘

目录边界、动作权限和执行后事件是三件不同的事:前者限制目标位置;中者决定本次 read/edit/bash 是否可执行;后者通知文件观察者/LSP。它们都发生在本地工具层,但 Session 持久化只消费最终的 ToolResult,而不替代文件事件。

第 2 层:模型和外部服务连接(Provider 与 MCP)

这里合并原先的“外部网络接入”和“模型接入”:模型 Provider 负责请求/流式返回,MCP 负责把外部能力接成工具;模型回合编译负责将两者接到 Session。

一句话:无论底下是 OpenAI、Claude、Azure,还是一个远程 MCP,上一层只看到“模型能流式回答、工具能被调用”。

Provider 与 MCP 通道

核心流程

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 建连与发现

// 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(...)请求准备调用

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 事件适配流消费

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。本地连接远程连接发现与缓存

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 回复在哪里统一处理”分两步:

  1. catalog.tsconvertTool 用统一 client.callTool(...) 接收 CallToolResult,处理协议级 isError
  2. session/tools.ts 的执行包装把 MCP content 统一成 OpenCode ToolResult:文本、图片、resource/blob 附件都会被规范化;随后进入当前 assistant message 的 tool Part
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

核心流程

Session 历史、system、候选工具和模型选择
  → StreamInput(OpenCode 的标准模型回合)
  → prepare() 编译 system / messages / tools / params / headers
  → run() 选择 native runtime 或 AI SDK runtime
  → Provider 发起流式模型调用
  → 原始流事件转换为 LLMEvent
  → SessionProcessor 消费事件,更新 Message / Part

最小代码骨架

这是 请求编译默认 runtime流事件适配 的主干;省略 Provider 兼容分支:

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.tsStreamInput 是 Host 侧模型请求,不是 Provider HTTP body:

{
  user, sessionID, parentSessionID?,
  model, agent, permission?,
  system, messages, tools,
  retries?, toolChoice?
}
数据组字段用途
本轮身份usersessionIDparentSessionID当前任务、会话归属、子 Session 关联;还会进入 header、日志、trace。
模型选择modelagent确定 Provider 模型、Agent prompt、采样参数和默认权限。
上下文systemmessages系统规则和由 Session 历史转换出的 ModelMessage[]
能力toolspermissiontoolChoice候选工具、可见性规则,以及 auto/required/none 调用策略。
控制retries重试、取消、遥测等 Host 控制。

其中 sessionIDagentpermission 并不原样发给模型:它们分别影响请求 header/观测、system/options、工具过滤。真正交给 AI SDK 的通用数据是 model + messages + tools + generation params + providerOptions + headers

prepare():把抽象模型回合编译为本次真实请求

request.tsLLMRequestPrep.prepare() 只负责准备请求:不发网络请求、不写 Session、不控制 loop。

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 的结构是:

{
  system,
  messages,
  tools,
  params: { temperature, topP, topK, maxOutputTokens, options },
  messageTransformOptions,
  headers,
}
输出字段编译规则这一步解决什么
systemAgent prompt / 模型默认 prompt + input.system + user.system得到模型本轮最高优先级规则。
messages通常为 system message + Session history得到模型实际看到的历史;特殊 Provider 可改用专用 instructions
tools候选工具 − Agent/Session 禁用项 − user 显式关闭项得到模型本轮真正可调用的能力。
paramsProvider base → model.options → agent.options → user variant得到最终温度、token 上限和 Provider options;右侧覆盖左侧。
headers模型 headers + Plugin headers + Session 标识将会话关联、客户端标识和 Provider 接入信息带到请求边界。

Plugin 的改写点也是分开的,而不是任意改整个请求:

Hook允许改写
experimental.chat.system.transformsystem
chat.params采样参数、token 上限、Provider options
chat.headersheaders

兼容分支的含义是:

分支为什么存在
OpenAI OAuth:system → instructions该接口路径使用专用 instructions 字段,而不是普通 system message。
Azure completion URL:删除字段这类 endpoint 不接受 reasoningSummaryinclude
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 是否结束。

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 字段组字段用途
模型与上下文modelmessages选择具体 adapter;提供本轮推理上下文。
工具能力toolsactiveToolstoolChoice工具完整定义、启用工具名、auto/required/none 调用策略。
生成控制temperaturetopPtopKmaxOutputTokensmaxRetries采样、输出上限、重试。
Provider / Host 控制providerOptionsheadersabortSignal供应商特有参数、请求头、取消信号。

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/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> 只是“异步依次产出标准事件”的类型名。实现流程只有一条:

AI SDK fullStream 的一个原始 event
  → 按 event.type 进入 switch 分支
  → 必要时读取/更新 adapterState
  → 输出 0、1 或多个标准 LLMEvent
  → 下一个原始 event
native.stream 已经是 LLMEvent
AI SDK fullStream → toLLMEvents(state, event) → LLMEvent
                                      ↑
                       只用于拼接 text / reasoning / tool 分片

流收口事件 switch

原始 AI SDK event适配动作输出
text-*reasoning-*用当前 block ID 关联前后分片对应 text-*reasoning-*
tool-input-*tool-calltool-result/error记录或查询 toolCallId → toolName对应工具事件。
finish-stepfinish记录 step 序号、读取 usage/finish reason,结束时 resetstep-finishfinish
startrawsourcefile不需要交给 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。这里有三个不能混淆的动作:

一句话:模型只能提出“调用什么、参数是什么”;这一层负责决定能不能做、怎么做、结果如何留下来。

注册(全局有哪些工具)
  ≠ resolve(当前这轮允许模型看见哪些工具)
  ≠ execute(模型已调用某工具后,真正执行动作)
核心流程
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 工具:

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 回写路径。

resolve():为本轮编译工具表

SessionTools.resolve() 不是注册工具,也不在此时实际调用工具。它为这一轮模型请求组装 AI SDK 能消费的映射:

Record<toolName, {
  description: string
  inputSchema: JSONSchema
  execute(args, options): Promise<ToolResult>
}>

其中 descriptioninputSchema 会随模型请求发出,使模型知道可调用什么、参数如何组织;execute 不会发给模型,而是留在 OpenCode 进程中,等待模型真的发出同名 tool-call 时被 AI SDK 调用。

resolve() 读取的运行时输入及其用途如下:

输入在本轮工具表中的作用
当前 Agent根据 Agent 的 tool / permission 配置筛掉不可用工具。
Session 与当前 message把工具执行结果归属到正确的会话和 assistant message。
当前模型按 Provider / 模型能力把输入 schema 转成兼容格式。
MCP client把已发现的远端工具加入同一候选集合。
processorAbortSignal让工具能够发布执行进度、响应取消并回写本轮事件。
模型调用后:执行包装如何落到真实工具

工具参数并不是一次性凭空拿到的。流中先有 tool-input-delta,表示参数 JSON 仍在增量生成;完整 tool-call 到达后,AI SDK 才执行第 4 步创建的闭包:

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 是真实执行的运行时边界,包含 sessionIDmessageIDcallIDagentabort、历史消息,以及两个回调:metadata() 用于更新执行中的工具 Part,ask() 用于在需要时发起权限审批。也就是说,resolve() 创建的是“带会话身份的执行入口”;第 5 步的模型调用才会触发真正的 shell、读写文件或 MCP RPC。

权限的两道边界
时机规则结果
resolve()工具被规则完全 deny不进入本轮 schema,模型根本看不见。
execute()对具体命令、文件路径或参数判断 allow / ask / denyallow 直接执行;deny 拒绝;ask 发布审批事件并等待 UI/SDK 回复。

前一道是在降低模型可见的能力面;后一道面对的是实际参数,因此能精确限制“哪条 shell 命令”“哪个文件路径”。ask 不是 UI 线程阻塞:权限服务发布请求事件后等待一个 Deferred 结果,收到用户的 allow/deny 回复再恢复这次工具调用。权限判断

结果如何回到模型与 Session

工具的统一返回形状包含 titlemetadata、文本 output 和可选 attachments。它不是由工具直接“回调模型”;先进入 AI SDK 的流,再成为 Session 的可持久化事实,最后由下一次模型请求重放。

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 → 模型”的外层循环。

真实工具执行
  → ToolResult
  → AI SDK fullStream: tool-result / tool-error
  → OpenCode LLMEvent
  → SessionProcessor 更新 tool Part
  → Agent Loop 重新读取 Session
  → tool Part 编译为 ModelMessage
  → 下一次模型请求

工具执行包装已经持有三类关联键:sessionID 指明会话,messageID 指明这次 assistant 回答,toolCallID 对应模型发出的具体调用。SessionProcessortoolCallID 找到此前由 tool-input-* 创建的 pending Part;收到 tool-call 后将其更新为 running,收到 tool-result 后将其更新为 completed,收到 tool-error 则标记 error。

tool-input-start  → pending
tool-call          → running
tool-result        → completed
tool-error         → error

完成后的事实形状可理解为:

{
  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 转成模型通用的工具输出块:

{
  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 回写历史重放权限服务

代码定位也属于这一层的工具反馈回路:

glob:按文件名模式找候选路径
grep:按内容返回真实路径、行号、命中片段
read:读取已定位文件或目录

相对路径由后端锚定到 instance 工作目录。模型依靠工作目录、搜索结果和失败反馈收敛到正确文件,而不是天然记住仓库每个路径。readgrepglob

第 4 层:会话整理与本轮输入准备(事实中心和 Prompt 编译)

Session 既是持久化事实中心,也是每轮模型输入的来源。它不会把全部本地状态直接送给模型,而是从历史和配置中选择、转换并编译出一轮上下文。组装位置

一句话:它把散落的历史、规则、工具结果整理成模型此刻真正看得懂的一包输入。

核心流程
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 注入和结构化输出分支:

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 })

每次实际发给模型的核心输入可简化为:

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。创建容器流事件回写

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 历史重新生成模型可读消息,而不是依赖上一轮请求的内存。消息重放

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 工具事实:

// 持久化 / UI 使用的 ToolPart
{
  type: "tool",
  tool: "read",
  callID: "call_xxx",
  state: {
    status: "completed",
    input: { filePath: "src/a.ts" },
    output: "...文件内容..."
  }
}

重放后变成模型通用的工具输出块:

// 发送给 Provider adapter 前的 ModelMessage content
{
  type: "tool-read",
  state: "output-available",
  toolCallId: "call_xxx",
  input: { filePath: "src/a.ts" },
  output: "...文件内容..."
}

Provider adapter 再将这个通用形状转换为 OpenAI、Anthropic、Gemini 等各自的实际协议。这里的“重放”只是在重建模型上下文,不会再次执行工具;它保证重试、断开恢复、压缩或下一模型回合仍能依据同一份工具结果继续推理。

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 后进入主循环。入口循环

用户输入
  → 写 user message
  → 读取 Session 历史和任务状态
  → 优先处理 subtask / compaction / revert
  → 编译本轮上下文与工具表
  → 调模型并消费 LLMEvent
  → 模型 / 工具结果写回 Session
  → 是否有未完成工具或需要 continue?是则回到“读取 Session”
  → 否则 Session idle
最小代码骨架

以下是 runLoop() 的循环骨架;真正代码还包含标题生成、提醒注入、重试和错误处理:

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 runneridlebusyretry同一 Session 串行、取消和运行态展示。
Assistant messagefinish=stop/tool-calls/error判断模型轮次是否可终止。
Tool partpendingrunningcompletederror判断真实动作是否仍在执行。
SessionProcessor 返回值continuecompactstop消费完一次模型流后,建议外层 Loop 继续、先压缩或立即停止。
空闲(`idle`) → 接收用户输入 → 运行(`busy`)
运行 → 优先处理子任务 / 上下文压缩 → 重新读取会话
运行 → 组装本轮模型输入 → 调用模型与执行工具 → 终态判断
终态判断 → 继续运行(仍有工具或需要继续)| 空闲(满足 `stop` 条件)

图 3: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-deltatool-callfinish 是模型流事件;pending/running/completed/error 是工具 Part 状态;而 continue/compact/stopSessionProcessor.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 使用,并让它们看到同一份过程状态。

三个关键关系
1. 同步 prompt 和异步 prompt 启动的是同一个 Agent Loop。
   区别只是:同步请求等本次 run 完成;异步请求立即返回,随后靠事件看进度和结果。

2. 模型和工具不直接更新 UI。
   它们先更新 Session 的 Message / Part;Runtime 发布变化事件;UI 订阅这些变化后再渲染。

3. Provider、MCP、工具、项目规则等基础配置先在初始化阶段建立 Runtime 能力。
   每一轮再按当前 Agent、Session 权限和模型筛选出本轮有效策略;最后才编译为模型输入。
核心流程
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

// 同步:本 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 端口。进程内 Serverprompt 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 事件桥接

模型 / 工具
  → Session Message / Part 更新
  → Runtime 事件
  → 进程内订阅 或 SSE
  → TUI / Web / Desktop 渲染

权限审批也走同一条返回路径:工具发起 ask,UI 订阅到审批事件;用户允许/拒绝后调用 permissionRespond,Server 再调用 Permission.reply() 解除或拒绝被暂停的工具执行。审批回复

运行控制:初始化能力 → 本轮策略 → 模型请求

“配置、Agent、项目规则、Plugin、Skill、MCP、Provider、工作区”不是一个会原样发送给模型的大对象,而是三层来源:

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:

父 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 立即返回 runningjob 完成后写 synthetic user message 到父 Session,再唤醒父 loop。

第四部分:长任务支撑机制(Agent Loop 的扩展)

这不是第七层;它是第 5 层为了处理长上下文、回退和后台工作而使用的持久化辅助机制。

一句话:任务太长时,不让 Loop 丢失现场,而是把“下一步要恢复什么”也写进 Session。

核心流程

正常 Session Loop
  → 发现上下文溢出、用户 revert、需要标题/摘要等条件
  → 创建或触发对应的持久化辅助任务 / 快照操作
  → 下一次 Loop 优先处理 compaction、subtask、revert 等状态
  → 更新 Session 历史或工作区事实
  → 回到正常模型 → 工具 → Session 循环

最小代码骨架

压缩不是在 Loop 外另开一个不可见任务;它先写成持久化的 compaction Part,随后由 Loop 优先处理。创建压缩任务Loop 分支

// 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 和原样保留的近期 tailtail 会按 token 预算和 tail_turns 配置从最新回合向前选择。head / tail 选择

原历史: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。

旧历史 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

推荐阅读路径

  1. 本地数据库Session 表:先明确本地事实边界。
  2. MCPLLM 请求:外部通道。
  3. 工具桥接权限:动作如何落地。
  4. 上下文消息转换:模型实际看到什么。
  5. 主 loop子 Agent:最后理解编排。

范围与证据边界

基于静态源码 2859603;未执行真实 Provider/MCP 调用。Loop、工具和子 Agent 主线以 packages/opencode/src/session 的现有 runtime 为准;Session 本地持久化补充 packages/core 的 V2 实现。不同 Provider 的实际 wire-format、不同 MCP server 的行为仍应以 adapter 实现或真实运行证据为准。