Agent Loop
opencode agent loop 的主心智模型:
B[SessionPrompt.prompt提示入口]
B –> C[createUserMessage
写入用户消息和 Parts]
C –> D[更新会话时间
写入临时工具权限]
D –> E[SessionPrompt.loop
进入会话循环]
E –> F[runLoop
while true 主循环]
F –> G[读取有效历史
filterCompactedEffect]
G –> H[定位最近消息
lastUser / lastAssistant / lastFinished]
H –> I{上一轮是否已完成?} I – 是 –> Z[返回最后一条助手消息] I – 否 –> J{是否有待处理任务?}
J – 子任务 subtask –> K[handleSubtask
运行子 Agent 会话]
K –> F
J – 压缩任务 compaction –> L[SessionCompaction.process
生成上下文摘要]
L –> F
J – 没有 –> M{是否上下文溢出?}
M – 是 –> N[SessionCompaction.create
写入压缩任务 Part]
N –> F
M – 否 –> O[读取当前 Agent 和 Model]
O –> P[insertReminders
插入规划/执行提醒]
P –> Q[创建助手消息
Assistant Message]
Q –> R[SessionProcessor.create
创建流事件处理器]
R –> S[resolveTools
解析可用工具]
S –> S1[ToolRegistry
内置工具 / 插件工具 / MCP 工具]
S1 –> S2[权限过滤
Agent 权限 + Session 权限]
S2 –> T[构造模型上下文
system / 环境 / skills / instructions / 历史]
T –> U[MessageV2.toModelMessagesEffect
Session 历史转模型消息]
U –> V[SessionProcessor.process
开始处理一次模型调用]
V –> W[LLM.stream
调用模型流式输出]
W –> X[SessionProcessor 处理流事件
文本 / reasoning / 工具 / step / 错误]
X –> X1[写回 Session
updateMessage / updatePart / updatePartDelta]
X1 –> Y{本轮结果}
Y – continue
继续 –> F
Y – compact
需要压缩 –> N
Y – stop
停止 –> Z
–>
编排器->>会话: updateMessage(助手消息)
编排器->>处理器: create(助手消息)
编排器->>处理器: process(模型输入)
处理器->>模型: stream(input)
模型-->>处理器: 文本 / reasoning / 工具 / step 事件
处理器->>会话: updatePart / updatePartDelta
alt 模型调用工具
模型->>工具: execute(args)
工具-->>模型: 返回工具结果
模型-->>处理器: tool-result
处理器->>会话: 写入 completed 工具 Part
end
处理器-->>编排器: 返回 continue / stop / compact
end
end
编排器–>>用户: 返回最后一条助手消息
–>
工具调用时序图
>模型: 工具结果模型->>处理器: tool-result 处理器->>会话: 更新为 completed 工具 Part
处理器–>>处理器: 返回 continue
处理器->>会话: 下一轮重新读取历史
–>
opencode 的 agent loop 是一个 session 驱动的 ReAct 循环:
读 session 记忆
-> 选择 agent/model/tools
-> 调 LLM
-> 把文本、工具调用、工具结果、patch、token 写回 session
-> 根据结果继续、停止或压缩
工具模块
工具层可以分成四层:
Tool 定义层
-> ToolRegistry 注册/筛选层
-> SessionPrompt.resolveTools 适配层
-> LLM/Processor 调用与落盘层
工具层架构图

