@deepagents/context 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -151,8 +151,10 @@ var Agent = class _Agent {
151
151
  providerOptions: this.#options.providerOptions,
152
152
  model: this.#options.model,
153
153
  system: systemPrompt,
154
- messages: await convertToModelMessages(messages),
155
- stopWhen: stepCountIs(25),
154
+ messages: await convertToModelMessages(messages, {
155
+ ignoreIncompleteToolCalls: true
156
+ }),
157
+ stopWhen: stepCountIs(200),
156
158
  tools: this.#options.tools,
157
159
  experimental_context: contextVariables,
158
160
  experimental_repairToolCall: createRepairToolCall(this.#options.model),
@@ -218,9 +220,11 @@ var Agent = class _Agent {
218
220
  providerOptions: this.#options.providerOptions,
219
221
  model,
220
222
  system: systemPrompt,
221
- messages: await convertToModelMessages(messages),
223
+ messages: await convertToModelMessages(messages, {
224
+ ignoreIncompleteToolCalls: true
225
+ }),
222
226
  experimental_repairToolCall: createRepairToolCall(model),
223
- stopWhen: stepCountIs(50),
227
+ stopWhen: stepCountIs(200),
224
228
  experimental_transform: config?.transform ?? smoothStream(),
225
229
  tools: this.#options.tools,
226
230
  experimental_context: contextVariables,
@@ -360,8 +364,10 @@ function structuredOutput(options) {
360
364
  providerOptions: options.providerOptions,
361
365
  model: options.model,
362
366
  system: systemPrompt,
363
- messages: await convertToModelMessages(messages),
364
- stopWhen: stepCountIs(25),
367
+ messages: await convertToModelMessages(messages, {
368
+ ignoreIncompleteToolCalls: true
369
+ }),
370
+ stopWhen: stepCountIs(200),
365
371
  experimental_repairToolCall: createRepairToolCall(options.model),
366
372
  experimental_context: contextVariables,
367
373
  output: Output.object({ schema: options.schema }),
@@ -385,8 +391,10 @@ function structuredOutput(options) {
385
391
  model: options.model,
386
392
  system: systemPrompt,
387
393
  experimental_repairToolCall: createRepairToolCall(options.model),
388
- messages: await convertToModelMessages(messages),
389
- stopWhen: stepCountIs(50),
394
+ messages: await convertToModelMessages(messages, {
395
+ ignoreIncompleteToolCalls: true
396
+ }),
397
+ stopWhen: stepCountIs(200),
390
398
  experimental_transform: config?.transform ?? smoothStream(),
391
399
  experimental_context: contextVariables,
392
400
  output: Output.object({ schema: options.schema }),
@@ -591,432 +599,95 @@ async function estimate(modelId, renderer, ...fragments) {
591
599
  }
592
600
 
593
601
  // packages/context/src/lib/fragments/message/user.ts
594
- import { generateId as generateId3, isTextUIPart } from "ai";
595
- function everyNTurns(n) {
596
- return ({ turn }) => turn % n === 0;
597
- }
598
- function once() {
599
- return ({ turn }) => turn === 1;
600
- }
601
- function firstN(n) {
602
- return ({ turn }) => turn <= n;
603
- }
604
- function afterTurn(n) {
605
- return ({ turn }) => turn > n;
606
- }
607
- function and(...predicates) {
608
- return (ctx) => predicates.every((p) => p(ctx));
609
- }
610
- function or(...predicates) {
611
- return (ctx) => predicates.some((p) => p(ctx));
612
- }
613
- function not(predicate) {
614
- return (ctx) => !predicate(ctx);
615
- }
616
- function contentIncludes(keywords) {
617
- const lower = keywords.map((k) => k.toLowerCase());
618
- return (ctx) => {
619
- const text = ctx.content.toLowerCase();
620
- return lower.some((kw) => text.includes(kw));
621
- };
622
- }
623
- function contentPattern(pattern) {
624
- return (ctx) => {
625
- pattern.lastIndex = 0;
626
- return pattern.test(ctx.content);
627
- };
628
- }
629
- function toDateParts(date, tz) {
630
- const parts = new Intl.DateTimeFormat("en-CA", {
631
- timeZone: tz,
632
- year: "numeric",
633
- month: "2-digit",
634
- day: "2-digit",
635
- hour: "2-digit",
636
- hourCycle: "h23"
637
- }).formatToParts(date);
638
- const get = (type) => parts.find((p) => p.type === type).value;
639
- return {
640
- year: get("year"),
641
- month: get("month"),
642
- day: get("day"),
643
- hour: get("hour")
644
- };
645
- }
646
- function temporalChanged(ctx, tz, getKey) {
647
- if (ctx.lastMessageAt === void 0) return true;
648
- const nowParts = toDateParts(/* @__PURE__ */ new Date(), tz);
649
- const prevParts = toDateParts(new Date(ctx.lastMessageAt), tz);
650
- return getKey(nowParts) !== getKey(prevParts);
651
- }
652
- function getSeason(month) {
653
- if (month >= 2 && month <= 4) return "Spring";
654
- if (month >= 5 && month <= 7) return "Summer";
655
- if (month >= 8 && month <= 10) return "Fall";
656
- return "Winter";
657
- }
658
- function isoWeekKey(parts) {
659
- const d = new Date(
660
- Date.UTC(
661
- parseInt(parts.year),
662
- parseInt(parts.month) - 1,
663
- parseInt(parts.day)
664
- )
665
- );
666
- d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
667
- const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
668
- const weekNo = Math.ceil(
669
- ((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
670
- );
671
- return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
672
- }
673
- function dayChanged(tz = "UTC") {
674
- return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}-${p.day}`);
675
- }
676
- function hourChanged(tz = "UTC") {
677
- return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}-${p.day}-${p.hour}`);
678
- }
679
- function monthChanged(tz = "UTC") {
680
- return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}`);
681
- }
682
- function yearChanged(tz = "UTC") {
683
- return (ctx) => temporalChanged(ctx, tz, (p) => p.year);
684
- }
685
- function seasonChanged(tz = "UTC") {
686
- return (ctx) => temporalChanged(ctx, tz, (p) => getSeason(parseInt(p.month) - 1));
687
- }
688
- function weekChanged(tz = "UTC") {
689
- return (ctx) => temporalChanged(ctx, tz, isoWeekKey);
690
- }
691
- function isConditionalReminder(fragment2) {
692
- return fragment2.name === "reminder" && !!fragment2.metadata?.reminder;
693
- }
694
- function getConditionalReminder(fragment2) {
695
- return fragment2.metadata.reminder;
696
- }
697
- var SYSTEM_REMINDER_OPEN_TAG = "<system-reminder>";
698
- var SYSTEM_REMINDER_CLOSE_TAG = "</system-reminder>";
699
- function getReminderRanges(metadata) {
700
- return metadata?.reminders ?? [];
701
- }
702
- function stripTextByRanges(text, ranges) {
703
- if (ranges.length === 0) {
704
- return text;
602
+ import { generateId as generateId3 } from "ai";
603
+
604
+ // packages/context/src/lib/renderers/abstract.renderer.ts
605
+ import pluralize from "pluralize";
606
+ import { titlecase } from "stringcase";
607
+ var ContextRenderer = class {
608
+ options;
609
+ constructor(options = {}) {
610
+ this.options = options;
705
611
  }
706
- const normalized = ranges.map((range) => ({
707
- start: Math.max(0, Math.min(text.length, range.start)),
708
- end: Math.max(0, Math.min(text.length, range.end))
709
- })).filter((range) => range.end > range.start).sort((a, b) => a.start - b.start);
710
- if (normalized.length === 0) {
711
- return text;
612
+ /**
613
+ * Check if data is a primitive (string, number, boolean).
614
+ */
615
+ isPrimitive(data) {
616
+ return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
712
617
  }
713
- let cursor = 0;
714
- let output = "";
715
- for (const range of normalized) {
716
- if (range.start < cursor) {
717
- if (range.end > cursor) {
718
- cursor = range.end;
618
+ /**
619
+ * Group fragments by name for groupFragments option.
620
+ */
621
+ groupByName(fragments) {
622
+ const groups = /* @__PURE__ */ new Map();
623
+ for (const fragment2 of fragments) {
624
+ const existing = groups.get(fragment2.name) ?? [];
625
+ existing.push(fragment2);
626
+ groups.set(fragment2.name, existing);
627
+ }
628
+ return groups;
629
+ }
630
+ /**
631
+ * Remove null/undefined from fragments and fragment data recursively.
632
+ * This protects renderers from nullish values and ensures they are ignored
633
+ * consistently across all output formats.
634
+ */
635
+ sanitizeFragments(fragments) {
636
+ const sanitized = [];
637
+ for (const fragment2 of fragments) {
638
+ const cleaned = this.sanitizeFragment(fragment2, /* @__PURE__ */ new WeakSet());
639
+ if (cleaned) {
640
+ sanitized.push(cleaned);
719
641
  }
720
- continue;
721
642
  }
722
- output += text.slice(cursor, range.start);
723
- cursor = range.end;
643
+ return sanitized;
724
644
  }
725
- output += text.slice(cursor);
726
- return output.trimEnd();
727
- }
728
- function stripReminders(message2) {
729
- const reminderRanges = getReminderRanges(
730
- isRecord(message2.metadata) ? message2.metadata : void 0
731
- );
732
- const rangesByPartIndex = /* @__PURE__ */ new Map();
733
- for (const range of reminderRanges) {
734
- const partRanges = rangesByPartIndex.get(range.partIndex) ?? [];
735
- partRanges.push({ start: range.start, end: range.end });
736
- rangesByPartIndex.set(range.partIndex, partRanges);
645
+ sanitizeFragment(fragment2, seen) {
646
+ const data = this.sanitizeData(getFragmentData(fragment2), seen);
647
+ if (data == null) {
648
+ return null;
649
+ }
650
+ return {
651
+ ...fragment2,
652
+ data
653
+ };
737
654
  }
738
- const strippedParts = message2.parts.flatMap((part, partIndex) => {
739
- const clonedPart = { ...part };
740
- const ranges = rangesByPartIndex.get(partIndex);
741
- if (clonedPart.type !== "text" || ranges === void 0) {
742
- return [clonedPart];
655
+ sanitizeData(data, seen) {
656
+ if (data == null) {
657
+ return void 0;
743
658
  }
744
- const strippedText = stripTextByRanges(clonedPart.text, ranges);
745
- if (strippedText.length === 0) {
746
- return [];
659
+ if (isFragment(data)) {
660
+ return this.sanitizeFragment(data, seen) ?? void 0;
747
661
  }
748
- return [{ ...clonedPart, text: strippedText }];
749
- });
750
- const nextMessage = {
751
- ...message2,
752
- parts: strippedParts
753
- };
754
- if (isRecord(message2.metadata)) {
755
- const metadata = { ...message2.metadata };
756
- delete metadata.reminders;
757
- if (Object.keys(metadata).length > 0) {
758
- nextMessage.metadata = metadata;
759
- } else {
760
- delete nextMessage.metadata;
662
+ if (Array.isArray(data)) {
663
+ if (seen.has(data)) {
664
+ return void 0;
665
+ }
666
+ seen.add(data);
667
+ const cleaned = [];
668
+ for (const item of data) {
669
+ const sanitizedItem = this.sanitizeData(item, seen);
670
+ if (sanitizedItem != null) {
671
+ cleaned.push(sanitizedItem);
672
+ }
673
+ }
674
+ return cleaned;
761
675
  }
762
- }
763
- return nextMessage;
764
- }
765
- function extractPlainText(message2) {
766
- return message2.parts.filter(isTextUIPart).map((part) => part.text).join(" ");
767
- }
768
- function isRecord(value) {
769
- return typeof value === "object" && value !== null && !Array.isArray(value);
770
- }
771
- function assertReminderText(text) {
772
- if (text.trim().length === 0) {
773
- throw new Error("Reminder text must not be empty");
774
- }
775
- }
776
- function formatTaggedReminder(text) {
777
- return `${SYSTEM_REMINDER_OPEN_TAG}${text}${SYSTEM_REMINDER_CLOSE_TAG}`;
778
- }
779
- function findLastTextPartIndex(message2) {
780
- for (let i = message2.parts.length - 1; i >= 0; i--) {
781
- if (message2.parts[i].type === "text") {
782
- return i;
676
+ if (isFragmentObject(data)) {
677
+ if (seen.has(data)) {
678
+ return void 0;
679
+ }
680
+ seen.add(data);
681
+ const cleaned = {};
682
+ for (const [key, value] of Object.entries(data)) {
683
+ const sanitizedValue = this.sanitizeData(value, seen);
684
+ if (sanitizedValue != null) {
685
+ cleaned[key] = sanitizedValue;
686
+ }
687
+ }
688
+ return cleaned;
783
689
  }
784
- }
785
- return void 0;
786
- }
787
- function ensureTextPart(message2) {
788
- const existingIndex = findLastTextPartIndex(message2);
789
- if (existingIndex !== void 0) {
790
- return existingIndex;
791
- }
792
- const reminderPart = {
793
- type: "text",
794
- text: ""
795
- };
796
- message2.parts.push(reminderPart);
797
- return message2.parts.length - 1;
798
- }
799
- function applyInlineReminder(message2, value) {
800
- const partIndex = ensureTextPart(message2);
801
- const textPart = message2.parts[partIndex];
802
- if (textPart.type !== "text") {
803
- throw new Error("Failed to resolve text part for inline reminder");
804
- }
805
- const reminderText = formatTaggedReminder(value);
806
- const start = textPart.text.length;
807
- const updatedText = `${textPart.text}${reminderText}`;
808
- message2.parts[partIndex] = { ...textPart, text: updatedText };
809
- return {
810
- id: generateId3(),
811
- text: value,
812
- partIndex,
813
- start,
814
- end: start + reminderText.length,
815
- mode: "inline"
816
- };
817
- }
818
- function applyPartReminder(message2, value) {
819
- const part = { type: "text", text: value };
820
- message2.parts.push(part);
821
- const partIndex = message2.parts.length - 1;
822
- return {
823
- id: generateId3(),
824
- text: value,
825
- partIndex,
826
- start: 0,
827
- end: value.length,
828
- mode: "part"
829
- };
830
- }
831
- function resolveReminderText(item, ctx) {
832
- return resolveReminder(item, ctx)?.text ?? "";
833
- }
834
- function normalizeReminderResolution(value) {
835
- if (typeof value === "string") {
836
- return value.trim().length === 0 ? null : { text: value };
837
- }
838
- if (value.text.trim().length === 0) {
839
- return null;
840
- }
841
- return value;
842
- }
843
- function resolveReminder(item, ctx) {
844
- const resolvedText = typeof item.text === "function" ? item.text(ctx) : item.text;
845
- const resolved = normalizeReminderResolution(resolvedText);
846
- if (!resolved) {
847
- return null;
848
- }
849
- const metadata = item.metadata || resolved.metadata ? {
850
- ...item.metadata ?? {},
851
- ...resolved.metadata ?? {}
852
- } : void 0;
853
- return metadata ? { ...resolved, metadata } : resolved;
854
- }
855
- function mergeMessageMetadata(message2, addedMetadata) {
856
- if (Object.keys(addedMetadata).length === 0) {
857
- return;
858
- }
859
- const metadata = isRecord(message2.metadata) ? { ...message2.metadata } : {};
860
- message2.metadata = { ...metadata, ...addedMetadata };
861
- }
862
- function applyReminderToMessage(message2, item, ctx) {
863
- const resolved = resolveReminder(item, ctx);
864
- if (!resolved) {
865
- return null;
866
- }
867
- if (resolved.metadata) {
868
- mergeMessageMetadata(message2, resolved.metadata);
869
- }
870
- return item.asPart ? applyPartReminder(message2, resolved.text) : applyInlineReminder(message2, resolved.text);
871
- }
872
- function mergeReminderMetadata(message2, addedReminders) {
873
- if (addedReminders.length === 0) return;
874
- const metadata = isRecord(message2.metadata) ? { ...message2.metadata } : {};
875
- const existing = Array.isArray(metadata.reminders) ? metadata.reminders : [];
876
- metadata.reminders = [...existing, ...addedReminders];
877
- message2.metadata = metadata;
878
- }
879
- function reminder(text, options) {
880
- if (typeof text === "string") {
881
- assertReminderText(text);
882
- }
883
- if (options && "when" in options && options.when) {
884
- return {
885
- name: "reminder",
886
- metadata: {
887
- reminder: {
888
- text,
889
- when: options.when,
890
- asPart: options.asPart ?? false
891
- }
892
- }
893
- };
894
- }
895
- return {
896
- text,
897
- asPart: options?.asPart ?? false
898
- };
899
- }
900
- function user(content, ...reminders) {
901
- const message2 = typeof content === "string" ? {
902
- id: generateId3(),
903
- role: "user",
904
- parts: [{ type: "text", text: content }]
905
- } : { ...content, role: "user", parts: [...content.parts] };
906
- if (reminders.length > 0) {
907
- const plainText = extractPlainText(message2);
908
- const added = [];
909
- for (const item of reminders) {
910
- const meta = applyReminderToMessage(message2, item, {
911
- content: plainText
912
- });
913
- if (meta) added.push(meta);
914
- }
915
- mergeReminderMetadata(message2, added);
916
- }
917
- return {
918
- id: message2.id,
919
- name: "user",
920
- type: "message",
921
- persist: true,
922
- codec: {
923
- decode() {
924
- return message2;
925
- },
926
- encode() {
927
- return message2;
928
- }
929
- }
930
- };
931
- }
932
-
933
- // packages/context/src/lib/renderers/abstract.renderer.ts
934
- import pluralize from "pluralize";
935
- import { titlecase } from "stringcase";
936
- var ContextRenderer = class {
937
- options;
938
- constructor(options = {}) {
939
- this.options = options;
940
- }
941
- /**
942
- * Check if data is a primitive (string, number, boolean).
943
- */
944
- isPrimitive(data) {
945
- return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
946
- }
947
- /**
948
- * Group fragments by name for groupFragments option.
949
- */
950
- groupByName(fragments) {
951
- const groups = /* @__PURE__ */ new Map();
952
- for (const fragment2 of fragments) {
953
- const existing = groups.get(fragment2.name) ?? [];
954
- existing.push(fragment2);
955
- groups.set(fragment2.name, existing);
956
- }
957
- return groups;
958
- }
959
- /**
960
- * Remove null/undefined from fragments and fragment data recursively.
961
- * This protects renderers from nullish values and ensures they are ignored
962
- * consistently across all output formats.
963
- */
964
- sanitizeFragments(fragments) {
965
- const sanitized = [];
966
- for (const fragment2 of fragments) {
967
- const cleaned = this.sanitizeFragment(fragment2, /* @__PURE__ */ new WeakSet());
968
- if (cleaned) {
969
- sanitized.push(cleaned);
970
- }
971
- }
972
- return sanitized;
973
- }
974
- sanitizeFragment(fragment2, seen) {
975
- const data = this.sanitizeData(getFragmentData(fragment2), seen);
976
- if (data == null) {
977
- return null;
978
- }
979
- return {
980
- ...fragment2,
981
- data
982
- };
983
- }
984
- sanitizeData(data, seen) {
985
- if (data == null) {
986
- return void 0;
987
- }
988
- if (isFragment(data)) {
989
- return this.sanitizeFragment(data, seen) ?? void 0;
990
- }
991
- if (Array.isArray(data)) {
992
- if (seen.has(data)) {
993
- return void 0;
994
- }
995
- seen.add(data);
996
- const cleaned = [];
997
- for (const item of data) {
998
- const sanitizedItem = this.sanitizeData(item, seen);
999
- if (sanitizedItem != null) {
1000
- cleaned.push(sanitizedItem);
1001
- }
1002
- }
1003
- return cleaned;
1004
- }
1005
- if (isFragmentObject(data)) {
1006
- if (seen.has(data)) {
1007
- return void 0;
1008
- }
1009
- seen.add(data);
1010
- const cleaned = {};
1011
- for (const [key, value] of Object.entries(data)) {
1012
- const sanitizedValue = this.sanitizeData(value, seen);
1013
- if (sanitizedValue != null) {
1014
- cleaned[key] = sanitizedValue;
1015
- }
1016
- }
1017
- return cleaned;
1018
- }
1019
- return data;
690
+ return data;
1020
691
  }
1021
692
  /**
1022
693
  * Template method - dispatches value to appropriate handler.
@@ -1806,24 +1477,391 @@ ${entries}`;
1806
1477
  }
1807
1478
  };
1808
1479
 
1809
- // packages/context/src/lib/store/store.ts
1810
- var ContextStore = class {
1811
- };
1480
+ // packages/context/src/lib/text.ts
1481
+ import { isTextUIPart } from "ai";
1482
+ function getTextParts(message2) {
1483
+ return message2.parts.filter(isTextUIPart).map((part) => part.text);
1484
+ }
1485
+ function extractPlainText(message2) {
1486
+ return getTextParts(message2).join(" ");
1487
+ }
1812
1488
 
1813
- // packages/context/src/lib/engine.ts
1814
- function estimateMessageContent(data) {
1815
- return typeof data === "string" ? data : JSON.stringify(data);
1489
+ // packages/context/src/lib/fragments/message/user.ts
1490
+ function everyNTurns(n) {
1491
+ return ({ turn }) => turn % n === 0;
1816
1492
  }
1817
- var ContextEngine = class {
1818
- /** Non-message fragments (role, hints, etc.) - not persisted in graph */
1819
- #fragments = [];
1820
- /** Pending message fragments to be added to graph */
1821
- #pendingMessages = [];
1822
- #store;
1823
- #chatId;
1824
- #userId;
1825
- #branchName;
1826
- #branch = null;
1493
+ function once() {
1494
+ return ({ turn }) => turn === 1;
1495
+ }
1496
+ function firstN(n) {
1497
+ return ({ turn }) => turn <= n;
1498
+ }
1499
+ function afterTurn(n) {
1500
+ return ({ turn }) => turn > n;
1501
+ }
1502
+ function and(...predicates) {
1503
+ return async (ctx) => {
1504
+ for (const it of predicates) {
1505
+ if (!await it(ctx)) return false;
1506
+ }
1507
+ return true;
1508
+ };
1509
+ }
1510
+ function or(...predicates) {
1511
+ return async (ctx) => {
1512
+ for (const it of predicates) {
1513
+ if (await it(ctx)) return true;
1514
+ }
1515
+ return false;
1516
+ };
1517
+ }
1518
+ function not(predicate) {
1519
+ return async (ctx) => !await predicate(ctx);
1520
+ }
1521
+ function contentIncludes(keywords) {
1522
+ const lower = keywords.map((k) => k.toLowerCase());
1523
+ return (ctx) => {
1524
+ const text = ctx.content.toLowerCase();
1525
+ return lower.some((kw) => text.includes(kw));
1526
+ };
1527
+ }
1528
+ function contentPattern(pattern) {
1529
+ return (ctx) => {
1530
+ pattern.lastIndex = 0;
1531
+ return pattern.test(ctx.content);
1532
+ };
1533
+ }
1534
+ function toDateParts(date, tz) {
1535
+ const parts = new Intl.DateTimeFormat("en-CA", {
1536
+ timeZone: tz,
1537
+ year: "numeric",
1538
+ month: "2-digit",
1539
+ day: "2-digit",
1540
+ hour: "2-digit",
1541
+ hourCycle: "h23"
1542
+ }).formatToParts(date);
1543
+ const get = (type) => parts.find((p) => p.type === type).value;
1544
+ return {
1545
+ year: get("year"),
1546
+ month: get("month"),
1547
+ day: get("day"),
1548
+ hour: get("hour")
1549
+ };
1550
+ }
1551
+ function temporalChanged(ctx, tz, getKey) {
1552
+ if (ctx.lastMessageAt === void 0) return true;
1553
+ const nowParts = toDateParts(/* @__PURE__ */ new Date(), tz);
1554
+ const prevParts = toDateParts(new Date(ctx.lastMessageAt), tz);
1555
+ return getKey(nowParts) !== getKey(prevParts);
1556
+ }
1557
+ function getSeason(month) {
1558
+ if (month >= 2 && month <= 4) return "Spring";
1559
+ if (month >= 5 && month <= 7) return "Summer";
1560
+ if (month >= 8 && month <= 10) return "Fall";
1561
+ return "Winter";
1562
+ }
1563
+ function isoWeekKey(parts) {
1564
+ const d = new Date(
1565
+ Date.UTC(
1566
+ parseInt(parts.year),
1567
+ parseInt(parts.month) - 1,
1568
+ parseInt(parts.day)
1569
+ )
1570
+ );
1571
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
1572
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
1573
+ const weekNo = Math.ceil(
1574
+ ((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
1575
+ );
1576
+ return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
1577
+ }
1578
+ function dayChanged(tz = "UTC") {
1579
+ return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}-${p.day}`);
1580
+ }
1581
+ function hourChanged(tz = "UTC") {
1582
+ return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}-${p.day}-${p.hour}`);
1583
+ }
1584
+ function monthChanged(tz = "UTC") {
1585
+ return (ctx) => temporalChanged(ctx, tz, (p) => `${p.year}-${p.month}`);
1586
+ }
1587
+ function yearChanged(tz = "UTC") {
1588
+ return (ctx) => temporalChanged(ctx, tz, (p) => p.year);
1589
+ }
1590
+ function seasonChanged(tz = "UTC") {
1591
+ return (ctx) => temporalChanged(ctx, tz, (p) => getSeason(parseInt(p.month) - 1));
1592
+ }
1593
+ function weekChanged(tz = "UTC") {
1594
+ return (ctx) => temporalChanged(ctx, tz, isoWeekKey);
1595
+ }
1596
+ function isConditionalReminder(fragment2) {
1597
+ return fragment2.name === "reminder" && !!fragment2.metadata?.reminder;
1598
+ }
1599
+ function getConditionalReminder(fragment2) {
1600
+ return fragment2.metadata.reminder;
1601
+ }
1602
+ var SYSTEM_REMINDER_OPEN_TAG = "<system-reminder>";
1603
+ var SYSTEM_REMINDER_CLOSE_TAG = "</system-reminder>";
1604
+ function getReminderRanges(metadata) {
1605
+ return metadata?.reminders ?? [];
1606
+ }
1607
+ function stripTextByRanges(text, ranges) {
1608
+ if (ranges.length === 0) {
1609
+ return text;
1610
+ }
1611
+ const normalized = ranges.map((range) => ({
1612
+ start: Math.max(0, Math.min(text.length, range.start)),
1613
+ end: Math.max(0, Math.min(text.length, range.end))
1614
+ })).filter((range) => range.end > range.start).sort((a, b) => a.start - b.start);
1615
+ if (normalized.length === 0) {
1616
+ return text;
1617
+ }
1618
+ let cursor = 0;
1619
+ let output = "";
1620
+ for (const range of normalized) {
1621
+ if (range.start < cursor) {
1622
+ if (range.end > cursor) {
1623
+ cursor = range.end;
1624
+ }
1625
+ continue;
1626
+ }
1627
+ output += text.slice(cursor, range.start);
1628
+ cursor = range.end;
1629
+ }
1630
+ output += text.slice(cursor);
1631
+ return output.trimEnd();
1632
+ }
1633
+ function stripReminders(message2) {
1634
+ const reminderRanges = getReminderRanges(
1635
+ isRecord(message2.metadata) ? message2.metadata : void 0
1636
+ );
1637
+ const rangesByPartIndex = /* @__PURE__ */ new Map();
1638
+ for (const range of reminderRanges) {
1639
+ const partRanges = rangesByPartIndex.get(range.partIndex) ?? [];
1640
+ partRanges.push({ start: range.start, end: range.end });
1641
+ rangesByPartIndex.set(range.partIndex, partRanges);
1642
+ }
1643
+ const strippedParts = message2.parts.flatMap((part, partIndex) => {
1644
+ const clonedPart = { ...part };
1645
+ const ranges = rangesByPartIndex.get(partIndex);
1646
+ if (clonedPart.type !== "text" || ranges === void 0) {
1647
+ return [clonedPart];
1648
+ }
1649
+ const strippedText = stripTextByRanges(clonedPart.text, ranges);
1650
+ if (strippedText.length === 0) {
1651
+ return [];
1652
+ }
1653
+ return [{ ...clonedPart, text: strippedText }];
1654
+ });
1655
+ const nextMessage = {
1656
+ ...message2,
1657
+ parts: strippedParts
1658
+ };
1659
+ if (isRecord(message2.metadata)) {
1660
+ const metadata = { ...message2.metadata };
1661
+ delete metadata.reminders;
1662
+ if (Object.keys(metadata).length > 0) {
1663
+ nextMessage.metadata = metadata;
1664
+ } else {
1665
+ delete nextMessage.metadata;
1666
+ }
1667
+ }
1668
+ return nextMessage;
1669
+ }
1670
+ function isRecord(value) {
1671
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1672
+ }
1673
+ function assertReminderText(text) {
1674
+ if (text.trim().length === 0) {
1675
+ throw new Error("Reminder text must not be empty");
1676
+ }
1677
+ }
1678
+ function formatTaggedReminder(text) {
1679
+ return `${SYSTEM_REMINDER_OPEN_TAG}${text}${SYSTEM_REMINDER_CLOSE_TAG}`;
1680
+ }
1681
+ function findLastTextPartIndex(message2) {
1682
+ for (let i = message2.parts.length - 1; i >= 0; i--) {
1683
+ if (message2.parts[i].type === "text") {
1684
+ return i;
1685
+ }
1686
+ }
1687
+ return void 0;
1688
+ }
1689
+ function ensureTextPart(message2) {
1690
+ const existingIndex = findLastTextPartIndex(message2);
1691
+ if (existingIndex !== void 0) {
1692
+ return existingIndex;
1693
+ }
1694
+ const reminderPart = {
1695
+ type: "text",
1696
+ text: ""
1697
+ };
1698
+ message2.parts.push(reminderPart);
1699
+ return message2.parts.length - 1;
1700
+ }
1701
+ function applyInlineReminder(message2, value) {
1702
+ const partIndex = ensureTextPart(message2);
1703
+ const textPart = message2.parts[partIndex];
1704
+ if (textPart.type !== "text") {
1705
+ throw new Error("Failed to resolve text part for inline reminder");
1706
+ }
1707
+ const reminderText = formatTaggedReminder(value);
1708
+ const start = textPart.text.length;
1709
+ const updatedText = `${textPart.text}${reminderText}`;
1710
+ message2.parts[partIndex] = { ...textPart, text: updatedText };
1711
+ return {
1712
+ id: generateId3(),
1713
+ text: value,
1714
+ partIndex,
1715
+ start,
1716
+ end: start + reminderText.length,
1717
+ mode: "inline"
1718
+ };
1719
+ }
1720
+ function applyPartReminder(message2, value) {
1721
+ const part = { type: "text", text: value };
1722
+ message2.parts.push(part);
1723
+ const partIndex = message2.parts.length - 1;
1724
+ return {
1725
+ id: generateId3(),
1726
+ text: value,
1727
+ partIndex,
1728
+ start: 0,
1729
+ end: value.length,
1730
+ mode: "part"
1731
+ };
1732
+ }
1733
+ function resolveReminderText(item, ctx) {
1734
+ return resolveReminder(item, ctx)?.text ?? "";
1735
+ }
1736
+ function normalizeReminderResolution(value) {
1737
+ if (typeof value === "string") {
1738
+ return value.trim().length === 0 ? null : { text: value };
1739
+ }
1740
+ if (value.text.trim().length === 0) {
1741
+ return null;
1742
+ }
1743
+ return value;
1744
+ }
1745
+ function resolveReminder(item, ctx) {
1746
+ const resolvedText = typeof item.text === "function" ? item.text(ctx) : item.text;
1747
+ const resolved = normalizeReminderResolution(resolvedText);
1748
+ if (!resolved) {
1749
+ return null;
1750
+ }
1751
+ const metadata = item.metadata || resolved.metadata ? {
1752
+ ...item.metadata ?? {},
1753
+ ...resolved.metadata ?? {}
1754
+ } : void 0;
1755
+ return metadata ? { ...resolved, metadata } : resolved;
1756
+ }
1757
+ async function resolveReminderAsync(item, ctx) {
1758
+ const text = await (typeof item.text === "function" ? item.text(ctx) : item.text);
1759
+ const resolved = normalizeReminderResolution(text);
1760
+ if (!resolved) return null;
1761
+ const metadata = item.metadata || resolved.metadata ? { ...item.metadata ?? {}, ...resolved.metadata ?? {} } : void 0;
1762
+ return metadata ? { ...resolved, metadata } : resolved;
1763
+ }
1764
+ function mergeMessageMetadata(message2, addedMetadata) {
1765
+ if (Object.keys(addedMetadata).length === 0) {
1766
+ return;
1767
+ }
1768
+ const metadata = isRecord(message2.metadata) ? { ...message2.metadata } : {};
1769
+ message2.metadata = { ...metadata, ...addedMetadata };
1770
+ }
1771
+ function applyReminderToMessage(message2, item, ctx) {
1772
+ const resolved = resolveReminder(item, ctx);
1773
+ if (!resolved) {
1774
+ return null;
1775
+ }
1776
+ if (resolved.metadata) {
1777
+ mergeMessageMetadata(message2, resolved.metadata);
1778
+ }
1779
+ return item.asPart ? applyPartReminder(message2, resolved.text) : applyInlineReminder(message2, resolved.text);
1780
+ }
1781
+ function mergeReminderMetadata(message2, addedReminders) {
1782
+ if (addedReminders.length === 0) return;
1783
+ const metadata = isRecord(message2.metadata) ? { ...message2.metadata } : {};
1784
+ const existing = Array.isArray(metadata.reminders) ? metadata.reminders : [];
1785
+ metadata.reminders = [...existing, ...addedReminders];
1786
+ message2.metadata = metadata;
1787
+ }
1788
+ function reminder(textOrFragment, options) {
1789
+ const text = isFragment(textOrFragment) ? new XmlRenderer().render([textOrFragment]) : textOrFragment;
1790
+ if (typeof text === "string") {
1791
+ assertReminderText(text);
1792
+ }
1793
+ if (options && "when" in options && options.when) {
1794
+ return {
1795
+ name: "reminder",
1796
+ data: null,
1797
+ metadata: {
1798
+ reminder: {
1799
+ text,
1800
+ when: options.when,
1801
+ asPart: options.asPart ?? false
1802
+ }
1803
+ }
1804
+ };
1805
+ }
1806
+ return {
1807
+ text,
1808
+ asPart: options?.asPart ?? false
1809
+ };
1810
+ }
1811
+ function user(content, ...reminders) {
1812
+ const message2 = typeof content === "string" ? {
1813
+ id: generateId3(),
1814
+ role: "user",
1815
+ parts: [{ type: "text", text: content }]
1816
+ } : { ...content, role: "user", parts: [...content.parts] };
1817
+ if (reminders.length > 0) {
1818
+ const plainText = extractPlainText(message2);
1819
+ const added = [];
1820
+ for (const item of reminders) {
1821
+ const meta = applyReminderToMessage(message2, item, {
1822
+ content: plainText
1823
+ });
1824
+ if (meta) added.push(meta);
1825
+ }
1826
+ mergeReminderMetadata(message2, added);
1827
+ }
1828
+ return {
1829
+ id: message2.id,
1830
+ name: "user",
1831
+ type: "message",
1832
+ persist: true,
1833
+ codec: {
1834
+ decode() {
1835
+ return message2;
1836
+ },
1837
+ encode() {
1838
+ return message2;
1839
+ }
1840
+ }
1841
+ };
1842
+ }
1843
+
1844
+ // packages/context/src/lib/store/store.ts
1845
+ var ContextStore = class {
1846
+ };
1847
+
1848
+ // packages/context/src/lib/engine.ts
1849
+ function estimateMessageContent(data) {
1850
+ return typeof data === "string" ? data : JSON.stringify(data);
1851
+ }
1852
+ function isLanguageModelUsage(value) {
1853
+ return typeof value === "object" && value !== null && "totalTokens" in value;
1854
+ }
1855
+ var ContextEngine = class {
1856
+ /** Non-message fragments (role, hints, etc.) - not persisted in graph */
1857
+ #fragments = [];
1858
+ /** Pending message fragments to be added to graph */
1859
+ #pendingMessages = [];
1860
+ #store;
1861
+ #chatId;
1862
+ #userId;
1863
+ #branchName;
1864
+ #branch = null;
1827
1865
  #chatData = null;
1828
1866
  #initialized = false;
1829
1867
  /** Initial metadata to merge on first initialization */
@@ -1949,17 +1987,7 @@ var ContextEngine = class {
1949
1987
  * Returns null if the chat hasn't been initialized yet.
1950
1988
  */
1951
1989
  get chat() {
1952
- if (!this.#chatData) {
1953
- return null;
1954
- }
1955
- return {
1956
- id: this.#chatData.id,
1957
- userId: this.#chatData.userId,
1958
- createdAt: this.#chatData.createdAt,
1959
- updatedAt: this.#chatData.updatedAt,
1960
- title: this.#chatData.title,
1961
- metadata: this.#chatData.metadata
1962
- };
1990
+ return this.#chatData;
1963
1991
  }
1964
1992
  /**
1965
1993
  * Count user turns in the conversation and return the previous saved user message context.
@@ -1968,13 +1996,19 @@ var ContextEngine = class {
1968
1996
  async #getChainContext() {
1969
1997
  await this.#ensureInitialized();
1970
1998
  let turn = 0;
1999
+ let messageCount = 0;
1971
2000
  let lastMessageAt;
1972
2001
  let lastMessage;
2002
+ let lastAssistantMessage2;
1973
2003
  if (this.#branch?.headMessageId) {
1974
2004
  const chain = await this.#store.getMessageChain(
1975
2005
  this.#branch.headMessageId
1976
2006
  );
1977
2007
  for (const msg of chain) {
2008
+ messageCount++;
2009
+ if (msg.name === "assistant") {
2010
+ lastAssistantMessage2 = msg.data;
2011
+ }
1978
2012
  if (msg.name !== "user") {
1979
2013
  continue;
1980
2014
  }
@@ -1984,9 +2018,16 @@ var ContextEngine = class {
1984
2018
  }
1985
2019
  }
1986
2020
  for (const fragment2 of this.#pendingMessages) {
2021
+ messageCount++;
1987
2022
  if (fragment2.name === "user") turn++;
1988
2023
  }
1989
- return { turn, lastMessageAt, lastMessage };
2024
+ return {
2025
+ turn,
2026
+ messageCount,
2027
+ lastMessageAt,
2028
+ lastMessage,
2029
+ lastAssistantMessage: lastAssistantMessage2
2030
+ };
1990
2031
  }
1991
2032
  async getTurnCount() {
1992
2033
  const { turn } = await this.#getChainContext();
@@ -2116,32 +2157,45 @@ var ContextEngine = class {
2116
2157
  if (lastUserIndex >= 0) {
2117
2158
  const lastUserFragment = this.#pendingMessages[lastUserIndex];
2118
2159
  if (lastUserFragment.codec) {
2119
- const { turn, lastMessageAt, lastMessage } = await this.#getChainContext();
2160
+ const {
2161
+ turn,
2162
+ messageCount,
2163
+ lastMessageAt,
2164
+ lastMessage,
2165
+ lastAssistantMessage: lastAssistantMessage2
2166
+ } = await this.#getChainContext();
2120
2167
  const original = lastUserFragment.codec.encode();
2121
2168
  const plainText = extractPlainText(original);
2122
- const firedReminders = conditionalReminders.map(getConditionalReminder).filter(
2123
- (config) => config.when({
2124
- turn,
2125
- content: plainText,
2126
- lastMessageAt,
2127
- lastMessage
2128
- })
2169
+ const rawUsage = this.#chatData?.metadata?.usage;
2170
+ const usage = isLanguageModelUsage(rawUsage) ? rawUsage : void 0;
2171
+ const elapsed = lastMessageAt !== void 0 ? Date.now() - lastMessageAt : void 0;
2172
+ const whenCtx = {
2173
+ turn,
2174
+ content: plainText,
2175
+ lastMessageAt,
2176
+ lastMessage,
2177
+ chat: this.#chatData,
2178
+ usage,
2179
+ branch: this.#branchName,
2180
+ elapsed,
2181
+ messageCount,
2182
+ lastAssistantMessage: lastAssistantMessage2
2183
+ };
2184
+ const configs = conditionalReminders.map(getConditionalReminder);
2185
+ const whenResults = await Promise.all(
2186
+ configs.map((it) => it.when(whenCtx))
2129
2187
  );
2188
+ const firedReminders = configs.filter((_, i) => whenResults[i]);
2130
2189
  if (firedReminders.length > 0) {
2131
- const reminders = firedReminders.flatMap((rem) => {
2132
- const resolved = resolveReminder(rem, {
2133
- content: plainText,
2134
- turn,
2135
- lastMessageAt,
2136
- lastMessage
2137
- });
2138
- if (!resolved) {
2139
- return [];
2140
- }
2190
+ const resolvedOrNull = await Promise.all(
2191
+ firedReminders.map((it) => resolveReminderAsync(it, whenCtx))
2192
+ );
2193
+ const reminders = resolvedOrNull.flatMap((resolved, i) => {
2194
+ if (!resolved) return [];
2141
2195
  return [
2142
2196
  {
2143
2197
  text: resolved.text,
2144
- asPart: rem.asPart,
2198
+ asPart: firedReminders[i].asPart,
2145
2199
  metadata: resolved.metadata
2146
2200
  }
2147
2201
  ];
@@ -7959,7 +8013,6 @@ export {
7959
8013
  everyNTurns,
7960
8014
  example,
7961
8015
  explain,
7962
- extractPlainText,
7963
8016
  fail,
7964
8017
  firstN,
7965
8018
  fragment,
@@ -8010,6 +8063,7 @@ export {
8010
8063
  render,
8011
8064
  resetAdaptivePolling,
8012
8065
  resolveReminder,
8066
+ resolveReminderAsync,
8013
8067
  resolveReminderText,
8014
8068
  role,
8015
8069
  runGuardrailChain,