/** * Provider Fallback Executor (Provider Fallback Tech Plan §"Core mechanism"). * * Provider-neutral candidate loop. Imports only the descriptor + error * types and the `ProviderContext` injection shape; it never imports a * concrete Adapter, transport, UTCP client, or command module. Every * shared-capability handler passes a per-provider `attempt` callback * (which builds its own Adapter and invokes the appropriate * `executeX` primitive) and receives a single {@link FallbackOutcome}. * * Boundary rules (Provider Fallback Tech Plan §"Core mechanism"): * - May import Provider descriptor + error types and the * `ProviderContext` injection shape. * - Must NOT import a concrete Adapter, a transport, a UTCP client, * a command module, a Capability, or `executeX`/shared-execution * primitives. * - The dispatch layer owns the call site; the handlers own request * shape and which `executeX` is invoked. This module owns * candidate ordering, preflight, error classification, exhaustion, * and notices. * * Algorithm (Tech Plan §"Algorithm"): * 1. Build a candidate plan (an ordered list, not a silent filter): * `[effective, ...descriptors]`, deduplicated by id, with each * entry tagged `eligible | incapable | unconfigured` from an * ordered preflight (capability metadata → `isConfigured` → * adapter-handle agreement). Ineligible entries are RETAINED * with their reason so skip-notices fire and the effective * provider's real error is preserved on exhaustion. * 2. Walk the plan. Emit skip-notices for ineligible entries * (`⚠
does not support ' is not configured — skipping`).
* 3. For each eligible candidate, run the adapter-handle agreement
* step and `await attempt(d)`. On success, return
* `{ result, provider: d.id, fellBack: d.id !== effective }`. On
* throw, classify (Tech Plan §"Error classification"): re-throw
* `ValidationError` (no loop) and unknown errors (fail closed);
* continue on `UnsupportedCapabilityError`, `UnsupportedOptionError`,
* and the runtime-error family. Emit a switch notice when
* continuing to the next candidate.
* 4. Exhaustion. If no eligible candidate succeeds, re-throw the
* **effective** provider's own error when it ran; otherwise the
* last eligible candidate's runtime error when the effective was
* skipped; otherwise the typed preflight error
* (`ConfigurationError` for `unconfigured`, `UnsupportedCapabilityError`
* for `incapable`). Never synthesize a substitute error type.
* Preserving the effective's own error when it ran keeps the
* 0.10.x exit codes (critique #7).
*
* Kill-switch (Tech Plan §"Kill-switch plumbing"). When
* `fallbackEnabled === false`, the plan is `[effective]` only, and
* the SAME preflight runs on it. The kill-switch narrows the plan;
* it does NOT bypass the preflight. So an incapable effective throws
* `UnsupportedCapabilityError` and an unconfigured effective throws
* `ConfigurationError` — the exact 0.10.x codes and ordering, with
* zero adapter work for the unsupported case (FR-023/024). No notices
* are emitted under the kill-switch.
*
* Notices (Tech Plan §"Failure, notice & cache semantics"). Every
* notice is written via the injected `writeStderr` only; the executor
* never writes to stdout. The summary line `✓
* (fallback)` is emitted only when `fellBack === true` (i.e. the
* winning provider is not the effective). Under the kill-switch,
* `fellBack` can never be true, so no summary is emitted.
*/
import type { ProviderCapability, ProviderDescriptor, ProviderId } from "../providers/types.js";
/**
* Options for {@link executeWithFallback}. The executor owns no
* environment, registry, or stderr writer of its own — every dependency
* is injected so a unit test can drive the loop with doubles and
* capture every emitted notice.
*
* - `capabilityId` — the Capability the handler is exercising. Used for
* descriptor preflight, notice wording, and the typed
* `UnsupportedCapabilityError` thrown on exhaustion.
* - `commandLabel` — short human label used in switch / summary notices
* (e.g. `"search"`, `"read"`, `"crawl"`). Distinct from
* `capabilityId` so a single Capability can be reached under
* different user-facing command names without rewriting notice text.
* - `effectiveProvider` — the Provider the handler resolved from
* explicit flag, env, or default. It is the first plan entry AND
* the Provider whose error is preserved on exhaustion.
* - `descriptors` — the Provider registry, in registry order
* `[zai, minimax, tavily, exa, brave, firecrawl]`. The executor
* dedupes the plan by id; the first occurrence of an id wins.
* - `env` — the environment used for `descriptor.isConfigured` and the
* `ProviderContext` passed to `descriptor.create`. Injected so the
* executor never reads `process.env` directly.
* - `fallbackEnabled` — kill-switch. `false` narrows the plan to
* `[effective]` only and suppresses all notices.
* - `writeStderr` — single-writer injection. All notices go through
* this function; the executor never calls `process.stderr.write`
* directly. This is the same `writeStderr` surface the handlers
* receive through `deps.invocation.writeStderr`.
*/
export interface FallbackExecutionOptions {
readonly capabilityId: ProviderCapability;
readonly commandLabel: string;
readonly effectiveProvider: ProviderId;
readonly descriptors: readonly ProviderDescriptor[];
readonly env: NodeJS.ProcessEnv;
readonly fallbackEnabled: boolean;
readonly writeStderr: (s: string) => void;
}
/**
* Result of {@link executeWithFallback}. The handler uses
* `result` for its presentation output, `provider` to report the
* winning Provider (later tickets will thread it into the data
* envelope), and `fellBack` to gate the summary notice and (later)
* downstream behaviour that depends on whether the original
* preference held.
*/
export interface FallbackOutcome (fallback)`.
* On exhaustion it re-throws the EFFECTIVE provider's own error when
* that provider ran; when the effective was ineligible and eligible
* candidates failed, it re-throws the last eligible failure so the
* envelope stays actionable; otherwise the typed preflight error
* (`ConfigurationError` / `UnsupportedCapabilityError`). The executor
* never synthesizes a substitute error type, so the 0.10.x exit codes
* are preserved when the effective ran (critique #7 fix).
*
* The kill-switch narrows the plan to `[effective]` and suppresses
* all notices. The same preflight still runs on the effective
* provider, so an incapable / unconfigured effective surfaces its
* exact preflight error (FR-023/024 preserved).
*
* Throws:
* - `Error` only if the effective Provider is not present in
* `descriptors`. This is a programmer error (the handler should
* have validated the Provider id before calling); surfacing it
* as a regular `Error` keeps the dispatch try/catch honest.
* - `ValidationError` re-thrown without looping (no Provider will
* succeed on bad input).
* - Any other `ScoutlineError` (or its `UnsupportedCapabilityError` /
* `UnsupportedOptionError` subclasses) re-thrown from the
* effective candidate on exhaustion.
* - Non-`ScoutlineError` values re-thrown unchanged when they leak
* from an attempt; the classifier treats them as "unknown" and
* re-throws to fail closed.
*/
export declare function executeWithFallback