/** * 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 '' — skipping`, * `⚠

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 `✓ completed via

* (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 { readonly result: T; readonly provider: ProviderId; readonly fellBack: boolean; } /** * Run a per-Provider attempt through the candidate loop. * * The executor walks the prebuilt plan, emitting skip-notices for * ineligible entries and switch-notices between failed candidates. * On success it returns the outcome and (if `fellBack === true`) * writes the summary notice `✓ completed via

(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(opts: FallbackExecutionOptions, attempt: (descriptor: ProviderDescriptor) => Promise): Promise>; //# sourceMappingURL=provider-fallback.d.ts.map