跳转到内容

Agent 源码解读

模块概述

在 TypeScript 版本中,Agent 不再是执行引擎,而是纯粹的配置定义。它描述了一个”角色”的行为参数——名称、权限规则、系统提示、模型偏好、温度设置——但不包含任何执行逻辑。

真正的执行循环由 Session 模块 驱动。Session 通过 SessionPrompt.loop() 启动主循环,在每次迭代中调用 SessionProcessor.process()LLM.stream() → Vercel AI SDK streamText() 完成 LLM 调用,然后由 SessionProcessor 处理流式事件、工具调用、上下文压缩等。

这种”配置与执行分离”的设计意味着:

  • 新增或修改一个 Agent 只需声明配置,无需编写执行逻辑
  • 所有 Agent 共享同一套 Session 执行循环,工具调用、错误处理、上下文管理只实现一次
  • Agent 的权限、工具集、系统提示都通过声明式配置控制

关键文件

文件路径行数职责
src/agent/agent.ts~230Agent 配置定义:Agent.Info schema、内置 Agent 列表、state() 工厂、generate() 动态创建
src/session/index.ts~400Session 管理:CRUD、消息持久化、费用计算、分页查询
src/session/prompt.ts~580核心入口:loop() 主循环、用户消息构造、工具解析、子任务分发
src/session/llm.ts~200LLM 调用:LLM.stream() 封装 Vercel AI SDK streamText(),处理 system prompt 拼接和参数合并
src/session/processor.ts~220消息处理器:流式事件分发、工具生命周期、Doom Loop 检测、快照追踪
src/session/compaction.ts~220上下文压缩:溢出检测、消息裁剪(prune)、摘要生成
src/session/overflow.ts~20溢出判断:纯函数,检查 token 用量是否超过模型上下文窗口
src/session/retry.ts~100重试策略:错误分类、指数退避、retry-after header 解析
src/session/revert.ts~160消息回退:快照恢复、消息删除、diff 计算
src/session/status.ts~80状态管理:idle/busy/retry 三态切换,通过 Bus 广播
src/session/message-v2.ts~600消息类型:Zod schema 定义所有消息和 Part 类型、持久化、流式查询

类型体系

Agent.Info — Agent 配置 Schema

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(),
    variant: z.string().optional(),
    prompt: z.string().optional(),
    options: z.record(z.string(), z.any()),
    steps: z.number().int().positive().optional(),
  })
  .meta({
    ref: "Agent",
  })

关键字段说明:

  • mode: "primary" 表示主 Agent(用户直接对话),"subagent" 表示子 Agent(由 Task 工具调用),"all" 表示自定义 Agent
  • permission: PermissionNext.Ruleset,定义该 Agent 的工具权限规则链
  • prompt: 自定义系统提示,覆盖 Provider 默认提示
  • model: 可选的固定模型绑定,不指定则使用用户当前选择的模型

内置 Agent 列表

Agent 名称modehidden用途
buildprimary默认 Agent,拥有全部工具权限,支持 question 和 plan_enter
planprimary规划模式,禁止所有编辑工具,只允许写入 plans 目录
exploresubagent快速代码探索,只有只读工具(grep、glob、read、bash、webfetch 等)
generalsubagent通用子 Agent,用于并行执行多步任务,禁用 todo 工具
compactionprimary上下文压缩专用,禁止所有工具,生成对话摘要
titleprimary为会话生成标题,禁止所有工具,temperature 固定 0.5
summaryprimary为会话生成摘要,禁止所有工具

Session.Info — 会话 Schema

export const Info = z
  .object({
    id: Identifier.schema("session"),
    slug: z.string(),
    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(),
  })

并发控制 — Runner 机制

每个 SessionID 绑定一个 Runner 实例,由 Effect 的 Runner.make() 创建。Runner 内部维护任务队列,确保同一 Session 的操作串行执行:

const runners = new Map<string, Runner<MessageV2.WithParts>>()
const getRunner = (runners, sessionID) => {
  const existing = runners.get(sessionID)
  if (existing) return existing
  const runner = Runner.make<MessageV2.WithParts>(scope, {
    onIdle: Effect.gen(function* () {
      runners.delete(sessionID)   // 空闲时自动清理,释放资源
      yield* status.set(sessionID, { type: "idle" })
    }),
    onBusy: status.set(sessionID, { type: "busy" }),
    onInterrupt: lastAssistant(sessionID),
    busy: () => { throw new Session.BusyError(sessionID) },
  })
  runners.set(sessionID, runner)
  return runner
}

