@sema-agent/core
Version:
Stateless, task-oriented AI agent core
118 lines (117 loc) • 7.79 kB
TypeScript
/**
* 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;
};