0%

跟着OpenCode学智能体设计和开发1:Agent系统

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export 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
18
You 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
12
You 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
43
You 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
11
Summarize 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
2
3
4
5
6
7
8
9
10
build: {
name: "build",
options: {},
permission: ...,
mode: "primary",
native: true,
// 没有 prompt 字段!
},
plan: { /* 同样没有 prompt 字段 */ },
general: { /* 同样没有 prompt 字段 */ }

而其他agent如explore、summary等都有明确的prompt:

1
2
3
4
5
explore: {
...
prompt: PROMPT_EXPLORE, // 明确指定了prompt
...
}

2. Prompt组装逻辑

关键在llm.ts:65-L77的stream函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
const system = SystemPrompt.header(input.model.providerID)
system.push(
[
// 使用agent的prompt,如果没有则使用provider的prompt
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
// 自定义传入的prompt
...input.system,
// 用户消息中的自定义prompt
...(input.user.system ? [input.user.system] : []),
]
.filter((x) => x)
.join("\n"),
)

核心逻辑

  • 如果input.agent.prompt存在,使用agent的prompt
  • 否则,使用SystemPrompt.provider(input.model)根据模型类型选择prompt

3. 根据模型类型选择Prompt

SystemPrompt.providersystem.ts:28-L34中定义:

1
2
3
4
5
6
7
8
export function provider(model: Provider.Model) {
if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
return [PROMPT_BEAST]
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}

总结

Agent Prompt来源 机制
build 根据模型类型动态选择 没有设置prompt字段,fallback到SystemPrompt.provider()
plan 根据模型类型动态选择 + plan模式特殊处理 同上,但在plan模式会额外注入plan.txt的只读限制
general 根据模型类型动态选择 同build

这种设计的好处是:

  1. 灵活性:同一个agent可以根据使用的不同模型自动适配对应的prompt
  2. 可维护性:不需要为每个agent复制粘贴相同的prompt模板
  3. 统一性:确保所有使用相同模型的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
7
const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const TaskTool = Tool.define("task", async (ctx) => {
// 获取所有非primary模式的agent
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))

// 根据调用者的权限过滤可访问的agent
const caller = ctx?.agent
const accessibleAgents = caller
? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny")
: agents

return {
description: DESCRIPTION.replace("{agents}", ...), // 动态生成可用agent列表
parameters,
execute(params, ctx) { ... }
}
})

关键点

  • 只有mode !== "primary"的agent才能被调用(即subagent)
  • 权限系统控制agent间调用关系
  • 动态生成agent描述,AI能看到可用的agent列表

2. 权限检查流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async execute(params: z.infer<typeof parameters>, ctx) {
// 跳过权限检查的特殊情况(用户通过@或命令触发)
if (!ctx.extra?.bypassAgentCheck) {
await ctx.ask({
permission: "task",
patterns: [params.subagent_type],
always: ["*"],
metadata: {
description: params.description,
subagent_type: params.subagent_type,
},
})
}
// ...
}

权限评估逻辑(在next.ts:107-L125):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export 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
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
const session = await iife(async () => {
if (params.session_id) {
// 继续已有会话
const found = await Session.get(params.session_id).catch(() => {})
if (found) return found
}

// 创建新会话,设置严格权限
return await Session.create({
parentID: ctx.sessionID, // 建立父子关系
title: params.description + ` (@${agent.name} subagent)`,
permission: [
// 禁止嵌套调用task(防止无限递归)
{ permission: "task", pattern: "*", action: "deny" },
// 禁止创建和管理todos
{ permission: "todowrite", pattern: "*", action: "deny" },
{ permission: "todoread", pattern: "*", action: "deny" },
// 允许配置的primary tools
...(config.experimental?.primary_tools?.map((t) => ({
pattern: "*",
action: "allow" as const,
permission: t,
})) ?? []),
],
})
})

安全隔离

  • 嵌套调用限制:禁止子agent再调用task(防递归)
  • 工具限制:子agent只能使用配置允许的工具
  • 会话隔离:独立的会话状态和上下文

4. 实时进度反馈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const parts: Record<string, {...}> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return
if (evt.properties.part.type !== "tool") return

parts[part.id] = {
id: part.id,
tool: part.tool,
state: { status: part.state.status, title: ... }
}

// 更新调用方的元数据
ctx.metadata({
title: params.description,
metadata: {
summary: Object.values(parts),
sessionId: session.id,
},
})
})

事件流

  • 子agent的工具调用通过事件系统广播
  • 调用方实时接收进度更新
  • 用户可以看到agent的工作状态

5. Agent执行和结果返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const result = await SessionPrompt.prompt({
messageID,
sessionID: session.id,
model: agent.model ?? { modelID: msg.info.modelID, providerID: msg.info.providerID },
agent: agent.name,
tools: { todowrite: false, todoread: false, task: false, ... }, // 禁用特定工具
parts: promptParts,
})

unsub() // 取消事件订阅

