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
TypeScript
/**
* 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>;