内部模块: Session (会话管理)

OpenCode 的核心大脑,管理对话生命周期、上下文和 Agent 执行循环。

1. 概览 (Overview)

  • 路径: packages/opencode/src/session/
  • 定位: OpenCode 最核心的模块,负责整个对话的生命周期管理
  • 核心职责:
    • 管理 Agent 与 LLM 的交互循环
    • 处理消息流和工具调用
    • 上下文压缩和 Token 管理
    • 会话状态持久化

核心文件列表

文件行数职责
index.ts~13,697会话数据结构、CRUD 操作、事件定义
prompt.ts~53,223核心循环,System Prompt 构建
processor.ts~15,461消息处理器,处理 LLM 响应和工具调用
message-v2.ts~19,260消息格式 v2,Part 系统
compaction.ts~7,216上下文压缩和 Token 管理
llm.ts~6,607LLM 调用封装
summary.ts~6,066会话摘要生成
system.ts~4,415System Prompt 构建
retry.ts~3,087错误重试逻辑
revert.ts~3,819撤销操作管理
status.ts~1,483会话状态管理
todo.ts~1,131待办事项追踪
message.ts~5,090旧版消息格式(v1,已废弃)

2. 核心概念

2.1 Session 数据结构

// src/session/index.ts
export const Info = z.object({
  id: Identifier.schema("session"),
  projectID: z.string(),
  directory: z.string(),
  parentID: Identifier.schema("session").optional(),  // 子会话支持
  
  // 会话摘要统计
  summary: z.object({
    additions: z.number(),
    deletions: z.number(),
    files: z.number(),
    diffs: Snapshot.FileDiff.array().optional(),
  }).optional(),
  
  // 分享信息
  share: z.object({
    url: z.string(),
  }).optional(),
  
  title: z.string(),
  version: z.string(),
  
  // 时间戳
  time: z.object({
    created: z.number(),
    updated: z.number(),
    compacting: z.number().optional(),
    archived: z.number().optional(),
  }),
  
  // 权限规则集
  permission: PermissionNext.Ruleset.optional(),
  
  // 撤销信息
  revert: z.object({
    messageID: z.string(),
    partID: z.string().optional(),
    snapshot: z.string().optional(),
    diff: z.string().optional(),
  }).optional(),
})

关键字段说明:

  • parentID: 支持子会话(由 Task Tool 创建)
  • summary: Git 级别的代码变更统计
  • permission: 会话级别的权限覆盖规则
  • revert: 撤销点信息,用于回滚

2.2 会话状态机

2.3 Message 格式演进

OpenCode 经历了两个消息格式版本:

版本文件状态特点
v1message.ts🔴 已废弃简单的 role + content 结构
v2message-v2.ts✅ 当前Part 系统,支持流式更新

v2 的 Part 系统 是核心设计:

// src/session/message-v2.ts
export type Part = 
  | TextPart          // 文本内容
  | FilePart          // 文件引用
  | ToolPart          // 工具调用
  | ReasoningPart     // 推理过程 (Claude 3.5 Thinking)
  | AgentPart         // Agent 切换
  | SubtaskPart       // 子任务
  | ErrorPart         // 错误信息

为什么需要 Part 系统?

  1. 流式更新: 每个 Part 可以独立增量更新
  2. 细粒度控制: 可以单独压缩、撤销某个 Part
  3. 类型安全: 每种 Part 有明确的数据结构
  4. UI 渲染: 前端可以为不同 Part 类型定制 UI

3. Thinking Loop (核心循环) 🧠

最重要的文件: src/session/prompt.ts

这是 OpenCode 的心跳,一个持续运行的循环,直到任务完成或用户中断。

3.1 循环入口

// src/session/prompt.ts
export const prompt = fn(PromptInput, async (input) => {
  // 1. 创建或获取 Assistant Message
  const assistantMessage = await iife(async () => {
    if (input.messageID) {
      return MessageV2.get({ id: input.messageID, sessionID: input.sessionID })
    }
    // 创建新的 Assistant Message
    return MessageV2.create({
      sessionID: input.sessionID,
      role: "assistant",
      parts: input.parts,
    })
  })
 
  // 2. 准备工具和上下文
  const tools = await prepareTools(input)
  const messages = await buildContext(input)
  
  // 3. 创建 Processor
  const processor = SessionProcessor.create({
    assistantMessage,
    sessionID: input.sessionID,
    model: selectedModel,
    abort: abortController.signal,
  })
 
  // 4. **开始循环**
  const result = await processor.process({
    messages,
    tools,
    model: selectedModel,
    system: systemPrompt,
    // ...
  })
 
  return assistantMessage
})

