UNPKG

agentfootprint

Version:

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

353 lines (352 loc) • 19.4 kB
/** * arguments/ask — the ONE typed ask per batch for the values `ask` rules need: * its fields, its declaration, the binding of the person's answer, the re-ask. * * Pattern: Walker, pure. Every function here is a function of what it is * handed — the batch's calls, the rules of the implementation that * will run (`ToolOf`), the ask's own state — and nothing reads a * clock, a model or scope. The stage glue (`stages/argumentAsk.ts`) * reads and writes scope and raises the pause. * Role: core/ layer leaf of the inputs layer (honesty layer 2). Imports * nothing from `findings/` (the folder's one-way law). * Emits: N/A. * * ## One ask for the whole batch, before anything in it runs * * The layer's Resolve stage names, per call, the `ask` arguments it left out * (`ArgumentResolution.ask`). They become ONE typed ask (`tools/inputRequest.ts`, * the door `requestInput` uses): one field per distinct (tool, argument), bound * to every call of the batch that needs it. Two PERIOD arguments of different * tools share one field when their spellings convert (`lookback` ↔ * `signed-lookback`) and their choices are the same periods — asking twice for * one period would ask the person what they already answered. An `iso-range` * never merges. * * - **Field ids are positional** (`f1`, `f2`, …) per declaration — never a * joined `tool.argument` string, which two pairs could spell alike (the * injective-key law). The (tool, argument, calls) behind each id rides the * declaration's `context` under the library's reserved key. * - **A value the model READ into the person's words** (declared sources: the * quote was found in the person's messages, the value is not in it) is asked * too, and its field's `context` entry carries those words as `quoted` — * the person's own words, never the model's value, and never while a tool * in reach can hide arguments (`resolve.ts` · `quotesMayShow`). * - **A fixed library question.** Each author's question is its field's * `description`; joined questions could break the 4096-character bound at * pause time although each passed it at definition. * - **Nothing the model proposed rides the ask** — no `supplied`, no value, no * choice the model wrote. * - **More than 32 fields** (the typed ask's limit) go in rounds of 32. * * ## The answer, re-checked, and a bounded re-ask * * The typed ask checks type and enum only, so an integer asked as a number can * come back `2.5`, and a string can break its `pattern`. Each answer is judged * against the PROPERTY's own schema of every argument it binds to, read off the * implementation that will run, through the one validator's honest subset * (`toolArgsValidation.ts` · `validatePropertyValue`: type, integer, enum, * `pattern`, `minLength`, `maxLength` — numeric `minimum` / `maximum` are NOT * judged, there or anywhere). One that fails is not bound: its rows say * `asked: 'invalid-answer'` and the field is asked again with a second fixed * question — at most `MAX_ASK_ROUNDS` times per field; after that, the calls * that needed it are refused. */ import { type AwaitingInput, type InputField, type InputRequestDeclaration, type InputValue } from '../../../tools/inputRequest.js'; import { type PeriodFactProblem, type PeriodFacts } from '../../../time/convert.js'; import { type InstantText } from '../../../time/instant.js'; import { type TimePolicy } from '../../../time/resolve.js'; import type { TimeAskMessages } from '../../../time/ask.js'; import { type CallWindowRow, type TimeAnswerRow, type TimeReadingRow } from '../../../time/rows.js'; import { type ZoneName } from '../../../time/zone.js'; import { convertSpelling, type PeriodSpelling } from '../../../tools/declare.js'; import { ARGUMENT_ASK_KIND, ASK_CONTEXT_KEY } from './askMarker.js'; import type { ArgumentFill, BatchCall, ToolOf } from './resolve.js'; import { type ArgumentRow } from './rows.js'; export { ARGUMENT_ASK_KIND, ASK_CONTEXT_KEY }; /** How many times one field may be put to the person before its calls are refused. */ export declare const MAX_ASK_ROUNDS = 3; /** Fields per declaration — the typed ask's own limit (`tools/inputRequest.ts`). */ export declare const MAX_ASK_FIELDS = 32; /** The declaration's `context` bound — the typed ask's own. */ export declare const ASK_CONTEXT_CHARS = 16384; /** The declaration id — the author's reusable id; each round is stamped with its own `requestId`. */ export declare const ARGUMENT_ASK_ID = "agentfootprint.arguments"; /** The question of a first ask. Each field's `description` is its author's question. */ export declare const ARGUMENT_ASK_QUESTION = "Before the next step can run, a few values are needed. Each field says what it is for."; /** The question of a re-ask, after an answer that did not fit the tool's own schema. */ export declare const ARGUMENT_REASK_QUESTION = "One answer did not fit what the tool accepts. Please answer that field again."; /** One (tool, argument) an ask field's answer binds to, and the calls that need it. */ export interface AskMember { readonly toolName: string; readonly argument: string; /** Set on the argument `Tool.period` names. */ readonly period?: true; /** The member's declared spelling, when its field is a shared period. */ readonly spelling?: PeriodSpelling; /** Every call of the batch that needs this argument asked, in batch order. */ readonly toolCallIds: readonly string[]; /** * Under declared sources: the person's own words a READING was made of * (the first call's that carried one) — shown with the field, never the * model's value. */ readonly quoted?: string; } /** One field of the batch's ask. */ export interface AskField { /** The typed ask's type — `integer` asks as `number`; the re-check judges the integer. */ readonly type: InputField['type']; /** The first member's question — the field's `description`. */ readonly question: string; /** The first member's choices' values — the field's `enum`. Absent: a free field. */ readonly choices?: readonly InputValue[]; /** The first member's spelling, when the field is a period shared across spellings. */ readonly spelling?: PeriodSpelling; /** The (tool, argument) pairs the answer binds to; more than one only for a shared period. */ readonly members: readonly AskMember[]; /** * Set on the ONE field that asks which window the person's own words meant * (the lazy word-driven ask, time design § 5.2, § 6.3): its members are the * period arguments of every call that left its period out while the turn's * one mention was still open; the answer is a window (or, first, the zone * the person meant by an abbreviation), converted into each call's form. */ readonly window?: WindowAskField; } /** One call's values from a bound window — and whether they read a WIDER window (no form held it exactly). */ export interface WindowFill { readonly values: Readonly<Record<string, unknown>>; readonly wider?: 'reads-more' | 'tool-trims'; } /** The time half of a window field — the typed ask's `format`, its labels, and the mention asked about. */ export interface WindowAskField { readonly format: 'time-range' | 'zone'; /** One label per choice (`time-range` with choices only), in the reader's locale. */ readonly labels?: readonly string[]; /** Per choice, the zone the candidate was read in — a wall or date form is written in it. */ readonly zones?: readonly ZoneName[]; /** The mention's index on its `time-reading` row. */ readonly mention: number; } /** Per field (same positions as the fields): how often asked, and how it settled. */ export interface AskProgress { /** How many declarations this field has been put in. */ readonly rounds: number; /** The bound answer, raw, in the field's own spelling. */ readonly answer?: InputValue; /** The last answer did not fit the property's schema and the field is to be asked again. */ readonly invalid?: true; /** Asked `MAX_ASK_ROUNDS` times without an answer that fits — its calls are refused. */ readonly exhausted?: true; /** What the last answer that did not fit failed — the property schema's own expectation, never the answer. */ readonly expected?: string; /** * A window field only: the bound window converted into each call's form, by * call id — the values the call runs with (raw working state, never a row). */ readonly fills?: Readonly<Record<string, WindowFill>>; /** A window field only: the zone was answered and a follow-up question about the same words is to be asked. */ readonly next?: true; } /** The ask now out, waiting for the person's answer. */ export interface AskWaiting { readonly requestId: string; /** The positions (into `fields`) of this declaration's fields, in `f1…fn` order. */ readonly fieldIndexes: readonly number[]; /** The stamped ask, exactly as the pause carries it — the answer is validated against it. */ readonly awaiting: AwaitingInput; } /** * The batch ask's working state (`AgentState.argumentAsk`), written by the * dispatch stage only. It holds the RAW answers bound so far, because the calls * must run with them — the same class as `argumentResolutions`: never a row, an * event or a lens view. */ export interface ArgumentAskState { /** The batch this ask belongs to — the iteration its calls were emitted in. */ readonly iteration: number; readonly fields: readonly AskField[]; readonly progress: readonly AskProgress[]; /** Present exactly while a question is out; absent once every field settled. */ readonly waiting?: AskWaiting; } /** One call's `ask` arguments, as the layer's Resolve stage named them. */ export interface CallAsk { readonly toolCallId: string; readonly ask: readonly string[]; /** Under declared sources: the person's words a reading was made of, per argument. */ readonly quoted?: readonly { readonly argument: string; readonly quote: string; }[]; /** Under `.time({ reader })`: the call left its period out while the turn's one mention was open. */ readonly window?: true; } /** * The lazy word-driven ask's plan (time design § 5.2): the question the open * mention needs (`time/readingAsk.ts` · `timeAskOf`) and its mention index. * Handed by the stage glue only when the turn's one mention is open. */ export interface WindowAskPlan { readonly question: string; readonly format: 'time-range' | 'zone'; readonly choices?: readonly string[]; readonly labels?: readonly string[]; readonly zones?: readonly ZoneName[]; readonly mention: number; /** The turn's clock — a choice outside a member tool's declared facts is not offered (§ 6.3). */ readonly now: InstantText; /** The turn's clock zone — a choice with no zone of its own is converted in it. */ readonly zone?: ZoneName; /** The app's `.time({ zone })`, for a form that declares `wallZone: 'app'`. */ readonly appZone?: ZoneName; } export { convertSpelling }; /** * Whether the period field `b` may share `a`'s field: both declare a spelling * the library converts, both declare choices, and the choices are the SAME * periods under the conversion — so every choice the person can pick is a * valid choice of each argument, whichever member's spelling it is read in. */ export declare function periodsShareField(a: AskField, b: AskField): boolean; /** * The batch ask's fields: one per distinct (tool, argument) the calls left out, * in first-seen batch order, each bound to every call that needs it; then a * period field merged into an earlier one it shares (`periodsShareField`). An * argument whose rule cannot be read here is left out — the dispatch re-read * refuses its call. */ export declare function planAskFields(asks: readonly CallAsk[], calls: readonly BatchCall[], toolOf: ToolOf, windowPlan?: WindowAskPlan): AskField[]; /** A fresh state for a batch's fields: nothing asked yet. */ export declare function initialAskState(iteration: number, fields: readonly AskField[]): ArgumentAskState; /** * The next declaration's fields: every field to ask AGAIN (an answer that did * not fit) first, on their own, under the re-ask question; else the next * never-asked fields, up to `MAX_ASK_FIELDS`. `undefined` when every field has * settled — bound or exhausted. */ export declare function nextAskRound(state: ArgumentAskState): { readonly fieldIndexes: readonly number[]; readonly reask: boolean; } | undefined; /** A JSON object — the shape the host hook must return. */ export type AskContextObject = Readonly<Record<string, unknown>>; /** * The host's own context for the ask (`AgentOptions.argumentAskContext`), * judged: a plain object that does not use the library's reserved key. Refused * — a `TypeError` naming the hook — rather than repaired or dropped. */ export declare function judgeAskContextHook(value: unknown): AskContextObject; /** * The typed ask for one round: the fixed question, one positional field per * field of the round, and the reserved `context` listing the (tool, argument, * calls) behind each id — within 16384 characters, so a round that would not * fit carries fewer fields (the rest go in the next round) and a single field * bound to more calls than fit lists the first ones and counts the rest * (`moreCalls`). Passes `validateInputDeclaration`. */ export declare function argumentAskDeclaration(state: ArgumentAskState, round: { readonly fieldIndexes: readonly number[]; readonly reask: boolean; }, host?: AskContextObject): { readonly declaration: InputRequestDeclaration; readonly fieldIndexes: readonly number[]; }; /** `state` with the round's fields counted as asked and the question out. */ export declare function withWaiting(state: ArgumentAskState, waiting: AskWaiting): ArgumentAskState; /** Whether a stored library ask is the one this state is waiting on (the reserved marker). */ export declare function isArgumentAsk(awaiting: AwaitingInput | undefined): boolean; /** * The person's answer to the ask now out, as field values by id. Accepts what * `Agent.resume` hands the run (`{ status: 'input_received', requestId, values }`, * already checked at its door) and a raw `InputResponse` (`{ requestId, values }`) * from a runner that resumes the executor directly — both judged by the SAME * validator (`tools/inputRequest.ts` · `applyInputResponse`): the request id, the * declared fields, their types and choices. Throws `InputRequestError` on a * reply to another request or a malformed one, and on a PARTIAL reply (only * `Agent.resume`'s door keeps a partial answer, without running anything); a * cancellation throws the door's `TypeError`. */ export declare function readAskAnswer(waiting: AskWaiting, input: unknown): Readonly<Record<string, InputValue>>; /** The answer in a member's own spelling — itself, unless the field is a shared period. */ export declare function memberValue(field: AskField, member: AskMember, answer: InputValue): InputValue | undefined; /** * Whether `answer` fits EVERY member's property schema (in the member's own * spelling) — the re-check the typed ask cannot do: an integer asked as a * number, a `pattern`, a string length (never a numeric bound — the one * validator ignores `minimum` / `maximum`). The rule is re-read through the * same resolver. * `expected` names what a misfit failed — the schema's expectation, never the * answer. */ export declare function checkAnswer(field: AskField, answer: InputValue, toolOf: ToolOf, time?: AskTime): { readonly fits: boolean; readonly expected?: string; }; /** Under `.time()`: the turn's clock and the app's zone — what a period answer is judged against. */ export interface AskTime { readonly now: InstantText; readonly appZone?: ZoneName; /** The turn's clock zone — a window answered with no zone of its own is written in it. */ readonly zone?: ZoneName; /** * The turn's clock zone is UNKNOWN (`zoneSource: 'unknown'`, G15): `zone` is * only the UTC spelling, and a zone answer is the person's own zone — the * mention is re-read in it. */ readonly zoneUnknown?: true; /** * Under `.time({ reader })`: the open mention's row, the policy and the * whole catalog — what a zone answer re-reads the mention with. */ readonly reading?: { readonly row: TimeReadingRow; readonly policy: TimePolicy; readonly messages: TimeAskMessages; }; } /** * What an answer for a period argument must be, in the words the refusal of * an exhausted ask names it (`serve.ts` · `unansweredRefusal`'s `expected`) — * the tool's own fact, never the person's answer. */ export declare function factExpectation(problem: PeriodFactProblem, facts: PeriodFacts): string; /** What binding one answer did: the new state and the rows it files. */ export interface BoundAnswer { readonly state: ArgumentAskState; /** * `answered` rows for bound fields; `asked: 'invalid-answer'` rows for fields * asked again or exhausted; and, for a bound WINDOW field, the `time-answer` * row that settles its mention for the rest of the turn (`time/rows.ts`) * and one `call-window` row per call it filled — `filled` from the person's * answered window, exactly or wider (`sent`, `differs.extra`). */ readonly rows: readonly (ArgumentRow | TimeAnswerRow | CallWindowRow)[]; } /** * Bind the person's answer to the ask now out: each field whose value fits * every member's schema is bound (`answered` rows, one per (call, argument), * the value in the tool's own argument view, `free` for a free-text string * field); each that does not is marked to ask again (`invalid-answer` rows), * or — asked `MAX_ASK_ROUNDS` times already — exhausted. The question is no * longer out afterwards. */ export declare function bindAnswer(state: ArgumentAskState, values: Readonly<Record<string, InputValue>>, calls: readonly BatchCall[], toolOf: ToolOf, stamp: { readonly turn: number; readonly iteration: number; }, time?: AskTime): BoundAnswer; /** What a window answer must be when no period form of a tool holds it. */ export declare const WINDOW_FORM_EXPECTATION = "a window one of the tool's declared period forms can hold"; /** What a zone answer must be when the person's words name no time in it. */ export declare const WINDOW_ZONE_EXPECTATION = "a time zone in which the time the person wrote exists"; /** What the settled ask hands the batch, per call: the answered fills and the exhausted arguments. */ export interface SettledCall { readonly fills: readonly ArgumentFill[]; /** The arguments whose answers never fit — the call is refused — with what they failed. */ readonly exhausted: readonly { readonly argument: string; readonly expected?: string; }[]; } /** Per call id, what the settled ask resolved for it (only calls the ask touched). */ export declare function settledCalls(state: ArgumentAskState): ReadonlyMap<string, SettledCall>;