@sema-agent/core
Version:
Stateless, task-oriented AI agent core
43 lines (42 loc) ⢠2.33 kB
TypeScript
/**
* `withRetry` (design/61 §9, core thin helper) ā the consumer side of the typed-retryable error contract
* (`remote-env.ts`). Retries a Result-returning op, but **only** on a whitelisted typed-retryable code, up to
* `maxAttempts`, with optional backoff. All remote adapters (E2B / SSH / ADB) reuse it so they don't each
* re-implement "retry by typed code + backoff" (and risk getting the contract wrong).
*
* š“ **Apply ONLY to IDEMPOTENT establishment ops** ā `connect` / `reconnect` (design/61 §2(A)). The error
* contract is "the caller decides retry per idempotency; the adapter MUST NOT blind-retry"; this helper IS that
* caller-side decision, scoped to the safe (idempotent) layer. **Never wrap a side-effecting operation** ā and
* never put a permanent code (`"auth_failed"`, `"unsupported"`) in `retryableCodes` (retrying a rejected key
* burns the budget / can lock the account).
*
* Determinism / fault injection (council §9 F): the `op(attempt)` closure is the injection point (a test
* scripts a per-attempt error sequence), and `opts.sleep` overrides the real backoff delay ā so
* "first attempt fails, second succeeds" is tested deterministically without a real device/network.
*/
export type RetryResult<T, E> = {
ok: true;
value: T;
} | {
ok: false;
error: E;
};
export interface RetryPolicy {
/** Error codes that warrant a retry (a typed-retryable whitelist). A code not in this list returns immediately. */
retryableCodes: readonly string[];
/** Total attempts, `>= 1`. (e.g. ADB first-auth = 2; SSH connect-drop = a few.) */
maxAttempts: number;
/** Backoff before attempt N (1-based; called with the attempt that just FAILED, so Nā„1). Default: no wait. */
backoffMs?: (failedAttempt: number) => number;
}
/**
* Run `op` until it succeeds, a non-retryable error surfaces, or `maxAttempts` is exhausted. `op` receives the
* 1-based attempt number. Returns the success, or the LAST failure (so an exhausted-but-still-failing op surfaces
* its final typed error to the caller).
*/
export declare function withRetry<T, E extends {
code: string;
}>(op: (attempt: number) => Promise<RetryResult<T, E>>, policy: RetryPolicy, opts?: {
sleep?: (ms: number) => Promise<void>;
signal?: AbortSignal;
}): Promise<RetryResult<T, E>>;