agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
118 lines • 5.95 kB
JavaScript
/**
* defaultPipeline — the 90%-use-case memory preset.
*
* Composes Layer 2-3 stages into two flowchart subflows that the wire
* layer mounts inside the agent's main flowchart:
*
* READ : LoadRecent → [Summarize] → [FilterByDecay] → PickByBudget →
* FormatDefault
* WRITE : WriteMessages
*
* The two bracketed stages are compiled only when their config is present
* (`summarize`, `decay`) — an absent option means an absent stage, never a
* stage that runs and does nothing.
*
* Why this particular composition?
* - Load-then-pick-then-format matches the cognitive sequence:
* retrieve candidates, choose what fits, present it.
* - Single-stage write keeps the persistence story simple for Phase 1;
* Phase 1.5 adds a second stage (extractFacts) to the write side.
*
* This preset is intentionally opinionated. Users who need more
* control should compose their own FlowChart and pass it to
* `.memory()` directly — the preset is teaching code, not a
* one-size-fits-all.
*
* **Build once, mount many.** Call `defaultPipeline(config)` at application
* startup (or whenever the config changes). The returned `{read, write}`
* are immutable compiled FlowChart objects safe to share across many
* agent builds and many `.run()` calls. Rebuilding per-turn is wasteful —
* the stages capture their config at build time and don't read it later.
*
* Most consumers should use `defineMemory({ type: MEMORY_TYPES.EPISODIC,
* strategy: { kind: MEMORY_STRATEGIES.WINDOW, ... }, store })` instead
* of calling `defaultPipeline()` directly — the factory dispatches
* onto this pipeline under the hood. Use `defaultPipeline()` directly
* only when composing memory subflows into a non-Agent flowchart via
* `mountMemoryRead`/`mountMemoryWrite`.
*
* @example Direct usage (low-level — custom flowchart composition):
* ```ts
* import { defaultPipeline, mountMemoryRead, InMemoryStore } from 'agentfootprint/memory';
* import { flowChart } from 'footprintjs';
*
* const pipeline = defaultPipeline({
* store: new InMemoryStore(),
* loadCount: 20,
* reserveTokens: 512,
* });
*
* let builder = flowChart('Seed', seedFn, 'seed');
* builder = mountMemoryRead(builder, { pipeline });
* // ... continue building your custom flowchart
* ```
*/
import { flowChart } from 'footprintjs';
import { loadRecent } from '../stages/loadRecent.js';
import { filterByDecay } from '../stages/filterByDecay.js';
import { summarize } from '../stages/summarize.js';
import { pickByBudget } from '../stages/pickByBudget.js';
import { formatDefault } from '../stages/formatDefault.js';
import { writeMessages } from '../stages/writeMessages.js';
/**
* Build the default read + write pipelines sharing a single store.
* Returns two FlowChart subflows ready to be mounted by the wire layer.
*/
export function defaultPipeline(config) {
// Explicit per-stage config construction — keeps each stage's defaults
// visible in the preset source (which doubles as teaching code).
const loadConfig = {
store: config.store,
...(config.loadCount !== undefined && { count: config.loadCount }),
...(config.tiers && { tiers: config.tiers }),
};
const pickConfig = {
...(config.reserveTokens !== undefined && { reserveTokens: config.reserveTokens }),
...(config.minimumTokens !== undefined && { minimumTokens: config.minimumTokens }),
...(config.maxEntries !== undefined && { maxEntries: config.maxEntries }),
};
const formatConfig = {
...(config.formatHeader !== undefined && { header: config.formatHeader }),
...(config.formatFooter !== undefined && { footer: config.formatFooter }),
};
const writeConfig = {
store: config.store,
...(config.writeTier && { tier: config.writeTier }),
...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
};
// Compose: LoadRecent → [Summarize] → [FilterByDecay] → [PickDecider →
// skip-empty | skip-no-budget | pick] → Format
// pickByBudget is a builder-extension — it appends a decider + 3
// branches to the pipeline so "why did / didn't we inject memory?" is
// answerable via FlowRecorder.onDecision evidence, not just emit events.
let readBuilder = flowChart('LoadRecent', loadRecent(loadConfig), 'load-recent', {
description: 'Read N most-recent entries from storage into scope.loaded',
});
if (config.summarize !== undefined) {
// Directly after the load, and before every stage that trims: decay and
// the budget picker both decide against what recall CONTAINS, and what
// recall contains is not settled until the fold has happened.
readBuilder = readBuilder.addFunction('Summarize', summarize({
...config.summarize,
store: config.store,
...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
}), 'summarize', 'Fold entries older than the verbatim tail into one stored summary entry');
}
if (config.decay !== undefined) {
// Before the picker: what has faded is not a budget question, and the
// budget is better spent on entries that are still worth something.
readBuilder = readBuilder.addFunction('FilterByDecay', filterByDecay(config.decay), 'filter-by-decay', 'Drop loaded entries whose age-decayed score is below the floor');
}
readBuilder = pickByBudget(pickConfig)(readBuilder);
const read = readBuilder
.addFunction('Format', formatDefault(formatConfig), 'format-default', 'Render selected entries as a system message; writes scope.formatted')
.build();
const write = flowChart('WriteMessages', writeMessages(writeConfig), 'write-messages', { description: 'Persist new turn messages to storage' }).build();
return { read, write };
}
//# sourceMappingURL=default.js.map