3.2 循环逻辑详解

核心在 SessionProcessor.process()

// src/session/processor.ts
export function create(input: {
  assistantMessage: MessageV2.Assistant
  sessionID: string
  model: Provider.Model
  abort: AbortSignal
}) {
  return {
    async process(streamInput: LLM.StreamInput) {
      while (true) {  // ← 无限循环!
        try {
          // 1. 调用 LLM 获取流式响应
          const stream = await LLM.stream(streamInput)
          
          for await (const value of stream.fullStream) {
            switch (value.type) {
              case "text-delta":
                // 增量更新文本 Part
                await updateTextPart(value.text)
                break
                
              case "tool-call-delta":
                // 工具调用参数增量更新
                await updateToolPart(value)
                break
                
              case "tool-call":
                // 工具调用完成,执行工具
                const result = await executeTool(value)
                
                // 将工具结果添加到消息历史
                streamInput.messages.push({
                  role: "tool",
                  content: result,
                })
                break
                
              case "finish":
                // LLM 决定停止
                if (value.finishReason === "stop") {
                  return "stop"  // ← 退出循环
                }
                break
            }
          }
          
          // 2. 如果有工具调用,继续下一轮循环
          // LLM 会看到工具结果,继续思考
          
        } catch (error) {
          // 错误处理和重试逻辑
          await SessionRetry.handle(error)
        }
      }
    }
  }
}

3.3 循环流程图

3.4 Context 收集

在每次循环开始前,需要构建完整的上下文:

// src/session/prompt.ts
async function buildContext(input: PromptInput) {
  // 1. 获取所有消息(包含压缩后的)
  const messages = await MessageV2.filterCompacted(
    MessageV2.stream(input.sessionID)
  )
  
  // 2. 转换为 LLM 格式
  const llmMessages = MessageV2.toModelMessage(messages)
  
  // 3. 添加 System Prompt
  const systemPrompt = await SystemPrompt.build({
    agent: selectedAgent,
    model: selectedModel,
  })
  
  return { messages: llmMessages, system: systemPrompt }
}

4. 消息处理 (Message Processing)

4.1 Processor 架构

SessionProcessor状态机事件处理器的结合:

// src/session/processor.ts
export function create(input) {
  const toolcalls: Record<string, MessageV2.ToolPart> = {}
  let snapshot: string | undefined
  let blocked = false
  let attempt = 0
  let needsCompaction = false
 
  return {
    // 核心处理函数
    async process(streamInput: LLM.StreamInput) {
      // 处理流式事件...
    },
    
    // 从 tool call ID 获取 Part
    partFromToolCall(toolCallID: string) {
      return toolcalls[toolCallID]
    },
    
    // 获取当前消息
    get message() {
      return input.assistantMessage
    }
  }
}

4.2 工具调用处理

工具调用经历多个阶段:

代码实现:

// src/session/processor.ts (简化版)
for await (const value of stream.fullStream) {
  switch (value.type) {
    case "tool-call-start":
      // 创建 ToolPart
      const part: MessageV2.ToolPart = {
        id: Identifier.ascending("part"),
        type: "tool",
        tool: value.toolName,
        args: {},
        state: { status: "pending" },
      }
      toolcalls[value.toolCallId] = part
      break
 
    case "tool-call-delta":
      // 增量更新参数
      const part = toolcalls[value.toolCallId]
      part.args = mergeJson(part.args, value.argsTextDelta)
      await Session.updatePart({ part, delta: value.argsTextDelta })
      break
 
    case "tool-call":
      // 执行工具
      const part = toolcalls[value.toolCallId]
      part.state.status = "executing"
      await Session.updatePart(part)
      
      try {
        // 工具执行(带权限检查)
        const result = await tools[value.toolName].execute(
          value.args,
          toolContext
        )
        
        part.state = {
          status: "completed",
          output: result,
        }
      } catch (error) {
        part.state = {
          status: "failed",
          error: error.message,
        }
      }
      
      await Session.updatePart(part)
      break
  }
}

