agentfootprint 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/esm/memory/facts/extractFacts.js +47 -0
  2. package/dist/esm/memory/facts/extractFacts.js.map +1 -0
  3. package/dist/esm/memory/facts/extractor.js +2 -0
  4. package/dist/esm/memory/facts/extractor.js.map +1 -0
  5. package/dist/esm/memory/facts/formatFacts.js +45 -0
  6. package/dist/esm/memory/facts/formatFacts.js.map +1 -0
  7. package/dist/esm/memory/facts/index.js +8 -0
  8. package/dist/esm/memory/facts/index.js.map +1 -0
  9. package/dist/esm/memory/facts/llmFactExtractor.js +133 -0
  10. package/dist/esm/memory/facts/llmFactExtractor.js.map +1 -0
  11. package/dist/esm/memory/facts/loadFacts.js +22 -0
  12. package/dist/esm/memory/facts/loadFacts.js.map +1 -0
  13. package/dist/esm/memory/facts/patternFactExtractor.js +106 -0
  14. package/dist/esm/memory/facts/patternFactExtractor.js.map +1 -0
  15. package/dist/esm/memory/facts/types.js +49 -0
  16. package/dist/esm/memory/facts/types.js.map +1 -0
  17. package/dist/esm/memory/facts/writeFacts.js +9 -0
  18. package/dist/esm/memory/facts/writeFacts.js.map +1 -0
  19. package/dist/esm/memory/pipeline/auto.js +177 -0
  20. package/dist/esm/memory/pipeline/auto.js.map +1 -0
  21. package/dist/esm/memory/pipeline/fact.js +87 -0
  22. package/dist/esm/memory/pipeline/fact.js.map +1 -0
  23. package/dist/esm/memory/pipeline/index.js +2 -0
  24. package/dist/esm/memory/pipeline/index.js.map +1 -1
  25. package/dist/esm/memory.barrel.js +3 -0
  26. package/dist/esm/memory.barrel.js.map +1 -1
  27. package/dist/memory/facts/extractFacts.js +51 -0
  28. package/dist/memory/facts/extractFacts.js.map +1 -0
  29. package/dist/memory/facts/extractor.js +3 -0
  30. package/dist/memory/facts/extractor.js.map +1 -0
  31. package/dist/memory/facts/formatFacts.js +49 -0
  32. package/dist/memory/facts/formatFacts.js.map +1 -0
  33. package/dist/memory/facts/index.js +21 -0
  34. package/dist/memory/facts/index.js.map +1 -0
  35. package/dist/memory/facts/llmFactExtractor.js +137 -0
  36. package/dist/memory/facts/llmFactExtractor.js.map +1 -0
  37. package/dist/memory/facts/loadFacts.js +26 -0
  38. package/dist/memory/facts/loadFacts.js.map +1 -0
  39. package/dist/memory/facts/patternFactExtractor.js +110 -0
  40. package/dist/memory/facts/patternFactExtractor.js.map +1 -0
  41. package/dist/memory/facts/types.js +56 -0
  42. package/dist/memory/facts/types.js.map +1 -0
  43. package/dist/memory/facts/writeFacts.js +13 -0
  44. package/dist/memory/facts/writeFacts.js.map +1 -0
  45. package/dist/memory/pipeline/auto.js +181 -0
  46. package/dist/memory/pipeline/auto.js.map +1 -0
  47. package/dist/memory/pipeline/fact.js +91 -0
  48. package/dist/memory/pipeline/fact.js.map +1 -0
  49. package/dist/memory/pipeline/index.js +5 -1
  50. package/dist/memory/pipeline/index.js.map +1 -1
  51. package/dist/memory.barrel.js +16 -1
  52. package/dist/memory.barrel.js.map +1 -1
  53. package/dist/types/memory/facts/extractFacts.d.ts +51 -0
  54. package/dist/types/memory/facts/extractFacts.d.ts.map +1 -0
  55. package/dist/types/memory/facts/extractor.d.ts +34 -0
  56. package/dist/types/memory/facts/extractor.d.ts.map +1 -0
  57. package/dist/types/memory/facts/formatFacts.d.ts +61 -0
  58. package/dist/types/memory/facts/formatFacts.d.ts.map +1 -0
  59. package/dist/types/memory/facts/index.d.ts +15 -0
  60. package/dist/types/memory/facts/index.d.ts.map +1 -0
  61. package/dist/types/memory/facts/llmFactExtractor.d.ts +65 -0
  62. package/dist/types/memory/facts/llmFactExtractor.d.ts.map +1 -0
  63. package/dist/types/memory/facts/loadFacts.d.ts +44 -0
  64. package/dist/types/memory/facts/loadFacts.d.ts.map +1 -0
  65. package/dist/types/memory/facts/patternFactExtractor.d.ts +3 -0
  66. package/dist/types/memory/facts/patternFactExtractor.d.ts.map +1 -0
  67. package/dist/types/memory/facts/types.d.ts +69 -0
  68. package/dist/types/memory/facts/types.d.ts.map +1 -0
  69. package/dist/types/memory/facts/writeFacts.d.ts +20 -0
  70. package/dist/types/memory/facts/writeFacts.d.ts.map +1 -0
  71. package/dist/types/memory/pipeline/auto.d.ts +60 -0
  72. package/dist/types/memory/pipeline/auto.d.ts.map +1 -0
  73. package/dist/types/memory/pipeline/fact.d.ts +27 -0
  74. package/dist/types/memory/pipeline/fact.d.ts.map +1 -0
  75. package/dist/types/memory/pipeline/index.d.ts +4 -0
  76. package/dist/types/memory/pipeline/index.d.ts.map +1 -1
  77. package/dist/types/memory.barrel.d.ts +7 -0
  78. package/dist/types/memory.barrel.d.ts.map +1 -1
  79. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ import { factId } from './types';