关键设计点:

  • 串行化保证:同一 Session 的所有操作通过 Runner 排队执行,不存在并发竞态
  • 自动清理:Runner 进入空闲状态时从 Map 中删除,避免内存泄漏
  • 中断回调onInterrupt 在取消时触发 lastAssistant(),确保中断后的状态一致
  • 忙碌拒绝:如果 Session 正忙且不支持排队,直接抛出 BusyError

Race Condition — SyncEvent 事件源

消息写入不是直接操作数据库,而是通过 SyncEvent 事件源同步执行:

const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
  Effect.gen(function* () {
    yield* Effect.sync(() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg }))
    return msg
  })

这个设计消除了竞态条件的可能性:

  • SyncEvent.run() 同步执行:先写入 SQLite WAL,再发布 BusEvent
  • Effect 的单线程协程模型保证了操作的原子性
  • 同一 Session 的操作已被 Runner 串行化,不存在多线程竞态
  • Part 更新有 delta 优化:流式文本只发 BusEvent 不写 DB,减少 IO 开销

核心流程 — Session 执行循环

完整调用链

用户输入 → SessionPrompt.prompt()
         → SessionPrompt.loop()
           ├─ 首条消息? → ensureTitle() 异步生成标题
           ├─ 有 pending subtask? → TaskTool 直接执行
           ├─ 有 pending compaction? → SessionCompaction.process()
           ├─ 上下文溢出? → SessionCompaction.create() → continue
           └─ 正常处理:
              ├─ SessionProcessor.create()
              ├─ resolveSystemPrompt() 拼接系统提示
              ├─ resolveTools() 注册内置 + MCP 工具
              └─ processor.process()
                 └─ LLM.stream()
                    └─ ai.streamText() (Vercel AI SDK)
                 └─ for await (stream.fullStream) 处理流式事件
                    ├─ text-start / text-delta / text-end → 文本 Part
                    ├─ reasoning-start / reasoning-delta / reasoning-end → 推理 Part
                    ├─ tool-input-start / tool-call / tool-result / tool-error → 工具 Part
                    ├─ start-step / finish-step → 快照追踪 + token 计费
                    └─ 错误? → SessionRetry.retryable() 判断是否重试

SessionPrompt.loop() — 主循环详解

loop() 是整个系统的核心。它在 SessionPrompt.prompt() 中被调用,通过 start() 注册一个 AbortController 实现取消控制。关键循环逻辑:

  1. 读取消息流MessageV2.filterCompacted(MessageV2.stream(sessionID)) 反向遍历消息,跳过已压缩的历史
  2. 判断循环退出条件:如果最后一条 Assistant 消息的 finish 不是 "tool-calls""unknown",且其 ID 大于最后的 User 消息 ID,则退出循环
  3. 优先处理 pending 任务:检查最后一条消息的 parts 是否有 compactionsubtask 类型的 Part
  4. 上下文溢出检测:通过 SessionCompaction.isOverflow() 判断是否需要压缩
  5. 正常处理:创建 Assistant 消息、解析工具、调用 processor.process()

SessionProcessor.process() — 流式事件处理

SessionProcessor 是一个有状态的对象,持有当前 Assistant 消息、工具调用映射表和快照引用。process() 方法内部是一个 while(true) 循环:

async process(streamInput: LLM.StreamInput) {
  while (true) {
    const stream = await LLM.stream(streamInput)
    for await (const value of stream.fullStream) {
      abort.throwIfAborted()
      switch (value.type) {
        // 文本流 → 创建/追加 TextPart
        // 推理流 → 创建/追加 ReasoningPart
        // 工具调用 → 创建 ToolPart (pending → running → completed/error)
        // 步骤追踪 → Snapshot.track() + StepStartPart/StepFinishPart
      }
      if (needsCompaction) break  // 上下文溢出时中断
    }
    // 错误处理 → retryable? 继续循环 : 标记错误退出
    // 正常结束 → 返回 "continue" | "stop" | "compact"
  }
}

LLM.stream() — Vercel AI SDK 封装