4.3 流式响应支持

OpenCode 支持多种流式内容:

类型事件用途
文本text-deltaAgent 的回复文本
推理reasoning-deltaClaude 3.5 的思考过程
工具调用tool-call-delta工具参数增量更新
完成finish本轮对话结束

5. 上下文管理

5.1 为什么需要压缩?

问题: LLM 的 Context Window 是有限的
  例如: Claude Sonnet 4 有 200K tokens
  
现实: 一个长对话可能包含:
  - 100 条消息
  - 50 次工具调用
  - 大量的文件内容
  
结果: Token 使用量 > Context Limit
  → 导致 API 调用失败
  
解决方案: 上下文压缩 (Compaction)

5.2 压缩策略

文件: src/session/compaction.ts

OpenCode 使用两阶段压缩

阶段 1: Pruning (修剪)

删除旧的、不相关的工具调用输出:

// src/session/compaction.ts
export const PRUNE_MINIMUM = 20_000   // 最少保留 20K tokens
export const PRUNE_PROTECT = 40_000   // 保护最近 40K tokens
 
export async function prune(input: { sessionID: string }) {
  const msgs = await Session.messages({ sessionID: input.sessionID })
  let total = 0
  let pruned = 0
  
  // 从后往前遍历消息
  for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) {
    const msg = msgs[msgIndex]
    
    for (const part of msg.parts) {
      if (part.type === "tool" && part.state.status === "completed") {
        const estimate = Token.estimate(part.state.output)
        total += estimate
        
        // 超过保护阈值的工具调用输出可以删除
        if (total > PRUNE_PROTECT) {
          pruned += estimate
          await compactPart(part)  // 清空输出,保留元数据
        }
      }
    }
  }
}

阶段 2: Summarization (摘要)

使用 LLM 生成历史消息的摘要:

// src/session/compaction.ts (简化逻辑)
export async function compact(input: { sessionID: string }) {
  // 1. 检查是否需要压缩
  const isOverflow = await SessionCompaction.isOverflow({
    tokens: latestMessage.tokens,
    model: selectedModel,
  })
  
  if (!isOverflow) return
  
  // 2. 先尝试修剪
  await prune({ sessionID: input.sessionID })
  
  // 3. 如果还不够,生成摘要
  const summary = await SessionSummary.generate({
    sessionID: input.sessionID,
    upToMessageID: lastUserMessage.id,
  })
  
  // 4. 用摘要替换原始消息
  await Message.markAsSummarized(messagesBeforeLastUser)
  await Message.createSummaryMessage(summary)
}

5.3 Token 预算管理

// src/session/compaction.ts
export async function isOverflow(input: {
  tokens: { input: number; output: number; cache: { read: number } }
  model: Provider.Model
}) {
  const context = input.model.limit.context  // 200K for Claude Sonnet 4
  const count = input.tokens.input + input.tokens.cache.read + input.tokens.output
  
  // 预留输出空间
  const output = Math.min(input.model.limit.output, 32_000)
  const usable = context - output
  
  return count > usable  // 超过可用空间?
}

5.4 压缩流程图


6. 辅助功能

6.1 会话摘要 (summary.ts)

自动生成对话摘要,用于上下文压缩:

// src/session/summary.ts
export namespace SessionSummary {
  export async function generate(input: {
    sessionID: string
    upToMessageID: string
  }) {
    const messages = await getMessagesUpTo(input.upToMessageID)
    
    // 使用特殊的 "summary" Agent
    const result = await LLM.generate({
      agent: "summary",  // 专门的摘要 Agent
      messages: messages,
      prompt: "总结以上对话的关键信息和决策...",
    })
    
    return result.summary
  }
}

6.2 错误重试 (retry.ts)

智能重试机制,处理 LLM API 的临时错误:

// src/session/retry.ts
export namespace SessionRetry {
  const MAX_RETRIES = 3
  
  export async function handle(error: Error, attempt: number) {
    // 可重试的错误类型
    if (error.code === "rate_limit_exceeded" || 
        error.code === "server_error") {
      
      if (attempt < MAX_RETRIES) {
        const delay = Math.pow(2, attempt) * 1000  // 指数退避
        await sleep(delay)
        return "retry"
      }
    }
    
    throw error  // 不可重试或超过次数
  }
}

