UNPKG

agentfootprint

Version:

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

122 lines • 5.99 kB
/** * adapter — AG-UI as a protocol adapter: what `streamHandler` takes to serve * runs to any AG-UI frontend — or to a gateway that speaks AG-UI in its own * request body and its own envelope. * * Three strategies, each replaceable on its own, around one CORE that is not: * - the READER — a request body → the turn it asks for (`reader`; default: * the body is a `RunAgentInput`). Mapping only; * - the TRANSLATION — one run's served events → AG-UI events (`translation`; * default `agUiSerializer`, tuned by `custom` and `reasoning`), one per * request; * - the FRAMING — AG-UI events → bytes (`framing`; default `agUiSse`, * `encoder` for bundles). The host numbers each stream's frames and hands * the number to the framing, so a gateway envelope's sequence is per run. * - the CORE (`request.ts`), whichever reader ran: the request's shape and * ids checked as plain JSON, a system/developer message refused, frontend * tools only by the app's allow-list, `state`/`context` only through the * app's validators (`accept`), an answer only for the pause the server * keeps. No strategy can reach past it. * * Holds no state between requests: its options are read once and frozen, and * every request gets its own translation and its own frontend tools. */ import { agUiSse } from './framing.js'; import { frontendToolBuilders, isFrontendToolPause, } from './frontendTools.js'; import { planAgUiTurn, readAgUiRequest, readAgUiSuspension, runAgentInputReader, } from './request.js'; import { agUiSerializer } from './serializer.js'; /** * The AG-UI protocol adapter. Hand it to a host: * * @example * ```ts * import { memorySessions, streamHandler } from 'agentfootprint/hosting'; * import { agUi } from 'agentfootprint/ag-ui'; * * export const POST = streamHandler({ * agent: ({ tools }) => Agent.create({ provider, model }).tools([...mine, ...tools]).build(), * adapter: agUi(), * threads: { bind: ({ conversation, headers }) => bindToSignedInUser(conversation, headers) }, * sessions: memorySessions(), * }); * ``` * * @throws TypeError at construction for a frontend tool whose name or contract * is not usable, and a validator that is not a function. */ export function agUi(options = {}) { const frontendTools = frontendToolBuilders(options.frontendTools); const accept = Object.freeze({ ...options.accept }); for (const [name, validator] of Object.entries(accept)) { if (validator !== undefined && typeof validator !== 'function') throw new TypeError(`agUi(): accept.${name} must be a function.`); } const rules = Object.freeze({ frontendTools, accept }); const reader = strategy(options.reader, 'reader') ?? runAgentInputReader; const translate = translationOf(options); const framing = framingOf(options); return Object.freeze({ name: 'ag-ui', readRequest: ({ body, headers }) => readAgUiRequest(reader, { body, headers }), conversation: (input) => input.threadId, plan: (input, held, facts) => planAgUiTurn(input, held, facts, rules), awaitsCaller: isFrontendToolPause, open: (input, resumed) => { const resume = resumed === undefined ? undefined : readAgUiSuspension(resumed); return translate({ threadId: input.threadId, runId: input.runId, ...(resume !== undefined && { resume }), }); }, framing, }); } /** A strategy option: absent, or a function — anything else refused by name. */ function strategy(value, name) { if (value !== undefined && typeof value !== 'function') throw new TypeError(`agUi(): ${name} must be a function.`); return value; } /** The translation strategy — the app's, or `agUiSerializer` tuned by `custom` and `reasoning`. */ function translationOf(options) { const { custom, reasoning } = options; const translation = strategy(options.translation, 'translation'); if (translation !== undefined) { if (custom !== undefined || reasoning !== undefined) throw new TypeError('agUi(): `translation` was given with `custom` or `reasoning`, which tune the default ' + 'translation only — your translation decides both itself. Pass one or the other.'); // One translation, one run: a strategy that hands one back twice would // write one run's open messages and ids into another run's stream. const opened = new WeakSet(); return (run) => { const opening = translation(run); if (typeof opening !== 'object' || opening === null || opened.has(opening)) throw new TypeError('agUi(): `translation` must return a NEW translation for every run — one it returned ' + "before would carry another run's state into this one."); opened.add(opening); return opening; }; } return (run) => agUiSerializer({ ...run, ...(custom !== undefined && { custom }), ...(reasoning !== undefined && { reasoning }), }); } /** The framing strategy — the app's, or `agUiSse` (with the `encoder` the app imported). */ function framingOf(options) { const { framing, encoder } = options; if (framing === undefined) return () => agUiSse(encoder === undefined ? {} : { encoder }); if (encoder !== undefined) throw new TypeError('agUi(): `framing` was given with `encoder`, which feeds the default framing only — ' + 'your framing writes its own frames. Pass one or the other.'); if (typeof framing !== 'object' || framing === null || typeof framing.frame !== 'function' || typeof framing.contentType !== 'string') throw new TypeError('agUi(): `framing` must be an SseFraming — { contentType, frame }.'); return () => framing; } //# sourceMappingURL=adapter.js.map