LLM.stream() 是对 Vercel AI SDK streamText() 的封装,负责:

  1. 获取语言模型Provider.getLanguage(model) 返回 AI SDK 兼容的 LanguageModel
  2. 拼接系统提示:按优先级合并 Agent prompt、Provider 默认 prompt、用户自定义 system
  3. 参数合并管道basemodel.optionsagent.optionsvariant,层层覆盖
  4. 工具解析resolveTools() 根据用户禁用列表和权限规则过滤工具
  5. 调用 streamText():传入 wrapLanguageModel() 中间件进行消息格式转换
export async function stream(input: StreamInput) {
  const [language, cfg, provider, auth] = await Promise.all([
    Provider.getLanguage(input.model),
    Config.get(),
    Provider.getProvider(input.model.providerID),
    Auth.get(input.model.providerID),
  ])
  // ... system prompt 拼接、参数合并 ...
  return streamText({
    model: wrapLanguageModel({ model: language, middleware: [...] }),
    messages: [...system.map(x => ({ role: "system", content: x })), ...input.messages],
    tools,
    abortSignal: input.abort,
    maxRetries: input.retries ?? 0,
    // ... 其他参数
  })
}

Doom Loop 检测机制

SessionProcessor 内部实现了 Doom Loop 检测,防止 LLM 陷入重复调用同一工具的死循环:

const DOOM_LOOP_THRESHOLD = 3

// 在 tool-call 事件处理中
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,
    // ...
  })
}

检测条件:最近 3 次工具调用都是同一个工具名、同一份输入参数。触发后通过 PermissionNext.ask() 请求用户确认,用户可以选择”始终允许”该工具的 doom loop。

Subtask 执行模型

子任务(Subtask)不创建新 Session,而是在当前 Session 中创建新的 Assistant Message + Tool Part:

const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input) {
  // 1. 创建独立 assistant message(mode = task.agent)
  // 2. 创建 tool part(status: "running")
  // 3. 在同一 session 中执行 taskTool.execute()
  //    使用子 agent 的 permission 规则
  //    传入当前 session 的消息历史
})

关键设计点:

  • 子 Task 复用当前 Session 的消息历史,避免重复加载
  • 使用子 Agent 的 permission 规则(合并 Session 级 permission),而非主 Agent 的权限
  • 如果通过 /command 触发,完成后自动注入一条 summary User Message,让主 Agent 知道子任务的执行结果
  • Tool Part 的生命周期:pendingrunningcompleted/error,与其他工具调用一致

Plugin Hook 系统

Session 执行循环在多个环节注入 Plugin Hook,允许外部扩展:

Hook 点触发时机用途
chat.system.transform系统提示构建时转换/注入系统提示内容
chat.paramsLLM 参数传递前修改 temperature、maxTokens 等参数
tool.execute.before工具执行前拦截、记录、修改工具输入
tool.execute.after工具执行后后处理工具输出、审计日志
command.execute.before命令执行前拦截/修改命令参数
chat.message消息构造时转换/增强消息内容
shell.envShell 命令执行时注入环境变量

这些 Hook 使得 Plugin 可以在不修改核心代码的情况下介入执行流程。例如,一个审计 Plugin 可以通过 tool.execute.before 记录所有工具调用的输入参数,或通过 shell.env 为 Shell 命令注入项目特定的环境变量。

上下文压缩(Compaction)— 双层优化

压缩分为两个阶段,每个阶段都有精细的保护机制:

1. 溢出检测SessionCompaction.isOverflow()):

// overflow.ts
export function isOverflow(input) {
  const reserved = input.cfg.compaction?.reserved ?? Math.min(20_000, maxOutputTokens(model))
  const usable = input.model.limit.input
    ? input - reserved
    : context - maxOutputTokens(model)
  return count >= usable
}
  • 计算 input + output + cache.read + cache.write 总 token 数
  • 可用空间 = model.limit.input - reserved(reserved 默认取 min(20000, maxOutputTokens)
  • 当 token 用量 >= 可用空间时触发压缩

2. 消息裁剪SessionCompaction.prune())— 双层保护:

第一层——最低阈值:

  • PRUNE_MINIMUM = 20,000 token,低于此值不触发裁剪
  • 避免对短对话执行无意义的裁剪操作

第二层——保护带:

  • PRUNE_PROTECT = 40,000 token,保护最近工具输出不被裁剪
  • PRUNE_PROTECTED_TOOLS = ["skill"],标记为受保护的工具输出永不 prune
  • 从最新消息向前遍历,跳过最近 2 轮对话