6.3 撤销操作 (revert.ts)

支持会话回滚到之前的状态:

// src/session/revert.ts
export namespace SessionRevert {
  export async function create(input: {
    sessionID: string
    messageID: string
    partID?: string
  }) {
    // 1. 创建 Git 快照
    const snapshot = await Snapshot.create()
    
    // 2. 记录撤销点
    await Session.update({
      id: input.sessionID,
      revert: {
        messageID: input.messageID,
        partID: input.partID,
        snapshot: snapshot.id,
      }
    })
  }
  
  export async function apply(input: { sessionID: string }) {
    const session = await Session.get(input.sessionID)
    if (!session.revert) throw new Error("No revert point")
    
    // 1. 恢复 Git 快照
    await Snapshot.restore(session.revert.snapshot)
    
    // 2. 删除撤销点之后的消息
    await Message.deleteAfter(session.revert.messageID)
    
    // 3. 清除撤销点
    await Session.update({
      id: input.sessionID,
      revert: undefined,
    })
  }
}

6.4 待办管理 (todo.ts)

追踪 Agent 的任务列表:

// src/session/todo.ts
export namespace SessionTodo {
  export const Item = z.object({
    id: z.string(),
    content: z.string(),
    status: z.enum(["pending", "in_progress", "completed", "cancelled"]),
    priority: z.enum(["high", "medium", "low"]),
  })
  
  export async function update(input: {
    sessionID: string
    todos: Item[]
  }) {
    // 存储在 Session Storage 中
    await Storage.set(`todo/${input.sessionID}`, input.todos)
    
    // 发布事件通知 UI
    await Bus.publish(Event.Updated, {
      sessionID: input.sessionID,
      todos: input.todos,
    })
  }
}

7. Prompt 模板系统

目录: src/session/prompt/

OpenCode 为不同的 LLM 提供商和场景优化了 System Prompt:

7.1 模板列表

文件用途优化对象
anthropic.txt标准 Anthropic PromptClaude 系列
anthropic-20250930.txt最新版本 PromptClaude Sonnet 4+
gemini.txtGoogle Gemini 优化Gemini 系列
qwen.txt通义千问优化Qwen 系列
codex.txt代码生成优化OpenAI Codex
copilot-gpt-5.txtGitHub Copilot 优化GPT-5
beast.txt高性能模式所有模型
plan.txt规划模式Plan Agent
build-switch.txt模式切换提示动态切换
max-steps.txt步数限制提示所有模型

7.2 Prompt 选择逻辑

// src/session/system.ts
export namespace SystemPrompt {
  export async function build(input: {
    agent: Agent.Info
    model: Provider.Model
  }) {
    let template = ""
    
    // 1. 根据 Provider 选择模板
    if (input.model.providerID === "anthropic") {
      if (input.model.modelID.includes("sonnet-4")) {
        template = ANTHROPIC_20250930
      } else {
        template = ANTHROPIC
      }
    } else if (input.model.providerID === "google") {
      template = GEMINI
    }
    
    // 2. 根据 Agent 选择
    if (input.agent.name === "plan") {
      template += PLAN
    }
    
    // 3. 添加 Agent 的自定义 Prompt
    if (input.agent.prompt) {
      template += "\n\n" + input.agent.prompt
    }
    
    return template
  }
}

8. 代码示例与实战场景

场景 1: 创建新会话并发送消息

// 1. 创建会话
const session = await Session.create({
  directory: "/path/to/project",
  title: "Implement user authentication",
})
 
// 2. 发送用户消息
await SessionPrompt.prompt({
  sessionID: session.id,
  parts: [
    {
      type: "text",
      text: "帮我实现用户登录功能,使用 JWT",
    }
  ],
})
 
// 3. Agent 会自动开始工作
// - 读取现有代码
// - 生成新文件
// - 运行测试
// - 返回结果

场景 2: 监听会话更新

// 订阅会话事件
Bus.subscribe(Session.Event.Updated, async (event) => {
  console.log("Session updated:", event.properties.info.id)
  console.log("Title:", event.properties.info.title)
})
 
