import type { AgentEnvPanelFocus } from "./agentEnvPanelActions"; /** * Run-failure codes actually emitted by the daemon runtime classifier * (packages/agent/daemon/runtime/visible_error.go `visibleFailureCode`). These * are the codes the conversation error card really receives — unlike the * aspirational `CODEX_*` codes, which the run pipeline never produces. * * Keep this union aligned with the Go switch in `visibleFailureCode`. */ export type AgentRunErrorCode = | "auth_required" | "account_not_allowed" | "billing_error" | "cli_not_found" | "cli_version_unsupported" | "network_error" | "runtime_unavailable" | "request_timed_out" | "provider_config_timeout" | "provider_stream_disconnected" | "provider_empty_response" | "provider_concurrency_limit" | "provider_unavailable" | "invalid_request" | "model_not_available" | "max_output_tokens" | "insufficient_credits" | "model_not_allowed" | "plugin_unavailable" | "quota_or_rate_limit" | "session_interrupted" | "subscription_required" | "process_exited" | "provider_error" | "unknown"; /** * Identifies who can remediate a visible error in the current AgentGUI view. * This is presentation-only host context; it does not alter canonical Turns * or provider error data. */ export type AgentVisibleErrorPresentationScope = | "local_owner" | "shared_caller"; export interface AgentVisibleErrorOverride { message: string; /** Provider ids for which this product-owned override is valid. */ providers: readonly string[]; action?: { label: string; url: string; } | null; } /** * Product-owned structured errors whose copy may be supplied by a Host. * Environment/runtime errors remain canonical AgentGUI policy. */ export type AgentVisibleErrorOverrideCode = "insufficient_credits"; export type AgentVisibleErrorOverrides = Partial< Record >; export interface AgentErrorPresentation { /** * i18n key for the one human sentence shown in the card, or null to let the * caller fall back to its phase-aware generic title. */ messageKey: string | null; /** * Env-panel section the remediation button deep-links to, or null when the * failure is transient/server-side and the wizard cannot fix it — in which * case no call-to-action is shown (showing one would misrepresent reality). */ focus: AgentEnvPanelFocus | null; /** i18n key for the remediation button. */ actionKey: string | null; /** External remediation destination, when the action is account-level. */ externalUrl?: string | null; } const NO_CTA = { focus: null, actionKey: null } as const; const SHARED_CALLER_OWNER_REMEDIABLE_CODES: readonly string[] = [ "auth_required", "cli_not_found", "cli_version_unsupported", "runtime_unavailable", "execution_start_failed", "execution_lost", "insufficient_credits", "subscription_required", "quota_exhausted", "model_not_allowed", "plugin_unavailable" ]; /** * Returns whether a structured failure requires the shared Agent owner to * remediate it. Transport and ambiguous failures deliberately stay neutral so * callers retain the existing retry guidance. */ export function isSharedCallerOwnerRemediableError( code: string | null | undefined ): boolean { return ( typeof code === "string" && SHARED_CALLER_OWNER_REMEDIABLE_CODES.includes(code) ); } // The escape hatch for hard failures whose cause is ambiguous from the message // alone (a non-zero exit, an unclassified provider error): send the user into // the wizard to self-detect, but keep the generic message. const SELF_DETECT = { messageKey: null, focus: "detect" as const, actionKey: "agentHost.agentGui.visibleErrorActionDetect" }; const PRESENTATIONS: Record = { // Environment problems the wizard can detect or repair → route to its step. auth_required: { messageKey: "agentHost.agentGui.visibleErrorAuthRequired", focus: "auth", actionKey: "agentHost.agentGui.visibleErrorActionRelogin" }, account_not_allowed: { messageKey: null, ...NO_CTA }, billing_error: { messageKey: null, ...NO_CTA }, cli_not_found: { messageKey: "agentHost.agentGui.visibleErrorCliNotFound", focus: "install", actionKey: "agentHost.agentGui.visibleErrorActionInstall" }, cli_version_unsupported: { messageKey: "agentHost.agentGui.visibleErrorVersionUnsupported", focus: "upgrade", actionKey: "agentHost.agentGui.visibleErrorActionUpgrade" }, network_error: { messageKey: "agentHost.agentGui.visibleErrorNetwork", focus: "network", actionKey: "agentHost.agentGui.visibleErrorActionCheckNetwork" }, runtime_unavailable: { messageKey: "agentHost.agentGui.visibleErrorRuntimeUnavailable", focus: "detect", actionKey: "agentHost.agentGui.visibleErrorActionDetect" }, // Transient / server-side failures: accurate copy, but no wizard CTA — it // cannot fix a rate limit or a dropped stream. request_timed_out: { messageKey: "agentHost.agentGui.visibleErrorRequestTimedOut", ...NO_CTA }, provider_config_timeout: { messageKey: "agentHost.agentGui.visibleErrorConfigTimeout", ...NO_CTA }, provider_stream_disconnected: { messageKey: "agentHost.agentGui.visibleErrorStreamDisconnected", ...NO_CTA }, provider_empty_response: { messageKey: "agentHost.agentGui.visibleErrorEmptyResponse", focus: "detect", actionKey: "agentHost.agentGui.visibleErrorActionDetect" }, provider_concurrency_limit: { messageKey: "agentHost.agentGui.visibleErrorConcurrencyLimit", ...NO_CTA }, provider_unavailable: { messageKey: null, ...NO_CTA }, invalid_request: { messageKey: null, ...NO_CTA }, model_not_available: { messageKey: null, ...NO_CTA }, max_output_tokens: { messageKey: null, ...NO_CTA }, insufficient_credits: { messageKey: "agentHost.agentGui.visibleErrorInsufficientCreditsUnknown", ...NO_CTA }, model_not_allowed: { messageKey: "agentHost.agentGui.visibleErrorModelNotAllowed", ...NO_CTA }, plugin_unavailable: { messageKey: "agentHost.agentGui.visibleErrorPluginUnavailable", ...NO_CTA }, quota_or_rate_limit: { messageKey: "agentHost.agentGui.visibleErrorQuotaOrRateLimit", ...NO_CTA }, session_interrupted: { messageKey: "agentHost.agentGui.visibleErrorSessionInterrupted", ...NO_CTA }, subscription_required: { messageKey: "agentHost.agentGui.visibleErrorSubscriptionRequired", ...NO_CTA }, // Ambiguous hard failures → generic message + self-detect escape hatch. process_exited: SELF_DETECT, provider_error: SELF_DETECT, unknown: SELF_DETECT }; /** * Resolves the card presentation for a run-failure code. Returns null for codes * outside the known vocabulary so the caller renders its plain generic card with * no call-to-action. */ export function resolveAgentErrorPresentation( code: string | null | undefined, scope: AgentVisibleErrorPresentationScope = "local_owner" ): AgentErrorPresentation | null { if (!code) { return null; } const presentation = PRESENTATIONS[code as AgentRunErrorCode] ?? null; if (!presentation || scope !== "shared_caller") { return presentation; } // Shared callers must never be sent to the current device's Agent Env // wizard. The caller-contact copy is rendered separately for structured // Owner-remediable failures; transient failures preserve their current copy. return { ...presentation, ...NO_CTA }; } const FAILED_MESSAGE_CODE_MARKERS: ReadonlyArray< readonly [AgentRunErrorCode, readonly string[]] > = [ [ "subscription_required", [ "unable to verify your membership benefits", "ensure your membership is active", "subscription does not have access", "current plan supports only", "membership expired", "subscription required" ] ], [ "insufficient_credits", [ "insufficient credits", "insufficient balance", "balance is insufficient", "402 payment required" ] ], ["provider_concurrency_limit", ["concurrency limit exceeded"]], [ "quota_or_rate_limit", [ "quota", "rate limit", "limit exceeded", "usage limit", "upgrade your plan to continue", "add a payment method to continue", "resource_exhausted", "too many requests", "status code: 429" ] ], ["model_not_allowed", ["model_not_allowed"]], ["request_timed_out", ["api error: 522", "error 522: connection timed out"]], [ "auth_required", [ "authentication_failed", "invalid authentication credentials", "401 invalid authentication", "unauthorized", "not logged in", "please run /login", "invalid api key" ] ], [ "cli_version_unsupported", ["requires a newer version", "version is too old", "unsupported version"] ], [ "cli_not_found", [ "no such file or directory", "command not found", "enoent", "executable file not found" ] ], [ "network_error", ["enotfound", "econnrefused", "econnreset", "getaddrinfo", "socket hang up"] ] ]; /** * Some providers (notably Claude Code) report an environment failure — e.g. a * dropped login (401) — as a plain failed assistant message rather than a * structured visibleError, so it never gets the remediation card. This recovers * the env-fixable code from that message's text so the caller can still route the * user to the wizard. Returns null when the text isn't a recognized env failure * (so transient/unknown failures stay plain). */ export function classifyFailedAgentMessage( body: string | null | undefined ): AgentRunErrorCode | null { if (!body) { return null; } const lower = body.toLowerCase(); for (const [code, markers] of FAILED_MESSAGE_CODE_MARKERS) { if (markers.some((marker) => lower.includes(marker))) { return code; } } return null; } const COMPLETED_AUTH_MESSAGE_MAX_LENGTH = 160; const COMPLETED_AUTH_MESSAGE_PATTERN = /^not logged in\s*(?:[·•:—-]|\.)?\s*please run\s+\/login[.!]?$/i; /** * Resolves a plain assistant message that should be recovered into a visible * error card. Failed messages keep the provider-agnostic fallback above. * * Claude Code demonstrates why completed messages need a narrow exception: its * SDK can return the standalone "Not logged in · Please run /login" notice * together with a successful result. Match the provider-owned output shape, * rather than branching on provider identity, and keep it short and * whole-message anchored so normal answers are not reclassified. */ export function classifyRecoverableAgentMessage(input: { body: string | null | undefined; statusKind: string | null | undefined; }): AgentRunErrorCode | null { if (input.statusKind === "failed") { return classifyFailedAgentMessage(input.body); } if ( input.statusKind !== "completed" || !input.body || input.body.length > COMPLETED_AUTH_MESSAGE_MAX_LENGTH ) { return null; } return COMPLETED_AUTH_MESSAGE_PATTERN.test(input.body.trim()) ? "auth_required" : null; }