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
夜雨聆风