主循环 SessionPrompt

四个入口、用户输入的展开、runLoop 的每一步,以及“一个会话只跑一个循环”的并发模型。

作者 David更新于 第 7 篇(共 38 篇)

packages/opencode/src/session/prompt.ts 是 V1 会话引擎的调度中心。它不直接和模型对话——那是流处理器的事——而是决定:这一步用哪个模型、带哪些工具、先压缩还是先跑子任务、什么时候停。

入口

SessionPrompt.Interface(prompt.ts:102)的方法与服务端接口大体一一对应:

方法服务端接口用途
prompt/session/:id/message、/prompt_async写入 user 消息,默认接着跑循环
command/session/:id/command展开斜杠命令模板后转给 prompt
shell/session/:id/shell用户直接执行 shell 命令
loop无不写新消息,从当前历史继续跑
cancel/session/:id/abort中断会话

prompt 本身很薄(prompt.ts:1052):若会话处于撤销状态,先用 revert.cleanup 删掉被撤销的消息;再 createUserMessage;noReply 为真就此返回,否则进入 loop。

createUserMessage:把输入展开成 Part

先定 agent 与模型:模型按“输入 → agent 配置 → 会话当前模型”兜底(prompt.ts:646),与会话头记录不同时调用 setAgentModel 更新。

接着逐个解析输入 Part。这里有个关键设计:用户 @ 的文件被伪装成一次已经发生过的 Read 工具调用。

  • 文本文件:真的执行一次 Read 工具(支持行范围),生成两条 synthetic 文本——“Called the Read tool with the following input: …”和工具输出——再附上原文件 Part。
  • 目录:同样走 Read 工具,得到目录列表。
  • 图片、PDF 等二进制:读成 base64 的 data: URL;MCP 资源同理展开,其二进制内容上限 10 MB。
  • agent Part(@ 某个子代理):保留原 Part,再追加一句 synthetic 提示,让模型调用 task 工具派发。

随后触发插件钩子 chat.message,图片统一缩放,schema 校验失败只记日志、不拒收,最后先写消息、再写 Part。命令模板里的 @路径 则由简化版的 resolvePromptParts 展开。

loop:一个会话只跑一个循环

loop(prompt.ts:1343)只做一件事:把 runLoop 交给 SessionRunState.ensureRunning。SessionRunState(run-state.ts)为每个会话维护一个 Runner(packages/opencode/src/effect/runner.ts),它是四态状态机:Idle、Running、Shell、ShellThenRun。

  • ensureRunning 在 Idle 时启动 runLoop;在 Running 时不另起循环,而是等待现有循环的结果;在 Shell 时把循环排到 shell 命令之后。
  • startShell 只在 Idle 时接受,否则返回 SessionBusyError。
  • cancel 中断正在跑的 fiber,等待者拿到最近一条 assistant 消息;回到 Idle 时 Runner 把会话状态置为 idle,而 busy 由 runLoop 每一步开头自己设置。

因此“连发几条消息”不需要显式队列:新 user 消息先落库,循环每一步都重新读取历史,下一步就会看到它。

runLoop:每一步做什么

图表加载中…

runLoop(prompt.ts:1081)每一步:

  1. 读历史:filterCompactedEffect 返回压缩感知的历史,MessageV2.latest 从中找出最后的 user 与 assistant,以及尚未处理的 compaction / subtask 任务。
  2. 判断是否结束,条件见下方摘录。
  3. 首步杂务:第 1 步 fork 出标题生成与差异统计,都不阻塞主流程。
  4. 取模型:找不到时发布带“Did you mean”建议的 session.error 后终止。
  5. 先办任务:subtask 交给 handleSubtask,compaction 交给上下文压缩;上一条已完成的 assistant 用量已超限时,写入自动压缩请求后重来。
  6. 准备这一步:判断是否已到最大步数;SessionReminders.apply 注入 plan 模式等提醒(见系统提示词);新建 assistant 消息与处理器;SessionTools.resolve 汇总注册表工具与 MCP 工具,要求结构化输出时再加 StructuredOutput 工具。
  7. 组装请求:system 依次是环境信息、指令文件、MCP 说明、Skills 列表(模型家族提示词稍后在 LLM 层前置);历史经 toModelMessagesEffect 转成 AI SDK 消息。
  8. 调用并收尾:handle.process 返回 "compact" | "stop" | "continue"。拿到结构化结果即退出;finish 为 content-filter 时记 ContentFilterError 退出;stop 退出;compact 写入压缩请求,请求真正溢出(finish 为空)时带上 overflow 标记。

循环退出后 fork 一次 compaction.prune,返回最后一条 assistant 消息。第 2 步的结束判断(prompt.ts:1103):

          // Some providers return "stop" even when the assistant message contains
          // tool calls. Keep the loop running so tool results can be sent back to
          // the model, but ignore cleanup-marked interrupted orphans.
          const hasToolCalls =
            lastAssistantMsg?.parts.some(
              (part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
            ) ?? false

          if (
            lastAssistant?.finish &&
            !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
            !hasToolCalls &&
            lastAssistant.parentID === lastUser.id
          ) {

四个条件同时成立才退出:有结束原因;结束原因不是 tool-calls 或 unknown;没有待回传结果的本地工具调用(注释说明有的提供方发起工具调用后仍返回 stop);这条 assistant 回应的正是最后一条 user 消息,保证新到的消息一定会被处理。

最大步数

agent.steps 默认无穷大。到达上限的那一步,工具表照常传入,只在消息末尾追加一条 assistant 角色的 MAX_STEPS_PROMPT(定义在 packages/core/src/session/runner/max-steps.ts),要求模型只用文字总结进度——约束完全靠提示词:

              messages: [
                ...modelMsgs,
                ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS_PROMPT }] : []),
              ],

(packages/opencode/src/session/prompt.ts:1279)

子任务与标题

handleSubtask(prompt.ts:255)不经过主会话的模型:直接新建 assistant 消息和一个 running 的 task 工具 Part,调用 task 工具执行子代理,再按结果改成 completed 或 error。来自斜杠命令的子任务还会追加一条 synthetic user 消息“Summarize the task tool output above and continue with your task.”。子代理见子代理、待办与提问。

ensureTitle(prompt.ts:193)只处理标题仍为默认值、且恰好有一条真实(非 synthetic)user 消息的根会话:用隐藏的 title agent 和提供方的小模型生成,去掉 <think> 块后取第一行,超过 100 字符截断。

shell 与 command

shellImpl(prompt.ts:451)服务于 TUI 输入框的 shell 模式:写一条 synthetic user 消息“The following tool was executed by the user”,再写 assistant 消息和一个 running 的 shell 工具 Part,以 TERM=dumb 执行命令并把输出边读边写进 Part。于是下一轮模型看到的,是一次“用户替它执行过”的工具调用。

command(prompt.ts:1356)展开斜杠命令:用 $1 到 $N 与 $ARGUMENTS 替换参数(最后一个编号占位符吞掉剩余参数),模板里感叹号加反引号包裹的 shell 片段先执行、再替换成输出。目标 agent 是 subagent(且未显式关闭)或命令声明 subtask: true 时,整条命令被包成一个 subtask Part 交给 handleSubtask。写法见Agent、命令与 Skills。

取消

cancel 先逐层取消会话及其子会话名下的后台任务,再中断 Runner;被中断的 assistant 消息由 finalizeInterruptedAssistant 补上 MessageAbortedError 与完成时间。

下一篇:流处理器与工具调用生命周期——handle.process 如何把模型事件流变成 Part。

本页目录