UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

291 lines • 15.7 kB
/** * Skills: an authored procedure the model pulls in when a task calls for it, instead of every * instruction living in the system prompt. * * WHAT A SKILL IS NOT: an agent. An agent is WHO is answering — its persona, its tools, its history * window, its output schema. A skill is HOW one particular task is done, and any agent may pull one * in. That is why a skill carries no model, no tool list and no schema: the moment it did, the two * would be the same thing wearing different names, and a consumer would have to choose between them * for reasons nobody could state. * * WHAT IT COSTS THE PROMPT: one line per skill. The catalog block below carries names, scopes and * descriptions only; a BODY reaches the model as a tool result, on the transcript, where the * `HistoryWindow` ceiling already governs it. So skills are not a fourth thing competing for the * system block with the agent's own prompt, its persona and injected retrieval — see * `AgentLoopDeps.skills` for how they compose. */ import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { Actor, PageContext, ToolDefinition } from './types.js'; /** How a skill is identified in the catalog, to the model and to a `/`-autocomplete alike. */ export interface SkillSummary { /** Unique within a scope. The handle the model passes to the `skill` tool. */ name: string; /** One line: what task this covers, and therefore when to load it. Read by the MODEL to choose. */ description: string; /** The opaque scope token this skill is published at — see {@link ScopeResolver}. */ scope: string; } /** A skill with the instructions themselves. Only ever materialized when something loads it. */ export interface Skill extends SkillSummary { body: string; } /** Whose turn is asking, and therefore which scopes apply. */ export interface ScopeContext { actor: Actor; threadId: string; /** The agent running this turn. Undefined → the default agent. */ agentName?: string; pageContext?: PageContext; } /** * The skills-facing name for {@link ScopeContext}, so a `SkillProvider`'s signature reads in its own * vocabulary. Deliberately the SAME type rather than a parallel one: skills and memory resolve * scopes through one {@link ScopeResolver}, and a deployment that could answer "which scopes does * this actor have" twice would eventually answer it differently. */ export type SkillContext = ScopeContext; /** The scope token every deployment has: skills nobody narrowed. */ export declare const GLOBAL_SCOPE = "global"; /** The token for one actor's own skills. */ export declare function actorScope(actor: Actor): string; /** The token for a tenant's skills — `Actor.tenantRef` is the only tenant key this library knows. */ export declare function tenantScope(tenantRef: string): string; /** * Which scopes a turn may draw from, MOST SPECIFIC FIRST. Precedence is the order: an entry from an * earlier token outranks a same-named one from a later token, and the model is shown which won. * * A HOST-SUPPLIED FUNCTION rather than an enum this library owns, because the axes a deployment * scopes by are the deployment's own. `Actor` gives an id and a tenant; it does not give a sector, a * squadron, a base, a shift — and every one of those is a real axis in some consumer. An enum here * would make each of them a schema change in a library that has no business knowing they exist, * while a token is a string a host mints for itself. Return `['sector:logistics', 'tenant:base-7', * 'global']` and precedence follows, with nothing in this package edited. * * MUST be a pure function of its context. It runs inside the `skills:catalog` / `memory:digest` * checkpoint and its result is journaled, so a resumed run reads back the scopes the first attempt * resolved rather than asking a membership table that may have changed since — which would let a * run's prompt differ from the one its journal records. Read a database here at your peril; read it * in the provider, whose answer is journaled at the same position. */ export interface ScopeResolver { resolve(ctx: ScopeContext): string[] | Promise<string[]>; } /** * The scopes derivable from an {@link Actor} alone — the common case, so a consumer wiring skills * for the first time supplies no resolver at all: the actor's own, their tenant's (when they have * one), and the deployment's. A host adding an axis of its own replaces this rather than extending * it, since the ORDER is the precedence and only the host knows where its axis belongs. */ export declare const defaultScopeResolver: ScopeResolver; /** Arguments to {@link SkillProvider.list}. */ export interface ListSkillsInput { scopes: readonly string[]; ctx: SkillContext; } /** * Arguments to {@link SkillProvider.load}. An object rather than positional arguments because `name` * and `scope` are both strings: transposed, a positional call compiles clean, returns `null`, and * the skill silently fails to load. */ export interface LoadSkillInput { name: string; scope: string; ctx: SkillContext; } /** * Where skills come from. Deliberately two calls rather than one: `list` is asked on EVERY turn and * must stay cheap, while a body is read only when the model decides it needs that procedure — the * whole point of the surface. A provider over a table selects name/description/scope for the first * and one row for the second. * * This library owns no skill table. A skill's real scoping axes, its authoring UI and its audit * trail are all the host's, and a host that related its own entity into a table this package created * at boot would be writing migrations against a schema the boot-time heal also edits. Tokens split * it the other way round: the library owns the contract (what a scope means, how precedence works, * what is journaled), the host owns the rows. */ export interface SkillProvider { /** * Every skill visible at `scopes`, in any order — this library sorts and resolves precedence. A * provider MAY return skills outside `scopes`; they are dropped rather than trusted, so a filter * bug in a host cannot widen what an actor is offered. */ list(input: ListSkillsInput): SkillSummary[] | Promise<SkillSummary[]>; /** * The instructions for one skill, or `null` when it is gone. Called only from inside the loading * checkpoint, so the body it returns is journaled and every replay reads THAT text back — an * edited skill never rewrites the prompt of a run already in flight. */ load(input: LoadSkillInput): string | null | Promise<string | null>; } /** A source of skills that is a fixed list — module config, or a host's own constant. */ export declare function staticSkillProvider(skills: readonly Skill[]): SkillProvider; /** * Read several sources as one. The order is the tie-break and nothing else: precedence between * skills is by SCOPE, so two sources only ever compete when they publish the same name at the same * scope, and then the earlier source wins. Wire the host's own provider first — a row it can edit is * a better answer than a body that needs a deploy to change. */ export declare function compositeSkillProvider(providers: readonly SkillProvider[]): SkillProvider; /** * One skill as it is offered — to the model in the catalog block, and to a client listing what a * user may type after a `/`. Both read the same list, built by the same call, so what a user can * reach and what the model can reach cannot drift apart. */ export interface SkillCatalogEntry { name: string; description: string; /** The scope token it resolved from — the provenance a user is entitled to see. */ scope: string; /** * Scope tokens of same-named skills this one outranks, widest last. Present ONLY when something * was shadowed, so a reader can tell "there is no org default" from "there is one and yours wins". * The model is shown it for the same reason: a silent override is indistinguishable from an * instruction nobody wrote, and the user can only be told "your setting differs from the org * default" by something that knows both existed. */ shadows?: string[]; } /** What a turn resolved — journaled whole, so the prompt is reconstructible from the journal alone. */ export interface SkillOffer { /** The scope tokens this turn drew from, most specific first. */ scopes: string[]; /** The skills offered, most specific first then by name. */ entries: SkillCatalogEntry[]; /** Applicable skills `maxSkills` left out. Non-zero means the catalog is not the whole truth. */ omitted: number; } /** * How many skills a catalog offers before it starts leaving some out. A ceiling on the SYSTEM * PROMPT's share, not on how many a deployment may have: the block costs one line each, and a * hundred lines of "here is something you could load" crowds out the agent's own instructions while * making the choice harder rather than easier. */ export declare const DEFAULT_MAX_SKILLS = 20; /** How a turn reaches its skills. See `AgentLoopDeps.skills`. */ export interface SkillsConfig { provider: SkillProvider; /** Undefined → {@link defaultScopeResolver}: the actor's own, their tenant's, the deployment's. */ scopes?: ScopeResolver; /** Undefined → {@link DEFAULT_MAX_SKILLS}. */ maxSkills?: number; } /** * Resolve a provider's skills against an ordered scope list: most specific wins, and the loser's * scope is recorded rather than discarded. * * Pure, and separately exported, because two callers must reach the identical answer — the loop, so * the model is offered it, and the listing endpoint, so a user is. A second implementation of "which * skills apply" is a second answer, and the two diverge the first time either is edited. */ export declare function resolveSkillCatalog(summaries: readonly SkillSummary[], scopes: readonly string[], maxSkills?: number): Omit<SkillOffer, 'scopes'>; /** Resolve the scopes and the catalog for one turn. Called INSIDE the loop's `skills:catalog` step. */ export declare function offerSkills(config: SkillsConfig, ctx: SkillContext): Promise<SkillOffer>; /** The reserved name of the built-in skill-loading tool. */ export declare const SKILL_TOOL_NAME = "skill"; /** * The catalog as the model reads it. One line per skill — name, scope, description, and what it * overrides — because the entire claim of progressive disclosure is that choosing what to read costs * far less than reading everything. */ export declare function buildSkillsBlock(entries: readonly SkillCatalogEntry[]): string; /** What the model passes to the `skill` tool. */ export interface SkillToolInput { name: string; } /** * The `skill` tool's input schema. Hand-written for the same reason `askInputSchema` is: this * package depends on no validator, and the schema has to publish a JSON Schema a provider can * constrain generation against. * * The available names are deliberately NOT an enum here. A per-turn enum would make the tool's * definition depend on the catalog, and a tool list re-derived by whichever process serves a turn is * exactly the process-local lookup this design keeps off a turn's decisions. The names live in the * prompt, where the journal holds them; a name that is not in the catalog comes back as an ordinary * tool failure listing the ones that are. */ export declare const skillInputSchema: StandardSchemaV1<unknown, SkillToolInput>; export declare const SKILL_TOOL_DESCRIPTION = "Read one of the procedures listed in <skills>. Call it before doing a task a listed skill covers, and follow what it returns. It performs nothing and changes nothing \u2014 it only gives you instructions you do not yet have."; /** * The `skill` tool as the model sees it. NOT a `ToolSpec` and never registered, exactly like `ask`: * it has no handler, because the loop serves it from the catalog the journal holds. Keeping it out * of the `ToolRegistry` is also what keeps its kind off a process-local lookup — see * `claimToolCall`. */ export declare function skillToolDefinition(): ToolDefinition; /** What a `skill` call resolved to — the body, or why it did not. */ export type SkillLoadOutcome = { ok: true; skill: Skill; shadows?: string[]; } | { ok: false; error: string; }; /** * Serve one `skill` call against the catalog THIS TURN was offered. * * The catalog is the authorization boundary, not just a menu: a name the turn was never offered is * refused here, so a model that invents one (or repeats one it saw in another actor's thread) cannot * reach a body through a provider that would happily serve it. Because the catalog came out of the * `skills:catalog` checkpoint, that boundary is the one the journal records rather than the one this * process's provider currently believes in. */ export declare function loadSkill(args: { config: SkillsConfig; offer: SkillOffer; name: string; ctx: SkillContext; }): Promise<SkillLoadOutcome>; /** Who is trying to write a skill. */ export interface SkillAuthor { /** * `'human'` is a person acting through a UI; `'agent'` is anything else — a tool, a turn, a batch * job. The distinction is the whole of the rule below, so it is not inferable and has to be stated * by the caller. */ kind: 'human' | 'agent'; actorRef?: string; } /** A request to author a skill at a scope. */ export interface SkillWriteRequest { /** The scope token the skill would be published at. */ scope: string; actor: Actor; /** The actor's resolved scopes, most specific first — the same list a turn draws from. */ scopes: readonly string[]; author: SkillAuthor; /** * The host's own answer to "may this person administer that scope" — a sector lead editing their * sector's skills, an operator editing the deployment's. This library cannot know it: it has no * notion of who administers `sector:logistics`, and inventing one would be a second, weaker * authorization model next to the host's real one. Undefined/false → a wider scope is refused. */ elevated?: boolean; } export type SkillWriteVerdict = { allowed: true; } | { allowed: false; reason: string; }; /** * May this author publish a skill at this scope? * * A skill's body is instructions the model follows, which makes writing one at a scope an edit to * everyone in that scope's system prompt. The rules follow from that, and only the third is * interesting: * * 1. You may only write into a scope you are yourself in. Writing into a scope you are not in is * authoring for people you have no relationship with. * 2. Your OWN scope is yours. A per-actor skill affects exactly one prompt — the author's. * 3. NOTHING BUT A HUMAN MAY WRITE ABOVE ITS OWN SCOPE, whatever `elevated` says. An agent that can * write a `tenant:` skill is an agent whose prompt anyone in the tenant can edit by talking to * it: the user asks for something, the turn writes the instruction, and every later turn for * every other user follows it. That is prompt injection with persistence, and no elevation a host * could grant makes it a different shape. An agent that has genuinely learned something worth * sharing proposes it; a person publishes it. * 4. A human writing above their own scope needs the host to say so (`elevated`). */ export declare function skillWriteVerdict(request: SkillWriteRequest): SkillWriteVerdict; //# sourceMappingURL=skills.d.ts.map