UNPKG

agentfootprint

Version:

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

213 lines • 10.8 kB
"use strict"; /** * arguments/subflow — the four stages of the inputs layer's subflow * (`sf-inputs`): Declare → Verify → Record → Resolve. * * Pattern: thin stage bodies over the pure steps in `resolve.ts`. Each reads * what the stage before it staged on the subflow's own (isolated) * scope and stages its own output; the mount's output mapper * (`honesty/mounts.ts`) carries the rows and the resolutions out. * Role: core/ layer, the inputs layer (honesty layer 2). Loaded through * `import()` by the mount's stage wrappers — never on a plain agent's * graph. Everything that is not this folder's arrives as `deps` * (the dispatch resolver, Route's predicate, the ledger's emit half), * so `arguments/` stays a leaf that never imports `findings/` or * `stages/`. * Emits: `agentfootprint.findings.argument` — one per row, from the Record * stage, through the ledger's emit half (`deps.emitRows`). * * ## Only a batch that will dispatch * * Declare asks Route's own predicate (`deps.willDispatch`) first: a reply with * no tool call, a run out of iterations, a halting cost budget — the batch is * going to the final branch, so nothing is planned and every later stage finds * an empty plan. The layer never files a row about a call Route will not run. * * ## Declared sources (`.inputsLayer({ argumentSources: true })`, `.findings({ argumentSources: true })`) * * Present only under the arm: `deps.sources` reads each call's `from` entries * through the ONE reader of `_findings` (`findings/reserved.ts` · * `readDeclaration`, handed in as a closure, so this folder still never * imports `findings/`), and Verify builds the corpora the checks read from the * raw pieces the mount handed in (`sourceInputs`) — loaded on first use, never * on a plain agent's graph. Every stage re-reads the entries rather than * staging a second copy of the model's quotes. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveArgumentsStage = exports.recordArgumentsStage = exports.verifyArgumentsStage = exports.declareArgumentsStage = void 0; const bind_js_1 = require("../../../time/bind.js"); const resolve_js_1 = require("./resolve.js"); /** The calls' declared sources, by call id — `undefined` when the arm is off. */ function declaredOf(deps, calls) { if (deps.sources === undefined) return undefined; return new Map(deps.sources.declaredOf(calls).map((d) => [d.toolCallId, d])); } /** * The arm as the pure steps take it — the entries, and (from Verify on) the * corpora and whether a tool in reach can hide arguments (the agent's * build-time fact: a registered tool with an argument view, or a ToolProvider). */ function armOf(declared, corpus, argumentViews) { if (declared === undefined) return undefined; return { declared, ...(corpus !== undefined && { corpus }), ...(argumentViews === true && { argumentViews }), }; } /** * This turn's kept answers as plain data — read only by a stage whose batch * left an `ask` argument out, the one case a kept answer can fill. */ function keptOf(scope) { const kept = scope.argumentAnswersKept; return kept === undefined ? undefined : [...kept].map((a) => ({ ...a })); } /** * The time arm as the pure steps take it — `undefined` when `.time()` is off, * or the mount handed no inputs (no call, or a turn with no clock). Read only * under the arm: a tracked read of a key a run never writes is a phantom source. */ function timeArmOf(scope, deps) { if (deps.time === undefined) return undefined; const inputs = scope.timeInputs; if (inputs === undefined) return undefined; // The rows are plain JSON (checked at the checkpoint door); a frozen input read is a live // proxy view, which `structuredClone` refuses — so the copy is a JSON round-trip. const plain = JSON.parse(JSON.stringify({ clock: inputs.clock, readings: [...inputs.readings], answers: [...(inputs.answers ?? [])], })); return { turn: (0, bind_js_1.turnWindowsOf)(plain.readings, plain.clock, plain.answers), now: plain.clock.now, zone: plain.clock.zone, ...(deps.time.appZone !== undefined && { appZone: deps.time.appZone }), }; } /** Whether a planned batch left an `ask`-ruled argument out — the one case a kept answer can fill. */ const leavesAskOut = (plan) => plan.some((p) => p.ruled.some((r) => r.rule === 'ask' && r.missing)); /** Whether the checked batch fills a kept answer anywhere. */ const fillsKept = (checked) => checked.some((c) => c.filled === true && c.source === 'answered' && c.window === undefined); /** The batch as plain data — a frozen input read is a live view, spread it once. */ function callsOf(scope) { return [...(scope.calls ?? [])].map((c) => ({ id: c.id, name: c.name, args: { ...(c.args ?? {}) }, ...(c.argumentsFailure !== undefined && { argumentsFailure: { ...c.argumentsFailure } }), })); } /** * DECLARE — which calls of a DISPATCHING batch carry rules, read off the * implementation that will answer each name, and which ruled values are * missing. Stages `argumentPlan` (identities and a flag — no value); an empty * plan when the batch will not dispatch or no call is ruled. */ function declareArgumentsStage(scope, deps) { const calls = callsOf(scope); // Read under Route's own short-circuit: the policy only once a budget was hit // (the key is handed in only when the agent configured one). const costBudgetHit = scope.costBudgetHit; const costBudgetOnExceed = costBudgetHit === true ? scope.costBudgetOnExceed : undefined; const dispatches = deps.willDispatch({ callCount: calls.length, iteration: scope.iteration, maxIterations: scope.maxIterations, ...(costBudgetHit !== undefined && { costBudgetHit }), ...(costBudgetOnExceed !== undefined && { costBudgetOnExceed }), ...(deps.toolsOffAfterRevision === true && scope.toolsOff === true && { toolsOff: true }), }); scope.argumentPlan = dispatches ? (0, resolve_js_1.declareBatch)(calls, deps.toolOf, armOf(declaredOf(deps, calls))) : []; } exports.declareArgumentsStage = declareArgumentsStage; /** * VERIFY — where each planned ruled value came from (`resolve.ts` · * `verifyPlan`); under declared sources, each present value checked against * the source the model declared (`checks.ts` · `checkSource`) over corpora * built from the mount's `sourceInputs`. Stages `argumentChecks` (identities, * enums and the checks' verdicts — no value, no quote). */ async function verifyArgumentsStage(scope, deps) { const plan = [...(scope.argumentPlan ?? [])]; if (plan.length === 0) { scope.argumentChecks = []; return; } const calls = callsOf(scope); const declared = declaredOf(deps, calls); // Read only under the arm: a tracked read of a key a run never writes is a phantom source. const inputs = deps.sources !== undefined ? scope.sourceInputs : undefined; const corpus = declared !== undefined && deps.sources !== undefined && inputs !== undefined ? await deps.sources.corpusOf(detached(inputs), calls, scope.turnNumber) : undefined; scope.argumentChecks = (0, resolve_js_1.verifyPlan)(plan, calls, deps.toolOf, leavesAskOut(plan) ? keptOf(scope) : undefined, armOf(declared, corpus, deps.sources?.argumentViews), timeArmOf(scope, deps)); } exports.verifyArgumentsStage = verifyArgumentsStage; /** The mount's frozen inputs as plain data — a frozen input read is a live view. */ function detached(inputs) { return structuredClone({ history: [...(inputs.history ?? [])], ...(inputs.systemPromptInjections !== undefined && { systemPromptInjections: [...inputs.systemPromptInjections], }), ...(inputs.ledger !== undefined && { ledger: [...inputs.ledger] }), ...(inputs.previousBatch !== undefined && { previousBatch: [...inputs.previousBatch] }), ...(inputs.composed === true && { composed: true }), }); } /** * RECORD — one row per checked argument, in the tool's own argument view, * stamped with the conversation turn; one event per row, fired HERE (inside the * subflow, before the call dispatches). Stages `argumentRows`; the mount's * output mapper merges them into the ledger in ONE write. */ function recordArgumentsStage(scope, deps) { const checked = [...(scope.argumentChecks ?? [])]; const stamp = { turn: scope.turnNumber, iteration: scope.iteration }; // Under `.time()`: the windows decide fills, so the decisions are read before the rows. const time = timeArmOf(scope, deps); const plan = time === undefined ? [] : [...(scope.argumentPlan ?? [])]; const calls = checked.length === 0 && plan.length === 0 ? [] : callsOf(scope); const decisions = (0, resolve_js_1.timeDecisionsOf)(plan, calls, deps.toolOf, time, armOf(declaredOf(deps, calls))); const rows = checked.length === 0 ? [] : (0, resolve_js_1.rowsOf)(checked, calls, deps.toolOf, stamp, fillsKept(checked) ? keptOf(scope) : undefined, armOf(declaredOf(deps, calls)), decisions); scope.argumentRows = rows; if (rows.length > 0) deps.emitRows(scope, rows); if (time !== undefined) scope.argumentWindowRows = (0, resolve_js_1.callWindowRowsOf)(decisions, calls, stamp); } exports.recordArgumentsStage = recordArgumentsStage; /** * RESOLVE — the fills and refusals ToolCalls applies (`resolve.ts` · * `resolutionsOf`). Stages `argumentResolutions`, each entry stamped with the * batch's iteration. */ function resolveArgumentsStage(scope, deps) { const plan = [...(scope.argumentPlan ?? [])]; const checked = [...(scope.argumentChecks ?? [])]; if (plan.length === 0) { scope.argumentResolutions = []; return; } // The calls are read only under an arm — a reading's quote rides the ask; a window fills. const time = timeArmOf(scope, deps); const calls = deps.sources !== undefined || time !== undefined ? callsOf(scope) : undefined; const sources = calls !== undefined ? armOf(declaredOf(deps, calls)) : undefined; scope.argumentResolutions = (0, resolve_js_1.resolutionsOf)(plan, checked, deps.toolOf, scope.iteration, fillsKept(checked) ? keptOf(scope) : undefined, sources, time !== undefined && calls !== undefined ? (0, resolve_js_1.timeDecisionsOf)(plan, calls, deps.toolOf, time, sources) : undefined); } exports.resolveArgumentsStage = resolveArgumentsStage; //# sourceMappingURL=subflow.js.map