// 生成输出,包含元数据
const output = text + "\n\n" + [
"<task_metadata>",
`session_id: ${session.id}`,
"</task_metadata>"
].join("\n")

return { title, metadata: { summary, sessionId: session.id }, output }

6. Agent调用链

1
2
3
4
5
6
7
8
9
10
11
12
User Prompt

Primary Agent (build/plan)
↓ Task(subagent_type="explore", prompt="find auth logic")
Subagent (explore) [独立会话]
├─ Grep("authentication")
├─ Read("src/auth.ts")
└─ Return: "Auth is in src/auth.ts:45"

Primary Agent 接收结果

Continue with result

关键设计要点

安全机制

  1. 权限隔离:每个agent有独立的权限集
  2. 嵌套限制:禁止子agent递归调用task
  3. 工具限制:子agent只能使用允许的工具
  4. 用户审批:敏感操作需要用户确认

会话管理

  1. 父子关系parentID建立会话层级
  2. 独立状态:子agent有自己的消息历史
  3. 状态隔离:修改不会影响父会话

通信机制

  1. 事件总线:通过Bus系统传递进度
  2. 实时反馈:调用方可以监控子agent状态
  3. 结果汇总:工具调用状态会被汇总返回

灵活性

  1. 动态agent列表:工具描述包含可用agent
  2. 会话续接:通过session_id可以继续之前的任务
  3. 配置化权限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
26
explore: {
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 配置时,请考虑以下准则:

  1. 主 Agent 应具有广泛的权限集,但对破坏性操作使用 “ask” 动作。这在保持安全的同时实现了灵活性。
  2. 子 Agent 应具有严格限制的范围和最少的权限。每个子 Agent 应专注于特定的能力。
  3. 隐藏 Agent 不得公开工具或写入操作。它们应该是仅处理现有数据的纯分析 Agent。
  4. 自定义 Agent 当你希望同时具有直接用户访问和任务工具调用能力时,应指定 mode: “all”。
  5. 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
2
3
4
5
6
7
{
"permission": {
"read": "allow",
"edit": "ask",
"bash": "deny"
}
}

为了进行更多控制,可以使用对象语法来指定模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"permission": {
"read": "allow",
"edit": {
"*.md": "allow",
"src/*.ts": "ask",
"node_modules/**": "deny"
},
"bash": {
"git*": "allow",
"npm install": "ask",
"rm -rf": "deny"
}
}
}

来源: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"agent": {
"coder": {
"permission": {
"edit": "allow",
"bash": {
"git*": "allow",
"npm install": "ask"
}
}
},
"reviewer": {
"permission": {
"edit": "deny",
"read": "allow"
}
}
}
}

Agent 特定权限会覆盖该 Agent 的全局设置,使您能够创建具有不同安全配置文件的 Agent。

来源:config/config.ts

权限评估流程

请求生成

当调用工具时,它会生成一个包含以下内容的权限请求:

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": "string", // 唯一的权限请求 ID
"sessionID": "string", // 当前会话标识符
"permission": "string", // 权限类型(例如 "edit", "bash")
"patterns": ["string"], // 此请求匹配的模式
"metadata": {}, // 附加上下文(文件路径、命令等)
"always": ["string"], // 如果用户选择“总是允许”则自动批准的模式
"tool": { // 工具调用上下文
"messageID": "string",
"callID": "string"
}
}

来源:permission/next.ts

规则评估

系统使用以下逻辑根据配置的规则集评估请求:

来源:permission/next.ts, permission/next.ts

评估算法

evaluate() 函数合并所有规则集,并使用通配符匹配找到最后一个匹配的规则:

1
2
3
4
5
6
7
export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule {
const merged = merge(...rulesets)
const match = merged.findLast(
(rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)
)
return match ?? { action: "ask", permission, pattern: "*" }
}

通配符匹配确保了像 src/**/*.ts 这样的模式能正确匹配嵌套目录结构。

来源:permission/next.ts

用户响应处理

响应类型

用户可以通过三个选项响应权限请求:

响应 行为 用例
once (仅此一次) 仅批准此单个请求 对一次性操作的临时批准
always (总是允许) 批准并为将来的匹配保存规则 您信任的重复操作
reject (拒绝) 拒绝此请求并停止执行 您想要阻止的操作

当选择“总是允许”时,系统会自动批准所有与保存的模式匹配的待处理请求。

来源:permission/next.ts

错误处理

系统为不同的拒绝场景提供了三种不同的错误类型:

  • DeniedError:由配置规则自动拒绝,包含匹配的规则集以供参考
  • RejectedError:用户拒绝且未提供消息,使用默认消息停止执行
  • CorrectedError:用户拒绝并提供了反馈消息,为使用不同参数重试提供指导

这种区别有助于 Agent 理解它们应该重试、修改方法还是完全停止。

来源:permission/next.ts

工具集成示例

文件编辑工具

编辑工具在修改文件之前请求权限:

1
2
3
4
5
6
7
8
9
await ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)],
always: ["*"],
metadata: {
filepath: filePath,
diff: "generated diff..."
}
})

