@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.
- package/dist/cjs/graphs/MultiAgentGraph.cjs +21 -4
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/format.cjs +124 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +10 -1
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +29 -1
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +78 -52
- package/dist/cjs/tools/intentArg.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +5 -5
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +21 -4
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +124 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +10 -1
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +29 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +77 -53
- package/dist/esm/tools/intentArg.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +5 -5
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +9 -8
- package/dist/types/prompts/activityLabel.d.ts +8 -1
- package/dist/types/run.d.ts +1 -1
- package/dist/types/tools/intentArg.d.ts +74 -12
- package/dist/types/tools/search/tool.d.ts +5 -5
- package/dist/types/types/activityLabel.d.ts +8 -0
- package/dist/types/types/stream.d.ts +27 -2
- package/package.json +1 -1
- package/src/graphs/MultiAgentGraph.ts +18 -4
- package/src/messages/format.ts +222 -50
- package/src/messages/formatAgentMessages.test.ts +308 -6
- package/src/messages/injected.test.ts +18 -1
- package/src/messages/injected.ts +8 -1
- package/src/prompts/activityLabel.ts +48 -0
- package/src/run.ts +10 -1
- package/src/specs/activity-label-prompt.test.ts +93 -0
- package/src/summarization/__tests__/node.test.ts +188 -0
- package/src/summarization/node.ts +67 -0
- package/src/tools/__tests__/intentArg.test.ts +101 -25
- package/src/tools/intentArg.ts +102 -68
- package/src/tools/search/outcome.test.ts +1 -1
- package/src/tools/search/tool.ts +5 -5
- package/src/types/activityLabel.ts +8 -0
- package/src/types/stream.ts +28 -2
|
@@ -564,10 +564,23 @@ function extractToolNamesFromSearchOutput(output) {
|
|
|
564
564
|
}
|
|
565
565
|
return [];
|
|
566
566
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
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
|
-
|
|
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
|
|
604
|
+
tokenCount
|
|
584
605
|
};
|
|
585
606
|
}
|
|
586
607
|
}
|
|
587
|
-
return
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 = {
|