agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
118 lines • 6.18 kB
JavaScript
/**
* artifacts/present — the hand-to-the-screen verb (Phase 2, Leg 2).
*
* `present({ ref, as, label? })` is how the model finishes an analytics
* answer WITHOUT serializing what the screen will show: it names the ticket
* (`ref`), the consumer vocabulary for how to render it (`as: 'bar-chart'`,
* `'table'`, …), and optionally the human title. The framework `head`s the
* ref under the run's own scope and the RESULT carries a DESCRIPTION
* SNAPSHOT — `{ kind, mediaType, bytes, label }` — so the claim ticket
* describes the parcel: a conversation reloaded after the artifact expired
* can still render an honest placeholder from history alone ("Chart —
* 'Q3 sales by region' (bar-chart, 41 KB) — expired; re-run to regenerate"),
* because the snapshot lives INSIDE the tool result, the one thing provider
* message history keeps typed and durable. Never a blank pane.
*
* `as` is deliberately NOT validated against a component registry — that
* registry is a later phase (the frontend leg); until then the value is
* stored as data and travels on the `artifacts.presented` event exactly as
* spoken.
*
* This module is the PURE half — the tool's name/schema constants, the
* snapshot shape, and the resolution over the five-verb port. The tool SHELL
* (a placeholder whose result the agent loop overwrites — the `skip_step`
* pattern) lives in core, which is also where the typed
* `agentfootprint.artifacts.presented` event is emitted. Core wires
* artifacts; never the reverse.
*/
/**
* The reserved name of the auto-attached tool. Reserved ONLY when a store is
* attached (the `read_skill` seam): a storeless agent may keep its own
* `present`, because the framework attaches nothing there.
*/
export const PRESENT_TOOL_NAME = 'present';
/** How many live refs a miss refusal lists. A lesson, not a catalog. */
const PRESENT_LIST_LIMIT = 10;
/** `art_h7Kq… [chart/spec · 41210 bytes · 'Q3 sales']` — one listed ref. */
function describeRef(meta) {
return `${meta.ref} [${meta.kind} · ${meta.bytes} bytes${meta.label !== undefined ? ` · '${meta.label}'` : ''}]`;
}
// LENS · tool-result · persistent-history
// reads: the artifact's meta ← store.head(scope, ref); the live refs ← store.list(scope, {limit:10})
// both from the ArtifactStore port, the sole owner of those facts
// the source is LIVE, so every clause is past tense and bound to the named call: a census rendered into a
// result is re-read after the store has changed.
// law: may omit, never deny; every clause anchored to the call it was composed on.
/**
* Resolve one `present` call over the port — ONE law for every dispatch
* door. `head`, never `get`: presenting is the render-by-ref decision, and
* the screen pays for the bytes later, under its own identity.
*/
export async function presentArtifact(store, scope, args) {
const ref = args.ref;
const as = args.as;
if (typeof ref !== 'string' || ref.trim().length === 0) {
return {
ok: false,
refusal: `present needs \`ref\` — the art_… ticket of the artifact to hand to the screen. ` +
`Store the data first (a tool that mints, or the placement threshold), then present ` +
`its ref.`,
};
}
if (typeof as !== 'string' || as.trim().length === 0) {
return {
ok: false,
refusal: `present needs \`as\` — the consumer vocabulary for how the screen should render ` +
`'${ref}' (e.g. 'bar-chart', 'table', 'image'). Say what it is; the screen picks ` +
`the renderer.`,
};
}
const meta = await store.head(scope, ref);
if (meta === null) {
const page = await store.list(scope, { limit: PRESENT_LIST_LIMIT });
// ANCHORED TO THE CALL, NOT TO THE MOMENT — BOTH ARMS. This refusal is a
// TOOL RESULT: it is written into `history` and re-read on every later call
// of the turn, including calls made after the model has stored something.
// "Nothing is live … right now" was therefore a forecast — true when
// composed, false the moment the run had an artifact — and a model
// re-reading it is being told its own stored data does not exist. The
// opening clause names the call (`present('<ref>')`), so the listing binds
// to that call and stays true wherever it is read.
//
// The STOCKED arm needed the same repair for the mirror-image reason, and
// did not get it until the sentence had a rule to fail: a bare "Live refs
// in scope: …" is a census, and a census goes wrong in both directions —
// a ref it names can be swept before the model re-reads it, and a ref
// minted since will be missing from it. Neither error is visible to the
// model, which reads a list and believes it is looking at the scope. Past
// tense plus the call it was taken for is the whole fix: an inventory of a
// finished moment cannot go stale.
const listing = page.artifacts.length === 0
? `Nothing was live in this run's scope when you made that call — store the data ` +
`first, then present its ref.`
: `These were live in this run's scope when you made that call: ` +
`${page.artifacts.map(describeRef).join(', ')}.`;
return {
ok: false,
missedRef: ref,
refusal: `present('${ref}') found nothing under that ref in this run's scope — expired, ` +
`swept, or never stored here. Nothing was presented. ${listing}`,
};
}
const label = typeof args.label === 'string' && args.label.trim().length > 0 ? args.label : meta.label;
return {
ok: true,
result: {
presented: true,
ref: meta.ref,
as,
snapshot: {
kind: meta.kind,
mediaType: meta.mediaType,
bytes: meta.bytes,
...(label !== undefined && { label }),
},
},
};
}
//# sourceMappingURL=present.js.map