@sema-agent/core
Version:
Stateless, task-oriented AI agent core
69 lines (68 loc) • 5.38 kB
TypeScript
import { traceHookCrash } from "./gate-crash-trace.js";
import type { WriteProtectionVerdict } from "./write-protect.js";
/**
* design/77 §4: resolve `p`, but REJECT (so the caller catches and fails closed) if it hasn't settled within
* `ms` OR the `signal` aborts first (the task timed out / was cancelled — its real deadline). A rejection of
* `p` propagates with its true cause. Both `ms` and `signal` undefined ⇒ no deadline (await `p`) — the
* helper stays generic; every gate call site supplies a finite `ms` ({@link createGateDeadline}), so the
* no-deadline arm is unreachable from the gate. Mirrors `tool-policy.ts withTimeout`, but REJECTS on
* timeout (no bounded await here has a safe fallback VALUE — the gate's only safe action on an unsettled
* one is its own fail-closed arm). `label` names the wait in the rejection, so the arm that catches it can
* say which one elapsed.
*/
/** WHY a bounded wait ended with no answer, stated by the WAIT rather than read off an error's text:
* `timeout` — the bound elapsed; `aborted` — the task ended. Neither mark ⇒ the awaited work's own. */
export type GateWaitFailure = "timeout" | "aborted";
/** The mark above, read back off a rejection. `undefined` ⇒ the awaited work's own rejection. */
export declare function gateWaitFailureOf(err: unknown): GateWaitFailure | undefined;
export declare function withGateTimeout<T>(p: Promise<T>, ms: number | undefined, signal?: AbortSignal, label?: string): Promise<T>;
/**
* The deadline used when the caller supplies no `approvalTimeoutMs`. The wait must be bounded EVERYWHERE:
* the engine's own assembly never passes `approvalTimeoutMs`, and its only other bound is the task abort
* signal, which a task with no walltime limit and no external cancel never fires — so a host driving the
* public `runToolGate` with neither knob had NO bound at all and one hung probe froze the call forever. A
* probe is an args inspection and a judgment is a path resolution, not an approval wait: 30s is generous
* for any legitimate one, and an elapse simply takes the caller's own fail-closed arm.
*/
export declare const DEFAULT_GATE_AWAIT_TIMEOUT_MS = 30000;
/**
* The gate's ONE deadline seat, resolved LAZILY and at most once per gate pass: a pass that makes no
* bounded await must not sound about a seat it never read. A garbage value (NaN/Infinity/negative, or
* above setTimeout's 2^31-1 ceiling, which it would silently clamp to ~1ms — an instant fail-closed the
* deployment never asked for) is refused LOUDLY to the default rather than silently reinterpreted
* (bad-value loudness discipline; the same ceiling the approve-wait seat refuses).
*/
export declare function createGateDeadline(input: {
approvalTimeoutMs?: number;
}, notifier: Parameters<typeof traceHookCrash>[2]): () => number;
/** What ONE source of a judgment answered: its value, or its absence and why (it threw — synchronously or not — or did not
* answer before the judgment's signal ended the wait; `gateWaitFailureOf(cause)` says which wait ended it). */
export type SourceAnswer<T> = {
readonly ok: true;
readonly value: T;
} | {
readonly ok: false;
readonly cause: unknown;
};
/**
* THE one way a source enters a path judgment — an environment lookup (the real target, the link chain, a root's key) or a
* provider of rows (a deployment's guarded write rows): asked, bounded by the signal the judgment was handed (the gate's
* deadline), and SETTLED — never thrown. A source that fails or does not answer in time is absent on its own (its view does not
* vote, its root exempts nothing, its rows are held unplaced), and every other source's answer stands: no source's failure
* reaches the judgment as a whole. No signal (a host calling a judge directly) ⇒ bounded by {@link DEFAULT_GATE_AWAIT_TIMEOUT_MS}.
*/
export declare function fromSource<T>(ask: () => Promise<T>, signal: AbortSignal | undefined, label: string): Promise<SourceAnswer<T>>;
/**
* The write-protection judgment at both of its gate call sites. The gate's ONE deadline is handed DOWN as the judgment's
* signal, so it bounds each of the judgment's sources on its own ({@link fromSource}): a view that did not answer in time takes no
* part, an exemption root whose key did not come back exempts nothing, and a row a view already met stays met — the one
* absence law of every path table, per lookup. `judge` returns `null` synchronously for a call it does not cover (no deadline
* is armed, no seat read); `undefined` is returned then.
*
* The wait here is the LIVENESS net for a judgment that does not observe the signal it was handed, and it ends one turn of the
* event loop AFTER the deadline: a judgment whose every lookup races the signal (the engine's) has settled by then with what it
* found, so the net never stands in for its answer. A judgment that never came back, or threw, determined nothing about where
* the write lands, and the table it judges for has rows: `unresolvable` (the one absence law's target arm — refused), reported
* as a deployment callback failing.
*/
export declare function awaitWriteProtectionJudgment(judge: (signal: AbortSignal) => Promise<WriteProtectionVerdict> | null, deadlineMs: () => number, signal?: AbortSignal, onFailure?: (err: unknown) => void): Promise<WriteProtectionVerdict | undefined>;