Harness · Coding Agent Book07 / OpenCode
研究总览
M07 · SOURCE-GROUNDED TUTORIAL

OpenCode
从源码学会它怎么工作

持久化 message-parts 状态机与插件生态很强;默认宿主执行是最明显安全缺口。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

TypeScript · Persistent Session PlatformMIT284e97068c9828 个结论 · 86 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 OpenCode 的 28 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

持久化消息状态驱动;每 step 重建 agent/tools/system

上下文

可用输入窗阈值;摘要保尾;工具输出后台 prune

适用建设

多前端、插件市场、持久任务与可回退编辑

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

它每一轮都重新看账本决定“接下来做什么”,所以进程中断、工具异步完成和压缩都能落在统一状态机里。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
主循环由持久化消息状态驱动,而不是一次性的 while(tool_call)packages/opencode/src/session/prompt.ts:1081它每一轮都重新看账本决定“接下来做什么”,所以进程中断、工具异步完成和压缩都能落在统一状态机里。
每一步动态重建 Agent、工具、系统上下文和模型请求packages/opencode/src/session/prompt.ts:1170不是开会前一次性发完所有资料;每走一步都按当前身份、模型和权限重新整理桌面。
重试、拒绝、上下文溢出和中断有不同终态packages/opencode/src/session/processor.ts:539网络抖动会重试,权限拒绝会刹车,记忆塞满会整理,进程被打断则把未完成工具明确标成中止,不会都混成一个“失败”。
01
L1 · fact · opencode-loop-001

主循环由持久化消息状态驱动,而不是一次性的 while(tool_call)

先看源码事实

runLoop 每步从数据库重建过滤后的消息视图,找最后 user/assistant/finished 与待处理 subtask/compaction;只有已完成、无工具调用且顺序闭合才退出,否则继续调度。

翻译成白话

它每一轮都重新看账本决定“接下来做什么”,所以进程中断、工具异步完成和压缩都能落在统一状态机里。

为什么这对自研重要

消息/part 是事实源,loop 是其投影;这比仅在内存追加数组更利于恢复和多客户端。

固定提交源码摘录
 1081      const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
 1082        function* (sessionID: SessionID) {
 1083          const ctx = yield* InstanceState.context
 1084          let structured: unknown
 1085          let step = 0
 1086          const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
 1087  
 1088          while (true) {
 1089            yield* status.set(sessionID, { type: "busy" })
 1090            yield* Effect.logInfo("loop", { "session.id": sessionID, step })
 1091  
 1092            let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
 1093              Effect.provideService(Database.Service, database),
 1094            )
 1095  
 1096            const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
 1097  
 1098            if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
 1099  
 1100            const lastAssistantMsg = msgs.findLast(
 1101              (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
 1102            )
 1103            // Some providers return "stop" even when the assistant message contains
 1104            // tool calls. Keep the loop running so tool results can be sent back to
 1105            // the model, but ignore cleanup-marked interrupted orphans.
      … 14 lines omitted; exact range 1081–1130 …
 1120              if (orphan) {
 1121                yield* Effect.logWarning("loop exit with orphaned interrupted tool", {
 1122                  "session.id": sessionID,
 1123                  messageID: lastAssistant.id,
 1124                  tool: orphan.tool,
 1125                  callID: orphan.callID,
 1126                })
 1127              }
 1128              yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
 1129              break
 1130            }
为什么相信这条结论?查看 2 处证据
02
L1 · fact · opencode-loop-002

每一步动态重建 Agent、工具、系统上下文和模型请求

先看源码事实

loop 解析 agent/model,应用 reminders,创建 assistant record,按当前 permission/provider 解析工具,再并行装配 skills、environment、instructions、MCP instructions 和 compacted model messages。

翻译成白话

不是开会前一次性发完所有资料;每走一步都按当前身份、模型和权限重新整理桌面。

为什么这对自研重要

支持运行中配置和权限变化,但每步装配链更复杂、需要缓存与测试。

固定提交源码摘录
 1170            const agent = yield* agents.get(lastUser.agent)
 1171            if (!agent) {
 1172              const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
 1173              const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
 1174              const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
 1175              yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
 1176              throw error
 1177            }
 1178            const maxSteps = agent.steps ?? Infinity
 1179            const isLastStep = step >= maxSteps
 1180            msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(
 1181              Effect.provideService(RuntimeFlags.Service, flags),
 1182              Effect.provideService(FSUtil.Service, fsys),
 1183              Effect.provideService(Session.Service, sessions),
 1184            )
 1185  
 1186            const msg: SessionV1.Assistant = {
 1187              id: MessageID.ascending(),
 1188              parentID: lastUser.id,
 1189              role: "assistant",
 1190              mode: agent.name,
 1191              agent: agent.name,
 1192              variant: lastUser.model.variant,
 1193              path: { cwd: ctx.directory, root: ctx.worktree },
 1194              cost: 0,
      … 36 lines omitted; exact range 1170–1241 …
 1231                bypassAgentCheck,
 1232                messages: msgs,
 1233                promptOps,
 1234              }).pipe(
 1235                Effect.provideService(Plugin.Service, plugin),
 1236                Effect.provideService(Permission.Service, permission),
 1237                Effect.provideService(ToolRegistry.Service, registry),
 1238                Effect.provideService(MCP.Service, mcp),
 1239                Effect.provideService(Truncate.Service, truncate),
 1240                Effect.provideService(RuntimeFlags.Service, flags),
 1241              )
为什么相信这条结论?查看 2 处证据
03
L1 · fact · opencode-retry-001

重试、拒绝、上下文溢出和中断有不同终态

先看源码事实

stream 应用 provider-aware retry policy;拒绝默认让 loop 停止,可配置继续;ContextOverflow 在 auto compact 开启时转为 compact,其他错误进入 session error;cleanup 会把残留 tool call 标为 interrupted error。

翻译成白话

网络抖动会重试,权限拒绝会刹车,记忆塞满会整理,进程被打断则把未完成工具明确标成中止,不会都混成一个“失败”。

为什么这对自研重要

错误分类是自治可靠性的核心,而不只是 UI 文案。

固定提交源码摘录
  539        const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () {
  540          if (ctx.snapshot) {
  541            const patch = yield* snapshot.patch(ctx.snapshot)
  542            if (patch.files.length) {
  543              yield* session.updatePart({
  544                id: PartID.ascending(),
  545                messageID: ctx.assistantMessage.id,
  546                sessionID: ctx.sessionID,
  547                type: "patch",
  548                hash: patch.hash,
  549                files: patch.files,
  550              })
  551            }
  552            ctx.snapshot = undefined
  553          }
  554  
  555          if (ctx.currentText) {
  556            const end = Date.now()
  557            ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
  558            yield* session.updatePart(ctx.currentText)
  559            ctx.currentText = undefined
  560          }
  561  
  562          for (const part of Object.values(ctx.reasoningMap)) {
  563            const end = Date.now()
      … 23 lines omitted; exact range 539–597 …
  587                status: "error",
  588                error: "Tool execution aborted",
  589                metadata: { ...metadata, interrupted: true },
  590                time: { start: "time" in part.state ? part.state.time.start : end, end },
  591              },
  592            })
  593          }
  594          ctx.toolcalls = {}
  595          ctx.assistantMessage.time.completed = Date.now()
  596          yield* session.updateMessage(ctx.assistantMessage)
  597        })
为什么相信这条结论?查看 3 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

