OpenCode Agent系统是一个多智能体架构,通过定义Agent结构,使用Task工具实现Agent间调用,集成Permission权限系统进行访问控制,通过Session会话处理器处理交互,并使用Tool工具系统提供可扩展能力。
Agent 类型和模式:主 Agent、子 Agent 和隐藏 Agent
本部分解释了 OpenCode 中 Agent 的架构组织,涵盖了三种不同的 Agent 类型(主 Agent、子 Agent 和隐藏 Agent)、它们的运行模式、配置机制以及它们如何在会话管理系统内进行交互。
架构概述
OpenCode 的 Agent 系统围绕 Agent.Info 结构中 mode 字段定义的分层分类构建。此分类决定了 Agent 如何向用户展示、如何被调用以及需要什么权限。该系统通过专门处理开发工作不同方面的 Agent(从代码探索到任务执行和会话维护)来实现模块化的 AI 辅助。
来源: agent.ts
1 | export namespace Agent { |
Agent 分类系统
主 Agent
主 Agent 作为 OpenCode 中用户交互的主要入口点。这些 Agent 可以通过用户界面直接访问,并可以被选为会话的活动 Agent。系统提供了两个内置的主 Agent:
构建 Agent (Build Agent):默认 Agent,专用于执行修改代码库的任务。它拥有广泛的权限,并启用了问题审批机制,允许其在需要时请求用户确认。构建 Agent 可以读写文件、执行 bash 命令以及使用系统中的大多数工具。
计划 Agent (Plan Agent):专注于创建和管理实施计划的专用 Agent。它具有受限的写入权限,仅允许修改 .opencode/plan/*.md 目录下的文件。这种设计鼓励 Agent 生成文档和计划,而不是直接修改源代码。
子 Agent
子 Agent 是为被其他 Agent 调用(而非直接由用户调用)而设计的专用助手。它们处理更大工作流中的特定任务,使主 Agent 能够委派专门的工作。OpenCode 包含两个原生的子 Agent:
通用子 Agent (General Subagent):用于执行并行工作单元和研究复杂问题的多用途助手。它无法读取或写入待办事项(todoread/todowrite 被拒绝),使其适合执行不影响项目跟踪系统的任务。
探索子 Agent (Explore Subagent):专用于代码库探索的快速、专用 Agent。它擅长通过模式查找文件、搜索代码内容以及回答有关代码库的结构性问题。探索子 Agent 具有严格限制的权限——仅允许读取操作、grep、glob、列表、bash 命令和 Web 操作。这种集中的权限集确保了安全的探索,同时防止了意外的修改。
探索子Agent的Prompt在explore.txt文件中:
1 | You are a file search specialist. You excel at thoroughly navigating and exploring codebases. |
隐藏 Agent
隐藏 Agent 是处理内部维护任务的系统 Agent,从不直接向用户展示。它们响应特定的系统事件自动运行:
压缩 Agent (Compaction Agent):管理会话历史压缩以控制 token 限制并保留上下文。当会话超过 token 阈值时,此 Agent 分析对话历史并生成浓缩的摘要,以在减小上下文大小的同时保留基本信息。它的Prompt在compaction.txt中:1
2
3
4
5
6
7
8
9
10
11
12You are a helpful AI assistant tasked with summarizing conversations.
When asked to summarize, provide a detailed but concise summary of the conversation.
Focus on information that would be helpful for continuing the conversation, including:
- What was done
- What is currently being worked on
- Which files are being modified
- What needs to be done next
- Key user requests, constraints, or preferences that should persist
- Important technical decisions and why they were made
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
标题 Agent (Title Agent):根据对话内容自动为会话生成有意义的标题。这在会话完成或用户请求生成标题时运行,使用较低的 temperature (0.5) 以获得更确定的输出。它的Prompt在title.txt中:
1 | You are a title generator. You output ONLY a thread title. Nothing else. |
摘要 Agent (Summary Agent):创建会话摘要以供历史参考和快速上下文检索。与压缩 Agent 一样,它在没有任何工具权限的情况下运行,仅分析对话内容。它的Prompt在summary.txt中:1
2
3
4
5
6
7
8
9
10
11Summarize what was done in this conversation. Write like a pull request description.
Rules:
- 2-3 sentences max
- Describe the changes made, not the process
- Do not mention running tests, builds, or other validation steps
- Do not explain what the user asked for
- Write in first person (I added..., I fixed...)
- Never ask questions or add new questions
- If the conversation ends with an unanswered question to the user, preserve that exact question
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
Build、Plan和General 3个Agent的提示词
这3个Agent没有固定的Prompt,是通过以下的Prompt添加机制实现:
1. Agent定义阶段
在agent.ts:66-L110中,这三个agent的定义都没有设置prompt字段:
1 | build: { |
而其他agent如explore、summary等都有明确的prompt:
1 | explore: { |
2. Prompt组装逻辑
关键在llm.ts:65-L77的stream函数中:
1 | const system = SystemPrompt.header(input.model.providerID) |
核心逻辑:
- 如果
input.agent.prompt存在,使用agent的prompt - 否则,使用
SystemPrompt.provider(input.model)根据模型类型选择prompt
3. 根据模型类型选择Prompt
SystemPrompt.provider在system.ts:28-L34中定义:
1 | export function provider(model: Provider.Model) { |
总结
| Agent | Prompt来源 | 机制 |
|---|---|---|
| build | 根据模型类型动态选择 | 没有设置prompt字段,fallback到SystemPrompt.provider() |
| plan | 根据模型类型动态选择 + plan模式特殊处理 | 同上,但在plan模式会额外注入plan.txt的只读限制 |
| general | 根据模型类型动态选择 | 同build |
这种设计的好处是:
- 灵活性:同一个agent可以根据使用的不同模型自动适配对应的prompt
- 可维护性:不需要为每个agent复制粘贴相同的prompt模板
- 统一性:确保所有使用相同模型的agent行为一致
主Agent和子Agent的关系
执行模式:同步阻塞(而非并行)
主agent调用子agent时会阻塞,等待子agent完全执行完成后才继续。
具体来说:
独立的Session,但顺序执行
- 子agent在新的session中运行(TaskTool.execute L58-88)
- 新session的parentID指向主agent的session
- 但调用是
await的,主agent的执行循环会暂停等待
阻塞调用链
1
2
3
4
5
6
7
8// 主agent的loop中(packages/opencode/src/session/prompt.ts L317-478)
if (task?.type === "subtask") {
const taskTool = await TaskTool.init()
// 执行TaskTool,这里会await子agent的整个执行过程
const result = await taskTool.execute(taskArgs, taskCtx)
// 子agent完成后才继续
continue
}TaskTool的实现(packages/opencode/src/tool/task.ts L139)
1
2
3
4
5const result = await SessionPrompt.prompt({
sessionID: session.id, // 子agent的session
// ...
})
// 这个await会等待子agent的整个loop完成
Session隔离和协作
| 特性 | 主Agent Session | 子Agent Session |
|---|---|---|
| 独立性 | 有独立的loop | 有自己独立的loop |
| 并发 | ❌ 只有一个active loop | ❌ 只有一个active loop |
| 父子关系 | 作为parentID被引用 | parentID指向主session |
| 执行 | await子agent完成后继续 | 完成自主任务后返回结果 |
为什么是阻塞而非并行?
- 因果依赖:主agent需要子agent的结果才能继续
- 资源管理:避免同时运行多个LLM调用导致不可控的成本
- 简化状态:更容易追踪和理解执行流程
- 可调试性:顺序执行更容易排查问题
执行时序图
1 | 时间 → |
特殊情况:多个子Agent
如果主agent需要调用多个子agent:
1 | // 示例:主agent可能的行为 |
这是顺序的,不是并行的。LLM会自然地按顺序调用不同的task工具。
“单线程”的真正含义
- Session级别:每个session同时只能有一个active的loop
- 进程级别:Node.js事件循环是单线程的,所有异步操作通过事件驱动
- 执行级别:主agent和子agent的loop不会同时运行,而是串行的
总结
- 主agent调用子agent = 阻塞等待,不是并行处理
- 子agent在独立session中运行,有自己的loop
- 但主agent的loop会暂停,等待子agent完成
- 这简化了执行流程,使结果可预测、可调试
- 多个子agent调用也是顺序的,由LLM决定何时调用哪个
需要注意的是在oh-my-opencode插件中实现了非阻塞的多agent的编排。
Agent 配置结构
Agent.Info 结构定义了所有 Agent 类型的完整配置结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18{
name: string // 唯一标识符
description?: string // 人类可读的描述
mode: "subagent" | "primary" | "all" // Agent 分类
native?: boolean // 是否为内置 Agent
hidden?: boolean // 是否从 UI 中隐藏
topP?: number // Nucleus 采样参数
temperature?: number // 创造性/随机性设置
color?: string // UI 显示颜色
permission: Ruleset // 权限配置
model?: { // 模型覆盖
modelID: string
providerID: string
}
prompt?: string // 自定义系统提示词
options: Record<string, any> // Agent 特定选项
steps?: number // 最大执行步数
}
来源: agent.ts
Agent 模式字段和可见性
mode 字段是决定 Agent 可见性和行为的主要因素:
| 模式 | UI 可见性 | 调用方式 | 用例 |
|---|---|---|---|
| primary | 在 Agent 选择器中可见 | 直接用户选择 | 主要交互 Agent (build, plan) |
| subagent | 在选择器中隐藏 | 任务工具调用 | 专用助手 (general, explore) |
| all | 在选择器中可见 | 直接和任务调用 | 用户定义的自定义 Agent |
hidden 布尔字段提供了额外的控制——当设置为 true 时,Agent 将被排除在模式列表之外,无论其 mode 设置如何。这用于绝不应出现在用户界面中的内部维护 Agent。
来源: agent.ts, acp/agent.ts
权限系统集成
每个 Agent 维护一个独特的权限规则集,确定它可以访问哪些工具和操作。权限按优先级顺序从多个来源合并:
默认权限:应用于所有 Agent 的基准规则
Agent 特定默认值:特定于模式的权限覆盖
用户配置:来自配置文件的项目级自定义
外部目录强制:确保对截断目录的访问
权限系统使用通配符模式和动作(allow、deny、ask)来创建灵活的安全边界。例如,探索子 Agent 的权限明确拒绝大多数操作,同时允许只读工具如 grep、glob 和 read。
来源: agent.ts, agent.ts
Agent 发现和过滤
系统提供了多种根据用例发现和过滤 Agent 的方法:1
2
3
4
5
6
7
8// 列出所有 Agent(按 default_agent 优先级排序)
await Agent.list()
// 按名称获取特定 Agent
await Agent.get("explore")
// 获取默认 Agent(排序列表中的第一个)
await Agent.defaultAgent()
在构建 UI 元素或创建任务工具描述时,Agent 按模式过滤。ACP 集成专门从可用模式列表中排除子 Agent 和隐藏 Agent:1
2
3
4
5
6
7const availableModes = agents
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
来源: agent.ts, acp/agent.ts
基于Task任务的子 Agent 调用
Task工具是OpenCode中实现agent间协作的关键机制,实现在task.ts中。
它使主 Agent 能够通过结构化的调用机制将工作委派给子 Agent。当 Agent 调用任务工具时:
1、权限验证:检查调用 Agent 的权限,以查看其是否允许调用目标子 Agent
2、会话创建:使用子 Agent 的配置创建一个新的分支会话
3、隔离执行:子 Agent 在受限权限下执行其任务
4、结果聚合:子 Agent 的输出返回给调用 Agent
任务工具动态生成描述,列出所有可用的子 Agent,并根据调用 Agent 的权限对其进行过滤。这使得可以进行上下文相关的委派,Agent 只能调用其有权使用的子 Agent。
通过源码具体分析上述过程:
1. 工具注册和权限过滤
1 | export const TaskTool = Tool.define("task", async (ctx) => { |
关键点:
- 只有
mode !== "primary"的agent才能被调用(即subagent) - 权限系统控制agent间调用关系
- 动态生成agent描述,AI能看到可用的agent列表
2. 权限检查流程
1 | async execute(params: z.infer<typeof parameters>, ctx) { |
权限评估逻辑(在next.ts:107-L125):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15export function evaluate(permission: string, pattern: string, ruleset: Ruleset, approved: Ruleset) {
// 1. 检查用户批准的规则
for (const rule of approved) {
if (Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) {
return { action: "allow" }
}
}
// 2. 检查默认规则集
for (const rule of ruleset) {
if (Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) {
return rule
}
}
return { action: "deny" }
}
三种权限:
allow:直接允许调用deny:拒绝调用,抛出异常ask:向用户请求批准
3. 子Agent会话创建
1 | const session = await iife(async () => { |
安全隔离:
- 嵌套调用限制:禁止子agent再调用task(防递归)
- 工具限制:子agent只能使用配置允许的工具
- 会话隔离:独立的会话状态和上下文
4. 实时进度反馈
1 | const parts: Record<string, {...}> = {} |
事件流:
- 子agent的工具调用通过事件系统广播
- 调用方实时接收进度更新
- 用户可以看到agent的工作状态
5. Agent执行和结果返回
1 | const result = await SessionPrompt.prompt({ |
6. Agent调用链
1 | User Prompt |
关键设计要点
安全机制
- 权限隔离:每个agent有独立的权限集
- 嵌套限制:禁止子agent递归调用task
- 工具限制:子agent只能使用允许的工具
- 用户审批:敏感操作需要用户确认
会话管理
- 父子关系:
parentID建立会话层级 - 独立状态:子agent有自己的消息历史
- 状态隔离:修改不会影响父会话
通信机制
- 事件总线:通过Bus系统传递进度
- 实时反馈:调用方可以监控子agent状态
- 结果汇总:工具调用状态会被汇总返回
灵活性
- 动态agent列表:工具描述包含可用agent
- 会话续接:通过
session_id可以继续之前的任务 - 配置化权限:
config.experimental.primary_tools控制可用工具
这种设计实现了agent间的安全协作,同时保持了系统的灵活性和可扩展性。
Agent 生命周期和状态管理
Agent 通过从多个来源合并配置的延迟加载状态系统进行配置:
内置定义:具有默认配置的原生 Agent(build、plan、general、explore、compaction、title、summary)
用户扩展:在 .opencode 目录或项目配置中定义的自定义 Agent
运行时覆盖:通过配置文件或程序化更改进行的修改
用户定义的 Agent 可以扩展或覆盖内置配置。当用户配置指定了与内置 Agent 同名的 Agent 时,用户配置将与内置定义合并并优先于内置定义。用户还可以通过在配置中设置 disable: true 来完全禁用 Agent。
来源: agent.ts
创建自定义 Agent 时,使用 mode: “all” 使其既可用于直接用户选择,也可通过任务工具由其他 Agent 调用。这在保持 UI 中清晰可见性的同时提供了最大的灵活性。
Agent 模式交互模式
主 Agent 通过将专门任务委派给子 Agent 来编排复杂的工作流。这种模式实现了高效的任务分解:
1、用户交互:用户选择一个主 Agent(例如 build)并提供高级请求
2、委派:主 Agent 分析请求并将子任务委派给适当的子 Agent(例如,用于代码库分析的 explore)
3、并行执行:多个子 Agent 可以同时在问题的不同方面工作
4、聚合:主 Agent 综合结果并协调最终执行
隐藏 Agent 独立于此模式运行,响应系统事件而非直接请求。例如,当超过 token 限制时,压缩 Agent 会自动触发,无论哪个主 Agent 处于活动状态。
会话与 Agent 模式的集成
创建会话时,系统将每个会话与特定的 Agent 关联。这种关联决定了:
- 可用工具:Agent 可以基于其权限规则集访问哪些工具
- 系统提示词:特定 Agent 的自定义提示词配置
- 模型选择:Agent 是使用默认模型还是配置的覆盖模型
- 执行限制:控制 Agent 行为的步数限制和 temperature 设置
会话可以使用不同的 Agent 进行分支,从而实现计划 Agent 创建实施计划,然后构建 Agent 在分支会话中执行它的工作流。
来源: prompt.ts, processor.ts
创建自定义 Agent
自定义 Agent 可以通过 .opencode 目录中的配置文件或通过程序化配置来定义。系统支持扩展内置 Agent 和创建全新的 Agent:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
"agent": {
"code-review": {
"description": "专用于代码审查和重构的 Agent",
"mode": "primary",
"temperature": 0.7,
"permission": {
"edit": "ask",
"read": "allow"
}
},
"explorer": {
"description": "快速代码库探索",
"mode": "subagent",
"permission": {
"grep": "allow",
"glob": "allow",
"read": "allow"
}
}
}
}
自定义 Agent 自动继承默认权限规则集,可以通过 permission 字段有选择地覆盖。系统还确保所有 Agent 都可以访问截断目录以进行上下文管理,除非明确拒绝。
来源: agent.ts, config.ts
Agent 专业化示例
探索子 Agent 展示了子 Agent 如何针对特定任务进行专业化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26explore: {
name: "explore",
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({
"*": "deny", // 默认拒绝所有
grep: "allow", // 允许代码搜索
glob: "allow", // 允许文件模式匹配
list: "allow", // 允许目录列表
bash: "allow", // 允许 shell 命令
webfetch: "allow", // 允许 web 获取
websearch: "allow", // 允许 web 搜索
codesearch: "allow", // 允许代码搜索
read: "allow", // 允许文件读取
external_directory: {
[Truncate.DIR]: "allow", // 允许截断目录
},
}),
user,
),
description: `专用于探索代码库的快速 Agent...`,
prompt: PROMPT_EXPLORE,
options: {},
mode: "subagent",
native: true,
}
此配置创建了一个只能执行读取操作的 Agent,使其可以安全地探索不熟悉的代码库,而不会有意外修改的风险。
来源: agent.ts, explore.txt
隐藏 Agent 应始终设置 hidden: true 并配置 permission: “*”: “deny”,以确保它们无法执行任何工具操作。它们唯一的交互应通过内部系统调用,而不是面向用户的工具。
Agent 模式最佳实践
设计 Agent 配置时,请考虑以下准则:
- 主 Agent 应具有广泛的权限集,但对破坏性操作使用 “ask” 动作。这在保持安全的同时实现了灵活性。
- 子 Agent 应具有严格限制的范围和最少的权限。每个子 Agent 应专注于特定的能力。
- 隐藏 Agent 不得公开工具或写入操作。它们应该是仅处理现有数据的纯分析 Agent。
- 自定义 Agent 当你希望同时具有直接用户访问和任务工具调用能力时,应指定 mode: “all”。
- Temperature 调整会影响行为——对于确定的 Agent(如 title/summary)使用较低的 temperature (0.3-0.5),对于创造性 Agent 使用较高的 temperature (0.7-1.0)。
权限系统
OpenCode 权限系统提供了一个灵活且可配置的安全层,用于控制 Agent 对工具和系统资源的访问。该系统支持配置驱动的策略、基于通配符的模式匹配以及运行时用户审批工作流,从而对 Agent 可执行的操作实现精细化的控制。
安全模型架构
权限系统基于分层安全模型运行,结合了声明式配置与运行时授权检查。当 Agent 尝试使用工具时,系统会在继续执行前根据配置的规则评估请求,确保需要用户干预的操作显示明确的审批对话框,而常规操作则可以自动进行。
权限系统维护了两个并行的实现:旧系统(packages/opencode/src/permission/index.ts)和下一代系统(packages/opencode/src/permission/next.ts)。新系统提供了增强的功能,包括规则集评估、更好的模式匹配以及更复杂的错误处理。
来源:permission/index.ts, permission/next.ts
核心权限概念
权限类型和操作
系统识别三种主要的操作,用于确定如何处理权限请求:
allow(允许):无需用户干预自动批准工具执行deny(拒绝):立即拒绝工具执行并报错ask(询问):在运行时提示用户进行批准(当没有匹配规则时的默认行为)
这些操作可以应用于不同的粒度级别,从全局工具策略到特定的路径或命令模式。
来源:config/config.ts
工具权限
以下工具类别可以通过权限系统进行控制:
| 权限类型 | 关联工具 | 描述 |
|---|---|---|
| edit | edit, write, patch, multiedit | 文件修改操作 |
| read | read, ls, glob | 文件读取和发现 |
| bash | bash | Shell 命令执行 |
| grep | grep | 代码和文本搜索 |
| task | task | Subagent 执行 |
| external_directory | external_directory | 项目目录外的操作 |
| webfetch, websearch, codesearch | 基于网络的工具 | 外部数据访问 |
| lsp | lsp | 语言服务器操作 |
| todoread, todowrite | todo | 任务列表管理 |
| question | question | 交互式查询 |
来源:config/config.ts
权限配置
配置结构
权限在 opencode.json 或 opencode.jsonc 文件中通过 permission 字段进行配置。系统支持多个配置层及其优先级:远程/已知配置(最低)→ 全局用户配置 → 项目配置 → 命令行标志(最高)。
来源:config/config.ts
基本配置示例
最简单的形式是为权限类型分配单个操作:
1 | { |
为了进行更多控制,可以使用对象语法来指定模式:
1 | { |
来源:config/config.ts
通配符模式匹配
权限系统使用通配符模式来灵活地匹配文件路径和命令。模式引擎支持:
*:匹配任意字符序列?:匹配任意单个字符- 标准正则表达式特殊字符将被转义以进行字面匹配
模式示例:
*.md- 匹配任意 Markdown 文件src/**/*.ts- 匹配 src 层级结构中的任意 TypeScript 文件git checkout *- 匹配任意 git checkout 命令npm run *- 匹配任意 npm run 命令
系统使用最长前缀匹配,其中更具体的模式优先于通用模式。
来源:util/wildcard.ts
配置通配符模式时,请在权限对象内将规则按从最具体到最不具体的顺序排列。评估引擎使用 findLast() 进行匹配,这意味着最后一个匹配的规则获胜。这允许您使用特定的例外覆盖通用规则。
Agent 特定权限
可以为每个 Agent 配置权限,以对不同 Agent 的行为进行精细控制:
1 | { |
Agent 特定权限会覆盖该 Agent 的全局设置,使您能够创建具有不同安全配置文件的 Agent。
来源:config/config.ts
权限评估流程
请求生成
当调用工具时,它会生成一个包含以下内容的权限请求:
1 | { |
来源:permission/next.ts
规则评估
系统使用以下逻辑根据配置的规则集评估请求:
来源:permission/next.ts, permission/next.ts
评估算法
evaluate() 函数合并所有规则集,并使用通配符匹配找到最后一个匹配的规则:
1 | export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { |
通配符匹配确保了像 src/**/*.ts 这样的模式能正确匹配嵌套目录结构。
来源:permission/next.ts
用户响应处理
响应类型
用户可以通过三个选项响应权限请求:
| 响应 | 行为 | 用例 |
|---|---|---|
| once (仅此一次) | 仅批准此单个请求 | 对一次性操作的临时批准 |
| always (总是允许) | 批准并为将来的匹配保存规则 | 您信任的重复操作 |
| reject (拒绝) | 拒绝此请求并停止执行 | 您想要阻止的操作 |
当选择“总是允许”时,系统会自动批准所有与保存的模式匹配的待处理请求。
来源:permission/next.ts
错误处理
系统为不同的拒绝场景提供了三种不同的错误类型:
DeniedError:由配置规则自动拒绝,包含匹配的规则集以供参考RejectedError:用户拒绝且未提供消息,使用默认消息停止执行CorrectedError:用户拒绝并提供了反馈消息,为使用不同参数重试提供指导
这种区别有助于 Agent 理解它们应该重试、修改方法还是完全停止。
来源:permission/next.ts
工具集成示例
文件编辑工具
编辑工具在修改文件之前请求权限:
1 | await ctx.ask({ |
这允许用户批准特定文件或像 src/**/*.ts 这样的模式以进行自动批准。
来源:tool/edit.ts
Bash 工具
Bash 工具执行复杂的命令解析以确定权限:
1 | // 解析命令结构 |
系统使用命令元数检测来识别“人类可理解”的命令部分。例如,npm install package-name 被识别为 npm install* 用于权限模式。
来源:tool/bash.ts, permission/arity.ts
Bash 工具包含一个全面的元数字典,涵盖 150 多个常用命令(git, npm, docker, kubectl 等)。这确保了 npm run dev 和 npm install 被视为不同的模式,从而允许精细的权限控制。
命令元数字典
系统包含一个预构建的字典,将命令前缀映射到它们的元数(定义命令的标记数量):
1 | const ARITY: Record<string, number> = { |
这实现了基于模式的权限,可以理解命令语义。
来源:permission/arity.ts
插件集成
权限 Hooks
插件可以通过 permission.ask hook 拦截权限请求,从而实现自定义授权逻辑:
1 | // 插件实现 |
这允许插件实现特定领域的安全策略,与外部授权系统集成,或根据上下文因素提供自动批准。
来源:plugin/index.ts, permission/index.ts
旧系统迁移
从 Tools 字段迁移
系统会自动将旧版 tools 配置迁移到新的 permission 格式:
1 | // 旧格式(已弃用) |
同样,已弃用的 autoshare 字段会迁移到 share 字段。这种向后兼容性确保现有配置无需手动更新即可继续工作。
来源:config/config.ts, config/config.ts
高级配置模式
特定环境权限
为不同的环境配置不同的权限集:
1 | { |
基于目录的安全性
将操作限制在特定的项目区域:
1 | { |
命令白名单
为生产环境实施严格的命令白名单:
1 | { |
这种默认拒绝的方法确保只有明确批准的命令才能执行。
来源:config/config.ts
状态管理和持久化
权限状态
系统维护权限状态,包括:
- 待处理请求:当前等待用户批准
- 已批准规则:在会话期间保存的自动批准模式
- 会话隔离:权限范围限定在每个会话
已批准的规则会持久化到 ["permission", projectID] 下的存储中,允许规则在会话之间持久化(目前处于注释状态,等待 UI 管理实现)。
来源:permission/next.ts, permission/next.ts
会话清理
会话终止时,系统会自动拒绝所有待处理的权限请求,以防止孤立操作:
1 | async (state) => { |
这确保了清晰的会话边界,并防止工具在会话结束后执行。
来源:permission/index.ts
事件系统
权限事件
系统发布事件以与 UI 和其他组件集成:
1 | export const Event = { |
组件可以订阅这些事件以:
- 向用户显示权限对话框
- 跟踪权限使用情况以进行分析
- 实现自定义审批工作流
- 监控安全态势
来源:permission/next.ts, permission/index.ts
禁用工具检测
系统提供了一个实用函数,用于识别通过配置全局禁用的工具:
1 | export function disabled(tools: string[], ruleset: Ruleset): Set<string> { |
这允许 UI 组件根据配置隐藏或禁用不可用的工具,通过防止挫败感来改善用户体验。
来源:permission/next.ts
最佳实践
安全考虑
- 默认询问:对于开发环境,使用 “ask” 作为默认操作,以保持对 Agent 操作的了解
- 生产白名单:在生产环境中,使用 “deny” 作为默认值,并为受信任的操作设置明确的允许规则
- 模式具体性:将模式按从具体到通用的顺序排列,以确保正确的覆盖行为
- 外部访问:始终要求对 external_directory 和网络工具进行批准
- 破坏性命令:明确拒绝危险模式,如 rm -rf 或 docker rm *
配置建议
1 | { |
这种平衡的方法允许安全的读取操作,同时要求对修改和外部访问进行监督。
来源:config/config.ts
Agent 权限配置文件
不同的 Agent 应具有适当的权限配置文件:
| Agent 类型 | 推荐权限 |
|---|---|
| 代码生成器 | edit: ask, read: allow, bash: deny |
| 重构 Agent | edit: allow 针对 src/**, read: allow, bash: allow 针对 git 命令 |
| 测试 Agent | edit: deny, read: allow, bash: allow 针对 npm test |
| 文档 Agent | edit: ask 针对 docs/**, read: allow, bash: deny |
Agent生命周期
OpenCode 中的 Agent 生命周期涵盖了从 Agent 注册、执行、状态转换到终止的完整旅程。该系统支持多种 Agent 类型(主 Agent、子 Agent、隐藏 Agent),并具备复杂的权限管理和实时状态跟踪功能。
1. 初始化阶段 (Initialization)
当用户发起对话时:
创建或获取Session
- 通过Session.create创建新会话,或通过Session.get获取已有会话
- Session包含projectID、directory、parentID(如果是子会话)、权限配置等元数据
具体源码见:
1
2
3
4
5
6
7
8
9
10
11export const create = fn(
z
.object({
parentID: Identifier.schema("session").optional(),
...
...
export const get = fn(Identifier.schema("session"), async (id) => {
const read = await Storage.read<Info>(["session", Instance.project.id, id])
return read as Info
})选择Agent
- 通过Agent.get获取指定的agent配置(默认是”build” agent)
- Agent配置包含:名称、权限规则集、prompt、model配置、步骤限制等
创建用户消息
- createUserMessage构建MessageV2.User对象
源码见:
- createUserMessage构建MessageV2.User对象
1 | async function createUserMessage(input: PromptInput) { |
- 处理文件附件、@agent引用等parts
- 将消息持久化存储
2. 执行循环阶段 (Execution Loop)
SessionPrompt.loop是核心驱动器,它是一个无限循环:
循环开始
- 检查最后一条用户消息和助手消息
- 如果助手已完成(finish状态不是tool-calls/unknown),退出循环
处理待办任务(优先级从高到低)
a. Subtask处理 (L317-478)
- 如果有挂起的subtask,优先执行
- Subtask是通过TaskTool调用的其他agent(如explore agent)
- 创建assistant消息,执行TaskTool,处理结果
b. Compaction处理 (L482-492)
- 如果有挂起的压缩任务,执行上下文压缩
- 调用SessionCompaction.process减少token使用
c. 上下文溢出检查 (L495-507)
- 检查最后一条完成的消息是否导致token溢出
- 如果溢出,自动创建compaction任务并继续
正常处理 (L509-620)
a. 创建Processor
- 通过SessionProcessor.create创建处理器
- 创建新的assistant消息,关联到用户消息
b. 解析工具
- resolveTools根据agent权限和session权限合并工具集
- 从ToolRegistry获取可用工具
- 包装MCP工具
c. LLM流式调用
- 调用processor.process开始处理
- 传入system prompt、历史消息、工具定义
3. LLM流式处理阶段 (Streaming Processing)
SessionProcessor.process处理LLM流式响应:
事件类型处理
start (L58)
- 设置session状态为busy
reasoning系列事件 (L62-101)
- 推理内容的开始、增量、结束
- 实时更新ReasoningPart
tool-input系列事件 (L103-124)
- 工具输入准备(当前未实际使用)
tool-call (L126-171)
- 执行工具调用前
- Doom Loop检测 (L146-168):如果最近3次调用相同工具用相同参数,触发权限询问
- 创建ToolPart,状态设为running
- 执行工具
tool-result (L172-194)
- 工具执行成功
- 更新ToolPart状态为completed,保存输出
tool-error (L196-221)
- 工具执行失败
- 如果是权限拒绝错误,设置blocked标志(可能导致循环终止)
- 更新ToolPart状态为error
start-step / finish-step (L225-277)
- 步骤边界标记
- 追踪文件快照变化,生成patch
- 更新token和成本统计
- 触发SessionSummary.summarize
- 检查是否需要compaction
text系列事件 (L279-326)
- 文本内容的开始、增量、结束
- 实时更新TextPart
error (L222-223)
- 抛出错误,进入重试逻辑
错误处理和重试 (L339-363)
- 捕获异常,判断是否可重试(SessionRetry.retryable)
- 计算重试延迟,更新session状态
- 重试最多3次后终止
循环终止条件 (L397-400)
- 如果需要compaction,返回”compact”
- 如果工具被权限拒绝,返回”stop”
- 如果有错误,返回”stop”
- 否则返回”continue”
4. 状态管理和持久化
在整个生命周期中:
- SessionStatus - 实时状态更新(idle/busy/retry)
- MessageV2 - 消息持久化(通过Session.updateMessage)
- Parts - 消息部分持久化(通过Session.updatePart)
- Snapshot - 文件变更追踪(通过Snapshot.track)
- 事件总线 - 发布事件供UI或其他组件订阅
5. 清理和完成
当循环结束后:
- SessionCompaction.prune - 清理过期的压缩记录
- 返回最后一条assistant消息给调用者
- 触发相应的事件(如完成、错误)
关键特性
- 单线程处理:每个session同时只能有一个active的循环(通过BusyError保护)
- 渐进式执行:工具调用流式处理,实时更新UI
- 智能压缩:自动检测token溢出并触发压缩
- 循环保护:检测并阻止doom loop(重复调用相同工具)
- 权限控制:每个工具调用前都会检查权限(PermissionNext.ask)
- 成本追踪:精确统计每次调用的token和成本
这就是agent从出生到完成任务的完整旅程。整个过程是异步的、流式的、可中断的,充满了状态检查和错误恢复机制。
其他关键点
Doom Loop 预防
系统实现了自动 doom loop 检测,以防止无限的工具调用循环:
1 | const parts = await MessageV2.parts(input.assistantMessage.id) |
来源:processor.ts
核心原理
系统监控连续的工具调用,如果检测到相同工具以相同参数被重复调用,就会触发用户确认。阈值设为 3 次。
具体实现步骤
参见 processor.tsL20-L168:
设定阈值(第 20 行):
1
const DOOM_LOOP_THRESHOLD = 3
检测时机:每次执行工具调用时(tool-call 事件)
检测逻辑(第 143-168 行):
- 获取当前消息的所有部分:
const parts = await MessageV2.parts(input.assistantMessage.id) - 检查最后 3 个工具调用:
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD) - 判断条件:只有当满足以下所有条件时才视为 doom loop:
- 恰好有 3 个工具调用
- 都是 tool 类型
- 使用相同的工具名称
- 不是 pending 状态(说明已经执行过)
- 输入参数完全相同(通过 JSON 序列化比较)
- 获取当前消息的所有部分:
触发权限询问:
- 调用
PermissionNext.ask()请求用户确认 - 提供
doom_loop权限类型 - 传递工具名称和输入参数作为元数据
- 调用
处理机制
如果用户拒绝继续:
参见 processor.tsL212-L217:
1 | // 工具会被标记为错误状态 |
这种设计既防止了无限循环浪费资源,又给用户提供了必要的控制权——如果是合理的重复操作,用户可以选择继续。
系统持久化机制详解
系统的持久化机制设计得相当优雅。
存储类型
基于文件系统的 JSON 存储,参见 storage.tsL143-L158:
- 所有数据存储在
{Global.Path.data}/storage/目录 - 每个实体都是独立的
.json文件 - 使用读写锁机制保证并发安全(参见
LockL172)
存储格式与目录结构
1 | storage/ |
核心数据结构
1. 会话(Session)
参见 session/index.tsL39-L79:
1 | type Session.Info = { |
2. 消息(MessageV2)
参见 message-v2.tsL298-L390:
用户消息:
1 | type User = { |
助手消息:
1 | type Assistant = { |
3. 消息部分(Parts)
消息由多个部分组成,支持多种类型,参见 message-v2.tsL323-L341:
| 类型 | 用途 |
|---|---|
| text | 文本内容(助手回复或用户输入) |
| tool | 工具调用(输入、输出、错误) |
| reasoning | 推理过程(思维链) |
| file | 文件附件 |
| snapshot | 文件快照 |
| patch | 代码补丁 |
| step-start/finish | 执行步骤标记 |
| compaction | 会话压缩标记 |
| subtask | 子任务 |
| retry | 重试信息 |
| agent | Agent 信息 |
工具部分详解(最复杂):
1 | type ToolPart = { |
持久化操作
核心 API 在 storage.tsL160-L226:
1 | // 读取 |
使用示例(来自 session/index.tsL209):
1 | // 创建会话 |
数据迁移
系统支持 schema 迁移,参见 storage.tsL23-L141:
- 迁移脚本在
MIGRATIONS数组中 - 每次启动时检查
migration文件 - 按顺序执行未执行的迁移
会话压缩(优化存储)
当会话接近上下文限制时,系统会进行压缩,参见 compaction.tsL30-L39:
主动修剪:
- 从旧工具调用中删除输出(保留调用信息)
- 保护最近的 40,000 tokens 和特定工具(如 skill)
- 只删除超过 20,000 tokens 的内容
会话压缩:
- 使用 LLM 生成对话摘要
- 在压缩点插入
compactionpart - 后续消息可以基于摘要恢复上下文
这种设计既保证了数据的完整性和可追溯性,又通过文件系统和 JSON 格式保持了简单性和可维护性。
生命周期事件
系统通过事件总线发布全面的事件,用于实时监控和 UI 更新:
1 | export const Event = { |
来源:index.ts
创建自定义 Agent:配置与最佳实践
本指南涵盖了在 OpenCode 中创建和配置自定义 agent 的完整流程,从基础设置到高级配置模式和最佳实践。
Agent 配置基础
OpenCode 中的自定义 agent 通过一个支持多种配置来源和分层合并的声明式系统进行配置。核心 agent schema 定义了所有 agent(无论是内置还是自定义)必须遵循的结构。
配置 Schema 概述
每个 agent 配置都遵循 Agent.Info schema,并包含以下核心属性:
| 属性 | 类型 | 必需 | 描述 | ||
|---|---|---|---|---|---|
| name | string | 是 | agent 的唯一标识符 | ||
| description | string | 否 | 描述何时使用该 agent 的可读说明 | ||
| mode | “subagent” \ | “primary” \ | “all” | 否 | 决定 agent 何时可用 |
| prompt | string | 否 | 定义 agent 行为的系统提示词 | ||
| model | object | 否 | 特定模型配置 (providerID, modelID) | ||
| temperature | number | 否 | 采样温度 (0.0-1.0) | ||
| topP | number | 否 | 核采样参数 | ||
| permission | PermissionObject | 否 | 工具权限覆盖 | ||
| hidden | boolean | 否 | 从 @ 自动补全菜单中隐藏(仅限 subagent) | ||
| color | string | 否 | UI 显示的十六进制颜色代码 (#RRGGBB) | ||
| steps | number | 否 | 纯文本响应前的最大 agent 迭代次数 | ||
| disable | boolean | 否 | 完全禁用该 agent |
来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts
配置加载层级
OpenCode 按照特定的优先级顺序从多个来源加载 agent 配置,后加载的来源会覆盖先前的来源:
Agent 文件通过扫描多个目录的 glob 模式发现:
.opencode/agent/**/*.md: 项目特定的 agent.opencode/mode/**/*.md:模式特定的 agent(已弃用,使用 mode: primary)- 全局配置目录中的 agent
- 向上至工作树根目录的祖先
.opencode目录
来源:packages/opencode/src/config/config.ts, packages/opencode/src/config/config.ts
创建你的第一个自定义 Agent
方法 1:Markdown 文件配置
创建自定义 agent 的推荐方法是使用带有 frontmatter 配置的 Markdown 文件:
1 | --- |
将此文件保存为项目目录中的 .opencode/agent/code-reviewer.md。
来源:packages/opencode/src/config/config.ts
方法 2:JSON 配置
对于程序化配置,你可以直接在 opencode.json 中定义 agent:
1 | { |
来源:packages/opencode/src/config/config.ts
方法 3:AI 辅助生成
OpenCode 提供了一个内置的 agent 生成功能,使用 AI 根据自然语言描述创建 agent 配置:
1 | const result = await Agent.generate({ |
此函数会验证生成的标识符不与现有 agent 冲突,并生成完整的可供使用的配置。
来源:packages/opencode/src/agent/agent.ts
Agent 模式和使用模式
理解 agent 模式对于正确的 agent 设计和用户体验至关重要。
模式类型
| 模式 | 可用性 | 用例 | 示例 |
|---|---|---|---|
| primary | 用户直接选择 | 主要工作流,面向用户的 agent | build, plan |
| subagent | 仅限委托 | 专门任务,@提及 | general, explore, 自定义专家 |
| all | 直接和委托均可 | 可充当任一角色的多功能 agent | (罕见,通常首选显式模式) |
来源:packages/opencode/src/agent/agent.ts
内置 Agent 模式
系统包括几个展示不同模式的内置 agent:
Explore Agent - 快速代码库导航专家
1 | --- |
来源:packages/opencode/src/agent/prompt/explore.txt, packages/opencode/src/agent/agent.ts
Build Agent - 用于代码生成的主要 agent
1 | build: { |
来源:packages/opencode/src/agent/agent.ts
权限系统集成
自定义 agent 可以覆盖全局权限配置,根据 agent 的预期用途限制或扩展工具访问。
权限配置结构
权限定义为一个分层对象,其中每个工具映射到一个操作:
| 操作 | 行为 | 用例 |
|---|---|---|
| allow | 始终允许 | 受信任环境下的安全工具 |
| deny | 始终阻止 | 危险或不适当的工具 |
| ask | 提示用户 | 需要确认的工具 |
1 | { |
来源:packages/opencode/src/config/config.ts, packages/opencode/src/permission/next.ts
权限合并行为
定义 agent 权限时,它们会按特定顺序与系统默认值合并:
- 系统默认权限
- Agent 特定的权限覆盖
- 用户配置的基础权限
- 外部目录访问许可(除非明确拒绝,否则始终允许 Truncate.DIR)
来源:packages/opencode/src/agent/agent.ts, packages/opencode/src/agent/agent.ts
权限最佳实践
1 | --- |
对于专用 agent,始终以 *: deny 开始限制,然后仅显式允许 agent 目的所需的工具。这可以防止意外副作用和安全风险。
高级配置模式
温度和创意控制
通过温度设置微调 agent 行为:
1 | { |
1 | { |
| Agent 类型 | 温度范围 | 基本原理 |
|---|---|---|
| 创意/探索性 | 0.7-0.9 | 鼓励多样化的输出 |
| 分析/调试 | 0.1-0.4 | 专注、确定性的响应 |
| 代码生成 | 0.3-0.5 | 正确性和多样性的平衡 |
来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts
步骤限制以实现受控执行
使用 steps 参数防止无限循环或过度使用工具:
1 | { |
这会强制 agent 在指定次数的工具调用后提供纯文本响应,使其适合时间敏感的操作。
来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts
每个 Agent 的模型选择
不同的 agent 可以根据其需求使用不同的模型:
1 | { |
来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts
自定义选项和元数据
通过 options 字段传递自定义配置:
1 | { |
frontmatter 中的自定义属性会自动合并到 options 对象中:
1 | --- |
来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts
Agent 生命周期和状态管理
初始化和状态
Agent 通过 Agent.state() 函数延迟加载,该函数将默认配置与用户覆盖合并:
1 | const state = Instance.state(async () => { |
来源:packages/opencode/src/agent/agent.ts
Agent 发现和列表
通过 list 函数检索可用的 agent:
1 | const agents = await Agent.list() |
来源:packages/opencode/src/agent/agent.ts
最佳实践和常见模式
1. 专用 Subagent 模式
为特定任务创建专注的 agent:
1 | --- |
2. 分层 Agent 组织
在子目录中组织 agent 以适应复杂项目:
1 | .opencode/ |
嵌套路径将成为 agent 名称的一部分:frontend/react-component。
来源:packages/opencode/src/config/config.ts
3. 渐进式权限升级
从限制性权限开始,并根据 agent 需求进行扩展:
1 | { |
然后随着测试发现需求,添加更多工具。
4. Agent 的提示词工程
构建清晰的、有效的 agent 提示词:
1 | --- |
5. 内部使用的隐藏 Agent
从 @ 自动补全菜单中隐藏专用 agent:
1 | { |
隐藏的 agent 仍然可供直接调用或被其他 agent 委托,但不会出现在面向用户的 agent 列表中。
来源:packages/opencode/src/config/config.ts
对于主要由其他 agent 以编程方式调用而非由用户直接调用的 agent,请使用 hidden: true 标志。这可以减少认知负荷并防止对可用选项的混淆。