UNPKG

agentfootprint

Version:

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

64 lines • 3.2 kB
/** * frontendTools — tools the PAGE runs (AG-UI "frontend tools"), as tools an * agent can be given. * * A frontend tool does its work in the browser: a date picker, a confirmation * dialog, a chart the person adjusts. The server never runs it. Its tool here * has no body: a call PAUSES the run, the page receives the call * (`RUN_FINISHED { outcome: success, pendingToolCallIds }`), runs it, and sends * the result as a `tool` message in its next request, which continues the * paused run with that result as the call's answer. * * WHAT IT CAN NEVER DO. The tool runs nothing on the server, so it holds no * server privilege: no credential (`needs`), no artifact (`wants`), no * capability. Its NAME and its contract (description, input schema) are the * APP's — `agUi({ frontendTools })` allow-lists it — never the page's: a * description the model reads is an instruction, and a page does not get to * write the agent's instructions. A page that declares a tool the app did not * list is refused by name. Its result is the page's word, read by the model * like any tool result. */ import { defineTool } from '../tools/tools.js'; import { pauseHere } from '../core/pause.js'; /** The key a frontend tool's pause carries — how a host knows the run waits on the page. */ const FRONTEND_TOOL_PAUSE = 'agUiFrontendTool'; /** A tool name every provider accepts. */ const NAME = /^[A-Za-z0-9_-]{1,64}$/; /** * Check the app's frontend tools at construction and return, per name, how to * build the tool an agent is given — fresh for every request. * * @throws TypeError naming the tool when its name or contract is not usable. */ export function frontendToolBuilders(declared) { const builders = new Map(); for (const [name, contract] of Object.entries(declared ?? {})) { if (!NAME.test(name)) throw new TypeError(`agUi(): frontend tool '${name}' — a tool name is letters, digits, _ and - (at most 64).`); if (contract === null || typeof contract !== 'object' || typeof contract.description !== 'string' || contract.description.trim().length === 0 || contract.inputSchema === null || typeof contract.inputSchema !== 'object') throw new TypeError(`agUi(): frontend tool '${name}' needs a description and an inputSchema — the app's ` + `own contract, which is what the model is shown.`); const { description } = contract; const inputSchema = structuredClone(contract.inputSchema); builders.set(name, () => defineTool({ name, description, inputSchema, // Runs nothing here: the page runs it, and its result continues the run. execute: async () => pauseHere({ [FRONTEND_TOOL_PAUSE]: name }), })); } return builders; } /** Does this pause wait on the page — a frontend tool's call? Read from the pause's own data. */ export function isFrontendToolPause(pauseData) { return (typeof pauseData === 'object' && pauseData !== null && typeof pauseData[FRONTEND_TOOL_PAUSE] === 'string'); } //# sourceMappingURL=frontendTools.js.map