@deepagents/context 0.6.0 → 0.7.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/dist/index.d.ts +286 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1713 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/context.d.ts +50 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/estimate.d.ts +108 -0
- package/dist/lib/estimate.d.ts.map +1 -0
- package/dist/lib/models.generated.d.ts +3 -0
- package/dist/lib/models.generated.d.ts.map +1 -0
- package/dist/lib/renderers/abstract.renderer.d.ts +101 -0
- package/dist/lib/renderers/abstract.renderer.d.ts.map +1 -0
- package/dist/lib/store/memory.store.d.ts +11 -0
- package/dist/lib/store/memory.store.d.ts.map +1 -0
- package/dist/lib/store/sqlite.store.d.ts +33 -0
- package/dist/lib/store/sqlite.store.d.ts.map +1 -0
- package/dist/lib/store/store.d.ts +222 -0
- package/dist/lib/store/store.d.ts.map +1 -0
- package/dist/lib/visualize.d.ts +15 -0
- package/dist/lib/visualize.d.ts.map +1 -0
- package/dist/usage.d.ts +2 -0
- package/dist/usage.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,1713 @@
|
|
|
1
|
+
// packages/context/src/lib/context.ts
|
|
2
|
+
function isFragment(data) {
|
|
3
|
+
return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
|
|
4
|
+
}
|
|
5
|
+
function isFragmentObject(data) {
|
|
6
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
|
|
7
|
+
}
|
|
8
|
+
function isMessageFragment(fragment2) {
|
|
9
|
+
return fragment2.type === "message";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// packages/context/src/lib/estimate.ts
|
|
13
|
+
import { encode } from "gpt-tokenizer";
|
|
14
|
+
var defaultTokenizer = {
|
|
15
|
+
encode(text) {
|
|
16
|
+
return encode(text);
|
|
17
|
+
},
|
|
18
|
+
count(text) {
|
|
19
|
+
return encode(text).length;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var ModelsRegistry = class {
|
|
23
|
+
#cache = /* @__PURE__ */ new Map();
|
|
24
|
+
#loaded = false;
|
|
25
|
+
#tokenizers = /* @__PURE__ */ new Map();
|
|
26
|
+
#defaultTokenizer = defaultTokenizer;
|
|
27
|
+
/**
|
|
28
|
+
* Load models data from models.dev API
|
|
29
|
+
*/
|
|
30
|
+
async load() {
|
|
31
|
+
if (this.#loaded) return;
|
|
32
|
+
const response = await fetch("https://models.dev/api.json");
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
for (const [providerId, provider] of Object.entries(data)) {
|
|
38
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
39
|
+
const info = {
|
|
40
|
+
id: model.id,
|
|
41
|
+
name: model.name,
|
|
42
|
+
family: model.family,
|
|
43
|
+
cost: model.cost,
|
|
44
|
+
limit: model.limit,
|
|
45
|
+
provider: providerId
|
|
46
|
+
};
|
|
47
|
+
this.#cache.set(`${providerId}:${modelId}`, info);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.#loaded = true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get model info by ID
|
|
54
|
+
* @param modelId - Model ID (e.g., "openai:gpt-4o")
|
|
55
|
+
*/
|
|
56
|
+
get(modelId) {
|
|
57
|
+
return this.#cache.get(modelId);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a model exists in the registry
|
|
61
|
+
*/
|
|
62
|
+
has(modelId) {
|
|
63
|
+
return this.#cache.has(modelId);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all available model IDs
|
|
67
|
+
*/
|
|
68
|
+
list() {
|
|
69
|
+
return [...this.#cache.keys()];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Register a custom tokenizer for specific model families
|
|
73
|
+
* @param family - Model family name (e.g., "llama", "claude")
|
|
74
|
+
* @param tokenizer - Tokenizer implementation
|
|
75
|
+
*/
|
|
76
|
+
registerTokenizer(family, tokenizer) {
|
|
77
|
+
this.#tokenizers.set(family, tokenizer);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Set the default tokenizer used when no family-specific tokenizer is registered
|
|
81
|
+
*/
|
|
82
|
+
setDefaultTokenizer(tokenizer) {
|
|
83
|
+
this.#defaultTokenizer = tokenizer;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the appropriate tokenizer for a model
|
|
87
|
+
*/
|
|
88
|
+
getTokenizer(modelId) {
|
|
89
|
+
const model = this.get(modelId);
|
|
90
|
+
if (model) {
|
|
91
|
+
const familyTokenizer = this.#tokenizers.get(model.family);
|
|
92
|
+
if (familyTokenizer) {
|
|
93
|
+
return familyTokenizer;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return this.#defaultTokenizer;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Estimate token count and cost for given text and model
|
|
100
|
+
* @param modelId - Model ID to use for pricing (e.g., "openai:gpt-4o")
|
|
101
|
+
* @param input - Input text (prompt)
|
|
102
|
+
*/
|
|
103
|
+
estimate(modelId, input) {
|
|
104
|
+
const model = this.get(modelId);
|
|
105
|
+
if (!model) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const tokenizer = this.getTokenizer(modelId);
|
|
111
|
+
const tokens = tokenizer.count(input);
|
|
112
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
113
|
+
return {
|
|
114
|
+
model: model.id,
|
|
115
|
+
provider: model.provider,
|
|
116
|
+
tokens,
|
|
117
|
+
cost,
|
|
118
|
+
limits: {
|
|
119
|
+
context: model.limit.context,
|
|
120
|
+
output: model.limit.output,
|
|
121
|
+
exceedsContext: tokens > model.limit.context
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var _registry = null;
|
|
127
|
+
function getModelsRegistry() {
|
|
128
|
+
if (!_registry) {
|
|
129
|
+
_registry = new ModelsRegistry();
|
|
130
|
+
}
|
|
131
|
+
return _registry;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// packages/context/src/lib/renderers/abstract.renderer.ts
|
|
135
|
+
import pluralize from "pluralize";
|
|
136
|
+
import { titlecase } from "stringcase";
|
|
137
|
+
var ContextRenderer = class {
|
|
138
|
+
options;
|
|
139
|
+
constructor(options = {}) {
|
|
140
|
+
this.options = options;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if data is a primitive (string, number, boolean).
|
|
144
|
+
*/
|
|
145
|
+
isPrimitive(data) {
|
|
146
|
+
return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Group fragments by name for groupFragments option.
|
|
150
|
+
*/
|
|
151
|
+
groupByName(fragments) {
|
|
152
|
+
const groups = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const fragment2 of fragments) {
|
|
154
|
+
const existing = groups.get(fragment2.name) ?? [];
|
|
155
|
+
existing.push(fragment2);
|
|
156
|
+
groups.set(fragment2.name, existing);
|
|
157
|
+
}
|
|
158
|
+
return groups;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Template method - dispatches value to appropriate handler.
|
|
162
|
+
*/
|
|
163
|
+
renderValue(key, value, ctx) {
|
|
164
|
+
if (value == null) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
if (isFragment(value)) {
|
|
168
|
+
return this.renderFragment(value, ctx);
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
return this.renderArray(key, value, ctx);
|
|
172
|
+
}
|
|
173
|
+
if (isFragmentObject(value)) {
|
|
174
|
+
return this.renderObject(key, value, ctx);
|
|
175
|
+
}
|
|
176
|
+
return this.renderPrimitive(key, String(value), ctx);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Render all entries of an object.
|
|
180
|
+
*/
|
|
181
|
+
renderEntries(data, ctx) {
|
|
182
|
+
return Object.entries(data).map(([key, value]) => this.renderValue(key, value, ctx)).filter(Boolean);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var XmlRenderer = class extends ContextRenderer {
|
|
186
|
+
render(fragments) {
|
|
187
|
+
return fragments.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
|
|
188
|
+
}
|
|
189
|
+
#renderTopLevel(fragment2) {
|
|
190
|
+
if (this.isPrimitive(fragment2.data)) {
|
|
191
|
+
return this.#leafRoot(fragment2.name, String(fragment2.data));
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(fragment2.data)) {
|
|
194
|
+
return this.#renderArray(fragment2.name, fragment2.data, 0);
|
|
195
|
+
}
|
|
196
|
+
if (isFragment(fragment2.data)) {
|
|
197
|
+
const child = this.renderFragment(fragment2.data, { depth: 1, path: [] });
|
|
198
|
+
return this.#wrap(fragment2.name, [child]);
|
|
199
|
+
}
|
|
200
|
+
return this.#wrap(
|
|
201
|
+
fragment2.name,
|
|
202
|
+
this.renderEntries(fragment2.data, { depth: 1, path: [] })
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
#renderArray(name, items, depth) {
|
|
206
|
+
const fragmentItems = items.filter(isFragment);
|
|
207
|
+
const nonFragmentItems = items.filter((item) => !isFragment(item));
|
|
208
|
+
const children = [];
|
|
209
|
+
for (const item of nonFragmentItems) {
|
|
210
|
+
if (item != null) {
|
|
211
|
+
children.push(
|
|
212
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
217
|
+
const groups = this.groupByName(fragmentItems);
|
|
218
|
+
for (const [groupName, groupFragments] of groups) {
|
|
219
|
+
const groupChildren = groupFragments.map(
|
|
220
|
+
(frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
|
|
221
|
+
);
|
|
222
|
+
const pluralName = pluralize.plural(groupName);
|
|
223
|
+
children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
for (const frag of fragmentItems) {
|
|
227
|
+
children.push(
|
|
228
|
+
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return this.#wrap(name, children);
|
|
233
|
+
}
|
|
234
|
+
#leafRoot(tag, value) {
|
|
235
|
+
const safe = this.#escape(value);
|
|
236
|
+
if (safe.includes("\n")) {
|
|
237
|
+
return `<${tag}>
|
|
238
|
+
${this.#indent(safe, 2)}
|
|
239
|
+
</${tag}>`;
|
|
240
|
+
}
|
|
241
|
+
return `<${tag}>${safe}</${tag}>`;
|
|
242
|
+
}
|
|
243
|
+
renderFragment(fragment2, ctx) {
|
|
244
|
+
const { name, data } = fragment2;
|
|
245
|
+
if (this.isPrimitive(data)) {
|
|
246
|
+
return this.#leaf(name, String(data), ctx.depth);
|
|
247
|
+
}
|
|
248
|
+
if (isFragment(data)) {
|
|
249
|
+
const child = this.renderFragment(data, { ...ctx, depth: ctx.depth + 1 });
|
|
250
|
+
return this.#wrapIndented(name, [child], ctx.depth);
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(data)) {
|
|
253
|
+
return this.#renderArrayIndented(name, data, ctx.depth);
|
|
254
|
+
}
|
|
255
|
+
const children = this.renderEntries(data, { ...ctx, depth: ctx.depth + 1 });
|
|
256
|
+
return this.#wrapIndented(name, children, ctx.depth);
|
|
257
|
+
}
|
|
258
|
+
#renderArrayIndented(name, items, depth) {
|
|
259
|
+
const fragmentItems = items.filter(isFragment);
|
|
260
|
+
const nonFragmentItems = items.filter((item) => !isFragment(item));
|
|
261
|
+
const children = [];
|
|
262
|
+
for (const item of nonFragmentItems) {
|
|
263
|
+
if (item != null) {
|
|
264
|
+
children.push(
|
|
265
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
270
|
+
const groups = this.groupByName(fragmentItems);
|
|
271
|
+
for (const [groupName, groupFragments] of groups) {
|
|
272
|
+
const groupChildren = groupFragments.map(
|
|
273
|
+
(frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
|
|
274
|
+
);
|
|
275
|
+
const pluralName = pluralize.plural(groupName);
|
|
276
|
+
children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
for (const frag of fragmentItems) {
|
|
280
|
+
children.push(
|
|
281
|
+
this.renderFragment(frag, { depth: depth + 1, path: [] })
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return this.#wrapIndented(name, children, depth);
|
|
286
|
+
}
|
|
287
|
+
renderPrimitive(key, value, ctx) {
|
|
288
|
+
return this.#leaf(key, value, ctx.depth);
|
|
289
|
+
}
|
|
290
|
+
renderArray(key, items, ctx) {
|
|
291
|
+
if (!items.length) {
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
const itemTag = pluralize.singular(key);
|
|
295
|
+
const children = items.filter((item) => item != null).map((item) => this.#leaf(itemTag, String(item), ctx.depth + 1));
|
|
296
|
+
return this.#wrapIndented(key, children, ctx.depth);
|
|
297
|
+
}
|
|
298
|
+
renderObject(key, obj, ctx) {
|
|
299
|
+
const children = this.renderEntries(obj, { ...ctx, depth: ctx.depth + 1 });
|
|
300
|
+
return this.#wrapIndented(key, children, ctx.depth);
|
|
301
|
+
}
|
|
302
|
+
#escape(value) {
|
|
303
|
+
if (value == null) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
return value.replaceAll(/&/g, "&").replaceAll(/</g, "<").replaceAll(/>/g, ">").replaceAll(/"/g, """).replaceAll(/'/g, "'");
|
|
307
|
+
}
|
|
308
|
+
#indent(text, spaces) {
|
|
309
|
+
if (!text.trim()) {
|
|
310
|
+
return "";
|
|
311
|
+
}
|
|
312
|
+
const padding = " ".repeat(spaces);
|
|
313
|
+
return text.split("\n").map((line) => line.length ? padding + line : padding).join("\n");
|
|
314
|
+
}
|
|
315
|
+
#leaf(tag, value, depth) {
|
|
316
|
+
const safe = this.#escape(value);
|
|
317
|
+
const pad = " ".repeat(depth);
|
|
318
|
+
if (safe.includes("\n")) {
|
|
319
|
+
return `${pad}<${tag}>
|
|
320
|
+
${this.#indent(safe, (depth + 1) * 2)}
|
|
321
|
+
${pad}</${tag}>`;
|
|
322
|
+
}
|
|
323
|
+
return `${pad}<${tag}>${safe}</${tag}>`;
|
|
324
|
+
}
|
|
325
|
+
#wrap(tag, children) {
|
|
326
|
+
const content = children.filter(Boolean).join("\n");
|
|
327
|
+
if (!content) {
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
return `<${tag}>
|
|
331
|
+
${content}
|
|
332
|
+
</${tag}>`;
|
|
333
|
+
}
|
|
334
|
+
#wrapIndented(tag, children, depth) {
|
|
335
|
+
const content = children.filter(Boolean).join("\n");
|
|
336
|
+
if (!content) {
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
const pad = " ".repeat(depth);
|
|
340
|
+
return `${pad}<${tag}>
|
|
341
|
+
${content}
|
|
342
|
+
${pad}</${tag}>`;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
var MarkdownRenderer = class extends ContextRenderer {
|
|
346
|
+
render(fragments) {
|
|
347
|
+
return fragments.map((f) => {
|
|
348
|
+
const title = `## ${titlecase(f.name)}`;
|
|
349
|
+
if (this.isPrimitive(f.data)) {
|
|
350
|
+
return `${title}
|
|
351
|
+
${String(f.data)}`;
|
|
352
|
+
}
|
|
353
|
+
if (Array.isArray(f.data)) {
|
|
354
|
+
return `${title}
|
|
355
|
+
${this.#renderArray(f.data, 0)}`;
|
|
356
|
+
}
|
|
357
|
+
if (isFragment(f.data)) {
|
|
358
|
+
return `${title}
|
|
359
|
+
${this.renderFragment(f.data, { depth: 0, path: [] })}`;
|
|
360
|
+
}
|
|
361
|
+
return `${title}
|
|
362
|
+
${this.renderEntries(f.data, { depth: 0, path: [] }).join("\n")}`;
|
|
363
|
+
}).join("\n\n");
|
|
364
|
+
}
|
|
365
|
+
#renderArray(items, depth) {
|
|
366
|
+
const fragmentItems = items.filter(isFragment);
|
|
367
|
+
const nonFragmentItems = items.filter((item) => !isFragment(item));
|
|
368
|
+
const lines = [];
|
|
369
|
+
for (const item of nonFragmentItems) {
|
|
370
|
+
if (item != null) {
|
|
371
|
+
lines.push(`${this.#pad(depth)}- ${String(item)}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
375
|
+
const groups = this.groupByName(fragmentItems);
|
|
376
|
+
for (const [groupName, groupFragments] of groups) {
|
|
377
|
+
const pluralName = pluralize.plural(groupName);
|
|
378
|
+
lines.push(`${this.#pad(depth)}- **${titlecase(pluralName)}**:`);
|
|
379
|
+
for (const frag of groupFragments) {
|
|
380
|
+
lines.push(this.renderFragment(frag, { depth: depth + 1, path: [] }));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
for (const frag of fragmentItems) {
|
|
385
|
+
lines.push(this.renderFragment(frag, { depth, path: [] }));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return lines.join("\n");
|
|
389
|
+
}
|
|
390
|
+
#pad(depth) {
|
|
391
|
+
return " ".repeat(depth);
|
|
392
|
+
}
|
|
393
|
+
#leaf(key, value, depth) {
|
|
394
|
+
return `${this.#pad(depth)}- **${key}**: ${value}`;
|
|
395
|
+
}
|
|
396
|
+
#arrayItem(item, depth) {
|
|
397
|
+
if (isFragment(item)) {
|
|
398
|
+
return this.renderFragment(item, { depth, path: [] });
|
|
399
|
+
}
|
|
400
|
+
if (isFragmentObject(item)) {
|
|
401
|
+
return this.renderEntries(item, {
|
|
402
|
+
depth,
|
|
403
|
+
path: []
|
|
404
|
+
}).join("\n");
|
|
405
|
+
}
|
|
406
|
+
return `${this.#pad(depth)}- ${String(item)}`;
|
|
407
|
+
}
|
|
408
|
+
renderFragment(fragment2, ctx) {
|
|
409
|
+
const { name, data } = fragment2;
|
|
410
|
+
const header = `${this.#pad(ctx.depth)}- **${name}**:`;
|
|
411
|
+
if (this.isPrimitive(data)) {
|
|
412
|
+
return `${this.#pad(ctx.depth)}- **${name}**: ${String(data)}`;
|
|
413
|
+
}
|
|
414
|
+
if (isFragment(data)) {
|
|
415
|
+
const child = this.renderFragment(data, { ...ctx, depth: ctx.depth + 1 });
|
|
416
|
+
return [header, child].join("\n");
|
|
417
|
+
}
|
|
418
|
+
if (Array.isArray(data)) {
|
|
419
|
+
const children2 = data.filter((item) => item != null).map((item) => this.#arrayItem(item, ctx.depth + 1));
|
|
420
|
+
return [header, ...children2].join("\n");
|
|
421
|
+
}
|
|
422
|
+
const children = this.renderEntries(data, {
|
|
423
|
+
...ctx,
|
|
424
|
+
depth: ctx.depth + 1
|
|
425
|
+
}).join("\n");
|
|
426
|
+
return [header, children].join("\n");
|
|
427
|
+
}
|
|
428
|
+
renderPrimitive(key, value, ctx) {
|
|
429
|
+
return this.#leaf(key, value, ctx.depth);
|
|
430
|
+
}
|
|
431
|
+
renderArray(key, items, ctx) {
|
|
432
|
+
const header = `${this.#pad(ctx.depth)}- **${key}**:`;
|
|
433
|
+
const children = items.filter((item) => item != null).map((item) => this.#arrayItem(item, ctx.depth + 1));
|
|
434
|
+
return [header, ...children].join("\n");
|
|
435
|
+
}
|
|
436
|
+
renderObject(key, obj, ctx) {
|
|
437
|
+
const header = `${this.#pad(ctx.depth)}- **${key}**:`;
|
|
438
|
+
const children = this.renderEntries(obj, {
|
|
439
|
+
...ctx,
|
|
440
|
+
depth: ctx.depth + 1
|
|
441
|
+
}).join("\n");
|
|
442
|
+
return [header, children].join("\n");
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
var TomlRenderer = class extends ContextRenderer {
|
|
446
|
+
render(fragments) {
|
|
447
|
+
return fragments.map((f) => {
|
|
448
|
+
if (this.isPrimitive(f.data)) {
|
|
449
|
+
return `${f.name} = ${this.#formatValue(f.data)}`;
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(f.data)) {
|
|
452
|
+
return this.#renderTopLevelArray(f.name, f.data);
|
|
453
|
+
}
|
|
454
|
+
if (isFragment(f.data)) {
|
|
455
|
+
return [
|
|
456
|
+
`[${f.name}]`,
|
|
457
|
+
this.renderFragment(f.data, { depth: 0, path: [f.name] })
|
|
458
|
+
].join("\n");
|
|
459
|
+
}
|
|
460
|
+
const entries = this.#renderObjectEntries(f.data, [f.name]);
|
|
461
|
+
return [`[${f.name}]`, ...entries].join("\n");
|
|
462
|
+
}).join("\n\n");
|
|
463
|
+
}
|
|
464
|
+
#renderTopLevelArray(name, items) {
|
|
465
|
+
const fragmentItems = items.filter(isFragment);
|
|
466
|
+
const nonFragmentItems = items.filter(
|
|
467
|
+
(item) => !isFragment(item) && item != null
|
|
468
|
+
);
|
|
469
|
+
if (fragmentItems.length > 0) {
|
|
470
|
+
const parts = [`[${name}]`];
|
|
471
|
+
for (const frag of fragmentItems) {
|
|
472
|
+
parts.push(this.renderFragment(frag, { depth: 0, path: [name] }));
|
|
473
|
+
}
|
|
474
|
+
return parts.join("\n");
|
|
475
|
+
}
|
|
476
|
+
const values = nonFragmentItems.map((item) => this.#formatValue(item));
|
|
477
|
+
return `${name} = [${values.join(", ")}]`;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Override renderValue to preserve type information for TOML formatting.
|
|
481
|
+
*/
|
|
482
|
+
renderValue(key, value, ctx) {
|
|
483
|
+
if (value == null) {
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
if (isFragment(value)) {
|
|
487
|
+
return this.renderFragment(value, ctx);
|
|
488
|
+
}
|
|
489
|
+
if (Array.isArray(value)) {
|
|
490
|
+
return this.renderArray(key, value, ctx);
|
|
491
|
+
}
|
|
492
|
+
if (isFragmentObject(value)) {
|
|
493
|
+
return this.renderObject(key, value, ctx);
|
|
494
|
+
}
|
|
495
|
+
return `${key} = ${this.#formatValue(value)}`;
|
|
496
|
+
}
|
|
497
|
+
renderPrimitive(key, value, _ctx) {
|
|
498
|
+
return `${key} = ${this.#formatValue(value)}`;
|
|
499
|
+
}
|
|
500
|
+
renderArray(key, items, _ctx) {
|
|
501
|
+
const values = items.filter((item) => item != null).map((item) => this.#formatValue(item));
|
|
502
|
+
return `${key} = [${values.join(", ")}]`;
|
|
503
|
+
}
|
|
504
|
+
renderObject(key, obj, ctx) {
|
|
505
|
+
const newPath = [...ctx.path, key];
|
|
506
|
+
const entries = this.#renderObjectEntries(obj, newPath);
|
|
507
|
+
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
508
|
+
}
|
|
509
|
+
#renderObjectEntries(obj, path) {
|
|
510
|
+
return Object.entries(obj).map(([key, value]) => {
|
|
511
|
+
if (value == null) {
|
|
512
|
+
return "";
|
|
513
|
+
}
|
|
514
|
+
if (isFragmentObject(value)) {
|
|
515
|
+
const newPath = [...path, key];
|
|
516
|
+
const entries = this.#renderObjectEntries(value, newPath);
|
|
517
|
+
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
518
|
+
}
|
|
519
|
+
if (Array.isArray(value)) {
|
|
520
|
+
const values = value.filter((item) => item != null).map((item) => this.#formatValue(item));
|
|
521
|
+
return `${key} = [${values.join(", ")}]`;
|
|
522
|
+
}
|
|
523
|
+
return `${key} = ${this.#formatValue(value)}`;
|
|
524
|
+
}).filter(Boolean);
|
|
525
|
+
}
|
|
526
|
+
renderFragment(fragment2, ctx) {
|
|
527
|
+
const { name, data } = fragment2;
|
|
528
|
+
const newPath = [...ctx.path, name];
|
|
529
|
+
if (this.isPrimitive(data)) {
|
|
530
|
+
return `${name} = ${this.#formatValue(data)}`;
|
|
531
|
+
}
|
|
532
|
+
if (isFragment(data)) {
|
|
533
|
+
return [
|
|
534
|
+
"",
|
|
535
|
+
`[${newPath.join(".")}]`,
|
|
536
|
+
this.renderFragment(data, { ...ctx, path: newPath })
|
|
537
|
+
].join("\n");
|
|
538
|
+
}
|
|
539
|
+
if (Array.isArray(data)) {
|
|
540
|
+
const fragmentItems = data.filter(isFragment);
|
|
541
|
+
const nonFragmentItems = data.filter(
|
|
542
|
+
(item) => !isFragment(item) && item != null
|
|
543
|
+
);
|
|
544
|
+
if (fragmentItems.length > 0) {
|
|
545
|
+
const parts = ["", `[${newPath.join(".")}]`];
|
|
546
|
+
for (const frag of fragmentItems) {
|
|
547
|
+
parts.push(this.renderFragment(frag, { ...ctx, path: newPath }));
|
|
548
|
+
}
|
|
549
|
+
return parts.join("\n");
|
|
550
|
+
}
|
|
551
|
+
const values = nonFragmentItems.map((item) => this.#formatValue(item));
|
|
552
|
+
return `${name} = [${values.join(", ")}]`;
|
|
553
|
+
}
|
|
554
|
+
const entries = this.#renderObjectEntries(data, newPath);
|
|
555
|
+
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
556
|
+
}
|
|
557
|
+
#escape(value) {
|
|
558
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
559
|
+
}
|
|
560
|
+
#formatValue(value) {
|
|
561
|
+
if (typeof value === "string") {
|
|
562
|
+
return `"${this.#escape(value)}"`;
|
|
563
|
+
}
|
|
564
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
565
|
+
return String(value);
|
|
566
|
+
}
|
|
567
|
+
if (typeof value === "object" && value !== null) {
|
|
568
|
+
return JSON.stringify(value);
|
|
569
|
+
}
|
|
570
|
+
return `"${String(value)}"`;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var ToonRenderer = class extends ContextRenderer {
|
|
574
|
+
render(fragments) {
|
|
575
|
+
return fragments.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
|
|
576
|
+
}
|
|
577
|
+
#renderTopLevel(fragment2) {
|
|
578
|
+
const { name, data } = fragment2;
|
|
579
|
+
if (this.isPrimitive(data)) {
|
|
580
|
+
return `${name}: ${this.#formatValue(data)}`;
|
|
581
|
+
}
|
|
582
|
+
if (Array.isArray(data)) {
|
|
583
|
+
return this.#renderArrayField(name, data, 0);
|
|
584
|
+
}
|
|
585
|
+
if (isFragment(data)) {
|
|
586
|
+
const child = this.renderFragment(data, { depth: 1, path: [] });
|
|
587
|
+
return `${name}:
|
|
588
|
+
${child}`;
|
|
589
|
+
}
|
|
590
|
+
if (isFragmentObject(data)) {
|
|
591
|
+
const entries = this.#renderObjectEntries(data, 1);
|
|
592
|
+
if (!entries) {
|
|
593
|
+
return `${name}:`;
|
|
594
|
+
}
|
|
595
|
+
return `${name}:
|
|
596
|
+
${entries}`;
|
|
597
|
+
}
|
|
598
|
+
return `${name}:`;
|
|
599
|
+
}
|
|
600
|
+
#renderArrayField(key, items, depth) {
|
|
601
|
+
const filtered = items.filter((item) => item != null);
|
|
602
|
+
if (filtered.length === 0) {
|
|
603
|
+
return `${this.#pad(depth)}${key}[0]:`;
|
|
604
|
+
}
|
|
605
|
+
const fragmentItems = filtered.filter(isFragment);
|
|
606
|
+
if (fragmentItems.length > 0) {
|
|
607
|
+
return this.#renderMixedArray(key, filtered, depth);
|
|
608
|
+
}
|
|
609
|
+
if (filtered.every((item) => this.#isPrimitiveValue(item))) {
|
|
610
|
+
return this.#renderPrimitiveArray(key, filtered, depth);
|
|
611
|
+
}
|
|
612
|
+
if (this.#isTabularArray(filtered)) {
|
|
613
|
+
return this.#renderTabularArray(key, filtered, depth);
|
|
614
|
+
}
|
|
615
|
+
return this.#renderMixedArray(key, filtered, depth);
|
|
616
|
+
}
|
|
617
|
+
#isPrimitiveValue(value) {
|
|
618
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
619
|
+
}
|
|
620
|
+
#isTabularArray(items) {
|
|
621
|
+
if (items.length === 0) return false;
|
|
622
|
+
const objects = items.filter(isFragmentObject);
|
|
623
|
+
if (objects.length !== items.length) return false;
|
|
624
|
+
const firstKeys = Object.keys(objects[0]).sort().join(",");
|
|
625
|
+
for (const obj of objects) {
|
|
626
|
+
if (Object.keys(obj).sort().join(",") !== firstKeys) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
for (const value of Object.values(obj)) {
|
|
630
|
+
if (!this.#isPrimitiveValue(value) && value !== null) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
#renderPrimitiveArray(key, items, depth) {
|
|
638
|
+
const values = items.map((item) => this.#formatValue(item)).join(",");
|
|
639
|
+
return `${this.#pad(depth)}${key}[${items.length}]: ${values}`;
|
|
640
|
+
}
|
|
641
|
+
#renderTabularArray(key, items, depth) {
|
|
642
|
+
if (items.length === 0) {
|
|
643
|
+
return `${this.#pad(depth)}${key}[0]:`;
|
|
644
|
+
}
|
|
645
|
+
const fields = Object.keys(items[0]);
|
|
646
|
+
const header = `${this.#pad(depth)}${key}[${items.length}]{${fields.join(",")}}:`;
|
|
647
|
+
const rows = items.map((obj) => {
|
|
648
|
+
const values = fields.map((f) => this.#formatValue(obj[f]));
|
|
649
|
+
return `${this.#pad(depth + 1)}${values.join(",")}`;
|
|
650
|
+
});
|
|
651
|
+
return [header, ...rows].join("\n");
|
|
652
|
+
}
|
|
653
|
+
#renderMixedArray(key, items, depth) {
|
|
654
|
+
const header = `${this.#pad(depth)}${key}[${items.length}]:`;
|
|
655
|
+
const lines = items.map((item) => this.#renderListItem(item, depth + 1));
|
|
656
|
+
return [header, ...lines].join("\n");
|
|
657
|
+
}
|
|
658
|
+
#renderListItem(item, depth) {
|
|
659
|
+
if (this.#isPrimitiveValue(item)) {
|
|
660
|
+
return `${this.#pad(depth)}- ${this.#formatValue(item)}`;
|
|
661
|
+
}
|
|
662
|
+
if (isFragment(item)) {
|
|
663
|
+
const rendered = this.renderFragment(item, {
|
|
664
|
+
depth: depth + 1,
|
|
665
|
+
path: []
|
|
666
|
+
});
|
|
667
|
+
if (this.isPrimitive(item.data)) {
|
|
668
|
+
return `${this.#pad(depth)}- ${item.name}: ${this.#formatValue(item.data)}`;
|
|
669
|
+
}
|
|
670
|
+
return `${this.#pad(depth)}- ${item.name}:
|
|
671
|
+
${rendered.split("\n").slice(1).join("\n")}`;
|
|
672
|
+
}
|
|
673
|
+
if (Array.isArray(item)) {
|
|
674
|
+
const content = this.#renderArrayField("", item, depth + 1);
|
|
675
|
+
return `${this.#pad(depth)}-${content.trimStart()}`;
|
|
676
|
+
}
|
|
677
|
+
if (isFragmentObject(item)) {
|
|
678
|
+
const entries = this.#renderObjectEntries(item, depth + 1);
|
|
679
|
+
if (!entries) {
|
|
680
|
+
return `${this.#pad(depth)}-`;
|
|
681
|
+
}
|
|
682
|
+
const lines = entries.split("\n");
|
|
683
|
+
const first = lines[0].trimStart();
|
|
684
|
+
const rest = lines.slice(1).join("\n");
|
|
685
|
+
return rest ? `${this.#pad(depth)}- ${first}
|
|
686
|
+
${rest}` : `${this.#pad(depth)}- ${first}`;
|
|
687
|
+
}
|
|
688
|
+
return `${this.#pad(depth)}- ${this.#formatValue(item)}`;
|
|
689
|
+
}
|
|
690
|
+
#renderObjectEntries(obj, depth) {
|
|
691
|
+
const lines = [];
|
|
692
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
693
|
+
if (value == null) continue;
|
|
694
|
+
if (this.#isPrimitiveValue(value)) {
|
|
695
|
+
lines.push(`${this.#pad(depth)}${key}: ${this.#formatValue(value)}`);
|
|
696
|
+
} else if (Array.isArray(value)) {
|
|
697
|
+
lines.push(this.#renderArrayField(key, value, depth));
|
|
698
|
+
} else if (isFragmentObject(value)) {
|
|
699
|
+
const nested = this.#renderObjectEntries(value, depth + 1);
|
|
700
|
+
if (nested) {
|
|
701
|
+
lines.push(`${this.#pad(depth)}${key}:
|
|
702
|
+
${nested}`);
|
|
703
|
+
} else {
|
|
704
|
+
lines.push(`${this.#pad(depth)}${key}:`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return lines.join("\n");
|
|
709
|
+
}
|
|
710
|
+
renderFragment(fragment2, ctx) {
|
|
711
|
+
const { name, data } = fragment2;
|
|
712
|
+
if (this.isPrimitive(data)) {
|
|
713
|
+
return `${this.#pad(ctx.depth)}${name}: ${this.#formatValue(data)}`;
|
|
714
|
+
}
|
|
715
|
+
if (isFragment(data)) {
|
|
716
|
+
const child = this.renderFragment(data, {
|
|
717
|
+
...ctx,
|
|
718
|
+
depth: ctx.depth + 1
|
|
719
|
+
});
|
|
720
|
+
return `${this.#pad(ctx.depth)}${name}:
|
|
721
|
+
${child}`;
|
|
722
|
+
}
|
|
723
|
+
if (Array.isArray(data)) {
|
|
724
|
+
return this.#renderArrayField(name, data, ctx.depth);
|
|
725
|
+
}
|
|
726
|
+
if (isFragmentObject(data)) {
|
|
727
|
+
const entries = this.#renderObjectEntries(data, ctx.depth + 1);
|
|
728
|
+
if (!entries) {
|
|
729
|
+
return `${this.#pad(ctx.depth)}${name}:`;
|
|
730
|
+
}
|
|
731
|
+
return `${this.#pad(ctx.depth)}${name}:
|
|
732
|
+
${entries}`;
|
|
733
|
+
}
|
|
734
|
+
return `${this.#pad(ctx.depth)}${name}:`;
|
|
735
|
+
}
|
|
736
|
+
renderPrimitive(key, value, ctx) {
|
|
737
|
+
return `${this.#pad(ctx.depth)}${key}: ${this.#formatValue(value)}`;
|
|
738
|
+
}
|
|
739
|
+
renderArray(key, items, ctx) {
|
|
740
|
+
return this.#renderArrayField(key, items, ctx.depth);
|
|
741
|
+
}
|
|
742
|
+
renderObject(key, obj, ctx) {
|
|
743
|
+
const entries = this.#renderObjectEntries(obj, ctx.depth + 1);
|
|
744
|
+
if (!entries) {
|
|
745
|
+
return `${this.#pad(ctx.depth)}${key}:`;
|
|
746
|
+
}
|
|
747
|
+
return `${this.#pad(ctx.depth)}${key}:
|
|
748
|
+
${entries}`;
|
|
749
|
+
}
|
|
750
|
+
#pad(depth) {
|
|
751
|
+
return " ".repeat(depth);
|
|
752
|
+
}
|
|
753
|
+
#needsQuoting(value) {
|
|
754
|
+
if (value === "") return true;
|
|
755
|
+
if (value !== value.trim()) return true;
|
|
756
|
+
if (["true", "false", "null"].includes(value.toLowerCase())) return true;
|
|
757
|
+
if (/^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value)) return true;
|
|
758
|
+
if (/[:\\"'[\]{}|,\t\n\r]/.test(value)) return true;
|
|
759
|
+
if (value.startsWith("-")) return true;
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
#escape(value) {
|
|
763
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
764
|
+
}
|
|
765
|
+
#canonicalizeNumber(n) {
|
|
766
|
+
if (!Number.isFinite(n)) return "null";
|
|
767
|
+
if (Object.is(n, -0)) return "0";
|
|
768
|
+
return String(n);
|
|
769
|
+
}
|
|
770
|
+
#formatValue(value) {
|
|
771
|
+
if (value === null) return "null";
|
|
772
|
+
if (typeof value === "boolean") return String(value);
|
|
773
|
+
if (typeof value === "number") return this.#canonicalizeNumber(value);
|
|
774
|
+
if (typeof value === "string") {
|
|
775
|
+
if (this.#needsQuoting(value)) {
|
|
776
|
+
return `"${this.#escape(value)}"`;
|
|
777
|
+
}
|
|
778
|
+
return value;
|
|
779
|
+
}
|
|
780
|
+
return `"${this.#escape(JSON.stringify(value))}"`;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// packages/context/src/lib/store/store.ts
|
|
785
|
+
var ContextStore = class {
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// packages/context/src/lib/store/sqlite.store.ts
|
|
789
|
+
import { DatabaseSync } from "node:sqlite";
|
|
790
|
+
var STORE_DDL = `
|
|
791
|
+
-- Chats table
|
|
792
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
793
|
+
id TEXT PRIMARY KEY,
|
|
794
|
+
title TEXT,
|
|
795
|
+
metadata TEXT,
|
|
796
|
+
createdAt INTEGER NOT NULL,
|
|
797
|
+
updatedAt INTEGER NOT NULL
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
|
|
801
|
+
|
|
802
|
+
-- Messages table (nodes in the DAG)
|
|
803
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
804
|
+
id TEXT PRIMARY KEY,
|
|
805
|
+
chatId TEXT NOT NULL,
|
|
806
|
+
parentId TEXT,
|
|
807
|
+
name TEXT NOT NULL,
|
|
808
|
+
type TEXT,
|
|
809
|
+
data TEXT NOT NULL,
|
|
810
|
+
persist INTEGER NOT NULL DEFAULT 1,
|
|
811
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
812
|
+
createdAt INTEGER NOT NULL,
|
|
813
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
814
|
+
FOREIGN KEY (parentId) REFERENCES messages(id)
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
|
|
818
|
+
CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
|
|
819
|
+
|
|
820
|
+
-- Branches table (pointers to head messages)
|
|
821
|
+
CREATE TABLE IF NOT EXISTS branches (
|
|
822
|
+
id TEXT PRIMARY KEY,
|
|
823
|
+
chatId TEXT NOT NULL,
|
|
824
|
+
name TEXT NOT NULL,
|
|
825
|
+
headMessageId TEXT,
|
|
826
|
+
isActive INTEGER NOT NULL DEFAULT 0,
|
|
827
|
+
createdAt INTEGER NOT NULL,
|
|
828
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
829
|
+
FOREIGN KEY (headMessageId) REFERENCES messages(id),
|
|
830
|
+
UNIQUE(chatId, name)
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
CREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);
|
|
834
|
+
|
|
835
|
+
-- Checkpoints table (pointers to message nodes)
|
|
836
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
837
|
+
id TEXT PRIMARY KEY,
|
|
838
|
+
chatId TEXT NOT NULL,
|
|
839
|
+
name TEXT NOT NULL,
|
|
840
|
+
messageId TEXT NOT NULL,
|
|
841
|
+
createdAt INTEGER NOT NULL,
|
|
842
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
843
|
+
FOREIGN KEY (messageId) REFERENCES messages(id),
|
|
844
|
+
UNIQUE(chatId, name)
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
|
|
848
|
+
`;
|
|
849
|
+
var SqliteContextStore = class extends ContextStore {
|
|
850
|
+
#db;
|
|
851
|
+
constructor(path) {
|
|
852
|
+
super();
|
|
853
|
+
this.#db = new DatabaseSync(path);
|
|
854
|
+
this.#db.exec("PRAGMA foreign_keys = ON");
|
|
855
|
+
this.#db.exec(STORE_DDL);
|
|
856
|
+
}
|
|
857
|
+
// ==========================================================================
|
|
858
|
+
// Chat Operations
|
|
859
|
+
// ==========================================================================
|
|
860
|
+
async createChat(chat) {
|
|
861
|
+
this.#db.prepare(
|
|
862
|
+
`INSERT INTO chats (id, title, metadata, createdAt, updatedAt)
|
|
863
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
864
|
+
).run(
|
|
865
|
+
chat.id,
|
|
866
|
+
chat.title ?? null,
|
|
867
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null,
|
|
868
|
+
chat.createdAt,
|
|
869
|
+
chat.updatedAt
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
async getChat(chatId) {
|
|
873
|
+
const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
|
|
874
|
+
if (!row) {
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
id: row.id,
|
|
879
|
+
title: row.title ?? void 0,
|
|
880
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
881
|
+
createdAt: row.createdAt,
|
|
882
|
+
updatedAt: row.updatedAt
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async updateChat(chatId, updates) {
|
|
886
|
+
const setClauses = [];
|
|
887
|
+
const params = [];
|
|
888
|
+
if (updates.title !== void 0) {
|
|
889
|
+
setClauses.push("title = ?");
|
|
890
|
+
params.push(updates.title ?? null);
|
|
891
|
+
}
|
|
892
|
+
if (updates.metadata !== void 0) {
|
|
893
|
+
setClauses.push("metadata = ?");
|
|
894
|
+
params.push(JSON.stringify(updates.metadata));
|
|
895
|
+
}
|
|
896
|
+
if (updates.updatedAt !== void 0) {
|
|
897
|
+
setClauses.push("updatedAt = ?");
|
|
898
|
+
params.push(updates.updatedAt);
|
|
899
|
+
}
|
|
900
|
+
if (setClauses.length === 0) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
params.push(chatId);
|
|
904
|
+
this.#db.prepare(`UPDATE chats SET ${setClauses.join(", ")} WHERE id = ?`).run(...params);
|
|
905
|
+
}
|
|
906
|
+
async listChats() {
|
|
907
|
+
const rows = this.#db.prepare(
|
|
908
|
+
`SELECT
|
|
909
|
+
c.id,
|
|
910
|
+
c.title,
|
|
911
|
+
c.createdAt,
|
|
912
|
+
c.updatedAt,
|
|
913
|
+
COUNT(DISTINCT m.id) as messageCount,
|
|
914
|
+
COUNT(DISTINCT b.id) as branchCount
|
|
915
|
+
FROM chats c
|
|
916
|
+
LEFT JOIN messages m ON m.chatId = c.id AND m.deleted = 0
|
|
917
|
+
LEFT JOIN branches b ON b.chatId = c.id
|
|
918
|
+
GROUP BY c.id
|
|
919
|
+
ORDER BY c.updatedAt DESC`
|
|
920
|
+
).all();
|
|
921
|
+
return rows.map((row) => ({
|
|
922
|
+
id: row.id,
|
|
923
|
+
title: row.title ?? void 0,
|
|
924
|
+
messageCount: row.messageCount,
|
|
925
|
+
branchCount: row.branchCount,
|
|
926
|
+
createdAt: row.createdAt,
|
|
927
|
+
updatedAt: row.updatedAt
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
// ==========================================================================
|
|
931
|
+
// Message Operations (Graph Nodes)
|
|
932
|
+
// ==========================================================================
|
|
933
|
+
async addMessage(message) {
|
|
934
|
+
this.#db.prepare(
|
|
935
|
+
`INSERT INTO messages (id, chatId, parentId, name, type, data, persist, deleted, createdAt)
|
|
936
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
937
|
+
).run(
|
|
938
|
+
message.id,
|
|
939
|
+
message.chatId,
|
|
940
|
+
message.parentId,
|
|
941
|
+
message.name,
|
|
942
|
+
message.type ?? null,
|
|
943
|
+
JSON.stringify(message.data),
|
|
944
|
+
message.persist ? 1 : 0,
|
|
945
|
+
message.deleted ? 1 : 0,
|
|
946
|
+
message.createdAt
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
async getMessage(messageId) {
|
|
950
|
+
const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
|
|
951
|
+
if (!row) {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
id: row.id,
|
|
956
|
+
chatId: row.chatId,
|
|
957
|
+
parentId: row.parentId,
|
|
958
|
+
name: row.name,
|
|
959
|
+
type: row.type ?? void 0,
|
|
960
|
+
data: JSON.parse(row.data),
|
|
961
|
+
persist: row.persist === 1,
|
|
962
|
+
deleted: row.deleted === 1,
|
|
963
|
+
createdAt: row.createdAt
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
async getMessageChain(headId) {
|
|
967
|
+
const rows = this.#db.prepare(
|
|
968
|
+
`WITH RECURSIVE chain AS (
|
|
969
|
+
SELECT *, 0 as depth FROM messages WHERE id = ?
|
|
970
|
+
UNION ALL
|
|
971
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
972
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
973
|
+
)
|
|
974
|
+
SELECT * FROM chain WHERE deleted = 0
|
|
975
|
+
ORDER BY depth DESC`
|
|
976
|
+
).all(headId);
|
|
977
|
+
return rows.map((row) => ({
|
|
978
|
+
id: row.id,
|
|
979
|
+
chatId: row.chatId,
|
|
980
|
+
parentId: row.parentId,
|
|
981
|
+
name: row.name,
|
|
982
|
+
type: row.type ?? void 0,
|
|
983
|
+
data: JSON.parse(row.data),
|
|
984
|
+
persist: row.persist === 1,
|
|
985
|
+
deleted: row.deleted === 1,
|
|
986
|
+
createdAt: row.createdAt
|
|
987
|
+
}));
|
|
988
|
+
}
|
|
989
|
+
async softDeleteMessage(messageId) {
|
|
990
|
+
this.#db.prepare("UPDATE messages SET deleted = 1 WHERE id = ?").run(messageId);
|
|
991
|
+
}
|
|
992
|
+
async hasChildren(messageId) {
|
|
993
|
+
const row = this.#db.prepare(
|
|
994
|
+
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ? AND deleted = 0) as hasChildren"
|
|
995
|
+
).get(messageId);
|
|
996
|
+
return row.hasChildren === 1;
|
|
997
|
+
}
|
|
998
|
+
// ==========================================================================
|
|
999
|
+
// Branch Operations
|
|
1000
|
+
// ==========================================================================
|
|
1001
|
+
async createBranch(branch) {
|
|
1002
|
+
this.#db.prepare(
|
|
1003
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
1004
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1005
|
+
).run(
|
|
1006
|
+
branch.id,
|
|
1007
|
+
branch.chatId,
|
|
1008
|
+
branch.name,
|
|
1009
|
+
branch.headMessageId,
|
|
1010
|
+
branch.isActive ? 1 : 0,
|
|
1011
|
+
branch.createdAt
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
async getBranch(chatId, name) {
|
|
1015
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
1016
|
+
if (!row) {
|
|
1017
|
+
return void 0;
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
id: row.id,
|
|
1021
|
+
chatId: row.chatId,
|
|
1022
|
+
name: row.name,
|
|
1023
|
+
headMessageId: row.headMessageId,
|
|
1024
|
+
isActive: row.isActive === 1,
|
|
1025
|
+
createdAt: row.createdAt
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
async getActiveBranch(chatId) {
|
|
1029
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
|
|
1030
|
+
if (!row) {
|
|
1031
|
+
return void 0;
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
id: row.id,
|
|
1035
|
+
chatId: row.chatId,
|
|
1036
|
+
name: row.name,
|
|
1037
|
+
headMessageId: row.headMessageId,
|
|
1038
|
+
isActive: true,
|
|
1039
|
+
createdAt: row.createdAt
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
async setActiveBranch(chatId, branchId) {
|
|
1043
|
+
this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
|
|
1044
|
+
this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
|
|
1045
|
+
}
|
|
1046
|
+
async updateBranchHead(branchId, messageId) {
|
|
1047
|
+
this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
|
|
1048
|
+
}
|
|
1049
|
+
async listBranches(chatId) {
|
|
1050
|
+
const branches = this.#db.prepare(
|
|
1051
|
+
`SELECT
|
|
1052
|
+
b.id,
|
|
1053
|
+
b.name,
|
|
1054
|
+
b.headMessageId,
|
|
1055
|
+
b.isActive,
|
|
1056
|
+
b.createdAt
|
|
1057
|
+
FROM branches b
|
|
1058
|
+
WHERE b.chatId = ?
|
|
1059
|
+
ORDER BY b.createdAt ASC`
|
|
1060
|
+
).all(chatId);
|
|
1061
|
+
const result = [];
|
|
1062
|
+
for (const branch of branches) {
|
|
1063
|
+
let messageCount = 0;
|
|
1064
|
+
if (branch.headMessageId) {
|
|
1065
|
+
const countRow = this.#db.prepare(
|
|
1066
|
+
`WITH RECURSIVE chain AS (
|
|
1067
|
+
SELECT id, parentId FROM messages WHERE id = ?
|
|
1068
|
+
UNION ALL
|
|
1069
|
+
SELECT m.id, m.parentId FROM messages m
|
|
1070
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
1071
|
+
)
|
|
1072
|
+
SELECT COUNT(*) as count FROM chain`
|
|
1073
|
+
).get(branch.headMessageId);
|
|
1074
|
+
messageCount = countRow.count;
|
|
1075
|
+
}
|
|
1076
|
+
result.push({
|
|
1077
|
+
id: branch.id,
|
|
1078
|
+
name: branch.name,
|
|
1079
|
+
headMessageId: branch.headMessageId,
|
|
1080
|
+
isActive: branch.isActive === 1,
|
|
1081
|
+
messageCount,
|
|
1082
|
+
createdAt: branch.createdAt
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
// ==========================================================================
|
|
1088
|
+
// Checkpoint Operations
|
|
1089
|
+
// ==========================================================================
|
|
1090
|
+
async createCheckpoint(checkpoint) {
|
|
1091
|
+
this.#db.prepare(
|
|
1092
|
+
`INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
|
|
1093
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1094
|
+
ON CONFLICT(chatId, name) DO UPDATE SET
|
|
1095
|
+
messageId = excluded.messageId,
|
|
1096
|
+
createdAt = excluded.createdAt`
|
|
1097
|
+
).run(
|
|
1098
|
+
checkpoint.id,
|
|
1099
|
+
checkpoint.chatId,
|
|
1100
|
+
checkpoint.name,
|
|
1101
|
+
checkpoint.messageId,
|
|
1102
|
+
checkpoint.createdAt
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
async getCheckpoint(chatId, name) {
|
|
1106
|
+
const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
1107
|
+
if (!row) {
|
|
1108
|
+
return void 0;
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
id: row.id,
|
|
1112
|
+
chatId: row.chatId,
|
|
1113
|
+
name: row.name,
|
|
1114
|
+
messageId: row.messageId,
|
|
1115
|
+
createdAt: row.createdAt
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async listCheckpoints(chatId) {
|
|
1119
|
+
const rows = this.#db.prepare(
|
|
1120
|
+
`SELECT id, name, messageId, createdAt
|
|
1121
|
+
FROM checkpoints
|
|
1122
|
+
WHERE chatId = ?
|
|
1123
|
+
ORDER BY createdAt DESC`
|
|
1124
|
+
).all(chatId);
|
|
1125
|
+
return rows.map((row) => ({
|
|
1126
|
+
id: row.id,
|
|
1127
|
+
name: row.name,
|
|
1128
|
+
messageId: row.messageId,
|
|
1129
|
+
createdAt: row.createdAt
|
|
1130
|
+
}));
|
|
1131
|
+
}
|
|
1132
|
+
async deleteCheckpoint(chatId, name) {
|
|
1133
|
+
this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
|
|
1134
|
+
}
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
// Visualization Operations
|
|
1137
|
+
// ==========================================================================
|
|
1138
|
+
async getGraph(chatId) {
|
|
1139
|
+
const messageRows = this.#db.prepare(
|
|
1140
|
+
`SELECT id, parentId, name, data, createdAt, deleted
|
|
1141
|
+
FROM messages
|
|
1142
|
+
WHERE chatId = ?
|
|
1143
|
+
ORDER BY createdAt ASC`
|
|
1144
|
+
).all(chatId);
|
|
1145
|
+
const nodes = messageRows.map((row) => {
|
|
1146
|
+
const data = JSON.parse(row.data);
|
|
1147
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
1148
|
+
return {
|
|
1149
|
+
id: row.id,
|
|
1150
|
+
parentId: row.parentId,
|
|
1151
|
+
role: row.name,
|
|
1152
|
+
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
1153
|
+
createdAt: row.createdAt,
|
|
1154
|
+
deleted: row.deleted === 1
|
|
1155
|
+
};
|
|
1156
|
+
});
|
|
1157
|
+
const branchRows = this.#db.prepare(
|
|
1158
|
+
`SELECT name, headMessageId, isActive
|
|
1159
|
+
FROM branches
|
|
1160
|
+
WHERE chatId = ?
|
|
1161
|
+
ORDER BY createdAt ASC`
|
|
1162
|
+
).all(chatId);
|
|
1163
|
+
const branches = branchRows.map((row) => ({
|
|
1164
|
+
name: row.name,
|
|
1165
|
+
headMessageId: row.headMessageId,
|
|
1166
|
+
isActive: row.isActive === 1
|
|
1167
|
+
}));
|
|
1168
|
+
const checkpointRows = this.#db.prepare(
|
|
1169
|
+
`SELECT name, messageId
|
|
1170
|
+
FROM checkpoints
|
|
1171
|
+
WHERE chatId = ?
|
|
1172
|
+
ORDER BY createdAt ASC`
|
|
1173
|
+
).all(chatId);
|
|
1174
|
+
const checkpoints = checkpointRows.map((row) => ({
|
|
1175
|
+
name: row.name,
|
|
1176
|
+
messageId: row.messageId
|
|
1177
|
+
}));
|
|
1178
|
+
return {
|
|
1179
|
+
chatId,
|
|
1180
|
+
nodes,
|
|
1181
|
+
branches,
|
|
1182
|
+
checkpoints
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// packages/context/src/lib/store/memory.store.ts
|
|
1188
|
+
var InMemoryContextStore = class extends SqliteContextStore {
|
|
1189
|
+
constructor() {
|
|
1190
|
+
super(":memory:");
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// packages/context/src/lib/visualize.ts
|
|
1195
|
+
function visualizeGraph(data) {
|
|
1196
|
+
if (data.nodes.length === 0) {
|
|
1197
|
+
return `[chat: ${data.chatId}]
|
|
1198
|
+
|
|
1199
|
+
(empty)`;
|
|
1200
|
+
}
|
|
1201
|
+
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
1202
|
+
const branchHeads = /* @__PURE__ */ new Map();
|
|
1203
|
+
const checkpointsByMessageId = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const node of data.nodes) {
|
|
1205
|
+
const children = childrenByParentId.get(node.parentId) ?? [];
|
|
1206
|
+
children.push(node);
|
|
1207
|
+
childrenByParentId.set(node.parentId, children);
|
|
1208
|
+
}
|
|
1209
|
+
for (const branch of data.branches) {
|
|
1210
|
+
if (branch.headMessageId) {
|
|
1211
|
+
const heads = branchHeads.get(branch.headMessageId) ?? [];
|
|
1212
|
+
heads.push(branch.isActive ? `${branch.name} *` : branch.name);
|
|
1213
|
+
branchHeads.set(branch.headMessageId, heads);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
for (const checkpoint of data.checkpoints) {
|
|
1217
|
+
const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
|
|
1218
|
+
cps.push(checkpoint.name);
|
|
1219
|
+
checkpointsByMessageId.set(checkpoint.messageId, cps);
|
|
1220
|
+
}
|
|
1221
|
+
const roots = childrenByParentId.get(null) ?? [];
|
|
1222
|
+
const lines = [`[chat: ${data.chatId}]`, ""];
|
|
1223
|
+
function renderNode(node, prefix, isLast, isRoot) {
|
|
1224
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1225
|
+
const roleLabel = node.deleted ? `~${node.role}~` : node.role;
|
|
1226
|
+
const contentPreview = node.content.replace(/\n/g, " ");
|
|
1227
|
+
let line = `${prefix}${connector}${node.id.slice(0, 8)} (${roleLabel}): "${contentPreview}"`;
|
|
1228
|
+
const branches = branchHeads.get(node.id);
|
|
1229
|
+
if (branches) {
|
|
1230
|
+
line += ` <- [${branches.join(", ")}]`;
|
|
1231
|
+
}
|
|
1232
|
+
const checkpoints = checkpointsByMessageId.get(node.id);
|
|
1233
|
+
if (checkpoints) {
|
|
1234
|
+
line += ` {${checkpoints.join(", ")}}`;
|
|
1235
|
+
}
|
|
1236
|
+
lines.push(line);
|
|
1237
|
+
const children = childrenByParentId.get(node.id) ?? [];
|
|
1238
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1239
|
+
for (let i = 0; i < children.length; i++) {
|
|
1240
|
+
renderNode(children[i], childPrefix, i === children.length - 1, false);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
for (let i = 0; i < roots.length; i++) {
|
|
1244
|
+
renderNode(roots[i], "", i === roots.length - 1, true);
|
|
1245
|
+
}
|
|
1246
|
+
lines.push("");
|
|
1247
|
+
lines.push("Legend: * = active branch, ~role~ = deleted, {...} = checkpoint");
|
|
1248
|
+
return lines.join("\n");
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// packages/context/src/index.ts
|
|
1252
|
+
var ContextEngine = class {
|
|
1253
|
+
/** Non-message fragments (role, hints, etc.) - not persisted in graph */
|
|
1254
|
+
#contextFragments = [];
|
|
1255
|
+
/** Pending message fragments to be added to graph */
|
|
1256
|
+
#pendingMessages = [];
|
|
1257
|
+
#store;
|
|
1258
|
+
#chatId;
|
|
1259
|
+
#branchName;
|
|
1260
|
+
#branch = null;
|
|
1261
|
+
#chatData = null;
|
|
1262
|
+
#initialized = false;
|
|
1263
|
+
constructor(options) {
|
|
1264
|
+
if (!options.chatId) {
|
|
1265
|
+
throw new Error("chatId is required");
|
|
1266
|
+
}
|
|
1267
|
+
this.#store = options.store;
|
|
1268
|
+
this.#chatId = options.chatId;
|
|
1269
|
+
this.#branchName = options.branch ?? "main";
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Initialize the chat and branch if they don't exist.
|
|
1273
|
+
*/
|
|
1274
|
+
async #ensureInitialized() {
|
|
1275
|
+
if (this.#initialized) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const existingChat = await this.#store.getChat(this.#chatId);
|
|
1279
|
+
if (existingChat) {
|
|
1280
|
+
this.#chatData = existingChat;
|
|
1281
|
+
} else {
|
|
1282
|
+
const now = Date.now();
|
|
1283
|
+
this.#chatData = {
|
|
1284
|
+
id: this.#chatId,
|
|
1285
|
+
createdAt: now,
|
|
1286
|
+
updatedAt: now
|
|
1287
|
+
};
|
|
1288
|
+
await this.#store.createChat(this.#chatData);
|
|
1289
|
+
}
|
|
1290
|
+
const existingBranch = await this.#store.getBranch(
|
|
1291
|
+
this.#chatId,
|
|
1292
|
+
this.#branchName
|
|
1293
|
+
);
|
|
1294
|
+
if (existingBranch) {
|
|
1295
|
+
this.#branch = existingBranch;
|
|
1296
|
+
} else {
|
|
1297
|
+
this.#branch = {
|
|
1298
|
+
id: crypto.randomUUID(),
|
|
1299
|
+
chatId: this.#chatId,
|
|
1300
|
+
name: this.#branchName,
|
|
1301
|
+
headMessageId: null,
|
|
1302
|
+
isActive: true,
|
|
1303
|
+
createdAt: Date.now()
|
|
1304
|
+
};
|
|
1305
|
+
await this.#store.createBranch(this.#branch);
|
|
1306
|
+
}
|
|
1307
|
+
this.#initialized = true;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Get the current chat ID.
|
|
1311
|
+
*/
|
|
1312
|
+
get chatId() {
|
|
1313
|
+
return this.#chatId;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Get the current branch name.
|
|
1317
|
+
*/
|
|
1318
|
+
get branch() {
|
|
1319
|
+
return this.#branchName;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Get metadata for the current chat.
|
|
1323
|
+
* Returns null if the chat hasn't been initialized yet.
|
|
1324
|
+
*/
|
|
1325
|
+
get chat() {
|
|
1326
|
+
if (!this.#chatData) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
id: this.#chatData.id,
|
|
1331
|
+
createdAt: this.#chatData.createdAt,
|
|
1332
|
+
updatedAt: this.#chatData.updatedAt,
|
|
1333
|
+
title: this.#chatData.title,
|
|
1334
|
+
metadata: this.#chatData.metadata
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Add fragments to the context.
|
|
1339
|
+
*
|
|
1340
|
+
* - Message fragments (user/assistant) are queued for persistence
|
|
1341
|
+
* - Non-message fragments (role/hint) are kept in memory for system prompt
|
|
1342
|
+
*/
|
|
1343
|
+
set(...fragments) {
|
|
1344
|
+
for (const fragment2 of fragments) {
|
|
1345
|
+
if (isMessageFragment(fragment2)) {
|
|
1346
|
+
this.#pendingMessages.push(fragment2);
|
|
1347
|
+
} else {
|
|
1348
|
+
this.#contextFragments.push(fragment2);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Render all fragments using the provided renderer.
|
|
1355
|
+
* @internal Use resolve() instead for public API.
|
|
1356
|
+
*/
|
|
1357
|
+
render(renderer) {
|
|
1358
|
+
return renderer.render(this.#contextFragments);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Resolve context into AI SDK-ready format.
|
|
1362
|
+
*
|
|
1363
|
+
* - Initializes chat and branch if needed
|
|
1364
|
+
* - Loads message history from the graph (walking parent chain)
|
|
1365
|
+
* - Separates context fragments for system prompt
|
|
1366
|
+
* - Combines with pending messages
|
|
1367
|
+
*
|
|
1368
|
+
* @example
|
|
1369
|
+
* ```ts
|
|
1370
|
+
* const context = new ContextEngine({ store, chatId: 'chat-1' })
|
|
1371
|
+
* .set(role('You are helpful'), user('Hello'));
|
|
1372
|
+
*
|
|
1373
|
+
* const { systemPrompt, messages } = await context.resolve();
|
|
1374
|
+
* await generateText({ system: systemPrompt, messages });
|
|
1375
|
+
* ```
|
|
1376
|
+
*/
|
|
1377
|
+
async resolve(options = {}) {
|
|
1378
|
+
await this.#ensureInitialized();
|
|
1379
|
+
const renderer = options.renderer ?? new XmlRenderer();
|
|
1380
|
+
const systemPrompt = renderer.render(this.#contextFragments);
|
|
1381
|
+
const persistedMessages = [];
|
|
1382
|
+
if (this.#branch?.headMessageId) {
|
|
1383
|
+
const chain = await this.#store.getMessageChain(
|
|
1384
|
+
this.#branch.headMessageId
|
|
1385
|
+
);
|
|
1386
|
+
persistedMessages.push(...chain);
|
|
1387
|
+
}
|
|
1388
|
+
const messages = persistedMessages.map((msg) => ({
|
|
1389
|
+
role: msg.name,
|
|
1390
|
+
content: String(msg.data)
|
|
1391
|
+
}));
|
|
1392
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1393
|
+
messages.push({
|
|
1394
|
+
role: fragment2.name,
|
|
1395
|
+
content: String(fragment2.data)
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return { systemPrompt, messages };
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Save pending messages to the graph.
|
|
1402
|
+
*
|
|
1403
|
+
* Each message is added as a node with parentId pointing to the previous message.
|
|
1404
|
+
* The branch head is updated to point to the last message.
|
|
1405
|
+
*
|
|
1406
|
+
* @example
|
|
1407
|
+
* ```ts
|
|
1408
|
+
* context.set(user('Hello'));
|
|
1409
|
+
* // AI responds...
|
|
1410
|
+
* context.set(assistant('Hi there!'));
|
|
1411
|
+
* await context.save(); // Persist to graph
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
async save() {
|
|
1415
|
+
await this.#ensureInitialized();
|
|
1416
|
+
if (this.#pendingMessages.length === 0) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
let parentId = this.#branch.headMessageId;
|
|
1420
|
+
const now = Date.now();
|
|
1421
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1422
|
+
const messageData = {
|
|
1423
|
+
id: fragment2.id ?? crypto.randomUUID(),
|
|
1424
|
+
chatId: this.#chatId,
|
|
1425
|
+
parentId,
|
|
1426
|
+
name: fragment2.name,
|
|
1427
|
+
type: fragment2.type,
|
|
1428
|
+
data: fragment2.data,
|
|
1429
|
+
persist: fragment2.persist ?? true,
|
|
1430
|
+
deleted: false,
|
|
1431
|
+
createdAt: now
|
|
1432
|
+
};
|
|
1433
|
+
await this.#store.addMessage(messageData);
|
|
1434
|
+
parentId = messageData.id;
|
|
1435
|
+
}
|
|
1436
|
+
await this.#store.updateBranchHead(this.#branch.id, parentId);
|
|
1437
|
+
this.#branch.headMessageId = parentId;
|
|
1438
|
+
await this.#store.updateChat(this.#chatId, { updatedAt: now });
|
|
1439
|
+
if (this.#chatData) {
|
|
1440
|
+
this.#chatData.updatedAt = now;
|
|
1441
|
+
}
|
|
1442
|
+
this.#pendingMessages = [];
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Estimate token count and cost for the current context.
|
|
1446
|
+
*
|
|
1447
|
+
* @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
|
|
1448
|
+
* @param options - Optional settings
|
|
1449
|
+
* @returns Estimate result with token counts and costs
|
|
1450
|
+
*/
|
|
1451
|
+
async estimate(modelId, options = {}) {
|
|
1452
|
+
const renderer = options.renderer ?? new XmlRenderer();
|
|
1453
|
+
const renderedContext = this.render(renderer);
|
|
1454
|
+
const registry = getModelsRegistry();
|
|
1455
|
+
await registry.load();
|
|
1456
|
+
return registry.estimate(modelId, renderedContext);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Rewind to a specific message by ID.
|
|
1460
|
+
*
|
|
1461
|
+
* Creates a new branch from that message, preserving the original branch.
|
|
1462
|
+
* The new branch becomes active.
|
|
1463
|
+
*
|
|
1464
|
+
* @param messageId - The message ID to rewind to
|
|
1465
|
+
* @returns The new branch info
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```ts
|
|
1469
|
+
* context.set(user('What is 2 + 2?', { id: 'q1' }));
|
|
1470
|
+
* context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
|
|
1471
|
+
* await context.save();
|
|
1472
|
+
*
|
|
1473
|
+
* // Rewind to the question, creates new branch
|
|
1474
|
+
* const newBranch = await context.rewind('q1');
|
|
1475
|
+
*
|
|
1476
|
+
* // Now add correct answer on new branch
|
|
1477
|
+
* context.set(assistant('The answer is 4.'));
|
|
1478
|
+
* await context.save();
|
|
1479
|
+
* ```
|
|
1480
|
+
*/
|
|
1481
|
+
async rewind(messageId) {
|
|
1482
|
+
await this.#ensureInitialized();
|
|
1483
|
+
const message = await this.#store.getMessage(messageId);
|
|
1484
|
+
if (!message) {
|
|
1485
|
+
throw new Error(`Message "${messageId}" not found`);
|
|
1486
|
+
}
|
|
1487
|
+
if (message.chatId !== this.#chatId) {
|
|
1488
|
+
throw new Error(`Message "${messageId}" belongs to a different chat`);
|
|
1489
|
+
}
|
|
1490
|
+
const branches = await this.#store.listBranches(this.#chatId);
|
|
1491
|
+
const newBranchName = `${this.#branchName}-v${branches.length + 1}`;
|
|
1492
|
+
const newBranch = {
|
|
1493
|
+
id: crypto.randomUUID(),
|
|
1494
|
+
chatId: this.#chatId,
|
|
1495
|
+
name: newBranchName,
|
|
1496
|
+
headMessageId: messageId,
|
|
1497
|
+
isActive: false,
|
|
1498
|
+
createdAt: Date.now()
|
|
1499
|
+
};
|
|
1500
|
+
await this.#store.createBranch(newBranch);
|
|
1501
|
+
await this.#store.setActiveBranch(this.#chatId, newBranch.id);
|
|
1502
|
+
this.#branch = { ...newBranch, isActive: true };
|
|
1503
|
+
this.#branchName = newBranchName;
|
|
1504
|
+
this.#pendingMessages = [];
|
|
1505
|
+
const chain = await this.#store.getMessageChain(messageId);
|
|
1506
|
+
return {
|
|
1507
|
+
id: newBranch.id,
|
|
1508
|
+
name: newBranch.name,
|
|
1509
|
+
headMessageId: newBranch.headMessageId,
|
|
1510
|
+
isActive: true,
|
|
1511
|
+
messageCount: chain.length,
|
|
1512
|
+
createdAt: newBranch.createdAt
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Create a checkpoint at the current position.
|
|
1517
|
+
*
|
|
1518
|
+
* A checkpoint is a named pointer to the current branch head.
|
|
1519
|
+
* Use restore() to return to this point later.
|
|
1520
|
+
*
|
|
1521
|
+
* @param name - Name for the checkpoint
|
|
1522
|
+
* @returns The checkpoint info
|
|
1523
|
+
*
|
|
1524
|
+
* @example
|
|
1525
|
+
* ```ts
|
|
1526
|
+
* context.set(user('I want to learn a new skill.'));
|
|
1527
|
+
* context.set(assistant('Would you like coding or cooking?'));
|
|
1528
|
+
* await context.save();
|
|
1529
|
+
*
|
|
1530
|
+
* // Save checkpoint before user's choice
|
|
1531
|
+
* const cp = await context.checkpoint('before-choice');
|
|
1532
|
+
* ```
|
|
1533
|
+
*/
|
|
1534
|
+
async checkpoint(name) {
|
|
1535
|
+
await this.#ensureInitialized();
|
|
1536
|
+
if (!this.#branch?.headMessageId) {
|
|
1537
|
+
throw new Error("Cannot create checkpoint: no messages in conversation");
|
|
1538
|
+
}
|
|
1539
|
+
const checkpoint = {
|
|
1540
|
+
id: crypto.randomUUID(),
|
|
1541
|
+
chatId: this.#chatId,
|
|
1542
|
+
name,
|
|
1543
|
+
messageId: this.#branch.headMessageId,
|
|
1544
|
+
createdAt: Date.now()
|
|
1545
|
+
};
|
|
1546
|
+
await this.#store.createCheckpoint(checkpoint);
|
|
1547
|
+
return {
|
|
1548
|
+
id: checkpoint.id,
|
|
1549
|
+
name: checkpoint.name,
|
|
1550
|
+
messageId: checkpoint.messageId,
|
|
1551
|
+
createdAt: checkpoint.createdAt
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Restore to a checkpoint by creating a new branch from that point.
|
|
1556
|
+
*
|
|
1557
|
+
* @param name - Name of the checkpoint to restore
|
|
1558
|
+
* @returns The new branch info
|
|
1559
|
+
*
|
|
1560
|
+
* @example
|
|
1561
|
+
* ```ts
|
|
1562
|
+
* // User chose cooking, but wants to try coding path
|
|
1563
|
+
* await context.restore('before-choice');
|
|
1564
|
+
*
|
|
1565
|
+
* context.set(user('I want to learn coding.'));
|
|
1566
|
+
* context.set(assistant('Python is a great starting language!'));
|
|
1567
|
+
* await context.save();
|
|
1568
|
+
* ```
|
|
1569
|
+
*/
|
|
1570
|
+
async restore(name) {
|
|
1571
|
+
await this.#ensureInitialized();
|
|
1572
|
+
const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
|
|
1573
|
+
if (!checkpoint) {
|
|
1574
|
+
throw new Error(
|
|
1575
|
+
`Checkpoint "${name}" not found in chat "${this.#chatId}"`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
return this.rewind(checkpoint.messageId);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Switch to a different branch by name.
|
|
1582
|
+
*
|
|
1583
|
+
* @param name - Branch name to switch to
|
|
1584
|
+
*
|
|
1585
|
+
* @example
|
|
1586
|
+
* ```ts
|
|
1587
|
+
* // List branches (via store)
|
|
1588
|
+
* const branches = await store.listBranches(context.chatId);
|
|
1589
|
+
* console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
|
|
1590
|
+
*
|
|
1591
|
+
* // Switch to original branch
|
|
1592
|
+
* await context.switchBranch('main');
|
|
1593
|
+
* ```
|
|
1594
|
+
*/
|
|
1595
|
+
async switchBranch(name) {
|
|
1596
|
+
await this.#ensureInitialized();
|
|
1597
|
+
const branch = await this.#store.getBranch(this.#chatId, name);
|
|
1598
|
+
if (!branch) {
|
|
1599
|
+
throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
|
|
1600
|
+
}
|
|
1601
|
+
await this.#store.setActiveBranch(this.#chatId, branch.id);
|
|
1602
|
+
this.#branch = { ...branch, isActive: true };
|
|
1603
|
+
this.#branchName = name;
|
|
1604
|
+
this.#pendingMessages = [];
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Update metadata for the current chat.
|
|
1608
|
+
*
|
|
1609
|
+
* @param updates - Partial metadata to merge (title, metadata)
|
|
1610
|
+
*
|
|
1611
|
+
* @example
|
|
1612
|
+
* ```ts
|
|
1613
|
+
* await context.updateChat({
|
|
1614
|
+
* title: 'Coding Help Session',
|
|
1615
|
+
* metadata: { tags: ['python', 'debugging'] }
|
|
1616
|
+
* });
|
|
1617
|
+
* ```
|
|
1618
|
+
*/
|
|
1619
|
+
async updateChat(updates) {
|
|
1620
|
+
await this.#ensureInitialized();
|
|
1621
|
+
const now = Date.now();
|
|
1622
|
+
const storeUpdates = {
|
|
1623
|
+
updatedAt: now
|
|
1624
|
+
};
|
|
1625
|
+
if (updates.title !== void 0) {
|
|
1626
|
+
storeUpdates.title = updates.title;
|
|
1627
|
+
}
|
|
1628
|
+
if (updates.metadata !== void 0) {
|
|
1629
|
+
storeUpdates.metadata = {
|
|
1630
|
+
...this.#chatData?.metadata,
|
|
1631
|
+
...updates.metadata
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
await this.#store.updateChat(this.#chatId, storeUpdates);
|
|
1635
|
+
if (this.#chatData) {
|
|
1636
|
+
if (storeUpdates.title !== void 0) {
|
|
1637
|
+
this.#chatData.title = storeUpdates.title;
|
|
1638
|
+
}
|
|
1639
|
+
if (storeUpdates.metadata !== void 0) {
|
|
1640
|
+
this.#chatData.metadata = storeUpdates.metadata;
|
|
1641
|
+
}
|
|
1642
|
+
this.#chatData.updatedAt = now;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Consolidate context fragments (no-op for now).
|
|
1647
|
+
*
|
|
1648
|
+
* This is a placeholder for future functionality that merges context fragments
|
|
1649
|
+
* using specific rules. Currently, it does nothing.
|
|
1650
|
+
*
|
|
1651
|
+
* @experimental
|
|
1652
|
+
*/
|
|
1653
|
+
consolidate() {
|
|
1654
|
+
return void 0;
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
function hint(text) {
|
|
1658
|
+
return {
|
|
1659
|
+
name: "hint",
|
|
1660
|
+
data: text
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function fragment(name, ...children) {
|
|
1664
|
+
return {
|
|
1665
|
+
name,
|
|
1666
|
+
data: children
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function role(content) {
|
|
1670
|
+
return {
|
|
1671
|
+
name: "role",
|
|
1672
|
+
data: content
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function user(content, options) {
|
|
1676
|
+
return {
|
|
1677
|
+
id: options?.id ?? crypto.randomUUID(),
|
|
1678
|
+
name: "user",
|
|
1679
|
+
data: content,
|
|
1680
|
+
type: "message",
|
|
1681
|
+
persist: true
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function assistant(content, options) {
|
|
1685
|
+
return {
|
|
1686
|
+
id: options?.id ?? crypto.randomUUID(),
|
|
1687
|
+
name: "assistant",
|
|
1688
|
+
data: content,
|
|
1689
|
+
type: "message",
|
|
1690
|
+
persist: true
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
export {
|
|
1694
|
+
ContextEngine,
|
|
1695
|
+
ContextStore,
|
|
1696
|
+
InMemoryContextStore,
|
|
1697
|
+
MarkdownRenderer,
|
|
1698
|
+
ModelsRegistry,
|
|
1699
|
+
SqliteContextStore,
|
|
1700
|
+
TomlRenderer,
|
|
1701
|
+
ToonRenderer,
|
|
1702
|
+
XmlRenderer,
|
|
1703
|
+
assistant,
|
|
1704
|
+
defaultTokenizer,
|
|
1705
|
+
fragment,
|
|
1706
|
+
getModelsRegistry,
|
|
1707
|
+
hint,
|
|
1708
|
+
isMessageFragment,
|
|
1709
|
+
role,
|
|
1710
|
+
user,
|
|
1711
|
+
visualizeGraph
|
|
1712
|
+
};
|
|
1713
|
+
//# sourceMappingURL=index.js.map
|