/** * Classification of LLM-decision failures into a stable code, a retry flag, and * a two-bucket user-facing message. * * ## Why this module exists * * When an action runs an LLM decision (`OpenClawLlmDecision.decide`, * `src/actions/llm-decision/openclaw-llm-decision.ts`) and the OpenClaw `llm-task` * tool fails, the only thing the runtime has is a flat error string. Two consumers * need to make decisions from that string: * * 1. The **notification layer** ({@link formatDecisionErrorNotification} in * `notifications.ts`) — what to tell the skill author. * 2. The **decision client** (`openclaw-llm-decision.ts`) — whether a retry can * plausibly succeed. * * ## Two pieces * * 1. {@link DECISION_ERROR_TABLE} — the ordered matchers (raw string → code) * and the `retryable` flag. Per-code detail is preserved here for logs and * telemetry, even though the two consumers above only see the collapsed * bucket. * 2. {@link classifyDecisionError} — walks the table, then renders the * user-facing message from {@link DECISION_ERROR_BUCKET_COPY}, keyed only * by `retryable` (see {@link decisionErrorBucket}). Author-facing copy * does not vary by code — dozens of failure-mode-specific messages proved * to be more than users act on; the two buckets are "this will likely * sort itself out" vs. "this needs you to look at it." * * To change matching or retry behavior: edit {@link DECISION_ERROR_TABLE}. * To change user-facing wording: edit {@link DECISION_ERROR_BUCKET_COPY}. * * ## Where the raw strings come from * * The granular OpenClaw messages (e.g. `"LLM returned empty output"`, * `"prompt required"`) are thrown by the **external gateway**, not by this plugin. * They reach us **embedded** inside one of the wrappers that * `OpenClawLlmDecision.decide` throws: * * - `OpenClaw gateway error (): ` (non-2xx HTTP) * - `OpenClaw gateway rejected request: ` (HTTP 200, `body.ok === false`) * * Matchers therefore look for the granular substring **anywhere** in the string, * not anchored. Our own parse failures (`Invalid llm-task response: ...` from * `extractDecision`) are classified too. * * ## v2026.2.22 caveat * * On OpenClaw v2026.2.22 (current prod) the gateway collapses most `llm-task` * failures into a generic `tool_error` whose message is `"tool execution failed"`, * so the granular substrings frequently do **not** surface there — only on * v2026.5.7+. The table is built in full anyway: on v2026.2.22 the generic * `tool_error` matches {@link DecisionErrorCode.ProviderError}; on newer gateways * the specific code matches. */ import type { NotificationCopy } from "../types/notification.js"; /** Stable identifier for each classified LLM-decision failure mode. */ export declare enum DecisionErrorCode { /** Gateway auth token missing in env/config — thrown before any HTTP call. */ GatewayTokenMissing = "GATEWAY_TOKEN_MISSING", /** `fetch()` rejected before a response (connection refused/reset/DNS). */ GatewayUnreachable = "GATEWAY_UNREACHABLE", /** Gateway returned 404 — plugin not installed at this path / version mismatch. */ PluginEndpointMissing = "PLUGIN_ENDPOINT_MISSING", /** `llm-task` invoked with empty/whitespace prompt — missing `decision_prompt`. */ PromptRequired = "PROMPT_REQUIRED", /** `decision_model` did not resolve to a known provider/model pair. */ ProviderModelUnresolved = "PROVIDER_MODEL_UNRESOLVED", /** Resolved model is not on the gateway's `allowedModels` allowlist. */ ModelNotAllowed = "MODEL_NOT_ALLOWED", /** Context/payload passed to `llm-task` was not JSON-serializable — internal bug. */ InputNotSerializable = "INPUT_NOT_SERIALIZABLE", /** Provider responded but produced no text (safety filter / refusal / empty completion). */ LlmEmptyOutput = "LLM_EMPTY_OUTPUT", /** Model reply could not be parsed as JSON (truncation / prose around JSON). */ LlmInvalidJson = "LLM_INVALID_JSON", /** Model reply was valid JSON but failed schema validation. */ LlmSchemaMismatch = "LLM_SCHEMA_MISMATCH", /** Upstream provider error (rate limit / outage) or generic `tool_error`. */ ProviderError = "PROVIDER_ERROR", /** Gateway/upstream timeout (408/504) or runtime-side abort — we already waited 90s. */ Timeout = "TIMEOUT", /** Embedded LLM helper failed to load on the host — corrupt/partial OpenClaw install. */ EmbeddedAgentUnavailable = "EMBEDDED_AGENT_UNAVAILABLE", /** Thinking level string does not normalize to any known level (v2026.5.7+). */ InvalidThinkingLevel = "INVALID_THINKING_LEVEL", /** Thinking level is known but unsupported by the resolved model (v2026.5.7+). */ ThinkingLevelUnsupported = "THINKING_LEVEL_UNSUPPORTED", /** Gateway returned 200 with a response shape this plugin can't parse — internal bug. */ PluginResponseShape = "PLUGIN_RESPONSE_SHAPE", /** Any other 5xx with no recognizable structured body. */ OpenClawUnexpected = "OPENCLAW_UNEXPECTED", /** No matcher matched — surfaces the raw message so operators can still diagnose. */ Unknown = "UNKNOWN" } /** Result of classifying a raw LLM-decision error string. */ export interface ClassifiedDecisionError { code: DecisionErrorCode; /** Whether a retry of the `llm-task` call can plausibly succeed. */ retryable: boolean; /** User-facing notification message (the bucket's in-app text — see {@link decisionErrorBucket}). */ userMessage: string; } /** One row of {@link DECISION_ERROR_TABLE}. */ interface DecisionErrorEntry { code: DecisionErrorCode; /** * Tested against the raw error string. Returning a {@link RegExpMatchArray} * (truthy) or `true` counts as a match; `null`/`false` means no match. */ match: (raw: string) => RegExpMatchArray | boolean | null; retryable: boolean; } /** * Ordered classification table — matchers + retry flag. First match wins, so * keep specific matchers above generic ones (e.g. `LlmEmptyOutput` before the * catch-all `ProviderError`, since both can appear inside a `"rejected * request: ..."` wrapper). `code` and `retryable` are the source of truth for * logs/telemetry; user-facing copy lives in {@link DECISION_ERROR_BUCKET_COPY}. */ export declare const DECISION_ERROR_TABLE: readonly DecisionErrorEntry[]; /** * The two user-facing buckets every decision error collapses into. A code's * long tail of remediation detail (which YAML field, which host setting) is * useful in logs but not something most users act on; retryable vs. not is * the one distinction that changes what the user should expect next. */ export type DecisionErrorBucket = "transient" | "attention"; /** * Map a classified error to its user-facing bucket. Its own code→bucket mapping, * independent of `retryable`: the infra-blip codes in {@link TRANSIENT_BUCKET_CODES} * are "transient" regardless of their retry flag; everything else follows * `retryable` (retryable → transient, else the author-attention bucket). */ export declare function decisionErrorBucket(code: DecisionErrorCode, retryable: boolean): DecisionErrorBucket; /** * Copy for each bucket. In-app text carries `` display markup and is label-free * (the front-end renders the strategy label as a separate tag). The push body is * plain text and carries the strategy name inline via a `` tag * after the leading emoji; the notifications service substitutes the resolved name * (from `strategyWalletAddress`) before send. */ export declare const DECISION_ERROR_BUCKET_COPY: Record; /** * Classify a raw LLM-decision error string into a {@link ClassifiedDecisionError}. * * Walks {@link DECISION_ERROR_TABLE} in order; the first matching entry wins, * giving `code` + `retryable` (preserved for logs/telemetry). If nothing * matches, returns {@link DecisionErrorCode.Unknown} (not retryable). The * user-facing `userMessage` is always the collapsed bucket's in-app copy from * {@link DECISION_ERROR_BUCKET_COPY} — it does not vary by code. * * @param rawMessage - The error string thrown by the decision client (may embed an * OpenClaw gateway message). Non-string / empty inputs are coerced safely. */ export declare function classifyDecisionError(rawMessage: string): ClassifiedDecisionError; /** * Convenience predicate for retry wiring: `true` when a retry of the failed * `llm-task` call can plausibly succeed. * * @param error - The thrown error (or any value); only its message is inspected. */ export declare function isRetryableDecisionError(error: unknown): boolean; export {}; //# sourceMappingURL=decision-errors.d.ts.map