agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
229 lines • 12.6 kB
TypeScript
/**
* skillSteps — the procedure grammar (9.18.0). ONE owner of everything
* "steps as data": the types, the validation, the step pointer's shape and
* re-key rule, the `skip_step` integrity tool, and EVERY sentence the model
* reads about a procedure (the banner, the result suffixes, the skip
* sentences, the nudge). Stages and slots call in here; none of them owns
* a word of the grammar.
*
* WHAT A PROCEDURE IS. `defineSkill({ steps })` declares an ordered list of
* `{ tool, note }` pairs — "run these tools, in this order, for these
* reasons". The framework then OWNS sequence and scope at the protocol
* level: while a stepped skill holds the tenure, the tools slot offers only
* the current step's tool (plus every escape hatch — see below), so the
* model cannot call step 5's tool from step 3 because step 5's schema was
* never sent. No refusal machinery, no new gate: an unoffered tool is
* uncallable by construction.
*
* WHAT THE MODEL STILL OWNS: judgment inside the step. It can run the tool,
* skip it with a recorded reason (`skip_step`), work around it with an
* escape hatch, or stop and say why. The escape hatches are load-bearing
* and stay offered under narrowing: `read_skill`, `list_skills`, every
* OTHER active skill's tools, the baseline `.tool()` registry, provider
* tools. A procedure is a declared order, not a cage.
*
* THE POINTER. `AgentState.stepPointer` records where the procedure stands:
* `{ skillId, step, total, skipped }`, 1-based, `step === total + 1` means
* complete. It is strictly SUBORDINATE to the one skill-graph cursor: it
* lives and dies inside one skill's tenure, is re-keyed at the same stage
* the cursor truth lives (Evaluate), and never crosses runs. It can never
* move the cursor.
*
* CARRIER SHAPE (load-bearing): the pointer travels through scope and the
* mount mappers as a 0-or-1-element ARRAY (`StepPointerCarrier`), never as
* a bare object. footprintjs's `applyOutputMapping` shallow-merges bare
* object values field-by-field — and APPENDS nested arrays — so a re-keyed
* pointer mapped as an object would inherit the previous tenure's `skipped`
* entries. A top-level array under `arrayMerge: Replace` is set wholesale.
* `[]` = the feature is on and no procedure is active; the key is absent
* entirely on agents with no stepped skill (zero-cost-when-unused).
*/
import type { SkillToolDescriptor } from './hostContract.js';
import type { Injection } from './types.js';
/** One step of a declared procedure. */
export interface SkillStep {
/** The tool this step runs. MUST be one of this skill's own `tools` —
* refused at `defineSkill` otherwise. Repeats across steps are legal
* ("look it up again after the change"). */
readonly tool: string;
/** Why/what — the sentence the model sees ("look up the order before
* touching money"). Required, non-empty: a step with no note is a
* force-march instruction, and the note is the whole point. */
readonly note: string;
/**
* Artifact KINDS this step leaves behind (9.25.0) — a declaration, not
* machinery: nothing at run time reads it, and a step without it is
* byte-identical to today. It makes the step's data leg checkable at build
* (`artifact-kind-unsatisfied`) and drawable by a lens. See
* `skillVocabulary.ts` for the satisfiability rule and its boundaries.
*/
readonly produces?: readonly string[];
/**
* Artifact KINDS this step needs to have arrived (9.25.0). Satisfiable
* three ways — by an EARLIER step of the same skill, by the skill's own
* `consumes` (it arrived from outside), or by the step's tool declaring
* `wants` for the kind (the framework redeems that ref at dispatch, from a
* store that outlives the turn). Anything else is a build-time WARNING.
*/
readonly consumes?: readonly string[];
}
/**
* What the framework does when the model skips a step with `skip_step`:
*
* - `'advance'` (default) — record the skip and move to the next step;
* - `'hold'` — record the skip and keep the step current (its tool stays
* the offer; the model may retry it, work around it with the escape
* hatches, or finish and explain).
*/
export type OnSkipPolicy = 'advance' | 'hold';
/**
* WHERE a declared procedure stands. 1-based; `step === total + 1` means
* the procedure completed. `skipped` holds the indexes the model skipped
* under the `'advance'` policy (a `'hold'` skip is recorded on the event
* stream, not here — the pointer did not move).
*/
export interface StepPointer {
/** The tenant this pointer belongs to — the stepped skill whose tenure
* is current. A cursor move away from it resets the pointer. */
readonly skillId: string;
/** Current step, 1-based. `total + 1` = procedure complete. */
readonly step: number;
readonly total: number;
/** Step indexes skipped-with-reason that the pointer moved past. */
readonly skipped: readonly number[];
}
/**
* How the pointer travels through scope and every mount mapper: a
* 0-or-1-element array. See the module docstring for why a bare object
* cannot cross a footprintjs outputMapper safely.
*/
export type StepPointerCarrier = readonly StepPointer[];
/** The build-time fold of one stepped skill — frozen at `Agent` build. */
export interface StepPlan {
readonly skillId: string;
readonly steps: readonly SkillStep[];
/** Every tool name the plan's steps name — the hold-out candidates. */
readonly toolNames: ReadonlySet<string>;
readonly onSkip: OnSkipPolicy;
}
/** The lookup the engine, the tools slot and the tool-calls stage share.
* Undefined ⇒ no skill on this agent declares steps (the zero-cost gate). */
export type StepPlanFor = (skillId: string) => StepPlan | undefined;
/** The integrity tool's reserved name. Auto-attached (dispatchable) when
* ≥1 registered skill declares steps; OFFERED only while a stepped tenure
* is active and unfinished. */
export declare const SKIP_STEP_TOOL_NAME = "skip_step";
/**
* Validate a `steps` declaration against the skill's own tools. All the
* data is in hand at `defineSkill`, so every refusal happens there — the
* `body-unknown-tool` sibling.
*/
export declare function validateSkillSteps(skillId: string, opts: {
readonly steps?: readonly SkillStep[];
readonly onSkip?: OnSkipPolicy;
readonly toolNames: ReadonlySet<string>;
}): void;
/** Read a skill injection's declared steps off its metadata bag (the
* `autoActivate` precedent — the engine ignores unknown keys). */
export declare function stepsOf(injection: Injection): {
readonly steps: readonly SkillStep[];
readonly onSkip: OnSkipPolicy;
} | undefined;
/**
* Fold every stepped skill into one frozen plan map. Runs once at
* `Agent` build; the map is threaded as closures — nothing rides
* `projectActiveInjection`, so the projection allow-list is untouched.
* Empty map ⇒ the feature is off everywhere (callers thread nothing).
*/
export declare function foldStepPlans(injections: readonly Injection[]): ReadonlyMap<string, StepPlan>;
/** Unwrap the carrier. `undefined` for an absent key, an empty carrier,
* or a value that is not a carrier at all. */
export declare function pointerOf(carrier: unknown): StepPointer | undefined;
/** True while the procedure has steps left (`step <= total`). A completed
* pointer stays on scope as the record; nothing narrows or nudges on it. */
export declare function stepInProgress(ptr: StepPointer | undefined): ptr is StepPointer;
/** The current step's declaration, when the procedure is in progress. */
export declare function currentStepOf(ptr: StepPointer, plan: StepPlan): SkillStep | undefined;
/**
* The tenure RE-KEY — Evaluate's half of the pointer discipline.
*
* `tenant` is who holds the tenure THIS iteration: the advanced cursor on
* graph agents, else the tail of `activatedInjectionIds` (the shipped
* `activeSkillId` notion). Tenant changed → a fresh pointer when the new
* tenant declares steps, else cleared. Tenant unchanged → pass-through.
* Always returns a carrier (possibly empty) — the caller writes it every
* iteration the feature is on, so downstream readers never see a stale one.
*/
export declare function rekeyStepPointer(args: {
readonly prior: StepPointerCarrier | undefined;
readonly tenant: string | undefined;
readonly stepPlanFor: StepPlanFor;
}): StepPointerCarrier;
/**
* DESCRIBE the `skip_step` tool — name, schema, and the placeholder result.
*
* A description, not a tool: this module owns every sentence the model reads
* about a procedure, and that includes this one, but building the framework's
* tool object would mean importing the framework's tool factory into the
* grammar. `skillTools.ts#buildSkipStepTool` does the wrapping (9.34.0).
*
* The tool is auto-attached to the dispatch registry when ≥1 registered skill
* declares steps (the `read_skill` auto-attach seam) — always dispatchable,
* but OFFERED (schema in the request) only while a stepped tenure is active
* and unfinished. Its execute returns a placeholder; the tool-calls stage
* overwrites the model-visible result with the authoritative sentence,
* exactly as the skill-graph gate overwrites `read_skill` refusals —
* bookkeeping lives where the batch order lives.
*/
export declare function skipStepDescriptor(): SkillToolDescriptor<{
reason: string;
}, string>;
/** `[Step 3 of 6 — <note>] ` — prefixed onto the current step's tool
* description in the offer (a rebuilt schema copy, substituted by name). */
export declare function stepBannerPrefix(ptr: StepPointer, step: SkillStep): string;
/** The per-iteration `skip_step` offer description: names the current
* step, the remaining count, and the declared onSkip policy. */
export declare function skipStepDescription(ptr: StepPointer, plan: StepPlan): string;
/** Suffix for the `read_skill` result that activated a stepped skill:
* where the procedure starts. */
export declare function readSkillStepIntro(plan: StepPlan): string;
/** Suffix for a completed step's tool result: done + what is next. */
export declare function stepAdvanceSuffix(next: StepPointer, plan: StepPlan): string;
/** The authoritative `skip_step` result under the `'advance'` policy. */
export declare function skipAdvanceSentence(skippedIndex: number, reason: string, next: StepPointer, plan: StepPlan): string;
/** The authoritative `skip_step` result under the `'hold'` policy. */
export declare function skipHoldSentence(ptr: StepPointer, reason: string, plan: StepPlan): string;
/** Teaching result for `skip_step` with an empty reason — no event. */
export declare function skipNeedsReasonSentence(): string;
/** Teaching result for `skip_step` outside a stepped tenure — no event. */
export declare function skipNothingActiveSentence(): string;
/** The remaining (unrun) steps — the `steps_unfinished` payload's list and
* the nudge's material. Under `'advance'` a skipped step is behind the
* pointer and therefore never "remaining". */
export declare function remainingStepsOf(ptr: StepPointer, plan: StepPlan): ReadonlyArray<{
readonly index: number;
readonly tool: string;
readonly note: string;
}>;
/**
* The one teaching nudge for a premature stop (at most once per turn).
*
* Written as an AUTHORED FRAME (9.86.0). `stepNudge` appends the result to
* `scope.history` wearing `role: 'user'`, so two things follow. First, it has
* to be recognisable as this library's own writing: it opens with
* {@link STEP_NUDGE_FRAME_PREFIX}, imported from the authorship registry that
* `isSaidByPerson` matches against, because the body lists a skill id and
* every unrun step's TOOL NAME — exactly the text a routing rule scanning
* history watches for, and before the opening was here such a rule fired on
* the library's own nudge.
*
* Second, it is re-read on every later call of the turn, so it may not speak
* in the present or issue a standing instruction: "Steps 2–3 have not run …
* Finish them" is false the moment they run, and it was still sitting in
* `history` when they did. Every clause now reports the call the nudge was
* written for, in the past tense, and the ask is stated as what THAT call was
* for. Pinned by `test/injection-engine/userTurnProducers.test.ts`, which
* runs the composed sentence through `unprovable`.
*/
export declare function nudgeTeachingMessage(ptr: StepPointer, plan: StepPlan): string;
//# sourceMappingURL=skillSteps.d.ts.map