UNPKG

agentfootprint

Version:

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

91 lines • 4.32 kB
/** * middleware/outcomes — PUBLIC. The three verbs a middleware speaks. * * Pattern: Smart constructors over a closed union. * Role: core/ layer. `allow` / `deny` / `ask` are the entire vocabulary * of both chains, which is why they are short: they are written * inside every middleware body, several times, and a governance * rule reads better as `deny('writes to prod need a ticket')` than * as a namespaced ceremony. * Emits: N/A. * * The `allow` overloads carry a rule: you can pass the value through * untouched with no explanation, but you cannot CHANGE it without one. A * transform is the only thing here that makes the trace and the wire * disagree, so it is the only thing required to say why. */ import { assertAskComponent } from '../../../tools/askComponent.js'; export function allow(value, why, origin) { if (value === undefined) { return typeof why === 'string' && why.length > 0 ? { kind: 'allow', why } : { kind: 'allow' }; } if (typeof why !== 'string' || why.length === 0) { throw new Error('allow(value, why): a transform must say why. Pass a short reason — it is what the ' + 'ledger shows next to the before/after pair. To pass the value through unchanged, ' + 'call allow() with no arguments.'); } if (origin === undefined) return { kind: 'allow', value, why }; return { kind: 'allow', value, why, from: assertOrigins(origin) }; } const ORIGINS = ['person', 'default', 'app']; /** A declared origin map, judged — refused here, in the middleware author's own stack. */ function assertOrigins(origin) { const from = origin !== null && typeof origin === 'object' ? origin.from : undefined; const valid = from !== null && typeof from === 'object' && !Array.isArray(from) && Object.values(from).every((v) => typeof v === 'string' && ORIGINS.includes(v)); if (!valid) { throw new Error("allow(value, why, { from }): `from` maps each rewritten argument's name to where its " + "value came from — 'person', 'app' or 'default'."); } return { ...from }; } /** * Refuse the call. * * For a tool the reason reaches the model verbatim, as the tool's result, * and the loop continues — the agent gets to adapt. For a message it * surfaces as a `MessageDeniedError`. * * At the after-tool moment it means "the model does not get to read this" — the * tool has already run, so the refusal replaces what the model reads while * the run keeps the real result in the ledger. Refusing there hides an answer * from the model; it cannot un-happen a side effect, and it does not pretend * to. */ export function deny(reason) { if (typeof reason !== 'string' || reason.length === 0) { throw new Error('deny(reason): a refusal must carry a reason. For a tool it is the text the model ' + 'reads and adapts to, so write it for the model, not for a log line.'); } return { kind: 'deny', reason }; } /** * Suspend the run and put the question to a person. Tool dispatch only — * `MessageOutcome` has no `ask` arm, so this cannot be returned from a * message middleware. * * The answer is a decision, not a result: approve and the chain continues * and the REAL tool runs; decline and it becomes a denial the model reads. * A middleware never gets to write the answer itself. */ export function ask(payload) { if (payload === null || typeof payload !== 'object' || typeof payload.question !== 'string' || payload.question.length === 0) { throw new Error('ask(payload): expected { question, detail?, component? } with a non-empty question. ' + 'The question is what a person is shown when the run pauses.'); } // A malformed component fails HERE, in the middleware author's own stack — // not as a raise-time refusal attributed to the tool call it happened to // gate. Whether a `propsRef` RESOLVES is the dispatch loop's question (it // has the store and the run's scope); the shape is this constructor's. if (payload.component !== undefined) { assertAskComponent(payload.component, 'ask(payload)'); } return { kind: 'ask', payload }; } //# sourceMappingURL=outcomes.js.map