@sema-agent/core
Version:
Stateless, task-oriented AI agent core
109 lines (108 loc) • 8.38 kB
TypeScript
/**
* The deployment seats a Runner may HOT-SWAP after construction — the vocabulary behind the ONE door,
* `Runner.swapDeps` (runtask.ts).
*
* WHY ONE DOOR. Each hot-swappable seat used to be a verb of its own (`swapModels` was the first), and a
* verb per seat restates the same three laws every time — validate the candidate BEFORE anything is
* written, write the deps object ONCE (a swap is atomic: a refused candidate leaves the current deps
* byte-identical), announce the change under a notice code — while the question "which seats may be
* swapped at all" was answered by the list of verbs, i.e. by nothing a compiler could see. With one door
* the laws are stated once, the swappable set is THIS closed list, and every seat carries its rows in
* the tables beside it: the compiler refuses a member without its rows ({@link AssertAllKeysHandled}),
* the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
* handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
*
* WHAT A SWAP MEANS — the natural snapshot, one sentence for every ROUTING seat: a running task reads the
* deps object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
* every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
* Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
* legs' lifetime, by design (the `swapModels` precedent).
*
* …WITH ONE SEAT OUTSIDE IT, and the boundary is the point rather than the exception: `contextSettings` is
* read LIVE by in-flight legs. The snapshot law protects the routing CARD — a request already in flight must
* reach the endpoint it was priced, credentialed and fenced for, and re-reading any of that mid-run buys
* nothing and breaks the request. A leg's usable WINDOW is not on that card: it is a setting about how much
* conversation may accumulate before the leg summarizes, the reference implementation keeps it off the model
* object for exactly this reason and re-reads it on every request (clamped to the model's physical window),
* and an operator who lowers it is asking the legs running RIGHT NOW to compact sooner — which is the whole
* request. So the window declarations live on their own seat (`context-settings.ts`), keyed by catalog word,
* and the legs read them at every boundary, every recovery pass, the end-of-task summary and every request
* build. Routing stays snapshotted; geometry follows the operator. The two are not the same question, and
* one law over both could only be wrong about one of them.
*
* KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
* CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
* no read face, `tiers: undefined` ⇒ no tier bindings); a key absent is KEPT. `Object.hasOwn`, never
* truthiness, so the two spellings cannot be confused. An EMPTY candidate is refused: a swap that names
* no seat is a caller defect, not a no-op.
*/
import type { AssertAllKeysHandled } from "./ask-origin.js";
import type { RunnerDeps } from "./runner-deps.js";
/**
* The closed set of hot-swappable deployment seats.
* - `models` — the model catalog generation (the same-name redirect, re-pricing and retirement rules
* of the former `swapModels`: a string ref resolves against the new catalog at the next
* prepare; a `Model` object a leg already holds is untouched).
* - `tiers` — the tier bindings, a BINDING OVER a catalog: it may only be swapped together with
* `models` (the door refuses `tiers` alone — re-expanding bindings over an already
* expanded catalog would keep the retired tier words as ordinary keys, which is exactly
* the stale-generation shape the swap exists to end). Omitted beside `models` ⇒ the
* current bindings are re-applied over the new catalog; present (including `undefined`)
* ⇒ replaced.
* - `readFace` — the deployment's read-face declaration ({@link RunnerDeps.readFace}); screened by the
* same value gate every prepare applies (`assertContainmentFaceValue`: `"open"` | `"roots"`
* | absent, anything else refused loudly). The RESOLVED face of a leg is still the
* resolver's business (task seat, governance, the read-only mount); the swap changes the
* deployment seat that resolution reads.
* - `writeFace` — the deployment's write-face declaration ({@link RunnerDeps.writeFace}); the read seat's
* twin (same value gate, same in-flight snapshot rule, its own notice) and independent of it.
* - `contextSettings` — the per-model context-window overlay ({@link RunnerDeps.contextSettings}); screened
* by the same value gate construction applies (`assertContextSettingsValue`). THE ONE SEAT
* IN-FLIGHT LEGS RE-READ — see the header: it carries geometry, not route.
*/
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace", "contextSettings"];
export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
/** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
/** Whether a key names a swappable seat. */
export declare function isSwappableDepSeat(k: unknown): k is SwappableDepSeat;
/**
* Which notice a seat's successful swap is announced under — the disposition table over the set (a seat
* with no row does not compile: {@link SwapNoticeTableCoversEverySeat}). `models` and `tiers` share the
* catalog-generation line (one swap, one line, both counts); each containment face has its own.
*/
export declare const SWAP_SEAT_NOTICE: {
readonly models: "config.models_swapped";
readonly tiers: "config.models_swapped";
readonly readFace: "config.read_face_swapped";
readonly writeFace: "config.write_face_swapped";
readonly contextSettings: "config.context_settings_swapped";
};
/** The fence over the notice table: `never` while every seat has a row. */
export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
/** What the screen hands back: the seats present (set order) and the ONE read of each present seat's value,
* copied at screen time — the door writes from THIS object and never re-reads the caller's. */
export interface ScreenedSwappableDeps {
readonly seats: readonly SwappableDepSeat[];
/** A fresh plain object: the screened value of every present seat (`models`/`tiers` shallow-copied,
* `readFace`/`writeFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
* tell "clear this seat" from "not named"). */
readonly candidate: SwappableDeps;
}
/**
* Screen a swap candidate BEFORE anything is written: the object shape, the key set (every own key must
* be a member of {@link SWAPPABLE_DEP_SEATS}; at least one), the `tiers`-rides-with-`models` rule, and
* each present seat's own value gate. Returns the seats present (set order) and a SNAPSHOT of their values
* — the one read. Throws on the first defect; the caller has written nothing yet, so the current deps stay
* in force byte-identical.
*
* ONE READ, BY CONSTRUCTION: every seat value is read off the caller's object exactly once, here, and the
* screened copy is what the door writes and announces. A getter-backed or concurrently-mutated candidate
* therefore cannot present a legal value to the screen and a different one to the write (or to the
* announcement, which runs AFTER the write — a throw there would have left a half-announced swap in
* force). The prepare doors read their deps seats the same way.
*
* The per-seat gate is the SAME one the seat's prepare-time reader applies (both face seats ⇒
* `assertContainmentFaceValue`), so a value the door admits is a value every later prepare admits.
*/
export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;