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的关系

执行模式:同步阻塞(而非并行)

主agent调用子agent时会阻塞,等待子agent完全执行完成后才继续。

具体来说:

  1. 独立的Session,但顺序执行

    • 子agent在新的session中运行(TaskTool.execute L58-88
    • 新session的parentID指向主agent的session
    • 但调用是await的,主agent的执行循环会暂停等待
  2. 阻塞调用链

    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
    }
  3. TaskTool的实现packages/opencode/src/tool/task.ts L139

    1
    2
    3
    4
    5
    const 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完成后继续 完成自主任务后返回结果

为什么是阻塞而非并行?

  1. 因果依赖:主agent需要子agent的结果才能继续
  2. 资源管理:避免同时运行多个LLM调用导致不可控的成本
  3. 简化状态:更容易追踪和理解执行流程
  4. 可调试性:顺序执行更容易排查问题

执行时序图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
时间 →

主Session A Loop:
├── Step 1: LLM生成 -> 调用TaskTool(explore)
│ └── 等待...

│ 子Session B Loop (作为TaskTool的一部分):
│ ├── SubStep 1: LLM生成 -> 调用grep/glob工具
│ ├── SubStep 2: 处理工具结果
│ ├── SubStep 3: LLM继续 -> 完成
│ └── 返回结果给Session A

├── Step 2: 收到子agent结果,LLM继续
└── Step 3: 完成任务

特殊情况:多个子Agent

如果主agent需要调用多个子agent:

1
2
3
4
// 示例:主agent可能的行为
1. 调用 explore agent(等待结果)
2. 根据结果调用 general agent(等待结果)
3. 综合两个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
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),并具备复杂的权限管理和实时状态跟踪功能。

1. 初始化阶段 (Initialization)

当用户发起对话时:

  1. 创建或获取Session

    • 通过Session.create创建新会话,或通过Session.get获取已有会话
    • Session包含projectID、directory、parentID(如果是子会话)、权限配置等元数据

    具体源码见:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      export 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
    })
  2. 选择Agent

    • 通过Agent.get获取指定的agent配置(默认是”build” agent)
    • Agent配置包含:名称、权限规则集、prompt、model配置、步骤限制等
  3. 创建用户消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
sessionID: input.sessionID,
time: {
created: Date.now(),
},
tools: input.tools,
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
system: input.system,
variant: input.variant,
}
  • 处理文件附件、@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)

c. 上下文溢出检查 (L495-507)

  • 检查最后一条完成的消息是否导致token溢出
  • 如果溢出,自动创建compaction任务并继续

正常处理 (L509-620)

a. 创建Processor

b. 解析工具

  • resolveTools根据agent权限和session权限合并工具集
  • 从ToolRegistry获取可用工具
  • 包装MCP工具

c. LLM流式调用

  • 调用processor.process开始处理
  • 传入system prompt、历史消息、工具定义

3. LLM流式处理阶段 (Streaming Processing)

SessionProcessor.process处理LLM流式响应:

事件类型处理

  1. start (L58)

    • 设置session状态为busy
  2. reasoning系列事件 (L62-101)

    • 推理内容的开始、增量、结束
    • 实时更新ReasoningPart
  3. tool-input系列事件 (L103-124)

    • 工具输入准备(当前未实际使用)
  4. tool-call (L126-171)

    • 执行工具调用前
    • Doom Loop检测 (L146-168):如果最近3次调用相同工具用相同参数,触发权限询问
    • 创建ToolPart,状态设为running
    • 执行工具
  5. tool-result (L172-194)

    • 工具执行成功
    • 更新ToolPart状态为completed,保存输出
  6. tool-error (L196-221)

    • 工具执行失败
    • 如果是权限拒绝错误,设置blocked标志(可能导致循环终止)
    • 更新ToolPart状态为error
  7. start-step / finish-step (L225-277)

    • 步骤边界标记
    • 追踪文件快照变化,生成patch
    • 更新token和成本统计
    • 触发SessionSummary.summarize
    • 检查是否需要compaction
  8. text系列事件 (L279-326)

    • 文本内容的开始、增量、结束
    • 实时更新TextPart
  9. 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. 清理和完成

当循环结束后:

  1. SessionCompaction.prune - 清理过期的压缩记录
  2. 返回最后一条assistant消息给调用者
  3. 触发相应的事件(如完成、错误)

关键特性

  • 单线程处理:每个session同时只能有一个active的循环(通过BusyError保护)
  • 渐进式执行:工具调用流式处理,实时更新UI
  • 智能压缩:自动检测token溢出并触发压缩
  • 循环保护:检测并阻止doom loop(重复调用相同工具)
  • 权限控制:每个工具调用前都会检查权限(PermissionNext.ask
  • 成本追踪:精确统计每次调用的token和成本

这就是agent从出生到完成任务的完整旅程。整个过程是异步的、流式的、可中断的,充满了状态检查和错误恢复机制。

其他关键点

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

核心原理

系统监控连续的工具调用,如果检测到相同工具以相同参数被重复调用,就会触发用户确认。阈值设为 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" 状态终止循环

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

系统持久化机制详解

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

存储类型

基于文件系统的 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 格式保持了简单性和可维护性。

生命周期事件

系统通过事件总线发布全面的事件,用于实时监控和 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

创建自定义 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 标志。这可以减少认知负荷并防止对可用选项的混淆。