@librechat/agents 3.3.3 → 3.3.5
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/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +21 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/events.cjs +10 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +793 -111
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +25 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +8 -0
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +331 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +132 -0
- package/dist/cjs/llm/preempt.cjs.map +1 -0
- package/dist/cjs/main.cjs +36 -5
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +290 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +63 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +4 -1
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +35 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +56 -63
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +9 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +19 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/events.mjs +10 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +798 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +25 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +9 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +332 -8
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +131 -0
- package/dist/esm/llm/preempt.mjs.map +1 -0
- package/dist/esm/main.mjs +14 -11
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +290 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +61 -0
- package/dist/esm/messages/handoffCue.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +4 -1
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +34 -11
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +56 -63
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +9 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +139 -2
- package/dist/types/hooks/HookRegistry.d.ts +15 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/types.d.ts +45 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +34 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/handoffCue.d.ts +40 -0
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/injected.d.ts +3 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +0 -7
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/run.d.ts +65 -0
- package/dist/types/types/stream.d.ts +1 -26
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
- package/src/common/constants.ts +21 -0
- package/src/events.ts +15 -1
- package/src/graphs/Graph.ts +1261 -198
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
- package/src/hooks/HookRegistry.ts +40 -0
- package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
- package/src/hooks/index.ts +16 -2
- package/src/hooks/types.ts +47 -3
- package/src/index.ts +1 -1
- package/src/langfuse.ts +26 -1
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
- package/src/llm/bedrock/utils/message_inputs.ts +100 -24
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +611 -8
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +454 -91
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/handoffCue.test.ts +96 -0
- package/src/messages/handoffCue.ts +78 -0
- package/src/messages/index.ts +3 -0
- package/src/messages/injected.test.ts +90 -0
- package/src/messages/injected.ts +74 -0
- package/src/messages/prune.ts +996 -183
- package/src/run.ts +91 -6
- package/src/scripts/preempt-probe.ts +330 -0
- package/src/scripts/preempt-scenarios.ts +388 -0
- package/src/session/handlers.ts +32 -12
- package/src/specs/handoffCue.test.ts +165 -0
- package/src/specs/langfuse-callbacks.test.ts +352 -2
- package/src/specs/preemptSeal.test.ts +309 -0
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +59 -23
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +150 -99
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/tool.ts +13 -2
- package/src/tools/search/types.ts +50 -3
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +1 -42
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
- package/dist/cjs/splitStream.cjs +0 -151
- package/dist/cjs/splitStream.cjs.map +0 -1
- package/dist/esm/splitStream.mjs +0 -150
- package/dist/esm/splitStream.mjs.map +0 -1
- package/dist/types/mockStream.d.ts +0 -32
- package/dist/types/splitStream.d.ts +0 -37
- package/src/mockStream.ts +0 -99
- package/src/splitStream.ts +0 -234
package/src/messages/format.ts
CHANGED
|
@@ -27,8 +27,15 @@ import type {
|
|
|
27
27
|
TPayload,
|
|
28
28
|
TMessage,
|
|
29
29
|
} from '@/types';
|
|
30
|
+
import {
|
|
31
|
+
compactToolContent,
|
|
32
|
+
getToolContentCharLength,
|
|
33
|
+
isAtomicToolContentBlock,
|
|
34
|
+
serializeStructuredValueBounded,
|
|
35
|
+
} from '@/utils/toolContent';
|
|
30
36
|
import { normalizeAnthropicToolCallId } from '@/llm/anthropic/utils/message_inputs';
|
|
31
37
|
import { toLangChainContent, toLangChainMessageFields } from './langchain';
|
|
38
|
+
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
|
|
32
39
|
import { Providers, ContentTypes, Constants } from '@/common';
|
|
33
40
|
import { emitAgentLog } from '@/utils/events';
|
|
34
41
|
|
|
@@ -459,6 +466,34 @@ function hasToolCallOutput(part: MessageContentComplex): boolean {
|
|
|
459
466
|
return output != null && output !== '';
|
|
460
467
|
}
|
|
461
468
|
|
|
469
|
+
function formatToolCallOutput(
|
|
470
|
+
output: ToolCallPart['output'] | undefined
|
|
471
|
+
): MessageContent {
|
|
472
|
+
if (output == null) {
|
|
473
|
+
return '';
|
|
474
|
+
}
|
|
475
|
+
return compactToolContent(output, HARD_MAX_TOOL_RESULT_CHARS).content;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Content for the synthetic assistant turn that separates a trailing steer
|
|
480
|
+
* from the next user turn. Non-empty by necessity — see the push site.
|
|
481
|
+
*/
|
|
482
|
+
const STEER_ANCHOR_PLACEHOLDER = '_';
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* True when an assistant message replayed as a steer and nothing followed it,
|
|
486
|
+
* so the emitted run ends on the steer's `HumanMessage`.
|
|
487
|
+
*/
|
|
488
|
+
function endsWithSteerMessage(
|
|
489
|
+
formatted: Array<RoleBearingMessage<BaseMessage>>
|
|
490
|
+
): boolean {
|
|
491
|
+
if (formatted.length === 0) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return formatted[formatted.length - 1].additional_kwargs.source === 'steer';
|
|
495
|
+
}
|
|
496
|
+
|
|
462
497
|
/**
|
|
463
498
|
* Helper function to format an assistant message
|
|
464
499
|
* @param message The message to format
|
|
@@ -718,7 +753,7 @@ function formatAssistantMessage(
|
|
|
718
753
|
new ToolMessage({
|
|
719
754
|
tool_call_id: tool_call.id ?? '',
|
|
720
755
|
name: tool_call.name,
|
|
721
|
-
content: output
|
|
756
|
+
content: formatToolCallOutput(output),
|
|
722
757
|
}),
|
|
723
758
|
'tool'
|
|
724
759
|
)
|
|
@@ -1250,7 +1285,11 @@ function contentPartCharLength(part: MessageContentComplex): number {
|
|
|
1250
1285
|
if (typeof input === 'string') {
|
|
1251
1286
|
len += input.length;
|
|
1252
1287
|
} else if (input != null && typeof input === 'object') {
|
|
1253
|
-
|
|
1288
|
+
const measured = serializeStructuredValueBounded(input, 0).originalChars;
|
|
1289
|
+
len +=
|
|
1290
|
+
measured === Number.MAX_SAFE_INTEGER
|
|
1291
|
+
? HARD_MAX_TOOL_RESULT_CHARS
|
|
1292
|
+
: Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
|
|
1254
1293
|
}
|
|
1255
1294
|
return len;
|
|
1256
1295
|
}
|
|
@@ -1316,6 +1355,36 @@ export const formatAgentMessages = (
|
|
|
1316
1355
|
| RoleBearingMessage<SystemMessage>
|
|
1317
1356
|
| RoleBearingMessage<ToolMessage>
|
|
1318
1357
|
> = [];
|
|
1358
|
+
/**
|
|
1359
|
+
* A steer ended the previous payload entry, so the next message emitted —
|
|
1360
|
+
* whichever entry finally produces one — must be separated from it by an
|
|
1361
|
+
* assistant turn. Held rather than emitted so an entry that produces
|
|
1362
|
+
* nothing cannot leave the anchor stranded as the final turn.
|
|
1363
|
+
*/
|
|
1364
|
+
let pendingSteerAnchor = false;
|
|
1365
|
+
/**
|
|
1366
|
+
* Emits the deferred anchor ahead of `next` — the message about to be
|
|
1367
|
+
* pushed. When that message is itself an assistant turn, it already IS the
|
|
1368
|
+
* separation the anchor exists to synthesize, so the intent is simply
|
|
1369
|
+
* discharged: emitting the placeholder anyway would put two assistant turns
|
|
1370
|
+
* back to back, which strict-alternation providers can reject and nothing downstream
|
|
1371
|
+
* repairs (`coalesceAdjacentUserTurns` merges user turns only).
|
|
1372
|
+
*/
|
|
1373
|
+
const flushSteerAnchor = (next: { role?: LangChainMessageRole }): void => {
|
|
1374
|
+
if (!pendingSteerAnchor) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
pendingSteerAnchor = false;
|
|
1378
|
+
if (next.role === 'assistant') {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
messages.push(
|
|
1382
|
+
withMessageRole(
|
|
1383
|
+
new AIMessage({ content: STEER_ANCHOR_PLACEHOLDER }),
|
|
1384
|
+
'assistant'
|
|
1385
|
+
)
|
|
1386
|
+
);
|
|
1387
|
+
};
|
|
1319
1388
|
// If indexTokenCountMap is provided, create a new map to track the updated indices
|
|
1320
1389
|
const updatedIndexTokenCountMap: Record<number, number> = {};
|
|
1321
1390
|
let boundaryTokenAdjustment:
|
|
@@ -1377,6 +1446,7 @@ export const formatAgentMessages = (
|
|
|
1377
1446
|
if (sourceMessageId != null && sourceMessageId !== '') {
|
|
1378
1447
|
formattedMessage.id = sourceMessageId;
|
|
1379
1448
|
}
|
|
1449
|
+
flushSteerAnchor(formattedMessage);
|
|
1380
1450
|
messages.push(formattedMessage);
|
|
1381
1451
|
|
|
1382
1452
|
// Update the index mapping for this message
|
|
@@ -1560,7 +1630,48 @@ export const formatAgentMessages = (
|
|
|
1560
1630
|
formattedMessage.id = sourceMessageId;
|
|
1561
1631
|
}
|
|
1562
1632
|
}
|
|
1633
|
+
/**
|
|
1634
|
+
* A steer that ends an assistant message leaves the replay on a
|
|
1635
|
+
* `HumanMessage`. The next payload message is itself a user turn, so the
|
|
1636
|
+
* sequence would reach the provider as two adjacent user turns — rejected
|
|
1637
|
+
* by strict-alternation providers. Anchor it with a placeholder assistant
|
|
1638
|
+
* turn.
|
|
1639
|
+
*
|
|
1640
|
+
* The placeholder must be NON-EMPTY. A string-content assistant message
|
|
1641
|
+
* with no tool calls passes through `_convertMessagesToAnthropicPayload`
|
|
1642
|
+
* verbatim — the empty-text repair there only covers array content and
|
|
1643
|
+
* tool-call turns — so an empty anchor would reach Anthropic as
|
|
1644
|
+
* `{role: 'assistant', content: ''}` and trade one invalid sequence for
|
|
1645
|
+
* another. Same single-underscore convention the Anthropic converter
|
|
1646
|
+
* already uses when it has to synthesize a non-empty block.
|
|
1647
|
+
*
|
|
1648
|
+
* Deferred rather than decided by lookahead. `i < payload.length - 1` only
|
|
1649
|
+
* proves a later ENTRY exists, not that it EMITS: entries with empty
|
|
1650
|
+
* content, and entries dropped by `applySummaryBoundary`, are skipped
|
|
1651
|
+
* silently. A trailing steer followed only by those would get the anchor
|
|
1652
|
+
* as the FINAL turn — an assistant prefill with no request after it, which
|
|
1653
|
+
* the model may simply never answer. So the intent is recorded and flushed
|
|
1654
|
+
* only when a message actually follows.
|
|
1655
|
+
*
|
|
1656
|
+
* Pushed AFTER the id stamping above, deliberately. `messagesStateReducer`
|
|
1657
|
+
* treats a repeated id as replace-in-place, so an anchor carrying the
|
|
1658
|
+
* shared `sourceMessageId` would overwrite the steer it exists to protect.
|
|
1659
|
+
* Left unstamped, it reaches the reducer with a null id and is assigned a
|
|
1660
|
+
* fresh one. `endsWithSteerMessage` reads only `additional_kwargs.source`,
|
|
1661
|
+
* so the deferral cannot change which messages get anchored.
|
|
1662
|
+
*/
|
|
1663
|
+
/**
|
|
1664
|
+
* Guarded on emission: an assistant entry whose blocks all filtered away
|
|
1665
|
+
* emits nothing, and flushing for it would strand the anchor as the final
|
|
1666
|
+
* turn — the pending flag stays set for whichever entry emits next.
|
|
1667
|
+
*/
|
|
1668
|
+
if (formattedMessages.length > 0) {
|
|
1669
|
+
flushSteerAnchor(formattedMessages[0]);
|
|
1670
|
+
}
|
|
1563
1671
|
messages.push(...formattedMessages);
|
|
1672
|
+
if (endsWithSteerMessage(formattedMessages)) {
|
|
1673
|
+
pendingSteerAnchor = true;
|
|
1674
|
+
}
|
|
1564
1675
|
|
|
1565
1676
|
// Capture index range BEFORE skill body injection so injected
|
|
1566
1677
|
// HumanMessages are excluded from the assistant's token distribution.
|
|
@@ -1689,7 +1800,14 @@ export const formatAgentMessages = (
|
|
|
1689
1800
|
if (typeof args === 'string') {
|
|
1690
1801
|
len += args.length;
|
|
1691
1802
|
} else if (args != null) {
|
|
1692
|
-
|
|
1803
|
+
const measured = serializeStructuredValueBounded(
|
|
1804
|
+
args,
|
|
1805
|
+
0
|
|
1806
|
+
).originalChars;
|
|
1807
|
+
len +=
|
|
1808
|
+
measured === Number.MAX_SAFE_INTEGER
|
|
1809
|
+
? HARD_MAX_TOOL_RESULT_CHARS
|
|
1810
|
+
: Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
|
|
1693
1811
|
}
|
|
1694
1812
|
}
|
|
1695
1813
|
}
|
|
@@ -1754,13 +1872,156 @@ export function shiftIndexTokenCountMap(
|
|
|
1754
1872
|
return shiftedMap;
|
|
1755
1873
|
}
|
|
1756
1874
|
|
|
1757
|
-
/** Block types that contain binary image data and must be preserved structurally. */
|
|
1758
|
-
const IMAGE_BLOCK_TYPES = new Set(['image_url', 'image']);
|
|
1759
|
-
|
|
1760
1875
|
/** Checks whether a BaseMessage is a tool-role message. */
|
|
1761
1876
|
const isToolMessage = (m: BaseMessage): boolean =>
|
|
1762
1877
|
m instanceof ToolMessage || ('role' in m && (m as any).role === 'tool');
|
|
1763
1878
|
|
|
1879
|
+
const PORTABLE_FOLDED_MEDIA_TYPES = new Set(['image', 'image_url']);
|
|
1880
|
+
const MAX_FOLDED_BLOCK_CHARS = 8_000;
|
|
1881
|
+
const MAX_FOLDED_CONTEXT_CHARS = HARD_MAX_TOOL_RESULT_CHARS;
|
|
1882
|
+
const MAX_FOLDED_CONTEXT_WORK = 100_000;
|
|
1883
|
+
const FOLDED_CONTEXT_TRUNCATION_NOTICE =
|
|
1884
|
+
'… [additional folded context omitted]';
|
|
1885
|
+
const syntheticProviderContextMessages = new WeakSet<BaseMessage>();
|
|
1886
|
+
|
|
1887
|
+
type FoldContextBudget = {
|
|
1888
|
+
remainingChars: number;
|
|
1889
|
+
remainingWork: number;
|
|
1890
|
+
truncated: boolean;
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
function createFoldContextBudget(): FoldContextBudget {
|
|
1894
|
+
return {
|
|
1895
|
+
remainingChars: MAX_FOLDED_CONTEXT_CHARS,
|
|
1896
|
+
remainingWork: MAX_FOLDED_CONTEXT_WORK,
|
|
1897
|
+
truncated: false,
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function readFoldedDataProperty(value: unknown, key: string): unknown {
|
|
1902
|
+
if (value == null || typeof value !== 'object') {
|
|
1903
|
+
return undefined;
|
|
1904
|
+
}
|
|
1905
|
+
try {
|
|
1906
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1907
|
+
return descriptor != null && 'value' in descriptor
|
|
1908
|
+
? descriptor.value
|
|
1909
|
+
: undefined;
|
|
1910
|
+
} catch {
|
|
1911
|
+
return undefined;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Adds one logical line without first concatenating caller-controlled strings.
|
|
1917
|
+
* The shared budget covers every synthetic message emitted by one transform,
|
|
1918
|
+
* so many individually bounded blocks cannot build an unbounded aggregate.
|
|
1919
|
+
*/
|
|
1920
|
+
function appendFoldedLine(
|
|
1921
|
+
textChunks: string[],
|
|
1922
|
+
budget: FoldContextBudget,
|
|
1923
|
+
pieces: readonly string[]
|
|
1924
|
+
): boolean {
|
|
1925
|
+
if (budget.remainingChars <= 0) {
|
|
1926
|
+
budget.truncated = true;
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const separatorChars = textChunks.length > 0 ? 1 : 0;
|
|
1931
|
+
const available = budget.remainingChars - separatorChars;
|
|
1932
|
+
if (available <= 0) {
|
|
1933
|
+
budget.remainingChars = 0;
|
|
1934
|
+
budget.truncated = true;
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
let totalChars = 0;
|
|
1939
|
+
for (const piece of pieces) {
|
|
1940
|
+
totalChars = Math.min(Number.MAX_SAFE_INTEGER, totalChars + piece.length);
|
|
1941
|
+
}
|
|
1942
|
+
if (totalChars <= available) {
|
|
1943
|
+
const line = pieces.join('');
|
|
1944
|
+
textChunks.push(line);
|
|
1945
|
+
budget.remainingChars -= separatorChars + line.length;
|
|
1946
|
+
return true;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const notice = FOLDED_CONTEXT_TRUNCATION_NOTICE.slice(0, available);
|
|
1950
|
+
let remainingHeadChars = Math.max(0, available - notice.length);
|
|
1951
|
+
const boundedPieces: string[] = [];
|
|
1952
|
+
for (const piece of pieces) {
|
|
1953
|
+
if (remainingHeadChars <= 0) {
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
const retained = piece.slice(0, remainingHeadChars);
|
|
1957
|
+
boundedPieces.push(retained);
|
|
1958
|
+
remainingHeadChars -= retained.length;
|
|
1959
|
+
}
|
|
1960
|
+
boundedPieces.push(notice);
|
|
1961
|
+
textChunks.push(boundedPieces.join(''));
|
|
1962
|
+
budget.remainingChars = 0;
|
|
1963
|
+
budget.truncated = true;
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function consumeFoldedWork(
|
|
1968
|
+
textChunks: string[],
|
|
1969
|
+
budget: FoldContextBudget
|
|
1970
|
+
): boolean {
|
|
1971
|
+
if (budget.remainingChars <= 0) {
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
if (budget.remainingWork-- > 0) {
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
appendFoldedLine(textChunks, budget, [FOLDED_CONTEXT_TRUNCATION_NOTICE]);
|
|
1978
|
+
budget.remainingChars = 0;
|
|
1979
|
+
budget.truncated = true;
|
|
1980
|
+
return false;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function serializeFoldedValue(value: unknown): string {
|
|
1984
|
+
const compacted = compactToolContent(value, MAX_FOLDED_BLOCK_CHARS).content;
|
|
1985
|
+
return typeof compacted === 'string'
|
|
1986
|
+
? compacted
|
|
1987
|
+
: serializeStructuredValueBounded(compacted, MAX_FOLDED_BLOCK_CHARS)
|
|
1988
|
+
.content;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function markSyntheticProviderContext<T extends BaseMessage>(message: T): T {
|
|
1992
|
+
syntheticProviderContextMessages.add(message);
|
|
1993
|
+
return message;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function appendSyntheticProviderContextMessage(
|
|
1997
|
+
result: BaseMessage[],
|
|
1998
|
+
parts: MessageContentComplex[]
|
|
1999
|
+
): boolean {
|
|
2000
|
+
if (parts.length === 0) {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
result.push(
|
|
2004
|
+
markSyntheticProviderContext(
|
|
2005
|
+
withMessageRole(
|
|
2006
|
+
new HumanMessage({ content: toLangChainContent(parts) }),
|
|
2007
|
+
'user'
|
|
2008
|
+
)
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Identifies provider-context placeholders created by this module without
|
|
2016
|
+
* trusting user-controlled content prefixes or leaking marker metadata onto
|
|
2017
|
+
* the provider wire.
|
|
2018
|
+
*/
|
|
2019
|
+
export function isSyntheticProviderContextMessage(
|
|
2020
|
+
message: BaseMessage
|
|
2021
|
+
): boolean {
|
|
2022
|
+
return syntheticProviderContextMessages.has(message);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1764
2025
|
/** Flushes accumulated text chunks into `parts` as a single text block. */
|
|
1765
2026
|
function flushTextChunks(
|
|
1766
2027
|
textChunks: string[],
|
|
@@ -1778,10 +2039,10 @@ function flushTextChunks(
|
|
|
1778
2039
|
|
|
1779
2040
|
/**
|
|
1780
2041
|
* Appends a single message's content to the running `textChunks` / `parts`
|
|
1781
|
-
* accumulators.
|
|
1782
|
-
* binary data
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
2042
|
+
* accumulators. Portable image blocks are shallow-copied into `parts` so
|
|
2043
|
+
* binary data never becomes text tokens. Provider-specific media/resource
|
|
2044
|
+
* blocks are retained as bounded text so a folded cross-provider history
|
|
2045
|
+
* cannot contain an unsupported native block or JSON-expand without limit.
|
|
1785
2046
|
*
|
|
1786
2047
|
* When `content` is an array containing tool_use blocks, `tool_calls` is NOT
|
|
1787
2048
|
* additionally serialized (avoiding double output). `tool_calls` is used as
|
|
@@ -1791,37 +2052,69 @@ function appendMessageContent(
|
|
|
1791
2052
|
msg: BaseMessage,
|
|
1792
2053
|
role: string,
|
|
1793
2054
|
textChunks: string[],
|
|
1794
|
-
parts: MessageContentComplex[]
|
|
2055
|
+
parts: MessageContentComplex[],
|
|
2056
|
+
budget: FoldContextBudget
|
|
1795
2057
|
): void {
|
|
1796
2058
|
const { content } = msg;
|
|
1797
2059
|
|
|
1798
2060
|
if (typeof content === 'string') {
|
|
1799
|
-
if (content) {
|
|
1800
|
-
textChunks
|
|
2061
|
+
if (content && consumeFoldedWork(textChunks, budget)) {
|
|
2062
|
+
appendFoldedLine(textChunks, budget, [`${role}: `, content]);
|
|
1801
2063
|
}
|
|
1802
|
-
appendToolCalls(msg, role, textChunks);
|
|
2064
|
+
appendToolCalls(msg, role, textChunks, budget);
|
|
1803
2065
|
return;
|
|
1804
2066
|
}
|
|
1805
2067
|
|
|
1806
2068
|
if (!Array.isArray(content)) {
|
|
1807
|
-
appendToolCalls(msg, role, textChunks);
|
|
2069
|
+
appendToolCalls(msg, role, textChunks, budget);
|
|
1808
2070
|
return;
|
|
1809
2071
|
}
|
|
1810
2072
|
|
|
1811
2073
|
let hasToolUseBlock = false;
|
|
1812
2074
|
|
|
1813
2075
|
for (const block of content as ExtendedMessageContent[]) {
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
2076
|
+
if (!consumeFoldedWork(textChunks, budget)) {
|
|
2077
|
+
hasToolUseBlock = true;
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
const blockTypeValue = readFoldedDataProperty(block, 'type');
|
|
2081
|
+
const blockType =
|
|
2082
|
+
typeof blockTypeValue === 'string' ? blockTypeValue : undefined;
|
|
2083
|
+
|
|
2084
|
+
if (
|
|
2085
|
+
blockType !== 'tool_use' &&
|
|
2086
|
+
blockType !== 'tool_call' &&
|
|
2087
|
+
blockType !== 'tool_result' &&
|
|
2088
|
+
isAtomicToolContentBlock(block)
|
|
2089
|
+
) {
|
|
2090
|
+
if (PORTABLE_FOLDED_MEDIA_TYPES.has(blockType ?? '')) {
|
|
2091
|
+
const blockChars = getToolContentCharLength([block]);
|
|
2092
|
+
if (blockChars > budget.remainingChars) {
|
|
2093
|
+
appendFoldedLine(textChunks, budget, [
|
|
2094
|
+
`${role}: [${blockType ?? 'media'} omitted: folded context limit]`,
|
|
2095
|
+
]);
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
flushTextChunks(textChunks, parts);
|
|
2099
|
+
parts.push({ ...block } as MessageContentComplex);
|
|
2100
|
+
budget.remainingChars -= blockChars;
|
|
2101
|
+
} else {
|
|
2102
|
+
appendFoldedLine(textChunks, budget, [
|
|
2103
|
+
`${role}: [${blockType ?? 'media'}] `,
|
|
2104
|
+
serializeFoldedValue(block),
|
|
2105
|
+
]);
|
|
2106
|
+
}
|
|
1817
2107
|
continue;
|
|
1818
2108
|
}
|
|
1819
2109
|
|
|
1820
|
-
if (
|
|
2110
|
+
if (blockType === 'tool_use') {
|
|
1821
2111
|
hasToolUseBlock = true;
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2112
|
+
const name = readFoldedDataProperty(block, 'name');
|
|
2113
|
+
const input = readFoldedDataProperty(block, 'input');
|
|
2114
|
+
appendFoldedLine(textChunks, budget, [
|
|
2115
|
+
`${role}: [tool_use] ${typeof name === 'string' ? name : ''} `,
|
|
2116
|
+
serializeFoldedValue(input ?? {}),
|
|
2117
|
+
]);
|
|
1825
2118
|
continue;
|
|
1826
2119
|
}
|
|
1827
2120
|
|
|
@@ -1830,17 +2123,32 @@ function appendMessageContent(
|
|
|
1830
2123
|
// toolUse) or this repo's `ToolCallContent` (`{ tool_call: { name, args,
|
|
1831
2124
|
// output } }`, from `convertMessagesToContent` / persisted history). Handle
|
|
1832
2125
|
// both, and emit any embedded output, so the name/args/result survive.
|
|
1833
|
-
if (
|
|
2126
|
+
if (blockType === 'tool_call') {
|
|
1834
2127
|
hasToolUseBlock = true;
|
|
1835
|
-
const nested = (block
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
2128
|
+
const nested = readFoldedDataProperty(block, 'tool_call');
|
|
2129
|
+
const nestedName = readFoldedDataProperty(nested, 'name');
|
|
2130
|
+
const topLevelName = readFoldedDataProperty(block, 'name');
|
|
2131
|
+
let name = '';
|
|
2132
|
+
if (typeof nestedName === 'string') {
|
|
2133
|
+
name = nestedName;
|
|
2134
|
+
} else if (typeof topLevelName === 'string') {
|
|
2135
|
+
name = topLevelName;
|
|
2136
|
+
}
|
|
2137
|
+
const nestedArgs = readFoldedDataProperty(nested, 'args');
|
|
2138
|
+
const topLevelArgs = readFoldedDataProperty(block, 'args');
|
|
2139
|
+
const rawArgs = nestedArgs ?? topLevelArgs ?? {};
|
|
1838
2140
|
const argsText =
|
|
1839
|
-
typeof rawArgs === 'string' ? rawArgs :
|
|
1840
|
-
textChunks
|
|
1841
|
-
|
|
2141
|
+
typeof rawArgs === 'string' ? rawArgs : serializeFoldedValue(rawArgs);
|
|
2142
|
+
appendFoldedLine(textChunks, budget, [
|
|
2143
|
+
`${role}: [tool_use] ${name}${argsText ? ' ' : ''}`,
|
|
2144
|
+
argsText,
|
|
2145
|
+
]);
|
|
2146
|
+
const output = readFoldedDataProperty(nested, 'output');
|
|
1842
2147
|
if (output != null && output !== '') {
|
|
1843
|
-
textChunks
|
|
2148
|
+
appendFoldedLine(textChunks, budget, [
|
|
2149
|
+
'Tool: ',
|
|
2150
|
+
typeof output === 'string' ? output : serializeFoldedValue(output),
|
|
2151
|
+
]);
|
|
1844
2152
|
}
|
|
1845
2153
|
continue;
|
|
1846
2154
|
}
|
|
@@ -1848,65 +2156,101 @@ function appendMessageContent(
|
|
|
1848
2156
|
// A `tool_result` content block (e.g. an AIMessage(tool_call) followed by a
|
|
1849
2157
|
// user message carrying the result). Preserve nested image blocks as-is
|
|
1850
2158
|
// instead of JSON-stringifying them through the generic fallback.
|
|
1851
|
-
if (
|
|
2159
|
+
if (blockType === 'tool_result') {
|
|
1852
2160
|
hasToolUseBlock = true;
|
|
1853
|
-
const inner = (block
|
|
1854
|
-
|
|
2161
|
+
const inner = readFoldedDataProperty(block, 'content') as
|
|
2162
|
+
| ToolResultContent['content']
|
|
2163
|
+
| undefined;
|
|
1855
2164
|
if (typeof inner === 'string') {
|
|
1856
2165
|
if (inner) {
|
|
1857
|
-
textChunks
|
|
2166
|
+
appendFoldedLine(textChunks, budget, [
|
|
2167
|
+
`${role}: [tool_result] `,
|
|
2168
|
+
inner,
|
|
2169
|
+
]);
|
|
1858
2170
|
}
|
|
1859
2171
|
} else if (Array.isArray(inner)) {
|
|
1860
2172
|
for (const innerBlock of inner as Array<
|
|
1861
2173
|
string | ExtendedMessageContent
|
|
1862
2174
|
>) {
|
|
2175
|
+
if (!consumeFoldedWork(textChunks, budget)) {
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
1863
2178
|
if (typeof innerBlock === 'string') {
|
|
1864
2179
|
if (innerBlock) {
|
|
1865
|
-
textChunks
|
|
2180
|
+
appendFoldedLine(textChunks, budget, [
|
|
2181
|
+
`${role}: [tool_result] `,
|
|
2182
|
+
innerBlock,
|
|
2183
|
+
]);
|
|
1866
2184
|
}
|
|
1867
|
-
} else if (IMAGE_BLOCK_TYPES.has(innerBlock.type ?? '')) {
|
|
1868
|
-
flushTextChunks(textChunks, parts);
|
|
1869
|
-
parts.push({ ...innerBlock } as MessageContentComplex);
|
|
1870
2185
|
} else {
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
2186
|
+
const innerTypeValue = readFoldedDataProperty(innerBlock, 'type');
|
|
2187
|
+
const innerType =
|
|
2188
|
+
typeof innerTypeValue === 'string' ? innerTypeValue : undefined;
|
|
2189
|
+
if (
|
|
2190
|
+
isAtomicToolContentBlock(innerBlock) &&
|
|
2191
|
+
PORTABLE_FOLDED_MEDIA_TYPES.has(innerType ?? '')
|
|
2192
|
+
) {
|
|
2193
|
+
const blockChars = getToolContentCharLength([innerBlock]);
|
|
2194
|
+
if (blockChars <= budget.remainingChars) {
|
|
2195
|
+
flushTextChunks(textChunks, parts);
|
|
2196
|
+
parts.push({ ...innerBlock } as MessageContentComplex);
|
|
2197
|
+
budget.remainingChars -= blockChars;
|
|
2198
|
+
} else {
|
|
2199
|
+
appendFoldedLine(textChunks, budget, [
|
|
2200
|
+
`${role}: [${innerType ?? 'media'} omitted: folded context limit]`,
|
|
2201
|
+
]);
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
const textValue = readFoldedDataProperty(innerBlock, 'text');
|
|
2205
|
+
const inputValue = readFoldedDataProperty(innerBlock, 'input');
|
|
2206
|
+
const innerText = textValue ?? inputValue;
|
|
2207
|
+
appendFoldedLine(textChunks, budget, [
|
|
2208
|
+
`${role}: [tool_result] `,
|
|
1874
2209
|
typeof innerText === 'string' && innerText
|
|
1875
2210
|
? innerText
|
|
1876
|
-
:
|
|
1877
|
-
|
|
1878
|
-
|
|
2211
|
+
: serializeFoldedValue(innerBlock),
|
|
2212
|
+
]);
|
|
2213
|
+
}
|
|
1879
2214
|
}
|
|
1880
2215
|
}
|
|
1881
2216
|
} else if (inner != null) {
|
|
1882
|
-
textChunks
|
|
2217
|
+
appendFoldedLine(textChunks, budget, [
|
|
2218
|
+
`${role}: [tool_result] `,
|
|
2219
|
+
serializeFoldedValue(inner),
|
|
2220
|
+
]);
|
|
1883
2221
|
}
|
|
1884
2222
|
continue;
|
|
1885
2223
|
}
|
|
1886
2224
|
|
|
1887
|
-
const text =
|
|
2225
|
+
const text =
|
|
2226
|
+
readFoldedDataProperty(block, 'text') ??
|
|
2227
|
+
readFoldedDataProperty(block, 'input');
|
|
1888
2228
|
if (typeof text === 'string' && text) {
|
|
1889
|
-
textChunks
|
|
2229
|
+
appendFoldedLine(textChunks, budget, [`${role}: `, text]);
|
|
1890
2230
|
continue;
|
|
1891
2231
|
}
|
|
1892
2232
|
|
|
1893
2233
|
// Fallback: serialize unrecognized block types to preserve context
|
|
1894
|
-
if (
|
|
1895
|
-
textChunks
|
|
2234
|
+
if (blockType != null && blockType !== '') {
|
|
2235
|
+
appendFoldedLine(textChunks, budget, [
|
|
2236
|
+
`${role}: [${blockType}] `,
|
|
2237
|
+
serializeFoldedValue(block),
|
|
2238
|
+
]);
|
|
1896
2239
|
}
|
|
1897
2240
|
}
|
|
1898
2241
|
|
|
1899
2242
|
// If content array had no tool_use blocks, fall back to tool_calls metadata
|
|
1900
2243
|
// (handles edge case: empty content array with tool_calls populated)
|
|
1901
2244
|
if (!hasToolUseBlock) {
|
|
1902
|
-
appendToolCalls(msg, role, textChunks);
|
|
2245
|
+
appendToolCalls(msg, role, textChunks, budget);
|
|
1903
2246
|
}
|
|
1904
2247
|
}
|
|
1905
2248
|
|
|
1906
2249
|
function appendToolCalls(
|
|
1907
2250
|
msg: BaseMessage,
|
|
1908
2251
|
role: string,
|
|
1909
|
-
textChunks: string[]
|
|
2252
|
+
textChunks: string[],
|
|
2253
|
+
budget: FoldContextBudget
|
|
1910
2254
|
): void {
|
|
1911
2255
|
if (role !== 'AI') {
|
|
1912
2256
|
return;
|
|
@@ -1914,7 +2258,16 @@ function appendToolCalls(
|
|
|
1914
2258
|
const aiMsg = msg as AIMessage;
|
|
1915
2259
|
if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
|
|
1916
2260
|
for (const tc of aiMsg.tool_calls) {
|
|
1917
|
-
|
|
2261
|
+
if (!consumeFoldedWork(textChunks, budget)) {
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
const name = readFoldedDataProperty(tc, 'name');
|
|
2265
|
+
const args = readFoldedDataProperty(tc, 'args');
|
|
2266
|
+
appendFoldedLine(textChunks, budget, [
|
|
2267
|
+
`AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
|
|
2268
|
+
serializeFoldedValue(args),
|
|
2269
|
+
')',
|
|
2270
|
+
]);
|
|
1918
2271
|
}
|
|
1919
2272
|
return;
|
|
1920
2273
|
}
|
|
@@ -1924,14 +2277,20 @@ function appendToolCalls(
|
|
|
1924
2277
|
return;
|
|
1925
2278
|
}
|
|
1926
2279
|
for (const tc of rawToolCalls) {
|
|
1927
|
-
|
|
1928
|
-
|
|
2280
|
+
if (!consumeFoldedWork(textChunks, budget)) {
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
const fn = readFoldedDataProperty(tc, 'function');
|
|
1929
2284
|
if (fn == null) {
|
|
1930
2285
|
continue;
|
|
1931
2286
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2287
|
+
const name = readFoldedDataProperty(fn, 'name');
|
|
2288
|
+
const args = readFoldedDataProperty(fn, 'arguments');
|
|
2289
|
+
appendFoldedLine(textChunks, budget, [
|
|
2290
|
+
`AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
|
|
2291
|
+
typeof args === 'string' ? args : '',
|
|
2292
|
+
')',
|
|
2293
|
+
]);
|
|
1935
2294
|
}
|
|
1936
2295
|
}
|
|
1937
2296
|
|
|
@@ -1991,6 +2350,7 @@ export function ensureThinkingBlockInMessages(
|
|
|
1991
2350
|
|
|
1992
2351
|
const result: BaseMessage[] =
|
|
1993
2352
|
lastHumanIndex >= 0 ? messages.slice(0, lastHumanIndex + 1) : [];
|
|
2353
|
+
const foldBudget = createFoldContextBudget();
|
|
1994
2354
|
let i = lastHumanIndex + 1;
|
|
1995
2355
|
|
|
1996
2356
|
while (i < messages.length) {
|
|
@@ -2021,13 +2381,14 @@ export function ensureThinkingBlockInMessages(
|
|
|
2021
2381
|
if (typeof c !== 'object') {
|
|
2022
2382
|
continue;
|
|
2023
2383
|
}
|
|
2024
|
-
|
|
2384
|
+
const type = readFoldedDataProperty(c, 'type');
|
|
2385
|
+
if (type === 'tool_use') {
|
|
2025
2386
|
hasToolUse = true;
|
|
2026
2387
|
} else if (
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2388
|
+
type === ContentTypes.THINKING ||
|
|
2389
|
+
type === ContentTypes.REASONING_CONTENT ||
|
|
2390
|
+
type === ContentTypes.REASONING ||
|
|
2391
|
+
type === 'redacted_thinking'
|
|
2031
2392
|
) {
|
|
2032
2393
|
hasThinkingBlock = true;
|
|
2033
2394
|
}
|
|
@@ -2082,30 +2443,33 @@ export function ensureThinkingBlockInMessages(
|
|
|
2082
2443
|
// ToolMessages — preserves image blocks as-is to avoid serializing
|
|
2083
2444
|
// binary data as text (which caused 174× token amplification).
|
|
2084
2445
|
const parts: MessageContentComplex[] = [];
|
|
2085
|
-
const textChunks: string[] = [
|
|
2446
|
+
const textChunks: string[] = [];
|
|
2447
|
+
appendFoldedLine(textChunks, foldBudget, ['[Previous agent context]']);
|
|
2086
2448
|
|
|
2087
|
-
appendMessageContent(msg, 'AI', textChunks, parts);
|
|
2449
|
+
appendMessageContent(msg, 'AI', textChunks, parts, foldBudget);
|
|
2088
2450
|
|
|
2089
2451
|
let j = i + 1;
|
|
2090
2452
|
while (j < messages.length && isToolMessage(messages[j])) {
|
|
2091
|
-
appendMessageContent(
|
|
2453
|
+
appendMessageContent(
|
|
2454
|
+
messages[j],
|
|
2455
|
+
'Tool',
|
|
2456
|
+
textChunks,
|
|
2457
|
+
parts,
|
|
2458
|
+
foldBudget
|
|
2459
|
+
);
|
|
2092
2460
|
j++;
|
|
2093
2461
|
}
|
|
2094
2462
|
|
|
2095
2463
|
flushTextChunks(textChunks, parts);
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
new HumanMessage({ content: toLangChainContent(parts) }),
|
|
2106
|
-
'user'
|
|
2107
|
-
)
|
|
2108
|
-
);
|
|
2464
|
+
if (appendSyntheticProviderContextMessage(result, parts)) {
|
|
2465
|
+
emitAgentLog(
|
|
2466
|
+
config,
|
|
2467
|
+
'warn',
|
|
2468
|
+
'format',
|
|
2469
|
+
'ensureThinkingBlockInMessages: injecting [Previous agent context] HumanMessage' +
|
|
2470
|
+
` (${parts.length} msgs at index ${i}, no thinking block in chain)`
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2109
2473
|
i = j;
|
|
2110
2474
|
} else {
|
|
2111
2475
|
// Keep the message as is
|
|
@@ -2140,11 +2504,10 @@ function messageHasToolContent(msg: BaseMessage): boolean {
|
|
|
2140
2504
|
}
|
|
2141
2505
|
if (Array.isArray(msg.content)) {
|
|
2142
2506
|
for (const block of msg.content as ExtendedMessageContent[]) {
|
|
2507
|
+
const type = readFoldedDataProperty(block, 'type');
|
|
2143
2508
|
if (
|
|
2144
2509
|
typeof block === 'object' &&
|
|
2145
|
-
(
|
|
2146
|
-
block.type === 'tool_call' ||
|
|
2147
|
-
block.type === 'tool_result')
|
|
2510
|
+
(type === 'tool_use' || type === 'tool_call' || type === 'tool_result')
|
|
2148
2511
|
) {
|
|
2149
2512
|
return true;
|
|
2150
2513
|
}
|
|
@@ -2164,7 +2527,9 @@ function isToolResultMessage(msg: BaseMessage): boolean {
|
|
|
2164
2527
|
}
|
|
2165
2528
|
if (Array.isArray(msg.content)) {
|
|
2166
2529
|
return (msg.content as ExtendedMessageContent[]).some(
|
|
2167
|
-
(block) =>
|
|
2530
|
+
(block) =>
|
|
2531
|
+
typeof block === 'object' &&
|
|
2532
|
+
readFoldedDataProperty(block, 'type') === 'tool_result'
|
|
2168
2533
|
);
|
|
2169
2534
|
}
|
|
2170
2535
|
return false;
|
|
@@ -2197,6 +2562,7 @@ export function foldToolBlocksForToollessAgent(
|
|
|
2197
2562
|
): BaseMessage[] {
|
|
2198
2563
|
let result: BaseMessage[] | null = null;
|
|
2199
2564
|
let foldedCount = 0;
|
|
2565
|
+
const foldBudget = createFoldContextBudget();
|
|
2200
2566
|
let i = 0;
|
|
2201
2567
|
while (i < messages.length) {
|
|
2202
2568
|
const msg = messages[i];
|
|
@@ -2212,29 +2578,26 @@ export function foldToolBlocksForToollessAgent(
|
|
|
2212
2578
|
}
|
|
2213
2579
|
|
|
2214
2580
|
const parts: MessageContentComplex[] = [];
|
|
2215
|
-
const textChunks: string[] = [
|
|
2581
|
+
const textChunks: string[] = [];
|
|
2582
|
+
appendFoldedLine(textChunks, foldBudget, ['[Previous tool interaction]']);
|
|
2216
2583
|
appendMessageContent(
|
|
2217
2584
|
msg,
|
|
2218
2585
|
isToolResultMessage(msg) ? 'Tool' : 'AI',
|
|
2219
2586
|
textChunks,
|
|
2220
|
-
parts
|
|
2587
|
+
parts,
|
|
2588
|
+
foldBudget
|
|
2221
2589
|
);
|
|
2222
2590
|
foldedCount++;
|
|
2223
2591
|
|
|
2224
2592
|
let j = i + 1;
|
|
2225
2593
|
while (j < messages.length && isToolResultMessage(messages[j])) {
|
|
2226
|
-
appendMessageContent(messages[j], 'Tool', textChunks, parts);
|
|
2594
|
+
appendMessageContent(messages[j], 'Tool', textChunks, parts, foldBudget);
|
|
2227
2595
|
foldedCount++;
|
|
2228
2596
|
j++;
|
|
2229
2597
|
}
|
|
2230
2598
|
|
|
2231
2599
|
flushTextChunks(textChunks, parts);
|
|
2232
|
-
result
|
|
2233
|
-
withMessageRole(
|
|
2234
|
-
new HumanMessage({ content: toLangChainContent(parts) }),
|
|
2235
|
-
'user'
|
|
2236
|
-
)
|
|
2237
|
-
);
|
|
2600
|
+
appendSyntheticProviderContextMessage(result, parts);
|
|
2238
2601
|
i = j;
|
|
2239
2602
|
}
|
|
2240
2603
|
|