UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

118 lines (117 loc) • 7.79 kB
/** * The run loop's CLOCK and LIMIT machinery — the pieces that turn a budget, a deadline or a ceiling into * something a lane can act on, shared by the turn boundary, the run body and the end-of-task lane: * · the limit-approach frames (budget-measured, advisory) and the turn-cap read; * · the governance-read budget against the environment's suspend deadline, the chunked absolute-deadline * race that enforces it, and the never-abandoned ledger charge with its slow-settle disclosure; * · the typed terminal for an external stop cause that could not be suspended durably; * · the once-per-task disclosure of an auto-compaction trigger that cannot be evaluated (a WeakSet keyed * on the task's `Prepared`, so the latch dies with the task and never touches the contract surface); * · the hard walltime backstop timer, chunked past the largest delay a timer honours. * * Every clock read is `Date.now()` or a timer; every host read is through the arguments a caller hands in. * Layer-1 machinery rather than a driven lane — initial-run-state.ts says why no run-loop machinery module * wears a family prefix. */ import { type AgentHarness } from "../../internal/harness.js"; import type { PlatformLimitReason } from "../checkpoint-store.js"; import type { TaskSpec } from "../types.js"; import type { Prepared } from "./contracts.js"; /** * The LIMIT-APPROACH frames, the successor to the retired deadline-nudge family. Two facts * make them a different mechanism, not a rename: * - they are measured in BUDGET (the tightest armed axis's fill ratio), not in wall-clock, so they need * no throughput estimate and cannot mis-fire on a starved event loop; * - they are ADVISORY. Neither frame stops anything; the run ends when a ceiling is actually crossed. * * Rendered by {@link limitApproachFrames}; registered in the prompt-assembly event registry under * `limit_approach_converge` / `limit_approach_deliver`. */ export declare const LIMIT_APPROACH_DEFAULT_THRESHOLDS: readonly [number, number]; /** The output-token figure the pre-call worst-case assumes when the model declares no cap of its own. * Shared by the $ and token arms so the two gates cannot disagree about the same hypothetical call. */ export declare const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096; /** The two frame bodies, indexed by how many have already been sent (0 → converge, 1 → deliver now). */ export declare function limitApproachFrames(index: 0 | 1, axis: string, percent: number, mark: string | undefined): string; /** * Resolve the effective turn cap. `undefined` = no turn cap; `0` (explicit) = unbounded. * * This is now a pure read of what the caller wrote. The two clauses it used to carry — * "a wall-clock bound was set, so drop the turn cap" and "neither was set, so apply the engine's * safety net" — are both gone: the axes no longer interact (each binds on its own), and there is no * engine-side net to apply. A task with no `maxTurns` has no turn ceiling. */ export declare function resolveMaxTurns(limits?: { maxTurns?: number; }): number | undefined; /** * How long past the environment's suspend deadline the turn-boundary lane will keep waiting for the * usage ledger's READ. * * The precedence ruling (usage_window > env_lifetime) means the ledger must be read BEFORE the env * cause can be adopted — you cannot rank a cause you have not read. But that read is store I/O with no * bound of its own, and past this instant every millisecond it spends is spent out of the margin that * exists to mint the env checkpoint. A stalled ledger must not cost a deployment the checkpoint it * could still have had, so the read runs against an ABSOLUTE deadline (`envLifetimeSuspendAt + this`) * and a run that gets no answer by then stops for the cause it can still prove. * * 5s against the 60s suspend margin: two orders of magnitude above any healthy ledger read (in-memory * and file stores answer in ms; a remote one in tens of ms), and it still leaves 55s — more than the * margin was sized for — to pause the workspace, mint the checkpoint and commit it. Not a knob: it is a * fraction of an engine constant, and a deployment that wants a wider band declares a shorter lifetime. * * ABSOLUTE, never a fresh per-boundary allowance: a boundary that finds the deadline one millisecond * away must not hand a stalled ledger the whole margin, and a boundary that is already overdue must get * LESS than the budget, not the same. {@link raceUntilDeadline} is what keeps that true for a deadline * of any distance, without an unrepresentable timer. */ export declare const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5000; export declare const GOVERNANCE_READ_STALLED: unique symbol; /** Await a ledger charge, firing `onSlow` ONCE if it has not settled after * {@link CHARGE_SETTLE_DISCLOSE_MS}. The charge itself is always awaited to completion. */ export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onSlow: () => void, discloseAfterMs?: number): Promise<T>; /** * Await `p` until the absolute epoch `deadline`, then give up with {@link GOVERNANCE_READ_STALLED}. * * Chunked, and deliberately so: the deadline is derived from a caller-declared environment lifetime, so * the distance to it can exceed {@link MAX_TIMER_DELAY_MS} — a single timer armed for that distance * would fire at once and abandon a perfectly healthy read. Each chunk re-reads the wall clock, so the * decision is always made against the deadline itself rather than against an allowance computed once. * * Exported for its own unit pin (the same posture as the brain-call guardrail primitive): the * behaviors below are clock behaviors, and only a virtual clock can assert them without spending the * wall-clock time they describe. Not re-exported from the package index. * * An already-overdue deadline still gets ONE zero-delay pass: a promise that is settled (or settles in a * microtask, which is every in-process store) must be allowed to win, because giving up on an answer we * already hold would be a fabricated stall. The second pass is what makes the loop terminate. */ export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>; /** * The TYPED terminal for an external stop cause that could NOT be suspended durably. * Carried on `rs.limits.platformTerminal` and adopted as the run's `threw`, so the caller reads the cause * on `TaskResult.errorCode` (`errorCodeOf` lifts `.code`) instead of the anonymous error an abort produces. * `retryAfterMs` rides the error object because a wait hint has no `TaskResult` seat — a host reads it from * the `RunnerDeps.onError` sink this is also delivered to. */ export declare function platformLimitTerminal(reason: PlatformLimitReason, retryAfterMs: number | undefined, moment?: "entry" | "turn_boundary" | "resume"): Error & { code: string; retryAfterMs?: number; }; export declare function discloseUnevaluableWindow(u: { estTokens: number; windowField: string; windowValue?: number; modelId?: string; }, prepared: Prepared, tracer: TaskSpec["tracer"], taskId: string, onError: ((err: unknown, context: { phase: "config"; sessionId: string; }) => void) | undefined): void; /** 1.296 件2a — report the hard-abort timer's lateness on `task.end` only above this bar: sub-5s * jitter is ordinary scheduling noise (GC pauses, sync work) and would only add trace lint. */ export declare const TIMER_LATENESS_REPORT_MS = 5000; export declare function startTimeout(harness: AgentHarness, abortController: AbortController, remainingMs?: number, softSuspendable?: boolean): { fired: boolean; latenessMs?: number; clear: () => void; };