UNPKG

agentfootprint

Version:

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

192 lines • 11.7 kB
"use strict"; /** * The answer a `read_skill` SELF-CALL gets back (9.84.0). * * A self-call is `read_skill("X")` from a cursor already standing on X. It used * to fall through the skill-graph gate's reachability arm — `makeReachableSkills` * filters the cursor out of its own successor set (a MOVE to where you already * are is not a move) and `openSkillIds()` excludes every graph-wired skill, so * the cursor's own id is in neither half of `hops ∪ open`. The model was told: * * read_skill("X") is not reachable from here. Reachable skills: … * * while X's body was in its system prompt and X's tools were in that very call's * tool list. Read as a statement about AVAILABILITY — which is how a model reads * "not reachable" — that is a flat contradiction of the request it arrived in, * and the observed behaviour was to stop and answer that it could not help. * * ── THE LAW THIS FILE IS WRITTEN AGAINST ────────────────────────────────── * * A TOOL RESULT IS NOT READ ONCE. It is composed during iteration N, written * into `history`, and then read by the model on call N+1 AND ON EVERY CALL * AFTER IT for the rest of the turn — including the out-of-budget WRAP-UP, * which `callLLM` serves with an EMPTY tool list under a user message saying * that no tools were offered on it. * * The consequence is the whole design: NO COMPOSE-TIME CHECK CAN MAKE A * FORWARD-LOOKING SENTENCE SAFE. A budget test, a posture test, a cursor test * — each is true at the instant it runs and each is outlived by the sentence * it admitted. Three rounds of this file fixed one clause at a time and each * time a different clause was still false, because every one of them was a * PREDICTION and the fix was always a better prediction. * * So the notice makes no predictions at all. Every clause is a fact about ONE * already-finished event — the call the model made `read_skill` on — and every * clause is anchored to that call by name ("that call"), never by deixis ("the * call you just made", which denotes a different call each time it is re-read). * A past fact about a named call cannot be falsified by anything that happens * afterwards: not by the budget running out, not by a posture refusing a hop, * not by a sibling tool in the same batch moving the cursor. * * ── WHAT WAS DELETED, AND WHY THE NOTICE STILL DOES ITS JOB ─────────────── * * Gone: the exhortation ("Go ahead and act on the request"), the move offer * ("read_skill MOVES you somewhere else — from here that would be: beta"), the * open-skill offer ("These activate without moving you: gamma"), and the * budget clause that was added to gate them. Each was false somewhere: * * • the exhortation, on the wrap-up call, beside a frame reporting that no * tools were offered on it; * • the move offer, on the wrap-up call (a hop is a tool call), under * `strictness: 'rails'` (every model hop refused) and under `'guard'` on a * decisively-routed turn (every hop not on an outstanding menu refused) — * where the sibling posture arm answers a taken-up offer with "read_skill * here reaches only the open skills: gamma", contradicting the notice from * the arm immediately above it, and spending escalation budget to do so; * • the budget clause, because a budget read at compose time is a claim * about a call that has not happened. * * The notice's JOB is to stop a model concluding its capability is gone. It * does that by stating what it held, not by promising what it may do: you were * standing in this skill, its instructions were in that call's prompt, its * tools were on that call's wire, and asking took nothing away. A model that * reads those four facts has no ground left for "I cannot help". * * The forward-looking half had a rightful owner all along, and it is not a * tool result: the `read_skill` DESCRIPTION is recomposed on every single call, * so it can safely speak in the present ("You are in 'beta'."), it is filtered * for per-role visibility at source, and it is absent from the wrap-up because * the whole tool list is. One owner for the present tense, one for the past, * and they can no longer disagree — which is the only property that survived * every round of this bug. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.selfCallNotice = exports.selfSkillTools = void 0; /** * The skill's tools, as of the call the model just made — or `undefined` when * that cannot be established, which is not the same answer and must not be * reported as one. * * `undefined` means the notice says nothing about tools at all, and there are * exactly two ways to get it (9.86.0 — the previous note named a third that * cannot happen). It happens when the cursor's skill is not in the ACTIVE set * for that call, and when the wire key is absent from scope altogether. * * `reactMode: 'classic'` is NOT one of them, which is worth stating because the * old note said it was: seed writes `scope.dynamicToolSchemas` from the * build-time schemas at turn start (`stages/seed.ts`), so under classic the key * is present and holds the list `callLLM` really sends — the notice would name * the build-time list, correctly. (No self-call can arise there anyway: * `.skillGraph()` is refused at build under classic, and only a graph gives the * gate a cursor to compare against.) The grouped chart used to be a real third * way — the Tools slot writes the key inside `sf-llm-call` and this gate reads * it outside — until that boundary began bubbling it out. * * Silence is the only honest output in the two remaining cases: the * alternative is a sentence naming tools the model may not have. * * The out-of-budget wrap-up is NOT one of these cases and must not be modelled * as one. Its empty tool list belongs to a LATER call; the wire read here is * always the one the model was really handed on the call being answered, and * the notice speaks of that call and only that call. */ function selfSkillTools(skillId, activeInjections, wire) { if (wire === undefined || activeInjections === undefined) return undefined; const mine = activeInjections.find((i) => i.id === skillId); if (mine === undefined) return undefined; const declared = (mine.inject.tools ?? []).map((t) => t.schema.name); const onWire = new Set(wire.map((s) => s.name)); return { declared, served: declared.filter((n) => onWire.has(n)) }; } exports.selfSkillTools = selfSkillTools; // LENS · tool-result · persistent-history // reads: skillId ← the request, verbatim; tools ← selfSkillTools(active injections ∩ the wire); body ← the surviving result // known: the wire handed in is scope.dynamicToolSchemas (toolCalls.ts · "── The SELF-CALL arm (9.84.0) — BEFORE reachability"), which is the PRE-withholding list; // that same arm argues the wrap-up latch can never be up when a self-call is composed. // law: may omit, never deny; every clause anchored to the call it was composed on. /** * The self-call's tool result. * * NOT a refusal to the MODEL, and deliberately so. The gate's other two arms * refuse because the model asked for something it may not have; a self-call * asks for a body it already holds, and the useful reply is the fact — where * you stood, and what rode that call — not a decline. It is still counted as a * refusal by the ESCALATION BUDGET (`noteSkillRefusal`, beside the emit), and * those two facts do not fight: the budget is not measuring how the sentence * reads, it is measuring a model that keeps asking the graph where it is * instead of working, which is exactly the stuck run the budget escalates. * * It still OVERWRITES the tool's own result the way every other arm does, * because that overwrite is what puts the authoritative sentence in front of * the governance rules, the refusal cap and the after-tool moment, on both * channels, before the model reads anything. * * The one thing not overwritten is a BODY. The live case is `surfaceMode: * 'both'` — `'tool-only'` cannot arise here, because `skillBodyDelivery` * refuses it at BUILD time for anything but an `llm-activated` skill and only a * graph-wired skill can be the cursor. Under `'both'` the body is in the system * prompt AND rides the tool result, and the second copy is the whole point of * the mode: recency-first delivery, the body at the end of the window rather * than only at the top of the prompt. Dropping it here would quietly demote * `'both'` to `'system-prompt'` on the one call that asked to re-read. * * TENSE IS LOAD-BEARING, AND SO IS DEIXIS. Every clause below is past tense * about a single named call, and every reference to that call is "that call" — * bound by the opening clause, which says which call it was. "The call you just * made" was the earlier wording and it is not safe: re-read four calls later it * denotes call four, and the tools it names rode call one. * * There are exactly two present-tense phrases in the output and both are * timeless rather than temporal: "It declares no tools of its own" (a * declaration is fixed when the skill is defined and cannot vary within a run) * and "Its instructions are repeated below" (the body is appended to this very * string, so the sentence is true wherever the string is read). */ function selfCallNotice(args) { const { skillId, tools, body } = args; // "were in that call's system prompt" is a claim about the prompt, and the // evidence for it is that the skill was in the ACTIVE set for that call — // which is exactly what `tools !== undefined` establishes (see // `selfSkillTools`). With no body to append and no proof it was mounted, the // notice says nothing about the instructions rather than asserting where // they were. const bodyClause = body !== undefined ? `Its instructions are repeated below. ` : tools !== undefined ? `Its instructions were in that call's system prompt. ` : ''; const toolClause = tools === undefined ? '' : tools.declared.length === 0 ? `It declares no tools of its own. ` : tools.served.length > 0 ? `Its own tools were on that call's tool list: ${tools.served.join(', ')}.` : `None of its own tools were on that call's tool list (${tools.declared.join(', ')} ` + `${tools.declared.length === 1 ? 'was' : 'were'} withheld).`; // The opening clause is what binds "that call" for every clause after it, so // it names the event rather than the state: not where the cursor IS, but // where it stood when this call was made. A sibling tool in the SAME batch // can fire a step edge and move the cursor before the model reads this — the // read_skill description in that request will then say "You are in 'beta'", // and the two must not contradict each other. Only one of them is entitled // to the present tense, and it is the one recomposed on every call. const notice = `read_skill("${skillId}") named the skill you were already standing in when you ` + `made that call, so the cursor did not move — nothing was activated, and nothing ` + `was taken away. ${bodyClause}${toolClause}`; const trimmed = notice.trimEnd(); return body === undefined ? trimmed : `${trimmed}\n\n${body}`; } exports.selfCallNotice = selfCallNotice; //# sourceMappingURL=selfCallNotice.js.map