裁剪执行:

  • 超出部分的工具输出标记为 compacted(设置 state.time.compacted = Date.now()
  • 后续 toModelMessages() 将被裁剪的输出替换为 [Old tool result content cleared]
  • 每次 prompt loop 结束时异步执行(Effect.forkIn(scope)),不阻塞主循环

3. 摘要生成SessionCompaction.process()):

  • 使用 compaction Agent(禁止所有工具)
  • 将全部历史消息 + 摘要指令发送给 LLM
  • 生成的摘要消息标记 summary: true,后续 filterCompacted() 会以此为裁剪点
  • 如果是自动压缩且生成成功,会自动追加一条”Continue”消息继续对话

错误处理与边界条件

LLM 调用重试策略

SessionRetry 模块实现了完整的重试策略:

可重试错误分类retryable()):

  • APIErrorisRetryable === true:包括 429 限速、500 服务器错误
  • ContextOverflowError不重试,直接触发压缩
  • FreeUsageLimitError:返回提示信息,不重试
  • 响应体中包含 "exhausted" / "unavailable" / "rate_limit" 的错误

退避算法delay()):

  • 优先读取响应头 retry-after-ms(毫秒)或 retry-after(秒/HTTP 日期)
  • 无 header 时使用指数退避:2000ms * 2^(attempt-1),上限 30 秒
  • 有 header 时上限为 2^31 - 1(32 位有符号整数最大值)
export const RETRY_INITIAL_DELAY = 2000
export const RETRY_BACKOFF_FACTOR = 2
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000

SessionProcessor.process() 的 catch 块中:

const error = MessageV2.fromError(e, { providerID: input.model.providerID })
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
  attempt++
  const delay = SessionRetry.delay(attempt, ...)
  SessionStatus.set(sessionID, { type: "retry", attempt, message: retry, next: ... })
  await SessionRetry.sleep(delay, input.abort)
  continue  // 回到 while(true) 顶部重试
}

工具执行错误处理

工具执行失败时(tool-error 事件),处理器根据错误类型采取不同策略:

  • PermissionNext.RejectedError:用户拒绝了权限请求,根据配置决定是否中断循环
  • Question.RejectedError:用户拒绝了问题确认,同样根据配置决定
  • 其他错误:记录错误信息,工具状态设为 error,继续处理后续事件

循环结束后,所有 pendingrunning 状态的工具都会被标记为 "Tool execution aborted" 错误。

消息回退(Revert)— 完整机制

SessionRevert 模块使用 Effect Service 模式实现,提供三个操作。回退的核心在于精确恢复文件系统状态:

revert() 完整流程

  1. 遍历消息和 Parts,定位到用户指定的回退点
  2. 从回退点开始,向后收集所有后续的 Patch Parts(文件编辑操作)
  3. 捕获当前文件系统 Snapshot 作为 revert 基准点
  4. 应用 Patch 的反向操作:snap.revert(patches) 逆序撤销所有文件修改
  5. 将回退元数据(messageID、partID、snapshot、diff)写入 Session.revert 字段
// 核心逻辑简化
const revert = Effect.fn("SessionRevert.revert")(function* (input) {
  // 1. 定位回退点
  // 2. 收集后续 patches
  // 3. 捕获当前 snapshot
  // 4. snap.revert(patches) — 反向应用所有编辑
  // 5. 更新 session.revert 元数据
})
  • revert():回退到指定消息/Part,通过 Snapshot 恢复文件系统状态,计算 diff
  • unrevert():撤销回退,恢复到回退前的快照
  • cleanup():在下次对话开始时(prompt() 调用中)清理回退状态,删除回退点之后的所有消息

这种设计确保回退是可逆的——unrevert 可以精确恢复到回退前的状态,因为每一步都保存了快照和 diff。

Provider 不可用时的降级

当 Provider 认证失败时,MessageV2.fromError() 会生成 AuthErrorSessionRetry.retryable() 对认证错误返回 undefined(不可重试),循环直接终止。上下文溢出错误(ContextOverflowError)同样不重试,而是在 loop() 中通过 isOverflow() 检测后触发自动压缩。

Cost 精确计算

Session 中的费用计算有多个细节需要精确处理:

  • cache write tokens:从 anthropic、vertex、bedrock、venice 等多个来源的 Provider 特定字段中提取
  • cached tokens 去重:AI SDK v6 的 inputTokens 已包含 cached tokens,需要减去以避免重复计费
  • 超 200K 定价:使用 experimentalOver200K 字段的差异化定价
  • reasoning tokens:按 output 价格计费(而非单独定价)
  • 精度保证:使用 Decimal.js 进行精确的浮点计算,避免 JavaScript 原生浮点误差

