@sema-agent/core
Version:
Stateless, task-oriented AI agent core
79 lines (78 loc) • 6.75 kB
TypeScript
/**
* 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 {};