UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

457 lines (456 loc) • 34 kB
import type { ToolCallRequest } from "./tool-policy.js"; /** * WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the model-facing * sentence's parenthetical and the `auto_mode.classified` trace frame's `cause`, so the three spell one * word (the deny's structural face carries only the round's FORM, `GateDisposition.cause`). `error` = the model leg * threw or rejected (a route failure at classify time reads here too — the derived-route pre-flight fell * back BEFORE any decide, so there is no separate word for it; so does a question that never ASSEMBLED, * whether its build threw or outran its cap — nothing was dispatched either way); `timeout` = a rung's * DISPATCH cap. */ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"]; export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number]; /** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */ export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause; /** * HOW a reply failed to carry a verdict — the closed set behind `parse_error.form`. `missing` = the reader found * NO answer (empty text, prose, an answer only inside a thinking block); `conflict` = it found TWO distinct * `<block>` answers (raw or thinking-stripped). The split decides who gets a SECOND ATTEMPT at the same * question, on both seats that offer one: a missing answer is asked for again (CC 2.1.259 `Vrr` re-asks an * unparseable reply) and handed on to the next candidate ({@link rungDidNotRule}); a conflict gets neither — * it carries a `yes`, and any second attempt that answered `no` would erase it. One rule, two seats: whatever * a re-ask may not overwrite, a later rung may not overwrite either. Not on any wire face; the trace frame and * the gate record keep the one word `parse_error`. */ export declare const AUTO_MODE_PARSE_FAILURES: readonly ["missing", "conflict"]; export type AutoModeParseFailure = (typeof AUTO_MODE_PARSE_FAILURES)[number]; /** Whether a value is a member of the parse-failure set. */ export declare function isAutoModeParseFailure(v: unknown): v is AutoModeParseFailure; /** * WHY a classification moved DOWN the candidate ladder — the closed set behind * {@link AutoModeClassifierRound}'s `fallback.cause`, i.e. the ways a candidate produces NO RULING * ({@link rungDidNotRule}). Two words are the unavailable causes verbatim (the leg threw / rejected; the * round-trip cap elapsed); `parse_error` is the candidate that ANSWERED with no answer in it at all (the * `missing` form). A ruling is not a member, and neither is a CONTRADICTORY reply (`conflict`): the ladder * stops at a rung that settled the classification, whichever way it settled it. */ export declare const AUTO_MODE_FALLBACK_CAUSES: readonly ["error", "timeout", "parse_error"]; export type AutoModeFallbackCause = (typeof AUTO_MODE_FALLBACK_CAUSES)[number]; /** Whether a value is a member of the fallback-cause set — the screen a row reader applies to a stored word. */ export declare function isAutoModeFallbackCause(v: unknown): v is AutoModeFallbackCause; /** * The deployment's answer to "what happens when the classifier seat does not rule?" * (`RunnerDeps.autoMode.classifierFallback`) — the closed set behind the candidate ladder's LENGTH: * `main` = [classifier seat, the run's main model]; `none` = [classifier seat]. */ export declare const AUTO_MODE_CLASSIFIER_FALLBACKS: readonly ["main", "none"]; export type AutoModeClassifierFallback = (typeof AUTO_MODE_CLASSIFIER_FALLBACKS)[number]; /** Whether a value is a member of the classifier-fallback set. */ export declare function isAutoModeClassifierFallback(v: unknown): v is AutoModeClassifierFallback; /** Read the knob: absent ⇒ the default `main`; a member ⇒ itself; ANYTHING ELSE ⇒ a loud refusal carrying * `config.auto_mode_classifier_fallback_invalid`. A typo'd knob must not read as the default: `"non"` would * keep paying for the rung it meant to turn off. */ export declare function autoModeClassifierFallbackOf(v: unknown): AutoModeClassifierFallback; /** Whether a present budget value is one the knob takes: a finite number of milliseconds in [1, 2147483647]. */ export declare function isAutoModeTimeoutMs(v: unknown): v is number; /** Read the per-level budget knob (`RunnerDeps.autoMode.timeoutMs` / `AutoModeDeciderOptions.timeoutMs`): * absent ⇒ {@link AUTO_MODE_DEFAULT_TIMEOUT_MS}; a finite number in [1, 2147483647] ⇒ itself; ANYTHING ELSE * (NaN, ±Infinity, `0`, a negative, a value a host timer would fire at once, a string, an object) ⇒ a loud * refusal carrying `config.auto_mode_timeout_invalid`. There is no OFF spelling: the budget bounds how long a * permission gate waits, and a gate with no bound is not a setting this knob offers. */ export declare function autoModeTimeoutMsOf(v: unknown): number; /** The classifier's verdict over one pending `ask`. Every arm may carry the classification's model * attribution ({@link AutoModeClassifierRound}), present iff the decider was wired with a candidate ladder. */ export type AutoModeVerdict = ({ kind: "allow"; } & AutoModeRoundAttributed) /** `category` is the matched BLOCK rule name (207 output contract); `reason` starts with `[Rule Name]`. */ | ({ kind: "block"; category: string; reason: string; } & AutoModeRoundAttributed) /** The classifier could not run: the model leg threw / rejected, or ran past the cap. NOT a decision — the * gate denies and says so ({@link classifierUnavailableDenyMessage}). */ | ({ kind: "unavailable"; cause: AutoModeUnavailableCause; } & AutoModeRoundAttributed) /** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC * parse-failure sentence ({@link classifierParseFailureDenyMessage}), counted by the denial limit. `form` * says WHICH way the reply failed ({@link AutoModeParseFailure}) and decides who may try the question a * second time: the classify leg re-asks a `missing` reply once and the ladder hands it to the next rung; * a `conflict` gets neither ({@link rungDidNotRule}). */ | ({ kind: "parse_error"; raw: string; form: AutoModeParseFailure; } & AutoModeRoundAttributed); /** The member every verdict arm shares: WHICH MODEL this classification asked and which one answered. */ export interface AutoModeRoundAttributed { /** Absent when the decider was given no candidate ladder (a hand-built decider, the cross-process rebuild's * single fresh leg): it ran on whatever model the leg is bound to and can vouch for no name. Never faked. */ readonly classifier?: AutoModeClassifierRound; } /** * WHICH MODEL answered one classification — the record a walk of the CANDIDATE LADDER * ({@link AutoModeDeciderOptions.candidates}) hands its consumers: `modelRequested` = the ladder's FIRST rung * (the seat the classification was for); `modelUsed` = the rung whose answer the gate acted on, and where NO * candidate ruled, the last rung ATTEMPTED (stated, not implied by absence); `fallback` = present exactly * when the ladder MOVED to reach `modelUsed` — which rung it left, which it arrived at, and why * ({@link AutoModeFallbackCause}); absent = the first rung answered. */ export interface AutoModeClassifierRound { readonly modelRequested: string; readonly modelUsed: string; readonly fallback?: { readonly from: string; readonly to: string; readonly cause: AutoModeFallbackCause; }; } /** * The word for "this classification was NEVER PUT TO A MODEL" — the failure is in assembling the QUESTION (the * transcript window every rung shares), not in a rung's answer. The decider raises it for its own * {@link AutoModeDeciderOptions.assemble} seat; a hook that owns assembly throws an error carrying this code. * * WHY IT EXISTS. The ladder advances on "the rung produced no ruling", and a hook that rejects looks exactly * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is * one shared promise, by contract — "the same prompt, re-asked" is literal), so without this word the walk * records a fallback and a deny sentence naming two models for a classification in which NO MODEL WAS EVER * DISPATCHED. The verdict is unchanged (fail-closed `unavailable`); what this changes is that the record stops * attributing it to models that were never asked. * * Carried as a `code` on the thrown error rather than a class so any classify hook — including one this * package never constructed — can raise it without importing a constructor. * * A build that OUTRUNS its cap is the same fact and takes the same exit — see * {@link AutoModeDeciderOptions.assemble}, the seat that bounds it. */ export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked"; /** Mint the marker above around whatever failed, keeping the original as `cause` for the host's own logs. * TOTAL by construction: the failure it wraps is an arbitrary value, and an `instanceof` or a `.message` read * on a revoked proxy (or a throwing getter) THROWS — which would replace the marker with an ordinary error and * hand the walk back the attribution this whole seat exists to remove. So the detail is a guarded read that * must already be a string, and the total describer otherwise. */ export declare function autoModeRoundNotAsked(reason: unknown): Error & { code: string; }; /** * Why a rung did not settle the classification, or `undefined` when it SETTLED it and the ladder stops — the * ONE predicate behind the advance (there is no `if (fallback)` arm anywhere). * * A rung settles the classification two ways: it RULES (`allow` / `block`), or it answers with a * CONTRADICTION (`parse_error` of form `conflict` — two distinct `<block>` answers in one reply). The second * is not a ruling and stays the fail-closed deny it is; what it is NOT is an unanswered question. It holds a * `yes`, so asking the SAME question again — of the same model as a re-ask, or of the next rung as a fallback * — can only overwrite that `yes` with a `no` and allow a call the classifier already objected to. One rule * for both second attempts: {@link AUTO_MODE_PARSE_FAILURES} names the form, the leg reads it for the re-ask * and this predicate reads it for the walk. Only a reply with NO answer in it (`missing`) moves the ladder. * * @contract auto_mode.ladder.advances_on_no_evidence — the candidate ladder advances on exactly one * condition, and it is the ABSENCE of evidence: the rung's leg threw or rejected, its round-trip cap elapsed, * or its reply carried no answer at all. A rung that RULED and a rung whose reply CONTRADICTED itself both * settle the classification where they stand — the contradiction as the fail-closed `parse_error` deny it * already is, attributed to that rung and disclosing no fallback — so no later candidate is ever asked a * question an earlier one has already answered `yes` to. */ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCause | undefined; /** * The model-facing deny text for an `unavailable` verdict — CC 2.1.250 `x1t`, with the parenthetical `EIt` * spells for the causes this engine distinguishes (a wall-clock / connection timeout reads " (timed out)"; * an unknown kind reads nothing). The subject is the classifier itself rather than a model id: without a * candidate ladder the decider cannot vouch for a name. One writer for every deny site. ROOT CAUSE, when * `attribution.fallback` is present: the sentence names BOTH rungs that failed to answer; a ladder that did * NOT move keeps the CC text byte for byte. */ export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string; /** * The text an ask carries to a person when the classifier did not rule on it ({@link rungDidNotRule}) and the leg has an * approval seat (the gate's classifier station and the inherited station): the ask's own text first — the card still * says what is being asked — then why auto mode is asking at all, read off the same cause the ask's `classifierNoRuling` * member carries (a client that does not read the member reads this). One writer for both stations. */ export declare function classifierNoRulingAskMessage(askText: string, cause: AutoModeFallbackCause): string; /** * The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate * this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict, * and auto mode blocks what it could not evaluate. With a candidate ladder wired the tail NAMES the model whose * reply carried no verdict (`attribution.modelUsed`) — a declared departure from upstream, whose card names no * model: the person reading this card is the one who can change the catalog, and on a single-model deployment * the name is the whole diagnosis. Without attribution the sentence is unchanged. The stem is a stable prefix * either way; one writer for both deny sites. */ export declare function classifierParseFailureDenyMessage(attribution?: AutoModeClassifierRound): string; /** * Parse the classifier's output contract — the CC 2.1.259 verdict reader (`MTt`, its thinking strip `QTt`, its * reason reader `TTt`; unchanged from 2.1.250 `wCe`/`xCe`/`yCe`), plus ONE arm of sema's own: * blocked: `<block>yes</block><category>Rule Name</category><reason>[Rule Name] …</reason>` * allowed: `<block>no</block>` * * The verdict is the set of `<block>yes|no` hits, NOT a prefix match: * 1. CONFLICT, read on the RAW text (thinking included): `<block>(yes|no)\b` collected over the whole * response — more than one distinct answer ⇒ no verdict (`parse_error`, form `conflict`). A scratch-space * "yes" contradicting a final "no" is a contradiction, not an allow. * 2. HITS, read on the text with thinking blocks removed ({@link stripThinking}): `<block>(yes|no)\b` with * the closing tag OPTIONAL, at ANY position. Two distinct answers ⇒ `conflict`; the one distinct answer ⇒ * `no` = allow, `yes` = block. * 3. BARE ANSWER (sema, ruled 2026-09-15 — small open models drop the tag; recorded as a divergence; a caller * that knows the reply is not a complete answer passes `bareAnswer: false` and this arm is closed): zero * hits AND the whole thinking-stripped text is an answer by {@link bareAnswerOf} ⇒ that word is the * verdict (a bare `yes` blocks with empty category/reason) — unless a `<block>` answer inside the * thinking disagrees with it (`conflict`). Zero hits and anything else — prose, a question with its * answer, a reply that only answered inside its thinking — ⇒ `parse_error`, form `missing`. * So a preamble before the tag, prose after `<block>no`, a missing `</block>` and a reply that repeats * its answer all read as the answer they carry; the tag must be spelled `<block>yes` / `<block>no` with * no whitespace inside (the word-boundary form — `<block>yesterday` is not a hit) and case does not matter. * * A stated BLOCK stands with or without its tail: `<category>` (the BLOCK rule NAME the prompt asked * for, read case-insensitively as free text — the deployment authors those names, so there is no fixed * vocabulary to screen them against) and `<reason>` (first `<reason>…</reason>` pair on the stripped * text, trimmed) are carried when present and `""` when absent; the consumers render their own fallback * line from the tool name in that case. Downgrading a stated block to `parse_error` would lose the * classifier's explicit verdict on the exact calls it flagged. * * `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.259's own outcome for a reply with no * verdict) that the denial limit counts like any other — never a silent allow, and never a question handed * to a person on the classifier's behalf. */ export declare function parseAutoModeResponse(text: string, options?: { bareAnswer?: boolean; }): AutoModeVerdict; /** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job * when it owns assembly; when using {@link buildAutoModePrompt} the decider passes its output). */ export interface AutoModeClassifyInput { /** The pending tool call — FINAL post-hook/post-rewrite args (the same args the gate adjudicated). */ req: ToolCallRequest; /** The human-readable text of the ask being decided (policy/safety message), when present. */ askMessage?: string; } /** One rung of the ladder, as the leg is told about it. `index` is the ADDRESS (a ladder may carry two rungs * whose model id is the same string), while `model` is the NAME the record will carry. */ export interface AutoModeLadderRung { readonly index: number; readonly model: string; } /** The pluggable model leg: given the classify input, the RUNG the ladder is on and whatever * {@link AutoModeAssembleFn} built for this classification, return that model's raw text response. * Deployment-injected; MAY throw / reject — the decider fail-closes. A rejection carrying * {@link AUTO_MODE_ROUND_NOT_ASKED_CODE} says the failure was in assembling the QUESTION rather than in this * rung's answer, and stops the walk instead of blaming the next rung. `rung` is `undefined` * for a decider wired with no ladder; `assembled` is `undefined` for one wired with no `assemble` seat (the * leg then owns assembly, inside this rung's cap). All trailing parameters are OPTIONAL, not `| undefined`: * a caller INVOKING it with one, two or three arguments must keep compiling. */ export type AutoModeClassifyFn<T = void> = (input: AutoModeClassifyInput, signal?: AbortSignal, rung?: AutoModeLadderRung, assembled?: T) => Promise<string>; /** The pluggable ASSEMBLY leg: build this classification's question ONCE, before any rung is asked, on its own * turn of {@link AutoModeDeciderOptions.timeoutMs}. The question is built from state that is not the model's * (a transcript window, a store read), so its cost is not a rung's: a build that fails or outruns the cap is * the {@link AUTO_MODE_ROUND_NOT_ASKED_CODE} fact (nothing dispatched, no rung blamed), and the rung cap * bounds a DISPATCH alone — which is what makes that cap's `timeout` word true. MAY throw / reject; the * decider fail-closes and does not start the walk. Its signal aborts when the run does AND at the cap. * Omitted ⇒ `assembled` reaches `classify` as `undefined`. */ export type AutoModeAssembleFn<T> = (input: AutoModeClassifyInput, signal?: AbortSignal) => Promise<T>; export interface AutoModeDeciderOptions<T = void> { classify: AutoModeClassifyFn<T>; /** Build the question ONCE per classification, before the walk, on its own turn of {@link timeoutMs} — see * {@link AutoModeAssembleFn}. Omitted = the leg assembles inside `classify`, inside the rung cap. */ assemble?: AutoModeAssembleFn<T>; /** * The CANDIDATE LADDER this decider walks, in order — the models a classification may be asked on, NOT a * second way to say what `classify` says (that hook is the DISPATCH; this is the sequence it dispatches * to). Rung 0 is the seat the classification is for; every later rung is consulted only when the one * before it produced NO RULING ({@link rungDidNotRule}). The FIRST ruling ends the walk and is the * verdict; when no rung rules, the LAST rung's own non-ruling verdict stands. * * Omitted = a one-rung anonymous ladder: the leg as wired, no attribution on the verdict. An EMPTY array, * or a rung that is not a non-empty string, is REFUSED loudly at construction (a deployment that computed * its ladder and got nothing has a bug, not a default). COST: {@link timeoutMs} bounds ONE rung's dispatch * and, once, the {@link assemble} call — worst case, the cap times the rungs walked, plus one. */ candidates?: readonly string[]; /** Hard cap on one classification round-trip — one LADDER RUNG's DISPATCH, not the whole classification * (see {@link candidates}) — and, separately, the one {@link assemble} call that precedes the walk (ONE * number: a second knob would be a second answer to "how long may a classification make the gate wait"). * Omitted ⇒ {@link AUTO_MODE_DEFAULT_TIMEOUT_MS}. A present value that is not a finite number of at least 1 * and at most 2147483647 (a host timer fires at once past that) is REFUSED at construction * (`config.auto_mode_timeout_invalid`, {@link autoModeTimeoutMsOf}) — never clamped, never read as the * default, and there is no OFF: a permission gate always has a bound. */ timeoutMs?: number; /** * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it waited * (`ms`, integer ≥ 0; a timed-out classification reads the deadline, one that walked two rungs the whole * walk). `cause` is present iff the verdict is `unavailable`; `classifier` iff a ladder was wired. A * throwing hook never breaks the gate. */ onClassified?: (info: AutoModeClassified) => void; } /** What {@link AutoModeDeciderOptions.onClassified} receives — one record per decision. */ export interface AutoModeClassified { /** The gated call the decision was about (`AutoModeClassifyInput.req.toolCallId`). */ toolCallId: string; /** Wall-clock milliseconds between the decide call and its verdict. */ ms: number; verdict: AutoModeVerdict["kind"]; cause?: Extract<AutoModeVerdict, { kind: "unavailable"; }>["cause"]; /** Present exactly when no model was asked because the QUESTION did not assemble: `"timeout"` = the build * outran its cap, `"failed"` = it threw (also what a classify hook owning its own assembly reports). ABSENT * = a model was asked. The verdict is `unavailable` / `error` either way — nothing was dispatched — so this * is what tells an operator "the build is slow" from "the provider is down". */ notAsked?: "failed" | "timeout"; /** The classification's model attribution, present exactly when the decider was wired with a candidate ladder * ({@link AutoModeClassifierRound}) — the same object the verdict carries. */ classifier?: AutoModeClassifierRound; } export interface AutoModeDecider { /** Never rejects. Any internal failure surfaces as `unavailable`/`parse_error` (fail-closed). */ decide(input: AutoModeClassifyInput, signal?: AbortSignal): Promise<AutoModeVerdict>; /** The per-level budget this decider holds each rung's dispatch (and the assembly) to, in ms — the resolved * {@link AutoModeDeciderOptions.timeoutMs}; what a notice about a timed-out classification names. Absent on a decider a * host implements by hand. */ readonly timeoutMs?: number; } /** The per-run decider: timeout + fail-closed error mapping, nothing carried between rounds. One instance per * run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the next, * which asks the model again. */ export declare function createAutoModeDecider<T = void>(opts: AutoModeDeciderOptions<T>): AutoModeDecider; /** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member * optional; an omitted member takes its CC default. A present member with a bad value is REFUSED * loudly at construction (never clamped, never read as the default). */ export interface AutoModeDenialLimitOptions { /** Consecutive classifier blocks (no classifier/human allow between them) that fall back to a person. * Default 3. A positive integer. */ maxConsecutive?: number; /** Total classifier blocks per run that fall back to a person and reset the budget. Default 20. A * positive integer. */ maxTotal?: number; /** The fallback ask's auto-deny window, ms. Default 120_000; `0` = no window (the ask waits on the * approver alone). A non-negative integer no greater than 2147483647 (a host timer truncates larger * delays and fires at once — the fail-closed direction, and invisible). */ autoDenyAfterMs?: number; } /** What BOTH shapes below say, and the whole of what a reader that only renders the card needs: the * counts that tripped the bound and which bound it was. Declared once so the two faces cannot drift. * A reader that only renders takes {@link DenialLimitFallbackFace} rather than this, so that an ARMED * object LITERAL is accepted too (a parameter of this type would meet the excess-property check). */ export interface DenialLimitCounts { /** Consecutive blocks INCLUDING the one that tripped the bound. */ readonly consecutive: number; /** Total blocks this run INCLUDING the one that tripped the bound (read before the total-bound reset). */ readonly total: number; /** Which bound tripped. */ readonly limit: "consecutive" | "total"; } /** * The DECISION-layer member of a denial-limit fallback ask (`PermissionResult` ask arm): the counts * that tripped the bound, and NOTHING about a window. A window is a fact of the ask's ROUTE — a live * function approver can count down, a durable park row and a headless deny cannot — and the route is * chosen downstream of the count. The type says so: there is no window member to write here, and the only * ways to reach a {@link DenialLimitFallback} (the member an {@link * import("./tool-policy.js").AskRequest} carries) are the two route stations, * {@link AutoModeDenialTracker.armTimedWindow} and {@link unarmedWindow}. */ export interface UnarmedDenialLimitFallback extends DenialLimitCounts { /** FORBIDDEN on a decision, and stated rather than merely omitted. Leaving the member out would only * stop a FRESH object literal (TypeScript's excess-property check); a policy that writes its window * through a `const`, an `as const`, or an annotated return type would still compile — and would then * have its number silently replaced at whichever route station the ask reached. `never` makes the old * shape a compile error at the policy boundary instead. (A host that casts past the type still meets * the route stations' runtime belt: they write the route's number, never the decision's.) */ readonly autoDenyAfterMs?: never; } /** The REQUEST-layer member (`AskRequest`, a durable park row, a checkpoint summary's echo): the same * counts with the route's window already decided. Reachable only through the two route stations — * {@link AutoModeDenialTracker.armTimedWindow} for a hand-out to a live function approver, and * {@link unarmedWindow} for every route that cannot count down (a durable park, a headless deny, a * non-function `onAsk` seat, a fallback no tracker of this run minted). Neither station accepts an * ALREADY-armed member: a window is decided once, at the route, and re-routing an armed one would be * a second decision with the first one silently discarded. */ export interface DenialLimitFallback extends DenialLimitCounts { /** The auto-deny window for THIS ask, ms; `0` = none. Non-zero only where a live function approver * is about to be called and the streak's timed card is still due — {@link * AutoModeDenialTracker.armTimedWindow} is its one writer. The ask resolver reads this member to * arm its deadline. */ readonly autoDenyAfterMs: number; } /** Either face of the member — what every READ-ONLY consumer accepts. A reader that renders the card * has no interest in the route, so it must take both, and as a UNION rather than as the shared base: * the base alone would reject a caller's four-member object literal on the excess-property check, * which is a break with nothing behind it (the routing invariant constrains where a window is WRITTEN, * never who may read one). */ export type DenialLimitFallbackFace = UnarmedDenialLimitFallback | DenialLimitFallback; /** * A stored or foreign value read back AS a fallback member, or `undefined` when it is not one: exactly * the four members, each of the right kind (finite non-negative counts, the closed `limit` word, a * finite non-negative window), nothing else carried. A deployment-written row (a checkpoint store is a * host's) can hold any shape; the faces that echo it (the inbox summary) read through here, so a bad * shape reads as ABSENT rather than reaching a consumer as a malformed card. Not a parser of intent — * `"3"` is not 3. */ export declare function readDenialLimitFallback(v: unknown): DenialLimitFallback | undefined; /** The window-less route's member, spelled in ONE place: every station that hands a fallback somewhere * nothing can count down (a durable park row, a headless deny, a string `onAsk` seat, a member this * run's tracker did not mint) passes through here rather than writing a `0` of its own. */ export declare function unarmedWindow(fallback: UnarmedDenialLimitFallback): DenialLimitFallback; export type DenialLimitVerdict = { limitReached: false; } | { limitReached: true; fallback: UnarmedDenialLimitFallback; }; /** The per-run denial tracker. Lives beside {@link AutoModeDecider} (same owner, same lifetime). */ export interface AutoModeDenialTracker { /** A classifier `block`: count first, judge second (CC `eme` → `tme`). Returns whether THIS block is * the one that falls back to a person, with the fallback's counts. There is no window here — the * route is not known yet, and the minted member has no member to hold one; see {@link armTimedWindow}. */ recordBlock(): DenialLimitVerdict; /** The ONE writer of the timed card (CC `u8t`: the timed shape exists only when a dialog is present, * the CONSECUTIVE bound tripped, and no timed card was shown this streak). Called by the station * that is about to hand `fallback` to a live function approver; returns the member that ask carries: * the configured window when the card is due, else the same counts with `0` ({@link unarmedWindow}). * Sets the once-per-streak mark and `windowArmedAt` exactly when it hands out a non-zero window. A * fallback this tracker did not mint (a deeper ancestor's, or a policy's self-declared member) comes * back with no window — the tracker arms only the streak it counted, and consults each minted member * ONCE (a second call with the same member arms nothing), so the hand-out is a recorded fact, not a * convention. */ armTimedWindow(fallback: UnarmedDenialLimitFallback): DenialLimitFallback; /** A classifier `allow`, a person's allow of a fallback ask (CC `yR`), or — in auto mode — an allow rule or the * read-only reader clearing the ask (the reference resets the streak when its pipeline answers allow): consecutive → 0 * and the timed-card mark cleared. A hook's allow does not call this. */ recordAllow(): void; /** The current counts (diagnostics / pins). `windowArmedAt` = the epoch-ms instant * {@link armTimedWindow} handed the CURRENT streak's fallback a non-zero window to a live approver * (the instant CC's `timedFallbackShown` mark was set); present ⇔ that mark stands, absent otherwise * — a fallback parked or denied headless WITHOUT reaching a function approver never arms it, a * total-bound card is never timed, a disarmed window (`autoDenyAfterMs: 0`) arms nothing, and every * reset (`recordAllow`, the total-bound budget reset) clears it. Present ⇒ the ask was handed to a * function approver carrying a window; the resolver arms its deadline from it unless it exits before * the wait (an already-aborted signal, unclonable args, or an approver that answers "unavailable" and * sends the ask to the park) — the card counts as shown in those cases too, which errs toward a * person being asked without a countdown, never toward an auto-deny nobody was shown. */ snapshot(): { consecutive: number; total: number; timedFallbackShown: boolean; windowArmedAt?: number; }; } export declare function autoModeDenialLimitOf(v: unknown): Required<AutoModeDenialLimitOptions>; /** The deployment face's three value knobs, read by their one readers — every leg's prepare runs it while * `RunnerDeps.autoMode` is present, whatever mode the leg starts in, so a bad knob refuses the run before a steer or a * parent's live ceiling can reach an arming that would have refused it. */ export declare function screenAutoModeKnobs(face: { readonly timeoutMs?: unknown; readonly classifierFallback?: unknown; readonly denialLimit?: unknown; }): void; /** Build the per-run tracker over {@link autoModeDenialLimitOf}'s bounds (a bad knob refuses there). */ export declare function createAutoModeDenialTracker(opts?: AutoModeDenialLimitOptions): AutoModeDenialTracker; /** The fallback ask's text — CC's sentence family, single-sourced for every mint site (the main gate * and the inherited-lane arms): `Classifier denial limit exceeded, falling back to prompting: <limit * sentence>` + a blank line + `Latest blocked action: <the classifier's own reason, or the tool name>`. * The reason is the classifier MODEL's text — the caller neutralizes it before it gets here. */ export declare function denialLimitFallbackMessage(fallback: DenialLimitFallbackFace, latestBlockedAction: string): string; /** The limit sentence alone (CC's two forms) — shared by the fallback ask and the headless terminal. */ export declare function denialLimitSentence(fallback: DenialLimitFallbackFace): string;