Tool 调用 repair 机制

LLM 返回的工具调用名称可能存在大小写错误。experimental_repairToolCall 提供自动修复:

async experimental_repairToolCall(failed) {
  const lower = failed.toolCall.toolName.toLowerCase()
  // 大小写修复:将工具名转为小写匹配
  // 如果仍无法匹配 → 路由到 "invalid" 工具
  // "invalid" 工具返回友好的错误提示
}

这避免了因模型输出 Read_File 而非 read_file 导致的工具调用失败——系统会自动尝试大小写不敏感的匹配,再不行则通过 “invalid” 工具给出明确错误信息。

状态管理

InstanceState 缓存

Agent 列表通过 Instance.state() 实现懒加载缓存:

const state = Instance.state(async () => {
  const cfg = await Config.get()
  // ... 构建 Agent 列表 ...
  return result as Record<string, Agent.Info>
})

export async function get(agent: string) {
  return state().then((x) => x[agent])
}

Instance.state() 返回一个异步函数,首次调用时执行工厂函数并缓存结果。后续调用直接返回缓存值。缓存在 Instance 销毁时自动清理。

Session 状态三态

SessionStatus 通过 Effect Service 管理每个 Session 的状态:

export const Info = z.union([
  z.object({ type: z.literal("idle") }),
  z.object({ type: z.literal("busy") }),
  z.object({ type: z.literal("retry"), attempt: z.number(), message: z.string(), next: z.number() }),
])

状态变更通过 Bus.publish(Event.Status, ...) 广播,UI 层订阅此事件更新界面。

取消控制与精确中断

SessionPrompt.loop() 通过 start() 为每个 Session 注册一个 AbortController

function start(sessionID: string) {
  const controller = new AbortController()
  s[sessionID] = { abort: controller, callbacks: [] }
  return controller.signal
}

cancel() 调用 controller.abort() 中断所有进行中的 LLM 调用和工具执行。如果同一 Session 有排队的请求(callbacks),它们会被 reject。

AbortController 精确中断点:LLM.Service 的 stream 方法通过 Effect.acquireRelease 创建 AbortController,中断时执行精确的资源清理:

  1. 未完成的 snapshot:计算当前文件系统状态的 patch,保存已完成的编辑
  2. 未完成的 text:保存已接收但未写入的流式文本
  3. Reasoning parts:清理未完成的推理内容
  4. 工具标记:所有 running / pending 状态的工具标记为 error("Tool execution aborted")

这种细粒度的中断处理确保了取消操作不会留下不一致的状态——未完成的编辑被回滚或保存,工具调用被优雅终止。

调用链示例

链路 1:用户发送消息 → LLM 调用 → 工具执行 → 响应渲染

1. SessionPrompt.prompt({ sessionID, parts: [{ type: "text", text: "读取 main.ts" }] })
   ├─ createUserMessage() → 写入 User 消息 + TextPart 到 Storage
   └─ loop(sessionID)
      ├─ start() → 注册 AbortController
      ├─ MessageV2.filterCompacted(stream) → 加载未压缩的消息历史
      ├─ Agent.get("build") → 获取 build Agent 配置
      ├─ SessionProcessor.create() → 创建空的 Assistant 消息
      ├─ resolveSystemPrompt() → [header, agentPrompt + environment + custom]
      ├─ resolveTools() → 注册 ReadTool, BashTool, EditTool 等内置工具 + MCP 工具
      └─ processor.process()
         ├─ LLM.stream() → ai.streamText() 发起流式请求
         ├─ 流式事件:
         │  ├─ text-delta → 写入 TextPart, Bus 广播 PartUpdated
         │  ├─ tool-input-start → 创建 ToolPart (status: "pending")
         │  ├─ tool-call → 更新 ToolPart (status: "running", input: { filePath: "main.ts" })
         │  ├─ ReadTool.execute() → 读取文件内容
         │  └─ tool-result → 更新 ToolPart (status: "completed", output: 文件内容)
         └─ 返回 "continue" (finish reason 不是 tool-calls)

2. UI 层通过 Bus 订阅 MessageV2.Event.PartUpdated 实时渲染
3. loop() 下一次迭代检测到 Assistant 已完成 → break → 返回最终消息

链路 2:上下文压缩触发 → 摘要生成 → 消息裁剪