这允许用户批准特定文件或像 src/**/*.ts 这样的模式以进行自动批准。

来源:tool/edit.ts

Bash 工具

Bash 工具执行复杂的命令解析以确定权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 解析命令结构
const tree = await parser().then((p) => p.parse(params.command))

// 为不同的命令类型提取模式
for (const node of tree.rootNode.descendantsOfType("command")) {
const command = extractCommandTokens(node)

// 检查文件系统操作
if (["rm", "cp", "mv", "mkdir"].includes(command[0])) {
directories.add(resolvedPath)
}

// 添加命令模式以进行权限检查
patterns.add(command.join(" "))
always.add(BashArity.prefix(command).join(" ") + "*")
}

系统使用命令元数检测来识别“人类可理解”的命令部分。例如,npm install package-name 被识别为 npm install* 用于权限模式。

来源:tool/bash.ts, permission/arity.ts

Bash 工具包含一个全面的元数字典,涵盖 150 多个常用命令(git, npm, docker, kubectl 等)。这确保了 npm run devnpm install 被视为不同的模式,从而允许精细的权限控制。

命令元数字典

系统包含一个预构建的字典,将命令前缀映射到它们的元数(定义命令的标记数量):

1
2
3
4
5
6
7
8
9
10
const ARITY: Record<string, number> = {
git: 2, // git checkout, git commit
"git config": 3, // git config user.name
npm: 2, // npm install
"npm run": 3, // npm run dev
docker: 2, // docker run
"docker compose": 3, // docker compose up
kubectl: 2, // kubectl get pods
// ... 150+ 命令
}

这实现了基于模式的权限,可以理解命令语义。

来源:permission/arity.ts

插件集成

权限 Hooks

插件可以通过 permission.ask hook 拦截权限请求,从而实现自定义授权逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 插件实现
export const MyPlugin: Plugin = async (input) => {
return {
permission: {
ask: async (request, output) => {
// 自定义逻辑以确定响应
if (shouldAutoApprove(request)) {
output.status = "allow"
} else if (shouldAutoDeny(request)) {
output.status = "deny"
} else {
output.status = "ask" // 让用户决定
}
}
}
}
}

这允许插件实现特定领域的安全策略,与外部授权系统集成,或根据上下文因素提供自动批准。

来源:plugin/index.ts, permission/index.ts

旧系统迁移

从 Tools 字段迁移

系统会自动将旧版 tools 配置迁移到新的 permission 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 旧格式(已弃用)
{
"tools": {
"edit": true,
"bash": false
}
}

// 自动转换为:
{
"permission": {
"edit": "allow",
"bash": "deny"
}
}

同样,已弃用的 autoshare 字段会迁移到 share 字段。这种向后兼容性确保现有配置无需手动更新即可继续工作。

来源:config/config.ts, config/config.ts

高级配置模式

特定环境权限

为不同的环境配置不同的权限集:

1
2
3
4
5
6
7
8
9
10
11
{
"permission": {
"bash": {
"git*": "allow",
"npm run dev": "ask",
"npm run build": "allow",
"docker-compose -f docker-compose.dev.yml *": "ask",
"docker-compose -f docker-compose.prod.yml *": "deny"
}
}
}

基于目录的安全性

将操作限制在特定的项目区域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"permission": {
"edit": {
"src/**": "allow",
"tests/**": "allow",
"docs/**": "ask",
"node_modules/**": "deny",
".git/**": "deny"
},
"external_directory": {
"/tmp/**": "ask",
"/home/user/**": "deny"
}
}
}

命令白名单

为生产环境实施严格的命令白名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"permission": {
"bash": "deny",
"bash": {
"git status": "allow",
"git diff": "allow",
"git log -10": "allow",
"npm run test": "allow",
"npm run lint": "allow",
"*": "deny"
}
}
}

这种默认拒绝的方法确保只有明确批准的命令才能执行。

来源:config/config.ts

状态管理和持久化

权限状态

系统维护权限状态,包括:

  • 待处理请求:当前等待用户批准
  • 已批准规则:在会话期间保存的自动批准模式
  • 会话隔离:权限范围限定在每个会话

已批准的规则会持久化到 ["permission", projectID] 下的存储中,允许规则在会话之间持久化(目前处于注释状态,等待 UI 管理实现)。

来源:permission/next.ts, permission/next.ts

会话清理

会话终止时,系统会自动拒绝所有待处理的权限请求,以防止孤立操作:

1
2
3
4
5
6
7
async (state) => {
for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) {
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
}
}
}

这确保了清晰的会话边界,并防止工具在会话结束后执行。

来源:permission/index.ts

事件系统

权限事件

系统发布事件以与 UI 和其他组件集成:

1
2
3
4
5
6
7
8
9
10
11
export const Event = {
Asked: BusEvent.define("permission.asked", Request),
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),
requestID: z.string(),
reply: Reply
})
)
}

