UNPKG

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
/** * 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[];