0. 本章学习目标
理解 session API payload、HTTP handler、createUserMessage、part 解析、message 持久化,以及它们如何成为 agent loop 的上下文。
1. 一句话讲明白
用户输入与会话模块负责把外部请求变成内部事实:一个 session 下的 user message 和一组 message parts。来源:packages/opencode/src/session/prompt.ts:689-731、packages/opencode/src/session/prompt.ts:1116-1230。
2. 它在 OpenCode agent 中的位置
CLI/API 只提供输入,agent loop 只消费消息历史。session/message 层负责把文本、附件、agent mention、MCP resource、权限覆盖和模型选择整理成统一结构。
3. 生活类比
session 是项目档案,message 是每次沟通记录,part 是正文、附件、工具结果或系统补充。agent 每轮工作前读档案。
4. Java 开发者类比
Session.Info类似会话 aggregate。MessageV2.User/ assistant 类似消息实体。Part是 message 的子实体集合。SessionPrompt.prompt是 Application Service。
5. 最小源码路径
groups/session.ts:66-68groups/session.ts:312-324handlers/session.ts:279-290prompt.ts:689-731prompt.ts:788-1085prompt.ts:1116-1230
6. 用户输入到 agent 行动的整体链路
client.session.prompt(payload)
-> PromptPayload
-> sessionHandlers.prompt
-> SessionPrompt.prompt
-> createUserMessage
-> resolvePart
-> sessions.updateMessage/updatePart
-> loop(sessionID)7. 核心源码逐段讲解
7.1 Payload 从内部 schema 派生
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))路径:packages/opencode/src/server/routes/instance/httpapi/groups/session.ts:66。
7.2 Handler 补 sessionID
const message = yield* promptSvc.prompt({
...ctx.payload,
sessionID: ctx.params.sessionID,
})路径:packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts:279-289。
7.3 User message schema
export const User = Schema.Struct({
role: Schema.Literal("user"),
agent: Schema.String,
model: Schema.Struct({ providerID: ProviderID, modelID: ModelID }),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
})路径:packages/opencode/src/session/message-v2.ts:327-350。
7.4 Part union
export const Part = Schema.Union([
TextPart, SubtaskPart, ReasoningPart, FilePart, ToolPart,
StepStartPart, StepFinishPart, SnapshotPart, PatchPart,
AgentPart, RetryPart, CompactionPart,
]).annotate({ discriminator: "type", identifier: "Part" })路径:packages/opencode/src/session/message-v2.ts:352-365。
7.5 创建消息并写入
const ag = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID))
const info: MessageV2.User = { role: "user", sessionID: input.sessionID, agent: ag.name, model: { providerID: model.providerID, modelID: model.modelID, variant } }
yield* sessions.updateMessage(info)
for (const part of parts) yield* sessions.updatePart(part)路径:packages/opencode/src/session/prompt.ts:689-731、packages/opencode/src/session/prompt.ts:1116-1117。
8. 关键 TypeScript 语法复习
Struct.omit:从内部 schema 派生 API payload。- object spread:
{ ...ctx.payload, sessionID }。 - optional field:
variant: Schema.optional(...)。 - discriminated union:
Part以type判别。 Effect.forEach并发解析 parts。
9. 设计模式和架构思想
- DTO 派生
- Aggregate
- Message/Part 子实体
- Plugin hook
- Controller 到 Application Service 的边界适配
10. 模块协作
Tool:file part 可调用 read tool;Provider:创建 message 时决定 model;Session:持久化 message/part;文件系统:file URL 被读取成 synthetic context。来源:packages/opencode/src/session/prompt.ts:867-1039。
11. mini agent 对应代码
async function prompt(input) {
const session = await sessions.get(input.sessionID)
const user = createUserMessage(input, session)
const parts = await resolveParts(input.parts)
await sessions.saveMessage(user)
await sessions.saveParts(user.id, parts)
return input.noReply ? { info: user, parts } : runLoop(input.sessionID)
}12. 费曼复述区
- 为什么 user message 里要保存 agent/model?
- part union 解决了什么问题?
- 为什么 file attachment 会在创建 user message 时被 read tool 读取?
13. 练习题
- 解释为什么
PromptPayloadomitsessionID。 - 列出 user message 的核心字段。
- 实现一个支持 text/file 的
resolveParts。
14. 源码追踪任务
groups/session.ts:312-324-> handler- handler ->
SessionPrompt.prompt createUserMessage-> model 选择- file part ->
read.execute
15. 面试式自测
- 为什么 message 和 part 分开?
- 为什么 prompt success 是
MessageV2.WithParts? - 插件修改消息的扩展点在哪里?