乐于分享
好东西不私藏

OpenCode 源码-Agent 核心系统——多 Agent 编排、Runne

OpenCode 源码-Agent 核心系统——多 Agent 编排、Runne

OpenCode 源码解析

第 19 讲:Agent 核心系统——多 Agent 编排、Runner 循环与权限治理

基于 dev 分支源码 · 2026-07-26

一、Agent:从"一个 Agent"到"多 Agent 编排"

前十八讲覆盖了 OpenCode 从 CLI 到 Session 处理器的完整链路。这一讲深入 Agent 核心系统——OpenCode 的真正"大脑"。V2 架构不再使用单一 Agent 处理所有任务,而是设计了 多 Agent 编排体系:每个 Agent 有独立的角色、System Prompt、权限规则和模型配置。Agent 系统横跨 8 个核心文件、约 1,500+ 行代码,是整个 V2 架构的控制中枢。

📦 本讲核心文件

agent.ts(111 行)— Agent 注册与选择

plugin/agent.ts(206 行)— 内建 Agent 定义

session/runner/llm.ts(427 行)— Agent 执行循环

session/runner/model.ts(218 行)— 模型解析引擎

system-context/index.ts(321 行)— 动态系统上下文

question.ts(153 行)— 用户交互系统

二、AgentV2:Agent 注册中心与服务接口

agent.ts 定义了 Agent 的注册、查询和选择机制。Agent 不再是硬编码的配置项,而是通过 State.Transformable 模式管理的动态注册表。

1. Agent Info Schema

📄 config/agent.ts (第 13-25 行)

class Info extends Schema.Class<Info>("ConfigV2.Agent")({
  model: Schema.String.pipe(Schema.optional),      // 绑定的模型
  variant: Schema.String.pipe(Schema.optional),    // 模型变体
  request: ConfigProvider.Request.pipe(Schema.optional),
  system: Schema.String.pipe(Schema.optional),     // System Prompt
  description: Schema.String.pipe(Schema.optional),
  mode: Schema.Literals(["subagent", "primary", "all"]),
  hidden: Schema.Boolean.pipe(Schema.optional),    // 是否对用户可见
  color: Color.pipe(Schema.optional),
  steps: PositiveInt.pipe(Schema.optional),        // 最大步数限制
  disabled: Schema.Boolean.pipe(Schema.optional),
  permissions: Permission.Ruleset.pipe(Schema.optional), // 权限规则集
})

Schema 设计要点:每个 Agent 拥有独立的 model/variant 绑定,独立的 System Prompt,以及最重要的——独立的 权限规则集(Permission Ruleset)。mode 字段区分了 primary(用户可直接选择的主 Agent)、subagent(只能被其他 Agent 调用的子 Agent)和 all(两者皆可)。

2. Agent 选择逻辑

📄 agent.ts (第 67-101 行)

// Selection 是 Runner 实际使用的 Agent 视图
interface Selection {
  readonly id: ID
  readonly info: Info | undefined
}

// 选择逻辑:排除 subagent 和 hidden
const selectable = (agent: Info | undefined) =>
  agent && agent.mode !== "subagent" && !agent.hidden
    ? agent : undefined

// 默认 Agent 三级回退链
const selectedDefault = () => {
  const data = state.get()
  // ① 用户配置的 default agent
  const configured = data.default
    ? selectable(data.agents.get(data.default)) : undefined
  if (configured) return configured
  // ② 回退到 "build" agent
  const build = selectable(data.agents.get(ID.make("build")))
  if (build) return build
  // ③ 遍历所有可用 agent,取第一个
  for (const agent of data.agents.values()) {
    const fallback = selectable(agent)
    if (fallback) return fallback
  }
}

三级回退链设计:用户配置的默认 Agent → 硬编码的 "build" Agent → 注册表中第一个可用的 primary Agent。这种设计保证了 Agent 选择永远不会失败——即使配置出错,系统也能自动回退到可用的 Agent。

三、内建 Agent:7 个角色的分工协作

plugin/agent.ts 通过插件机制注册了 7 个内建 Agent,每个 Agent 有明确的角色分工和权限边界:

1. build(默认主 Agent)

📄 plugin/agent.ts (第 125-135 行)

draft.update(AgentV2.defaultID, (item) => {
  item.description = "The default agent. Executes tools..."
  item.system = "You are an AI coding agent. Help the user..."
  item.mode = "primary"
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "question", resource: "*", effect: "allow" },
      { action: "plan_enter", resource: "*", effect: "allow" },
    ]),
  )
})

