@openanonymity/nanomem 0.1.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 +194 -0
- package/package.json +85 -0
- package/src/backends/BaseStorage.js +177 -0
- package/src/backends/filesystem.js +177 -0
- package/src/backends/indexeddb.js +208 -0
- package/src/backends/ram.js +113 -0
- package/src/backends/schema.js +42 -0
- package/src/bullets/bulletIndex.js +125 -0
- package/src/bullets/compaction.js +109 -0
- package/src/bullets/index.js +16 -0
- package/src/bullets/normalize.js +241 -0
- package/src/bullets/parser.js +199 -0
- package/src/bullets/scoring.js +53 -0
- package/src/cli/auth.js +323 -0
- package/src/cli/commands.js +411 -0
- package/src/cli/config.js +120 -0
- package/src/cli/diff.js +68 -0
- package/src/cli/help.js +84 -0
- package/src/cli/output.js +269 -0
- package/src/cli/spinner.js +54 -0
- package/src/cli.js +178 -0
- package/src/engine/compactor.js +247 -0
- package/src/engine/executors.js +152 -0
- package/src/engine/ingester.js +229 -0
- package/src/engine/retriever.js +414 -0
- package/src/engine/toolLoop.js +176 -0
- package/src/imports/chatgpt.js +160 -0
- package/src/imports/index.js +14 -0
- package/src/imports/markdown.js +104 -0
- package/src/imports/oaFastchat.js +124 -0
- package/src/index.js +199 -0
- package/src/llm/anthropic.js +264 -0
- package/src/llm/openai.js +179 -0
- package/src/prompt_sets/conversation/ingestion.js +51 -0
- package/src/prompt_sets/document/ingestion.js +43 -0
- package/src/prompt_sets/index.js +31 -0
- package/src/types.js +382 -0
- package/src/utils/portability.js +174 -0
- package/types/backends/BaseStorage.d.ts +42 -0
- package/types/backends/filesystem.d.ts +11 -0
- package/types/backends/indexeddb.d.ts +12 -0
- package/types/backends/ram.d.ts +8 -0
- package/types/backends/schema.d.ts +14 -0
- package/types/bullets/bulletIndex.d.ts +47 -0
- package/types/bullets/compaction.d.ts +10 -0
- package/types/bullets/index.d.ts +36 -0
- package/types/bullets/normalize.d.ts +95 -0
- package/types/bullets/parser.d.ts +31 -0
- package/types/bullets/scoring.d.ts +12 -0
- package/types/engine/compactor.d.ts +27 -0
- package/types/engine/executors.d.ts +46 -0
- package/types/engine/ingester.d.ts +29 -0
- package/types/engine/retriever.d.ts +50 -0
- package/types/engine/toolLoop.d.ts +9 -0
- package/types/imports/chatgpt.d.ts +14 -0
- package/types/imports/index.d.ts +3 -0
- package/types/imports/markdown.d.ts +31 -0
- package/types/imports/oaFastchat.d.ts +30 -0
- package/types/index.d.ts +21 -0
- package/types/llm/anthropic.d.ts +16 -0
- package/types/llm/openai.d.ts +16 -0
- package/types/prompt_sets/conversation/ingestion.d.ts +7 -0
- package/types/prompt_sets/document/ingestion.d.ts +7 -0
- package/types/prompt_sets/index.d.ts +11 -0
- package/types/types.d.ts +293 -0
- package/types/utils/portability.d.ts +33 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Messages API client.
|
|
3
|
+
*
|
|
4
|
+
* Translates the standard LLM client interface (OpenAI Chat Completions format)
|
|
5
|
+
* into Anthropic's Messages API format, so the memory system can use Claude models.
|
|
6
|
+
*
|
|
7
|
+
* Uses `fetch` (built into Node 18+ and browsers).
|
|
8
|
+
*/
|
|
9
|
+
/** @import { ChatCompletionParams, ChatCompletionResponse, LLMClient, LLMClientOptions, LLMMessage, ToolCall, ToolDefinition } from '../types.js' */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {LLMClientOptions} [options]
|
|
13
|
+
* @returns {LLMClient}
|
|
14
|
+
*/
|
|
15
|
+
export function createAnthropicClient({ apiKey, baseUrl = 'https://api.anthropic.com', headers = {} } = /** @type {LLMClientOptions} */ ({ apiKey: '' })) {
|
|
16
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
17
|
+
|
|
18
|
+
function buildHeaders() {
|
|
19
|
+
return {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'x-api-key': apiKey,
|
|
22
|
+
'anthropic-version': '2023-06-01',
|
|
23
|
+
...headers,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function convertMessages(messages) {
|
|
28
|
+
let system = '';
|
|
29
|
+
const converted = [];
|
|
30
|
+
|
|
31
|
+
for (const msg of messages) {
|
|
32
|
+
if (msg.role === 'system') {
|
|
33
|
+
system += (system ? '\n\n' : '') + msg.content;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (msg.role === 'tool') {
|
|
38
|
+
// Anthropic uses tool_result content blocks inside "user" messages
|
|
39
|
+
converted.push({
|
|
40
|
+
role: 'user',
|
|
41
|
+
content: [{
|
|
42
|
+
type: 'tool_result',
|
|
43
|
+
tool_use_id: msg.tool_call_id,
|
|
44
|
+
content: msg.content,
|
|
45
|
+
}],
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
51
|
+
// Build content blocks: text (if any) + tool_use blocks
|
|
52
|
+
const content = [];
|
|
53
|
+
if (msg.content) {
|
|
54
|
+
content.push({ type: 'text', text: msg.content });
|
|
55
|
+
}
|
|
56
|
+
for (const tc of msg.tool_calls) {
|
|
57
|
+
let input;
|
|
58
|
+
try {
|
|
59
|
+
input = typeof tc.function?.arguments === 'string'
|
|
60
|
+
? JSON.parse(tc.function.arguments)
|
|
61
|
+
: (tc.function?.arguments || {});
|
|
62
|
+
} catch {
|
|
63
|
+
input = {};
|
|
64
|
+
}
|
|
65
|
+
content.push({
|
|
66
|
+
type: 'tool_use',
|
|
67
|
+
id: tc.id,
|
|
68
|
+
name: tc.function?.name || '',
|
|
69
|
+
input,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
converted.push({ role: 'assistant', content });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Regular user/assistant message
|
|
77
|
+
converted.push({
|
|
78
|
+
role: msg.role,
|
|
79
|
+
content: msg.content || '',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { system, messages: converted };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function convertTools(tools) {
|
|
87
|
+
if (!tools || tools.length === 0) return undefined;
|
|
88
|
+
return tools.map(t => ({
|
|
89
|
+
name: t.function?.name || '',
|
|
90
|
+
description: t.function?.description || '',
|
|
91
|
+
input_schema: t.function?.parameters || { type: 'object', properties: {} },
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function createChatCompletion({ model, messages, tools, max_tokens, temperature }) {
|
|
96
|
+
const { system, messages: convertedMessages } = convertMessages(messages);
|
|
97
|
+
const body = {
|
|
98
|
+
model,
|
|
99
|
+
messages: convertedMessages,
|
|
100
|
+
max_tokens: max_tokens || 1024,
|
|
101
|
+
temperature: temperature ?? 0,
|
|
102
|
+
};
|
|
103
|
+
if (system) body.system = system;
|
|
104
|
+
const anthropicTools = convertTools(tools);
|
|
105
|
+
if (anthropicTools) body.tools = anthropicTools;
|
|
106
|
+
|
|
107
|
+
const response = await fetch(`${base}/v1/messages`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: buildHeaders(),
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const text = await response.text().catch(() => '');
|
|
115
|
+
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
return parseAnthropicResponse(data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function streamChatCompletion({ model, messages, tools, max_tokens, temperature, onDelta, onReasoning }) {
|
|
123
|
+
const { system, messages: convertedMessages } = convertMessages(messages);
|
|
124
|
+
const body = {
|
|
125
|
+
model,
|
|
126
|
+
messages: convertedMessages,
|
|
127
|
+
max_tokens: max_tokens || 1024,
|
|
128
|
+
temperature: temperature ?? 0,
|
|
129
|
+
stream: true,
|
|
130
|
+
};
|
|
131
|
+
if (system) body.system = system;
|
|
132
|
+
const anthropicTools = convertTools(tools);
|
|
133
|
+
if (anthropicTools) body.tools = anthropicTools;
|
|
134
|
+
|
|
135
|
+
const response = await fetch(`${base}/v1/messages`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: buildHeaders(),
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const text = await response.text().catch(() => '');
|
|
143
|
+
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let content = '';
|
|
147
|
+
const toolCalls = [];
|
|
148
|
+
let currentToolIndex = -1;
|
|
149
|
+
|
|
150
|
+
await readSSE(response, (event) => {
|
|
151
|
+
const type = event.type;
|
|
152
|
+
|
|
153
|
+
if (type === 'content_block_start') {
|
|
154
|
+
const block = event.content_block;
|
|
155
|
+
if (block?.type === 'tool_use') {
|
|
156
|
+
currentToolIndex++;
|
|
157
|
+
toolCalls.push({
|
|
158
|
+
id: block.id || '',
|
|
159
|
+
type: 'function',
|
|
160
|
+
function: { name: block.name || '', arguments: '' },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (type === 'content_block_delta') {
|
|
166
|
+
const delta = event.delta;
|
|
167
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
168
|
+
content += delta.text;
|
|
169
|
+
onDelta?.(delta.text);
|
|
170
|
+
}
|
|
171
|
+
if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
172
|
+
onReasoning?.(delta.thinking);
|
|
173
|
+
}
|
|
174
|
+
if (delta?.type === 'input_json_delta' && delta.partial_json != null && currentToolIndex >= 0) {
|
|
175
|
+
toolCalls[currentToolIndex].function.arguments += delta.partial_json;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content,
|
|
182
|
+
tool_calls: toolCalls,
|
|
183
|
+
usage: null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { createChatCompletion, streamChatCompletion };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function parseAnthropicResponse(data) {
|
|
193
|
+
let content = '';
|
|
194
|
+
const toolCalls = [];
|
|
195
|
+
|
|
196
|
+
for (const block of data.content || []) {
|
|
197
|
+
if (block.type === 'text') {
|
|
198
|
+
content += block.text;
|
|
199
|
+
}
|
|
200
|
+
if (block.type === 'tool_use') {
|
|
201
|
+
toolCalls.push(/** @type {ToolCall} */ ({
|
|
202
|
+
id: block.id,
|
|
203
|
+
type: 'function',
|
|
204
|
+
function: {
|
|
205
|
+
name: block.name,
|
|
206
|
+
arguments: JSON.stringify(block.input || {}),
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
content,
|
|
214
|
+
tool_calls: toolCalls,
|
|
215
|
+
usage: data.usage ? {
|
|
216
|
+
prompt_tokens: data.usage.input_tokens,
|
|
217
|
+
completion_tokens: data.usage.output_tokens,
|
|
218
|
+
} : null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── SSE Parser (Anthropic format) ──────────────────────────
|
|
223
|
+
|
|
224
|
+
async function readSSE(response, onMessage) {
|
|
225
|
+
if (!response.body) {
|
|
226
|
+
throw new Error('Streaming response body is not available.');
|
|
227
|
+
}
|
|
228
|
+
const reader = response.body.getReader();
|
|
229
|
+
const decoder = new TextDecoder();
|
|
230
|
+
let buffer = '';
|
|
231
|
+
|
|
232
|
+
while (true) {
|
|
233
|
+
const { done, value } = await reader.read();
|
|
234
|
+
if (done) break;
|
|
235
|
+
buffer += decoder.decode(value, { stream: true });
|
|
236
|
+
const lines = buffer.split('\n');
|
|
237
|
+
buffer = lines.pop() || '';
|
|
238
|
+
|
|
239
|
+
let currentEvent = null;
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed) {
|
|
243
|
+
currentEvent = null;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (trimmed.startsWith('event:')) {
|
|
247
|
+
currentEvent = trimmed.replace(/^event:\s*/, '');
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
251
|
+
|
|
252
|
+
const data = trimmed.replace(/^data:\s*/, '');
|
|
253
|
+
if (!data || data === '[DONE]') continue;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(data);
|
|
257
|
+
if (currentEvent) parsed.type = parsed.type || currentEvent;
|
|
258
|
+
onMessage(parsed);
|
|
259
|
+
} catch {
|
|
260
|
+
// Skip unparseable SSE lines
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Works with OpenAI, Tinfoil, OpenRouter, or any provider
|
|
5
|
+
* that implements the OpenAI Chat Completions API format.
|
|
6
|
+
*
|
|
7
|
+
* Uses `fetch` (built into Node 18+ and browsers).
|
|
8
|
+
*/
|
|
9
|
+
/** @import { ChatCompletionParams, ChatCompletionResponse, LLMClient, LLMClientOptions, ToolCall } from '../types.js' */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {LLMClientOptions} [options]
|
|
13
|
+
* @returns {LLMClient}
|
|
14
|
+
*/
|
|
15
|
+
export function createOpenAIClient({ apiKey, baseUrl = 'https://api.openai.com/v1', headers = {} } = /** @type {LLMClientOptions} */ ({ apiKey: '' })) {
|
|
16
|
+
// Normalize: strip trailing slash
|
|
17
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
18
|
+
|
|
19
|
+
function buildHeaders() {
|
|
20
|
+
return {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
23
|
+
...headers,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function createChatCompletion({ model, messages, tools, max_tokens, temperature }) {
|
|
28
|
+
const body = { model, messages, temperature };
|
|
29
|
+
if (max_tokens != null) body.max_tokens = max_tokens;
|
|
30
|
+
if (tools && tools.length > 0) body.tools = tools;
|
|
31
|
+
|
|
32
|
+
const response = await fetch(`${base}/chat/completions`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: buildHeaders(),
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const text = await response.text().catch(() => '');
|
|
40
|
+
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const choice = data.choices?.[0]?.message || {};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: choice.content || '',
|
|
48
|
+
tool_calls: (choice.tool_calls || []).map((tc) => ({
|
|
49
|
+
id: tc.id,
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: tc.function?.name || '',
|
|
53
|
+
arguments: tc.function?.arguments || '{}',
|
|
54
|
+
},
|
|
55
|
+
})),
|
|
56
|
+
usage: data.usage || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function streamChatCompletion({ model, messages, tools, max_tokens, temperature, onDelta, onReasoning }) {
|
|
61
|
+
const body = { model, messages, temperature, stream: true };
|
|
62
|
+
if (max_tokens != null) body.max_tokens = max_tokens;
|
|
63
|
+
if (tools && tools.length > 0) body.tools = tools;
|
|
64
|
+
|
|
65
|
+
const response = await fetch(`${base}/chat/completions`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: buildHeaders(),
|
|
68
|
+
body: JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const text = await response.text().catch(() => '');
|
|
73
|
+
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Accumulate the full response from SSE deltas
|
|
77
|
+
let content = '';
|
|
78
|
+
const toolCallAccumulator = new Map();
|
|
79
|
+
|
|
80
|
+
await readSSE(response, (chunk) => {
|
|
81
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
82
|
+
if (!delta) return;
|
|
83
|
+
|
|
84
|
+
// Content delta
|
|
85
|
+
if (delta.content) {
|
|
86
|
+
content += delta.content;
|
|
87
|
+
onDelta?.(delta.content);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Reasoning delta (some providers send this)
|
|
91
|
+
if (delta.reasoning) {
|
|
92
|
+
onReasoning?.(delta.reasoning);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tool call deltas — accumulate by index
|
|
96
|
+
if (delta.tool_calls) {
|
|
97
|
+
for (const tc of delta.tool_calls) {
|
|
98
|
+
const idx = tc.index ?? 0;
|
|
99
|
+
if (!toolCallAccumulator.has(idx)) {
|
|
100
|
+
toolCallAccumulator.set(idx, {
|
|
101
|
+
id: tc.id || '',
|
|
102
|
+
type: 'function',
|
|
103
|
+
function: { name: '', arguments: '' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const acc = toolCallAccumulator.get(idx);
|
|
107
|
+
if (!acc) continue;
|
|
108
|
+
if (tc.id) acc.id = tc.id;
|
|
109
|
+
if (tc.function?.name) acc.function.name += tc.function.name;
|
|
110
|
+
if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const tool_calls = [...toolCallAccumulator.entries()]
|
|
116
|
+
.sort(([a], [b]) => a - b)
|
|
117
|
+
.map(([, tc]) => tc);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content,
|
|
121
|
+
tool_calls,
|
|
122
|
+
usage: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { createChatCompletion, streamChatCompletion };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── SSE Parser ──────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async function readSSE(response, onMessage) {
|
|
132
|
+
if (!response.body) {
|
|
133
|
+
throw new Error('Streaming response body is not available.');
|
|
134
|
+
}
|
|
135
|
+
const reader = response.body.getReader();
|
|
136
|
+
const decoder = new TextDecoder();
|
|
137
|
+
let buffer = '';
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
buffer += decoder.decode(value, { stream: true });
|
|
143
|
+
const lines = buffer.split('\n');
|
|
144
|
+
buffer = lines.pop() || '';
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const trimmed = line.trim();
|
|
148
|
+
if (!trimmed) continue;
|
|
149
|
+
if (trimmed.startsWith(':')) continue;
|
|
150
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
151
|
+
|
|
152
|
+
const data = trimmed.replace(/^data:\s*/, '');
|
|
153
|
+
if (!data || data === '[DONE]') {
|
|
154
|
+
if (data === '[DONE]') return;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(data);
|
|
160
|
+
onMessage(parsed);
|
|
161
|
+
} catch {
|
|
162
|
+
// Skip unparseable SSE lines
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const remaining = buffer.trim();
|
|
168
|
+
if (remaining && remaining.startsWith('data:')) {
|
|
169
|
+
const data = remaining.replace(/^data:\s*/, '');
|
|
170
|
+
if (data && data !== '[DONE]') {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(data);
|
|
173
|
+
onMessage(parsed);
|
|
174
|
+
} catch {
|
|
175
|
+
// Skip
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt set for conversation ingestion.
|
|
3
|
+
*
|
|
4
|
+
* Strict mode: only saves facts the user explicitly stated.
|
|
5
|
+
* Used when importing chat history, conversation logs, or live sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const ingestionPrompt = `You are a memory manager. After reading a conversation, decide if any concrete, reusable facts should be saved to the user's memory files.
|
|
9
|
+
|
|
10
|
+
CRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate information.
|
|
11
|
+
|
|
12
|
+
Save information that is likely to help in a future conversation. Be selective — only save durable facts, not transient conversation details.
|
|
13
|
+
|
|
14
|
+
Do NOT save:
|
|
15
|
+
- Anything the user did not explicitly say (no inferences, no extrapolations, no "likely" facts)
|
|
16
|
+
- Information already present in existing files (the system deduplicates automatically)
|
|
17
|
+
- Transient details (greetings, "help me with this", "thanks", questions without lasting answers)
|
|
18
|
+
- The assistant's own reasoning, suggestions, or knowledge — only what the user stated
|
|
19
|
+
- Sensitive secrets (passwords, auth tokens, private keys, full payment data, government IDs)
|
|
20
|
+
- Opinions the assistant expressed unless the user explicitly agreed with them
|
|
21
|
+
|
|
22
|
+
Current memory index:
|
|
23
|
+
\`\`\`
|
|
24
|
+
{INDEX}
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
**Key principle: Prefer fewer, broader files over many narrow ones.** Organize files into folders by domain (e.g. health/, work/, personal/). Within each folder, group related facts into the same file rather than splitting every sub-topic into its own file. Before creating a new file, check whether an existing file in the same domain could absorb the facts. A single file with many bullets on related sub-topics is better than many files with one or two bullets each.
|
|
28
|
+
|
|
29
|
+
Instructions:
|
|
30
|
+
1. Read the conversation below and identify facts the user explicitly stated.
|
|
31
|
+
2. Check the memory index above. Default to append_memory when an existing file covers the same domain or a closely related topic. Only use create_new_file when no existing file is thematically close. Do not read files before writing — the system deduplicates automatically.
|
|
32
|
+
3. Use this bullet format: "- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD"
|
|
33
|
+
4. Source values:
|
|
34
|
+
- source=user_statement — the user directly said this. This is the PRIMARY source. Use it for the vast majority of saved facts.
|
|
35
|
+
- source=llm_infer — use ONLY when combining multiple explicit user statements into an obvious conclusion (e.g. user said "I work at Acme" and "Acme is in SF" → "Works in SF"). Never use this to guess, extrapolate, or fill in gaps. When in doubt, do not save.
|
|
36
|
+
5. Confidence: high for direct user statements, medium for llm_infer. Never save low-confidence items.
|
|
37
|
+
6. You may optionally add tier=working for clearly short-term or in-progress context. If you are unsure, omit tier and just save the fact.
|
|
38
|
+
7. Facts worth saving: allergies, health conditions, location, job/role, tech stack, pets, family members, durable preferences, and active plans — but ONLY if the user explicitly mentioned them.
|
|
39
|
+
8. If a fact is time-sensitive, include date context in the text. You may optionally add review_at or expires_at.
|
|
40
|
+
9. If nothing new is worth remembering, simply stop without calling any write tools. Saving nothing is better than saving something wrong.
|
|
41
|
+
|
|
42
|
+
Rules:
|
|
43
|
+
- Write facts in a timeless, archival format: use absolute dates (YYYY-MM-DD) rather than relative terms like "recently", "currently", "just", or "last week". A fact must be interpretable correctly even years after it was written.
|
|
44
|
+
- Favor broad thematic files. A file can hold multiple related sub-topics — only truly unrelated facts need separate files.
|
|
45
|
+
- Only create a new file when nothing in the index is thematically close. When in doubt, append.
|
|
46
|
+
- When creating a new file, choose a broad, thematic name that can absorb future related facts — not a narrow label for a single detail.
|
|
47
|
+
- Use update_memory only if a fact is now stale or contradicted.
|
|
48
|
+
- When a new explicit user statement contradicts an older one on the same topic, prefer the newer statement. If a user statement conflicts with an inference, the user statement always wins.
|
|
49
|
+
- If a conflict is ambiguous, preserve both versions rather than deleting one.
|
|
50
|
+
- Do not skip obvious facts just because the schema supports extra metadata.
|
|
51
|
+
- Content should be raw facts only — no filler commentary.`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt set for document ingestion.
|
|
3
|
+
*
|
|
4
|
+
* Relaxed mode: extracts and reasonably infers facts from reference material.
|
|
5
|
+
* Used when importing notes, READMEs, articles, code repositories, or knowledge bases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const ingestionPrompt = `You are a memory manager. You are reading documents (notes, README files, code repositories, articles) and extracting facts about the subject into a structured memory bank.
|
|
9
|
+
|
|
10
|
+
Unlike conversation ingestion, you may extract and reasonably infer facts from what the documents show — not just what was explicitly stated word-for-word. Use good judgment: extract what is clearly supported by the content, avoid speculation.
|
|
11
|
+
|
|
12
|
+
Save information that would be useful when answering questions about this subject in the future. Be generous — capture expertise, projects, preferences, philosophy, and patterns that emerge from the documents.
|
|
13
|
+
|
|
14
|
+
Do NOT save:
|
|
15
|
+
- Speculation or guesses not supported by the content
|
|
16
|
+
- Boilerplate (installation steps, license text, generic disclaimers)
|
|
17
|
+
- Information already present in existing files (use read_file to check first)
|
|
18
|
+
- Sensitive secrets (passwords, auth tokens, private keys)
|
|
19
|
+
|
|
20
|
+
Current memory index:
|
|
21
|
+
\`\`\`
|
|
22
|
+
{INDEX}
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
**Key principle: Create a NEW file for each distinct topic.** Organize into domain folders (e.g. projects/, expertise/, education/, philosophy/) with topic-specific files within them.
|
|
26
|
+
|
|
27
|
+
Instructions:
|
|
28
|
+
1. Read the document content and identify concrete, reusable facts about the subject.
|
|
29
|
+
2. If a matching file already exists in the index, use read_file first to avoid duplicates.
|
|
30
|
+
3. Use create_new_file for new topics, append_memory to add to existing files.
|
|
31
|
+
4. Use this bullet format: "- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD"
|
|
32
|
+
5. Source values (IMPORTANT — never use source=user_statement here):
|
|
33
|
+
- source=document — the fact is directly stated or clearly shown in the document. Use for the majority of facts.
|
|
34
|
+
- source=document_infer — a reasonable inference from what multiple parts of the document collectively show (e.g. a repo with only C files and a README praising simplicity → "prefers low-level, minimal implementations"). Use sparingly.
|
|
35
|
+
6. Confidence: high for source=document facts, medium for source=document_infer.
|
|
36
|
+
7. Facts worth extracting: skills and expertise, projects built, stated opinions and philosophy, tools and languages used, patterns across work, goals and motivations, background and experience.
|
|
37
|
+
8. If nothing meaningful can be extracted from a document, stop without calling any write tools.
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- Write facts in a timeless, archival format: use absolute dates (YYYY-MM-DD) rather than relative terms like "recently", "currently", "just", or "last week". A fact must be interpretable correctly even years after it was written.
|
|
41
|
+
- One file per distinct topic. Do NOT put unrelated facts in the same file.
|
|
42
|
+
- Create new files freely — focused files are better than bloated ones.
|
|
43
|
+
- Content should be raw facts only — no filler commentary.`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt set registry.
|
|
3
|
+
*
|
|
4
|
+
* Each mode provides an ingestionPrompt (and optionally others in future).
|
|
5
|
+
* resolvePromptSet(mode) returns the full prompt set, falling back to 'conversation'.
|
|
6
|
+
*
|
|
7
|
+
* Adding a new mode: create src/prompt_sets/<mode>/ingestion.js, export ingestionPrompt,
|
|
8
|
+
* then add it to PROMPT_SETS below.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ingestionPrompt as conversationIngestion } from './conversation/ingestion.js';
|
|
12
|
+
import { ingestionPrompt as documentIngestion } from './document/ingestion.js';
|
|
13
|
+
|
|
14
|
+
/** @type {Record<string, { ingestionPrompt: string }>} */
|
|
15
|
+
const PROMPT_SETS = {
|
|
16
|
+
conversation: { ingestionPrompt: conversationIngestion },
|
|
17
|
+
document: { ingestionPrompt: documentIngestion },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the prompt set for a given mode.
|
|
22
|
+
* Falls back to 'conversation' for unknown modes.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} [mode]
|
|
25
|
+
* @returns {{ ingestionPrompt: string }}
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePromptSet(mode = 'conversation') {
|
|
28
|
+
return PROMPT_SETS[mode] || PROMPT_SETS.conversation;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AVAILABLE_MODES = Object.keys(PROMPT_SETS);
|