@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.
Files changed (50) hide show
  1. package/README.md +114 -119
  2. package/dist/example-error-recovery.d.ts +2 -0
  3. package/dist/example-error-recovery.d.ts.map +1 -0
  4. package/dist/index.d.ts +18 -388
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2765 -1091
  7. package/dist/index.js.map +4 -4
  8. package/dist/lib/agent.d.ts +87 -12
  9. package/dist/lib/agent.d.ts.map +1 -1
  10. package/dist/lib/engine.d.ts +325 -0
  11. package/dist/lib/engine.d.ts.map +1 -0
  12. package/dist/lib/estimate.d.ts +1 -1
  13. package/dist/lib/estimate.d.ts.map +1 -1
  14. package/dist/lib/fragments/domain.d.ts +537 -0
  15. package/dist/lib/fragments/domain.d.ts.map +1 -0
  16. package/dist/lib/fragments/user.d.ts +122 -0
  17. package/dist/lib/fragments/user.d.ts.map +1 -0
  18. package/dist/lib/fragments.d.ts +103 -0
  19. package/dist/lib/fragments.d.ts.map +1 -0
  20. package/dist/lib/guardrail.d.ts +138 -0
  21. package/dist/lib/guardrail.d.ts.map +1 -0
  22. package/dist/lib/guardrails/error-recovery.guardrail.d.ts +3 -0
  23. package/dist/lib/guardrails/error-recovery.guardrail.d.ts.map +1 -0
  24. package/dist/lib/render.d.ts +21 -0
  25. package/dist/lib/render.d.ts.map +1 -0
  26. package/dist/lib/renderers/abstract.renderer.d.ts +11 -3
  27. package/dist/lib/renderers/abstract.renderer.d.ts.map +1 -1
  28. package/dist/lib/sandbox/binary-bridges.d.ts +31 -0
  29. package/dist/lib/sandbox/binary-bridges.d.ts.map +1 -0
  30. package/dist/lib/sandbox/container-tool.d.ts +134 -0
  31. package/dist/lib/sandbox/container-tool.d.ts.map +1 -0
  32. package/dist/lib/sandbox/docker-sandbox.d.ts +471 -0
  33. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -0
  34. package/dist/lib/sandbox/index.d.ts +4 -0
  35. package/dist/lib/sandbox/index.d.ts.map +1 -0
  36. package/dist/lib/skills/fragments.d.ts +24 -0
  37. package/dist/lib/skills/fragments.d.ts.map +1 -0
  38. package/dist/lib/skills/index.d.ts +31 -0
  39. package/dist/lib/skills/index.d.ts.map +1 -0
  40. package/dist/lib/skills/loader.d.ts +28 -0
  41. package/dist/lib/skills/loader.d.ts.map +1 -0
  42. package/dist/lib/skills/types.d.ts +40 -0
  43. package/dist/lib/skills/types.d.ts.map +1 -0
  44. package/dist/lib/store/sqlite.store.d.ts +4 -2
  45. package/dist/lib/store/sqlite.store.d.ts.map +1 -1
  46. package/dist/lib/store/store.d.ts +36 -2
  47. package/dist/lib/store/store.d.ts.map +1 -1
  48. package/package.json +8 -4
  49. package/dist/lib/context.d.ts +0 -56
  50. 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