组件可以订阅这些事件以:

  • 向用户显示权限对话框
  • 跟踪权限使用情况以进行分析
  • 实现自定义审批工作流
  • 监控安全态势

来源:permission/next.ts, permission/index.ts

禁用工具检测

系统提供了一个实用函数,用于识别通过配置全局禁用的工具:

1
2
3
4
5
6
7
8
9
10
export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
const result = new Set<string>()
for (const tool of tools) {
const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool
const rule = ruleset.findLast((r) => Wildcard.match(permission, r.permission))
if (!rule) continue
if (rule.pattern === "*" && rule.action === "deny") result.add(tool)
}
return result
}

这允许 UI 组件根据配置隐藏或禁用不可用的工具,通过防止挫败感来改善用户体验。

来源:permission/next.ts

最佳实践

安全考虑

  1. 默认询问:对于开发环境,使用 “ask” 作为默认操作,以保持对 Agent 操作的了解
  2. 生产白名单:在生产环境中,使用 “deny” 作为默认值,并为受信任的操作设置明确的允许规则
  3. 模式具体性:将模式按从具体到通用的顺序排列,以确保正确的覆盖行为
  4. 外部访问:始终要求对 external_directory 和网络工具进行批准
  5. 破坏性命令:明确拒绝危险模式,如 rm -rf 或 docker rm *

配置建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"permission": {
"read": "allow",
"edit": "ask",
"bash": {
"git*": "allow",
"npm*": "ask",
"rm -rf": "deny",
"*": "ask"
},
"external_directory": "ask",
"webfetch": "ask",
"websearch": "deny"
}
}

这种平衡的方法允许安全的读取操作,同时要求对修改和外部访问进行监督。

来源: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
permission: PermissionNext.merge(defaults, user),
options: {},
native: false,
}
// 合并用户针对 model, prompt, temperature, topP, mode, color, hidden, name, steps, options, permission 的覆盖配置
}

来源:agent.ts

会话创建与 Agent 分配

会话作为 Agent 交互的执行上下文。每个会话与一个项目关联,并维护自己的消息历史、状态和元数据。

会话创建遵循以下流程:

  1. 使用 Identifier.descending() 生成唯一会话 ID
  2. 创建带有 ISO 时间戳的默认标题
  3. 初始化时间跟踪(创建时间、更新时间)
  4. 持久化到存储并发布创建事件
  5. 根据配置选择性启用自动共享

来源:index.ts

Session.initialize() 函数将初始命令(INIT)注入到消息流中,从而建立 Agent 上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const initialize = fn(
z.object({
sessionID: Identifier.schema("session"),
modelID: z.string(),
providerID: z.string(),
messageID: Identifier.schema("message"),
}),
async (input) => {
await SessionPrompt.command({
sessionID: input.sessionID,
messageID: input.messageID,
model: input.providerID + "/" + input.modelID,
command: Command.Default.INIT,
arguments: "",
})
},
)

来源:index.ts

Agent 执行流程

执行引擎遵循流式事件驱动架构,其中 Agent 通过 SessionProcessor 处理输入。处理器管理从请求启动到完成的完整生命周期。

执行状态机

消息处理生命周期

处理器处理来自 LLM 流的多种事件类型:

  • 开始阶段:将会话状态设置为 “busy”
  • 推理阶段:捕获带有元数据跟踪的思维链推理
  • 工具调用阶段:
    • tool-input-start:创建待处理的工具部分
    • tool-call:转换到运行状态,执行工具
    • Doom loop 检测:监控相同的工具调用(阈值:3)
  • 响应阶段:使用工具结果或错误进行更新
  • 文本生成阶段:实时流式传输助手响应
  • 步骤完成:跟踪快照、使用情况和补丁生成

来源:processor.ts

Doom Loop 预防

系统实现了自动 doom loop 检测,以防止无限的工具调用循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const parts = await MessageV2.parts(input.assistantMessage.id)
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)

if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
(p) =>
p.type === "tool" &&
p.tool === value.toolName &&
p.state.status !== "pending" &&
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await PermissionNext.ask({
permission: "doom_loop",
patterns: [value.toolName],
sessionID: input.assistantMessage.sessionID,
metadata: { tool: value.toolName, input: value.input },
always: [value.toolName],
ruleset: agent.permission,
})
}

来源:processor.ts

Doom Loop 检测机制

核心原理

系统监控连续的工具调用,如果检测到相同工具以相同参数被重复调用,就会触发用户确认。阈值设为 3 次。

具体实现步骤

参见 processor.tsL20-L168

  1. 设定阈值(第 20 行)

    1
    const DOOM_LOOP_THRESHOLD = 3
  2. 检测时机:每次执行工具调用时(tool-call 事件)

  3. 检测逻辑(第 143-168 行)

    • 获取当前消息的所有部分:const parts = await MessageV2.parts(input.assistantMessage.id)
    • 检查最后 3 个工具调用:const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
    • 判断条件:只有当满足以下所有条件时才视为 doom loop:
      1. 恰好有 3 个工具调用
      2. 都是 tool 类型
      3. 使用相同的工具名称
      4. 不是 pending 状态(说明已经执行过)
      5. 输入参数完全相同(通过 JSON 序列化比较)
  4. 触发权限询问

    • 调用 PermissionNext.ask() 请求用户确认
    • 提供 doom_loop 权限类型
    • 传递工具名称和输入参数作为元数据
