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