UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

242 lines (241 loc) • 18.4 kB
import type { PermissionResult, ToolCallRequest } from "./tool-policy.js"; import type { PermissionMode } from "./ask-origin.js"; import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js"; import type { AgentTool } from "../internal/harness-types.js"; import { type SkillGrantBindingMembers } from "./skill-grant-binding.js"; import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js"; /** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a * delegated child's seed from its parent. */ export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"]; export type SkillGrantOrigin = (typeof SKILL_GRANT_ORIGINS)[number]; /** The PORTABLE form of one row — what a delegated child captures and what a park checkpoint section freezes: the * skill, the list it came from, and the entry as the skill spells it. The compiled reading is re-derived wherever a * record is seeded (never trusted off the record). */ export interface SkillGrantRecord { readonly skill: string; readonly behavior: "allow" | "deny"; readonly entry: string; readonly origin?: SkillGrantOrigin; } /** One row of the table. */ export interface SkillGrantRow { readonly skill: string; readonly origin: SkillGrantOrigin; readonly behavior: "allow" | "deny"; readonly entry: string; readonly compiled: CompiledAllowEntry; } /** An entry the grammar reads as granting or refusing nothing. */ export type SkillInertEntry = { readonly skill: string; readonly list: "allowed-tools" | "disallowed-tools"; } & AllowEntryInert; /** The names the run mounts, read when a list is — a skill is activated later than prepare, and the roster is read as it stands. */ export type MountedToolNames = () => ReadonlySet<string>; /** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */ export interface SkillGrantSource { readonly name: string; readonly allowedTools?: readonly string[]; readonly disallowedTools?: readonly string[]; } /** * The skill a `Skill` call's arguments name among `skills`: the definition whose `name` equals `args.skill` exactly — * case-sensitive, no alias, no trimming, first of a name (a prepared table is already deduplicated) — or undefined (no such * name, or a `skill` member that is not a string). The ONE resolution: the `Skill` tool activates through it, the card is * asked through it ({@link skillSeatReader}), a parked approval is bound and judged through it ({@link skillGrantDigest}) and * the person's invoked names are read through it ({@link seedSkillGrants}). */ export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined; /** The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases ({@link skillGrantBinding}). The read point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation). */ export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string; /** * The card members of a call to the engine's `Skill` tool — the ONE factory every ask spreads them from: the live * `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection. * `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was * shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and * however the question was worded. Every other call — and any call when no skill is loaded (no engine `Skill` tool is * mounted, so a call by that name is not an activation) — carries the absence word `not_bindable` instead: one of the two, * always, so no reader infers the binding from a tool name. */ export declare function skillGrantBindingMembers(call: { readonly toolName: string; }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): SkillGrantBindingMembers; /** * The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it * carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the * row's wire name). For such a row: the refusal sentence when the skill the arguments name — as the resume's skills define * it, none loaded included — does not digest as the row's `boundGrantDigest`, or the row carries none (parked before the * binding); undefined otherwise. ONE judgement (the digest), judged at two sites: the claim (`site: "claim"`, the decided * bytes — the row stays pending: deny it, then activate again) and the resumed execution boundary (`site: "execution"`, the * bytes that will run, after every station that can rewrite them — the approval is spent: the call does not run). The * sentence says which side of the digest differs, read off the row's own arguments: the arguments name another skill than * the card's (`row.args`), or the card's skill's lists no longer bind to the card's digest (or it is not served) — its * definition changed, or this engine reads an entry of it differently than the engine that parked the row (an entry that * reads as nothing: the digest is the list reader's reading, not the text). Names the skills, never a path; says the way out. */ export declare function parkedSkillApprovalRefusal(row: { readonly toolName: string; readonly args: unknown; readonly boundGrantDigest?: string; }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string, site?: "claim" | "execution"): string | undefined; /** * A skill's table (`allowed-tools` / `disallowed-tools`) READ ONCE, the one reading every later reader takes instead of the host's * value: absent (`undefined`, or `null` — a frontmatter's empty key) is no table; a list is its dense copy (by index, no iterator * of the host's runs; a member that is not a string stays as it is); a value that is not a list, or a list whose read throws, is * ONE entry that reads as nothing ({@link nonListValueLabel}), so each table applies the law it already has to an entry that reads * as nothing — a deny table keeps its Skill from activating, an allow table is announced and grants nothing. A string is not spread. */ export declare function readSkillTable(value: unknown): string[] | undefined; /** A resume's skills read ONCE: each definition copied with its two tables ({@link readSkillTable}), so the parked Skill approval's * binding and the resumed leg's prepare read one value — a later edit to the caller's own objects or arrays reaches neither. */ export declare function detachSkillLists<T extends SkillGrantSource>(skills: readonly T[]): T[]; /** Does this skill declare a list (either, non-empty)? The one test of "the Skill card is asked for this skill". */ export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowedTools" | "disallowedTools">): boolean; /** * Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list * holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction). * A `*` on the deny list reads as nothing by itself, like any deny entry that does — and a skill whose deny list holds one * is not written at all ({@link SkillGrantTable.activate}). */ export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin, roster?: ReadonlySet<string>): { rows: SkillGrantRow[]; inert: SkillInertEntry[]; }; /** What spoke for a call: the skill and the entries it stands on. */ export interface SkillGrantHit { readonly skill: string; readonly entries: readonly string[]; /** On a refusal only: the call activates `skill`, whose `disallowed-tools` entries `entries` read as nothing (why, each). */ readonly readsAsNothing?: readonly AllowEntryInert[]; } /** The refusal a deny hit writes: a deny row's, or an activation's whose deny entries read as nothing. */ export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult; /** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one * reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */ export declare function skillRowRefusal(lane: Pick<SkillGrantLane, "denying"> | undefined, req: ToolCallRequest): PermissionResult | undefined; /** The allow a grant writes (the allow layer's skill member). */ export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unknown): PermissionResult; /** * The Skill card's question: the reference implementation's sentence (`Execute skill: <name>`), and — when approving * releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or * stands down in this mode, is not named). Deployment text, sanitized and capped. */ export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined, roster?: ReadonlySet<string>): string; /** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */ export interface SkillGrantLane { /** The allow layer's skill member: what grants this call in this mode, or undefined. */ allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined; /** What refuses this call, or undefined: the run's deny rows, else — for a call to the `Skill` tool — a named skill whose * `disallowed-tools` holds an entry that reads as nothing (`readsAsNothing`). */ denying(req: ToolCallRequest): SkillGrantHit | undefined; } /** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */ export interface SkillGateSeats { /** * The active skills' rows for this run. DENY rows (a skill's `disallowed-tools`) are read where a person's deny rows are * read — on every decision right after them, and on an approved edit's bytes — and refuse with `decisionReason: "skill"` * (`deniedBy: "policy"`). ALLOW rows are the allow layer's member beside a person's stored allow rows, under the same * consumption predicate: they clear only an ask an allow rule may clear (never a mandated one, a hook's, an ask rule's, * governance's or a marked call's) and are not re-read on an approved edit. `onAllowed` observes a call they cleared (the * allow-side attribution; never affects the outcome). Absent ⇒ no skill rows. */ skillGrants?: SkillGrantLane & { onAllowed?: (info: { toolName: string; toolCallId: string; skill: string; entries: readonly string[]; }) => void; }; } /** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */ export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined; /** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */ export declare function skillSeatReader(skills: readonly SkillGrantSource[], mounted?: MountedToolNames): SkillSeatReader | undefined; /** * The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill` * tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`), * the run's skill rows with their attribution sink, and the request's and the deployment's entry lists with theirs. */ export declare function skillGateWiring(opts: { tools: readonly AgentTool[]; skillToolName: string; seatOf: SkillSeatReader | undefined; mode: () => PermissionMode | undefined; lane: SkillGrantLane; onAllowed: NonNullable<SkillGateSeats["skillGrants"]>["onAllowed"]; /** The two entry lists that are not a skill's (the request's, the deployment's), with their attribution sinks. */ allowEntries: AllowEntryGateSeats; }): (calledName: string, wireName: string) => PermissionSeatInputs & SkillGateSeats & AllowEntryGateSeats; /** * Seed a task's table at prepare, before any call is gated: the rows from outside this leg's own writers (`inherited` — a * parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the * model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked * name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly * (`config.user_invoked_skill_unknown`), never dropped; an invoked skill, or a carried deny record, whose deny entry reads as * nothing refuses it too ({@link SKILL_DENY_UNREADABLE_CODE}). Returns the entries the grammar reads as nothing, across every * loaded skill — the start-of-run notice's content. */ export declare function seedSkillGrants(table: SkillGrantTable, opts: { skills: readonly SkillGrantSource[]; userInvokedSkills: unknown; inherited: readonly SkillGrantRecord[] | undefined; }): readonly SkillInertEntry[]; /** * The user-invoked half of {@link seedSkillGrants}, read where no roster exists yet — a resume's pre-claim screen, over the * re-supplied config: the invoked names seeded exactly as prepare seeds them, on a table told no mounted names, so only what * every roster refuses alike refuses here (a name the config's skills do not carry; a deny entry the grammar alone reads as * nothing). A name the leg's roster reads as stale or a near miss is prepare's to refuse; any other throw (a value prepare * reports its own way) is left to prepare. */ export declare function screenUserInvokedSkills(config: { readonly skills?: readonly SkillGrantSource[]; readonly userInvokedSkills?: unknown; }): void; /** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the * lane all hold this one object. */ export declare class SkillGrantTable { private rowsHeld; private readonly rowKeys; private mounted; private activations; /** State where the run's mounted names are read (prepare, once the roster is assembled): a skill's entries are read against * them when seeded and whenever the skill is activated. A table never told reads the grammar alone. */ bindMountedNames(mounted: MountedToolNames): void; /** State the skills the run's `Skill` tool activates (the list it serves) and its wire name, so a call naming one whose deny * entries read as nothing is refused where the deny rows are read. A table never told refuses such a skill at activation only. */ bindSkills(skills: readonly SkillGrantSource[], skillToolName: string): void; /** The names the run mounts as they stand, or undefined when none were stated. */ mountedNames(): ReadonlySet<string> | undefined; /** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a * capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that * changed across a park writes its new rows when it is activated again. */ private hold; /** Write a skill's rows (rows already held are not held twice) — or none, refused with {@link SKILL_DENY_UNREADABLE_CODE}, * when its `disallowed-tools` holds an entry that reads as nothing. Returns the allow entries that read as nothing. */ activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[]; /** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. Each * record is read as its writer read it, against the leg's roster: an allow record that reads as nothing grants nothing * (not held); a deny record that does refuses the seed ({@link SKILL_DENY_UNREADABLE_CODE}) and nothing is held. */ seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void; /** The rows as portable records (a copy). */ records(): readonly SkillGrantRecord[]; /** The lane over this table, bound to the leg's call facts. */ lane(ctx: AllowEntryJudgeContext): SkillGrantLane; /** A call to the `Skill` tool naming a skill whose deny entries read as nothing, read as the activation would read it. */ private activationRefused; } /** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */ export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined; /** The section a park freezes (`CheckpointState.skillGrants`): the table's rows under the frozen origins, or undefined * when there are none (an absent section IS zero rows on every reader). */ export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records">): SkillGrantRecord[] | undefined; /** Read a park's frozen section ({@link readCarriedRecords}): absent ⇒ none; `undefined` ⇒ damaged — the resume refuses * it before the claim and prepare never seeds from it. */ export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined; /** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */ export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown"; /** The refusal for a skill whose `disallowed-tools` holds an entry that reads as nothing: the activation, or the leg that * would carry the skill's deny record, does not happen. */ export declare const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";