处理机制

如果用户拒绝继续:

参见 processor.tsL212-L217

1
2
3
// 工具会被标记为错误状态
// 如果配置允许(continue_loop_on_deny),会继续执行;否则停止处理
// 返回 "stop" 状态终止循环

这种设计既防止了无限循环浪费资源,又给用户提供了必要的控制权——如果是合理的重复操作,用户可以选择继续。

状态管理与持久化

消息部分跟踪

每个 Agent 交互由多个部分(文本、工具调用、推理、快照)组成,这些部分作为独立的实体存储。这种细粒度的存储能够实现实时 UI 更新和高效的状态重构。

部分通过增量流式更新:

  • 文本部分:text-delta 事件追加到现有内容
  • 推理部分:带有提供程序元数据的类似增量更新
  • 工具部分:状态转换(pending → running → completed/error)

来源:processor.ts

会话元数据

会话维护全面的元数据,包括:

  • 摘要跟踪:添加/删除计数、文件更改、diff 快照
  • 时间跟踪:created、updated、compacting、archived 时间戳
  • 共享状态:用于协作的 URL 和密钥
  • 恢复状态:时间点恢复信息
  • 权限状态:Agent 特定的权限覆盖

来源:index.ts

快照和 Diff 管理

系统使用快照在步骤级别跟踪文件系统更改:

1
2
3
4
5
6
7
8
9
10
case "start-step":
snapshot = await Snapshot.track()
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: input.assistantMessage.id,
sessionID: input.sessionID,
snapshot,
type: "step-start",
})
break

在步骤完成时,生成并存储补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case "finish-step":
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: input.assistantMessage.id,
sessionID: input.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
}
break

来源:processor.ts

系统持久化机制详解

系统的持久化机制设计得相当优雅。

存储类型

基于文件系统的 JSON 存储,参见 storage.tsL143-L158

  • 所有数据存储在 {Global.Path.data}/storage/ 目录
  • 每个实体都是独立的 .json 文件
  • 使用读写锁机制保证并发安全(参见 LockL172

存储格式与目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
storage/
├── session/ # 会话信息
│ └── <projectID>/
│ └── <sessionID>.json
├── message/ # 消息
│ └── <sessionID>/
│ └── <messageID>.json
├── part/ # 消息部分(文本、工具调用等)
│ └── <messageID>/
│ └── <partID>.json
├── session_diff/ # 会话变更历史
│ └── <sessionID>.json
├── share/ # 分享信息
│ └── <sessionID>.json
├── project/ # 项目信息
│ └── <projectID>.json
└── migration # 迁移版本号

核心数据结构

1. 会话(Session)

参见 session/index.tsL39-L79

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Session.Info = {
id: string // 会话 ID
projectID: string // 所属项目
directory: string // 工作目录
parentID?: string // 父会话(分叉场景)
title: string // 会话标题
version: string // 创建时的版本
summary?: { // 变更摘要
additions: number
deletions: number
files: number
diffs?: FileDiff[]
}
share?: { url: string } // 分享 URL
time: { // 时间戳
created: number
updated: number
compacting?: number
archived?: number
}
permission?: Ruleset // 权限规则集
revert?: RevertInfo // 回滚信息
}
2. 消息(MessageV2)

参见 message-v2.tsL298-L390

用户消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type User = {
id: string
sessionID: string
role: "user"
time: { created: number }
agent: string // 使用的 Agent
model: { // 模型配置
providerID: string
modelID: string
}
system?: string // 系统提示
tools?: Record<string, boolean> // 工具启用状态
variant?: string // 模型变体
}
助手消息:
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
type Assistant = {
id: string
sessionID: string
role: "assistant"
parentID: string // 关联的用户消息 ID
modelID: string
providerID: string
agent: string
mode: string // @deprecated
path: { cwd: string, root: string }
cost: number // 成本
tokens: { // Token 使用统计
input: number
output: number
reasoning: number
cache: { read: number, write: number }
}
finish?: string // 完成原因
time: {
created: number
completed?: number
}
error?: Error // 错误信息
summary?: boolean // 是否为摘要消息
}
3. 消息部分(Parts)

消息由多个部分组成,支持多种类型,参见 message-v2.tsL323-L341

类型 用途
text 文本内容(助手回复或用户输入)
tool 工具调用(输入、输出、错误)
reasoning 推理过程(思维链)
file 文件附件
snapshot 文件快照
patch 代码补丁
step-start/finish 执行步骤标记
compaction 会话压缩标记
subtask 子任务
retry 重试信息
agent Agent 信息
工具部分详解(最复杂):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type ToolPart = {
id: string
sessionID: string
messageID: string
callID: string // LLM 的工具调用 ID
tool: string // 工具名称
state: ToolState // 状态机
}

type ToolState =
| { status: "pending", input: any, raw: string }
| { status: "running", input: any, time: { start: number } }
| { status: "completed",
input: any,
output: string,
title: string,
time: { start: number, end: number, compacted?: number },
attachments?: FilePart[] }
| { status: "error",
input: any,
error: string,
time: { start: number, end: number } }

持久化操作

核心 API 在 storage.tsL160-L226

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 读取
Storage.read<T>(key: string[]) -> T

// 写入(覆盖)
Storage.write<T>(key: string[], content: T) -> void

// 更新(读取-修改-写入)
Storage.update<T>(key: string[], fn: (draft: T) => void) -> T

// 删除
Storage.remove(key: string[]) -> void

// 列举
Storage.list(prefix: string[]) -> string[][]

使用示例(来自 session/index.tsL209):

1
2
3
4
5
6
7
8
9
10
11
// 创建会话
await Storage.write(
["session", Instance.project.id, result.id],
result
)

// 更新消息
await Storage.write(["message", msg.sessionID, msg.id], msg)

// 更新部分
await Storage.write(["part", part.messageID, part.id], part)

数据迁移

系统支持 schema 迁移,参见 storage.tsL23-L141

  • 迁移脚本在 MIGRATIONS 数组中
  • 每次启动时检查 migration 文件
  • 按顺序执行未执行的迁移

会话压缩(优化存储)

当会话接近上下文限制时,系统会进行压缩,参见 compaction.tsL30-L39

主动修剪:
  • 从旧工具调用中删除输出(保留调用信息)
  • 保护最近的 40,000 tokens 和特定工具(如 skill)
  • 只删除超过 20,000 tokens 的内容
会话压缩:
  • 使用 LLM 生成对话摘要
  • 在压缩点插入 compaction part
  • 后续消息可以基于摘要恢复上下文

这种设计既保证了数据的完整性和可追溯性,又通过文件系统和 JSON 格式保持了简单性和可维护性。

错误处理与恢复

重试机制

处理器针对可重试错误(速率限制、网络问题)实现指数退避:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
attempt++
const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
SessionStatus.set(input.sessionID, {
type: "retry",
attempt,
message: retry,
next: Date.now() + delay,
})
await SessionRetry.sleep(delay, input.abort).catch(() => {})
continue
}

