UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

69 lines (68 loc) • 5.38 kB
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>;