1import { PROVIDER_DISPLAY_NAMES, type ProviderDriverKind } from "@t3tools/contracts";
2import * as Predicate from "effect/Predicate";
3
4const 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 */
15const 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
33function 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. */
42function 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
55function textValue(value: unknown): string | undefined {
56 return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
57}
58
59function 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
76function isGenericErrorMessage(message: string): boolean {
77 return GENERIC_ERROR_PREFIXES.some((prefix) => message.startsWith(prefix));
78}
79
80function 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
104export 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}