工具规范
工具统一用 Tool.Def 。
{
id: string
description: string
parameters: Schema
execute(args, ctx): Effect<{
title: string
metadata: Record<string, unknown>
output: string
attachments?: FilePart[]
}>
}
工具执行时会拿到 Tool.Context:
{
sessionID,
messageID,
agent,
abort,
callID,
messages,
metadata(...),
ask(...)
}
两个最重要的上下文函数:
ctx.ask(...):发起权限请求,例如 bash/edit/write/task 这类敏感工具。
ctx.metadata(...):更新工具运行中的标题和 metadata,让 UI 能看到工具正在做什么。
Tool.define() 还会统一做两件事:
- 参数 schema 校验
- 工具输出截断,避免超长 output 塞爆上下文
内置工具
ToolRegistry 初始化内置工具:
invalid
模型工具调用修复失败时的兜底工具。正常情况下模型不该主动用它。比如模型调用了不存在的工具,或参数坏到无法修复,系统会转成 invalid,输出错误说明。
question
向用户提问。用于需求不清、需要用户做选择、需要确认偏好时。它会把问题发到 UI/TUI,拿到用户回答后,把回答作为工具结果返回给模型。
bash
执行 shell 命令。
read
读取文件或目录。文件内容会带行号返回。支持 offset / limit,也能读取图片/PDF 并作为附件返回。
glob
按文件名模式找文件。
grep
按正则搜索文件内容。
edit
对已有文件做精确字符串替换。适合小改动、局部替换。
write
写完整文件。会覆盖目标文件。
task
启动一个子 agent。它是 subagent 编排的核心工具。
它会:
1.创建或复用子 session
2.把 prompt 发给指定 subagent
3.等待子 agent 完成
4.把结果作为 task 工具输出返回主 agent
webfetch
抓取指定 URL 内容,返回 markdown/text/html。适合用户给了具体网页、文档链接时读取内容。
todowrite
维护任务列表
websearch
实时网页搜索,基于 Exa。适合查最新信息、新闻、当前文档、网页资料。
codesearch
代码/文档搜索,基于 Exa Code API。适合查库、SDK、API、框架用法,例如 React、FastAPI、Next.js 等。
skill
加载 skill 指令。
apply_patch
用 patch 语言批量增删改文件。适合模型生成结构化补丁,尤其是 GPT 系模型会优先暴露这个工具,而不是普通 edit/write。
lsp
调用 Language Server Protocol,适合语义级代码探索,比如查定义、查引用、查调用关系。
plan_exit
规划模式结束工具。
还有两类动态工具,由用户自己配置:
配置目录里的 tool/*.js 或 tools/*.ts
插件暴露的 plugin.tool
这些会被转换成统一的 Tool.Def。
MCP 工具不直接进 ToolRegistry.builtin/custom,而是在 SessionPrompt.resolveTools() 里额外合并。
怎么决定“有哪些工具可用”
第一层:Registry 按模型/provider 过滤。
主要规则:
websearch / codesearch
-> 只有 provider 是 opencode 或开启 Exa 时可用
apply_patch
-> GPT 系且不是 oss/gpt-4 时可用
edit/write
-> apply_patch 不可用时才暴露
第二层:ToolRegistry 动态增强描述。
task 工具会注入当前 agent 可以调用的 subagent 列表。
skill 工具会注入当前 agent 可用的 skill 列表。
这会影响模型“知道有哪些子 agent/skill 可以用”。
第三层:LLM 层按权限和用户工具开关再过滤。
resolveTools(input)
它会去掉:
用户本轮显式禁用的工具
agent/session permission 禁用的工具
然后 streamText 里传:
activeTools: Object.keys(tools).filter((x) => x !== "invalid")
tools
toolChoice
怎么决定“调用哪个工具”
这个要分清楚:opencode 决定“给模型哪些工具”,但具体调用哪个工具,通常由模型根据工具描述、参数 schema、上下文自主决定。
流程是:
opencode 暴露工具集合
-> 模型看到工具名/描述/参数
-> 模型生成 tool-call
-> AI SDK 调用对应 execute(args)
例外/约束:
toolChoice: "required"时,模型必须调用工具,例如结构化输出StructuredOutput。- 工具被 permission deny 时不会暴露或执行会被拒绝。
- 参数不合法时,
Tool.define()会返回 schema 校验错误。 - 工具名大小写不匹配时,
LLM.experimental_repairToolCall会尝试转小写修复;修不好会调用invalid工具。
工具调用如何落盘
工具调用状态由 SessionProcessor 记录。
处理器->>会话: 写入 ToolPart pending
模型–>>处理器: tool-call(args) 处理器->>会话: 更新 ToolPart running + input
SDK->>工具: execute(args, Tool.Context) 工具->>工具: ctx.ask 权限检查 工具->>处理器: ctx.metadata 更新运行信息 工具–>>SDK: output / metadata / attachments
模型–>>处理器: tool-result 处理器->>会话: 更新 ToolPart completed
处理器–>>会话: 下一轮模型上下文包含工具结果 –>
ToolPart 状态机:
pending -> running -> completed
-> error
下一轮 MessageV2.toModelMessagesEffect() 会把 completed tool part 转成模型能理解的 tool result。于是模型就能基于工具结果继续推理。
权限
工具权限是三处共同作用:
Agent.permission
Session.permission
Tool.Context.ask()
C[Permission.merge]
B[Session 临时权限] –> C
C –> D[ctx.ask]
D –> E{allow / ask / deny}
E – allow –> F[继续执行工具]
E – ask –> G[向用户请求批准]
G –> F
E – deny –> H[工具失败/停止] –>
resolveTools() 给每个工具注入 ctx.ask。
记忆模块
Create[创建用户消息SessionPrompt.createUserMessage]
Create –> Store[(会话消息存储
Session Message Store)]
Model[模型流 / 工具执行] –> Processor[会话处理器
SessionProcessor]
Processor –> Parts[消息片段
Message Parts]
Parts –> Store
Parts –> Text[文本片段
用户文本 / 助手回复]
Parts –> Reasoning[推理片段
模型 reasoning 流]
Parts –> Tool[工具片段
工具输入 / 输出 / 错误]
Parts –> Step[步骤片段
step-start / step-finish]
Parts –> Patch[补丁片段
文件变更摘要]
Parts –> CompactMark[压缩片段
上下文压缩标记]
Store –> ReadHistory[读取有效历史
过滤已压缩内容]
ReadHistory –> Convert[转换为模型消息
toModelMessagesEffect]
Convert –> Model
Step –> Snapshot[代码快照
Snapshot]
Snapshot –> Diff[计算文件差异
SessionSummary.computeDiff]
Diff –> Summary[(会话摘要
文件数 / 新增 / 删除)]
Summary –> Store
Store –> Overflow{上下文是否过长?}
Overflow – 是 –> Compaction[上下文压缩
SessionCompaction]
Compaction –> CompactSummary[压缩后的任务摘要]
CompactSummary –> Store
Overflow – 否 –> ReadHistory
Tool –> Truncate[工具输出截断
避免结果过长]
Truncate –> Store
–>
核心设计
opencode 的记忆模块是围绕 Session 事件日志设计的。每次对话、工具调用、模型输出、文件 diff、token usage 都会变成 message 或 part 写回 session。
主要有三层:
Session
└── Message
└── Part
Session 是一次会话。
Message 是 user / assistant 消息。
Part 是消息里的细粒度内容,比如文本、工具调用、reasoning、step、patch、compaction。
记忆如何进入下一轮模型
session 历史
-> filterCompactedEffect()
-> toModelMessagesEffect()
-> LLM.stream()
也就是说,模型看到的是经过处理后的 session 历史。已压缩的旧内容会被摘要替代,最近几轮会尽量保留原始细节。
工具调用记忆
工具调用也被当作记忆保存:
所以模型能“记住”刚才读了什么文件、bash 输出是什么、apply_patch 改了哪些文件,本质上是因为这些结果都变成了 tool part,下一轮又被 toModelMessagesEffect() 转回模型消息。
压缩记忆
当 token 溢出时:
Select[选择要压缩的旧 turn]Select –> Summarize[compaction agent 生成摘要]
Summarize –> Keep[保留最近几轮原文]
Keep –> NewContext[摘要 + 最近原文]
NewContext –> NextLLM[下一轮模型上下文] –>
压缩摘要的结构包含:
Goal 目标
Constraints & Preferences 约束&偏好
Progress 进度
Key Decisions 关键决策
Next Steps 下一步行动
Critical Context 关键上下文
Relevant Files 相关文件
三层记忆模型—Session、Message、Part
opencode 记忆系统的主干:
Session
└── Message
└── Part
可以把它类比成:
一次会话
└── 一轮用户/助手消息
└── 这条消息里的结构化片段
Session 层
Session 表示一整个会话,是最高层的容器。
核心字段:
{
id,
slug,
projectID,
workspaceID?,
directory,
parentID?,
title,
version,
summary?,
permission?,
revert?,
time
}
它回答的是:“这次对话属于哪里、叫什么、状态如何?”
几个关键含义:
id:会话唯一 ID。
slug:短标识,常用于 plan 文件名。
projectID / directory:这个会话绑定的项目和工作目录。
parentID:父会话。subagent 会创建子 session,所以 agent 编排天然是 session tree。
title:会话标题,初始自动生成,后续可由 title agent 生成。
summary:整个 session 的 diff 摘要,比如改了几个文件、增删多少行。
permission:session 级工具权限覆盖,例如某次请求临时禁用 bash。
revert:撤销状态,记录可回滚到哪个 message/part/snapshot。
time:创建、更新、压缩、归档时间。
Session 层不保存具体文本,它保存“会话元信息”。
Message 层
Message 是一次会话里的消息头。
Message 有两种:
User | Assistant
User message 表示用户输入:
{
id,
sessionID,
role: "user",
time: { created },
agent,
model,
system?,
tools?,
format?,
summary?
}
它回答的是:“用户这一轮想让哪个 agent、哪个 model、带什么临时配置来处理?”
agent:比如 build、plan。
model:本轮使用的 provider/model。
tools:本轮工具开关。
format:是否要求 JSON schema 输出。
system:用户附加 system prompt。
Assistant message 表示模型/agent 的一次响应:
{
id,
sessionID,
role: "assistant",
parentID,
agent,
modelID,
providerID,
path,
cost,
tokens,
finish?,
error?,
summary?,
structured?
}
它回答的是:“agent 这一轮执行结果是什么状态?”
parentID:指向对应 user message。
agent:实际执行的 agent。
path:执行时 cwd/root。
cost / tokens:模型消耗。
finish:结束原因,比如 stop 或 tool-calls。
error:失败信息。
summary:是否是上下文压缩摘要消息。
Message 层也不一定直接保存正文,正文主要在 Part 层。
Part 层
Part 是消息的实际内容片段。
所有 Part 都有:
{
id,
sessionID,
messageID,
type
}
它回答的是:“这条消息里具体发生了什么?”
常见 Part 类型:
text:文本内容。
{
type: "text",
text,
synthetic?,
ignored?,
time?,
metadata?
}
用户输入、助手回复、系统自动插入的 reminder 都可以是 text part。
file:文件或附件。
{
type: "file",
mime,
filename?,
url,
source?
}
用户附加文件、图片、MCP resource、LSP symbol 来源都可以放这里。
reasoning:模型 reasoning 流。
{
type: "reasoning",
text,
metadata?,
time
}
tool:工具调用记录。
{
type: "tool",
callID,
tool,
state,
metadata?
}
工具 state 有:
pending -> running -> completed | error
这是 agent 能“记住工具结果”的关键。
step-start / step-finish:模型 step 边界。
{
type: "step-start",
snapshot?
}
{
type: "step-finish",
reason,
snapshot?,
cost,
tokens
}
用于记录一次模型调用的开始、结束、费用、token 和文件快照。
patch:文件改动摘要。
{
type: "patch",
hash,
files
}
compaction:上下文压缩任务。
{
type: "compaction",
auto,
overflow?,
tail_start_id?
}
subtask:子 agent 任务。
{
type: "subtask",
prompt,
description,
agent,
model?,
command?
}
数据库上对应 PartTable,见 session.sql.ts。
三层如何关联
关系字段是:
Session.id
= Message.sessionID
Message.id
= Part.messageID
Session.id
= Part.sessionID
为什么 Part 也存 sessionID?因为这样可以直接按 session 查 part,方便索引、删除、同步和事件发布。
实际结构像这样:
const session = {
id: "ses_123",
title: "Implement auth",
}
const userMessage = {
id: "msg_001",
sessionID: "ses_123",
role: "user",
agent: "build",
model: { providerID: "openai", modelID: "gpt-5" },
}
const userTextPart = {
id: "prt_001",
sessionID: "ses_123",
messageID: "msg_001",
type: "text",
text: "帮我实现登录",
}
const assistantMessage = {
id: "msg_002",
sessionID: "ses_123",
parentID: "msg_001",
role: "assistant",
agent: "build",
modelID: "gpt-5",
providerID: "openai",
}
const toolPart = {
id: "prt_002",
sessionID: "ses_123",
messageID: "msg_002",
type: "tool",
tool: "read",
callID: "call_abc",
state: {
status: "completed",
input: { filePath: "src/auth.ts" },
output: "...file content...",
title: "",
metadata: {},
time: { start: 1, end: 2 },
},
}
为什么要拆成三层
因为 agent 执行不是简单聊天,它有很多结构化过程:
用户文本
模型文本流
模型 reasoning 流
工具调用输入
工具执行输出
工具错误
文件附件
文件 diff
token/cost
上下文压缩摘要
子 agent 任务
如果都塞进一条字符串,会很难做:
- 工具结果回放
- 流式 UI
- 权限审批展示
- 历史压缩
- 文件 diff 统计
- retry/error 恢复
- subagent session tree
- JSON schema 输出
- 多 provider tool message 转换
所以 opencode 选择:
Session 管会话
Message 管轮次和角色
Part 管具体内容和执行痕迹
模型上下文如何生成
每次调用模型时,并不是直接拿数据库文本拼 prompt,而是:
Session.messages()
-> MessageV2.filterCompactedEffect()
-> MessageV2.toModelMessagesEffect()
-> LLM.stream()
toModelMessagesEffect() 会把 Part 转成模型能理解的消息:
text part -> user/assistant text
file part -> file attachment
tool part -> tool call/result
reasoning part -> reasoning
compaction part -> "What did we do so far?"
subtask part -> "The following tool was executed by the user"