1. processor.process() 中 finish-step 事件:
   ├─ Session.getUsage() → 计算 token 用量: { input: 180000, output: 8000, ... }
   ├─ SessionCompaction.isOverflow() → 180000 + 8000 >= 190000 → true
   └─ needsCompaction = true → break 中断流式处理

2. process() 返回 "compact" → loop() 继续
3. loop() 下一次迭代:
   ├─ 检测到 isOverflow() → true
   └─ SessionCompaction.create() → 写入 User 消息 + CompactionPart

4. loop() 再次迭代:
   ├─ 检测到 pending compaction task
   └─ SessionCompaction.process()
      ├─ Agent.get("compaction") → 获取压缩专用 Agent
      ├─ 创建 Assistant 消息 (mode: "compaction", summary: true)
      ├─ SessionProcessor.create()
      └─ processor.process()
         ├─ 历史消息 + 摘要指令 → LLM 生成结构化摘要
         │  (Goal / Instructions / Discoveries / Accomplished / Relevant files)
         └─ 返回 "continue"

5. 后续 filterCompacted() 以 summary:true 的消息为裁剪点
6. SessionCompaction.prune() → 清理旧工具输出(保留最近 40000 token)

设计取舍

为什么 Agent 是配置而不是类?

TypeScript 版本将 Agent 从 Go 版本的”接口 + 实现”模式改为纯配置对象。原因:

  1. 消除代码重复:Go 版本中 Coder Agent 和 Task Agent 的执行循环几乎相同,只是工具集不同。TS 版本中所有 Agent 共享 SessionPrompt.loop()SessionProcessor.process() 的执行路径
  2. 声明式组合:权限通过 PermissionNext.merge() 组合,工具通过 ToolRegistry.enabled() 过滤,模型/温度通过配置覆盖——无需子类化
  3. 运行时可扩展generate() 方法可以通过 LLM 动态创建新 Agent,用户配置中可以 disable 内置 Agent 或添加自定义 Agent

为什么用 Vercel AI SDK?

ai 包(Vercel AI SDK)提供了统一的 streamText() API,屏蔽了不同 Provider(Anthropic、OpenAI、Google 等)的协议差异。OpenCode 在此基础上通过 wrapLanguageModel() 中间件注入自己的消息转换逻辑(ProviderTransform.message()),以及通过 ProviderTransform.providerOptions() 处理各 Provider 特有的参数格式。

为什么 prompt.ts 如此庞大(580 行)?

prompt.ts 承担了 Go 版本中分散在 agent.gotools.gosession.go 中的职责:

  • 用户消息构造(文件读取、Agent 引用解析、目录列表)
  • 工具解析和注册(内置工具 + MCP 工具 + 权限过滤)
  • 子任务(Subtask)直接执行
  • 命令(/command)模板展开
  • Shell 命令执行
  • 标题自动生成

与其他模块的关系

  • Providersrc/provider/):LLM.stream() 通过 Provider.getLanguage() 获取 AI SDK 兼容的 LanguageModel;通过 Provider.getModel() 获取模型元数据(上下文窗口、价格等)
  • Configsrc/config/):Agent 列表的 state() 读取 Config.get() 获取用户自定义 Agent 配置和权限覆盖
  • Permissionsrc/permission/):resolveTools() 调用 PermissionNext.disabled() 过滤被禁用的工具;Doom Loop 检测通过 PermissionNext.ask() 请求用户确认
  • Storagesrc/storage/):所有消息、Part、Session 通过 Storage.write/update/read 持久化
  • Bussrc/bus/):消息更新、Part 更新、状态变更都通过 Bus 广播,UI 层订阅这些事件驱动渲染
  • Snapshotsrc/snapshot/):SessionProcessor 在每个步骤(step-start/step-finish)追踪文件系统快照,用于 diff 计算和回退
  • ToolRegistrysrc/tool/):resolveTools()ToolRegistry.tools() 获取内置工具列表,通过 ToolRegistry.enabled() 合并 Agent 的工具启用配置
  • MCPsrc/mcp/):resolveTools()MCP.tools() 获取外部工具,包装为 AI SDK tool() 格式
  • Pluginsrc/plugin/):在 system prompt 构建、工具执行前后、聊天参数等多个环节提供 hook 点
  • Instancesrc/project/):Instance.state() 提供 Agent 列表缓存的生命周期管理;Instance.directory/worktree 提供工作目录信息