模型的思考、文字、每次工具起止和文件变化都不是终端里一闪而过,而是独立可回放的事件。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
stream processor 把 reasoning、text、tool、usage、patch 全部事件化持久packages/opencode/src/session/processor.ts:315模型的思考、文字、每次工具起止和文件变化都不是终端里一闪而过,而是独立可回放的事件。
Provider 层是 AI SDK 适配矩阵,不只兼容 OpenAI APIpackages/opencode/src/provider/provider.ts:101它不是把所有厂商硬塞成同一种 HTTP;每家方言由独立适配器翻译。
请求准备层统一合并 prompt、variant、provider options 与 hookspackages/opencode/src/session/llm/request.ts:56模型请求像一张多层样式表:厂商默认、模型设置、Agent 设置、当前档位和插件逐层覆盖。
04
L1 · fact · opencode-stream-001

stream processor 把 reasoning、text、tool、usage、patch 全部事件化持久

先看源码事实

processor 为 reasoning/text/tool call 建 part,tool result 归一化附件;step-finish 记录 finish、cost、tokens、snapshot 和 patch,并异步更新 session summary。

翻译成白话

模型的思考、文字、每次工具起止和文件变化都不是终端里一闪而过,而是独立可回放的事件。

为什么这对自研重要

TUI、Web、Desktop、ACP 可以消费同一事件模型。

固定提交源码摘录
  315            case "tool-input-start":
  316              if (ctx.assistantMessage.summary) {
  317                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  318              }
  319              yield* ensureToolCall(value)
  320              return
  321  
  322            case "tool-input-delta":
  323              yield* ensureToolCall(value)
  324              return
  325  
  326            case "tool-input-end": {
  327              yield* ensureToolCall(value)
  328              return
  329            }
  330  
  331            case "tool-call": {
  332              if (ctx.assistantMessage.summary) {
  333                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  334              }
  335              yield* ensureToolCall(value)
  336              const input = isRecord(value.input) ? value.input : { value: value.input }
  337              yield* updateToolCall(value.id, (match) => ({
  338                ...match,
  339                tool: value.name,
      … 63 lines omitted; exact range 315–413 …
  403              const attachments = normalized.filter(Exit.isSuccess).map((item) => item.value)
  404              const output = {
  405                ...rawOutput,
  406                output:
  407                  omitted === 0
  408                    ? rawOutput.output
  409                    : `${rawOutput.output}\n\n[${omitted} image${omitted === 1 ? "" : "s"} omitted: could not be resized below the image size limit.]`,
  410                attachments: attachments.length ? attachments : undefined,
  411              }
  412              yield* completeToolCall(value.id, output)
  413              return
为什么相信这条结论?查看 3 处证据
05
L1 · fact · opencode-provider-001

Provider 层是 AI SDK 适配矩阵,不只兼容 OpenAI API

先看源码事实

内置动态加载 Anthropic、OpenAI/Azure、Google/Vertex、Bedrock、xAI、Mistral、Groq、Cohere、OpenRouter、GitHub Copilot 等 SDK,并为不同 provider 决定 responses/chat/messages、认证和区域模型规则。

翻译成白话

它不是把所有厂商硬塞成同一种 HTTP;每家方言由独立适配器翻译。

为什么这对自研重要

模型覆盖广,但 provider 特例数量大,回归测试成本高。

固定提交源码摘录
  101  type BundledSDK = {
  102    languageModel(modelId: string): LanguageModelV3
  103    chat?: (modelId: string) => LanguageModelV3
  104    responses?: (modelId: string) => LanguageModelV3
  105  }
  106  
  107  const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
  108    "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
  109    "@ai-sdk/amazon-bedrock/mantle": () => import("@ai-sdk/amazon-bedrock/mantle").then((m) => m.createBedrockMantle),
  110    "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
  111    "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
  112    "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
  113    "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
  114    "@ai-sdk/google-vertex/anthropic": () =>
  115      import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
  116    "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
  117    "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
  118    "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
  119    "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
  120    "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
  121    "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
  122    "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
  123    "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
  124    "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
  125    "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
      … 9 lines omitted; exact range 101–145 …
  135  
  136  type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>, model?: Model) => Promise<any>
  137  type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
  138  type CustomDiscoverModels = () => Promise<Record<string, Model>>
  139  type CustomLoader = (provider: Info) => Effect.Effect<{
  140    autoload: boolean
  141    getModel?: CustomModelLoader
  142    vars?: CustomVarsLoader
  143    options?: Record<string, any>
  144    discoverModels?: CustomDiscoverModels
  145  }>
为什么相信这条结论?查看 3 处证据
06
L1 · fact · opencode-provider-002

请求准备层统一合并 prompt、variant、provider options 与 hooks

先看源码事实

system 由 agent/provider prompt、环境/项目指令和 user system 合成;参数按 provider base→model→agent→variant 合并,plugins 可改 system、params、headers;工具按 permission 和本轮 user.tools 过滤后排序。

翻译成白话

模型请求像一张多层样式表:厂商默认、模型设置、Agent 设置、当前档位和插件逐层覆盖。

为什么这对自研重要

可扩展性强,但必须记录最终展开结果才能排查“为什么这轮表现不同”。

固定提交源码摘录
   56  export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: PrepareInput) {
   57    const isOpenaiOauth = input.provider.id === "openai" && input.auth?.type === "oauth"
   58    const system = [
   59      [
   60        ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
   61        ...input.system,
   62        ...(input.user.system ? [input.user.system] : []),
   63      ]
   64        .filter((x) => x)
   65        .join("\n"),
   66    ]
   67  
   68    const header = system[0]
   69    yield* input.plugin.trigger(
   70      "experimental.chat.system.transform",
   71      { sessionID: input.sessionID, model: input.model },
   72      { system },
   73    )
   74    if (system.length > 2 && system[0] === header) {
   75      const rest = system.slice(1)
   76      system.length = 0
   77      system.push(header, rest.join("\n"))
   78    }
   79  
   80    const variant =
      … 9 lines omitted; exact range 56–100 …
   90        })
   91    const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
   92    if (
   93      input.model.api.npm === "@ai-sdk/azure" &&
   94      (input.provider.options.useCompletionUrls || input.model.options.useCompletionUrls || options.useCompletionUrls)
   95    ) {
   96      delete options.reasoningSummary
   97      delete options.include
   98    }
   99    if (isOpenaiOauth) options.instructions = system.join("\n")
  100  
为什么相信这条结论?查看 3 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

它会拆开一条复杂命令,分别看里面有哪些子命令和文件路径,而不是只看第一个单词。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
shell 权限不是简单字符串前缀,而是 Bash/PowerShell 语法树扫描packages/opencode/src/tool/shell.ts:257它会拆开一条复杂命令,分别看里面有哪些子命令和文件路径,而不是只看第一个单词。
工具注册表统一 builtin、项目脚本和 npm/file plugin 工具packages/opencode/src/tool/registry.ts:86内置扳手、项目自制工具和插件工具最后都进同一个工具箱,走同一套执行上下文。
Edit 在写前生成 diff、请求权限,写后格式化并回送 LSP 错误packages/opencode/src/tool/edit.ts:35先把拟修改内容展示给门卫,再落盘;落盘后自动格式化,并立刻告诉模型有没有新语法错误。
MCP 同时支持 stdio、Streamable HTTP、SSE、OAuth、prompts 和 resourcespackages/opencode/src/mcp/index.ts:164既能在本机拉起一个工具进程,也能连远程工具站;不只会调函数,还能取提示模板和资料。
07
L1 · fact · opencode-shell-001

shell 权限不是简单字符串前缀,而是 Bash/PowerShell 语法树扫描

先看源码事实

tree-sitter 解析 Bash/PowerShell,逐 command 生成 permission pattern 和 arity-based always pattern;对 rm/cp/mv/cat/PowerShell 文件 cmdlet 解析路径,项目外目录单独请求 external_directory。

翻译成白话

它会拆开一条复杂命令,分别看里面有哪些子命令和文件路径,而不是只看第一个单词。

为什么这对自研重要

比 startsWith 白名单稳健,但动态变量/命令替换的静态路径推断仍有天然边界。

固定提交源码摘录
  257  const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boolean) {
  258    const tree = yield* Effect.promise(() => parser().then((p) => (ps ? p.ps : p.bash).parse(command)))
  259    if (!tree) throw new Error("Failed to parse command")
  260    return tree
  261  })
  262  
  263  const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan, input: { command: string }) {
  264    if (scan.dirs.size > 0) {
  265      const directories = Array.from(scan.dirs)
  266      const globs = directories.map((dir) => {
  267        if (process.platform === "win32") return FSUtil.normalizePathPattern(path.join(dir, "*"))
  268        return path.join(dir, "*")
  269      })
  270      yield* ctx.ask({
  271        permission: "external_directory",
  272        patterns: globs,
  273        always: globs,
  274        metadata: {
  275          command: input.command,
  276          directories,
  277          patterns: globs,
  278        },
  279      })
  280    }
  281  
  282    if (scan.patterns.size === 0) return
  283    yield* ctx.ask({
  284      permission: ShellID.ToolID,
  285      patterns: Array.from(scan.patterns),
  286      always: Array.from(scan.always),
  287      metadata: {
  288        command: input.command,
  289      },
  290    })
  291  })
