@librechat/agents 3.3.6 → 3.3.8

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 (58) hide show
  1. package/dist/cjs/graphs/MultiAgentGraph.cjs +21 -4
  2. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +2 -0
  4. package/dist/cjs/messages/format.cjs +124 -15
  5. package/dist/cjs/messages/format.cjs.map +1 -1
  6. package/dist/cjs/messages/injected.cjs +10 -1
  7. package/dist/cjs/messages/injected.cjs.map +1 -1
  8. package/dist/cjs/prompts/activityLabel.cjs +29 -1
  9. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  10. package/dist/cjs/run.cjs +7 -2
  11. package/dist/cjs/run.cjs.map +1 -1
  12. package/dist/cjs/summarization/node.cjs +55 -0
  13. package/dist/cjs/summarization/node.cjs.map +1 -1
  14. package/dist/cjs/tools/intentArg.cjs +78 -52
  15. package/dist/cjs/tools/intentArg.cjs.map +1 -1
  16. package/dist/cjs/tools/search/tool.cjs +5 -5
  17. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  18. package/dist/esm/graphs/MultiAgentGraph.mjs +21 -4
  19. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  20. package/dist/esm/main.mjs +2 -2
  21. package/dist/esm/messages/format.mjs +124 -15
  22. package/dist/esm/messages/format.mjs.map +1 -1
  23. package/dist/esm/messages/injected.mjs +10 -1
  24. package/dist/esm/messages/injected.mjs.map +1 -1
  25. package/dist/esm/prompts/activityLabel.mjs +29 -1
  26. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  27. package/dist/esm/run.mjs +7 -2
  28. package/dist/esm/run.mjs.map +1 -1
  29. package/dist/esm/summarization/node.mjs +55 -0
  30. package/dist/esm/summarization/node.mjs.map +1 -1
  31. package/dist/esm/tools/intentArg.mjs +77 -53
  32. package/dist/esm/tools/intentArg.mjs.map +1 -1
  33. package/dist/esm/tools/search/tool.mjs +5 -5
  34. package/dist/esm/tools/search/tool.mjs.map +1 -1
  35. package/dist/types/messages/format.d.ts +9 -8
  36. package/dist/types/prompts/activityLabel.d.ts +8 -1
  37. package/dist/types/run.d.ts +1 -1
  38. package/dist/types/tools/intentArg.d.ts +74 -12
  39. package/dist/types/tools/search/tool.d.ts +5 -5
  40. package/dist/types/types/activityLabel.d.ts +8 -0
  41. package/dist/types/types/stream.d.ts +27 -2
  42. package/package.json +1 -1
  43. package/src/graphs/MultiAgentGraph.ts +18 -4
  44. package/src/messages/format.ts +222 -50
  45. package/src/messages/formatAgentMessages.test.ts +308 -6
  46. package/src/messages/injected.test.ts +18 -1
  47. package/src/messages/injected.ts +8 -1
  48. package/src/prompts/activityLabel.ts +48 -0
  49. package/src/run.ts +10 -1
  50. package/src/specs/activity-label-prompt.test.ts +93 -0
  51. package/src/summarization/__tests__/node.test.ts +188 -0
  52. package/src/summarization/node.ts +67 -0
  53. package/src/tools/__tests__/intentArg.test.ts +101 -25
  54. package/src/tools/intentArg.ts +102 -68
  55. package/src/tools/search/outcome.test.ts +1 -1
  56. package/src/tools/search/tool.ts +5 -5
  57. package/src/types/activityLabel.ts +8 -0
  58. package/src/types/stream.ts +28 -2
@@ -564,10 +564,23 @@ function extractToolNamesFromSearchOutput(output) {
564
564
  }
565
565
  return [];
566
566
  }
