@sema-agent/core
Version:
Stateless, task-oriented AI agent core
177 lines (176 loc) • 16.4 kB
TypeScript
/**
* THE judgment of every tool-name list one prepared task receives — the run's own selector lists (`TaskSpec.allowTools` /
* `TaskSpec.excludeTools`; when the delegation lane spawned it, its spawning run judged them where they were mounted, and the evidence
* it handed down spares them here), the definitions it carries (`TaskSpec.agents`), and the definitions and tool-level lists a mounted
* delegation tool carries (the `authored` faces — the engine's built-ins are not judged; only a tool a deployment mounted (depth 0)
* publishes them). One rule for both sides: a value that is not a list, an entry that is not a string, a spelling that can name no tool
* and a spelling that is another spelling of a tool the list's universe carries (`BASH` beside `Bash`) are REFUSED; an entry whose
* name no tool of the universe answers to (a server this deployment does not declare, a misspelt name) is KEPT as written — the
* selector reads every entry by name, so a denied one hides a tool of that name whenever one is mounted and an allowed one admits
* it — and told to the host, one account per entry. And one contradiction, judged on every leg: a denylist (the run's own or one
* of its ceiling) that reaches a tool the leg's kind relies on (the catalog's protocol column) is refused — neither a silently
* ignored deny nor a silently crippled leg.
*
* Two kinds of list, two universes. A ROOT-facing list bounds the tools of the run that holds it: the run's own lists (also the
* ceiling of every child), the policy name audit and the session-rules write gate ask the same question of it. Its universe is the
* run's roster before the valve, the caller's own tools, every tool the catalog describes (`rootFacingToolNames`) and the exact
* names of the pools its reachable delegation tools declare and can list — and another spelling of a carried name is always refused,
* whatever a delegation tool injects per spawn. A CHILD-facing list bounds a child that does not exist yet: a definition's two
* lists and a delegation tool's two lists. Its universe is the pool the delegation tool that hands the child declares (names, aliases, the
* declared `extraToolNames`, and the tool's own name: the child is handed the tool again, rebuilt, while its depth allows) and the catalog — not the
* spawning run's roster, which the child is not handed; every definition is judged against each reachable delegation tool's own pool; a delegation tool whose factory
* injects tools per spawn with no declared roster lists none of its pool, so there another spelling is kept and told (it may be one
* of that pool's own). Only the delegation tools the run's own selector leaves are read, in both: a tool it takes out hands nobody
* anything. A layer a run inherits (its ceiling) is not asked again: the run that wrote it asked its own universe, and a layer a
* checkpoint row restored is read for its spelling before the claim. The same goes for the lists a delegation tool hands its child
* (the child's own): when the spawning run read them against a pool it could list, its evidence spares them in the child. When it
* could not (the keep above), the child is the run whose roster can answer — it refuses another spelling of a tool of its own
* universe (its roster, the catalog, the pools it can list, and the delegation tool that spawned it, under its exact mounted name: a name
* of the child's lineage, mounted at its depth or not — an exact hit of that name is the tool's own, whatever else reads as a twin of it),
* and the spawning run already told what it kept, so nothing is told a second time.
*
* Throws `config.tool_face_control` for the run's own lists and the protocol contradiction, `config.agent.unknown_tool` for a
* definition's or a delegation tool's lists (`detail.reason`: `unknown_name` for a spelling no tool name can have, `near_miss` for
* another spelling of a tool of the universe) — synchronous, like the phase that calls it.
*/
import type { AgentTool } from "../../internal/harness.js";
import { valveLeaves } from "../../agents/tool-filter.js";
import { type ProtocolId } from "../protocol-table.js";
import type { ToolLegKind, ToolSelectorLayer } from "../tool-face.js";
import { type ConstraintChainEntry } from "../tool-policy.js";
import type { TaskSpec } from "../types.js";
import type { CheckpointErrorReason } from "../checkpoint-store.js";
import type { AnnounceOnceSink, RunInternals } from "./contracts.js";
/** What the delegation tools a run mounts hand their children (the pools of {@link delegationPoolReading}). */
interface DelegationPoolReading {
readonly tools: NonNullable<TaskSpec["tools"]>;
readonly names: Set<string>;
readonly complete: boolean;
}
/**
* THE judgment of the name a delegation tool is mounted under: the name it declares for itself (`createSubagentTool({ name })`, the default
* name when none is given), which the factory binds to the `execute` that carries the delegation out ({@link declaredDelegationNameOf}) —
* so a copy of the tool, a product of it, a stripped or shell copy all still say it. A delegation tool is ONE tool with ONE name: the lists
* of its agent definitions, the copy of it a child is handed, the rebuilt copy a run's own definitions produce and the prompt text it speaks
* all say the declared name, while the roster, the pool reading and the model's call say the mounted one. A mount whose `name` differs from
* the declared one makes the tool answer to two names — a denied list that names one of them is read against the other — so it is REFUSED,
* naming both names and the two ways to one name. This is the sentence of that refusal for the first such entry of the rosters given — or
* `undefined`; it never throws (an entry whose members cannot be read is not judged here: the roster snapshot refuses it). A tool the
* factory did not make, and a roster that is no list (the doors' to judge), are not read.
*/
export declare function renamedDelegationMountSentence(...rosters: unknown[]): string | undefined;
/**
* The refusal of a renamed delegation mount at prepare (`config.tool_face_control`, `detail.reason: "renamed_mount"`), asked of every roster
* the run holds: the one the caller mounted and the one the per-task rebuild (a function the caller's tool supplies) handed back. A resume
* asks the roster it is handed once more before the claim (this same function, through the resume screen's config-refusal channel), so the row stays pending.
*/
export declare function refuseRenamedDelegationMounts(...rosters: unknown[]): void;
/**
* THE reading of what the delegation tools a run mounts hand their children, read after the run's own tool valve: only a delegation
* tool the valve leaves (`leaves`, the valve's one reading — `valveLeaves`) counts — one it takes out hands nobody anything, and neither
* its pool names nor an unlistable pool of it belong to any universe. The tools themselves, every pool tool's name and alias (a declared
* `extraToolNames` roster included), and whether every one of those pools can be listed: a delegation tool whose factory injects tools
* per spawn with no declared roster has no listable pool (the static `tools` it was given included), so a name outside the listed ones
* may be one of its own. This is the one place the pool side of both universes is defined; the list judgment below and the policy name
* audit ask it.
*/
export declare function delegationPoolReading(callerTools: NonNullable<TaskSpec["tools"]>, leaves: ReturnType<typeof valveLeaves>): DelegationPoolReading;
/** The arms of this module's `config.tool_face_control` refusal that a resume carries before the claim as `detail.reason` of a `checkpoint.config_refused` — the same words
* ({@link CheckpointErrorReason}): a list the row recorded that is not a list / has a member that is not a string / has a spelling no tool name can have / is not in the
* spelling a row records, a record that is not one, a renamed delegation mount, an occupied table of declared names. */
export type PersistedToolFaceReason = Extract<CheckpointErrorReason, "not_a_list" | "not_a_string" | "unknown_name" | "unsupported_pattern" | "invalid_record" | "renamed_mount" | "registry_occupied">;
/**
* The names a definition's denied-tools entries stand for, for a lane that has neither a roster nor a notice sink at the point
* it builds its own policy (a workflow leg's agentType): each entry is read by the one reading, so `Tool(content)` denies the
* whole tool there too, a spelling no tool name can have is refused by the same refusal, and a name no tool answers to is
* returned as written (the policy denies a call of that name whenever one is made). The account of such a name needs the
* leg's roster and a sink; the leg's own policy-name audit is what reads them.
*/
export declare function deniedToolNamesOf(where: string, value: unknown): string[];
/**
* THE read-back of the tool names a checkpoint row carries, before the claim: the ceiling the resumed leg will run under when it is
* the row's (`ceiling`: absent when a live seat hands one) — each layer read by {@link readSelectorLayer} as a recorded layer — and
* the frozen policy chain (`inheritedGate.constraintChain`; each component's members {@link projectionToolNames} names), whose
* replay matches a name as recorded, which is the rule syntax's reading only for a name in its canonical spelling. The refusal
* sentence of the first entry neither reading may be applied to, naming it and where it sits, or `undefined`; a member that is not
* a list, or an entry that is not a string, is named by its type only. And the delegation tool the row records as having written the
* leg's own lists (`ownLists`, absent when a live seat hands one): it joins the universe those lists are asked of, so it must be a name.
* The refusal is its sentence and the arm it is ({@link CheckpointErrorReason}); {@link refusePersistedToolNames} throws it as prepare's own refusal.
*/
export declare function persistedToolNamesRefusal(ceiling: unknown, chain: readonly ConstraintChainEntry[] | undefined, ownLists?: unknown): {
readonly message: string;
readonly reason: PersistedToolFaceReason;
} | undefined;
/** {@link persistedToolNamesRefusal} as prepare's own refusal (`config.tool_face_control`, the arm as `detail.reason`): what a resume's pre-claim screen throws through the resume's one refusal channel. */
export declare function refusePersistedToolNames(ceiling: unknown, chain: readonly ConstraintChainEntry[] | undefined, ownLists?: unknown): void;
/**
* The upstream judgment of a run's own lists, when it holds: the evidence digest the spawning run handed down (live) or the resumed row
* recorded, returned only when it equals the digest of the lists this prepare read — the evidence is bound to the lists it was minted
* for, so a resume that hands different lists is judged again. The value returned is what this leg's rows record.
*/
export declare function boundToolListsJudgment(evidence: string | undefined, own: {
readonly allow: readonly string[] | undefined;
readonly exclude: readonly string[] | undefined;
}): string | undefined;
/**
* The record of the delegation tool that wrote a leg's own lists, held only for the lists it wrote: the live seat's (the delegation lane
* mints it with the digest of exactly the lists it handed the child) or, with none, the resumed row's. A prepare whose own lists digest to
* the record's `lists` keeps it — they are the lists that tool wrote, so their refusals name it and its name is a name of the leg's
* lineage; any other lists (a resume whose host handed another table) are the host's own `TaskSpec` seats, and the leg asks them as such
* with no lineage name cleared. A record that is not one (a host that set the live seat itself) refuses the leg, as a row's does before the
* claim: `config.tool_face_control`, `detail.reason: "invalid_record"`.
*/
export declare function boundOwnToolLists(record: unknown, own: {
readonly allow: readonly string[] | undefined;
readonly exclude: readonly string[] | undefined;
}): RunInternals["ownToolLists"];
export interface RunToolListsInput {
/** borrowed-readonly — the REBOUND spec: `agents` (the task's definitions), `tools` (the caller's tools and the mounted delegation tools) and `excludeAllTools` (the valve's whole-face switch, which takes the delegation tools out too). */
spec: Pick<TaskSpec, "agents" | "tools" | "excludeAllTools">;
/** borrowed-readonly — the assembled mount array BEFORE the selector's valve (names + aliases = the roster half of the universe). */
tools: ReadonlyArray<AgentTool>;
/** borrowed-readonly — the peers the task declares, per protocol of the protocol table (its MCP servers, its A2A peers): a namespaced
* spelling under one names a tool of it, whether or not the peer answered with any tool. */
declaredPeers: {
readonly [P in ProtocolId]: ReadonlyArray<{
readonly name: string;
}> | undefined;
};
/** borrowed-readonly — the run's own lists (the selector's own layer), as the doors froze them. */
own: {
readonly allow: readonly string[] | undefined;
readonly exclude: readonly string[] | undefined;
};
/** borrowed-readonly — every layer of the run's selector (the ceiling, then its own) and the kinds of leg it is. */
layers: readonly ToolSelectorLayer[];
legs: readonly ToolLegKind[];
/** True ⇔ the spawning run's prepare judged exactly these own lists where they were mounted, against a pool it could list (as the
* child-facing lists of the delegation tool or definition that wrote them) — the evidence digest it handed down (live, or recorded
* on the resumed row) equals the digest of the lists this prepare read ({@link boundToolListsJudgment}). They are then not judged
* again: neither told nor refused. */
judgedUpstream: boolean;
/** True ⇔ a delegation lane spawned this leg inside a prepared run, which read these own lists against a pool it could not list: it
* kept what it could not judge and told it (no evidence digest was handed down). They are judged here, against this leg's roster,
* and refused as any own list is — but nothing is told a second time. Absent ⇒ nobody told. */
toldUpstream?: boolean;
/** The delegation tool (and the agent definition) whose lists the run's own are, when the run that spawned it wrote them
* (`RunInternals.ownToolLists`); absent ⇒ the run's own `TaskSpec` seats. Its sentence is {@link delegationListWhere}; its tool name is
* a name of the leg's lineage, so the run's own lists are asked of a universe that carries it. */
ownBy: RunInternals["ownToolLists"];
/** borrowed-readonly — the once-per-session ledger whose operator sink carries the notices. */
onceLedger: Pick<AnnounceOnceSink, "onError">;
/** borrowed-readonly — the acquired session's id (operator-line context). */
sessionId: string;
}
export declare function judgeRunToolLists(input: RunToolListsInput): void;
/**
* THE judgment of a tool ceiling a HOST wrote (`RunWorkflowOptions.toolCeiling`), asked of one workflow agent's spec before it launches.
* A ceiling a prepared run hands down was judged where it was written and is not asked again; one a host wrote was judged nowhere, and
* it bounds the tools of every agent that holds it — a root-facing list: it is read against the catalog, the agent's caller tools and the
* listable pools of the delegation tools the agent's own selector leaves (the layers above the host's — a launching run's —, the host's
* and the agent's own lists together: a tool they take out hands nobody anything), and another spelling of one of those is refused (`config.tool_face_control`,
* `detail.reason: "near_miss"`) rather than kept as a deny that hides nothing. A name no tool answers to stays kept (a tool of that
* name may be mounted later), unannounced: the host wrote no spec this leg could name the list of.
*/
export declare function judgeHostToolCeiling(layers: readonly ToolSelectorLayer[], agent: Pick<TaskSpec, "tools" | "allowTools" | "excludeTools" | "excludeAllTools">, above?: readonly ToolSelectorLayer[]): void;
export {};