为什么相信这条结论?查看 3 处证据
08
L1 · fact · opencode-tool-001

工具注册表统一 builtin、项目脚本和 npm/file plugin 工具

先看源码事实

registry 初始化 read/grep/glob/edit/write/apply_patch/shell/task/web/skill/LSP 等 builtins,扫描 config directories 的 tool(s)/*.js|ts,再合并 plugin.tool;所有 plugin 输出走统一 truncation 与 tracing。

翻译成白话

内置扳手、项目自制工具和插件工具最后都进同一个工具箱,走同一套执行上下文。

为什么这对自研重要

能力面一致,但本地 JS/TS plugin 是可执行代码,信任边界等同于宿主进程。

固定提交源码摘录
   86  const layer = Layer.effect(
   87    Service,
   88    Effect.gen(function* () {
   89      const config = yield* Config.Service
   90      const plugin = yield* Plugin.Service
   91      const agents = yield* Agent.Service
   92      const truncate = yield* Truncate.Service
   93      const flags = yield* RuntimeFlags.Service
   94      const mcp = yield* MCP.Service
   95  
   96      const invalid = yield* InvalidTool
   97      const task = yield* TaskTool
   98      const read = yield* ReadTool
   99      const question = yield* QuestionTool
  100      const todo = yield* TodoWriteTool
  101      const lsptool = yield* LspTool
  102      const plan = yield* PlanExitTool
  103      const webfetch = yield* WebFetchTool
  104      const websearch = yield* WebSearchTool
  105      const shell = yield* ShellTool
  106      const globtool = yield* GlobTool
  107      const writetool = yield* WriteTool
  108      const edit = yield* EditTool
  109      const greptool = yield* GrepTool
  110      const patchtool = yield* ApplyPatchTool
      … 54 lines omitted; exact range 86–175 …
  165                }).pipe(
  166                  Effect.withSpan("Tool.execute", {
  167                    attributes: {
  168                      "tool.name": id,
  169                      "session.id": toolCtx.sessionID,
  170                      "message.id": toolCtx.messageID,
  171                      ...(toolCtx.callID ? { "tool.call_id": toolCtx.callID } : {}),
  172                    },
  173                  }),
  174                ),
  175            }
为什么相信这条结论?查看 3 处证据
09
L1 · fact · opencode-edit-001

Edit 在写前生成 diff、请求权限,写后格式化并回送 LSP 错误

先看源码事实

每文件 semaphore 防并发覆盖;路径先做 external-directory check,精确/容错 replacer 计算新内容和 diff,permission metadata 携带 diff;写入保持 BOM/换行,formatter 后重新读,最后触发 file events 和 LSP diagnostics。

翻译成白话

先把拟修改内容展示给门卫,再落盘;落盘后自动格式化,并立刻告诉模型有没有新语法错误。

为什么这对自研重要

编辑是有事务边界感的工具,而非裸 fs.writeFile。

固定提交源码摘录
   35  const locks = new Map<string, Semaphore.Semaphore>()
   36  
   37  function lock(filePath: string) {
   38    const resolvedFilePath = FSUtil.resolve(filePath)
   39    const hit = locks.get(resolvedFilePath)
   40    if (hit) return hit
   41  
   42    const next = Semaphore.makeUnsafe(1)
   43    locks.set(resolvedFilePath, next)
   44    return next
   45  }
   46  
   47  export const Parameters = Schema.Struct({
   48    filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
   49    oldString: Schema.String.annotate({ description: "The text to replace" }),
   50    newString: Schema.String.annotate({
   51      description: "The text to replace it with (must be different from oldString)",
   52    }),
   53    replaceAll: Schema.optional(Schema.Boolean).annotate({
   54      description: "Replace all occurrences of oldString (default false)",
   55    }),
   56  })
为什么相信这条结论?查看 4 处证据
10
L1 · fact · opencode-mcp-001

MCP 同时支持 stdio、Streamable HTTP、SSE、OAuth、prompts 和 resources

先看源码事实

local MCP 直接启动 configured command;remote 依次尝试 StreamableHTTP/SSE,支持 headers、timeout 和 OAuth。连接后缓存 tool defs/instructions,并暴露 prompts/resources/templates/readResource。

翻译成白话

既能在本机拉起一个工具进程,也能连远程工具站;不只会调函数,还能取提示模板和资料。

为什么这对自研重要

连接器覆盖完整,但 local MCP 与 plugin 一样拥有宿主代码执行权。

固定提交源码摘录
  164  export interface Interface {
  165    readonly status: () => Effect.Effect<Record<string, Status>>
  166    readonly clients: () => Effect.Effect<Record<string, MCPClient>>
  167    readonly instructions: () => Effect.Effect<ServerInstructions[]>
  168    readonly tools: () => Effect.Effect<Record<string, McpTool>>
  169    readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
  170    readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
  171    readonly resourceTemplates: (
  172      clientName?: string,
  173    ) => Effect.Effect<Record<string, ResourceTemplateInfo & { client: string }>>
  174    readonly add: (name: string, mcp: ConfigMCPV1.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
  175    readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
  176    readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
  177    readonly getPrompt: (
  178      clientName: string,
  179      name: string,
  180      args?: Record<string, string>,
  181    ) => Effect.Effect<Awaited<ReturnType<MCPClient["getPrompt"]>> | undefined>
  182    readonly readResource: (
  183      clientName: string,
  184      resourceUri: string,
  185    ) => Effect.Effect<Awaited<ReturnType<MCPClient["readResource"]>> | undefined>
  186    readonly startAuth: (
  187      mcpName: string,
  188    ) => Effect.Effect<{ authorizationUrl: string; oauthState: string }, NotFoundError>
  189    readonly authenticate: (
  190      mcpName: string,
  191      onAuthorization?: (authorizationUrl: string) => void,
  192    ) => Effect.Effect<Status, NotFoundError>
  193    readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
  194    readonly removeAuth: (mcpName: string) => Effect.Effect<void>
  195    readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
  196    readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
  197    readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
  198  }
为什么相信这条结论?查看 4 处证据
11
L1 · fact · opencode-plugin-001

插件是进程内代码,可改 prompt、请求、工具定义和执行结果

先看源码事实

loader 可按 npm/file spec 安装、检查版本兼容并 dynamic import;运行时 hooks 包括 system/messages/params/headers、compaction、tool.definition、tool.execute.before/after、text.complete、shell.env 等。

翻译成白话

插件不是只提供一段文字,而是能伸手进模型请求和工具执行链,因此能力很强、信任也很高。

为什么这对自研重要

需要插件来源签名、版本 pin、allowlist 与隔离策略;当前 dynamic import 与主进程同权限。

固定提交源码摘录
   76    // Normalize a config item into the loader's internal representation.
   77    function plan(item: ConfigPluginV1.Spec): Plan {
   78      const spec = ConfigPlugin.pluginSpecifier(item)
   79      return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
   80    }
   81  
   82    // Resolve a configured plugin into a concrete entrypoint that can later be imported.
   83    //
   84    // The stages here intentionally separate install/target resolution, entrypoint detection,
   85    // and compatibility checks so callers can report the exact reason a plugin was skipped.
   86    export async function resolve(
   87      plan: Plan,
   88      kind: PluginKind,
   89    ): Promise<
   90      | { ok: true; value: Resolved }
   91      | { ok: false; stage: "missing"; value: Missing }
   92      | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
   93    > {
   94      // First make sure the plugin exists locally, installing npm plugins on demand.
   95      let target = ""
   96      try {
   97        target = await resolvePluginTarget(plan.spec)
   98      } catch (error) {
   99        return { ok: false, stage: "install", error }
  100      }
      … 33 lines omitted; exact range 76–144 …
  134  
  135    // Import the resolved module only after all earlier validation has succeeded.
  136    export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
  137      let mod
  138      try {
  139        mod = await import(row.entry)
  140      } catch (error) {
  141        return { ok: false, error }
  142      }
  143      if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
  144      return { ok: true, value: { ...row, mod } }
为什么相信这条结论?查看 4 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

它会先给模型回答预留座位,再判断历史是否坐满,避免输入刚好塞满后没有空间输出。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
overflow 阈值为可用输入窗口,而非模型总窗口packages/opencode/src/session/overflow.ts:8它会先给模型回答预留座位,再判断历史是否坐满,避免输入刚好塞满后没有空间输出。
压缩保留近期原文尾部,而不是只剩一段摘要packages/opencode/src/session/compaction.ts:28老故事写成摘要,最近几轮尽量保留原话;必要时甚至保留半个超长回合。
摘要前先去媒体、限制工具输出,失败时可 replay 原请求packages/opencode/src/session/compaction.ts:289整理书桌前先把大图片和长日志搬走;如果一张附件让请求根本进不了门,就去掉附件后把原问题再问一次。
工具输出先独立裁剪,旧输出还能在后台 prunepackages/opencode/src/tool/truncate.ts:13长日志不会直接淹没聊天:模型看摘要,全文留在旁边可查;更老的工具输出再逐步从上下文退场。
12
L1 · fact · opencode-context-001

overflow 阈值为可用输入窗口,而非模型总窗口

先看源码事实

usable 优先使用 model.limit.input;否则从 context 中扣除最大输出 token,reserved 默认最多 20k。token total 达到 usable 才判 overflow,auto=false 时关闭。

翻译成白话

它会先给模型回答预留座位,再判断历史是否坐满,避免输入刚好塞满后没有空间输出。

为什么这对自研重要

比简单按 context×百分比更贴合各 provider 的 input/output 限制。

固定提交源码摘录
    8  const COMPACTION_BUFFER = 20_000
    9  
   10  export function usable(input: { cfg: ConfigV1.Info; model: Provider.Model; outputTokenMax?: number }) {
   11    const context = input.model.limit.context
   12    if (context === 0) return 0
   13  
   14    const reserved =
   15      input.cfg.compaction?.reserved ??
   16      Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
   17    return input.model.limit.input
   18      ? Math.max(0, input.model.limit.input - reserved)
   19      : Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
   20  }
   21  
   22  export function isOverflow(input: {
   23    cfg: ConfigV1.Info
   24    tokens: SessionV1.Assistant["tokens"]
   25    model: Provider.Model
   26    outputTokenMax?: number
   27  }) {
   28    if (input.cfg.compaction?.auto === false) return false
   29    if (input.model.limit.context === 0) return false
   30  
   31    const count =
   32      input.tokens.total || input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write
   33    return count >= usable(input)
为什么相信这条结论?查看 2 处证据
13
L1 · fact · opencode-context-002

压缩保留近期原文尾部,而不是只剩一段摘要

先看源码事实

默认尝试保留最近 2 个 user turn,预算为 usable 的 25%、上下限 2k–8k;若整轮放不下可从轮中切分。模型视图被重排为 compaction marker、summary、retained tail、continue。

翻译成白话

老故事写成摘要,最近几轮尽量保留原话;必要时甚至保留半个超长回合。

为什么这对自研重要

降低摘要丢失当前执行细节的风险,但消息顺序投影更复杂。

固定提交源码摘录
   28  export const PRUNE_MINIMUM = 20_000
   29  export const PRUNE_PROTECT = 40_000
   30  const TOOL_OUTPUT_MAX_CHARS = 2_000
   31  const PRUNE_PROTECTED_TOOLS = ["skill"]
   32  const DEFAULT_TAIL_TURNS = 2
   33  const MIN_PRESERVE_RECENT_TOKENS = 2_000
   34  const MAX_PRESERVE_RECENT_TOKENS = 8_000
   35  type Turn = {
为什么相信这条结论?查看 3 处证据
14
L1 · fact · opencode-context-003

摘要前先去媒体、限制工具输出,失败时可 replay 原请求

先看源码事实

compaction 对历史去除媒体并把单个工具输出限制 2000 字符;可把前次 summary 带入增量摘要,plugins 可替换 prompt/加 context。若 provider 因大媒体直接 overflow,会回退到之前 user turn,压缩后以文字占位重放。

翻译成白话

整理书桌前先把大图片和长日志搬走;如果一张附件让请求根本进不了门,就去掉附件后把原问题再问一次。

为什么这对自研重要

覆盖了“请求发送前就超限”和“响应后发现临界”两类路径。

固定提交源码摘录
  289      const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: {
  290        parentID: MessageID
  291        messages: SessionV1.WithParts[]
  292        sessionID: SessionID
  293        auto: boolean
  294        overflow?: boolean
  295      }) {
  296        const parent = input.messages.findLast((m) => m.info.id === input.parentID)
  297        if (!parent || parent.info.role !== "user") {
  298          throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
  299        }
  300        const userMessage = parent.info
  301        const compactionPart = parent.parts.find((part): part is SessionV1.CompactionPart => part.type === "compaction")
  302  
  303        let messages = input.messages
  304        let replay:
  305          | {
  306              info: SessionV1.User
  307              parts: SessionV1.Part[]
  308            }
  309          | undefined
  310        if (input.overflow) {
  311          const idx = input.messages.findIndex((m) => m.info.id === input.parentID)
  312          for (let i = idx - 1; i >= 0; i--) {
  313            const msg = input.messages[i]
      … 30 lines omitted; exact range 289–354 …
  344          "experimental.session.compacting",
  345          { sessionID: input.sessionID },
  346          { context: [], prompt: undefined },
  347        )
  348        const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
  349        const msgs = structuredClone(selected.head)
  350        yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
  351        const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
  352          stripMedia: true,
  353          toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
  354        })
为什么相信这条结论?查看 3 处证据
15
L1 · fact · opencode-context-004

工具输出先独立裁剪,旧输出还能在后台 prune

先看源码事实

普通工具输出默认上限 2000 行/50KiB,全文保存到临时文件 7 天并给 Grep/Read 或 subagent 指引;compaction prune 在保护最近 40k token 后,累计可释放超过 20k 才标记旧 tool output compacted,skill 输出受保护。

翻译成白话

长日志不会直接淹没聊天:模型看摘要,全文留在旁边可查;更老的工具输出再逐步从上下文退场。

为什么这对自研重要

上下文治理覆盖生成前、工具后和会话历史三层。

固定提交源码摘录
   13  const RETENTION = Duration.days(7)
   14  
   15  export const MAX_LINES = 2000
   16  export const MAX_BYTES = 50 * 1024
   17  export const DIR = TRUNCATION_DIR
   18  export const GLOB = path.join(TRUNCATION_DIR, "*")
   19  
   20  export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
   21  
   22  export interface Options {
   23    maxLines?: number
   24    maxBytes?: number
   25    direction?: "head" | "tail"
   26  }
   27  
   28  function hasTaskTool(agent?: Agent.Info) {
   29    if (!agent?.permission) return false
   30    return evaluate("task", "*", agent.permission).action !== "deny"
   31  }
   32  
   33  export interface Interface {
   34    readonly cleanup: () => Effect.Effect<void>
   35    readonly write: (text: string) => Effect.Effect<string>
   36    /**
   37     * Returns output unchanged when it fits within the limits, otherwise writes the full text
   38     * to the truncation directory and returns a preview plus a hint to inspect the saved file.
   39     */
   40    readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
   41    /**
   42     * Resolved truncation limits: values from `tool_output` in opencode config, or MAX_LINES / MAX_BYTES if unset.
   43     */
   44    readonly limits: () => Effect.Effect<{ maxLines: number; maxBytes: number }>
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

