UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

352 lines • 19.8 kB
"use strict"; /** * 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). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.nudgeTeachingMessage = exports.remainingStepsOf = exports.skipNothingActiveSentence = exports.skipNeedsReasonSentence = exports.skipHoldSentence = exports.skipAdvanceSentence = exports.stepAdvanceSuffix = exports.readSkillStepIntro = exports.skipStepDescription = exports.stepBannerPrefix = exports.skipStepDescriptor = exports.rekeyStepPointer = exports.currentStepOf = exports.stepInProgress = exports.pointerOf = exports.foldStepPlans = exports.stepsOf = exports.validateSkillSteps = exports.SKIP_STEP_TOOL_NAME = void 0; const saidByPerson_js_1 = require("../lib/saidByPerson.js"); const skillVocabulary_js_1 = require("./skillVocabulary.js"); /** 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. */ exports.SKIP_STEP_TOOL_NAME = 'skip_step'; // ─── Validation (the defineSkill checkup — refuse at the earliest point) ── /** * 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. */ function validateSkillSteps(skillId, opts) { const where = `defineSkill(${skillId})`; if (opts.steps === undefined) { if (opts.onSkip !== undefined) { throw new Error(`${where}: \`onSkip\` is legal only beside \`steps\` — it declares what a skipped ` + `step does, and this skill declares no steps. Add \`steps\` or drop \`onSkip\`.`); } return; } if (opts.steps.length === 0) { throw new Error(`${where}: \`steps: []\` declares nothing — say what you mean and omit the field. ` + `A skill without steps is byte-identical to one that never heard of them.`); } if (opts.onSkip !== undefined && opts.onSkip !== 'advance' && opts.onSkip !== 'hold') { throw new Error(`${where}: onSkip '${String(opts.onSkip)}' is not a policy this library has. The two ` + `are 'advance' (record the skip and move on — the default) and 'hold' (record the ` + `skip and keep the step current).`); } if (opts.toolNames.size === 0) { throw new Error(`${where}: \`steps\` declares a tool sequence but this skill carries no \`tools\`. ` + `Every step runs one of the skill's own tools — add them to \`tools\`, or drop ` + `\`steps\`.`); } opts.steps.forEach((step, i) => { const n = i + 1; // The vocabularies ride the same declaration, so they are refused here // too — one authoring point, one place a mistake is caught. (0, skillVocabulary_js_1.assertArtifactVocabulary)(`${where} step ${n}`, 'produces', step?.produces); (0, skillVocabulary_js_1.assertArtifactVocabulary)(`${where} step ${n}`, 'consumes', step?.consumes); if (!step || typeof step.tool !== 'string' || step.tool.trim().length === 0) { throw new Error(`${where}: step ${n} names no tool. Every step is { tool, note } — the tool is ` + `what the step runs.`); } if (typeof step.note !== 'string' || step.note.trim().length === 0) { throw new Error(`${where}: step ${n} ('${step.tool}') has no note. The note is the sentence the ` + `model reads about WHY this step exists — a step without one is a force-march ` + `instruction, and the note is the whole point.`); } if (!opts.toolNames.has(step.tool)) { throw new Error(`${where}: step ${n} names '${step.tool}', which this skill's \`tools\` does not ` + `carry — add the tool or fix the name. Steps may only sequence the skill's own ` + `tools; everything else stays available as an escape hatch, unsequenced.`); } }); } exports.validateSkillSteps = validateSkillSteps; // ─── The build-time fold ────────────────────────────────────────────── /** Read a skill injection's declared steps off its metadata bag (the * `autoActivate` precedent — the engine ignores unknown keys). */ function stepsOf(injection) { const meta = injection.metadata; if (!meta?.steps || meta.steps.length === 0) return undefined; return { steps: meta.steps, onSkip: meta.onSkip ?? 'advance' }; } exports.stepsOf = stepsOf; /** * 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). */ function foldStepPlans(injections) { const plans = new Map(); for (const inj of injections) { if (inj.flavor !== 'skill') continue; const declared = stepsOf(inj); if (!declared) continue; plans.set(inj.id, { skillId: inj.id, steps: declared.steps.map((s) => ({ tool: s.tool, note: s.note })), toolNames: new Set(declared.steps.map((s) => s.tool)), onSkip: declared.onSkip, }); } return plans; } exports.foldStepPlans = foldStepPlans; // ─── Pointer helpers ────────────────────────────────────────────────── /** Unwrap the carrier. `undefined` for an absent key, an empty carrier, * or a value that is not a carrier at all. */ function pointerOf(carrier) { if (!Array.isArray(carrier) || carrier.length === 0) return undefined; const p = carrier[0]; return typeof p?.skillId === 'string' && typeof p?.step === 'number' ? p : undefined; } exports.pointerOf = pointerOf; /** True while the procedure has steps left (`step <= total`). A completed * pointer stays on scope as the record; nothing narrows or nudges on it. */ function stepInProgress(ptr) { return ptr !== undefined && ptr.step <= ptr.total; } exports.stepInProgress = stepInProgress; /** The current step's declaration, when the procedure is in progress. */ function currentStepOf(ptr, plan) { return stepInProgress(ptr) ? plan.steps[ptr.step - 1] : undefined; } exports.currentStepOf = currentStepOf; /** * 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. */ function rekeyStepPointer(args) { const prior = pointerOf(args.prior); if (args.tenant === undefined) return []; if (prior?.skillId === args.tenant) return [prior]; const plan = args.stepPlanFor(args.tenant); if (!plan) return []; return [{ skillId: args.tenant, step: 1, total: plan.steps.length, skipped: [] }]; } exports.rekeyStepPointer = rekeyStepPointer; // ─── The skip_step integrity tool ───────────────────────────────────── /** * 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. */ function skipStepDescriptor() { return { name: exports.SKIP_STEP_TOOL_NAME, description: 'Skip the current step of the active skill procedure, with the reason on the record. ' + 'Use it when the step cannot or should not run for this input.', inputSchema: { type: 'object', properties: { reason: { type: 'string', description: 'Why this step cannot or should not run. Goes on the record.', }, }, required: ['reason'], additionalProperties: false, }, execute: () => 'skip_step noted. (Raw placeholder — the agent loop replaces this result with the ' + 'authoritative step record; reading it verbatim means the runner that dispatched ' + 'skip_step does not track step procedures.)', }; } exports.skipStepDescriptor = skipStepDescriptor; // ─── Every sentence the model reads ─────────────────────────────────── // LENS · tool-description · request-ephemeral // reads: the pointer and the step's declared note; prefixed onto a schema copy rebuilt for THIS request // law: may omit, never deny; every clause anchored to the call it was composed on. /** `[Step 3 of 6 — <note>] ` — prefixed onto the current step's tool * description in the offer (a rebuilt schema copy, substituted by name). */ function stepBannerPrefix(ptr, step) { return `[Step ${ptr.step} of ${ptr.total} — ${step.note}] `; } exports.stepBannerPrefix = stepBannerPrefix; // LENS · tool-description · request-ephemeral // reads: the pointer and the plan's DECLARED onSkip policy — named as declared, never as a prediction // law: may omit, never deny; every clause anchored to the call it was composed on. /** The per-iteration `skip_step` offer description: names the current * step, the remaining count, and the declared onSkip policy. */ function skipStepDescription(ptr, plan) { const step = plan.steps[ptr.step - 1]; const remaining = ptr.total - ptr.step + 1; const policy = plan.onSkip === 'hold' ? 'the step stays current until it runs or the turn ends' : 'the procedure moves to the next step'; return (`Skip step ${ptr.step} of ${ptr.total} ('${step.tool}' — ${step.note}) with a recorded ` + `reason. ${remaining} step(s) remain in the '${ptr.skillId}' procedure. On skip, ` + `${policy} (declared policy: '${plan.onSkip}').`); } exports.skipStepDescription = skipStepDescription; /** Suffix for the `read_skill` result that activated a stepped skill: * where the procedure starts. */ function readSkillStepIntro(plan) { const first = plan.steps[0]; return (` You are on step 1 of ${plan.steps.length}: ${first.note}. ` + `Its tool: \`${first.tool}\`.`); } exports.readSkillStepIntro = readSkillStepIntro; // LENS · tool-result · persistent-history // reads: the advanced pointer and the frozen plan, both handed in // known gap: ' Now on step N of M' is present tense on a surface re-read all turn; listed as unrepaired // in this folder's README ledger. // law: may omit, never deny; every clause anchored to the call it was composed on. /** Suffix for a completed step's tool result: done + what is next. */ function stepAdvanceSuffix(next, plan) { const done = next.step - 1; if (next.step > next.total) { return ` Step ${done} of ${next.total} done. All ${next.total} steps complete.`; } const coming = plan.steps[next.step - 1]; return (` Step ${done} of ${next.total} done. Now on step ${next.step} of ${next.total}: ` + `${coming.note} (tool: \`${coming.tool}\`).`); } exports.stepAdvanceSuffix = stepAdvanceSuffix; // LENS · tool-result · persistent-history // reads: the pointer AFTER the skip was applied, and the plan — the verdict, never a prediction of it // known gap: `skillSteps.ts` · skipNothingActiveSentence is present tense on a persistent surface; on the record as // unrepaired in this folder's README ledger. // law: may omit, never deny; every clause anchored to the call it was composed on. /** The authoritative `skip_step` result under the `'advance'` policy. */ function skipAdvanceSentence(skippedIndex, reason, next, plan) { if (next.step > next.total) { return (`Step ${skippedIndex} skipped: ${reason}. That was the last step — the ` + `'${next.skillId}' procedure is complete (${next.skipped.length} step(s) skipped).`); } const coming = plan.steps[next.step - 1]; return (`Step ${skippedIndex} skipped: ${reason}. Now on step ${next.step} of ${next.total}: ` + `${coming.note} (tool: \`${coming.tool}\`).`); } exports.skipAdvanceSentence = skipAdvanceSentence; /** The authoritative `skip_step` result under the `'hold'` policy. */ function skipHoldSentence(ptr, reason, plan) { const step = plan.steps[ptr.step - 1]; return (`Skip of step ${ptr.step} recorded: ${reason}. Step ${ptr.step} of ${ptr.total} stays ` + `current (declared policy 'hold') — run \`${step.tool}\`, or finish and say why.`); } exports.skipHoldSentence = skipHoldSentence; /** Teaching result for `skip_step` with an empty reason — no event. */ function skipNeedsReasonSentence() { return ('skip_step was not applied: `reason` is required and must be non-empty — it goes on ' + 'the record. Say why this step cannot or should not run, then call skip_step again.'); } exports.skipNeedsReasonSentence = skipNeedsReasonSentence; /** Teaching result for `skip_step` outside a stepped tenure — no event. */ function skipNothingActiveSentence() { return ('skip_step has nothing to skip: no step procedure is active (or the active one is ' + 'already complete). Continue with your other tools, or finish.'); } exports.skipNothingActiveSentence = skipNothingActiveSentence; /** 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". */ function remainingStepsOf(ptr, plan) { if (!stepInProgress(ptr)) return []; return plan.steps .map((s, i) => ({ index: i + 1, tool: s.tool, note: s.note })) .filter((s) => s.index >= ptr.step); } exports.remainingStepsOf = remainingStepsOf; // LENS · injected-turn · persistent-history // reads: the unrun steps ← `skillSteps.ts` · remainingStepsOf, the owner of that fold // the opening ← STEP_NUDGE_FRAME_PREFIX from src/lib/saidByPerson.ts (writer and recogniser share it) // law: may omit, never deny; every clause anchored to the call it was composed on. /** * 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`. */ function nudgeTeachingMessage(ptr, plan) { const remaining = remainingStepsOf(ptr, plan); const span = remaining.length === 1 ? `Step ${remaining[0].index}` : `Steps ${remaining[0].index}–${remaining[remaining.length - 1].index}`; const list = remaining.map((s) => `${s.index}: ${s.note} (\`${s.tool}\`)`).join('; '); // "This message asked …" rather than "This call was for …" (9.86.1): the // nudge persists in `history` past the call it was written for, and a bare // `this call` resolves to whichever call re-reads it. The message is the // one thing that stays where it is. return (`${saidByPerson_js_1.STEP_NUDGE_FRAME_PREFIX} — ${span} of '${ptr.skillId}' had not run when the answer ` + `above was given: ${list}. This message asked for them to be run, or for the reason ` + `they were not run.]`); } exports.nudgeTeachingMessage = nudgeTeachingMessage; //# sourceMappingURL=skillSteps.js.map