UNPKG

agentfootprint

Version:

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

125 lines • 5.73 kB
"use strict"; /** * displayName — the ONE rule for a tool's `displayName` (the app's own words * for what a call of the tool does, read by a person), and the ONE way the * library names a set of calls in those words: the declared ones named, the * rest counted. * * Pattern: two pure functions and one small reader, no state. * Role: Leaf. Asked for the RULE by `tools/tools.ts` · `defineTool` and by * the registration funnel (`core/agent/buildToolRegistry.ts`), and for * the NAMING by the two texts the library appends after an answer — * the standing line (`core/agent/assessment/compose.ts`) and the * limits block (`semantics/coverage/answer.ts`). Those two cannot * import each other, so the naming lives here and they cannot name * the same calls in two different ways. * Emits: N/A. * * ## Why only the app's words * * A tool's registered name is the MODEL's vocabulary (`load_attachment`), and * an app may forbid showing it to the people it serves. This module never * prints it: a call is named only by the words its tool declared, and a call * whose tool declared none is counted ("and 1 other call"), never named. A * caller that gets `undefined` back says what it always said. Not every * surface asks this module yet — the time layer's limits lines and the * answer account's rows still print the registered name (a known follow-up). * * ## Why the plain-line rule * * The words reach a person verbatim, after an answer, so they are held to the * rule every declared label a report prints is held to (`lib/plainLine.ts` · * `plainLineProblem`: no control, line or paragraph separator, or invisible * format character — a bidi override would reverse what the person reads), * judged BEFORE the length, and capped at the 60 characters a skill's `title` * and an account label are capped at. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.namedCalls = exports.readDisplayName = exports.displayNameProblem = exports.MAX_NAMED_CALLS = exports.MAX_DISPLAY_NAME_CHARS = void 0; const plainLine_js_1 = require("./plainLine.js"); /** The longest display name a person reads — the cap every declared label a report prints has. */ exports.MAX_DISPLAY_NAME_CHARS = 60; /** * At most this many declared names in one phrase; any further call is counted * with the calls whose tools declared none. A line for a person, not a list. */ exports.MAX_NAMED_CALLS = 3; /** * Why `value` is not a display name, or `undefined` when it is — judged on the * trimmed text (the text a person would read). `label` names the field in the * sentence, so a refusal says where it was written. */ function displayNameProblem(label, value) { if (typeof value !== 'string' || value.trim().length === 0) { return (`${label} must be a non-empty string — the app's own words for what a call of this ` + `tool does, read by a person (got ${typeof value === 'string' ? JSON.stringify(value) : typeof value}). Omit it to ` + 'leave the call unnamed.'); } const text = value.trim(); const notPlain = (0, plainLine_js_1.plainLineProblem)(label, text); if (notPlain !== undefined) return notPlain; if (text.length > exports.MAX_DISPLAY_NAME_CHARS) { return `${label} is ${text.length} characters; the limit is ${exports.MAX_DISPLAY_NAME_CHARS}.`; } return undefined; } exports.displayNameProblem = displayNameProblem; /** * The display name a person may read — trimmed — or `undefined`. A value the * rule refuses is never printed: a tool assembled by hand, or served by a * ToolProvider after the agent was built, is read through this too. */ function readDisplayName(value) { return displayNameProblem('displayName', value) === undefined ? value.trim() : undefined; } exports.readDisplayName = readDisplayName; /** * The calls, named in the app's words — each distinct declared name once, in * the calls' order, at most {@link MAX_NAMED_CALLS}; every other call counted, * with `alsoUnnamed` (calls the record does not name at all). `calls` holds one * entry per CALL (the caller has already said each call once), each with its * tool's registered name — the key a declared name is looked up by, never * printed: * * - `loading the attached data` * - `loading the attached data and searching the catalog` * - `loading the attached data and 1 other call` * * `undefined` when no call's tool declared a name: the caller then says what * it always said — the registered name is never the fallback. * * @example * ```ts * namedCalls( * [{ toolCallId: 'c1', toolName: 'load_attachment' }, { toolCallId: 'c2', toolName: 'list_runs' }], * 0, * (tool) => (tool === 'load_attachment' ? 'loading the attached data' : undefined), * ); // 'loading the attached data and 1 other call' * ``` */ function namedCalls(calls, alsoUnnamed, displayNameOf) { const named = []; let rest = alsoUnnamed; for (const call of calls) { const words = displayNameOf(call.toolName); if (words === undefined) rest += 1; else if (named.includes(words)) continue; else if (named.length < exports.MAX_NAMED_CALLS) named.push(words); else rest += 1; } if (named.length === 0) return undefined; const parts = rest > 0 ? [...named, `${rest} other ${rest === 1 ? 'call' : 'calls'}`] : named; return parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`; } exports.namedCalls = namedCalls; //# sourceMappingURL=displayName.js.map