| 1 | import type { |
| 2 | NodeId, |
| 3 | OrchestrationV2ProviderFailure, |
| 4 | OrchestrationV2ProviderFailureClass, |
| 5 | OrchestrationV2ProviderRetry, |
| 6 | OrchestrationV2TurnItem, |
| 7 | ProviderDriverKind, |
| 8 | ProviderThreadId, |
| 9 | ProviderTurnId, |
| 10 | RunId, |
| 11 | ThreadId, |
| 12 | } from "@t3tools/contracts"; |
| 13 | import * as DateTime from "effect/DateTime"; |
| 14 | import * as Cause from "effect/Cause"; |
| 15 | |
| 16 | import * as IdAllocator from "./IdAllocator.ts"; |
| 17 | import * as Schema from "effect/Schema"; |
| 18 | |
| 19 | /** Raised when portable history does not fit the target provider's context window. */ |
| 20 | export class ContextHandoffBudgetError extends Schema.TaggedError<ContextHandoffBudgetError>()( |
| 21 | "ContextHandoffBudgetError", |
| 22 | {}, |
| 23 | ) { |
| 24 | override get message() { |
| 25 | return "Insufficient context allowance for the provider handoff. Compact the target conversation or use a larger-context model; the current request has not been truncated."; |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | export const MAX_PROVIDER_FAILURE_MESSAGE_LENGTH = 4_096; |
| 30 | export const MAX_PROVIDER_FAILURE_CODE_LENGTH = 128; |
| 31 | |
| 32 | const DEFAULT_PROVIDER_FAILURE_MESSAGE = "Provider turn failed."; |
| 33 | |
| 34 | /** Translate known categories without exposing arbitrary provider defect text. */ |
| 35 | function causeMessage(cause: unknown): string | undefined { |
| 36 | const seen = new Set<unknown>(); |
| 37 | let message: string | undefined; |
| 38 | for (let depth = 0; depth < 16 && cause != null && !seen.has(cause); depth++) { |
| 39 | seen.add(cause); |
| 40 | try { |
| 41 | if (Cause.isCause(cause)) { |
| 42 | cause = Cause.squash(cause); |
| 43 | continue; |
| 44 | } |
| 45 | if (typeof cause !== "object") break; |
| 46 | switch ((cause as Record<string, unknown>)._tag) { |
| 47 | case "ContextHandoffBudgetError": |
| 48 | return new ContextHandoffBudgetError().message; |
| 49 | case "ClaudeBackgroundWorkBlocksQueryReplacementError": |
| 50 | return stringField(cause, "message"); |
| 51 | case "ContextHandoffDeliveryUncertainError": |
| 52 | return "T3 could not confirm whether conversation history reached the provider. Retry the turn to recover the session."; |
| 53 | case "ProviderAdapterTurnStartError": |
| 54 | message = |
| 55 | "The provider could not start this turn. Retry the turn; if it keeps failing, check the provider setup and server logs."; |
| 56 | break; |
| 57 | case "ProviderAdapterEventStreamError": |
| 58 | message = |
| 59 | "The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs."; |
| 60 | break; |
| 61 | case "ProviderAdapterOpenSessionError": |
| 62 | message = |
| 63 | "The provider session could not be opened. Check that the provider is installed and signed in, then retry the turn."; |
| 64 | break; |
| 65 | case "ProviderAdapterResumeThreadError": |
| 66 | message = |
| 67 | "The provider conversation could not be resumed. Retry the turn; if it keeps failing, check the provider and server logs."; |
| 68 | break; |
| 69 | } |
| 70 | cause = (cause as Record<string, unknown>).cause; |
| 71 | } catch { |
| 72 | break; |
| 73 | } |
| 74 | } |
| 75 | return message; |
| 76 | } |
| 77 | |
| 78 | function stringField(value: unknown, key: "message" | "code"): string | undefined { |
| 79 | if (typeof value !== "object" || value === null) return undefined; |
| 80 | try { |
| 81 | const candidate = (value as Record<string, unknown>)[key]; |
| 82 | return typeof candidate === "string" ? candidate : undefined; |
| 83 | } catch { |
| 84 | return undefined; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | function redactUrl(match: string): string { |
| 89 | const trailing = /[),.;!?]+$/u.exec(match)?.[0] ?? ""; |
| 90 | const candidate = trailing.length === 0 ? match : match.slice(0, -trailing.length); |
| 91 | try { |
| 92 | const url = new URL(candidate); |
| 93 | url.username = ""; |
| 94 | url.password = ""; |
| 95 | url.search = ""; |
| 96 | url.hash = ""; |
| 97 | return `${url.toString()}${trailing}`; |
| 98 | } catch { |
| 99 | return "[REDACTED_URL]"; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | function replaceUnsafeControlCharacters(value: string): string { |
| 104 | const sanitized: Array<string> = []; |
| 105 | for (const character of value) { |
| 106 | const codePoint = character.codePointAt(0) ?? 0; |
| 107 | sanitized.push( |
| 108 | codePoint <= 0x08 || |
| 109 | (codePoint >= 0x0b && codePoint <= 0x0c) || |
| 110 | (codePoint >= 0x0e && codePoint <= 0x1f) || |
| 111 | codePoint === 0x7f |
| 112 | ? " " |
| 113 | : character, |
| 114 | ); |
| 115 | } |
| 116 | return sanitized.join(""); |
| 117 | } |
| 118 | |
| 119 | /** Removes common credential forms before provider text crosses a transport boundary. */ |
| 120 | function redactProviderFailureText(value: string): string { |
| 121 | return replaceUnsafeControlCharacters(value) |
| 122 | .replace(/\bhttps?:\/\/[^\s<>"']+/giu, redactUrl) |
| 123 | .replace(/\b(Bearer|Basic)\s+[^\s,;]+/giu, "$1 [REDACTED]") |
| 124 | .replace( |
| 125 | /(["'](?:access[_-]?token|api[_-]?key|authorization|credential|password|secret|token)["']\s*:\s*["'])[^"']*(["'])/giu, |
| 126 | "$1[REDACTED]$2", |
| 127 | ) |
| 128 | .replace( |
| 129 | /(\b(?:access[_-]?token|api[_-]?key|authorization|credential|password|secret|token)\b\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/giu, |
| 130 | "$1[REDACTED]", |
| 131 | ) |
| 132 | .replace(/\bsk-[A-Za-z0-9_-]{16,}\b/gu, "[REDACTED]") |
| 133 | .trim(); |
| 134 | } |
| 135 | |
| 136 | function boundedText(value: string, maxLength: number): string { |
| 137 | const redacted = redactProviderFailureText(value); |
| 138 | if (redacted.length <= maxLength) return redacted; |
| 139 | let end = Math.max(0, maxLength - 1); |
| 140 | const finalCodeUnit = redacted.charCodeAt(end - 1); |
| 141 | if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) { |
| 142 | end -= 1; |
| 143 | } |
| 144 | return `${redacted.slice(0, end)}…`; |
| 145 | } |
| 146 | |
| 147 | export function makeProviderFailure(input: { |
| 148 | readonly cause?: unknown; |
| 149 | readonly message?: string | undefined; |
| 150 | readonly code?: string | null | undefined; |
| 151 | readonly class?: OrchestrationV2ProviderFailureClass; |
| 152 | readonly retryable?: boolean | null; |
| 153 | readonly resetAt?: string | null; |
| 154 | }): OrchestrationV2ProviderFailure { |
| 155 | const rawMessage = input.message ?? causeMessage(input.cause) ?? DEFAULT_PROVIDER_FAILURE_MESSAGE; |
| 156 | const message = boundedText(rawMessage, MAX_PROVIDER_FAILURE_MESSAGE_LENGTH); |
| 157 | const rawCode = input.code ?? stringField(input.cause, "code") ?? null; |
| 158 | const code = |
| 159 | rawCode === null ? null : boundedText(rawCode, MAX_PROVIDER_FAILURE_CODE_LENGTH) || null; |
| 160 | |
| 161 | return { |
| 162 | class: input.class ?? "unknown", |
| 163 | message: message || DEFAULT_PROVIDER_FAILURE_MESSAGE, |
| 164 | code, |
| 165 | retryable: input.retryable ?? null, |
| 166 | ...(input.class === "usage_limit" && |
| 167 | input.resetAt != null && |
| 168 | Number.isFinite(Date.parse(input.resetAt)) |
| 169 | ? { resetAt: DateTime.formatIso(DateTime.makeUnsafe(input.resetAt)) } |
| 170 | : {}), |
| 171 | }; |
| 172 | } |
| 173 | |
| 174 | export function makeProviderFailureTurnItem(input: { |
| 175 | readonly driver: ProviderDriverKind; |
| 176 | readonly threadId: ThreadId; |
| 177 | readonly runId: RunId | null; |
| 178 | readonly nodeId: NodeId | null; |
| 179 | readonly providerThreadId: ProviderThreadId; |
| 180 | readonly providerTurnId: ProviderTurnId; |
| 181 | readonly itemOrdinal: number; |
| 182 | readonly failure: OrchestrationV2ProviderFailure; |
| 183 | readonly retry?: OrchestrationV2ProviderRetry; |
| 184 | readonly retryStartedAt?: DateTime.Utc; |
| 185 | readonly occurredAt: DateTime.Utc; |
| 186 | }): Extract<OrchestrationV2TurnItem, { readonly type: "error" }> { |
| 187 | return { |
| 188 | id: IdAllocator.derive.turnItemFromProviderItem({ |
| 189 | driver: input.driver, |
| 190 | nativeItemId: `terminal-failure:${input.providerTurnId}`, |
| 191 | }), |
| 192 | threadId: input.threadId, |
| 193 | runId: input.runId, |
| 194 | nodeId: input.nodeId, |
| 195 | providerThreadId: input.providerThreadId, |
| 196 | providerTurnId: input.providerTurnId, |
| 197 | nativeItemRef: null, |
| 198 | parentItemId: null, |
| 199 | ordinal: input.itemOrdinal, |
| 200 | status: "failed", |
| 201 | title: input.failure.class === "usage_limit" ? "Usage limit reached" : "Provider error", |
| 202 | startedAt: input.retryStartedAt ?? input.occurredAt, |
| 203 | completedAt: input.occurredAt, |
| 204 | updatedAt: input.occurredAt, |
| 205 | type: "error", |
| 206 | failure: input.failure, |
| 207 | ...(input.retry === undefined ? {} : { retry: input.retry }), |
| 208 | }; |
| 209 | } |
| 210 | |
| 211 | export function makeProviderRetryTurnItem(input: { |
| 212 | readonly driver: ProviderDriverKind; |
| 213 | readonly threadId: ThreadId; |
| 214 | readonly runId: RunId | null; |
| 215 | readonly nodeId: NodeId | null; |
| 216 | readonly providerThreadId: ProviderThreadId; |
| 217 | readonly providerTurnId: ProviderTurnId; |
| 218 | readonly itemOrdinal: number; |
| 219 | readonly failure: OrchestrationV2ProviderFailure; |
| 220 | readonly retry: OrchestrationV2ProviderRetry; |
| 221 | readonly status: Extract< |
| 222 | OrchestrationV2TurnItem["status"], |
| 223 | "running" | "completed" | "failed" | "interrupted" | "cancelled" |
| 224 | >; |
| 225 | readonly startedAt: DateTime.Utc; |
| 226 | readonly updatedAt: DateTime.Utc; |
| 227 | }): Extract<OrchestrationV2TurnItem, { readonly type: "error" }> { |
| 228 | const completed = input.status !== "running"; |
| 229 | let title = "Provider retry"; |
| 230 | if (input.status === "completed") { |
| 231 | title = "Provider recovered"; |
| 232 | } else if (input.status === "failed") { |
| 233 | title = input.failure.class === "usage_limit" ? "Usage limit reached" : "Provider error"; |
| 234 | } else if (input.status === "interrupted" || input.status === "cancelled") { |
| 235 | title = "Provider retry stopped"; |
| 236 | } |
| 237 | return { |
| 238 | id: IdAllocator.derive.turnItemFromProviderItem({ |
| 239 | driver: input.driver, |
| 240 | nativeItemId: `terminal-failure:${input.providerTurnId}`, |
| 241 | }), |
| 242 | threadId: input.threadId, |
| 243 | runId: input.runId, |
| 244 | nodeId: input.nodeId, |
| 245 | providerThreadId: input.providerThreadId, |
| 246 | providerTurnId: input.providerTurnId, |
| 247 | nativeItemRef: null, |
| 248 | parentItemId: null, |
| 249 | ordinal: input.itemOrdinal, |
| 250 | status: input.status, |
| 251 | title, |
| 252 | startedAt: input.startedAt, |
| 253 | completedAt: completed ? input.updatedAt : null, |
| 254 | updatedAt: input.updatedAt, |
| 255 | type: "error", |
| 256 | failure: input.failure, |
| 257 | retry: input.retry, |
| 258 | }; |
| 259 | } |