@jeremiaheth/neolata-mem 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Graph-native memory engine for AI agents.** Zettelkasten-inspired linking, biological decay, conflict resolution.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/@jeremiaheth/neolata-mem.svg)](https://www.npmjs.com/package/@jeremiaheth/neolata-mem)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@jeremiaheth/neolata-mem)](https://bundlephobia.com/package/@jeremiaheth/neolata-mem)
5
7
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
7
9
 
@@ -13,6 +15,58 @@ No Python. No Docker. No Neo4j. Just `npm install`.
13
15
  npm install @jeremiaheth/neolata-mem
14
16
  ```
15
17
 
18
+ ## Why neolata-mem?
19
+
20
+ **Because AI agents need memory that works like actual memory** — connected, decaying, evolving. Not a flat vector database with timestamps. A living knowledge graph that links related facts, forgets what doesn't matter, and resolves contradictions automatically.
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────────────────┐
26
+ │ STORE WORKFLOW (A-MEM) │
27
+ ├─────────────────────────────────────────────────────────────────────┤
28
+ │ │
29
+ │ Text Input │
30
+ │ │ │
31
+ │ ├──► [Embed] ──────────────┐ │
32
+ │ │ ▼ │
33
+ │ │ Find Related Memories │
34
+ │ │ (similarity > threshold) │
35
+ │ │ │ │
36
+ │ │ ▼ │
37
+ │ │ Auto-Link Bidirectionally │
38
+ │ │ (top N similar) │
39
+ │ │ │ │
40
+ │ └────────────────────────── ┘ │
41
+ │ │ │
42
+ │ ▼ │
43
+ │ [Store in Graph] │
44
+ │ Memory + Links + Embedding │
45
+ │ │
46
+ └─────────────────────────────────────────────────────────────────────┘
47
+
48
+ ┌─────────────────────────────────────────────────────────────────────┐
49
+ │ DECAY CYCLE │
50
+ ├─────────────────────────────────────────────────────────────────────┤
51
+ │ │
52
+ │ For each Memory: │
53
+ │ │ │
54
+ │ ├──► Calculate Strength: │
55
+ │ │ • Base: importance (0.0-1.0) │
56
+ │ │ • Age decay: exp(-age / half-life) │
57
+ │ │ • Link reinforcement: +5% per link (max +30%) │
58
+ │ │ • Category stickiness: decisions 1.3x, preferences 1.4x │
59
+ │ │ • Access boost: +2% per touch (max +20%) │
60
+ │ │ │
61
+ │ ├──► strength < 0.05 ? ──► DELETE │
62
+ │ │ │
63
+ │ ├──► strength < 0.15 ? ──► ARCHIVE (remove embedding, keep) │
64
+ │ │ │
65
+ │ └──► Clean broken links │
66
+ │ │
67
+ └─────────────────────────────────────────────────────────────────────┘
68
+ ```
69
+
16
70
  ## Quick Start (3 lines)
17
71
 
18
72
  ```javascript
@@ -292,6 +346,39 @@ Decay Cycle:
292
346
  | Zero-config start | ✅ | ❌ | ❌ | ❌ |
293
347
  | LLM optional | ✅ | ❌ | ❌ | ❌ |
294
348
 
349
+ ## Documentation
350
+
351
+ Full documentation available at [neolata-mem docs](https://github.com/Jeremiaheth/neolata-mem/tree/main/docs-site).
352
+
353
+ Run locally:
354
+ ```bash
355
+ cd docs-site
356
+ npm install
357
+ npm start
358
+ ```
359
+
360
+ ## Contributing
361
+
362
+ Contributions welcome! This is built by people who break things for a living.
363
+
364
+ **Before submitting:**
365
+ - Run tests: `npm test`
366
+ - Keep the hacker aesthetic — no corporate fluff
367
+ - Code examples must actually work
368
+ - Break it before you ship it
369
+
370
+ **Areas that need work:**
371
+ - More storage backends (Supabase, Redis, SQLite)
372
+ - Additional embedding providers
373
+ - Performance optimizations for 100k+ memory graphs
374
+ - Visualization tools
375
+
376
+ Open an issue or PR. No bureaucracy.
377
+
378
+ ## Changelog
379
+
380
+ See [CHANGELOG.md](CHANGELOG.md) for version history and breaking changes.
381
+
295
382
  ## License
296
383
 
297
384
  MIT — do whatever you want.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeremiaheth/neolata-mem",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Graph-native memory engine for AI agents with Zettelkasten linking, biological decay, and conflict resolution",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
@@ -22,7 +22,8 @@
22
22
  "README.md"
23
23
  ],
24
24
  "scripts": {
25
- "test": "node --test test/*.test.mjs"
25
+ "test": "node --test test/*.test.mjs",
26
+ "lint": "echo 'no linter configured'"
26
27
  },
27
28
  "keywords": [
28
29
  "ai",
@@ -35,8 +36,7 @@
35
36
  "llm",
36
37
  "vector-search",
37
38
  "multi-agent",
38
- "decay",
39
- "conflict-resolution"
39
+ "decay"
40
40
  ],
41
41
  "author": "Jeremiaheth",
42
42
  "license": "MIT",
@@ -46,5 +46,7 @@
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18.0.0"
49
- }
49
+ },
50
+ "dependencies": {},
51
+ "devDependencies": {}
50
52
  }
@@ -1,13 +1,50 @@
1
1
  /**
2
- * Embedding provider interface and implementations.
3
- * All providers must implement: embed(texts) → number[][]
2
+ * @module embeddings
3
+ * @description Embedding provider interface and implementations.
4
+ *
5
+ * All embedding providers must implement the following interface:
6
+ * ```typescript
7
+ * interface EmbeddingProvider {
8
+ * name: string;
9
+ * model: string | null;
10
+ * embed(texts: string | string[]): Promise<(number[] | null)[]>;
11
+ * }
12
+ * ```
13
+ *
14
+ * @example
15
+ * // OpenAI embeddings
16
+ * import { openaiEmbeddings } from '@jeremiaheth/neolata-mem/embeddings';
17
+ * const embedder = openaiEmbeddings({
18
+ * apiKey: process.env.OPENAI_API_KEY,
19
+ * model: 'text-embedding-3-small',
20
+ * });
21
+ * const vectors = await embedder.embed(['Hello world', 'Goodbye world']);
22
+ *
23
+ * @example
24
+ * // No-op embeddings (keyword search only)
25
+ * import { noopEmbeddings } from '@jeremiaheth/neolata-mem/embeddings';
26
+ * const embedder = noopEmbeddings();
27
+ * const vectors = await embedder.embed(['text']); // → [null]
4
28
  */
5
29
 
6
30
  /**
7
- * Cosine similarity between two vectors.
8
- * @param {number[]} a
9
- * @param {number[]} b
10
- * @returns {number}
31
+ * @typedef {Object} EmbeddingProvider
32
+ * @property {string} name - Provider identifier
33
+ * @property {string|null} model - Model name or null
34
+ * @property {function(string|string[]): Promise<(number[]|null)[]>} embed - Embed text(s) to vectors
35
+ */
36
+
37
+ /**
38
+ * Calculate cosine similarity between two embedding vectors.
39
+ *
40
+ * @param {number[]} a - First vector
41
+ * @param {number[]} b - Second vector (must be same length as a)
42
+ * @returns {number} Similarity score (0.0 to 1.0, higher = more similar)
43
+ * @throws {Error} If vectors are different lengths or empty
44
+ *
45
+ * @example
46
+ * const sim = cosineSimilarity([1, 0, 0], [1, 0, 0]); // → 1.0 (identical)
47
+ * const sim2 = cosineSimilarity([1, 0, 0], [0, 1, 0]); // → 0.0 (orthogonal)
11
48
  */
12
49
  export function cosineSimilarity(a, b) {
13
50
  let dot = 0, normA = 0, normB = 0;
@@ -19,21 +56,66 @@ export function cosineSimilarity(a, b) {
19
56
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
20
57
  }
21
58
 
22
- // ─── OpenAI-Compatible Provider ─────────────────────────────
23
59
  /**
24
- * Works with OpenAI, NVIDIA NIM, Ollama, Azure, any OpenAI-compatible API.
25
- * @param {object} opts
26
- * @param {string} opts.apiKey
27
- * @param {string} opts.model - e.g. 'text-embedding-3-small', 'baai/bge-m3'
28
- * @param {string} [opts.baseUrl='https://api.openai.com/v1'] - API base URL
29
- * @param {object} [opts.extraBody] - Extra body params (e.g. { input_type: 'passage' })
30
- * @param {number} [opts.retryMs=2000] - Retry delay on 429
60
+ * OpenAI-compatible embedding provider.
61
+ *
62
+ * Works with any API that implements the OpenAI `/v1/embeddings` endpoint:
63
+ * - OpenAI (text-embedding-3-small, text-embedding-3-large)
64
+ * - NVIDIA NIM (baai/bge-m3, nvidia/nv-embed-v1)
65
+ * - Ollama (nomic-embed-text, mxbai-embed-large)
66
+ * - Azure OpenAI
67
+ * - Groq, Together, etc.
68
+ *
69
+ * **Rate limiting:** Automatically retries on 429 with exponential backoff.
70
+ *
71
+ * @param {Object} opts - Configuration options
72
+ * @param {string} opts.apiKey - API key for authentication
73
+ * @param {string} opts.model - Model identifier (e.g. 'text-embedding-3-small', 'baai/bge-m3')
74
+ * @param {string} [opts.baseUrl='https://api.openai.com/v1'] - API base URL (without /embeddings)
75
+ * @param {Object} [opts.extraBody={}] - Additional request body parameters (e.g. { input_type: 'passage' } for NIM)
76
+ * @param {number} [opts.retryMs=2000] - Base retry delay on 429 in milliseconds
77
+ * @param {number} [opts.maxRetries=3] - Maximum retry attempts on 429
78
+ * @returns {EmbeddingProvider} Configured embedding provider
79
+ * @throws {Error} If apiKey or model is missing
80
+ *
81
+ * @example
82
+ * // OpenAI
83
+ * const emb = openaiEmbeddings({
84
+ * apiKey: process.env.OPENAI_API_KEY,
85
+ * model: 'text-embedding-3-small',
86
+ * });
87
+ *
88
+ * @example
89
+ * // NVIDIA NIM (free tier)
90
+ * const emb = openaiEmbeddings({
91
+ * apiKey: process.env.NVIDIA_API_KEY,
92
+ * model: 'baai/bge-m3',
93
+ * baseUrl: 'https://integrate.api.nvidia.com/v1',
94
+ * extraBody: { input_type: 'passage', truncate: 'END' },
95
+ * });
96
+ *
97
+ * @example
98
+ * // Local Ollama
99
+ * const emb = openaiEmbeddings({
100
+ * apiKey: 'ollama', // Ollama ignores auth
101
+ * model: 'nomic-embed-text',
102
+ * baseUrl: 'http://localhost:11434/v1',
103
+ * });
31
104
  */
32
- export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.com/v1', extraBody = {}, retryMs = 2000 }) {
105
+ export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.com/v1', extraBody = {}, retryMs = 2000, maxRetries = 3 }) {
106
+ if (!apiKey) throw new Error('openaiEmbeddings: apiKey is required');
107
+ if (!model) throw new Error('openaiEmbeddings: model is required');
33
108
  return {
34
109
  name: `openai-compatible(${model})`,
35
110
  model,
36
- async embed(texts) {
111
+ /**
112
+ * Embed one or more texts.
113
+ * @param {string|string[]} texts - Text(s) to embed
114
+ * @param {number} [_attempt=0] - Internal retry counter
115
+ * @returns {Promise<number[][]>} Array of embedding vectors
116
+ * @throws {Error} On network failure, API error, or rate limit exceeded
117
+ */
118
+ async embed(texts, _attempt = 0) {
37
119
  const input = Array.isArray(texts) ? texts : [texts];
38
120
  const res = await fetch(`${baseUrl}/embeddings`, {
39
121
  method: 'POST',
@@ -41,8 +123,9 @@ export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.
41
123
  body: JSON.stringify({ model, input, ...extraBody }),
42
124
  });
43
125
  if (res.status === 429) {
44
- await new Promise(r => setTimeout(r, retryMs));
45
- return this.embed(texts);
126
+ if (_attempt >= maxRetries) throw new Error(`Embedding 429: rate limited after ${maxRetries} retries`);
127
+ await new Promise(r => setTimeout(r, retryMs * (_attempt + 1)));
128
+ return this.embed(texts, _attempt + 1);
46
129
  }
47
130
  if (!res.ok) throw new Error(`Embedding ${res.status}: ${await res.text()}`);
48
131
  const data = await res.json();
@@ -51,15 +134,31 @@ export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.
51
134
  };
52
135
  }
53
136
 
54
- // ─── Noop Provider (keyword-only mode) ──────────────────────
55
137
  /**
56
- * No-op embedding provider. Returns null embeddings.
57
- * Use this when you don't want/need vector search — keyword matching still works.
138
+ * No-op embedding provider for keyword-only search.
139
+ *
140
+ * Returns `null` embeddings, which disables semantic search and falls back
141
+ * to substring matching. Useful for:
142
+ * - Testing without API dependencies
143
+ * - Offline/airgapped environments
144
+ * - Simple exact-match use cases
145
+ *
146
+ * @param {Object} [opts] - Options (currently unused, reserved for future)
147
+ * @returns {EmbeddingProvider} No-op provider
148
+ *
149
+ * @example
150
+ * const mem = createMemory({ embeddings: { type: 'noop' } });
151
+ * await mem.store('a', 'User prefers dark mode');
152
+ * const results = await mem.search('a', 'dark mode'); // Keyword match
58
153
  */
59
154
  export function noopEmbeddings() {
60
155
  return {
61
156
  name: 'noop',
62
157
  model: null,
158
+ /**
159
+ * @param {string|string[]} texts
160
+ * @returns {Promise<null[]>} Array of nulls
161
+ */
63
162
  async embed(texts) {
64
163
  const input = Array.isArray(texts) ? texts : [texts];
65
164
  return input.map(() => null);
@@ -1,22 +1,55 @@
1
1
  /**
2
- * Fact extraction providers.
3
- * All providers must implement: extract(text) Fact[]
4
- * Fact = { fact: string, category: string, importance: number, tags: string[] }
2
+ * @module extraction
3
+ * @description Fact extraction providers for bulk ingestion.
4
+ *
5
+ * All extraction providers must implement:
6
+ * ```typescript
7
+ * interface ExtractionProvider {
8
+ * name: string;
9
+ * extract(text: string): Promise<Fact[]>;
10
+ * }
11
+ *
12
+ * interface Fact {
13
+ * fact: string;
14
+ * category: string;
15
+ * importance: number;
16
+ * tags: string[];
17
+ * }
18
+ * ```
19
+ *
20
+ * @example
21
+ * // LLM extraction
22
+ * import { llmExtraction } from '@jeremiaheth/neolata-mem/extraction';
23
+ * const extractor = llmExtraction({
24
+ * apiKey: process.env.OPENAI_API_KEY,
25
+ * model: 'gpt-4.1-nano',
26
+ * });
27
+ * const facts = await extractor.extract('User deployed v2.1 with Redis caching on port 6379');
28
+ * // [{ fact: 'Deployed v2.1', category: 'event', importance: 0.7, tags: ['deployment'] }, ...]
5
29
  */
6
30
 
7
- // ─── LLM-Based Extraction (OpenAI-Compatible) ──────────────
31
+ import { openaiChat } from './llm.mjs';
32
+
8
33
  /**
9
- * Uses any OpenAI-compatible chat API to extract atomic facts.
10
- * @param {object} opts
11
- * @param {string} opts.apiKey
12
- * @param {string} [opts.model='gpt-4.1-nano'] - Chat model
13
- * @param {string} [opts.baseUrl='https://api.openai.com/v1']
34
+ * @typedef {Object} Fact
35
+ * @property {string} fact - Atomic fact statement
36
+ * @property {string} category - Fact category (decision|finding|fact|insight|task|event|preference)
37
+ * @property {number} importance - Importance score 0.0-1.0
38
+ * @property {string[]} tags - Extracted keywords/tags
14
39
  */
15
- export function llmExtraction({ apiKey, model = 'gpt-4.1-nano', baseUrl = 'https://api.openai.com/v1' }) {
16
- return {
17
- name: `llm(${model})`,
18
- async extract(text) {
19
- const prompt = `You are a precise fact extractor. Extract discrete, atomic facts from the following text. Each fact should be self-contained and include specific details (names, numbers, dates, decisions, preferences).
40
+
41
+ /**
42
+ * @typedef {Object} ExtractionProvider
43
+ * @property {string} name - Provider identifier
44
+ * @property {function(string): Promise<Fact[]>} extract - Extract facts from text
45
+ */
46
+
47
+ /**
48
+ * LLM prompt for structured fact extraction.
49
+ * @const {string}
50
+ * @private
51
+ */
52
+ const EXTRACT_PROMPT = `You are a precise fact extractor. Extract discrete, atomic facts from the following text. Each fact should be self-contained and include specific details (names, numbers, dates, decisions, preferences).
20
53
 
21
54
  Output as a JSON array of objects with fields:
22
55
  - "fact": the extracted fact (string)
@@ -25,43 +58,122 @@ Output as a JSON array of objects with fields:
25
58
  - "tags": array of relevant keywords
26
59
 
27
60
  Text to extract from:
28
- ${text}
29
-
30
- Respond ONLY with the JSON array, no markdown formatting.`;
61
+ `;
31
62
 
63
+ /**
64
+ * LLM-based fact extraction (OpenAI-compatible API).
65
+ *
66
+ * Sends text to an LLM with structured prompt to extract atomic facts.
67
+ * Falls back to single-fact passthrough on LLM failure.
68
+ *
69
+ * **Recommended models:**
70
+ * - `gpt-4.1-nano` (fast, cheap)
71
+ * - `gpt-4o-mini`
72
+ * - `claude-3-haiku` (if using Anthropic)
73
+ *
74
+ * **Cost:** ~$0.0001-0.001 per 1000 chars depending on model.
75
+ *
76
+ * @param {Object} opts - Configuration options
77
+ * @param {string} opts.apiKey - API key for OpenAI-compatible endpoint
78
+ * @param {string} [opts.model='gpt-4.1-nano'] - Chat model for extraction
79
+ * @param {string} [opts.baseUrl='https://api.openai.com/v1'] - API base URL
80
+ * @returns {ExtractionProvider} Configured extraction provider
81
+ * @throws {Error} If apiKey is missing
82
+ *
83
+ * @example
84
+ * // OpenAI
85
+ * const ext = llmExtraction({
86
+ * apiKey: process.env.OPENAI_API_KEY,
87
+ * model: 'gpt-4.1-nano',
88
+ * });
89
+ *
90
+ * @example
91
+ * // Groq (fast inference)
92
+ * const ext = llmExtraction({
93
+ * apiKey: process.env.GROQ_API_KEY,
94
+ * model: 'llama-3.3-70b-versatile',
95
+ * baseUrl: 'https://api.groq.com/openai/v1',
96
+ * });
97
+ *
98
+ * @example
99
+ * // Use with ingest
100
+ * const mem = createMemory({
101
+ * extraction: { type: 'llm', apiKey: KEY, model: 'gpt-4.1-nano' },
102
+ * });
103
+ * const result = await mem.ingest('agent', longDocument);
104
+ * // Extracted facts automatically stored with A-MEM linking
105
+ */
106
+ export function llmExtraction({ apiKey, model = 'gpt-4.1-nano', baseUrl = 'https://api.openai.com/v1' }) {
107
+ if (!apiKey) throw new Error('llmExtraction: apiKey is required');
108
+ const chat = openaiChat({ apiKey, model, baseUrl, maxTokens: 2000, temperature: 0.1 });
109
+ return {
110
+ name: `llm(${model})`,
111
+
112
+ /**
113
+ * Extract structured facts from text using LLM.
114
+ * @param {string} text - Input text (up to ~4000 tokens)
115
+ * @returns {Promise<Fact[]>} Array of extracted facts
116
+ * @throws Never throws — falls back to passthrough on failure
117
+ *
118
+ * @example
119
+ * const facts = await extractor.extract(`
120
+ * Meeting notes 2024-02-22:
121
+ * - Decided to migrate to PostgreSQL
122
+ * - Redis will handle session cache on port 6379
123
+ * - Deploy v2.1 by Friday
124
+ * `);
125
+ * // [
126
+ * // { fact: 'Decided to migrate to PostgreSQL', category: 'decision', importance: 0.9, tags: ['postgresql', 'migration'] },
127
+ * // { fact: 'Redis handles session cache on port 6379', category: 'fact', importance: 0.7, tags: ['redis', 'cache'] },
128
+ * // { fact: 'Deploy v2.1 by Friday', category: 'task', importance: 0.8, tags: ['deployment', 'deadline'] },
129
+ * // ]
130
+ */
131
+ async extract(text) {
32
132
  try {
33
- const res = await fetch(`${baseUrl}/chat/completions`, {
34
- method: 'POST',
35
- headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
36
- body: JSON.stringify({
37
- model,
38
- messages: [{ role: 'user', content: prompt }],
39
- max_tokens: 2000,
40
- temperature: 0.1,
41
- }),
42
- });
43
- if (!res.ok) throw new Error(`LLM ${res.status}: ${await res.text()}`);
44
- const data = await res.json();
45
- const content = data.choices?.[0]?.message?.content || '';
133
+ const content = await chat.chat(`${EXTRACT_PROMPT}${text}\n\nRespond ONLY with the JSON array, no markdown formatting.`);
46
134
  const jsonStr = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
47
135
  return JSON.parse(jsonStr);
48
136
  } catch (e) {
137
+ // Fallback: treat entire text as single fact
49
138
  return [{ fact: text, category: 'fact', importance: 0.5, tags: [] }];
50
139
  }
51
140
  },
52
141
  };
53
142
  }
54
143
 
55
- // ─── Passthrough Extraction (No LLM) ────────────────────────
56
144
  /**
57
- * Treats the entire input as a single fact. No LLM required.
58
- * @param {object} [opts]
59
- * @param {string} [opts.defaultCategory='fact']
60
- * @param {number} [opts.defaultImportance=0.5]
145
+ * Passthrough extraction (no LLM, treats input as single fact).
146
+ *
147
+ * Wraps the entire input text as a single fact. Useful when:
148
+ * - Input is already atomic/structured
149
+ * - No LLM access (offline mode)
150
+ * - You want manual control over categories/importance
151
+ *
152
+ * @param {Object} [opts] - Configuration
153
+ * @param {string} [opts.defaultCategory='fact'] - Default category for all facts
154
+ * @param {number} [opts.defaultImportance=0.5] - Default importance (0.0-1.0)
155
+ * @returns {ExtractionProvider} Passthrough provider
156
+ *
157
+ * @example
158
+ * const ext = passthroughExtraction({ defaultCategory: 'event', defaultImportance: 0.7 });
159
+ * const facts = await ext.extract('Deployed v2.1');
160
+ * // [{ fact: 'Deployed v2.1', category: 'event', importance: 0.7, tags: [] }]
161
+ *
162
+ * @example
163
+ * // Use with ingest when you control fact structure
164
+ * const mem = createMemory({
165
+ * extraction: { type: 'passthrough' },
166
+ * });
167
+ * await mem.ingest('agent', 'Server runs on port 8080'); // Single fact
61
168
  */
62
169
  export function passthroughExtraction({ defaultCategory = 'fact', defaultImportance = 0.5 } = {}) {
63
170
  return {
64
171
  name: 'passthrough',
172
+
173
+ /**
174
+ * @param {string} text
175
+ * @returns {Promise<Fact[]>}
176
+ */
65
177
  async extract(text) {
66
178
  return [{ fact: text, category: defaultCategory, importance: defaultImportance, tags: [] }];
67
179
  },