agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
309 lines (308 loc) • 17.3 kB
TypeScript
/**
* arguments/resolve — the inputs layer's checks and its one table, as pure
* functions of what the layer is handed.
*
* Pattern: Walker. Four pure steps, one per stage of the `sf-inputs` subflow
* (`arguments/subflow.ts` wires them to scope): DECLARE (which calls
* carry rules, and which ruled values are missing), VERIFY (where each
* ruled value came from), RECORD (the rows, in the tool's own view),
* RESOLVE (the fills and refusals ToolCalls applies). The same inputs
* always give the same outputs; nothing here reads a clock, a model or
* scope.
* Role: core/ layer, the inputs layer (honesty layer 2). Loaded through
* `import()` when the layer is armed — never on a plain agent's graph.
* Imports nothing from `findings/` (the one-way law).
* Emits: N/A.
*
* ## The table (declared sources unarmed)
*
* | The value | `assume` rule | `ask` rule |
* |-----------------------------------------|---------------------------------|-----------------------------------|
* | missing | `default` — fill the default | `asked: 'missing'` — ask, once per batch |
* | missing, an answer KEPT for it (`kept.ts`) | — | `answered` — fill the person's kept answer |
* | present, equal to the declared default | `default` — run (model's value) | — (an `ask` rule has no default) |
* | present, any other value | `model` — run, flagged | `model` — run, flagged (adopted Q2) |
*
* ## Under declared sources (`.inputsLayer({ argumentSources: true })`, `.findings({ argumentSources: true })`)
*
* A PRESENT value is checked against the source the model declared for it
* (`checks.ts` · `checkSource`) — and so is every free argument a `from`
* entry names, filed with no rule. A missing value is resolved exactly as
* above: it has no source to check.
*
* | The present value | `assume` rule | `ask` rule | no rule |
* |-----------------------------------------------------------------------|----------------------|-----------------------------------|---------|
* | traced — `said` (quote or phrase), `answered`, `result`, `app` | run | run | run, a row |
* | the declared default, not the person's (V1) | run → `default` | — | — |
* | untraced — nothing declared, `assumed`, a hint, a failed claim, a reading | run, flagged | `asked: 'unverified'` — ask | run, a row |
*
* A reading (`said` + `reading`: the person's words were found, the value is
* not in them) ASKS under an `ask` rule — otherwise any exact fragment of the
* person's message would carry any value past the rule — and the ask's field
* shows the person their own words (`quoted`) unless a tool in reach may hide
* arguments (`quotesMayShow`). The model's value never rides the ask.
* "Equal" is the evidence module's same-value rule (`tools/declare.ts` ·
* `sameArgumentValue`). A present value equal to the default is filed as
* `default`, never as the model's own choice: a model that copies a default
* from a description chose nothing (the row's `proposed` tells it from a fill).
* A present value on an `ask` argument is not asked in this version: the model
* cannot yet say where a value came from (declared sources), so every present
* value is unverified, and asking would fire on nearly every call — it runs,
* flagged `model`. A missing one is asked: RESOLVE names it on the call's
* entry (`ask`), and ToolCalls raises the ONE typed ask for the whole batch
* before anything in it runs (`arguments/ask.ts`). A missing one the person
* already answered for a call the batch that asked could not finish — its
* answer KEPT (`kept.ts`: that batch had no second pause to give) — is filled
* with the kept answer instead of asked again, and filed `answered`. A call
* whose tool's rules cannot be read at dispatch is refused, never run unruled
* and never repaired.
*/
import type { InputValue } from '../../../tools/inputRequest.js';
import { type CallWindow, type TurnWindows, type WindowSource } from '../../../time/bind.js';
import type { InstantText } from '../../../time/instant.js';
import type { CallWindowRow } from '../../../time/rows.js';
import type { ZoneName } from '../../../time/zone.js';
import { type SourceCheck, type SourceCorpus } from './checks.js';
import { type RuledToolLike } from '../../../tools/declare.js';
import { type KeptAnswer } from './kept.js';
import { type ArgumentRow } from './rows.js';
import type { CallSources } from './sources.js';
/** One call of the batch, as the model emitted it (its raw arguments). */
export interface BatchCall {
readonly id: string;
readonly name: string;
readonly args: Readonly<Record<string, unknown>>;
readonly argumentsFailure?: import('../../../provider-contract/types.js').LLMResponse['toolCalls'][number]['argumentsFailure'];
}
/** The implementation that will answer a name — the shared dispatch resolver's. */
export type ToolOf = (toolName: string) => (RuledToolLike & {
readonly argumentsFrom?: readonly string[];
}) | undefined;
/** One ruled argument of one call, as DECLARE found it. Identities and a flag — no value. */
export interface PlannedArgument {
readonly argument: string;
readonly rule: 'ask' | 'assume';
readonly period?: true;
readonly missing: boolean;
}
/** One call whose tool carries rules, as DECLARE found it. */
export interface PlannedCall {
readonly toolCallId: string;
readonly toolName: string;
/** The dispatch re-read could not read the rules — the assert's own sentence. */
readonly refused?: string;
readonly ruled: readonly PlannedArgument[];
/**
* Under declared sources: the FREE arguments (no rule) a `from` entry named
* — each is checked and filed with no rule, never filled and never asked
* (adopted Q18). Absent when there are none.
*/
readonly free?: readonly string[];
}
/** One ruled argument, as VERIFY placed it. No value — the rows take the value in the tool's view. */
export interface CheckedArgument {
readonly toolCallId: string;
readonly toolName: string;
readonly argument: string;
/** The argument's rule — absent only on a FREE argument a `from` entry named. */
readonly rule?: 'ask' | 'assume';
readonly period?: true;
/** Where the value came from — absent exactly on an argument to ASK (`asked`). */
readonly source?: 'default' | 'model' | 'answered' | 'said' | 'result' | 'app';
/**
* The library fills the value — the call left the argument out: the declared
* default (`source: 'default'`), or the person's answer KEPT for this
* (tool, argument) (`source: 'answered'`, `kept.ts`).
*/
readonly filled?: true;
/**
* The person is asked, once per batch: the call left an `ask` argument out
* (`missing`), or — under declared sources — its present value is not traced
* to a source the record holds (`unverified`).
*/
readonly asked?: 'missing' | 'unverified';
/**
* The declared-sources check's verdict on a PRESENT value — set exactly when
* the arm is on for the call (`checks.ts` · `checkSource`). Enums, flags, a
* result id and an app label: never the value, never the quote.
*/
readonly check?: SourceCheck;
/**
* The model's quote for this argument may be SHOWN — on the row and on the
* ask (`quotesMayShow`: no tool in reach can hide arguments). Absent → a quote
* reads `'REDACTED'` on the row and rides no ask.
*/
readonly quoteShown?: true;
/**
* Under `.time()`: the library fills the value from the turn's ONE window of
* the person's (`time/bind.ts`) — who that window is. Set with `filled`.
*/
readonly window?: WindowSource;
}
/**
* Under `.time()` (the time layer, step T5a): the turn's windows and clock, as
* the record holds them — the `time-reading` rows and the `clock` row of this
* turn, read by `time/bind.ts` · `turnWindowsOf`. A tool whose period
* declares forms is then filled from the turn's one window, bound to the
* person's window, or recorded as the model's (`callWindowOf`). Absent → the
* arm is off and every call is resolved exactly as before.
*/
export interface TimeArm {
readonly turn: TurnWindows;
/** The turn's frozen clock. */
readonly now: InstantText;
/** The clock's zone. */
readonly zone: ZoneName;
/** The app's `.time({ zone })` — a form declaring `wallZone: 'app'` reads in it. */
readonly appZone?: ZoneName;
}
/**
* The declared sources, as the layer's stages are handed them: each call's
* `from` entries (`sources.ts` · `CallSources`, by call id — a call with no
* entry declares nothing: its tool owns the reserved argument) and, for the
* checks, the corpora (`checks.ts` · `SourceCorpus`). Absent → the arm is off.
*/
export interface SourcesArm {
readonly declared: ReadonlyMap<string, CallSources>;
/** Present from VERIFY on — DECLARE needs only the entries. */
readonly corpus?: SourceCorpus;
/**
* A tool in reach may carry an arguments view — one the agent registers
* carries one (`core/toolShownArgs.ts` · `carriesArgumentView`), or a
* ToolProvider is wired (any provider: its list is known only per
* iteration). Decided once, at build; present only then; read by VERIFY
* (`quotesMayShow`): no quote is shown on a row or an ask.
*/
readonly argumentViews?: true;
}
/**
* One value the library fills into a call, raw: the declared default
* (`default`), the person's answer to the batch ask (`answered`), or — under
* `.time()` — the turn's one window of the person's, converted into the tool's
* form (`window`; `from` says whose window: their words, a model reader's
* reading, a UI control). A window fill of an `object` form is an object.
*/
export type ArgumentFill = {
readonly argument: string;
readonly value: InputValue;
readonly source: 'default';
} | {
readonly argument: string;
/** A window answered for an `object` form is an object (the lazy word-driven ask). */
readonly value: InputValue | Readonly<Record<string, InputValue>>;
readonly source: 'answered';
/** The value was written by the library from the WINDOW the person chose (the lazy word-driven ask). */
readonly window?: true;
/** …and no form held that window exactly, so it reads a wider one (as a `window` fill's `wider`). */
readonly wider?: 'reads-more' | 'tool-trims';
} | {
readonly argument: string;
readonly value: InputValue | Readonly<Record<string, InputValue>>;
readonly source: 'window';
readonly from: WindowSource;
/**
* No form holds the window exactly, so the value reads a WIDER one (step
* T5b): `reads-more` — the call reads parts nobody asked for;
* `tool-trims` — the tool declares `filtersToAsked` and drops them.
*/
readonly wider?: 'reads-more' | 'tool-trims';
};
/**
* What ToolCalls applies to ONE call of the batch the layer resolved: the
* values to fill, or the sentence the call reads instead of running. Working
* state, never a row: it holds the RAW filled value, because the call must run
* with it — the same class as the assistant message's own arguments. Stamped
* with the batch's iteration, so an entry is only ever applied to the batch it
* was resolved for.
*/
export interface ArgumentResolution {
readonly toolCallId: string;
readonly iteration: number;
readonly fills?: readonly ArgumentFill[];
readonly refused?: string;
/**
* The `ask`-ruled arguments to ask the person for, in the rule's declared
* order — names only: the ones this call left out, and, under declared
* sources, the ones whose present value is not traced to a source the record
* holds. ToolCalls asks the person for every one of them across the batch,
* ONCE and before anything runs (`arguments/ask.ts`), and fills the answers
* as `answered`.
*/
readonly ask?: readonly string[];
/**
* Under declared sources: for an argument asked because the model READ a
* value into the person's words (`said` + `reading`), those words — the
* quote the model declared, found in the person's own messages — so the ask
* can show the person what was read. Never the model's value, and never
* while a tool in reach may hide arguments (`quotesMayShow`).
*/
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 still OPEN (`time/bind.ts` · `not-filled` /
* `open-reading`) — its period arguments in `ask` are asked as that
* mention's window, in one field for the batch (`ask.ts` · `planAskFields`,
* the lazy word-driven ask of time design § 5.2), not one by one.
*/
readonly window?: true;
}
/**
* Every call of the batch whose tool carries rules — read through `rulesOf`,
* the dispatch re-read of the SAME assert `defineTool` runs — with each ruled
* argument marked missing or present. A call whose rules cannot be read is
* planned as refused. Calls to tools without rules are not planned — unless,
* under declared sources (`sources`), a `from` entry names one of their
* arguments: that argument is planned FREE, to be checked and filed.
*/
export declare function declareBatch(calls: readonly BatchCall[], toolOf: ToolOf, sources?: SourcesArm): PlannedCall[];
/**
* Under `.time()`: per planned call whose tool's period declares forms, which
* window the call carries (`time/bind.ts` · `callWindowOf`) — a pure
* function of the call, the tool's forms and facts, the turn's windows and the
* model's declared quotes. Empty when the arm is off. Every stage asks it
* afresh (the same inputs, the same answer), so no decision is staged twice.
*/
export declare function timeDecisionsOf(plan: readonly PlannedCall[], calls: readonly BatchCall[], toolOf: ToolOf, time: TimeArm | undefined, sources?: SourcesArm): ReadonlyMap<string, CallWindow>;
/** The `call-window` rows for this batch's decisions, in batch order (`time/rows.ts`). */
export declare function callWindowRowsOf(decisions: ReadonlyMap<string, CallWindow>, calls: readonly BatchCall[], stamp: {
readonly turn: number;
readonly iteration: number;
}): CallWindowRow[];
/**
* Where each ruled value of each planned call came from, by the table above.
* Unarmed (no `sources`), a present value either IS the declared default (an
* `assume` rule) or is the model's own; a missing value on an `ask` rule is
* placed `asked: 'missing'` — or, when this turn KEPT the person's answer for
* that (tool, argument) (`kept`), filled with it and placed `answered`. Under
* declared sources, a PRESENT value — and every free argument a `from` entry
* names — is checked (`checks.ts` · `checkSource`), and an untraced value on
* an `ask` rule is asked about (`asked: 'unverified'`).
*/
export declare function verifyPlan(plan: readonly PlannedCall[], calls: readonly BatchCall[], toolOf: ToolOf, kept?: readonly KeptAnswer[], sources?: SourcesArm, time?: TimeArm): CheckedArgument[];
/**
* One row per checked argument, its value in the tool's OWN argument view
* (`shownArgsOf`) — a fill is shown as the call will run with it, a model's
* value as the model sent it; an argument to ASK has no value yet, so its row
* carries none (under declared sources, the model's proposal rides as
* `proposed`); a kept answer files `answered` (`free` for a free-text field),
* exactly as the answer did when the person gave it. Under declared sources a
* present value's row carries the check's verdict (`claimed`, `matched`,
* `quote`, `failed`, …), and the call's first row carries the count of its
* dropped `from` entries when no basis row does. The raw value never reaches
* a row.
*/
export declare function rowsOf(checked: readonly CheckedArgument[], calls: readonly BatchCall[], toolOf: ToolOf, stamp: {
readonly turn: number;
readonly iteration: number;
}, kept?: readonly KeptAnswer[], sources?: SourcesArm, decisions?: ReadonlyMap<string, CallWindow>): ArgumentRow[];
/**
* What ToolCalls applies: per call, the declared defaults and the kept answers
* to fill and the `ask` arguments to ask the person for (with, under declared
* sources, the person's words a reading was made of), or — for a call whose
* rules could not be read — the sentence it reads instead of running. Nothing
* for a call that runs as the model sent it.
*/
export declare function resolutionsOf(plan: readonly PlannedCall[], checked: readonly CheckedArgument[], toolOf: ToolOf, iteration: number, kept?: readonly KeptAnswer[], sources?: SourcesArm, decisions?: ReadonlyMap<string, CallWindow>): ArgumentResolution[];