agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
97 lines • 4.51 kB
JavaScript
;
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ask = exports.deny = exports.allow = void 0;
const askComponent_js_1 = require("../../../tools/askComponent.js");
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) };
}
exports.allow = allow;
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.
*/
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 };
}
exports.deny = deny;
/**
* 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.
*/
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) {
(0, askComponent_js_1.assertAskComponent)(payload.component, 'ask(payload)');
}
return { kind: 'ask', payload };
}
exports.ask = ask;
//# sourceMappingURL=outcomes.js.map