UNPKG

agentfootprint

Version:

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

155 lines • 8.39 kB
/** * request — an AG-UI request (`RunAgentInput`) read as UNTRUSTED browser input, * and the one turn it may ask for. * * Three steps. The first is the app's to replace; the other two are the CORE, * and run whichever reader produced the request — so a reader can map another * body onto a turn, and never weaken a rule: * * the READER a body → an {@link AgUiTurnRequest}. The default takes * the body as an AG-UI `RunAgentInput`; a gateway that * sends its own body shape passes its own * ({@link AgUiRequestReader}). Mapping only — it decides * nothing below; * readAgUiRequest what a custom reader produced, copied as plain JSON (so * a getter or a proxy cannot answer the check one way and * the plan another; the default reader's body is parsed * JSON already and is read as it is), then * readAgUiRunInput its SHAPE, field by field, and * the ids' FORMAT and length — nothing here is trusted, * nothing the server holds is read; * planAgUiTurn the turn, from the request AND what the server holds * for the bound conversation. * * The core's steps refuse by NAME (a `WireRequestRefusal`: its own status and * code; the field named, the value never quoted back). * * WHAT A REQUEST MAY DECIDE, and what it may not: * - the newest USER message is the run's message. Nothing else the browser * sends is ever conversation: its assistant and tool messages are its own * copy of what the server said, and the server's copy is the one kept * (`sessions`, or the agent's memory under the bound session). A system or * developer message is refused — the agent's instructions are its own; * - an ANSWER is taken only for the pause the server is keeping, matched to * the ids that pause handed out (`AgUiSuspension`): a resume entry naming * its interrupt, or the tool message answering the frontend tool call it is * waiting on. Any other tool result is refused — never folded in. A * decision answering a consent gate's interrupt is named for the request * that interrupt stands for (`answeringRequest`) — the page never sees the * request id, and the matched interrupt is what binds its answer; * - a frontend tool runs only when the app allow-listed its NAME — with the * app's own description and schema, never the page's (a description the * model reads is an instruction) — and only on a host that keeps the paused * run until the page answers; * - `state` and `context` reach the agent only through the app's validators; * without one, a request carrying either is refused; * - identity, credentials and redaction never come from a request: the host * binds the thread to the caller the app authenticates (`ThreadBinder`). * `forwardedProps` is never read. */ import type { HeldConversation, PlanFacts, ProtocolPlan, ProtocolRequestFacts, ThreadBinding } from '../hosting/protocolAdapter.js'; import type { Tool } from '../tools/tools.js'; import type { AgUiMessage, AgUiResumeEntry, AgUiRunInput, AgUiSuspension } from './types.js'; /** The longest thread or run id read — longer is refused, never truncated. */ export declare const AG_UI_MAX_ID_LENGTH = 128; /** * Read one AG-UI request body — its shape and its ids — refusing what the * protocol does not allow by the field's NAME, never by its value. * * @throws WireRequestRefusal (400, `ERR_INVALID_RUN_INPUT`) */ export declare function readAgUiRunInput(body: unknown): AgUiRunInput; /** * What a request reader hands the core: the one turn a request asks for, in * AG-UI's terms. `AgUiRunInput` is one (a `RunAgentInput` as sent); a reader * for another body builds one from it — `tools` and `context` may be left out. * * It carries no identity, credential or redaction setting, and the core reads * nothing else a reader adds: the caller is bound by the host's `ThreadBinder`, * a credential reaches a run only through the host's `sealRequest`. */ export interface AgUiTurnRequest { /** The conversation the request NAMES — format-checked, never a key. */ readonly threadId: string; /** The run id the stream names this run by — format-checked. */ readonly runId: string; readonly parentRunId?: string; /** * The messages, newest last. Only the newest is read — a user message (the * run's message), or the `tool` message answering the frontend tool call a * paused run waits on. A system or developer message anywhere is refused. */ readonly messages: readonly AgUiMessage[]; /** The frontend tools the caller offers — run only when the app allow-listed the name. */ readonly tools?: AgUiRunInput['tools']; /** Reaches the agent only through the app's `accept.context` validator. */ readonly context?: AgUiRunInput['context']; /** Reaches the agent only through the app's `accept.state` validator. */ readonly state?: unknown; /** Answers to the interrupt a paused run waits on — matched to the server's own record. */ readonly resume?: readonly AgUiResumeEntry[]; } /** * What a request reader is given: the request as the host read it — UNTRUSTED. * The host port's own `ProtocolRequestFacts` (the parsed body, the headers). */ export type AgUiRequestFacts = ProtocolRequestFacts; /** * The request-reading strategy: a body → the turn it asks for. Mapping only; * every rule runs after it, in the core. Refuse a body it cannot read by * throwing a `WireRequestRefusal` (its status and code reach the caller); * anything else it throws is refused as a malformed request, in fixed words. * * @example A gateway that sends `{ conversationId, requestId, text }` * ```ts * const reader: AgUiRequestReader = ({ body }) => { * const b = body as { conversationId?: unknown; requestId?: unknown; text?: unknown }; * return { * threadId: String(b.conversationId), * runId: String(b.requestId), * messages: [{ id: 'gateway-turn', role: 'user', content: String(b.text) }], * }; * }; * ``` */ export type AgUiRequestReader = (request: AgUiRequestFacts) => AgUiTurnRequest; /** The default reader: the body IS an AG-UI `RunAgentInput`. */ export declare const runAgentInputReader: AgUiRequestReader; /** * The core's read of one request: `reader` maps the body, and what it produced * is checked by {@link readAgUiRunInput} — whichever reader it was. A custom * reader's answer is copied as plain JSON first; the default reader's is the * parsed body itself, read exactly as before readers were replaceable. * * @throws WireRequestRefusal — the reader's own refusal, or the core's by name * (400, `ERR_INVALID_RUN_INPUT`). */ export declare function readAgUiRequest(reader: AgUiRequestReader, request: AgUiRequestFacts): AgUiRunInput; /** What a page's `state` or `context` is checked by before the agent may see it. */ export type AgUiValidator = (value: unknown, at: { /** The conversation the request was bound to — whose state this claims to be. */ readonly thread?: ThreadBinding; readonly headers: Readonly<Record<string, string>>; }) => unknown; /** The rules one adapter plans by — built from `agUi()`'s options. */ export interface AgUiTurnRules { /** The frontend tools the app allow-listed, by name: each builds the tool the agent is given. */ readonly frontendTools: ReadonlyMap<string, () => Tool>; readonly accept: { readonly state?: AgUiValidator; readonly context?: AgUiValidator; }; } /** * Plan one turn: the request (read by {@link readAgUiRunInput}) against what * the server holds for its bound conversation. * * @throws WireRequestRefusal — by name, for everything a page may not decide. */ export declare function planAgUiTurn(input: AgUiRunInput, held: HeldConversation, facts: PlanFacts, rules: AgUiTurnRules): ProtocolPlan; /** * A stored suspension record, checked — the store holds bytes; a record that * is not one this adapter wrote is `undefined`, and nothing is matched to it. */ export declare function readAgUiSuspension(value: unknown): AgUiSuspension | undefined; //# sourceMappingURL=request.d.ts.map