来源:processor.ts

权限拒绝处理

权限错误根据配置触发不同的行为:

  • continue_loop_on_deny:false(默认)在拒绝时停止执行
  • continue_loop_on_deny:true 允许在权限拒绝后继续执行
1
2
3
4
5
6
if (
value.error instanceof PermissionNext.RejectedError ||
value.error instanceof Question.RejectedError
) {
blocked = shouldBreak
}

来源:processor.ts

终止与清理

中止信号传播到整个执行堆栈:

1
2
3
4
for await (const value of stream.fullStream) {
input.abort.throwIfAborted()
// 处理事件...
}

终止时,系统会:

  1. 完成所有待处理的快照
  2. 将未完成的工具标记为错误
  3. 更新消息完成时间戳
  4. 发布终止状态

来源:processor.ts

会话分支与分叉

分叉操作创建独立的子会话,这些子会话继承指定点之前的消息历史:

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
export const fork = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
}),
async (input) => {
const session = await createNext({
directory: Instance.directory,
})
const msgs = await messages({ sessionID: input.sessionID })
const idMap = new Map<string, string>()

for (const msg of msgs) {
if (input.messageID && msg.info.id >= input.messageID) break
const newID = Identifier.ascending("message")
idMap.set(msg.info.id, newID)

const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined
const cloned = await updateMessage({
...msg.info,
sessionID: session.id,
id: newID,
...(parentID && { parentID }),
})

for (const part of msg.parts) {
await updatePart({
...part,
id: Identifier.ascending("part"),
messageID: cloned.id,
sessionID: session.id,
})
}
}
return session
},
)

来源:index.ts

这使得在不影响主要对话上下文的情况下创建实验性分支成为可能。

生命周期事件

系统通过事件总线发布全面的事件,用于实时监控和 UI 更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
export const Event = {
Created: BusEvent.define("session.created", z.object({ info: Info })),
Updated: BusEvent.define("session.updated", z.object({ info: Info })),
Deleted: BusEvent.define("session.deleted", z.object({ info: Info })),
Diff: BusEvent.define("session.diff", z.object({
sessionID: z.string(),
diff: Snapshot.FileDiff.array(),
})),
Error: BusEvent.define("session.error", z.object({
sessionID: z.string().optional(),
error: MessageV2.Assistant.shape.error,
})),
}

来源:index.ts

会话终止与清理

