@sema-agent/core
Version:
Stateless, task-oriented AI agent core
313 lines (312 loc) • 20.4 kB
TypeScript
/**
* The tool FACE vocabulary (design/388 §2.1–§2.2): the closed word sets and the small declaration
* shapes that describe a tool WITHOUT executing it — its display family, the parameter that names its
* path target, the parameter vocabulary a permission rule may speak about it, the render hints a shell
* may read, and the mount-condition tags the static catalog lists it under.
*
* Layer-0 vocabulary: nothing here reads an env, a deps object, or a tool instance. `ToolSpec`
* (tool-spec.ts) carries the declaration half of these shapes as optional fields; the static catalog
* (tool-catalog-entries.ts) lists every built-in's face; the effect roster (tool-roster.ts) reads the
* same shapes off the MOUNTED instance. One vocabulary, three readers, no second table.
*/
/** The display / dispatch family of a tool. INPUT to grouping and card pre-selection only — never an
* authorization source (a `"file-write"` family grants nothing; the path-confinable predicate reads
* {@link ToolPathTarget}, and the approval card is chosen by {@link ToolRenderHints.approvalCard}). */
export declare const TOOL_FAMILIES: readonly ["shell", "file-read", "file-write", "search", "web", "delegate", "plan", "scaffold", "memory", "protocol", "other"];
export type ToolFamily = (typeof TOOL_FAMILIES)[number];
/**
* The closed set of MOUNT-CONDITION tags a static catalog entry is listed under. The static half of the
* catalog carries an ARRAY (a tool can be mounted by more than one condition — TaskOutput rides both the
* hand band and the delegation surface); the effect roster records the ONE tag that actually fired.
*
* - `hands` — the hand band (an execution env is wired: `deps.executionEnv` / `executionEnvFactory`);
* - `backgroundTasks` — the background-task surface (`Monitor`);
* - `workflow` — the workflow tool surface (`Workflow`);
* - `delegation` — the union of the background-task and workflow surfaces (TaskOutput/TaskStop/
* AgentTranscript, SendMessage's first condition);
* - `hostInternals` — a host-internal seat is present (`RunInternals.parentNotify`);
* - `peerLane` — the peer-session lane is mounted;
* - `scheduler` — the env carries a scheduler daemon (the Cron family + ScheduleWakeup);
* - `memory` — an engine-memory / shared-memory session is wired;
* - `scenario` — mounted by a HOST scenario factory through `TaskSpec.tools` (core defines the tool,
* the host decides to mount it);
* - `always` — mounted on every task unless excluded (ReportBlocked/ReportFindings);
* - `optional` — mounted by a per-task opt-in or a wired seat (plan mode, an output schema, an
* offload store, a question face, a skills listing, an MCP configuration).
*/
export declare const TOOL_MOUNT_TAGS: readonly ["hands", "backgroundTasks", "workflow", "delegation", "hostInternals", "peerLane", "scheduler", "memory", "scenario", "always", "optional"];
export type ToolMountTag = (typeof TOOL_MOUNT_TAGS)[number];
import type { AssertAllKeysHandled } from "./ask-origin.js";
/** The closed set of specialised approval cards a shell may pre-select from a render hint. The hint is a
* PRE-FILTER: the shell owns each card's input schema and validates the call's full args before
* mounting the card (design/388 §2.5 boundary ①). */
export declare const TOOL_APPROVAL_CARDS: readonly ["file-edit", "file-write", "notebook-edit", "shell", "fetch", "plan", "question", "generic"];
export type ToolApprovalCard = (typeof TOOL_APPROVAL_CARDS)[number];
/** Membership in the closed set — the one screen a stored or declared card word passes before it is read. */
export declare function isToolApprovalCard(word: unknown): word is ToolApprovalCard;
/** The cards only an engine-defined tool owns: a caller or protocol tool cannot declare one, and the card is minted
* from the engine's own judgment of the call, never from a row's declaration. */
export declare const RESERVED_APPROVAL_CARDS: readonly ["question", "plan"];
export type ReservedApprovalCard = (typeof RESERVED_APPROVAL_CARDS)[number];
/** How a path-targeting tool touches its target. `read` never enters a write-confinement face. */
export declare const TOOL_PATH_ACCESSES: readonly ["read", "create", "edit"];
export type ToolPathAccess = (typeof TOOL_PATH_ACCESSES)[number];
/**
* The BASE a RELATIVE spelling in the target slot resolves against — the tool's OWN resolution base,
* DECLARED so every fence resolves the very path the tool will open instead of guessing one:
*
* - `cwd` — the live tracked working directory, falling back to the task root when no tracker moved
* (Read / Edit / Write / NotebookEdit: each hands `cwdRef.current` to `canonicalizeTarget` as its base);
* - `root` — the task root, always (Grep / Glob: their `path` describes say "relative to root", and the
* tools hand `rootCanonical` to the same resolver whatever the shell's `cd` did).
*
* Absent ⇒ `cwd`: the pre-declaration reading every fence took, kept as the floor for a face (a caller's,
* an MCP server's) that declares no base. This one word is the whole difference the fences had to model,
* and getting it wrong is SILENT — a rule about the path the tool really opens simply never fires.
*/
export declare const TOOL_PATH_BASES: readonly ["cwd", "root"];
export type ToolPathBase = (typeof TOOL_PATH_BASES)[number];
/**
* What the call targets when the target slot carries NO usable spelling:
*
* - `none` — nothing. A file tool with no path names no file, so no path rule speaks about the call;
* - `base` — the resolution base ITSELF. A search tool with no `path` searches the WHOLE base, so a rule
* naming the base (or anything containing it) does reach the call.
*
* Absent ⇒ `none` (the pre-declaration reading: an absent slot was "no resolvable path" for every tool).
*/
export declare const TOOL_PATH_ABSENCES: readonly ["none", "base"];
export type ToolPathAbsence = (typeof TOOL_PATH_ABSENCES)[number];
/**
* The parameter that names a tool's filesystem target, how the target is touched, and how a spelling in that
* slot RESOLVES — as a READER sees it, whichever side it came from. Present ⇔ the tool can be confirmed BY
* PATH (write confinement, sensitive-path guards, edited-file tracking, the transcript-directory gate, the
* permission-rule path family). `param` outranks every alias (NotebookEdit's `notebook_path` beats a decoy
* `file_path`). `skillScopeEligible` (default false) is a SEPARATE authorization domain (its one reader, the
* skill-scope write fence, is retired and no reader remains; the bit stays a declared member of the roster row) — a
* confinable write tool is not automatically eligible there.
*
* ONE shape, two spellings: a DECLARATION ({@link ToolPathTarget}, what a catalog row / a `ToolSpec` / an
* MCP overlay writes) and the roster row's NORMALIZED form (`aliases` always an array, `skillScopeEligible`
* always a boolean) that the wire carries and the gate stamps on a call. Both are readable through this
* type, which is what every fence reads — the shape used to be re-declared at each of them, and a member
* added to one spelling silently never reached the others.
*
* The three RESOLUTION members are read by one function (`effectivePathTargetOf`, effective-path-target.ts)
* that every fence and the tools themselves go through: the declaration is the single source, so a fence
* and the tool it fences cannot resolve one call to two different files.
*/
export interface ToolCallPathTarget {
param: string;
aliases?: readonly string[];
access: ToolPathAccess;
skillScopeEligible?: boolean;
/** How a RELATIVE spelling in `param` (or an alias) resolves. Absent ⇒ `cwd`. */
base?: ToolPathBase;
/** What the call targets when the slot is absent. Absent ⇒ `none`. */
absent?: ToolPathAbsence;
/**
* A top-level parameter whose ABSOLUTE glob pattern carries its own base directory (Glob's `pattern`:
* `/proj/secret/**` searches `/proj/secret` with no `path` at all). Consulted ONLY when the target slot
* is absent and `absent` is `base` — the tool's own precedence, in which an explicit `path` wins and a
* relative pattern leaves the base alone.
*/
patternParam?: string;
}
/** The DECLARATION spelling of {@link ToolCallPathTarget}: what a catalog row, a `ToolSpec` or an MCP
* overlay writes. `skillScopeEligible` is narrowed to `true` — a declaration says "eligible" or says
* nothing; "eligible: false" is the absence, and spelling it would be a second way to say one thing. */
export interface ToolPathTarget extends ToolCallPathTarget {
skillScopeEligible?: true;
}
/**
* The NORMALIZED spelling of {@link ToolCallPathTarget}: what the roster MINTS, what the wire carries, and
* what the gate stamps on a call. `aliases` and `skillScopeEligible` are always present because the mint
* fills them — a guarantee a consumer reading a stamped face has always had, and one the shared reader type
* deliberately does not weaken (an optional member here would break `face.pathTarget.aliases.includes(…)`
* at compile time for every downstream that already reads it).
*/
export interface ToolRosterPathTarget extends ToolCallPathTarget {
aliases: readonly string[];
skillScopeEligible: boolean;
}
/**
* The SLOT half of a target — which argument names the path — with the RESOLUTION half (`base` / `absent` /
* `patternParam`) dropped, so the reader falls back to the documented defaults.
*
* #635, from the adversarial round: the protective floor (design/388 B6 — a face that declares no target of
* its own is judged against the CATALOG's declaration for the name) rests on "naming a slot can only TIGHTEN
* a deny". Naming a slot does. Claiming how a FOREIGN tool of the same name RESOLVES that slot does not: a
* wrong claim moves the judged path to a different file, and the deny the floor exists to keep then misses.
* So the floor hands over the slot and keeps its mouth shut about the resolution.
*/
export declare function pathTargetSlotOnly(pt: ToolCallPathTarget): ToolCallPathTarget;
/**
* The parameter vocabulary a permission rule (`Tool(param:pattern)`) may name. DERIVED by default from
* the tool's schema: the SCALAR top-level parameters (string/number/integer/boolean and their enum/const/
* union forms) plus {@link ToolPathTarget.aliases}; object/array parameters never enter (a rule on them
* can never match — `matchParamRules` skips non-scalar values). A declaration can only NARROW the derived
* set; `primaryParams` defaults to the path target's `param` + aliases.
*/
export interface ToolRuleFace {
primaryParams?: readonly string[];
params?: readonly string[];
}
/** The roster's WIRE bounds on a tool name and a contract member (design/388 §2.5 / B20). Distinct from
* protocol-naming's `TOOL_NAME_MAX_CHARS` (64), the provider-facing bound the MCP/A2A namespacing helper
* budgets a MINTED name to — a minted name is always inside this wire bound by construction; a CALLER
* declaration is judged against it at prepare. */
export declare const TOOL_WIRE_NAME_MAX_CHARS = 128;
/** The roster's wire bound on a SHORT identity member: a contract's `contractId` / `implementationRevision`, and the
* `modelGate` class a definition carries. Bounded at the mint (`rosterMemberBoundProblem`), never truncated. */
export declare const TOOL_CONTRACT_MAX_CHARS = 64;
/** The roster's wire bound on a row's `cardId` — a COMPOSITE minted from bounded members, so its bound is DERIVED
* from theirs rather than declared beside them: the widest form is `<mcp|a2a>:<peer>/<tool>` with the peer and the
* protocol tool segment each inside the wire NAME bound (`builtin:<name>` and `<caller|external>:<contractId>` are
* narrower). A row minted from in-bound members therefore fits by construction; the mint's own check on it is a
* tripwire for a new source form, never a working rule. */
export declare const TOOL_CARD_ID_MAX_CHARS: number;
/** The roster's wire bound on a KEY — a schema property name, a path-target `param`/alias, a rule-face param. A key is a
* SEMANTIC name every fence reads an argument by, so it is never truncated: a declaration over the bound is refused at
* the door (caller) or dropped with a disclosed reason (MCP intake), and a row carries every key exactly. */
export declare const TOOL_KEY_MAX_CHARS = 256;
/** The roster's bound on how many top-level keys one row carries (the same refuse/drop treatment). */
export declare const TOOL_KEYS_MAX = 1024;
/** Bounds every render-hint string is held to on the roster (design/388 §2.2 ④). */
export declare const RENDER_HINT_MAX_CHARS = 64;
export declare const RENDER_HINT_ACTIVITY_MAX_CHARS = 32;
export declare const RENDER_HINT_MAX_LIST = 16;
/**
* Declarative, template-free display hints (design/388 §2.2 ④, D-4). UNTRUSTED display input on the
* wire: the engine length-caps them at the roster mint; the shell strips control / newline / bidi
* characters before rendering. `resultCards` may only name cards in the tool's OWN namespace
* (`caller:<contractId>/<type>`) — engine cards (`builtin:*`, `mcp`, the refusal cards) are minted by
* the engine alone and a declaration naming one is refused at prepare.
*/
export interface ToolRenderHints {
userFacingName?: string;
activity?: string;
summaryParams?: readonly string[];
resultCards?: readonly string[];
/** The card family this tool asks under; absent reads `generic`. `question` / `plan` are the engine's own tools' cards — a declaration of either is refused. */
approvalCard?: ToolApprovalCard;
}
/**
* The static FACE of a tool — the subset of `ToolSpec` a catalog entry declares and a definition module
* spreads into its spec. Everything here is readable without an env or a deps object; the effect roster
* reads the same members off the mounted instance.
*/
export interface ToolFaceDeclaration {
family: ToolFamily;
effect?: import("./tool-spec.js").ToolEffect;
egress?: true;
irreversibility?: "never" | "maybe" | "always";
contentOrigin?: import("./tool-spec.js").ToolContentOrigin;
contract?: {
contractId: string;
implementationRevision: string;
};
aliases?: string[];
pathTarget?: ToolPathTarget;
/**
* The rule face ({@link ToolRuleFace}) and, for a definition whose primary parameter is a shell command line, the grammar
* that judges it — `grammar: "command"`, the SHELL FACE. Every `Bash(<command>)` rule (the command grammar's one spelling,
* deny / ask / allow, from every source) is judged on a call that answers to the name of such a definition, against that
* parameter; a bare `Bash` rule stays a rule on the tool of that name. Only a catalog row declares it (a caller's
* `ToolSpec.ruleFace` is a {@link ToolRuleFace}): the static catalog is the one place the shell face is read from.
*/
ruleFace?: ToolRuleFace & {
readonly grammar?: "command";
};
renderHints?: ToolRenderHints;
offload?: boolean;
offloadThresholdChars?: number;
compactable?: true;
budgetExempt?: true;
}
/**
* The kinds of leg a run can be, as far as which tools a leg itself relies on: a delegated child (every lane a delegation
* tool, a workflow or a team spawns), a NAMED delegated child (a teammate), an iteration of a goal loop, an observer sidecar,
* a run with an output schema (it finishes by submitting through the structured-output tool). A catalog row's
* {@link ToolCatalogEntry.protocol} names the kinds whose legs carry that tool as part of their protocol.
*/
export declare const TOOL_LEG_KINDS: readonly ["delegated", "teammate", "goal", "observer", "structured"];
export type ToolLegKind = (typeof TOOL_LEG_KINDS)[number];
/**
* One layer of a tool selector, as data: an allowed-tools list (absent = no allowlist on this layer) and a denied-tools list,
* each a frozen copy of the entries a list was written with. A run's own layer is its `allowTools` and `excludeTools`; the
* layers above it are the ones the run that spawned it was bounded by, plus that run's own.
*/
export interface ToolSelectorLayer {
readonly allow?: readonly string[];
readonly deny: readonly string[];
}
/**
* The compiled tool selector of one run — the one answer to "is this tool available on this run", read at the roster valve
* and on every later roster change (an MCP refresh or re-dial, the deferred-search lane), and handed down as the ceiling of
* every run this one spawns (`layers`). A tool is admitted when it is a protocol tool of this run's leg kind, or when every
* layer admits it: the layer's allowlist (if any) reaches it and its denylist does not.
*/
export interface ToolSelector {
readonly layers: readonly ToolSelectorLayer[];
admits(tool: {
readonly name: string;
readonly aliases?: readonly string[];
}): boolean;
/** Can ANY tool of the protocol peer whose minted prefix is `prefix` (`mcp__srv__`) be admitted — read off the lists alone,
* never off what the peer lists now, so a tool the peer lists later is judged by the same answer. */
admitsPeer(prefix: string): boolean;
}
/**
* One row of the static tool catalog (design/388 §2.1 "静态目录"). Keyed by DEFINITION identity, not by
* name: two definitions may share a wire name (the full and the read-only shell both answer to `Bash`),
* and every catalog predicate counts a name as a member when ANY entry under it matches.
*/
export interface ToolCatalogEntry {
/** Definition identity — unique across the catalog (`bash`, `bash-readonly`). */
id: string;
/** Wire name; may repeat across entries. */
name: string;
/** `builtin` = a core definition mounted by core or by a host factory; `synthetic` = core mints it
* per task with a per-task shape (the structured-output tool). */
source: "builtin" | "synthetic";
/** Where the definition lives (repo-relative), so a catalog gate can name the file. */
definedIn: string;
/** The conditions under which core (or a host scenario) mounts this definition. */
mountedBy: readonly ToolMountTag[];
/** The `details.type` cards this definition mints on `tool_end.structured`. */
cards: readonly string[];
/** The declared face — spread into the definition's `ToolSpec` by the module that owns it. */
face: ToolFaceDeclaration;
/** A `modelGate` class the default-bundle assembler stamps on this definition's default arm. */
modelGate?: string;
/** The leg kinds whose legs rely on this tool as part of their protocol: on such a leg the tool's availability is not a
* selector's to decide (an allowlist need not name it), and a denylist that reaches it there is a contradiction the run
* refuses at prepare. */
protocol?: readonly ToolLegKind[];
/** A protocol row the leg shares with the run that spawned it (the team task-list family a teammate shares with its leader): on its
* leg the leg's OWN lists do not decide it, but the ceiling still does — the leg holds it exactly when its spawner may. */
protocolUnderCeiling?: true;
}
/**
* The band-membership table over the mount tags: a host derives its hand band as the definitions listed under a
* TRUE row (server B-1). Every tag has a row — the fence reds `tsc` when a tag is added without one. The
* background-task surface is hands-derived (`backgroundTaskToolsActive` requires the shell), so a definition
* mounted by it alone is a band member that is NOT dual-use.
*/
export declare const MOUNT_TAG_IN_HANDS_BAND: {
readonly hands: true;
readonly backgroundTasks: true;
readonly workflow: false;
readonly delegation: true;
readonly hostInternals: true;
readonly peerLane: true;
readonly scheduler: false;
readonly memory: false;
readonly scenario: false;
readonly always: false;
readonly optional: false;
};
export type HandsBandTableCoversEveryMountTag = AssertAllKeysHandled<Exclude<ToolMountTag, keyof typeof MOUNT_TAG_IN_HANDS_BAND>>;
/** The tags whose definitions form the hand band a host derives (server B-1 `HANDS_BAND_TAGS`). */
export declare const HANDS_BAND_TAGS: ReadonlySet<ToolMountTag>;