UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

180 lines • 8.65 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.autoPipeline = void 0; /** * autoPipeline — opinionated preset combining facts + beats on one store. * * The one-line default for "I want memory — just make it good." * * READ : LoadAll → split by payload shape → FormatAuto * (one system msg with facts block + narrative paragraph) * * WRITE : LoadFacts → ExtractFacts → WriteFacts * → ExtractBeats → WriteBeats * * Facts carry "what's currently true" (identity, preferences, * commitments — dedup on key). Beats carry "what happened" (traceable * summaries — append-only). Together they cover the two memory shapes * that matter for long-running agents without forcing the consumer to * compose three presets by hand. * * **Default extractors**: zero-LLM-cost defaults so `autoPipeline({ store })` * works out of the box: * - facts: `patternFactExtractor()` (regex heuristics, free) * - beats: `heuristicExtractor()` (keyword heuristics, free) * * Pass `provider` to upgrade BOTH to LLM-backed extractors in one * knob — `llmFactExtractor({ provider })` and `llmExtractor({ provider })`. * The same cheap model (e.g. `claude-haiku-4-5`) handles both extraction * calls per turn. * * **Why not include raw messages?** Raw messages come from the agent's * ongoing conversation state — they don't need a memory strategy. This * preset focuses on the two *compressed / deduplicated* shapes. Layer on * `defaultPipeline` or `semanticPipeline` manually if you want raw recall. * * Most consumers reach for `autoPipeline` indirectly through * `defineMemory({ type: MEMORY_TYPES.SEMANTIC, strategy: { kind: * MEMORY_STRATEGIES.HYBRID, ... }, store })`, which dispatches here * for hybrid-style composition. * * @example Direct usage (low-level — custom flowchart composition): * ```ts * import { autoPipeline, mountMemoryRead, mountMemoryWrite, InMemoryStore } from 'agentfootprint/memory'; * * // Free defaults — regex + heuristics, no LLM cost. * const pipeline = autoPipeline({ store: new InMemoryStore() }); * * // Or upgrade both extractors in one knob (pass any LLMProvider): * const hq = autoPipeline({ * store: new InMemoryStore(), * provider: yourLLMProvider, * }); * ``` */ const footprintjs_1 = require("footprintjs"); const index_js_1 = require("../facts/index.js"); const index_js_2 = require("../beats/index.js"); const DEFAULT_FACTS_HEADER = 'Known facts about the user:'; const DEFAULT_LEAD_IN = 'From earlier: '; const DEFAULT_LOAD_LIMIT = 200; /** Escape `</memory>` in user-controlled text to prevent tag-escape injection. */ function escapeMemoryTag(text) { return text.replace(/<\/memory>/gi, '</m\u200Demory>'); } function renderValue(v) { if (typeof v === 'string') return v; try { return JSON.stringify(v); } catch { return String(v); } } /** * Emit a single system message combining facts (key/value block) and * beats (narrative paragraph). Empty sections are skipped; when BOTH * are empty, `formatted = []` (no injection). */ function renderAutoMessage(facts, beats, opts) { const sections = []; if (facts.length > 0) { const lines = facts.map((entry) => { const f = entry.value; const value = escapeMemoryTag(renderValue(f.value)); const conf = opts.showConfidence && typeof f.confidence === 'number' ? ` (conf ${f.confidence.toFixed(2)})` : ''; return `- ${f.key}: ${value}${conf}`; }); sections.push(`${opts.factsHeader}\n\n${lines.join('\n')}`); } if (beats.length > 0) { const sentences = beats.map((entry) => { const beat = entry.value; const text = escapeMemoryTag(beat.summary.trim()); const withRefs = opts.showRefs && beat.refs.length > 0 ? `${text} (refs: ${beat.refs.join(', ')})` : text; return /[.!?]$/.test(withRefs) ? withRefs : `${withRefs}.`; }); sections.push(`${opts.leadIn}${sentences.join(' ')}`); } if (sections.length === 0) return []; return [{ role: 'system', content: sections.join('\n\n') }]; } function autoPipeline(config) { const factExtractor = config.factExtractor ?? (config.provider ? (0, index_js_1.llmFactExtractor)({ provider: config.provider }) : (0, index_js_1.patternFactExtractor)()); const beatExtractor = config.beatExtractor ?? (config.provider ? (0, index_js_2.llmExtractor)({ provider: config.provider }) : (0, index_js_2.heuristicExtractor)()); const loadLimit = config.loadLimit ?? DEFAULT_LOAD_LIMIT; const factsHeader = config.factsHeader ?? DEFAULT_FACTS_HEADER; const leadIn = config.narrativeLeadIn ?? DEFAULT_LEAD_IN; const showConfidence = config.showConfidence ?? false; const showRefs = config.showRefs ?? false; // ── READ subflow ──────────────────────────────────────────── // One combined load + split stage. Keeps the subflow short and // avoids pulling `loadRecent` + `pickByBudget` (which were designed // for single-payload pipelines) into a mixed-payload context. const read = (0, footprintjs_1.flowChart)('LoadAll', async (scope) => { const listOpts = { limit: loadLimit, ...(config.tiers && { tiers: config.tiers }), }; const { entries } = await config.store.list(scope.identity, listOpts); const loadedFacts = []; const loadedBeats = []; for (const entry of entries) { if ((0, index_js_1.isFactId)(entry.id)) { loadedFacts.push(entry); } else if ((0, index_js_2.isNarrativeBeat)(entry.value)) { loadedBeats.push(entry); } // Other payloads (raw messages) are intentionally ignored — // auto() is the "compressed + deduped" preset. } // Stores typically return most-recently-updated first. Beats are // more readable oldest-first (chronological narrative), so // reverse before injection. Facts render as a list, order-agnostic. scope.loadedFacts = loadedFacts; scope.loadedBeats = [...loadedBeats].reverse(); }, 'load-all', { description: 'Load facts + beats from the shared store; split by payload shape' }) .addFunction('FormatAuto', async (scope) => { const facts = (scope.loadedFacts ?? []); const beats = (scope.loadedBeats ?? []); scope.formatted = renderAutoMessage(facts, beats, { factsHeader, leadIn, showConfidence, showRefs, }); }, 'format-auto', 'Emit one system message with facts block + narrative paragraph') .build(); // ── WRITE subflow ─────────────────────────────────────────── // LoadFacts first so the (possibly LLM-backed) fact extractor sees // existing facts via scope.loadedFacts and can update rather than // duplicate. Then extract+write facts, then extract+write beats. const write = (0, footprintjs_1.flowChart)('LoadFacts', (0, index_js_1.loadFacts)({ store: config.store, limit: loadLimit, ...(config.tiers && { tiers: config.tiers }), }), 'load-facts', { description: 'Surface existing facts for update-awareness' }) .addFunction('ExtractFacts', (0, index_js_1.extractFacts)({ extractor: factExtractor, ...(config.writeTier && { tier: config.writeTier }), ...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }), }), 'extract-facts', 'Distill scope.newMessages into stable Fact entries') .addFunction('WriteFacts', (0, index_js_1.writeFacts)({ store: config.store }), 'write-facts', 'Persist facts via store.putMany (overwrite on key collision)') .addFunction('ExtractBeats', (0, index_js_2.extractBeats)({ extractor: beatExtractor, ...(config.writeTier && { tier: config.writeTier }), ...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }), }), 'extract-beats', 'Compress scope.newMessages into NarrativeBeat entries') .addFunction('WriteBeats', (0, index_js_2.writeBeats)({ store: config.store }), 'write-beats', 'Persist beats via store.putMany') .build(); return { read, write }; } exports.autoPipeline = autoPipeline; //# sourceMappingURL=auto.js.map