@sema-agent/core
Version:
Stateless, task-oriented AI agent core
114 lines (113 loc) • 7.75 kB
TypeScript
import type { OracleIsolationVerdict } from "./oracle-isolation.js";
import type { SkillSpec } from "./types.js";
/**
* The terminal of {@link decideAutoPromote}.
* - `"auto_promote"` — the ONLY exit that skips human review. Reachable only when all four prior guards
* (tripwire / irreversible / statistics / concurrence) clear AND all three gates are present+passing.
* - `"candidate_only"` — surface the artifact as a candidate; the SOLE shortfall is the oracle isolation class
* being downgraded to `in_process_probe_only` (oracle-isolation.ts:34) with everything else passing.
* - `"needs_human"` — fail-closed default: escalate to a human (any guard tripped, any other gate failing).
*/
export type AutoPromoteTerminal = "auto_promote" | "candidate_only" | "needs_human";
/**
* Gate-2 handle (裁决净增③): the verdict the promoted artifact's OWN promotable write landed with, held in one place
* so the decision reads one verdict and never a loose boolean. (It used to carry the artifact's lineage id as the scope
* key of the retired per-skill narrowing; with that gone the lineage keys nothing the fold reads.)
*/
export interface PromotedRecord {
/**
* Gate-2 chokepoint result: the promotable write landed WITH a positive utility verdict (memory.ts:240),
* or it was absent/ungated. Only `"present_positive"` is gate-passing; `"absent_or_ungated"` ⇒ needs_human.
*/
gate2Verdict: "present_positive" | "absent_or_ungated";
}
/**
* Inputs to {@link decideAutoPromote}. Every field is a PRESENCE/boolean/set the profile has already computed —
* the core fold inspects no numeric threshold, no stats shape, no taxonomy. The caller (a profile-promotion job)
* has already RE-asserted Gate-1 (`assertOracleIsolation`), run the exec-gate, read the appended Gate-2 verdict,
* and armed the Gate-4 seam BEFORE building this struct; this function only READS those outputs.
*/
export interface AutoPromoteInputs {
/** Gate-1 verdict (oracle-isolation.ts:37); the caller re-asserted it before calling this function. */
oracle: OracleIsolationVerdict;
/** Gate-2 lineage-bound single handle (净增③). */
record: PromotedRecord;
/** Gate-4: the irreversibility tighten is in place (or the artifact touches no irreversible tool). */
gate4Armed: boolean;
/**
* §6 permanent human door (净增①): the artifact touches at least one irreversible TYPED tool. When true the
* decision is ALWAYS `needs_human`, even if everything else would pass — because G4 is pure in-process
* gate-PRESENCE (2-of-3 asymmetry) and `gate4Armed` alone ("the gate is wired") is not trustworthy for an
* irreversible action.
*/
touchesIrreversibleTool: boolean;
/**
* The profile's statistics RAN for this run (it produced a verdict). Core requires PRESENCE only — the
* statistical THRESHOLDS (train/val/test split, canary, min-samples, cost-per-truly-correct) are profile.
*/
statisticsPresent: boolean;
/**
* The KIND labels of the independent mechanical signals that concur. Core enforces `size >= 2` DISTINCT
* kinds (净增②) — feeding the SAME kind twice still yields size 1 (it's a Set keyed by kind). WHICH kinds
* count, and whether they are truly de-correlated, are profile concerns (§7 honest residual).
*/
concurringSignals: ReadonlySet<string>;
/** Tripwire result — ESCALATE-ONLY (§5 ③); when `escalate` it always wins and is never cleared. */
tripwire: TripwireResult;
}
/**
* The fail-closed PRESENCE AND-chain (design/79 §1 §2). Any missing/failing gate ⇒ NEVER `auto_promote`; it
* degrades to `needs_human` (or, for the single oracle-isolation-class shortfall, `candidate_only`). This
* function computes NO numeric threshold — only PRESENCE/boolean/set-size checks.
*
* **Guard order (load-bearing):**
* 1. `tripwire.escalate` → needs_human (tripwire always wins, never clears — §5 ③)
* 2. `touchesIrreversibleTool` → needs_human (§6 permanent human door — 净增①; even if all else passes)
* 3. `!statisticsPresent` → needs_human (statistics must have RAN — core-required; thresholds are profile)
* 4. `concurringSignals.size < 2` → needs_human (≥2 DISTINCT KIND mechanical signals concur — 净增②)
* 5. the three-gate present-passing check; if not all pass, the SOLE-shortfall arm:
* `in_process_probe_only` oracle (and every other gate passing) → `candidate_only`; otherwise needs_human.
* 6. all three gates present+passing (with guards clear) → `auto_promote` (the only auto exit).
*/
export declare function decideAutoPromote(i: AutoPromoteInputs): AutoPromoteTerminal;
/**
* The result of {@link deriveTripwire}: an ESCALATE-ONLY advisory. `escalate` can only ever be set to `true`
* by a scan; a clean scan contributes nothing and NEVER clears another scan's escalation. `reasons` carries
* the matched tokens / drifted symbols for observability.
*/
export interface TripwireResult {
escalate: boolean;
reasons: string[];
}
/**
* The CORE-FROZEN denylist floor (design/79 §3 净增④ / P0#4): a safety invariant the profile MAY NOT remove.
* `deriveTripwire`'s `profileTokens` may only UNION (add) to this — never subtract — resolving the tension
* between "the token set can rot (new exfil primitives appear)" and "constitution-minimal". These are RAW-BYTE
* substrings scanned against the artifact's raw bytes INCLUDING its markdown bytes (extractSymbols skips
* markdown, so the byte scan is what covers prose/fenced-code that names an egress/exec primitive).
*/
export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>;
/**
* Build a tripwire verdict over a promoted artifact's text. **ESCALATE-ONLY** (design/79 §3,守 design/76
* §5.1 r3): it can ONLY ever raise `escalate:true`; a clean artifact yields `{escalate:false, reasons:[]}` and
* NEVER clears anything — it contains nothing itself: the real protection is the out-of-process boundary and the
* permission gate at call time, not this advisory. Two mechanical scans, both escalate-only:
*
* 1. **symbol-mention drift** via {@link extractSymbols} (repo-map.ts). extractSymbols is a v1 heuristic:
* column-0 / source-only, it skips indented lines and is structurally INCAPABLE of being a containment
* proof (it can't enumerate everything the artifact can call). So it is used ONLY to ESCALATE — if a
* surfaced top-level symbol names a tool/capability that is NOT in `declared.allowedTools`, escalate.
* 2. **raw-byte denylist** over the artifact's RAW bytes INCLUDING markdown bytes — {@link FROZEN_DENYLIST_FLOOR}
* UNIONed with `profileTokens` (add-only). A substring hit → escalate, with the matched token in `reasons`.
*
* **Fail-closed:** if EITHER scan THROWS on real content, return `{escalate:true, ...}` — a scan failure must
* NOT silently become "no escalation". (This is disjoint from the composition-layer "tripwire DOWN ⇒ zero
* contribution, never block" case, which is the caller's concern, not a throw inside the scan.)
*
* @param artifactText the artifact's raw text (markdown body included).
* @param declared the artifact's declared tool list (the `SkillSpec` member the loader copies) — its `allowedTools`
* entries, read by the list reader as a rule for the tool each names (one it reads as nothing names none), are the
* symbol-drift baseline.
* @param profileTokens optional ADD-ONLY extra denylist tokens (UNIONed onto the frozen floor; cannot remove).
*/
export declare function deriveTripwire(artifactText: string, declared: Pick<SkillSpec, "allowedTools">, profileTokens?: ReadonlySet<string>): TripwireResult;