2
+ export function extractFacts(config) {
3
+ const { extractor } = config;
4
+ return async (scope) => {
5
+ const messages = (scope.newMessages ?? []);
6
+ const turnNumber = scope.turnNumber ?? 1;
7
+ const identity = scope.identity;
8
+ if (messages.length === 0) {
9
+ scope.newFacts = [];
10
+ return;
11
+ }
12
+ const env = scope.$getEnv?.();
13
+ const signal = env?.signal;
14
+ // Pass existing facts (if loaded) to the extractor so LLM-based
15
+ // extractors can update/refine rather than duplicate.
16
+ const existing = (scope.loadedFacts ?? []).map((e) => e.value);
17
+ const facts = await extractor.extract({
18
+ messages,
19
+ turnNumber,
20
+ ...(existing.length > 0 ? { existing } : {}),
21
+ ...(signal ? { signal } : {}),
22
+ });
23
+ if (facts.length === 0) {
24
+ scope.newFacts = [];
25
+ return;
26
+ }
27
+ const now = Date.now();
28
+ const ttl = config.ttlMs !== undefined ? now + config.ttlMs : undefined;
29
+ const entries = facts.map((fact) => ({
30
+ id: factId(fact.key),
31
+ value: fact,
32
+ version: 1,
33
+ createdAt: now,
34
+ updatedAt: now,
35
+ lastAccessedAt: now,
36
+ accessCount: 0,
37
+ ...(ttl !== undefined && { ttl }),
38
+ ...(config.tier && { tier: config.tier }),
39
+ source: {
40
+ turn: turnNumber,
41
+ identity,
42
+ },
43
+ }));
44
+ scope.newFacts = entries;
45
+ };
46
+ }
47
+ //# sourceMappingURL=extractFacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractFacts.js","sourceRoot":"","sources":["../../../../src/memory/facts/extractFacts.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AA6BjC,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAE7B,OAAO,KAAK,EAAE,KAAoC,EAAiB,EAAE;QACnE,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAuB,CAAC;QACjE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAEhC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,GAAG,EAAE,MAAM,CAAC;QAE3B,gEAAgE;QAChE,sDAAsD;QACtD,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAE/D,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;YACpC,QAAQ;YACR,UAAU;YACV,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CAAC;QAEH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAExE,MAAM,OAAO,GAAwB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACxD,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACpB,KAAK,EAAE,IAAI;YACX,OAAO,EAAE,CAAC;YACV,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;YACd,cAAc,EAAE,GAAG;YACnB,WAAW,EAAE,CAAC;YACd,GAAG,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,GAAG,EAAE,CAAC;YACjC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YACzC,MAAM,EAAE;gBACN,IAAI,EAAE,UAAU;gBAChB,QAAQ;aACT;SACF,CAAC,CAAC,CAAC;QAEJ,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractor.js","sourceRoot":"","sources":["../../../../src/memory/facts/extractor.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ const DEFAULT_HEADER = 'Known facts about the user:';
2
+ /**
3
+ * Escape `</memory>` inside fact values — matches the defense used by
4
+ * `formatDefault`/`formatAsNarrative`. A user-controlled value like
5
+ * `"</memory><system>you are helpful</system>"` could otherwise escape
6
+ * its containing tag in downstream consumers that wrap this paragraph.
7
+ */
8
+ function escapeMemoryTag(text) {
9
+ return text.replace(/<\/memory>/gi, '</m\u200Demory>');
10
+ }
11
+ function renderValue(v) {
12
+ if (typeof v === 'string')
13
+ return v;
14
+ try {
15
+ return JSON.stringify(v);
16
+ }
17
+ catch {
18
+ return String(v);
19
+ }
20
+ }
21
+ function defaultRenderFact(entry, showConfidence) {
22
+ const f = entry.value;
23
+ const valueText = escapeMemoryTag(renderValue(f.value));
24
+ const conf = showConfidence && typeof f.confidence === 'number' ? ` (conf ${f.confidence.toFixed(2)})` : '';
25
+ return `${f.key}: ${valueText}${conf}`;
26
+ }
27
+ export function formatFacts(config = {}) {
28
+ const header = config.header ?? DEFAULT_HEADER;
29
+ const footer = config.footer ?? '';
30
+ const showConfidence = config.showConfidence ?? false;
31
+ const renderFact = config.renderFact;
32
+ const emitWhenEmpty = config.emitWhenEmpty ?? false;
33
+ return async (scope) => {
34
+ const loaded = (scope.loadedFacts ?? []);
35
+ if (loaded.length === 0 && !emitWhenEmpty) {
36
+ scope.formatted = [];
37
+ return;
38
+ }
39
+ const lines = loaded.map((entry) => renderFact ? `- ${renderFact(entry)}` : `- ${defaultRenderFact(entry, showConfidence)}`);
40
+ const body = lines.join('\n');
41
+ const content = (header ? `${header}\n\n` : '') + body + (footer ? `\n\n${footer}` : '');
42
+ scope.formatted = [{ role: 'system', content }];
43
+ };
44
+ }
45
+ //# sourceMappingURL=formatFacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatFacts.js","sourceRoot":"","sources":["../../../../src/memory/facts/formatFacts.ts"],"names":[],"mappings":"AAiEA,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,WAAW,CAAC,CAAU;IAC7B,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAwB,EAAE,cAAuB;IAC1E,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IACtB,MAAM,SAAS,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GACR,cAAc,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACjG,OAAO,GAAG,CAAC,CAAC,GAAG,KAAK,SAAS,GAAG,IAAI,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAA4B,EAAE;IACxD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,cAAc,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;IACnC,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,KAAK,CAAC;IACtD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACrC,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,KAAK,CAAC;IAEpD,OAAO,KAAK,EAAE,KAAoC,EAAiB,EAAE;QACnE,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAiC,CAAC;QAEzE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAC1C,KAAK,CAAC,SAAS,GAAG,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACjC,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE,CACxF,CAAC;QAEF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAEzF,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { factId, isFactId, isFact, asConfidence } from './types';
2
+ export { patternFactExtractor } from './patternFactExtractor';
3
+ export { llmFactExtractor } from './llmFactExtractor';
4
+ export { extractFacts } from './extractFacts';
5
+ export { writeFacts } from './writeFacts';
6
+ export { loadFacts } from './loadFacts';
7
+ export { formatFacts } from './formatFacts';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/memory/facts/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,133 @@
1
+ import { asConfidence } from './types';
2
+ const DEFAULT_SYSTEM_PROMPT = `You are an extractor that distills a turn of conversation into stable, timeless facts for long-term memory.
3
+
4
+ A "fact" is a key/value claim that is currently true — not a narration of what happened. Facts dedupe by key, so later turns overwrite earlier claims.
5
+
6
+ Return JSON in this exact shape:
7
+ {
8
+ "facts": [
9
+ {
10
+ "key": "user.name",
11
+ "value": "Alice",
12
+ "confidence": 0.0_to_1.0,
13
+ "category": "identity|contact|profile|preference|commitment|fact|other",
14
+ "refs": ["msg-<turn>-<index>", ...]
15
+ }
16
+ ]
17
+ }
18
+
19
+ Guidelines:
20
+ - Use dotted keys for nested taxonomies: user.name, user.email, user.preferences.color, task.ORD-123.status.
21
+ - Values are JSON-serializable: strings, numbers, booleans, arrays, small objects.
22
+ - Confidence 0.9+ for direct self-disclosures ("my name is X"); 0.6-0.8 for inferences; below 0.5 for guesses.
23
+ - Only extract what the user explicitly claimed or committed to. Do not invent, do not infer personality traits.
24
+ - If a prior fact is being corrected ("actually, my name is Alicia"), emit the corrected value under the SAME key.
25
+ - Return [] if no stable claims appeared in this turn.
26
+ - Return ONLY the JSON object — no prose, no code fences.`;
27
+ /** Build a stable ref id matching the beat extractor's convention. */
28
+ function refId(turnNumber, index) {
29
+ return `msg-${turnNumber}-${index}`;
30
+ }
31
+ /** Serialize messages for the extractor LLM's user prompt. */
32
+ function formatMessagesForExtractor(messages, turnNumber) {
33
+ const lines = [`Turn ${turnNumber}:`];
34
+ for (let i = 0; i < messages.length; i++) {
35
+ const m = messages[i];
36
+ if (m.role === 'system')
37
+ continue;
38
+ const ref = refId(turnNumber, i);
39
+ const content = typeof m.content === 'string'
40
+ ? m.content
41
+ : Array.isArray(m.content)
42
+ ? m.content
43
+ .map((b) => {
44
+ if (b && typeof b === 'object') {
45
+ const blk = b;
46
+ if (blk.type === 'text' && typeof blk.text === 'string')
47
+ return blk.text;
48
+ }
49
+ return '';
50
+ })
51
+ .filter(Boolean)
52
+ .join(' ')
53
+ : '';
54
+ lines.push(`[${ref}] ${m.role}: ${content}`);
55
+ }
56
+ return lines.join('\n');
57
+ }
58
+ /** Serialize existing facts for the LLM's update-awareness context. */
59
+ function formatExistingFacts(existing, limit) {
60
+ if (limit <= 0 || existing.length === 0)
61
+ return '';
62
+ const take = existing.slice(0, limit);
63
+ const lines = ['Previously known facts (update or extend — do NOT re-emit unchanged):'];
64
+ for (const f of take) {
65
+ const conf = typeof f.confidence === 'number' ? ` (conf ${f.confidence.toFixed(2)})` : '';
66
+ const cat = f.category ? ` [${f.category}]` : '';
67
+ lines.push(`- ${f.key}: ${JSON.stringify(f.value)}${cat}${conf}`);
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+ /**
72
+ * Parse the extractor's raw JSON response into validated facts.
73
+ * Returns an empty array on any parse / shape failure — the `onParseError`
74
+ * callback fires so consumers can observe failures without crashing turns.
75
+ *
76
+ * Dedup policy: within one response, if the LLM emits the same key
77
+ * twice, the LAST occurrence wins (matches patternFactExtractor).
78
+ */
79
+ function parseFactsResponse(raw, onParseError) {
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ const rawFacts = (parsed?.facts ?? []);
83
+ if (!Array.isArray(rawFacts))
84
+ return [];
85
+ const byKey = new Map();
86
+ for (const rf of rawFacts) {
87
+ if (!rf || typeof rf !== 'object')
88
+ continue;
89
+ const f = rf;
90
+ if (typeof f.key !== 'string' || f.key.length === 0)
91
+ continue;
92
+ if (!('value' in f))
93
+ continue;
94
+ const refs = Array.isArray(f.refs) ? f.refs.filter((r) => typeof r === 'string') : [];
95
+ const category = typeof f.category === 'string' ? f.category : undefined;
96
+ byKey.set(f.key, {
97
+ key: f.key,
98
+ value: f.value,
99
+ confidence: asConfidence(f.confidence),
100
+ ...(category ? { category } : {}),
101
+ ...(refs.length > 0 ? { refs } : {}),
102
+ });
103
+ }
104
+ return Array.from(byKey.values());
105
+ }
106
+ catch (err) {
107
+ onParseError(err, raw);
108
+ return [];
109
+ }
110
+ }
111
+ export function llmFactExtractor(config) {
112
+ const { provider } = config;
113
+ const systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
114
+ const includeExistingLimit = config.includeExistingLimit ?? 16;
115
+ const onParseError = config.onParseError ??
116
+ ((err, raw) => {
117
+ // eslint-disable-next-line no-console
118
+ console.warn('[agentfootprint] llmFactExtractor: failed to parse LLM response — returning no facts', { error: err, rawPreview: raw.slice(0, 200) });
119
+ });
120
+ return {
121
+ async extract(args) {
122
+ const turn = formatMessagesForExtractor(args.messages, args.turnNumber);
123
+ const prior = formatExistingFacts(args.existing ?? [], includeExistingLimit);
124
+ const userContent = prior.length > 0 ? `${prior}\n\n${turn}` : turn;
125
+ const response = await provider.chat([
126
+ { role: 'system', content: systemPrompt },
127
+ { role: 'user', content: userContent },
128
+ ], args.signal ? { signal: args.signal } : undefined);
129
+ return parseFactsResponse(response.content ?? '', onParseError);
130
+ },
131
+ };
132
+ }
133
+ //# sourceMappingURL=llmFactExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llmFactExtractor.js","sourceRoot":"","sources":["../../../../src/memory/facts/llmFactExtractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { isFactId } from './types';
2
+ const DEFAULT_LIMIT = 100;
3
+ export function loadFacts(config) {
4
+ const limit = config.limit ?? DEFAULT_LIMIT;
5
+ return async (scope) => {
6
+ const { entries } = await config.store.list(scope.identity, {
7
+ limit,
8
+ ...(config.tiers && { tiers: config.tiers }),
9
+ });
10
+ // Filter by fact-id prefix. `list` may return mixed payloads
11
+ // (messages + beats + facts) if the store is shared. Prefix filter
12
+ // keeps only the fact-shaped entries.
13
+ const facts = [];
14
+ for (const entry of entries) {
15
+ if (isFactId(entry.id))
16
+ facts.push(entry);
17
+ }
18
+ const existing = scope.loadedFacts ?? [];
19
+ scope.loadedFacts = [...existing, ...facts];
20
+ };
21
+ }
22
+ //# sourceMappingURL=loadFacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadFacts.js","sourceRoot":"","sources":["../../../../src/memory/facts/loadFacts.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAsBnC,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,aAAa,CAAC;IAE5C,OAAO,KAAK,EAAE,KAAoC,EAAiB,EAAE;QACnE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAO,KAAK,CAAC,QAAQ,EAAE;YAChE,KAAK;YACL,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;SAC7C,CAAC,CAAC;QAEH,6DAA6D;QAC7D,mEAAmE;QACnE,sCAAsC;QACtC,MAAM,KAAK,GAAwB,EAAE,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;QACzC,KAAK,CAAC,WAAW,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { asConfidence } from './types';
2
+ /** Extract plaintext from any Message content shape. */
3
+ function textOf(message) {
4
+ const c = message.content;
5
+ if (typeof c === 'string')
6
+ return c;
7
+ if (Array.isArray(c)) {
8
+ const parts = [];
9
+ for (const block of c) {
10
+ if (block && typeof block === 'object') {
11
+ const b = block;
12
+ if (b.type === 'text' && typeof b.text === 'string')
13
+ parts.push(b.text);
14
+ }
15
+ }
16
+ return parts.join(' ');
17
+ }
18
+ return '';
19
+ }
20
+ const RULES = [
21
+ {
22
+ key: 'user.name',
23
+ category: 'identity',
24
+ confidence: 0.9,
25
+ patterns: [
26
+ // "my name is Alice" / "My name is Alice Smith" — lead-in case-insensitive
27
+ // via explicit [Mm], but captured name must be capitalized to avoid matching
28
+ // "my name is bob" (lowercase name is almost certainly not self-disclosure).
29
+ /\b[Mm]y name is\s+([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)?)/,
30
+ // "I'm Alice" / "I am Alice" — capitalized single word to reduce false positives
31
+ /\bI(?:'m|\s+am)\s+([A-Z][a-zA-Z]+)(?=[\s,.!?]|$)/,
32
+ ],
33
+ },
34
+ {
35
+ key: 'user.email',
36
+ category: 'contact',
37
+ confidence: 0.95,
38
+ patterns: [
39
+ // Basic RFC-5322-ish: username@domain.tld. Not exhaustive; good enough.
40
+ /\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/,
41
+ ],
42
+ },
43
+ {
44
+ key: 'user.location',
45
+ category: 'profile',
46
+ confidence: 0.8,
47
+ patterns: [
48
+ // "I live in San Francisco" / "I'm in NYC" / "I live in New York City"
49
+ // Captures 1-3 capitalized words (handles multi-word place names).
50
+ /\bI\s+(?:live|am)\s+in\s+([A-Z][\w-]*(?:\s+[A-Z][\w-]*){0,2})(?=[.!?,;]|$)/,
51
+ ],
52
+ },
53
+ {
54
+ key: 'user.preferences',
55
+ category: 'preference',
56
+ confidence: 0.7,
57
+ patterns: [
58
+ // "I prefer dark mode" / "I like pizza" / "I prefer hot coffee"
59
+ // Captures 1-3 words — stops at sentence-ending punctuation.
60
+ /\bI\s+(?:prefer|like)\s+([a-zA-Z][\w-]*(?:\s+[a-zA-Z][\w-]*){0,2})(?=[.!?,;]|$)/,
61
+ ],
62
+ },
63
+ ];
64
+ /** Trim trailing punctuation / whitespace from an extracted value. */
65
+ function cleanValue(s) {
66
+ return s.replace(/[\s.!?,;:]+$/, '').trim();
67
+ }
68
+ export function patternFactExtractor() {
69
+ return {
70
+ async extract(args) {
71
+ // Track matches by key so later messages in this turn override
72
+ // earlier ones (user may restate with correction). Within a turn,
73
+ // last-write-wins per key.
74
+ const byKey = new Map();
75
+ for (const msg of args.messages) {
76
+ if (msg.role !== 'user')
77
+ continue;
78
+ const text = textOf(msg);
79
+ if (text.length === 0)
80
+ continue;
81
+ for (const rule of RULES) {
82
+ for (const pattern of rule.patterns) {
83
+ const match = text.match(pattern);
84
+ if (!match)
85
+ continue;
86
+ const raw = match[1];
87
+ if (!raw)
88
+ continue;
89
+ const value = cleanValue(raw);
90
+ if (value.length === 0)
91
+ continue;
92
+ byKey.set(rule.key, {
93
+ key: rule.key,
94
+ value,
95
+ confidence: asConfidence(rule.confidence),
96
+ category: rule.category,
97
+ });
98
+ break; // first pattern-match per rule wins for this message
99
+ }
100
+ }
101
+ }
102
+ return Array.from(byKey.values());
103
+ },
104
+ };
105
+ }
106
+ //# sourceMappingURL=patternFactExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patternFactExtractor.js","sourceRoot":"","sources":["../../../../src/memory/facts/patternFactExtractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Facts — stable, timeless claims about the user or world.
3
+ *
4
+ * Unlike beats (which summarize what happened in a turn) and messages
5
+ * (which are the raw conversation), facts capture *what's currently
6
+ * true*:
7
+ * - Identity: "user.name" = "Alice"
8
+ * - Preferences: "user.favorite_color" = "blue"
9
+ * - Commitments: "task.ORD-123.status" = "refunded"
10
+ *
11
+ * Facts dedupe by `key`. The storage layer uses stable ids of the form
12
+ * `fact:${key}`, so a second write to the same key overwrites the
13
+ * first. This is the difference from beats/messages (which are
14
+ * append-only log entries).
15
+ */
16
+ /** Build the stable `MemoryStore` id for a fact with the given key. */
17
+ export function factId(key) {
18
+ return `fact:${key}`;
19
+ }
20
+ /** True iff the string is a fact id (starts with the `fact:` prefix). */
21
+ export function isFactId(id) {
22
+ return id.startsWith('fact:');
23
+ }
24
+ /**
25
+ * Duck-typed guard — true iff `value` has the shape of a `Fact`.
26
+ * Used by pipelines that handle mixed-payload stores (facts +
27
+ * beats + raw messages) to route entries correctly.
28
+ */
29
+ export function isFact(value) {
30
+ if (!value || typeof value !== 'object')
31
+ return false;
32
+ const v = value;
33
+ return typeof v.key === 'string' && v.key.length > 0 && 'value' in v;
34
+ }
35
+ /**
36
+ * Clamp a value to `[0, 1]`; non-finite → 0.5 (neutral). Matches the
37
+ * `asImportance` convention in the beats layer so pickers can treat
38
+ * `confidence` and `importance` the same way.
39
+ */
40
+ export function asConfidence(value) {
41
+ if (typeof value !== 'number' || !Number.isFinite(value))
42
+ return 0.5;
43
+ if (value < 0)
44
+ return 0;
45
+ if (value > 1)
46
+ return 1;
47
+ return value;
48
+ }
49
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/memory/facts/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4CH,uEAAuE;AACvE,MAAM,UAAU,MAAM,CAAC,GAAW;IAChC,OAAO,QAAQ,GAAG,EAAE,CAAC;AACvB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,QAAQ,CAAC,EAAU;IACjC,OAAO,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,IAAI,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACrE,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACxB,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,9 @@
1
+ export function writeFacts(config) {
2
+ return async (scope) => {
3
+ const facts = (scope.newFacts ?? []);
4
+ if (facts.length === 0)
5
+ return;
6
+ await config.store.putMany(scope.identity, facts);
7
+ };
8
+ }
9
+ //# sourceMappingURL=writeFacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeFacts.js","sourceRoot":"","sources":["../../../../src/memory/facts/writeFacts.ts"],"names":[],"mappings":"AAsBA,MAAM,UAAU,UAAU,CAAC,MAAwB;IACjD,OAAO,KAAK,EAAE,KAAoC,EAAiB,EAAE;QACnE,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAiC,CAAC;QACrE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * autoPipeline — opinionated preset combining facts + beats on one store.
3
+ *
4
+ * The one-line default for "I want memory — just make it good."
5
+ *
6
+ * READ : LoadAll → split by payload shape → FormatAuto
7
+ * (one system msg with facts block + narrative paragraph)
8
+ *
9
+ * WRITE : LoadFacts → ExtractFacts → WriteFacts
10
+ * → ExtractBeats → WriteBeats
11
+ *
12
+ * Facts carry "what's currently true" (identity, preferences,
13
+ * commitments — dedup on key). Beats carry "what happened" (traceable
14
+ * summaries — append-only). Together they cover the two memory shapes
15
+ * that matter for long-running agents without forcing the consumer to
16
+ * compose three presets by hand.
17
+ *
18
+ * **Default extractors**: zero-LLM-cost defaults so `autoPipeline({ store })`
19
+ * works out of the box:
20
+ * - facts: `patternFactExtractor()` (regex heuristics, free)
21
+ * - beats: `heuristicExtractor()` (keyword heuristics, free)
22
+ *
23
+ * Pass `provider` to upgrade BOTH to LLM-backed extractors in one
24
+ * knob — `llmFactExtractor({ provider })` and `llmExtractor({ provider })`.
25
+ * The same cheap model (e.g. `claude-haiku-4-5`) handles both extraction
26
+ * calls per turn.
27
+ *
28
+ * **Why not include raw messages?** Raw messages come from the agent's
29
+ * ongoing conversation state — they don't need a memory strategy. This
30
+ * preset focuses on the two *compressed / deduplicated* shapes. Layer on
31
+ * `defaultPipeline` or `semanticPipeline` manually if you want raw recall.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { Agent, anthropic } from 'agentfootprint';
36
+ * import { autoPipeline, InMemoryStore } from 'agentfootprint/memory';
37
+ *
38
+ * // Free defaults — regex + heuristics, no LLM cost.
39
+ * const pipeline = autoPipeline({ store: new InMemoryStore() });
40
+ *
41
+ * // Or upgrade both extractors in one knob:
42
+ * const hq = autoPipeline({
43
+ * store: new InMemoryStore(),
44
+ * provider: anthropic('claude-haiku-4-5'),
45
+ * });
46
+ *
47
+ * const agent = Agent.create({ provider: anthropic('claude-sonnet-4-5') })
48
+ * .system('You remember the user across turns.')
49
+ * .memoryPipeline(pipeline)
50
+ * .build();
51
+ * ```
52
+ */
53
+ import { flowChart } from 'footprintjs';
54
+ import { extractFacts, writeFacts, loadFacts, patternFactExtractor, llmFactExtractor, isFactId, } from '../facts';
55
+ import { extractBeats, writeBeats, heuristicExtractor, llmExtractor, isNarrativeBeat, } from '../beats';
56
+ const DEFAULT_FACTS_HEADER = 'Known facts about the user:';
57
+ const DEFAULT_LEAD_IN = 'From earlier: ';
58
+ const DEFAULT_LOAD_LIMIT = 200;
59
+ /** Escape `</memory>` in user-controlled text to prevent tag-escape injection. */
60
+ function escapeMemoryTag(text) {
61
+ return text.replace(/<\/memory>/gi, '</m\u200Demory>');
62
+ }
63
+ function renderValue(v) {
64
+ if (typeof v === 'string')
65
+ return v;
66
+ try {
67
+ return JSON.stringify(v);
68
+ }
69
+ catch {
70
+ return String(v);
71
+ }
72
+ }
73
+ /**
74
+ * Emit a single system message combining facts (key/value block) and
75
+ * beats (narrative paragraph). Empty sections are skipped; when BOTH
76
+ * are empty, `formatted = []` (no injection).
77
+ */
78
+ function renderAutoMessage(facts, beats, opts) {
79
+ const sections = [];
80
+ if (facts.length > 0) {
81
+ const lines = facts.map((entry) => {
82
+ const f = entry.value;
83
+ const value = escapeMemoryTag(renderValue(f.value));
84
+ const conf = opts.showConfidence && typeof f.confidence === 'number'
85
+ ? ` (conf ${f.confidence.toFixed(2)})`
86
+ : '';
87
+ return `- ${f.key}: ${value}${conf}`;
88
+ });
89
+ sections.push(`${opts.factsHeader}\n\n${lines.join('\n')}`);
90
+ }
91
+ if (beats.length > 0) {
92
+ const sentences = beats.map((entry) => {
93
+ const beat = entry.value;
94
+ const text = escapeMemoryTag(beat.summary.trim());
95
+ const withRefs = opts.showRefs && beat.refs.length > 0 ? `${text} (refs: ${beat.refs.join(', ')})` : text;
96
+ return /[.!?]$/.test(withRefs) ? withRefs : `${withRefs}.`;
97
+ });
98
+ sections.push(`${opts.leadIn}${sentences.join(' ')}`);
99
+ }
100
+ if (sections.length === 0)
101
+ return [];
102
+ return [{ role: 'system', content: sections.join('\n\n') }];
103
+ }
104
+ export function autoPipeline(config) {
105
+ const factExtractor = config.factExtractor ??
106
+ (config.provider ? llmFactExtractor({ provider: config.provider }) : patternFactExtractor());
107
+ const beatExtractor = config.beatExtractor ??
108
+ (config.provider ? llmExtractor({ provider: config.provider }) : heuristicExtractor());
109
+ const loadLimit = config.loadLimit ?? DEFAULT_LOAD_LIMIT;
110
+ const factsHeader = config.factsHeader ?? DEFAULT_FACTS_HEADER;
111
+ const leadIn = config.narrativeLeadIn ?? DEFAULT_LEAD_IN;
112
+ const showConfidence = config.showConfidence ?? false;
113
+ const showRefs = config.showRefs ?? false;
114
+ // ── READ subflow ────────────────────────────────────────────
115
+ // One combined load + split stage. Keeps the subflow short and
116
+ // avoids pulling `loadRecent` + `pickByBudget` (which were designed
117
+ // for single-payload pipelines) into a mixed-payload context.
118
+ const read = flowChart('LoadAll', async (scope) => {
119
+ const listOpts = {
120
+ limit: loadLimit,
121
+ ...(config.tiers && { tiers: config.tiers }),
122
+ };
123
+ const { entries } = await config.store.list(scope.identity, listOpts);
124
+ const loadedFacts = [];
125
+ const loadedBeats = [];
126
+ for (const entry of entries) {
127
+ if (isFactId(entry.id)) {
128
+ loadedFacts.push(entry);
129
+ }
130
+ else if (isNarrativeBeat(entry.value)) {
131
+ loadedBeats.push(entry);
132
+ }
133
+ // Other payloads (raw messages) are intentionally ignored —
134
+ // auto() is the "compressed + deduped" preset.
135
+ }
136
+ // Stores typically return most-recently-updated first. Beats are
137
+ // more readable oldest-first (chronological narrative), so
138
+ // reverse before injection. Facts render as a list, order-agnostic.
139
+ scope.loadedFacts = loadedFacts;
140
+ scope.loadedBeats = [...loadedBeats].reverse();
141
+ }, 'load-all', undefined, 'Load facts + beats from the shared store; split by payload shape')
142
+ .addFunction('FormatAuto', async (scope) => {
143
+ const facts = (scope.loadedFacts ?? []);
144
+ const beats = (scope.loadedBeats ?? []);
145
+ scope.formatted = renderAutoMessage(facts, beats, {
146
+ factsHeader,
147
+ leadIn,
148
+ showConfidence,
149
+ showRefs,
150
+ });
151
+ }, 'format-auto', 'Emit one system message with facts block + narrative paragraph')
152
+ .build();
153
+ // ── WRITE subflow ───────────────────────────────────────────
154
+ // LoadFacts first so the (possibly LLM-backed) fact extractor sees
155
+ // existing facts via scope.loadedFacts and can update rather than
156
+ // duplicate. Then extract+write facts, then extract+write beats.
157
+ const write = flowChart('LoadFacts', loadFacts({
158
+ store: config.store,
159
+ limit: loadLimit,
160
+ ...(config.tiers && { tiers: config.tiers }),
161
+ }), 'load-facts', undefined, 'Surface existing facts for update-awareness')
162
+ .addFunction('ExtractFacts', extractFacts({
163
+ extractor: factExtractor,
164
+ ...(config.writeTier && { tier: config.writeTier }),
165
+ ...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
166
+ }), 'extract-facts', 'Distill scope.newMessages into stable Fact entries')
167
+ .addFunction('WriteFacts', writeFacts({ store: config.store }), 'write-facts', 'Persist facts via store.putMany (overwrite on key collision)')
168
+ .addFunction('ExtractBeats', extractBeats({
169
+ extractor: beatExtractor,
170
+ ...(config.writeTier && { tier: config.writeTier }),
171
+ ...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
172
+ }), 'extract-beats', 'Compress scope.newMessages into NarrativeBeat entries')
173
+ .addFunction('WriteBeats', writeBeats({ store: config.store }), 'write-beats', 'Persist beats via store.putMany')
174
+ .build();
175
+ return { read, write };
176
+ }
177
+ //# sourceMappingURL=auto.js.map