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
JavaScript
/**
* 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