数据截至 (上游 commit a5ac26f27989)
会话编排与安全护栏(SessionRuntime)
本章讲什么: 01 章的
AgentRuntime是无状态的 —— 它不记得上一轮。那「记忆」和「别让 agent 失控」由谁管?答案是SessionRuntime。这章讲它怎么把无状态内核包成有记忆的会话,以及两道护栏:循环检测(老调同一个工具)和连错熔断(连续犯错)。
1. 它要解决的小问题
AgentRuntime 每次 run() 都从给定消息开始、跑完就忘。但真实会话需要:
- 跨轮记忆 —— 用户第二句话要能接上第一句的上下文。
- 失控防护 —— agent 可能陷进死循环(反复
read_files同一个文件),或连续犯错(每次工具都失败)。
SessionRuntime(sdk/packages/core/src/runtime/orchestration/session-runtime-orchestrator.ts:304)就是为这两件事存在的「每会话编排器」。
2. 核心思路:有状态的壳,无状态的核
关键设计(文件顶部注释写得很清楚,session-runtime-orchestrator.ts:1-20):所有跨轮状态活在 SessionRuntime 里,每一轮新造一个一次性的 AgentRuntime。
SessionRuntime(活整个会话)
├─ ConversationStore 消息历史(跨轮)
├─ MistakeTracker 连续犯错计数器(跨轮)
├─ LoopDetectionTracker 重复工具调用检测器(跨轮)
├─ MessageBuilder 供应商消息组装缓存
└─ 每个 run ──▶ 新建 AgentRuntime(无状态)
把完整历史当 initialMessages 喂进去,跑完丢弃
为什么每轮新建? 注释里点明:让 OAuth 重试、run 重放变得可行 —— 因为内核不持有状态,重跑一遍是干净的。会话级状态「活得比任何一个 AgentRuntime 都久」。
看 executeRunInternal(session-runtime-orchestrator.ts:769)的主干:它先把用户消息存进 ConversationStore,然后把整个历史作为 initialMessages 传给新建的 runtime,跑完再存回(session-runtime-orchestrator.ts:870-876)。注释还专门记了一个修过的 P1 bug:之前没传 seed 导致历史被 replaceMessages(runResult.messages) 覆盖丢失。
3. 护栏一:循环检测
问题: agent 有时会鬼打墙 —— 一模一样的工具调用连发好几次,白烧 token。
检测逻辑(LoopDetectionTracker,sdk/packages/core/src/runtime/safety/loop-detection.ts:125):给每个工具调用算一个「签名」(工具名 + 排序后序列化的参数,toolCallSignature 在 loop-detection.ts:50),数连续相同的次数:
// 示意,非源码 —— 提炼自 loop-detection.ts:66-87 checkRepeatedToolCall
if (toolName === last.name && signature === last.signature) {
count++; // 和上次一模一样 → 累加
} else {
count = 1; // 不一样 → 归零重数
}
return {
softWarning: count === softThreshold, // 默认 3:温柔提醒
hardEscalation: count >= hardThreshold, // 默认 5:硬熔断
};
两级反应(默认阈值 soft=3 / hard=5,loop-detection.ts:113):
| 级别 | 触发 | 反应 |
|---|---|---|
soft | 连续 3 次相同 | 往对话里塞一句「检测到重复,换个思路试试」的恢复提示,不阻断 |
hard | 连续 ≥5 次相同 | 喂给连错熔断器(forceAtLimit),触发 abort |
4. 护栏二:连错熔断
问题: agent 可能连续犯错 —— 每一轮工具都失败,再转下去也是浪费。
计数逻辑(MistakeTracker,sdk/packages/core/src/runtime/safety/mistake-tracker.ts:80):维护 consecutiveMistakes 计数,到上限(默认 6,session-runtime-orchestrator.ts:430)就停。但停之前给一个逃生口 —— onLimitReached 回调可以裁决「继续(给条指导)还是停」(mistake-tracker.ts:124 调 resolveConsecutiveMistakeDecision,定义在 :198):
- 裁决
continue+ 有guidance→ 把指导塞进对话当恢复提示,计数清零,接着跑。 - 裁决
stop(默认无回调时)→ 生成一句解释性停止消息(buildMistakeLimitStopMessage,mistake-tracker.ts:157),结束 run。
什么算一次「错」? 在 turn-finished 时判定(session-runtime-orchestrator.ts:1196):当本轮有工具失败、且没有任何工具成功,记一次错;只要有一个工具成功就 reset() 清零 —— 避免无关的偶发失败累积。
5. 两道护栏怎么挂上去:事件 + 序列化队列
护栏不是写在内核循环里的,而是订阅内核事件挂上去的。SessionRuntime 订阅 AgentRuntime 的事件流(session-runtime-orchestrator.ts:908 runtime.subscribe),在 handleRuntimeEvent(:897)里:
tool-started ──▶ inspectLoopForToolCall(...) 循环检测在这里跑
tool-finished ──▶ 记录成功/失败计数
turn-finished ──▶ 全失败无成功?→ enqueueMistakeRecord(...)
有个并发细节值得看:内核事件流是同步的,但 MistakeTracker.record() 是异步的。所以护栏的副作用被串进一个序列化的 promise 链 activeTrackerWork(session-runtime-orchestrator.ts:1271 enqueueMistakeRecord),executeRun 在返回结果前会 await 它排空(:836)。这样既保证顺序(legacy 行为),又能让一个迟到的 abort 仍然送达内核。当熔断决定 stop 时,它把停止消息塞进对话并调 activeRuntime?.abort(...)(:1253)。
6. 巧妙之处
- 状态分层干净:无状态内核 + 有状态壳,使「每轮重建」成为可能,直接换来 OAuth 重试 / run 重放能力(
:1-20注释)。 - 护栏是观察者,不是侵入:循环检测和连错熔断完全靠订阅事件实现,内核对它们一无所知 —— 想加新护栏只需再订阅一个事件。
- 熔断带逃生口:不是粗暴地一到上限就死,
onLimitReached能注入「再给一次机会 + 指导」(mistake-tracker.ts:121)。 - abort 防误杀守护进程:
abort()里那段长注释(:553-600)记录了一个真实坑 —— hub 模式下取消运行导致的 promise rejection 曾被误判为崩溃,用一个.catch(() => {})安全观察者解决。
7. 边界与局限
- 循环检测只看连续完全相同的调用 —— 参数稍有变化(哪怕语义一样)就不算重复,绕得过去。
- 连错只在「整轮全失败」时计数,部分失败 + 部分成功不计 —— 这是为了避免误伤,但也意味着「一半工具一直失败」可能不触发熔断。
SessionRuntime同一时刻只允许一个 run(canStartRun(),:449),并发请求要靠上层排队。
8. 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 会话编排器 | sdk/packages/core/src/runtime/orchestration/session-runtime-orchestrator.ts | SessionRuntime、executeRunInternal |
| 事件处理 | sdk/packages/core/src/runtime/orchestration/session-runtime-orchestrator.ts | handleRuntimeEvent、enqueueMistakeRecord |
| 循环检测 | sdk/packages/core/src/runtime/safety/loop-detection.ts | LoopDetectionTracker、checkRepeatedToolCall、toolCallSignature |
| 连错熔断 | sdk/packages/core/src/runtime/safety/mistake-tracker.ts | MistakeTracker、buildMistakeLimitStopMessage |
| 对话存储 | sdk/packages/core/src/session/stores/conversation-store.ts | ConversationStore |