@librechat/agents 3.3.3 → 3.3.4
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/graphs/Graph.cjs +342 -109
- 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/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 +37 -10
- 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 +64 -3
- 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/main.cjs +19 -2
- 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 +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- 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 +54 -41
- package/dist/cjs/tools/ToolNode.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/graphs/Graph.mjs +349 -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/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 +37 -10
- 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 +65 -5
- 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/main.mjs +7 -7
- 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 +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- 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 +54 -41
- package/dist/esm/tools/ToolNode.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/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -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/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/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- 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/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- 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 +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- 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/messages/__tests__/observationMasking.test.ts +93 -2
- 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 +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- 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.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- 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/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- 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
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
1
3
|
import { MemorySaver } from '@langchain/langgraph';
|
|
2
|
-
import { describe, expect, it } from '@jest/globals';
|
|
3
4
|
import { Runnable } from '@langchain/core/runnables';
|
|
5
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
4
6
|
import {
|
|
5
7
|
AIMessageChunk,
|
|
6
8
|
HumanMessage,
|
|
7
9
|
AIMessage,
|
|
10
|
+
ToolMessage,
|
|
8
11
|
} from '@langchain/core/messages';
|
|
9
12
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
10
13
|
import type * as t from '@/types';
|
|
11
14
|
import { OVERFLOW_SIGNATURES } from '@/utils/__tests__/fixtures/contextOverflowSignatures';
|
|
12
|
-
import { GraphEvents, Providers } from '@/common';
|
|
15
|
+
import { ContentTypes, GraphEvents, Providers } from '@/common';
|
|
16
|
+
import * as init from '@/llm/init';
|
|
13
17
|
import { Run } from '@/run';
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -64,6 +68,36 @@ class OverflowThenSucceedModel extends Runnable<BaseMessage[], AIMessageChunk> {
|
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
class SizeBoundModel extends Runnable<BaseMessage[], AIMessageChunk> {
|
|
72
|
+
lc_namespace = ['tests'];
|
|
73
|
+
readonly toolContentChars: number[] = [];
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
private readonly maxToolContentChars: number,
|
|
77
|
+
private readonly error: Record<string, unknown>
|
|
78
|
+
) {
|
|
79
|
+
super();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async invoke(messages: BaseMessage[]): Promise<AIMessageChunk> {
|
|
83
|
+
let toolContentChars = 0;
|
|
84
|
+
for (const message of messages) {
|
|
85
|
+
if (message.getType() !== 'tool') {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
toolContentChars +=
|
|
89
|
+
typeof message.content === 'string'
|
|
90
|
+
? message.content.length
|
|
91
|
+
: JSON.stringify(message.content).length;
|
|
92
|
+
}
|
|
93
|
+
this.toolContentChars.push(toolContentChars);
|
|
94
|
+
if (toolContentChars > this.maxToolContentChars) {
|
|
95
|
+
throw throwable(this.error);
|
|
96
|
+
}
|
|
97
|
+
return new AIMessageChunk({ content: 'recovered' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
67
101
|
function buildConversation(turns: number): BaseMessage[] {
|
|
68
102
|
const messages: BaseMessage[] = [];
|
|
69
103
|
for (let i = 0; i < turns; i++) {
|
|
@@ -78,17 +112,31 @@ async function createRun(options: {
|
|
|
78
112
|
runId: string;
|
|
79
113
|
maxContextTokens: number;
|
|
80
114
|
checkpointer?: boolean;
|
|
115
|
+
provider?: Providers;
|
|
116
|
+
tokenCounter?: t.TokenCounter;
|
|
117
|
+
indexTokenCountMap?: Record<string, number>;
|
|
118
|
+
tools?: t.GraphTools;
|
|
119
|
+
maxToolResultChars?: number;
|
|
120
|
+
model?: string;
|
|
121
|
+
promptCache?: boolean;
|
|
122
|
+
toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
123
|
+
fallbacks?: t.FallbackConfig[];
|
|
81
124
|
}): Promise<Run<t.IState>> {
|
|
82
125
|
return Run.create<t.IState>({
|
|
83
126
|
runId: options.runId,
|
|
84
127
|
graphConfig: {
|
|
85
128
|
type: 'standard',
|
|
86
129
|
llmConfig: {
|
|
87
|
-
provider: Providers.ANTHROPIC,
|
|
130
|
+
provider: options.provider ?? Providers.ANTHROPIC,
|
|
131
|
+
...(options.model != null ? { model: options.model } : {}),
|
|
132
|
+
...(options.promptCache === true ? { promptCache: true } : {}),
|
|
133
|
+
...(options.fallbacks != null ? { fallbacks: options.fallbacks } : {}),
|
|
88
134
|
disableStreaming: true,
|
|
89
135
|
streamUsage: false,
|
|
90
136
|
},
|
|
91
137
|
maxContextTokens: options.maxContextTokens,
|
|
138
|
+
maxToolResultChars: options.maxToolResultChars,
|
|
139
|
+
tools: options.tools,
|
|
92
140
|
compileOptions:
|
|
93
141
|
options.checkpointer === true
|
|
94
142
|
? { checkpointer: new MemorySaver() }
|
|
@@ -96,7 +144,9 @@ async function createRun(options: {
|
|
|
96
144
|
},
|
|
97
145
|
returnContent: true,
|
|
98
146
|
skipCleanup: true,
|
|
99
|
-
tokenCounter,
|
|
147
|
+
tokenCounter: options.tokenCounter ?? tokenCounter,
|
|
148
|
+
indexTokenCountMap: options.indexTokenCountMap,
|
|
149
|
+
toolOutputReferences: options.toolOutputReferences,
|
|
100
150
|
});
|
|
101
151
|
}
|
|
102
152
|
|
|
@@ -107,6 +157,1080 @@ const streamConfig = {
|
|
|
107
157
|
};
|
|
108
158
|
|
|
109
159
|
describe('context overflow recovery', () => {
|
|
160
|
+
it('projects structured OpenAI tool content before the final payload check', async () => {
|
|
161
|
+
const toolCallId = 'tc-openai-structured';
|
|
162
|
+
const toolMessage = new ToolMessage({
|
|
163
|
+
content: [
|
|
164
|
+
{ type: ContentTypes.TEXT, text: 'rendered chart' },
|
|
165
|
+
{
|
|
166
|
+
type: 'image_url',
|
|
167
|
+
image_url: {
|
|
168
|
+
url: `data:image/png;base64,${'A'.repeat(2_000)}`,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
tool_call_id: toolCallId,
|
|
173
|
+
name: 'render_chart',
|
|
174
|
+
});
|
|
175
|
+
const messages: BaseMessage[] = [
|
|
176
|
+
new HumanMessage('render the chart'),
|
|
177
|
+
new AIMessage({
|
|
178
|
+
content: '',
|
|
179
|
+
tool_calls: [
|
|
180
|
+
{
|
|
181
|
+
id: toolCallId,
|
|
182
|
+
name: 'render_chart',
|
|
183
|
+
args: {},
|
|
184
|
+
type: 'tool_call',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
toolMessage,
|
|
189
|
+
];
|
|
190
|
+
const measuredToolContents: string[] = [];
|
|
191
|
+
const projectionCounter: t.TokenCounter = (message) => {
|
|
192
|
+
if (message.getType() === 'tool' && typeof message.content === 'string') {
|
|
193
|
+
measuredToolContents.push(message.content);
|
|
194
|
+
}
|
|
195
|
+
return typeof message.content === 'string'
|
|
196
|
+
? Math.max(1, Math.ceil(message.content.length / 4))
|
|
197
|
+
: 1;
|
|
198
|
+
};
|
|
199
|
+
const run = await createRun({
|
|
200
|
+
runId: 'openai-structured-final-projection',
|
|
201
|
+
maxContextTokens: 10_000,
|
|
202
|
+
maxToolResultChars: 200,
|
|
203
|
+
provider: Providers.OPENAI,
|
|
204
|
+
tokenCounter: projectionCounter,
|
|
205
|
+
indexTokenCountMap: {
|
|
206
|
+
0: projectionCounter(messages[0]),
|
|
207
|
+
1: projectionCounter(messages[1]),
|
|
208
|
+
2: projectionCounter(messages[2]),
|
|
209
|
+
},
|
|
210
|
+
tools: [
|
|
211
|
+
tool(async () => 'unused', {
|
|
212
|
+
name: 'render_chart',
|
|
213
|
+
description: 'Renders a chart',
|
|
214
|
+
schema: z.object({}),
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
if (!run.Graph) {
|
|
219
|
+
throw new Error('Expected graph to be initialized');
|
|
220
|
+
}
|
|
221
|
+
const model = new OverflowThenSucceedModel(signatureFor('gpt-4o-mini'), 0);
|
|
222
|
+
run.Graph.overrideModel = model;
|
|
223
|
+
|
|
224
|
+
await run.processStream({ messages }, streamConfig);
|
|
225
|
+
|
|
226
|
+
expect(model.calls).toHaveLength(1);
|
|
227
|
+
const projectedTool = model.calls[0].find(
|
|
228
|
+
(message) => message.getType() === 'tool'
|
|
229
|
+
) as ToolMessage | undefined;
|
|
230
|
+
expect(typeof projectedTool?.content).toBe('string');
|
|
231
|
+
expect((projectedTool?.content as string).length).toBeLessThanOrEqual(200);
|
|
232
|
+
expect(measuredToolContents).toContain(projectedTool?.content);
|
|
233
|
+
expect(Array.isArray(toolMessage.content)).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('measures the bounded generic-provider tool payload that is invoked', async () => {
|
|
237
|
+
const toolCallId = 'tc-google-structured';
|
|
238
|
+
const structuredContent = Array.from({ length: 25_000 }, () => ({
|
|
239
|
+
type: ContentTypes.TEXT,
|
|
240
|
+
text: 'x',
|
|
241
|
+
}));
|
|
242
|
+
const toolMessage = new ToolMessage({
|
|
243
|
+
content: structuredContent,
|
|
244
|
+
tool_call_id: toolCallId,
|
|
245
|
+
name: 'dense_result',
|
|
246
|
+
});
|
|
247
|
+
const messages: BaseMessage[] = [
|
|
248
|
+
new HumanMessage('return the dense result'),
|
|
249
|
+
new AIMessage({
|
|
250
|
+
content: '',
|
|
251
|
+
tool_calls: [
|
|
252
|
+
{
|
|
253
|
+
id: toolCallId,
|
|
254
|
+
name: 'dense_result',
|
|
255
|
+
args: {},
|
|
256
|
+
type: 'tool_call',
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
}),
|
|
260
|
+
toolMessage,
|
|
261
|
+
];
|
|
262
|
+
const measuredToolContents: string[] = [];
|
|
263
|
+
const projectionCounter: t.TokenCounter = (message) => {
|
|
264
|
+
if (message.getType() === 'tool' && typeof message.content === 'string') {
|
|
265
|
+
measuredToolContents.push(message.content);
|
|
266
|
+
}
|
|
267
|
+
return typeof message.content === 'string'
|
|
268
|
+
? Math.max(1, Math.ceil(message.content.length / 4))
|
|
269
|
+
: 1;
|
|
270
|
+
};
|
|
271
|
+
const run = await createRun({
|
|
272
|
+
runId: 'google-structured-final-projection',
|
|
273
|
+
maxContextTokens: 10_000,
|
|
274
|
+
maxToolResultChars: 2_000,
|
|
275
|
+
provider: Providers.GOOGLE,
|
|
276
|
+
tokenCounter: projectionCounter,
|
|
277
|
+
indexTokenCountMap: {
|
|
278
|
+
0: projectionCounter(messages[0]),
|
|
279
|
+
1: projectionCounter(messages[1]),
|
|
280
|
+
2: projectionCounter(messages[2]),
|
|
281
|
+
},
|
|
282
|
+
tools: [
|
|
283
|
+
tool(async () => 'unused', {
|
|
284
|
+
name: 'dense_result',
|
|
285
|
+
description: 'Returns a dense structured result',
|
|
286
|
+
schema: z.object({}),
|
|
287
|
+
}),
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
if (!run.Graph) {
|
|
291
|
+
throw new Error('Expected graph to be initialized');
|
|
292
|
+
}
|
|
293
|
+
const model = new OverflowThenSucceedModel(signatureFor('gpt-4o-mini'), 0);
|
|
294
|
+
run.Graph.overrideModel = model;
|
|
295
|
+
|
|
296
|
+
await run.processStream({ messages }, streamConfig);
|
|
297
|
+
|
|
298
|
+
expect(model.calls).toHaveLength(1);
|
|
299
|
+
const projectedTool = model.calls[0].find(
|
|
300
|
+
(message) => message.getType() === 'tool'
|
|
301
|
+
) as ToolMessage | undefined;
|
|
302
|
+
expect(typeof projectedTool?.content).toBe('string');
|
|
303
|
+
expect((projectedTool?.content as string).length).toBeLessThanOrEqual(
|
|
304
|
+
2_000
|
|
305
|
+
);
|
|
306
|
+
expect(measuredToolContents).toContain(projectedTool?.content);
|
|
307
|
+
expect(toolMessage.content).toBe(structuredContent);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('guards a fallback-specific projection before invoking the fallback', async () => {
|
|
311
|
+
const toolCallId = 'tc-fallback-structured';
|
|
312
|
+
const structuredContent = Array.from({ length: 25_000 }, () => ({
|
|
313
|
+
type: ContentTypes.TEXT,
|
|
314
|
+
text: '',
|
|
315
|
+
}));
|
|
316
|
+
const messages: BaseMessage[] = [
|
|
317
|
+
new HumanMessage('return the dense fallback result'),
|
|
318
|
+
new AIMessage({
|
|
319
|
+
content: '',
|
|
320
|
+
tool_calls: [
|
|
321
|
+
{
|
|
322
|
+
id: toolCallId,
|
|
323
|
+
name: 'dense_result',
|
|
324
|
+
args: {},
|
|
325
|
+
type: 'tool_call',
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
}),
|
|
329
|
+
new ToolMessage({
|
|
330
|
+
content: structuredContent,
|
|
331
|
+
tool_call_id: toolCallId,
|
|
332
|
+
name: 'dense_result',
|
|
333
|
+
}),
|
|
334
|
+
];
|
|
335
|
+
const projectionCounter: t.TokenCounter = (message) =>
|
|
336
|
+
typeof message.content === 'string'
|
|
337
|
+
? Math.max(1, message.content.length)
|
|
338
|
+
: 1;
|
|
339
|
+
const run = await createRun({
|
|
340
|
+
runId: 'fallback-structured-final-projection',
|
|
341
|
+
maxContextTokens: 1_000_000,
|
|
342
|
+
provider: Providers.ANTHROPIC,
|
|
343
|
+
tokenCounter: projectionCounter,
|
|
344
|
+
indexTokenCountMap: {
|
|
345
|
+
0: projectionCounter(messages[0]),
|
|
346
|
+
1: projectionCounter(messages[1]),
|
|
347
|
+
2: projectionCounter(messages[2]),
|
|
348
|
+
},
|
|
349
|
+
fallbacks: [
|
|
350
|
+
{
|
|
351
|
+
provider: Providers.GOOGLE,
|
|
352
|
+
maxContextTokens: 5_000,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
tools: [
|
|
356
|
+
tool(async () => 'unused', {
|
|
357
|
+
name: 'dense_result',
|
|
358
|
+
description: 'Returns a dense structured result',
|
|
359
|
+
schema: z.object({}),
|
|
360
|
+
}),
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
if (!run.Graph) {
|
|
364
|
+
throw new Error('Expected graph to be initialized');
|
|
365
|
+
}
|
|
366
|
+
const primary = new OverflowThenSucceedModel(
|
|
367
|
+
{ message: '503 primary unavailable' },
|
|
368
|
+
1
|
|
369
|
+
);
|
|
370
|
+
let fallbackInvocations = 0;
|
|
371
|
+
const fallback = {
|
|
372
|
+
invoke: async (): Promise<AIMessageChunk> => {
|
|
373
|
+
fallbackInvocations++;
|
|
374
|
+
return new AIMessageChunk({ content: 'fallback should not run' });
|
|
375
|
+
},
|
|
376
|
+
} as unknown as ReturnType<typeof init.initializeModel>;
|
|
377
|
+
const initializeSpy = jest
|
|
378
|
+
.spyOn(init, 'initializeModel')
|
|
379
|
+
.mockReturnValue(fallback);
|
|
380
|
+
run.Graph.overrideModel = primary;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
384
|
+
|
|
385
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
386
|
+
expect(primary.calls).toHaveLength(2);
|
|
387
|
+
expect(fallbackInvocations).toBe(0);
|
|
388
|
+
expect(
|
|
389
|
+
run.Graph.agentContexts.get('default')?.overflowRecoveryAttempts
|
|
390
|
+
).toBe(1);
|
|
391
|
+
} finally {
|
|
392
|
+
initializeSpy.mockRestore();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('projects unsafe tool-call args before measuring or invoking the provider', async () => {
|
|
397
|
+
let toJSONCalls = 0;
|
|
398
|
+
const toolCallId = 'tc-unsafe-input';
|
|
399
|
+
const unsafeArgs = {
|
|
400
|
+
query: 'safe',
|
|
401
|
+
toJSON() {
|
|
402
|
+
toJSONCalls++;
|
|
403
|
+
return { query: 'x'.repeat(100_000) };
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
const messages: BaseMessage[] = [
|
|
407
|
+
new HumanMessage('run the lookup'),
|
|
408
|
+
new AIMessage({
|
|
409
|
+
content: '',
|
|
410
|
+
tool_calls: [
|
|
411
|
+
{
|
|
412
|
+
id: toolCallId,
|
|
413
|
+
name: 'lookup_records',
|
|
414
|
+
args: unsafeArgs,
|
|
415
|
+
type: 'tool_call',
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}),
|
|
419
|
+
new ToolMessage({
|
|
420
|
+
content: 'done',
|
|
421
|
+
tool_call_id: toolCallId,
|
|
422
|
+
name: 'lookup_records',
|
|
423
|
+
}),
|
|
424
|
+
new AIMessage('The lookup completed.'),
|
|
425
|
+
new HumanMessage('continue'),
|
|
426
|
+
];
|
|
427
|
+
const run = await createRun({
|
|
428
|
+
runId: 'unsafe-tool-input-final-projection',
|
|
429
|
+
maxContextTokens: 10_000,
|
|
430
|
+
provider: Providers.OPENAI,
|
|
431
|
+
tokenCounter: () => 1,
|
|
432
|
+
indexTokenCountMap: { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 },
|
|
433
|
+
tools: [
|
|
434
|
+
tool(async () => 'unused', {
|
|
435
|
+
name: 'lookup_records',
|
|
436
|
+
description: 'Looks up records',
|
|
437
|
+
schema: z.object({}),
|
|
438
|
+
}),
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
if (!run.Graph) {
|
|
442
|
+
throw new Error('Expected graph to be initialized');
|
|
443
|
+
}
|
|
444
|
+
const model = new OverflowThenSucceedModel(signatureFor('gpt-4o-mini'), 0);
|
|
445
|
+
run.Graph.overrideModel = model;
|
|
446
|
+
|
|
447
|
+
await run.processStream({ messages }, streamConfig);
|
|
448
|
+
|
|
449
|
+
const projectedCall = (
|
|
450
|
+
model.calls[0].find((message) => message.getType() === 'ai') as AIMessage
|
|
451
|
+
).tool_calls?.[0];
|
|
452
|
+
expect(projectedCall?.args).toEqual({ query: 'safe' });
|
|
453
|
+
expect(toJSONCalls).toBe(0);
|
|
454
|
+
expect(messages[1]).toBeInstanceOf(AIMessage);
|
|
455
|
+
expect((messages[1] as AIMessage).tool_calls?.[0].args).toBe(unsafeArgs);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('compacts cached structured tool output before the first provider call', async () => {
|
|
459
|
+
const toolCallId = 'tc-structured';
|
|
460
|
+
const messages: BaseMessage[] = [
|
|
461
|
+
new HumanMessage('query the table'),
|
|
462
|
+
new AIMessage({
|
|
463
|
+
content: '',
|
|
464
|
+
tool_calls: [
|
|
465
|
+
{
|
|
466
|
+
id: toolCallId,
|
|
467
|
+
name: 'run_select_query',
|
|
468
|
+
args: {},
|
|
469
|
+
type: 'tool_call',
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
}),
|
|
473
|
+
new ToolMessage({
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: ContentTypes.TEXT,
|
|
477
|
+
text: JSON.stringify(
|
|
478
|
+
Array.from({ length: 240 }, (_, index) => ({
|
|
479
|
+
id: index,
|
|
480
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
481
|
+
}))
|
|
482
|
+
),
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
tool_call_id: toolCallId,
|
|
486
|
+
name: 'run_select_query',
|
|
487
|
+
}),
|
|
488
|
+
new AIMessage('The query returned 240 rows.'),
|
|
489
|
+
new HumanMessage('compact context'),
|
|
490
|
+
];
|
|
491
|
+
const structuredTokenCounter: t.TokenCounter = (message) => {
|
|
492
|
+
const content =
|
|
493
|
+
typeof message.content === 'string'
|
|
494
|
+
? message.content
|
|
495
|
+
: JSON.stringify(message.content);
|
|
496
|
+
return Math.ceil(content.length / 4);
|
|
497
|
+
};
|
|
498
|
+
const indexTokenCountMap: Record<string, number> = {};
|
|
499
|
+
for (let i = 0; i < messages.length; i++) {
|
|
500
|
+
indexTokenCountMap[i] = i === 2 ? 0 : structuredTokenCounter(messages[i]);
|
|
501
|
+
}
|
|
502
|
+
const run = await createRun({
|
|
503
|
+
runId: 'structured-output-preflight',
|
|
504
|
+
maxContextTokens: 5_000,
|
|
505
|
+
provider: Providers.BEDROCK,
|
|
506
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
507
|
+
tokenCounter: structuredTokenCounter,
|
|
508
|
+
indexTokenCountMap,
|
|
509
|
+
tools: [
|
|
510
|
+
tool(async () => 'unused', {
|
|
511
|
+
name: 'run_select_query',
|
|
512
|
+
description: 'Queries ClickHouse',
|
|
513
|
+
schema: z.object({}),
|
|
514
|
+
}),
|
|
515
|
+
],
|
|
516
|
+
});
|
|
517
|
+
if (!run.Graph) {
|
|
518
|
+
throw new Error('Expected graph to be initialized');
|
|
519
|
+
}
|
|
520
|
+
const model = new SizeBoundModel(
|
|
521
|
+
1_500,
|
|
522
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0')
|
|
523
|
+
);
|
|
524
|
+
run.Graph.overrideModel = model;
|
|
525
|
+
|
|
526
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
527
|
+
|
|
528
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
529
|
+
expect(model.toolContentChars).toHaveLength(1);
|
|
530
|
+
expect(model.toolContentChars[0]).toBeGreaterThan(0);
|
|
531
|
+
expect(model.toolContentChars[0]).toBeLessThanOrEqual(1_500);
|
|
532
|
+
expect(
|
|
533
|
+
run.Graph.agentContexts.get('default')?.overflowRecoveryAttempts
|
|
534
|
+
).toBe(0);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('compacts an unconsumed structured tool result before its first provider call', async () => {
|
|
538
|
+
const toolCallId = 'tc-unconsumed-structured';
|
|
539
|
+
const messages: BaseMessage[] = [
|
|
540
|
+
new HumanMessage('query the table'),
|
|
541
|
+
new AIMessage({
|
|
542
|
+
content: '',
|
|
543
|
+
tool_calls: [
|
|
544
|
+
{
|
|
545
|
+
id: toolCallId,
|
|
546
|
+
name: 'run_select_query',
|
|
547
|
+
args: {},
|
|
548
|
+
type: 'tool_call',
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
}),
|
|
552
|
+
new ToolMessage({
|
|
553
|
+
content: [
|
|
554
|
+
{
|
|
555
|
+
type: ContentTypes.TEXT,
|
|
556
|
+
text: JSON.stringify(
|
|
557
|
+
Array.from({ length: 240 }, (_, index) => ({
|
|
558
|
+
id: index,
|
|
559
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
560
|
+
}))
|
|
561
|
+
),
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
tool_call_id: toolCallId,
|
|
565
|
+
name: 'run_select_query',
|
|
566
|
+
}),
|
|
567
|
+
];
|
|
568
|
+
const structuredTokenCounter: t.TokenCounter = (message) => {
|
|
569
|
+
const content =
|
|
570
|
+
typeof message.content === 'string'
|
|
571
|
+
? message.content
|
|
572
|
+
: JSON.stringify(message.content);
|
|
573
|
+
return Math.ceil(content.length / 4);
|
|
574
|
+
};
|
|
575
|
+
const run = await createRun({
|
|
576
|
+
runId: 'unconsumed-structured-output-preflight',
|
|
577
|
+
maxContextTokens: 5_000,
|
|
578
|
+
maxToolResultChars: 1_500,
|
|
579
|
+
provider: Providers.BEDROCK,
|
|
580
|
+
tokenCounter: structuredTokenCounter,
|
|
581
|
+
indexTokenCountMap: {
|
|
582
|
+
0: structuredTokenCounter(messages[0]),
|
|
583
|
+
1: structuredTokenCounter(messages[1]),
|
|
584
|
+
2: 0,
|
|
585
|
+
},
|
|
586
|
+
tools: [
|
|
587
|
+
tool(async () => 'unused', {
|
|
588
|
+
name: 'run_select_query',
|
|
589
|
+
description: 'Queries ClickHouse',
|
|
590
|
+
schema: z.object({}),
|
|
591
|
+
}),
|
|
592
|
+
],
|
|
593
|
+
});
|
|
594
|
+
if (!run.Graph) {
|
|
595
|
+
throw new Error('Expected graph to be initialized');
|
|
596
|
+
}
|
|
597
|
+
const model = new SizeBoundModel(
|
|
598
|
+
1_500,
|
|
599
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0')
|
|
600
|
+
);
|
|
601
|
+
run.Graph.overrideModel = model;
|
|
602
|
+
|
|
603
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
604
|
+
|
|
605
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
606
|
+
expect(model.toolContentChars).toHaveLength(1);
|
|
607
|
+
expect(model.toolContentChars[0]).toBeGreaterThan(0);
|
|
608
|
+
expect(model.toolContentChars[0]).toBeLessThanOrEqual(1_500);
|
|
609
|
+
expect(
|
|
610
|
+
run.Graph.agentContexts.get('default')?.overflowRecoveryAttempts
|
|
611
|
+
).toBe(0);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('trusts the pruner baseline for an unchanged payload at high calibration', async () => {
|
|
615
|
+
const messages: BaseMessage[] = [
|
|
616
|
+
new HumanMessage('earlier question'),
|
|
617
|
+
new AIMessage('provider-counted answer'),
|
|
618
|
+
new HumanMessage('latest question'),
|
|
619
|
+
];
|
|
620
|
+
const localCounter: t.TokenCounter = (message) =>
|
|
621
|
+
message.getType() === 'ai' ? 120 : 5;
|
|
622
|
+
const providerGroundedTokenMap = { 0: 5, 1: 10, 2: 5 };
|
|
623
|
+
const run = await createRun({
|
|
624
|
+
runId: 'provider-grounded-final-projection',
|
|
625
|
+
maxContextTokens: 200,
|
|
626
|
+
tokenCounter: localCounter,
|
|
627
|
+
indexTokenCountMap: providerGroundedTokenMap,
|
|
628
|
+
});
|
|
629
|
+
if (!run.Graph) {
|
|
630
|
+
throw new Error('Expected graph to be initialized');
|
|
631
|
+
}
|
|
632
|
+
const agentContext = run.Graph.agentContexts.get('default');
|
|
633
|
+
if (agentContext == null) {
|
|
634
|
+
throw new Error('Expected default agent context');
|
|
635
|
+
}
|
|
636
|
+
agentContext.calibrationRatio = 5;
|
|
637
|
+
const model = new OverflowThenSucceedModel(
|
|
638
|
+
signatureFor('claude-haiku-4-5-20251001'),
|
|
639
|
+
0
|
|
640
|
+
);
|
|
641
|
+
run.Graph.overrideModel = model;
|
|
642
|
+
|
|
643
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
644
|
+
|
|
645
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
646
|
+
expect(model.calls).toHaveLength(1);
|
|
647
|
+
expect(model.calls[0]).toHaveLength(messages.length);
|
|
648
|
+
expect(agentContext.overflowRecoveryAttempts).toBe(0);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('does not let an unrelated provider-format shrink hide artifact growth', async () => {
|
|
652
|
+
const toolCallId = 'tc-artifact-independent-growth';
|
|
653
|
+
const artifactSentinel = 'ARTIFACT_INDEPENDENT_GROWTH_SENTINEL';
|
|
654
|
+
const toolMessage = new ToolMessage({
|
|
655
|
+
content: 'rendered',
|
|
656
|
+
tool_call_id: toolCallId,
|
|
657
|
+
name: 'render_report',
|
|
658
|
+
artifact: {
|
|
659
|
+
content: [
|
|
660
|
+
{
|
|
661
|
+
type: ContentTypes.TEXT,
|
|
662
|
+
text: artifactSentinel,
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
const messages: BaseMessage[] = [
|
|
668
|
+
new HumanMessage('render the report'),
|
|
669
|
+
new AIMessageChunk({
|
|
670
|
+
content: 'calling render_report',
|
|
671
|
+
tool_calls: [
|
|
672
|
+
{
|
|
673
|
+
id: toolCallId,
|
|
674
|
+
name: 'render_report',
|
|
675
|
+
args: {},
|
|
676
|
+
type: 'tool_call',
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
}),
|
|
680
|
+
toolMessage,
|
|
681
|
+
];
|
|
682
|
+
const transformCounter: t.TokenCounter = (message) => {
|
|
683
|
+
if (
|
|
684
|
+
message instanceof AIMessageChunk &&
|
|
685
|
+
typeof message.content === 'string'
|
|
686
|
+
) {
|
|
687
|
+
return 100;
|
|
688
|
+
}
|
|
689
|
+
if (
|
|
690
|
+
message instanceof ToolMessage &&
|
|
691
|
+
JSON.stringify(message.content).includes(artifactSentinel)
|
|
692
|
+
) {
|
|
693
|
+
return 50;
|
|
694
|
+
}
|
|
695
|
+
return 1;
|
|
696
|
+
};
|
|
697
|
+
const run = await createRun({
|
|
698
|
+
runId: 'artifact-independent-transform-growth',
|
|
699
|
+
maxContextTokens: 50,
|
|
700
|
+
provider: Providers.BEDROCK,
|
|
701
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
702
|
+
tokenCounter: transformCounter,
|
|
703
|
+
indexTokenCountMap: { 0: 1, 1: 1, 2: 1 },
|
|
704
|
+
tools: [
|
|
705
|
+
tool(async () => 'unused', {
|
|
706
|
+
name: 'render_report',
|
|
707
|
+
description: 'Renders a report',
|
|
708
|
+
schema: z.object({}),
|
|
709
|
+
}),
|
|
710
|
+
],
|
|
711
|
+
});
|
|
712
|
+
if (!run.Graph) {
|
|
713
|
+
throw new Error('Expected graph to be initialized');
|
|
714
|
+
}
|
|
715
|
+
const model = new OverflowThenSucceedModel(
|
|
716
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
|
|
717
|
+
0
|
|
718
|
+
);
|
|
719
|
+
run.Graph.overrideModel = model;
|
|
720
|
+
|
|
721
|
+
await run.processStream({ messages }, streamConfig);
|
|
722
|
+
|
|
723
|
+
expect(model.calls).toHaveLength(1);
|
|
724
|
+
expect(
|
|
725
|
+
JSON.stringify(model.calls[0].map((message) => message.content))
|
|
726
|
+
).not.toContain(artifactSentinel);
|
|
727
|
+
expect(toolMessage.artifact.content[0].text).toBe(artifactSentinel);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('preserves provider attribution for an un-IDd AI clone during orphan sanitization', async () => {
|
|
731
|
+
const droppedCallId = 'tc-dropped-ai';
|
|
732
|
+
const missingCallId = 'tc-missing-result';
|
|
733
|
+
const unrelatedResultId = 'tc-unrelated-result';
|
|
734
|
+
const messages: BaseMessage[] = [
|
|
735
|
+
new HumanMessage('earlier question'),
|
|
736
|
+
new AIMessage({
|
|
737
|
+
content: [
|
|
738
|
+
{
|
|
739
|
+
type: 'tool_use',
|
|
740
|
+
id: droppedCallId,
|
|
741
|
+
name: 'declared_tool',
|
|
742
|
+
input: {},
|
|
743
|
+
},
|
|
744
|
+
],
|
|
745
|
+
tool_calls: [
|
|
746
|
+
{
|
|
747
|
+
id: droppedCallId,
|
|
748
|
+
name: 'declared_tool',
|
|
749
|
+
args: {},
|
|
750
|
+
type: 'tool_call',
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
}),
|
|
754
|
+
new AIMessage({
|
|
755
|
+
content: 'provider-counted answer',
|
|
756
|
+
tool_calls: [
|
|
757
|
+
{
|
|
758
|
+
id: missingCallId,
|
|
759
|
+
name: 'declared_tool',
|
|
760
|
+
args: {},
|
|
761
|
+
type: 'tool_call',
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
}),
|
|
765
|
+
new ToolMessage({
|
|
766
|
+
content: 'orphaned result',
|
|
767
|
+
tool_call_id: unrelatedResultId,
|
|
768
|
+
name: 'declared_tool',
|
|
769
|
+
}),
|
|
770
|
+
new HumanMessage('latest question'),
|
|
771
|
+
];
|
|
772
|
+
const skewedCounter: t.TokenCounter = (message) => {
|
|
773
|
+
if (message.getType() !== 'ai') {
|
|
774
|
+
return 5;
|
|
775
|
+
}
|
|
776
|
+
return Array.isArray(message.content) ? 1 : 120;
|
|
777
|
+
};
|
|
778
|
+
const run = await createRun({
|
|
779
|
+
runId: 'orphan-sanitize-provider-origin',
|
|
780
|
+
maxContextTokens: 1_000,
|
|
781
|
+
provider: Providers.ANTHROPIC,
|
|
782
|
+
promptCache: true,
|
|
783
|
+
tokenCounter: skewedCounter,
|
|
784
|
+
indexTokenCountMap: { 0: 5, 1: 80, 2: 10, 3: 5, 4: 5 },
|
|
785
|
+
tools: [
|
|
786
|
+
tool(async () => 'unused', {
|
|
787
|
+
name: 'declared_tool',
|
|
788
|
+
description: 'Declared only to prevent tool-less folding',
|
|
789
|
+
schema: z.object({}),
|
|
790
|
+
}),
|
|
791
|
+
],
|
|
792
|
+
});
|
|
793
|
+
if (!run.Graph) {
|
|
794
|
+
throw new Error('Expected graph to be initialized');
|
|
795
|
+
}
|
|
796
|
+
const agentContext = run.Graph.agentContexts.get('default');
|
|
797
|
+
if (agentContext == null) {
|
|
798
|
+
throw new Error('Expected default agent context');
|
|
799
|
+
}
|
|
800
|
+
agentContext.calibrationRatio = 5;
|
|
801
|
+
const model = new OverflowThenSucceedModel(
|
|
802
|
+
signatureFor('claude-haiku-4-5-20251001'),
|
|
803
|
+
0
|
|
804
|
+
);
|
|
805
|
+
run.Graph.overrideModel = model;
|
|
806
|
+
|
|
807
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
808
|
+
|
|
809
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
810
|
+
expect(model.calls).toHaveLength(1);
|
|
811
|
+
expect(model.calls[0]).toHaveLength(3);
|
|
812
|
+
expect(model.calls[0].some((message) => message.getType() === 'tool')).toBe(
|
|
813
|
+
false
|
|
814
|
+
);
|
|
815
|
+
const sanitizedAI = model.calls[0].find(
|
|
816
|
+
(message) => message.getType() === 'ai'
|
|
817
|
+
) as AIMessage | undefined;
|
|
818
|
+
expect(sanitizedAI?.content).toBe('provider-counted answer');
|
|
819
|
+
expect(sanitizedAI?.tool_calls ?? []).toHaveLength(0);
|
|
820
|
+
expect(agentContext.overflowRecoveryAttempts).toBe(0);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('reserves the reply primer before accepting fast-path context', async () => {
|
|
824
|
+
const messages: BaseMessage[] = [
|
|
825
|
+
new HumanMessage('earlier question'),
|
|
826
|
+
new AIMessage('earlier answer'),
|
|
827
|
+
new HumanMessage('latest question'),
|
|
828
|
+
];
|
|
829
|
+
const run = await createRun({
|
|
830
|
+
runId: 'fast-path-reply-primer',
|
|
831
|
+
maxContextTokens: 100,
|
|
832
|
+
tokenCounter: () => 31,
|
|
833
|
+
indexTokenCountMap: { 0: 31, 1: 31, 2: 31 },
|
|
834
|
+
});
|
|
835
|
+
if (!run.Graph) {
|
|
836
|
+
throw new Error('Expected graph to be initialized');
|
|
837
|
+
}
|
|
838
|
+
const model = new OverflowThenSucceedModel(
|
|
839
|
+
signatureFor('claude-haiku-4-5-20251001'),
|
|
840
|
+
0
|
|
841
|
+
);
|
|
842
|
+
run.Graph.overrideModel = model;
|
|
843
|
+
|
|
844
|
+
await run.processStream({ messages }, streamConfig);
|
|
845
|
+
|
|
846
|
+
expect(model.calls).toHaveLength(1);
|
|
847
|
+
expect(model.calls[0].length).toBeLessThan(messages.length);
|
|
848
|
+
expect(3 + model.calls[0].length * 31).toBeLessThanOrEqual(95);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('includes artifact expansion when it fits the post-prune budget', async () => {
|
|
852
|
+
const toolCallId = 'tc-artifact-fits';
|
|
853
|
+
const artifactSentinel = 'ARTIFACT_FITS_SENTINEL';
|
|
854
|
+
const toolMessage = new ToolMessage({
|
|
855
|
+
content: 'rendered',
|
|
856
|
+
tool_call_id: toolCallId,
|
|
857
|
+
name: 'render_report',
|
|
858
|
+
artifact: {
|
|
859
|
+
content: [
|
|
860
|
+
{
|
|
861
|
+
type: ContentTypes.TEXT,
|
|
862
|
+
text: `${artifactSentinel}:complete`,
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
const messages: BaseMessage[] = [
|
|
868
|
+
new HumanMessage('render the report'),
|
|
869
|
+
new AIMessageChunk({
|
|
870
|
+
content: '',
|
|
871
|
+
tool_calls: [
|
|
872
|
+
{
|
|
873
|
+
id: toolCallId,
|
|
874
|
+
name: 'render_report',
|
|
875
|
+
args: {},
|
|
876
|
+
type: 'tool_call',
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
}),
|
|
880
|
+
toolMessage,
|
|
881
|
+
];
|
|
882
|
+
const artifactTokenCounter: t.TokenCounter = (message) => {
|
|
883
|
+
const content =
|
|
884
|
+
typeof message.content === 'string'
|
|
885
|
+
? message.content
|
|
886
|
+
: JSON.stringify(message.content);
|
|
887
|
+
return Math.ceil(content.length / 4);
|
|
888
|
+
};
|
|
889
|
+
const run = await createRun({
|
|
890
|
+
runId: 'artifact-budget-control',
|
|
891
|
+
maxContextTokens: 10_000,
|
|
892
|
+
maxToolResultChars: 2_000,
|
|
893
|
+
provider: Providers.BEDROCK,
|
|
894
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
895
|
+
tokenCounter: artifactTokenCounter,
|
|
896
|
+
indexTokenCountMap: {
|
|
897
|
+
0: artifactTokenCounter(messages[0]),
|
|
898
|
+
1: artifactTokenCounter(messages[1]),
|
|
899
|
+
2: artifactTokenCounter(messages[2]),
|
|
900
|
+
},
|
|
901
|
+
tools: [
|
|
902
|
+
tool(async () => 'unused', {
|
|
903
|
+
name: 'render_report',
|
|
904
|
+
description: 'Renders a report',
|
|
905
|
+
schema: z.object({}),
|
|
906
|
+
}),
|
|
907
|
+
],
|
|
908
|
+
});
|
|
909
|
+
if (!run.Graph) {
|
|
910
|
+
throw new Error('Expected graph to be initialized');
|
|
911
|
+
}
|
|
912
|
+
const model = new OverflowThenSucceedModel(
|
|
913
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
|
|
914
|
+
0
|
|
915
|
+
);
|
|
916
|
+
run.Graph.overrideModel = model;
|
|
917
|
+
|
|
918
|
+
await run.processStream({ messages }, streamConfig);
|
|
919
|
+
|
|
920
|
+
expect(model.calls).toHaveLength(1);
|
|
921
|
+
expect(
|
|
922
|
+
JSON.stringify(model.calls[0].map((message) => message.content))
|
|
923
|
+
).toContain(artifactSentinel);
|
|
924
|
+
expect(toolMessage.content).toBe('rendered');
|
|
925
|
+
expect(toolMessage.artifact.content[0].text).toContain(artifactSentinel);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('rechecks artifact expansion after provider message transforms', async () => {
|
|
929
|
+
const toolCallId = 'tc-artifact-final-transform';
|
|
930
|
+
const artifactSentinel = 'ARTIFACT_FINAL_TRANSFORM_SENTINEL';
|
|
931
|
+
const toolMessage = new ToolMessage({
|
|
932
|
+
content: 'rendered',
|
|
933
|
+
tool_call_id: toolCallId,
|
|
934
|
+
name: 'render_report',
|
|
935
|
+
artifact: {
|
|
936
|
+
content: [
|
|
937
|
+
{
|
|
938
|
+
type: ContentTypes.TEXT,
|
|
939
|
+
text: `${artifactSentinel}:complete`,
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
const messages: BaseMessage[] = [
|
|
945
|
+
new HumanMessage('render the report'),
|
|
946
|
+
new AIMessageChunk({
|
|
947
|
+
content: '',
|
|
948
|
+
tool_calls: [
|
|
949
|
+
{
|
|
950
|
+
id: toolCallId,
|
|
951
|
+
name: 'render_report',
|
|
952
|
+
args: {},
|
|
953
|
+
type: 'tool_call',
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
}),
|
|
957
|
+
toolMessage,
|
|
958
|
+
];
|
|
959
|
+
const transformSensitiveCounter: t.TokenCounter = (message) => {
|
|
960
|
+
const content =
|
|
961
|
+
typeof message.content === 'string'
|
|
962
|
+
? message.content
|
|
963
|
+
: JSON.stringify(message.content);
|
|
964
|
+
if (
|
|
965
|
+
message instanceof HumanMessage &&
|
|
966
|
+
content.includes(artifactSentinel)
|
|
967
|
+
) {
|
|
968
|
+
return 10_000;
|
|
969
|
+
}
|
|
970
|
+
return Math.max(1, Math.ceil(content.length / 4));
|
|
971
|
+
};
|
|
972
|
+
const run = await createRun({
|
|
973
|
+
runId: 'artifact-final-transform-guard',
|
|
974
|
+
maxContextTokens: 5_000,
|
|
975
|
+
maxToolResultChars: 2_000,
|
|
976
|
+
provider: Providers.BEDROCK,
|
|
977
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
978
|
+
tokenCounter: transformSensitiveCounter,
|
|
979
|
+
indexTokenCountMap: {
|
|
980
|
+
0: transformSensitiveCounter(messages[0]),
|
|
981
|
+
1: transformSensitiveCounter(messages[1]),
|
|
982
|
+
2: transformSensitiveCounter(messages[2]),
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
if (!run.Graph) {
|
|
986
|
+
throw new Error('Expected graph to be initialized');
|
|
987
|
+
}
|
|
988
|
+
const model = new OverflowThenSucceedModel(
|
|
989
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
|
|
990
|
+
0
|
|
991
|
+
);
|
|
992
|
+
run.Graph.overrideModel = model;
|
|
993
|
+
|
|
994
|
+
await run.processStream({ messages }, streamConfig);
|
|
995
|
+
|
|
996
|
+
expect(model.calls).toHaveLength(1);
|
|
997
|
+
const providerContent = JSON.stringify(
|
|
998
|
+
model.calls[0].map((message) => message.content)
|
|
999
|
+
);
|
|
1000
|
+
expect(providerContent).toContain('[Previous tool interaction]');
|
|
1001
|
+
expect(providerContent).not.toContain(artifactSentinel);
|
|
1002
|
+
expect(toolMessage.artifact.content[0].text).toContain(artifactSentinel);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('compacts expanded synthetic context without an artifact', async () => {
|
|
1006
|
+
const toolCallId = 'tc-final-transform-without-artifact';
|
|
1007
|
+
const messages: BaseMessage[] = [
|
|
1008
|
+
new HumanMessage('query the table'),
|
|
1009
|
+
new AIMessageChunk({
|
|
1010
|
+
content: '',
|
|
1011
|
+
tool_calls: [
|
|
1012
|
+
{
|
|
1013
|
+
id: toolCallId,
|
|
1014
|
+
name: 'run_select_query',
|
|
1015
|
+
args: { query: `SELECT '${'x'.repeat(5_000)}'` },
|
|
1016
|
+
type: 'tool_call',
|
|
1017
|
+
},
|
|
1018
|
+
],
|
|
1019
|
+
}),
|
|
1020
|
+
new ToolMessage({
|
|
1021
|
+
content: 'query complete',
|
|
1022
|
+
tool_call_id: toolCallId,
|
|
1023
|
+
name: 'run_select_query',
|
|
1024
|
+
}),
|
|
1025
|
+
];
|
|
1026
|
+
const transformSensitiveCounter: t.TokenCounter = (message) => {
|
|
1027
|
+
const content =
|
|
1028
|
+
typeof message.content === 'string'
|
|
1029
|
+
? message.content
|
|
1030
|
+
: JSON.stringify(message.content);
|
|
1031
|
+
if (
|
|
1032
|
+
message instanceof HumanMessage &&
|
|
1033
|
+
content.includes('[Previous tool interaction]')
|
|
1034
|
+
) {
|
|
1035
|
+
return content.length * 10;
|
|
1036
|
+
}
|
|
1037
|
+
return 1;
|
|
1038
|
+
};
|
|
1039
|
+
const run = await createRun({
|
|
1040
|
+
runId: 'final-transform-without-artifact',
|
|
1041
|
+
maxContextTokens: 500,
|
|
1042
|
+
provider: Providers.BEDROCK,
|
|
1043
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
1044
|
+
tokenCounter: transformSensitiveCounter,
|
|
1045
|
+
indexTokenCountMap: { 0: 1, 1: 1, 2: 1 },
|
|
1046
|
+
});
|
|
1047
|
+
if (!run.Graph) {
|
|
1048
|
+
throw new Error('Expected graph to be initialized');
|
|
1049
|
+
}
|
|
1050
|
+
const model = new OverflowThenSucceedModel(
|
|
1051
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
|
|
1052
|
+
0
|
|
1053
|
+
);
|
|
1054
|
+
run.Graph.overrideModel = model;
|
|
1055
|
+
|
|
1056
|
+
await run.processStream({ messages }, streamConfig);
|
|
1057
|
+
|
|
1058
|
+
expect(model.calls).toHaveLength(1);
|
|
1059
|
+
const humanMessages = model.calls[0].filter(
|
|
1060
|
+
(message) => message instanceof HumanMessage
|
|
1061
|
+
);
|
|
1062
|
+
expect(humanMessages).toHaveLength(2);
|
|
1063
|
+
expect(
|
|
1064
|
+
JSON.stringify(humanMessages[humanMessages.length - 1].content).length
|
|
1065
|
+
).toBeLessThan(100);
|
|
1066
|
+
expect(
|
|
1067
|
+
JSON.stringify(humanMessages[humanMessages.length - 1].content)
|
|
1068
|
+
).not.toContain('x'.repeat(1_000));
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('counts unresolved-reference annotations before invoking the provider', async () => {
|
|
1072
|
+
const toolCallId = 'tc-unresolved-projection';
|
|
1073
|
+
const unresolvedRefs = Array.from(
|
|
1074
|
+
{ length: 1_200 },
|
|
1075
|
+
(_, index) => `missing_tool_${index}_turn_${index}`
|
|
1076
|
+
);
|
|
1077
|
+
const messages: BaseMessage[] = [
|
|
1078
|
+
new HumanMessage(`old question ${'q'.repeat(5_500)}`),
|
|
1079
|
+
new AIMessage({
|
|
1080
|
+
content: '',
|
|
1081
|
+
tool_calls: [
|
|
1082
|
+
{
|
|
1083
|
+
id: toolCallId,
|
|
1084
|
+
name: 'lookup_records',
|
|
1085
|
+
args: {},
|
|
1086
|
+
type: 'tool_call',
|
|
1087
|
+
},
|
|
1088
|
+
],
|
|
1089
|
+
}),
|
|
1090
|
+
new ToolMessage({
|
|
1091
|
+
content: `old result ${'r'.repeat(3_000)}`,
|
|
1092
|
+
tool_call_id: toolCallId,
|
|
1093
|
+
name: 'lookup_records',
|
|
1094
|
+
additional_kwargs: { _unresolvedRefs: unresolvedRefs },
|
|
1095
|
+
}),
|
|
1096
|
+
new AIMessage(`old answer ${'a'.repeat(4_500)}`),
|
|
1097
|
+
new HumanMessage(`latest question ${'n'.repeat(3_500)}`),
|
|
1098
|
+
];
|
|
1099
|
+
const projectionCounter: t.TokenCounter = (message) => {
|
|
1100
|
+
const content =
|
|
1101
|
+
typeof message.content === 'string'
|
|
1102
|
+
? message.content
|
|
1103
|
+
: JSON.stringify(message.content);
|
|
1104
|
+
return Math.max(1, content.length);
|
|
1105
|
+
};
|
|
1106
|
+
const indexTokenCountMap: Record<string, number> = {};
|
|
1107
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1108
|
+
indexTokenCountMap[i] = projectionCounter(messages[i]);
|
|
1109
|
+
}
|
|
1110
|
+
const run = await createRun({
|
|
1111
|
+
runId: 'unresolved-reference-final-projection',
|
|
1112
|
+
maxContextTokens: 20_000,
|
|
1113
|
+
provider: Providers.ANTHROPIC,
|
|
1114
|
+
tokenCounter: projectionCounter,
|
|
1115
|
+
indexTokenCountMap,
|
|
1116
|
+
toolOutputReferences: { enabled: true },
|
|
1117
|
+
tools: [
|
|
1118
|
+
tool(async () => 'unused', {
|
|
1119
|
+
name: 'lookup_records',
|
|
1120
|
+
description: 'Looks up records',
|
|
1121
|
+
schema: z.object({}),
|
|
1122
|
+
}),
|
|
1123
|
+
],
|
|
1124
|
+
});
|
|
1125
|
+
if (!run.Graph) {
|
|
1126
|
+
throw new Error('Expected graph to be initialized');
|
|
1127
|
+
}
|
|
1128
|
+
const model = new OverflowThenSucceedModel(
|
|
1129
|
+
signatureFor('claude-haiku-4-5-20251001'),
|
|
1130
|
+
0
|
|
1131
|
+
);
|
|
1132
|
+
run.Graph.overrideModel = model;
|
|
1133
|
+
|
|
1134
|
+
const content = await run.processStream({ messages }, streamConfig);
|
|
1135
|
+
|
|
1136
|
+
expect(content).toEqual([{ type: 'text', text: 'recovered' }]);
|
|
1137
|
+
expect(model.calls).toHaveLength(1);
|
|
1138
|
+
expect(
|
|
1139
|
+
run.Graph.agentContexts.get('default')?.overflowRecoveryAttempts
|
|
1140
|
+
).toBeGreaterThan(0);
|
|
1141
|
+
const providerPayload = JSON.stringify(
|
|
1142
|
+
model.calls[0].map((message) => message.content)
|
|
1143
|
+
);
|
|
1144
|
+
expect(providerPayload).not.toContain(unresolvedRefs[0]);
|
|
1145
|
+
expect(providerPayload).not.toContain(unresolvedRefs.at(-1));
|
|
1146
|
+
const sentMessageTokens =
|
|
1147
|
+
3 +
|
|
1148
|
+
model.calls[0].reduce(
|
|
1149
|
+
(total, message) => total + projectionCounter(message),
|
|
1150
|
+
0
|
|
1151
|
+
);
|
|
1152
|
+
expect(sentMessageTokens).toBeLessThan(
|
|
1153
|
+
run.Graph.agentContexts.get('default')?.maxContextTokens ?? 0
|
|
1154
|
+
);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('omits artifact expansion that would exceed the post-prune budget', async () => {
|
|
1158
|
+
const toolCallId = 'tc-artifact';
|
|
1159
|
+
const artifactSentinel = 'ARTIFACT_SENTINEL';
|
|
1160
|
+
const toolMessage = new ToolMessage({
|
|
1161
|
+
content: 'result'.repeat(100),
|
|
1162
|
+
tool_call_id: toolCallId,
|
|
1163
|
+
name: 'render_report',
|
|
1164
|
+
artifact: {
|
|
1165
|
+
content: [
|
|
1166
|
+
{
|
|
1167
|
+
type: ContentTypes.TEXT,
|
|
1168
|
+
text: `${artifactSentinel}:${'a'.repeat(5_000)}`,
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
const messages: BaseMessage[] = [
|
|
1174
|
+
new HumanMessage('h'.repeat(2_400)),
|
|
1175
|
+
new AIMessageChunk({
|
|
1176
|
+
content: '',
|
|
1177
|
+
tool_calls: [
|
|
1178
|
+
{
|
|
1179
|
+
id: toolCallId,
|
|
1180
|
+
name: 'render_report',
|
|
1181
|
+
args: {},
|
|
1182
|
+
type: 'tool_call',
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
}),
|
|
1186
|
+
toolMessage,
|
|
1187
|
+
];
|
|
1188
|
+
const artifactTokenCounter: t.TokenCounter = (message) => {
|
|
1189
|
+
const content =
|
|
1190
|
+
typeof message.content === 'string'
|
|
1191
|
+
? message.content
|
|
1192
|
+
: JSON.stringify(message.content);
|
|
1193
|
+
return Math.ceil(content.length / 4);
|
|
1194
|
+
};
|
|
1195
|
+
const indexTokenCountMap: Record<string, number> = {};
|
|
1196
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1197
|
+
indexTokenCountMap[i] = artifactTokenCounter(messages[i]);
|
|
1198
|
+
}
|
|
1199
|
+
const run = await createRun({
|
|
1200
|
+
runId: 'artifact-budget-guard',
|
|
1201
|
+
maxContextTokens: 1_000,
|
|
1202
|
+
maxToolResultChars: 2_000,
|
|
1203
|
+
provider: Providers.BEDROCK,
|
|
1204
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
1205
|
+
tokenCounter: artifactTokenCounter,
|
|
1206
|
+
indexTokenCountMap,
|
|
1207
|
+
tools: [
|
|
1208
|
+
tool(async () => 'unused', {
|
|
1209
|
+
name: 'render_report',
|
|
1210
|
+
description: 'Renders a report',
|
|
1211
|
+
schema: z.object({}),
|
|
1212
|
+
}),
|
|
1213
|
+
],
|
|
1214
|
+
});
|
|
1215
|
+
if (!run.Graph) {
|
|
1216
|
+
throw new Error('Expected graph to be initialized');
|
|
1217
|
+
}
|
|
1218
|
+
const model = new OverflowThenSucceedModel(
|
|
1219
|
+
signatureFor('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
|
|
1220
|
+
0
|
|
1221
|
+
);
|
|
1222
|
+
run.Graph.overrideModel = model;
|
|
1223
|
+
|
|
1224
|
+
await run.processStream({ messages }, streamConfig);
|
|
1225
|
+
|
|
1226
|
+
expect(model.calls).toHaveLength(1);
|
|
1227
|
+
expect(
|
|
1228
|
+
JSON.stringify(model.calls[0].map((message) => message.content))
|
|
1229
|
+
).not.toContain(artifactSentinel);
|
|
1230
|
+
expect(toolMessage.content).toBe('result'.repeat(100));
|
|
1231
|
+
expect(toolMessage.artifact.content[0].text).toContain(artifactSentinel);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
110
1234
|
it('preserves masked tool originals while checkpointed messages survive', async () => {
|
|
111
1235
|
const run = await createRun({
|
|
112
1236
|
runId: 'overflow-originals-checkpoint',
|