越靠后的规则优先;没写明能不能做时先问人。点“始终允许”会记住本次运行,但不是永久改配置。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
权限采用 last-match wildcard 规则,默认 ask 而非默认 allowpackages/opencode/src/permission/index.ts:28越靠后的规则优先;没写明能不能做时先问人。点“始终允许”会记住本次运行,但不是永久改配置。
默认 build 可执行,但敏感文件、外部目录和死循环会询问packages/opencode/src/agent/agent.ts:98日常改项目尽量顺畅,碰到密钥文件、项目外路径或重复工具循环才踩刹车;规划和探索 Agent 的能力更窄。
连续相同工具调用触发 doom-loop 二次确认packages/opencode/src/session/processor.ts:331模型若一直用相同参数撞同一扇门,系统不会无限烧 token,而是停下来问人。
shell 最终直接启动宿主子进程,没有内建 OS 沙箱packages/opencode/src/tool/shell.ts:293权限门像门卫,但命令一旦放行就在你的真实机器上跑,并不是在一次性隔离箱里。
16
L1 · fact · opencode-permission-001

权限采用 last-match wildcard 规则,默认 ask 而非默认 allow

先看源码事实

evaluate 从所有 ruleset 尾部找最后匹配 permission+pattern 的规则,无匹配返回 ask。ask 可 deny、直接 allow 或发事件等待 reply;always approval 只在当前 instance state 追加规则并自动解开同 session 匹配请求。