// 订阅消息更新
Bus.subscribe(MessageV2.Event.Updated, async (event) => {
  const message = event.properties.info
  console.log("New message:", message.role, message.parts.length)
})
 
// 订阅 Part 更新(实时流式)
Bus.subscribe(MessageV2.Event.PartUpdated, async (event) => {
  const part = event.properties.part
  if (part.type === "text") {
    console.log("Text delta:", event.properties.delta)
  }
})

场景 3: 手动触发压缩

// 检查是否需要压缩
const lastMessage = await MessageV2.latest({ sessionID })
const needsCompaction = await SessionCompaction.isOverflow({
  tokens: lastMessage.tokens,
  model: selectedModel,
})
 
if (needsCompaction) {
  // 先修剪
  await SessionCompaction.prune({ sessionID })
  
  // 如果还不够,生成摘要
  const stillOverflow = await SessionCompaction.isOverflow(...)
  if (stillOverflow) {
    await SessionCompaction.compact({ sessionID })
  }
}

场景 4: 实现自定义 Agent

// 在配置中定义
const customAgent: Agent.Info = {
  name: "code-reviewer",
  description: "专门进行代码审查",
  mode: "primary",
  temperature: 0.3,
  prompt: `
你是一个资深的代码审查专家。
你的任务是:
1. 检查代码质量
2. 发现潜在 bug
3. 提出改进建议
  `,
  permission: {
    "*": "allow",
    "edit": "deny",  // 只读,不能修改代码
    "write": "deny",
    "bash": "deny",
  }
}
 
// 使用自定义 Agent
await SessionPrompt.prompt({
  sessionID,
  agent: "code-reviewer",
  parts: [{ type: "text", text: "审查 src/auth.ts" }],
})

9. 常见陷阱与最佳实践

❌ 陷阱 1: 忽略 Token 限制

错误做法:

// 不检查 token 使用量就发送大量消息
for (const file of 100files) {
  await SessionPrompt.prompt({
    sessionID,
    parts: [{ type: "file", path: file }],
  })
}

正确做法:

// 监控 token 使用量
const session = await Session.get(sessionID)
const lastMessage = await MessageV2.latest({ sessionID })
 
if (lastMessage.tokens.input > 150_000) {
  // 触发压缩
  await SessionCompaction.compact({ sessionID })
}

❌ 陷阱 2: 阻塞式等待

错误做法:

// 同步等待结果
const result = await SessionPrompt.prompt({ ... })
// 这会阻塞,直到 Agent 完成所有工作

正确做法:

// 使用事件监听
const promise = SessionPrompt.prompt({ ... })
 
Bus.subscribe(SessionStatus.Event.Updated, (event) => {
  if (event.properties.status.type === "idle") {
    console.log("Agent 完成工作")
  }
})
 
await promise

✅ 最佳实践 1: 使用子会话

对于复杂任务,创建子会话隔离上下文:

// 主会话
const mainSession = await Session.create({ ... })
 
// 创建子会话处理子任务
const childSession = await Session.create({
  parentID: mainSession.id,
  directory: mainSession.directory,
  title: "Subtask: Generate tests",
})
 
// 子会话完成后,结果会汇总到主会话

✅ 最佳实践 2: 定期保存撤销点

// 在重要操作前创建撤销点
await SessionRevert.create({
  sessionID,
  messageID: lastSafeMessage.id,
})
 
// 执行高风险操作
await dangerousOperation()
 
// 如果出错,可以回滚
if (failed) {
  await SessionRevert.apply({ sessionID })
}

10. 总结

Session 模块是 OpenCode 最核心的模块

核心职责

  • Thinking Loop: 管理 Agent 与 LLM 的交互循环
  • 消息处理: 处理流式响应、工具调用、错误重试
  • 上下文管理: 智能压缩,保持 Token 在限制内
  • 状态持久化: 保存会话历史和撤销点

关键设计

  • Part 系统: 细粒度的消息片段,支持流式更新
  • 双阶段压缩: Pruning + Summarization
  • 事件驱动: 所有状态变化通过 Bus 广播
  • 错误恢复: 重试、撤销、快照机制

与其他模块的关系

下一步: 阅读 Server 模块 了解 HTTP API 如何调用 Session