UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

79 lines (78 loc) • 6.75 kB
/** * The caller roster's ONE snapshot (#1167). A run's tool roster is the caller's `spec.tools` as it stands when prepare * starts reading the request — before its first await — and nothing later, down to each entry's members: the driver * rebinds onto a private copy of the spec whose `tools` is an own, fixed data property — a frozen array of frozen entry * FACES, each member read once (an omitted roster stays omitted, a value that is no array stays as read), never the * caller's accessor or an inherited one. The safety scan, the mount, the listings and every other phase read those faces, * so no phase can read the caller's objects a second time after a caller microtask had the chance to change them — * not the array (a push, a reassignment of `spec.tools`) and not an entry (a name, an effect or egress mark, a schema, * the `execute` function swapped in place, a getter or a Proxy answering differently on a later read). * * The snapshot holds no live entry. Every object entry (a function included) becomes a face: its own enumerable members * (string and symbol keys — what the mount's spread carries) plus every ToolSpec member name read through an ordinary * property read (how the phases read a declaration — non-enumerable, inherited or Proxy-answered included), each read once; * every declaration value is deep-copied and frozen by the one copier. A primitive is immutable and stays as read. A * read that throws refuses the run at the snapshot. Whether the ToolSpec arm can build the entry ({@link specArmCanConstruct}) * is asked of the entry once, there, and recorded on its face ({@link specArmCanBuild}); the mount refuses an entry it * cannot build from that record, so an `execute` that grows on the caller's object later is never read. The mount calls a * spec face's `execute` with the caller's own object as `this` ({@link callerEntryOrigin}). A `defineTool` product's face is * what every phase reads in its place, and the mount keeps the one authenticated rebind of the product object itself * ({@link rebindProductEntry}; the factory's seal refuses a product whose `execute` was replaced or which is not the object * it stamped): the factory rebuilds the product from its own members at the mount, and the run is refused when the rebuilt * product no longer answers as its face did. The tool a product mounts as is built from that face around the rebound * `execute`, never the rebuilt object itself (it shares list members and accessors with the caller's product): the * brand is asked once, here at the snapshot, and no object the caller can reach is ever a mounted tool. * * An array the caller changes after that instant is refused instead of run without the change: * {@link refuseLateRosterChanges}, called once at the end of prepare, re-reads the caller's `spec.tools` and refuses * the run with the declaration door's code (`config.tool_mount_denied`) naming every entry that is not, in order, the * object the snapshot read (a getter that hands back a fresh array of the same objects is not a change). A change to an * entry's members is not compared — the run never reads them again. A change after prepare returned is read by nothing. */ import type { AgentTool } from "../../internal/harness.js"; import type { TaskSpec } from "../types.js"; import type { ToolSpec } from "../tool-spec.js"; import { type ToolCtxEnricher } from "../tools.js"; declare const ROSTER_FACE: unique symbol; /** A roster entry as the snapshot minted it. The brand is private to this module and {@link rosterFaceOf} is the only * function that returns one, so a phase that takes `RosterFace`s cannot be handed a live caller object: code that tries * to put one into the roster does not compile. */ export type RosterFace = ToolSpec & { readonly [ROSTER_FACE]: true; }; /** A task spec whose tool roster is faces — what the driver and every phase that reads the roster hold after the snapshot. */ export type RosterSpec = Omit<TaskSpec, "tools"> & { tools?: RosterFace[]; }; /** What the refusal compares: the caller's own spec (re-read, never mutated here) and the objects the snapshot read * (`undefined` when the roster was not an array — an omitted roster snapshots as no entries, a non-array value is the * doors' to judge and is not compared). */ export interface CallerRosterSnapshot { readonly caller: TaskSpec; readonly handed: readonly unknown[] | undefined; } /** The object a snapshot face was read from (the entry itself when it was held as handed). */ export declare function callerEntryOrigin(entry: object): object; /** Can the mount's ToolSpec arm build this roster entry? Only the verdict recorded at the snapshot answers: an object that * is no face of this snapshot, a primitive, a product's face, is not built from. */ export declare function specArmCanBuild(entry: RosterFace): boolean; /** One entry of the roster, and the ONLY place a {@link RosterFace} is minted. Every object entry (a function included) becomes a frozen face, so no entry the snapshot holds is a * live object: a primitive is immutable and stays as read; a `defineTool` product's face names the product for its sealed * rebind; an entry the ToolSpec arm cannot build gets a face too, and the mount refuses it from the verdict recorded here — * an `execute` or a mark that grows on the caller's object afterwards is never read. A read that throws refuses the run. */ export declare function rosterFaceOf(t: unknown, index: number): RosterFace; /** The mount's arm for a roster entry that is the face of a `defineTool` product: the exact product object is rebound to * the run's ctx through the factory's sealed seat, compared with its face ({@link refuseProductDrift}), and the tool the * run mounts is built from the face around the rebound `execute` ({@link mountedProduct}). `undefined` when the entry is * not a product's face (the mount's ToolSpec arm takes it). */ export declare function rebindProductEntry(entry: object, enrich: ToolCtxEnricher): AgentTool | undefined; /** Take the snapshot and rebind onto a private spec copy carrying it. The copy keeps the caller's prototype and every own * property descriptor (an inherited or non-enumerable request field reads exactly as it did on the caller's spec); only * `tools` becomes the copy's own frozen array of faces when the request carried one. */ export declare function snapshotCallerRoster(spec: TaskSpec): { spec: RosterSpec; roster: CallerRosterSnapshot; }; /** Refuse the run when the caller's `spec.tools` no longer holds exactly the objects the snapshot read, in order. */ export declare function refuseLateRosterChanges(roster: CallerRosterSnapshot): void; export {};