翻译成白话

越靠后的规则优先;没写明能不能做时先问人。点“始终允许”会记住本次运行,但不是永久改配置。

为什么这对自研重要

顺序非常重要,配置合并必须可解释;临时批准不会静默写回磁盘。

固定提交源码摘录
   28  export function evaluate(permission: string, pattern: string, ...rulesets: PermissionV1.Ruleset[]): PermissionV1.Rule {
   29    return (
   30      rulesets
   31        .flat()
   32        .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? {
   33        action: "ask",
   34        permission,
   35        pattern: "*",
   36      }
   37    )
为什么相信这条结论?查看 3 处证据
17
L1 · fact · opencode-permission-002

默认 build 可执行,但敏感文件、外部目录和死循环会询问

先看源码事实

默认权限总体 allow;doom_loop 与 external_directory 为 ask,.env/.env.* 为 ask、.env.example allow。build 开放 question/plan_enter;plan 禁 edit,仅允许计划文件;explore 只开放搜索/读取/web/bash 等白名单。

翻译成白话

日常改项目尽量顺畅,碰到密钥文件、项目外路径或重复工具循环才踩刹车;规划和探索 Agent 的能力更窄。

为什么这对自研重要

Agent profile 本身就是 capability bundle。

固定提交源码摘录
   98      const state = yield* InstanceState.make<State>(
   99        Effect.fn("Agent.state")(function* (ctx) {
  100          const cfg = yield* config.get()
  101          const skillDirs = yield* skill.dirs()
  102          const referenceDirs = Object.keys(cfg.references ?? cfg.reference ?? {}).length
  103            ? yield* Effect.gen(function* () {
  104                yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
  105                return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
  106              }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
  107            : []
  108          const whitelistedDirs = [
  109            Truncate.GLOB,
  110            path.join(Global.Path.tmp, "*"),
  111            ...skillDirs.map((dir) => path.join(dir, "*")),
  112            ...referenceDirs.map((dir) => path.join(dir, "*")),
  113          ]
  114          const readonlyExternalDirectory = {
  115            "*": "ask",
  116            ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
  117          } satisfies Record<string, "allow" | "ask" | "deny">
  118  
  119          const defaults = Permission.fromConfig({
  120            "*": "allow",
  121            doom_loop: "ask",
  122            external_directory: {
      … 22 lines omitted; exact range 98–155 …
  145              permission: Permission.merge(
  146                defaults,
  147                Permission.fromConfig({
  148                  question: "allow",
  149                  plan_enter: "allow",
  150                }),
  151                user,
  152              ),
  153              mode: "primary",
  154              native: true,
  155            },
为什么相信这条结论?查看 3 处证据
18
L1 · fact · opencode-permission-003

连续相同工具调用触发 doom-loop 二次确认

先看源码事实

processor 检查最近固定阈值的 parts;若工具名和 JSON 输入全部相同且非 pending,就请求 doom_loop 权限。

翻译成白话

模型若一直用相同参数撞同一扇门,系统不会无限烧 token,而是停下来问人。

为什么这对自研重要

这是运行时行为检测,不依赖模型自觉。

固定提交源码摘录
  331            case "tool-call": {
  332              if (ctx.assistantMessage.summary) {
  333                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  334              }
  335              yield* ensureToolCall(value)
  336              const input = isRecord(value.input) ? value.input : { value: value.input }
  337              yield* updateToolCall(value.id, (match) => ({
  338                ...match,
  339                tool: value.name,
  340                state:
  341                  match.state.status === "running"
  342                    ? { ...match.state, input }
  343                    : {
  344                        status: "running",
  345                        input,
  346                        time: { start: Date.now() },
  347                      },
  348                metadata: match.metadata?.providerExecuted
  349                  ? { ...value.providerMetadata, providerExecuted: true }
  350                  : value.providerMetadata,
  351              }))
  352  
  353              const parts = yield* MessageV2.parts(ctx.assistantMessage.id).pipe(
  354                Effect.provideService(Database.Service, database),
  355              )
      … 14 lines omitted; exact range 331–380 …
  370  
  371              const agent = yield* agents.get(ctx.assistantMessage.agent)
  372              yield* permission.ask({
  373                permission: "doom_loop",
  374                patterns: [value.name],
  375                sessionID: ctx.assistantMessage.sessionID,
  376                metadata: { tool: value.name, input },
  377                always: [value.name],
  378                ruleset: agent.permission,
  379              })
  380              return
为什么相信这条结论?查看 1 处证据
19
L1 · limitation · opencode-sandbox-001

shell 最终直接启动宿主子进程,没有内建 OS 沙箱

先看源码事实

ShellTool 用真实环境变量和 plugin env,在当前/指定 cwd 通过 ChildProcess 启动系统 shell;超时或 abort 后 kill。源码未施加容器、namespace、seccomp、Seatbelt 或 Windows job sandbox。

翻译成白话

权限门像门卫,但命令一旦放行就在你的真实机器上跑,并不是在一次性隔离箱里。

为什么这对自研重要

全自动或不可信仓库需要外层 VM/container;permission 不应被宣传成 sandbox。

固定提交源码摘录
  293  function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
  294    if (process.platform === "win32" && Shell.ps(shell)) {
  295      return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], {
  296        cwd,
  297        env,
  298        stdin: "ignore",
  299        detached: false,
  300      })
  301    }
  302  
  303    return ChildProcess.make(command, [], {
  304      shell,
  305      cwd,
  306      env,
  307      stdin: "ignore",
  308      detached: process.platform !== "win32",
  309    })
为什么相信这条结论?查看 3 处证据
20
L1 · fact · opencode-mcp-002

MCP OAuth 有 state 校验,但远程连接没有内建 SSRF 私网拦截

先看源码事实

OAuth state 用 32 随机字节并在 callback 后比对,错配报潜在 CSRF;remoteURL 只验证 URL 可解析,StreamableHTTP/SSE 直接使用目标 URL,未见 private/loopback/metadata IP guard。

翻译成白话

登录回调防伪造做了,但“这个 MCP 地址是不是公司内网或云元数据地址”没有额外门卫。

为什么这对自研重要

在服务端/共享环境运行时,应由网络策略或 URL guard 补上 SSRF 边界。

固定提交源码摘录
  123  function remoteURL(value: string) {
  124    if (URL.canParse(value)) return new URL(value)
  125  }
为什么相信这条结论?查看 3 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

公司总规章开会前发,走进某个子目录时再补当地规章;同一轮不会反复念。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
指令既有全局/项目 system 层,也有读文件时的局部层packages/opencode/src/session/instruction.ts:60公司总规章开会前发,走进某个子目录时再补当地规章;同一轮不会反复念。
Skills 支持 OpenCode、Claude、agents 目录与远程 discoverypackages/opencode/src/skill/index.ts:21它能复用多种 Agent 生态的技能目录,也能从远程拉技能;最终只有当前 Agent 有权用的技能会出现。
21
L1 · fact · opencode-instruction-001

指令既有全局/项目 system 层,也有读文件时的局部层

先看源码事实

system 搜索全局 AGENTS.md 或 ~/.claude/CLAUDE.md,并在项目树中选择首类 AGENTS/CLAUDE/CONTEXT;config 还能引用 glob 与 URL。Read 某文件时沿目录向上找更近指令,每条 assistant message 只附一次,已在未压缩 Read 中加载的也不重复。

翻译成白话

公司总规章开会前发,走进某个子目录时再补当地规章;同一轮不会反复念。

为什么这对自研重要

层级指令与代码位置绑定,适合 monorepo,但 remote instruction URL 引入供应链和可用性风险。

固定提交源码摘录
   60      const globalFiles = [
   61        path.join(global.config, "AGENTS.md"),
   62        ...(!flags.disableClaudeCodePrompt ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),
   63      ]
   64      const instructionFiles = [
   65        "AGENTS.md",
   66        ...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []),
   67        "CONTEXT.md", // deprecated
   68      ]
   69  
   70      const state = yield* InstanceState.make(
   71        Effect.fn("Instruction.state")(() =>
   72          Effect.succeed({
   73            // Track which instruction files have already been attached for a given assistant message.
   74            claims: new Map<MessageID, Set<string>>(),
   75          }),
   76        ),
   77      )
   78  
   79      const relative = Effect.fnUntraced(function* (instruction: string) {
   80        const ctx = yield* InstanceState.context
   81        if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
   82          return yield* fs
   83            .globUp(instruction, ctx.directory, ctx.worktree)
   84            .pipe(Effect.catch(() => Effect.succeed([] as string[])))
   85        }
   86        return yield* fs
   87          .globUp(instruction, global.config, global.config)
   88          .pipe(Effect.catch(() => Effect.succeed([] as string[])))
   89      })
为什么相信这条结论?查看 3 处证据
22
L1 · fact · opencode-skill-001

Skills 支持 OpenCode、Claude、agents 目录与远程 discovery

先看源码事实

扫描 ~/.claude/.agents 及项目祖先的 skills/**/SKILL.md、各 config dir 的 skill(s)、显式 paths 和 URLs;解析 frontmatter,重复 name 后加载者覆盖并告警,available 再按 agent skill permission 过滤。

翻译成白话

它能复用多种 Agent 生态的技能目录,也能从远程拉技能;最终只有当前 Agent 有权用的技能会出现。

为什么这对自研重要

兼容性强,但远程技能内容属于 prompt 供应链,应配合 pin/hash/审计。

固定提交源码摘录
   21  const CLAUDE_EXTERNAL_DIR = ".claude"
   22  const AGENTS_EXTERNAL_DIR = ".agents"
   23  const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
   24  const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
   25  const SKILL_PATTERN = "**/SKILL.md"
   26  
   27  // Built-in skill that ships with opencode. The model's intuition for what an
   28  // opencode.json should look like is often wrong, and opencode hard-fails on
   29  // invalid config, so users hit cryptic startup errors. Loading this skill
   30  // when the model is asked to touch opencode's own config files gives it the
   31  // actual schemas instead of guesses.
   32  const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode"
   33  const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION =
   34    "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself."
   35  const CUSTOMIZE_OPENCODE_SKILL_BODY = SkillPlugin.CustomizeOpencodeContent
   36  
   37  export const Info = Schema.Struct({
   38    name: Schema.String,
   39    description: Schema.optional(Schema.String),
   40    location: Schema.String,
   41    content: Schema.String,
   42  })
   43  export type Info = Schema.Schema.Type<typeof Info>
为什么相信这条结论?查看 3 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

子任务有自己的聊天记录,不是父对话里的一段临时函数;父亲的禁区会传下去,默认也不能无限生孩子。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
子 Agent 是独立持久 session,可恢复、限深度并继承关键 denypackages/opencode/src/agent/subagent-permissions.ts:4子任务有自己的聊天记录,不是父对话里的一段临时函数;父亲的禁区会传下去,默认也不能无限生孩子。
子 Agent 支持 foreground/background、结果自动注回和递归取消packages/opencode/src/tool/task.ts:24前台像打电话等对方答完,后台像发工单继续做别的;工单结束会主动回报,父任务取消时孩子也停。
23
L1 · fact · opencode-subagent-001

子 Agent 是独立持久 session,可恢复、限深度并继承关键 deny

先看源码事实

TaskTool 可用 task_id 恢复 child session;新 child 写 parentID、独立 agent/model/permission。默认 subagent_depth=1;child 继承 parent session 的所有 deny 和 external_directory 规则,默认禁 todowrite/task,除非子 Agent 自己显式声明。

翻译成白话

子任务有自己的聊天记录,不是父对话里的一段临时函数;父亲的禁区会传下去,默认也不能无限生孩子。

为什么这对自研重要

上下文隔离、恢复和权限边界都较完整。

固定提交源码摘录
    4  /**
    5   * Build the `permission` ruleset for a subagent's session when it's spawned
    6   * via the task tool. Combines:
    7   *
    8   * 1. The parent session's deny rules and external_directory rules.
    9   *    Parent agent restrictions only govern that agent; the subagent's own
   10   *    permissions determine its capabilities.
   11   * 2. Default `todowrite` and `task` denies if the subagent's own ruleset
   12   *    doesn't already permit them.
   13   */
   14  export function deriveSubagentSessionPermission(input: {
   15    parentSessionPermission: PermissionV1.Ruleset
   16    subagent: Agent.Info
   17  }): PermissionV1.Ruleset {
   18    const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
   19    const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
   20    return [
   21      ...input.parentSessionPermission.filter(
   22        (rule) => rule.permission === "external_directory" || rule.action === "deny",
   23      ),
   24      ...(canTodo ? [] : [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
   25      ...(canTask ? [] : [{ permission: "task" as const, pattern: "*" as const, action: "deny" as const }]),
   26    ]
为什么相信这条结论?查看 3 处证据
24
L1 · fact · opencode-subagent-002

子 Agent 支持 foreground/background、结果自动注回和递归取消

先看源码事实

foreground 等待 child job,background 由实验 flag 开启后立即返回;完成/失败会以 synthetic parent prompt 自动注回。父执行 abort 同时 cancel child prompt/job,删除 parent session 也递归删除 children。

翻译成白话

前台像打电话等对方答完,后台像发工单继续做别的;工单结束会主动回报,父任务取消时孩子也停。

为什么这对自研重要

已经具备实用并行协作骨架,但 background 仍是实验能力。

固定提交源码摘录
   24  const id = "task"
   25  const BACKGROUND_DESCRIPTION = [
   26    "Background mode: background=true launches the subagent asynchronously and returns immediately.",
   27    "Foreground is the default; use it when you need the result before continuing.",
   28    "Use background only for independent work that can run while you continue elsewhere.",
   29    "You will be notified automatically when it finishes.",
   30  ].join(" ")
   31  const BACKGROUND_STARTED = [
   32    "The task is working in the background. You will be notified automatically when it finishes.",
   33    "DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work — avoid working with the same files or topics it is using.",
   34    "Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.",
   35  ].join("\n")
   36  const BACKGROUND_UPDATED = [
   37    "Additional context sent to the running background task.",
   38    "The task is still working in the background. You will be notified automatically when it finishes.",
   39    "DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work — avoid working with the same files or topics it is using.",
   40    "Work on non-overlapping tasks, or briefly tell the user what you sent and end your response.",
   41  ].join("\n")
   42  
   43  const BaseParameterFields = {
   44    description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
   45    prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
   46    subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
   47    task_id: Schema.optional(Schema.String).annotate({
   48      description:
      … 3 lines omitted; exact range 24–62 …
   52  }
   53  
   54  const BaseParameters = Schema.Struct(BaseParameterFields)
   55  
   56  export const Parameters = Schema.Struct({
   57    ...BaseParameterFields,
   58    background: Schema.optional(Schema.Boolean).annotate({
   59      description:
   60        "Run the agent in the background. You will be notified when it completes. DO NOT sleep, poll, or proactively check on its progress",
   61    }),
   62  })
为什么相信这条结论?查看 4 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

一条任务不只存聊天文本,还存它用了哪个 Agent/模型、花了多少钱、改了哪些文件、能否回退以及是谁的子任务。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
session 持久化 agent/model/permission/cost/tokens/summary/revert 与 parentpackages/opencode/src/session/session.ts:120一条任务不只存聊天文本,还存它用了哪个 Agent/模型、花了多少钱、改了哪些文件、能否回退以及是谁的子任务。
每个模型 step 前后用影子 Git 仓库生成可回退 patchpackages/opencode/src/session/processor.ts:98每走一步都拍“修改前后”照片,照片存进旁边的 Git 仓库,不污染用户当前分支。
Share 是显式远程同步会话、消息、parts、diff 和模型packages/opencode/src/share/share-next.ts:23分享链接不是只上传一张截图,而是持续同步完整任务数据;若开了自动分享,必须按数据出境功能治理。
25
L1 · fact · opencode-observe-001

session 持久化 agent/model/permission/cost/tokens/summary/revert 与 parent

先看源码事实

Session row 包含 parent、agent/model、permission、input/output/reasoning/cache tokens、cost、文件统计、share、revert snapshot 和 timestamps;messages/parts 通过事件桥更新并写数据库。

翻译成白话

一条任务不只存聊天文本,还存它用了哪个 Agent/模型、花了多少钱、改了哪些文件、能否回退以及是谁的子任务。

为什么这对自研重要

为恢复、审计、成本和协作提供统一数据底座。

固定提交源码摘录
  120  export function toRow(info: Info) {
  121    return {
  122      id: info.id,
  123      project_id: info.projectID,
  124      workspace_id: info.workspaceID,
  125      parent_id: info.parentID,
  126      slug: info.slug,
  127      directory: info.directory,
  128      path: info.path,
  129      title: info.title,
  130      agent: info.agent,
  131      model: info.model,
  132      version: info.version,
  133      share_url: info.share?.url,
  134      summary_additions: info.summary?.additions,
  135      summary_deletions: info.summary?.deletions,
  136      summary_files: info.summary?.files,
  137      summary_diffs: info.summary?.diffs,
  138      metadata: info.metadata,
  139      cost: info.cost ?? 0,
  140      tokens_input: (info.tokens ?? EmptyTokens).input,
  141      tokens_output: (info.tokens ?? EmptyTokens).output,
  142      tokens_reasoning: (info.tokens ?? EmptyTokens).reasoning,
  143      tokens_cache_read: (info.tokens ?? EmptyTokens).cache.read,
  144      tokens_cache_write: (info.tokens ?? EmptyTokens).cache.write,
      … 3 lines omitted; exact range 120–158 …
  148            partID: info.revert.partID,
  149            snapshot: info.revert.snapshot,
  150            diff: info.revert.diff,
  151          }
  152        : null,
  153      permission: info.permission,
  154      time_created: info.time.created,
  155      time_updated: info.time.updated,
  156      time_compacting: info.time.compacting,
  157      time_archived: info.time.archived,
  158    }
为什么相信这条结论?查看 3 处证据
26
L1 · fact · opencode-snapshot-001

每个模型 step 前后用影子 Git 仓库生成可回退 patch

先看源码事实

processor 在 stream 前预抓 snapshot,step-finish 后 track 并 patch;Snapshot 使用独立 gitdir,复用源仓对象库/索引,忽略规则同步,跳过大于 2MiB 的未跟踪文件并只 stage 候选路径。

翻译成白话

每走一步都拍“修改前后”照片,照片存进旁边的 Git 仓库,不污染用户当前分支。

为什么这对自研重要

支持精确 diff/revert;非 Git 项目或 snapshot=false 时能力降级。

固定提交源码摘录
   98      const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
   99        // Pre-capture snapshot before the LLM stream starts. The AI SDK
  100        // may execute tools internally before emitting start-step events,
  101        // so capturing inside the event handler can be too late.
  102        const initialSnapshot = yield* snapshot.track()
  103        const ctx: ProcessorContext = {
  104          assistantMessage: input.assistantMessage,
  105          sessionID: input.sessionID,
  106          model: input.model,
  107          toolcalls: {},
  108          shouldBreak: false,
  109          snapshot: initialSnapshot,
  110          blocked: false,
  111          needsCompaction: false,
  112          currentText: undefined,
  113          reasoningMap: {},
  114        }
为什么相信这条结论?查看 5 处证据
27
L1 · risk · opencode-share-001

Share 是显式远程同步会话、消息、parts、diff 和模型

先看源码事实

创建 share 后,event subscribers 将 session/message/part/session_diff/model 聚合并发到 opncd.ai 或组织 console;可用 OPENCODE_DISABLE_SHARE 全局关闭,配置还支持 share=auto。

翻译成白话

分享链接不是只上传一张截图,而是持续同步完整任务数据;若开了自动分享,必须按数据出境功能治理。

为什么这对自研重要

企业默认应关闭或接自有 console,并在 UI 明确展示共享状态与数据范围。

固定提交源码摘录
   23  const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
   24  
   25  export type Api = {
   26    create: string
   27    sync: (shareID: string) => string
   28    remove: (shareID: string) => string
   29    data: (shareID: string) => string
   30  }
   31  
   32  export type Req = {
   33    headers: Record<string, string>
   34    api: Api
   35    baseUrl: string
   36  }
   37  
   38  const ShareSchema = Schema.Struct({
   39    id: Schema.String,
   40    url: Schema.String,
   41    secret: Schema.String,
   42  })
   43  export type Share = typeof ShareSchema.Type
   44  
   45  type State = {
   46    queue: Map<SessionID, Map<string, Data>>
   47    scope: Scope.Closeable
      … 14 lines omitted; exact range 23–72 …
   62        data: SDK.Part
   63      }
   64    | {
   65        type: "session_diff"
   66        data: SDK.SnapshotFileDiff[]
   67      }
   68    | {
   69        type: "model"
   70        data: SDK.Model[]
   71      }
   72  
为什么相信这条结论?查看 4 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

这套实现先回答了什么?

零件台架测试很丰富,但还没有一张公开的整车赛道成绩单。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
关键机制测试密集,但缺统一任务成功率评测闭环packages/opencode/test/session/compaction.test.ts:920零件台架测试很丰富,但还没有一张公开的整车赛道成绩单。
28
L3 · limitation · opencode-maturity-001

关键机制测试密集,但缺统一任务成功率评测闭环

先看源码事实

仓库有 compaction、processor、recorded provider loop、permission、shell、task/background、MCP lifecycle/OAuth、plugin、snapshot、server/ACP 等测试;未发现统一基准将真实 coding task、成本、时延和回归 verdict 连成 eval pipeline。

翻译成白话

零件台架测试很丰富,但还没有一张公开的整车赛道成绩单。

为什么这对自研重要

建设自有 Agent 时应保留其机制级测试,同时补 golden workflow 和端到端 regression harness。

固定提交源码摘录
  920        }
  921      }),
  922    )
  923  
  924    itCompaction.instance(
  925      "persists tail_start_id for retained recent turns",
  926      Effect.gen(function* () {
  927        const ssn = yield* SessionNs.Service
  928        const session = yield* ssn.create({})
  929        yield* createUserMessage(session.id, "first")
  930        const keep = yield* createUserMessage(session.id, "second")
  931        yield* createUserMessage(session.id, "third")
  932        yield* createSummaryCompaction(session.id)
  933  
  934        const msgs = yield* ssn.messages({ sessionID: session.id })
  935        const parent = msgs.at(-1)?.info.id
  936        expect(parent).toBeTruthy()
  937        yield* SessionCompaction.use.process({
  938          parentID: parent!,
  939          messages: msgs,
  940          sessionID: session.id,
  941          auto: false,
  942        })
  943  
  944        const part = yield* readCompactionPart(session.id)
      … 90 lines omitted; exact range 920–1045 …
 1035        }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
 1036      },
 1037      { git: true },
 1038    )
 1039  
 1040    itCompaction.instance(
 1041      "retains a split turn suffix when a later message fits the preserve token budget",
 1042      () => {
 1043        const stub = llm()
 1044        let captured = ""
 1045        stub.push(reply("summary", (input) => (captured = JSON.stringify(input.messages))))
为什么相信这条结论?查看 3 处证据
小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

主循环由持久化消息状态驱动,而不是一次性的 while(tool_call)

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

消息/part 是事实源,loop 是其投影;这比仅在内存追加数组更利于恢复和多客户端。

证据:packages/opencode/src/session/prompt.ts:1081
Q2

每一步动态重建 Agent、工具、系统上下文和模型请求

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

支持运行中配置和权限变化,但每步装配链更复杂、需要缓存与测试。

证据:packages/opencode/src/session/prompt.ts:1170
Q3

stream processor 把 reasoning、text、tool、usage、patch 全部事件化持久

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

TUI、Web、Desktop、ACP 可以消费同一事件模型。

证据:packages/opencode/src/session/processor.ts:315
Q4

重试、拒绝、上下文溢出和中断有不同终态

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

错误分类是自治可靠性的核心,而不只是 UI 文案。

证据:packages/opencode/src/session/processor.ts:539
Q5

Provider 层是 AI SDK 适配矩阵,不只兼容 OpenAI API

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

模型覆盖广,但 provider 特例数量大,回归测试成本高。

证据:packages/opencode/src/provider/provider.ts:101
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01packages/opencode/src/session/prompt.tsL1081–1130, L1170–1241, L1252–1286
  2. 02packages/opencode/src/session/message-v2.tsL578–600, L521–571
  3. 03packages/opencode/src/session/processor.tsL315–413, L424–483, L486–531, L539–597, L599–625, L627–681, L435–482, L331–380, L98–114, L435–469
  4. 04packages/opencode/src/provider/provider.tsL101–145, L154–238, L294–368
  5. 05packages/opencode/src/session/llm/request.tsL56–100, L114–158, L181–213, L68–78
  6. 06packages/opencode/src/session/overflow.tsL8–33
  7. 07packages/opencode/src/session/compaction.tsL28–35, L180–239, L289–354, L383–413, L422–503, L241–287, L342–350
  8. 08packages/opencode/src/tool/truncate.tsL13–44, L85–140
  9. 09packages/opencode/src/permission/index.tsL28–37, L67–107, L109–166
  10. 10packages/opencode/src/agent/agent.tsL98–155, L156–180, L182–218
  11. 11packages/opencode/src/tool/shell.tsL293–309, L416–425, L481–559, L257–291, L378–413, L597–640
  12. 12packages/opencode/src/tool/registry.tsL86–175, L178–244, L286–334
  13. 13packages/opencode/src/tool/edit.tsL35–56, L79–119, L123–171, L175–211
  14. 14packages/opencode/src/session/instruction.tsL60–89, L110–169, L179–220
  15. 15packages/opencode/src/skill/index.tsL21–43, L105–140, L173–232
  16. 16packages/opencode/src/mcp/index.tsL164–198, L236–338, L340–370, L666–738, L123–125, L806–849, L898–915
  17. 17packages/opencode/src/agent/subagent-permissions.tsL4–26
  18. 18packages/opencode/src/tool/task.tsL104–142, L143–172, L24–62, L200–253, L273–347
  19. 19packages/opencode/src/session/session.tsL608–629, L120–158, L224–244, L631–666
  20. 20packages/opencode/src/snapshot/index.tsL23–27, L195–232, L235–298
  21. 21packages/opencode/src/share/share-next.tsL23–72, L124–200, L247–298
  22. 22packages/opencode/src/config/config.tsL575–577
  23. 23packages/opencode/src/plugin/loader.tsL76–144
  24. 24packages/opencode/src/session/tools.tsL92–129
  25. 25packages/opencode/test/session/compaction.test.tsL920–1045
  26. 26packages/opencode/test/tool/task.test.tsL630–690
  27. 27packages/opencode/test/tool/shell.test.tsL920–1005
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 OpenCode 报告 ↗