agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
134 lines (133 loc) • 6.91 kB
TypeScript
/**
* mountMemoryPipeline — mount a MemoryPipeline's read + write subflows
* into an arbitrary agent flowchart.
*
* Given:
* - a `FlowChartBuilder<AgentState>` the caller has been assembling,
* - a `MemoryPipeline { read, write }` (typically from a preset),
* - identity + turn + budget inputs sourced from agent scope,
* - mount points (which stage id to insert the read subflow before),
*
* returns the builder with:
* 1. the `read` subflow mounted BEFORE the given anchor stage, with an
* inputMapper that reads identity/turn/budget from agent scope and
* an outputMapper that writes `formatted` messages back,
* 2. the `write` subflow (if present) appended AT THE END, with an
* inputMapper that reads `newMessages` from agent scope.
*
* The agent's existing stages are responsible for:
* - populating `identity`, `turnNumber`, `contextTokensRemaining`,
* `newMessages` in scope BEFORE the relevant memory subflow runs,
* - consuming the injected `formatted` messages (merge into the
* agent's outgoing LLM prompt).
*
* This helper does NOT own any of those concerns — it owns only the
* mechanical subflow mounting. Consumer-facing API
* (`AgentBuilder.memoryPipeline()`) is layered on top.
*
* Why a standalone helper, not a direct `AgentBuilder` patch?
* - Lets us test the wire mechanism end-to-end in isolation (Layer 6)
* without changing the existing AgentRunner path (100+ tests).
* - The wire is a small, reviewable unit; the AgentBuilder refactor is
* a larger concern that can ship separately.
* - Future non-Agent concepts (Swarm, Parallel) can use the same helper
* — memory isn't tied to Agent conceptually.
*/
import type { FlowChartBuilder } from 'footprintjs';
import type { MemoryPipeline } from '../pipeline/types.js';
/**
* Keys this helper reads from / writes to on the parent agent scope.
* Kept as fields on the config so consumers with non-standard field names
* can override without renaming scope properties.
*/
export interface MountMemoryPipelineConfig<ParentState> {
/** The compiled read + write subflows from a pipeline preset. */
readonly pipeline: MemoryPipeline;
/**
* Scope field name the read subflow reads identity from. Default
* `'identity'` — matches `MemoryState.identity`. Override when the
* host flowchart uses a different name.
*/
readonly identityKey?: keyof ParentState & string;
/** Scope field name for the turn counter. Default `'turnNumber'`. */
readonly turnNumberKey?: keyof ParentState & string;
/**
* Scope field name for the context-tokens-remaining signal.
* Default `'contextTokensRemaining'`.
*/
readonly contextTokensKey?: keyof ParentState & string;
/**
* Scope field the read subflow writes its `formatted` output to.
* Default `'memoryInjection'` — agent stages consume this to prepend
* to the LLM prompt. Distinct from the pipeline's own `formatted`
* field so there's no ambiguity between subflow-local and parent scope.
*/
readonly injectionKey?: keyof ParentState & string;
/**
* Scope field the write subflow reads messages to persist from.
* Default `'newMessages'` — populated by the agent at turn end.
*/
readonly newMessagesKey?: keyof ParentState & string;
/** Subflow id for the read mount. Default `'sf-memory-read'`. */
readonly readSubflowId?: string;
/** Subflow id for the write mount. Default `'sf-memory-write'`. */
readonly writeSubflowId?: string;
/**
* Read and write under THIS namespace instead of whatever the parent
* scope's identity key holds (8.8.0).
*
* A conversation memory wants the run's identity — that is how each
* conversation remembers its own turns. A shared document corpus does
* not: it belongs to the application, not to a conversation, and
* scoping it per run is what made `defineRAG`'s documented example
* retrieve nothing at all. `MemoryDefinition.corpus` feeds this.
*/
readonly identityOverride?: import('../identity/index.js').MemoryIdentity;
/**
* Scope field the read subflow writes its retrieval record to (8.8.0).
*
* The record has to be lifted OUT of the subflow to be worth anything:
* a subflow's own scope never reaches the root commit log, which is why
* a backward slice from the answer used to bottom out at "the memory
* mount wrote it" with no way to ask which passage. Lifted here, the
* per-chunk ids and scores are ordinary root state, and `sliceForKey`
* / `backtrack` / `who_wrote` reach them like any other value.
*
* Omitted → nothing is lifted, and pipelines that do no retrieval are
* unaffected (they never write the record in the first place).
*/
readonly evidenceKey?: keyof ParentState & string;
/**
* Evidence source for CAUSAL pipelines (the evidence bridge, backlog #5).
* Called by the write mount's inputMapper at write time; its result lands on
* `MemoryState.runEvidence` so `writeSnapshot` persists real decisions /
* tool calls / iterations / duration / token usage instead of zeros. The
* Agent threads `causalEvidenceRecorder().collect` here automatically when a
* CAUSAL memory is mounted. Omit for non-causal pipelines.
*/
readonly evidenceSource?: () => import('../causal/evidenceRecorder.js').RunEvidence;
}
/**
* Mount only the READ subflow. Appends at the current builder tail, so
* callers typically invoke this BEFORE their LLM-call stage:
*
* let b = flowChart('Seed', seedFn, 'seed');
* b = mountMemoryRead(b, { pipeline });
* b = b.addFunction('CallLLM', llmStage, 'call-llm'); // reads memoryInjection
* b = mountMemoryWrite(b, { pipeline }); // persists newMessages
*
* Returns the same builder reference (fluent).
*/
export declare function mountMemoryRead<ParentState>(builder: FlowChartBuilder<ParentState>, config: MountMemoryPipelineConfig<ParentState>): FlowChartBuilder<ParentState>;
/**
* Mount only the WRITE subflow. No-op when the pipeline has no `write`
* (e.g., ephemeral pipelines) — returns the builder unchanged.
*/
export declare function mountMemoryWrite<ParentState>(builder: FlowChartBuilder<ParentState>, config: MountMemoryPipelineConfig<ParentState>): FlowChartBuilder<ParentState>;
/**
* Convenience: mount both read and write subflows back-to-back.
* Appropriate ONLY when the host flowchart has no stages between memory
* read and memory write (rare — most agents have the LLM call between).
* Prefer `mountMemoryRead` + stages + `mountMemoryWrite` for typical agents.
*/
export declare function mountMemoryPipeline<ParentState>(builder: FlowChartBuilder<ParentState>, config: MountMemoryPipelineConfig<ParentState>): FlowChartBuilder<ParentState>;