Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
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<AgentVisibleErrorOverrideCode, AgentVisibleErrorOverride>
|
|
>;
|
|
|
|
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<AgentRunErrorCode, AgentErrorPresentation> = {
|
|
// 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;
|
|
}
|