@x-otto/agent 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["logger"],"sources":["../src/errors.ts","../src/ports.ts","../src/engine-nodes.ts","../src/debug-port.ts","../src/memory-lifecycle.ts","../src/tool-result-clamp.ts","../src/context-pipeline.ts","../src/image-degradation.ts","../src/vision-gate.ts","../src/progress-window.ts","../src/stall-detector.ts","../src/trace.ts","../src/pipeline.ts","../src/trace-views.ts","../src/lifecycle-async-tool.ts","../src/tool-turn-runner.ts","../src/work-loop-stream-error.ts","../src/work-loop-turn-outcome.ts","../src/work-loop.ts","../src/tool-executor.ts","../src/agent.ts"],"sourcesContent":["import { AgentError } from '@x-otto/shared'\n\nexport class AbortError extends AgentError {\n constructor(message?: string) {\n super(message ?? 'Agent aborted', { code: 'AGENT_ABORTED' })\n this.name = 'AbortError'\n }\n}\n\nexport class MaxToolTurnsError extends AgentError {\n readonly maxTurns: number\n readonly partialText: string\n readonly extensions: number\n\n constructor(maxTurns: number, partialText = '', extensions = 0) {\n const effective = maxTurns * (1 + extensions)\n super(\n extensions > 0\n ? `Max tool turns exceeded: ${effective} (base ${maxTurns} × ${1 + extensions} extensions)`\n : `Max tool turns exceeded: ${maxTurns}`,\n { code: 'AGENT_MAX_TOOL_TURNS' },\n )\n this.name = 'MaxToolTurnsError'\n this.maxTurns = maxTurns\n this.partialText = partialText\n this.extensions = extensions\n }\n\n get effectiveMaxTurns(): number {\n return this.maxTurns * (1 + this.extensions)\n }\n}\n\n/**\n * RFC-104 D3:per-prompt token/时间预算超额(steer 收尾轮之后仍超才抛)。\n * 与 MaxToolTurnsError 并存:轮次触顶抛 MaxToolTurnsError(API 兼容),\n * token/时间超额抛本错误;边界层(run-modes)需对称 catch 两者(RFC-104 评审 A2)。\n */\nexport class PromptBudgetExceededError extends AgentError {\n readonly dimension: 'tokens' | 'time'\n readonly used: number\n readonly budget: number\n readonly partialText: string\n\n constructor(dimension: 'tokens' | 'time', used: number, budget: number, partialText = '') {\n super(\n dimension === 'tokens'\n ? `Prompt output-token budget exceeded: ${used} / ${budget} tokens`\n : `Prompt wall-clock budget exceeded: ${Math.round(used / 1000)}s / ${Math.round(budget / 1000)}s`,\n { code: 'AGENT_PROMPT_BUDGET_EXCEEDED' },\n )\n this.name = 'PromptBudgetExceededError'\n this.dimension = dimension\n this.used = used\n this.budget = budget\n this.partialText = partialText\n }\n}\n","import { randomUUID } from 'node:crypto'\n\nexport interface ClockPort {\n now(): number\n}\n\nexport interface RandomPort {\n random(): number\n}\n\nexport interface IdPort {\n uuid(): string\n}\n\nexport interface SleepPort {\n (ms: number, signal?: AbortSignal): Promise<void>\n}\n\nexport interface DeterminismPorts {\n clock: ClockPort\n random: RandomPort\n id: IdPort\n}\n\n/**\n * RFC-093:工具结果溢出落盘端口。宿主(runtime AgentSession)注入实现(闭包捕获 sessionId),\n * agent 包零存储依赖——与 approval 回调注入同型。\n * 返回磁盘路径(占位符引用);undefined = 未配置/写盘失败 → 调用方 fail-open 回退截断。\n */\nexport interface SpillPort {\n (toolCallId: string, text: string): Promise<string | undefined>\n}\n\nexport const systemClock: ClockPort = { now: () => Date.now() }\nexport const systemRandom: RandomPort = { random: () => Math.random() }\nexport const systemId: IdPort = { uuid: () => randomUUID() }\n\nexport const systemSleep: SleepPort = (ms, signal) =>\n new Promise<void>((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n","export type EngineNodeCategory =\n | 'agent_work_loop'\n | 'work_loop_injection'\n | 'tool_executor'\n | 'session_prompt_lifecycle'\n | 'lifecycle_hook'\n | 'turn_observable'\n\nexport interface EngineNodeDef {\n readonly id: string\n readonly category: EngineNodeCategory\n readonly label: string\n readonly pausable?: boolean\n readonly hook?: 'interceptor' | 'observer'\n readonly observable?: boolean\n readonly traceable?: boolean\n}\n\nexport const ENGINE_NODES = [\n {\n id: 'loop.start',\n category: 'agent_work_loop',\n label: 'Loop Start',\n pausable: true,\n traceable: true,\n },\n { id: 'model.before', category: 'agent_work_loop', label: 'Model Before', pausable: true },\n { id: 'model.after', category: 'agent_work_loop', label: 'Model After', pausable: true },\n { id: 'tool.before', category: 'agent_work_loop', label: 'Tool Before', pausable: true },\n { id: 'tool.after', category: 'agent_work_loop', label: 'Tool After', pausable: true },\n {\n id: 'loop.end',\n category: 'agent_work_loop',\n label: 'Loop End',\n pausable: true,\n traceable: true,\n },\n { id: 'loop.cleanup', category: 'agent_work_loop', label: 'Loop Cleanup', pausable: true },\n {\n id: 'agent.aborted',\n category: 'agent_work_loop',\n label: 'Agent Aborted',\n pausable: true,\n traceable: true,\n },\n {\n id: 'agent.error',\n category: 'agent_work_loop',\n label: 'Agent Error',\n pausable: true,\n traceable: true,\n },\n {\n id: 'agent.done',\n category: 'agent_work_loop',\n label: 'Agent Done',\n pausable: true,\n traceable: true,\n },\n {\n id: 'steering.check',\n category: 'work_loop_injection',\n label: 'Steering Check',\n pausable: true,\n },\n {\n id: 'follow.up.check',\n category: 'work_loop_injection',\n label: 'Follow Up Check',\n pausable: true,\n },\n {\n id: 'context.transform',\n category: 'work_loop_injection',\n label: 'Context Transform',\n pausable: true,\n },\n { id: 'tool.resolve', category: 'tool_executor', label: 'Tool Resolve', pausable: true },\n { id: 'tool.validate', category: 'tool_executor', label: 'Tool Validate', pausable: true },\n { id: 'tool.hook.before', category: 'tool_executor', label: 'Tool Hook Before', pausable: true },\n { id: 'tool.hook.after', category: 'tool_executor', label: 'Tool Hook After', pausable: true },\n {\n id: 'prompt.before',\n category: 'session_prompt_lifecycle',\n label: 'Prompt Before',\n pausable: true,\n traceable: true,\n },\n {\n id: 'prompt.after',\n category: 'session_prompt_lifecycle',\n label: 'Prompt After',\n pausable: true,\n traceable: true,\n },\n {\n id: 'context.build',\n category: 'session_prompt_lifecycle',\n label: 'Context Build',\n pausable: true,\n traceable: true,\n },\n {\n id: 'messages.persist',\n category: 'session_prompt_lifecycle',\n label: 'Messages Persist',\n pausable: true,\n traceable: true,\n },\n {\n id: 'memory.prune',\n category: 'work_loop_injection',\n label: 'Memory Prune',\n traceable: true,\n },\n {\n id: 'memory.archive',\n category: 'session_prompt_lifecycle',\n label: 'Memory Archive',\n traceable: true,\n },\n\n {\n id: 'chat.message.before',\n category: 'lifecycle_hook',\n label: 'Chat Message Before',\n hook: 'interceptor',\n },\n {\n id: 'chat.message.after',\n category: 'lifecycle_hook',\n label: 'Chat Message After',\n hook: 'interceptor',\n },\n { id: 'chat.params', category: 'lifecycle_hook', label: 'Chat Params', hook: 'interceptor' },\n {\n id: 'system.prompt.transform',\n category: 'lifecycle_hook',\n label: 'System Prompt Transform',\n hook: 'interceptor',\n },\n {\n id: 'messages.transform',\n category: 'lifecycle_hook',\n label: 'Messages Transform',\n hook: 'interceptor',\n },\n {\n id: 'tool.execute.before',\n category: 'lifecycle_hook',\n label: 'Tool Execute Before',\n hook: 'interceptor',\n },\n {\n id: 'tool.execute.after',\n category: 'lifecycle_hook',\n label: 'Tool Execute After',\n hook: 'interceptor',\n },\n {\n id: 'memory.prune.before',\n category: 'lifecycle_hook',\n label: 'Memory Prune Before',\n hook: 'interceptor',\n },\n { id: 'session.idle', category: 'lifecycle_hook', label: 'Session Idle', hook: 'observer' },\n { id: 'session.error', category: 'lifecycle_hook', label: 'Session Error', hook: 'observer' },\n\n { id: 'status.change', category: 'turn_observable', label: 'Status Change', observable: true },\n {\n id: 'turn.start',\n category: 'turn_observable',\n label: 'Turn Start',\n observable: true,\n traceable: true,\n },\n {\n id: 'turn.end',\n category: 'turn_observable',\n label: 'Turn End',\n observable: true,\n traceable: true,\n },\n {\n id: 'stream.event',\n category: 'turn_observable',\n label: 'Stream Event',\n observable: true,\n traceable: true,\n },\n {\n id: 'tool.call.start',\n category: 'turn_observable',\n label: 'Tool Call Start',\n observable: true,\n traceable: true,\n },\n {\n id: 'tool.call.end',\n category: 'turn_observable',\n label: 'Tool Call End',\n observable: true,\n traceable: true,\n },\n {\n id: 'steering.injected',\n category: 'turn_observable',\n label: 'Steering Injected',\n observable: true,\n },\n {\n id: 'follow.up.start',\n category: 'turn_observable',\n label: 'Follow Up Start',\n observable: true,\n },\n {\n id: 'max.turns',\n category: 'turn_observable',\n label: 'Max Tool Turns Reached',\n observable: true,\n traceable: true,\n },\n {\n // RFC-203 D2:滑动窗口停滞检出(软引导注入时 emit + record;每 prompt 至多一次)。\n id: 'progress.stall',\n category: 'turn_observable',\n label: 'Progress Stall Detected',\n observable: true,\n traceable: true,\n },\n {\n // RFC-232 D4:工具调用协议违约(stopReason='tool_use' 但响应无 tool_call block)。\n // 兜底 handler 触发时 record + emit(outcome=retry 注入纠正提示重跑;outcome=terminate\n // 重试耗尽干净终止)。observable+traceable 双开——traceable 供事后重建违约态复发率\n // (M2 gate 的数据源)、observable 供 TUI 实时 toast 提示用户(避免无声中断,G4)。\n id: 'tool.call.mismatch',\n category: 'turn_observable',\n label: 'Tool Call Protocol Mismatch',\n observable: true,\n traceable: true,\n },\n {\n // RFC-104 D3:per-prompt token/时间预算超额(steer 注入或即将硬停时 emit + record)。\n id: 'prompt.budget',\n category: 'turn_observable',\n label: 'Prompt Budget Exceeded',\n observable: true,\n traceable: true,\n },\n {\n id: 'compaction.reactive',\n category: 'turn_observable',\n label: 'Reactive Compaction',\n observable: true,\n traceable: true,\n },\n {\n // RFC-099 D6:每轮常态化投影 cap(静默,无 emit)。category 沿用 'turn_observable'\n // 仅表示\"属于 turn 级生命周期\",不代表 observable——本节点故意不设 observable:true\n // (D6 是无用户可见副作用的纯投影层,不应每轮产生事件噪音)。仅 traceable,\n // 供事后 trace 诊断\"这轮 cap 掉了几张图\"。终局审核发现此 category 命名易误导\n // (字面像 observable),保留 category 名不改(避免影响其他既有节点的分类语义),\n // 以此注释显式澄清。\n id: 'image.cap',\n category: 'turn_observable',\n label: 'Image Cap',\n traceable: true,\n },\n {\n // RFC-181 M1:视觉能力门控(模型不支持图像输入时的投影层拦截,见 vision-gate.ts)。\n // category 沿用 'turn_observable' 惯例(同 'image.cap' 注释——表示\"属于 turn 级\n // 生命周期\",不代表 observable)。只 traceable:诊断专用,供事后查\n // \"这轮有几张图被门控/委托成功几张\"。TUI toast(M3)直接消费 trace 而非\n // AgentEventType/emitter.emit——门控/委托是投影层纯函数副作用,不需要实时事件总线。\n id: 'image.vision-gate',\n category: 'turn_observable',\n label: 'Vision Capability Gate',\n traceable: true,\n },\n {\n // RFC-181 M3:视觉委托结果(仅委托端口被调用时触发,见 work-loop.ts 接线注释)。\n // 与 image.vision-gate 拆分为两个独立锚点(RFC-181 D6):前者标记门控本身,\n // 后者标记委托的实际结果(区分\"无委托能力\"与\"委托了但失败\")。observable:true——\n // 供 AgentSession→TUI 渲染\"N 张图已由 vision 模型代述\"一次性提示(M3 起有真实\n // 消费方)。\n id: 'image.vision-delegate',\n category: 'turn_observable',\n label: 'Vision Delegation Result',\n observable: true,\n traceable: true,\n },\n {\n // RFC-099 D3/D4/D7:413/400 兜底降级触发时 emit + record(双路,见\n // work-loop-stream-error.ts attemptImageDegradation)。TUI 消费方已接线(2026-07-21\n // 终局 review 补齐 image.degradation.notice → interactive-events.ts 的 toast,见\n // agent-session-event-forwarding.ts:114)——observable:true 有真实实时消费方,\n // traceable:true 供事后 trace 诊断。\n id: 'image.degradation',\n category: 'turn_observable',\n label: 'Image Degradation',\n observable: true,\n traceable: true,\n },\n {\n // RFC-115 追补:进程级内存采样落 trace(此前 MemoryGovernor 仅走 pino 日志,\n // 长会话事后无法重建内存曲线,需临时脚本外挂诊断——见 2026-07-11 诊断会话)。\n // category 用 'turn_observable' 仅表示\"随 turn 边界可关联\"的既有惯例(同\n // 'image.cap' 注释),本节点实际与 turn 无强绑定(MemoryGovernor 是进程级\n // 单例,采样时刻可能不落在任何活跃 turn 内)——record 时 turn 留空。\n // 只 traceable 不 observable:诊断专用,无用户可见实时副作用(同 'image.cap')。\n id: 'process.memory.sample',\n category: 'turn_observable',\n label: 'Process Memory Sample',\n traceable: true,\n },\n {\n id: 'stream.retry',\n category: 'turn_observable',\n label: 'Stream Retry',\n observable: true,\n traceable: true,\n },\n {\n id: 'stream.retry.accelerated',\n category: 'turn_observable',\n label: 'Stream Retry Accelerated',\n observable: true,\n traceable: true,\n },\n {\n id: 'turn.truncated',\n category: 'turn_observable',\n label: 'Turn Truncated',\n observable: true,\n traceable: true,\n },\n {\n // D1(RFC-074 M1-01):rate.limit.exhausted 由 work-loop-stream-error 同时 emit + record,\n // 此前只在 AgentEvent 联合声明、漏登记本表 → 进不了 OBSERVABLE/TRACEABLE 投影(观测/trace 口径错)。\n id: 'rate.limit.exhausted',\n category: 'turn_observable',\n label: 'Rate Limit Exhausted',\n observable: true,\n traceable: true,\n },\n {\n // 鉴权失败(本地无凭据/服务端401)——handleStreamError 的 fatal 分支此前对该场景\n // 零记录,用户看到 TUI 弹窗后无法回溯失效时间点/频率/涉及的 provider。\n id: 'stream.auth-failed',\n category: 'turn_observable',\n label: 'Stream Auth Failed',\n observable: true,\n traceable: true,\n },\n { id: 'error', category: 'turn_observable', label: 'Error', observable: true },\n { id: 'abort', category: 'turn_observable', label: 'Abort', observable: true },\n {\n // RFC-144 M1:lifecycleAsync 工具完成旁路通知(不参与 auto-trigger 控制流,\n // 仅供宿主感知)。\n id: 'lifecycle.tool.completed',\n category: 'turn_observable',\n label: 'Lifecycle Tool Completed',\n observable: true,\n },\n {\n // RFC-144 M4:lifecycleAsync 工具跨越 prompt 边界后完成——叠加于 lifecycle.tool.completed\n // 触发(非互斥,跨边界时两者都会 emit):完成通知**不会**落库注入 LLM 对话(避免污染\n // 新任务上下文),额外 emit 本事件供宿主决定如何呈现给用户(如 toast,接线归后续项)。\n id: 'lifecycle.tool.completed.crossBoundary',\n category: 'turn_observable',\n label: 'Lifecycle Tool Completed (Cross Prompt Boundary)',\n observable: true,\n },\n] as const satisfies readonly EngineNodeDef[]\n\nexport type EngineNode = (typeof ENGINE_NODES)[number]\n\ntype PausableNode = Extract<EngineNode, { pausable: true }>\ntype ObservableNode = Extract<EngineNode, { observable: true }>\ntype TraceableNode = Extract<EngineNode, { traceable: true }>\ntype LifecycleHookNode = Extract<EngineNode, { hook: 'interceptor' | 'observer' }>\n\nexport type BreakpointPoint = PausableNode['id']\nexport type ObservableNodeId = ObservableNode['id']\nexport type TraceableNodeId = TraceableNode['id']\nexport type LifecycleTiming = LifecycleHookNode['id']\n\n/**\n * RFC-199 D6:HookTiming ↔ ENGINE_NODES 编译期 Parity 守卫(单向子集断言)。\n *\n * 语义:ENGINE_NODES 中每个带 `hook:` 字段的节点 id 必须是 `@x-otto/hook-contracts` 里\n * 真实存在的 HookTiming——防止节点表登记虚构 timing(hook 注册/emit 运行时正常但\n * trace/断点双向盲区)。反方向不断言:HookTiming 的全集(25 种)含 orchestrator/runtime/coding\n * 宿主层 emit 的 task/process/notification/session/stream/compaction 类 timing(如\n * `session.created`/`stream.start`/`compaction.before` 均由 `@x-otto/runtime` 的\n * `SessionManager`/`AgentSession`/`agent-session-compaction` emit),本就不应全部登记进\n * 引擎节点表。参照 `_AgentEventObservableParity`(types.ts)模式;运行时的\n * preset-emit-alignment 测试保留为第二道防线。export 仅为消除 TS6196,不进公共 API。\n */\ntype _AssertSubset<A extends B, B> = A\nexport type _LifecycleTimingHookContractParity = _AssertSubset<\n LifecycleTiming,\n import('@x-otto/hook-contracts').HookTiming\n>\n\nconst NODES: readonly EngineNodeDef[] = ENGINE_NODES\n\nexport function nodesWithCapability(\n cap: 'pausable' | 'hook' | 'observable' | 'traceable',\n): readonly EngineNodeDef[] {\n return NODES.filter((node) => node[cap] !== undefined && node[cap] !== false)\n}\n\nexport const ENGINE_LIFECYCLE_TIMINGS: readonly LifecycleTiming[] = NODES.filter(\n (node) => node.hook !== undefined,\n).map((node) => node.id as LifecycleTiming)\n\nexport const OBSERVABLE_NODE_IDS: readonly ObservableNodeId[] = NODES.filter(\n (node) => node.observable === true,\n).map((node) => node.id as ObservableNodeId)\n\nexport const TRACEABLE_NODE_IDS: readonly TraceableNodeId[] = NODES.filter(\n (node) => node.traceable === true,\n).map((node) => node.id as TraceableNodeId)\n\nexport type BreakpointCategory = PausableNode['category'] | 'custom'\n\nexport interface BreakpointDefinition {\n point: BreakpointPoint\n name: string\n category: BreakpointCategory\n}\n\nexport const BREAKPOINT_POINTS: readonly BreakpointDefinition[] = NODES.filter(\n (node) => node.pausable === true,\n).map((node) => ({\n point: node.id as BreakpointPoint,\n name: node.label,\n category: node.category as BreakpointCategory,\n}))\n\nexport const ENGINE_DEFAULTS = {\n /** 工具轮次预算缺省(单源)。触顶后按 maxToolTurnExtensions 决定是否压缩续跑。\n * agent 核心 + runtime RUNTIME_DEFAULTS 均引用此处,消除散落的硬编码 `?? 120`。 */\n maxToolTurns: 120,\n /** tool.call.end trace 的 ToolResult 内容截断阈值(字符数)。默认 512。 */\n traceResultMaxChars: 512,\n /** R7:单条工具结果进模型上下文的字符上限(默认 ~100k≈25k token)。超出头尾保留+中段省略,\n * 防全仓 grep 等病态巨型结果撑爆 context window。仅截极端值,正常 read/edit 不受影响。 */\n maxToolResultChars: 100_000,\n /**\n * RFC-340 M3 D5-2:单条工具结果的**保底**字符份额。\n *\n * per-prompt 额度耗尽后,后续工具结果的有效预算不再递减到低于此值——否则模型会收到\n * 一堆只剩省略号的结果,比给一个头尾可辨的截断版更难判断刚才发生了什么。配合 clamp\n * 既有的头 70%/尾 30% 保留策略,2000 字符足以看清结果的开头与结尾。\n */\n minToolResultChars: 2_000,\n /**\n * RFC-340 M3 D5-4:单个 prompt 内所有工具结果可占用的上下文比例。\n *\n * 缺口背景:`maxToolResultChars` 是 **per-tool** 固定值,不跨工具累计——20 个工具各\n * 返回 99K 字符全部合法通过,但合计足以撑爆窗口,只能靠 `hardCeiling` 触发压缩事后\n * 补救(= 用丢对话历史偿还工具输出超发,代价错配)。本比例给出 per-prompt 总闸。\n *\n * 取窗口的一半:留另一半给系统前缀 + 对话历史 + 模型输出。按窗口派生而非魔数,\n * 小窗口模型自动收紧,无需逐模型调参。\n */\n promptToolResultFraction: 0.5,\n /** 单 turn 内可并行工具(readonly 或 isConcurrencySafe)的并发上限。\n * 对齐 CC 的 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 默认 10——防一轮几十个委托/读取压垮资源。 */\n maxToolConcurrency: 10,\n /** RFC-104 D1:进展闸门的签名多样性阈值——纯读窗口 distinct 签名占比 ≥ 此值判「有进展」。\n * 0.5 保守起步(同一调用反复 → ratio→0 → 循环嫌疑;研究型多样 grep → ratio→1)。 */\n progressDistinctRatio: 0.5,\n /** RFC-104 D1:进展窗口签名 Map 上限;溢出只计 totalCalls(ratio 退化方向=更宽松,不误杀)。 */\n progressSignatureCap: 512,\n /** RFC-104 D1:签名对 args stringify 的截断长度(大 args 如 write 全文件内容只 hash 头部;\n * 截断只影响签名精度且方向是「更易判相同」=更严格,不影响语义)。 */\n progressSignatureArgsMaxChars: 1024,\n /** RFC-203 D1:早期停滞检测的滑动窗口大小(turn 数)——窗口未满恒不判停。 */\n stallWindowTurns: 8,\n /** RFC-203 D1(评审 H3):同一工具调用签名在窗口内重复 ≥ 此值且零写成功 → 判停滞。\n * 不用 distinctRatio(会误伤\"反复 read 同几个文件各 2-3 次\"的合法审查),maxRepeat\n * 只命中\"同一件事反复做 N 次以上\"的真打转。 */\n stallRepeatThreshold: 4,\n} as const\n","import type { ToolResultMessage } from '@x-otto/interchange'\nimport { AbortError } from './errors'\nimport { systemClock } from './ports'\nimport type { AgentMessage } from './types'\n\nexport { BREAKPOINT_POINTS } from './engine-nodes'\nexport type { BreakpointCategory, BreakpointDefinition, BreakpointPoint } from './engine-nodes'\nimport type { BreakpointPoint } from './engine-nodes'\n\nexport interface MessageSummaryItem {\n index: number\n role: 'user' | 'assistant' | 'tool_result' | 'system'\n preview: string\n toolName?: string\n tokenEstimate?: number\n}\n\nexport interface DebugSnapshot {\n turn: number\n point: BreakpointPoint\n frameDepth: number\n messagesCount: number\n lastToolName?: string\n tokenUsage?: { input: number; output: number }\n metadata?: Record<string, string | number | boolean | null>\n systemPrompt?: string\n messagesSummary?: MessageSummaryItem[]\n llmParams?: {\n temperature?: number\n maxTokens?: number\n thinkingLevel?: string\n }\n}\n\nexport interface InjectedMessage {\n // role 'system' 会被 consumePause 注入循环静默丢弃(仅注入 user);保留 union 因 API 入口\n // (service/routes/debugger) 接受外部传入的 system 消息,由 consumePause 决定忽略。\n role: 'user' | 'system'\n content: string\n}\n\nexport interface PauseResumePayload {\n systemPrompt?: string\n injectMessages?: InjectedMessage[]\n removeMessageIndices?: number[]\n llmParams?: {\n temperature?: number\n maxTokens?: number\n thinkingLevel?: string\n }\n metadata?: Record<string, string | number | boolean | null>\n}\n\nexport type DebugCommand =\n | { type: 'next'; seq: number }\n | { type: 'step'; seq: number }\n | { type: 'over'; seq: number; depth: number }\n | { type: 'continue'; seq: number }\n | { type: 'stop'; seq: number; reason?: string }\n\nexport interface PauseResult {\n pauseId: string\n command: DebugCommand\n payload: PauseResumePayload | null\n}\n\nexport interface DebugPort {\n pause(snapshot: DebugSnapshot): Promise<PauseResult | undefined>\n}\n\nexport function summarizeMessages(\n messages: readonly AgentMessage[],\n lastN: number,\n): MessageSummaryItem[] {\n const start = Math.max(0, messages.length - lastN)\n return messages.slice(start).map((msg, i) => {\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)\n return {\n index: start + i,\n role: msg.role as MessageSummaryItem['role'],\n preview: content.slice(0, 200),\n toolName: msg.role === 'tool_result' ? (msg as ToolResultMessage).toolName : undefined,\n tokenEstimate: Math.ceil(content.length / 4),\n }\n })\n}\n\nexport function buildSnapshot(\n partial: Partial<DebugSnapshot> & { point: BreakpointPoint },\n): DebugSnapshot {\n return {\n turn: 0,\n frameDepth: 0,\n messagesCount: 0,\n tokenUsage: { input: 0, output: 0 },\n metadata: {},\n systemPrompt: '',\n messagesSummary: [],\n llmParams: {},\n ...partial,\n }\n}\n\n/**\n * RFC-118 S15:pause 闭包工厂——收敛 work-loop.ts 三处(workLoop/runTurn/runTools)\n * copy-paste 的 debug?.pause(buildSnapshot({...})) 模式。差异经 getters 参数化:\n * turn/messagesCount/tokenUsage 是随循环推进的可变量(用 getter 惰性取值),\n * llmParams/systemPrompt 仅前两处需要(可选 getter,缺省不进快照)。\n */\nexport interface PauseControllerDeps {\n debug?: DebugPort\n getTurn: () => number\n getMessagesCount: () => number\n getTokenUsage: () => { input: number; output: number } | undefined\n /** 提供时快照携带 systemPrompt + llmParams(workLoop/runTurn 场景)。 */\n getLlmParams?: () => {\n systemPrompt?: string\n temperature?: number\n maxTokens?: number\n thinkingLevel?: string\n }\n /** 提供时快照携带 messagesSummary(workLoop/runTurn 场景)。 */\n getMessagesForSummary?: () => AgentMessage[]\n}\n\nexport interface PauseOptions {\n messagesCount?: number\n summarySource?: AgentMessage[]\n lastToolName?: string\n metadata?: DebugSnapshot['metadata']\n}\n\nexport function createPauseController(deps: PauseControllerDeps) {\n return (\n point: BreakpointPoint,\n frameDepth: number,\n options: PauseOptions = {},\n ): Promise<PauseResult | undefined> | undefined => {\n const llm = deps.getLlmParams?.()\n const summaryMessages = options.summarySource ?? deps.getMessagesForSummary?.()\n return deps.debug?.pause(\n buildSnapshot({\n turn: deps.getTurn(),\n point,\n frameDepth,\n messagesCount: options.messagesCount ?? deps.getMessagesCount(),\n lastToolName: options.lastToolName,\n tokenUsage: deps.getTokenUsage() ?? { input: 0, output: 0 },\n metadata: options.metadata ?? {},\n ...(llm\n ? {\n systemPrompt: llm.systemPrompt ?? '',\n llmParams: {\n temperature: llm.temperature,\n maxTokens: llm.maxTokens,\n thinkingLevel: llm.thinkingLevel,\n },\n }\n : {}),\n ...(summaryMessages ? { messagesSummary: summarizeMessages(summaryMessages, 10) } : {}),\n }),\n )\n }\n}\n\nexport interface PauseApplyTarget {\n messages?: AgentMessage[]\n setSystemPrompt?: (value: string) => void\n setTemperature?: (value: number) => void\n setMaxTokens?: (value: number) => void\n setThinkingLevel?: (value: string) => void\n now?: () => number\n}\n\nexport function consumePause(result: PauseResult | undefined, target?: PauseApplyTarget): void {\n if (result?.command.type === 'stop') {\n throw new AbortError('Stopped by debugger')\n }\n\n const payload = result?.payload\n if (!payload || !target) {\n return\n }\n\n if (payload.systemPrompt !== undefined) {\n target.setSystemPrompt?.(payload.systemPrompt)\n }\n\n if (target.messages && payload.removeMessageIndices?.length) {\n const sorted = [...payload.removeMessageIndices].sort((a, b) => b - a)\n for (const idx of sorted) {\n if (idx >= 0 && idx < target.messages.length) {\n target.messages.splice(idx, 1)\n }\n }\n }\n\n if (target.messages && payload.injectMessages?.length) {\n const now = target.now ?? systemClock.now\n for (const msg of payload.injectMessages) {\n if (msg.role === 'user') {\n target.messages.push({ role: 'user', content: msg.content, timestamp: now() })\n }\n }\n }\n\n if (payload.llmParams) {\n if (payload.llmParams.temperature !== undefined) {\n target.setTemperature?.(payload.llmParams.temperature)\n }\n if (payload.llmParams.maxTokens !== undefined) {\n target.setMaxTokens?.(payload.llmParams.maxTokens)\n }\n if (payload.llmParams.thinkingLevel !== undefined) {\n target.setThinkingLevel?.(payload.llmParams.thinkingLevel)\n }\n }\n}\n","import type { TraceableNodeId } from './engine-nodes'\nimport type { TraceRecorder } from './trace'\n\n/**\n * 记忆子系统生命周期(原设计稿 docs/design/memory-lifecycle.md 已随 RFC-067 M123-03 文档收敛\n * 删除,内容并入 packages/memory/ARCHITECTURE.md,见 commit 5d6a9f5b)。\n *\n * P1(最小可观测):仅录 trace —— 让裁剪/持久化在 inspector/replay/OTel 现形。\n * 后续 P2 把本模块扩展为多通道 sink(同一事件再扇出 emitter(AgentEvent) + hooks 干预面),\n * P3 收编 compaction.* 把压缩三路归一。当前仅 prune/archive 接线(compact 暂留 compaction.*;\n * load/update 属其它层,待 P1b)。\n */\nexport type MemoryPhase = 'load' | 'prune' | 'compact' | 'archive' | 'update'\n\nexport type MemoryTrigger = 'startup' | 'in_loop' | 'reactive_overflow' | 'manual' | 'tool'\n\nexport interface MemoryMetrics {\n prunedCount?: number\n tokensSaved?: number\n messagesBefore?: number\n messagesAfter?: number\n archivePath?: string\n archivedCount?: number\n}\n\nexport interface MemoryLifecycleEvent {\n phase: MemoryPhase\n sessionId: string\n turn?: number\n trigger: MemoryTrigger\n metrics: MemoryMetrics\n}\n\n/**\n * MemoryPhase → 已登记的 traceable 锚点(编译期绑回 ENGINE_NODES,取代 `memory.${phase}` 模板拼接)。\n * 仅 prune/archive 当前有 trace 锚点;load/compact/update 暂无(compact 走 compaction.* 观测面,\n * load/update 属其它层,待 P1b 接线)——映射缺位即不录,绝不产生未登记的 `memory.*` 漂移节点。\n */\nconst PHASE_TRACE_NODE: Partial<Record<MemoryPhase, TraceableNodeId>> = {\n prune: 'memory.prune',\n archive: 'memory.archive',\n}\n\n/** P1:把一条记忆生命周期事件录成对应 trace 锚点(node 由 PHASE_TRACE_NODE 编译期约束为 traceable)。 */\nexport function recordMemoryLifecycle(recorder: TraceRecorder, event: MemoryLifecycleEvent): void {\n const node = PHASE_TRACE_NODE[event.phase]\n if (node === undefined) return\n recorder.record({\n kind: 'lifecycle',\n node,\n turn: event.turn,\n payload: { trigger: event.trigger, sessionId: event.sessionId, ...event.metrics },\n })\n}\n","/**\n * 工具结果内容钳制与有界并行执行原语。\n */\nimport type { ToolResultMessage } from '@x-otto/interchange'\nimport { PROVIDER_IMAGE_INLINE_MAX_BYTES } from '@x-otto/shared'\nimport type { SpillPort } from './ports'\n\n/**\n * RFC-151 D2:image 块内联字节上限(5MB)。超限时走 spill 落盘或占位符丢弃。\n * 跨包单源 @x-otto/shared PROVIDER_IMAGE_INLINE_MAX_BYTES(C 组收敛,2026-08-09)——re-export\n * 保持本包公共 API 名不变(消费方/测试 import 自本包)。底层是 provider 单图输入字节约束,\n * 与 fs/read、mcp、clipboard 同源;shared 常量为纯字面量(不经 env),保证 agent 引擎确定性\n * 重放不受环境影响(RFC-192)。\n */\nexport const MAX_IMAGE_INLINE_BYTES = PROVIDER_IMAGE_INLINE_MAX_BYTES\n\n/**\n * 剥离 ANSI 转义序列——完整 5 道正则(与 `@x-otto/tui` message-lines.ts 同源同构)。\n * 本包独立维护不引 tui——agent 不应反向依赖 TUI 包。\n *\n * 道 1: OSC 序列(超链接/标题等)\n * 道 2: 完整 CSI 序列(颜色/光标等,必须有终止字节)\n * 道 3: 截断的 ESC 前缀(无终止字节的 CSI 碎片,如 `\\x1b[38;2;86;182`)\n * 道 4: 孤儿多参数 SGR 尾(无 ESC 前缀,上游 byte-slice 产物,如 `;194m` / `38;2;86;182;194m`)\n * 道 5: 孤儿 truecolor 参数(精确匹配 38;2;R;G;Bm / 48;2;R;G;Bm 无 ESC[,英文文本假阳性极低)\n */\nexport function stripAnsi(text: string): string {\n return text\n .replace(/\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)/g, '') // 1: OSC sequences\n .replace(/\\x1b\\[[\\d;?]*[ -/]*[@-~]/g, '') // 2: complete CSI sequences\n .replace(/\\x1b\\[[\\d;?]*(?![\\d;@-~])/g, '') // 3: truncated ESC prefix (no final byte)\n .replace(/(?:3[89]|4[0-9]);2;\\d{1,3};\\d{1,3};\\d{1,3}m/g, '') // 4: orphaned truecolor (38;2;R;G;Bm / 48;2;R;G;Bm)\n .replace(/(?:3[89]|4[0-9]);5;\\d{1,3}m/g, '') // 5: orphaned 256-color (38;5;Nm / 48;5;Nm)\n .replace(/(?:^|(?<=[\\s;m]))(?:\\d{1,3}(?:;\\d{1,3})+|;\\d{1,3})m/gi, '') // 6: remaining orphaned multi-param SGR tails\n}\n\n/**\n * 源头钩子:工具执行器返回后、任何消费者拿到之前,统一剥离所有 text 块的 ANSI 转义序列。\n *\n * 设计意图——替代\"散落在各消费路径上的独立 stripAnsi 调用\"(打地鼠模式)为\"执行边界单次\n * 归一化\"(源头截断)。`tool-turn-runner.ts` 的两个执行入口(sync 工具 + lifecycleAsync 工具)\n * 在 `toolExecutor.execute()` 返回后立即调用本函数,确保 emitter、recorder、clamp、通知、\n * trace 等全部下游自动获得洁净文本。下游已有的 stripAnsi 调用保留为纵深防御(对已洁净文本\n * 是零开销 no-op)。\n *\n * 类型设计:泛型 `T extends { type: string; text?: string }` 兼容 `ToolResult.content`\n * (`@x-otto/hook-contracts`)与 `ToolResultMessage['content']`(`@x-otto/interchange`)\n * 两套 shape——text 块有 `{ type: 'text', text: string }`,image 块 `{ type: 'image', ... }`\n * 无 text 字段原样返回。\n */\nexport function normalizeToolResultContent<T extends { type: string; text?: string }>(\n content: T[],\n): T[] {\n return content.map((block) =>\n block.type === 'text' && typeof block.text === 'string'\n ? { ...block, text: stripAnsi(block.text) }\n : block,\n )\n}\n\n/**\n * R7:钳制工具结果文本进模型上下文的大小。仅截极端值(默认 100k 字符≈25k token):\n * text 块超阈值时头尾保留、中段以 `[… N chars truncated …]` 替换;非 text 块原样保留。\n * 防全仓 grep / 巨型文件读等病态结果撑爆 context window;正常 read/edit 远低于阈值不受影响。\n *\n * 终局修复(ANSI 泄漏):**钳制前先剥离 ANSI**,再基于洁净文本算 head/tail/slice——\n * 否则 slice 可能恰好落在 ANSI 序列中间(如 head 落到 `\\x1b[38;2;86;182` 中间位置),\n * 导致下游 stripAnsi 仅剥离前半段 `\\x1b[38;2;86;182` 无效序列、后半段 `;194mfunction`\n * 无 ESC 前缀的残缺参数裸漏进对话流。先 strip → 基于洁净长度钳制 → 用洁净文本拼接。\n * 钳制输出本就降级为截断预览,丢失 ANSI 颜色是可接受的。\n */\nexport function clampToolResultContent(\n content: ToolResultMessage['content'],\n maxChars: number,\n): ToolResultMessage['content'] {\n return content.map((block) => {\n if (block.type !== 'text') return block\n // 先剥离 ANSI,防切片边界落入 CSI 序列中间产生残缺参数裸漏。\n // 不超预算时也必须返回洁净文本——ANSI 码对模型无意义,保留会导致\n // `\\x1b[` 被终端吃掉后参数尾(如 `38;2;86;182;194m`)裸漏进对话流。\n const clean = stripAnsi(block.text)\n if (clean.length <= maxChars) return { ...block, text: clean }\n const head = Math.floor(maxChars * 0.7)\n const tail = maxChars - head\n const omitted = clean.length - maxChars\n return {\n ...block,\n text: `${clean.slice(0, head)}\\n\\n[… ${omitted} chars truncated (tool result exceeded ${maxChars} char budget) …]\\n\\n${clean.slice(clean.length - tail)}`,\n }\n })\n}\n\n/** RFC-093 占位符 preview 窗口(头尾保留,行式结果两端都有信息量)。 */\nconst SPILL_PREVIEW_CHARS = 2000\n\n/**\n * 统计一段 tool result content 真正进入模型上下文的文本字符数。\n *\n * 调用方应传 **clamp/spill 之后**的 content——它才是模型下一轮实际看到的内容。\n * image 块按 `IMAGE_CONTEXT_CHARS_EQUIVALENT` 折算(图片不是文本,但仍占窗口;\n * 这里只给宿主一个量级可用的估算基数,精确图片 token 由 provider usage 结算)。\n */\nexport const IMAGE_CONTEXT_CHARS_EQUIVALENT = 4000\n\nexport function toolResultContextChars(content: ToolResultMessage['content']): number {\n let chars = 0\n for (const block of content) {\n if (block.type === 'text') chars += block.text.length\n else chars += IMAGE_CONTEXT_CHARS_EQUIVALENT\n }\n return chars\n}\n\n/**\n * RFC-340 M3:单个 prompt 内工具结果的累计额度。\n *\n * 缺口(M3 前置实测确认):`clampWithSpill` 的预算是 **per-tool 固定字符数**\n * (`ENGINE_DEFAULTS.maxToolResultChars = 100_000`),不跨工具累计——20 个工具各返回\n * 99K 全部合法通过,合计却足以撑爆窗口,届时只能靠 `hardCeiling` 触发压缩事后补救,\n * 等于**用丢对话历史来偿还工具输出超发**,代价错配。\n *\n * 本类给出 per-prompt 总闸:已发放越多,后续单次预算越紧(D5-1 递减而非拒绝——工具\n * 副作用已发生,让模型看不到自己刚做的事比给截断版更糟),但恒不低于保底份额\n * (D5-2,`minToolResultChars`),保证结果始终头尾可辨。\n *\n * **纯状态容器 + 纯计算**:不读时钟、不做 IO,符合引擎确定性重放约束(RFC D13)。\n */\nexport class PromptToolResultAllowance {\n private consumed = 0\n\n /**\n * @param totalChars per-prompt 总额度(字符)。`undefined` = 不设总闸(窗口未知时\n * 退化为既有 per-tool 行为,向后兼容,见 D5-4)。\n * @param minPerTool 单次保底份额(D5-2)。\n */\n constructor(\n private readonly totalChars: number | undefined,\n private readonly minPerTool: number,\n ) {}\n\n /**\n * 计算本次 clamp 的有效预算。\n *\n * 语义要点:**只收紧不放大**——`declaredBudget`(per-tool 声明或引擎默认)是上界,\n * 剩余额度只能把它压小,绝不会让某个工具拿到超过自己声明的预算(D5 测试 4 锁定)。\n */\n budgetFor(declaredBudget: number): number {\n if (this.totalChars === undefined) return declaredBudget\n const remaining = Math.max(0, this.totalChars - this.consumed)\n // 保底份额本身也不得突破声明预算——`resultBudgetChars` 的既有语义是\"本工具声明更小\n // 值以促使大输出更早落盘\"(见 `@x-otto/interchange` AgentTool.resultBudgetChars)。\n // 若无条件抬到 minPerTool,一个声明 500 字符的工具会被放大到 2000,等于本额度机制\n // 反过来推翻了工具自己的声明——那是放大而非收紧,违反本方法的核心不变量。\n const floor = Math.min(this.minPerTool, declaredBudget)\n return Math.max(floor, Math.min(declaredBudget, remaining))\n }\n\n /** 记录本次真正进入上下文的字符量(应传 clamp/spill **之后**的 content 统计值)。 */\n charge(chars: number): void {\n this.consumed += Math.max(0, chars)\n }\n\n /** 已发放字符数(观测/测试用)。 */\n get consumedChars(): number {\n return this.consumed\n }\n}\n\n/**\n * RFC-340 M3 D5-4:由上下文窗口派生 per-prompt 工具结果总额度(字符)。\n *\n * 字符↔token 按 ~4 字符/token 粗放换算(与 `@x-otto/memory` 启发式估算同量级;此处只需\n * 量级正确,精确 token 由 provider usage 结算)。窗口未知 → `undefined` = 不设总闸。\n */\nexport function promptToolResultAllowanceChars(\n contextWindow: number | undefined,\n fraction: number,\n): number | undefined {\n if (!contextWindow || contextWindow <= 0) return undefined\n return Math.max(0, Math.floor(contextWindow * fraction * 4))\n}\n\n/**\n * RFC-093:spill-aware 钳制。超预算 text 块先尝试落盘(SpillPort):\n * 成功 → 占位符含磁盘路径 + 头尾 preview(模型可用 read 工具恢复全文);\n * 失败/未注入 → fail-open 回退 clampToolResultContent 纯截断(现状行为)。\n */\nexport async function clampWithSpill(\n content: ToolResultMessage['content'],\n maxChars: number,\n toolCallId: string,\n spill: SpillPort | undefined,\n): Promise<ToolResultMessage['content']> {\n const needsProcessing = content.some(\n (b) =>\n (b.type === 'text' && b.text.length > maxChars) ||\n (b.type === 'image' && b.source.length * 0.75 > MAX_IMAGE_INLINE_BYTES),\n )\n if (!needsProcessing) return clampToolResultContent(content, maxChars)\n\n // 多 text block 去重:每个 block 独立文件名(block 序号),防后写覆盖前写\n let blockIdx = 0\n let imageIdx = 0\n const imageBlockCount = content.filter(b => b.type === 'image').length\n\n const out: ToolResultMessage['content'] = []\n for (const block of content) {\n // RFC-151 D2:image 块超阈值 → spill 落盘或占位符丢弃\n if (block.type === 'image') {\n const estimatedBytes = block.source.length * 0.75\n if (estimatedBytes <= MAX_IMAGE_INLINE_BYTES) {\n out.push(block)\n continue\n }\n if (!spill) {\n out.push({\n type: 'text',\n text: `[image: ${Math.round(estimatedBytes)} bytes — omitted, no spill port configured]`,\n })\n continue\n }\n // RFC-151 M2 完成审核修复:image key 必须与 text key 在同一 tool result 内天然不同——\n // 若各自计数都不超过 1(同一 result 里恰好各 1 个超限 text + 1 个超限 image),\n // 两者会各自退化为裸 toolCallId,导致 spill() 写入同一路径、后写覆盖前写(真实数据丢失)。\n // 用 `_img` 后缀固定区分 image 落盘 key 与 text 落盘 key(text 侧 blockId 逻辑不变,\n // 图片始终带后缀,不依赖\"只有一张图就用裸 id\"的假设)。\n const imageBlockId = imageBlockCount > 1 ? `${toolCallId}_img${imageIdx}` : `${toolCallId}_img`\n imageIdx++\n const path = await spill(imageBlockId, block.source)\n if (!path) {\n out.push({\n type: 'text',\n text: `[image: ${Math.round(estimatedBytes)} bytes — omitted, no spill port configured]`,\n })\n continue\n }\n out.push({\n type: 'text',\n text: `[image: ${Math.round(estimatedBytes)} bytes, stored to ${path}, use read tool to inspect if needed]`,\n })\n continue\n }\n\n if (block.type !== 'text' || block.text.length <= maxChars) {\n out.push(block.type === 'text' ? { ...block, text: stripAnsi(block.text) } : block)\n continue\n }\n if (!spill) {\n out.push(...clampToolResultContent([block], maxChars))\n continue\n }\n const blockId = content.filter(b => b.type === 'text').length > 1\n ? `${toolCallId}_b${blockIdx}`\n : toolCallId\n blockIdx++\n const path = await spill(blockId, block.text)\n if (!path) {\n out.push(...clampToolResultContent([block], maxChars))\n continue\n }\n const clean = stripAnsi(block.text)\n const head = Math.floor(SPILL_PREVIEW_CHARS * 0.7)\n const tail = SPILL_PREVIEW_CHARS - head\n out.push({\n ...block,\n text: `[tool result stored to ${path} (${clean.length} chars). Use the read tool on that path to recover the full output.]\\n\\nPreview:\\n${clean.slice(0, head)}\\n\\n[… ${clean.length - SPILL_PREVIEW_CHARS} chars omitted …]\\n\\n${clean.slice(clean.length - tail)}`,\n })\n }\n return out\n}\n\n/**\n * 有上限的并行执行(allSettled 语义)。最多 `limit` 个并发,其余排队;每个任务\n * 自捕获异常成 PromiseSettledResult,绝不抛出(无孤儿)——调用方据 settled 数组统一传播。\n * 保留输入顺序对应结果索引。\n */\nexport async function runBounded<T>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<void>,\n): Promise<PromiseSettledResult<void>[]> {\n const results: PromiseSettledResult<void>[] = new Array(items.length)\n let next = 0\n const worker = async (): Promise<void> => {\n for (;;) {\n const i = next++\n if (i >= items.length) return\n try {\n await fn(items[i]!)\n results[i] = { status: 'fulfilled', value: undefined }\n } catch (reason) {\n results[i] = { status: 'rejected', reason }\n }\n }\n }\n const safeLimit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : 1\n const workerCount = Math.min(safeLimit, items.length)\n await Promise.all(Array.from({ length: workerCount }, () => worker()))\n return results\n}\n","import { CONVERSATION_SUMMARY_PREFIX } from '@x-otto/interchange'\nimport { recordMemoryLifecycle } from './memory-lifecycle'\nimport { stripAnsi } from './tool-result-clamp'\n\nimport type { Message, Model, ThinkingLevel } from '@x-otto/interchange'\nimport type { CacheRetention, StreamContext, SystemPromptSegment, ToolDefinition } from '@x-otto/provider'\nimport type { MemoryTrigger } from './memory-lifecycle'\nimport type { ClockPort } from './ports'\nimport type { MemoryPort, PersistencePort, PipelineHooks, PromptParams } from './pipeline'\nimport type { TraceRecorder } from './trace'\nimport type { AgentMessage, AgentTool } from './types'\n\n/**\n * ContextPipeline 投影层 —— \"会话 state → 模型 input\" 的唯一投影。\n *\n * 设计(见 docs/design/rfc020-p3-context-pipeline.md,方案 H):今天的 context 组装是一条**隐式\n * pipeline**——顺序涌现自 hook 优先级数字 + 散在 pipeline.ts/work-loop.ts 的直调。本模块把这条链\n * 收敛成**显式、命名、可单测**的 section 函数。受两套 cadence 约束(钉死、不拍平):\n *\n * ① prompt-scoped(每用户轮一次,冻结复用跨工具回合):\n * selectContextMessages —— 选消息 + 摘要恢复(本文件)\n * + system prompt 组装 / 注入(仍由 system.prompt.transform hook 链产出;P3-03/04 收编)\n * ② turn-scoped(每工具回合一次):\n * messages.transform + memory 压缩(P3-02 收编)→ convertToLLM →\n * layerSystemPrompt —— cache 分层(本文件)\n *\n * P3-01 只立骨架 + 把现有 pipeline.ts/work-loop.ts 的两段纯逻辑迁入(行为零变,逐字节等价);\n * P3-02..05 再逐包退零件填进各 section。\n */\n\nexport interface PromptContextMessages {\n messages: AgentMessage[]\n /** 本轮进入上下文的消息数(含注入的摘要消息)——persistNew 的写树起点基线。 */\n messagesBefore: number\n hasSummary: boolean\n}\n\n/**\n * prompt-scoped section:从会话状态核(session.buildContext,T2 含 summary + replacement 恢复)\n * 选出本轮上下文消息。摘要在场则以 `CONVERSATION_SUMMARY_PREFIX` 重建为一条 user 摘要消息置顶,\n * 使 memory 的 isSummaryMessage 能识别树上注入的摘要、再压缩时不重复编码。\n *\n * 纯函数(给定 persistence/clock 快照即定)——迁自 pipeline.ts 原 buildPromptContextMessages,\n * 逐字节等价(前缀 + 单 `\\n` 分隔不变;仅以单一真相源常量替代原硬编码字面量)。\n */\nexport function selectContextMessages(\n persistence: PersistencePort,\n clock: ClockPort,\n): PromptContextMessages {\n const ctx = persistence.buildContext()\n const messages: AgentMessage[] = []\n\n if (ctx.summary) {\n messages.push({\n role: 'user',\n timestamp: clock.now(),\n content: [{ type: 'text', text: `${CONVERSATION_SUMMARY_PREFIX}\\n${ctx.summary}` }],\n } as Message)\n }\n messages.push(...ctx.messages)\n\n // 终局防御:读侧归一化——即使写侧源头钩子已截断新 ANSI,历史上落库的脏 tool_result\n // 在 resume/--continue 时经 buildContext() 原样回放,此处统一扫除。对已洁净消息是零开销 no-op\n // (正则全量 no-match)。只扫 text 块,image 块不触动。\n const sanitized = messages.map((msg) => {\n if (!Array.isArray(msg.content)) return msg\n const cleaned = (msg.content as Array<{ type: string; text?: string }>).map((block) =>\n block.type === 'text' && typeof block.text === 'string'\n ? { ...block, text: stripAnsi(block.text) }\n : block,\n )\n return { ...msg, content: cleaned as typeof msg.content }\n })\n\n return {\n messages: sanitized as AgentMessage[],\n messagesBefore: sanitized.length,\n hasSummary: Boolean(ctx.summary),\n }\n}\n\n/**\n * turn-scoped section:prompt cache 分层。stable 前缀(system prompt 主体)进缓存;volatile 尾段\n * (如 phase context,会话内逐轮变化)放在 cache 断点之后,使其变化不击穿 system/messages 缓存。\n *\n * 纯函数——迁自 work-loop.ts runTurn,逐字节等价:\n * - `volatileTail` 须为调用方已过滤空串后的尾段(与原 `(ctx.systemTail ?? []).filter(len>0)` 一致);\n * - 尾段非空 → `fullSystemPrompt = [stable, ...tail].join('\\n\\n')` + 显式 segments(stable/volatile);\n * - 尾段为空 → 退化为单块(fullSystemPrompt = stable,segments=undefined,单尾部断点)。\n */\nexport function layerSystemPrompt(\n stableSystem: string,\n volatileTail: string[],\n): { fullSystemPrompt: string; systemPromptSegments: SystemPromptSegment[] | undefined } {\n if (volatileTail.length === 0) {\n return { fullSystemPrompt: stableSystem, systemPromptSegments: undefined }\n }\n return {\n fullSystemPrompt: [stableSystem, ...volatileTail].join('\\n\\n'),\n systemPromptSegments: [\n { text: stableSystem, cache: 'stable' },\n ...volatileTail.map((text) => ({ text, cache: 'volatile' as const })),\n ],\n }\n}\n\n/**\n * apply/forceMemoryTransform 的**共享副作用**单源——prune 通知 +\n * 压缩写回持久化 + 成对 emit compaction.*。两入口的触发语义不同(阈值 vs 413 反应式强制,故**不**\n * 合并入口),但 `memory.process` 结果的下游副作用逐字相同 → 抽此 helper,改副作用只改一处。\n * `messageCountBefore`=压缩前条数(apply=llmMessages.length / force=messages.length,二者等长),\n * 供 emitAutoCompaction 计数配对。\n */\nasync function applyMemorySideEffects(\n result: Awaited<ReturnType<MemoryPort['process']>>,\n persistence: PersistencePort | undefined,\n sink: { compacted: boolean; replacement?: AgentMessage[] } | undefined,\n hooks: PipelineHooks | undefined,\n sessionId: string,\n messageCountBefore: number,\n): Promise<void> {\n if (result.pruned && persistence?.notifyPrune) {\n persistence.notifyPrune(\n result.pruneMetrics?.tokensSaved ?? 0,\n result.pruneMetrics\n ? {\n prunedCount: result.pruneMetrics.prunedCount,\n messagesBefore: result.pruneMetrics.messagesBefore,\n messagesAfter: result.pruneMetrics.messagesAfter,\n }\n : undefined,\n )\n }\n if (result.compacted && result.summary && result.replacement && persistence?.recordCompaction) {\n persistence.recordCompaction(result.summary, result.replacement as AgentMessage[])\n if (sink) {\n sink.compacted = true\n // RFC-170 D1:把 replacement 透出给调用方(context-pipeline.ts 的 transformContext/\n // reactiveCompact),供其驱动 onCompaction 原地收缩 ctx.messages——之前只传\n // sink.compacted 布尔标志,调用方无法拿到\"压缩后应该保留哪些消息\"这个关键信息,\n // 导致 compaction 只影响发给模型的上下文、从不回写进程持有的消息数组(RFC-170 D1\n // 根因)。\n sink.replacement = result.replacement as AgentMessage[]\n }\n await emitAutoCompaction(hooks, sessionId, messageCountBefore, result.messages.length)\n }\n}\n\n/**\n * RFC-074 R-CTXARG:尾随可选项收成 options 对象(原 recorder/hooks/persistence/sink 四个位置参\n * 在 apply/force 两函数间顺序不一致 → 易传错位)。memory/messages/sessionId/signal 仍位置参。\n */\nexport interface MemoryTransformOptions {\n recorder?: TraceRecorder\n hooks?: PipelineHooks\n persistence?: PersistencePort\n sink?: { compacted: boolean; replacement?: AgentMessage[] }\n}\n\nexport async function applyMemoryTransform(\n memory: MemoryPort | undefined,\n messages: AgentMessage[],\n sessionId: string,\n signal?: AbortSignal,\n opts: MemoryTransformOptions = {},\n): Promise<AgentMessage[]> {\n const { recorder, hooks, persistence, sink } = opts\n if (!memory || signal?.aborted) {\n return messages\n }\n\n const llmMessages = messages as Message[]\n const willPrune = memory.needsPrune(llmMessages)\n\n if (!willPrune && !memory.needsCompaction(llmMessages)) {\n return messages\n }\n\n let skipPrune = false\n if (willPrune && hooks) {\n const out = { cancelled: false }\n await hooks.execute('memory.prune.before', { sessionId, messageCount: llmMessages.length }, out)\n skipPrune = out.cancelled\n }\n\n const result = await memory.process(llmMessages, sessionId, signal, { skipPrune })\n observeMemoryResult(recorder, sessionId, 'in_loop', result)\n\n await applyMemorySideEffects(result, persistence, sink, hooks, sessionId, llmMessages.length)\n\n return result.messages as AgentMessage[]\n}\n\n/**\n * 引擎 review Tier-3:自动压缩点 emit compaction.before/after(**observer**,不可 veto——两 timing 的\n * output=void,见 hook-contracts/timings)。成对在「确已压缩」分支 emit:messageCount=压缩前条数、\n * retainedCount=压缩后条数。成对保证 CompactionLogger 的 before→after 配对(before 累加计数、after 计时长),\n * 避免未配对的 before 虚增计数。语义上两者紧邻 emit(自动压缩无独立\"压缩中\"窗口可观测)。\n */\nasync function emitAutoCompaction(\n hooks: PipelineHooks | undefined,\n sessionId: string,\n messageCount: number,\n retainedCount: number,\n): Promise<void> {\n if (!hooks) return\n await hooks.emit('compaction.before', { sessionId, messageCount })\n await hooks.emit('compaction.after', { sessionId, retainedCount })\n}\n\n/** P1 memory-lifecycle:从 process 结果录 memory.prune / memory.archive trace 锚点(compact 暂留 compaction.*)。 */\nfunction observeMemoryResult(\n recorder: TraceRecorder | undefined,\n sessionId: string,\n trigger: MemoryTrigger,\n result: {\n pruned: boolean\n archivePath?: string\n pruneMetrics?: {\n prunedCount: number\n tokensSaved: number\n messagesBefore: number\n messagesAfter: number\n }\n },\n): void {\n if (!recorder) return\n if (result.pruned) {\n recordMemoryLifecycle(recorder, {\n phase: 'prune',\n sessionId,\n trigger,\n metrics: { ...result.pruneMetrics },\n })\n }\n if (result.archivePath) {\n recordMemoryLifecycle(recorder, {\n phase: 'archive',\n sessionId,\n trigger,\n metrics: { archivePath: result.archivePath },\n })\n }\n}\n\n/**\n * 反应式压缩——本轮 stream 因上下文溢出(413)失败时调用。无条件强制压缩\n * (绕过 token 阈值闸),返回压缩后的消息数组;若无可压缩内容(消息太少等)返回 null,\n * 调用方据此放弃重试并上抛原始溢出错误。**不**再跑 messages.transform hook:该 hook 已在\n * 本轮 transformContext 跑过,重跑可能引入新内容、与「缩小上下文」的目标相悖。\n */\nexport async function forceMemoryTransform(\n memory: MemoryPort | undefined,\n messages: AgentMessage[],\n sessionId: string,\n signal?: AbortSignal,\n opts: MemoryTransformOptions = {},\n): Promise<AgentMessage[] | null> {\n const { recorder, hooks, persistence, sink } = opts\n if (!memory || signal?.aborted) {\n return null\n }\n\n const result = await memory.process(messages as Message[], sessionId, signal, { force: true })\n observeMemoryResult(recorder, sessionId, 'reactive_overflow', result)\n\n await applyMemorySideEffects(result, persistence, sink, hooks, sessionId, messages.length)\n\n if (!result.compacted && !result.pruned) {\n return null\n }\n return result.messages as AgentMessage[]\n}\n\nexport interface TurnMessageTransformDeps {\n agentName: string\n sessionId: string\n tools: AgentTool[]\n hooks: PipelineHooks\n memory?: MemoryPort\n recorder?: TraceRecorder\n persistence: PersistencePort\n /**\n * 压缩后回调(RFC-170 D1 扩展签名):baseLen=该轮上下文消息数,replacement=压缩保留的\n * 近期消息(不含摘要消息本身——摘要只属于\"发给模型的这一轮上下文\",不该进入循环持有的\n * 持久消息数组,那会让摘要在 persistNew 落树时被当成一条新消息重复写入)。\n *\n * 调用方(pipeline.ts)据此原地收缩循环持有的 `messages` 数组(splice 保引用,不换新\n * 数组——见 RFC-170 D1 的消费方安全性核实:workLoop 解构/pause 闭包/runTools/\n * classifyTurnOutcome/sealUnpairedToolCalls 均读取同一引用,原地收缩后各处读到的都是\n * 收缩后的最新内容,语义正确)。\n */\n onCompaction?: (baseLen: number, replacement: AgentMessage[]) => void\n}\n\n/**\n * turn-scoped section 装配:返回 runner 每个 model turn 调用的 `transformContext`(messages.transform\n * hook + 自动压缩写回)与 `reactiveCompact`(413 反应式压缩写回)。从 pipeline.ts runTurnLoop 的内联\n * 闭包迁出,行为零变——使\"每回合消息变换 + 压缩\"成为 ContextPipeline 的显式命名 section。\n */\nexport function createTurnMessageTransforms(deps: TurnMessageTransformDeps): {\n transformContext: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>\n reactiveCompact?: (\n messages: AgentMessage[],\n signal?: AbortSignal,\n ) => Promise<AgentMessage[] | null>\n} {\n const { agentName, sessionId, tools, hooks, memory, recorder, persistence, onCompaction } = deps\n\n const transformContext = async (\n contextMessages: AgentMessage[],\n signal?: AbortSignal,\n ): Promise<AgentMessage[]> => {\n if (signal?.aborted) {\n return contextMessages\n }\n\n const output = { messages: contextMessages }\n await hooks.execute(\n 'messages.transform',\n { messages: contextMessages, tools, agentName, sessionId },\n output,\n )\n\n const baseLen = contextMessages.length\n const sink: { compacted: boolean; replacement?: AgentMessage[] } = { compacted: false }\n const out = await applyMemoryTransform(memory, output.messages, sessionId, signal, {\n recorder,\n hooks,\n persistence,\n sink,\n })\n if (sink.compacted && sink.replacement) onCompaction?.(baseLen, sink.replacement)\n return out\n }\n\n const reactiveCompact = memory\n ? async (\n contextMessages: AgentMessage[],\n signal?: AbortSignal,\n ): Promise<AgentMessage[] | null> => {\n const baseLen = contextMessages.length\n const sink: { compacted: boolean; replacement?: AgentMessage[] } = { compacted: false }\n const out = await forceMemoryTransform(memory, contextMessages, sessionId, signal, {\n recorder,\n persistence,\n sink,\n hooks,\n })\n if (sink.compacted && sink.replacement) onCompaction?.(baseLen, sink.replacement)\n return out\n }\n : undefined\n\n return { transformContext, reactiveCompact }\n}\n\nexport function isThinkingLevel(value: string): value is ThinkingLevel {\n return (\n value === 'low' ||\n value === 'medium' ||\n value === 'high' ||\n value === 'xhigh' ||\n value === 'max'\n )\n}\n\n/** section:chat.params hook(原地改写 params.temperature/maxTokens/thinkingLevel)。 */\nasync function applyChatParams(\n hooks: PipelineHooks,\n params: PromptParams,\n model: Model,\n sessionId: string,\n): Promise<void> {\n const output = {\n temperature: params.temperature,\n maxTokens: params.maxTokens,\n thinkingLevel: params.thinkingLevel as string,\n metadata: {},\n }\n\n await hooks.execute(\n 'chat.params',\n {\n sessionId,\n model: model.id,\n provider: model.provider,\n thinkingLevel: params.thinkingLevel,\n },\n output,\n )\n\n params.temperature = output.temperature\n params.maxTokens = output.maxTokens\n if (isThinkingLevel(output.thinkingLevel)) {\n params.thinkingLevel = output.thinkingLevel\n }\n}\n\n/** section:system.prompt.transform hook 链(stable 前缀 + volatile 尾段;尾段以 params.systemTail 种子起头)。 */\nfunction messageText(message: AgentMessage): string | undefined {\n if (message.role !== 'user') return undefined\n if (typeof message.content === 'string') return message.content\n if (!Array.isArray(message.content)) return undefined\n return message.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .filter((text) => text.length > 0)\n .join('\\n')\n}\n\nasync function transformSystemPrompt(\n hooks: PipelineHooks,\n params: PromptParams,\n sessionId: string,\n tools: AgentTool[],\n userText?: string,\n): Promise<{ systemPrompt: string; systemTail: string[] }> {\n const output: { systemPrompt: string; systemTail: string[] } = {\n systemPrompt: params.systemPrompt,\n systemTail: params.systemTail ? [...params.systemTail] : [],\n }\n\n await hooks.execute(\n 'system.prompt.transform',\n {\n systemPrompt: params.systemPrompt,\n sessionId,\n tools,\n userText,\n },\n output,\n )\n\n return { systemPrompt: output.systemPrompt, systemTail: output.systemTail }\n}\n\nexport interface PromptContextPlan {\n messages: AgentMessage[]\n /** 本轮进入上下文的消息数(含注入摘要)——persistNew 写树起点基线。 */\n messagesBefore: number\n hasSummary: boolean\n /** prompt-scoped 解析后的 stable system 前缀(进 cache),冻结复用整个工具循环。 */\n stableSystem: string\n /** prompt-scoped volatile system 尾段(原始;消费侧过滤空串)。 */\n systemTail: string[]\n}\n\nexport interface PlanPromptContextInput {\n persistence: PersistencePort\n clock: ClockPort\n hooks: PipelineHooks\n params: PromptParams\n model: Model\n sessionId: string\n tools: AgentTool[]\n /** RFC-284 T3a:当前 prompt 的显式 user query,供 volatile Relevant Hints 使用。 */\n userText?: string\n /** 选消息后的观测钩子(pause/trace),由引擎注入——保持 context.build 断点的原位时序。 */\n onContextSelected?: (messages: AgentMessage[], hasSummary: boolean) => Promise<void>\n}\n\nexport interface BuildTurnInput {\n stableSystem: string\n volatileTail: string[]\n llmMessages: StreamContext['messages']\n model: Model\n tools: ToolDefinition[]\n cacheRetention?: CacheRetention\n thinkingLevel?: ThinkingLevel\n maxTokens?: number\n temperature?: number\n}\n\nexport const ContextPipeline = {\n /**\n * prompt-scoped 阶段(每 prompt 一次):① 选消息 + 摘要恢复 → ② chat.params → ③ system prompt\n * 各段 + 注入。产出冻结复用整个工具循环的 PromptContextPlan。params 原地改写(chat.params 语义)。\n */\n async planPromptContext(input: PlanPromptContextInput): Promise<PromptContextPlan> {\n const { persistence, clock, hooks, params, model, sessionId, tools, onContextSelected } = input\n\n const { messages, messagesBefore, hasSummary } = selectContextMessages(persistence, clock)\n await onContextSelected?.(messages, hasSummary)\n\n await applyChatParams(hooks, params, model, sessionId)\n\n const currentUserText = input.userText ?? [...messages].reverse().map(messageText).find(Boolean)\n const { systemPrompt, systemTail } = await transformSystemPrompt(\n hooks,\n params,\n sessionId,\n tools,\n currentUserText,\n )\n\n return { messages, messagesBefore, hasSummary, stableSystem: systemPrompt, systemTail }\n },\n\n /**\n * turn-scoped 阶段(每 stream 尝试一次):⑥ convertToLLM 的结果 + ⑦ cache 分层 → StreamContext。\n * stable 前缀进缓存、volatile 尾段(角色块/phase/delta)落断点后。纯函数。\n */\n buildTurnInput(input: BuildTurnInput): StreamContext {\n const { fullSystemPrompt, systemPromptSegments } = layerSystemPrompt(\n input.stableSystem,\n input.volatileTail,\n )\n return {\n model: input.model,\n systemPrompt: fullSystemPrompt,\n ...(systemPromptSegments ? { systemPromptSegments } : {}),\n ...(input.cacheRetention ? { cacheRetention: input.cacheRetention } : {}),\n messages: input.llmMessages,\n tools: input.tools.length > 0 ? input.tools : undefined,\n thinkingLevel: input.thinkingLevel,\n maxTokens: input.maxTokens,\n temperature: input.temperature,\n }\n },\n}\n","import type { AgentMessage } from './types'\n\n/** RFC-148 M5:模型未声明图片数量约束时的兜底上限。 */\nexport const MAX_IMAGES_PER_REQUEST = 20\n\nfunction imagePlaceholder(kind: 'capped' | 'omitted', toolName: string | undefined, turnIdx: number): string {\n const prefix = kind === 'capped'\n ? '[earlier image capped'\n : '[earlier image omitted to fit context'\n return `${prefix}: ${toolName ?? 'attachment'}, turn ${turnIdx}]`\n}\n\n/**\n * D4:413/400 兜底最后防线。\n *\n * 刻意穿透 RFC-066 compaction 的 preservedMessages 窗口——常规压缩已对这批消息\n * 尝试过仍失败,问题就出在\"最近保留\"的消息本身;只保护 currentRoundStartIndex 及之后\n * (当前轮)。boundary 之前的所有 image block 替换为占位文本,其余原样保留。\n *\n * 纯函数:不修改入参数组/消息对象(map + spread)。**浅拷贝告警**:`{...msg}` 对\n * 非 content 字段(如 ToolResultMessage.details)是引用共享,非深拷贝——调用方不得\n * 修改返回消息的这些字段,否则会污染原始 messages 数组中的同一引用。\n */\nexport function degradeOldImages(\n messages: AgentMessage[],\n currentRoundStartIndex: number,\n): { messages: AgentMessage[]; degradedCount: number } {\n if (messages.length === 0) return { messages, degradedCount: 0 }\n\n const boundary = Math.max(0, Math.min(currentRoundStartIndex, messages.length))\n let degradedCount = 0\n\n const out = messages.map((msg, idx) => {\n if (idx >= boundary) return msg\n if (!Array.isArray(msg.content)) return msg\n\n const hasImage = msg.content.some((c) => c.type === 'image')\n if (!hasImage) return msg\n\n const toolName =\n 'toolName' in msg ? (msg as unknown as { toolName?: string }).toolName : undefined\n const newContent = msg.content.map((c) => {\n if (c.type !== 'image') return c\n degradedCount++\n return {\n type: 'text' as const,\n text: imagePlaceholder('omitted', toolName, idx),\n }\n })\n return { ...msg, content: newContent } as unknown as AgentMessage\n })\n\n return { messages: out, degradedCount }\n}\n\n/**\n * D6:投影层图片数量 cap。\n *\n * 每轮常态投影——从尾部(最新)保留最多 maxImages 个 image block,更早的\n * image block 替换为占位文本。纯投影:调用方不得将结果回流到持久态/循环变量\n * (RFC-099 D6 \"投影副本不回流\")。\n *\n * 按消息序(前→后)、块序遍历;单条消息内跨边界时按 block 粒度部分替换。\n * maxImages === 0 → 全部降级。**引用告警**:`total <= maxImages` 时返回的\n * `messages` 是入参同一引用(非拷贝)——调用方(只读投影场景)不得修改返回值。\n */\nexport function capImagesPerRequest(\n messages: AgentMessage[],\n maxImages = MAX_IMAGES_PER_REQUEST,\n): { messages: AgentMessage[]; cappedCount: number } {\n // 计数全部 image block\n let total = 0\n for (const msg of messages) {\n if (!Array.isArray(msg.content)) continue\n for (const block of msg.content) {\n if (block.type === 'image') total++\n }\n }\n\n if (total <= maxImages) return { messages, cappedCount: 0 }\n\n const toRemove = total - maxImages\n let removed = 0\n\n const out = messages.map((msg, idx) => {\n if (!Array.isArray(msg.content)) return msg\n if (removed >= toRemove) return msg\n\n let hasImages = false\n for (const block of msg.content) {\n if (block.type === 'image') { hasImages = true; break }\n }\n if (!hasImages) return msg\n\n const toolName =\n 'toolName' in msg ? (msg as unknown as { toolName?: string }).toolName : undefined\n const newContent = msg.content.map((block) => {\n if (block.type !== 'image' || removed >= toRemove) return block\n removed++\n return {\n type: 'text' as const,\n text: imagePlaceholder('capped', toolName, idx),\n }\n })\n return { ...msg, content: newContent } as unknown as AgentMessage\n })\n\n return { messages: out, cappedCount: removed }\n}\n","/**\n * vision-gate.ts — RFC-181 D2/D3:视觉能力门控投影。\n *\n * 与 `image-degradation.ts`(D4/D6,处理\"图片太多/太旧\")职责正交:本模块处理\n * \"模型看不见图片\"——当会话主模型不支持视觉输入时,任何 image block 若原样发出,\n * 会被 provider 直接拒绝(400)或被不支持视觉的模型静默忽略。门控在投影层拦截,\n * 把 image block 替换为委托描述(若已委托成功)或明确占位文本(模型据此知道\n * \"这里有图但我看不到\",而非幻觉\"已经看过\")。\n *\n * 纯投影语义(同 image-degradation.ts 红线):返回新数组,不修改入参,不回流\n * 持久态/循环变量——模型切回视觉模型后,持久历史中的原始 image block 依然完整。\n *\n * M1 范围:`describeImages` 端口尚未接线(RFC-181 M2 引入),门控恒走占位分支\n * (模型不支持视觉 → 全部 image block 变为\"不可见\"占位)。M2 接入委托后,\n * 占位分支只在委托不可用/失败时触发。\n */\nimport { modelSupportsImages } from '@x-otto/interchange'\nimport type { Model } from '@x-otto/interchange'\n\nimport { capImagesPerRequest } from './image-degradation'\nimport type { AgentMessage } from './types'\n\n/**\n * 图像委托描述端口(RFC-181 D3)。宿主(coding 包 VisionDelegateService)实现,\n * 引擎侧只消费返回值——不感知模型选择、prompt、缓存。返回值与入参一一对应;\n * `null` 表示该图描述失败(单图失败不连坐,其余图正常处理)。\n *\n * 约定:返回的非 null 字符串是**最终注入文本**(宿主已按 RFC-181 D2 格式拼好\n * `[image seen by vision model <id>]\\n<描述>` 前缀),引擎侧原样使用,不做二次\n * 格式化——保持\"agent 包零模型路由知识\"(RFC-181 §4 规则 5)。\n */\nexport type DescribeImagesPort = (\n images: Array<{ mime: string; source: string; contextHint?: string }>,\n signal?: AbortSignal,\n) => Promise<Array<string | null>>\n\n/** 委托不可用/失败时的占位文案(RFC-181 D2)。 */\nconst UNAVAILABLE_PLACEHOLDER =\n '[image not viewable: current model has no vision capability and no vision model is available for delegation]'\n\nexport interface VisionGateResult {\n messages: AgentMessage[]\n /** 因能力错配被处理的 image block 总数(含委托成功与占位两类)。 */\n gatedCount: number\n /** 其中委托成功、替换为描述文本的数量。 */\n describedCount: number\n}\n\n/**\n * 视觉能力门控投影。`model` 支持图像输入时直通(引用原样返回,零开销);\n * 不支持时收集全部 image block(不分新旧、不设边界——模型对任何图像都不可见),\n * 尝试委托描述,按结果逐块替换。\n */\nexport async function applyVisionGate(\n messages: AgentMessage[],\n model: Pick<Model, 'input'>,\n describeImages?: DescribeImagesPort,\n signal?: AbortSignal,\n): Promise<VisionGateResult> {\n if (modelSupportsImages(model)) {\n return { messages, gatedCount: 0, describedCount: 0 }\n }\n\n const imageRefs: Array<{ mime: string; source: string }> = []\n for (const msg of messages) {\n if (!Array.isArray(msg.content)) continue\n for (const block of msg.content) {\n if (block.type === 'image') imageRefs.push({ mime: block.mime, source: block.source })\n }\n }\n if (imageRefs.length === 0) {\n return { messages, gatedCount: 0, describedCount: 0 }\n }\n\n let descriptions: Array<string | null> = imageRefs.map(() => null)\n if (describeImages) {\n try {\n descriptions = await describeImages(imageRefs, signal)\n } catch {\n // best-effort:委托失败与主 loop 的 stream retry 完全隔离(RFC-181 D3)——\n // 吞掉异常,全部走占位分支,不向上抛出、不影响主模型流。\n descriptions = imageRefs.map(() => null)\n }\n }\n\n let describedCount = 0\n let refIdx = 0\n const out = messages.map((msg) => {\n if (!Array.isArray(msg.content)) return msg\n const hasImage = msg.content.some((c) => c.type === 'image')\n if (!hasImage) return msg\n\n const newContent = msg.content.map((block) => {\n if (block.type !== 'image') return block\n const description = descriptions[refIdx]\n refIdx++\n if (description) {\n describedCount++\n return { type: 'text' as const, text: description }\n }\n return { type: 'text' as const, text: UNAVAILABLE_PLACEHOLDER }\n })\n return { ...msg, content: newContent } as unknown as AgentMessage\n })\n\n return { messages: out, gatedCount: imageRefs.length, describedCount }\n}\n\n/**\n * RFC-183 O-3:视觉投影管线单入口——把 runTurn 顶部散落的三步(capImagesPerRequest\n * 图片数量上限 → applyVisionGate 能力门控/委托描述 → 两类 trace/emit 副作用)收束为\n * 一次调用。runTurn 只关心\"拿到投影后的 messages\",不再内联 ~30 行编排细节。\n *\n * 副作用(trace record + emitter.emit)保持与收束前逐字节一致:\n * - image.cap:仅 cappedCount>0 时 record(RFC-099/148 M5)\n * - image.vision-gate:仅 gatedCount>0 时 record(RFC-181 D6)\n * - image.vision-delegate:仅 gatedCount>0 且 describeImages 已注入时 record+emit\n * (区分\"无委托能力\"与\"委托了但部分/全部失败\")\n */\nexport interface VisionPipelineDeps {\n model: Model\n describeImages: DescribeImagesPort | undefined\n signal: AbortSignal\n turnIndex: number\n maxImagesDefault: number\n record: (node: 'image.cap' | 'image.vision-gate' | 'image.vision-delegate', payload: Record<string, unknown>) => void\n emitDelegate: (payload: { turnIndex: number; attemptedCount: number; describedCount: number; failedCount: number }) => void\n}\n\nexport async function applyVisionPipeline(\n messages: AgentMessage[],\n deps: VisionPipelineDeps,\n): Promise<AgentMessage[]> {\n const maxImages = deps.model.imageConstraints?.maxImagesPerRequest ?? deps.maxImagesDefault\n const imageCap = capImagesPerRequest(messages, maxImages)\n if (imageCap.cappedCount > 0) {\n deps.record('image.cap', { cappedCount: imageCap.cappedCount, maxImages })\n }\n\n const visionGate = await applyVisionGate(imageCap.messages, deps.model, deps.describeImages, deps.signal)\n if (visionGate.gatedCount > 0) {\n deps.record('image.vision-gate', {\n gatedCount: visionGate.gatedCount,\n describedCount: visionGate.describedCount,\n modelId: deps.model.id,\n })\n if (deps.describeImages) {\n const delegatePayload = {\n turnIndex: deps.turnIndex,\n attemptedCount: visionGate.gatedCount,\n describedCount: visionGate.describedCount,\n failedCount: visionGate.gatedCount - visionGate.describedCount,\n }\n deps.record('image.vision-delegate', delegatePayload)\n deps.emitDelegate(delegatePayload)\n }\n }\n return visionGate.messages\n}\n","/**\n * progress-window.ts — RFC-104 D1:工具预算窗口的进展统计与判定.\n *\n * 窗口 = 一个 maxToolTurns 预算周期(扩展重置 / followUp 重置时清零)。\n * 进展判定(触顶时求值):\n * hasProgress = writeToolSuccesses > 0 —— 任一写类工具成功即进展\n * || distinctRatio >= 阈值(0.5) —— 纯读窗口看签名多样性\n *\n * 设计约束(RFC-104 R6 agent 包纯净):判定只用包内已有信息——\n * AgentTool.readonly 旗标、调用签名(toolName+args)、结果 isError。\n * 禁止宿主回调 / 文件系统探测。\n *\n * 已知有意接受的漏放(RFC-104 §7):失败重试 + args 变化(isError 不计写成功、\n * distinctRatio 却高)会判进展获扩展——仍受扩展封顶 + steer + 硬停三层兜底。\n */\nimport { ENGINE_DEFAULTS } from './engine-nodes'\n\nexport interface ProgressWindowStats {\n /** readonly!==true 且 !isError 的工具调用数(含 write_todos——todo 推进的可见形态)。 */\n writeToolSuccesses: number\n /** 调用签名 → 出现次数(签名 = toolName + args 截断 stringify;Map 有上限)。 */\n signatures: Map<string, number>\n /** 窗口内全部工具调用数(含签名 Map 溢出后未入 Map 的)。 */\n totalCalls: number\n}\n\nexport function createProgressWindow(): ProgressWindowStats {\n return { writeToolSuccesses: 0, signatures: new Map(), totalCalls: 0 }\n}\n\n/** 扩展授予 / followUp 时窗口清零(原地,保持引用稳定)。 */\nexport function resetProgressWindow(stats: ProgressWindowStats): void {\n stats.writeToolSuccesses = 0\n stats.signatures.clear()\n stats.totalCalls = 0\n}\n\n/**\n * 工具调用签名:toolName + stableStringify(args) 截断。\n * 稳定化 = 顶层键排序(一层足够:同一工具的 args 形状稳定,嵌套顺序漂移场景罕见,\n * 深度递归 stringify 对大 args 是纯浪费)。\n */\nexport function toolCallSignature(toolName: string, args: unknown): string {\n let body: string\n if (args !== null && typeof args === 'object' && !Array.isArray(args)) {\n const record = args as Record<string, unknown>\n const keys = Object.keys(record).sort()\n const parts: string[] = []\n for (const key of keys) {\n let value: string\n try {\n value = JSON.stringify(record[key]) ?? 'undefined'\n } catch {\n value = '[unserializable]'\n }\n parts.push(`${key}:${value}`)\n }\n body = parts.join(',')\n } else {\n try {\n body = JSON.stringify(args) ?? 'undefined'\n } catch {\n body = '[unserializable]'\n }\n }\n const cap = ENGINE_DEFAULTS.progressSignatureArgsMaxChars\n return `${toolName}(${body.length > cap ? body.slice(0, cap) : body})`\n}\n\n/** 每次工具调用完成后记录(work-loop executeSingleTool 调用)。 */\nexport function recordToolCall(\n stats: ProgressWindowStats,\n input: { toolName: string; args: unknown; readonly: boolean; isError: boolean },\n): void {\n stats.totalCalls++\n if (!input.readonly && !input.isError) {\n stats.writeToolSuccesses++\n }\n if (stats.signatures.size < ENGINE_DEFAULTS.progressSignatureCap) {\n const sig = toolCallSignature(input.toolName, input.args)\n stats.signatures.set(sig, (stats.signatures.get(sig) ?? 0) + 1)\n }\n // 溢出:只计 totalCalls。distinctRatio 分母增大、分子封顶 → ratio 下降方向。\n // 但溢出意味着窗口内 distinct 签名已 ≥512(远超 0.5×totalCalls 所需的多样性下限\n // 在 totalCalls≤1024 时),实际难以因此误杀;真正的循环(少数签名反复)不会触发溢出。\n}\n\n/**\n * 进展判定(RFC-104 D1,触顶时求值)。\n * 空窗口(totalCalls=0,理论不可达——触顶前提是跑过 maxToolTurns 轮工具)按无进展处理。\n */\nexport function hasWindowProgress(stats: ProgressWindowStats): boolean {\n if (stats.writeToolSuccesses > 0) return true\n if (stats.totalCalls === 0) return false\n const distinctRatio = stats.signatures.size / stats.totalCalls\n return distinctRatio >= ENGINE_DEFAULTS.progressDistinctRatio\n}\n","/**\n * stall-detector.ts — RFC-203:早期停滞检测(触顶前的滑动窗口打转发现)。\n *\n * RFC-104 的 `ProgressWindow` 只在预算触顶时求值一次(扩展授予闸门)——`maxToolTurns=50`\n * 的窗口里 agent 从第 3 turn 开始原地打转要烧满 50 turns 才被发现。本模块补上触顶前的\n * 周期性检测:维护最近 `windowTurns` 个 turn 的环形桶,每 turn 结束求值一次。\n *\n * 判定(RFC-203 D1,评审 H3 收窄后):\n * stalled = 窗口已满\n * && 窗口内 writeToolSuccesses === 0 —— 零写成功(与 RFC-104 同构)\n * && 窗口内 maxSignatureRepeat >= repeatThreshold —— 同一签名反复 ≥N 次\n * 第三条件不用 distinctRatio:纯 ratio 会误伤\"反复 read 同几个文件各 2-3 次\"的合法代码\n * 审查(8 turn、10 calls、4 distinct → ratio 0.4 误判),maxRepeat 只命中\"同一件事反复\n * 做 N 次以上\"的真打转。\n *\n * 设计约束(RFC-104 R6 延续):只用包内已有信号——工具签名(复用 `toolCallSignature`)、\n * readonly 旗标、结果 isError。零宿主回调、零文件系统探测。\n *\n * 与 `ProgressWindowStats` 分离不共享状态(RFC-203 D4):后者生命周期是\"预算周期\"\n * (扩展/followUp 清零),本检测器是跨预算周期滑动的 turn 环形窗——清零时机不同,\n * 共享会互相污染。两者从 `recordToolCall` 同一挂点各自记账。\n */\nimport { ENGINE_DEFAULTS } from './engine-nodes'\nimport { toolCallSignature } from './progress-window'\n\n/** 一个 turn 的工具调用记录桶。 */\ninterface TurnBucket {\n /** 本 turn 内全部工具调用签名(含重复)。 */\n signatures: string[]\n /** 本 turn 内 readonly!==true 且 !isError 的调用数。 */\n writeToolSuccesses: number\n}\n\nexport interface StallDetectorOptions {\n /** 滑动窗口大小(turn 数)。缺省 ENGINE_DEFAULTS.stallWindowTurns。 */\n windowTurns?: number\n /** 同一签名重复次数阈值(≥ 即判停滞)。缺省 ENGINE_DEFAULTS.stallRepeatThreshold。 */\n repeatThreshold?: number\n}\n\nexport interface StallEvaluation {\n stalled: boolean\n /** 窗口内最大单签名重复次数(stalled=false 时也返回,供事件/诊断)。 */\n maxRepeat: number\n /** 求值时窗口内的 turn 数。 */\n windowTurns: number\n}\n\n/**\n * 停滞检测器。turn 内经 `recordCall` 累积;`onTurnEnd` 在每 turn 边界(`classifyTurnOutcome`\n * 的 stall handler 求值前,利用其每 turn 恰好一次的既有调用节奏——不新增边界信号,\n * RFC-203 评审 H1)切桶;`evaluate` 对已关闭的窗口桶求值。\n */\nexport class StallDetector {\n private readonly windowTurns: number\n private readonly repeatThreshold: number\n private readonly buckets: TurnBucket[] = []\n private current: TurnBucket = { signatures: [], writeToolSuccesses: 0 }\n\n constructor(options: StallDetectorOptions = {}) {\n this.windowTurns = options.windowTurns ?? ENGINE_DEFAULTS.stallWindowTurns\n this.repeatThreshold = options.repeatThreshold ?? ENGINE_DEFAULTS.stallRepeatThreshold\n }\n\n /** 每次工具调用完成后记录(tool-turn-runner 的 recordToolCall 同一挂点)。 */\n recordCall(input: { toolName: string; args: unknown; readonly: boolean; isError: boolean }): void {\n // 签名上限防护:单 turn 内超长调用列表截断记录(真打转的签名重复远早于此上限)。\n if (this.current.signatures.length < ENGINE_DEFAULTS.progressSignatureCap) {\n this.current.signatures.push(toolCallSignature(input.toolName, input.args))\n }\n if (!input.readonly && !input.isError) {\n this.current.writeToolSuccesses++\n }\n }\n\n /**\n * turn 边界切桶:当前桶归档进环形窗(超窗滑出最旧桶),开新桶。\n * 纯文本 turn(零工具调用)同样产生空桶——纯文本打转也是停滞形态,正确参与窗口计数。\n */\n onTurnEnd(): void {\n this.buckets.push(this.current)\n if (this.buckets.length > this.windowTurns) {\n this.buckets.shift()\n }\n this.current = { signatures: [], writeToolSuccesses: 0 }\n }\n\n /** 对已关闭的窗口桶求值(O(窗口内调用数))。窗口未满恒不判停。 */\n evaluate(): StallEvaluation {\n const windowTurns = this.buckets.length\n if (windowTurns < this.windowTurns) {\n return { stalled: false, maxRepeat: 0, windowTurns }\n }\n let writeSuccesses = 0\n const counts = new Map<string, number>()\n let maxRepeat = 0\n for (const bucket of this.buckets) {\n writeSuccesses += bucket.writeToolSuccesses\n for (const sig of bucket.signatures) {\n const next = (counts.get(sig) ?? 0) + 1\n counts.set(sig, next)\n if (next > maxRepeat) maxRepeat = next\n }\n }\n const stalled = writeSuccesses === 0 && maxRepeat >= this.repeatThreshold\n return { stalled, maxRepeat, windowTurns }\n }\n\n /** followUp(新用户回合)重置:窗口清空重新累积(同预算基准重置语义)。 */\n reset(): void {\n this.buckets.length = 0\n this.current = { signatures: [], writeToolSuccesses: 0 }\n }\n}\n","import type { TraceableNodeId } from './engine-nodes'\nimport type { ClockPort, DeterminismPorts, IdPort, RandomPort } from './ports'\nimport { systemClock } from './ports'\n\nexport type TraceEventKind = 'lifecycle' | 'stream' | 'tool' | 'nondet' | 'checkpoint'\n\nexport interface TraceEvent {\n ts: number\n sessionId: string\n turn?: number\n kind: TraceEventKind\n /** 存档/回放事件保留 `string`——历史 trace 与跨版本节点名须容忍读取(生产侧由 Draft 编译期约束)。 */\n node?: string\n payload?: unknown\n parentEventId?: string\n causalChain?: string[]\n evalRunId?: string\n}\n\nexport interface TraceEventDraft {\n kind: TraceEventKind\n /** 生产侧 trace 锚点编译期绑回 ENGINE_NODES(traceable 子集)——拼错/未登记即编译错,取代正则扫描守。 */\n node?: TraceableNodeId\n turn?: number\n payload?: unknown\n parentEventId?: string\n causalChain?: string[]\n evalRunId?: string\n}\n\nexport interface TraceRecorder {\n record(draft: TraceEventDraft): void\n}\n\nexport interface TraceSink {\n append(event: TraceEvent): void | Promise<void>\n}\n\nexport interface NondetDraw {\n source: 'clock' | 'random' | 'uuid'\n value: number | string\n}\n\nexport interface TraceRecorderOptions {\n sink: TraceSink\n sessionId: string\n clock?: ClockPort\n onError?: (error: unknown) => void\n}\n\nexport function createTraceRecorder(options: TraceRecorderOptions): TraceRecorder {\n const { sink, sessionId } = options\n const clock = options.clock ?? systemClock\n const onError = options.onError ?? (() => {})\n\n return {\n record(draft) {\n const event: TraceEvent = {\n ts: clock.now(),\n sessionId,\n turn: draft.turn,\n kind: draft.kind,\n node: draft.node,\n payload: draft.payload,\n parentEventId: draft.parentEventId,\n causalChain: draft.causalChain,\n evalRunId: draft.evalRunId,\n }\n try {\n const maybe = sink.append(event)\n if (maybe instanceof Promise) {\n maybe.catch(onError)\n }\n } catch (error) {\n onError(error)\n }\n },\n }\n}\n\nexport const noopRecorder: TraceRecorder = { record() {} }\n\n/**\n * RFC-102 D1:stream 事件持久化白名单(8/12 StreamEvent 类型)。\n * delta 三类(text/thinking/tool_call_delta)只走内存旁路(emitter/traceLive),不落盘——\n * 实测单会话 delta 占 trace 字节 95%(260MB/24 万行),且无持久化消费方依赖其正确性\n * (replay 终态取 done.message,traceback/otel 只读 done 与 tool.call.end)。\n * `error` 类型由 work-loop 前置过滤从未落盘;若未来移除该过滤须同步加入本白名单。\n */\nconst PERSISTED_STREAM_EVENT_TYPES = new Set([\n 'start',\n 'text_start',\n 'text_end',\n 'thinking_start',\n 'thinking_end',\n 'tool_call_start',\n 'tool_call_end',\n 'done',\n])\n\nexport function shouldPersistStreamEvent(type: string): boolean {\n return PERSISTED_STREAM_EVENT_TYPES.has(type)\n}\n\n/** `*_end` 事件类型(携带 partial 全量累积快照,D2 剥离目标)。 */\nconst BLOCK_END_TYPES = new Set(['text_end', 'thinking_end', 'tool_call_end'])\n\nfunction stripPartial(payload: Record<string, unknown>): Record<string, unknown> {\n if (!('partial' in payload)) return payload\n const { partial: _dropped, ...rest } = payload\n return rest\n}\n\nexport interface StreamTracePersister {\n /** 喂入一个 stream 事件(按到达序);内部按白名单过滤 + 滞后一拍剥离 partial。 */\n push(payload: { type?: string } & Record<string, unknown>, turn?: number): void\n /**\n * 段结束冲洗(abort/流式错误/循环退出时调用;幂等)。\n * 若缓存中还有未落盘的 `*_end`(= 段内无 done 到达),**保留 partial** 写出——\n * replay 对无 done 段 fallback 到最后事件的 partial(replay.ts),无条件剥离会把\n * abort 段回放从「降级」变「完全失败」(RFC-102 D2,真实 trace 8/1306 段无 done)。\n */\n flush(): void\n}\n\n/**\n * RFC-102 D1+D2:stream 事件的 trace 持久化器。\n * 白名单过滤 delta;`*_end` 缓存一拍——下一事件到达说明段还在继续(其 partial 已被\n * 后续事件覆盖,安全剥离);段边界(flush)到达则保留 partial(abort 保护)。\n */\nexport function createStreamTracePersister(recorder: TraceRecorder): StreamTracePersister {\n let buffered: { payload: Record<string, unknown>; turn?: number } | undefined\n\n const flushBuffered = (retainPartial: boolean): void => {\n if (!buffered) return\n const { payload, turn } = buffered\n buffered = undefined\n recorder.record({\n kind: 'stream',\n node: 'stream.event',\n turn,\n payload: retainPartial ? payload : stripPartial(payload),\n })\n }\n\n return {\n push(payload, turn) {\n const type = payload.type\n if (typeof type !== 'string' || !shouldPersistStreamEvent(type)) {\n return\n }\n // 有事件跟进 = 段仍在推进,缓存的 *_end 可安全剥离 partial\n flushBuffered(false)\n if (BLOCK_END_TYPES.has(type)) {\n buffered = { payload, turn }\n return\n }\n recorder.record({ kind: 'stream', node: 'stream.event', turn, payload })\n },\n flush() {\n flushBuffered(true)\n },\n }\n}\n\n/**\n * RFC-145 D3:录制端口 + nondet 批量冲洗。draw 不再逐条落盘(每条即一次 trace append,\n * 实测占 trace 行数 36%、行数第一——RFC-102 白名单只治理 stream 类,nondet 是其盲区),\n * 改为进程内累积、由宿主在回合边界与 work-loop 退出路径调用 `flushNondetBatch()`\n * 落成单条 batch 事件 `{ kind:'nondet', payload:{ draws: NondetDraw[] } }`。\n *\n * batch 事件不设 node 字段——与既有逐条 nondet 记录一致(TraceEventDraft.node 是\n * TraceableNodeId 编译期联合,nondet 类事件从不设 node);消费侧以\n * `Array.isArray(payload.draws)` 区分 batch 与旧逐条形态(extractNondetDraws 双形态兼容)。\n *\n * 崩溃语义(RFC-145 D3 显式承认):崩溃时当前回合未 flush 的 draws 丢失 → 该回合重放\n * underrun。可接受降级——重放以回合为单元消费(done.message 为终态权威),与 RFC-102 D2\n * 的 abort 段降级语义一致。\n */\nexport interface RecordingPorts extends DeterminismPorts {\n /** 把累积的 nondet draws 落成单条 batch 事件并清空(空累积时 no-op,幂等)。 */\n flushNondetBatch(): void\n}\n\n/** 单回合 draws 聚合上限(防异常膨胀);超限先落一条 batch 再重新累积。 */\nconst NONDET_BATCH_MAX_DRAWS = 4096\n\nexport function createRecordingPorts(\n base: DeterminismPorts,\n recorder: TraceRecorder,\n): RecordingPorts {\n let draws: NondetDraw[] = []\n\n const flushNondetBatch = (): void => {\n if (draws.length === 0) return\n const batch = draws\n draws = []\n recorder.record({ kind: 'nondet', payload: { draws: batch } })\n }\n const push = (draw: NondetDraw): void => {\n draws.push(draw)\n if (draws.length >= NONDET_BATCH_MAX_DRAWS) {\n flushNondetBatch()\n }\n }\n\n const clock: ClockPort = {\n now() {\n const value = base.clock.now()\n push({ source: 'clock', value })\n return value\n },\n }\n const random: RandomPort = {\n random() {\n const value = base.random.random()\n push({ source: 'random', value })\n return value\n },\n }\n const id: IdPort = {\n uuid() {\n const value = base.id.uuid()\n push({ source: 'uuid', value })\n return value\n },\n }\n return { clock, random, id, flushNondetBatch }\n}\n\nexport function createReplayPorts(draws: readonly NondetDraw[]): DeterminismPorts {\n const queues: Record<NondetDraw['source'], (number | string)[]> = {\n clock: [],\n random: [],\n uuid: [],\n }\n for (const draw of draws) {\n queues[draw.source].push(draw.value)\n }\n\n const next = (source: NondetDraw['source']): number | string => {\n const queue = queues[source]\n if (queue.length === 0) {\n throw new Error(`Replay underrun: no more recorded \"${source}\" draws`)\n }\n return queue.shift() as number | string\n }\n\n return {\n clock: { now: () => next('clock') as number },\n random: { random: () => next('random') as number },\n id: { uuid: () => next('uuid') as string },\n }\n}\n\n/**\n * RFC-145 D3 双形态兼容:同时接受旧的逐条 NondetDraw(存量 trace)与新的 batch\n * `{draws: NondetDraw[]}`(展开,内部顺序 = 录制顺序)。旧形态兼容在存量 trace\n * 存在期内不许移除(RFC-145 §5 规则 3)。\n */\nexport function extractNondetDraws(events: Iterable<TraceEvent>): NondetDraw[] {\n const draws: NondetDraw[] = []\n for (const event of events) {\n if (event.kind !== 'nondet') continue\n const payload = event.payload as { draws?: unknown } | null | undefined\n if (payload && Array.isArray(payload.draws)) {\n for (const draw of payload.draws) {\n if (isNondetDraw(draw)) draws.push(draw)\n }\n } else if (isNondetDraw(event.payload)) {\n draws.push(event.payload)\n }\n }\n return draws\n}\n\nfunction isNondetDraw(payload: unknown): payload is NondetDraw {\n return (\n typeof payload === 'object' && payload !== null && 'source' in payload && 'value' in payload\n )\n}\n","import { getToolCallsByAssistantMessage, hasToolCalls } from '@x-otto/interchange'\nimport { ContextPipeline, createTurnMessageTransforms, isThinkingLevel } from './context-pipeline'\nimport { buildSnapshot, consumePause, summarizeMessages } from './debug-port'\nimport { systemClock } from './ports'\nimport { noopRecorder } from './trace'\n\nimport type {\n AssistantMessage,\n ContentPart,\n Message,\n Model,\n ThinkingLevel,\n ToolResultMessage,\n Usage,\n} from '@x-otto/interchange'\nimport type { CacheRetention } from '@x-otto/provider'\nimport type { HookInput, HookOutput, InterceptorTiming, ObserverTiming } from '@x-otto/hook-contracts'\nimport type { DebugPort, DebugSnapshot } from './debug-port'\nimport type { TraceableNodeId } from './engine-nodes'\nimport type { ClockPort } from './ports'\nimport type { TraceRecorder } from './trace'\nimport type { AgentMessage, AgentRunContext, AgentTool } from './types'\n\nexport interface PipelineHooks {\n execute<T extends InterceptorTiming>(\n timing: T,\n input: HookInput<T>,\n output: HookOutput<T>,\n ): Promise<void>\n emit<T extends ObserverTiming>(timing: T, input: HookInput<T>): Promise<void>\n}\n\nfunction userMessageText(message: Message): string | undefined {\n if (typeof message.content === 'string') return message.content\n return message.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .filter((text) => text.length > 0)\n .join('\\n')\n}\n\nexport interface PersistencePort {\n messages(): readonly AgentMessage[]\n append(message: AgentMessage): void\n buildContext(): { messages: AgentMessage[]; summary?: string }\n persistNew(messages: AgentMessage[], fromIndex: number): void\n /**\n * 自动压缩(keep-recent)写回——把摘要 + 保留的近期消息记入会话树\n * (replacement_history 模型),使压缩成为持久事件:重启原样恢复、不每轮重算。可选(旧适配器可不实现)。\n */\n recordCompaction?(summary: string, replacement: AgentMessage[]): void\n /**\n * 裁剪(prune)发生时的 UI 通知——无持久化语义,仅供 AgentSession publish memory.pruned 会话事件\n * 驱动「裁剪已启用」提示。与 recordCompaction 同为「agent 包调、runtime 实现并 publish」的 facade 桥。\n * 可选(旧适配器/无 UI 时可不实现)。\n *\n * `metrics`:裁剪明细(prunedCount/messagesBefore/messagesAfter,可选——供 `/compact log`\n * 详情面板展示\"裁剪了多少条工具输出\",而非仅有 tokensSaved 单一数字)。\n */\n notifyPrune?(\n tokensSaved: number,\n metrics?: { prunedCount: number; messagesBefore: number; messagesAfter: number },\n ): void\n /**\n * RFC-283 D2:移除最后一条 append 的 message entry(内存态回滚)。\n * 用于 prompt 失败时回滚从未到达 LLM 的 user message。\n * 只移除最后一条 message 类型 entry,回退 _leafId;不触碰 compaction/clear 等结构 entry。\n * 只在 prompt 执行期间(save 之前)调用,不违反 RFC-159 save 路径不删行约束。\n */\n removeLast?(): void\n /**\n * RFC-336 D2:**输入边界**落盘请求——本轮 user message 已进会话树、且上下文准备\n * (planPromptContext)已成功,但尚未开始调用模型。宿主据此把新增 entries 落库。\n *\n * 要解决的问题:落库此前只由 `turn.end`/`prompt.end` 触发(RFC-305),两者都在**首个\n * 模型回合完成之后**。故\"用户已回车、模型还没吐字\"这段窗口(≈ 一次完整模型往返,\n * 数秒~数十秒)内进程被强杀,那条 user message 从未落库。RFC-336 M1 已保证会话行\n * 存在(不再整个蒸发),本钩子进一步保证**消息本身**不丢。\n *\n * **调用时机固定在 prep 成功之后**(RFC-336 §D3 grill 定案方向 A,规则 7):不得前移到\n * `append()` 紧后——prep 失败时 RFC-283 D2 要 `removeLast()` 做内存回滚,若此前已落库\n * 就会造成内存与 DB 不一致。放在 prep 成功后,两者语义天然自洽。\n *\n * **契约**:实现方必须 fire-and-forget(不得阻塞工作循环,RFC-336 规则 3)、必须复用\n * 宿主既有的 per-session 串行写链(不得新开并发写路径,规则 2/9)。可选:旧适配器/\n * 无持久化场景不实现即退回改动前行为(首个 turn 后才落库)。\n */\n persistInputBoundary?(): void\n}\n\nexport interface MemoryPort {\n needsPrune(messages: readonly Message[]): boolean\n needsCompaction(messages: readonly Message[]): boolean\n /**\n * `/clear`——驱逐某会话的工作记忆(滚动压缩摘要桶)。可选:注入式/无 memory\n * 场景安全跳过。清后下一轮压缩从空摘要重建,不再把已清内容的摘要喂回模型。\n */\n clearSession?(sessionId: string): void\n /**\n * 审计修复:会话销毁/驱逐时遗忘其全部 per-session 状态(摘要桶 + 压缩游标 + archive 计数)。\n * 区别于 clearSession(/clear 保留 archive 计数,会话继续):forget 是终态清理,防内存无界增长。\n */\n forgetSession?(sessionId: string): void\n /**\n * 模型切换时更新上下文窗口 + 重算 compaction/prune 触发阈值。\n * 切换低窗口模型后立即调用,防止下一轮 413。\n */\n reconfigure?(model: { contextWindow: number; maxOutput: number }): void\n /**\n * 主动检查当前消息量是否超过压缩触发线;超过则 force-compact,\n * 返回压缩后消息 + compacted flag + summary/replacement(供调用方写回 session 树)。\n */\n ensureFitsWindow?(\n messages: readonly Message[],\n sessionId?: string,\n signal?: AbortSignal,\n ): Promise<{ messages: Message[]; compacted: boolean; summary?: string; replacement?: Message[] }>\n process(\n messages: readonly Message[],\n sessionId?: string,\n signal?: AbortSignal,\n options?: { force?: boolean; skipPrune?: boolean },\n ): Promise<{\n messages: Message[]\n pruned: boolean\n compacted: boolean\n summary?: string\n replacement?: Message[]\n archivePath?: string\n pruneMetrics?: {\n prunedCount: number\n tokensSaved: number\n messagesBefore: number\n messagesAfter: number\n }\n }>\n}\n\nexport interface TurnRunner {\n run(context: AgentRunContext): Promise<AssistantMessage>\n}\n\nexport interface PromptParams {\n systemPrompt: string\n thinkingLevel: ThinkingLevel\n temperature?: number\n maxTokens?: number\n /** prompt cache 保留档(none/short/long);由会话据特征选档,透传至 provider。 */\n cacheRetention?: CacheRetention\n /**\n * 会话恒定的 **volatile system 尾段种子**(落在 cache 断点之后,不击穿 stable 前缀缓存)。\n * 由宿主据会话配置填充——典型用例:append 模式子代理的角色块(保留共享 base system prompt 进缓存,\n * 角色差异走断点后的尾段)。transformSystemPrompt 以此为 systemTail 起点,hook(phase/memory-delta)\n * 续接其后。\n */\n systemTail?: string[]\n}\n\nexport interface PromptInput {\n text: string\n /**\n * RFC-111 D4b:可选的图片附件,随本轮用户消息一起注入上下文(如飞书桥接下载的图片)。\n * 缺省(`undefined`)时行为与改动前完全一致——`createUserMessage` 只产出纯文本 content\n * (§6 规则7 向后兼容红线)。\n */\n images?: Array<{ type: 'image'; data: string; mediaType: string }>\n /**\n * RFC-094:引擎注入的内部触发消息(todo 闸门续跑提示)——模型需要看到、但不渲染给用户\n * (对齐 RFC-050 REV-1 internal 语义;provider 序列化忽略此字段)。\n */\n internal?: boolean\n sessionId: string\n agentName: string\n model: Model\n tools: AgentTool[]\n params: PromptParams\n runner: TurnRunner\n hooks: PipelineHooks\n persistence: PersistencePort\n memory?: MemoryPort\n debug?: DebugPort\n clock?: ClockPort\n recorder?: TraceRecorder\n /** RFC-180:turn 边界暂停请求探针——透传给 runner.run() 的 AgentRunContext.isPauseRequested。 */\n isPauseRequested?: () => boolean\n /**\n * RFC-181 D3/M3:图像委托描述端口(宿主 VisionDelegateService 实现)——透传给\n * runner.run() 的 AgentRunContext.describeImages(见 work-loop.ts vision-gate.ts\n * 完整设计说明)。缺省 = 主模型不支持视觉时,image block 降级为占位文本。\n */\n describeImages?: import('./vision-gate').DescribeImagesPort\n}\n\nexport interface PromptResult {\n cancelled: boolean\n message?: AssistantMessage\n newMessages: AgentMessage[]\n /** D1:usage 增量聚合结果——AgentSession.promptUsage/hadToolActivity 不再扫描 newMessages 数组。 */\n newMessagesUsageSummary: NewMessagesAccumulator\n params: PromptParams\n}\n\n/** D1:消息切片元数据聚合——替代 newMessages 数组的 usage/hadTool 扫描路径。 */\nexport interface NewMessagesAccumulator {\n usage: Usage\n hadToolActivity: boolean\n lastAssistantApi: string | undefined\n count: number\n}\n\nfunction emptyUsageAccumulator(): NewMessagesAccumulator {\n return {\n usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },\n hadToolActivity: false,\n lastAssistantApi: undefined,\n count: 0,\n }\n}\n\n/** 把一批消息切片并入累加器(原地更新)——供 advanceCompactionFrom/persistTurn/末尾收尾三处共用。 */\nfunction accumulateUsage(acc: NewMessagesAccumulator, slice: readonly AgentMessage[]): void {\n for (const message of slice) {\n if ((message as { role?: string }).role !== 'assistant') continue\n const assistant = message as AssistantMessage\n acc.usage.inputTokens += assistant.usage.inputTokens\n acc.usage.outputTokens += assistant.usage.outputTokens\n acc.usage.cacheReadTokens += assistant.usage.cacheReadTokens\n acc.usage.cacheWriteTokens += assistant.usage.cacheWriteTokens\n acc.lastAssistantApi = assistant.api\n acc.count += 1\n if (hasToolCalls(assistant)) {\n acc.hadToolActivity = true\n }\n }\n}\n\ntype SnapshotMetadata = Record<string, string | number | boolean | null>\n\ninterface PromptPauseController {\n pause(\n point: DebugSnapshot['point'],\n snapshotMessages: readonly AgentMessage[],\n metadata: SnapshotMetadata,\n ): Promise<void>\n}\n\ninterface RunTurnOptions {\n agentName: string\n sessionId: string\n model: Model\n tools: AgentTool[]\n runner: TurnRunner\n hooks: PipelineHooks\n memory?: MemoryPort\n params: PromptParams\n systemPrompt: string\n systemTail: string[]\n messages: AgentMessage[]\n recorder?: TraceRecorder\n /** 用于自动压缩 keep-recent 写回(persistence.recordCompaction)。 */\n persistence: PersistencePort\n /**\n * 某轮压缩后回调(RFC-170 D1 扩展签名,见 context-pipeline.ts TurnMessageTransformDeps\n * 同名字段的完整设计说明):baseLen=该轮上下文消息数,replacement=压缩保留的近期消息。\n * prompt() 据此原地收缩循环持有的 `messages` 数组(不仅仅推进 persistFrom 索引)——\n * 这是 RFC-170 D1 的核心修复:此前只推进索引,`messages` 数组本身从不收缩,长循环\n * (数百 turn)场景下被压缩折叠的旧消息持续驻留在进程内存直到整个 prompt 结束。\n */\n onCompaction?: (baseLen: number, replacement: AgentMessage[]) => void\n /**\n * RFC-170 D4:turn 边界增量持久化回调——透传给 runner.run() 的 AgentRunContext.onTurnPersist\n * (见该字段完整设计说明)。work-loop.ts 在每个 turn 的 terminate/continue/run-tools 三条\n * 安全出路(均已确认无未配对 tool_use)各调用一次。\n */\n onTurnPersist?: () => void\n /** RFC-180:turn 边界暂停请求探针(见 PromptInput.isPauseRequested 完整设计说明)。 */\n isPauseRequested?: () => boolean\n /** RFC-181 D3/M3:图像委托描述端口(见 PromptInput.describeImages 完整设计说明)。 */\n describeImages?: import('./vision-gate').DescribeImagesPort\n}\n\nfunction createPromptPauseController(\n debug: DebugPort | undefined,\n params: PromptParams,\n clock: ClockPort,\n): PromptPauseController {\n return {\n async pause(point, snapshotMessages, metadata) {\n const result = await debug?.pause(\n buildSnapshot({\n point,\n messagesCount: snapshotMessages.length,\n metadata,\n systemPrompt: params.systemPrompt,\n messagesSummary: summarizeMessages(snapshotMessages, 10),\n llmParams: {\n temperature: params.temperature,\n maxTokens: params.maxTokens,\n thinkingLevel: params.thinkingLevel,\n },\n }),\n )\n\n consumePause(result, {\n now: () => clock.now(),\n setSystemPrompt: (value) => {\n params.systemPrompt = value\n },\n setTemperature: (value) => {\n params.temperature = value\n },\n setMaxTokens: (value) => {\n params.maxTokens = value\n },\n setThinkingLevel: (value) => {\n if (isThinkingLevel(value)) {\n params.thinkingLevel = value\n }\n },\n })\n },\n }\n}\n\n/**\n * RFC-111 D4b:`images` 缺省或空数组时产出与改动前完全一致的纯文本 content\n * (`[{type:'text',text}]`),保持向后兼容(§6 规则7)。有图片时追加 `ImageContent`\n * 元素——图片顺序在文本之后,对齐仓库既有工具结果消息(`ToolResultMessage`)的排列惯例。\n */\nfunction createUserMessage(\n text: string,\n clock: ClockPort,\n internal?: boolean,\n images?: PromptInput['images'],\n): Message {\n const content: ContentPart[] = [{ type: 'text', text }]\n if (images && images.length > 0) {\n for (const image of images) {\n content.push({ type: 'image', mime: image.mediaType, source: image.data })\n }\n }\n return {\n role: 'user',\n timestamp: clock.now(),\n content,\n ...(internal ? { internal: true } : {}),\n }\n}\n\n/**\n * 把 catch 捕获的真实终止原因转成占位文本,替代通用\"aborted or errored\"。\n * 中断时传真实 errorMessage / 'Interrupted by user' 而非笼统文案,让模型看到确切终止原因。\n * 无 reason(直接调用/未知)时退回原通用文案,保持向后兼容(既有测试不传 reason)。\n */\nfunction sealReasonText(reason?: { code?: string; message?: string }): string {\n if (reason?.code === 'AGENT_ABORTED') return 'Tool call interrupted: aborted by user.'\n if (reason?.message) return `Tool call interrupted: ${reason.message}`\n return 'Tool call interrupted before completion (session aborted or errored).'\n}\n\n/**\n * RFC-118 M1(T1b):tool_use↔tool_result 补配对的**唯一共享实现**。\n * 两个调用场景语义不同,差异经参数显式化(合并前是 pipeline/work-loop 两份 80% 重复实现):\n * - pipeline abort 封口:`isError: true`(错误终止),扫描范围 = 末条 assistant 之后(`resolvedFrom`)。\n * - work-loop steering 封口:`isError: false`(有意跳过非失败,标 error 会诱导模型道歉/重试而非\n * 干净转向——用中性 aborted 占位而非 error-framed repair),全量扫描。\n * 不补 tool_result 的后果:dangling tool_use 落树 → buildContext 回放 → provider 400(fatal) → 会话卡死。\n */\nexport function sealToolCalls(\n messages: AgentMessage[],\n toolCalls: readonly { id: string; name: string }[],\n clock: ClockPort,\n options: {\n isError: boolean\n text: string\n /** 计算已配对 tool_result 的扫描范围;缺省全量扫描 messages。 */\n resolvedFrom?: readonly AgentMessage[]\n /** 合成的 tool_result 是否标记 internal(不渲染给用户)。缺省 false。 */\n internal?: boolean\n },\n): void {\n const scan = options.resolvedFrom ?? messages\n const resolved = new Set(\n scan\n .filter((m): m is ToolResultMessage => m.role === 'tool_result')\n .map((m) => m.toolCallId),\n )\n for (const tc of toolCalls) {\n if (!resolved.has(tc.id)) {\n messages.push({\n role: 'tool_result',\n toolCallId: tc.id,\n toolName: tc.name,\n content: [{ type: 'text', text: options.text }],\n isError: options.isError,\n details: {},\n timestamp: clock.now(),\n ...(options.internal ? { internal: true } : {}),\n } as AgentMessage)\n }\n }\n}\n\n/**\n * 引擎 review Tier-1 #2:中断/出错前,给最后一条 assistant 消息里**未配对的 tool_use** 补一条\n * isError tool_result。只封最后一条 assistant:常规流每轮 tool_use 在下一条 assistant 前已配对,\n * abort 仅悬空末轮。共享封口逻辑见 sealToolCalls。\n */\nexport function sealUnpairedToolCalls(\n messages: AgentMessage[],\n clock: ClockPort,\n reason?: { code?: string; message?: string },\n): void {\n let lastAssistantIdx = -1\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i]?.role === 'assistant') {\n lastAssistantIdx = i\n break\n }\n }\n if (lastAssistantIdx === -1) {\n return\n }\n const toolCalls = getToolCallsByAssistantMessage(messages[lastAssistantIdx] as AssistantMessage)\n if (toolCalls.length === 0) {\n return\n }\n sealToolCalls(messages, toolCalls, clock, {\n isError: true,\n text: sealReasonText(reason),\n resolvedFrom: messages.slice(lastAssistantIdx + 1),\n })\n}\n\nasync function runTurnLoop(options: RunTurnOptions): Promise<AssistantMessage> {\n const {\n agentName,\n sessionId,\n model,\n tools,\n runner,\n hooks,\n memory,\n params,\n systemPrompt,\n systemTail,\n messages,\n recorder,\n persistence,\n onCompaction,\n onTurnPersist,\n isPauseRequested,\n describeImages,\n } = options\n\n const { transformContext, reactiveCompact } = createTurnMessageTransforms({\n agentName,\n sessionId,\n tools,\n hooks,\n memory,\n recorder,\n persistence,\n onCompaction,\n })\n\n return runner.run({\n model,\n systemPrompt,\n systemTail,\n cacheRetention: params.cacheRetention,\n tools,\n messages,\n thinkingLevel: params.thinkingLevel,\n temperature: params.temperature,\n maxTokens: params.maxTokens,\n transformContext,\n reactiveCompact,\n onTurnPersist,\n isPauseRequested,\n describeImages,\n })\n}\n\nfunction recordLifecycle(\n recorder: TraceRecorder,\n node: TraceableNodeId,\n payload: Record<string, unknown>,\n): void {\n recorder.record({ kind: 'lifecycle', node, payload })\n}\n\nexport async function prompt(input: PromptInput): Promise<PromptResult> {\n const { text, sessionId, agentName, model, tools, runner, hooks, persistence, memory, debug } =\n input\n const clock = input.clock ?? systemClock\n const recorder = input.recorder ?? noopRecorder\n\n const params: PromptParams = { ...input.params }\n const pause = createPromptPauseController(debug, params, clock)\n\n const messagesBeforePrompt = persistence.messages()\n const isFirstMessage = messagesBeforePrompt.length === 0\n\n await pause.pause('prompt.before', messagesBeforePrompt, {\n sessionId,\n isFirstMessage,\n })\n recordLifecycle(recorder, 'prompt.before', {\n sessionId,\n isFirstMessage,\n text,\n })\n\n const beforeInput = {\n message: createUserMessage(text, clock, input.internal, input.images),\n sessionId,\n isFirstMessage,\n metadata: {},\n }\n const beforeOutput = {\n message: beforeInput.message,\n cancelled: false,\n metadata: {},\n }\n\n await hooks.execute('chat.message.before', beforeInput, beforeOutput)\n\n if (beforeOutput.cancelled) {\n return {\n cancelled: true,\n newMessages: [],\n newMessagesUsageSummary: emptyUsageAccumulator(),\n params,\n }\n }\n\n const userMessage = beforeOutput.message as Message\n\n // RFC-283 D2:prep 阶段(append + planPromptContext)单独 try-catch——\n // 此阶段失败意味着 user message 从未到达 LLM,回滚(removeLast)。\n // workLoop 阶段失败(已有 catch 块)不回滚(消息已是事实)。\n let messages: AgentMessage[]\n let messagesBefore: number\n let effectiveSystemPrompt: string\n let systemTail: string[] | undefined | undefined\n try {\n persistence.append(userMessage)\n const plan = await ContextPipeline.planPromptContext({\n persistence,\n clock,\n hooks,\n params,\n model,\n sessionId,\n tools,\n userText: userMessageText(userMessage),\n onContextSelected: async (selected, hasSummary) => {\n await pause.pause('context.build', selected, { sessionId, hasSummary })\n recordLifecycle(recorder, 'context.build', {\n sessionId,\n hasSummary,\n messages: selected.length,\n })\n },\n })\n messages = plan.messages\n messagesBefore = plan.messagesBefore\n effectiveSystemPrompt = plan.stableSystem\n systemTail = plan.systemTail\n // RFC-336 D2:输入边界落盘——prep 已成功(user message 进树 + 上下文就绪),\n // 此后即将进入模型往返(数秒~数十秒的崩溃窗口)。在此请求落库,使强杀不再丢失\n // 用户那条输入。fire-and-forget,不阻塞下方 runTurnLoop。\n // 位置不可前移到 append() 紧后:prep 失败要走 removeLast() 内存回滚(RFC-283 D2),\n // 提前落库会造成内存/DB 不一致(RFC-336 规则 7,grill 定案方向 A)。\n persistence.persistInputBoundary?.()\n } catch (prepError) {\n // RFC-283 D2:prep 阶段失败——user message 从未到达 LLM,回滚\n persistence.removeLast?.()\n throw prepError\n }\n\n // RFC-170 D1+D4:`persistedCursor` 是\"已落树/已归档基准\"的单一真源,被两个独立触发点\n // 共同推进:① compaction 折叠(onCompaction,原 D1 的 compactionFrom)——把 `messages`\n // 原地 splice 收缩为 `replacement`(不含摘要,摘要已由 recordCompaction 单独写入会话树\n // 的 compaction 条目),游标重置为 replacement.length;② turn 边界增量持久化\n // (onTurnPersist,RFC-170 D4 新增)——把游标之后的新消息 persistNew 落树,游标推进到\n // messages.length(数组本身不收缩,只是\"已确认落树\")。\n //\n // 两者共享同一个 `newMessagesAcc` 累加器:无论消息是\"被 compaction 折叠\"还是\"被\n // turn-persist 提前落树\"离开了\"游标之后的待处理窗口\",都必须先归档进 newMessagesAcc\n // 再推进游标——这保证 `newMessages`(prompt() 返回值,AgentSession.promptUsage 的\n // token/cost 统计唯一数据源)语义完整,不因为中途落树/折叠而漏计(独立评审后自查发现\n // 的缺口,D1 阶段已修复,此处扩展到 D4 的 turn-persist 触发点同样适用)。\n //\n // 正确性保证:`persistedCursor` 严格单调推进(compaction 重置到 replacement.length、\n // turn-persist 推进到 messages.length,两者都是\"游标之后再无待处理内容\");\n // `newMessagesAcc` 的归档区间 [trackFrom, 触发时刻的处理范围) 与游标推进量精确对应,\n // 不重叠、不遗漏。turn-persist 触发时不 splice(数组不收缩,只是心智上\"已落树的前缀\n // 不再需要 persistNew 重复处理\")——数组物理收缩仍只在 compaction 才发生(D1 的内存\n // 收益点),turn-persist 只解决\"崩溃时循环内消息未落盘\"(D4 的持久化收益点),两者\n // 目标正交、机制统一。\n let persistedCursor: number | undefined\n const newMessagesAcc: AgentMessage[] = []\n // D1:并行维护的元数据聚合器——两个闭包(advanceCompactionFrom/persistTurn)每处理\n // 一片消息切片时同步提取 usage/hadToolActivity/lastAssistantApi,不依赖事后扫描\n // newMessages 数组。AgentSession.promptUsage 消费此聚合结果。\n const usageAcc = emptyUsageAccumulator()\n const advanceCompactionFrom = (_baseLen: number, replacement: AgentMessage[]): void => {\n const trackFrom = persistedCursor ?? messagesBefore\n const slice = messages.slice(trackFrom)\n newMessagesAcc.push(...slice)\n accumulateUsage(usageAcc, slice)\n messages.splice(0, messages.length, ...replacement)\n persistedCursor = replacement.length\n }\n // RFC-170 D4:turn 边界增量持久化——work-loop.ts 在每个 turn 的安全出路(无未配对\n // tool_use)各调用一次。把游标之后的新消息落树,防止进程崩溃在 prompt 循环中途时\n // 这些消息从未被持久化(RFC-167 事故场景:单 prompt 数百 turn,进程被 OOM kill 前\n // 只有 prompt 结束时的一次性 persistNew,从未触发)。\n const persistTurn = (): void => {\n const from = persistedCursor ?? messagesBefore\n if (from >= messages.length) return // 无新消息,no-op(幂等,多次调用安全)\n const slice = messages.slice(from)\n newMessagesAcc.push(...slice)\n accumulateUsage(usageAcc, slice)\n persistence.persistNew(messages, from)\n persistedCursor = messages.length\n }\n try {\n const message = await runTurnLoop({\n agentName,\n sessionId,\n model,\n tools,\n runner,\n hooks,\n memory,\n params,\n systemPrompt: effectiveSystemPrompt,\n systemTail,\n messages,\n recorder,\n persistence,\n onCompaction: advanceCompactionFrom,\n onTurnPersist: persistTurn,\n isPauseRequested: input.isPauseRequested,\n describeImages: input.describeImages,\n })\n\n const persistFrom = persistedCursor ?? messagesBefore\n persistence.persistNew(messages, persistFrom)\n const tailSlice = messages.slice(persistFrom)\n accumulateUsage(usageAcc, tailSlice)\n const newMessages = [...newMessagesAcc, ...tailSlice]\n\n await pause.pause('messages.persist', messages, {\n sessionId,\n newMessages: newMessages.length,\n })\n recordLifecycle(recorder, 'messages.persist', {\n sessionId,\n newMessages: newMessages.length,\n })\n\n await hooks.execute('chat.message.after', beforeInput, {\n message: beforeOutput.message,\n cancelled: false,\n metadata: {},\n })\n\n await pause.pause('prompt.after', messages, { sessionId })\n recordLifecycle(recorder, 'prompt.after', { sessionId })\n\n await hooks.emit('session.idle', { sessionId, metadata: {} })\n\n return { cancelled: false, message, newMessages, newMessagesUsageSummary: usageAcc, params }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n sealUnpairedToolCalls(messages, clock, {\n code: (err as { code?: string }).code,\n message: err.message,\n })\n persistence.persistNew(messages, persistedCursor ?? messagesBefore)\n\n await hooks.emit('session.error', { sessionId, metadata: {}, error: err })\n throw err\n }\n}\n","import type { TraceEvent } from './trace'\nimport { stripAnsi } from './tool-result-clamp'\n\nexport interface TraceRecord {\n seq: number\n entry: TraceEvent\n}\n\nexport interface TraceSummary {\n totalEvents: number\n kinds: Record<string, number>\n turns: number\n toolCalls: number\n firstTs?: number\n lastTs?: number\n}\n\nexport interface TraceSpan {\n seq: number\n ts: number\n kind: TraceEvent['kind']\n node?: string\n turn?: number\n digest?: string\n}\n\ntype Records = AsyncIterable<TraceRecord> | Iterable<TraceRecord>\n\nexport async function summarizeTrace(records: Records): Promise<TraceSummary> {\n const summary: TraceSummary = { totalEvents: 0, kinds: {}, turns: 0, toolCalls: 0 }\n\n for await (const { entry } of records) {\n summary.totalEvents++\n summary.kinds[entry.kind] = (summary.kinds[entry.kind] ?? 0) + 1\n if (entry.turn !== undefined && entry.turn + 1 > summary.turns) {\n summary.turns = entry.turn + 1\n }\n if (entry.kind === 'tool' && entry.node === 'tool.call.end') {\n summary.toolCalls++\n }\n if (summary.firstTs === undefined) {\n summary.firstTs = entry.ts\n }\n summary.lastTs = entry.ts\n }\n\n return summary\n}\n\nexport async function traceToSpans(\n records: Records,\n options: { digestMax?: number } = {},\n): Promise<TraceSpan[]> {\n const max = options.digestMax ?? 120\n const spans: TraceSpan[] = []\n\n for await (const record of records) {\n const entry = record.entry\n spans.push({\n seq: record.seq,\n ts: entry.ts,\n kind: entry.kind,\n node: entry.node,\n turn: entry.turn,\n digest: digestTracePayload(entry.payload, max, entry.node),\n })\n }\n\n return spans\n}\n\n/**\n * node-aware 精确摘要(终局审查 2026-07-21 修复):已知事件类型的 payload 形状是\n * 编译期确定的(`TraceEvent.node`),不需要靠字段存在性猜测。`digestTracePayload`\n * 下方的字段形状匹配链条已经因为\"新事件类型带通用字段名(reason/name等)被更早的\n * 通用分支误吞\"产生过两次真实事故(stream.retry、stream.auth-failed 均一度被误摘要\n * 成 `done:<reason>`,靠手动调整分支顺序才压下去)——分支顺序依赖本身就是脆弱性。\n *\n * 本函数按 `node` 做精确分发,命中时**不再落入**下方的形状匹配 fallback 链,新增事件\n * 类型忘记补精确摘要只会退化到通用兜底(可接受),不会误吃其它已知事件的分支。\n * 返回 `undefined` 表示该 node 未收编(或未传 node),调用方回落到形状匹配链。\n */\nfunction digestByNode(node: string, p: Record<string, unknown>, max: number): string | undefined {\n switch (node) {\n case 'stream.auth-failed': {\n const provider = typeof p.provider === 'string' ? p.provider : undefined\n const modelId = typeof p.modelId === 'string' ? p.modelId : undefined\n if (!provider || !modelId) return undefined\n const reason = typeof p.reason === 'string' ? p.reason : ''\n // modelId 本身已含 `<provider>/<model>` 前缀(RFC-105 D3 命名空间约定),provider\n // 字段在此仅用于聚合过滤,展示时不重复拼接避免 \"anthropic/anthropic/claude-...\" 冗余。\n return truncate(`auth-failed ${modelId} (${provider}): ${reason}`, max)\n }\n case 'stream.retry':\n case 'stream.retry.accelerated': {\n const attempt = typeof p.attempt === 'number' ? p.attempt : undefined\n const maxRetries = typeof p.maxRetries === 'number' ? p.maxRetries : undefined\n if (attempt === undefined || maxRetries === undefined) return undefined\n const reason = typeof p.reason === 'string' ? p.reason : ''\n const delay = typeof p.delayMs === 'number' ? ` in ${p.delayMs}ms` : ''\n return truncate(`retry ${attempt}/${maxRetries}${delay}: ${reason}`, max)\n }\n // 终局(2026-07-31):model/effort 切换——observe 时间线/span 树直接可读的审计摘要。\n case 'model.switch': {\n const from = typeof p.from === 'string' ? p.from : null\n const to = typeof p.to === 'string' ? p.to : ''\n return truncate(`model ${from ? `${from} → ` : ''}${to}`, max)\n }\n case 'effort.change': {\n const from = typeof p.from === 'string' ? p.from : null\n const to = typeof p.to === 'string' ? p.to : ''\n return truncate(`effort ${from ? `${from} → ` : ''}${to}`, max)\n }\n // 终局(2026-07-31):回合级重试卡动作 + 流程暂停四态——observe 时间线可读摘要。\n case 'retry.queued': {\n const attempt = typeof p.attempt === 'number' ? p.attempt : undefined\n const m = typeof p.max === 'number' ? p.max : undefined\n if (attempt === undefined || m === undefined) return undefined\n const mode = typeof p.mode === 'string' && p.mode ? ` [${p.mode}]` : ''\n return truncate(`retry ${attempt}/${m}${mode}`, max)\n }\n case 'retry.cancelled': {\n const attempt = typeof p.attempt === 'number' ? p.attempt : undefined\n return attempt === undefined ? 'retry cancelled' : truncate(`retry cancelled (after ${attempt})`, max)\n }\n case 'retry.reset':\n return 'retry counter reset'\n case 'pause.requested':\n return 'pause requested'\n case 'pause.paused':\n return 'paused'\n case 'pause.cancelled':\n return 'pause cancelled'\n case 'pause.resumed':\n return 'resumed'\n default:\n return undefined\n }\n}\n\n/**\n * payload 摘要(归一自旧 cli formatDigest 的富格式 + 旧 service 的长度上限语义):\n * text → 引号包裹;name → `name(args)`;reason → `done:reason`;message → `role:content`;\n * 其余 JSON 截断。大 payload 只截输出长度(stringify 成本与旧实现持平,不引入新风险)。\n *\n * `node` 可选(向后兼容既有 2 参调用点/测试)——传入时优先走 `digestByNode` 精确分发,\n * 未命中或未传时落回下方形状匹配链(见 digestByNode 上方注释的完整设计说明)。\n */\nexport function digestTracePayload(payload: unknown, max = 120, node?: string): string | undefined {\n if (payload === undefined || payload === null) {\n return undefined\n }\n if (typeof payload === 'string') {\n return truncate(payload, max)\n }\n if (typeof payload === 'object') {\n const p = payload as Record<string, unknown>\n\n if (node !== undefined) {\n const precise = digestByNode(node, p, max)\n if (precise !== undefined) return precise\n }\n\n if (typeof p.resultSummary === 'object' && p.resultSummary !== null) {\n const rs = p.resultSummary as Record<string, unknown>\n const toolName = typeof p.name === 'string' ? p.name : 'tool'\n const isErr = rs.isError ? ' ERR' : ''\n const preview =\n Array.isArray(rs.contentPreview) && rs.contentPreview.length > 0\n ? (rs.contentPreview[0] as Record<string, unknown>)\n : undefined\n const text = preview\n ? typeof preview.preview === 'string'\n ? preview.preview\n : typeof preview.text === 'string'\n ? preview.text\n : ''\n : ''\n const dur = typeof p.durationMs === 'number' ? ` ${p.durationMs}ms` : ''\n const cat = typeof p.errorCategory === 'string' ? ` [${p.errorCategory}]` : ''\n return truncate(`${toolName}${isErr}${cat}${dur}: ${text}`, max)\n }\n\n if (typeof p.result === 'object' && p.result !== null) {\n const r = p.result as Record<string, unknown>\n const toolName = typeof p.name === 'string' ? p.name : 'tool'\n const isErr = r.isError ? ' ERR' : ''\n const content = Array.isArray(r.content)\n ? (r.content[0] as Record<string, unknown> | undefined)\n : undefined\n const text = content && typeof content.text === 'string' ? content.text : ''\n return truncate(`${toolName}${isErr}: ${text}`, max)\n }\n\n if (typeof p.text === 'string') {\n return truncate(`\"${p.text}\"`, max)\n }\n if (typeof p.name === 'string') {\n const args =\n typeof p.arguments === 'object' && p.arguments !== null ? JSON.stringify(p.arguments) : ''\n const duration = typeof p.durationMs === 'number' ? ` (${p.durationMs}ms)` : ''\n const idx = typeof p.toolCallIndex === 'number' ? `#${p.toolCallIndex} ` : ''\n return truncate(`${idx}${p.name}(${args})${duration}`, max)\n }\n // stream.auth-failed 专属摘要({provider, modelId, reason})——必须排在通用 `p.reason`\n // 分支之前,否则会被误摘要成 `done:<reason>`(那条分支是为 stream.event 的 done 帧\n // {reason: stopReason} 设计的单字段场景,本 payload 同时带 provider/modelId 更具体的\n // 签名,命中它是真实 bug:`stream.retry`(同样带 reason 字段)此前也一直被这条通用\n // 分支误摘要成 `done:<reason>`,看起来像\"流正常结束、原因是限流\"而非\"正在重试\"——\n // 本次一并修正,不让 stream.auth-failed 重蹈同一个坑。\n if (typeof p.provider === 'string' && typeof p.modelId === 'string') {\n const reason = typeof p.reason === 'string' ? p.reason : ''\n // modelId 本身已含 `<provider>/<model>` 前缀(RFC-105 D3 命名空间约定),provider\n // 字段在此仅用于聚合过滤(如未来按 provider 统计失败次数),展示时不重复拼接避免\n // \"anthropic/anthropic/claude-...\" 这种冗余。\n return truncate(`auth-failed ${p.modelId} (${p.provider}): ${reason}`, max)\n }\n // stream.retry 专属摘要({attempt, maxRetries, delayMs, reason})——同上,必须排在\n // 通用 `p.reason` 分支之前,否则会被误摘要成 `done:<reason>`。\n if (typeof p.attempt === 'number' && typeof p.maxRetries === 'number') {\n const reason = typeof p.reason === 'string' ? p.reason : ''\n const delay = typeof p.delayMs === 'number' ? ` in ${p.delayMs}ms` : ''\n return truncate(`retry ${p.attempt}/${p.maxRetries}${delay}: ${reason}`, max)\n }\n if (typeof p.reason === 'string') {\n return truncate(`done:${p.reason}`, max)\n }\n if (typeof p.message === 'object' && p.message !== null) {\n const m = p.message as Record<string, unknown>\n if (typeof m.role === 'string' && m.content !== undefined) {\n return truncate(`${m.role}:${JSON.stringify(m.content)}`, max)\n }\n }\n return truncate(JSON.stringify(p), max)\n }\n return truncate(String(payload), max)\n}\n\nfunction truncate(s: string, max: number): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`\n}\n\nexport interface CausalNode {\n eventId: string\n seq: number\n kind: string\n node?: string\n turn?: number\n parentEventId?: string\n causalChain?: string[]\n}\n\nexport interface CausalDag {\n nodes: CausalNode[]\n edges: Array<{ from: string; to: string }>\n roots: string[]\n leaves: string[]\n}\n\n/**\n * 从 trace 事件列表构建因果 DAG(M34-01)。\n * 利用 TraceEvent 中的 parentEventId / causalChain 字段构建有向无环图。\n * 不支持字段的事件降级为平铺列表(每个节点独立为根)。\n */\nexport function causalDag(records: TraceRecord[]): CausalDag {\n const nodeMap = new Map<string, CausalNode>()\n const incoming = new Map<string, string[]>()\n const outgoing = new Map<string, string[]>()\n\n for (const record of records) {\n const ev = record.entry\n const eventId = String(record.seq)\n\n nodeMap.set(eventId, {\n eventId,\n seq: record.seq,\n kind: ev.kind,\n node: ev.node,\n turn: ev.turn,\n parentEventId: ev.parentEventId,\n causalChain: ev.causalChain,\n })\n\n if (ev.parentEventId) {\n const from = ev.parentEventId\n incoming.set(eventId, [...(incoming.get(eventId) ?? []), from])\n outgoing.set(from, [...(outgoing.get(from) ?? []), eventId])\n }\n\n if (ev.causalChain) {\n for (const from of ev.causalChain) {\n if (from === ev.parentEventId) continue\n incoming.set(eventId, [...(incoming.get(eventId) ?? []), from])\n outgoing.set(from, [...(outgoing.get(from) ?? []), eventId])\n }\n }\n }\n\n const events = [...nodeMap.values()]\n const edges: Array<{ from: string; to: string }> = []\n for (const [to, froms] of incoming) {\n for (const from of froms) {\n edges.push({ from, to })\n }\n }\n\n const roots = events.filter((n) => !incoming.has(n.eventId)).map((n) => n.eventId)\n const leaves = events.filter((n) => !outgoing.has(n.eventId)).map((n) => n.eventId)\n\n return { nodes: events, edges, roots, leaves }\n}\n\nexport function traceWhy(records: TraceRecord[], targetSeq: number): string[] {\n const target = records.find((r) => r.seq === targetSeq)\n if (!target?.entry.parentEventId) return []\n\n const path: string[] = [String(targetSeq)]\n let currentId = target.entry.parentEventId\n\n for (let i = 0; i < 100 && currentId; i++) {\n const parent = records.find((r) => r.seq === Number(currentId))\n if (!parent) break\n path.unshift(String(parent.seq))\n currentId = parent.entry.parentEventId ?? ''\n }\n return path\n}\n\nimport type { ToolResult } from '@x-otto/hook-contracts'\n\ninterface CompressedContent {\n type: string\n text?: string\n preview?: string\n totalLength?: number\n}\n\nexport function compressToolResult(\n result: ToolResult,\n maxLength = 512,\n): {\n isError: boolean\n contentCount: number\n contentPreview: CompressedContent[]\n details?: Record<string, unknown>\n} {\n return {\n isError: result.isError ?? false,\n contentCount: result.content.length,\n contentPreview: result.content.map((c) => {\n if (c.type === 'text') {\n const clean = stripAnsi(c.text)\n if (clean.length > maxLength) {\n return { type: 'text', preview: clean.slice(0, maxLength), totalLength: clean.length }\n }\n return { type: 'text', text: clean }\n }\n return c as CompressedContent\n }),\n details: result.details ?? undefined,\n }\n}\n","/**\n * lifecycle-async-tool.ts — RFC-144 M1:lifecycleAsync 工具的 placeholder 构造与\n * system 通知消息构造。\n *\n * 本文件只负责**纯函数式的消息内容构造**,不涉及 runTools 分类接线(M3)或\n * provider 序列化(M2)。职责边界:给定 toolCallId/toolName,产出满足 RFC-144\n * 决策 1 \"Q2 修正\"要求的禁止性措辞 placeholder ToolResultMessage,以及工具真正\n * 完成时的 SystemNotificationMessage。\n */\nimport type { ToolResultMessage, SystemNotificationMessage } from '@x-otto/interchange'\nimport type { ToolResult } from './types'\nimport { stripAnsi } from './tool-result-clamp'\n\n/**\n * RFC-144 决策 1 \"Q2 修正\":placeholder 内容必须是强制性、无歧义的行为约束,而非\n * 状态陈述——LLM 对结构信号(tool_result 存在即代表完成)的服从度高于对文本指令,\n * 简单陈述句(\"已启动,尚未完成\")不足以抑制虚假完成信号。已通过真实 Claude Opus 4.8\n * 端到端验证(2026-07-10,见 RFC M0 执行状态),确认此措辞有效。\n */\nexport function buildLifecycleAsyncPlaceholderText(\n toolName: string,\n taskId: string,\n note?: string,\n): string {\n const base =\n `[BACKGROUND TASK STARTED — DO NOT TREAT AS COMPLETE]\\n` +\n `Tool '${toolName}' (task id: ${taskId}) has started running in the background and has NOT finished yet.\\n` +\n `Do NOT attempt to read its output, assume its result, or take any action that depends on\\n` +\n `its completion. You will receive a separate system message titled '[Tool completion]'\\n` +\n `when — and only when — this specific task actually finishes. Continue with other work\\n` +\n `or wait; do not fabricate or guess the result.`\n // RFC-144 采纳评估:工具级附加警示(如 task_delegate 的\"子代理可能编辑工作区文件\")。\n // 追加在通用禁止性措辞之后,不替换经真实模型验证过的基础文本。\n return note ? `${base}\\n${note}` : base\n}\n\n/**\n * 构造 lifecycleAsync 工具启动时的终态 placeholder tool_result。\n *\n * **终态**:一次写入后永不被替换或重排(见 RFC-144 决策 1、硬约束 1)——从写入的\n * 那一刻起就满足 provider 的 tool_call↔tool_result 1:1 配对约束,不再需要任何\n * 后续的 seal 排除或重排逻辑(原破窗点 #1/#7 因此被架构性消除)。\n */\nexport function buildLifecycleAsyncPlaceholder(\n toolCallId: string,\n toolName: string,\n clock: { now(): number },\n note?: string,\n): ToolResultMessage {\n return {\n role: 'tool_result',\n toolCallId,\n toolName,\n content: [{ type: 'text', text: buildLifecycleAsyncPlaceholderText(toolName, toolCallId, note) }],\n isError: false,\n details: {},\n timestamp: clock.now(),\n }\n}\n\n/**\n * 构造工具真正完成时的 mid-conversation system 通知消息(第五轮修正:落回持久数组,\n * 非临时 context 副本;provider 序列化归属 M2)。\n */\nexport function buildLifecycleAsyncCompletionNotice(\n toolName: string,\n toolCallId: string,\n result: ToolResult,\n clock: { now(): number },\n): SystemNotificationMessage {\n const summary = summarizeToolResult(result)\n const isError = result.isError ?? false\n const status = isError ? 'failed' : 'completed'\n return {\n role: 'system_notification',\n content: `[Tool completion] '${toolName}' ${status}: ${summary}`,\n isError,\n relatedToolCallId: toolCallId,\n timestamp: clock.now(),\n }\n}\n\n/**\n * 工具结果摘要化——第五轮修正 Q1d 风险缓解(tool 输出以 system 角色传递的\n * prompt injection 面):只取首个 text 块并截断长度,不原样嵌入完整工具输出。\n * 完整结果仍在 M2 决定的落库形态中留痕;此处只产出简短摘要供 LLM 感知状态。\n */\nconst MAX_SUMMARY_CHARS = 500\n\nfunction summarizeToolResult(result: ToolResult): string {\n const firstText = result.content.find((c) => c.type === 'text')\n if (!firstText || firstText.type !== 'text') {\n return result.isError ? 'error (no text output)' : 'done (no text output)'\n }\n const text = stripAnsi(firstText.text)\n return text.length > MAX_SUMMARY_CHARS\n ? `${text.slice(0, MAX_SUMMARY_CHARS)}… [truncated]`\n : text\n}\n","/**\n * 单轮工具调度与执行:并发安全工具走并行桶,其余按序串行执行。\n */\nimport type { AssistantMessage, ToolCall, ToolResultMessage } from '@x-otto/interchange'\nimport type { AgentMessage } from './types'\nimport type { ToolErrorKind, ToolResult } from '@x-otto/hook-contracts'\nimport { getToolCallsByAssistantMessage } from '@x-otto/interchange'\nimport { ENGINE_DEFAULTS } from './engine-nodes'\nimport { createPauseController } from './debug-port'\nimport { sealToolCalls } from './pipeline'\nimport { AbortError } from './errors'\nimport { recordToolCall, type ProgressWindowStats } from './progress-window'\nimport type { StallDetector } from './stall-detector'\nimport { systemClock } from './ports'\nimport { noopRecorder } from './trace'\nimport { compressToolResult } from './trace-views'\nimport {\n clampWithSpill,\n normalizeToolResultContent,\n type PromptToolResultAllowance,\n runBounded,\n toolResultContextChars,\n} from './tool-result-clamp'\nimport type { WorkLoopContext } from './work-loop'\nimport { buildLifecycleAsyncPlaceholder, buildLifecycleAsyncCompletionNotice } from './lifecycle-async-tool'\n\n/** 单源迁至 @x-otto/hook-contracts `ToolErrorKind`;保留 ErrorCategory 名向后兼容。 */\nexport type ErrorCategory = ToolErrorKind\n\n/**\n * 错误类别在**产生处定型**——executor 抛错路径 + 全部工具的 isError 返回\n * 均设 `errorKind`(@x-otto/tools 37 处 + missingCapability helper)。**英文子串启发式分类器已删**\n * (旧 type-erasure 毒)。未定型的错误(如不透明 MCP 工具错误)记为 'unknown'。此分类仅供 trace 观测\n * 标签,不驱动控制流。\n */\nexport function categoriseToolError(result: ToolResult): ErrorCategory | undefined {\n if (!result.isError) return undefined\n return result.errorKind ?? 'unknown'\n}\n\nexport async function runTools(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n turnIndex: number,\n lastTokenUsage: { input: number; output: number } | undefined,\n windowStats?: ProgressWindowStats,\n stallDetector?: StallDetector,\n /** RFC-340 M3:per-prompt 工具结果额度(缺省 = 退化为既有 per-tool 行为)。 */\n allowance?: PromptToolResultAllowance,\n): Promise<{ ranAnyTool: boolean }> {\n const { toolExecutor, messages, signal, emitter, logger, getSteeringMessages } = ctx\n const debug = ctx.ports?.debug\n const clock = ctx.ports?.clock ?? systemClock\n const recorder = ctx.ports?.recorder ?? noopRecorder\n\n // RFC-118 S15:runTools 场景无 llmParams/summary(与提取前快照字段一致)。\n const pause = createPauseController({\n debug,\n getTurn: () => turnIndex,\n getMessagesCount: () => messages.length,\n getTokenUsage: () => lastTokenUsage,\n })\n\n // RFC-142 D4:max_tokens 截断的半截参数调用(argumentsParseFailed,由\n // handleMaxTokensBrokenToolCall 已合成错误 tool_result)不执行——否则同一\n // toolCallId 产生两个 tool_result,破坏配对不变量(provider 400)。\n const allToolCalls = getToolCallsByAssistantMessage(assistantMessage).filter(\n (tc) => tc.argumentsParseFailed !== true,\n )\n const toolDefs = toolExecutor.list()\n const toolByName = new Map(toolDefs.map((t) => [t.name, t]))\n\n // RFC-144 M1(决策 2 / 硬约束 3):lifecycleAsync 工具率先过滤,绝不进入\n // syncCalls/asyncCalls/sequentialCalls 任一执行桶——它们不占 worker 槽位、不被\n // Promise.all/串行 await 阻塞。fail-open 降级:`ctx.lifecycleAsync` 控制器未注入\n // (宿主未接线,RFC-183 P1-1 收束后为单一接口对象)时,声明了 lifecycleAsync 的\n // 工具退化为普通同步工具执行,而非静默丢弃调用——保证行为可预期。\n const lifecycleAsyncEnabled = ctx.lifecycleAsync !== undefined\n // RFC-144 采纳评估:lifecycleAsync 支持谓词形式(与 isConcurrencySafe 同构)——同一\n // 工具的不同调用参数可有不同生命周期语义(task_delegate 仅 mode=sync 长阻塞)。\n // 谓词求值异常按 false 处理(fail-safe 回退同步执行,绝不因分桶判定崩掉整轮工具执行)。\n const isLifecycleAsyncCall = (tc: ToolCall): boolean => {\n const declared = toolByName.get(tc.name)?.lifecycleAsync\n if (typeof declared === 'function') {\n try {\n return declared(tc.arguments as never) === true\n } catch {\n return false\n }\n }\n return declared === true\n }\n const lifecycleAsyncCalls = lifecycleAsyncEnabled ? allToolCalls.filter(isLifecycleAsyncCall) : []\n const toolCalls = lifecycleAsyncEnabled\n ? allToolCalls.filter((tc) => !isLifecycleAsyncCall(tc))\n : allToolCalls\n\n if (!lifecycleAsyncEnabled) {\n const declaredButUnwired = allToolCalls.filter(isLifecycleAsyncCall)\n if (declaredButUnwired.length > 0) {\n logger.warn(\n 'lifecycleAsync tool(s) called but ctx.lifecycleAsync controller not wired (falling back to synchronous execution, loop will block): %s',\n declaredButUnwired.map((tc) => tc.name).join(', '),\n )\n }\n }\n\n const isParallelizable = (tc: ToolCall): boolean => {\n const tool = toolByName.get(tc.name)\n if (!tool) return false\n if (tool.readonly) return true\n return tool.isConcurrencySafe?.(tc.arguments as never) === true\n }\n const parallelCalls = toolCalls.filter(isParallelizable)\n const sequentialCalls = toolCalls.filter((tc) => !isParallelizable(tc))\n\n // RFC-135 D1:async 工具必须声明 readonly 或 isConcurrencySafe——否则进不了并行桶,\n // 而串行桶中 async 工具的 Promise 收集逻辑是死代码(工具结果不回 inject,后续串行工具\n // 拿不到前序结果)。runTools 入口 assertion 强制此约束,不满足的工具注册期即应被拒。\n //\n // RFC-144 M1(对抗评审 Finding 1 修正):此校验此前只扫 `toolCalls`(已排除 lifecycleAsync\n // 工具),若某工具同时声明 `async:true` + `lifecycleAsync:true` 但缺 readonly/isConcurrencySafe,\n // 会绕过此断言——虽然 lifecycleAsync 优先、永不进 asyncCalls 桶(无实际行为风险),但校验\n // 覆盖缺口本身是隐患:同一断言应对所有声明 async:true 的工具一致生效,不因 lifecycleAsync\n // 分流而产生\"部分工具免检\"的不一致语义。改为扫 `allToolCalls`(两类工具全覆盖)。\n for (const tc of allToolCalls) {\n const tool = toolByName.get(tc.name)\n if (tool?.async && !tool.readonly && !tool.isConcurrencySafe) {\n throw new Error(\n `Tool \"${tc.name}\" declared async:true but is neither readonly nor isConcurrencySafe. ` +\n `Async tools must be concurrency-safe (isConcurrencySafe or readonly) to enter the parallel bucket.`,\n )\n }\n }\n\n // RFC-135 D2:分离 async 工具与 sync 工具。async 工具不占 runBounded worker 槽位,\n // 改为 Promise.all 统一等待——长工具并行执行而非串行阻塞 worker。\n const asyncCalls = parallelCalls.filter((tc) => toolByName.get(tc.name)?.async === true)\n const syncCalls = parallelCalls.filter((tc) => !toolByName.get(tc.name)?.async)\n\n // RFC-144 M1(决策 1,硬约束 1):lifecycleAsync 工具的终态 placeholder 必须在\n // steering 检查**之前**推入——placeholder 一次写入即为该 tool_call 的最终 tool_result,\n // 不依赖后续流程是否被 steering 打断。若放在 steering 检查之后才推,一旦 steering 命中,\n // 这些 tool_call 会被排除在 `sealUnexecutedToolCalls` 的扫描范围外(因为它们已从\n // `toolCalls` 过滤出去),导致 dangling tool_use → provider 400。工具的真正执行(异步、\n // 不阻塞)在 placeholder 写入后立即启动,与 steering 与否无关——lifecycleAsync 工具一旦\n // 被模型调用就会开始跑,用户的 steering 消息不取消它。跨 prompt 边界的取消(M4):见下方\n // 每个工具独立的 `toolAbortController`,由 Agent.run() 在新 prompt 开始时对旧\n // promptSeq 的未消费 pending 条目主动 abort。\n let ranAnyToolLifecycleAsync = false\n // RFC-144 M4:本轮启动的 lifecycleAsync 工具所属的 prompt 序号快照——工具完成时\n // (可能是数分钟后)对比 `ctx.getCurrentPromptSeq?.()` 的当前值,判定是否跨越了\n // prompt 边界。在循环外只读取一次(同一轮 runTools 调用内启动的多个工具共享同一个\n // promptSeq,语义上它们都属于\"这一次 run() 调用\",不需要逐个重新读取)。\n const startingPromptSeq = ctx.lifecycleAsync?.currentPromptSeq() ?? 0\n for (const tc of lifecycleAsyncCalls) {\n const placeholder = buildLifecycleAsyncPlaceholder(\n tc.id,\n tc.name,\n clock,\n toolByName.get(tc.name)?.lifecycleAsyncNote,\n )\n messages.push(placeholder as AgentMessage)\n\n // RFC-144 M4:per-tool 取消句柄——独立于 loop 级别的 `signal`(那个只在用户主动\n // abort 当前 prompt 时才会触发)。与 loop signal 用 `AbortSignal.any()` 组合,\n // 工具会同时响应\"当前 prompt 被中断\"与\"这个具体调用跨越 prompt 边界被取消\"两种\n // 信号。真正的进程终止(SIGTERM+grace+SIGKILL)由 `packages/tools/src/shell/\n // process.ts` 的 `spawn()` 监听 signal 的 'abort' 事件完成,此处只需正确传播。\n const toolAbortController = new AbortController()\n const combinedSignal = AbortSignal.any([signal, toolAbortController.signal])\n const executionPromise = toolExecutor.execute(tc, combinedSignal)\n\n /**\n * RFC-144 M2/M3:工具真正完成后的落库——**必须等落库真正完成**(`messages.push`\n * 已执行)才 resolve,不能只等 `executionPromise` settle。\n *\n * 竞态说明(M3 实现时发现并修正):若 `PendingLifecycleTool.promise` 字段直接存\n * `executionPromise`,`Agent.waitForPendingLifecycleTools()` 的 `Promise.allSettled`\n * 只保证工具**执行**完成,不保证下面这段落库逻辑(`ctx.notifyToolCompletion()` +\n * `messages.push()`,在 `executionPromise.then()` 回调里异步执行)已经跑完——\n * `waitForPendingLifecycleTools()` 可能在通知消息真正落库前就返回,外层循环\n * `continue` 重新进入 `runTurnsUntilTerminate` 发起下一次 `runTurn` 时,\n * `messages` 里可能还没有这条通知,模型看不到工具完成的结果。\n *\n * 修正:`registerPendingLifecycleTool` 存的 `promise` 字段改为**落库函数本身的\n * Promise**(`notifyCompletion()` 的返回值),而非裸 `executionPromise`——这样\n * `waitForPendingLifecycleTools()` 等待的就是\"落库真正完成\",语义正确。\n *\n * fail-open 安全网(独立评审 S1 延续):`toolExecutor.execute()` 内部已把绝大多数\n * 异常转换为 `{isError:true}` 的 ToolResult(极少真正 reject),但极端情况(如\n * signal abort 竞态)仍可能 reject——这里显式 catch 并合成一个 isError 结果落库,\n * 而非让通知永久缺失;`notifyCompletion()` 本身也不 reject(内部吞掉所有异常,\n * 保证 `waitForPendingLifecycleTools()` 的 `Promise.allSettled` 永远拿到 fulfilled)。\n */\n const notifyCompletion = async (): Promise<void> => {\n let result: import('./types').ToolResult\n try {\n result = await executionPromise\n // 源头钩子:执行边界归一化 ANSI——所有下游(notifyCompletion/通知/crossBoundary 事件)自动获得洁净文本\n result.content = normalizeToolResultContent(result.content as Array<{ type: string; text?: string }>) as typeof result.content\n } catch (err) {\n logger.warn(\n 'lifecycleAsync tool %s (task id: %s) promise rejected: %s',\n tc.name,\n tc.id,\n err instanceof Error ? err.message : String(err),\n )\n result = {\n content: [\n {\n type: 'text' as const,\n text: `Tool ${tc.name} failed: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n\n // RFC-183 P1-1:原独立评审 L4 担心的\"部分接线\"(接了 hasPending/waitForPending\n // 却漏接 notifyToolCompletion)在收束为单一 LifecycleAsyncController 接口后已在\n // 类型上不可表达——能进到这里必然 ctx.lifecycleAsync 存在(本循环由\n // lifecycleAsyncEnabled 门控),notifyCompletion 是接口必备方法。原防御分支\n // 随收束移除。\n // 独立评审 L2:`consumed:false` 在正常流程下不应发生(entry 刚注册、toolCallId\n // 唯一)——仅当 `notifyToolCompletion` 自身有 bug(如误判已消费)时才会触发。\n // 即便发生,也不会死循环:`notifyCompletion()` 的 promise 依然会 resolve\n // (entry.consumed 却仍为 false),下一次 `hasPendingLifecycleTools()` 检测仍\n // 为 true,但 `waitForPendingLifecycleTools()` 收集的是已 resolve 的 promise、\n // 立即返回——外层 `continue` 重新进入内层循环,被 LLM turn 本身的网络往返延迟\n // 自然节流(不是真正的空转死循环),且 `maxToolTurns`/预算闸门最终会终止。\n const { consumed } = await ctx.lifecycleAsync!.notifyCompletion(tc.id, result)\n if (!consumed) return\n\n // RFC-144 M4:跨 prompt 边界判定——工具启动时记录的 `startingPromptSeq` 与工具\n // 完成此刻的 `ctx.getCurrentPromptSeq?.()` 不一致,说明原 prompt(`run()` 调用)\n // 已经结束、一个新的 `run()` 已经开始(用户发起了全新任务)。此时**不**把完成\n // 通知作为 mid-conversation system message 注入新 prompt 的对话历史——那是上一个\n // 已结束任务的事件,混入当前无关任务的上下文会让模型困惑(system 角色被视为\n // 运营方权威事实,见 RFC-144 决策 4 Q4 修正)。上面的 `ctx.notifyToolCompletion()`\n // 已经无条件 emit 了 `lifecycle.tool.completed`(通用审计信号,不关心是否跨\n // 边界)——此处**额外**再 emit 一次 `lifecycle.tool.completed.crossBoundary`\n // (叠加,非互斥),交给宿主(CLI/TUI)决定如何呈现给用户(如 toast)——事件\n // 消费方的具体接线归后续项,不阻塞 M4 的核心不变量(不污染新任务 context,即\n // 跳过下面的 `messages.push(notice)`)。\n const currentPromptSeq = ctx.lifecycleAsync?.currentPromptSeq()\n const crossedPromptBoundary =\n currentPromptSeq !== undefined && currentPromptSeq !== startingPromptSeq\n if (crossedPromptBoundary) {\n // 注意:pino LogFn 的模板字面量类型推导对多段 `+` 拼接的字符串会解析异常\n // (TS2345 误报),故此处保持单行字符串字面量,不做多行拼接。\n logger.info(\n 'lifecycleAsync tool %s (task id: %s) crossed prompt boundary (startSeq=%d, nowSeq=%d) — completion notice suppressed, emitting crossBoundary event instead',\n tc.name,\n tc.id,\n startingPromptSeq,\n currentPromptSeq,\n )\n emitter.emit('lifecycle.tool.completed.crossBoundary', {\n toolCallId: tc.id,\n toolName: tc.name,\n result,\n })\n return\n }\n\n const notice = buildLifecycleAsyncCompletionNotice(tc.name, tc.id, result, clock)\n messages.push(notice as AgentMessage)\n }\n\n ctx.lifecycleAsync?.register({\n toolCallId: tc.id,\n toolName: tc.name,\n promise: notifyCompletion(),\n promptSeq: startingPromptSeq,\n consumed: false,\n abortController: toolAbortController,\n })\n\n emitter.emit('tool.call.start', {\n toolCall: { id: tc.id, name: tc.name, arguments: tc.arguments },\n })\n logger.info('Started lifecycleAsync tool %s (task id: %s), not blocking loop', tc.name, tc.id)\n }\n if (lifecycleAsyncCalls.length > 0) {\n ranAnyToolLifecycleAsync = true\n }\n\n const steeringMessages = await getSteeringMessages()\n\n await pause('steering.check', 1, {\n metadata: {\n hasSteeringMessages: steeringMessages.length > 0,\n steeringCount: steeringMessages.length,\n },\n })\n\n let ranAnyTool = ranAnyToolLifecycleAsync\n\n // Steering is not an error: the tool was intentionally superseded by the user's\n // new instruction (isError:false — see sealToolCalls JSDoc for the semantics split\n // between this call site and pipeline's abort sealing).\n const sealUnexecutedToolCalls = (): void => {\n sealToolCalls(messages, toolCalls, clock, {\n isError: false,\n text: 'Skipped: superseded by the user’s new instruction (not executed; no error).',\n })\n }\n\n if (steeringMessages.length > 0) {\n sealUnexecutedToolCalls()\n messages.push(...steeringMessages)\n emitter.emit('steering.injected', { messages: steeringMessages })\n return { ranAnyTool: ranAnyToolLifecycleAsync }\n }\n\n let toolCallIndex = 0\n\n /**\n * 执行单个工具,返回 ToolResultMessage(不 push 到 messages,由调用方处理)。\n * 包含全部副作用:emit start/end、recorder、pause、clampWithSpill、recordToolCall。\n * 与 executeSingleTool 的区别仅在于 messages.push 由调用方负责。\n */\n const executeSingleToolAsync = async (toolCall: ToolCall): Promise<ToolResultMessage> => {\n if (signal.aborted) {\n throw new AbortError()\n }\n\n const currentToolIndex = toolCallIndex++\n\n emitter.emit('tool.call.start', {\n toolCall: {\n id: toolCall.id,\n name: toolCall.name,\n arguments: toolCall.arguments,\n },\n })\n recorder.record({\n kind: 'tool',\n node: 'tool.call.start',\n turn: turnIndex,\n payload: {\n id: toolCall.id,\n name: toolCall.name,\n arguments: toolCall.arguments,\n turnIndex,\n toolCallIndex: currentToolIndex,\n },\n })\n\n logger.info('Executing tool %s', toolCall.name)\n\n await pause('tool.before', 1, { lastToolName: toolCall.name })\n\n const execStart = clock.now()\n const result = await toolExecutor.execute(toolCall, signal)\n\n // 源头钩子:执行边界归一化 ANSI——所有下游(emitter/recorder/clamp/通知)自动获得洁净文本\n result.content = normalizeToolResultContent(result.content as Array<{ type: string; text?: string }>) as typeof result.content\n\n if (signal.aborted) {\n throw new AbortError()\n }\n\n const durationMs = clock.now() - execStart\n\n logger.info(\n 'Tool %s completed%s (%d ms)',\n toolCall.name,\n result.isError ? ' with error' : '',\n durationMs,\n )\n\n // RFC-093:per-tool 预算 + 超预算 spill 落盘(fail-open 回退纯截断)\n const declaredBudget =\n toolByName.get(toolCall.name)?.resultBudgetChars ?? ENGINE_DEFAULTS.maxToolResultChars\n /**\n * RFC-340 M3:per-prompt 额度收紧(只收紧不放大——见 `PromptToolResultAllowance.budgetFor`)。\n *\n * **并行批次的已知近似**:同一 turn 内并行执行的工具会各自在 charge 之前先取\n * budgetFor,故一个批次内可能集体按当时的剩余额度放行、轻微超发。这是刻意接受的\n * 取舍——批内并发上限本就受 `maxToolConcurrency`(默认 10)约束,且额度在**跨 turn**\n * 维度持续收紧(真实撑爆场景是几十轮累积而非单批),换取不引入锁/串行化对并行\n * 执行路径的破坏。\n */\n const budget = allowance?.budgetFor(declaredBudget) ?? declaredBudget\n const clampedContent = await clampWithSpill(result.content, budget, toolCall.id, ctx.ports?.spill)\n // 记账用 clamp/spill **之后**的实际入上下文字符量(与 contextChars 观测同一口径)。\n allowance?.charge(toolResultContextChars(clampedContent))\n\n const toolResultMessage: ToolResultMessage = {\n role: 'tool_result',\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n content: clampedContent,\n isError: result.isError ?? false,\n details: result.details ?? {},\n timestamp: clock.now(),\n }\n\n // RFC-104 D1:进展窗口记账(签名 + 写成功;readonly 缺省按写类处理 = 更宽松不误杀)\n if (windowStats) {\n recordToolCall(windowStats, {\n toolName: toolCall.name,\n args: toolCall.arguments,\n readonly: toolByName.get(toolCall.name)?.readonly === true,\n isError: result.isError ?? false,\n })\n }\n // RFC-203 D4:停滞检测器从同一挂点独立记账(与 windowStats 生命周期不同,不共享状态)。\n stallDetector?.recordCall({\n toolName: toolCall.name,\n args: toolCall.arguments,\n readonly: toolByName.get(toolCall.name)?.readonly === true,\n isError: result.isError ?? false,\n })\n\n const errorCategory = categoriseToolError(result)\n\n emitter.emit('tool.call.end', {\n toolCall: {\n id: toolCall.id,\n name: toolCall.name,\n arguments: toolCall.arguments,\n },\n result,\n // 入模口径(非原始 result):宿主据此做上下文占用实时估算,见 AgentEvent.contextChars。\n contextChars: toolResultContextChars(clampedContent),\n })\n recorder.record({\n kind: 'tool',\n node: 'tool.call.end',\n turn: turnIndex,\n payload: {\n id: toolCall.id,\n name: toolCall.name,\n resultSummary: compressToolResult(result, ENGINE_DEFAULTS.traceResultMaxChars),\n durationMs,\n toolCallIndex: currentToolIndex,\n ...(errorCategory ? { errorCategory } : {}),\n },\n })\n\n await pause('tool.after', 1, { lastToolName: toolCall.name })\n\n return toolResultMessage\n }\n\n /** 执行单个工具 + push 到 messages(同步工具路径,行为与改造前完全等效)。 */\n const executeSingleTool = async (toolCall: ToolCall): Promise<void> => {\n const msg = await executeSingleToolAsync(toolCall)\n messages.push(msg)\n }\n\n if (syncCalls.length > 0) {\n const cap = ENGINE_DEFAULTS.maxToolConcurrency\n logger.info(\n 'Executing %d concurrency-safe tools in parallel (cap %d)',\n syncCalls.length,\n cap,\n )\n const settled = await runBounded(syncCalls, cap, executeSingleTool)\n ranAnyTool = true\n const abortReject = settled.some(\n (s) => s.status === 'rejected' && s.reason instanceof AbortError,\n )\n if (abortReject || signal.aborted) {\n throw new AbortError()\n }\n const otherReject = settled.find((s) => s.status === 'rejected')\n if (otherReject && otherReject.status === 'rejected') {\n throw otherReject.reason\n }\n }\n\n // RFC-135 D2:异步工具——Promise.all 统一等待,不占 worker 槽位。\n // 多个长工具(如 build + test)并行执行而非串行排队。\n if (asyncCalls.length > 0) {\n logger.info(\n 'Starting %d async tools in parallel (Promise.all, no worker slot)',\n asyncCalls.length,\n )\n const promises = asyncCalls.map((tc) => executeSingleToolAsync(tc))\n const results = await Promise.all(promises)\n for (const r of results) {\n messages.push(r)\n }\n ranAnyTool = true\n }\n\n for (const toolCall of sequentialCalls) {\n if (signal.aborted) {\n throw new AbortError()\n }\n\n const steering = await getSteeringMessages()\n if (steering.length > 0) {\n sealUnexecutedToolCalls()\n messages.push(...steering)\n emitter.emit('steering.injected', { messages: steering })\n return { ranAnyTool }\n }\n\n await executeSingleTool(toolCall)\n ranAnyTool = true\n }\n\n return { ranAnyTool }\n}\n\n","/**\n * work-loop-stream-error.ts — runTurn 的流错误处置(H1/H2/H3,RFC-016).\n *\n * 从 work-loop 抽出:一轮 stream 失败时决定「重试本轮」还是「上抛」。\n * - context-overflow(413) → 反应式压缩本轮上下文后重试(单次,H3)\n * - retryable(429/5xx/断连) → 退避后重发整请求(上限 maxRetries,H1)\n * - 鉴权失败(fatal 子类:本地无凭据/服务端401)→ 记录 stream.auth-failed 事件后上抛\n * - 其它 → 上抛(rethrow,由调用方抛出原 error)\n * abort 永远直接上抛(不重试、不计数)。\n */\nimport {\n classifyStreamError,\n isNetworkDisconnectError,\n isAuthError,\n classifyAuthErrorType,\n extractAnthropicRateLimit,\n extractRetryAfterMs,\n} from '@x-otto/provider'\nimport { degradeOldImages } from './image-degradation'\nimport { AbortError } from './errors'\nimport { noopRecorder } from './trace'\nimport type { AgentMessage } from './types'\nimport type { WorkLoopContext } from './work-loop'\n\nexport interface StreamRetryConfig {\n maxRetries: number\n initialDelayMs: number\n backoffFactor: number\n maxDelayMs: number\n}\n\nexport const DEFAULT_STREAM_RETRY: StreamRetryConfig = {\n maxRetries: 5,\n initialDelayMs: 500,\n backoffFactor: 2,\n maxDelayMs: 60_000,\n}\n\n/**\n * 网络断连子类(ECONNRESET/socket hang up/undici `terminated` 等,见 isNetworkDisconnectError)\n * 专属重试节奏:固定 30 秒间隔 × 5 次,不做指数退避。\n * 与 429/5xx 的 DEFAULT_STREAM_RETRY 分开的理由:429/5xx 是服务端主动限流/过载信号,指数退避 +\n * Retry-After 头能最快恢复;网络断连是连接被动中断(代理/NAT/对端 keep-alive 超时),固定间隔\n * 给链路充分恢复时间更符合直觉,且不应让\"terminated\"这类瞬时抖动拖累限流场景的既有节奏\n * (两者共用一套 retryCfg 会互相影响,故拆分为独立配置——backoffFactor:1 使指数退避公式退化为常量)。\n */\nexport const NETWORK_DISCONNECT_RETRY: StreamRetryConfig = {\n maxRetries: 5,\n initialDelayMs: 30_000,\n backoffFactor: 1,\n maxDelayMs: 30_000,\n}\n\n/**\n * H1:指数退避 + 乘性 jitter(0.9–1.1)。attempt 0-based(0=首次重试)。\n * Retry-After(毫秒)优先,仍以 maxDelayMs 封顶防失控等待。\n */\nexport function computeBackoffDelay(\n attempt: number,\n cfg: StreamRetryConfig,\n jitter: number,\n retryAfterMs?: number,\n): number {\n if (retryAfterMs !== undefined && retryAfterMs >= 0) {\n return Math.min(Math.round(retryAfterMs), cfg.maxDelayMs)\n }\n const base = cfg.initialDelayMs * Math.pow(cfg.backoffFactor, attempt)\n const multiplier = 0.9 + 0.2 * jitter\n return Math.min(Math.round(base * multiplier), cfg.maxDelayMs)\n}\n\n/** 本轮内累计的流错误重试状态(由 runTurn 持有,跨同一轮的多次重试累加)。 */\nexport interface StreamErrorState {\n /** H3:反应式压缩单次守卫——本轮最多触发一次(压缩后仍溢出则上抛,防死循环)。 */\n hasAttemptedReactiveCompact: boolean\n /** D3/D7:图片专项降级单次守卫——本轮最多触发一次,防死循环(RFC-099)。 */\n hasAttemptedImageDegradation: boolean\n /** H1:可重试流错误的退避重发计数(本轮内累计,上限 maxRetries)。 */\n transientRetries: number\n /**\n * RFC-137 D4:鉴权失败(isAuthError)单次自动重试守卫——本轮最多自动重试一次。\n * 独立于 `transientRetries`(不共用配额,不走 computeBackoffDelay 退避),理由:\n * ① OAuth token 到期后第一次请求触发刷新,该次请求本身会短暂失败(正常设计行为,\n * 非网络抖动),下一次请求通常已能用上刷新后的新 token——这类瞬时失败应立即重试,\n * 不该套用为 429/5xx 设计的指数退避延迟;② 若与 transientRetries 共用配额,auth\n * 重试会挤占后续真正网络抖动的重试次数。auth 重试无论成功失败都不影响\n * `transientRetries` 计数。\n */\n hasAttemptedAuthRetry: boolean\n}\n\n/** retry:重试本轮(可选携带压缩后的 contextMessages);rethrow:由调用方抛出原 error。 */\nexport type StreamErrorOutcome =\n | { action: 'retry'; contextMessages?: AgentMessage[] }\n | { action: 'rethrow' }\n\n/**\n * D3/D7 共享图片降级 helper:本轮至多触发一次(set hasAttemptedImageDegradation),防死循环。\n * currentRoundStartIndex = contextMessages 尾部最后一个 role === 'user' 的索引(找不到则 0)。\n * 返回 retry 指令(携降级后消息)或 undefined(无可降级图片 → 调用方继续 rethrow)。\n */\nfunction attemptImageDegradation(\n errorClass: 'context-overflow' | 'image-constraint',\n contextMessages: AgentMessage[],\n turnIndex: number,\n state: StreamErrorState,\n ctx: WorkLoopContext,\n): StreamErrorOutcome | undefined {\n state.hasAttemptedImageDegradation = true\n\n let currentRoundStartIndex = 0\n for (let i = contextMessages.length - 1; i >= 0; i--) {\n if (contextMessages[i]!.role === 'user') {\n currentRoundStartIndex = i\n break\n }\n }\n\n const { messages: degraded, degradedCount } = degradeOldImages(\n contextMessages,\n currentRoundStartIndex,\n )\n if (degradedCount === 0) return undefined\n\n const { logger, emitter } = ctx\n const recorder = ctx.ports?.recorder ?? noopRecorder\n\n logger.warn(\n 'Image degradation triggered (turn %d, trigger=%s): degraded %d images',\n turnIndex + 1,\n errorClass,\n degradedCount,\n )\n emitter.emit('image.degradation', { turnIndex, degradedCount, trigger: errorClass })\n recorder.record({\n kind: 'lifecycle',\n node: 'image.degradation',\n turn: turnIndex,\n payload: { degradedCount, trigger: errorClass },\n })\n\n return { action: 'retry', contextMessages: degraded }\n}\n\n/**\n * 处置一轮 stream 失败。可能 throw(abort 直接上抛)或返回重试/上抛指令。\n * 副作用(emit/record/sleep)在内联完成;state 计数原地累加。\n */\nexport async function handleStreamError(\n error: unknown,\n ctx: WorkLoopContext,\n turnIndex: number,\n contextMessages: AgentMessage[],\n state: StreamErrorState,\n retryCfg: StreamRetryConfig,\n retryAccelerate: { skip: boolean } | undefined,\n random: { random(): number },\n sleep: (ms: number, signal?: AbortSignal) => Promise<void>,\n): Promise<StreamErrorOutcome> {\n const { signal, emitter, logger } = ctx\n const recorder = ctx.ports?.recorder ?? noopRecorder\n\n if (\n error instanceof AbortError ||\n (error instanceof Error && error.name === 'AbortError') ||\n signal.aborted\n ) {\n throw error\n }\n\n const errorClass = classifyStreamError(error)\n\n if (\n errorClass === 'context-overflow' &&\n ctx.contextTransform?.reactiveCompact &&\n !state.hasAttemptedReactiveCompact\n ) {\n state.hasAttemptedReactiveCompact = true\n const before = contextMessages.length\n const compacted = await ctx.contextTransform.reactiveCompact(contextMessages, signal)\n if (compacted) {\n logger.warn(\n 'Reactive compaction on context overflow (turn %d): %d -> %d messages',\n turnIndex + 1,\n before,\n compacted.length,\n )\n emitter.emit('compaction.reactive', {\n turnIndex,\n messagesBefore: before,\n messagesAfter: compacted.length,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'compaction.reactive',\n turn: turnIndex,\n payload: { messagesBefore: before, messagesAfter: compacted.length },\n })\n return { action: 'retry', contextMessages: compacted }\n }\n }\n\n if (\n (errorClass === 'context-overflow' || errorClass === 'image-constraint') &&\n !state.hasAttemptedImageDegradation\n ) {\n const outcome = attemptImageDegradation(\n errorClass,\n contextMessages,\n turnIndex,\n state,\n ctx,\n )\n if (outcome) return outcome\n }\n\n // 网络断连子类(ECONNRESET/socket hang up/undici terminated 等)走固定 30 秒节奏,\n // 429/5xx 等服务端响应类走调用方传入的 retryCfg(默认 DEFAULT_STREAM_RETRY 指数退避)。\n const effectiveRetryCfg =\n errorClass === 'retryable' && isNetworkDisconnectError(error)\n ? (ctx.retry?.networkDisconnectRetry ?? NETWORK_DISCONNECT_RETRY)\n : retryCfg\n\n if (errorClass === 'retryable' && state.transientRetries < effectiveRetryCfg.maxRetries) {\n const reason = error instanceof Error ? error.message : String(error)\n const retryAfterMs = extractRetryAfterMs(error)\n const delayMs = computeBackoffDelay(\n state.transientRetries,\n effectiveRetryCfg,\n random.random(),\n retryAfterMs,\n )\n state.transientRetries++\n\n logger.warn(\n 'Retryable stream error (turn %d, attempt %d/%d, delay %dms): %s',\n turnIndex + 1,\n state.transientRetries,\n effectiveRetryCfg.maxRetries,\n delayMs,\n reason,\n )\n emitter.emit('stream.retry', {\n turnIndex,\n attempt: state.transientRetries,\n maxRetries: effectiveRetryCfg.maxRetries,\n delayMs,\n reason,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'stream.retry',\n turn: turnIndex,\n payload: {\n attempt: state.transientRetries,\n maxRetries: effectiveRetryCfg.maxRetries,\n delayMs,\n reason,\n },\n })\n\n if (retryAccelerate) retryAccelerate.skip = false\n\n const pollIntervalMs = 1000\n let elapsed = 0\n while (elapsed < delayMs) {\n if (signal.aborted) {\n throw new AbortError()\n }\n if (retryAccelerate?.skip) {\n retryAccelerate.skip = false\n logger.info(\n 'Retry accelerated by user (turn %d, attempt %d/%d, after %dms of %dms)',\n turnIndex + 1,\n state.transientRetries,\n effectiveRetryCfg.maxRetries,\n elapsed,\n delayMs,\n )\n emitter.emit('stream.retry.accelerated', {\n turnIndex,\n attempt: state.transientRetries,\n maxRetries: effectiveRetryCfg.maxRetries,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'stream.retry.accelerated',\n turn: turnIndex,\n payload: {\n attempt: state.transientRetries,\n maxRetries: effectiveRetryCfg.maxRetries,\n elapsedMs: elapsed,\n },\n })\n break\n }\n const step = Math.min(pollIntervalMs, delayMs - elapsed)\n await sleep(step, signal)\n elapsed += step\n }\n\n if (signal.aborted) {\n throw new AbortError()\n }\n return { action: 'retry' }\n }\n\n const rateLimit = extractAnthropicRateLimit(error)\n if (rateLimit?.status === 'rejected') {\n const reason = error instanceof Error ? error.message : String(error)\n emitter.emit('rate.limit.exhausted', {\n limitType: rateLimit.type,\n resetsAt: rateLimit.resetsAt,\n message: reason,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'rate.limit.exhausted',\n turn: turnIndex,\n payload: { limitType: rateLimit.type, resetsAt: rateLimit.resetsAt },\n })\n } else if (isAuthError(error)) {\n // RFC-137 D4:鉴权失败首次遇到时自动重试一次,不再直接终止本轮 loop——\n // OAuth token 到期后第一次请求触发刷新,这次请求本身会短暂失败(正常设计行为,见\n // auth-store.ts 的 resolveOAuthAccessKey/refreshOAuthWithCrossProcessLock),下一次\n // 请求通常已能用上刷新后的新 token(真实 trace 实测约 72% 样本一次重试即恢复)。\n // 此前该分支从不重试,用户被迫手动重新输入才能感知\"其实已经恢复了\"。\n if (!state.hasAttemptedAuthRetry) {\n state.hasAttemptedAuthRetry = true\n const reason = error instanceof Error ? error.message : String(error)\n logger.warn(\n 'Auth error on first attempt (turn %d, provider=%s, model=%s), retrying once: %s',\n turnIndex + 1,\n ctx.model.provider,\n ctx.model.id,\n reason,\n )\n return { action: 'retry' }\n }\n\n // 重试后仍然失败——鉴权失败(本地无凭据/服务端401/OAuth刷新失败)——此前该 fatal\n // 子类零记录(既不 logger.warn 也不 emit/record),用户在 TUI 看到弹窗后无法回溯\n // \"什么时候、哪个 provider/model 失效、失效了几次\"。ctx.model 取本轮实际使用的模型\n // (非会话当前配置模型,两者在模型刚切换后可能不一致),对齐 prompt.end 事件\n // \"入口捕获快照\"的既有原则(session-event.ts:52-53)。\n const reason = error instanceof Error ? error.message : String(error)\n logger.warn(\n 'Auth failed (turn %d, provider=%s, model=%s): %s',\n turnIndex + 1,\n ctx.model.provider,\n ctx.model.id,\n reason,\n )\n // RFC-139 D3:errorType 机器可读细分——credential-missing(本地无凭据,含\"凭据被\n // 外部删除后经 fs.watch 传播\"事故场景)/ refresh-failed(RFC-137 刷新失败)/\n // server-rejected(服务端 401,含无 code 的 raw SDK 错误回退)。事后按类聚合可直接\n // 区分\"凭据丢失\"与\"token 过期\"两类完全不同的故障,免去逐条读 reason 文本。\n const errorType = classifyAuthErrorType(error)\n emitter.emit('stream.auth-failed', {\n turnIndex,\n provider: ctx.model.provider,\n modelId: ctx.model.id,\n reason,\n errorType,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'stream.auth-failed',\n turn: turnIndex,\n payload: { provider: ctx.model.provider, modelId: ctx.model.id, reason, errorType },\n })\n }\n\n return { action: 'rethrow' }\n}\n","/**\n * work-loop-turn-outcome.ts — runTurn 完成后的 stopReason 分派(H2/RFC-016).\n *\n * 从 workLoop 抽出:据 assistantMessage 的 stopReason 决定本轮去向,\n * 把循环控制(break/continue/run-tools)归约为一个 TurnOutcome 指令。\n * - refusal → terminate(拒答干净结束)\n * - max_tokens 且无 tool_use → continue(append 续写提示)或 terminate(续写耗尽)\n * - 含 tool_use → run-tools(预算内);预算耗尽则扩展或抛 MaxToolTurnsError\n * - 正常停止 → terminate\n * 副作用(emit/record/push 续写消息/state 计数)内联完成;循环控制由调用方据指令执行。\n */\nimport { getToolCallsByAssistantMessage, hasToolCalls } from '@x-otto/interchange'\nimport type { AssistantMessage } from '@x-otto/interchange'\nimport { hasTrailingInvokeXml } from '@x-otto/provider'\nimport { ENGINE_DEFAULTS } from './engine-nodes'\nimport { MaxToolTurnsError, PromptBudgetExceededError } from './errors'\nimport { sealToolCalls } from './pipeline'\nimport { systemClock, type ClockPort } from './ports'\nimport {\n hasWindowProgress,\n resetProgressWindow,\n type ProgressWindowStats,\n} from './progress-window'\nimport type { StallDetector } from './stall-detector'\nimport { noopRecorder, type TraceRecorder } from './trace'\nimport type { AgentMessage } from './types'\nimport type { WorkLoopContext } from './work-loop'\n\n/** 提取 assistant message 内容里最后一个 text block 的文本(供 RFC-232 协议违约判据复用)。 */\nfunction lastTextContent(message: AssistantMessage): string | null {\n for (let i = message.content.length - 1; i >= 0; i--) {\n const block = message.content[i]!\n if (block.type === 'text') return block.text\n }\n return null\n}\n\nexport const MAX_TRUNCATION_CONTINUATIONS = 5\n\n/**\n * RFC-232 D2:协议违约(stopReason='tool_use' 但响应无任何 tool_call block)的最大重试次数。\n * 违约多为模型/代理稳定故障——重试 2 次仍违约则硬停并提示用户(换模型/重发),优于无限重试。\n */\nexport const MAX_PROTOCOL_MISMATCH_RETRIES = 2\n\n/**\n * RFC-232 D2:协议违约态注入的纠正提示(internal user 消息,provider 安全,匹配既有续写模式)。\n * 违约态无 tool_call block → 无待配对的 tool_use,纯 push user 文本不涉及配对(RFC-232 风险2)。\n */\nexport const PROTOCOL_MISMATCH_STEER_PROMPT =\n 'Your previous response indicated a tool call (stop reason was tool_use) but contained no valid ' +\n 'tool invocation — do NOT write tool calls as literal text or XML in the response body. ' +\n 'Re-issue the tool call using the proper tool-calling mechanism, or respond with text only.'\n\n/**\n * RFC-050 D5:工具预算耗尽时注入的收尾 steering(budget-limit 语义:\n * 不硬停,先 steer 模型用一轮把活收尾——总结进度/剩余/给用户明确下一步)。\n * 作为 user 消息注入(provider 安全,匹配既有截断续写模式);收尾轮只产文本 → 干净 terminate。\n */\nexport const BUDGET_STEER_PROMPT =\n 'You have reached your tool-call budget for this turn. Do not start any new tool work or call any tools. ' +\n 'Wrap up now: briefly summarize the concrete progress you made, list what still remains or is blocked, ' +\n 'and give the user a clear next step. Respond with text only.'\n\n/**\n * RFC-203 D2:停滞检出时的软引导——与 BUDGET_STEER_PROMPT 的\"最后一轮收尾\"语义刻意区分:\n * 给模型二选一出口(换策略推进 / 认定无法推进则收敛汇报),不禁止继续调用工具——\n * 停滞判定有合法长探索的误报可能,引导代价必须止于一条 internal message。\n */\nexport const STALL_STEER_PROMPT =\n 'Progress check: your recent tool calls repeat the same operations without producing changes — ' +\n 'you may be stuck in a loop. Either switch to a different approach now (different tools, different ' +\n 'angle, or narrower scope), or if the task genuinely cannot advance, stop and report: what you tried, ' +\n 'what blocked you, and what the user should do next.'\n\n/**\n * RFC-104 D3/D5:token/时间预算超额时的收尾 steering——携带真实用量数字(R7 诚实报数,\n * 沿用 budget-limit 模板的 tokens_used/token_budget 字段)。\n */\nexport function buildPromptBudgetSteerPrompt(\n dimension: 'tokens' | 'time',\n used: number,\n budget: number,\n): string {\n const usage =\n dimension === 'tokens'\n ? `You have used ${used} of your ${budget} output-token budget for this prompt.`\n : `You have used ${Math.round(used / 1000)}s of your ${Math.round(budget / 1000)}s time budget for this prompt.`\n return (\n `${usage} Do not start any new tool work or call any tools. ` +\n 'Wrap up now: briefly summarize the concrete progress you made, list what still remains or is blocked, ' +\n 'and give the user a clear next step. Respond with text only.'\n )\n}\n\n/** 跨轮累加的循环计数(由 workLoop 持有;follow-up 时重置 toolTurnCount/extensionsUsed)。 */\nexport interface TurnLoopState {\n /** H2:截断续写计数(跨续写轮累计,上限 MAX_TRUNCATION_CONTINUATIONS)。 */\n continuationCount: number\n /** 已消耗的工具轮数(含扩展后重置)。 */\n toolTurnCount: number\n /** 已用的工具预算扩展次数。 */\n extensionsUsed: number\n /** RFC-050 D5:本回合是否已注入过预算收尾 steering(确保至多一轮收尾,之后硬停)。 */\n budgetSteeringIssued: boolean\n /** RFC-104 D1:当前预算窗口的进展统计(扩展/followUp 重置时清零)。 */\n windowStats: ProgressWindowStats\n /** RFC-104 D3:本 prompt 累计 output tokens(turn.end usage 累加;followUp 重置)。 */\n promptOutputTokens: number\n /** RFC-104 D3:本 prompt 起始时刻(clock ms;followUp 重置)。墙钟预算基准。 */\n promptStartedAt: number\n /** RFC-203:滑动窗口停滞检测器(undefined = 关闭;followUp 时 reset)。 */\n stallDetector?: StallDetector\n /** RFC-203 D2:本 prompt 是否已注入过停滞软引导(至多一次,同 budgetSteeringIssued 纹理)。 */\n stallSteeringIssued: boolean\n /** RFC-232 D2:本 prompt 协议违约(stopReason=tool_use 但无 tool_call block)已重试次数(per-prompt,followUp 重置)。 */\n protocolMismatchRetryCount: number\n}\n\n/** terminate:结束循环返回;continue:append 续写后重跑本轮;run-tools:进入工具执行。 */\nexport type TurnOutcome = { kind: 'terminate' } | { kind: 'continue' } | { kind: 'run-tools' }\n\n/** classifyTurnOutcome 内部依赖(RFC-118 S4 责任链拆分后各 handler 共用)。 */\ninterface OutcomeDeps {\n clock: ClockPort\n recorder: TraceRecorder\n}\n\n/** stopReason='refusal' → 干净终止。 */\nfunction handleRefusal(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n _state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n if (assistantMessage.stopReason !== 'refusal') return null\n const { emitter, logger } = ctx\n logger.warn('Model refused to respond on turn %d; terminating cleanly', completedTurn + 1)\n emitter.emit('turn.truncated', { turnIndex: completedTurn, action: 'refusal' })\n deps.recorder.record({\n kind: 'lifecycle',\n node: 'turn.truncated',\n turn: completedTurn,\n payload: { action: 'refusal' },\n })\n return { kind: 'terminate' }\n}\n\n/** stopReason='max_tokens' 且无 tool_use → 续写提示(continue)或续写耗尽(terminate)。 */\nfunction handleMaxTokensTruncation(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n if (assistantMessage.stopReason !== 'max_tokens' || hasToolCalls(assistantMessage)) return null\n const { emitter, logger, messages } = ctx\n const { clock, recorder } = deps\n\n if (state.continuationCount < MAX_TRUNCATION_CONTINUATIONS) {\n state.continuationCount++\n messages.push({\n role: 'user',\n content: [\n {\n type: 'text',\n text: 'Your previous response was cut off because it reached the output token limit. Continue exactly where you left off — do not repeat content already produced.',\n },\n ],\n timestamp: clock.now(),\n internal: true,\n } as AgentMessage)\n logger.warn(\n 'Output truncated (max_tokens) on turn %d; appending continuation prompt (%d/%d)',\n completedTurn + 1,\n state.continuationCount,\n MAX_TRUNCATION_CONTINUATIONS,\n )\n emitter.emit('turn.truncated', { turnIndex: completedTurn, action: 'continue' })\n recorder.record({\n kind: 'lifecycle',\n node: 'turn.truncated',\n turn: completedTurn,\n payload: { action: 'continue', continuation: state.continuationCount },\n })\n return { kind: 'continue' }\n }\n logger.warn(\n 'Output still truncated after %d continuations; returning partial response',\n MAX_TRUNCATION_CONTINUATIONS,\n )\n return { kind: 'terminate' }\n}\n\n/** token/时间预算超额检查(run-tools 前)。命中则 steer 收尾(continue)或抛 PromptBudgetExceededError。 */\nfunction evaluateBudgetBreachOutcome(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n const { emitter, logger, messages } = ctx\n const { clock, recorder } = deps\n const budgetBreach = evaluatePromptBudget(ctx, state, clock)\n if (!budgetBreach) return null\n\n const { dimension, used, budget } = budgetBreach\n if (ctx.budget.budgetSteerWrapUp && !state.budgetSteeringIssued) {\n state.budgetSteeringIssued = true\n sealToolCallsForWrapUp(assistantMessage, messages, clock)\n messages.push({\n role: 'user',\n content: [{ type: 'text', text: buildPromptBudgetSteerPrompt(dimension, used, budget) }],\n timestamp: clock.now(),\n internal: true,\n } as AgentMessage)\n emitter.emit('prompt.budget', { turnIndex: completedTurn, dimension, used, budget, steered: true })\n recorder.record({\n kind: 'lifecycle',\n node: 'prompt.budget',\n turn: completedTurn,\n payload: { dimension, used, budget, steered: true },\n })\n logger.info('Prompt %s budget exceeded (%d/%d); steering model to wrap up', dimension, used, budget)\n return { kind: 'continue' }\n }\n const partialText = assistantMessage.content\n .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n .trim()\n emitter.emit('prompt.budget', { turnIndex: completedTurn, dimension, used, budget, steered: false })\n recorder.record({\n kind: 'lifecycle',\n node: 'prompt.budget',\n turn: completedTurn,\n payload: { dimension, used, budget, steered: false },\n })\n throw new PromptBudgetExceededError(dimension, used, budget, partialText)\n}\n\n/** 工具轮数上限检查:未耗尽 → run-tools;耗尽则扩展/steer 收尾(continue)/硬停(抛 MaxToolTurnsError)。 */\nfunction evaluateMaxToolTurnsOutcome(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome {\n const { emitter, logger, messages } = ctx\n const { clock, recorder } = deps\n\n if (state.toolTurnCount < effectiveMaxToolTurns(ctx)) {\n return { kind: 'run-tools' }\n }\n\n // R7 诚实报数:配置 token 预算后窗口 ×4,limit 报有效窗口值而非原始配置。\n const limit = effectiveMaxToolTurns(ctx)\n const maxExtensions = ctx.budget.maxToolTurnExtensions ?? 0\n // RFC-104 D1(R1):扩展必须过进展闸门——上一预算窗口无真实进展(无写成功、\n // 签名多样性低 = 循环嫌疑)则不再续命,直接走 steer 收尾 / 硬停。\n const windowProgress = hasWindowProgress(state.windowStats)\n if (state.extensionsUsed < maxExtensions && windowProgress) {\n state.extensionsUsed++\n emitter.emit('max.turns', { turnIndex: completedTurn, maxTurns: limit, extended: true })\n recorder.record({\n kind: 'lifecycle',\n node: 'max.turns',\n turn: completedTurn,\n payload: { maxTurns: limit, extended: true, extension: state.extensionsUsed, maxExtensions },\n })\n logger.info(\n 'Tool budget exhausted (%d); extending %d/%d (per-turn transformContext compacts on token threshold)',\n limit,\n state.extensionsUsed,\n maxExtensions,\n )\n state.toolTurnCount = 0\n resetProgressWindow(state.windowStats)\n return { kind: 'run-tools' }\n }\n\n if (ctx.budget.budgetSteerWrapUp && !state.budgetSteeringIssued) {\n state.budgetSteeringIssued = true\n sealToolCallsForWrapUp(assistantMessage, messages, clock)\n messages.push({\n role: 'user',\n content: [{ type: 'text', text: BUDGET_STEER_PROMPT }],\n timestamp: clock.now(),\n internal: true,\n } as AgentMessage)\n // progressDenied:还有扩展额度、但窗口无进展被闸门拒绝(区别于额度耗尽)。\n const progressDenied = state.extensionsUsed < maxExtensions && !windowProgress\n emitter.emit('max.turns', {\n turnIndex: completedTurn,\n maxTurns: limit,\n extended: false,\n steered: true,\n ...(progressDenied ? { progressDenied: true } : {}),\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'max.turns',\n turn: completedTurn,\n payload: {\n maxTurns: limit,\n extended: false,\n steered: true,\n progressDenied,\n extension: state.extensionsUsed,\n maxExtensions,\n },\n })\n logger.info(\n progressDenied\n ? 'Tool budget window showed no progress (loop suspected); steering model to wrap up'\n : 'Tool budget exhausted (%d); steering model to wrap up in one final turn',\n limit,\n )\n return { kind: 'continue' }\n }\n\n const partialText = assistantMessage.content\n .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n .trim()\n const progressDenied = state.extensionsUsed < maxExtensions && !windowProgress\n emitter.emit('max.turns', {\n turnIndex: completedTurn,\n maxTurns: limit,\n extended: false,\n ...(progressDenied ? { progressDenied: true } : {}),\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'max.turns',\n turn: completedTurn,\n payload: {\n maxTurns: limit,\n extended: false,\n steered: false,\n progressDenied,\n extension: state.extensionsUsed,\n maxExtensions,\n },\n })\n throw new MaxToolTurnsError(limit, partialText, maxExtensions)\n}\n\n/**\n * RFC-142 D4:max_tokens 截断导致工具参数半截 JSON(`argumentsParseFailed` 标记,\n * 由 provider 流层 `toolCallFromParsed` 置位)→ 不执行损坏的调用,合成明确错误\n * tool_result 引导模型重发;同轮其余参数完整的调用不受影响(照常走 handleToolCalls)。\n *\n * 判据显式依赖解析状态而非\"arguments 为空\"——合法的无参工具调用(arguments 恰为 {})\n * 不会被误伤(RFC-142 重要事项规则9)。若本轮全部 tool_call 均损坏,仍返回 null 委托\n * handleToolCalls:合成的 tool_result 已入 messages,run-tools 阶段对已有结果的调用\n * 天然跳过(与 sealToolCallsForWrapUp 同款配对保护路径)。\n */\nfunction handleMaxTokensBrokenToolCall(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n _state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n if (assistantMessage.stopReason !== 'max_tokens') return null\n const broken = getToolCallsByAssistantMessage(assistantMessage).filter(\n (tc) => tc.argumentsParseFailed === true,\n )\n if (broken.length === 0) return null\n\n const { logger, messages } = ctx\n for (const tc of broken) {\n messages.push({\n role: 'tool_result',\n toolCallId: tc.id,\n toolName: tc.name,\n content: [\n {\n type: 'text',\n text: 'Tool call arguments were cut off by the output token limit and could not be parsed. Re-issue the call, splitting large content into smaller pieces if necessary.',\n },\n ],\n isError: true,\n details: {},\n timestamp: deps.clock.now(),\n internal: true,\n } as AgentMessage)\n logger.warn(\n 'Tool call %s (%s) had truncated arguments (max_tokens) on turn %d; synthesized error result',\n tc.id,\n tc.name,\n completedTurn + 1,\n )\n }\n deps.recorder.record({\n kind: 'lifecycle',\n node: 'turn.truncated',\n turn: completedTurn,\n payload: { action: 'broken-tool-call', count: broken.length },\n })\n // 不终止本轮分派:委托 handleToolCalls 让参数完整的其余调用照常执行。\n return null\n}\n\n/** 含 tool_use → 先查 token/时间预算,再查工具轮数上限;否则委托链上继续(null)。 */\n/**\n * RFC-203:滑动窗口停滞检测 handler。位于预算 handler 之前(handleToolCalls 内部先于\n * evaluateBudgetBreachOutcome)——停滞检出早于预算触顶是本机制的存在意义。\n *\n * 时序(评审 H1):`classifyTurnOutcome` 每 turn 恰好调用一次——此处先 `onTurnEnd()` 切桶\n * (把 runTools 期间经 recordCall 累积的本 turn 调用归档进环形窗),再对已关闭窗口求值。\n * 注意切桶发生在**所有** turn(含无 tool_use 的纯文本 turn 与已引导后的 turn),保证窗口\n * 计数正确滑动;只有判定命中且未引导过才注入。\n *\n * 注入序列(评审 C1):`classifyTurnOutcome` 在 runTools **之前**被调用,此时\n * assistantMessage 可能携带未执行的 tool_calls——必须先 `sealToolCallsForWrapUp` 合成\n * \"not executed\" tool_result 配对,否则下一轮 prompt 的 tool_use 无配对 provider 400。\n * 逐字复刻 budgetSteer 的 seal→push→emit→record 序列。\n */\nfunction handleStallDetection(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n const detector = state.stallDetector\n if (!detector) return null\n detector.onTurnEnd()\n if (state.stallSteeringIssued) return null\n const evaluation = detector.evaluate()\n if (!evaluation.stalled) return null\n\n const { emitter, logger, messages } = ctx\n const { clock, recorder } = deps\n state.stallSteeringIssued = true\n sealToolCallsForWrapUp(assistantMessage, messages, clock)\n messages.push({\n role: 'user',\n content: [{ type: 'text', text: STALL_STEER_PROMPT }],\n timestamp: clock.now(),\n internal: true,\n } as AgentMessage)\n emitter.emit('progress.stall', {\n turnIndex: completedTurn,\n windowTurns: evaluation.windowTurns,\n maxRepeat: evaluation.maxRepeat,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'progress.stall',\n turn: completedTurn,\n payload: { windowTurns: evaluation.windowTurns, maxRepeat: evaluation.maxRepeat },\n })\n logger.info(\n 'Stall detected (same signature repeated %d times over %d turns, zero write successes); steering model to change approach',\n evaluation.maxRepeat,\n evaluation.windowTurns,\n )\n return { kind: 'continue' }\n}\n\nfunction handleToolCalls(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n if (!hasToolCalls(assistantMessage)) return null\n return (\n evaluateBudgetBreachOutcome(assistantMessage, ctx, completedTurn, state, deps) ??\n evaluateMaxToolTurnsOutcome(assistantMessage, ctx, completedTurn, state, deps)\n )\n}\n\n/**\n * RFC-232 D1/D2(终局修正,2026-07-25):工具调用协议违约兜底——模型/代理把工具调用写成了\n * 字面文本 XML(`<invoke name=\"X\">...</invoke>`),而非发出结构化 tool_call block。\n *\n * **纵深防御的第二层**:第一层是 wire 层 `recoverLiteralInvokeToolCalls`\n * (`@x-otto/provider` stream-state.ts,4 个 wire 协议实现的组装 done 事件前调用一次,\n * provider-agnostic)——绝大多数违约态在那里已被直接救回执行,`hasToolCalls` 变为\n * true,本 handler 不会触发。本 handler 只处理**救回失败的残留**(XML 结构不完整、\n * 工具名不在白名单、极端情况下 registeredToolNames 未正确传递)。\n *\n * 判据演进(事故 f9bab4c0 复现 8+ 次,跨两种形态、跨两个 wire 协议才摸清全貌):\n * - 形态A:stopReason='tool_use' 但无 tool_call block(turn 7,oxygen-claude/anthropic-messages)。\n * - 形态B:stopReason='end_turn',纯文本内容以完整 `<invoke>` 块收尾(turn 5+,\n * oxygen/openai-completions,GLM-5.2)——模型把工具调用写完就\"正常结束\"。\n * 原判据 `stopReason==='tool_use'` 只覆盖形态A。终局判据不依赖 stopReason(provider-\n * agnostic):`!hasToolCalls(message) && hasTrailingInvokeXml(lastText, toolNames)`——\n * 复用与 wire 层完全相同的检测逻辑(`@x-otto/provider` 单一实现,不重复判据)。\n *\n * 位置约束(评审 P0,对新判据依然有效):必须放在 `handleStallDetection` 之后——后者第一行\n * `detector.onTurnEnd()` 是每 turn 无条件切桶(RFC-203 停滞窗口不变量),放其之前 return 会\n * 跳过切桶破坏停滞检测。放在 `handleToolCalls` 之前——判据 `!hasToolCalls` 与 handleToolCalls\n * 的 `hasToolCalls` 早退互斥。\n *\n * 未耗尽 → 注入纠正提示重跑本轮(continue);耗尽 → 干净 terminate(不再无声)。\n * 两个结局都 emit + record `tool.call.mismatch`(RFC-232 D4):observable 供 TUI toast、\n * traceable 供事后统计违约态复发率(wire 层救回成功的不计入——只统计救回失败的残留)。\n */\nfunction handleToolCallProtocolMismatch(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: OutcomeDeps,\n): TurnOutcome | null {\n if (hasToolCalls(assistantMessage)) return null\n const lastText = lastTextContent(assistantMessage)\n if (lastText === null) return null\n const toolNames = new Set(ctx.toolExecutor.list().map((t) => t.name))\n if (!hasTrailingInvokeXml(lastText, toolNames)) return null\n const { emitter, logger, messages } = ctx\n const { clock, recorder } = deps\n\n if (state.protocolMismatchRetryCount < MAX_PROTOCOL_MISMATCH_RETRIES) {\n state.protocolMismatchRetryCount++\n messages.push({\n role: 'user',\n content: [{ type: 'text', text: PROTOCOL_MISMATCH_STEER_PROMPT }],\n timestamp: clock.now(),\n internal: true,\n } as AgentMessage)\n logger.warn(\n 'Tool-call protocol mismatch on turn %d (stopReason=%s, message ends with literal <invoke> XML instead of a structured tool call — wire-layer recovery did not catch it); injecting correction prompt (%d/%d)',\n completedTurn + 1,\n assistantMessage.stopReason,\n state.protocolMismatchRetryCount,\n MAX_PROTOCOL_MISMATCH_RETRIES,\n )\n emitter.emit('tool.call.mismatch', {\n turnIndex: completedTurn,\n outcome: 'retry',\n retryCount: state.protocolMismatchRetryCount,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'tool.call.mismatch',\n turn: completedTurn,\n payload: { outcome: 'retry', retryCount: state.protocolMismatchRetryCount },\n })\n return { kind: 'continue' }\n }\n logger.warn(\n 'Tool-call protocol mismatch persisted after %d retries on turn %d; terminating (not silently)',\n MAX_PROTOCOL_MISMATCH_RETRIES,\n completedTurn + 1,\n )\n emitter.emit('tool.call.mismatch', {\n turnIndex: completedTurn,\n outcome: 'terminate',\n retryCount: state.protocolMismatchRetryCount,\n })\n recorder.record({\n kind: 'lifecycle',\n node: 'tool.call.mismatch',\n turn: completedTurn,\n payload: { outcome: 'terminate', retryCount: state.protocolMismatchRetryCount },\n })\n return { kind: 'terminate' }\n}\n\n/**\n * RFC-118 S4:责任链分派(原 ~180 行平铺 if/else 拆分,参照按任务类型枚举 + match 分派的模式)。\n * 每个 handler 只裁决自己的 stopReason 分支,返回 null 委托下一个;链尽头兜底 terminate\n * (正常停止且无 tool_use)。行为逐分支零变化——顺序与判定条件与拆分前逐行一致。\n */\nconst OUTCOME_HANDLERS = [\n handleRefusal,\n handleMaxTokensTruncation,\n handleMaxTokensBrokenToolCall,\n // RFC-203:停滞检测在预算裁决(handleToolCalls 内的 budget/maxTurns)之前——早发现是\n // 本机制的存在意义;且它对所有 turn 做切桶(含纯文本 turn),必须在 handleToolCalls 的\n // hasToolCalls 早退之前执行。refusal/truncation 两个 handler 命中即终止/续写,属异常\n // 分支,不参与停滞窗口统计(其 turn 桶由下一次正常 turn 的切桶自然吸收)。\n handleStallDetection,\n // RFC-232 D1:协议违约兜底——放在 handleStallDetection 之后(保 onTurnEnd 切桶先执行)、\n // handleToolCalls 之前(!hasToolCalls 与 hasToolCalls 早退互斥)。\n handleToolCallProtocolMismatch,\n handleToolCalls,\n] as const\n\n/**\n * 据 stopReason 分派本轮去向。可能 throw MaxToolTurnsError(预算耗尽且无扩展)。\n * state 计数原地累加;续写消息直接 push 进 ctx.messages。\n */\nexport function classifyTurnOutcome(\n assistantMessage: AssistantMessage,\n ctx: WorkLoopContext,\n completedTurn: number,\n state: TurnLoopState,\n deps: { clock?: ClockPort; recorder?: TraceRecorder } = {},\n): TurnOutcome {\n const resolvedDeps: OutcomeDeps = {\n clock: deps.clock ?? systemClock,\n recorder: deps.recorder ?? noopRecorder,\n }\n for (const handler of OUTCOME_HANDLERS) {\n const outcome = handler(assistantMessage, ctx, completedTurn, state, resolvedDeps)\n if (outcome) return outcome\n }\n return { kind: 'terminate' }\n}\n\n/**\n * RFC-104 D3:轮次上限有效值——配置 token 预算时 ×4 放宽(固定倍数,与 extensions 解耦;\n * 理由见 RFC-104 §3 D3:安全阀退位到足够大的固定后备位)。未配置 = 原值(零行为变化)。\n */\nexport function effectiveMaxToolTurns(ctx: WorkLoopContext): number {\n const base = ctx.budget.maxToolTurns ?? ENGINE_DEFAULTS.maxToolTurns\n return ctx.budget.promptOutputTokenBudget !== undefined && ctx.budget.promptOutputTokenBudget > 0\n ? base * 4\n : base\n}\n\n/** RFC-104 D3:预算超额求值(任一维度超即返回;tokens 优先——两者同超时报 tokens)。 */\nfunction evaluatePromptBudget(\n ctx: WorkLoopContext,\n state: TurnLoopState,\n clock: ClockPort,\n): { dimension: 'tokens' | 'time'; used: number; budget: number } | null {\n const tokenBudget = ctx.budget.promptOutputTokenBudget\n if (tokenBudget !== undefined && tokenBudget > 0 && state.promptOutputTokens >= tokenBudget) {\n return { dimension: 'tokens', used: state.promptOutputTokens, budget: tokenBudget }\n }\n const timeBudget = ctx.budget.promptWallClockBudgetMs\n if (timeBudget !== undefined && timeBudget > 0) {\n const elapsed = clock.now() - state.promptStartedAt\n if (elapsed >= timeBudget) {\n return { dimension: 'time', used: elapsed, budget: timeBudget }\n }\n }\n return null\n}\n\n/**\n * steer 收尾轮前,为本轮未执行的 tool_use 合成拒绝 tool_result——保 tool_use↔tool_result\n * 配对不变量(orphan tool_use → provider 400)。轮次与 token/时间两条 steer 路径共用。\n *\n * 改用共享的 `sealToolCalls`(终局审查 2026-07-21 修复)而非直接 push:此前本函数无差别\n * 为 `assistantMessage` 的全部 tool_use 合成 tool_result,不检查是否已有配对——\n * `handleMaxTokensBrokenToolCall`(同一责任链、位于本 handler 之前)会为\n * `argumentsParseFailed` 的调用先合成一条 tool_result;若同一 turn 恰好也命中停滞检测,\n * 本函数会为同一批 tool_call 再合成一条,产生同一 toolCallId 两条 tool_result(违反\n * provider 的 1:1 配对不变量)。`sealToolCalls` 内部按 `resolvedFrom` 扫描已配对的\n * toolCallId 集合,天然跳过已处理的调用。\n */\nfunction sealToolCallsForWrapUp(\n assistantMessage: AssistantMessage,\n messages: AgentMessage[],\n clock: ClockPort,\n): void {\n sealToolCalls(messages, getToolCallsByAssistantMessage(assistantMessage), clock, {\n isError: true,\n text: 'Tool budget reached; not executed — wrap-up requested.',\n internal: true,\n })\n}\n","import { hasToolCalls } from '@x-otto/interchange'\nimport type { AssistantMessage, Message, Model, ThinkingLevel } from '@x-otto/interchange'\nimport type { CacheRetention, ToolDefinition } from '@x-otto/provider'\nimport {\n consumePause,\n createPauseController,\n} from './debug-port'\nimport { ContextPipeline } from './context-pipeline'\nimport { AbortError } from './errors'\nimport { MAX_IMAGES_PER_REQUEST } from './image-degradation'\nimport { applyVisionPipeline } from './vision-gate'\nimport { createProgressWindow, resetProgressWindow } from './progress-window'\nimport { ENGINE_DEFAULTS } from './engine-nodes'\nimport {\n PromptToolResultAllowance,\n promptToolResultAllowanceChars,\n} from './tool-result-clamp'\nimport { StallDetector } from './stall-detector'\nimport {\n systemClock,\n systemRandom,\n systemSleep,\n} from './ports'\nimport { createStreamTracePersister, noopRecorder } from './trace'\nimport type { ToolExecutor } from './tool-executor'\nimport { runTools } from './tool-turn-runner'\nimport {\n DEFAULT_STREAM_RETRY,\n handleStreamError,\n type StreamErrorState,\n} from './work-loop-stream-error'\nimport type {\n EnginePorts,\n LoopBudget,\n RetryPolicy,\n ContextTransform,\n} from './work-loop-context-groups'\nimport { classifyTurnOutcome, effectiveMaxToolTurns, type TurnLoopState } from './work-loop-turn-outcome'\nimport type {\n AgentEventType,\n AgentEvents,\n AgentMessage,\n AgentStatus,\n AgentTool,\n StreamFunction,\n} from './types'\n\nexport {\n DEFAULT_STREAM_RETRY,\n NETWORK_DISCONNECT_RETRY,\n computeBackoffDelay,\n type StreamRetryConfig,\n} from './work-loop-stream-error'\nexport { MAX_TRUNCATION_CONTINUATIONS } from './work-loop-turn-outcome'\nexport type {\n EnginePorts,\n LoopBudget,\n RetryPolicy,\n ContextTransform,\n} from './work-loop-context-groups'\n\ninterface Emitter {\n emit<K extends AgentEventType>(type: K, ...args: Parameters<AgentEvents[K]>): void\n}\n\nexport interface WorkLoopContext {\n model: Model\n systemPrompt: string\n /** 每轮可变的 system 尾段(volatile):进 StreamContext.systemPromptSegments,落在 cache 断点之后。 */\n systemTail?: string[]\n /** prompt cache 保留档(none/short/long);透传给 provider 选 ephemeral TTL。 */\n cacheRetention?: CacheRetention\n messages: AgentMessage[]\n thinkingLevel: ThinkingLevel\n maxTokens?: number\n temperature?: number\n convertToLLM: (\n messages: AgentMessage[],\n ) => Message[] | Promise<Message[]>\n /** RFC-170 D4:turn 边界增量持久化回调(见 AgentRunContext.onTurnPersist 完整设计说明)。 */\n onTurnPersist?: () => void\n logger: import('@x-otto/shared').Logger\n getSteeringMessages: () => Promise<AgentMessage[]>\n getFollowUpMessages: () => Promise<AgentMessage[]>\n toolExecutor: ToolExecutor\n stream: StreamFunction\n signal: AbortSignal\n emitter: Emitter\n transitionTo: (status: AgentStatus) => void\n /**\n * RFC-183 P1-1(收束 RFC-144 M1-M4 的 5 个散装 callback):lifecycleAsync 工具生命\n * 周期控制器——register/hasPending/waitForPending/notifyCompletion/currentPromptSeq\n * 五个操作面收束为单一接口对象(完整设计说明见 types.ts LifecycleAsyncController)。\n *\n * fail-open:缺省(未注入)= runTools 遇到 lifecycleAsync 工具记录告警并退化为同步\n * 执行(不静默丢弃工具调用,只失去\"不阻塞 loop\"的收益)——与 RFC-144 M1 起的缺省\n * 行为逐字节一致。收束的关键收益:**部分接线在类型上不可表达**——原先\"只接\n * hasPending 不接 waitForPending 会造成 pending yield 空转死循环\"的防御性双检查\n * (三处 `hasPending?.() && waitForPending` 注释负担)简化为单一存在性判断。\n */\n lifecycleAsync?: import('./types').LifecycleAsyncController\n\n /** RFC-192 D2:引擎确定性端口注入组。全部可选(缺省回退系统实现)。 */\n ports?: EnginePorts\n /** RFC-192 D2:预算与暂停控制组。maxToolTurns 必填。 */\n budget: LoopBudget\n /** RFC-192 D2:流错误重试策略组。全部可选。 */\n retry?: RetryPolicy\n /** RFC-192 D2:上下文变换与视觉委托组。全部可选。 */\n contextTransform?: ContextTransform\n}\n\ninterface MutableLLMParams {\n systemPrompt: string\n thinkingLevel: ThinkingLevel | undefined\n maxTokens: number | undefined\n temperature: number | undefined\n}\n\n/** H2 tier1:截断升档——把 maxTokens 抬到 min(current*8, 模型上限);已达上限/无法抬高返回 undefined。 */\nexport function computeEscalatedMaxTokens(\n current: number | undefined,\n model: Model,\n): number | undefined {\n const cap = model.maxOutputTokens\n if (!cap || cap <= 0) {\n return current ? current * 8 : undefined\n }\n if (current === undefined) return cap\n if (current >= cap) return undefined\n return Math.min(current * 8, cap)\n}\n\n/** RFC-144 M3:等待 lifecycleAsync pending 完成期间轮询 steering 队列的周期(ms)。\n * 短于 TUI 后台面板轮询频率(既有 500ms 惯例)——工具等待场景更需要及时响应用户\n * 中断意图,200ms 在\"及时性\"与\"轮询开销\"之间取得平衡(同周期数量级的检测延迟对\n * 用户几乎不可感知,且 pending 场景轮询次数有限,不是长驻高频循环)。 */\nconst STEERING_POLL_DURING_PENDING_MS = 200\n\n/**\n * RFC-144 M3:`waitForPendingLifecycleTools()` 与\"steering 消息到达\"race——满足 RFC\n * 硬性要求\"等工具结果期间,外层循环仍正常消费 steering/followUp\"。\n *\n * 谁先完成就返回谁的结果:pending 先 settle → 返回空数组(外层 continue 正常重入\n * 内层循环,此时 messages 里已有落库的完成通知,LLM 能立即看到);steering 先到达 →\n * drain 并返回消息(外层立即注入 messages,不再等待 pending——pending 工具仍在后台\n * 跑,不会被取消,下次进入 runTools 前会再次检查是否已完成)。\n *\n * 实现:`waitForPending` 与一个轮询 loop 同时挂起,用一个共享的 `settled` 标记确保\n * 只有先完成的一方产生效果——轮询到 steering 后立即停止(不需要额外的 cleanup 句柄,\n * `while` 循环条件检查 `settled` 即自然退出)。\n */\n// 已知边界(未进一步处理,微任务级窗口、影响可忽略):若 steering 消息恰好在\n// `getSteeringMessages()` 解析的同一微任务窗口内到达、且 `waitForPending` 也在此刻\n// settle,存在极窄的竞态——消息可能被 drain 但因外层 race 已选择 pending 分支而未被\n// 交付。真实影响窗口是亚毫秒级(远小于 200ms 轮询间隔本身覆盖的场景),且这是 UX\n// 增强而非正确性关键路径(steering 消息只是提前一点被消费,不消费也会在下次\n// runTools 入口被拿到),不值得为此引入额外的同步原语。\n/**\n * RFC-144 M6(独立评审 HIGH 修复):把 `signal` 的 abort 事件也纳入等待 race——\n * 此前 `raceWaitWithSteering()` 只在 pending settle 与 steering 到达之间 race,用户\n * 在等待期间 `Agent.abort()`(如 Ctrl+C)不会被感知,直到 pending 工具真正完成或\n * steering 到达才会继续,abort 意图被静默吞掉长达数秒到数分钟(lifecycleAsync 工具\n * 通常是长任务)。\n *\n * 用一个绑定 `signal` 的 Promise 加入 race——一旦 abort,立即让 `Promise.race` 落定,\n * 调用方据 `aborted` 标记决定是否短路抛出 `AbortError`(不能在本函数内部直接抛——\n * `pendingWaiter`/`steeringPoller` 仍在后台跑,抛出会跳过后续的 listener 清理,\n * 交还调用方在明确的地方处理更清晰)。\n */\nasync function raceWaitWithSteeringAndAbort(\n waitForPending: Promise<void>,\n getSteeringMessages: () => Promise<AgentMessage[]>,\n signal: AbortSignal,\n): Promise<{ steeringMessages: AgentMessage[]; aborted: boolean }> {\n let settled = false\n let steeringResult: AgentMessage[] = []\n let aborted = signal.aborted\n\n const pendingWaiter = waitForPending.then(() => {\n settled = true\n })\n\n const abortWaiter = new Promise<void>((resolve) => {\n if (signal.aborted) {\n aborted = true\n // 独立评审 LOW 修复:已提前 abort 时也要置 settled——否则 steeringPoller 会继续\n // 空转到下一次 200ms 轮询才发现 settled 为 true(unref 定时器不阻塞进程退出,\n // 非功能性 bug,但与下方 onAbort 分支的行为不一致,此处补齐保持语义统一)。\n settled = true\n resolve()\n return\n }\n const onAbort = () => {\n aborted = true\n settled = true\n resolve()\n }\n signal.addEventListener('abort', onAbort, { once: true })\n })\n\n const steeringPoller = (async (): Promise<void> => {\n while (!settled) {\n // 防御性兜底(独立评审建议):`getSteeringMessages()` 契约上不应 reject(Agent 侧\n // 实现是纯 Map/数组操作),但接口签名本身未通过类型系统强制这一点。若宿主注入了\n // 会 reject 的实现,吞掉异常退化为\"本轮未拿到 steering\",而非让 unhandled rejection\n // 淹没到进程级——等待 pending settle 仍是安全的兜底路径(steeringPoller 静默失败\n // 不影响 pendingWaiter 继续等待真正的工具完成)。\n const messages = await getSteeringMessages().catch(() => [] as AgentMessage[])\n if (messages.length > 0) {\n steeringResult = messages\n settled = true\n return\n }\n if (settled) return\n // `unref()`:定时器不应阻止进程/事件循环退出——若 `pendingWaiter` 先 settle,\n // 这个已排队的 setTimeout 仍会在原定延迟后真正 fire(timer 本身不能被取消,\n // 只是 while 条件会让回调空转直接退出循环),但不应因为它还没到期就让整个\n // 进程/测试 worker 挂着等它(曾在批量跑多个测试文件时观察到进程迟迟不退出)。\n await new Promise((r) => {\n const timer = setTimeout(r, STEERING_POLL_DURING_PENDING_MS)\n timer.unref?.()\n })\n }\n })()\n\n await Promise.race([pendingWaiter, abortWaiter, steeringPoller])\n return { steeringMessages: steeringResult, aborted }\n}\n\n/**\n * RFC-144 M6:`waitForPendingLifecycleTools()` 与 `signal` 的 abort 事件 race(无\n * steering 消费需求的简化版,用于 workLoop 返回前的最终兜底等待)。返回 `true`\n * 表示 abort 先发生(调用方应据此抛 `AbortError`,而非误认为 pending 已 settle)。\n */\nexport async function raceWaitWithAbort(waitForPending: Promise<void>, signal: AbortSignal): Promise<boolean> {\n if (signal.aborted) return true\n\n let aborted = false\n const abortWaiter = new Promise<void>((resolve) => {\n const onAbort = () => {\n aborted = true\n resolve()\n }\n signal.addEventListener('abort', onAbort, { once: true })\n })\n\n await Promise.race([waitForPending, abortWaiter])\n return aborted\n}\n\nasync function runTurn(\n ctx: WorkLoopContext,\n params: MutableLLMParams,\n turnIndex: number,\n lastTokenUsage: { input: number; output: number } | undefined,\n): Promise<AssistantMessage> {\n const { signal, emitter, logger } = ctx\n const debug = ctx.ports?.debug\n const recorder = ctx.ports?.recorder ?? noopRecorder\n\n // RFC-118 S15:pause 快照构造收敛至 createPauseController(原三处 copy-paste)。\n const pause = createPauseController({\n debug,\n getTurn: () => turnIndex,\n getMessagesCount: () => ctx.messages.length,\n getTokenUsage: () => lastTokenUsage,\n getLlmParams: () => params,\n getMessagesForSummary: () => ctx.messages,\n })\n\n let contextMessages = [...ctx.messages]\n if (ctx.contextTransform?.transformContext) {\n const transformed = await ctx.contextTransform.transformContext(contextMessages, signal)\n contextMessages = transformed\n\n if (contextMessages.length !== ctx.messages.length) {\n logger.info(\n 'Context transformed for turn %d: %d -> %d messages',\n turnIndex + 1,\n ctx.messages.length,\n contextMessages.length,\n )\n }\n\n await pause('context.transform', 0, {\n messagesCount: contextMessages.length,\n summarySource: contextMessages,\n metadata: {\n originalCount: ctx.messages.length,\n transformedCount: contextMessages.length,\n },\n })\n }\n\n const tools = createToolDefinitions(ctx.toolExecutor.list())\n\n consumePause(await pause('model.before', 0), {\n messages: ctx.messages,\n setSystemPrompt: (v) => {\n params.systemPrompt = v\n },\n setTemperature: (v) => {\n params.temperature = v\n },\n setMaxTokens: (v) => {\n params.maxTokens = v\n },\n setThinkingLevel: (v) => {\n params.thinkingLevel = v as ThinkingLevel\n },\n })\n\n const errorState: StreamErrorState = { hasAttemptedReactiveCompact: false, hasAttemptedImageDegradation: false, transientRetries: 0, hasAttemptedAuthRetry: false }\n const retryCfg = ctx.retry?.streamRetry ?? DEFAULT_STREAM_RETRY\n const random = ctx.ports?.random ?? systemRandom\n const sleep = ctx.ports?.sleep ?? systemSleep\n\n let hasEscalatedMaxTokens = false\n\n const volatileTail = (ctx.systemTail ?? []).filter((t) => t.length > 0)\n\n while (true) {\n // RFC-183 O-3:视觉投影管线单入口——capImagesPerRequest 图片上限(RFC-148 M5\n // imageConstraints 声明优先)→ applyVisionGate 能力门控/委托描述(RFC-181)→\n // image.cap / image.vision-gate / image.vision-delegate 三类 trace/emit 副作用,\n // 编排细节收束进 vision-gate.ts 的 applyVisionPipeline,副作用语义逐字节保持\n //(含 RFC-181 D6 的双事件区分:vision-gate 标记门控触发,vision-delegate 仅在\n // 委托实际调用时额外触发)。\n const projectedMessages = await applyVisionPipeline(contextMessages, {\n model: ctx.model,\n describeImages: ctx.contextTransform?.describeImages,\n signal: ctx.signal,\n turnIndex,\n maxImagesDefault: MAX_IMAGES_PER_REQUEST,\n record: (node, payload) => recorder.record({ kind: 'lifecycle', node, turn: turnIndex, payload }),\n emitDelegate: (payload) => ctx.emitter.emit('image.vision-delegate', payload),\n })\n const llmMessages = await ctx.convertToLLM(projectedMessages)\n\n const streamContext = ContextPipeline.buildTurnInput({\n stableSystem: params.systemPrompt,\n volatileTail,\n llmMessages,\n model: ctx.model,\n tools,\n cacheRetention: ctx.cacheRetention,\n thinkingLevel: params.thinkingLevel,\n maxTokens: params.maxTokens,\n temperature: params.temperature,\n })\n\n const es = ctx.stream(streamContext, signal)\n\n // RFC-102 D1+D2:持久化边走白名单过滤(delta 不落盘)+ *_end 滞后一拍剥离 partial;\n // emitter 内存旁路保持全量事件(TUI live 渲染/投影器/观测依赖 delta)。\n const tracePersister = createStreamTracePersister(recorder)\n\n try {\n for await (const event of es) {\n if ((event as { type?: string }).type !== 'error') {\n emitter.emit('stream.event', { event })\n tracePersister.push(event as { type?: string } & Record<string, unknown>, turnIndex)\n }\n\n if (signal.aborted) {\n throw new AbortError()\n }\n }\n tracePersister.flush()\n\n const assistantMessage = await es.result()\n\n logger.info(\n 'Turn %d completed with stop reason %s (input=%d, output=%d)',\n turnIndex + 1,\n assistantMessage.stopReason,\n assistantMessage.usage.inputTokens,\n assistantMessage.usage.outputTokens,\n )\n\n if (\n assistantMessage.stopReason === 'max_tokens' &&\n !hasToolCalls(assistantMessage) &&\n !hasEscalatedMaxTokens\n ) {\n const bumped = computeEscalatedMaxTokens(params.maxTokens, ctx.model)\n if (bumped !== undefined && bumped > (params.maxTokens ?? 0)) {\n hasEscalatedMaxTokens = true\n logger.warn(\n 'Output truncated (max_tokens, no tool calls) on turn %d; escalating maxTokens %s -> %d and re-running',\n turnIndex + 1,\n params.maxTokens ?? '(default)',\n bumped,\n )\n params.maxTokens = bumped\n emitter.emit('turn.truncated', { turnIndex, action: 'escalate', maxTokens: bumped })\n recorder.record({\n kind: 'lifecycle',\n node: 'turn.truncated',\n turn: turnIndex,\n payload: { action: 'escalate', maxTokens: bumped },\n })\n continue\n }\n }\n\n await pause('model.after', 0)\n\n return assistantMessage\n } catch (error) {\n // abort/流式错误段:缓存的 *_end 保留 partial 落盘(无 done 段的 replay fallback 依赖它)\n tracePersister.flush()\n const outcome = await handleStreamError(\n error,\n ctx,\n turnIndex,\n contextMessages,\n errorState,\n retryCfg,\n ctx.retry?.retryAccelerate,\n random,\n sleep,\n )\n if (outcome.action === 'retry') {\n if (outcome.contextMessages) {\n contextMessages = outcome.contextMessages\n }\n continue\n }\n throw error\n }\n }\n}\n\nexport async function workLoop(context: WorkLoopContext): Promise<AssistantMessage> {\n let turnIndex = 0\n // RFC-203:停滞检测缺省启用;stallDetection: false 显式关闭。\n const stallConfig = context.budget.stallDetection\n const turnState: TurnLoopState = {\n continuationCount: 0,\n toolTurnCount: 0,\n extensionsUsed: 0,\n budgetSteeringIssued: false,\n windowStats: createProgressWindow(),\n promptOutputTokens: 0,\n promptStartedAt: (context.ports?.clock ?? systemClock).now(),\n stallDetector: stallConfig === false ? undefined : new StallDetector(stallConfig ?? {}),\n stallSteeringIssued: false,\n protocolMismatchRetryCount: 0,\n }\n let lastAssistantMessage: AssistantMessage | null = null\n let lastTokenUsage: { input: number; output: number } | undefined\n\n /**\n * RFC-340 M3:per-prompt 工具结果额度。窗口未知时 `promptToolResultAllowanceChars`\n * 返回 undefined → 退化为既有 per-tool 行为(D5-4 向后兼容)。\n */\n const createToolResultAllowance = (): PromptToolResultAllowance =>\n new PromptToolResultAllowance(\n promptToolResultAllowanceChars(\n context.model.contextWindow,\n ENGINE_DEFAULTS.promptToolResultFraction,\n ),\n ENGINE_DEFAULTS.minToolResultChars,\n )\n let toolResultAllowance = createToolResultAllowance()\n\n const { messages, signal, logger, emitter, transitionTo, getSteeringMessages } = context\n const debug = context.ports?.debug\n const clock = context.ports?.clock ?? systemClock\n const recorder = context.ports?.recorder ?? noopRecorder\n\n const params: MutableLLMParams = {\n systemPrompt: context.systemPrompt,\n thinkingLevel: context.thinkingLevel,\n maxTokens: context.maxTokens,\n temperature: context.temperature,\n }\n\n // RFC-118 S15:pause 快照构造收敛(原三处 copy-paste 之三)。\n const pause = createPauseController({\n debug,\n getTurn: () => turnIndex,\n getMessagesCount: () => messages.length,\n getTokenUsage: () => lastTokenUsage,\n getLlmParams: () => params,\n getMessagesForSummary: () => messages,\n })\n\n try {\n logger.info(\n 'Agent loop started for model %s with %d messages',\n context.model.id,\n messages.length,\n )\n\n await pause('loop.start', 0)\n recorder.record({ kind: 'lifecycle', node: 'loop.start', turn: turnIndex })\n\n /**\n * RFC-144 M3:内层 turn 循环的两种退出原因——`terminate` 是 `classifyTurnOutcome`\n * 判定的真正终止(原有语义不变);`pending` 是新增的 yield 原因:post-runTools\n * 检测到仍有未消费的 lifecycleAsync pending,内层循环主动让出控制权给外层,\n * 而非进入下一轮 `runTurn`(此时模型还没看到工具完成的结果,提前推理没有意义)。\n * 外层循环据此区分\"该走 followUp 检查收尾\"还是\"该等 pending 清空后重新进入\"。\n */\n type TurnLoopExitReason = 'terminate' | 'pending'\n\n /**\n * 内层 turn 循环(RFC-118 S2:从 `outer: while` + `continue outer` 标签跳转结构\n * 提取为内嵌函数,消除类 goto 控制流)——单个 prompt 回合内的 LLM turn + 工具执行\n * 循环,直到 classifyTurnOutcome 判 terminate(或 RFC-144 M3 新增的 pending yield)。\n * 闭包共享外层 turnState/params/turnIndex 等可变量(与提取前语义逐行一致,除\n * pending yield 外行为零变化)。\n */\n const runTurnsUntilTerminate = async (): Promise<TurnLoopExitReason> => {\n while (true) {\n if (signal.aborted) {\n throw new AbortError()\n }\n\n emitter.emit('turn.start', {\n turnIndex,\n toolTurnCount: turnState.toolTurnCount,\n // R7 诚实报数:x/y 的 y 报有效窗口值(配置 token 预算时 ×4,见 effectiveMaxToolTurns)。\n maxToolTurns: effectiveMaxToolTurns(context),\n promptOutputTokens: turnState.promptOutputTokens,\n promptOutputTokenBudget: context.budget.promptOutputTokenBudget,\n })\n recorder.record({ kind: 'lifecycle', node: 'turn.start', turn: turnIndex })\n logger.info('Turn %d started', turnIndex + 1)\n\n const assistantMessage = await runTurn(context, params, turnIndex, lastTokenUsage)\n\n messages.push(assistantMessage)\n lastAssistantMessage = assistantMessage\n lastTokenUsage = {\n input: assistantMessage.usage.inputTokens,\n output: assistantMessage.usage.outputTokens,\n }\n // RFC-104 D3:per-prompt output-token 累计(CC 口径——只计 output,见 RFC §3 D3)。\n turnState.promptOutputTokens += assistantMessage.usage.outputTokens\n\n emitter.emit('turn.end', { turnIndex, message: assistantMessage })\n // model/provider 随 payload 落盘——同一会话可跨回合切模型(`/model`),trace 回放\n // 需按 model 归因单个回合。此前 payload 丢弃了 assistantMessage 已有的 model 身份,\n // 迫使回放侧关联 `prompt.end` 事件才能得知。payload 是 `unknown`(trace 无 schema\n // 版本门)+ trace-view 白名单拾取解析 → 加字段对新旧 trace 双向兼容。\n recorder.record({\n kind: 'lifecycle',\n node: 'turn.end',\n turn: turnIndex,\n payload: {\n stopReason: assistantMessage.stopReason,\n usage: assistantMessage.usage,\n model: assistantMessage.model,\n provider: assistantMessage.provider,\n },\n })\n const completedTurn = turnIndex\n turnIndex++\n\n const outcome = classifyTurnOutcome(assistantMessage, context, completedTurn, turnState, {\n clock,\n recorder,\n })\n if (outcome.kind === 'terminate') {\n // RFC-144 M3:即使 LLM 判定本轮终止(如 steering 消费后模型直接确认收到、\n // 不再调用工具),若仍有此前发起、尚未 settle 的 lifecycleAsync pending\n // 工具,也不能真正终止——那些工具在跑,其 SystemNotificationMessage 会在\n // workLoop 返回后才落库,永远不会被本次 prompt 的任何一轮 LLM 看到(也不会\n // 有后续机会——见 M4 前 promptSeq 跨边界隔离尚未实现,新 prompt 不会捡起旧\n // pending 的通知)。修正:terminate 前先检查 pending,非空则视为 'pending'\n // 退出原因,交给外层循环等待(steering 消费不豁免这份等待义务,只是提前\n // 让 LLM 响应了用户的新指令,原 pending 工具仍会被等到完成)。\n //\n // RFC-183 P1-1:原先此处必须双检查 `hasPending?.() && waitForPending`(防止\n // \"只接前者不接后者\"的部分接线造成 pending yield 空转死循环)——收束为单一\n // LifecycleAsyncController 接口对象后,部分接线在类型上不可表达,单一存在性\n // 判断即保证\"返回 pending 后一定有真实等待发生\"的不变量。\n if (context.lifecycleAsync?.hasPending()) {\n return 'pending'\n }\n // RFC-170 D4:terminate 路径落盘——此刻 hasToolCalls(assistantMessage) 恒为\n // false(handleToolCalls 命中才会走 run-tools 分支,terminate 只来自\n // handleRefusal/handleMaxTokensTruncation 的无 tool_use 场景或链尾兜底),\n // messages 数组不含未配对 tool_use,可安全落盘。\n context.onTurnPersist?.()\n return 'terminate'\n }\n if (outcome.kind === 'continue') {\n // RFC-170 D4:continue 路径落盘——'continue' 只来自 handleMaxTokensTruncation\n // (本身无 tool_use)或 evaluateBudgetBreachOutcome/evaluateMaxToolTurnsOutcome\n // 的 steer 收尾分支(已在返回前调用 sealToolCallsForWrapUp 把所有未执行的\n // tool_use 补上 isError tool_result),落盘时同样不含未配对 tool_use。\n context.onTurnPersist?.()\n continue\n }\n\n transitionTo('tool_executing')\n\n const { ranAnyTool } = await runTools(\n assistantMessage,\n context,\n completedTurn,\n lastTokenUsage,\n turnState.windowStats,\n turnState.stallDetector,\n toolResultAllowance,\n )\n\n if (ranAnyTool) {\n turnState.toolTurnCount++\n }\n\n // RFC-170 D4:run-tools 路径落盘——runTools 已完成,本轮全部 tool_call 均已配对\n // tool_result(含合成的 isError 占位,见 tool-turn-runner.ts 的 sealToolCalls\n // 系列辅助),落盘时无未配对 tool_use。\n context.onTurnPersist?.()\n\n // RFC-145 D3:回合边界冲洗 nondet batch——runTools 之后、下一轮 runTurn 之前,\n // 本轮(模型流式 + 工具执行)累积的 draws 落成单条 batch 事件。\n context.ports?.nondetFlush?.()\n\n // RFC-144 M3:post-runTools yield——runTools 返回后(而非 classifyTurnOutcome\n // 内部,见 RFC \"重要性\"说明:outcome 判定在 runTools 之前,此时 lifecycleAsync\n // 工具尚未启动、pending 区必然为空,架构上不可能在那里感知 pending)检查是否\n // 仍有未消费的 lifecycleAsync pending——若有,内层循环主动 return,不进入下一次\n // `runTurn`(模型此刻还看不到工具完成结果,提前推理无意义,且会造成\"占着位置空转\")。\n // 外层循环收到 'pending' 退出原因后转 lifecycleAsync.waitForPending(),不误判为\n // \"本轮 turn 循环已正常收尾\"而走 followUp 检查/退出分支。\n //\n // RFC-183 P1-1:原 fail-open 双检查(hasPending && waitForPending 都接线才 yield,\n // 防部分接线空转)收束为单一接口存在性判断——部分接线在类型上已不可表达。\n // lifecycleAsync 缺省时退化为忽略 pending、继续内层循环(回到 M2 前行为:工具\n // 异步执行但 loop 不感知,仍安全,只失去\"等待汇合\"收益)。\n if (context.lifecycleAsync?.hasPending()) {\n return 'pending'\n }\n\n transitionTo('streaming')\n }\n }\n\n // 外层 prompt 循环:跑 turn 循环到 terminate,检查 followUp 队列——有则重置\n // per-prompt 状态再来一轮,无则收工。\n while (true) {\n if (signal.aborted) {\n throw new AbortError()\n }\n\n transitionTo('streaming')\n\n const exitReason = await runTurnsUntilTerminate()\n\n // RFC-144 M3:'pending' 退出——内层循环因未消费的 lifecycleAsync pending 而 yield,\n // 不是真正的 terminate。等待所有 pending settle(Agent.waitForPendingLifecycleTools),\n // 然后 `continue` 直接回到内层循环重新进入 `runTurnsUntilTerminate()`——不走下方的\n // followUp 检查/状态重置逻辑(那是\"整个 prompt 已完成,检查是否有排队的新用户消息\"\n // 的语义,与\"当前 prompt 内还有工具在跑\"是完全不同的两件事,混淆会导致下一轮\n // runTurn 前误把 per-prompt 状态清零)。\n //\n // RFC 硬性要求\"等工具结果期间,外层循环仍正常消费 steering/followUp\"——若单纯\n // `await waitForPendingLifecycleTools()`,用户在等待期间发的 steer 消息只会静默\n // 排队到 pending 工具真正完成后才被消费(下一次进 runTools 才检查),这不满足\n // \"用户可中断\"的即时性要求(尤其 lifecycleAsync 工具通常是长任务,用户等待期间\n // 想中途改变主意的场景真实存在)。用 pollSteeringDuringWait 与等待 race——谁先\n // 完成就走谁:pending settle 先到 → 正常 continue;steering 先到 → 立即消费并\n // 注入 messages(同 runTools 的 steering 处理语义:告知用户新指令已被采纳),\n // 但**不** seal 任何 tool_call(lifecycleAsync 工具的 tool_call 早已在启动时\n // 拿到终态 placeholder,没有\"未执行的 tool_call\"需要 seal——这与 runTools 内部\n // steering 分支的语义不同,那里要处理尚未执行的同步/异步工具调用)。\n if (exitReason === 'pending') {\n if (context.lifecycleAsync) {\n // RFC-144 M6:abort 信号也纳入 race——用户在 pending 等待期间 abort(如\n // Ctrl+C)应该被立即感知,而非静默吞掉直到工具完成或 steering 到达。\n const { steeringMessages, aborted } = await raceWaitWithSteeringAndAbort(\n context.lifecycleAsync.waitForPending(),\n getSteeringMessages,\n signal,\n )\n if (aborted) {\n throw new AbortError()\n }\n if (steeringMessages.length > 0) {\n messages.push(...steeringMessages)\n emitter.emit('steering.injected', { messages: steeringMessages })\n logger.info(\n 'Steering message(s) consumed while %d lifecycleAsync tool(s) still pending',\n steeringMessages.length,\n )\n }\n }\n continue\n }\n\n // RFC-180 D5:turn 边界暂停检查——'pending' 分支已在上方 `continue` 短路,故此处\n // 只有真正的 'terminate' 退出原因会到达(外层 turn 循环已跑完,onTurnPersist 已\n // 在 runTurnsUntilTerminate 内部三个落盘分支之一调用过,消息数组无未配对 tool_use,\n // 是安全的落盘边界)。命中暂停请求则不检查 followUp、不触发 todo 闸门续跑,直接\n // 跳出外层 while(true) 正常收尾(非 AbortError,workLoop 完整 return)。\n //\n // 独立评审 F4:暂停命中时下方 `pause('loop.end', ...)` 与对应 recorder 事件被跳过\n // ——trace 消费方需将 `run.paused` 会话事件(AgentSession 发出)视为等价的可观测\n // 锚点,\"该次 prompt 缺少 loop.end 事件\"是暂停场景下的合法状态,非数据缺失。\n if (context.budget.isPauseRequested?.()) {\n logger.info('Turn boundary reached with pause requested — yielding without followUp check')\n break\n }\n\n await pause('loop.end', 0)\n recorder.record({ kind: 'lifecycle', node: 'loop.end', turn: turnIndex })\n\n const followUpMessages = await context.getFollowUpMessages()\n\n await pause('follow.up.check', 0, {\n metadata: {\n hasFollowUp: followUpMessages.length > 0,\n followUpCount: followUpMessages.length,\n },\n })\n\n if (followUpMessages.length === 0) {\n break\n }\n\n messages.push(...followUpMessages)\n logger.info('Queued %d follow-up message(s)', followUpMessages.length)\n emitter.emit('follow.up.start', { messages: followUpMessages })\n turnState.toolTurnCount = 0\n turnState.extensionsUsed = 0\n turnState.budgetSteeringIssued = false\n resetProgressWindow(turnState.windowStats)\n // RFC-104 D3:预算是 per-prompt 语义——followUp(新用户回合)重置 token/时间基准。\n turnState.promptOutputTokens = 0\n turnState.promptStartedAt = clock.now()\n // RFC-203:停滞窗口同为 per-prompt 语义——followUp 清空重新累积。\n turnState.stallDetector?.reset()\n turnState.stallSteeringIssued = false\n // RFC-232 D2:协议违约重试计数同为 per-prompt 语义——followUp 重置。\n turnState.protocolMismatchRetryCount = 0\n // RFC-340 M3 D5-3:工具结果额度同为 per-prompt 语义——followUp(新用户回合)重新起算。\n // 上一轮的工具输出已经过压缩/裁剪治理,不应继续占用本轮额度。\n toolResultAllowance = createToolResultAllowance()\n }\n\n // RFC-144 M3(对抗测试发现的真实缺口,修正):兜底等待——若外层循环走到这里时仍有\n // 未消费的 lifecycleAsync pending(可能路径:pending yield 后 steering 抢先到达并被\n // 消费,`continue` 重新进入内层循环,这次 `classifyTurnOutcome` 完全不知道 pending\n // 的存在(架构决定,检测只能在 post-runTools),若模型这次响应恰好判定为\n // `terminate`(如模型只是简单确认 steering 指令、未再调用工具),内层循环会在\n // pending 尚未清空时就返回 `terminate`,外层循环随即走向此处),必须先等它们\n // settle(= 落库完成)再返回——否则后台仍在跑的工具永远不会有人 await 它的\n // `notifyCompletion()` promise,`messages.push(SystemNotificationMessage)` 虽然\n // 仍会异步执行(闭包捕获的是同一个数组),但发生在 `persistence.persistNew()`\n // 已经跑完之后,导致通知消息静默丢失(不进树、重启后也找不回)。\n //\n // 不需要 race steering(这里已经是\"即将返回\"的最终节点,没有\"外层循环继续跑\"的\n // 后续机会可以延后处理新到达的 steering——若有 steering 在此刻到达,留给调用方\n // 在下一次 prompt() 时处理,不在本次 workLoop 返回前额外消费,保持职责边界清晰)。\n //\n // RFC-144 M6(独立评审 HIGH 修复):仍需 race abort——若用户在这最后一段兜底等待\n // 期间 abort,不应该让它悬空到 pending 工具真正完成才响应(同上方 raceWaitWithSteeringAndAbort\n // 的理由,这里场景更简单,不涉及 steering,只需 pending 与 abort 两路 race)。\n if (context.lifecycleAsync?.hasPending()) {\n logger.info(\n 'Waiting for remaining lifecycleAsync pending tool(s) to settle before returning ' +\n '(reached loop exit with pending still outstanding)',\n )\n const aborted = await raceWaitWithAbort(context.lifecycleAsync.waitForPending(), signal)\n if (aborted) {\n throw new AbortError()\n }\n }\n\n if (!lastAssistantMessage) {\n throw new Error('Agent loop completed without producing a message')\n }\n\n await pause('agent.done', 0)\n recorder.record({ kind: 'lifecycle', node: 'agent.done', turn: turnIndex })\n logger.info('Agent loop finished after %d turn(s)', turnIndex)\n\n return lastAssistantMessage\n } catch (error) {\n if (error instanceof AbortError || signal.aborted) {\n logger.warn('Agent loop aborted at turn %d', turnIndex + 1)\n await pause('agent.aborted', 0)\n recorder.record({ kind: 'lifecycle', node: 'agent.aborted', turn: turnIndex })\n throw error\n }\n\n logger.error(\n 'Agent loop failed at turn %d: %s',\n turnIndex + 1,\n error instanceof Error ? error.message : String(error),\n )\n await pause('agent.error', 0)\n recorder.record({\n kind: 'lifecycle',\n node: 'agent.error',\n turn: turnIndex,\n payload: { message: error instanceof Error ? error.message : String(error) },\n })\n throw error\n } finally {\n // RFC-145 D3:兜底冲洗——覆盖 terminate/pending/abort/error 全部退出路径上\n // 尚未落盘的 nondet draws(正常路径回合边界已 flush,此处 no-op 幂等)。\n context.ports?.nondetFlush?.()\n await pause('loop.cleanup', 0)\n }\n}\n\nfunction createToolDefinitions(tools: AgentTool[]): ToolDefinition[] {\n return tools\n .filter((t) => t.exposure !== 'deferred' && t.exposure !== 'hidden')\n .map((tool) => ({\n name: tool.name,\n description: tool.description as string,\n parameters: tool.parameters,\n }))\n}\n","import { createLogger } from '@x-otto/shared'\n\nimport type { ToolCall } from '@x-otto/interchange'\nimport { buildSnapshot, consumePause, type DebugPort, type DebugSnapshot } from './debug-port'\nimport { systemClock, type ClockPort } from './ports'\nimport type { ApprovalRisk, AgentTool, ToolHookExecutor, ToolResult } from './types'\n\nconst logger = createLogger('@x-otto/agent:tool-executor')\n\n/**\n * RFC-142 D3:精确识别旧版 prune 污染残留的包装形态——顶层唯一键 `_truncated`\n * 且值为非 null 对象(非数组)。其余任何形态(多键、值为字符串/数组等)不匹配。\n */\nfunction isTruncatedWrapperShape(args: unknown): boolean {\n if (typeof args !== 'object' || args === null || Array.isArray(args)) return false\n const keys = Object.keys(args as Record<string, unknown>)\n if (keys.length !== 1 || keys[0] !== '_truncated') return false\n const inner = (args as Record<string, unknown>)['_truncated']\n return typeof inner === 'object' && inner !== null && !Array.isArray(inner)\n}\n\ntype SnapshotMetadata = Record<string, string | number | boolean | null>\n\nexport interface ToolExecutor {\n execute(toolCall: ToolCall, signal: AbortSignal): Promise<ToolResult>\n\n list(): AgentTool[]\n}\n\nexport interface ToolExecutorOptions {\n tools: AgentTool[]\n hookExecutor?: ToolHookExecutor\n agentName?: string\n sessionId?: string\n debug?: DebugPort\n approval?: (\n toolName: string,\n args: Record<string, unknown>,\n reason: string,\n risk?: ApprovalRisk,\n ) => Promise<boolean>\n clock?: ClockPort\n depth?: number\n}\n\nfunction abortedResult(name: string): ToolResult {\n return {\n content: [{ type: 'text', text: `Tool ${name} execution was aborted` }],\n isError: true,\n errorKind: 'aborted',\n details: {},\n }\n}\n\nclass DefaultToolExecutor implements ToolExecutor {\n private readonly tools: Map<string, AgentTool>\n private readonly hookExecutor: ToolHookExecutor | undefined\n private readonly agentName: string\n private readonly sessionId: string\n private readonly debug: DebugPort | undefined\n private readonly clock: ClockPort\n private readonly depth: number | undefined\n private readonly approval:\n | ((\n toolName: string,\n args: Record<string, unknown>,\n reason: string,\n risk?: ApprovalRisk,\n ) => Promise<boolean>)\n | undefined\n\n constructor(options: ToolExecutorOptions) {\n this.tools = new Map()\n for (const tool of options.tools) {\n this.tools.set(tool.name, tool)\n }\n this.hookExecutor = options.hookExecutor\n this.agentName = options.agentName ?? ''\n this.sessionId = options.sessionId ?? ''\n this.debug = options.debug\n this.clock = options.clock ?? systemClock\n this.depth = options.depth\n this.approval = options.approval\n }\n\n list(): AgentTool[] {\n return [...this.tools.values()]\n }\n\n async execute(toolCall: ToolCall, signal: AbortSignal): Promise<ToolResult> {\n const { id, name } = toolCall\n const tool = this.tools.get(name)\n // RFC-222 D3:metadata 补 sessionId——service 侧 DebugBridge 据此把 Debugger.paused/\n // resumed/commandRejected 定向路由到订阅该 session 的客户端,而非广播给全部连接\n // (见 packages/service/src/debug-bridge.ts forwardDebugEvent)。\n const pause = (point: DebugSnapshot['point'], metadata: SnapshotMetadata) =>\n this.debug?.pause(\n buildSnapshot({\n point,\n frameDepth: 2,\n lastToolName: name,\n metadata: { ...metadata, sessionId: this.sessionId },\n }),\n )\n\n consumePause(await pause('tool.resolve', { found: !!tool, toolCallId: id }))\n\n if (!tool) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n errorKind: 'not_found',\n }\n }\n\n const startTime = this.clock.now()\n let args = toolCall.arguments as Record<string, unknown>\n\n try {\n if (this.hookExecutor) {\n consumePause(await pause('tool.hook.before', { toolCallId: id }))\n\n const outcome = await this.hookExecutor.executeBeforeHooks({\n toolName: name,\n toolCallId: id,\n args,\n agentName: this.agentName,\n sessionId: this.sessionId,\n paths: resolveToolPaths(tool, args),\n readonly: tool.readonly,\n })\n\n if (outcome.kind === 'deny') {\n return {\n content: [{ type: 'text', text: outcome.reason }],\n isError: true,\n errorKind: 'permission',\n }\n }\n\n args = outcome.args\n\n if (outcome.kind === 'ask') {\n const reason = outcome.reason\n if (!this.approval) {\n return {\n content: [\n {\n type: 'text',\n text: `Tool ${name} requires approval but no approval handler is configured: ${reason}`,\n },\n ],\n isError: true,\n errorKind: 'permission',\n }\n }\n const approved = await this.approval(name, args, reason, outcome.risk)\n if (signal.aborted) {\n return abortedResult(name)\n }\n if (!approved) {\n return {\n content: [{ type: 'text', text: `Tool ${name} was rejected by user: ${reason}` }],\n isError: true,\n errorKind: 'permission',\n }\n }\n }\n }\n\n let parsed = tool.parameters.safeParse(args)\n\n // RFC-142 D3 防御纵深:历史会话中被旧版 prune 污染的 `{_truncated: \"...\"}` 结构\n // 曾被模型误学并模仿(事故 trace 9a4a8cd5:生成 `{_truncated: {path, content}}` 嵌套,\n // 内层参数完整正确)。精确匹配\"顶层唯一键 _truncated 且值为对象\"这一种形态,\n // 解包一层重试校验——解包后仍走完整 zod 校验(权限判定已在上方完成,不重开窗口)。\n // 不做模糊匹配:真实业务字段恰好叫 _truncated 的第三方工具不误伤(fail-closed)。\n if (!parsed.success && isTruncatedWrapperShape(args)) {\n const unwrapped = (args as Record<string, unknown>)['_truncated'] as Record<string, unknown>\n const retried = tool.parameters.safeParse(unwrapped)\n if (retried.success) {\n logger.warn(\n { toolName: name, toolCallId: id },\n 'RFC-142: unwrapped legacy _truncated wrapper from model tool-call arguments (context-poisoning residue); executing with inner arguments',\n )\n parsed = retried\n }\n }\n\n const { success } = parsed\n\n consumePause(await pause('tool.validate', { valid: success, toolCallId: id }))\n\n if (!parsed.success) {\n // RFC-142 D3:指令式错误反馈——比裸 zod JSON 更能引导模型下一轮自我修正\n //(明确告诉它不要包裹额外的键,按 schema 顶层字段重发)。zod 细节仍附上供诊断。\n return {\n content: [\n {\n type: 'text',\n text:\n `Invalid arguments for tool ${name}: ${String(parsed.error)}\\n\\n` +\n `Do NOT wrap arguments in any extra key; emit the tool's schema fields at the top level and retry.`,\n },\n ],\n isError: true,\n errorKind: 'validation',\n }\n }\n\n if (signal.aborted) {\n return abortedResult(name)\n }\n\n let result = await tool.execute({\n id: toolCall.id,\n params: parsed.data,\n signal,\n sessionId: this.sessionId || undefined,\n agentName: this.agentName || undefined,\n depth: this.depth,\n })\n\n if (signal.aborted) {\n return abortedResult(name)\n }\n\n if (this.hookExecutor) {\n const afterResult = await this.hookExecutor.executeAfterHooks({\n toolName: toolCall.name,\n toolCallId: toolCall.id,\n args,\n result,\n agentName: this.agentName,\n sessionId: this.sessionId,\n durationMs: this.clock.now() - startTime,\n readonly: tool.readonly,\n })\n\n result = afterResult.result\n\n consumePause(\n await pause('tool.hook.after', {\n toolCallId: id,\n durationMs: this.clock.now() - startTime,\n }),\n )\n }\n\n if (signal.aborted) {\n return abortedResult(name)\n }\n\n return result\n } catch (error) {\n if (signal.aborted) {\n return abortedResult(name)\n }\n const message = error instanceof Error ? error.message : String(error)\n // RFC-074 R-ERRKIND:产生处定型——工具经 @x-otto/tools `toolError(msg, kind)` 抛出时附带\n // errorKind(鸭子类型),此处优先读取它,取代把一切 throw 盲目归类为 'runtime'。\n const taggedKind = (error as { errorKind?: ToolResult['errorKind'] } | null)?.errorKind\n // RFC-153 D6:插件代码式工具(module-wiring.ts toAgentTool)在异常上补的 pluginSource\n // 标记同样鸭子类型读取——只服务\"标记来源、供 UI 展示\",不影响 errorKind 分类逻辑本身。\n const taggedSource = (error as { pluginSource?: string } | null)?.pluginSource\n return {\n content: [{ type: 'text', text: `Tool ${toolCall.name} failed: ${message}` }],\n isError: true,\n errorKind: taggedKind ?? 'runtime',\n details: {\n error: error instanceof Error ? error.name : 'UnknownError',\n },\n ...(taggedSource ? { pluginSource: taggedSource } : {}),\n }\n }\n }\n}\n\nfunction resolveToolPaths(tool: AgentTool, args: Record<string, unknown>): string[] {\n if (!tool.pathParams?.length) {\n return []\n }\n const paths: string[] = []\n for (const param of tool.pathParams) {\n const value = args[param]\n if (typeof value === 'string') {\n paths.push(tool.resolvePath ? tool.resolvePath(value) : value)\n }\n }\n return paths\n}\n\nexport function createToolExecutor(\n tools: AgentTool[],\n options?: Omit<ToolExecutorOptions, 'tools'>,\n): ToolExecutor {\n return new DefaultToolExecutor({ tools, ...options })\n}\n","import { createLogger, TypedEventEmitter } from '@x-otto/shared'\n\nimport { workLoop } from './work-loop'\nimport { AbortError } from './errors'\nimport { ENGINE_DEFAULTS } from './engine-nodes'\nimport { createToolExecutor } from './tool-executor'\n\nimport type { AssistantMessage, Message as LlmMessage } from '@x-otto/interchange'\nimport type {\n AgentConfig,\n AgentEvents,\n AgentMessage,\n AgentRunContext,\n AgentState,\n AgentStatus,\n PendingLifecycleTool,\n} from './types'\n\nconst logger = createLogger('@x-otto/agent')\n\nconst VALID_TRANSITIONS: Record<AgentStatus, Set<AgentStatus>> = {\n idle: new Set(['streaming', 'aborted', 'error', 'archived']),\n streaming: new Set(['tool_executing', 'completed', 'aborted', 'error']),\n tool_executing: new Set(['streaming', 'aborted', 'error']),\n completed: new Set(['streaming', 'idle', 'aborted', 'error', 'archived']),\n error: new Set(['idle', 'aborted', 'streaming', 'archived']),\n aborted: new Set(['streaming', 'idle', 'error', 'archived']),\n archived: new Set(['idle']),\n}\n\nexport class Agent extends TypedEventEmitter<AgentEvents> {\n private state: AgentState\n private abortController: AbortController | null = null\n private pendingAbort = false\n private steeringQueue: AgentMessage[] = []\n private followUpQueue: AgentMessage[] = []\n /**\n * RFC-144 M1:lifecycleAsync 工具的执行态追踪,独立于 messages 数组(Agent 实例在两次\n * run() 之间不持有 messages 引用——由调用方每次通过 AgentRunContext.messages 传入)。\n *\n * 键 = toolCallId。存在期:runTools 启动 lifecycleAsync 工具时创建条目(M3 接线);\n * `notifyToolCompletion()` 消费(`consumed=true`)后条目保留供审计/溯源,不立即 delete\n * (若需要严格释放内存,调用方可在 prompt 边界批量清理已 consumed 条目)。\n *\n * 真正把 system 通知消息写回 messages 数组的逻辑,依赖调用方持有的 messages 引用,\n * 归属 M2(落库基础设施)+ M3(auto-trigger 控制流集成)——本字段仅提供状态追踪骨架。\n */\n private readonly pendingLifecycleTools = new Map<string, PendingLifecycleTool>()\n\n /**\n * RFC-144 M4:prompt 边界序号——每次 `run()` 被调用(即一次完整的 turn-loop 生命周期\n * 起点)自增。用于 lifecycleAsync 工具的跨 prompt 隔离判定:工具启动时记录当前\n * `promptSeq`(见 `PendingLifecycleTool.promptSeq`),完成时若序号已变化(说明工具\n * 跨越了 prompt 边界——原 prompt 已经结束,一个新的 `run()` 已经开始),其完成通知\n * 不应注入新 prompt 的 LLM 对话(避免\"上一个任务的构建完成了\"这类历史事件用一条\n * mid-conversation system message 打断当前用户正在做的新任务,见 RFC-144 决策 4\n * Q4 修正——system 角色被模型视为运营方权威事实,混入无关任务上下文比普通消息更\n * 容易造成困惑或被误解为需要处理的指令)。\n *\n * **递增时机边界(重要)**:`Agent.run()` 是 turn-loop 生命周期的唯一入口,包括\n * `AgentSession` 内部的 todo 续跑(`todoGateRunner`,`internal=true` 场景)——这类\n * 续跑虽然是\"同一用户任务\"的延伸,但确实是走完整独立的 `run()` 调用(前一次\n * `run()` 已经 `completed`/返回),语义上工具若跨越了这次调用边界,模型也确实已经\n * 推进到了下一轮独立的 turn-loop,此时再看到上一轮的工具完成通知同样可能造成\n * \"为什么突然冒出一个我不认识的构建结果\"的困惑——因此续跑同样应该视为新 promptSeq,\n * 不额外区分 internal,保持递增规则简单统一(\"每次 run() 调用 = 一个新 promptSeq\")。\n */\n private promptSeq = 0\n\n /** 当前 prompt 序号(只读)——`work-loop.ts` 的 lifecycleAsync 工具启动逻辑读取它记录到\n * `PendingLifecycleTool.promptSeq`;完成时对比判定是否已跨越 prompt 边界。 */\n get currentPromptSeq(): number {\n return this.promptSeq\n }\n\n /**\n * 构造期静态配置(终局审查 2026-07-18 M7 收束;元审查 A 修正为派生类型):`AgentConfig`\n * 的字段此前逐个拷贝为同名私有字段,构造函数因此膨胀为纯粹的透传列表。收束为单一只读\n * 配置对象后,构造函数只需一次赋值;下游读取点(`runLoop()` 内两处:`createToolExecutor`\n * 配置 + `workLoop` 参数)从 `this.xxx` 改为 `this.config.xxx`。\n *\n * **类型从 `AgentConfig` 派生而非手写**(元审查 2026-07-18 A 项修正,lesson_46 手抄类型\n * 副本反模式):`Required<Pick<...>>` 段 = 构造期已消解默认值的字段(maxToolTurns 等\n * 5 个,运行期非空);`Pick<...>` 段 = 原样透传的可选字段。`AgentConfig` 加字段时本类型\n * 不会静默漂移——新字段若需进 config 必须显式加进 Pick 列表(编译器在构造函数赋值处\n * 报错提示)。唯一的重命名字段 `agentLogger`(源自 `AgentConfig.logger`)单独声明。\n * 不改变公开的 `AgentConfig` 接口,仅内部实现重构(零行为变化)。\n */\n private readonly config: Required<\n Pick<\n AgentConfig,\n 'maxToolTurns' | 'maxToolTurnExtensions' | 'budgetSteerWrapUp' | 'agentName' | 'sessionId'\n >\n > &\n Pick<\n AgentConfig,\n | 'stream'\n | 'promptOutputTokenBudget'\n | 'promptWallClockBudgetMs'\n | 'stallDetection'\n | 'toolHookExecutor'\n | 'debug'\n | 'clock'\n | 'recorder'\n | 'nondetFlush'\n | 'depth'\n | 'retryAccelerate'\n | 'streamRetry'\n | 'networkDisconnectRetry'\n | 'spill'\n | 'approval'\n > & {\n /** `AgentConfig.logger` 的实例内命名(与包级 `logger` 单例区分)。 */\n agentLogger: import('@x-otto/shared').Logger\n }\n\n get status(): AgentStatus {\n return this.state.status\n }\n\n get turnCount(): number {\n return this.state.turnCount\n }\n\n constructor(config: AgentConfig) {\n super()\n\n this.config = {\n stream: config.stream,\n agentLogger: config.logger,\n maxToolTurns: config.maxToolTurns ?? ENGINE_DEFAULTS.maxToolTurns,\n maxToolTurnExtensions: config.maxToolTurnExtensions ?? 0,\n budgetSteerWrapUp: config.budgetSteerWrapUp ?? false,\n promptOutputTokenBudget: config.promptOutputTokenBudget,\n promptWallClockBudgetMs: config.promptWallClockBudgetMs,\n stallDetection: config.stallDetection,\n agentName: config.agentName ?? '',\n sessionId: config.sessionId ?? '',\n toolHookExecutor: config.toolHookExecutor,\n debug: config.debug,\n clock: config.clock,\n recorder: config.recorder,\n nondetFlush: config.nondetFlush,\n depth: config.depth,\n retryAccelerate: config.retryAccelerate,\n streamRetry: config.streamRetry,\n networkDisconnectRetry: config.networkDisconnectRetry,\n spill: config.spill,\n approval: config.approval,\n }\n\n this.state = {\n status: 'idle',\n turnCount: 0,\n tokenUsage: { input: 0, output: 0, cacheRead: 0 },\n isStreaming: false,\n currentStreamMessage: null,\n error: undefined,\n }\n\n this.on('status.change', ({ to }) => {\n this.state.isStreaming = to === 'streaming'\n })\n\n this.on('turn.end', ({ message }) => {\n this.state.turnCount++\n this.state.currentStreamMessage = null\n this.state.tokenUsage.input += message.usage.inputTokens\n this.state.tokenUsage.output += message.usage.outputTokens\n this.state.tokenUsage.cacheRead += message.usage.cacheReadTokens\n })\n\n this.on('stream.event', ({ event }) => {\n if (event.type === 'start') {\n this.state.currentStreamMessage = event.partial\n }\n })\n }\n\n getState(): Readonly<AgentState> {\n // 防御性快照:tokenUsage 是可变嵌套对象——浅拷会让返回的\"快照\"随后续 turn.end 累加\n // (this.state.tokenUsage.input += …)retroactively 变化(共享引用)。连同深拷一份,\n // 保证快照真正稳定、且消费方改返回值不污染内部状态。\n return { ...this.state, tokenUsage: { ...this.state.tokenUsage } }\n }\n\n async run(context: AgentRunContext): Promise<AssistantMessage> {\n if (this.state.status === 'streaming' || this.state.status === 'tool_executing') {\n throw new Error(`Cannot run in ${this.state.status} status`)\n }\n\n if (this.state.status === 'archived') {\n throw new Error('Cannot run: session is archived')\n }\n\n if (this.state.status === 'error') {\n this.state.error = undefined\n }\n\n // RFC-144 M4:新一轮 turn-loop 生命周期起点。先取消上一个 prompt 遗留的、跨越了\n // 本次边界仍未 settle 的 lifecycleAsync pending 工具(见 cancelStalePendingLifecycleTools\n // 注释),再递增 promptSeq——顺序很重要:必须用\"旧\" promptSeq 判断\"哪些条目属于\n // 即将成为历史的那个 prompt\",递增后再判断会把本该取消的条目误判为\"当前 prompt\"。\n this.cancelStalePendingLifecycleTools()\n this.promptSeq++\n\n return this.runLoop(context)\n }\n\n /**\n * RFC-144 M4:取消上一个 prompt 遗留、仍未消费的 lifecycleAsync pending 工具执行。\n *\n * 触发时机:每次 `run()` 被调用、**递增 `promptSeq` 之前**(新一轮 turn-loop 生命\n * 周期起点)。由于同一时刻只会有一个 `run()` 在执行(`run()` 顶部对\n * streaming/tool_executing 状态直接抛错拒绝并发调用),此刻任何仍未消费的 pending\n * 条目必然是在**上一个** `run()` 调用期间注册的(`promptSeq` 等于递增前的\n * `this.promptSeq`)——它们理应已经被上一个 `workLoop()` 返回前的兜底等待\n * (M3,`waitForPendingLifecycleTools()`)消费完毕;仍然存在未消费条目,说明上一个\n * `workLoop()` 是被 `AbortError` 中断退出的(用户主动 `abort()` 了上一个 prompt),\n * 没有机会走到那段兜底等待——这正是本方法要处理的真实场景:用户中断了一个启动了\n * 长任务工具的 prompt,随即发起了全新任务,旧工具仍在后台跑。\n *\n * 继续让它在后台跑对用户已无意义(新任务的模型不会再看到它的结果——见 work-loop.ts\n * `notifyCompletion()` 的 `crossedPromptBoundary` 判定,跨边界完成不落库注入 LLM\n * 对话),白白消耗资源(CPU/内存/可能仍在运行的子进程),故主动取消。不需要按\n * `promptSeq` 数值比较——调用时刻的每一个未消费条目都定义上属于\"即将成为历史\"的\n * 那个 prompt(若属于当前正要开始的这个新 prompt,此刻它还未被注册——工具注册\n * 发生在 `runTools()` 内部,晚于本方法的调用时机)。\n *\n * 只 abort 每个条目独立的 `abortController`(M4 新增),不影响 loop 级别的\n * `this.abortController`——那个 controller 属于\"已经结束\"的上一次 `runLoop()`\n * 调用,`run()` 即将为本次调用创建一个全新的 controller,两者本就互不干扰;\n * 用独立的 per-tool controller 是为了在**未来**\"上一个 prompt 仍在正常运行、\n * 只是启动了 lifecycleAsync 工具\"的场景下也能精确取消,不会误伤同一个共享\n * controller 下的其它逻辑。\n *\n * 已消费(`consumed:true`)的条目不取消(它们代表已经落库或已判定跨边界抑制的\n * 工具,取消一个已经完成的工具是无意义操作),但**在此顺便清理**——终局审查\n * 2026-07-21 发现:`notifyToolCompletion()` 只标记 `consumed=true` 从不 `delete`,\n * 而 `reset()`(Map 唯一的清理点)在生产代码中从未被调用(`AgentSession` 持有的\n * `Agent` 实例与会话同生命周期,一辈子不 reset),导致 Map 无界增长(慢性内存\n * 泄漏,与 RFC-159/160/170/178 治理的\"会话级状态无界增长\"同类问题,只是发生在\n * Agent 类私有字段、未被那几轮驻留审计覆盖到)。\n *\n * 清理时机安全性:本方法调用时刻严格早于 `this.promptSeq++`(见 `run()`),此刻\n * Map 中的**全部**条目都必然注册于此前已经结束/被中断的 run() 调用——本次即将\n * 开始的新 prompt 还未注册任何条目(注册发生在 `runTools()` 内部,晚于此调用)。\n * 因此已消费的条目在此处删除对\"当前 prompt 内的审计可见性\"零影响,不需要按\n * `promptSeq` 数值比较区分\"是否跨越了本次边界\"——它们全部跨越了。\n */\n private cancelStalePendingLifecycleTools(): void {\n for (const [id, entry] of this.pendingLifecycleTools) {\n if (entry.consumed) {\n this.pendingLifecycleTools.delete(id)\n continue\n }\n entry.abortController.abort()\n this.config.agentLogger.info(\n 'Cancelling stale lifecycleAsync tool %s (task id: %s) from prompt %d — new prompt %d starting',\n entry.toolName,\n entry.toolCallId,\n entry.promptSeq,\n this.promptSeq + 1,\n )\n }\n }\n\n steer(message: AgentMessage): void {\n this.steeringQueue.push(message)\n }\n\n /**\n * RFC-337 D2:按 `uuid` 从 steering 队列移除一条尚未被消费的 steer 消息,返回是否命中。\n *\n * 命中判定即「该 steer 是否仍未被模型消费」的权威真源:`drainSteeringQueue()` 一旦把队列\n * 清空并交付给 workLoop(消息进入 LLM 上下文),本方法返回 `false`——调用方(cli ESC 撤回)\n * 据此退化为「中止整回合」(已进历史的消息不可干净撤回,见 RFC-337 §D3 退化路径)。\n *\n * 只操作**内存态** `steeringQueue`,不触碰已落库消息、不涉及 save 路径——不违反 RFC-159。\n */\n removeSteer(uuid: string): boolean {\n const idx = this.steeringQueue.findIndex(\n (m) => (m as { uuid?: string }).uuid === uuid,\n )\n if (idx === -1) return false\n this.steeringQueue.splice(idx, 1)\n return true\n }\n\n followUp(message: AgentMessage): void {\n this.followUpQueue.push(message)\n }\n\n /**\n * RFC-144 M1:注册一个 lifecycleAsync 工具的执行态(runTools 启动工具时调用)。\n * 幂等:同一 toolCallId 重复注册会覆盖旧条目(不应发生,调用方保证每个 tool_call 唯一)。\n *\n * 不在此处挂 promise 完成回调——落库闭环的驱动方是 `work-loop.ts` 的 `runTools()`\n * (见其 `promise.then(...)`/`.catch(...)` 直接调用 `ctx.notifyToolCompletion`),\n * 因为它直接持有 `messages` 数组引用,能在 consume-once 通过后立即\n * `messages.push(SystemNotificationMessage)`。Agent 实例本身不持有 messages,\n * 若在此处也挂一次完成回调会与 workLoop 的调用重复触发 `notifyToolCompletion`\n * (consume-once 会吞掉第二次调用,不产生双重落库,但属于职责重复的设计混乱,\n * 故明确只保留 workLoop 一处驱动)。\n */\n registerPendingLifecycleTool(entry: PendingLifecycleTool): void {\n this.pendingLifecycleTools.set(entry.toolCallId, entry)\n }\n\n /** 当前仍有 lifecycleAsync 工具在等待(M3 用于 post-runTools yield 判定)。 */\n hasPendingLifecycleTools(): boolean {\n for (const entry of this.pendingLifecycleTools.values()) {\n if (!entry.consumed) return true\n }\n return false\n }\n\n /**\n * RFC-144 M3:等待当前所有未消费的 lifecycleAsync pending 条目 settle。\n *\n * 用于外层 prompt 循环在内层 turn 循环因 pending 非空 yield 后的等待点——不轮询\n * `hasPendingLifecycleTools()`(会引入轮询延迟 + CPU 空转),而是直接 await 每个\n * pending 条目的 `promise` 字段。\n *\n * **`entry.promise` 是\"落库完成\"信号,不是\"工具执行完成\"信号**(M3 实现时的关键\n * 修正,见 `PendingLifecycleTool.promise` 类型定义注释与 work-loop.ts 的\n * `notifyCompletion()` 函数):work-loop.ts 把工具执行 + consume-once 通知 +\n * `messages.push(SystemNotificationMessage)` 三步包成一个函数,本字段存它的返回值。\n * 若只等原始工具执行 Promise(不等落库这一步),本方法可能在通知真正写进 `messages`\n * **之前**就返回,外层循环重新进入 `runTurnsUntilTerminate` 发起下一次 `runTurn` 时\n * 模型会看不到工具完成结果——这是首版实现遗漏、二版修正的竞态。`entry.promise` 恒\n * fulfilled(`notifyCompletion()` 内部吞掉所有异常,包括工具执行 reject 的情况),\n * `Promise.allSettled` 不会因任何 pending 条目而挂起或需要错误处理。\n *\n * **快照语义**:只等待调用时刻已注册的 pending 条目——若等待期间又有新的\n * lifecycleAsync 工具被启动(理论上不会发生,因为外层循环在等待期间不会重新调用\n * `runTools`),不纳入本次等待范围。这与\"等到 `hasPendingLifecycleTools()` 变\n * false\"语义等价,但避免了轮询。\n */\n async waitForPendingLifecycleTools(): Promise<void> {\n const pending = [...this.pendingLifecycleTools.values()]\n .filter((entry) => !entry.consumed)\n .map((entry) => entry.promise)\n if (pending.length === 0) return\n await Promise.allSettled(pending)\n }\n\n /**\n * RFC-144 M1/M2:工具真正完成后的通知钩子——检查 consume-once、标记已消费、发出携带\n * 完整 `result` 的事件。M2 起本方法主要由 `registerPendingLifecycleTool` 注册时挂的\n * promise 完成回调**自动调用**(事件驱动,无需外部轮询);也保留公开可调用(防御性/\n * 测试场景下手动触发)。\n *\n * **不在此处把 system 通知写回 messages**——Agent 实例在两次 run() 之间不持有 messages\n * 引用(见类字段注释)。真正的落库由订阅方(AgentSession,M2 落库基础设施)在\n * `lifecycle.tool.completed` 事件回调中完成:拿到 `result` 后自行构造\n * `SystemNotificationMessage` 并追加到其持有的 messages 数组。\n *\n * 返回 `consumed=false` 的情况:toolCallId 未注册(未知调用,防御性容错)或已被消费过\n * (consume-once 语义,防止重复生成通知——即使 promise 的 `.then`/`.catch` 因竞态被\n * 调用两次,也只有第一次真正标记 consumed 并 emit 事件)。\n */\n async notifyToolCompletion(\n toolCallId: string,\n result: import('./types').ToolResult,\n ): Promise<{ consumed: boolean; entry?: PendingLifecycleTool }> {\n const entry = this.pendingLifecycleTools.get(toolCallId)\n if (!entry || entry.consumed) {\n return { consumed: false }\n }\n entry.consumed = true\n this.emit('lifecycle.tool.completed', {\n toolCallId,\n toolName: entry.toolName,\n isError: result.isError ?? false,\n result,\n })\n return { consumed: true, entry }\n }\n\n abort(): void {\n if (this.state.status === 'aborted' && !this.abortController) {\n return\n }\n\n if (this.abortController) {\n this.abortController.abort()\n } else {\n this.pendingAbort = true\n }\n\n this.transitionTo('aborted')\n this.emit('abort')\n }\n\n /** 归档:禁止后续 run()。仅终态(idle/completed/error/aborted)可归档。 */\n archive(): void {\n this.transitionTo('archived')\n }\n\n /** 取消归档:从 archived 回到 idle。 */\n unarchive(): void {\n this.transitionTo('idle')\n }\n\n /**\n * 强制归零:不经 `VALID_TRANSITIONS` 表——`streaming`/`tool_executing` 没有到\n * `idle` 的合法边(reset 的语义恰恰是\"哪怕正在跑也强行归零\",先 abort 掉\n * in-flight controller 再复位,这与常规状态转换的前提相反),故不能改用\n * `transitionTo()`(那会在这两个状态下被表拒绝、静默不转换,状态反而卡死)。\n *\n * 但此前直接赋值 `this.state.status = 'idle'` 会跳过 `status.change` 事件——\n * 任何订阅方(TUI 状态栏、Session 层状态镜像)收不到通知,与 Agent 内部真实\n * 状态漂移(终局审查 2026-07-21 发现)。补发事件但保留强制赋值本身。\n */\n reset(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n\n this.pendingAbort = false\n this.steeringQueue = []\n this.followUpQueue = []\n this.pendingLifecycleTools.clear()\n this.state.turnCount = 0\n this.state.tokenUsage = { input: 0, output: 0, cacheRead: 0 }\n this.state.currentStreamMessage = null\n this.state.error = undefined\n\n const from = this.state.status\n this.state.status = 'idle'\n if (from !== 'idle') {\n this.emit('status.change', { from, to: 'idle' })\n }\n }\n\n private async runLoop(context: AgentRunContext): Promise<AssistantMessage> {\n const controller = new AbortController()\n this.abortController = controller\n if (this.pendingAbort) {\n this.pendingAbort = false\n controller.abort()\n }\n const signal = controller.signal\n\n const toolExecutor = createToolExecutor(context.tools, {\n hookExecutor: this.config.toolHookExecutor,\n agentName: this.config.agentName,\n sessionId: this.config.sessionId,\n debug: this.config.debug,\n clock: this.config.clock,\n depth: this.config.depth,\n approval: this.config.approval,\n })\n\n try {\n const result = await workLoop({\n model: context.model,\n systemPrompt: context.systemPrompt,\n systemTail: context.systemTail,\n cacheRetention: context.cacheRetention,\n messages: context.messages,\n thinkingLevel: context.thinkingLevel,\n maxTokens: context.maxTokens,\n temperature: context.temperature,\n convertToLLM: context.convertToLLM ?? defaultConvertToLLM,\n onTurnPersist: context.onTurnPersist,\n logger: this.config.agentLogger,\n toolExecutor,\n stream: this.config.stream,\n signal,\n emitter: this,\n getSteeringMessages: () => this.drainSteeringQueue(),\n getFollowUpMessages: () => this.drainFollowUpQueue(),\n transitionTo: (to) => this.transitionTo(to),\n // RFC-144 M1-M4 + RFC-183 P1-1 收束:lifecycleAsync 生命周期控制器单接口注入\n // ——workLoop 的 runTools/runTurnsUntilTerminate 通过它与 Agent 实例的\n // pendingLifecycleTools Map / promptSeq 交互,不直接持有 Agent 引用。\n lifecycleAsync: {\n register: (entry) => this.registerPendingLifecycleTool(entry),\n hasPending: () => this.hasPendingLifecycleTools(),\n waitForPending: () => this.waitForPendingLifecycleTools(),\n notifyCompletion: (toolCallId, result) => this.notifyToolCompletion(toolCallId, result),\n currentPromptSeq: () => this.currentPromptSeq,\n },\n ports: {\n clock: this.config.clock,\n debug: this.config.debug,\n recorder: this.config.recorder,\n nondetFlush: this.config.nondetFlush,\n spill: this.config.spill,\n },\n budget: {\n maxToolTurns: this.config.maxToolTurns,\n maxToolTurnExtensions: this.config.maxToolTurnExtensions,\n budgetSteerWrapUp: this.config.budgetSteerWrapUp,\n promptOutputTokenBudget: this.config.promptOutputTokenBudget,\n promptWallClockBudgetMs: this.config.promptWallClockBudgetMs,\n // RFC-204 D4:三层断链修复第二层——此前逐字段列出唯独漏了 stallDetection,\n // 导致 workLoop 恒收到 undefined(缺省启用但零配置入口)。\n stallDetection: this.config.stallDetection,\n isPauseRequested: context.isPauseRequested,\n },\n retry: {\n retryAccelerate: this.config.retryAccelerate,\n streamRetry: this.config.streamRetry,\n networkDisconnectRetry: this.config.networkDisconnectRetry,\n },\n contextTransform: {\n transformContext: context.transformContext,\n reactiveCompact: context.reactiveCompact,\n describeImages: context.describeImages,\n },\n })\n\n this.transitionTo('completed')\n return result\n } catch (error) {\n if (error instanceof AbortError || signal.aborted) {\n this.transitionTo('aborted')\n throw error\n }\n\n this.state.error = error instanceof Error ? error : new Error(String(error))\n this.transitionTo('error')\n this.emit('error', { error: this.state.error })\n throw this.state.error\n } finally {\n if (this.abortController === controller) {\n this.abortController = null\n }\n this.state.isStreaming = false\n }\n }\n\n private transitionTo(to: AgentStatus): void {\n const from = this.state.status\n if (from === to) {\n return\n }\n\n const allowed = VALID_TRANSITIONS[from]\n if (!allowed?.has(to)) {\n logger.warn('Invalid state transition: %s → %s (ignored)', from, to)\n return\n }\n\n this.state.status = to\n this.emit('status.change', { from, to })\n }\n\n private async drainSteeringQueue(): Promise<AgentMessage[]> {\n const messages = [...this.steeringQueue]\n this.steeringQueue = []\n return messages\n }\n\n private async drainFollowUpQueue(): Promise<AgentMessage[]> {\n const messages = [...this.followUpQueue]\n this.followUpQueue = []\n return messages\n }\n}\n\n/**\n * 类型边界(M40-B0-04,RFC-144 M2 扩展):AgentMessage → 协议消息。放行 user/assistant/\n * tool_result/system_notification 四个协议角色——`SystemAgentMessage`(hook-contracts 的\n * `role:'system'`,UI/引擎通知,如 turn_duration/memory_saved)**仍然**绝不进 provider\n * 请求(providers 按 role if/else 分支,未知角色会被静默丢弃或破坏请求构建)。\n *\n * `role:'system_notification'`(`@x-otto/interchange` SystemNotificationMessage,RFC-144\n * lifecycleAsync 工具完成通知)与 `role:'system'`(SystemAgentMessage)是两个不同的字符串\n * 判别值,精确匹配不会误放行后者——这是刻意保留 M40-B0-04 原有边界、只新增一个协议角色的\n * 最小扩展,不是放宽原有的\"UI 通知不进 provider\"规则。\n *\n * 导出供测试与自定义 convertToLLM 复用。\n */\nexport function defaultConvertToLLM(messages: AgentMessage[]): LlmMessage[] {\n return messages.filter(\n (m): m is LlmMessage =>\n typeof m === 'object' &&\n m !== null &&\n 'role' in m &&\n (m.role === 'user' ||\n m.role === 'assistant' ||\n m.role === 'tool_result' ||\n m.role === 'system_notification'),\n )\n}\n\nexport function createAgent(config: AgentConfig): Agent {\n return new Agent(config)\n}\n"],"mappings":"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"}