| 1 | import { PROVIDER_DISPLAY_NAMES, type ProviderDriverKind } from "@t3tools/contracts"; |
| 2 | import * as Predicate from "effect/Predicate"; |
| 3 | |
| 4 | const GENERIC_ERROR_PREFIXES = [ |
| 5 | "Failed to dispatch orchestration V2 command", |
| 6 | "Failed to dispatch orchestration command ", |
| 7 | "Provider adapter failed while dispatching orchestration command ", |
| 8 | ]; |
| 9 | |
| 10 | /** |
| 11 | * User-facing prose for capability rejections. The internal error messages |
| 12 | * keep command ids and capability codes for logs; the toast should say what |
| 13 | * did not happen and why in the provider's own name. |
| 14 | */ |
| 15 | const CAPABILITY_REJECTION_MESSAGES: Record<string, (provider: string) => string> = { |
| 16 | queued_messages: (p) => `${p} cannot queue messages behind an active run.`, |
| 17 | active_steering: (p) => |
| 18 | `${p} cannot redirect an active run. Stop it first, then send the message.`, |
| 19 | interrupt_restart_steering: (p) => |
| 20 | `${p} cannot redirect an active run. Stop it first, then send the message.`, |
| 21 | interrupt: (p) => `${p} cannot stop a run once it has started.`, |
| 22 | native_fork: (p) => `${p} cannot fork this thread natively.`, |
| 23 | fork_from_turn: (p) => `${p} cannot fork from an earlier point in this thread.`, |
| 24 | rollback: (p) => |
| 25 | `${p} cannot rewind its conversation, so this checkpoint cannot be restored on this thread.`, |
| 26 | rollback_snapshot: (p) => |
| 27 | `${p} did not report its rewound conversation state, so the checkpoint was not restored.`, |
| 28 | context_handoff: (p) => `${p} cannot receive the context handoff needed for this switch.`, |
| 29 | strong_terminal_status: (p) => |
| 30 | `${p} cannot confirm when its runs finish reliably enough for this.`, |
| 31 | }; |
| 32 | |
| 33 | function providerDisplayName(instanceId: string): string { |
| 34 | const known = PROVIDER_DISPLAY_NAMES[instanceId as ProviderDriverKind]; |
| 35 | if (known !== undefined) return known; |
| 36 | const trimmed = instanceId.replace(/Agent$/i, "").trim(); |
| 37 | if (trimmed.length === 0) return instanceId; |
| 38 | return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); |
| 39 | } |
| 40 | |
| 41 | /** Friendly translation for command-policy rejections; undefined otherwise. */ |
| 42 | function policyRejectionMessage(value: unknown): string | undefined { |
| 43 | if (!Predicate.isObject(value) || typeof value.providerInstanceId !== "string") return undefined; |
| 44 | const provider = providerDisplayName(value.providerInstanceId); |
| 45 | if (value._tag === "CommandPolicyCapabilityUnsupportedError") { |
| 46 | const capability = typeof value.capability === "string" ? value.capability : ""; |
| 47 | return CAPABILITY_REJECTION_MESSAGES[capability]?.(provider); |
| 48 | } |
| 49 | if (value._tag === "CommandPolicyUnsupportedError") { |
| 50 | return `${provider} cannot deliver a message that way right now.`; |
| 51 | } |
| 52 | return undefined; |
| 53 | } |
| 54 | |
| 55 | function textValue(value: unknown): string | undefined { |
| 56 | return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; |
| 57 | } |
| 58 | |
| 59 | function messageFrom(value: unknown): string | undefined { |
| 60 | if (typeof value === "string") { |
| 61 | return textValue(value); |
| 62 | } |
| 63 | const friendly = policyRejectionMessage(value); |
| 64 | if (friendly !== undefined) { |
| 65 | return friendly; |
| 66 | } |
| 67 | if (value instanceof Error) { |
| 68 | return textValue(value.message); |
| 69 | } |
| 70 | if (!Predicate.isObject(value)) { |
| 71 | return undefined; |
| 72 | } |
| 73 | return textValue(value.detail) ?? textValue(value.message); |
| 74 | } |
| 75 | |
| 76 | function isGenericErrorMessage(message: string): boolean { |
| 77 | return GENERIC_ERROR_PREFIXES.some((prefix) => message.startsWith(prefix)); |
| 78 | } |
| 79 | |
| 80 | function collectErrorMessages(value: unknown, seen: Set<unknown>): ReadonlyArray<string> { |
| 81 | if (value === undefined || value === null || seen.has(value)) { |
| 82 | return []; |
| 83 | } |
| 84 | seen.add(value); |
| 85 | |
| 86 | if (Array.isArray(value)) { |
| 87 | return value.flatMap((item) => collectErrorMessages(item, seen)); |
| 88 | } |
| 89 | |
| 90 | const message = messageFrom(value); |
| 91 | if (!Predicate.isObject(value)) { |
| 92 | return message === undefined ? [] : [message]; |
| 93 | } |
| 94 | |
| 95 | const nested = [ |
| 96 | ...collectErrorMessages(value.cause, seen), |
| 97 | ...collectErrorMessages(value.error, seen), |
| 98 | ...collectErrorMessages(value.errors, seen), |
| 99 | ]; |
| 100 | |
| 101 | return message === undefined ? nested : [message, ...nested]; |
| 102 | } |
| 103 | |
| 104 | export function userFacingDispatchErrorMessage(cause: unknown): string | undefined { |
| 105 | const messages = collectErrorMessages(cause, new Set()).filter( |
| 106 | (message, index, allMessages) => |
| 107 | !isGenericErrorMessage(message) && allMessages.indexOf(message) === index, |
| 108 | ); |
| 109 | return messages.at(-1); |
| 110 | } |