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.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25export namespace Agent {
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
native: z.boolean().optional(),
hidden: z.boolean().optional(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
permission: PermissionNext.Ruleset,
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
prompt: z.string().optional(),
options: z.record(z.string(), z.any()),
steps: z.number().int().positive().optional(),
})
.meta({
ref: "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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents
Guidelines:
- Use Glob for broad file pattern matching
- Use Grep for searching file contents with regex
- Use Read when you know the specific file path you need to read
- Use Bash for file operations like copying, moving, or listing directory contents
- Adapt your search approach based on the thoroughness level specified by the caller
- Return file paths as absolute paths in your final response
- For clear communication, avoid using emojis
- Do not create any files, or run bash commands that modify the user's system state in any way
Complete the user's search request efficiently and report your findings clearly.
隐藏 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43You are a title generator. You output ONLY a thread title. Nothing else.
<task>
Generate a brief title that would help the user find this conversation later.
Follow all rules in <rules>
Use the <examples> so you know what a good title looks like.
Your output must be:
- A single line
- ≤50 characters
- No explanations
</task>
<rules>
- Title must be grammatically correct and read naturally - no word salad
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
- Focus on the main topic or question the user needs to retrieve
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to questions, just generate a title for the conversation
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
→ create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
</rules>
<examples>
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → app.js failure investigation
"implement rate limiting" → Rate limiting implementation
"how do I connect postgres to my API" → Postgres API connection
"best practices for React hooks" → React hooks best practices
"@src/auth.ts can you add refresh token support" → Auth refresh token support
"@utils/parser.ts this is broken" → Parser bug fix
"look at @config.json" → Config review
"@App.tsx add dark mode toggle" → Dark mode toggle in App
</examples>
摘要 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.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),并具备复杂的权限管理和实时状态跟踪功能。
Agent 初始化与注册
Agent 通过集中式状态管理系统进行初始化,该系统将默认配置与用户自定义的配置相结合。注册过程在启动时通过 Instance.state() 机制发生,该机制会从配置系统延迟加载 Agent 配置。
核心 Agent 定义结构包括:
- 模式分类:”primary”、”subagent” 或 “all”,决定可见性和可访问性
- 权限规则集:从默认规则、Agent 特定规则和用户配置中合并而来
- 模型偏好:用于 LLM 路由的可选 providerID/modelID 对
- 提示词模板:覆盖默认值的自定义系统提示词
- 执行参数:Temperature、topP 和步数限制
来源:agent.ts
原生 Agent 使用预定义配置进行注册:
- build:具有完整文件编辑权限的主 Agent
- plan:仅限于修改计划文件的主 Agent
- explore:专用于代码库发现的子 Agent
- general:用于多步任务执行的子 Agent
- 隐藏 Agent:用于内部操作的 compaction、title、summary
来源:agent.ts
自定义 Agent 将用户配置合并到注册表中:
1 | for (const [key, value] of Object.entries(cfg.agent ?? {})) { |
来源:agent.ts
会话创建与 Agent 分配
会话作为 Agent 交互的执行上下文。每个会话与一个项目关联,并维护自己的消息历史、状态和元数据。
会话创建遵循以下流程:
- 使用 Identifier.descending() 生成唯一会话 ID
- 创建带有 ISO 时间戳的默认标题
- 初始化时间跟踪(创建时间、更新时间)
- 持久化到存储并发布创建事件
- 根据配置选择性启用自动共享
来源:index.ts
Session.initialize() 函数将初始命令(INIT)注入到消息流中,从而建立 Agent 上下文:
1 | export const initialize = fn( |
来源:index.ts
Agent 执行流程
执行引擎遵循流式事件驱动架构,其中 Agent 通过 SessionProcessor 处理输入。处理器管理从请求启动到完成的完整生命周期。
执行状态机
消息处理生命周期
处理器处理来自 LLM 流的多种事件类型:
- 开始阶段:将会话状态设置为 “busy”
- 推理阶段:捕获带有元数据跟踪的思维链推理
- 工具调用阶段:
- tool-input-start:创建待处理的工具部分
- tool-call:转换到运行状态,执行工具
- Doom loop 检测:监控相同的工具调用(阈值:3)
- 响应阶段:使用工具结果或错误进行更新
- 文本生成阶段:实时流式传输助手响应
- 步骤完成:跟踪快照、使用情况和补丁生成
来源:processor.ts
Doom Loop 预防
系统实现了自动 doom loop 检测,以防止无限的工具调用循环:
1 | const parts = await MessageV2.parts(input.assistantMessage.id) |
来源:processor.ts
Doom Loop 检测机制
核心原理
系统监控连续的工具调用,如果检测到相同工具以相同参数被重复调用,就会触发用户确认。阈值设为 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 | // 工具会被标记为错误状态 |
这种设计既防止了无限循环浪费资源,又给用户提供了必要的控制权——如果是合理的重复操作,用户可以选择继续。
状态管理与持久化
消息部分跟踪
每个 Agent 交互由多个部分(文本、工具调用、推理、快照)组成,这些部分作为独立的实体存储。这种细粒度的存储能够实现实时 UI 更新和高效的状态重构。
部分通过增量流式更新:
- 文本部分:text-delta 事件追加到现有内容
- 推理部分:带有提供程序元数据的类似增量更新
- 工具部分:状态转换(pending → running → completed/error)
来源:processor.ts
会话元数据
会话维护全面的元数据,包括:
- 摘要跟踪:添加/删除计数、文件更改、diff 快照
- 时间跟踪:created、updated、compacting、archived 时间戳
- 共享状态:用于协作的 URL 和密钥
- 恢复状态:时间点恢复信息
- 权限状态:Agent 特定的权限覆盖
来源:index.ts
快照和 Diff 管理
系统使用快照在步骤级别跟踪文件系统更改:
1 | case "start-step": |
在步骤完成时,生成并存储补丁:
1 | case "finish-step": |
来源:processor.ts
系统持久化机制详解
系统的持久化机制设计得相当优雅。
存储类型
基于文件系统的 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 格式保持了简单性和可维护性。
错误处理与恢复
重试机制
处理器针对可重试错误(速率限制、网络问题)实现指数退避:
1 | const error = MessageV2.fromError(e, { providerID: input.model.providerID }) |
来源:processor.ts
权限拒绝处理
权限错误根据配置触发不同的行为:
- continue_loop_on_deny:false(默认)在拒绝时停止执行
- continue_loop_on_deny:true 允许在权限拒绝后继续执行
1 | if ( |
来源:processor.ts
终止与清理
中止信号传播到整个执行堆栈:
1 | for await (const value of stream.fullStream) { |
终止时,系统会:
- 完成所有待处理的快照
- 将未完成的工具标记为错误
- 更新消息完成时间戳
- 发布终止状态
来源:processor.ts
会话分支与分叉
分叉操作创建独立的子会话,这些子会话继承指定点之前的消息历史:
1 | export const fork = fn( |
来源:index.ts
这使得在不影响主要对话上下文的情况下创建实验性分支成为可能。
生命周期事件
系统通过事件总线发布全面的事件,用于实时监控和 UI 更新:
1 | export const Event = { |
来源:index.ts
会话终止与清理
会话支持级联删除,在移除父会话之前递归删除子会话:
1 | export const remove = fn(Identifier.schema("session"), async (sessionID) => { |
创建自定义 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 标志。这可以减少认知负荷并防止对可用选项的混淆。