- return fragments.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
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
- return this.#wrap(
242
- fragment2.name,
243
- this.renderEntries(fragment2.data, { depth: 1, path: [] })
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
- children.push(
253
- this.#leaf(pluralize.singular(name), String(item), depth + 1)
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
- const children = this.renderEntries(data, { ...ctx, depth: ctx.depth + 1 });
297
- return this.#wrapIndented(name, children, ctx.depth);
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
- children.push(
306
- this.#leaf(pluralize.singular(name), String(item), depth + 1)
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) => this.#leaf(itemTag, String(item), ctx.depth + 1));
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
- return `${title}
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 children2 = data.filter((item) => item != null).map((item) => this.#arrayItem(item, ctx.depth + 1));
461
- return [header, ...children2].join("\n");
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
- const children = this.renderEntries(data, {
464
- ...ctx,
465
- depth: ctx.depth + 1
466
- }).join("\n");
467
- return [header, children].join("\n");
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
- return fragments.map((f) => {
672
+ const rendered = [];
673
+ for (const f of this.sanitizeFragments(fragments)) {
489
674
  if (this.isPrimitive(f.data)) {
490
- return `${f.name} = ${this.#formatValue(f.data)}`;
491
- }
492
- if (Array.isArray(f.data)) {
493
- return this.#renderTopLevelArray(f.name, f.data);
494
- }
495
- if (isFragment(f.data)) {
496
- return [
497
- `[${f.name}]`,
498
- this.renderFragment(f.data, { depth: 0, path: [f.name] })
499
- ].join("\n");
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
- const entries = this.#renderObjectEntries(f.data, [f.name]);
502
- return [`[${f.name}]`, ...entries].join("\n");
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, _ctx) {
725
+ renderPrimitive(key, value, ctx) {
726
+ void ctx;
539
727
  return `${key} = ${this.#formatValue(value)}`;
540
728
  }
541
- renderArray(key, items, _ctx) {
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, path) {
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 = [...path, key];
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
- const entries = this.#renderObjectEntries(data, newPath);
596
- return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
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
- return fragments.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
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
- const firstKeys = Object.keys(objects[0]).sort().join(",");
858
+ let intersection = new Set(Object.keys(objects[0]));
666
859
  for (const obj of objects) {
667
- if (Object.keys(obj).sort().join(",") !== firstKeys) {
668
- return false;
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 (!this.#isPrimitiveValue(value) && value !== null) {
863
+ if (value == null) continue;
864
+ if (!this.#isPrimitiveValue(value)) {
672
865
  return false;
673
866
  }
674
867
  }
675
868
  }
676
- return true;
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 = Object.keys(items[0]);
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) => this.#formatValue(obj[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/store/sqlite.store.ts
830
- import { DatabaseSync } from "node:sqlite";
831
- var STORE_DDL = `
832
- -- Chats table
833
- -- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
834
- CREATE TABLE IF NOT EXISTS chats (
835
- id TEXT PRIMARY KEY,
836
- title TEXT,
837
- metadata TEXT,
838
- createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
839
- updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
840
- );
841
-
842
- CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
843
-
844
- -- Messages table (nodes in the DAG)
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 (updates.metadata !== void 0) {
961
- setClauses.push("metadata = ?");
962
- params.push(JSON.stringify(updates.metadata));
1045
+ if (!options.userId) {
1046
+ throw new Error("userId is required");
963
1047
  }
964
- params.push(chatId);
965
- const row = this.#db.prepare(
966
- `UPDATE chats SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`
967
- ).get(...params);
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
- async listChats() {
977
- const rows = this.#db.prepare(
978
- `SELECT
979
- c.id,
980
- c.title,
981
- c.createdAt,
982
- c.updatedAt,
983
- COUNT(DISTINCT m.id) as messageCount,
984
- COUNT(DISTINCT b.id) as branchCount
985
- FROM chats c
986
- LEFT JOIN messages m ON m.chatId = c.id
987
- LEFT JOIN branches b ON b.chatId = c.id
988
- GROUP BY c.id
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
- // Message Operations (Graph Nodes)
1002
- // ==========================================================================
1003
- async addMessage(message2) {
1004
- this.#db.prepare(
1005
- `INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
1006
- VALUES (?, ?, ?, ?, ?, ?, ?)
1007
- ON CONFLICT(id) DO UPDATE SET
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 content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
1022
- this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
1023
- this.#db.prepare(
1024
- `INSERT INTO messages_fts(messageId, chatId, name, content)
1025
- VALUES (?, ?, ?, ?)`
1026
- ).run(message2.id, message2.chatId, message2.name, content);
1027
- }
1028
- async getMessage(messageId) {
1029
- const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
1030
- if (!row) {
1031
- return void 0;
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: row.id,
1035
- chatId: row.chatId,
1036
- parentId: row.parentId,
1037
- name: row.name,
1038
- type: row.type ?? void 0,
1039
- data: JSON.parse(row.data),
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
- async getMessageChain(headId) {
1044
- const rows = this.#db.prepare(
1045
- `WITH RECURSIVE chain AS (
1046
- SELECT *, 0 as depth FROM messages WHERE id = ?
1047
- UNION ALL
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
- // Branch Operations
1072
- // ==========================================================================
1073
- async createBranch(branch) {
1074
- this.#db.prepare(
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
- async getBranch(chatId, name) {
1087
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
1088
- if (!row) {
1089
- return void 0;
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: row.id,
1093
- chatId: row.chatId,
1094
- name: row.name,
1095
- headMessageId: row.headMessageId,
1096
- isActive: row.isActive === 1,
1097
- createdAt: row.createdAt
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
- async getActiveBranch(chatId) {
1101
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
1102
- if (!row) {
1103
- return void 0;
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
- async setActiveBranch(chatId, branchId) {
1115
- this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
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
- async updateBranchHead(branchId, messageId) {
1119
- this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
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
- async listBranches(chatId) {
1122
- const branches = this.#db.prepare(
1123
- `SELECT
1124
- b.id,
1125
- b.name,
1126
- b.headMessageId,
1127
- b.isActive,
1128
- b.createdAt
1129
- FROM branches b
1130
- WHERE b.chatId = ?
1131
- ORDER BY b.createdAt ASC`
1132
- ).all(chatId);
1133
- const result = [];
1134
- for (const branch of branches) {
1135
- let messageCount = 0;
1136
- if (branch.headMessageId) {
1137
- const countRow = this.#db.prepare(
1138
- `WITH RECURSIVE chain AS (
1139
- SELECT id, parentId FROM messages WHERE id = ?
1140
- UNION ALL
1141
- SELECT m.id, m.parentId FROM messages m
1142
- INNER JOIN chain c ON m.id = c.parentId
1143
- )
1144
- SELECT COUNT(*) as count FROM chain`
1145
- ).get(branch.headMessageId);
1146
- messageCount = countRow.count;
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
- return result;
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
- // Checkpoint Operations
1161
- // ==========================================================================
1162
- async createCheckpoint(checkpoint) {
1163
- this.#db.prepare(
1164
- `INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
1165
- VALUES (?, ?, ?, ?, ?)
1166
- ON CONFLICT(chatId, name) DO UPDATE SET
1167
- messageId = excluded.messageId,
1168
- createdAt = excluded.createdAt`
1169
- ).run(
1170
- checkpoint.id,
1171
- checkpoint.chatId,
1172
- checkpoint.name,
1173
- checkpoint.messageId,
1174
- checkpoint.createdAt
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
- async getCheckpoint(chatId, name) {
1178
- const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
1179
- if (!row) {
1180
- return void 0;
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
- id: row.id,
1184
- chatId: row.chatId,
1185
- name: row.name,
1186
- messageId: row.messageId,
1187
- createdAt: row.createdAt
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
- * Initialize the chat and branch if they don't exist.
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 #ensureInitialized() {
1390
- if (this.#initialized) {
1391
- return;
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
- this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
1394
- const existingBranch = await this.#store.getBranch(
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.#initialized = true;
1340
+ return this.#createBranchFrom(messageId, true);
1412
1341
  }
1413
1342
  /**
1414
- * Create a new branch from a specific message.
1415
- * Shared logic between rewind() and btw().
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 #createBranchFrom(messageId, switchTo) {
1418
- const branches = await this.#store.listBranches(this.#chatId);
1419
- const samePrefix = branches.filter(
1420
- (b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
1421
- );
1422
- const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
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: newBranchName,
1427
- headMessageId: messageId,
1428
- isActive: false,
1369
+ name,
1370
+ messageId: this.#branch.headMessageId,
1429
1371
  createdAt: Date.now()
1430
1372
  };
1431
- await this.#store.createBranch(newBranch);
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: newBranch.id,
1441
- name: newBranch.name,
1442
- headMessageId: newBranch.headMessageId,
1443
- isActive: switchTo,
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
- * Get the current chat ID.
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
- * - Initializes chat and branch if needed
1503
- * - Loads message history from the graph (walking parent chain)
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
- * const context = new ContextEngine({ store, chatId: 'chat-1' })
1510
- * .set(role('You are helpful'), user('Hello'));
1389
+ * // User chose cooking, but wants to try coding path
1390
+ * await context.restore('before-choice');
1511
1391
  *
1512
- * const { systemPrompt, messages } = await context.resolve();
1513
- * await generateText({ system: systemPrompt, messages });
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 resolve(options) {
1397
+ async restore(name) {
1517
1398
  await this.#ensureInitialized();
1518
- const systemPrompt = options.renderer.render(this.#fragments);
1519
- const messages = [];
1520
- if (this.#branch?.headMessageId) {
1521
- const chain = await this.#store.getMessageChain(
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 { systemPrompt, messages };
1405
+ return this.rewind(checkpoint.messageId);
1533
1406
  }
1534
1407
  /**
1535
- * Save pending messages to the graph.
1408
+ * Switch to a different branch by name.
1536
1409
  *
1537
- * Each message is added as a node with parentId pointing to the previous message.
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
- * context.set(user('Hello'));
1543
- * // AI responds...
1544
- * context.set(assistant('Hi there!'));
1545
- * await context.save(); // Persist to graph
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 save() {
1422
+ async switchBranch(name) {
1549
1423
  await this.#ensureInitialized();
1550
- if (this.#pendingMessages.length === 0) {
1551
- return;
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.updateBranchHead(this.#branch.id, parentId);
1569
- this.#branch.headMessageId = parentId;
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
- * Estimate token count and cost for the full context.
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
- * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
1581
- * @param options - Optional settings
1582
- * @returns Estimate result with token counts, costs, and per-fragment breakdown
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
- * Creates a new branch from that message, preserving the original branch.
1654
- * The new branch becomes active.
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
- * @param messageId - The message ID to rewind to
1657
- * @returns The new branch info
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
- * context.set(user('What is 2 + 2?', { id: 'q1' }));
1662
- * context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
1450
+ * // User asked a question, model is generating...
1451
+ * context.set(user('What is the weather?'));
1663
1452
  * await context.save();
1664
1453
  *
1665
- * // Rewind to the question, creates new branch
1666
- * const newBranch = await context.rewind('q1');
1454
+ * // User wants to ask something else without waiting
1455
+ * const newBranch = await context.btw();
1456
+ * // newBranch = { name: 'main-v2', ... }
1667
1457
  *
1668
- * // Now add correct answer on new branch
1669
- * context.set(assistant('The answer is 4.'));
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 rewind(messageId) {
1464
+ async btw() {
1674
1465
  await this.#ensureInitialized();
1675
- const message2 = await this.#store.getMessage(messageId);
1676
- if (!message2) {
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(messageId, true);
1469
+ return this.#createBranchFrom(this.#branch.headMessageId, false);
1683
1470
  }
1684
1471
  /**
1685
- * Create a checkpoint at the current position.
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 name - Name for the checkpoint
1691
- * @returns The checkpoint info
1474
+ * @param updates - Partial metadata to merge (title, metadata)
1692
1475
  *
1693
1476
  * @example
1694
1477
  * ```ts
1695
- * context.set(user('I want to learn a new skill.'));
1696
- * context.set(assistant('Would you like coding or cooking?'));
1697
- * await context.save();
1698
- *
1699
- * // Save checkpoint before user's choice
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 checkpoint(name) {
1484
+ async updateChat(updates) {
1704
1485
  await this.#ensureInitialized();
1705
- if (!this.#branch?.headMessageId) {
1706
- throw new Error("Cannot create checkpoint: no messages in conversation");
1486
+ const storeUpdates = {};
1487
+ if (updates.title !== void 0) {
1488
+ storeUpdates.title = updates.title;
1707
1489
  }
1708
- const checkpoint = {
1709
- id: crypto.randomUUID(),
1710
- chatId: this.#chatId,
1711
- name,
1712
- messageId: this.#branch.headMessageId,
1713
- createdAt: Date.now()
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
- * Restore to a checkpoint by creating a new branch from that point.
1499
+ * Consolidate context fragments (no-op for now).
1725
1500
  *
1726
- * @param name - Name of the checkpoint to restore
1727
- * @returns The new branch info
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
- * // User chose cooking, but wants to try coding path
1732
- * await context.restore('before-choice');
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
- * context.set(user('I want to learn coding.'));
1735
- * context.set(assistant('Python is a great starting language!'));
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 restore(name) {
1528
+ async inspect(options) {
1740
1529
  await this.#ensureInitialized();
1741
- const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
1742
- if (!checkpoint) {
1743
- throw new Error(
1744
- `Checkpoint "${name}" not found in chat "${this.#chatId}"`
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 this.rewind(checkpoint.messageId);
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
- * Switch to a different branch by name.
1751
- *
1752
- * @param name - Branch name to switch to
1753
- *
1754
- * @example
1755
- * ```ts
1756
- * // List branches (via store)
1757
- * const branches = await store.listBranches(context.chatId);
1758
- * console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
1759
- *
1760
- * // Switch to original branch
1761
- * await context.switchBranch('main');
1762
- * ```
1763
- */
1764
- async switchBranch(name) {
1765
- await this.#ensureInitialized();
1766
- const branch = await this.#store.getBranch(this.#chatId, name);
1767
- if (!branch) {
1768
- throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
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
- await this.#store.setActiveBranch(this.#chatId, branch.id);
1771
- this.#branch = { ...branch, isActive: true };
1772
- this.#branchName = name;
1773
- this.#pendingMessages = [];
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
- * Create a parallel branch from the current position ("by the way").
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
- * Unlike rewind(), this method:
1783
- * - Uses the current HEAD (no messageId needed)
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
- * ```ts
1792
- * // User asked a question, model is generating...
1793
- * context.set(user('What is the weather?'));
1794
- * await context.save();
3372
+ * ```typescript
3373
+ * const stream = await agent.stream({});
1795
3374
  *
1796
- * // User wants to ask something else without waiting
1797
- * const newBranch = await context.btw();
1798
- * // newBranch = { name: 'main-v2', ... }
3375
+ * // With guardrails - use toUIMessageStream for protection
3376
+ * await printer.readableStream(stream.toUIMessageStream());
1799
3377
  *
1800
- * // Later, switch to the new branch and add the question
1801
- * await context.switchBranch(newBranch.name);
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 btw() {
1807
- await this.#ensureInitialized();
1808
- if (!this.#branch?.headMessageId) {
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
- return this.#createBranchFrom(this.#branch.headMessageId, false);
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
- if (updates.metadata !== void 0) {
1833
- storeUpdates.metadata = {
1834
- ...this.#chatData?.metadata,
1835
- ...updates.metadata
1836
- };
3389
+ const result = await this.#createRawStream(contextVariables, config);
3390
+ if (this.#guardrails.length === 0) {
3391
+ return result;
1837
3392
  }
1838
- this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
3393
+ return this.#wrapWithGuardrails(result, contextVariables, config);
1839
3394
  }
1840
3395
  /**
1841
- * Consolidate context fragments (no-op for now).
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
- consolidate() {
1849
- return void 0;
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
- * Inspect the full context state for debugging.
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
- * @example
1859
- * ```ts
1860
- * const inspection = await context.inspect({
1861
- * modelId: 'openai:gpt-4o',
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
- async inspect(options) {
1871
- await this.#ensureInitialized();
1872
- const { renderer } = options;
1873
- const estimateResult = await this.estimate(options.modelId, { renderer });
1874
- const rendered = renderer.render(this.#fragments);
1875
- const persistedMessages = [];
1876
- if (this.#branch?.headMessageId) {
1877
- const chain = await this.#store.getMessageChain(
1878
- this.#branch.headMessageId
1879
- );
1880
- persistedMessages.push(...chain);
1881
- }
1882
- const graph = await this.#store.getGraph(this.#chatId);
1883
- return {
1884
- estimate: estimateResult,
1885
- rendered,
1886
- fragments: {
1887
- context: [...this.#fragments],
1888
- pending: [...this.#pendingMessages],
1889
- persisted: persistedMessages
1890
- },
1891
- graph,
1892
- meta: {
1893
- chatId: this.#chatId,
1894
- branch: this.#branchName,
1895
- timestamp: Date.now()
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 hint(text) {
1901
- return {
1902
- name: "hint",
1903
- data: text
1904
- };
3519
+ function agent(options) {
3520
+ return new Agent(options);
1905
3521
  }
1906
- function fragment(name, ...children) {
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
- id: message2.id,
1926
- name: "user",
1927
- data: "content",
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
- function message(content) {
1958
- const message2 = typeof content === "string" ? {
1959
- id: generateId(),
1960
- role: "user",
1961
- parts: [{ type: "text", text: content }]
1962
- } : content;
1963
- return {
1964
- id: message2.id,
1965
- name: "message",
1966
- data: "content",
1967
- type: "message",
1968
- persist: true,
1969
- codec: {
1970
- decode() {
1971
- return message2;
1972
- },
1973
- encode() {
1974
- return message2;
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
- function assistantText(content, options) {
1980
- const id = options?.id ?? crypto.randomUUID();
1981
- return assistant({
1982
- id,
1983
- role: "assistant",
1984
- parts: [{ type: "text", text: content }]
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
- visualizeGraph
3680
+ userContext,
3681
+ visualizeGraph,
3682
+ workflow
2009
3683
  };
2010
3684
  //# sourceMappingURL=index.js.map