@librechat/agents 3.9.0 → 3.9.2
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/Graph.cjs +3 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +4 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/prepareProviderRequest.cjs +3 -2
- package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
- package/dist/cjs/main.cjs +5 -1
- package/dist/cjs/messages/contextPruning.cjs +2 -1
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +27 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +3 -3
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +6 -6
- package/dist/cjs/utils/toolContent.cjs.map +1 -1
- package/dist/cjs/utils/truncation.cjs +14 -5
- package/dist/cjs/utils/truncation.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +4 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/prepareProviderRequest.mjs +3 -2
- package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/contextPruning.mjs +2 -1
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +25 -1
- package/dist/esm/messages/handoffCue.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +4 -4
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +7 -7
- package/dist/esm/utils/toolContent.mjs.map +1 -1
- package/dist/esm/utils/truncation.mjs +14 -6
- package/dist/esm/utils/truncation.mjs.map +1 -1
- package/dist/types/llm/prepareProviderRequest.d.ts +1 -1
- package/dist/types/messages/handoffCue.d.ts +11 -0
- package/dist/types/utils/toolContent.d.ts +1 -1
- package/dist/types/utils/truncation.d.ts +2 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +10 -4
- package/src/graphs/MultiAgentGraph.ts +23 -10
- package/src/llm/prepareProviderRequest.ts +6 -4
- package/src/messages/contextPruning.ts +8 -2
- package/src/messages/handoffCue.ts +51 -0
- package/src/messages/prune.ts +15 -10
- package/src/utils/toolContent.ts +23 -7
- package/src/utils/truncation.ts +31 -6
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Prevents oversized tool outputs from entering the message array and
|
|
5
5
|
* consuming the entire context window.
|
|
6
6
|
*/
|
|
7
|
+
/** Slices by UTF-16 code units, dropping orphaned surrogate halves at either edge. */
|
|
8
|
+
export declare function sliceWithoutSplittingSurrogates(value: string, start: number, end?: number): string;
|
|
7
9
|
/**
|
|
8
10
|
* Absolute hard cap on tool result length (characters).
|
|
9
11
|
* Even if the model has a 1M-token context, a single tool result
|
package/package.json
CHANGED
package/src/graphs/Graph.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
convertInjectedMessages,
|
|
71
71
|
coalesceAdjacentUserTurns,
|
|
72
72
|
appendPredecessorHandoffCue,
|
|
73
|
+
appendInstructionlessHandoffCue,
|
|
73
74
|
stampSyntheticProviderMessage,
|
|
74
75
|
} from '@/messages';
|
|
75
76
|
import {
|
|
@@ -3721,11 +3722,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3721
3722
|
* Applied HERE for the primary so the cue is part of the MEASURED
|
|
3722
3723
|
* payload — the pre-invoke projection and overflow guard run on this
|
|
3723
3724
|
* stage's output, and a post-measure append could push a just-fits
|
|
3724
|
-
* prompt over budget unreported (#346 round 2).
|
|
3725
|
-
*
|
|
3726
|
-
*
|
|
3727
|
-
*
|
|
3725
|
+
* prompt over budget unreported (#346 round 2). Instructionless tool
|
|
3726
|
+
* handoffs are grounded for every transport. Only the direct-edge
|
|
3727
|
+
* predecessor cue is re-keyed per SERVING provider: tolerant
|
|
3728
|
+
* fallbacks strip it, while Claude fallbacks add it.
|
|
3728
3729
|
*/
|
|
3730
|
+
const beforeHandoffCue = transformed;
|
|
3731
|
+
transformed = trackProviderMessageOrigins(
|
|
3732
|
+
beforeHandoffCue,
|
|
3733
|
+
appendInstructionlessHandoffCue(beforeHandoffCue, callConfig)
|
|
3734
|
+
);
|
|
3729
3735
|
if (isAnthropicLike(provider, clientOptions as { model?: string })) {
|
|
3730
3736
|
const before = transformed;
|
|
3731
3737
|
transformed = trackProviderMessageOrigins(
|
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
setProviderMessageProvenance,
|
|
29
29
|
stampSyntheticProviderMessage,
|
|
30
30
|
} from '@/messages/provenance';
|
|
31
|
-
import { serializeToolContentBounded } from '@/utils/toolContent';
|
|
32
|
-
import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
|
|
33
31
|
import {
|
|
34
32
|
calculateMaxToolResultChars,
|
|
35
33
|
HARD_MAX_TOOL_RESULT_CHARS,
|
|
36
34
|
} from '@/utils/truncation';
|
|
35
|
+
import { withInstructionlessHandoffCue } from '@/messages/handoffCue';
|
|
36
|
+
import { serializeToolContentBounded } from '@/utils/toolContent';
|
|
37
|
+
import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
|
|
37
38
|
import { StandardGraph } from './Graph';
|
|
38
39
|
|
|
39
40
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
@@ -1354,12 +1355,6 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1354
1355
|
this.memberRecursionLimit == null
|
|
1355
1356
|
? config
|
|
1356
1357
|
: { ...config, recursionLimit: this.memberRecursionLimit };
|
|
1357
|
-
const memberConfig = withActiveAgentMetadata(
|
|
1358
|
-
recursionLimitedConfig,
|
|
1359
|
-
agentId,
|
|
1360
|
-
agentContext?.name
|
|
1361
|
-
);
|
|
1362
|
-
|
|
1363
1358
|
/**
|
|
1364
1359
|
* Check if this agent is receiving a handoff.
|
|
1365
1360
|
* If so, filter out the transfer messages and inject instructions as preamble.
|
|
@@ -1371,6 +1366,19 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1371
1366
|
agentId
|
|
1372
1367
|
);
|
|
1373
1368
|
|
|
1369
|
+
const memberConfig = withInstructionlessHandoffCue(
|
|
1370
|
+
withActiveAgentMetadata(
|
|
1371
|
+
recursionLimitedConfig,
|
|
1372
|
+
agentId,
|
|
1373
|
+
agentContext?.name
|
|
1374
|
+
),
|
|
1375
|
+
handoffContext != null &&
|
|
1376
|
+
(handoffContext.instructions == null ||
|
|
1377
|
+
handoffContext.instructions === '')
|
|
1378
|
+
? handoffContext.filteredMessages.at(-1)
|
|
1379
|
+
: undefined
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1374
1382
|
if (
|
|
1375
1383
|
handoffContext?.sourceAgentName != null &&
|
|
1376
1384
|
handoffContext.sourceAgentName !== ''
|
|
@@ -1582,7 +1590,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1582
1590
|
}
|
|
1583
1591
|
|
|
1584
1592
|
const startingNodes =
|
|
1585
|
-
summarizeOnlyAgentId != null
|
|
1593
|
+
summarizeOnlyAgentId != null
|
|
1594
|
+
? [summarizeOnlyAgentId]
|
|
1595
|
+
: this.startingNodes;
|
|
1586
1596
|
for (const startNode of startingNodes) {
|
|
1587
1597
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1588
1598
|
/** @ts-ignore */
|
|
@@ -1634,7 +1644,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1634
1644
|
this.resolveMaxRoutingPromptChars(destination);
|
|
1635
1645
|
|
|
1636
1646
|
if (typeof prompt === 'function') {
|
|
1637
|
-
const resolvedPrompt = await prompt(
|
|
1647
|
+
const resolvedPrompt = await prompt(
|
|
1648
|
+
state.messages,
|
|
1649
|
+
this.startIndex
|
|
1650
|
+
);
|
|
1638
1651
|
promptText =
|
|
1639
1652
|
resolvedPrompt == null
|
|
1640
1653
|
? undefined
|
|
@@ -6,8 +6,8 @@ import type {
|
|
|
6
6
|
} from '@langchain/core/messages';
|
|
7
7
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
8
8
|
import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
9
|
-
import type * as t from '@/types';
|
|
10
9
|
import type { ToolHistoryPreparation } from '@/messages/toolHistoryProjection';
|
|
10
|
+
import type * as t from '@/types';
|
|
11
11
|
import {
|
|
12
12
|
projectCacheControlledToolOutputsToText,
|
|
13
13
|
projectComputerCallOutputsToText,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
coalesceAdjacentUserTurns,
|
|
23
23
|
appendPredecessorHandoffCue,
|
|
24
|
+
appendInstructionlessHandoffCue,
|
|
24
25
|
removePredecessorHandoffCue,
|
|
25
26
|
} from '@/messages';
|
|
26
27
|
import {
|
|
@@ -28,12 +29,12 @@ import {
|
|
|
28
29
|
stripBedrockCacheControl,
|
|
29
30
|
cloneMessage,
|
|
30
31
|
} from '@/messages/cache';
|
|
32
|
+
import { createToolHistoryPreparation } from '@/messages/toolHistoryProjection';
|
|
31
33
|
import { isAnthropicLike, isGoogleLike, isOpenAILike } from '@/utils/llm';
|
|
32
34
|
import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
|
|
33
35
|
import { providerRequiresStrictAlternation } from '@/llm/providers';
|
|
34
36
|
import { getProviderFamily } from '@/llm/providerRegistry';
|
|
35
37
|
import { Providers } from '@/common';
|
|
36
|
-
import { createToolHistoryPreparation } from '@/messages/toolHistoryProjection';
|
|
37
38
|
|
|
38
39
|
const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
|
|
39
40
|
const OMITTED_ATTACHMENT_TEXT =
|
|
@@ -555,16 +556,17 @@ export function prepareProviderRequest({
|
|
|
555
556
|
const annotated = annotateMessagesForLLM(projected, registry, runId);
|
|
556
557
|
const isRunProduced = context?.isRunProducedMessage;
|
|
557
558
|
const modelId = resolveServingModelId(model);
|
|
559
|
+
const handoffGrounded = appendInstructionlessHandoffCue(annotated, config);
|
|
558
560
|
const cued = isAnthropicLike(provider, {
|
|
559
561
|
model: modelId,
|
|
560
562
|
})
|
|
561
563
|
? appendPredecessorHandoffCue(
|
|
562
|
-
|
|
564
|
+
handoffGrounded,
|
|
563
565
|
isRunProduced == null
|
|
564
566
|
? undefined
|
|
565
567
|
: (message): boolean => isRunProduced.call(context, message)
|
|
566
568
|
)
|
|
567
|
-
: removePredecessorHandoffCue(
|
|
569
|
+
: removePredecessorHandoffCue(handoffGrounded);
|
|
568
570
|
const preparedMessages = providerRequiresStrictAlternation(provider)
|
|
569
571
|
? coalesceAdjacentUserTurns(cued)
|
|
570
572
|
: cued;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
serializeToolContentBounded,
|
|
27
27
|
} from '@/utils/toolContent';
|
|
28
28
|
import { resolveContextPruningSettings } from './contextPruningSettings';
|
|
29
|
+
import { sliceWithoutSplittingSurrogates } from '@/utils/truncation';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Applies head+tail soft-trim to tool result content.
|
|
@@ -36,7 +37,11 @@ function softTrimContent(
|
|
|
36
37
|
): string {
|
|
37
38
|
const { headChars, tailChars } = settings;
|
|
38
39
|
const indicator = `\n\n… [soft-trimmed: ${content.length} chars → ${headChars + tailChars} chars, middle removed] …\n\n`;
|
|
39
|
-
return
|
|
40
|
+
return (
|
|
41
|
+
sliceWithoutSplittingSurrogates(content, 0, headChars) +
|
|
42
|
+
indicator +
|
|
43
|
+
sliceWithoutSplittingSurrogates(content, -tailChars)
|
|
44
|
+
);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export interface ContextPruningResult {
|
|
@@ -140,7 +145,8 @@ export function applyContextPruning(params: {
|
|
|
140
145
|
}
|
|
141
146
|
const content = message.content;
|
|
142
147
|
const contentLength = getToolContentCharLength(content);
|
|
143
|
-
const eligibilityContent =
|
|
148
|
+
const eligibilityContent =
|
|
149
|
+
params.canonicalMessages?.[i]?.content ?? content;
|
|
144
150
|
if (
|
|
145
151
|
getToolContentCharLength(eligibilityContent) <
|
|
146
152
|
settings.minPrunableToolChars
|
|
@@ -1,8 +1,59 @@
|
|
|
1
1
|
// src/messages/handoffCue.ts
|
|
2
2
|
import { HumanMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
3
4
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
5
|
import { stampSyntheticProviderMessage } from './provenance';
|
|
5
6
|
|
|
7
|
+
const HANDOFF_CUE_MESSAGE_ID = '__handoff_cue_message_id';
|
|
8
|
+
export const INSTRUCTIONLESS_HANDOFF_CUE =
|
|
9
|
+
'Continue as the receiving agent using the preceding user request and context.';
|
|
10
|
+
|
|
11
|
+
/** Scope transport-only grounding to this recipient's incoming assistant turn.
|
|
12
|
+
* Every agent entry resets the marker, including direct edges and cycles. The
|
|
13
|
+
* reducer has already assigned the source message's id, including on replay. */
|
|
14
|
+
export function withInstructionlessHandoffCue(
|
|
15
|
+
config: RunnableConfig | undefined,
|
|
16
|
+
tail?: BaseMessage
|
|
17
|
+
): RunnableConfig {
|
|
18
|
+
return {
|
|
19
|
+
...config,
|
|
20
|
+
metadata: {
|
|
21
|
+
...config?.metadata,
|
|
22
|
+
[HANDOFF_CUE_MESSAGE_ID]:
|
|
23
|
+
tail?.getType() === 'ai' ? (tail.id ?? null) : null,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Only provider projections get this cue. Matching the incoming turn's id
|
|
29
|
+
* prevents it leaking into later tool iterations or deliberate assistant
|
|
30
|
+
* prefill. It is independent of transport provider and run-produced ids, so
|
|
31
|
+
* gateways and checkpoint replays follow the same handoff contract. */
|
|
32
|
+
export function appendInstructionlessHandoffCue(
|
|
33
|
+
messages: BaseMessage[],
|
|
34
|
+
config?: RunnableConfig
|
|
35
|
+
): BaseMessage[] {
|
|
36
|
+
const tail = messages.at(-1);
|
|
37
|
+
const sourceId = config?.metadata?.[HANDOFF_CUE_MESSAGE_ID];
|
|
38
|
+
if (
|
|
39
|
+
typeof sourceId !== 'string' ||
|
|
40
|
+
sourceId === '' ||
|
|
41
|
+
tail?.getType() !== 'ai' ||
|
|
42
|
+
tail.id !== sourceId
|
|
43
|
+
) {
|
|
44
|
+
return messages;
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
...messages,
|
|
48
|
+
stampSyntheticProviderMessage(
|
|
49
|
+
new HumanMessage({
|
|
50
|
+
content: INSTRUCTIONLESS_HANDOFF_CUE,
|
|
51
|
+
additional_kwargs: { role: 'user', isMeta: true, source: 'routing' },
|
|
52
|
+
})
|
|
53
|
+
),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
6
57
|
/**
|
|
7
58
|
* Bracketed-meta convention, like the handoff path's
|
|
8
59
|
* `[Processed tool result and transferring to …]` bridge. The wording makes
|
package/src/messages/prune.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
HARD_MAX_TOOL_CALL_INPUT_CHARS,
|
|
20
20
|
HARD_MAX_TOOL_RESULT_CHARS,
|
|
21
21
|
MIN_JSON_VALUE_CHARS,
|
|
22
|
+
sliceWithoutSplittingSurrogates,
|
|
22
23
|
calculateMaxToolCallInputChars,
|
|
23
24
|
calculateMaxToolResultChars,
|
|
24
25
|
} from '@/utils/truncation';
|
|
@@ -1591,7 +1592,8 @@ function createBoundedTruncationValue(
|
|
|
1591
1592
|
const next = Math.ceil((low + high) / 2);
|
|
1592
1593
|
const candidate = {
|
|
1593
1594
|
_truncated:
|
|
1594
|
-
TOOL_INPUT_TRUNCATION_MARKER +
|
|
1595
|
+
TOOL_INPUT_TRUNCATION_MARKER +
|
|
1596
|
+
sliceWithoutSplittingSurrogates(canonicalPrefix, 0, next),
|
|
1595
1597
|
_originalChars: originalChars,
|
|
1596
1598
|
};
|
|
1597
1599
|
if (JSON.stringify(candidate).length <= normalizedMaxChars) {
|
|
@@ -1604,7 +1606,8 @@ function createBoundedTruncationValue(
|
|
|
1604
1606
|
// Keep the marker separate from a pure canonical prefix so another,
|
|
1605
1607
|
// slightly smaller cap can be derived without nesting the envelope.
|
|
1606
1608
|
_truncated:
|
|
1607
|
-
TOOL_INPUT_TRUNCATION_MARKER +
|
|
1609
|
+
TOOL_INPUT_TRUNCATION_MARKER +
|
|
1610
|
+
sliceWithoutSplittingSurrogates(canonicalPrefix, 0, low),
|
|
1608
1611
|
_originalChars: originalChars,
|
|
1609
1612
|
};
|
|
1610
1613
|
}
|
|
@@ -1847,8 +1850,12 @@ function projectStringInputWithinLimit(
|
|
|
1847
1850
|
return {
|
|
1848
1851
|
value:
|
|
1849
1852
|
marker.length >= normalizedMaxChars
|
|
1850
|
-
? prefix
|
|
1851
|
-
:
|
|
1853
|
+
? sliceWithoutSplittingSurrogates(prefix, 0, normalizedMaxChars)
|
|
1854
|
+
: sliceWithoutSplittingSurrogates(
|
|
1855
|
+
prefix,
|
|
1856
|
+
0,
|
|
1857
|
+
normalizedMaxChars - marker.length
|
|
1858
|
+
) + marker,
|
|
1852
1859
|
changed: true,
|
|
1853
1860
|
};
|
|
1854
1861
|
}
|
|
@@ -2366,7 +2373,9 @@ function applyToolCallInputCaps(params: {
|
|
|
2366
2373
|
additionalKwargsChanges
|
|
2367
2374
|
);
|
|
2368
2375
|
}
|
|
2369
|
-
if (
|
|
2376
|
+
if (
|
|
2377
|
+
capped.response_metadata.output !== canonical.response_metadata.output
|
|
2378
|
+
) {
|
|
2370
2379
|
changes.response_metadata = cloneWithProjectedProperties(
|
|
2371
2380
|
current.response_metadata,
|
|
2372
2381
|
{ output: capped.response_metadata.output }
|
|
@@ -2523,11 +2532,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
2523
2532
|
originalToolContent.clear();
|
|
2524
2533
|
originalToolContentSize = 0;
|
|
2525
2534
|
}
|
|
2526
|
-
for (
|
|
2527
|
-
let i = toolExchangeWidthThrough;
|
|
2528
|
-
i < canonicalMessages.length;
|
|
2529
|
-
i++
|
|
2530
|
-
) {
|
|
2535
|
+
for (let i = toolExchangeWidthThrough; i < canonicalMessages.length; i++) {
|
|
2531
2536
|
maxToolExchangeWidth = Math.max(
|
|
2532
2537
|
maxToolExchangeWidth,
|
|
2533
2538
|
getToolCallIds(canonicalMessages[i]).size
|
package/src/utils/toolContent.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
|
|
|
3
3
|
import {
|
|
4
4
|
HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
|
|
5
5
|
truncateToolResultContent,
|
|
6
|
+
sliceWithoutSplittingSurrogates,
|
|
6
7
|
} from './truncation';
|
|
7
8
|
|
|
8
9
|
type ToolContent = BaseMessage['content'];
|
|
@@ -421,7 +422,7 @@ type SegmentedStringBuffer = {
|
|
|
421
422
|
export type BoundedStructuredSerialization = {
|
|
422
423
|
/** Provider-facing head/tail preview, bounded by `maxChars`. */
|
|
423
424
|
content: string;
|
|
424
|
-
/**
|
|
425
|
+
/** Code-point-aligned serialized prefix up to the traversal-work ceiling. */
|
|
425
426
|
prefix: string;
|
|
426
427
|
/** Exact length, or `Number.MAX_SAFE_INTEGER` when traversal was capped. */
|
|
427
428
|
originalChars: number;
|
|
@@ -505,6 +506,10 @@ function materializeSegmentedStringBuffer(
|
|
|
505
506
|
return segments.join('');
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Retains exact code-unit prefixes internally. Trimming surrogate edges here
|
|
511
|
+
* would let later chunks fill the gap and corrupt the prefix; trim on exposure.
|
|
512
|
+
*/
|
|
508
513
|
function appendSerializedChunk(
|
|
509
514
|
collector: StructuredSerializationCollector,
|
|
510
515
|
chunk: string
|
|
@@ -953,7 +958,7 @@ function formatBoundedStructuredContent(
|
|
|
953
958
|
suffix: string
|
|
954
959
|
): string {
|
|
955
960
|
if (collector.totalChars <= maxChars) {
|
|
956
|
-
return prefix
|
|
961
|
+
return sliceWithoutSplittingSurrogates(prefix, 0, collector.totalChars);
|
|
957
962
|
}
|
|
958
963
|
|
|
959
964
|
const indicator = collector.work.exceeded
|
|
@@ -962,10 +967,13 @@ function formatBoundedStructuredContent(
|
|
|
962
967
|
`${maxChars} limit] …\n\n`;
|
|
963
968
|
const available = maxChars - indicator.length;
|
|
964
969
|
if (available <= 0) {
|
|
965
|
-
return prefix
|
|
970
|
+
return sliceWithoutSplittingSurrogates(prefix, 0, maxChars);
|
|
966
971
|
}
|
|
967
972
|
if (available < 200) {
|
|
968
|
-
return
|
|
973
|
+
return (
|
|
974
|
+
sliceWithoutSplittingSurrogates(prefix, 0, available) +
|
|
975
|
+
indicator.trimEnd()
|
|
976
|
+
);
|
|
969
977
|
}
|
|
970
978
|
|
|
971
979
|
const headSize = Math.ceil(available * 0.7);
|
|
@@ -982,7 +990,11 @@ function formatBoundedStructuredContent(
|
|
|
982
990
|
tailStart = tailNewline + 1;
|
|
983
991
|
}
|
|
984
992
|
|
|
985
|
-
return
|
|
993
|
+
return (
|
|
994
|
+
sliceWithoutSplittingSurrogates(prefix, 0, headEnd) +
|
|
995
|
+
indicator +
|
|
996
|
+
sliceWithoutSplittingSurrogates(suffix, tailStart)
|
|
997
|
+
);
|
|
986
998
|
}
|
|
987
999
|
|
|
988
1000
|
/**
|
|
@@ -1036,7 +1048,7 @@ export function serializeStructuredValueBounded(
|
|
|
1036
1048
|
prefix,
|
|
1037
1049
|
suffix
|
|
1038
1050
|
),
|
|
1039
|
-
prefix: prefix
|
|
1051
|
+
prefix: sliceWithoutSplittingSurrogates(prefix, 0, normalizedPrefixChars),
|
|
1040
1052
|
originalChars,
|
|
1041
1053
|
truncated:
|
|
1042
1054
|
collector.work.exceeded || collector.totalChars > normalizedMaxChars,
|
|
@@ -1931,7 +1943,11 @@ function serializeDenseTextBlocksWithinLimit(
|
|
|
1931
1943
|
}
|
|
1932
1944
|
preview += textBlocks[i].text.slice(0, available - preview.length);
|
|
1933
1945
|
}
|
|
1934
|
-
return (
|
|
1946
|
+
return sliceWithoutSplittingSurrogates(
|
|
1947
|
+
sliceWithoutSplittingSurrogates(preview, 0) + indicator,
|
|
1948
|
+
0,
|
|
1949
|
+
normalizedMaxChars
|
|
1950
|
+
);
|
|
1935
1951
|
}
|
|
1936
1952
|
|
|
1937
1953
|
export type CompactToolContentResult = {
|
package/src/utils/truncation.ts
CHANGED
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
* consuming the entire context window.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/** Slices by UTF-16 code units, dropping orphaned surrogate halves at either edge. */
|
|
9
|
+
export function sliceWithoutSplittingSurrogates(
|
|
10
|
+
value: string,
|
|
11
|
+
start: number,
|
|
12
|
+
end?: number
|
|
13
|
+
): string {
|
|
14
|
+
const sliced = value.slice(start, end);
|
|
15
|
+
const first = sliced.charCodeAt(0);
|
|
16
|
+
const last = sliced.charCodeAt(sliced.length - 1);
|
|
17
|
+
const trimStart = first >= 0xdc00 && first <= 0xdfff;
|
|
18
|
+
const trimEnd = last >= 0xd800 && last <= 0xdbff;
|
|
19
|
+
return trimStart || trimEnd
|
|
20
|
+
? sliced.slice(trimStart ? 1 : 0, sliced.length - (trimEnd ? 1 : 0))
|
|
21
|
+
: sliced;
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
/**
|
|
9
25
|
* Absolute hard cap on tool result length (characters).
|
|
10
26
|
* Even if the model has a 1M-token context, a single tool result
|
|
@@ -85,7 +101,9 @@ export function truncateToolInput(
|
|
|
85
101
|
|
|
86
102
|
if (available < 100) {
|
|
87
103
|
return {
|
|
88
|
-
_truncated:
|
|
104
|
+
_truncated:
|
|
105
|
+
sliceWithoutSplittingSurrogates(serialized, 0, maxChars) +
|
|
106
|
+
indicator.trimEnd(),
|
|
89
107
|
_originalChars: serialized.length,
|
|
90
108
|
};
|
|
91
109
|
}
|
|
@@ -95,9 +113,9 @@ export function truncateToolInput(
|
|
|
95
113
|
|
|
96
114
|
return {
|
|
97
115
|
_truncated:
|
|
98
|
-
serialized
|
|
116
|
+
sliceWithoutSplittingSurrogates(serialized, 0, headSize) +
|
|
99
117
|
indicator +
|
|
100
|
-
|
|
118
|
+
sliceWithoutSplittingSurrogates(serialized, serialized.length - tailSize),
|
|
101
119
|
_originalChars: serialized.length,
|
|
102
120
|
};
|
|
103
121
|
}
|
|
@@ -126,12 +144,15 @@ export function truncateToolResultContent(
|
|
|
126
144
|
const indicator = `\n\n… [truncated: ${content.length} chars exceeded ${maxChars} limit] …\n\n`;
|
|
127
145
|
const available = maxChars - indicator.length;
|
|
128
146
|
if (available <= 0) {
|
|
129
|
-
return content
|
|
147
|
+
return sliceWithoutSplittingSurrogates(content, 0, maxChars);
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
// When budget is too small for a meaningful tail, fall back to head-only
|
|
133
151
|
if (available < 200) {
|
|
134
|
-
return
|
|
152
|
+
return (
|
|
153
|
+
sliceWithoutSplittingSurrogates(content, 0, available) +
|
|
154
|
+
indicator.trimEnd()
|
|
155
|
+
);
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
const headSize = Math.ceil(available * 0.7);
|
|
@@ -150,7 +171,11 @@ export function truncateToolResultContent(
|
|
|
150
171
|
tailStart = tailNewline + 1;
|
|
151
172
|
}
|
|
152
173
|
|
|
153
|
-
return
|
|
174
|
+
return (
|
|
175
|
+
sliceWithoutSplittingSurrogates(content, 0, headEnd) +
|
|
176
|
+
indicator +
|
|
177
|
+
sliceWithoutSplittingSurrogates(content, tailStart)
|
|
178
|
+
);
|
|
154
179
|
}
|
|
155
180
|
|
|
156
181
|
/** Absolute hard cap on a single tool-call input (characters). */
|