UNPKG

agentfootprint

Version:

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

94 lines (93 loc) • 5.13 kB
/** * hosting/turnArtifacts — the artifact hand-over a turn gives its host, and * how long it lives. * * turnArtifacts: async (turn) => { * if (turn.bound) story = await turn.artifacts.put({ kind: 'story/turn', mediaType, data }); * }, * * `standingAgent` opens one per turn (`openTurnArtifacts`), awaits the host's * `HostReply.turnArtifacts` hook with it and then `drain()` — both BOUNDED by * the composer (`turnArtifactsTimeoutMs`, the request's signal) — and finally * `revoke()`s it before the reply's terminal. This module owns that lifetime * and the failures it can see; the scope, the bound and the record are the * composer's. * * ── Why the hand-over has a lifetime at all ────────────────────────────────── * The scope never widens — the binding closes over one tuple — so a late write * can only ever land in the same person's scope. What a late write breaks is * the RECORD: the binding's facts are delivered on the serving agent's * dispatcher, and by then the agent may be running somebody else's turn (the * shared shape), a stopped instance (the pool evicted the lane), or the next * turn of the same conversation. So the binding is live only while its turn * is — the footprintjs `ScopeFacade · assertLive` law, "a handle held past its * stage is refused", applied to the turn. * * ── Drain, then revoke — and nothing here can crash the process ────────────── * `drain()` waits for every operation started through the binding — a `put` * the hook never awaited included, and one an earlier operation's * continuation starts while it waits. `revoke()` then refuses every later call * by name (`TurnArtifactsExpiredError`). Every promise this module hands out * is handled the moment it is created — an operation's, and a revoked call's * refusal alike — so none of them can become an unhandled rejection that ends * the process; a host that awaits one still receives its error. (A promise the * HOST derives from one — `put(...).then(f)` — is the host's own.) * * ── Failures are reported, never swallowed silently ────────────────────────── * Every failed operation (`'operation'`, with its verb and error class) and * every refused late call (`'expired'`) goes to `onFailure`, which the * composer puts on the serving agent's stream. `reported(err)` lets the * composer tell a hook that merely re-threw an operation's own failure from a * hook that failed for a reason of its own — one failure, counted once. * * Pattern: Proxy (a lifetime-bounded view over the `ctx.artifacts` capability). */ import { type ArtifactEventSink } from '../artifacts/capability.js'; import type { ArtifactScope, ArtifactStore } from '../artifacts/types.js'; import { TurnArtifactsExpiredError } from './errors.js'; import type { TurnArtifacts } from './types.js'; /** The five verbs of the capability, by name — what a refusal names. */ type TurnVerb = TurnArtifactsExpiredError['op']; /** One failure of the hand-over this module can see. */ export type TurnArtifactsFailure = { readonly cause: 'operation'; readonly op: TurnVerb; readonly error: unknown; } | { readonly cause: 'expired'; readonly op: TurnVerb; }; /** What one turn's hand-over is built from. */ export interface TurnArtifactsSource { /** The serving agent's store. Absent ⇒ `{ bound: false, reason: 'no-store' }`. */ readonly store: ArtifactStore | undefined; /** This request's redemption scope. Absent ⇒ `{ bound: false, reason: 'no-session' }`. */ readonly scope: ArtifactScope | undefined; /** The run this turn executed, captured when the binding is built — its `origin`. */ readonly runId: string | undefined; /** Where the binding's facts go (the hosting door's adapter onto the record). */ readonly onEvent: ArtifactEventSink; /** Where a failed operation or a refused late call is reported. */ readonly onFailure: (failure: TurnArtifactsFailure) => void; } /** One turn's hand-over: the value the host is given, and the ways to end it. */ export interface OpenTurnArtifacts { readonly turn: TurnArtifacts; /** Wait for every operation started through the binding — including one * started while this waits. Never rejects; resolves at once when nothing * was bound. The composer races it against its bound. */ drain(): Promise<void>; /** Refuse every later call by name. Idempotent. */ revoke(): void; /** Was this error already reported as a failed operation? */ reported(err: unknown): boolean; } /** * Open this turn's hand-over — bound to `scope`, or the stated reason there is * nothing to bind. `'no-session'` is checked first: with no session there is no * scope to name at all, whatever the store. * * @internal — the composer's; hosts receive the `TurnArtifacts` value only. */ export declare function openTurnArtifacts(source: TurnArtifactsSource): OpenTurnArtifacts; export {};