567
- function getLatestSummaryBoundary(payload) {
568
- let summaryBoundary;
567
+ function resolveCoverageIndex(coverage, indexBySourceId, summaryMessageIndex) {
568
+ /** Persisted JSON, so the declared string type is not a runtime guarantee. */
569
+ if (typeof coverage?.retainedFromMessageId !== "string") return;
570
+ const retainedFromMessageId = coverage.retainedFromMessageId.trim();
571
+ if (retainedFromMessageId === "") return;
572
+ const retainedIndex = indexBySourceId.get(retainedFromMessageId);
573
+ return retainedIndex != null && retainedIndex <= summaryMessageIndex ? retainedIndex : void 0;
574
+ }
575
+ function scanSummaryBlocks(payload) {
576
+ let boundary;
577
+ /** Filled as the scan advances, so a coverage lookup only ever resolves to a
578
+ * message already passed — no second pass over the payload. */
579
+ const indexBySourceId = /* @__PURE__ */ new Map();
569
580
  for (let i = 0; i < payload.length; i++) {
570
581
  const message = payload[i];
582
+ const sourceMessageId = getSourceMessageId(message);
583
+ if (sourceMessageId != null) indexBySourceId.set(sourceMessageId, i);
571
584
  if (!Array.isArray(message.content)) continue;
572
585
  for (let j = 0; j < message.content.length; j++) {
573
586
  const part = message.content[j];
@@ -576,18 +589,31 @@ function getLatestSummaryBoundary(payload) {
576
589
  let summaryText = (summaryPart.content ?? []).map((block) => "text" in block ? block.text : "").join("").trim();
577
590
  if (summaryText.length === 0 && typeof summaryPart.text === "string") summaryText = summaryPart.text.trim();
578
591
  if (summaryText.length === 0) continue;
579
- summaryBoundary = {
592
+ const tokenCount = typeof summaryPart.tokenCount === "number" && Number.isFinite(summaryPart.tokenCount) ? summaryPart.tokenCount : 0;
593
+ const retainedIndex = resolveCoverageIndex(summaryPart.coverage, indexBySourceId, i);
594
+ boundary = retainedIndex != null ? {
595
+ mode: "coverage",
596
+ messageIndex: retainedIndex,
597
+ text: summaryText,
598
+ tokenCount
599
+ } : {
600
+ mode: "positional",
580
601
  messageIndex: i,
581
602
  contentIndex: j,
582
603
  text: summaryText,
583
- tokenCount: typeof summaryPart.tokenCount === "number" && Number.isFinite(summaryPart.tokenCount) ? summaryPart.tokenCount : 0
604
+ tokenCount
584
605
  };
585
606
  }
586
607
  }
587
- return summaryBoundary;
608
+ return { boundary };
588
609
  }
589
610
  function applySummaryBoundary(message, messageIndex, summaryBoundary) {
590
611
  if (!summaryBoundary) return message;
612
+ /** The boundary names the first retained message, so it is exclusive: that
613
+ * message and everything after it — the recency tail included — stays
614
+ * verbatim, and only genuinely covered history is dropped. Summary parts on
615
+ * surviving messages are filtered later by `formatAssistantMessage`. */
616
+ if (summaryBoundary.mode === "coverage") return messageIndex < summaryBoundary.messageIndex ? null : message;
591
617
  if (messageIndex < summaryBoundary.messageIndex) return null;
592
618
  if (messageIndex !== summaryBoundary.messageIndex || !Array.isArray(message.content)) return message;
593
619
  return {
@@ -595,16 +621,53 @@ function applySummaryBoundary(message, messageIndex, summaryBoundary) {
595
621
  content: message.content.slice(summaryBoundary.contentIndex + 1)
596
622
  };
597
623
  }
624
+ /**
625
+ * Whether `formatAssistantMessage` filters this part out of the emitted message.
626
+ * Such a part contributes no prompt tokens, so measuring it as zero characters
627
+ * is accurate — it must not be mistaken for content the heuristic cannot see.
628
+ */
629
+ function isDroppedByFormatting(part) {
630
+ if (part == null) return true;
631
+ if (part.type === "summary" || part.type === "error" || part.type === "agent_update" || part.type === "activity_label") return true;
632
+ return part.type === "text" && getTextContent(part).trim() === "";
633
+ }
634
+ function measureValueChars(value) {
635
+ if (typeof value === "string") return value.length;
636
+ if (value == null || typeof value !== "object") return 0;
637
+ const measured = serializeStructuredValueBounded(value, 0).originalChars;
638
+ return measured === Number.MAX_SAFE_INTEGER ? HARD_MAX_TOOL_RESULT_CHARS : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
639
+ }
640
+ /**
641
+ * Whether a retained part's whole prompt cost is the text the char heuristic
642
+ * reads, making it safe to represent in a character ratio.
643
+ *
644
+ * An allowlist, not a denylist. Media, resources, and tool calls carry cost that
645
+ * is unrelated to their serialized length — a short image URL nested in
646
+ * `tool_call.output` stands in for a fixed four-figure media charge — and
647
+ * rejecting those case by case has repeatedly missed a nesting level. Listing
648
+ * the two shapes whose characters `contentPartCharLength` actually reads makes
649
+ * every other shape, present or future, ineligible by default: the ratio is
650
+ * skipped and the entry keeps its original count, which prunes early rather than
651
+ * exceeding the window.
652
+ */
653
+ function isCharRatioEligible(part) {
654
+ if (part == null) return false;
655
+ return part.type === "text" || part.type === "thinking";
656
+ }
598
657
  function contentPartCharLength(part) {
599
658
  const record = part;
600
659
  let len = 0;
601
660
  if (typeof record.text === "string") len += record.text.length;
602
661
  if (typeof record.thinking === "string") len += record.thinking.length;
603
- const { input } = record;
604
- if (typeof input === "string") len += input.length;
605
- else if (input != null && typeof input === "object") {
606
- const measured = serializeStructuredValueBounded(input, 0).originalChars;
607
- len += measured === Number.MAX_SAFE_INTEGER ? HARD_MAX_TOOL_RESULT_CHARS : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
662
+ len += measureValueChars(record.input);
663
+ /** Tool calls nest their payload a level down, so measuring only the
664
+ * top-level fields scores an entire tool turn as zero characters. */
665
+ const { tool_call: toolCall } = record;
666
+ if (toolCall != null && typeof toolCall === "object") {
667
+ const call = toolCall;
668
+ len += measureValueChars(call.name);
669
+ len += measureValueChars(call.args);
670
+ len += measureValueChars(call.output);
608
671
  }
609
672
  return len;
610
673
  }
@@ -654,7 +717,7 @@ const formatAgentMessages = (payload, indexTokenCountMap, tools, skills, options
654
717
  const updatedIndexTokenCountMap = {};
655
718
  let boundaryTokenAdjustment;
656
719
  const indexMapping = {};
657
- const summaryBoundary = getLatestSummaryBoundary(payload);
720
+ const { boundary: summaryBoundary } = scanSummaryBlocks(payload);
658
721
  /**
659
722
  * Create a mutable copy of the tools set that can be expanded dynamically.
660
723
  * When we encounter tool_search results, we add discovered tools to this set,
@@ -853,18 +916,64 @@ const formatAgentMessages = (payload, indexTokenCountMap, tools, skills, options
853
916
  const resultIndices = indexMapping[originalIndex] || [];
854
917
  let tokenCount = indexTokenCountMap[originalIndex];
855
918
  if (tokenCount === void 0) continue;
856
- if (summaryBoundary && originalIndex === summaryBoundary.messageIndex && Array.isArray(payload[originalIndex].content)) {
919
+ /**
920
+ * Coverage mode deliberately leaves the count alone, even though the entry
921
+ * holding the block is charged for summary text that `formatAssistantMessage`
922
+ * filters out and `summary.tokenCount` accounts separately.
923
+ *
924
+ * Discounting it needs the summary's cost in the same units as
925
+ * `indexTokenCountMap`, and that figure is not obtainable here: this
926
+ * function receives no tokenizer, and a count recorded at write time is in
927
+ * the writing run's units — `Run.create` derives its counter from the model
928
+ * in play, and a consumer may supply its own — so a conversation continued
929
+ * on a different model would subtract across encodings. Attempts to proxy
930
+ * it (character ratios, provider identity) all under-count some shape,
931
+ * which risks an over-context request; over-counting merely prunes early.
932
+ * Fixing it properly means passing the reader a tokenizer, which is a
933
+ * consumer-facing change and out of scope here.
934
+ */
935
+ if (summaryBoundary?.mode === "positional" && originalIndex === summaryBoundary.messageIndex && Array.isArray(payload[originalIndex].content)) {
857
936
  const content = payload[originalIndex].content;
858
937
  const { contentIndex } = summaryBoundary;
859
938
  if (contentIndex >= 0 && contentIndex < content.length - 1) {
860
939
  let totalCharLen = 0;
861
940
  let remainingCharLen = 0;
941
+ /**
942
+ * The ratio applies only when *every* part of the entry is one whose
943
+ * token cost tracks its character length. A single ineligible part
944
+ * cancels the discount, whichever side of the boundary it sits on.
945
+ *
946
+ * Both sides can break it, in opposite directions. A retained image has
947
+ * its fixed cost scaled away, collapsing the entry. A removed base64
948
+ * payload inflates the denominator — serializing to a huge length while
949
+ * the counter charges a fixed estimate — dragging retained text below
950
+ * its real cost. Either way the request can exceed the window.
951
+ *
952
+ * Telling a text-bearing tool payload from a media-bearing one means
953
+ * recursing into arbitrary nested output, which has already missed a
954
+ * level twice here. Cancelling instead keeps the original count: an
955
+ * over-count that prunes early rather than overflowing. Entries of
956
+ * plain text and reasoning — the common shape — still proportion.
957
+ */
958
+ let everyRetainedPartMeasurable = true;
862
959
  for (let p = 0; p < content.length; p++) {
863
- const charLen = contentPartCharLength(content[p]);
960
+ const part = content[p];
961
+ const retained = p > contentIndex;
962
+ if (isDroppedByFormatting(part)) {
963
+ /** Removed summary text is real removed content: it is read as
964
+ * plain text and belongs in the denominator. */
965
+ if (!retained && part.type === "summary") totalCharLen += contentPartCharLength(part);
966
+ continue;
967
+ }
968
+ const charLen = contentPartCharLength(part);
969
+ if (!isCharRatioEligible(part) || retained && charLen === 0) {
970
+ everyRetainedPartMeasurable = false;
971
+ break;
972
+ }
864
973
  totalCharLen += charLen;
865
- if (p > contentIndex) remainingCharLen += charLen;
974
+ if (retained) remainingCharLen += charLen;
866
975
  }
867
- if (totalCharLen > 0) {
976
+ if (totalCharLen > 0 && everyRetainedPartMeasurable) {
868
977
  const original = tokenCount;
869
978
  tokenCount = Math.max(1, Math.round(tokenCount * (remainingCharLen / totalCharLen)));
870
979
  boundaryTokenAdjustment = {