单轮 vs 多轮:架构层面的差异
- 单轮(一次 Agentic Loop):
query()函数的一次完整执行——组装上下文 → 调 API → 处理工具调用 → 循环直到结束 - 多轮(一个 Session):
QueryEngine类管理的一次会话——跨越数十轮submitMessage()调用,持续数小时
QueryEngine(src/QueryEngine.ts:186)是单轮 Agentic Loop 之上的会话编排器,它管理的状态远不止消息列表:
QueryEngine 的核心方法:submitMessage()
每次用户输入一条消息,REPL 或 SDK 调用submitMessage(),它会执行完整的 turn 初始化链路:
submitMessage() 是 async *Generator——它逐步 yield SDKMessage,让调用方(REPL/SDK)能实时展示进度,而不是等整个 turn 结束。
会话持久化:JSONL Transcript
每次对话事件都被追加写入 transcript 文件(src/utils/sessionStorage.ts):
存储路径
project-hash由getProjectDir(originalCwd)生成,同一项目目录的会话归入同一子目录- 每条记录是一行 JSON(JSONL 格式),支持追加写入而不需要读取-修改-写入整个文件
- 读取上限为 50MB(
MAX_TRANSCRIPT_READ_BYTES),防止超大会话导致 OOM
Transcript 写入器
TranscriptWriter(src/utils/sessionStorage.ts:1200+)是一个写队列,确保并发的消息追加不会互相覆盖:
会话恢复链路
--resume 参数触发的恢复流程(src/main.tsx:3620+):
成本追踪:从 API Usage 到美元
成本追踪贯穿三个模块,形成完整的记录→累计→展示链路:记录层:API 响应中的 Usage
每个message_delta 事件携带 usage 字段(input_tokens、output_tokens、cache_creation_input_tokens、cache_read_input_tokens)。accumulateUsage() 将增量 usage 累加到会话总量。
累计层:cost-tracker.ts
addToTotalSessionCost() 根据模型定价计算每次 API 调用的费用,累计到 totalCostUSD。按模型的 ModelUsage 支持在同一会话中切换模型后分别统计。
持久化:跨重启保留
预算熔断
QueryEngineConfig.maxBudgetUsd 提供了会话级的硬性预算上限。在 REPL 中,当累计费用超过 $5 时(src/screens/REPL.tsx:2208),弹出费用提醒对话框——这不是硬性阻断,而是”软提醒”。
模型热切换
在一个会话中切换模型不会丢失对话历史——因为mutableMessages 与模型选择是解耦的:
contextWindowTokens 和 maxOutputTokens 也会根据新模型的规格重新计算——例如从 Sonnet 切换到 Opus 时,上下文窗口可能从 200K 变为 1M。
文件快照与回滚
fileHistoryMakeSnapshot()(src/utils/fileHistory.ts)在 AI 每次修改文件前自动保存当前内容。快照绑定到具体的 message.id,使得 --rewind-files <user-message-id> 可以精确恢复到对话中任意时间点的文件状态——这比 git 更细粒度(git 只追踪已提交的内容)。