会话支持级联删除,在移除父会话之前递归删除子会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
const project = Instance.project
try {
const session = await get(sessionID)
for (const child of await children(sessionID)) {
await remove(child.id)
}
await unshare(sessionID).catch(() => {})
for (const msg of await Storage.list(["message", sessionID])) {
for (const part of await Storage.list(["part", msg.at(-1)!])) {
await Storage.remove(part)
}
await Storage.remove(msg)
}
await Storage.remove(["session", project.id, sessionID])
Bus.publish(Event.Deleted, { info: session })
} catch (e) {
log.error(e)
}
})

创建自定义 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
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
---
name: code-reviewer
description: 分析代码变更的质量、安全漏洞和最佳实践违规
mode: subagent
temperature: 0.3
permission:
read: allow
grep: allow
websearch: deny
---

你是一位代码审查专家。你的专业能力包括:

- 安全漏洞检测
- 性能优化机会识别
- 代码风格和最佳实践执行
- 架构模式分析

在审查代码时:
1. 识别潜在的安全问题
2. 检查常见的反模式
3. 提出可读性改进建议
4. 验证适当的错误处理
5. 评估测试覆盖需求

在适当的地方提供带有具体代码示例的建设性反馈。

将此文件保存为项目目录中的 .opencode/agent/code-reviewer.md

来源:packages/opencode/src/config/config.ts

方法 2:JSON 配置

对于程序化配置,你可以直接在 opencode.json 中定义 agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"agent": {
"code-reviewer": {
"name": "code-reviewer",
"description": "分析代码变更的质量和安全性",
"mode": "subagent",
"temperature": 0.3,
"permission": {
"read": "allow",
"grep": "allow",
"websearch": "deny"
},
"prompt": "你是一位代码审查专家..."
}
}
}

来源:packages/opencode/src/config/config.ts

方法 3:AI 辅助生成

OpenCode 提供了一个内置的 agent 生成功能,使用 AI 根据自然语言描述创建 agent 配置:

1
2
3
4
5
6
const result = await Agent.generate({
description: "一个专注于将遗留 JavaScript 代码重构为现代 TypeScript 的 agent",
model: { providerID: "anthropic", modelID: "claude-3-sonnet-20240229" }
})

// 返回:{ identifier, whenToUse, systemPrompt }

此函数会验证生成的标识符不与现有 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
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
---
name: explore
mode: subagent
permission:
"*": deny
grep: allow
glob: allow
list: allow
bash: allow
webfetch: allow
websearch: allow
codesearch: allow
read: allow
---

你是一位文件搜索专家。你擅长彻底地导航和探索代码库。

你的强项:
- 使用 glob 模式快速查找文件
- 使用强大的正则表达式搜索代码和文本
- 阅读和分析文件内容

指导原则:
- 使用 Glob 进行广泛的文件模式匹配
- 使用 Grep 通过正则表达式搜索文件内容
- 当你知道需要阅读的具体文件路径时,使用 Read
- 在最终响应中以绝对路径返回文件路径
- 为了清晰沟通,请避免使用表情符号
- 不要创建任何文件,或运行修改用户系统状态的 bash 命令

来源:packages/opencode/src/agent/prompt/explore.txt, packages/opencode/src/agent/agent.ts

Build Agent - 用于代码生成的主要 agent

1
2
3
4
5
6
7
8
9
10
11
12
13
build: {
name: "build",
options: {},
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({
question: "allow",
}),
user,
),
mode: "primary",
native: true,
}

来源:packages/opencode/src/agent/agent.ts

权限系统集成

自定义 agent 可以覆盖全局权限配置,根据 agent 的预期用途限制或扩展工具访问。

权限配置结构

权限定义为一个分层对象,其中每个工具映射到一个操作:

操作 行为 用例
allow 始终允许 受信任环境下的安全工具
deny 始终阻止 危险或不适当的工具
ask 提示用户 需要确认的工具
1
2
3
4
5
6
7
8
9
10
11
12
{
"permission": {
"read": "allow",
"edit": {
"*.js": "allow",
"*.env": "deny",
"node_modules/**": "deny"
},
"bash": "ask",
"websearch": "deny"
}
}

来源:packages/opencode/src/config/config.ts, packages/opencode/src/permission/next.ts

权限合并行为

定义 agent 权限时,它们会按特定顺序与系统默认值合并:

  1. 系统默认权限
  2. Agent 特定的权限覆盖
  3. 用户配置的基础权限
  4. 外部目录访问许可(除非明确拒绝,否则始终允许 Truncate.DIR)

来源:packages/opencode/src/agent/agent.ts, packages/opencode/src/agent/agent.ts

权限最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
name: security-scanner
description: 扫描代码中的安全漏洞
mode: subagent
permission:
"*": deny
read: allow
grep: allow
glob: allow
webfetch: allow
bash: deny
edit: deny
---

你是一位安全专家。仅专注于阅读和分析代码。
在安全分析期间切勿执行任意命令或修改文件。

对于专用 agent,始终以 *: deny 开始限制,然后仅显式允许 agent 目的所需的工具。这可以防止意外副作用和安全风险。

高级配置模式

温度和创意控制

