UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

257 lines (256 loc) • 16.3 kB
/** * 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]; /** 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[]; 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; ruleFace?: ToolRuleFace; renderHints?: ToolRenderHints; offload?: boolean; offloadThresholdChars?: number; compactable?: true; budgetExempt?: true; } /** * 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 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>;