/** * Shared Execution Contract (DESIGN.md §10). * * Owns cache lookup, legacy read-through, retry policy, and local count * truncation for every Provider Capability. Capability Modules define * shared meaning; Adapters perform a single transport attempt; this * module is the single retry and cache-policy owner. * * Boundary rules (ARCHITECTURE.md §2): * - May import capability types, Provider identity types, cache, and * normalized errors. * - Must NOT import a concrete Provider Adapter, UTCP, or `mmx-cli`. * - Must NOT import command presentation or output mode modules. * * Order of operations for `executeSearch`: * 1. `capability.validate(request)` * 1b. Early return `[]` when the resolved count is `<= 0` (#230) — * before cache identity, read, legacy read-through, invoke, * consumption, and cache write. * 2. `capability.cacheIdentity(request, { legacyCount, count })` * 3. Read the provider-partitioned cache key * 4. Try and decode Adapter-supplied legacy candidates when applicable * 5. Invoke through `executeProviderOperation` (search also passes * the optional count; only Bocha consumes it, #211) * 6. Retry only normalized retryable failures * 7. Cache the full normalized result * 8. Apply local count truncation */ import type { SearchCapability, SearchRequest, SearchSource } from "../capabilities/search.js"; import type { RepositoryOperation, RepositoryOperationKind } from "../capabilities/repository.js"; import type { ReaderFetchRequest, ReaderFetchResult, ReaderOperation, ReaderOperationKind } from "../capabilities/reader.js"; import { type ResponseCache } from "./cache.js"; import type { ProviderId } from "../providers/types.js"; import type { ConsumptionSink, ConsumptionContext } from "./consumption.js"; /** * Retry policy for a single Provider operation. Defaults are applied * per-operation from {@link defaultRetryPolicy}. */ export interface RetryPolicy { maxRetries: number; baseDelayMs: number; maxDelayMs: number; jitterMs: number; } /** * Dependencies shared execution requires. `sleep` and `random` are * injected so retry backoff is deterministic under test. * * `consume` (PB-T2) is an OPTIONAL consumption sink. When present and * a billable wrapper supplies a {@link ConsumptionContext}, the retry * loop emits one event per `invoke()` attempt through this sink. When * absent (the default — and what every existing test passes), no event * is emitted and behavior is byte-for-byte identical to pre-PB-T2. * The sink is awaited; its failure is converted to a warning inside * the sink and never reaches the retry classifier. */ export interface ExecutionDependencies { readonly cache: ResponseCache; readonly sleep: (ms: number) => Promise; readonly random: () => number; readonly consume?: ConsumptionSink; readonly now?: () => number; } /** * Provider operations that share a single retry policy table. * * The four legacy values identify Provider Capabilities that have a * 1:1 Capability-to-operation mapping (`"search"`, `"vision"`, ...) * The three repository operations are composed from the P6-02 * {@link RepositoryOperationKind} source of truth so a future change * to that union (e.g. adding a fourth repository kind) propagates * here without further manual upkeep. The reader operation is * composed from the Reader Migration {@link ReaderOperationKind} * source of truth for the same reason. */ export type ProviderOperation = "search" | "vision" | "quota" | "diagnostics" | RepositoryOperationKind | ReaderOperationKind | "crawl" | "map" | "research" | "science-search" | "science-get"; /** * Default retry policy per operation. Search, quota, diagnostics, the * three repository operations, the reader operation, and the two science * operations allow one retry; Vision allows two to preserve shipped Z.AI * behaviour. Base delay 500 ms, max delay 8000 ms, jitter up to 250 ms. * * The repository, reader, and science operations inherit the existing * single-retry non-Vision policy (DESIGN.md §18) without altering the * behaviour of Search/Vision/Quota/Diagnostics; the new values are * routed through the same default branch as Search/Quota/Diagnostics. */ export declare function defaultRetryPolicy(operation: ProviderOperation): RetryPolicy; /** * Generic uncached retry wrapper. Performs no I/O of its own; the * caller supplies the invoke thunk and the cache strategy. Each * outward Provider operation has exactly one retry wrapper; Adapter * transport methods perform one attempt and never retry internally. * * PB-T2 — Consumption emission (the execution seam): * - When `dependencies.consume` AND `consumption` are both supplied, * one {@link ConsumptionEvent} is emitted **per `invoke()` call** * (success or failure), BEFORE the invoke runs. Wrappers that have * already returned on a cache hit never reach this loop, so cache * hits emit no event. `quota` / `diagnostics` call this function * directly with no `consumption` argument, so observational * handlers emit nothing. * - When either is absent (the default), zero events are emitted * and behavior is byte-for-byte identical to pre-PB-T2. * - The sink promise is awaited; its rejection is converted to a * warning inside the sink and never reaches the retry classifier. * * The 5th parameter is additive and optional; existing 4-arg callers * (Quota, Doctor, and every pre-PB-T2 test) compile and behave * unchanged. * * Cancellation (issue #47): a pre-aborted `signal` rejects BEFORE the * consumption emission and the invoke — a caller that is already gone * performs no Provider work, no consumption event, and no backoff. An * abort that lands mid-backoff unwinds the sleep immediately * (`abortableSleep`) instead of running it to completion. * * Provider retry hint: an error carrying a finite, non-negative * `retryAfterMs` (parsed by an Adapter from `Retry-After` / * `X-RateLimit-Retry-After`) raises the backoff to * `min(maxDelayMs, max(policy backoff + jitter, retryAfterMs))`. An * absent field leaves the sleep byte-identical to the policy value. * Only retryable-class errors reach this point, so a hint on a * terminal `QuotaError` is never consumed. */ export declare function executeProviderOperation(operation: ProviderOperation, invoke: () => Promise, dependencies: Pick, retryPolicy?: RetryPolicy, consumption?: ConsumptionContext, signal?: AbortSignal): Promise; /** * Execute a normalized Search through the shared pipeline: * validate → cache identity → new-key read → optional legacy * read-through → invoke with retry → cache the full result → apply * count truncation. * * Count stays a local concern for every Adapter except Bocha (#211). * The count is OFFERED to every Adapter through the cacheIdentity * compatibility options and the third invoke argument; only Bocha * consumes it (forwarding it to the wire and partitioning its cache * entries by it). Every other Adapter ignores both channels, so for * them the count enters neither the cache identity request nor the * Provider request — for Z.AI it reaches the Adapter only as * `legacyCount`, to reconstruct old keys. */ export declare function executeSearch(capability: SearchCapability, request: SearchRequest, options: { count?: number; noCache?: boolean; retryPolicy?: RetryPolicy; signal?: AbortSignal; }, dependencies: ExecutionDependencies): Promise; /** * Options for {@link executeRepositoryOperation}. `noCache` bypasses * the provider-partitioned cache and the legacy read-through cache * for both reads and writes; it never bypasses validation, identity, * invoke, or retry semantics. `retryPolicy` overrides the default * single-retry non-Vision policy from {@link defaultRetryPolicy}. */ export interface ExecuteRepositoryOptions { noCache?: boolean; retryPolicy?: RetryPolicy; /** * Cooperative-cancellation signal forwarded to `operation.invoke`. When * aborted, a supporting Adapter stops early. Unsupported operations * ignore it. */ signal?: AbortSignal; } /** * Generic cache + retry executor for every cacheable Repository * Capability operation (Search, Read File, Directory Listing). * * The observable order is fixed and exhaustively enumerated in * DESIGN.md §18: * * 1. `operation.validate(request)` — throws `ValidationError` * synchronously before any cache or Adapter work. * 2. `operation.cacheIdentity(request)` — Adapter computes the * provider-partitioned cache key and ordered legacy candidates * from the validated request and a single resolved credential. * 3. Read the provider-partitioned cache key and pass the raw value * through `operation.decodeCached`. A valid decode returns * immediately; `null` is a miss; malformed values never propagate * through a generic cast. * 4. For each legacy candidate, in declaration order, read the * legacy key and pass it through the candidate's `decode`. A * valid legacy hit is written through to the normalized key * (the legacy file is never changed or deleted) and returned. * A malformed legacy value is a miss. * 5. `operation.invoke(request)` is wrapped through * `executeProviderOperation` with the existing single-retry * non-Vision policy. Each retry creates a fresh Adapter * transport attempt; cache hits create no transport. * 6. The normalized result is written to the provider-partitioned * cache key. * * `--no-cache` skips steps 3, 4, and 6. It never skips validation * (1), identity (2), invoke (5), or retry semantics. * * `executeRepositoryOperation` imports no Z.AI, MiniMax, MCP, UTCP, * command-output, BFS, selection, or presentation module. */ export declare function executeRepositoryOperation(operation: RepositoryOperation, request: Request, options: ExecuteRepositoryOptions, dependencies: ExecutionDependencies): Promise; /** * Options for {@link executeReaderOperation}. `noCache` bypasses the * provider-partitioned cache and the legacy read-through cache for * both reads and writes; it never bypasses validation, identity, * invoke, or retry semantics. `retryPolicy` overrides the default * single-retry non-Vision policy from {@link defaultRetryPolicy}. */ export interface ExecuteReaderOptions { noCache?: boolean; retryPolicy?: RetryPolicy; /** * Cooperative-cancellation signal forwarded to `operation.invoke`. When * aborted, a supporting Adapter stops early. Unsupported operations * ignore it. */ signal?: AbortSignal; } /** * Generic cache + retry executor for the Reader Capability's * `reader-fetch` operation. Structurally identical to * {@link executeRepositoryOperation}; the two are duplicated rather * than shared because factoring them would widen this ticket's * scope (modifying repository.ts further). Future consolidation is * a separate refactor. * * The observable order is fixed: * * 1. `operation.validate(request)` — throws `ValidationError` * synchronously before any cache or Adapter work. * 2. `operation.cacheIdentity(request)` — Adapter computes the * provider-partitioned cache key and ordered legacy candidates * from the validated request and a single resolved credential. * 3. Read the provider-partitioned cache key and pass the raw * value through `operation.decodeCached`. A valid decode * returns immediately; `null` is a miss; malformed values * never propagate through a generic cast. * 4. For each legacy candidate, in declaration order, read the * legacy key and pass it through the candidate's `decode`. A * valid legacy hit is written through to the normalized key * (the legacy file is never changed or deleted) and returned. * A malformed legacy value is a miss. * 5. `operation.invoke(request)` is wrapped through * `executeProviderOperation` with the single-retry non-Vision * policy. Each retry creates a fresh Adapter transport attempt; * cache hits create no transport. * 6. The normalized result is written to the provider-partitioned * cache key. * * `--no-cache` skips steps 3, 4, and 6. It never skips validation * (1), identity (2), invoke (5), or retry semantics. * * `executeReaderOperation` imports no Z.AI, MiniMax, MCP, UTCP, * command-output, selection, or presentation module. */ export declare function executeReaderOperation(operation: ReaderOperation, request: ReaderFetchRequest, options: ExecuteReaderOptions, dependencies: ExecutionDependencies): Promise; /** * Identity used to read and write a provider-partitioned cache entry for * a generic {@link CachedOperation}. `credentialFingerprint` is the full * lowercase SHA-256 hex digest of the resolved credential; `request` is * the normalized Capability request. * * Unlike {@link ReaderCacheIdentity} and the repository identity, this * shape carries NO `operation` field and NO `legacyCandidates` — the new * capabilities (crawl, map, research) have no v0.2 legacy cache entries * to read through (tech-plan §4 / D2). */ export interface CacheIdentity { readonly provider: ProviderId; readonly capability: string; readonly credentialFingerprint: string; readonly request: Readonly; } /** * Generic cached operation descriptor for capabilities built on the * simplified execution wrapper (crawl, map, research). Same surface as * {@link RepositoryOperation} and {@link ReaderOperation} — `validate`, * `cacheIdentity`, `decodeCached`, `invoke` — minus the legacy-candidate * machinery. */ export interface CachedOperation { readonly kind: string; /** * Validate the request before any Provider access. Throws * `ValidationError` for missing required fields and * `UnsupportedOptionError` for Provider-specific options the Adapter * does not accept. */ validate(request: Request): void; /** * Build the cache identity for a request. Called only after `validate` * succeeds. */ cacheIdentity(request: Request): CacheIdentity; /** * Total decoder for cached normalized entries. Accepts `unknown`, * validates shape, returns the typed result or `null`. NEVER throws. */ decodeCached(value: unknown): Result | null; /** * Invoke the Provider and return the normalized result. The Adapter * closes its transport and never retries inside this method; shared * execution owns retry policy. * * `signal` is an OPTIONAL cooperative-cancellation channel. When a * caller threads an `AbortSignal` through `executeCachedOperation`, the * Adapter MAY observe it (e.g. in a long-running poll loop) to stop * early and release pending timers so the process can exit. Operations * that have nothing to abort (crawl, map) simply ignore it. */ invoke(request: Request, signal?: AbortSignal): Promise; } /** * Options for {@link executeCachedOperation}. `noCache` bypasses the * provider-partitioned cache for both reads and writes; it never bypasses * validation, identity, invoke, or retry semantics. `retryPolicy` * overrides the default policy from {@link defaultRetryPolicy}. */ export interface ExecuteCachedOptions { noCache?: boolean; retryPolicy?: RetryPolicy; /** * Cooperative-cancellation signal forwarded to `operation.invoke`. When * aborted, a supporting Adapter (currently research's poll loop) stops * early and releases pending timers so a CLI that has already timed out * can exit cleanly instead of staying alive on a lingering `setTimeout`. * Unsupported operations ignore it. */ signal?: AbortSignal; } /** * Simplified generic cache + retry executor for capabilities without * legacy cache entries (crawl, map, research). Mirrors the observable * ordering of {@link executeReaderOperation} but omits the * legacy-candidate loop (tech-plan §4 / D2). * * The observable order is fixed: * * 1. `operation.validate(request)` — throws `ValidationError` * synchronously before any cache or Adapter work. * 2. `operation.cacheIdentity(request)` — Adapter computes the * provider-partitioned cache key from the validated request and * a single resolved credential. * 3. Read the provider-partitioned cache key and pass the raw value * through `operation.decodeCached`. A valid decode returns * immediately; `null` is a miss; malformed values never propagate. * 4. `operation.invoke(request)` is wrapped through * `executeProviderOperation` with the default retry policy derived * from `identity.capability` (which is a valid `ProviderOperation` * member for every capability using this wrapper). Each retry * creates a fresh Adapter transport attempt; cache hits create no * transport. * 5. The normalized result is written to the provider-partitioned * cache key. * * `--no-cache` skips steps 3 and 5. It never skips validation (1), * identity (2), invoke (4), or retry semantics. */ export declare function executeCachedOperation(operation: CachedOperation, request: Request, options: ExecuteCachedOptions, dependencies: ExecutionDependencies): Promise; //# sourceMappingURL=execution.d.ts.map