@deepagents/context 0.10.2 → 0.12.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 +114 -119
- package/dist/example-error-recovery.d.ts +2 -0
- package/dist/example-error-recovery.d.ts.map +1 -0
- package/dist/index.d.ts +18 -388
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2765 -1091
- package/dist/index.js.map +4 -4
- package/dist/lib/agent.d.ts +87 -12
- package/dist/lib/agent.d.ts.map +1 -1
- package/dist/lib/engine.d.ts +325 -0
- package/dist/lib/engine.d.ts.map +1 -0
- package/dist/lib/estimate.d.ts +1 -1
- package/dist/lib/estimate.d.ts.map +1 -1
- package/dist/lib/fragments/domain.d.ts +537 -0
- package/dist/lib/fragments/domain.d.ts.map +1 -0
- package/dist/lib/fragments/user.d.ts +122 -0
- package/dist/lib/fragments/user.d.ts.map +1 -0
- package/dist/lib/fragments.d.ts +103 -0
- package/dist/lib/fragments.d.ts.map +1 -0
- package/dist/lib/guardrail.d.ts +138 -0
- package/dist/lib/guardrail.d.ts.map +1 -0
- package/dist/lib/guardrails/error-recovery.guardrail.d.ts +3 -0
- package/dist/lib/guardrails/error-recovery.guardrail.d.ts.map +1 -0
- package/dist/lib/render.d.ts +21 -0
- package/dist/lib/render.d.ts.map +1 -0
- package/dist/lib/renderers/abstract.renderer.d.ts +11 -3
- package/dist/lib/renderers/abstract.renderer.d.ts.map +1 -1
- package/dist/lib/sandbox/binary-bridges.d.ts +31 -0
- package/dist/lib/sandbox/binary-bridges.d.ts.map +1 -0
- package/dist/lib/sandbox/container-tool.d.ts +134 -0
- package/dist/lib/sandbox/container-tool.d.ts.map +1 -0
- package/dist/lib/sandbox/docker-sandbox.d.ts +471 -0
- package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -0
- package/dist/lib/sandbox/index.d.ts +4 -0
- package/dist/lib/sandbox/index.d.ts.map +1 -0
- package/dist/lib/skills/fragments.d.ts +24 -0
- package/dist/lib/skills/fragments.d.ts.map +1 -0
- package/dist/lib/skills/index.d.ts +31 -0
- package/dist/lib/skills/index.d.ts.map +1 -0
- package/dist/lib/skills/loader.d.ts +28 -0
- package/dist/lib/skills/loader.d.ts.map +1 -0
- package/dist/lib/skills/types.d.ts +40 -0
- package/dist/lib/skills/types.d.ts.map +1 -0
- package/dist/lib/store/sqlite.store.d.ts +4 -2
- package/dist/lib/store/sqlite.store.d.ts.map +1 -1
- package/dist/lib/store/store.d.ts +36 -2
- package/dist/lib/store/store.d.ts.map +1 -1
- package/package.json +8 -4
- package/dist/lib/context.d.ts +0 -56
- package/dist/lib/context.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
// packages/context/src/index.ts
|
|
2
|
-
import { generateId } from "ai";
|
|
3
|
-
|
|
4
|
-
// packages/context/src/lib/context.ts
|
|
5
|
-
function isFragment(data) {
|
|
6
|
-
return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
|
|
7
|
-
}
|
|
8
|
-
function isFragmentObject(data) {
|
|
9
|
-
return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
|
|
10
|
-
}
|
|
11
|
-
function isMessageFragment(fragment2) {
|
|
12
|
-
return fragment2.type === "message";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
1
|
// packages/context/src/lib/estimate.ts
|
|
16
2
|
import { encode } from "gpt-tokenizer";
|
|
17
3
|
var defaultTokenizer = {
|
|
@@ -172,6 +158,93 @@ async function estimate(modelId, renderer, ...fragments) {
|
|
|
172
158
|
};
|
|
173
159
|
}
|
|
174
160
|
|
|
161
|
+
// packages/context/src/lib/fragments.ts
|
|
162
|
+
import { generateId } from "ai";
|
|
163
|
+
function isFragment(data) {
|
|
164
|
+
return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
|
|
165
|
+
}
|
|
166
|
+
function isFragmentObject(data) {
|
|
167
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
|
|
168
|
+
}
|
|
169
|
+
function isMessageFragment(fragment2) {
|
|
170
|
+
return fragment2.type === "message";
|
|
171
|
+
}
|
|
172
|
+
function fragment(name, ...children) {
|
|
173
|
+
return {
|
|
174
|
+
name,
|
|
175
|
+
data: children
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function user(content) {
|
|
179
|
+
const message2 = typeof content === "string" ? {
|
|
180
|
+
id: generateId(),
|
|
181
|
+
role: "user",
|
|
182
|
+
parts: [{ type: "text", text: content }]
|
|
183
|
+
} : content;
|
|
184
|
+
return {
|
|
185
|
+
id: message2.id,
|
|
186
|
+
name: "user",
|
|
187
|
+
data: "content",
|
|
188
|
+
type: "message",
|
|
189
|
+
persist: true,
|
|
190
|
+
codec: {
|
|
191
|
+
decode() {
|
|
192
|
+
return message2;
|
|
193
|
+
},
|
|
194
|
+
encode() {
|
|
195
|
+
return message2;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assistant(message2) {
|
|
201
|
+
return {
|
|
202
|
+
id: message2.id,
|
|
203
|
+
name: "assistant",
|
|
204
|
+
data: "content",
|
|
205
|
+
type: "message",
|
|
206
|
+
persist: true,
|
|
207
|
+
codec: {
|
|
208
|
+
decode() {
|
|
209
|
+
return message2;
|
|
210
|
+
},
|
|
211
|
+
encode() {
|
|
212
|
+
return message2;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function message(content) {
|
|
218
|
+
const message2 = typeof content === "string" ? {
|
|
219
|
+
id: generateId(),
|
|
220
|
+
role: "user",
|
|
221
|
+
parts: [{ type: "text", text: content }]
|
|
222
|
+
} : content;
|
|
223
|
+
return {
|
|
224
|
+
id: message2.id,
|
|
225
|
+
name: "message",
|
|
226
|
+
data: "content",
|
|
227
|
+
type: "message",
|
|
228
|
+
persist: true,
|
|
229
|
+
codec: {
|
|
230
|
+
decode() {
|
|
231
|
+
return message2;
|
|
232
|
+
},
|
|
233
|
+
encode() {
|
|
234
|
+
return message2;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function assistantText(content, options) {
|
|
240
|
+
const id = options?.id ?? crypto.randomUUID();
|
|
241
|
+
return assistant({
|
|
242
|
+
id,
|
|
243
|
+
role: "assistant",
|
|
244
|
+
parts: [{ type: "text", text: content }]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
175
248
|
// packages/context/src/lib/renderers/abstract.renderer.ts
|
|
176
249
|
import pluralize from "pluralize";
|
|
177
250
|
import { titlecase } from "stringcase";
|
|
@@ -198,6 +271,68 @@ var ContextRenderer = class {
|
|
|
198
271
|
}
|
|
199
272
|
return groups;
|
|
200
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Remove null/undefined from fragments and fragment data recursively.
|
|
276
|
+
* This protects renderers from nullish values and ensures they are ignored
|
|
277
|
+
* consistently across all output formats.
|
|
278
|
+
*/
|
|
279
|
+
sanitizeFragments(fragments) {
|
|
280
|
+
const sanitized = [];
|
|
281
|
+
for (const fragment2 of fragments) {
|
|
282
|
+
const cleaned = this.sanitizeFragment(fragment2, /* @__PURE__ */ new WeakSet());
|
|
283
|
+
if (cleaned) {
|
|
284
|
+
sanitized.push(cleaned);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return sanitized;
|
|
288
|
+
}
|
|
289
|
+
sanitizeFragment(fragment2, seen) {
|
|
290
|
+
const data = this.sanitizeData(fragment2.data, seen);
|
|
291
|
+
if (data == null) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
...fragment2,
|
|
296
|
+
data
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
sanitizeData(data, seen) {
|
|
300
|
+
if (data == null) {
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
if (isFragment(data)) {
|
|
304
|
+
return this.sanitizeFragment(data, seen) ?? void 0;
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(data)) {
|
|
307
|
+
if (seen.has(data)) {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
seen.add(data);
|
|
311
|
+
const cleaned = [];
|
|
312
|
+
for (const item of data) {
|
|
313
|
+
const sanitizedItem = this.sanitizeData(item, seen);
|
|
314
|
+
if (sanitizedItem != null) {
|
|
315
|
+
cleaned.push(sanitizedItem);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return cleaned;
|
|
319
|
+
}
|
|
320
|
+
if (isFragmentObject(data)) {
|
|
321
|
+
if (seen.has(data)) {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
seen.add(data);
|
|
325
|
+
const cleaned = {};
|
|
326
|
+
for (const [key, value] of Object.entries(data)) {
|
|
327
|
+
const sanitizedValue = this.sanitizeData(value, seen);
|
|
328
|
+
if (sanitizedValue != null) {
|
|
329
|
+
cleaned[key] = sanitizedValue;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return cleaned;
|
|
333
|
+
}
|
|
334
|
+
return data;
|
|
335
|
+
}
|
|
201
336
|
/**
|
|
202
337
|
* Template method - dispatches value to appropriate handler.
|
|
203
338
|
*/
|
|
@@ -225,7 +360,8 @@ var ContextRenderer = class {
|
|
|
225
360
|
};
|
|
226
361
|
var XmlRenderer = class extends ContextRenderer {
|
|
227
362
|
render(fragments) {
|
|
228
|
-
|
|
363
|
+
const sanitized = this.sanitizeFragments(fragments);
|
|
364
|
+
return sanitized.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
|
|
229
365
|
}
|
|
230
366
|
#renderTopLevel(fragment2) {
|
|
231
367
|
if (this.isPrimitive(fragment2.data)) {
|
|
@@ -238,10 +374,13 @@ var XmlRenderer = class extends ContextRenderer {
|
|
|
238
374
|
const child = this.renderFragment(fragment2.data, { depth: 1, path: [] });
|
|
239
375
|
return this.#wrap(fragment2.name, [child]);
|
|
240
376
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
377
|
+
if (isFragmentObject(fragment2.data)) {
|
|
378
|
+
return this.#wrap(
|
|
379
|
+
fragment2.name,
|
|
380
|
+
this.renderEntries(fragment2.data, { depth: 1, path: [] })
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return "";
|
|
245
384
|
}
|
|
246
385
|
#renderArray(name, items, depth) {
|
|
247
386
|
const fragmentItems = items.filter(isFragment);
|
|
@@ -249,9 +388,19 @@ var XmlRenderer = class extends ContextRenderer {
|
|
|
249
388
|
const children = [];
|
|
250
389
|
for (const item of nonFragmentItems) {
|
|
251
390
|
if (item != null) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
391
|
+
if (isFragmentObject(item)) {
|
|
392
|
+
children.push(
|
|
393
|
+
this.#wrapIndented(
|
|
394
|
+
pluralize.singular(name),
|
|
395
|
+
this.renderEntries(item, { depth: depth + 2, path: [] }),
|
|
396
|
+
depth + 1
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
children.push(
|
|
401
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
255
404
|
}
|
|
256
405
|
}
|
|
257
406
|
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
@@ -293,8 +442,14 @@ ${this.#indent(safe, 2)}
|
|
|
293
442
|
if (Array.isArray(data)) {
|
|
294
443
|
return this.#renderArrayIndented(name, data, ctx.depth);
|
|
295
444
|
}
|
|
296
|
-
|
|
297
|
-
|
|
445
|
+
if (isFragmentObject(data)) {
|
|
446
|
+
const children = this.renderEntries(data, {
|
|
447
|
+
...ctx,
|
|
448
|
+
depth: ctx.depth + 1
|
|
449
|
+
});
|
|
450
|
+
return this.#wrapIndented(name, children, ctx.depth);
|
|
451
|
+
}
|
|
452
|
+
return "";
|
|
298
453
|
}
|
|
299
454
|
#renderArrayIndented(name, items, depth) {
|
|
300
455
|
const fragmentItems = items.filter(isFragment);
|
|
@@ -302,9 +457,19 @@ ${this.#indent(safe, 2)}
|
|
|
302
457
|
const children = [];
|
|
303
458
|
for (const item of nonFragmentItems) {
|
|
304
459
|
if (item != null) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
460
|
+
if (isFragmentObject(item)) {
|
|
461
|
+
children.push(
|
|
462
|
+
this.#wrapIndented(
|
|
463
|
+
pluralize.singular(name),
|
|
464
|
+
this.renderEntries(item, { depth: depth + 2, path: [] }),
|
|
465
|
+
depth + 1
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
} else {
|
|
469
|
+
children.push(
|
|
470
|
+
this.#leaf(pluralize.singular(name), String(item), depth + 1)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
308
473
|
}
|
|
309
474
|
}
|
|
310
475
|
if (this.options.groupFragments && fragmentItems.length > 0) {
|
|
@@ -333,7 +498,19 @@ ${this.#indent(safe, 2)}
|
|
|
333
498
|
return "";
|
|
334
499
|
}
|
|
335
500
|
const itemTag = pluralize.singular(key);
|
|
336
|
-
const children = items.filter((item) => item != null).map((item) =>
|
|
501
|
+
const children = items.filter((item) => item != null).map((item) => {
|
|
502
|
+
if (isFragment(item)) {
|
|
503
|
+
return this.renderFragment(item, { ...ctx, depth: ctx.depth + 1 });
|
|
504
|
+
}
|
|
505
|
+
if (isFragmentObject(item)) {
|
|
506
|
+
return this.#wrapIndented(
|
|
507
|
+
itemTag,
|
|
508
|
+
this.renderEntries(item, { ...ctx, depth: ctx.depth + 2 }),
|
|
509
|
+
ctx.depth + 1
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return this.#leaf(itemTag, String(item), ctx.depth + 1);
|
|
513
|
+
});
|
|
337
514
|
return this.#wrapIndented(key, children, ctx.depth);
|
|
338
515
|
}
|
|
339
516
|
renderObject(key, obj, ctx) {
|
|
@@ -385,7 +562,7 @@ ${pad}</${tag}>`;
|
|
|
385
562
|
};
|
|
386
563
|
var MarkdownRenderer = class extends ContextRenderer {
|
|
387
564
|
render(fragments) {
|
|
388
|
-
return fragments.map((f) => {
|
|
565
|
+
return this.sanitizeFragments(fragments).map((f) => {
|
|
389
566
|
const title = `## ${titlecase(f.name)}`;
|
|
390
567
|
if (this.isPrimitive(f.data)) {
|
|
391
568
|
return `${title}
|
|
@@ -399,8 +576,12 @@ ${this.#renderArray(f.data, 0)}`;
|
|
|
399
576
|
return `${title}
|
|
400
577
|
${this.renderFragment(f.data, { depth: 0, path: [] })}`;
|
|
401
578
|
}
|
|
402
|
-
|
|
579
|
+
if (isFragmentObject(f.data)) {
|
|
580
|
+
return `${title}
|
|
403
581
|
${this.renderEntries(f.data, { depth: 0, path: [] }).join("\n")}`;
|
|
582
|
+
}
|
|
583
|
+
return `${title}
|
|
584
|
+
`;
|
|
404
585
|
}).join("\n\n");
|
|
405
586
|
}
|
|
406
587
|
#renderArray(items, depth) {
|
|
@@ -457,14 +638,17 @@ ${this.renderEntries(f.data, { depth: 0, path: [] }).join("\n")}`;
|
|
|
457
638
|
return [header, child].join("\n");
|
|
458
639
|
}
|
|
459
640
|
if (Array.isArray(data)) {
|
|
460
|
-
const
|
|
461
|
-
return [header, ...
|
|
641
|
+
const children = data.filter((item) => item != null).map((item) => this.#arrayItem(item, ctx.depth + 1));
|
|
642
|
+
return [header, ...children].join("\n");
|
|
462
643
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
644
|
+
if (isFragmentObject(data)) {
|
|
645
|
+
const children = this.renderEntries(data, {
|
|
646
|
+
...ctx,
|
|
647
|
+
depth: ctx.depth + 1
|
|
648
|
+
}).join("\n");
|
|
649
|
+
return [header, children].join("\n");
|
|
650
|
+
}
|
|
651
|
+
return header;
|
|
468
652
|
}
|
|
469
653
|
renderPrimitive(key, value, ctx) {
|
|
470
654
|
return this.#leaf(key, value, ctx.depth);
|
|
@@ -485,22 +669,25 @@ ${this.renderEntries(f.data, { depth: 0, path: [] }).join("\n")}`;
|
|
|
485
669
|
};
|
|
486
670
|
var TomlRenderer = class extends ContextRenderer {
|
|
487
671
|
render(fragments) {
|
|
488
|
-
|
|
672
|
+
const rendered = [];
|
|
673
|
+
for (const f of this.sanitizeFragments(fragments)) {
|
|
489
674
|
if (this.isPrimitive(f.data)) {
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
675
|
+
rendered.push(`${f.name} = ${this.#formatValue(f.data)}`);
|
|
676
|
+
} else if (Array.isArray(f.data)) {
|
|
677
|
+
rendered.push(this.#renderTopLevelArray(f.name, f.data));
|
|
678
|
+
} else if (isFragment(f.data)) {
|
|
679
|
+
rendered.push(
|
|
680
|
+
[
|
|
681
|
+
`[${f.name}]`,
|
|
682
|
+
this.renderFragment(f.data, { depth: 0, path: [f.name] })
|
|
683
|
+
].join("\n")
|
|
684
|
+
);
|
|
685
|
+
} else if (isFragmentObject(f.data)) {
|
|
686
|
+
const entries = this.#renderObjectEntries(f.data, [f.name]);
|
|
687
|
+
rendered.push([`[${f.name}]`, ...entries].join("\n"));
|
|
500
688
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}).join("\n\n");
|
|
689
|
+
}
|
|
690
|
+
return rendered.join("\n\n");
|
|
504
691
|
}
|
|
505
692
|
#renderTopLevelArray(name, items) {
|
|
506
693
|
const fragmentItems = items.filter(isFragment);
|
|
@@ -535,10 +722,12 @@ var TomlRenderer = class extends ContextRenderer {
|
|
|
535
722
|
}
|
|
536
723
|
return `${key} = ${this.#formatValue(value)}`;
|
|
537
724
|
}
|
|
538
|
-
renderPrimitive(key, value,
|
|
725
|
+
renderPrimitive(key, value, ctx) {
|
|
726
|
+
void ctx;
|
|
539
727
|
return `${key} = ${this.#formatValue(value)}`;
|
|
540
728
|
}
|
|
541
|
-
renderArray(key, items,
|
|
729
|
+
renderArray(key, items, ctx) {
|
|
730
|
+
void ctx;
|
|
542
731
|
const values = items.filter((item) => item != null).map((item) => this.#formatValue(item));
|
|
543
732
|
return `${key} = [${values.join(", ")}]`;
|
|
544
733
|
}
|
|
@@ -547,13 +736,13 @@ var TomlRenderer = class extends ContextRenderer {
|
|
|
547
736
|
const entries = this.#renderObjectEntries(obj, newPath);
|
|
548
737
|
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
549
738
|
}
|
|
550
|
-
#renderObjectEntries(obj,
|
|
739
|
+
#renderObjectEntries(obj, path3) {
|
|
551
740
|
return Object.entries(obj).map(([key, value]) => {
|
|
552
741
|
if (value == null) {
|
|
553
742
|
return "";
|
|
554
743
|
}
|
|
555
744
|
if (isFragmentObject(value)) {
|
|
556
|
-
const newPath = [...
|
|
745
|
+
const newPath = [...path3, key];
|
|
557
746
|
const entries = this.#renderObjectEntries(value, newPath);
|
|
558
747
|
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
559
748
|
}
|
|
@@ -592,8 +781,11 @@ var TomlRenderer = class extends ContextRenderer {
|
|
|
592
781
|
const values = nonFragmentItems.map((item) => this.#formatValue(item));
|
|
593
782
|
return `${name} = [${values.join(", ")}]`;
|
|
594
783
|
}
|
|
595
|
-
|
|
596
|
-
|
|
784
|
+
if (isFragmentObject(data)) {
|
|
785
|
+
const entries = this.#renderObjectEntries(data, newPath);
|
|
786
|
+
return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
|
|
787
|
+
}
|
|
788
|
+
return "";
|
|
597
789
|
}
|
|
598
790
|
#escape(value) {
|
|
599
791
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -613,7 +805,8 @@ var TomlRenderer = class extends ContextRenderer {
|
|
|
613
805
|
};
|
|
614
806
|
var ToonRenderer = class extends ContextRenderer {
|
|
615
807
|
render(fragments) {
|
|
616
|
-
|
|
808
|
+
const sanitized = this.sanitizeFragments(fragments);
|
|
809
|
+
return sanitized.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
|
|
617
810
|
}
|
|
618
811
|
#renderTopLevel(fragment2) {
|
|
619
812
|
const { name, data } = fragment2;
|
|
@@ -662,18 +855,18 @@ ${entries}`;
|
|
|
662
855
|
if (items.length === 0) return false;
|
|
663
856
|
const objects = items.filter(isFragmentObject);
|
|
664
857
|
if (objects.length !== items.length) return false;
|
|
665
|
-
|
|
858
|
+
let intersection = new Set(Object.keys(objects[0]));
|
|
666
859
|
for (const obj of objects) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
860
|
+
const keys = new Set(Object.keys(obj));
|
|
861
|
+
intersection = new Set([...intersection].filter((k) => keys.has(k)));
|
|
670
862
|
for (const value of Object.values(obj)) {
|
|
671
|
-
if (
|
|
863
|
+
if (value == null) continue;
|
|
864
|
+
if (!this.#isPrimitiveValue(value)) {
|
|
672
865
|
return false;
|
|
673
866
|
}
|
|
674
867
|
}
|
|
675
868
|
}
|
|
676
|
-
return
|
|
869
|
+
return intersection.size > 0;
|
|
677
870
|
}
|
|
678
871
|
#renderPrimitiveArray(key, items, depth) {
|
|
679
872
|
const values = items.map((item) => this.#formatValue(item)).join(",");
|
|
@@ -683,10 +876,16 @@ ${entries}`;
|
|
|
683
876
|
if (items.length === 0) {
|
|
684
877
|
return `${this.#pad(depth)}${key}[0]:`;
|
|
685
878
|
}
|
|
686
|
-
const fields =
|
|
879
|
+
const fields = Array.from(
|
|
880
|
+
new Set(items.flatMap((obj) => Object.keys(obj)))
|
|
881
|
+
);
|
|
687
882
|
const header = `${this.#pad(depth)}${key}[${items.length}]{${fields.join(",")}}:`;
|
|
688
883
|
const rows = items.map((obj) => {
|
|
689
|
-
const values = fields.map((f) =>
|
|
884
|
+
const values = fields.map((f) => {
|
|
885
|
+
const value = obj[f];
|
|
886
|
+
if (value == null) return "";
|
|
887
|
+
return this.#formatValue(value);
|
|
888
|
+
});
|
|
690
889
|
return `${this.#pad(depth + 1)}${values.join(",")}`;
|
|
691
890
|
});
|
|
692
891
|
return [header, ...rows].join("\n");
|
|
@@ -826,1185 +1025,2660 @@ ${entries}`;
|
|
|
826
1025
|
var ContextStore = class {
|
|
827
1026
|
};
|
|
828
1027
|
|
|
829
|
-
// packages/context/src/lib/
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
846
|
-
id TEXT PRIMARY KEY,
|
|
847
|
-
chatId TEXT NOT NULL,
|
|
848
|
-
parentId TEXT,
|
|
849
|
-
name TEXT NOT NULL,
|
|
850
|
-
type TEXT,
|
|
851
|
-
data TEXT NOT NULL,
|
|
852
|
-
createdAt INTEGER NOT NULL,
|
|
853
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
854
|
-
FOREIGN KEY (parentId) REFERENCES messages(id)
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
|
|
858
|
-
CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
|
|
859
|
-
|
|
860
|
-
-- Branches table (pointers to head messages)
|
|
861
|
-
CREATE TABLE IF NOT EXISTS branches (
|
|
862
|
-
id TEXT PRIMARY KEY,
|
|
863
|
-
chatId TEXT NOT NULL,
|
|
864
|
-
name TEXT NOT NULL,
|
|
865
|
-
headMessageId TEXT,
|
|
866
|
-
isActive INTEGER NOT NULL DEFAULT 0,
|
|
867
|
-
createdAt INTEGER NOT NULL,
|
|
868
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
869
|
-
FOREIGN KEY (headMessageId) REFERENCES messages(id),
|
|
870
|
-
UNIQUE(chatId, name)
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
CREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);
|
|
874
|
-
|
|
875
|
-
-- Checkpoints table (pointers to message nodes)
|
|
876
|
-
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
877
|
-
id TEXT PRIMARY KEY,
|
|
878
|
-
chatId TEXT NOT NULL,
|
|
879
|
-
name TEXT NOT NULL,
|
|
880
|
-
messageId TEXT NOT NULL,
|
|
881
|
-
createdAt INTEGER NOT NULL,
|
|
882
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
883
|
-
FOREIGN KEY (messageId) REFERENCES messages(id),
|
|
884
|
-
UNIQUE(chatId, name)
|
|
885
|
-
);
|
|
886
|
-
|
|
887
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
|
|
888
|
-
|
|
889
|
-
-- FTS5 virtual table for full-text search
|
|
890
|
-
-- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
|
|
891
|
-
-- Only 'content' is indexed for full-text search
|
|
892
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
893
|
-
messageId UNINDEXED,
|
|
894
|
-
chatId UNINDEXED,
|
|
895
|
-
name UNINDEXED,
|
|
896
|
-
content,
|
|
897
|
-
tokenize='porter unicode61'
|
|
898
|
-
);
|
|
899
|
-
`;
|
|
900
|
-
var SqliteContextStore = class extends ContextStore {
|
|
901
|
-
#db;
|
|
902
|
-
constructor(path) {
|
|
903
|
-
super();
|
|
904
|
-
this.#db = new DatabaseSync(path);
|
|
905
|
-
this.#db.exec("PRAGMA foreign_keys = ON");
|
|
906
|
-
this.#db.exec(STORE_DDL);
|
|
907
|
-
}
|
|
908
|
-
// ==========================================================================
|
|
909
|
-
// Chat Operations
|
|
910
|
-
// ==========================================================================
|
|
911
|
-
async createChat(chat) {
|
|
912
|
-
this.#db.prepare(
|
|
913
|
-
`INSERT INTO chats (id, title, metadata)
|
|
914
|
-
VALUES (?, ?, ?)`
|
|
915
|
-
).run(
|
|
916
|
-
chat.id,
|
|
917
|
-
chat.title ?? null,
|
|
918
|
-
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
async upsertChat(chat) {
|
|
922
|
-
const row = this.#db.prepare(
|
|
923
|
-
`INSERT INTO chats (id, title, metadata)
|
|
924
|
-
VALUES (?, ?, ?)
|
|
925
|
-
ON CONFLICT(id) DO UPDATE SET id = excluded.id
|
|
926
|
-
RETURNING *`
|
|
927
|
-
).get(
|
|
928
|
-
chat.id,
|
|
929
|
-
chat.title ?? null,
|
|
930
|
-
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
931
|
-
);
|
|
932
|
-
return {
|
|
933
|
-
id: row.id,
|
|
934
|
-
title: row.title ?? void 0,
|
|
935
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
936
|
-
createdAt: row.createdAt,
|
|
937
|
-
updatedAt: row.updatedAt
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
async getChat(chatId) {
|
|
941
|
-
const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
|
|
942
|
-
if (!row) {
|
|
943
|
-
return void 0;
|
|
944
|
-
}
|
|
945
|
-
return {
|
|
946
|
-
id: row.id,
|
|
947
|
-
title: row.title ?? void 0,
|
|
948
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
949
|
-
createdAt: row.createdAt,
|
|
950
|
-
updatedAt: row.updatedAt
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
async updateChat(chatId, updates) {
|
|
954
|
-
const setClauses = ["updatedAt = strftime('%s', 'now') * 1000"];
|
|
955
|
-
const params = [];
|
|
956
|
-
if (updates.title !== void 0) {
|
|
957
|
-
setClauses.push("title = ?");
|
|
958
|
-
params.push(updates.title ?? null);
|
|
1028
|
+
// packages/context/src/lib/engine.ts
|
|
1029
|
+
var ContextEngine = class {
|
|
1030
|
+
/** Non-message fragments (role, hints, etc.) - not persisted in graph */
|
|
1031
|
+
#fragments = [];
|
|
1032
|
+
/** Pending message fragments to be added to graph */
|
|
1033
|
+
#pendingMessages = [];
|
|
1034
|
+
#store;
|
|
1035
|
+
#chatId;
|
|
1036
|
+
#userId;
|
|
1037
|
+
#branchName;
|
|
1038
|
+
#branch = null;
|
|
1039
|
+
#chatData = null;
|
|
1040
|
+
#initialized = false;
|
|
1041
|
+
constructor(options) {
|
|
1042
|
+
if (!options.chatId) {
|
|
1043
|
+
throw new Error("chatId is required");
|
|
959
1044
|
}
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
params.push(JSON.stringify(updates.metadata));
|
|
1045
|
+
if (!options.userId) {
|
|
1046
|
+
throw new Error("userId is required");
|
|
963
1047
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return {
|
|
969
|
-
id: row.id,
|
|
970
|
-
title: row.title ?? void 0,
|
|
971
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
972
|
-
createdAt: row.createdAt,
|
|
973
|
-
updatedAt: row.updatedAt
|
|
974
|
-
};
|
|
1048
|
+
this.#store = options.store;
|
|
1049
|
+
this.#chatId = options.chatId;
|
|
1050
|
+
this.#userId = options.userId;
|
|
1051
|
+
this.#branchName = "main";
|
|
975
1052
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
ORDER BY c.updatedAt DESC`
|
|
990
|
-
).all();
|
|
991
|
-
return rows.map((row) => ({
|
|
992
|
-
id: row.id,
|
|
993
|
-
title: row.title ?? void 0,
|
|
994
|
-
messageCount: row.messageCount,
|
|
995
|
-
branchCount: row.branchCount,
|
|
996
|
-
createdAt: row.createdAt,
|
|
997
|
-
updatedAt: row.updatedAt
|
|
998
|
-
}));
|
|
1053
|
+
/**
|
|
1054
|
+
* Initialize the chat and branch if they don't exist.
|
|
1055
|
+
*/
|
|
1056
|
+
async #ensureInitialized() {
|
|
1057
|
+
if (this.#initialized) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
this.#chatData = await this.#store.upsertChat({
|
|
1061
|
+
id: this.#chatId,
|
|
1062
|
+
userId: this.#userId
|
|
1063
|
+
});
|
|
1064
|
+
this.#branch = await this.#store.getActiveBranch(this.#chatId);
|
|
1065
|
+
this.#initialized = true;
|
|
999
1066
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
parentId = excluded.parentId,
|
|
1009
|
-
name = excluded.name,
|
|
1010
|
-
type = excluded.type,
|
|
1011
|
-
data = excluded.data`
|
|
1012
|
-
).run(
|
|
1013
|
-
message2.id,
|
|
1014
|
-
message2.chatId,
|
|
1015
|
-
message2.parentId,
|
|
1016
|
-
message2.name,
|
|
1017
|
-
message2.type ?? null,
|
|
1018
|
-
JSON.stringify(message2.data),
|
|
1019
|
-
message2.createdAt
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a new branch from a specific message.
|
|
1069
|
+
* Shared logic between rewind() and btw().
|
|
1070
|
+
*/
|
|
1071
|
+
async #createBranchFrom(messageId, switchTo) {
|
|
1072
|
+
const branches = await this.#store.listBranches(this.#chatId);
|
|
1073
|
+
const samePrefix = branches.filter(
|
|
1074
|
+
(b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
|
|
1020
1075
|
);
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1076
|
+
const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
|
|
1077
|
+
const newBranch = {
|
|
1078
|
+
id: crypto.randomUUID(),
|
|
1079
|
+
chatId: this.#chatId,
|
|
1080
|
+
name: newBranchName,
|
|
1081
|
+
headMessageId: messageId,
|
|
1082
|
+
isActive: false,
|
|
1083
|
+
createdAt: Date.now()
|
|
1084
|
+
};
|
|
1085
|
+
await this.#store.createBranch(newBranch);
|
|
1086
|
+
if (switchTo) {
|
|
1087
|
+
await this.#store.setActiveBranch(this.#chatId, newBranch.id);
|
|
1088
|
+
this.#branch = { ...newBranch, isActive: true };
|
|
1089
|
+
this.#branchName = newBranchName;
|
|
1090
|
+
this.#pendingMessages = [];
|
|
1032
1091
|
}
|
|
1092
|
+
const chain = await this.#store.getMessageChain(messageId);
|
|
1033
1093
|
return {
|
|
1034
|
-
id:
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
createdAt: row.createdAt
|
|
1094
|
+
id: newBranch.id,
|
|
1095
|
+
name: newBranch.name,
|
|
1096
|
+
headMessageId: newBranch.headMessageId,
|
|
1097
|
+
isActive: switchTo,
|
|
1098
|
+
messageCount: chain.length,
|
|
1099
|
+
createdAt: newBranch.createdAt
|
|
1041
1100
|
};
|
|
1042
1101
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
SELECT m.*, c.depth + 1 FROM messages m
|
|
1049
|
-
INNER JOIN chain c ON m.id = c.parentId
|
|
1050
|
-
)
|
|
1051
|
-
SELECT * FROM chain
|
|
1052
|
-
ORDER BY depth DESC`
|
|
1053
|
-
).all(headId);
|
|
1054
|
-
return rows.map((row) => ({
|
|
1055
|
-
id: row.id,
|
|
1056
|
-
chatId: row.chatId,
|
|
1057
|
-
parentId: row.parentId,
|
|
1058
|
-
name: row.name,
|
|
1059
|
-
type: row.type ?? void 0,
|
|
1060
|
-
data: JSON.parse(row.data),
|
|
1061
|
-
createdAt: row.createdAt
|
|
1062
|
-
}));
|
|
1063
|
-
}
|
|
1064
|
-
async hasChildren(messageId) {
|
|
1065
|
-
const row = this.#db.prepare(
|
|
1066
|
-
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
|
|
1067
|
-
).get(messageId);
|
|
1068
|
-
return row.hasChildren === 1;
|
|
1102
|
+
/**
|
|
1103
|
+
* Get the current chat ID.
|
|
1104
|
+
*/
|
|
1105
|
+
get chatId() {
|
|
1106
|
+
return this.#chatId;
|
|
1069
1107
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
this.#
|
|
1075
|
-
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
1076
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1077
|
-
).run(
|
|
1078
|
-
branch.id,
|
|
1079
|
-
branch.chatId,
|
|
1080
|
-
branch.name,
|
|
1081
|
-
branch.headMessageId,
|
|
1082
|
-
branch.isActive ? 1 : 0,
|
|
1083
|
-
branch.createdAt
|
|
1084
|
-
);
|
|
1108
|
+
/**
|
|
1109
|
+
* Get the current branch name.
|
|
1110
|
+
*/
|
|
1111
|
+
get branch() {
|
|
1112
|
+
return this.#branchName;
|
|
1085
1113
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1114
|
+
/**
|
|
1115
|
+
* Get metadata for the current chat.
|
|
1116
|
+
* Returns null if the chat hasn't been initialized yet.
|
|
1117
|
+
*/
|
|
1118
|
+
get chat() {
|
|
1119
|
+
if (!this.#chatData) {
|
|
1120
|
+
return null;
|
|
1090
1121
|
}
|
|
1091
1122
|
return {
|
|
1092
|
-
id:
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1123
|
+
id: this.#chatData.id,
|
|
1124
|
+
userId: this.#chatData.userId,
|
|
1125
|
+
createdAt: this.#chatData.createdAt,
|
|
1126
|
+
updatedAt: this.#chatData.updatedAt,
|
|
1127
|
+
title: this.#chatData.title,
|
|
1128
|
+
metadata: this.#chatData.metadata
|
|
1098
1129
|
};
|
|
1099
1130
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1131
|
+
/**
|
|
1132
|
+
* Add fragments to the context.
|
|
1133
|
+
*
|
|
1134
|
+
* - Message fragments (user/assistant) are queued for persistence
|
|
1135
|
+
* - Non-message fragments (role/hint) are kept in memory for system prompt
|
|
1136
|
+
*/
|
|
1137
|
+
set(...fragments) {
|
|
1138
|
+
for (const fragment2 of fragments) {
|
|
1139
|
+
if (isMessageFragment(fragment2)) {
|
|
1140
|
+
this.#pendingMessages.push(fragment2);
|
|
1141
|
+
} else {
|
|
1142
|
+
this.#fragments.push(fragment2);
|
|
1143
|
+
}
|
|
1104
1144
|
}
|
|
1105
|
-
return
|
|
1106
|
-
id: row.id,
|
|
1107
|
-
chatId: row.chatId,
|
|
1108
|
-
name: row.name,
|
|
1109
|
-
headMessageId: row.headMessageId,
|
|
1110
|
-
isActive: true,
|
|
1111
|
-
createdAt: row.createdAt
|
|
1112
|
-
};
|
|
1145
|
+
return this;
|
|
1113
1146
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
|
|
1147
|
+
// Unset a fragment by ID (not implemented yet)
|
|
1148
|
+
unset(fragmentId) {
|
|
1117
1149
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1150
|
+
/**
|
|
1151
|
+
* Render all fragments using the provided renderer.
|
|
1152
|
+
* @internal Use resolve() instead for public API.
|
|
1153
|
+
*/
|
|
1154
|
+
render(renderer) {
|
|
1155
|
+
return renderer.render(this.#fragments);
|
|
1120
1156
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1157
|
+
/**
|
|
1158
|
+
* Resolve context into AI SDK-ready format.
|
|
1159
|
+
*
|
|
1160
|
+
* - Initializes chat and branch if needed
|
|
1161
|
+
* - Loads message history from the graph (walking parent chain)
|
|
1162
|
+
* - Separates context fragments for system prompt
|
|
1163
|
+
* - Combines with pending messages
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```ts
|
|
1167
|
+
* const context = new ContextEngine({ store, chatId: 'chat-1', userId: 'user-1' })
|
|
1168
|
+
* .set(role('You are helpful'), user('Hello'));
|
|
1169
|
+
*
|
|
1170
|
+
* const { systemPrompt, messages } = await context.resolve();
|
|
1171
|
+
* await generateText({ system: systemPrompt, messages });
|
|
1172
|
+
* ```
|
|
1173
|
+
*/
|
|
1174
|
+
async resolve(options) {
|
|
1175
|
+
await this.#ensureInitialized();
|
|
1176
|
+
const systemPrompt = options.renderer.render(this.#fragments);
|
|
1177
|
+
const messages = [];
|
|
1178
|
+
if (this.#branch?.headMessageId) {
|
|
1179
|
+
const chain = await this.#store.getMessageChain(
|
|
1180
|
+
this.#branch.headMessageId
|
|
1181
|
+
);
|
|
1182
|
+
for (const msg of chain) {
|
|
1183
|
+
messages.push(message(msg.data).codec?.decode());
|
|
1147
1184
|
}
|
|
1148
|
-
result.push({
|
|
1149
|
-
id: branch.id,
|
|
1150
|
-
name: branch.name,
|
|
1151
|
-
headMessageId: branch.headMessageId,
|
|
1152
|
-
isActive: branch.isActive === 1,
|
|
1153
|
-
messageCount,
|
|
1154
|
-
createdAt: branch.createdAt
|
|
1155
|
-
});
|
|
1156
1185
|
}
|
|
1157
|
-
|
|
1186
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1187
|
+
const decoded = fragment2.codec.decode();
|
|
1188
|
+
messages.push(decoded);
|
|
1189
|
+
}
|
|
1190
|
+
return { systemPrompt, messages };
|
|
1158
1191
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
)
|
|
1192
|
+
/**
|
|
1193
|
+
* Save pending messages to the graph.
|
|
1194
|
+
*
|
|
1195
|
+
* Each message is added as a node with parentId pointing to the previous message.
|
|
1196
|
+
* The branch head is updated to point to the last message.
|
|
1197
|
+
*
|
|
1198
|
+
* @example
|
|
1199
|
+
* ```ts
|
|
1200
|
+
* context.set(user('Hello'));
|
|
1201
|
+
* // AI responds...
|
|
1202
|
+
* context.set(assistant('Hi there!'));
|
|
1203
|
+
* await context.save(); // Persist to graph
|
|
1204
|
+
* ```
|
|
1205
|
+
*/
|
|
1206
|
+
async save() {
|
|
1207
|
+
await this.#ensureInitialized();
|
|
1208
|
+
if (this.#pendingMessages.length === 0) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
let parentId = this.#branch.headMessageId;
|
|
1212
|
+
const now = Date.now();
|
|
1213
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1214
|
+
const messageData = {
|
|
1215
|
+
id: fragment2.id ?? crypto.randomUUID(),
|
|
1216
|
+
chatId: this.#chatId,
|
|
1217
|
+
parentId,
|
|
1218
|
+
name: fragment2.name,
|
|
1219
|
+
type: fragment2.type,
|
|
1220
|
+
data: fragment2.codec.encode(),
|
|
1221
|
+
createdAt: now
|
|
1222
|
+
};
|
|
1223
|
+
await this.#store.addMessage(messageData);
|
|
1224
|
+
parentId = messageData.id;
|
|
1225
|
+
}
|
|
1226
|
+
await this.#store.updateBranchHead(this.#branch.id, parentId);
|
|
1227
|
+
this.#branch.headMessageId = parentId;
|
|
1228
|
+
this.#pendingMessages = [];
|
|
1176
1229
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1230
|
+
/**
|
|
1231
|
+
* Estimate token count and cost for the full context.
|
|
1232
|
+
*
|
|
1233
|
+
* Includes:
|
|
1234
|
+
* - System prompt fragments (role, hints, etc.)
|
|
1235
|
+
* - Persisted chat messages (from store)
|
|
1236
|
+
* - Pending messages (not yet saved)
|
|
1237
|
+
*
|
|
1238
|
+
* @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
|
|
1239
|
+
* @param options - Optional settings
|
|
1240
|
+
* @returns Estimate result with token counts, costs, and per-fragment breakdown
|
|
1241
|
+
*/
|
|
1242
|
+
async estimate(modelId, options = {}) {
|
|
1243
|
+
await this.#ensureInitialized();
|
|
1244
|
+
const renderer = options.renderer ?? new XmlRenderer();
|
|
1245
|
+
const registry = getModelsRegistry();
|
|
1246
|
+
await registry.load();
|
|
1247
|
+
const model = registry.get(modelId);
|
|
1248
|
+
if (!model) {
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const tokenizer = registry.getTokenizer(modelId);
|
|
1254
|
+
const fragmentEstimates = [];
|
|
1255
|
+
for (const fragment2 of this.#fragments) {
|
|
1256
|
+
const rendered = renderer.render([fragment2]);
|
|
1257
|
+
const tokens = tokenizer.count(rendered);
|
|
1258
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
1259
|
+
fragmentEstimates.push({
|
|
1260
|
+
id: fragment2.id,
|
|
1261
|
+
name: fragment2.name,
|
|
1262
|
+
tokens,
|
|
1263
|
+
cost
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (this.#branch?.headMessageId) {
|
|
1267
|
+
const chain = await this.#store.getMessageChain(
|
|
1268
|
+
this.#branch.headMessageId
|
|
1269
|
+
);
|
|
1270
|
+
for (const msg of chain) {
|
|
1271
|
+
const content = String(msg.data);
|
|
1272
|
+
const tokens = tokenizer.count(content);
|
|
1273
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
1274
|
+
fragmentEstimates.push({
|
|
1275
|
+
name: msg.name,
|
|
1276
|
+
id: msg.id,
|
|
1277
|
+
tokens,
|
|
1278
|
+
cost
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1283
|
+
const content = String(fragment2.data);
|
|
1284
|
+
const tokens = tokenizer.count(content);
|
|
1285
|
+
const cost = tokens / 1e6 * model.cost.input;
|
|
1286
|
+
fragmentEstimates.push({
|
|
1287
|
+
name: fragment2.name,
|
|
1288
|
+
id: fragment2.id,
|
|
1289
|
+
tokens,
|
|
1290
|
+
cost
|
|
1291
|
+
});
|
|
1181
1292
|
}
|
|
1293
|
+
const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
|
|
1294
|
+
const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
|
|
1182
1295
|
return {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1296
|
+
model: model.id,
|
|
1297
|
+
provider: model.provider,
|
|
1298
|
+
tokens: totalTokens,
|
|
1299
|
+
cost: totalCost,
|
|
1300
|
+
limits: {
|
|
1301
|
+
context: model.limit.context,
|
|
1302
|
+
output: model.limit.output,
|
|
1303
|
+
exceedsContext: totalTokens > model.limit.context
|
|
1304
|
+
},
|
|
1305
|
+
fragments: fragmentEstimates
|
|
1188
1306
|
};
|
|
1189
1307
|
}
|
|
1190
|
-
async listCheckpoints(chatId) {
|
|
1191
|
-
const rows = this.#db.prepare(
|
|
1192
|
-
`SELECT id, name, messageId, createdAt
|
|
1193
|
-
FROM checkpoints
|
|
1194
|
-
WHERE chatId = ?
|
|
1195
|
-
ORDER BY createdAt DESC`
|
|
1196
|
-
).all(chatId);
|
|
1197
|
-
return rows.map((row) => ({
|
|
1198
|
-
id: row.id,
|
|
1199
|
-
name: row.name,
|
|
1200
|
-
messageId: row.messageId,
|
|
1201
|
-
createdAt: row.createdAt
|
|
1202
|
-
}));
|
|
1203
|
-
}
|
|
1204
|
-
async deleteCheckpoint(chatId, name) {
|
|
1205
|
-
this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
|
|
1206
|
-
}
|
|
1207
|
-
// ==========================================================================
|
|
1208
|
-
// Search Operations
|
|
1209
|
-
// ==========================================================================
|
|
1210
|
-
async searchMessages(chatId, query, options) {
|
|
1211
|
-
const limit = options?.limit ?? 20;
|
|
1212
|
-
const roles = options?.roles;
|
|
1213
|
-
let sql = `
|
|
1214
|
-
SELECT
|
|
1215
|
-
m.id,
|
|
1216
|
-
m.chatId,
|
|
1217
|
-
m.parentId,
|
|
1218
|
-
m.name,
|
|
1219
|
-
m.type,
|
|
1220
|
-
m.data,
|
|
1221
|
-
m.createdAt,
|
|
1222
|
-
fts.rank,
|
|
1223
|
-
snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
|
|
1224
|
-
FROM messages_fts fts
|
|
1225
|
-
JOIN messages m ON m.id = fts.messageId
|
|
1226
|
-
WHERE messages_fts MATCH ?
|
|
1227
|
-
AND fts.chatId = ?
|
|
1228
|
-
`;
|
|
1229
|
-
const params = [query, chatId];
|
|
1230
|
-
if (roles && roles.length > 0) {
|
|
1231
|
-
const placeholders = roles.map(() => "?").join(", ");
|
|
1232
|
-
sql += ` AND fts.name IN (${placeholders})`;
|
|
1233
|
-
params.push(...roles);
|
|
1234
|
-
}
|
|
1235
|
-
sql += " ORDER BY fts.rank LIMIT ?";
|
|
1236
|
-
params.push(limit);
|
|
1237
|
-
const rows = this.#db.prepare(sql).all(...params);
|
|
1238
|
-
return rows.map((row) => ({
|
|
1239
|
-
message: {
|
|
1240
|
-
id: row.id,
|
|
1241
|
-
chatId: row.chatId,
|
|
1242
|
-
parentId: row.parentId,
|
|
1243
|
-
name: row.name,
|
|
1244
|
-
type: row.type ?? void 0,
|
|
1245
|
-
data: JSON.parse(row.data),
|
|
1246
|
-
createdAt: row.createdAt
|
|
1247
|
-
},
|
|
1248
|
-
rank: row.rank,
|
|
1249
|
-
snippet: row.snippet
|
|
1250
|
-
}));
|
|
1251
|
-
}
|
|
1252
|
-
// ==========================================================================
|
|
1253
|
-
// Visualization Operations
|
|
1254
|
-
// ==========================================================================
|
|
1255
|
-
async getGraph(chatId) {
|
|
1256
|
-
const messageRows = this.#db.prepare(
|
|
1257
|
-
`SELECT id, parentId, name, data, createdAt
|
|
1258
|
-
FROM messages
|
|
1259
|
-
WHERE chatId = ?
|
|
1260
|
-
ORDER BY createdAt ASC`
|
|
1261
|
-
).all(chatId);
|
|
1262
|
-
const nodes = messageRows.map((row) => {
|
|
1263
|
-
const data = JSON.parse(row.data);
|
|
1264
|
-
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
1265
|
-
return {
|
|
1266
|
-
id: row.id,
|
|
1267
|
-
parentId: row.parentId,
|
|
1268
|
-
role: row.name,
|
|
1269
|
-
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
1270
|
-
createdAt: row.createdAt
|
|
1271
|
-
};
|
|
1272
|
-
});
|
|
1273
|
-
const branchRows = this.#db.prepare(
|
|
1274
|
-
`SELECT name, headMessageId, isActive
|
|
1275
|
-
FROM branches
|
|
1276
|
-
WHERE chatId = ?
|
|
1277
|
-
ORDER BY createdAt ASC`
|
|
1278
|
-
).all(chatId);
|
|
1279
|
-
const branches = branchRows.map((row) => ({
|
|
1280
|
-
name: row.name,
|
|
1281
|
-
headMessageId: row.headMessageId,
|
|
1282
|
-
isActive: row.isActive === 1
|
|
1283
|
-
}));
|
|
1284
|
-
const checkpointRows = this.#db.prepare(
|
|
1285
|
-
`SELECT name, messageId
|
|
1286
|
-
FROM checkpoints
|
|
1287
|
-
WHERE chatId = ?
|
|
1288
|
-
ORDER BY createdAt ASC`
|
|
1289
|
-
).all(chatId);
|
|
1290
|
-
const checkpoints = checkpointRows.map((row) => ({
|
|
1291
|
-
name: row.name,
|
|
1292
|
-
messageId: row.messageId
|
|
1293
|
-
}));
|
|
1294
|
-
return {
|
|
1295
|
-
chatId,
|
|
1296
|
-
nodes,
|
|
1297
|
-
branches,
|
|
1298
|
-
checkpoints
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
|
-
|
|
1303
|
-
// packages/context/src/lib/store/memory.store.ts
|
|
1304
|
-
var InMemoryContextStore = class extends SqliteContextStore {
|
|
1305
|
-
constructor() {
|
|
1306
|
-
super(":memory:");
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
// packages/context/src/lib/visualize.ts
|
|
1311
|
-
function visualizeGraph(data) {
|
|
1312
|
-
if (data.nodes.length === 0) {
|
|
1313
|
-
return `[chat: ${data.chatId}]
|
|
1314
|
-
|
|
1315
|
-
(empty)`;
|
|
1316
|
-
}
|
|
1317
|
-
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
1318
|
-
const branchHeads = /* @__PURE__ */ new Map();
|
|
1319
|
-
const checkpointsByMessageId = /* @__PURE__ */ new Map();
|
|
1320
|
-
for (const node of data.nodes) {
|
|
1321
|
-
const children = childrenByParentId.get(node.parentId) ?? [];
|
|
1322
|
-
children.push(node);
|
|
1323
|
-
childrenByParentId.set(node.parentId, children);
|
|
1324
|
-
}
|
|
1325
|
-
for (const branch of data.branches) {
|
|
1326
|
-
if (branch.headMessageId) {
|
|
1327
|
-
const heads = branchHeads.get(branch.headMessageId) ?? [];
|
|
1328
|
-
heads.push(branch.isActive ? `${branch.name} *` : branch.name);
|
|
1329
|
-
branchHeads.set(branch.headMessageId, heads);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
for (const checkpoint of data.checkpoints) {
|
|
1333
|
-
const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
|
|
1334
|
-
cps.push(checkpoint.name);
|
|
1335
|
-
checkpointsByMessageId.set(checkpoint.messageId, cps);
|
|
1336
|
-
}
|
|
1337
|
-
const roots = childrenByParentId.get(null) ?? [];
|
|
1338
|
-
const lines = [`[chat: ${data.chatId}]`, ""];
|
|
1339
|
-
function renderNode(node, prefix, isLast, isRoot) {
|
|
1340
|
-
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1341
|
-
const contentPreview = node.content.replace(/\n/g, " ");
|
|
1342
|
-
let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
|
|
1343
|
-
const branches = branchHeads.get(node.id);
|
|
1344
|
-
if (branches) {
|
|
1345
|
-
line += ` <- [${branches.join(", ")}]`;
|
|
1346
|
-
}
|
|
1347
|
-
const checkpoints = checkpointsByMessageId.get(node.id);
|
|
1348
|
-
if (checkpoints) {
|
|
1349
|
-
line += ` {${checkpoints.join(", ")}}`;
|
|
1350
|
-
}
|
|
1351
|
-
lines.push(line);
|
|
1352
|
-
const children = childrenByParentId.get(node.id) ?? [];
|
|
1353
|
-
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1354
|
-
for (let i = 0; i < children.length; i++) {
|
|
1355
|
-
renderNode(children[i], childPrefix, i === children.length - 1, false);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
for (let i = 0; i < roots.length; i++) {
|
|
1359
|
-
renderNode(roots[i], "", i === roots.length - 1, true);
|
|
1360
|
-
}
|
|
1361
|
-
lines.push("");
|
|
1362
|
-
lines.push("Legend: * = active branch, {...} = checkpoint");
|
|
1363
|
-
return lines.join("\n");
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// packages/context/src/index.ts
|
|
1367
|
-
var ContextEngine = class {
|
|
1368
|
-
/** Non-message fragments (role, hints, etc.) - not persisted in graph */
|
|
1369
|
-
#fragments = [];
|
|
1370
|
-
/** Pending message fragments to be added to graph */
|
|
1371
|
-
#pendingMessages = [];
|
|
1372
|
-
#store;
|
|
1373
|
-
#chatId;
|
|
1374
|
-
#branchName;
|
|
1375
|
-
#branch = null;
|
|
1376
|
-
#chatData = null;
|
|
1377
|
-
#initialized = false;
|
|
1378
|
-
constructor(options) {
|
|
1379
|
-
if (!options.chatId) {
|
|
1380
|
-
throw new Error("chatId is required");
|
|
1381
|
-
}
|
|
1382
|
-
this.#store = options.store;
|
|
1383
|
-
this.#chatId = options.chatId;
|
|
1384
|
-
this.#branchName = options.branch ?? "main";
|
|
1385
|
-
}
|
|
1386
1308
|
/**
|
|
1387
|
-
*
|
|
1309
|
+
* Rewind to a specific message by ID.
|
|
1310
|
+
*
|
|
1311
|
+
* Creates a new branch from that message, preserving the original branch.
|
|
1312
|
+
* The new branch becomes active.
|
|
1313
|
+
*
|
|
1314
|
+
* @param messageId - The message ID to rewind to
|
|
1315
|
+
* @returns The new branch info
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* ```ts
|
|
1319
|
+
* context.set(user('What is 2 + 2?', { id: 'q1' }));
|
|
1320
|
+
* context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
|
|
1321
|
+
* await context.save();
|
|
1322
|
+
*
|
|
1323
|
+
* // Rewind to the question, creates new branch
|
|
1324
|
+
* const newBranch = await context.rewind('q1');
|
|
1325
|
+
*
|
|
1326
|
+
* // Now add correct answer on new branch
|
|
1327
|
+
* context.set(assistant('The answer is 4.'));
|
|
1328
|
+
* await context.save();
|
|
1329
|
+
* ```
|
|
1388
1330
|
*/
|
|
1389
|
-
async
|
|
1390
|
-
|
|
1391
|
-
|
|
1331
|
+
async rewind(messageId) {
|
|
1332
|
+
await this.#ensureInitialized();
|
|
1333
|
+
const message2 = await this.#store.getMessage(messageId);
|
|
1334
|
+
if (!message2) {
|
|
1335
|
+
throw new Error(`Message "${messageId}" not found`);
|
|
1392
1336
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
this.#chatId,
|
|
1396
|
-
this.#branchName
|
|
1397
|
-
);
|
|
1398
|
-
if (existingBranch) {
|
|
1399
|
-
this.#branch = existingBranch;
|
|
1400
|
-
} else {
|
|
1401
|
-
this.#branch = {
|
|
1402
|
-
id: crypto.randomUUID(),
|
|
1403
|
-
chatId: this.#chatId,
|
|
1404
|
-
name: this.#branchName,
|
|
1405
|
-
headMessageId: null,
|
|
1406
|
-
isActive: true,
|
|
1407
|
-
createdAt: Date.now()
|
|
1408
|
-
};
|
|
1409
|
-
await this.#store.createBranch(this.#branch);
|
|
1337
|
+
if (message2.chatId !== this.#chatId) {
|
|
1338
|
+
throw new Error(`Message "${messageId}" belongs to a different chat`);
|
|
1410
1339
|
}
|
|
1411
|
-
this.#
|
|
1340
|
+
return this.#createBranchFrom(messageId, true);
|
|
1412
1341
|
}
|
|
1413
1342
|
/**
|
|
1414
|
-
* Create a
|
|
1415
|
-
*
|
|
1343
|
+
* Create a checkpoint at the current position.
|
|
1344
|
+
*
|
|
1345
|
+
* A checkpoint is a named pointer to the current branch head.
|
|
1346
|
+
* Use restore() to return to this point later.
|
|
1347
|
+
*
|
|
1348
|
+
* @param name - Name for the checkpoint
|
|
1349
|
+
* @returns The checkpoint info
|
|
1350
|
+
*
|
|
1351
|
+
* @example
|
|
1352
|
+
* ```ts
|
|
1353
|
+
* context.set(user('I want to learn a new skill.'));
|
|
1354
|
+
* context.set(assistant('Would you like coding or cooking?'));
|
|
1355
|
+
* await context.save();
|
|
1356
|
+
*
|
|
1357
|
+
* // Save checkpoint before user's choice
|
|
1358
|
+
* const cp = await context.checkpoint('before-choice');
|
|
1359
|
+
* ```
|
|
1416
1360
|
*/
|
|
1417
|
-
async
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
(
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
const newBranch = {
|
|
1361
|
+
async checkpoint(name) {
|
|
1362
|
+
await this.#ensureInitialized();
|
|
1363
|
+
if (!this.#branch?.headMessageId) {
|
|
1364
|
+
throw new Error("Cannot create checkpoint: no messages in conversation");
|
|
1365
|
+
}
|
|
1366
|
+
const checkpoint = {
|
|
1424
1367
|
id: crypto.randomUUID(),
|
|
1425
1368
|
chatId: this.#chatId,
|
|
1426
|
-
name
|
|
1427
|
-
|
|
1428
|
-
isActive: false,
|
|
1369
|
+
name,
|
|
1370
|
+
messageId: this.#branch.headMessageId,
|
|
1429
1371
|
createdAt: Date.now()
|
|
1430
1372
|
};
|
|
1431
|
-
await this.#store.
|
|
1432
|
-
if (switchTo) {
|
|
1433
|
-
await this.#store.setActiveBranch(this.#chatId, newBranch.id);
|
|
1434
|
-
this.#branch = { ...newBranch, isActive: true };
|
|
1435
|
-
this.#branchName = newBranchName;
|
|
1436
|
-
this.#pendingMessages = [];
|
|
1437
|
-
}
|
|
1438
|
-
const chain = await this.#store.getMessageChain(messageId);
|
|
1373
|
+
await this.#store.createCheckpoint(checkpoint);
|
|
1439
1374
|
return {
|
|
1440
|
-
id:
|
|
1441
|
-
name:
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
messageCount: chain.length,
|
|
1445
|
-
createdAt: newBranch.createdAt
|
|
1375
|
+
id: checkpoint.id,
|
|
1376
|
+
name: checkpoint.name,
|
|
1377
|
+
messageId: checkpoint.messageId,
|
|
1378
|
+
createdAt: checkpoint.createdAt
|
|
1446
1379
|
};
|
|
1447
1380
|
}
|
|
1448
1381
|
/**
|
|
1449
|
-
*
|
|
1450
|
-
*/
|
|
1451
|
-
get chatId() {
|
|
1452
|
-
return this.#chatId;
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Get the current branch name.
|
|
1456
|
-
*/
|
|
1457
|
-
get branch() {
|
|
1458
|
-
return this.#branchName;
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Get metadata for the current chat.
|
|
1462
|
-
* Returns null if the chat hasn't been initialized yet.
|
|
1463
|
-
*/
|
|
1464
|
-
get chat() {
|
|
1465
|
-
if (!this.#chatData) {
|
|
1466
|
-
return null;
|
|
1467
|
-
}
|
|
1468
|
-
return {
|
|
1469
|
-
id: this.#chatData.id,
|
|
1470
|
-
createdAt: this.#chatData.createdAt,
|
|
1471
|
-
updatedAt: this.#chatData.updatedAt,
|
|
1472
|
-
title: this.#chatData.title,
|
|
1473
|
-
metadata: this.#chatData.metadata
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Add fragments to the context.
|
|
1478
|
-
*
|
|
1479
|
-
* - Message fragments (user/assistant) are queued for persistence
|
|
1480
|
-
* - Non-message fragments (role/hint) are kept in memory for system prompt
|
|
1481
|
-
*/
|
|
1482
|
-
set(...fragments) {
|
|
1483
|
-
for (const fragment2 of fragments) {
|
|
1484
|
-
if (isMessageFragment(fragment2)) {
|
|
1485
|
-
this.#pendingMessages.push(fragment2);
|
|
1486
|
-
} else {
|
|
1487
|
-
this.#fragments.push(fragment2);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
return this;
|
|
1491
|
-
}
|
|
1492
|
-
/**
|
|
1493
|
-
* Render all fragments using the provided renderer.
|
|
1494
|
-
* @internal Use resolve() instead for public API.
|
|
1495
|
-
*/
|
|
1496
|
-
render(renderer) {
|
|
1497
|
-
return renderer.render(this.#fragments);
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Resolve context into AI SDK-ready format.
|
|
1382
|
+
* Restore to a checkpoint by creating a new branch from that point.
|
|
1501
1383
|
*
|
|
1502
|
-
* -
|
|
1503
|
-
*
|
|
1504
|
-
* - Separates context fragments for system prompt
|
|
1505
|
-
* - Combines with pending messages
|
|
1384
|
+
* @param name - Name of the checkpoint to restore
|
|
1385
|
+
* @returns The new branch info
|
|
1506
1386
|
*
|
|
1507
1387
|
* @example
|
|
1508
1388
|
* ```ts
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1389
|
+
* // User chose cooking, but wants to try coding path
|
|
1390
|
+
* await context.restore('before-choice');
|
|
1511
1391
|
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1392
|
+
* context.set(user('I want to learn coding.'));
|
|
1393
|
+
* context.set(assistant('Python is a great starting language!'));
|
|
1394
|
+
* await context.save();
|
|
1514
1395
|
* ```
|
|
1515
1396
|
*/
|
|
1516
|
-
async
|
|
1397
|
+
async restore(name) {
|
|
1517
1398
|
await this.#ensureInitialized();
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
this.#branch.headMessageId
|
|
1399
|
+
const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
|
|
1400
|
+
if (!checkpoint) {
|
|
1401
|
+
throw new Error(
|
|
1402
|
+
`Checkpoint "${name}" not found in chat "${this.#chatId}"`
|
|
1523
1403
|
);
|
|
1524
|
-
for (const msg of chain) {
|
|
1525
|
-
messages.push(message(msg.data).codec?.decode());
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
for (const fragment2 of this.#pendingMessages) {
|
|
1529
|
-
const decoded = fragment2.codec.decode();
|
|
1530
|
-
messages.push(decoded);
|
|
1531
1404
|
}
|
|
1532
|
-
return
|
|
1405
|
+
return this.rewind(checkpoint.messageId);
|
|
1533
1406
|
}
|
|
1534
1407
|
/**
|
|
1535
|
-
*
|
|
1408
|
+
* Switch to a different branch by name.
|
|
1536
1409
|
*
|
|
1537
|
-
*
|
|
1538
|
-
* The branch head is updated to point to the last message.
|
|
1410
|
+
* @param name - Branch name to switch to
|
|
1539
1411
|
*
|
|
1540
1412
|
* @example
|
|
1541
1413
|
* ```ts
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1414
|
+
* // List branches (via store)
|
|
1415
|
+
* const branches = await store.listBranches(context.chatId);
|
|
1416
|
+
* console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
|
|
1417
|
+
*
|
|
1418
|
+
* // Switch to original branch
|
|
1419
|
+
* await context.switchBranch('main');
|
|
1546
1420
|
* ```
|
|
1547
1421
|
*/
|
|
1548
|
-
async
|
|
1422
|
+
async switchBranch(name) {
|
|
1549
1423
|
await this.#ensureInitialized();
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
let parentId = this.#branch.headMessageId;
|
|
1554
|
-
const now = Date.now();
|
|
1555
|
-
for (const fragment2 of this.#pendingMessages) {
|
|
1556
|
-
const messageData = {
|
|
1557
|
-
id: fragment2.id ?? crypto.randomUUID(),
|
|
1558
|
-
chatId: this.#chatId,
|
|
1559
|
-
parentId,
|
|
1560
|
-
name: fragment2.name,
|
|
1561
|
-
type: fragment2.type,
|
|
1562
|
-
data: fragment2.codec.encode(),
|
|
1563
|
-
createdAt: now
|
|
1564
|
-
};
|
|
1565
|
-
await this.#store.addMessage(messageData);
|
|
1566
|
-
parentId = messageData.id;
|
|
1424
|
+
const branch = await this.#store.getBranch(this.#chatId, name);
|
|
1425
|
+
if (!branch) {
|
|
1426
|
+
throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
|
|
1567
1427
|
}
|
|
1568
|
-
await this.#store.
|
|
1569
|
-
this.#branch
|
|
1428
|
+
await this.#store.setActiveBranch(this.#chatId, branch.id);
|
|
1429
|
+
this.#branch = { ...branch, isActive: true };
|
|
1430
|
+
this.#branchName = name;
|
|
1570
1431
|
this.#pendingMessages = [];
|
|
1571
1432
|
}
|
|
1572
1433
|
/**
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
* Includes:
|
|
1576
|
-
* - System prompt fragments (role, hints, etc.)
|
|
1577
|
-
* - Persisted chat messages (from store)
|
|
1578
|
-
* - Pending messages (not yet saved)
|
|
1434
|
+
* Create a parallel branch from the current position ("by the way").
|
|
1579
1435
|
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*/
|
|
1584
|
-
async estimate(modelId, options = {}) {
|
|
1585
|
-
await this.#ensureInitialized();
|
|
1586
|
-
const renderer = options.renderer ?? new XmlRenderer();
|
|
1587
|
-
const registry = getModelsRegistry();
|
|
1588
|
-
await registry.load();
|
|
1589
|
-
const model = registry.get(modelId);
|
|
1590
|
-
if (!model) {
|
|
1591
|
-
throw new Error(
|
|
1592
|
-
`Model "${modelId}" not found. Call load() first or check model ID.`
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
const tokenizer = registry.getTokenizer(modelId);
|
|
1596
|
-
const fragmentEstimates = [];
|
|
1597
|
-
for (const fragment2 of this.#fragments) {
|
|
1598
|
-
const rendered = renderer.render([fragment2]);
|
|
1599
|
-
const tokens = tokenizer.count(rendered);
|
|
1600
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1601
|
-
fragmentEstimates.push({
|
|
1602
|
-
id: fragment2.id,
|
|
1603
|
-
name: fragment2.name,
|
|
1604
|
-
tokens,
|
|
1605
|
-
cost
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
if (this.#branch?.headMessageId) {
|
|
1609
|
-
const chain = await this.#store.getMessageChain(
|
|
1610
|
-
this.#branch.headMessageId
|
|
1611
|
-
);
|
|
1612
|
-
for (const msg of chain) {
|
|
1613
|
-
const content = String(msg.data);
|
|
1614
|
-
const tokens = tokenizer.count(content);
|
|
1615
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1616
|
-
fragmentEstimates.push({
|
|
1617
|
-
name: msg.name,
|
|
1618
|
-
id: msg.id,
|
|
1619
|
-
tokens,
|
|
1620
|
-
cost
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
for (const fragment2 of this.#pendingMessages) {
|
|
1625
|
-
const content = String(fragment2.data);
|
|
1626
|
-
const tokens = tokenizer.count(content);
|
|
1627
|
-
const cost = tokens / 1e6 * model.cost.input;
|
|
1628
|
-
fragmentEstimates.push({
|
|
1629
|
-
name: fragment2.name,
|
|
1630
|
-
id: fragment2.id,
|
|
1631
|
-
tokens,
|
|
1632
|
-
cost
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
|
|
1636
|
-
const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
|
|
1637
|
-
return {
|
|
1638
|
-
model: model.id,
|
|
1639
|
-
provider: model.provider,
|
|
1640
|
-
tokens: totalTokens,
|
|
1641
|
-
cost: totalCost,
|
|
1642
|
-
limits: {
|
|
1643
|
-
context: model.limit.context,
|
|
1644
|
-
output: model.limit.output,
|
|
1645
|
-
exceedsContext: totalTokens > model.limit.context
|
|
1646
|
-
},
|
|
1647
|
-
fragments: fragmentEstimates
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Rewind to a specific message by ID.
|
|
1436
|
+
* Use this when you want to fork the conversation without leaving
|
|
1437
|
+
* the current branch. Common use case: user wants to ask another
|
|
1438
|
+
* question while waiting for the model to respond.
|
|
1652
1439
|
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1440
|
+
* Unlike rewind(), this method:
|
|
1441
|
+
* - Uses the current HEAD (no messageId needed)
|
|
1442
|
+
* - Does NOT switch to the new branch
|
|
1443
|
+
* - Keeps pending messages intact
|
|
1655
1444
|
*
|
|
1656
|
-
* @
|
|
1657
|
-
* @
|
|
1445
|
+
* @returns The new branch info (does not switch to it)
|
|
1446
|
+
* @throws Error if no messages exist in the conversation
|
|
1658
1447
|
*
|
|
1659
1448
|
* @example
|
|
1660
1449
|
* ```ts
|
|
1661
|
-
*
|
|
1662
|
-
* context.set(
|
|
1450
|
+
* // User asked a question, model is generating...
|
|
1451
|
+
* context.set(user('What is the weather?'));
|
|
1663
1452
|
* await context.save();
|
|
1664
1453
|
*
|
|
1665
|
-
* //
|
|
1666
|
-
* const newBranch = await context.
|
|
1454
|
+
* // User wants to ask something else without waiting
|
|
1455
|
+
* const newBranch = await context.btw();
|
|
1456
|
+
* // newBranch = { name: 'main-v2', ... }
|
|
1667
1457
|
*
|
|
1668
|
-
* //
|
|
1669
|
-
* context.
|
|
1458
|
+
* // Later, switch to the new branch and add the question
|
|
1459
|
+
* await context.switchBranch(newBranch.name);
|
|
1460
|
+
* context.set(user('Also, what time is it?'));
|
|
1670
1461
|
* await context.save();
|
|
1671
1462
|
* ```
|
|
1672
1463
|
*/
|
|
1673
|
-
async
|
|
1464
|
+
async btw() {
|
|
1674
1465
|
await this.#ensureInitialized();
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
throw new Error(`Message "${messageId}" not found`);
|
|
1678
|
-
}
|
|
1679
|
-
if (message2.chatId !== this.#chatId) {
|
|
1680
|
-
throw new Error(`Message "${messageId}" belongs to a different chat`);
|
|
1466
|
+
if (!this.#branch?.headMessageId) {
|
|
1467
|
+
throw new Error("Cannot create btw branch: no messages in conversation");
|
|
1681
1468
|
}
|
|
1682
|
-
return this.#createBranchFrom(
|
|
1469
|
+
return this.#createBranchFrom(this.#branch.headMessageId, false);
|
|
1683
1470
|
}
|
|
1684
1471
|
/**
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
* A checkpoint is a named pointer to the current branch head.
|
|
1688
|
-
* Use restore() to return to this point later.
|
|
1472
|
+
* Update metadata for the current chat.
|
|
1689
1473
|
*
|
|
1690
|
-
* @param
|
|
1691
|
-
* @returns The checkpoint info
|
|
1474
|
+
* @param updates - Partial metadata to merge (title, metadata)
|
|
1692
1475
|
*
|
|
1693
1476
|
* @example
|
|
1694
1477
|
* ```ts
|
|
1695
|
-
* context.
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
* const cp = await context.checkpoint('before-choice');
|
|
1701
|
-
* ```
|
|
1478
|
+
* await context.updateChat({
|
|
1479
|
+
* title: 'Coding Help Session',
|
|
1480
|
+
* metadata: { tags: ['python', 'debugging'] }
|
|
1481
|
+
* });
|
|
1482
|
+
* ```
|
|
1702
1483
|
*/
|
|
1703
|
-
async
|
|
1484
|
+
async updateChat(updates) {
|
|
1704
1485
|
await this.#ensureInitialized();
|
|
1705
|
-
|
|
1706
|
-
|
|
1486
|
+
const storeUpdates = {};
|
|
1487
|
+
if (updates.title !== void 0) {
|
|
1488
|
+
storeUpdates.title = updates.title;
|
|
1707
1489
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
await this.#store.createCheckpoint(checkpoint);
|
|
1716
|
-
return {
|
|
1717
|
-
id: checkpoint.id,
|
|
1718
|
-
name: checkpoint.name,
|
|
1719
|
-
messageId: checkpoint.messageId,
|
|
1720
|
-
createdAt: checkpoint.createdAt
|
|
1721
|
-
};
|
|
1490
|
+
if (updates.metadata !== void 0) {
|
|
1491
|
+
storeUpdates.metadata = {
|
|
1492
|
+
...this.#chatData?.metadata,
|
|
1493
|
+
...updates.metadata
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
|
|
1722
1497
|
}
|
|
1723
1498
|
/**
|
|
1724
|
-
*
|
|
1499
|
+
* Consolidate context fragments (no-op for now).
|
|
1725
1500
|
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1501
|
+
* This is a placeholder for future functionality that merges context fragments
|
|
1502
|
+
* using specific rules. Currently, it does nothing.
|
|
1503
|
+
*
|
|
1504
|
+
* @experimental
|
|
1505
|
+
*/
|
|
1506
|
+
consolidate() {
|
|
1507
|
+
return void 0;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Inspect the full context state for debugging.
|
|
1511
|
+
* Returns a JSON-serializable object with context information.
|
|
1512
|
+
*
|
|
1513
|
+
* @param options - Inspection options (modelId and renderer required)
|
|
1514
|
+
* @returns Complete inspection data including estimates, rendered output, fragments, and graph
|
|
1728
1515
|
*
|
|
1729
1516
|
* @example
|
|
1730
1517
|
* ```ts
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1518
|
+
* const inspection = await context.inspect({
|
|
1519
|
+
* modelId: 'openai:gpt-4o',
|
|
1520
|
+
* renderer: new XmlRenderer(),
|
|
1521
|
+
* });
|
|
1522
|
+
* console.log(JSON.stringify(inspection, null, 2));
|
|
1733
1523
|
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
* await context.save();
|
|
1524
|
+
* // Or write to file for analysis
|
|
1525
|
+
* await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
|
|
1737
1526
|
* ```
|
|
1738
1527
|
*/
|
|
1739
|
-
async
|
|
1528
|
+
async inspect(options) {
|
|
1740
1529
|
await this.#ensureInitialized();
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1530
|
+
const { renderer } = options;
|
|
1531
|
+
const estimateResult = await this.estimate(options.modelId, { renderer });
|
|
1532
|
+
const rendered = renderer.render(this.#fragments);
|
|
1533
|
+
const persistedMessages = [];
|
|
1534
|
+
if (this.#branch?.headMessageId) {
|
|
1535
|
+
const chain = await this.#store.getMessageChain(
|
|
1536
|
+
this.#branch.headMessageId
|
|
1537
|
+
);
|
|
1538
|
+
persistedMessages.push(...chain);
|
|
1539
|
+
}
|
|
1540
|
+
const graph = await this.#store.getGraph(this.#chatId);
|
|
1541
|
+
return {
|
|
1542
|
+
estimate: estimateResult,
|
|
1543
|
+
rendered,
|
|
1544
|
+
fragments: {
|
|
1545
|
+
context: [...this.#fragments],
|
|
1546
|
+
pending: [...this.#pendingMessages],
|
|
1547
|
+
persisted: persistedMessages
|
|
1548
|
+
},
|
|
1549
|
+
graph,
|
|
1550
|
+
meta: {
|
|
1551
|
+
chatId: this.#chatId,
|
|
1552
|
+
branch: this.#branchName,
|
|
1553
|
+
timestamp: Date.now()
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
// packages/context/src/lib/fragments/domain.ts
|
|
1560
|
+
function term(name, definition) {
|
|
1561
|
+
return {
|
|
1562
|
+
name: "term",
|
|
1563
|
+
data: { name, definition }
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function hint(text) {
|
|
1567
|
+
return {
|
|
1568
|
+
name: "hint",
|
|
1569
|
+
data: text
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function guardrail(input) {
|
|
1573
|
+
return {
|
|
1574
|
+
name: "guardrail",
|
|
1575
|
+
data: {
|
|
1576
|
+
rule: input.rule,
|
|
1577
|
+
...input.reason && { reason: input.reason },
|
|
1578
|
+
...input.action && { action: input.action }
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function explain(input) {
|
|
1583
|
+
return {
|
|
1584
|
+
name: "explain",
|
|
1585
|
+
data: {
|
|
1586
|
+
concept: input.concept,
|
|
1587
|
+
explanation: input.explanation,
|
|
1588
|
+
...input.therefore && { therefore: input.therefore }
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function example(input) {
|
|
1593
|
+
return {
|
|
1594
|
+
name: "example",
|
|
1595
|
+
data: {
|
|
1596
|
+
question: input.question,
|
|
1597
|
+
answer: input.answer,
|
|
1598
|
+
...input.note && { note: input.note }
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function clarification(input) {
|
|
1603
|
+
return {
|
|
1604
|
+
name: "clarification",
|
|
1605
|
+
data: {
|
|
1606
|
+
when: input.when,
|
|
1607
|
+
ask: input.ask,
|
|
1608
|
+
reason: input.reason
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function workflow(input) {
|
|
1613
|
+
return {
|
|
1614
|
+
name: "workflow",
|
|
1615
|
+
data: {
|
|
1616
|
+
task: input.task,
|
|
1617
|
+
steps: input.steps,
|
|
1618
|
+
...input.triggers?.length && { triggers: input.triggers },
|
|
1619
|
+
...input.notes && { notes: input.notes }
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
function quirk(input) {
|
|
1624
|
+
return {
|
|
1625
|
+
name: "quirk",
|
|
1626
|
+
data: {
|
|
1627
|
+
issue: input.issue,
|
|
1628
|
+
workaround: input.workaround
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
function styleGuide(input) {
|
|
1633
|
+
return {
|
|
1634
|
+
name: "styleGuide",
|
|
1635
|
+
data: {
|
|
1636
|
+
prefer: input.prefer,
|
|
1637
|
+
...input.never && { never: input.never },
|
|
1638
|
+
...input.always && { always: input.always }
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function analogy(input) {
|
|
1643
|
+
return {
|
|
1644
|
+
name: "analogy",
|
|
1645
|
+
data: {
|
|
1646
|
+
concepts: input.concepts,
|
|
1647
|
+
relationship: input.relationship,
|
|
1648
|
+
...input.insight && { insight: input.insight },
|
|
1649
|
+
...input.therefore && { therefore: input.therefore },
|
|
1650
|
+
...input.pitfall && { pitfall: input.pitfall }
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function glossary(entries) {
|
|
1655
|
+
return {
|
|
1656
|
+
name: "glossary",
|
|
1657
|
+
data: Object.entries(entries).map(([term2, expression]) => ({
|
|
1658
|
+
term: term2,
|
|
1659
|
+
expression
|
|
1660
|
+
}))
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function role(content) {
|
|
1664
|
+
return {
|
|
1665
|
+
name: "role",
|
|
1666
|
+
data: content
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function principle(input) {
|
|
1670
|
+
return {
|
|
1671
|
+
name: "principle",
|
|
1672
|
+
data: {
|
|
1673
|
+
title: input.title,
|
|
1674
|
+
description: input.description,
|
|
1675
|
+
...input.policies?.length && { policies: input.policies }
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function policy(input) {
|
|
1680
|
+
return {
|
|
1681
|
+
name: "policy",
|
|
1682
|
+
data: {
|
|
1683
|
+
rule: input.rule,
|
|
1684
|
+
...input.before && { before: input.before },
|
|
1685
|
+
...input.reason && { reason: input.reason },
|
|
1686
|
+
...input.policies?.length && { policies: input.policies }
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// packages/context/src/lib/fragments/user.ts
|
|
1692
|
+
function identity(input) {
|
|
1693
|
+
return {
|
|
1694
|
+
name: "identity",
|
|
1695
|
+
data: {
|
|
1696
|
+
...input.name && { name: input.name },
|
|
1697
|
+
...input.role && { role: input.role }
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function persona(input) {
|
|
1702
|
+
return {
|
|
1703
|
+
name: "persona",
|
|
1704
|
+
data: {
|
|
1705
|
+
name: input.name,
|
|
1706
|
+
...input.role && { role: input.role },
|
|
1707
|
+
...input.objective && { objective: input.objective },
|
|
1708
|
+
...input.tone && { tone: input.tone }
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function alias(term2, meaning) {
|
|
1713
|
+
return {
|
|
1714
|
+
name: "alias",
|
|
1715
|
+
data: { term: term2, meaning }
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function preference(aspect, value) {
|
|
1719
|
+
return {
|
|
1720
|
+
name: "preference",
|
|
1721
|
+
data: { aspect, value }
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function userContext(description) {
|
|
1725
|
+
return {
|
|
1726
|
+
name: "userContext",
|
|
1727
|
+
data: description
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function correction(subject, clarification2) {
|
|
1731
|
+
return {
|
|
1732
|
+
name: "correction",
|
|
1733
|
+
data: { subject, clarification: clarification2 }
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// packages/context/src/lib/guardrail.ts
|
|
1738
|
+
function pass(part) {
|
|
1739
|
+
return { type: "pass", part };
|
|
1740
|
+
}
|
|
1741
|
+
function fail(feedback) {
|
|
1742
|
+
return { type: "fail", feedback };
|
|
1743
|
+
}
|
|
1744
|
+
function runGuardrailChain(part, guardrails, context) {
|
|
1745
|
+
let currentPart = part;
|
|
1746
|
+
for (const guardrail2 of guardrails) {
|
|
1747
|
+
const result = guardrail2.handle(currentPart, context);
|
|
1748
|
+
if (result.type === "fail") {
|
|
1749
|
+
return result;
|
|
1750
|
+
}
|
|
1751
|
+
currentPart = result.part;
|
|
1752
|
+
}
|
|
1753
|
+
return pass(currentPart);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// packages/context/src/lib/guardrails/error-recovery.guardrail.ts
|
|
1757
|
+
import chalk from "chalk";
|
|
1758
|
+
var errorRecoveryGuardrail = {
|
|
1759
|
+
id: "error-recovery",
|
|
1760
|
+
name: "API Error Recovery",
|
|
1761
|
+
handle: (part, context) => {
|
|
1762
|
+
if (part.type !== "error") {
|
|
1763
|
+
return pass(part);
|
|
1764
|
+
}
|
|
1765
|
+
const errorText = part.errorText || "";
|
|
1766
|
+
const prefix = chalk.bold.magenta("[ErrorRecovery]");
|
|
1767
|
+
console.log(
|
|
1768
|
+
`${prefix} ${chalk.red("Caught error:")} ${chalk.dim(errorText.slice(0, 150))}`
|
|
1769
|
+
);
|
|
1770
|
+
const logAndFail = (pattern, feedback) => {
|
|
1771
|
+
console.log(
|
|
1772
|
+
`${prefix} ${chalk.yellow("Pattern:")} ${chalk.cyan(pattern)}`
|
|
1773
|
+
);
|
|
1774
|
+
console.log(
|
|
1775
|
+
`${prefix} ${chalk.green("Feedback:")} ${chalk.dim(feedback.slice(0, 80))}...`
|
|
1776
|
+
);
|
|
1777
|
+
return fail(feedback);
|
|
1778
|
+
};
|
|
1779
|
+
if (errorText.includes("Tool choice is none")) {
|
|
1780
|
+
if (context.availableTools.length > 0) {
|
|
1781
|
+
return logAndFail(
|
|
1782
|
+
"Tool choice is none",
|
|
1783
|
+
`I tried to call a tool that doesn't exist. Available tools: ${context.availableTools.join(", ")}. Let me use one of these instead.`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return logAndFail(
|
|
1787
|
+
"Tool choice is none (no tools)",
|
|
1788
|
+
"I tried to call a tool, but no tools are available. Let me respond with plain text instead."
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
if (errorText.includes("not in request.tools") || errorText.includes("tool") && errorText.includes("not found")) {
|
|
1792
|
+
const toolMatch = errorText.match(/tool '([^']+)'/);
|
|
1793
|
+
const toolName = toolMatch ? toolMatch[1] : "unknown";
|
|
1794
|
+
if (context.availableTools.length > 0) {
|
|
1795
|
+
return logAndFail(
|
|
1796
|
+
`Unregistered tool: ${toolName}`,
|
|
1797
|
+
`I tried to call "${toolName}" but it doesn't exist. Available tools: ${context.availableTools.join(", ")}. Let me use one of these instead.`
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
return logAndFail(
|
|
1801
|
+
`Unregistered tool: ${toolName} (no tools)`,
|
|
1802
|
+
`I tried to call "${toolName}" but no tools are available. Let me respond with plain text instead.`
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
if (errorText.includes("Failed to parse tool call arguments") || errorText.includes("parse tool call") || errorText.includes("invalid JSON")) {
|
|
1806
|
+
return logAndFail(
|
|
1807
|
+
"Malformed JSON arguments",
|
|
1808
|
+
"I generated malformed JSON for the tool arguments. Let me format my tool call properly with valid JSON."
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (errorText.includes("Parsing failed")) {
|
|
1812
|
+
return logAndFail(
|
|
1813
|
+
"Parsing failed",
|
|
1814
|
+
"My response format was invalid. Let me try again with a properly formatted response."
|
|
1745
1815
|
);
|
|
1746
1816
|
}
|
|
1747
|
-
return
|
|
1817
|
+
return logAndFail(
|
|
1818
|
+
"Unknown error",
|
|
1819
|
+
`An error occurred: ${errorText.slice(0, 100)}. Let me try a different approach.`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
// packages/context/src/lib/sandbox/binary-bridges.ts
|
|
1825
|
+
import { existsSync } from "fs";
|
|
1826
|
+
import { defineCommand } from "just-bash";
|
|
1827
|
+
import spawn from "nano-spawn";
|
|
1828
|
+
import * as path from "path";
|
|
1829
|
+
function createBinaryBridges(...binaries) {
|
|
1830
|
+
return binaries.map((input) => {
|
|
1831
|
+
const config = typeof input === "string" ? { name: input } : input;
|
|
1832
|
+
const { name, binaryPath = name, allowedArgs } = config;
|
|
1833
|
+
return defineCommand(name, async (args, ctx) => {
|
|
1834
|
+
if (allowedArgs) {
|
|
1835
|
+
const invalidArg = args.find((arg) => !allowedArgs.test(arg));
|
|
1836
|
+
if (invalidArg) {
|
|
1837
|
+
return {
|
|
1838
|
+
stdout: "",
|
|
1839
|
+
stderr: `${name}: argument '${invalidArg}' not allowed by security policy`,
|
|
1840
|
+
exitCode: 1
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const realCwd = resolveRealCwd(ctx);
|
|
1846
|
+
const resolvedArgs = args.map((arg) => {
|
|
1847
|
+
if (arg.startsWith("-")) {
|
|
1848
|
+
return arg;
|
|
1849
|
+
}
|
|
1850
|
+
const hasExtension = path.extname(arg) !== "";
|
|
1851
|
+
const hasPathSep = arg.includes(path.sep) || arg.includes("/");
|
|
1852
|
+
const isRelative = arg.startsWith(".");
|
|
1853
|
+
if (hasExtension || hasPathSep || isRelative) {
|
|
1854
|
+
return path.resolve(realCwd, arg);
|
|
1855
|
+
}
|
|
1856
|
+
return arg;
|
|
1857
|
+
});
|
|
1858
|
+
const mergedEnv = {
|
|
1859
|
+
...process.env,
|
|
1860
|
+
...ctx.env,
|
|
1861
|
+
PATH: process.env.PATH
|
|
1862
|
+
// Always use host PATH for binary bridges
|
|
1863
|
+
};
|
|
1864
|
+
const result = await spawn(binaryPath, resolvedArgs, {
|
|
1865
|
+
cwd: realCwd,
|
|
1866
|
+
env: mergedEnv
|
|
1867
|
+
});
|
|
1868
|
+
return {
|
|
1869
|
+
stdout: result.stdout,
|
|
1870
|
+
stderr: result.stderr,
|
|
1871
|
+
exitCode: 0
|
|
1872
|
+
};
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if (error && typeof error === "object") {
|
|
1875
|
+
const err = error;
|
|
1876
|
+
const cause = err.cause;
|
|
1877
|
+
if (cause?.code === "ENOENT") {
|
|
1878
|
+
return {
|
|
1879
|
+
stdout: "",
|
|
1880
|
+
stderr: `${name}: ${binaryPath} not found`,
|
|
1881
|
+
exitCode: 127
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (error && typeof error === "object" && "exitCode" in error) {
|
|
1886
|
+
const subprocessError = error;
|
|
1887
|
+
return {
|
|
1888
|
+
stdout: subprocessError.stdout ?? "",
|
|
1889
|
+
stderr: subprocessError.stderr ?? "",
|
|
1890
|
+
exitCode: subprocessError.exitCode ?? 1
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
return {
|
|
1894
|
+
stdout: "",
|
|
1895
|
+
stderr: `${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1896
|
+
exitCode: 127
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
function resolveRealCwd(ctx) {
|
|
1903
|
+
const fs2 = ctx.fs;
|
|
1904
|
+
let realCwd;
|
|
1905
|
+
if (fs2.root) {
|
|
1906
|
+
realCwd = path.join(fs2.root, ctx.cwd);
|
|
1907
|
+
} else if (typeof fs2.getMountPoint === "function" && typeof fs2.toRealPath === "function") {
|
|
1908
|
+
const real = fs2.toRealPath(ctx.cwd);
|
|
1909
|
+
realCwd = real ?? process.cwd();
|
|
1910
|
+
} else {
|
|
1911
|
+
realCwd = process.cwd();
|
|
1912
|
+
}
|
|
1913
|
+
if (!existsSync(realCwd)) {
|
|
1914
|
+
realCwd = process.cwd();
|
|
1915
|
+
}
|
|
1916
|
+
return realCwd;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// packages/context/src/lib/sandbox/docker-sandbox.ts
|
|
1920
|
+
import "bash-tool";
|
|
1921
|
+
import spawn2 from "nano-spawn";
|
|
1922
|
+
import { createHash } from "node:crypto";
|
|
1923
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1924
|
+
var DockerSandboxError = class extends Error {
|
|
1925
|
+
containerId;
|
|
1926
|
+
constructor(message2, containerId) {
|
|
1927
|
+
super(message2);
|
|
1928
|
+
this.name = "DockerSandboxError";
|
|
1929
|
+
this.containerId = containerId;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
var DockerNotAvailableError = class extends DockerSandboxError {
|
|
1933
|
+
constructor() {
|
|
1934
|
+
super("Docker is not available. Ensure Docker daemon is running.");
|
|
1935
|
+
this.name = "DockerNotAvailableError";
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
var ContainerCreationError = class extends DockerSandboxError {
|
|
1939
|
+
image;
|
|
1940
|
+
cause;
|
|
1941
|
+
constructor(message2, image, cause) {
|
|
1942
|
+
super(`Failed to create container from image "${image}": ${message2}`);
|
|
1943
|
+
this.name = "ContainerCreationError";
|
|
1944
|
+
this.image = image;
|
|
1945
|
+
this.cause = cause;
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
var PackageInstallError = class extends DockerSandboxError {
|
|
1949
|
+
packages;
|
|
1950
|
+
image;
|
|
1951
|
+
packageManager;
|
|
1952
|
+
stderr;
|
|
1953
|
+
constructor(packages, image, packageManager, stderr, containerId) {
|
|
1954
|
+
super(
|
|
1955
|
+
`Package installation failed for [${packages.join(", ")}] using ${packageManager} on ${image}: ${stderr}`,
|
|
1956
|
+
containerId
|
|
1957
|
+
);
|
|
1958
|
+
this.name = "PackageInstallError";
|
|
1959
|
+
this.packages = packages;
|
|
1960
|
+
this.image = image;
|
|
1961
|
+
this.packageManager = packageManager;
|
|
1962
|
+
this.stderr = stderr;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
var BinaryInstallError = class extends DockerSandboxError {
|
|
1966
|
+
binaryName;
|
|
1967
|
+
url;
|
|
1968
|
+
reason;
|
|
1969
|
+
constructor(binaryName, url, reason, containerId) {
|
|
1970
|
+
super(
|
|
1971
|
+
`Failed to install binary "${binaryName}" from ${url}: ${reason}`,
|
|
1972
|
+
containerId
|
|
1973
|
+
);
|
|
1974
|
+
this.name = "BinaryInstallError";
|
|
1975
|
+
this.binaryName = binaryName;
|
|
1976
|
+
this.url = url;
|
|
1977
|
+
this.reason = reason;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
var MountPathError = class extends DockerSandboxError {
|
|
1981
|
+
hostPath;
|
|
1982
|
+
containerPath;
|
|
1983
|
+
constructor(hostPath, containerPath) {
|
|
1984
|
+
super(
|
|
1985
|
+
`Mount path does not exist on host: "${hostPath}" -> "${containerPath}"`
|
|
1986
|
+
);
|
|
1987
|
+
this.name = "MountPathError";
|
|
1988
|
+
this.hostPath = hostPath;
|
|
1989
|
+
this.containerPath = containerPath;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
var DockerfileBuildError = class extends DockerSandboxError {
|
|
1993
|
+
stderr;
|
|
1994
|
+
constructor(stderr) {
|
|
1995
|
+
super(`Dockerfile build failed: ${stderr}`);
|
|
1996
|
+
this.name = "DockerfileBuildError";
|
|
1997
|
+
this.stderr = stderr;
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
var ComposeStartError = class extends DockerSandboxError {
|
|
2001
|
+
composeFile;
|
|
2002
|
+
stderr;
|
|
2003
|
+
constructor(composeFile, stderr) {
|
|
2004
|
+
super(`Docker Compose failed to start: ${stderr}`);
|
|
2005
|
+
this.name = "ComposeStartError";
|
|
2006
|
+
this.composeFile = composeFile;
|
|
2007
|
+
this.stderr = stderr;
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
function isDebianBased(image) {
|
|
2011
|
+
const debianPatterns = ["debian", "ubuntu", "node", "python"];
|
|
2012
|
+
return debianPatterns.some(
|
|
2013
|
+
(pattern) => image.toLowerCase().includes(pattern)
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
function isDockerfileOptions(opts) {
|
|
2017
|
+
return "dockerfile" in opts;
|
|
2018
|
+
}
|
|
2019
|
+
function isComposeOptions(opts) {
|
|
2020
|
+
return "compose" in opts;
|
|
2021
|
+
}
|
|
2022
|
+
var DockerSandboxStrategy = class {
|
|
2023
|
+
context;
|
|
2024
|
+
mounts;
|
|
2025
|
+
resources;
|
|
2026
|
+
constructor(mounts = [], resources = {}) {
|
|
2027
|
+
this.mounts = mounts;
|
|
2028
|
+
this.resources = resources;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Template method - defines the algorithm skeleton for creating a sandbox.
|
|
2032
|
+
*
|
|
2033
|
+
* Steps:
|
|
2034
|
+
* 1. Validate mount paths exist on host
|
|
2035
|
+
* 2. Get/build the Docker image (strategy-specific)
|
|
2036
|
+
* 3. Start the container
|
|
2037
|
+
* 4. Configure the container (strategy-specific)
|
|
2038
|
+
* 5. Create and return sandbox methods
|
|
2039
|
+
*/
|
|
2040
|
+
async create() {
|
|
2041
|
+
this.validateMounts();
|
|
2042
|
+
const image = await this.getImage();
|
|
2043
|
+
const containerId = await this.startContainer(image);
|
|
2044
|
+
this.context = { containerId, image };
|
|
2045
|
+
try {
|
|
2046
|
+
await this.configure();
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
await this.stopContainer(containerId);
|
|
2049
|
+
throw error;
|
|
2050
|
+
}
|
|
2051
|
+
return this.createSandboxMethods();
|
|
2052
|
+
}
|
|
2053
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2054
|
+
// Common implementations (shared by all strategies)
|
|
2055
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2056
|
+
/**
|
|
2057
|
+
* Validates that all mount paths exist on the host filesystem.
|
|
2058
|
+
*/
|
|
2059
|
+
validateMounts() {
|
|
2060
|
+
for (const mount of this.mounts) {
|
|
2061
|
+
if (!existsSync2(mount.hostPath)) {
|
|
2062
|
+
throw new MountPathError(mount.hostPath, mount.containerPath);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Builds the docker run command arguments.
|
|
2068
|
+
*/
|
|
2069
|
+
buildDockerArgs(image, containerId) {
|
|
2070
|
+
const { memory = "1g", cpus = 2 } = this.resources;
|
|
2071
|
+
const args = [
|
|
2072
|
+
"run",
|
|
2073
|
+
"-d",
|
|
2074
|
+
// Detached mode
|
|
2075
|
+
"--rm",
|
|
2076
|
+
// Remove container when stopped
|
|
2077
|
+
"--name",
|
|
2078
|
+
containerId,
|
|
2079
|
+
`--memory=${memory}`,
|
|
2080
|
+
`--cpus=${cpus}`,
|
|
2081
|
+
"-w",
|
|
2082
|
+
"/workspace"
|
|
2083
|
+
// Set working directory
|
|
2084
|
+
];
|
|
2085
|
+
for (const mount of this.mounts) {
|
|
2086
|
+
const mode = mount.readOnly !== false ? "ro" : "rw";
|
|
2087
|
+
args.push("-v", `${mount.hostPath}:${mount.containerPath}:${mode}`);
|
|
2088
|
+
}
|
|
2089
|
+
args.push(image, "tail", "-f", "/dev/null");
|
|
2090
|
+
return args;
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Starts a Docker container with the given image.
|
|
2094
|
+
*/
|
|
2095
|
+
async startContainer(image) {
|
|
2096
|
+
const containerId = `sandbox-${crypto.randomUUID().slice(0, 8)}`;
|
|
2097
|
+
const args = this.buildDockerArgs(image, containerId);
|
|
2098
|
+
try {
|
|
2099
|
+
await spawn2("docker", args);
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
const err = error;
|
|
2102
|
+
if (err.message?.includes("Cannot connect") || err.message?.includes("docker daemon") || err.stderr?.includes("Cannot connect")) {
|
|
2103
|
+
throw new DockerNotAvailableError();
|
|
2104
|
+
}
|
|
2105
|
+
throw new ContainerCreationError(err.message || String(err), image, err);
|
|
2106
|
+
}
|
|
2107
|
+
return containerId;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Stops a Docker container.
|
|
2111
|
+
*/
|
|
2112
|
+
async stopContainer(containerId) {
|
|
2113
|
+
try {
|
|
2114
|
+
await spawn2("docker", ["stop", containerId]);
|
|
2115
|
+
} catch {
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Executes a command in the container.
|
|
2120
|
+
*/
|
|
2121
|
+
async exec(command) {
|
|
2122
|
+
try {
|
|
2123
|
+
const result = await spawn2("docker", [
|
|
2124
|
+
"exec",
|
|
2125
|
+
this.context.containerId,
|
|
2126
|
+
"sh",
|
|
2127
|
+
"-c",
|
|
2128
|
+
command
|
|
2129
|
+
]);
|
|
2130
|
+
return {
|
|
2131
|
+
stdout: result.stdout,
|
|
2132
|
+
stderr: result.stderr,
|
|
2133
|
+
exitCode: 0
|
|
2134
|
+
};
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
const err = error;
|
|
2137
|
+
return {
|
|
2138
|
+
stdout: err.stdout || "",
|
|
2139
|
+
stderr: err.stderr || err.message || "",
|
|
2140
|
+
exitCode: err.exitCode ?? 1
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Creates the DockerSandbox interface with all methods.
|
|
2146
|
+
*/
|
|
2147
|
+
createSandboxMethods() {
|
|
2148
|
+
const { containerId } = this.context;
|
|
2149
|
+
const sandbox = {
|
|
2150
|
+
executeCommand: async (command) => {
|
|
2151
|
+
return this.exec(command);
|
|
2152
|
+
},
|
|
2153
|
+
readFile: async (path3) => {
|
|
2154
|
+
const result = await sandbox.executeCommand(`base64 "${path3}"`);
|
|
2155
|
+
if (result.exitCode !== 0) {
|
|
2156
|
+
throw new Error(`Failed to read file "${path3}": ${result.stderr}`);
|
|
2157
|
+
}
|
|
2158
|
+
return Buffer.from(result.stdout, "base64").toString("utf-8");
|
|
2159
|
+
},
|
|
2160
|
+
writeFiles: async (files) => {
|
|
2161
|
+
for (const file of files) {
|
|
2162
|
+
const dir = file.path.substring(0, file.path.lastIndexOf("/"));
|
|
2163
|
+
if (dir) {
|
|
2164
|
+
await sandbox.executeCommand(`mkdir -p "${dir}"`);
|
|
2165
|
+
}
|
|
2166
|
+
const base64Content = Buffer.from(file.content).toString("base64");
|
|
2167
|
+
const result = await sandbox.executeCommand(
|
|
2168
|
+
`echo "${base64Content}" | base64 -d > "${file.path}"`
|
|
2169
|
+
);
|
|
2170
|
+
if (result.exitCode !== 0) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`Failed to write file "${file.path}": ${result.stderr}`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
},
|
|
2177
|
+
dispose: async () => {
|
|
2178
|
+
await this.stopContainer(containerId);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
return sandbox;
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
var RuntimeStrategy = class extends DockerSandboxStrategy {
|
|
2185
|
+
image;
|
|
2186
|
+
packages;
|
|
2187
|
+
binaries;
|
|
2188
|
+
constructor(image = "alpine:latest", packages = [], binaries = [], mounts, resources) {
|
|
2189
|
+
super(mounts, resources);
|
|
2190
|
+
this.image = image;
|
|
2191
|
+
this.packages = packages;
|
|
2192
|
+
this.binaries = binaries;
|
|
2193
|
+
}
|
|
2194
|
+
async getImage() {
|
|
2195
|
+
return this.image;
|
|
2196
|
+
}
|
|
2197
|
+
async configure() {
|
|
2198
|
+
await this.installPackages();
|
|
2199
|
+
await this.installBinaries();
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Installs packages using the appropriate package manager (apk/apt-get).
|
|
2203
|
+
*/
|
|
2204
|
+
async installPackages() {
|
|
2205
|
+
if (this.packages.length === 0) return;
|
|
2206
|
+
const useApt = isDebianBased(this.image);
|
|
2207
|
+
const installCmd = useApt ? `apt-get update && apt-get install -y ${this.packages.join(" ")}` : `apk add --no-cache ${this.packages.join(" ")}`;
|
|
2208
|
+
try {
|
|
2209
|
+
await spawn2("docker", [
|
|
2210
|
+
"exec",
|
|
2211
|
+
this.context.containerId,
|
|
2212
|
+
"sh",
|
|
2213
|
+
"-c",
|
|
2214
|
+
installCmd
|
|
2215
|
+
]);
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
const err = error;
|
|
2218
|
+
throw new PackageInstallError(
|
|
2219
|
+
this.packages,
|
|
2220
|
+
this.image,
|
|
2221
|
+
useApt ? "apt-get" : "apk",
|
|
2222
|
+
err.stderr || err.message,
|
|
2223
|
+
this.context.containerId
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Installs binaries from URLs.
|
|
2229
|
+
*/
|
|
2230
|
+
async installBinaries() {
|
|
2231
|
+
if (this.binaries.length === 0) return;
|
|
2232
|
+
await this.ensureCurl();
|
|
2233
|
+
const arch = await this.detectArchitecture();
|
|
2234
|
+
for (const binary of this.binaries) {
|
|
2235
|
+
await this.installBinary(binary, arch);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Ensures curl is installed in the container.
|
|
2240
|
+
*/
|
|
2241
|
+
async ensureCurl() {
|
|
2242
|
+
const checkResult = await spawn2("docker", [
|
|
2243
|
+
"exec",
|
|
2244
|
+
this.context.containerId,
|
|
2245
|
+
"which",
|
|
2246
|
+
"curl"
|
|
2247
|
+
]).catch(() => null);
|
|
2248
|
+
if (checkResult) return;
|
|
2249
|
+
const useApt = isDebianBased(this.image);
|
|
2250
|
+
const curlInstallCmd = useApt ? "apt-get update && apt-get install -y curl" : "apk add --no-cache curl";
|
|
2251
|
+
try {
|
|
2252
|
+
await spawn2("docker", [
|
|
2253
|
+
"exec",
|
|
2254
|
+
this.context.containerId,
|
|
2255
|
+
"sh",
|
|
2256
|
+
"-c",
|
|
2257
|
+
curlInstallCmd
|
|
2258
|
+
]);
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
const err = error;
|
|
2261
|
+
throw new BinaryInstallError(
|
|
2262
|
+
"curl",
|
|
2263
|
+
"package-manager",
|
|
2264
|
+
`Required for binary downloads: ${err.stderr || err.message}`,
|
|
2265
|
+
this.context.containerId
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Detects the container's CPU architecture.
|
|
2271
|
+
*/
|
|
2272
|
+
async detectArchitecture() {
|
|
2273
|
+
try {
|
|
2274
|
+
const result = await spawn2("docker", [
|
|
2275
|
+
"exec",
|
|
2276
|
+
this.context.containerId,
|
|
2277
|
+
"uname",
|
|
2278
|
+
"-m"
|
|
2279
|
+
]);
|
|
2280
|
+
return result.stdout.trim();
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
const err = error;
|
|
2283
|
+
throw new DockerSandboxError(
|
|
2284
|
+
`Failed to detect container architecture: ${err.stderr || err.message}`,
|
|
2285
|
+
this.context.containerId
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Installs a single binary from URL.
|
|
2291
|
+
*/
|
|
2292
|
+
async installBinary(binary, arch) {
|
|
2293
|
+
let url;
|
|
2294
|
+
if (typeof binary.url === "string") {
|
|
2295
|
+
url = binary.url;
|
|
2296
|
+
} else {
|
|
2297
|
+
const archUrl = binary.url[arch];
|
|
2298
|
+
if (!archUrl) {
|
|
2299
|
+
throw new BinaryInstallError(
|
|
2300
|
+
binary.name,
|
|
2301
|
+
`arch:${arch}`,
|
|
2302
|
+
`No URL provided for architecture "${arch}". Available: ${Object.keys(binary.url).join(", ")}`,
|
|
2303
|
+
this.context.containerId
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
url = archUrl;
|
|
2307
|
+
}
|
|
2308
|
+
const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
|
|
2309
|
+
let installCmd;
|
|
2310
|
+
if (isTarGz) {
|
|
2311
|
+
const binaryPathInArchive = binary.binaryPath || binary.name;
|
|
2312
|
+
installCmd = `
|
|
2313
|
+
set -e
|
|
2314
|
+
TMPDIR=$(mktemp -d)
|
|
2315
|
+
cd "$TMPDIR"
|
|
2316
|
+
curl -fsSL "${url}" -o archive.tar.gz
|
|
2317
|
+
tar -xzf archive.tar.gz
|
|
2318
|
+
BINARY_FILE=$(find . -name "${binaryPathInArchive}" -o -name "${binary.name}" | head -1)
|
|
2319
|
+
if [ -z "$BINARY_FILE" ]; then
|
|
2320
|
+
echo "Binary not found in archive. Contents:" >&2
|
|
2321
|
+
find . -type f >&2
|
|
2322
|
+
exit 1
|
|
2323
|
+
fi
|
|
2324
|
+
chmod +x "$BINARY_FILE"
|
|
2325
|
+
mv "$BINARY_FILE" /usr/local/bin/${binary.name}
|
|
2326
|
+
cd /
|
|
2327
|
+
rm -rf "$TMPDIR"
|
|
2328
|
+
`;
|
|
2329
|
+
} else {
|
|
2330
|
+
installCmd = `
|
|
2331
|
+
curl -fsSL "${url}" -o /usr/local/bin/${binary.name}
|
|
2332
|
+
chmod +x /usr/local/bin/${binary.name}
|
|
2333
|
+
`;
|
|
2334
|
+
}
|
|
2335
|
+
try {
|
|
2336
|
+
await spawn2("docker", [
|
|
2337
|
+
"exec",
|
|
2338
|
+
this.context.containerId,
|
|
2339
|
+
"sh",
|
|
2340
|
+
"-c",
|
|
2341
|
+
installCmd
|
|
2342
|
+
]);
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
const err = error;
|
|
2345
|
+
throw new BinaryInstallError(
|
|
2346
|
+
binary.name,
|
|
2347
|
+
url,
|
|
2348
|
+
err.stderr || err.message,
|
|
2349
|
+
this.context.containerId
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
var DockerfileStrategy = class extends DockerSandboxStrategy {
|
|
2355
|
+
imageTag;
|
|
2356
|
+
dockerfile;
|
|
2357
|
+
dockerContext;
|
|
2358
|
+
constructor(dockerfile, dockerContext = ".", mounts, resources) {
|
|
2359
|
+
super(mounts, resources);
|
|
2360
|
+
this.dockerfile = dockerfile;
|
|
2361
|
+
this.dockerContext = dockerContext;
|
|
2362
|
+
this.imageTag = this.computeImageTag();
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Computes a deterministic image tag based on Dockerfile content.
|
|
2366
|
+
* Same Dockerfile → same tag → Docker skips rebuild if image exists.
|
|
2367
|
+
*/
|
|
2368
|
+
computeImageTag() {
|
|
2369
|
+
const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync(this.dockerfile, "utf-8");
|
|
2370
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
2371
|
+
return `sandbox-${hash}`;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Checks if the dockerfile property is inline content or a file path.
|
|
2375
|
+
*/
|
|
2376
|
+
isInlineDockerfile() {
|
|
2377
|
+
return this.dockerfile.includes("\n");
|
|
2378
|
+
}
|
|
2379
|
+
async getImage() {
|
|
2380
|
+
const exists = await this.imageExists();
|
|
2381
|
+
if (!exists) {
|
|
2382
|
+
await this.buildImage();
|
|
2383
|
+
}
|
|
2384
|
+
return this.imageTag;
|
|
2385
|
+
}
|
|
2386
|
+
async configure() {
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Checks if the image already exists locally.
|
|
2390
|
+
*/
|
|
2391
|
+
async imageExists() {
|
|
2392
|
+
try {
|
|
2393
|
+
await spawn2("docker", ["image", "inspect", this.imageTag]);
|
|
2394
|
+
return true;
|
|
2395
|
+
} catch {
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Builds the Docker image from the Dockerfile.
|
|
2401
|
+
*/
|
|
2402
|
+
async buildImage() {
|
|
2403
|
+
try {
|
|
2404
|
+
if (this.isInlineDockerfile()) {
|
|
2405
|
+
const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
|
|
2406
|
+
await spawn2("sh", ["-c", buildCmd]);
|
|
2407
|
+
} else {
|
|
2408
|
+
await spawn2("docker", [
|
|
2409
|
+
"build",
|
|
2410
|
+
"-t",
|
|
2411
|
+
this.imageTag,
|
|
2412
|
+
"-f",
|
|
2413
|
+
this.dockerfile,
|
|
2414
|
+
this.dockerContext
|
|
2415
|
+
]);
|
|
2416
|
+
}
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
const err = error;
|
|
2419
|
+
throw new DockerfileBuildError(err.stderr || err.message);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
var ComposeStrategy = class extends DockerSandboxStrategy {
|
|
2424
|
+
projectName;
|
|
2425
|
+
composeFile;
|
|
2426
|
+
service;
|
|
2427
|
+
constructor(composeFile, service, resources) {
|
|
2428
|
+
super([], resources);
|
|
2429
|
+
this.composeFile = composeFile;
|
|
2430
|
+
this.service = service;
|
|
2431
|
+
this.projectName = this.computeProjectName();
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Deterministic project name based on compose file content for caching.
|
|
2435
|
+
* Same compose file → same project name → faster subsequent startups.
|
|
2436
|
+
*/
|
|
2437
|
+
computeProjectName() {
|
|
2438
|
+
const content = readFileSync(this.composeFile, "utf-8");
|
|
2439
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
2440
|
+
return `sandbox-${hash}`;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Override: No image to get - compose manages its own images.
|
|
2444
|
+
*/
|
|
2445
|
+
async getImage() {
|
|
2446
|
+
return "";
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Override: Start all services with docker compose up.
|
|
2450
|
+
*/
|
|
2451
|
+
async startContainer(_image) {
|
|
2452
|
+
try {
|
|
2453
|
+
await spawn2("docker", [
|
|
2454
|
+
"compose",
|
|
2455
|
+
"-f",
|
|
2456
|
+
this.composeFile,
|
|
2457
|
+
"-p",
|
|
2458
|
+
this.projectName,
|
|
2459
|
+
"up",
|
|
2460
|
+
"-d"
|
|
2461
|
+
]);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
const err = error;
|
|
2464
|
+
if (err.stderr?.includes("Cannot connect")) {
|
|
2465
|
+
throw new DockerNotAvailableError();
|
|
2466
|
+
}
|
|
2467
|
+
throw new ComposeStartError(this.composeFile, err.stderr || err.message);
|
|
2468
|
+
}
|
|
2469
|
+
return this.projectName;
|
|
2470
|
+
}
|
|
2471
|
+
async configure() {
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Override: Execute commands in the target service.
|
|
2475
|
+
*/
|
|
2476
|
+
async exec(command) {
|
|
2477
|
+
try {
|
|
2478
|
+
const result = await spawn2("docker", [
|
|
2479
|
+
"compose",
|
|
2480
|
+
"-f",
|
|
2481
|
+
this.composeFile,
|
|
2482
|
+
"-p",
|
|
2483
|
+
this.projectName,
|
|
2484
|
+
"exec",
|
|
2485
|
+
"-T",
|
|
2486
|
+
// -T disables pseudo-TTY
|
|
2487
|
+
this.service,
|
|
2488
|
+
"sh",
|
|
2489
|
+
"-c",
|
|
2490
|
+
command
|
|
2491
|
+
]);
|
|
2492
|
+
return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
const err = error;
|
|
2495
|
+
return {
|
|
2496
|
+
stdout: err.stdout || "",
|
|
2497
|
+
stderr: err.stderr || err.message || "",
|
|
2498
|
+
exitCode: err.exitCode ?? 1
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Override: Stop all services with docker compose down.
|
|
2504
|
+
*/
|
|
2505
|
+
async stopContainer(_containerId) {
|
|
2506
|
+
try {
|
|
2507
|
+
await spawn2("docker", [
|
|
2508
|
+
"compose",
|
|
2509
|
+
"-f",
|
|
2510
|
+
this.composeFile,
|
|
2511
|
+
"-p",
|
|
2512
|
+
this.projectName,
|
|
2513
|
+
"down"
|
|
2514
|
+
]);
|
|
2515
|
+
} catch {
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
};
|
|
2519
|
+
async function createDockerSandbox(options = {}) {
|
|
2520
|
+
let strategy;
|
|
2521
|
+
if (isComposeOptions(options)) {
|
|
2522
|
+
strategy = new ComposeStrategy(
|
|
2523
|
+
options.compose,
|
|
2524
|
+
options.service,
|
|
2525
|
+
options.resources
|
|
2526
|
+
);
|
|
2527
|
+
} else if (isDockerfileOptions(options)) {
|
|
2528
|
+
strategy = new DockerfileStrategy(
|
|
2529
|
+
options.dockerfile,
|
|
2530
|
+
options.context,
|
|
2531
|
+
options.mounts,
|
|
2532
|
+
options.resources
|
|
2533
|
+
);
|
|
2534
|
+
} else {
|
|
2535
|
+
strategy = new RuntimeStrategy(
|
|
2536
|
+
options.image,
|
|
2537
|
+
options.packages,
|
|
2538
|
+
options.binaries,
|
|
2539
|
+
options.mounts,
|
|
2540
|
+
options.resources
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
return strategy.create();
|
|
2544
|
+
}
|
|
2545
|
+
async function useSandbox(options, fn) {
|
|
2546
|
+
const sandbox = await createDockerSandbox(options);
|
|
2547
|
+
try {
|
|
2548
|
+
return await fn(sandbox);
|
|
2549
|
+
} finally {
|
|
2550
|
+
await sandbox.dispose();
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// packages/context/src/lib/sandbox/container-tool.ts
|
|
2555
|
+
import {
|
|
2556
|
+
createBashTool
|
|
2557
|
+
} from "bash-tool";
|
|
2558
|
+
async function createContainerTool(options = {}) {
|
|
2559
|
+
let sandboxOptions;
|
|
2560
|
+
let bashOptions;
|
|
2561
|
+
if (isComposeOptions(options)) {
|
|
2562
|
+
const { compose, service, resources, ...rest } = options;
|
|
2563
|
+
sandboxOptions = { compose, service, resources };
|
|
2564
|
+
bashOptions = rest;
|
|
2565
|
+
} else if (isDockerfileOptions(options)) {
|
|
2566
|
+
const { dockerfile, context, mounts, resources, ...rest } = options;
|
|
2567
|
+
sandboxOptions = { dockerfile, context, mounts, resources };
|
|
2568
|
+
bashOptions = rest;
|
|
2569
|
+
} else {
|
|
2570
|
+
const { image, packages, binaries, mounts, resources, ...rest } = options;
|
|
2571
|
+
sandboxOptions = { image, packages, binaries, mounts, resources };
|
|
2572
|
+
bashOptions = rest;
|
|
2573
|
+
}
|
|
2574
|
+
const sandbox = await createDockerSandbox(sandboxOptions);
|
|
2575
|
+
const toolkit = await createBashTool({
|
|
2576
|
+
...bashOptions,
|
|
2577
|
+
sandbox
|
|
2578
|
+
});
|
|
2579
|
+
return {
|
|
2580
|
+
bash: toolkit.bash,
|
|
2581
|
+
tools: toolkit.tools,
|
|
2582
|
+
sandbox
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// packages/context/src/lib/skills/loader.ts
|
|
2587
|
+
import * as fs from "node:fs";
|
|
2588
|
+
import * as path2 from "node:path";
|
|
2589
|
+
import YAML from "yaml";
|
|
2590
|
+
function parseFrontmatter(content) {
|
|
2591
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
2592
|
+
const match = content.match(frontmatterRegex);
|
|
2593
|
+
if (!match) {
|
|
2594
|
+
throw new Error("Invalid SKILL.md: missing or malformed frontmatter");
|
|
2595
|
+
}
|
|
2596
|
+
const [, yamlContent, body] = match;
|
|
2597
|
+
const frontmatter = YAML.parse(yamlContent);
|
|
2598
|
+
if (!frontmatter.name || typeof frontmatter.name !== "string") {
|
|
2599
|
+
throw new Error('Invalid SKILL.md: frontmatter must have a "name" field');
|
|
2600
|
+
}
|
|
2601
|
+
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
|
2602
|
+
throw new Error(
|
|
2603
|
+
'Invalid SKILL.md: frontmatter must have a "description" field'
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
return {
|
|
2607
|
+
frontmatter,
|
|
2608
|
+
body: body.trim()
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
function loadSkillMetadata(skillMdPath) {
|
|
2612
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
2613
|
+
const parsed = parseFrontmatter(content);
|
|
2614
|
+
const skillDir = path2.dirname(skillMdPath);
|
|
2615
|
+
return {
|
|
2616
|
+
name: parsed.frontmatter.name,
|
|
2617
|
+
description: parsed.frontmatter.description,
|
|
2618
|
+
path: skillDir,
|
|
2619
|
+
skillMdPath
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
function discoverSkillsInDirectory(directory) {
|
|
2623
|
+
const skills2 = [];
|
|
2624
|
+
const expandedDir = directory.startsWith("~") ? path2.join(process.env.HOME || "", directory.slice(1)) : directory;
|
|
2625
|
+
if (!fs.existsSync(expandedDir)) {
|
|
2626
|
+
return skills2;
|
|
2627
|
+
}
|
|
2628
|
+
const entries = fs.readdirSync(expandedDir, { withFileTypes: true });
|
|
2629
|
+
for (const entry of entries) {
|
|
2630
|
+
if (!entry.isDirectory()) continue;
|
|
2631
|
+
const skillMdPath = path2.join(expandedDir, entry.name, "SKILL.md");
|
|
2632
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
2633
|
+
try {
|
|
2634
|
+
const metadata = loadSkillMetadata(skillMdPath);
|
|
2635
|
+
skills2.push(metadata);
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
console.warn(`Warning: Failed to load skill at ${skillMdPath}:`, error);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return skills2;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// packages/context/src/lib/skills/fragments.ts
|
|
2644
|
+
function skills(options) {
|
|
2645
|
+
const skillsMap = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const dir of options.paths) {
|
|
2647
|
+
const discovered = discoverSkillsInDirectory(dir);
|
|
2648
|
+
for (const skill of discovered) {
|
|
2649
|
+
skillsMap.set(skill.name, skill);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const allSkills = Array.from(skillsMap.values());
|
|
2653
|
+
let filteredSkills = allSkills;
|
|
2654
|
+
if (options.include) {
|
|
2655
|
+
filteredSkills = allSkills.filter((s) => options.include.includes(s.name));
|
|
2656
|
+
}
|
|
2657
|
+
if (options.exclude) {
|
|
2658
|
+
filteredSkills = filteredSkills.filter(
|
|
2659
|
+
(s) => !options.exclude.includes(s.name)
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
const skillFragments = filteredSkills.map((skill) => ({
|
|
2663
|
+
name: "skill",
|
|
2664
|
+
data: {
|
|
2665
|
+
name: skill.name,
|
|
2666
|
+
path: skill.skillMdPath,
|
|
2667
|
+
description: skill.description
|
|
2668
|
+
}
|
|
2669
|
+
}));
|
|
2670
|
+
return {
|
|
2671
|
+
name: "available_skills",
|
|
2672
|
+
data: [
|
|
2673
|
+
{
|
|
2674
|
+
name: "instructions",
|
|
2675
|
+
data: SKILLS_INSTRUCTIONS
|
|
2676
|
+
},
|
|
2677
|
+
...skillFragments
|
|
2678
|
+
]
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
var SKILLS_INSTRUCTIONS = `When a user's request matches one of the skills listed below, read the skill's SKILL.md file to get detailed instructions before proceeding. Skills provide specialized knowledge and workflows for specific tasks.
|
|
2682
|
+
|
|
2683
|
+
To use a skill:
|
|
2684
|
+
1. Identify if the user's request matches a skill's description
|
|
2685
|
+
2. Read the SKILL.md file at the skill's path to load full instructions
|
|
2686
|
+
3. Follow the skill's guidance to complete the task
|
|
2687
|
+
|
|
2688
|
+
Skills are only loaded when relevant - don't read skill files unless needed.`;
|
|
2689
|
+
|
|
2690
|
+
// packages/context/src/lib/store/sqlite.store.ts
|
|
2691
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2692
|
+
var STORE_DDL = `
|
|
2693
|
+
-- Chats table
|
|
2694
|
+
-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
|
|
2695
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
2696
|
+
id TEXT PRIMARY KEY,
|
|
2697
|
+
userId TEXT NOT NULL,
|
|
2698
|
+
title TEXT,
|
|
2699
|
+
metadata TEXT,
|
|
2700
|
+
createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
2701
|
+
updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
2702
|
+
);
|
|
2703
|
+
|
|
2704
|
+
CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
|
|
2705
|
+
CREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);
|
|
2706
|
+
|
|
2707
|
+
-- Messages table (nodes in the DAG)
|
|
2708
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
2709
|
+
id TEXT PRIMARY KEY,
|
|
2710
|
+
chatId TEXT NOT NULL,
|
|
2711
|
+
parentId TEXT,
|
|
2712
|
+
name TEXT NOT NULL,
|
|
2713
|
+
type TEXT,
|
|
2714
|
+
data TEXT NOT NULL,
|
|
2715
|
+
createdAt INTEGER NOT NULL,
|
|
2716
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2717
|
+
FOREIGN KEY (parentId) REFERENCES messages(id)
|
|
2718
|
+
);
|
|
2719
|
+
|
|
2720
|
+
CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
|
|
2721
|
+
CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
|
|
2722
|
+
|
|
2723
|
+
-- Branches table (pointers to head messages)
|
|
2724
|
+
CREATE TABLE IF NOT EXISTS branches (
|
|
2725
|
+
id TEXT PRIMARY KEY,
|
|
2726
|
+
chatId TEXT NOT NULL,
|
|
2727
|
+
name TEXT NOT NULL,
|
|
2728
|
+
headMessageId TEXT,
|
|
2729
|
+
isActive INTEGER NOT NULL DEFAULT 0,
|
|
2730
|
+
createdAt INTEGER NOT NULL,
|
|
2731
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2732
|
+
FOREIGN KEY (headMessageId) REFERENCES messages(id),
|
|
2733
|
+
UNIQUE(chatId, name)
|
|
2734
|
+
);
|
|
2735
|
+
|
|
2736
|
+
CREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);
|
|
2737
|
+
|
|
2738
|
+
-- Checkpoints table (pointers to message nodes)
|
|
2739
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
2740
|
+
id TEXT PRIMARY KEY,
|
|
2741
|
+
chatId TEXT NOT NULL,
|
|
2742
|
+
name TEXT NOT NULL,
|
|
2743
|
+
messageId TEXT NOT NULL,
|
|
2744
|
+
createdAt INTEGER NOT NULL,
|
|
2745
|
+
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2746
|
+
FOREIGN KEY (messageId) REFERENCES messages(id),
|
|
2747
|
+
UNIQUE(chatId, name)
|
|
2748
|
+
);
|
|
2749
|
+
|
|
2750
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
|
|
2751
|
+
|
|
2752
|
+
-- FTS5 virtual table for full-text search
|
|
2753
|
+
-- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
|
|
2754
|
+
-- Only 'content' is indexed for full-text search
|
|
2755
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
2756
|
+
messageId UNINDEXED,
|
|
2757
|
+
chatId UNINDEXED,
|
|
2758
|
+
name UNINDEXED,
|
|
2759
|
+
content,
|
|
2760
|
+
tokenize='porter unicode61'
|
|
2761
|
+
);
|
|
2762
|
+
`;
|
|
2763
|
+
var SqliteContextStore = class extends ContextStore {
|
|
2764
|
+
#db;
|
|
2765
|
+
constructor(path3) {
|
|
2766
|
+
super();
|
|
2767
|
+
this.#db = new DatabaseSync(path3);
|
|
2768
|
+
this.#db.exec("PRAGMA foreign_keys = ON");
|
|
2769
|
+
this.#db.exec(STORE_DDL);
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Execute a function within a transaction.
|
|
2773
|
+
* Automatically commits on success or rolls back on error.
|
|
2774
|
+
*/
|
|
2775
|
+
#useTransaction(fn) {
|
|
2776
|
+
this.#db.exec("BEGIN TRANSACTION");
|
|
2777
|
+
try {
|
|
2778
|
+
const result = fn();
|
|
2779
|
+
this.#db.exec("COMMIT");
|
|
2780
|
+
return result;
|
|
2781
|
+
} catch (error) {
|
|
2782
|
+
this.#db.exec("ROLLBACK");
|
|
2783
|
+
throw error;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
// ==========================================================================
|
|
2787
|
+
// Chat Operations
|
|
2788
|
+
// ==========================================================================
|
|
2789
|
+
async createChat(chat) {
|
|
2790
|
+
this.#useTransaction(() => {
|
|
2791
|
+
this.#db.prepare(
|
|
2792
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
2793
|
+
VALUES (?, ?, ?, ?)`
|
|
2794
|
+
).run(
|
|
2795
|
+
chat.id,
|
|
2796
|
+
chat.userId,
|
|
2797
|
+
chat.title ?? null,
|
|
2798
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
2799
|
+
);
|
|
2800
|
+
this.#db.prepare(
|
|
2801
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
2802
|
+
VALUES (?, ?, 'main', NULL, 1, ?)`
|
|
2803
|
+
).run(crypto.randomUUID(), chat.id, Date.now());
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
async upsertChat(chat) {
|
|
2807
|
+
return this.#useTransaction(() => {
|
|
2808
|
+
const row = this.#db.prepare(
|
|
2809
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
2810
|
+
VALUES (?, ?, ?, ?)
|
|
2811
|
+
ON CONFLICT(id) DO UPDATE SET id = excluded.id
|
|
2812
|
+
RETURNING *`
|
|
2813
|
+
).get(
|
|
2814
|
+
chat.id,
|
|
2815
|
+
chat.userId,
|
|
2816
|
+
chat.title ?? null,
|
|
2817
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
2818
|
+
);
|
|
2819
|
+
this.#db.prepare(
|
|
2820
|
+
`INSERT OR IGNORE INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
2821
|
+
VALUES (?, ?, 'main', NULL, 1, ?)`
|
|
2822
|
+
).run(crypto.randomUUID(), chat.id, Date.now());
|
|
2823
|
+
return {
|
|
2824
|
+
id: row.id,
|
|
2825
|
+
userId: row.userId,
|
|
2826
|
+
title: row.title ?? void 0,
|
|
2827
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2828
|
+
createdAt: row.createdAt,
|
|
2829
|
+
updatedAt: row.updatedAt
|
|
2830
|
+
};
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
async getChat(chatId) {
|
|
2834
|
+
const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
|
|
2835
|
+
if (!row) {
|
|
2836
|
+
return void 0;
|
|
2837
|
+
}
|
|
2838
|
+
return {
|
|
2839
|
+
id: row.id,
|
|
2840
|
+
userId: row.userId,
|
|
2841
|
+
title: row.title ?? void 0,
|
|
2842
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2843
|
+
createdAt: row.createdAt,
|
|
2844
|
+
updatedAt: row.updatedAt
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
async updateChat(chatId, updates) {
|
|
2848
|
+
const setClauses = ["updatedAt = strftime('%s', 'now') * 1000"];
|
|
2849
|
+
const params = [];
|
|
2850
|
+
if (updates.title !== void 0) {
|
|
2851
|
+
setClauses.push("title = ?");
|
|
2852
|
+
params.push(updates.title ?? null);
|
|
2853
|
+
}
|
|
2854
|
+
if (updates.metadata !== void 0) {
|
|
2855
|
+
setClauses.push("metadata = ?");
|
|
2856
|
+
params.push(JSON.stringify(updates.metadata));
|
|
2857
|
+
}
|
|
2858
|
+
params.push(chatId);
|
|
2859
|
+
const row = this.#db.prepare(
|
|
2860
|
+
`UPDATE chats SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`
|
|
2861
|
+
).get(...params);
|
|
2862
|
+
return {
|
|
2863
|
+
id: row.id,
|
|
2864
|
+
userId: row.userId,
|
|
2865
|
+
title: row.title ?? void 0,
|
|
2866
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2867
|
+
createdAt: row.createdAt,
|
|
2868
|
+
updatedAt: row.updatedAt
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
async listChats(options) {
|
|
2872
|
+
const params = [];
|
|
2873
|
+
let whereClause = "";
|
|
2874
|
+
let limitClause = "";
|
|
2875
|
+
if (options?.userId) {
|
|
2876
|
+
whereClause = "WHERE c.userId = ?";
|
|
2877
|
+
params.push(options.userId);
|
|
2878
|
+
}
|
|
2879
|
+
if (options?.limit !== void 0) {
|
|
2880
|
+
limitClause = " LIMIT ?";
|
|
2881
|
+
params.push(options.limit);
|
|
2882
|
+
if (options.offset !== void 0) {
|
|
2883
|
+
limitClause += " OFFSET ?";
|
|
2884
|
+
params.push(options.offset);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
const rows = this.#db.prepare(
|
|
2888
|
+
`SELECT
|
|
2889
|
+
c.id,
|
|
2890
|
+
c.userId,
|
|
2891
|
+
c.title,
|
|
2892
|
+
c.createdAt,
|
|
2893
|
+
c.updatedAt,
|
|
2894
|
+
COUNT(DISTINCT m.id) as messageCount,
|
|
2895
|
+
COUNT(DISTINCT b.id) as branchCount
|
|
2896
|
+
FROM chats c
|
|
2897
|
+
LEFT JOIN messages m ON m.chatId = c.id
|
|
2898
|
+
LEFT JOIN branches b ON b.chatId = c.id
|
|
2899
|
+
${whereClause}
|
|
2900
|
+
GROUP BY c.id
|
|
2901
|
+
ORDER BY c.updatedAt DESC${limitClause}`
|
|
2902
|
+
).all(...params);
|
|
2903
|
+
return rows.map((row) => ({
|
|
2904
|
+
id: row.id,
|
|
2905
|
+
userId: row.userId,
|
|
2906
|
+
title: row.title ?? void 0,
|
|
2907
|
+
messageCount: row.messageCount,
|
|
2908
|
+
branchCount: row.branchCount,
|
|
2909
|
+
createdAt: row.createdAt,
|
|
2910
|
+
updatedAt: row.updatedAt
|
|
2911
|
+
}));
|
|
2912
|
+
}
|
|
2913
|
+
async deleteChat(chatId, options) {
|
|
2914
|
+
return this.#useTransaction(() => {
|
|
2915
|
+
const messageIds = this.#db.prepare("SELECT id FROM messages WHERE chatId = ?").all(chatId);
|
|
2916
|
+
let sql = "DELETE FROM chats WHERE id = ?";
|
|
2917
|
+
const params = [chatId];
|
|
2918
|
+
if (options?.userId !== void 0) {
|
|
2919
|
+
sql += " AND userId = ?";
|
|
2920
|
+
params.push(options.userId);
|
|
2921
|
+
}
|
|
2922
|
+
const result = this.#db.prepare(sql).run(...params);
|
|
2923
|
+
if (result.changes > 0 && messageIds.length > 0) {
|
|
2924
|
+
const placeholders = messageIds.map(() => "?").join(", ");
|
|
2925
|
+
this.#db.prepare(
|
|
2926
|
+
`DELETE FROM messages_fts WHERE messageId IN (${placeholders})`
|
|
2927
|
+
).run(...messageIds.map((m) => m.id));
|
|
2928
|
+
}
|
|
2929
|
+
return result.changes > 0;
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
// ==========================================================================
|
|
2933
|
+
// Message Operations (Graph Nodes)
|
|
2934
|
+
// ==========================================================================
|
|
2935
|
+
async addMessage(message2) {
|
|
2936
|
+
this.#db.prepare(
|
|
2937
|
+
`INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
|
|
2938
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2939
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2940
|
+
parentId = excluded.parentId,
|
|
2941
|
+
name = excluded.name,
|
|
2942
|
+
type = excluded.type,
|
|
2943
|
+
data = excluded.data`
|
|
2944
|
+
).run(
|
|
2945
|
+
message2.id,
|
|
2946
|
+
message2.chatId,
|
|
2947
|
+
message2.parentId,
|
|
2948
|
+
message2.name,
|
|
2949
|
+
message2.type ?? null,
|
|
2950
|
+
JSON.stringify(message2.data),
|
|
2951
|
+
message2.createdAt
|
|
2952
|
+
);
|
|
2953
|
+
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
2954
|
+
this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
|
|
2955
|
+
this.#db.prepare(
|
|
2956
|
+
`INSERT INTO messages_fts(messageId, chatId, name, content)
|
|
2957
|
+
VALUES (?, ?, ?, ?)`
|
|
2958
|
+
).run(message2.id, message2.chatId, message2.name, content);
|
|
2959
|
+
}
|
|
2960
|
+
async getMessage(messageId) {
|
|
2961
|
+
const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
|
|
2962
|
+
if (!row) {
|
|
2963
|
+
return void 0;
|
|
2964
|
+
}
|
|
2965
|
+
return {
|
|
2966
|
+
id: row.id,
|
|
2967
|
+
chatId: row.chatId,
|
|
2968
|
+
parentId: row.parentId,
|
|
2969
|
+
name: row.name,
|
|
2970
|
+
type: row.type ?? void 0,
|
|
2971
|
+
data: JSON.parse(row.data),
|
|
2972
|
+
createdAt: row.createdAt
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
async getMessageChain(headId) {
|
|
2976
|
+
const rows = this.#db.prepare(
|
|
2977
|
+
`WITH RECURSIVE chain AS (
|
|
2978
|
+
SELECT *, 0 as depth FROM messages WHERE id = ?
|
|
2979
|
+
UNION ALL
|
|
2980
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
2981
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
2982
|
+
)
|
|
2983
|
+
SELECT * FROM chain
|
|
2984
|
+
ORDER BY depth DESC`
|
|
2985
|
+
).all(headId);
|
|
2986
|
+
return rows.map((row) => ({
|
|
2987
|
+
id: row.id,
|
|
2988
|
+
chatId: row.chatId,
|
|
2989
|
+
parentId: row.parentId,
|
|
2990
|
+
name: row.name,
|
|
2991
|
+
type: row.type ?? void 0,
|
|
2992
|
+
data: JSON.parse(row.data),
|
|
2993
|
+
createdAt: row.createdAt
|
|
2994
|
+
}));
|
|
2995
|
+
}
|
|
2996
|
+
async hasChildren(messageId) {
|
|
2997
|
+
const row = this.#db.prepare(
|
|
2998
|
+
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
|
|
2999
|
+
).get(messageId);
|
|
3000
|
+
return row.hasChildren === 1;
|
|
3001
|
+
}
|
|
3002
|
+
async getMessages(chatId) {
|
|
3003
|
+
const chat = await this.getChat(chatId);
|
|
3004
|
+
if (!chat) {
|
|
3005
|
+
throw new Error(`Chat "${chatId}" not found`);
|
|
3006
|
+
}
|
|
3007
|
+
const activeBranch = await this.getActiveBranch(chatId);
|
|
3008
|
+
if (!activeBranch?.headMessageId) {
|
|
3009
|
+
return [];
|
|
3010
|
+
}
|
|
3011
|
+
return this.getMessageChain(activeBranch.headMessageId);
|
|
3012
|
+
}
|
|
3013
|
+
// ==========================================================================
|
|
3014
|
+
// Branch Operations
|
|
3015
|
+
// ==========================================================================
|
|
3016
|
+
async createBranch(branch) {
|
|
3017
|
+
this.#db.prepare(
|
|
3018
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
3019
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
3020
|
+
).run(
|
|
3021
|
+
branch.id,
|
|
3022
|
+
branch.chatId,
|
|
3023
|
+
branch.name,
|
|
3024
|
+
branch.headMessageId,
|
|
3025
|
+
branch.isActive ? 1 : 0,
|
|
3026
|
+
branch.createdAt
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
async getBranch(chatId, name) {
|
|
3030
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
3031
|
+
if (!row) {
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
return {
|
|
3035
|
+
id: row.id,
|
|
3036
|
+
chatId: row.chatId,
|
|
3037
|
+
name: row.name,
|
|
3038
|
+
headMessageId: row.headMessageId,
|
|
3039
|
+
isActive: row.isActive === 1,
|
|
3040
|
+
createdAt: row.createdAt
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
async getActiveBranch(chatId) {
|
|
3044
|
+
const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
|
|
3045
|
+
if (!row) {
|
|
3046
|
+
return void 0;
|
|
3047
|
+
}
|
|
3048
|
+
return {
|
|
3049
|
+
id: row.id,
|
|
3050
|
+
chatId: row.chatId,
|
|
3051
|
+
name: row.name,
|
|
3052
|
+
headMessageId: row.headMessageId,
|
|
3053
|
+
isActive: true,
|
|
3054
|
+
createdAt: row.createdAt
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
async setActiveBranch(chatId, branchId) {
|
|
3058
|
+
this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
|
|
3059
|
+
this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
|
|
3060
|
+
}
|
|
3061
|
+
async updateBranchHead(branchId, messageId) {
|
|
3062
|
+
this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
|
|
3063
|
+
}
|
|
3064
|
+
async listBranches(chatId) {
|
|
3065
|
+
const branches = this.#db.prepare(
|
|
3066
|
+
`SELECT
|
|
3067
|
+
b.id,
|
|
3068
|
+
b.name,
|
|
3069
|
+
b.headMessageId,
|
|
3070
|
+
b.isActive,
|
|
3071
|
+
b.createdAt
|
|
3072
|
+
FROM branches b
|
|
3073
|
+
WHERE b.chatId = ?
|
|
3074
|
+
ORDER BY b.createdAt ASC`
|
|
3075
|
+
).all(chatId);
|
|
3076
|
+
const result = [];
|
|
3077
|
+
for (const branch of branches) {
|
|
3078
|
+
let messageCount = 0;
|
|
3079
|
+
if (branch.headMessageId) {
|
|
3080
|
+
const countRow = this.#db.prepare(
|
|
3081
|
+
`WITH RECURSIVE chain AS (
|
|
3082
|
+
SELECT id, parentId FROM messages WHERE id = ?
|
|
3083
|
+
UNION ALL
|
|
3084
|
+
SELECT m.id, m.parentId FROM messages m
|
|
3085
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
3086
|
+
)
|
|
3087
|
+
SELECT COUNT(*) as count FROM chain`
|
|
3088
|
+
).get(branch.headMessageId);
|
|
3089
|
+
messageCount = countRow.count;
|
|
3090
|
+
}
|
|
3091
|
+
result.push({
|
|
3092
|
+
id: branch.id,
|
|
3093
|
+
name: branch.name,
|
|
3094
|
+
headMessageId: branch.headMessageId,
|
|
3095
|
+
isActive: branch.isActive === 1,
|
|
3096
|
+
messageCount,
|
|
3097
|
+
createdAt: branch.createdAt
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
return result;
|
|
3101
|
+
}
|
|
3102
|
+
// ==========================================================================
|
|
3103
|
+
// Checkpoint Operations
|
|
3104
|
+
// ==========================================================================
|
|
3105
|
+
async createCheckpoint(checkpoint) {
|
|
3106
|
+
this.#db.prepare(
|
|
3107
|
+
`INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
|
|
3108
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3109
|
+
ON CONFLICT(chatId, name) DO UPDATE SET
|
|
3110
|
+
messageId = excluded.messageId,
|
|
3111
|
+
createdAt = excluded.createdAt`
|
|
3112
|
+
).run(
|
|
3113
|
+
checkpoint.id,
|
|
3114
|
+
checkpoint.chatId,
|
|
3115
|
+
checkpoint.name,
|
|
3116
|
+
checkpoint.messageId,
|
|
3117
|
+
checkpoint.createdAt
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
async getCheckpoint(chatId, name) {
|
|
3121
|
+
const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
|
|
3122
|
+
if (!row) {
|
|
3123
|
+
return void 0;
|
|
3124
|
+
}
|
|
3125
|
+
return {
|
|
3126
|
+
id: row.id,
|
|
3127
|
+
chatId: row.chatId,
|
|
3128
|
+
name: row.name,
|
|
3129
|
+
messageId: row.messageId,
|
|
3130
|
+
createdAt: row.createdAt
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
async listCheckpoints(chatId) {
|
|
3134
|
+
const rows = this.#db.prepare(
|
|
3135
|
+
`SELECT id, name, messageId, createdAt
|
|
3136
|
+
FROM checkpoints
|
|
3137
|
+
WHERE chatId = ?
|
|
3138
|
+
ORDER BY createdAt DESC`
|
|
3139
|
+
).all(chatId);
|
|
3140
|
+
return rows.map((row) => ({
|
|
3141
|
+
id: row.id,
|
|
3142
|
+
name: row.name,
|
|
3143
|
+
messageId: row.messageId,
|
|
3144
|
+
createdAt: row.createdAt
|
|
3145
|
+
}));
|
|
3146
|
+
}
|
|
3147
|
+
async deleteCheckpoint(chatId, name) {
|
|
3148
|
+
this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
|
|
3149
|
+
}
|
|
3150
|
+
// ==========================================================================
|
|
3151
|
+
// Search Operations
|
|
3152
|
+
// ==========================================================================
|
|
3153
|
+
async searchMessages(chatId, query, options) {
|
|
3154
|
+
const limit = options?.limit ?? 20;
|
|
3155
|
+
const roles = options?.roles;
|
|
3156
|
+
let sql = `
|
|
3157
|
+
SELECT
|
|
3158
|
+
m.id,
|
|
3159
|
+
m.chatId,
|
|
3160
|
+
m.parentId,
|
|
3161
|
+
m.name,
|
|
3162
|
+
m.type,
|
|
3163
|
+
m.data,
|
|
3164
|
+
m.createdAt,
|
|
3165
|
+
fts.rank,
|
|
3166
|
+
snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
|
|
3167
|
+
FROM messages_fts fts
|
|
3168
|
+
JOIN messages m ON m.id = fts.messageId
|
|
3169
|
+
WHERE messages_fts MATCH ?
|
|
3170
|
+
AND fts.chatId = ?
|
|
3171
|
+
`;
|
|
3172
|
+
const params = [query, chatId];
|
|
3173
|
+
if (roles && roles.length > 0) {
|
|
3174
|
+
const placeholders = roles.map(() => "?").join(", ");
|
|
3175
|
+
sql += ` AND fts.name IN (${placeholders})`;
|
|
3176
|
+
params.push(...roles);
|
|
3177
|
+
}
|
|
3178
|
+
sql += " ORDER BY fts.rank LIMIT ?";
|
|
3179
|
+
params.push(limit);
|
|
3180
|
+
const rows = this.#db.prepare(sql).all(...params);
|
|
3181
|
+
return rows.map((row) => ({
|
|
3182
|
+
message: {
|
|
3183
|
+
id: row.id,
|
|
3184
|
+
chatId: row.chatId,
|
|
3185
|
+
parentId: row.parentId,
|
|
3186
|
+
name: row.name,
|
|
3187
|
+
type: row.type ?? void 0,
|
|
3188
|
+
data: JSON.parse(row.data),
|
|
3189
|
+
createdAt: row.createdAt
|
|
3190
|
+
},
|
|
3191
|
+
rank: row.rank,
|
|
3192
|
+
snippet: row.snippet
|
|
3193
|
+
}));
|
|
3194
|
+
}
|
|
3195
|
+
// ==========================================================================
|
|
3196
|
+
// Visualization Operations
|
|
3197
|
+
// ==========================================================================
|
|
3198
|
+
async getGraph(chatId) {
|
|
3199
|
+
const messageRows = this.#db.prepare(
|
|
3200
|
+
`SELECT id, parentId, name, data, createdAt
|
|
3201
|
+
FROM messages
|
|
3202
|
+
WHERE chatId = ?
|
|
3203
|
+
ORDER BY createdAt ASC`
|
|
3204
|
+
).all(chatId);
|
|
3205
|
+
const nodes = messageRows.map((row) => {
|
|
3206
|
+
const data = JSON.parse(row.data);
|
|
3207
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
3208
|
+
return {
|
|
3209
|
+
id: row.id,
|
|
3210
|
+
parentId: row.parentId,
|
|
3211
|
+
role: row.name,
|
|
3212
|
+
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
3213
|
+
createdAt: row.createdAt
|
|
3214
|
+
};
|
|
3215
|
+
});
|
|
3216
|
+
const branchRows = this.#db.prepare(
|
|
3217
|
+
`SELECT name, headMessageId, isActive
|
|
3218
|
+
FROM branches
|
|
3219
|
+
WHERE chatId = ?
|
|
3220
|
+
ORDER BY createdAt ASC`
|
|
3221
|
+
).all(chatId);
|
|
3222
|
+
const branches = branchRows.map((row) => ({
|
|
3223
|
+
name: row.name,
|
|
3224
|
+
headMessageId: row.headMessageId,
|
|
3225
|
+
isActive: row.isActive === 1
|
|
3226
|
+
}));
|
|
3227
|
+
const checkpointRows = this.#db.prepare(
|
|
3228
|
+
`SELECT name, messageId
|
|
3229
|
+
FROM checkpoints
|
|
3230
|
+
WHERE chatId = ?
|
|
3231
|
+
ORDER BY createdAt ASC`
|
|
3232
|
+
).all(chatId);
|
|
3233
|
+
const checkpoints = checkpointRows.map((row) => ({
|
|
3234
|
+
name: row.name,
|
|
3235
|
+
messageId: row.messageId
|
|
3236
|
+
}));
|
|
3237
|
+
return {
|
|
3238
|
+
chatId,
|
|
3239
|
+
nodes,
|
|
3240
|
+
branches,
|
|
3241
|
+
checkpoints
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
|
|
3246
|
+
// packages/context/src/lib/store/memory.store.ts
|
|
3247
|
+
var InMemoryContextStore = class extends SqliteContextStore {
|
|
3248
|
+
constructor() {
|
|
3249
|
+
super(":memory:");
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
|
|
3253
|
+
// packages/context/src/lib/visualize.ts
|
|
3254
|
+
function visualizeGraph(data) {
|
|
3255
|
+
if (data.nodes.length === 0) {
|
|
3256
|
+
return `[chat: ${data.chatId}]
|
|
3257
|
+
|
|
3258
|
+
(empty)`;
|
|
3259
|
+
}
|
|
3260
|
+
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
3261
|
+
const branchHeads = /* @__PURE__ */ new Map();
|
|
3262
|
+
const checkpointsByMessageId = /* @__PURE__ */ new Map();
|
|
3263
|
+
for (const node of data.nodes) {
|
|
3264
|
+
const children = childrenByParentId.get(node.parentId) ?? [];
|
|
3265
|
+
children.push(node);
|
|
3266
|
+
childrenByParentId.set(node.parentId, children);
|
|
3267
|
+
}
|
|
3268
|
+
for (const branch of data.branches) {
|
|
3269
|
+
if (branch.headMessageId) {
|
|
3270
|
+
const heads = branchHeads.get(branch.headMessageId) ?? [];
|
|
3271
|
+
heads.push(branch.isActive ? `${branch.name} *` : branch.name);
|
|
3272
|
+
branchHeads.set(branch.headMessageId, heads);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
for (const checkpoint of data.checkpoints) {
|
|
3276
|
+
const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
|
|
3277
|
+
cps.push(checkpoint.name);
|
|
3278
|
+
checkpointsByMessageId.set(checkpoint.messageId, cps);
|
|
3279
|
+
}
|
|
3280
|
+
const roots = childrenByParentId.get(null) ?? [];
|
|
3281
|
+
const lines = [`[chat: ${data.chatId}]`, ""];
|
|
3282
|
+
function renderNode(node, prefix, isLast, isRoot) {
|
|
3283
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
3284
|
+
const contentPreview = node.content.replace(/\n/g, " ");
|
|
3285
|
+
let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
|
|
3286
|
+
const branches = branchHeads.get(node.id);
|
|
3287
|
+
if (branches) {
|
|
3288
|
+
line += ` <- [${branches.join(", ")}]`;
|
|
3289
|
+
}
|
|
3290
|
+
const checkpoints = checkpointsByMessageId.get(node.id);
|
|
3291
|
+
if (checkpoints) {
|
|
3292
|
+
line += ` {${checkpoints.join(", ")}}`;
|
|
3293
|
+
}
|
|
3294
|
+
lines.push(line);
|
|
3295
|
+
const children = childrenByParentId.get(node.id) ?? [];
|
|
3296
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
3297
|
+
for (let i = 0; i < children.length; i++) {
|
|
3298
|
+
renderNode(children[i], childPrefix, i === children.length - 1, false);
|
|
3299
|
+
}
|
|
1748
3300
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
3301
|
+
for (let i = 0; i < roots.length; i++) {
|
|
3302
|
+
renderNode(roots[i], "", i === roots.length - 1, true);
|
|
3303
|
+
}
|
|
3304
|
+
lines.push("");
|
|
3305
|
+
lines.push("Legend: * = active branch, {...} = checkpoint");
|
|
3306
|
+
return lines.join("\n");
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// packages/context/src/lib/agent.ts
|
|
3310
|
+
import { groq } from "@ai-sdk/groq";
|
|
3311
|
+
import {
|
|
3312
|
+
NoSuchToolError,
|
|
3313
|
+
Output,
|
|
3314
|
+
convertToModelMessages,
|
|
3315
|
+
createUIMessageStream,
|
|
3316
|
+
generateId as generateId2,
|
|
3317
|
+
generateText,
|
|
3318
|
+
smoothStream,
|
|
3319
|
+
stepCountIs,
|
|
3320
|
+
streamText
|
|
3321
|
+
} from "ai";
|
|
3322
|
+
import chalk2 from "chalk";
|
|
3323
|
+
import "zod";
|
|
3324
|
+
import "@deepagents/agent";
|
|
3325
|
+
var Agent = class _Agent {
|
|
3326
|
+
#options;
|
|
3327
|
+
#guardrails = [];
|
|
3328
|
+
tools;
|
|
3329
|
+
constructor(options) {
|
|
3330
|
+
this.#options = options;
|
|
3331
|
+
this.tools = options.tools || {};
|
|
3332
|
+
this.#guardrails = options.guardrails || [];
|
|
3333
|
+
}
|
|
3334
|
+
async generate(contextVariables, config) {
|
|
3335
|
+
if (!this.#options.context) {
|
|
3336
|
+
throw new Error(`Agent ${this.#options.name} is missing a context.`);
|
|
1769
3337
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
3338
|
+
if (!this.#options.model) {
|
|
3339
|
+
throw new Error(`Agent ${this.#options.name} is missing a model.`);
|
|
3340
|
+
}
|
|
3341
|
+
const { messages, systemPrompt } = await this.#options.context.resolve({
|
|
3342
|
+
renderer: new XmlRenderer()
|
|
3343
|
+
});
|
|
3344
|
+
return generateText({
|
|
3345
|
+
abortSignal: config?.abortSignal,
|
|
3346
|
+
providerOptions: this.#options.providerOptions,
|
|
3347
|
+
model: this.#options.model,
|
|
3348
|
+
system: systemPrompt,
|
|
3349
|
+
messages: await convertToModelMessages(messages),
|
|
3350
|
+
stopWhen: stepCountIs(25),
|
|
3351
|
+
tools: this.#options.tools,
|
|
3352
|
+
experimental_context: contextVariables,
|
|
3353
|
+
experimental_repairToolCall: repairToolCall,
|
|
3354
|
+
toolChoice: this.#options.toolChoice,
|
|
3355
|
+
onStepFinish: (step) => {
|
|
3356
|
+
const toolCall = step.toolCalls.at(-1);
|
|
3357
|
+
if (toolCall) {
|
|
3358
|
+
console.log(
|
|
3359
|
+
`Debug: ${chalk2.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
1774
3364
|
}
|
|
1775
3365
|
/**
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
* Use this when you want to fork the conversation without leaving
|
|
1779
|
-
* the current branch. Common use case: user wants to ask another
|
|
1780
|
-
* question while waiting for the model to respond.
|
|
3366
|
+
* Stream a response from the agent.
|
|
1781
3367
|
*
|
|
1782
|
-
*
|
|
1783
|
-
* -
|
|
1784
|
-
* - Does NOT switch to the new branch
|
|
1785
|
-
* - Keeps pending messages intact
|
|
1786
|
-
*
|
|
1787
|
-
* @returns The new branch info (does not switch to it)
|
|
1788
|
-
* @throws Error if no messages exist in the conversation
|
|
3368
|
+
* When guardrails are configured, `toUIMessageStream()` is wrapped to provide
|
|
3369
|
+
* self-correction behavior. Direct access to fullStream/textStream bypasses guardrails.
|
|
1789
3370
|
*
|
|
1790
3371
|
* @example
|
|
1791
|
-
* ```
|
|
1792
|
-
*
|
|
1793
|
-
* context.set(user('What is the weather?'));
|
|
1794
|
-
* await context.save();
|
|
3372
|
+
* ```typescript
|
|
3373
|
+
* const stream = await agent.stream({});
|
|
1795
3374
|
*
|
|
1796
|
-
* //
|
|
1797
|
-
*
|
|
1798
|
-
* // newBranch = { name: 'main-v2', ... }
|
|
3375
|
+
* // With guardrails - use toUIMessageStream for protection
|
|
3376
|
+
* await printer.readableStream(stream.toUIMessageStream());
|
|
1799
3377
|
*
|
|
1800
|
-
* //
|
|
1801
|
-
* await
|
|
1802
|
-
* context.set(user('Also, what time is it?'));
|
|
1803
|
-
* await context.save();
|
|
3378
|
+
* // Or use printer.stdout which uses toUIMessageStream internally
|
|
3379
|
+
* await printer.stdout(stream);
|
|
1804
3380
|
* ```
|
|
1805
3381
|
*/
|
|
1806
|
-
async
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
throw new Error("Cannot create btw branch: no messages in conversation");
|
|
3382
|
+
async stream(contextVariables, config) {
|
|
3383
|
+
if (!this.#options.context) {
|
|
3384
|
+
throw new Error(`Agent ${this.#options.name} is missing a context.`);
|
|
1810
3385
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
/**
|
|
1814
|
-
* Update metadata for the current chat.
|
|
1815
|
-
*
|
|
1816
|
-
* @param updates - Partial metadata to merge (title, metadata)
|
|
1817
|
-
*
|
|
1818
|
-
* @example
|
|
1819
|
-
* ```ts
|
|
1820
|
-
* await context.updateChat({
|
|
1821
|
-
* title: 'Coding Help Session',
|
|
1822
|
-
* metadata: { tags: ['python', 'debugging'] }
|
|
1823
|
-
* });
|
|
1824
|
-
* ```
|
|
1825
|
-
*/
|
|
1826
|
-
async updateChat(updates) {
|
|
1827
|
-
await this.#ensureInitialized();
|
|
1828
|
-
const storeUpdates = {};
|
|
1829
|
-
if (updates.title !== void 0) {
|
|
1830
|
-
storeUpdates.title = updates.title;
|
|
3386
|
+
if (!this.#options.model) {
|
|
3387
|
+
throw new Error(`Agent ${this.#options.name} is missing a model.`);
|
|
1831
3388
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
...updates.metadata
|
|
1836
|
-
};
|
|
3389
|
+
const result = await this.#createRawStream(contextVariables, config);
|
|
3390
|
+
if (this.#guardrails.length === 0) {
|
|
3391
|
+
return result;
|
|
1837
3392
|
}
|
|
1838
|
-
|
|
3393
|
+
return this.#wrapWithGuardrails(result, contextVariables, config);
|
|
1839
3394
|
}
|
|
1840
3395
|
/**
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
* This is a placeholder for future functionality that merges context fragments
|
|
1844
|
-
* using specific rules. Currently, it does nothing.
|
|
1845
|
-
*
|
|
1846
|
-
* @experimental
|
|
3396
|
+
* Create a raw stream without guardrail processing.
|
|
1847
3397
|
*/
|
|
1848
|
-
|
|
1849
|
-
|
|
3398
|
+
async #createRawStream(contextVariables, config) {
|
|
3399
|
+
const { messages, systemPrompt } = await this.#options.context.resolve({
|
|
3400
|
+
renderer: new XmlRenderer()
|
|
3401
|
+
});
|
|
3402
|
+
const runId = generateId2();
|
|
3403
|
+
return streamText({
|
|
3404
|
+
abortSignal: config?.abortSignal,
|
|
3405
|
+
providerOptions: this.#options.providerOptions,
|
|
3406
|
+
model: this.#options.model,
|
|
3407
|
+
system: systemPrompt,
|
|
3408
|
+
messages: await convertToModelMessages(messages),
|
|
3409
|
+
experimental_repairToolCall: repairToolCall,
|
|
3410
|
+
stopWhen: stepCountIs(50),
|
|
3411
|
+
experimental_transform: config?.transform ?? smoothStream(),
|
|
3412
|
+
tools: this.#options.tools,
|
|
3413
|
+
experimental_context: contextVariables,
|
|
3414
|
+
toolChoice: this.#options.toolChoice,
|
|
3415
|
+
onStepFinish: (step) => {
|
|
3416
|
+
const toolCall = step.toolCalls.at(-1);
|
|
3417
|
+
if (toolCall) {
|
|
3418
|
+
console.log(
|
|
3419
|
+
`Debug: (${runId}) ${chalk2.bold.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
1850
3424
|
}
|
|
1851
3425
|
/**
|
|
1852
|
-
*
|
|
1853
|
-
* Returns a comprehensive JSON-serializable object with all context information.
|
|
1854
|
-
*
|
|
1855
|
-
* @param options - Inspection options (modelId and renderer required)
|
|
1856
|
-
* @returns Complete inspection data including estimates, rendered output, fragments, and graph
|
|
3426
|
+
* Wrap a StreamTextResult with guardrail protection on toUIMessageStream().
|
|
1857
3427
|
*
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1860
|
-
*
|
|
1861
|
-
*
|
|
1862
|
-
* renderer: new XmlRenderer(),
|
|
1863
|
-
* });
|
|
1864
|
-
* console.log(JSON.stringify(inspection, null, 2));
|
|
1865
|
-
*
|
|
1866
|
-
* // Or write to file for analysis
|
|
1867
|
-
* await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
|
|
1868
|
-
* ```
|
|
3428
|
+
* When a guardrail fails:
|
|
3429
|
+
* 1. Accumulated text + feedback is appended as the assistant's self-correction
|
|
3430
|
+
* 2. The feedback is written to the output stream (user sees the correction)
|
|
3431
|
+
* 3. A new stream is started and the model continues from the correction
|
|
1869
3432
|
*/
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
3433
|
+
#wrapWithGuardrails(result, contextVariables, config) {
|
|
3434
|
+
const maxRetries = config?.maxRetries ?? this.#options.maxGuardrailRetries ?? 3;
|
|
3435
|
+
const context = this.#options.context;
|
|
3436
|
+
const originalToUIMessageStream = result.toUIMessageStream.bind(result);
|
|
3437
|
+
result.toUIMessageStream = (options) => {
|
|
3438
|
+
return createUIMessageStream({
|
|
3439
|
+
generateId: generateId2,
|
|
3440
|
+
execute: async ({ writer }) => {
|
|
3441
|
+
let currentResult = result;
|
|
3442
|
+
let attempt = 0;
|
|
3443
|
+
const guardrailContext = {
|
|
3444
|
+
availableTools: Object.keys(this.tools)
|
|
3445
|
+
};
|
|
3446
|
+
while (attempt < maxRetries) {
|
|
3447
|
+
if (config?.abortSignal?.aborted) {
|
|
3448
|
+
writer.write({ type: "finish" });
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
attempt++;
|
|
3452
|
+
let accumulatedText = "";
|
|
3453
|
+
let guardrailFailed = false;
|
|
3454
|
+
let failureFeedback = "";
|
|
3455
|
+
const uiStream = currentResult === result ? originalToUIMessageStream(options) : currentResult.toUIMessageStream(options);
|
|
3456
|
+
for await (const part of uiStream) {
|
|
3457
|
+
const checkResult = runGuardrailChain(
|
|
3458
|
+
part,
|
|
3459
|
+
this.#guardrails,
|
|
3460
|
+
guardrailContext
|
|
3461
|
+
);
|
|
3462
|
+
if (checkResult.type === "fail") {
|
|
3463
|
+
guardrailFailed = true;
|
|
3464
|
+
failureFeedback = checkResult.feedback;
|
|
3465
|
+
console.log(
|
|
3466
|
+
chalk2.yellow(
|
|
3467
|
+
`[${this.#options.name}] Guardrail triggered (attempt ${attempt}/${maxRetries}): ${failureFeedback.slice(0, 50)}...`
|
|
3468
|
+
)
|
|
3469
|
+
);
|
|
3470
|
+
break;
|
|
3471
|
+
}
|
|
3472
|
+
if (checkResult.part.type === "text-delta") {
|
|
3473
|
+
accumulatedText += checkResult.part.delta;
|
|
3474
|
+
}
|
|
3475
|
+
writer.write(checkResult.part);
|
|
3476
|
+
}
|
|
3477
|
+
if (!guardrailFailed) {
|
|
3478
|
+
writer.write({ type: "finish" });
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
if (attempt >= maxRetries) {
|
|
3482
|
+
console.error(
|
|
3483
|
+
chalk2.red(
|
|
3484
|
+
`[${this.#options.name}] Guardrail retry limit (${maxRetries}) exceeded.`
|
|
3485
|
+
)
|
|
3486
|
+
);
|
|
3487
|
+
writer.write({ type: "finish" });
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
writer.write({
|
|
3491
|
+
type: "text-delta",
|
|
3492
|
+
id: generateId2(),
|
|
3493
|
+
delta: ` ${failureFeedback}`
|
|
3494
|
+
});
|
|
3495
|
+
const selfCorrectionText = accumulatedText + " " + failureFeedback;
|
|
3496
|
+
context.set(assistantText(selfCorrectionText));
|
|
3497
|
+
await context.save();
|
|
3498
|
+
currentResult = await this.#createRawStream(
|
|
3499
|
+
contextVariables,
|
|
3500
|
+
config
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
},
|
|
3504
|
+
onError: (error) => {
|
|
3505
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
3506
|
+
return `Stream failed: ${message2}`;
|
|
3507
|
+
}
|
|
3508
|
+
});
|
|
1897
3509
|
};
|
|
3510
|
+
return result;
|
|
3511
|
+
}
|
|
3512
|
+
clone(overrides) {
|
|
3513
|
+
return new _Agent({
|
|
3514
|
+
...this.#options,
|
|
3515
|
+
...overrides
|
|
3516
|
+
});
|
|
1898
3517
|
}
|
|
1899
3518
|
};
|
|
1900
|
-
function
|
|
1901
|
-
return
|
|
1902
|
-
name: "hint",
|
|
1903
|
-
data: text
|
|
1904
|
-
};
|
|
3519
|
+
function agent(options) {
|
|
3520
|
+
return new Agent(options);
|
|
1905
3521
|
}
|
|
1906
|
-
function
|
|
1907
|
-
return {
|
|
1908
|
-
name,
|
|
1909
|
-
data: children
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1912
|
-
function role(content) {
|
|
1913
|
-
return {
|
|
1914
|
-
name: "role",
|
|
1915
|
-
data: content
|
|
1916
|
-
};
|
|
1917
|
-
}
|
|
1918
|
-
function user(content) {
|
|
1919
|
-
const message2 = typeof content === "string" ? {
|
|
1920
|
-
id: generateId(),
|
|
1921
|
-
role: "user",
|
|
1922
|
-
parts: [{ type: "text", text: content }]
|
|
1923
|
-
} : content;
|
|
3522
|
+
function structuredOutput(options) {
|
|
1924
3523
|
return {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
type: "message",
|
|
1929
|
-
persist: true,
|
|
1930
|
-
codec: {
|
|
1931
|
-
decode() {
|
|
1932
|
-
return message2;
|
|
1933
|
-
},
|
|
1934
|
-
encode() {
|
|
1935
|
-
return message2;
|
|
3524
|
+
async generate(contextVariables, config) {
|
|
3525
|
+
if (!options.context) {
|
|
3526
|
+
throw new Error(`structuredOutput is missing a context.`);
|
|
1936
3527
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
function assistant(message2) {
|
|
1941
|
-
return {
|
|
1942
|
-
id: message2.id,
|
|
1943
|
-
name: "assistant",
|
|
1944
|
-
data: "content",
|
|
1945
|
-
type: "message",
|
|
1946
|
-
persist: true,
|
|
1947
|
-
codec: {
|
|
1948
|
-
decode() {
|
|
1949
|
-
return message2;
|
|
1950
|
-
},
|
|
1951
|
-
encode() {
|
|
1952
|
-
return message2;
|
|
3528
|
+
if (!options.model) {
|
|
3529
|
+
throw new Error(`structuredOutput is missing a model.`);
|
|
1953
3530
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
3531
|
+
const { messages, systemPrompt } = await options.context.resolve({
|
|
3532
|
+
renderer: new XmlRenderer()
|
|
3533
|
+
});
|
|
3534
|
+
const result = await generateText({
|
|
3535
|
+
abortSignal: config?.abortSignal,
|
|
3536
|
+
providerOptions: options.providerOptions,
|
|
3537
|
+
model: options.model,
|
|
3538
|
+
system: systemPrompt,
|
|
3539
|
+
messages: await convertToModelMessages(messages),
|
|
3540
|
+
stopWhen: stepCountIs(25),
|
|
3541
|
+
experimental_repairToolCall: repairToolCall,
|
|
3542
|
+
experimental_context: contextVariables,
|
|
3543
|
+
output: Output.object({ schema: options.schema }),
|
|
3544
|
+
tools: options.tools
|
|
3545
|
+
});
|
|
3546
|
+
return result.output;
|
|
3547
|
+
},
|
|
3548
|
+
async stream(contextVariables, config) {
|
|
3549
|
+
if (!options.context) {
|
|
3550
|
+
throw new Error(`structuredOutput is missing a context.`);
|
|
3551
|
+
}
|
|
3552
|
+
if (!options.model) {
|
|
3553
|
+
throw new Error(`structuredOutput is missing a model.`);
|
|
1975
3554
|
}
|
|
3555
|
+
const { messages, systemPrompt } = await options.context.resolve({
|
|
3556
|
+
renderer: new XmlRenderer()
|
|
3557
|
+
});
|
|
3558
|
+
return streamText({
|
|
3559
|
+
abortSignal: config?.abortSignal,
|
|
3560
|
+
providerOptions: options.providerOptions,
|
|
3561
|
+
model: options.model,
|
|
3562
|
+
system: systemPrompt,
|
|
3563
|
+
experimental_repairToolCall: repairToolCall,
|
|
3564
|
+
messages: await convertToModelMessages(messages),
|
|
3565
|
+
stopWhen: stepCountIs(50),
|
|
3566
|
+
experimental_transform: config?.transform ?? smoothStream(),
|
|
3567
|
+
experimental_context: contextVariables,
|
|
3568
|
+
output: Output.object({ schema: options.schema }),
|
|
3569
|
+
tools: options.tools
|
|
3570
|
+
});
|
|
1976
3571
|
}
|
|
1977
3572
|
};
|
|
1978
3573
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
3574
|
+
var repairToolCall = async ({
|
|
3575
|
+
toolCall,
|
|
3576
|
+
tools,
|
|
3577
|
+
inputSchema,
|
|
3578
|
+
error
|
|
3579
|
+
}) => {
|
|
3580
|
+
console.log(
|
|
3581
|
+
`Debug: ${chalk2.yellow("RepairingToolCall")}: ${toolCall.toolName}`,
|
|
3582
|
+
error.name
|
|
3583
|
+
);
|
|
3584
|
+
if (NoSuchToolError.isInstance(error)) {
|
|
3585
|
+
return null;
|
|
3586
|
+
}
|
|
3587
|
+
const tool = tools[toolCall.toolName];
|
|
3588
|
+
const { output } = await generateText({
|
|
3589
|
+
model: groq("openai/gpt-oss-20b"),
|
|
3590
|
+
output: Output.object({ schema: tool.inputSchema }),
|
|
3591
|
+
prompt: [
|
|
3592
|
+
`The model tried to call the tool "${toolCall.toolName}" with the following inputs:`,
|
|
3593
|
+
JSON.stringify(toolCall.input),
|
|
3594
|
+
`The tool accepts the following schema:`,
|
|
3595
|
+
JSON.stringify(inputSchema(toolCall)),
|
|
3596
|
+
"Please fix the inputs."
|
|
3597
|
+
].join("\n")
|
|
1985
3598
|
});
|
|
3599
|
+
return { ...toolCall, input: JSON.stringify(output) };
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
// packages/context/src/lib/render.ts
|
|
3603
|
+
function render(tag, ...fragments) {
|
|
3604
|
+
if (fragments.length === 0) {
|
|
3605
|
+
return "";
|
|
3606
|
+
}
|
|
3607
|
+
const renderer = new XmlRenderer();
|
|
3608
|
+
const wrapped = fragment(tag, ...fragments);
|
|
3609
|
+
return renderer.render([wrapped]);
|
|
1986
3610
|
}
|
|
1987
3611
|
export {
|
|
3612
|
+
BinaryInstallError,
|
|
3613
|
+
ComposeStartError,
|
|
3614
|
+
ComposeStrategy,
|
|
3615
|
+
ContainerCreationError,
|
|
1988
3616
|
ContextEngine,
|
|
3617
|
+
ContextRenderer,
|
|
1989
3618
|
ContextStore,
|
|
3619
|
+
DockerNotAvailableError,
|
|
3620
|
+
DockerSandboxError,
|
|
3621
|
+
DockerSandboxStrategy,
|
|
3622
|
+
DockerfileBuildError,
|
|
3623
|
+
DockerfileStrategy,
|
|
1990
3624
|
InMemoryContextStore,
|
|
1991
3625
|
MarkdownRenderer,
|
|
1992
3626
|
ModelsRegistry,
|
|
3627
|
+
MountPathError,
|
|
3628
|
+
PackageInstallError,
|
|
3629
|
+
RuntimeStrategy,
|
|
1993
3630
|
SqliteContextStore,
|
|
1994
3631
|
TomlRenderer,
|
|
1995
3632
|
ToonRenderer,
|
|
1996
3633
|
XmlRenderer,
|
|
3634
|
+
agent,
|
|
3635
|
+
alias,
|
|
3636
|
+
analogy,
|
|
1997
3637
|
assistant,
|
|
1998
3638
|
assistantText,
|
|
3639
|
+
clarification,
|
|
3640
|
+
correction,
|
|
3641
|
+
createBinaryBridges,
|
|
3642
|
+
createContainerTool,
|
|
3643
|
+
createDockerSandbox,
|
|
1999
3644
|
defaultTokenizer,
|
|
3645
|
+
discoverSkillsInDirectory,
|
|
3646
|
+
errorRecoveryGuardrail,
|
|
2000
3647
|
estimate,
|
|
3648
|
+
example,
|
|
3649
|
+
explain,
|
|
3650
|
+
fail,
|
|
2001
3651
|
fragment,
|
|
2002
3652
|
getModelsRegistry,
|
|
3653
|
+
glossary,
|
|
3654
|
+
guardrail,
|
|
2003
3655
|
hint,
|
|
3656
|
+
identity,
|
|
3657
|
+
isComposeOptions,
|
|
3658
|
+
isDockerfileOptions,
|
|
3659
|
+
isFragment,
|
|
3660
|
+
isFragmentObject,
|
|
2004
3661
|
isMessageFragment,
|
|
3662
|
+
loadSkillMetadata,
|
|
2005
3663
|
message,
|
|
3664
|
+
parseFrontmatter,
|
|
3665
|
+
pass,
|
|
3666
|
+
persona,
|
|
3667
|
+
policy,
|
|
3668
|
+
preference,
|
|
3669
|
+
principle,
|
|
3670
|
+
quirk,
|
|
3671
|
+
render,
|
|
2006
3672
|
role,
|
|
3673
|
+
runGuardrailChain,
|
|
3674
|
+
skills,
|
|
3675
|
+
structuredOutput,
|
|
3676
|
+
styleGuide,
|
|
3677
|
+
term,
|
|
3678
|
+
useSandbox,
|
|
2007
3679
|
user,
|
|
2008
|
-
|
|
3680
|
+
userContext,
|
|
3681
|
+
visualizeGraph,
|
|
3682
|
+
workflow
|
|
2009
3683
|
};
|
|
2010
3684
|
//# sourceMappingURL=index.js.map
|