OpenCode提示词处理流程
OpenCode提示词处理流程
当前版本1.2.6。
0. 资料
- OpenCode Compaction 上下文压缩 和 清理旧工具输出
- OpenCode Subtask 子任务处理
- OpenCode 调用大模型
- OpenCode Agent
- OpenCode 构建提示词
- OpenCode 工具系统
- OpenCode 服务端流式响应 和 用户确认机制
- OpenCode LSP
1. 整体架构概览
1.1 代码位置
客户端:web端,代码在packages/app目录下。
服务端:代码在packages/opencode目录下。
1.2 客户端提示词组件代码位置和提交链路
app.tsx
↓
pages/session.tsx Session组件
↓
pages/session/session-prompt-dock.tsx SessionPromptDock组件
↓
components/prompt-input.tsx PromptInput组件
↓
components/prompt-input/submit.ts handleSubmit函数
- send
- client.session.promptAsync url: /session/{sessionID}/prompt_async
/session/{sessionID}/prompt_async会将用户输入的提示词发送给服务端然后立即返回。
服务端会处理的流式过程、结果会通过context/global-sync.tsx中的下列代码监听:
const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
if ((event.type as string) !== "server.heartbeat") {
// Do not print heartbeat
log.info(`[event][listen] directory=${directory}, event=${JSON.stringify(event)}`)
}
if (directory === "global") {
applyGlobalEvent({
event,
project: globalStore.project,
refresh: queue.refresh,
setGlobalProject(next) {
if (typeof next === "function") {
setGlobalStore("project", produce(next))
return
}
setGlobalStore("project", next)
},
})
if (event.type === "server.connected" || event.type === "global.disposed") {
for (const directory of Object.keys(children.children)) {
queue.push(directory)
}
}
return
}
const existing = children.children[directory]
if (!existing) return
children.mark(directory)
const [store, setStore] = existing
applyDirectoryEvent({
event,
directory,
store,
setStore,
push: queue.push,
setSessionTodo,
vcsCache: children.vcsCache.get(directory),
loadLsp: () => {
sdkFor(directory)
.lsp.status()
.then((x) => setStore("lsp", x.data ?? []))
},
})
})
1.3 调用链路概览
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端 │
├─────────────────────────────────────────────────────────────────────┤
components/prompt-input/submit.ts handleSubmit函数
- send
- client.session.promptAsync url: /session/{sessionID}/prompt_async
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 服务端 │
├─────────────────────────────────────────────────────────────────────┤
入口点:server/server.ts SessionRoutes -> server/routes/session.ts /:sessionID/prompt_async
↓
SessionPrompt.prompt # 提示词处理 session/prompt.ts
↓
SessionPrompt.loop() # 主循环,处理提示词到模型的交互 session/prompt.ts
↓
├─ resume_existing? ──是──► resume(sessionID) ──┐
│ │
└─ resume_existing? ──否──► start(sessionID) ──┴──► abort
│
┌─────────────────────────────────────┘
▼
┌───────────────────┐
│ while (true) │ ◄─────────────────────────────┐
└───────────────────┘ │
│ │
▼ │
读取消息历史 + 倒序遍历 │
│ │
▼ │
┌───────────────────┐ │
│ finish 检查 │──是──► break ──► 结束阶段 │
└───────────────────┘ │
│ 否 │
▼ │
┌───────────────────┐ │
│ subtask? │──是──► 执行 ──► continue ──────┤
└───────────────────┘ │
│ 否 │
▼ │
┌───────────────────┐ │
│ compaction? │──是──► 执行 ──► continue ──────┤
└───────────────────┘ │
│ 否 │
▼ │
processor.process() │
流式响应 → 数据库 → Bus 事件 → 前端展示 │
│ │
├─ stop ──► break ──► 结束阶段 │
├─ compact ──► 压缩 ──► continue ────────────┤
└─ 其他 ──► continue ──────────────────────┘
1.4 核心文件位置
| 模块 | 文件路径 |
|---|---|
| 提示词处理入口 | src/session/prompt.ts |
| 消息数据结构 | src/session/message-v2.ts |
| 处理器 | src/session/processor.ts |
| 大模型调用 | src/session/llm.ts |
| 上下文压缩 | src/session/compaction.ts |
| 系统提示词 | src/session/system.ts |
| 指令提示词 | src/session/instruction.ts |
| Session 管理 | src/session/index.ts |
| Agent 配置 | src/agent/agent.ts |
| 工具注册 | src/tool/registry.ts |
| 权限管理 | src/permission/next.ts |
2. 数据模型:Session、Message 和 Part 的关系
理解 OpenCode 的数据处理流程,首先需要理解其核心数据模型。OpenCode 使用三层结构来组织对话数据:Session → Message → Part。
2.1 数据库表结构
位置: src/session/session.sql.ts
-- Session 表:一次完整的对话会话
SessionTable:
id (PK)
project_id (FK)
title
permission (JSON)
time_created, time_updated, ...
-- Message 表:一次对话轮次(用户或助手)
MessageTable:
id (PK)
session_id (FK → SessionTable.id, CASCADE DELETE)
time_created
data (JSON: role, agent, model, tokens, finish, ...)
-- Part 表:消息内的具体内容单元
PartTable:
id (PK)
message_id (FK → MessageTable.id, CASCADE DELETE)
session_id (冗余字段,用于快速查询)
time_created
data (JSON: type, text, tool, state, ...)
2.2 层级关系图
Session (会话)
一个完整的对话会话
│
│ 1:N
▼
┌──────────────────────────────────────┐
│ Message │
│ (一次对话轮次) │
│ │
│ 角色: "user" | "assistant" │
│ 元数据: agent, model, tokens, finish │
│ 关联: parentID (助手消息关联用户消息) │
└──────────────────────────────────────┘
│
│ 1:N
▼
┌──────────────────────────────────────┐
│ Part │
│ (消息内的内容单元) │
│ │
│ 类型: text | tool | reasoning | ... │
│ 内容: 由 type 决定具体字段 │
│ 关联: messageID (所属消息) │
└──────────────────────────────────────┘
2.3 Message(消息)详解
Message 代表对话中的一次交互轮次,分为两种角色:
User Message(用户消息)
{
id: "message_abc123",
sessionID: "session_xyz",
role: "user",
agent: "build", // 使用的 agent
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
time: { created: 1700000000000 }
}
Assistant Message(助手消息)
{
id: "message_def456",
sessionID: "session_xyz",
role: "assistant",
parentID: "message_abc123", // 关联到触发它的用户消息
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
tokens: { input: 100, output: 200, ... },
finish: "stop", // 完成原因
time: { created: 1700000001000, completed: 1700000005000 }
}
2.4 Part(消息部分)详解
Part 代表一条消息内的具体内容单元,一条消息可以包含多个 part:
type Part =
| TextPart // 文本内容
| ToolPart // 工具调用
| ReasoningPart // 推理过程(如 Claude 的思考)
| FilePart // 文件附件
| SubtaskPart // 子任务
| CompactionPart // 压缩标记
| StepStartPart // 步骤开始标记
| StepFinishPart // 步骤结束标记(含 token 统计)
| ...
2.5 实际示例
假设用户发送 "读取 package.json 文件并解释它",完整的消息和 part 结构如下:
┌─────────────────────────────────────────────────────────────────────┐
│ Session: session_xyz │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Message 1 (User) - id: msg_001 │
│ ├── Part 1: TextPart │
│ │ └── text: "读取 package.json 文件并解释它" │
│ └── Part 2: FilePart (如果用户拖拽了文件) │
│ └── url: "file:///path/to/package.json" │
│ │
│ Message 2 (Assistant) - id: msg_002, parentID: msg_001 │
│ ├── Part 1: StepStartPart │
│ │ └── 标记步骤开始 │
│ ├── Part 2: TextPart │
│ │ └── text: "我来读取这个文件..." │
│ ├── Part 3: ToolPart │
│ │ ├── tool: "read" │
│ │ ├── callID: "call_abc123" │
│ │ └── state: { status: "completed", output: "{ ... }" } │
│ ├── Part 4: TextPart │
│ │ └── text: "这是 package.json 的解释..." │
│ └── Part 5: StepFinishPart │
│ ├── reason: "stop" │
│ └── tokens: { input: 500, output: 300 } │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.6 设计原因
Message 的作用
- 对话层级: 区分用户和助手的交互轮次
- 元数据存储: 模型信息、token 使用量、完成状态等
- 关联追踪: 通过
parentID知道哪个助手消息响应用户消息
Part 的作用
- 细粒度更新: 流式返回时可以逐个 part 更新,前端实时显示
- 类型区分: 不同类型的内容有不同的处理方式
- 工具追踪: 每个 tool 调用是独立的 part,可以追踪状态
- 压缩支持: 可以单独标记旧 part 为已压缩,减少上下文
2.7 代码中的使用
// 创建用户消息
const userMessage = await Session.updateMessage({
id: Identifier.ascending("message"),
sessionID,
role: "user",
agent: "build",
model: { providerID, modelID },
})
// 添加文本 part
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: userMessage.id, // 关联到消息
sessionID,
type: "text",
text: "用户输入的内容",
})
// 创建助手消息
const assistantMessage = await Session.updateMessage({
id: Identifier.ascending("message"),
parentID: userMessage.id, // 关联到用户消息
sessionID,
role: "assistant",
// ...
})
// 流式更新文本 part
await Session.updatePartDelta({
sessionID,
messageID: assistantMessage.id,
partID: textPartID,
field: "text",
delta: "新增的文本片段",
})
updateMessage 和 updatePart 的 Upsert 机制
这两个函数虽然命名为 update,但实际是 "创建或更新"(Upsert) 操作:
位置: src/session/index.ts:581-601 和 src/session/index.ts:646-666
// 核心实现:INSERT ... ON CONFLICT DO UPDATE
db.insert(MessageTable)
.values({...})
.onConflictDoUpdate({ target: MessageTable.id, set: { data } })
.run()
| 情况 | 行为 |
|---|---|
| id 不存在 | 插入新记录(创建) |
| id 已存在 | 更新现有记录 |
返回值:两个函数都返回传入的数据本身(return msg / return part)。
设计优点:
- 统一接口:不需要区分 create 和 update,简化调用方逻辑
- 幂等性:多次调用同一个 id 不会产生重复数据
- 流式更新友好:先创建消息框架,后续再更新内容(如添加 finish 状态)
使用示例:
// 1. 创建新消息
const msg = await Session.updateMessage({
id: Identifier.ascending("message"), // 新 id
sessionID,
role: "user",
})
// 2. 更新已有消息(使用同一个 id)
await Session.updateMessage({
...msg, // 复用原消息的 id 和其他字段
finish: "stop",
time: { ...msg.time, completed: Date.now() }
})
2.8 查询完整消息
// 获取消息及其所有 parts
export const parts = fn(Identifier.schema("message"), async (message_id) => {
const rows = Database.use((db) =>
db.select().from(PartTable)
.where(eq(PartTable.message_id, message_id))
.orderBy(PartTable.id)
.all(),
)
return rows.map((row) => ({
...row.data,
id: row.id,
sessionID: row.session_id,
messageID: row.message_id,
}))
})
// 完整的消息结构
type WithParts = {
info: MessageV2.Info // 消息元数据
parts: MessageV2.Part[] // 该消息的所有 parts
}
2.9 总结
| 概念 | 粒度 | 作用 | 关键字段 |
|---|---|---|---|
| Session | 会话级别 | 一次完整的对话会话 | id, title, permission |
| Message | 轮次级别 | 用户或助手的一次交互 | id, role, parentID, tokens |
| Part | 内容单元 | 消息内的具体内容 | id, messageID, type |
关系: Session → 1:N → Message → 1:N → Part
3. [服务端] SessionPrompt.loop提示词循环处理函数执行流程图
代码在session/prompt.ts的loop函数中。
3.1 loop函数执行流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【初始化阶段】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ loop(sessionID, resume_existing) │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ resume_existing ? │ │
│ └─────────────────────┘ │
│ │ │ │
│ 是 │ │ 否 │
│ ▼ ▼ │
│ resume() start() │
│ │ │ │
│ └─────┬─────┘ │
│ ▼ │
│ abort controller │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【主循环 while(true)】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ① 读取消息历史: MessageV2.filterCompacted(stream) │ │
│ │ ② 倒序遍历查找: lastUser, lastAssistant, lastFinished, tasks │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ lastAssistant.finish 存在 │ │
│ │ 且 非 tool-calls/unknown ? │ │
│ └─────────────────────────────────────────────┘ │
│ │ │ │
│ 是 │ │ 否 │
│ ▼ ▼ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ break 退出 │ │ task.type === │ │
│ │ (→结束阶段) │ │ 'subtask' ? │ │
│ └──────────────┘ └─────────────────────┘ │
│ │ │ │
│ 是 │ │ 否 │
│ ▼ ▼ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ Subtask. │ │ task.type === │ │
│ │ process() │ │ 'compaction' ? │ │
│ └──────────────┘ └─────────────────────┘ │
│ │ │ │ │
│ │ 是 │ │ 否 │
│ │ ▼ ▼ │
│ │ ┌──────────────┐ ┌────────────┐ │
│ │ │ Compaction. │ │ 正常处理 │ │
│ │ │ process() │ │ 流程 │ │
│ │ └──────────────┘ └────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌──────────────────┐ │
│ │ │ │ 获取 Agent │ │
│ │ │ │ 创建 Processor │ │
│ │ │ │ processor.process│ │
│ │ │ └──────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌──────────────────┐ │
│ │ │ │ 处理结果 │ │
│ │ │ └──────────────────┘ │
│ │ │ │ │ │ │
│ │ │ stop │ │ │其他 │
│ │ │ │ compact │ │
│ │ │ ▼ ▼ ▼ │
│ │ │ (退出) (压缩)(继续) │
│ │ │ │ │
│ │ │ ▼ │
│ ▼ ▼ continue │
│ ◄─────────────────────────────┘ │
│ │ │
│ continue │
│ │ │
│ └───────────────────────────► (返回循环开始) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【结束阶段】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 构建最终消息 finalMessage │
│ │ │
│ ▼ │
│ return finalMessage │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.2 SessionProcessor.process执行流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【初始化】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ process(streamInput) │
│ │ │
│ ▼ │
│ needsCompaction = false │
│ shouldBreak = 配置项 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ while (true) │ ◄─────────────────────────┐ │
│ └───────────────────────────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ LLM.stream(streamInput) ─────────────────► 大模型 API │ │
│ │ │ │
│ ▼ │ │
│ ┌───────────────────────────────────────┐ │ │
│ │ for await (value of fullStream) │ ◄─────────┐ │ │
│ └───────────────────────────────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌───────────────────────────────────────────────────┤ │ │
│ │ switch (value.type) │ │ │
│ └───────────────────────────────────────────────────┤ │ │
│ │ │ │ │
│ ├─ start → 设置状态 busy │ │ │
│ ├─ reasoning-* → 处理推理内容 │ │ │
│ ├─ text-* → 处理文本内容 │ │ │
│ ├─ tool-input-* → 处理工具输入 │ │ │
│ ├─ tool-call → 执行工具(检测 doom loop) │ │ │
│ ├─ tool-result → 更新工具结果 │ │ │
│ ├─ tool-error → 更新错误状态(可能 blocked) │ │ │
│ ├─ start-step → 跟踪快照 │ │ │
│ ├─ finish-step → 更新 tokens/cost │ │ │
│ │ 检查 isOverflow │ │ │
│ ├─ finish → 流结束 │ │ │
│ └─ error → 抛出错误 ────────────────────┼──► 异常处理 │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌───────────────────────┐ │ │ │
│ │ needsCompaction ? │──是──► break ──────────────┼──► 清理阶段 │ │
│ └───────────────────────┘ │ │ │
│ │ 否 │ │ │
│ └───────────────────────────────────────────────┘ │ │
│ │ │
│ 异常处理: │ │
│ ┌───────────────────────┐ │ │
│ │ 可重试错误 ? │──是──► 等待延迟 ───────────────────────────┘ │
│ └───────────────────────┘ │
│ │ 否 │
│ ▼ │
│ 设置 error → 发布事件 → 清理阶段 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【清理阶段】 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 处理未完成的 tool parts (status 设为 error) │
│ 2. 更新 message.time.completed │
│ 3. Session.updateMessage 保存消息 │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 返回值判断 │ │
│ ├───────────────────────────────────────────────────────────────┤ │
│ │ needsCompaction ? ──是──► return "compact" │ │
│ │ blocked ? ──是──► return "stop" │ │
│ │ error ? ──是──► return "stop" │ │
│ │ 正常 ──► return "continue" │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4. [服务端] 读取消息历史MessageV2.filterCompacted解析
位置: src/session/message-v2.ts
export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
const result = [] as MessageV2.WithParts[]
const completed = new Set<string>()
for await (const msg of stream) {
result.push(msg)
// 如果是用户消息,且已完成压缩,且包含 compaction part,则停止,因为后续的消息都是压缩后的结果,不需要再处理了
if (
msg.info.role === "user" &&
completed.has(msg.info.id) &&
msg.parts.some((part) => part.type === "compaction")
)
break
// 如果是助手消息且已总结完成,标记其父消息为已完成,父消息即用户消息
if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish)
completed.add(msg.info.parentID)
}
// 反转顺序,数据库是倒序查询的,但模型需要按时间正序处理(最早的在前)
result.reverse()
return result
}
4.1 为什么要读取历史数据?
- 构建上下文: 大模型需要历史对话来理解当前任务
- 保持连续性: 维持对话的上下文连贯性
- 支持压缩点: 通过
compaction标记确定有效历史范围
4.2 为什么要 reverse()?
MessageV2.stream() 从数据库按时间倒序读取(最新的在前),但模型需要按时间正序处理(最早的在前),所以需要 reverse()。
5. [服务端] MessageV2.filterCompacted的结果使用解析
返回结果的使用位置:src/session/prompt.ts的loop函数中。
5.1 为什么要倒着遍历 (for let i = msgs.length - 1)?
// 查找最后的用户消息和助手消息
let lastUser: MessageV2.User | undefined
let lastAssistant: MessageV2.Assistant | undefined
let lastFinished: MessageV2.Assistant | undefined
let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
// 倒序遍历,可以快速找到最近的消息、收集最近的待处理任务(subtask/compaction)
for (let i = msgs.length - 1; i >= 0; i--) {
const msg = msgs[i]
if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
lastFinished = msg.info as MessageV2.Assistant
if (lastUser && lastFinished) break
const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
if (task && !lastFinished) {
tasks.push(...task)
}
}
变量含义和后续用途:
| 变量 | 含义 | 后续用途 |
|---|---|---|
lastUser |
最后一条用户消息 | 1. 获取 agent、model 配置2. 作为 processor.process() 的 user 参数3. 创建 compaction 时需要这些信息 |
lastAssistant |
最后一条助手消息 | 1. 判断模型是否已完成响应(通过 finish 字段)2. 退出循环的判断条件 |
lastFinished |
最后一条已完成的助手消息(有 finish 字段) |
1. 获取 tokens 统计信息2. 判断是否触发压缩( isOverflow 需要 tokens)3. 压缩点有效性判断( summary && finish) |
tasks |
待处理的 compaction/subtask parts | 1. 后续取出最新的 task 进行处理 2. 通过 task.type 判断处理逻辑 |
5.1.1 为什么要倒着遍历?
原因:
- 倒序遍历可以快速找到最近的消息
- 找到
lastUser和lastFinished后立即break,提高效率 - 收集最近的待处理任务(subtask/compaction)
5.1.2 为什么需要区分 lastAssistant 和 lastFinished?
// lastAssistant:只要最后一条助手消息
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
// lastFinished:需要已完成(有 finish 字段)
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
lastAssistant可能没有 finish(正在生成中、被中断等)lastFinished一定有finish,所以有tokens统计,可以用于判断是否需要压缩
5.1.3 有 lastAssistant 但没有 lastFinished 的情况
什么情况下会发生?
| 情况 | 说明 |
|---|---|
| 正在生成中 | 助手消息刚开始创建,还没有完成,finish 字段尚未设置 |
| 被中断 | 用户取消了请求,助手消息没有正常完成 |
| 出错 | API 调用失败,没有生成完整的响应 |
代码判断逻辑:
// lastAssistant:只要有助手消息就设置
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
// lastFinished:需要有 finish 字段才设置
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
lastFinished = msg.info
这种情况下的处理:
// 1. 退出循环检查 - lastAssistant.finish 不存在,不会退出
if (lastAssistant?.finish && ...) {
break // 不进入,继续循环
}
// 2. 自动压缩检查 - lastFinished 不存在,不会触发
if (lastFinished && lastFinished.summary !== true && isOverflow(...)) {
await SessionCompaction.create(...) // 不进入
}
// 3. 直接进入 normal processing,继续生成响应
总结:有 lastAssistant 但没有 lastFinished 时:
- 不会退出循环
- 不会触发自动压缩
- 直接进入 normal processing,让模型继续生成/补全响应
5.2 tasks 的设计思路
tasks是part表中的数据,当part.type === "compaction" || part.type === "subtask"的时候会做特殊处理。
5.2.1 为什么只存 part,不存 message?
const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
if (task && !lastFinished) {
tasks.push(...task) // 存的是 part,不是 msg
}
原因:
-
part 包含处理所需的所有信息:
// part 结构 { type: "compaction" | "subtask", auto: boolean, // 是否自动触发 messageID: string, // 可以找到对应的 message sessionID: string, // 可以找到对应的 session // ... 其他字段 } -
process 函数通过其他参数获取 message:
SessionCompaction.process({ messages: msgs, // ← 完整消息历史已经传入 parentID: lastUser.id, // ← 触发压缩的用户消息 ID auto: task.auto, // ← 只需要 part 中的这个字段 }) -
避免数据冗余:
msgs数组已经包含了所有 message 数据,不需要再存储一份
5.2.2 tasks 后续如何使用?
const task = tasks.pop() // 取最新一个待处理任务
if (task?.type === "compaction") {
// 处理压缩任务
await SessionCompaction.process({ auto: task.auto, ... })
}
if (task?.type === "subtask") {
// 处理子任务
// ...
}
设计思路:tasks 是一个待处理任务队列,按时间倒序收集,然后用 pop() 取出最新的一个进行处理。
5.2.3 没有待处理任务时的流程
如果没有 compaction 或 subtask 类型的 part:
tasks 数组为空,tasks.pop() 返回 undefined,直接进入 normal processing:
const task = tasks.pop() // 返回 undefined
if (task?.type === "subtask") {
// 不进入
}
if (task?.type === "compaction") {
// 不进入
}
// context overflow - 需要 lastFinished 存在才会触发
if (lastFinished && lastFinished.summary !== true && isOverflow(...)) {
// 如果 lastFinished 不存在,不进入
}
// normal processing - 直接进入这里
const agent = await Agent.get(lastUser.agent)
const processor = SessionProcessor.create({...})
// ... 调用 processor.process() → 大模型 API
流程图:
没有 compaction/subtask part
↓
task = undefined
↓
跳过 subtask 处理
↓
跳过 compaction 处理
↓
检查是否需要自动压缩(需要 lastFinished 存在)
↓
进入 normal processing
↓
调用 processor.process() → 大模型 API
5.2.4 未完成的 part 数据如何处理?
进入 normal processing 时,会带着未完成助手消息的 part 数据调用大模型 API,但有特殊处理:
位置:src/session/message-v2.ts:606-670
for (const part of msg.parts) {
if (part.type === "text")
assistantMessage.parts.push({ type: "text", text: part.text })
if (part.type === "tool") {
if (part.state.status === "completed") {
// 已完成的工具调用 → 正常输出
assistantMessage.parts.push({
type: ("tool-" + part.tool),
state: "output-available",
input: part.state.input,
output: part.state.output,
})
}
// ★ 关键:未完成的工具调用 → 标记为中断
if (part.state.status === "pending" || part.state.status === "running")
assistantMessage.parts.push({
type: ("tool-" + part.tool),
state: "output-error",
input: part.state.input,
errorText: "[Tool execution was interrupted]", // ← 特殊标记
})
}
}
处理逻辑:
| part 状态 | 处理方式 |
|---|---|
completed |
正常输出,包含 input 和 output |
error |
输出错误信息 |
pending / running |
输出 "[Tool execution was interrupted]" |
为什么这样处理?
// Handle pending/running tool calls to prevent dangling tool_use blocks
// Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
原因:Anthropic API 要求每个 tool_use 都有对应的 tool_result。如果之前的工具调用没有完成,必须提供一个"假"的结果,否则 API 会报错。
完整流程:
有 lastAssistant 但没有 lastFinished
↓
msgs 包含未完成的助手消息(有 parts,无 finish)
↓
toModelMessages() 转换:
- text parts → 正常包含
- completed tools → 正常输出
- pending/running tools → "[Tool execution was interrupted]"
↓
processor.process() 带着转换后的消息调用大模型 API
↓
模型看到之前的内容 + 中断标记,决定如何继续
6. [服务端] Compaction 上下文压缩
大模型有上下文长度限制(如 Claude 的 200K tokens),当对话历史过长时:
- 无法继续对话:超过模型限制会导致 API 报错
- 成本增加:每次请求都发送完整历史,token 消耗巨大
- 效率降低:模型处理大量无关历史信息
Compaction 机制通过生成对话摘要来解决这些问题,将长对话压缩为简洁的上下文摘要。
详情见OpenCode Compaction 上下文压缩 和 清理旧工具输出。
7. [服务端] 处理提示词并调用大模型
构建和使用系统提示词:
在src/session/prompt.ts的loop函数中会创建系统提示词:
const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())]
返回的系统提示词当做processor.process函数的参数传入:
const result = await processor.process({
......
system,
......
})
调用大模型的关键组件:
| 组件 | 位置 | 职责 |
|---|---|---|
| LLM.stream | src/session/llm.ts |
统一的大模型调用入口,整合配置、认证、工具定义 |
| streamText | Vercel AI SDK | 流式调用大模型,返回统一格式的事件流 |
| ProviderTransform | src/provider/transform.ts |
多平台模型适配(消息格式、缓存、推理配置等) |
详情见OpenCode 调用大模型。
8. [服务端] loop循环控制逻辑
8.1 循环退出条件
位置: src/session/prompt.ts:324-331
if (
lastAssistant?.finish &&
!["tool-calls", "unknown"].includes(lastAssistant.finish) &&
lastUser.id < lastAssistant.id
) {
// 模型已完成,退出循环
log.info("exiting loop", { sessionID })
break
}
这段代码判断模型已经完成对用户消息的响应,不需要继续处理工具调用。三个条件必须同时满足:
条件 1:lastAssistant?.finish
最后一条助手消息有 finish 原因。finish 字段表示模型完成响应的原因,可能的值包括:
| finish 值 | 含义 |
|---|---|
"stop" |
正常结束(没有工具调用) |
"tool-calls" |
模型调用了工具,需要继续处理 |
"length" |
达到最大 token 限制 |
"content-filter" |
内容被过滤 |
"unknown" |
未知原因 |
条件 2:!["tool-calls", "unknown"].includes(lastAssistant.finish)
finish 不是 "tool-calls" 或 "unknown"。
- 排除
"tool-calls":如果模型调用了工具,循环需要继续处理工具调用结果 - 排除
"unknown":无法确定完成原因时,不退出循环
条件 3:lastUser.id < lastAssistant.id
用户消息的 id 小于助手消息的 id。由于 id 是按时间递增生成的,这意味着助手消息是对这条用户消息的响应(而不是旧的历史记录)。
退出条件总结
当三个条件都满足时,说明:
- 模型已完成响应(有 finish 原因)
- 没有待处理的工具调用(finish 不是 tool-calls)
- 这是对用户输入的完整回复(用户消息在助手消息之前)
所以可以安全退出循环。
8.2 循环继续条件
当模型返回 finish: "tool-calls" 时,循环继续,等待工具执行结果。
8.3 processor.process 返回值
"continue": 继续循环"stop": 停止循环"compact": 触发压缩后继续
8.4 最大步数限制 (maxSteps)
maxSteps 用于限制 agent 的最大执行步数,防止无限循环。
8.4.1 定义和判断
位置: src/session/prompt.ts:572-573
const maxSteps = agent.steps ?? Infinity // 从 agent 配置读取,默认无限制
const isLastStep = step >= maxSteps // 判断是否达到最大步数
8.4.2 达到最大步数时的处理
位置: src/session/prompt.ts:677-687
当 isLastStep = true 时,会在消息末尾注入 MAX_STEPS 提示:
messages: [
...MessageV2.toModelMessages(msgs, model),
...(isLastStep
? [
{
role: "assistant" as const,
content: MAX_STEPS, // ← 注入特殊提示
},
]
: []),
],
MAX_STEPS 提示内容(src/session/prompt/max-steps.txt):
CRITICAL - MAXIMUM STEPS REACHED
The maximum number of steps allowed for this task has been reached.
Tools are disabled until next user input. Respond with text only.
STRICT REQUIREMENTS:
1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
2. MUST provide a text response summarizing work done so far
3. This constraint overrides ALL other instructions
Response must include:
- Statement that maximum steps for this agent have been reached
- Summary of what has been accomplished so far
- List of any remaining tasks that were not completed
- Recommendations for what should be done next
8.4.3 执行流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ Agent 执行循环 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ step = 0 │
│ while (true) { │
│ step++ // 每次循环递增 │
│ │
│ const maxSteps = agent.steps ?? Infinity │
│ const isLastStep = step >= maxSteps │
│ │
│ if (isLastStep) { │
│ // 在消息末尾注入 MAX_STEPS 提示 │
│ // 告诉模型:停止调用工具,只返回文本总结 │
│ } │
│ │
│ result = await processor.process(...) │
│ │
│ if (result === "stop") break // 模型主动停止 │
│ if (finishReason !== "tool-calls") break // 没有工具调用,停止 │
│ │
│ // 如果有工具调用,继续下一轮(step++) │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8.4.4 配置方式
在 agent 配置中设置 steps:
// src/agent/agent.ts:44
steps: z.number().int().positive().optional(), // 最大步数
配置文件示例:
{
"agent": {
"my-agent": {
"steps": 10,
"description": "最多执行 10 步"
}
}
}
8.4.5 使用场景
| 场景 | 作用 |
|---|---|
| 防止无限循环 | 避免模型陷入重复调用工具的死循环 |
| 限制成本 | 控制单次任务的最大 token 消耗 |
| 强制总结 | 达到步数限制后,要求模型总结当前进展 |
| 安全边界 | 为自动化任务设置安全边界 |
8.4.6 示例
假设设置 steps: 5:
Step 1: 模型调用 read 工具 → 继续循环
Step 2: 模型调用 edit 工具 → 继续循环
Step 3: 模型调用 bash 工具 → 继续循环
Step 4: 模型调用 read 工具 → 继续循环
Step 5: isLastStep = true → 注入 MAX_STEPS 提示
模型返回文本总结,不再调用工具 → 循环结束

浙公网安备 33010602011771号