build Agent 是 OpenCode 的默认主 Agent。权限最宽泛:workspace 内所有操作默认 allow,.env 文件需要 ask 确认,外部目录默认 ask,问题和计划模式可交互。

2. plan(计划模式 Agent)

📄 plugin/agent.ts (第 137-154 行)

draft.update(AgentV2.ID.make("plan"), (item) => {
  item.description = "Plan mode. Disallows all edit tools."
  item.mode = "primary"
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "question", resource: "*", effect: "allow" },
      { action: "plan_exit", resource: "*", effect: "allow" },
      { action: "edit", resource: "*", effect: "deny" },      // 禁止编辑
      { action: "edit", resource: ".opencode/plans/*.md", effect: "allow" },
    ]),
  )
})

plan Agent 是计划模式的核心。它 禁止所有编辑操作,只允许在 .opencode/plans/*.md 下创建计划文档。用户可以在 plan 模式下让 Agent 分析代码、制定方案,而不用担心误改文件。

3. general(通用子 Agent)

📄 plugin/agent.ts (第 156-161 行)

draft.update(AgentV2.ID.make("general"), (item) => {
  item.description =
    "General-purpose agent for researching complex questions..."
  item.mode = "subagent"
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "todowrite", resource: "*", effect: "deny" }
    ])
  )
})

general Agent 是通用子 Agent,用于并行执行多个任务单元。mode 为 subagent,意味着用户不能直接选择它——它只能被主 Agent 通过 ACP 协议调度。禁用了 todowrite 工具,防止子 Agent 干扰主任务列表。

4. explore(代码探索专家)

📄 plugin/agent.ts (第 163-182 行)

draft.update(AgentV2.ID.make("explore"), (item) => {
  item.system = PROMPT_EXPLORE  // 专门的探索 Prompt
  item.mode = "subagent"
  item.permissions.push(
    ...PermissionV2.merge(
      defaults,
      [
        { action: "*", resource: "*", effect: "deny" },  // 先全部禁止
        { action: "grep", resource: "*", effect: "allow" },
        { action: "glob", resource: "*", effect: "allow" },
        { action: "webfetch", resource: "*", effect: "allow" },
        { action: "websearch", resource: "*", effect: "allow" },
        { action: "read", resource: "*", effect: "allow" },
      ],
      readonlyExternalDirectory,
    ),
  )
})

explore Agent 是最精细权限控制的 Agent。采用 白名单策略——先 deny all,再逐个 allow。只允许 grep、glob、webfetch、websearch 和 read 操作。它的 System Prompt 专门训练 Agent 做文件搜索和代码浏览,不做任何修改操作。

5. compaction / title / summary(隐藏辅助 Agent)

📄 plugin/agent.ts (第 184-203 行)

// Compaction Agent:上下文压缩
draft.update(AgentV2.ID.make("compaction"), (item) => {
  item.mode = "primary"
  item.hidden = true
  item.system = PROMPT_COMPACTION
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "*", resource: "*", effect: "deny" }
    ])
  )
})

// Title Agent:会话标题生成
draft.update(AgentV2.ID.make("title"), (item) => {
  item.hidden = true
  item.system = PROMPT_TITLE
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "*", resource: "*", effect: "deny" }
    ])
  )
})

// Summary Agent:会话摘要生成
draft.update(AgentV2.ID.make("summary"), (item) => {
  item.hidden = true
  item.system = PROMPT_SUMMARY
  item.permissions.push(
    ...PermissionV2.merge(defaults, [
      { action: "*", resource: "*", effect: "deny" }
    ])
  )
})

三个隐藏 Agent:compaction 负责对话上下文压缩(锚定摘要模式),title 负责生成会话标题,summary 负责生成 Pull Request 风格的摘要。它们的共同特点是:hidden = true(用户不可见),权限全部 deny(不执行任何工具),只通过纯文本输出完成工作。

四、SessionRunner:Agent 执行引擎

session/runner/llm.ts(427 行)是整个 Agent 系统的执行引擎。它不直接调用 LLM,而是通过 Effect 服务依赖注入 协调 Agent 选择、模型解析、工具注册、系统上下文、压缩引擎等 12 个核心服务。

1. 服务依赖图

📄 session/runner/llm.ts (第 409-427 行)

export const node = makeLocationNode({
  service: Service,
  layer,
  deps: [
    EventV2.node,              // 事件总线
    llmClient,                 // LLM 客户端
    AgentV2.node,              // Agent 注册中心
    ToolRegistry.node,         // 工具注册表
    SessionRunnerModel.node,   // 模型解析
    SessionStore.node,         // 会话存储
    Location.node,             // 工作空间位置
    SystemContextRegistry.node,// 系统上下文
    SkillGuidance.node,        // 技能指导
    ReferenceGuidance.node,    // 引用指导
    Config.node,               // 配置
    Snapshot.node,             // 文件系统快照
    Database.node,             // 数据库
  ],
})

12 个依赖节点 构成了 Agent 执行引擎的完整依赖图。每个节点都是 Effect Location-scoped 服务,通过 makeLocationNode 组合成可复用的服务树。这种设计让 Runner 可以在不同的 Location(工作空间)中独立运行,互不干扰。

2. runTurn:单次 Provider 轮次

📄 session/runner/llm.ts (第 168-343 行)

const runTurn = Effect.fn("SessionRunner.runTurn")(
  function* (sessionID, promotion, step, recoverOverflow) {
    // ① 加载 Session 和 Agent
    const session = yield* getSession(sessionID)
    const agent = yield* agents.select(session.agent)

    // ② 初始化系统上下文(Skill + Reference 指导)
    const initialized = yield* SessionContextEpoch.initialize(
      db, loadSystemContext(agent), session.id
    )

    // ③ 用户输入提升(steer / queue)
    if (promotion) {
      const cutoff = yield* EventV2.latestSequence(db, session.id)
      if (promotion === "steer")
        promoted = yield* SessionInput.promoteSteers(...)
      if (promotion === "queue") {
        promoted += yield* SessionInput.promoteNextQueued(...)
        promoted += yield* SessionInput.promoteSteers(...)
      }
    }

    // ④ 模型解析 + 历史加载
    const model = yield* models.resolve(session)
    const entries = yield* SessionHistory.entriesForRunner(
      db, session.id, system.baselineSeq
    )

    // ⑤ 步数检查:最后一步禁用工具
    const isLastStep = agent.info?.steps !== undefined
      && currentStep >= agent.info.steps
    const toolMaterialization = isLastStep
      ? undefined : yield* tools.materialize(...)

    // ⑥ 构建 LLM Request
    const request = LLM.request({
      model,
      system: [agent.info?.system, system.baseline].filter(...),
      messages: [...toLLMMessages(context, model),
        ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
      tools: toolMaterialization?.definitions ?? [],
      toolChoice: isLastStep ? "none" : undefined,
    })

    // ⑦ 压缩检查
    if (yield* compaction.compactIfNeeded({ sessionID, entries, model }))
      return yield* Effect.die(continueAfterCompaction(currentStep))

    // ⑧ 流式 Provider 调用 + 工具结算
    const providerStream = llm.stream(request).pipe(
      Stream.runForEach((event) => Effect.gen(function* () {
        yield* publish(event)
        if (event.type === "tool-call" && !event.providerExecuted) {
          needsContinuation = true
          yield* toolMaterialization.settle({
            sessionID, agent, assistantMessageID, call: event
          })
        }
      }))
    )
  }
)

runTurn 的 8 步流程:

🔹 步骤①②:加载 Session 信息,选择 Agent,初始化系统上下文(包含 Skill Guidance 和 Reference Guidance)

🔹 步骤③:处理用户输入提升——"steer" 模式提升用户实时干预指令,"queue" 模式提升排队中的消息

🔹 步骤④⑤:解析模型,加载历史消息,检查是否达到 Agent 最大步数限制

🔹 步骤⑥:构建 LLM Request,合并 Agent 的 System Prompt 和动态系统上下文

🔹 步骤⑦:触发压缩检查,如果历史过长则先压缩再继续

🔹 步骤⑧:流式调用 Provider,逐事件处理并异步结算工具调用

3. 主循环:双层 while 结构

📄 session/runner/llm.ts (第 378-401 行)

const run = Effect.fn("SessionRunner.run")(
  function* (input: { sessionID, force }) {
    // 检查是否有 pending 输入
    const hasSteer = yield* SessionInput.hasPending(db, id, "steer")
    const hasQueue = hasSteer ? false
      : yield* SessionInput.hasPending(db, id, "queue")
    if (!input.force && !hasSteer && !hasQueue) return

    // 外层循环:处理排队消息
    while (shouldRun) {
      let needsContinuation = true
      let step = 1

      // 内层循环:Agent 工具执行循环
      while (needsContinuation) {
        const result = yield* runTurn(id, promotion, step)
        needsContinuation = result.needsContinuation
        step = result.step + 1
        promotion = "steer"
        // 检查是否有新的用户干预
        if (!needsContinuation)
          needsContinuation = yield* SessionInput.hasPending(
            db, id, "steer"
          )
      }

      // 继续处理排队消息
      shouldRun = yield* SessionInput.hasPending(db, id, "queue")
    }
  }
)

双层 while 循环设计:内层循环是 Agent 的工具执行循环(think → tool → think → tool),外层循环处理排队消息。两个循环之间有用户干预通道——Agent 每完成一轮工具循环,就检查是否有新的 steer 输入。

五、SystemContext:动态系统上下文引擎

system-context/index.ts(321 行)是 Agent 系统的"动态记忆"。它不硬编码 System Prompt 内容,而是通过 可组合的 Source 抽象 实现系统上下文的动态刷新和增量更新。

1. Source 抽象:类型安全的上下文源

📄 system-context/index.ts (第 32-39 行)

interface Source<A> {
  readonly key: Key                                  // 唯一键
  readonly codec: Schema.Codec<A, Schema.Json>     // 序列化
  readonly load: Effect.Effect<A | Unavailable>    // 加载数据
  readonly baseline: (current: A) => string          // 首次渲染
  readonly update: (previous: A, current: A) => string // 增量更新
  readonly removed?: (previous: A) => string         // 移除渲染
}

Source 抽象的五个方法:load 负责异步加载数据,baseline 负责首次渲染为文本,update 负责增量更新(比较新旧值),removed 负责源被移除时的通知。codec 定义了数据的序列化格式,key 是全局唯一的命名空间标识符。

2. 三阶段生命周期

📄 system-context/index.ts (第 198-285 行)

// 阶段 1:initialize — 创建基线
function initialize(value: SystemContext):
  Effect.Effect<Generation, InitializationBlocked> {
  return observe(value).pipe(
    Effect.flatMap((entries) => {
      const unavailable = entries.flatMap(
        (e) => e._tag === "Unavailable" ? [e.key] : []
      )
      if (unavailable.length > 0)
        return new InitializationBlocked({ keys: unavailable })
      return Effect.succeed(initializeObservation(entries))
    })
  )
}

// 阶段 2:reconcile — 增量比较
function reconcile(value: SystemContext,
  previous: Snapshot): Effect.Effect<ReconcileResult> {
  return observe(value).pipe(
    Effect.map((entries) => reconcileObservation(entries, previous))
  )
}

// 阶段 3:replace — 完全替换
function replace(value: SystemContext,
  previous: Snapshot): Effect.Effect<ReplacementResult>

三阶段设计:initialize 在会话开始时创建基线快照;reconcile 在后续轮次中比较新旧值,只渲染变化的部分;replace 在数据结构不兼容时触发完全替换。Snapshot 是持久化的比较状态,避免了每次都要重新加载所有上下文源。

3. SkillGuidance 与 ReferenceGuidance

📄 skill/guidance.ts + reference/guidance.ts

// Skill Guidance:向 Agent 展示可用技能
return SystemContext.make({
  key: SystemContext.Key.make("core/skill-guidance"),
  codec: Schema.toCodecJson(Schema.Array(Summary)),
  load: Effect.succeed(available),
  baseline: render,  // 渲染为 XML 格式
  update: (_prev, current) =>
    "The available skills have changed...\n" + render(current),
  removed: () => "Skill guidance is no longer available.",
})

// Reference Guidance:向 Agent 展示项目引用
return SystemContext.make({
  key: SystemContext.Key.make("core/reference-guidance"),
  codec: Schema.toCodecJson(Schema.Array(Summary)),
  load: Effect.succeed(available),
  baseline: render,
  update: (_prev, current) =>
    "The available references have changed...\n" + render(current),
  removed: () => "Reference guidance is no longer available.",
})

两个 Guidance Source 通过 SystemContext 机制将可用技能和项目引用注入 Agent 的 System Prompt。渲染格式是 XML 标签(<available_skills><available_references>),让 LLM 更容易解析结构化信息。当技能或引用变更时,update 方法只渲染差异部分。

六、QuestionV2:用户交互与权限确认

question.ts(153 行)实现了 Agent 与用户的交互通道。当 Agent 需要确认权限、回答问题或请求用户输入时,通过 QuestionV2 服务发起异步请求。

📄 question.ts (第 56-141 行)

interface Interface {
  readonly ask: (input: AskInput)
    => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
  readonly reply: (input: ReplyInput)
    => Effect.Effect<void, NotFoundError>
  readonly reject: (requestID: ID)
    => Effect.Effect<void, NotFoundError>
  readonly list: () => Effect.Effect<ReadonlyArray<Request>>
}

// ask:创建 Deferred,发布事件,等待用户回复
const ask = (input: AskInput) =>
  Effect.uninterruptibleMask((restore) =>
    Effect.gen(function* () {
      const id = ID.ascending()
      const deferred = yield* Deferred.make(...)
      pending.set(id, { request, deferred })
      yield* events.publish(Event.Asked, request)
      return yield* restore(Deferred.await(deferred))
    })
  )

// reply:用户回复,解析 Deferred
const reply = (input: ReplyInput) =>
  Effect.uninterruptible(Effect.gen(function* () {
    const existing = pending.get(input.requestID)
    yield* events.publish(Event.Replied, { answers: ... })
    yield* Deferred.succeed(existing.deferred, input.answers)
  }))

// reject:用户拒绝,失败 Deferred
const reject = (requestID: ID) =>
  Effect.uninterruptible(Effect.gen(function* () {
    yield* Deferred.fail(existing.deferred, new RejectedError())
  }))

Deferred 模式:ask 创建 Deferred 并等待,reply/reject 分别解析/失败 Deferred。RejectedError 是一个特殊错误——Runner 捕获后会中断工具执行循环(见 isQuestionRejected 逻辑)。pending Map 是 Location-scoped 的,不同工作空间的问题不会互相干扰。

七、模型解析引擎:SessionRunnerModel

session/runner/model.ts(218 行)负责将 Session 配置中的模型引用解析为实际的 LLM Model 对象。它支持三种协议:OpenAI Responses、Anthropic Messages 和 OpenAI Compatible Chat。

📄 session/runner/model.ts (第 131-170 行)

export const fromCatalogModel = (
  model: ModelV2.Info, credential?: Credential.Value
): Effect.Effect<Model, UnsupportedApiError> => {
  // OpenAI Responses API
  if (resolved.api.type === "aisdk"
    && resolved.api.package === "@ai-sdk/openai") {
    return Effect.succeed(
      withDefaults(resolved, OpenAIResponses.route)
        .with({ auth: key === undefined
          ? Auth.none : Auth.bearer(key) })
        .model({ id: resolved.api.id })
    )
  }
  // Anthropic Messages API
  if (resolved.api.type === "aisdk"
    && resolved.api.package === "@ai-sdk/anthropic") {
    return Effect.succeed(
      withDefaults(resolved, AnthropicMessages.route)
        .with({ auth: key === undefined
          ? Auth.none : Auth.header("x-api-key", key) })
        .model({ id: resolved.api.id })
    )
  }
  // OpenAI Compatible Chat API
  if (resolved.api.type === "aisdk"
    && resolved.api.package === "@ai-sdk/openai-compatible") {
    return Effect.succeed(
      withDefaults(resolved, OpenAICompatibleChat.route)
        .with({ auth: ... })
        .model({ id: resolved.api.id })
    )
  }
}

三协议支持:解析引擎根据 Catalog 中配置的 API 类型自动选择对应的协议路由。每个协议有不同的认证方式(Bearer token vs x-api-key header)。withDefaults 函数将模型的 endpoint、headers、limits 等配置注入路由。

八、总结

🔹 AgentV2 通过 State.Transformable 模式管理动态 Agent 注册表,支持三级回退链选择默认 Agent

🔹 7 个内建 Agent 分工明确:build(主 Agent)、plan(只读计划)、general(通用子 Agent)、explore(代码探索)、compaction/title/summary(隐藏辅助)

🔹 每个 Agent 有独立的 System Prompt、模型绑定和 Permission Ruleset

🔹 SessionRunner 的双层 while 循环:内层工具执行循环 + 外层排队消息处理,支持用户实时 steer 干预

🔹 SystemContext 的三阶段生命周期:initialize → reconcile → replace,支持增量更新和快照比较

🔹 SkillGuidance + ReferenceGuidance 通过 SystemContext.Source 注入动态上下文

🔹 QuestionV2 使用 Deferred 模式实现异步用户交互,RejectedError 中断执行循环

🔹 SessionRunnerModel 支持 OpenAI/Anthropic/OpenAI-Compatible 三种协议自动路由

← 系列导航 →

← 第 18 讲:Session 处理器 | 第 20 讲:TUI 终端界面 →

关注公众号获取更多 OpenCode 源码解析干货

源码:https://github.com/opencode-ai/opencode