适配器只读取 OpenAI SDK 的稳定结构:status、error、headers、requestID。分类规则显式:429 → ModelRateLimitError;529 → ModelOverloadedError;400 + context_length_exceeded 等 → ModelPromptTooLongError。不读取 message 猜测。
if (status === 429) throw new ModelRateLimitError(...);
if (status === 529) throw new ModelOverloadedError(...);
if (status === 400 && PROMPT_TOO_LONG_CODES.has(errorCode))
throw new ModelPromptTooLongError(...);
细节:code 统一转小写后再比对(大写也识别);判断用 instanceof APIError,伪造的 status=429 对象不识别;message 里的文案不作为契约。
返回 200 不代表结构可信。normalizeResponse 逐字段验证:恰好一个 choice、role=assistant、finish_reason 只能落在白名单内——未知新枚举值明确失败而非宽容当 stop(否则截断回复会以完整答案身份进历史)。content=null 但有 refusal 时可当最终内容。content_filter 能通过适配器,但在 Loop 层显式失败(AgentRunError),不重试。
finish_reason 未知值 → OpenAIResponseError(不当成 stop)
content=null + refusal → content = 拒答文本
content_filter → Loop 层 AgentRunError,不重试首次 length:输出预算 8000→64000,丢弃残缺回复重试。仍截断才续写:把中间片段和 CONTINUATION_PROMPT 追加到 requestMessages 局部快照,默认最多 3 次,成功后合并为一个 assistantMessage。
if (reply.finishReason === "length" && !state.hasEscalated) {
state.currentMaxTokens = escalatedMaxTokens;
state.hasEscalated = true;
continue;
}
// 仍截断 → requestMessages 局部快照 + 续写提示
复用 CompactionManager.compactOnPromptTooLong()。保留首条 system prompt,其后的请求快照按完整消息组压缩。同一个逻辑请求只允许一次响应式压缩,第二次 PromptTooLongRetryError 明确失败。
const [leadingSystem, compactable] = splitLeadingSystem(requestMessages);
const outcome = await compaction.compactOnPromptTooLong(
compactable, { retryCount: promptTooLongRetries }, signal);
requestMessages = [...leadingSystem, ...outcome.history];
退避基线 0.5→1→2→4…→32s,加 0..base*25% 抖动;默认最多 10 次。Retry-After 优先级更高;等待越限直接 RecoveryDeadlineExceeded。连续 3 次 529 后在下一个请求切 fallback model。
0.5s → 1s → 2s → 4s → 8s → 16s → 32s → 32s ...
连续 3 次 529 → state.currentModel = fallbackModel
默认 turn 总时限 300s。CancellationToken、单调时钟、sleeper、jitter 可注入。模型调用、退避 sleep、压缩外层统一竞争取消与 deadline,把 AbortSignal 传给 ModelClient 和 Summarizer。
典型 typed failure:
- RecoveryCancelledError
- RecoveryDeadlineExceeded
- RecoveryRetriesExhausted
- InvalidRetryAfterError
- PromptTooLongRetryError
恢复层只接管带工具的主 Loop 请求;记忆 selector/extractor、压缩摘要器、subagent 仍走裸模型——各自的会话所有权不同,共享 RecoveryState 会互相污染。能力位与配置必须成对:给 P10 传 recoveryConfig 报错,P11 不传也报错(否则静默退化成裸模型)。.env 从三字段变四字段:OPENAI_FALLBACK_MODEL 必填,file:// 协议的 baseUrl 被当没填。
主 Loop 请求 maxTokens = [8000, 64000] ← 走恢复层
旁路请求 model = undefined ← 裸模型
10 次重试等待 0.5+1+2+4+8+16+32… 总和正好解释 300s 默认时限RecoveryManager 接管每个逻辑请求,内部重试不计入新 turn。
升级预算 → 仍截断则续写 → 成功合并或耗尽。
压缩请求快照一次 → 重试 → 二次失败。
Retry-After/退避/fallback;有效期与总时限约束。
唯一合并后的 assistantMessage 进入 canonical history。