UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

177 lines (176 loc) • 16.4 kB
/** * 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 {};