通过温度设置微调 agent 行为:

1
2
3
4
5
{
"name": "creative-writer",
"temperature": 0.9,
"topP": 0.95
}
1
2
3
4
5
{
"name": "api-designer",
"temperature": 0.2,
"topP": 0.7
}
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
2
3
4
5
6
{
"name": "quick-audit",
"mode": "subagent",
"steps": 3,
"description": "具有有限迭代的快速代码审计"
}

这会强制 agent 在指定次数的工具调用后提供纯文本响应,使其适合时间敏感的操作。

来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts

每个 Agent 的模型选择

不同的 agent 可以根据其需求使用不同的模型:

1
2
3
4
5
6
7
8
9
10
11
12
{
"agent": {
"explore": {
"model": "anthropic/claude-3-haiku-20240307",
"description": "使用轻量级模型快速探索"
},
"code-analysis": {
"model": "anthropic/claude-3-opus-20240229",
"description": "使用最强大的模型进行深入分析"
}
}
}

来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts

自定义选项和元数据

通过 options 字段传递自定义配置:

1
2
3
4
5
6
7
8
9
{
"name": "test-generator",
"options": {
"framework": "jest",
"coverageThreshold": 80,
"preferAsync": true,
"mockExternalApis": true
}
}

frontmatter 中的自定义属性会自动合并到 options 对象中:

1
2
3
4
5
---
name: custom-agent
framework: react
testRunner: vitest
---

来源:packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts

Agent 生命周期和状态管理

初始化和状态

Agent 通过 Agent.state() 函数延迟加载,该函数将默认配置与用户覆盖合并:

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
const state = Instance.state(async () => {
const cfg = await Config.get()

// 定义默认权限
const defaults = PermissionNext.fromConfig({
"*": "allow",
doom_loop: "ask",
external_directory: {
"*": "ask",
[Truncate.DIR]: "allow",
},
question: "deny",
read: {
"*": "allow",
"*.env": "deny",
"*.env.*": "deny",
"*.env.example": "allow",
},
})

const user = PermissionNext.fromConfig(cfg.permission ?? {})

// 定义内置 agent
const result: Record<string, Info> = {
// ... 内置 agent 定义
}

// 合并用户定义的 agent
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
// 合并配置
// ...
}

return result
})

来源:packages/opencode/src/agent/agent.ts

Agent 发现和列表

通过 list 函数检索可用的 agent:

1
2
const agents = await Agent.list()
// 返回已排序的 agent,default_agent 在前

来源:packages/opencode/src/agent/agent.ts

最佳实践和常见模式

1. 专用 Subagent 模式

为特定任务创建专注的 agent:

1
2
3
4
5
6
7
8
9
10
11
---
name: database-migrator
mode: subagent
description: 生成和验证数据库迁移脚本
permission:
"*": deny
read: allow
glob: allow
grep: allow
write: allow
---

2. 分层 Agent 组织

在子目录中组织 agent 以适应复杂项目:

1
2
3
4
5
6
7
8
9
10
.opencode/
├── agent/
│ ├── frontend/
│ │ ├── react-component.md
│ │ └── css-reviewer.md
│ ├── backend/
│ │ ├── api-endpoint.md
│ │ └── database-schema.md
│ └── devops/
│ └── ci-pipeline.md

嵌套路径将成为 agent 名称的一部分:frontend/react-component

来源:packages/opencode/src/config/config.ts

3. 渐进式权限升级

从限制性权限开始,并根据 agent 需求进行扩展:

1
2
3
4
5
6
7
{
"permission": {
"*": "deny",
"read": "allow",
"grep": "allow"
}
}

然后随着测试发现需求,添加更多工具。

4. 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
26
27
28
29
30
31
32
33
---
name: documentation-writer
---

你是一位技术文档专家。

## 核心原则
- 为你的受众写作:开发者、用户或两者兼有
- 简洁但全面
- 为描述的每个 API 包含代码示例
- 使用清晰、一致的术语

## 你擅长的文档类型
- API 参考文档
- 入门指南
- 教程内容
- 架构概述

## 输出格式
生成文档时,将其结构化为:
1. 简要描述(是什么和为什么)
2. 先决条件
3. 快速示例
4. 详细解释
5. 常见陷阱
6. 相关资源

## 质量检查
在定稿文档之前,验证:
- 所有代码示例都可运行
- 没有未定义的术语或缩略词
- 层次清晰,标题级别一致
- 语法和拼写正确

5. 内部使用的隐藏 Agent

从 @ 自动补全菜单中隐藏专用 agent:

1
2
3
4
5
{
"name": "internal-formatter",
"mode": "subagent",
"hidden": true
}

隐藏的 agent 仍然可供直接调用或被其他 agent 委托,但不会出现在面向用户的 agent 列表中。

来源:packages/opencode/src/config/config.ts

对于主要由其他 agent 以编程方式调用而非由用户直接调用的 agent,请使用 hidden: true 标志。这可以减少认知负荷并防止对可用选项的混淆。