agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
112 lines • 5.07 kB
JavaScript
/**
* runbook/dispatch — the bridge's wrapper over `ctx.tools`.
*
* Pattern: decorator over {@link ToolDispatch}. The RAW dispatch (delivered
* by the agent) returns every result untouched; POLICY about what a
* result means belongs to the consumer — and this is the runbook's
* policy: every inner outcome is RECORDED (for the coverage fold and
* the provenance carry), and an inner ABSENCE short-circuits the
* procedure unless the call site declared it survivable.
* Role: core/runbook, pure.
* Emits: N/A.
*
* WHY an absence short-circuits: "the inventory found nothing" IS the
* runbook's answer, and it must go back EXACTLY as it arrived so the
* framework still reads it as an absence — a verdict reached over a source
* that answered "nothing here" would be the confident-partial-answer failure
* this whole envelope exists to prevent. A stage that can genuinely carry on
* without the source says so at the call site (`{ allowAbsent: true }`) and
* owns stating the gap (usually as a coverage entry).
*/
import { readAbsence } from '../../semantics/coverage/absent.js';
/** The marker property that survives any error wrapping between a stage and
* the bridge's catch. */
const ABSENCE_SIGNAL = 'af_runbook_absence';
/**
* The control signal an un-survivable inner absence throws through the
* chart. The engine commits staged state and rethrows (commit-on-error is
* the law upstream), so the bridge catches this at `executor.run` and
* returns the absence verbatim.
*/
export class RunbookAbsenceSignal extends Error {
absence;
[ABSENCE_SIGNAL] = true;
constructor(absence, innerTool) {
super(`runbook inner tool '${innerTool}' answered with an absence — the runbook's answer ` +
`IS that absence, passed through verbatim. (Declare the call survivable with ` +
`{ allowAbsent: true } if the procedure can carry on without this source.)`);
this.absence = absence;
this.name = 'RunbookAbsenceSignal';
}
}
/** Recognize the signal on an error OR anywhere down its `cause` chain — an
* engine layer that wraps the stage's throw must not defeat the pass-through. */
export function absenceSignalOf(err) {
let current = err;
for (let hops = 0; hops < 8 && current !== null && typeof current === 'object'; hops += 1) {
if (current[ABSENCE_SIGNAL] === true) {
return current;
}
current = current.cause;
}
return undefined;
}
/**
* Wrap the delivered dispatch (or its absence) for one runbook invocation.
*
* With NO dispatch delivered (`ctx.tools` absent — a hand-built context, a
* door with no dispatch map) the wrapper is the fail-closed teacher: `has`
* answers false and `call` refuses naming the fix, so a procedure that needs
* inner tools fails loudly at its first call instead of half-running.
*/
export function recordingDispatch(delivered, runbookName) {
const records = [];
const tools = {
has(name) {
return delivered?.has(name) ?? false;
},
async call(name, args, opts) {
if (delivered === undefined) {
throw new Error(`runbookAsTool('${runbookName}'): the procedure called tools.call('${name}') but ` +
`no dispatch was delivered on ctx.tools. Register this runbook on an Agent ` +
`(agent dispatch delivers ctx.tools at execute time), or supply a ToolDispatch ` +
`on the execution context when invoking the tool directly.`);
}
let raw;
try {
raw = await delivered.call(name, args, opts);
}
catch (err) {
records.push({ tool: name, outcome: 'error' });
throw err;
}
const absence = readAbsence(raw);
if (absence !== undefined) {
records.push({ tool: name, outcome: 'absent', result: raw });
if (opts?.allowAbsent === true)
return raw;
throw new RunbookAbsenceSignal(absence, name);
}
records.push({ tool: name, outcome: 'ok', result: raw });
return raw;
},
};
return { tools, records };
}
/**
* The definition-time probe dispatch — handed to the procedure factory ONCE
* at `runbookAsTool(...)` so the bridge can read the chart's declared
* contract. Stage bodies do not run at build; a factory that calls tools at
* build time hears exactly why that cannot work.
*/
export function probeDispatch(runbookName) {
return {
has: () => false,
call: (name) => {
throw new Error(`runbookAsTool('${runbookName}'): tools.call('${name}') was invoked while BUILDING ` +
`the chart. The dispatch only exists at execute time — call tools from inside ` +
`stage functions (which close over it), never from the factory body.`);
},
};
}
//# sourceMappingURL=dispatch.js.map