agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
231 lines (230 loc) • 12.6 kB
TypeScript
/**
* runRedaction — one run's redaction: the policy in force, the rule it is
* decided by, and the two places an event leaves a stage.
*
* Pattern: a per-run object (`createRunRedaction`) opened by the runner that
* owns the executor (`RunnerBase · openRunRedaction`), plus a scope
* registry so the one emit helper can find the run a stage belongs to.
* Role: the Walker half of `src/redaction/` — the wiring every runner's
* `createExecutor` goes through, for every run, policy or not.
*
* WHY EVENTS ARE SERVED AT THEIR SOURCE. A typed event leaves a stage through
* footprintjs's `$emit`, and footprintjs hands that payload to every channel
* at once: the runner's EmitBridge (→ `agent.on`), every recorder attached to
* the executor, the deferred-observer tier, and the executor's own narrative
* (`[emit] name: payload` lines). footprintjs serves a `$emit` payload by its
* NAME only (`emitPatterns`); a selected key inside it reaches all of those as
* it is. Serving it anywhere downstream would be serving it in one channel of
* five. So `emitServed` serves it BEFORE `$emit`, once, and every channel gets
* the same served payload — footprintjs's own rule for a record handed out
* whole: decided once, at the boundary, before any observer sees it.
*
* WHY THERE IS A SECOND, REAL-VALUE PATH. A handful of the library's own
* mechanisms run ON events: the crash checkpoint `resumeOnError` replays
* (`Agent · installCheckpointTracker`), the window strategy's token reading
* (`CompactionMeter`), the reply a hosted agent streams to the person who
* asked (`standingAgent`), the reply `toSSE({ format: 'text' })` streams. Each
* is what the agent computes on or hands back to its caller — the class the
* redaction law never covers (live state, the resume checkpoint, the caller's
* own answer). They read the event as the stage produced it, through
* `EventDispatcher · onRealEvent`, which no consumer can reach. Nothing on
* that path is a record: it is never stored, exported or shown.
*
* WHY A FEW WRITES ARE RELAYED. Some events are DERIVED from scope writes
* rather than emitted: the context recorder turns each slot's injection and
* composition writes into `context.injected` / `slot_composed` /
* `budget_pressure`. The scope channel serves a write of a selected key as the
* placeholder, and an event derived from a placeholder is no event — the
* record would deny the injections happened instead of keeping their content
* out. So those writes go through `setEventSource`, which hands the written
* value to the run (`RunRedaction · takeRealWrite`) as it writes it; the
* recorder derives from that value, and the event it dispatches is served by
* name like every other.
*
* THE RULE. Events are decided by the executor's OWN rule — the one footprintjs
* builds from the policy for the run and installs on every stage context
* (`StageContext · getRedactionRule`) — so a mark the run made (a per-call
* `$setValue(key, value, true)`, a subflow mapper's copy of a selected value
* under a new name) covers the events too. Until the first stage has started
* there is no executor rule to read, so a rule built from the same policy
* stands in: it has marked nothing, and neither has the run.
*/
import { RedactionRule } from 'footprintjs/advanced';
import type { FlowChart, FlowChartExecutor, RedactionPolicy, ScopeFactory } from 'footprintjs';
import { type RunContext } from '../bridge/eventMeta.js';
import type { EventDispatcher } from '../events/dispatcher.js';
import { type RedactionCoverage } from './coverage.js';
import { type EventServing } from './served.js';
/** One run's redaction, as the runner that owns the executor holds it. */
export interface RunRedaction {
/** The policy in force for this run — declared ∪ handed down — or `undefined`. */
readonly policy: RedactionPolicy | undefined;
/** What this run's events are served as. */
readonly serving: EventServing;
/**
* The scope factory for this run's executor: the chart's own factory, with
* every scope it makes tied to this run (source serving and the real-value
* path both find the run through it).
*/
scopeFactoryFor(spec: FlowChart): ScopeFactory;
/**
* Hand the policy to the executor — before `run()` / `resume()` — and mark
* its served snapshot as served under one (`marker.ts`).
*/
applyTo(executor: FlowChartExecutor): void;
/**
* The value the stage `runtimeStageId` wrote under `key` through
* `setEventSource` — the oldest one not yet taken — for a recorder whose
* served write is the placeholder (`ContextRecorder`). `keptOut` says the
* run's rule selects `key` itself: what is DERIVED from the value then keeps
* that verdict (footprintjs's rule for an object written under a selected
* name), so the recorder serves the derived record's content as the
* placeholder and keeps only its structure. `undefined` when none waits: the
* run has no policy (the served write IS the value), or the write did not
* come through `setEventSource`.
*/
takeRealWrite(runtimeStageId: string, key: string): {
readonly value: unknown;
readonly keptOut: boolean;
} | undefined;
}
/**
* What a stage's scope is tied to — internal to this module and its helpers.
*
* @internal
*/
export interface ScopeRun {
/** What the run is covered by — `unknown` only for a runner whose declaration is unknown. */
readonly coverage: RedactionCoverage;
readonly serving: EventServing;
/** Deliver an event, as made, to the real-value path — from the stage `stageId` that emitted it. */
real(type: string, payload: unknown, stageId: string): void;
noteWrite(runtimeStageId: string, key: string, value: unknown): void;
/** Whether the run's rule keeps `name` out of its records. */
keepsOut(name: string): boolean;
}
/**
* What `executor`'s run was covered by: `covered`, `declared-none`, or
* `unknown` — an executor no run of this library opened, whose records
* nothing vouches for. A reader serves an unknown executor's records under
* nothing it could guess: it refuses (`RunnerBase · getLastSnapshot` hands
* back nothing).
*/
export declare function coverageOfExecutor(executor: object): RedactionCoverage;
/**
* The serving of the run that opened `executor` — what a fact the runner
* files about that run is served as. `undefined` for an executor no run of
* this library opened: such a fact is refused (`EventDispatcher ·
* dispatchForRun`).
*/
export declare function servingOfExecutor(executor: object): EventServing | undefined;
/**
* Open the redaction for one run. Called by every runner's `createExecutor`
* for every run — with no policy the serving is the identity and nothing is
* walked, but the scopes are still tied to the run, because the real-value path
* is wiring, not redaction.
*/
export declare function createRunRedaction(args: {
readonly policy: RedactionPolicy | undefined;
readonly dispatcher: EventDispatcher;
readonly getRunContext: () => RunContext;
}): RunRedaction;
/**
* A settled run's rule as its serving keeps it: the policy and the marks —
* names only — and nothing else of the run. footprintjs's rule also remembers
* the masked form of every thrown value it served (so `onRunFailed`, the
* logger and a fork envelope agree); the live run needed that, a late fact
* does not, and keeping it would keep each run's thrown values reachable for
* as long as the dispatcher keeps the run's serving.
*
* @internal — exported for the test that pins it.
*/
export declare function retiredRule(rule: RedactionRule, policy: RedactionPolicy | undefined): RedactionRule;
/**
* The serving for facts a runner dispatches BEFORE its run's executor exists —
* a resume's `pause.resume`, whose payload is the person's reply — under the
* policy that run will be covered by, which the runner computed from typed
* declarations (`undefined` there is positively none: the identity). A rule
* built from the policy alone: no stage has run yet, so the run has marked
* nothing.
*/
export declare function servingAhead(policy: RedactionPolicy | undefined): EventServing;
/**
* The run a stage of a runner's chart belongs to when NO run of the library
* made its scope — the chart mounted into an executor the app built itself
* (`parent.addSubFlowChartNext('sf-agent', agent.getSpec(), …)`): the runner's
* DECLARED policy, served as a run's would be (`servingAhead`), so its stages'
* events never leave raw because the runner was not the one running them.
* There is no run here to feed the real-value path or a relay, so those are
* idle. Built for ONE runner and held by it (`RunnerBase`) — never a registry
* shared between runners — and it holds no value of any run: only the
* declaration's rule.
*/
export interface OutsideRun {
/** The runner's declaration this run serves under. */
readonly declaration: RedactionCoverage;
/** @internal */
readonly scopeRun: ScopeRun;
}
/**
* The {@link OutsideRun} of a runner whose declaration is `declaration`:
* `covered` serves under its policy, `declared-none` as emitted (positively
* none), `unknown` refuses every payload — the placeholder, fail closed.
*/
export declare function outsideRunFor(declaration: RedactionCoverage): OutsideRun;
/**
* Tie `scope` to `outside` — the run of its runner's chart mounted outside any
* of that runner's runs (`chartBinding.ts` · `bindChartStages` calls it as each
* stage starts, with the runner's own {@link OutsideRun}). A scope a run
* already made keeps its run. A runner that declares no policy ties the scope
* as positively none (served as emitted) — never left untied, which would
* read as unknown.
*/
export declare function adoptScopeOutsideRun(scope: unknown, outside: OutsideRun): void;
/** A scope that can emit — structurally footprintjs's `TypedScope` `$emit`. */
export interface EmitScope {
$emit(name: string, payload?: unknown): void;
}
/**
* THE way a typed event leaves a stage. The real value goes to the run's own
* mechanisms (`onRealEvent`), the served value to footprintjs's `$emit` — and
* from there to every channel at once. A runner's chart mounted into an
* executor the app built is tied to the runner's declaration as each stage
* starts (`adoptScopeOutsideRun`). A scope NOTHING tied — not made by a run of
* this library, not bound to a runner — is a run whose state is unknown: its
* payload is the placeholder (fail closed), never the payload as made.
*/
export declare function emitServed(scope: EmitScope, type: string, payload: unknown): void;
/** A scope that can set a value — structurally footprintjs's `TypedScope` `$setValue`. */
export interface SetValueScope {
$setValue(key: string, value: unknown): void;
}
/**
* THE way a stage writes a value the library derives events from (a slot's
* injections and composition — `ContextRecorder`): written as `$setValue`
* writes it, and handed to the run as written (`RunRedaction ·
* takeRealWrite`), so the derived events exist under a policy that keeps the
* key itself out of the record. A scope no run made just writes.
*/
export declare function setEventSource(scope: SetValueScope, key: string, value: unknown): void;
/**
* Whether the run `scope` belongs to keeps `name` out of its records — asked
* of the run's own rule. For text the library writes OUTSIDE any record from
* a value the run keeps out (a console warning that would quote the model's
* draft): it leaves the value out too. `true` for a scope no run made — its
* run's state is unknown, so the value is left out (fail closed).
*/
export declare function runKeepsOut(scope: object, name: string): boolean;
/**
* What the run `scope` belongs to is covered by (`coverage.ts`) — `unknown`
* for a scope no run of this library made.
*/
export declare function coverageInForce(scope: object): RedactionCoverage;
/**
* The policy in force for the run `scope` belongs to — what a tool call hands
* down to a run it starts (`ToolExecutionContext.redact`), what seed commits
* for a pause to carry. `undefined` ONLY for a run positively covered by none;
* a run whose state is unknown hands down the whole conversation vocabulary —
* fail closed, never "none".
*/
export declare function policyInForce(scope: object): RedactionPolicy | undefined;