agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
143 lines • 7.76 kB
JavaScript
;
/**
* askComponent — the typed-HITL component payload, and its ONE grammar owner.
*
* A pause that asks a person something has always carried the question as
* PROSE. This file adds the optional typed half: WHICH registered screen
* component should collect the answer, and the props it renders with. The
* registry itself lives in the FRONTEND — this library ships ids and props,
* never markup and never code, so a model (or a tool) can nominate a picker
* without ever being allowed to author one (the no-eval law).
*
* ── The shape, and why it is three fields ────────────────────────────────────
* • `componentId` — consumer vocabulary, exactly like an artifact `kind`:
* meaningful to whoever registered the component, opaque here.
* • `props` — small inline JSON. It rides the ask, which rides the
* CHECKPOINT, so it is for the payloads a checkpoint can afford.
* • `propsRef` — a claim ticket (`art_…`) for the big half. A 200-option
* picker's options belong in the ARTIFACT STORE, not in every stored
* session envelope; the screen redeems the ref through the same wire
* `head`/`get` every other artifact rides, under the same session scope.
*
* ── Who mints `propsRef` ─────────────────────────────────────────────────────
* Usually the tool that is about to ask: `ctx.artifacts.put(...)` first, then
* `askHuman({ question, component: { componentId, propsRef: meta.ref } })`.
* The ask API only CARRIES the ref — and the dispatch loop validates it
* RESOLVES in the run's own scope at raise time, so a dangling ref is refused
* at its source instead of being discovered by the screen (see
* `InvalidAskComponentError`).
*
* This is the LEAF that owns the shape, the shape refusal, and the one duck
* reader of `component` on a pause payload. Zero runtime imports beyond a
* type, so `core/pause.ts`, `core/checkin.ts` and `hosting/types.ts` can all
* speak it without a cycle.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.readAskComponent = exports.assertAskComponent = exports.InvalidAskComponentError = void 0;
/**
* An ask nominated a component this run cannot honor as stated — a malformed
* shape, a `propsRef` with no artifact store attached, or a `propsRef` that
* does not resolve in the run's own scope.
*
* Raised AT THE SOURCE — the moment the ask is raised — and it fails the run
* loudly rather than pausing: a pause whose component the screen cannot
* render is a question a person half-receives, and a consent gate silently
* downgraded to prose (or to nothing) is the accepted-and-silently-wrong
* failure this refusal exists to prevent. The author fixes the ask; nothing
* was executed behind anyone's back.
*/
class InvalidAskComponentError extends Error {
code = 'ERR_INVALID_ASK_COMPONENT';
/** Which rule refused. */
reason;
/** The door the component arrived through (`askHuman`, `ask middleware '<name>'`,
* `tool '<name>' checkInComponent`, …). */
door;
/** The ref that could not be honored, when the refusal is about one. */
ref;
constructor(reason, door, detail, ref) {
super(`[askComponent] ${door} refused: ${detail}`);
this.name = 'InvalidAskComponentError';
this.reason = reason;
this.door = door;
if (ref !== undefined)
this.ref = ref;
}
}
exports.InvalidAskComponentError = InvalidAskComponentError;
/**
* Refuse a malformed component BY NAME, at whichever door it arrived through.
*
* Deliberately checks the three fields and nothing more: `componentId` is
* consumer vocabulary (no charset opinion beyond "say something"), `props`
* must be a plain object because it is spread into JSON payloads, and
* `propsRef` must be a non-empty string because an empty ticket resolves
* nothing. Whether the ref RESOLVES is a runtime question answered at raise
* time by the dispatch loop, not here — this leaf has no store.
*/
function assertAskComponent(value, door) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new InvalidAskComponentError('shape', door, `component must be an object { componentId, props?, propsRef? }, got ${describe(value)}. ` +
`componentId names a component REGISTERED in your frontend; props is small inline ` +
`JSON; propsRef is an artifact ref for the big half.`);
}
const c = value;
if (typeof c.componentId !== 'string' || c.componentId.trim().length === 0) {
throw new InvalidAskComponentError('shape', door, `component.componentId must be a non-empty string (got ${describe(c.componentId)}). It ` +
`is the id your frontend registered the component under — the screen picks the ` +
`renderer from it, so an ask without one has nothing to render.`);
}
if (c.props !== undefined &&
(typeof c.props !== 'object' || c.props === null || Array.isArray(c.props))) {
throw new InvalidAskComponentError('shape', door, `component.props must be a plain object when present (got ${describe(c.props)}). It is ` +
`the small inline half — for a list of options or anything big, mint an artifact ` +
`and pass its ref as component.propsRef instead.`);
}
if (c.propsRef !== undefined && (typeof c.propsRef !== 'string' || c.propsRef.length === 0)) {
throw new InvalidAskComponentError('shape', door, `component.propsRef must be a non-empty artifact ref string when present (got ` +
`${describe(c.propsRef)}) — the ticket ctx.artifacts.put(...) handed back.`);
}
}
exports.assertAskComponent = assertAskComponent;
/**
* The ONE reader of `component` on a pause payload — wherever the pause kind
* happens to keep it.
*
* A plain `askHuman` / `pauseHere` pause carries it at the TOP of `pauseData`
* (the tool's own bag, spread by the dispatch loop); a check-in keeps it on
* the typed request under `pauseData.checkIn`; a middleware ask keeps it on
* the question under `pauseData.ask`. The kinds are mutually exclusive, so at
* most one home is occupied — and every consumer that wants "the component of
* this pause, whatever kind it is" asks HERE, so the homes cannot drift apart
* from their readers.
*
* Duck-shaped on purpose (`pauseData` is uninterpreted caller data): a value
* that is not a plausible component is reported absent, never guessed at.
*/
function readAskComponent(pauseData) {
if (typeof pauseData !== 'object' || pauseData === null)
return undefined;
const bag = pauseData;
const candidate = bag.component ??
(typeof bag.checkIn === 'object' && bag.checkIn !== null ? bag.checkIn.component : undefined) ??
(typeof bag.ask === 'object' && bag.ask !== null ? bag.ask.component : undefined);
if (typeof candidate !== 'object' || candidate === null)
return undefined;
const c = candidate;
if (typeof c.componentId !== 'string' || c.componentId.length === 0)
return undefined;
return candidate;
}
exports.readAskComponent = readAskComponent;
/** Name the SHAPE of a bad value for a refusal — never its contents. */
function describe(value) {
if (value === undefined)
return 'undefined';
if (value === null)
return 'null';
if (Array.isArray(value))
return 'an array';
const t = typeof value;
return t === 'object' ? 'an object' : t === 'string' ? 'an empty/blank string' : `a ${t}`;
}
//# sourceMappingURL=askComponent.js.map