UNPKG

agentfootprint

Version:

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

107 lines (106 loc) • 5.14 kB
/** * toolChoice/pick — ask the classifier which tool, and decide what is served. * * Pattern: a pure question builder (`toolChoiceQuestion`) + one async asker * that never throws (`pickTools`) + one pure narrowing decision * (`narrowServed`) — the `findings/judge.ts` split, with the * serving half kept pure so every skip reason is testable without * a run. * Role: core/ layer. Reached ONLY from `slots/buildToolsSlot.ts · * composeStage` through `import()` under `.toolChoice()` — the * optional-family law of docs-next's site budget (the * `toolCalls.ts · judgeLanded` precedent): an agent that never * asked never loads this module. * * WHAT IS ASKED. One `choice` question, id `TOOL_CHOICE_QUESTION`, whose * criteria are the OFFERED tools by name with their own descriptions — * declared before the call, the same law as declared tags and routes. The * state is the user's message and, when a skill is active, its id; nothing * the library wrote. The doors are not candidates: a door is served whatever * the classifier says, so asking about it would only dilute the distribution. * * WHAT IS DECIDED. `narrowServed` serves the top-N of the provider's ranking * plus the doors, in the merged wire's order, and the full wire — with the * reason on the row — when the classifier failed or scored fewer than N * names (`unavailable`), when fewer than N + 1 candidates were offered * (`too-few` — there is nothing to leave out), on the call right after a * miss (`after-miss` — the model just showed the ranking was wrong), and on * the wrap-up call (`wrap-up` — every tool comes off at assembly anyway). */ import type { Classifier, ClassifyRequest } from '../../../classify/types.js'; import { type NarrowSkipReason, type ToolChoiceEntry, type ToolChoiceScore } from './types.js'; /** One offered tool as the classifier reads it. */ export interface ToolCandidate { readonly name: string; readonly description: string; } /** * Build the classifier request for one call. Pure. `criteria` is `{ name: * description }` over the candidates in offered order; an empty description * is sent as the empty string, never invented. */ export declare function toolChoiceQuestion(userMessage: string, currentSkillId: string | undefined, candidates: readonly ToolCandidate[]): ClassifyRequest; /** The classifier's answer for one call, before the serving decision — or the failure. */ export type Pick = { readonly ok: true; readonly classifier: { readonly name: string; readonly model: string; }; readonly ranked: readonly ToolChoiceScore[]; readonly chosen?: string; readonly confidence: number; readonly usage?: { readonly inputTokens: number; readonly outputTokens: number; }; readonly latencyMs: number; } | { readonly ok: false; readonly classifier: { readonly name: string; }; readonly status?: number; readonly message: string; readonly latencyMs: number; }; /** * Ask the classifier once. Never throws: a provider failure, a network * error, an abort, a reply with no `choice` answer under the question id — * all come back as `ok: false` with the latency spent, because the pick is * advisory and its absence must never cost the run its answer. */ export declare function pickTools(classifier: Classifier, input: { readonly userMessage: string; readonly currentSkillId?: string; readonly candidates: readonly ToolCandidate[]; }, signal?: AbortSignal): Promise<Pick>; /** What `narrowServed` decides for one call. */ export interface Narrowing { /** The names to commit, in the merged wire's order. */ readonly served: readonly string[]; readonly narrowed: boolean; readonly narrowedSkipped?: NarrowSkipReason; } /** * Decide what is served for one call. Pure. `wire` is the merged list's * names in order; `doors` every name that is served whatever the ranking * says (`ALWAYS_SERVED_TOOLS` plus the app's own); `top` is N, or * `undefined` for an advisory-only agent (the full wire, `narrowed: false`, * no reason — nothing was skipped because nothing was asked). */ export declare function narrowServed(input: { readonly wire: readonly string[]; readonly doors: ReadonlySet<string>; readonly pick: Pick; readonly top: number | undefined; /** This call's iteration — `after-miss` reads the outcome row at `iteration - 1`, never merely the last row on the record. */ readonly iteration: number; /** The rows already on the record — the immediately-prior iteration's outcome is read for `after-miss`. */ readonly prior: readonly ToolChoiceEntry[]; readonly wrapUpAsked: boolean; /** The run's tools are off after the evidence revision (an enforcing answer validation). */ readonly toolsOff?: boolean; }): Narrowing; /** Every name served whatever the ranking says: the framework's doors plus the app's own. */ export declare function alwaysServedNames(extra: readonly string[] | undefined): ReadonlySet<string>;