UNPKG

agentfootprint

Version:

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

189 lines • 11.5 kB
"use strict"; /** * saidByPerson — which `role: 'user'` message a PERSON wrote, and which ones * this library wrote in a person's voice. * * Pattern: One predicate over one message, over a registry of frames. No * imports, no state, no clock. * Role: Leaf. Several layers ask the same question and must not answer it * differently: the window's refusal engine (`currentRequest.ts`, * which will not let a strategy drop the request), the six authors * of library-written user turns (`window/notice.ts`, * `window/summarize.ts`, `outputEnforcement.ts`, `evidence/gate.ts`, * `stages/wrapUp.ts`, and `skillSteps.ts` for the nudge * `stages/stepNudge.ts` appends — they own the sentences and take * the markers from here), the three writers of request-only late * lines (`arguments/serve.ts`, `evidence/figures.ts`, * `stagedRefs.ts` — since G17), and — since 9.84.0 — a rule * author reading `InjectionContext.history` (`saidByPerson(ctx)`). * Emits: N/A. * * ## Why this is a leaf and not a private helper * * This library writes SEVEN kinds of `role: 'user'` message into `history` * that nobody said (and serves three more on a request only — below): * a compaction frame, a drop notice, a schema-check correction, an * evidence-check correction, a budget wrap-up instruction, a stepped-skill * nudge, and a message an Injection delivered. Six of the seven are read off * the OPENING they carry ({@link LIBRARY_AUTHORED_PREFIXES}); the delivered * one carries an `injectedBy` marker instead. The window layer knew two of the * openings and the marker; the evidence layer knew two more * (`evidence/frames.ts`); neither list was reachable from the routing layer at * all — the skill-graph fence forbids it from importing the agent loop, and * rightly. So a predicate reading `history` could filter some of the classes * and not the rest, and would match on our own bookkeeping text. * * A drop notice NAMES TOOLS ("Tool results are among them (lookup_order) …"). * A rule watching history for a tool name therefore fired on the notice about * that tool's result leaving the window — pinning a skill on exactly the long * sessions where the notice appears, which is the opposite of what the author * wrote. That is a fixable bug in one place only: the rule has to be one * implementation both sides import, not two that agree until they do not. * * The two frames added in 9.86.0 are the same bug found again, and found by * counting rather than by reading: 9.84.0 registered four openings and wrote * "five classes" in prose, while the tree already had SEVEN producers of a * user-role message that reach `scope.history`. The budget wrap-up * (`stages/wrapUp.ts`) carried an unprefixed sentence containing "Do not * request tools", and the stepped-skill nudge (`stages/stepNudge.ts`) carries * a skill id and every unrun step's TOOL NAME — so both were credited to a * person, a `saidByPerson(ctx).some(m => m.content.includes(…))` rule matched * on them, and the window could anchor its refusal on the wrap-up frame. * * G17 found the same bug a third time, on a channel the walk had filed as * safe. Three more `role: 'user'` lines are served on a REQUEST and never * written to `history`: the time layer's late line (`arguments/serve.ts` · * `timeLine`, on EVERY request under `.time()` since 9.134.1), the figures * dial's conclusion (`evidence/figures.ts` · `figuresConclusionLine`) and the * staged-refs nudge (`stagedRefs.ts` · `stagedRefsNudgeLine`). "Never in * history" was read as "no reader can mistake it", but a reader of the * REQUEST — a provider, a mock's echo, a host finding "this turn" as the last * user-role message the model was sent — sees the library's line LAST and * calls it the person's. So their openings are registered here too * ({@link LIBRARY_NOTE_OPENING}, {@link STAGED_DATA_FRAME_PREFIX}), and an * `ephemeral` message (a reliability retry's feedback, on one attempt's * request) is never a person's either. * * The markers live here, as a LIST rather than one constant per writing file. * A prefix is what the recognizer matches AND what the writer emits; * separating those is how a prefix silently stops being recognized, and an * authored frame that lands anywhere else is a hole nobody will see until a * rule fires on it. Adding one means adding it to * {@link LIBRARY_AUTHORED_PREFIXES}, and every reader is fixed at once — * which is why the count is no longer asserted in prose alone: * `test/injection-engine/userTurnProducers.test.ts` WALKS the tree for * every `role: 'user'` construction site and fails, with the file and line, on * one that is neither registered here, nor a person's own turn, nor * wire-only. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.isSaidByPerson = exports.isCompactedSummary = exports.isDropNotice = exports.isLibraryAuthoredFrame = exports.LIBRARY_AUTHORED_PREFIXES = exports.FINDINGS_DATA_FRAME_PREFIX = exports.STAGED_DATA_FRAME_PREFIX = exports.LIBRARY_NOTE_OPENING = exports.STEP_NUDGE_FRAME_PREFIX = exports.WRAP_UP_FRAME_PREFIX = exports.EVIDENCE_CHECK_FRAME_PREFIX = exports.SCHEMA_CHECK_FRAME_PREFIX = exports.COMPACTED_FRAME_PREFIX = exports.DROP_NOTICE_PREFIX = void 0; /** Opening of the authored notice a DROP leaves behind. Stable — tests and * readers match on it. Written by `buildDropNotice`. */ exports.DROP_NOTICE_PREFIX = '[dropped history'; /** Opening of the authored label a FOLD leaves behind. Stable — tests and * readers match on it. Written by `buildSummaryMessage`. */ exports.COMPACTED_FRAME_PREFIX = '[compacted history'; /** Opening of the authored frame a failed output validation writes. Stable — * tests and readers match on it. Written by `buildCorrectiveTurn`. */ exports.SCHEMA_CHECK_FRAME_PREFIX = '[schema check'; /** Opening of the authored frame the evidence gate writes. Stable — tests, * docs and readers match on it. Written by `buildEvidenceCorrection`. */ exports.EVIDENCE_CHECK_FRAME_PREFIX = '[evidence check'; /** Opening of the authored instruction the out-of-budget WRAP-UP call carries. * Stable — tests and readers match on it. Written by `wrapUpStage` * (`WRAP_UP_INSTRUCTION`). */ exports.WRAP_UP_FRAME_PREFIX = '[budget exhausted'; /** Opening of the authored frame the stepped-skill NUDGE carries. Stable — * tests and readers match on it. Written by `nudgeTeachingMessage`, appended * by `buildStepNudgeStage`. */ exports.STEP_NUDGE_FRAME_PREFIX = '[steps unrun'; /** * Opening of the LIBRARY NOTE — the late line this library serves LAST on a * request and never writes to `history`: the time layer's ONE served time line * (`arguments/serve.ts` · `timeLine`, which opens with it as `TIME_LINE_SOURCE`) * and the figures dial's conclusion (`evidence/figures.ts` · * `figuresConclusionLine`). It says WHO speaks because an unmarked request-only * `user` line reads as the person talking — measured: 37 of 37 answers opened * "You're right… I apologize" after one (the T6b paid run). Stable — tests and * readers match on it, and it is the one copy both writers emit. */ exports.LIBRARY_NOTE_OPENING = '[A note from the library that runs the tools — not from the person, and not a correction from them: when you answer, answer the person directly, as you would from the tool results alone.]'; /** Opening of the request-only STAGED-REFS nudge. Stable — tests and readers * match on it. Written by `stagedRefsNudgeLine`, appended by `callLLM`. */ exports.STAGED_DATA_FRAME_PREFIX = '[staged data'; /** Request-only findings DATA, quoted from the record rather than a person's message. */ exports.FINDINGS_DATA_FRAME_PREFIX = '[AgentFootprint findings DATA v1]'; /** * Every opening this library puts on a `role: 'user'` message it wrote itself. * * The registry, not a convenience: this is the list a reader has to have ALL * of to answer "did a person say this?", and until 9.84.0 no reader had it — * the window held two entries, the evidence gate held the other two, and the * routing layer could reach neither. A new authored frame belongs here on the * day it is written. * * The two correction frames both QUOTE untrusted text after their label (a * validator's error, the model's own flagged values), which is exactly why * they are matched by PREFIX and never by anything further in. The nudge is * the same shape for a different reason: its body is a list of a skill's own * step notes and tool names, which is exactly the text a rule watches for. * * The list is frozen because it is a registry, not a scratch array: a consumer * holds the same object this library's own readers hold, and a `push` into it * would change what every reader calls a person's message. */ exports.LIBRARY_AUTHORED_PREFIXES = Object.freeze([ exports.DROP_NOTICE_PREFIX, exports.COMPACTED_FRAME_PREFIX, exports.SCHEMA_CHECK_FRAME_PREFIX, exports.EVIDENCE_CHECK_FRAME_PREFIX, exports.WRAP_UP_FRAME_PREFIX, exports.STEP_NUDGE_FRAME_PREFIX, exports.LIBRARY_NOTE_OPENING, exports.STAGED_DATA_FRAME_PREFIX, exports.FINDINGS_DATA_FRAME_PREFIX, ]); /** True when this user-role message opens with a frame this library authored. */ function isLibraryAuthoredFrame(msg) { return (msg !== undefined && msg.role === 'user' && exports.LIBRARY_AUTHORED_PREFIXES.some((prefix) => msg.content.startsWith(prefix))); } exports.isLibraryAuthoredFrame = isLibraryAuthoredFrame; /** True when this message is a notice a previous drop wrote. */ function isDropNotice(msg) { return msg !== undefined && msg.role === 'user' && msg.content.startsWith(exports.DROP_NOTICE_PREFIX); } exports.isDropNotice = isDropNotice; /** True when this message is a frame a previous fold wrote. */ function isCompactedSummary(msg) { return msg !== undefined && msg.role === 'user' && msg.content.startsWith(exports.COMPACTED_FRAME_PREFIX); } exports.isCompactedSummary = isCompactedSummary; // FOLD · the one owner of which role:'user' message a PERSON wrote, as opposed to the ones this library writes in a person's voice // consumers read this and never re-derive it: core/agent/window/currentRequest.ts, injection-engine/types.ts's // saidByPerson(ctx) (injection-engine/types.ts · saidByPerson), and src/index.ts (published) // detached: yes — a boolean over one message; the writers import the prefixes the recogniser matches on. /** * THE rule: true when this message is something a PERSON said. * * Deliberately narrow, and narrow in one direction: a message we are not sure * about is not credited to a person. The exclusions are the ways this library * authors a user turn — a delivery marker its own stage stamps, the * `ephemeral` flag on a line composed for one request, and the openings in * {@link LIBRARY_AUTHORED_PREFIXES} (the request-only late lines included) — * never a guess at prose. * * A message from a restored conversation, a hand-built window, or a person * typing passes every exclusion and is theirs. */ function isSaidByPerson(msg) { return (msg !== undefined && msg.role === 'user' && msg.injectedBy === undefined && msg.ephemeral !== true && !isLibraryAuthoredFrame(msg)); } exports.isSaidByPerson = isSaidByPerson; //# sourceMappingURL=saidByPerson.js.map