UNPKG

agentfootprint

Version:

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

123 lines • 6.19 kB
/** * act — PUBLIC. One bundle for every place a rule may speak. * * Pattern: Facade over five doors, with the facade's KEYS type-locked to the * list of moments they cover. * Role: core/ layer. `.act()` builds nothing new: it validates a bundle * and calls `.messageMiddleware()`, `.toolMiddleware()` and * `.window()` with exactly what a person would have passed by hand. * Everything about a run is decided by those doors, here and there * alike. * Emits: N/A. * * ## Why a bundle at all, when the doors already exist * * Because the doors are findable one at a time and the POSTURE is not. An * agent's governance currently reads as four unrelated calls scattered * through a builder chain, and the question a reviewer actually has — * "what does this agent do at each point in its loop?" — has no place to be * answered. `.act({ … })` is that place: five optional keys, named for the * five moments, so autocomplete on an empty object teaches the loop. * * ## The completeness lock * * The keys of {@link ActOptions} are pinned, at compile time, against * `ActKey<LoopMoment>` — the moment list, camel-cased. Add a sixth moment to * `LOOP_MOMENTS` and this file stops compiling until the bundle grows the key * for it; delete a key and the same. A surface that claims to be complete has * to be made unable to fall behind, or the claim is just a sentence in a doc. * * The runtime half is derived from the same list: the accepted key set is * `LOOP_MOMENTS.map(actKeyFor)`, so a typo'd key is refused by a validator * that cannot drift from the type it is validating. */ import { messageAt } from './middleware/messagePhase.js'; import { actKeyFor, LOOP_MOMENTS } from '../../lib/moments.js'; const _keysAreExactlyTheMoments = true; void _keysAreExactlyTheMoments; /** The keys `.act()` accepts, derived from the moment list rather than typed. */ export const ACT_KEYS = LOOP_MOMENTS.map(actKeyFor); function assertList(value, key) { if (!Array.isArray(value)) { throw new Error(`AgentBuilder.act: '${key}' expects an array of middleware, got ${typeof value}. ` + `Wrap a single rule in an array — the bundle is a list per moment.`); } return value; } function assertNamedHook(mw, key, hook) { if (mw === null || typeof mw !== 'object' || typeof mw.name !== 'string' || mw.name.length === 0 || typeof mw[hook] !== 'function') { throw new Error(`AgentBuilder.act: every '${key}' entry needs a non-empty \`name\` and a \`${hook}(...)\` ` + `method. The name is what every ledger row and event says decided the call, so it ` + `cannot be blank` + (hook === 'onToolCall' ? `; a rule that only has \`onToolResult\` belongs under 'afterTool'.` : hook === 'onToolResult' ? `; a rule that only has \`onToolCall\` belongs under 'beforeTool'.` : `.`)); } } /** * Validate a bundle and turn it into the arguments the five doors take. * * Pure — it throws or it returns; it touches no builder state. `.act()` is * the caller that hands the result to the doors, which is what makes the * sugar provably the same agent. */ export function resolveAct(options) { if (options === null || typeof options !== 'object' || Array.isArray(options)) { throw new Error(`AgentBuilder.act: expected an options object, got ${Array.isArray(options) ? 'an array' : typeof options}. The keys are ${ACT_KEYS.join(', ')}.`); } for (const key of Object.keys(options)) { if (!ACT_KEYS.includes(key)) { throw new Error(`AgentBuilder.act: unknown key '${key}'. The loop has ${LOOP_MOMENTS.length} moments and ` + `this bundle has one key for each: ${ACT_KEYS.join(', ')}. A key nobody reads is a ` + `governance rule that silently never runs, so it is refused here rather than ignored.`); } } // ── The message halves ────────────────────────────────────────────── // A link named at BOTH phases is attached once, unguarded — which is the // plain `.messageMiddleware(mw)` spelling, and the one that files one row // per phase instead of two. const input = assertList(options.input ?? [], 'input'); const output = assertList(options.output ?? [], 'output'); for (const mw of input) assertNamedHook(mw, 'input', 'onMessage'); for (const mw of output) assertNamedHook(mw, 'output', 'onMessage'); const message = []; for (const mw of input) { message.push(output.includes(mw) ? mw : messageAt('input', mw)); } for (const mw of output) { if (!input.includes(mw)) message.push(messageAt('output', mw)); } // ── The tool moments ──────────────────────────────────────────────── // One list, walked forwards for calls and backwards for results. The // buckets are named for the MOMENTS (`afterTool` ⇄ `'after-tool'`); the // hooks are named for what they RECEIVE (`onToolResult`). The bucket says // where a rule is MEANT to speak and is checked for the hook it names; what // it actually does is decided by the hooks it has, so a rule can never // silently fail to run because it was written in the other bucket. const beforeTool = assertList(options.beforeTool ?? [], 'beforeTool'); const afterTool = assertList(options.afterTool ?? [], 'afterTool'); for (const mw of beforeTool) assertNamedHook(mw, 'beforeTool', 'onToolCall'); for (const mw of afterTool) assertNamedHook(mw, 'afterTool', 'onToolResult'); const tool = [...beforeTool]; for (const mw of afterTool) { if (!tool.includes(mw)) tool.push(mw); } return { message, tool, ...(options.window !== undefined && { window: options.window }), }; } //# sourceMappingURL=act.js.map