UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

109 lines (108 loc) • 8.38 kB
/** * 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;