@librechat/agents 3.3.13 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +121 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +35 -206
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +121 -241
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +19 -12
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/llm/mistral/index.cjs +26 -0
- package/dist/cjs/llm/mistral/index.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +82 -80
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs +19 -19
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
- package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
- package/dist/cjs/llm/stream/smoother.cjs +369 -0
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
- package/dist/cjs/llm/vertexai/index.cjs +13 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +25 -16
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +82 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs +316 -94
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +664 -80
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/cjs/utils/tokens.cjs +76 -29
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +121 -27
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +34 -205
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +120 -240
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +19 -12
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/llm/mistral/index.mjs +26 -0
- package/dist/esm/llm/mistral/index.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +82 -80
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs +3 -3
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
- package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
- package/dist/esm/llm/stream/smoother.mjs +365 -0
- package/dist/esm/llm/stream/smoother.mjs.map +1 -0
- package/dist/esm/llm/vertexai/index.mjs +13 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +13 -11
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +83 -16
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs +317 -95
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +665 -81
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/esm/utils/tokens.mjs +76 -30
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +19 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +2 -0
- package/dist/types/llm/mistral/index.d.ts +11 -0
- package/dist/types/llm/openai/index.d.ts +20 -4
- package/dist/types/llm/openrouter/index.d.ts +4 -1
- package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
- package/dist/types/llm/stream/smoother.d.ts +95 -0
- package/dist/types/llm/vertexai/index.d.ts +2 -0
- package/dist/types/run.d.ts +4 -1
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +49 -7
- package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +5 -5
- package/dist/types/types/hitl.d.ts +15 -0
- package/dist/types/types/llm.d.ts +21 -14
- package/dist/types/utils/tokens.d.ts +10 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +257 -52
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +8 -0
- package/src/llm/anthropic/index.ts +85 -354
- package/src/llm/bedrock/index.ts +174 -390
- package/src/llm/bedrock/llm.spec.ts +2 -0
- package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
- package/src/llm/custom-chat-models.smoke.test.ts +16 -1
- package/src/llm/google/index.ts +17 -18
- package/src/llm/google/streamSmoothing.test.ts +121 -0
- package/src/llm/mistral/index.ts +33 -0
- package/src/llm/mistral/streamSmoothing.test.ts +97 -0
- package/src/llm/openai/deepseek.test.ts +56 -0
- package/src/llm/openai/index.ts +119 -126
- package/src/llm/openrouter/index.ts +4 -1
- package/src/llm/providers.ts +3 -3
- package/src/llm/stream/chunkAdapters.test.ts +202 -0
- package/src/llm/stream/chunkAdapters.ts +317 -0
- package/src/llm/stream/reassembly.test.ts +241 -0
- package/src/llm/stream/smoother.bench.test.ts +155 -0
- package/src/llm/stream/smoother.test.ts +519 -0
- package/src/llm/stream/smoother.ts +574 -0
- package/src/llm/vertexai/index.ts +17 -1
- package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +164 -18
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/subagent.test.ts +44 -0
- package/src/specs/tokens.test.ts +159 -14
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/ToolNode.ts +631 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +590 -7
- package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/subagent/SubagentExecutor.ts +1438 -136
- package/src/tools/subagent/SubagentReplay.ts +575 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +5 -5
- package/src/types/hitl.ts +16 -0
- package/src/types/llm.ts +53 -36
- package/src/utils/tokens.ts +115 -30
package/src/specs/tokens.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
estimateImageBlockTokens,
|
|
14
14
|
estimateDocumentBlockTokens,
|
|
15
15
|
estimateTimedMediaBlockTokens,
|
|
16
|
+
UnsafeTokenMeasurementError,
|
|
16
17
|
} from '@/utils/tokens';
|
|
17
18
|
|
|
18
19
|
/** Builds a minimal PNG data URI whose IHDR encodes the given dimensions. */
|
|
@@ -71,6 +72,32 @@ describe('createTokenCounter with different encodings', () => {
|
|
|
71
72
|
expect(count).toBeGreaterThan(0);
|
|
72
73
|
});
|
|
73
74
|
|
|
75
|
+
test('claude correction keeps proxy-backed tool history safely measurable', async () => {
|
|
76
|
+
let getCalls = 0;
|
|
77
|
+
const args = new Proxy(
|
|
78
|
+
{ query: 'select * from reports' },
|
|
79
|
+
{
|
|
80
|
+
get(target, property, receiver) {
|
|
81
|
+
getCalls++;
|
|
82
|
+
return Reflect.get(target, property, receiver);
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
const message = {
|
|
87
|
+
content: '',
|
|
88
|
+
tool_calls: [{ id: 'proxy-call', name: 'query', args }],
|
|
89
|
+
getType: () => 'ai',
|
|
90
|
+
} as unknown as AIMessage;
|
|
91
|
+
const counter = await createTokenCounter('claude');
|
|
92
|
+
|
|
93
|
+
const count = counter(message);
|
|
94
|
+
|
|
95
|
+
expect(Number.isSafeInteger(count)).toBe(true);
|
|
96
|
+
expect(count).toBeGreaterThan(0);
|
|
97
|
+
expect(count).toBeLessThan(1_000);
|
|
98
|
+
expect(getCalls).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
74
101
|
test('o200k_base encoding produces valid token counts', async () => {
|
|
75
102
|
const counter = await createTokenCounter('o200k_base');
|
|
76
103
|
const msg = new HumanMessage('Hello, world!');
|
|
@@ -110,6 +137,22 @@ describe('getTokenCountForMessage', () => {
|
|
|
110
137
|
expect(count).toBe(600_003);
|
|
111
138
|
});
|
|
112
139
|
|
|
140
|
+
test.each([
|
|
141
|
+
-1,
|
|
142
|
+
1.5,
|
|
143
|
+
Number.MAX_SAFE_INTEGER,
|
|
144
|
+
Number.MAX_SAFE_INTEGER + 1,
|
|
145
|
+
Number.POSITIVE_INFINITY,
|
|
146
|
+
Number.NaN,
|
|
147
|
+
])('rejects an unsafe tokenizer result of %s', (unsafeCount) => {
|
|
148
|
+
expect(() =>
|
|
149
|
+
getTokenCountForMessage(
|
|
150
|
+
new HumanMessage('unsafe count'),
|
|
151
|
+
() => unsafeCount
|
|
152
|
+
)
|
|
153
|
+
).toThrow(UnsafeTokenMeasurementError);
|
|
154
|
+
});
|
|
155
|
+
|
|
113
156
|
test('bounds direct string tool args before tokenization and charges omitted characters', () => {
|
|
114
157
|
const callbackLengths: number[] = [];
|
|
115
158
|
const args = 'x'.repeat(300_000);
|
|
@@ -153,10 +196,13 @@ describe('getTokenCountForMessage', () => {
|
|
|
153
196
|
);
|
|
154
197
|
|
|
155
198
|
expect(Math.max(...callbackLengths)).toBeLessThanOrEqual(200_000);
|
|
156
|
-
|
|
199
|
+
// A traversal-work sentinel is unknown size, not real context usage.
|
|
200
|
+
// The provider-input projection will compact the value before invoke.
|
|
201
|
+
expect(count).toBeGreaterThan(0);
|
|
202
|
+
expect(count).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
157
203
|
});
|
|
158
204
|
|
|
159
|
-
test('
|
|
205
|
+
test('counts an opaque proxy argument as its bounded provider-safe placeholder', () => {
|
|
160
206
|
let prototypeCalls = 0;
|
|
161
207
|
const args = new Proxy(
|
|
162
208
|
{ safe: true },
|
|
@@ -171,11 +217,11 @@ describe('getTokenCountForMessage', () => {
|
|
|
171
217
|
expect(hasUnsafeStructuredSerialization(args)).toBe(true);
|
|
172
218
|
expect(
|
|
173
219
|
getTokenCountForMessage(messageWithToolArgs(args), (text) => text.length)
|
|
174
|
-
).
|
|
220
|
+
).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
175
221
|
expect(prototypeCalls).toBeLessThanOrEqual(2);
|
|
176
222
|
});
|
|
177
223
|
|
|
178
|
-
test('
|
|
224
|
+
test('does not recurse through self-referential prototype proxies', () => {
|
|
179
225
|
let prototypeCalls = 0;
|
|
180
226
|
const args: Record<string, unknown> = new Proxy<Record<string, unknown>>(
|
|
181
227
|
{},
|
|
@@ -190,11 +236,11 @@ describe('getTokenCountForMessage', () => {
|
|
|
190
236
|
expect(hasUnsafeStructuredSerialization(args)).toBe(true);
|
|
191
237
|
expect(
|
|
192
238
|
getTokenCountForMessage(messageWithToolArgs(args), (text) => text.length)
|
|
193
|
-
).
|
|
239
|
+
).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
194
240
|
expect(prototypeCalls).toBeLessThanOrEqual(2);
|
|
195
241
|
});
|
|
196
242
|
|
|
197
|
-
test('
|
|
243
|
+
test('uses bounded placeholders when descriptor and own-key proxy traps throw', () => {
|
|
198
244
|
const descriptorProxy = new Proxy(
|
|
199
245
|
{},
|
|
200
246
|
{
|
|
@@ -219,11 +265,11 @@ describe('getTokenCountForMessage', () => {
|
|
|
219
265
|
messageWithToolArgs(args),
|
|
220
266
|
(text) => text.length
|
|
221
267
|
)
|
|
222
|
-
).
|
|
268
|
+
).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
223
269
|
}
|
|
224
270
|
});
|
|
225
271
|
|
|
226
|
-
test('
|
|
272
|
+
test('does not invoke get traps or revoked proxies while measuring arguments', () => {
|
|
227
273
|
let getCalls = 0;
|
|
228
274
|
const getProxy = new Proxy(
|
|
229
275
|
{},
|
|
@@ -250,12 +296,12 @@ describe('getTokenCountForMessage', () => {
|
|
|
250
296
|
messageWithToolArgs(args),
|
|
251
297
|
(text) => text.length
|
|
252
298
|
)
|
|
253
|
-
).
|
|
299
|
+
).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
254
300
|
}
|
|
255
301
|
expect(getCalls).toBe(0);
|
|
256
302
|
});
|
|
257
303
|
|
|
258
|
-
test('
|
|
304
|
+
test('reports a typed terminal error for a proxied content block', () => {
|
|
259
305
|
let getCalls = 0;
|
|
260
306
|
const contentBlock = new Proxy(
|
|
261
307
|
{ type: 'text', text: 'safe' },
|
|
@@ -271,12 +317,88 @@ describe('getTokenCountForMessage', () => {
|
|
|
271
317
|
getType: () => 'tool',
|
|
272
318
|
} as unknown as ToolMessage;
|
|
273
319
|
|
|
274
|
-
expect(getTokenCountForMessage(message, (text) => text.length)).
|
|
275
|
-
|
|
320
|
+
expect(() => getTokenCountForMessage(message, (text) => text.length)).toThrow(
|
|
321
|
+
UnsafeTokenMeasurementError
|
|
276
322
|
);
|
|
323
|
+
try {
|
|
324
|
+
getTokenCountForMessage(message, (text) => text.length);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
expect(JSON.parse((error as Error).message)).toEqual({
|
|
327
|
+
type: 'unsafe_token_measurement',
|
|
328
|
+
reason: 'content_proxy',
|
|
329
|
+
path: 'content[0]',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
277
332
|
expect(getCalls).toBe(0);
|
|
278
333
|
});
|
|
279
334
|
|
|
335
|
+
test('reports typed errors for unsafe message and metadata branches', () => {
|
|
336
|
+
let getterCalls = 0;
|
|
337
|
+
const messageProxy = new Proxy(new HumanMessage('secret payload'), {
|
|
338
|
+
get() {
|
|
339
|
+
getterCalls++;
|
|
340
|
+
throw new Error('message read denied');
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const metadataProxy = new Proxy(
|
|
344
|
+
{},
|
|
345
|
+
{
|
|
346
|
+
get() {
|
|
347
|
+
getterCalls++;
|
|
348
|
+
throw new Error('metadata read denied');
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
const accessorMetadata = {};
|
|
353
|
+
Object.defineProperty(accessorMetadata, 'type', {
|
|
354
|
+
get() {
|
|
355
|
+
getterCalls++;
|
|
356
|
+
return 'computer_call_output';
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
const cases = [
|
|
360
|
+
{
|
|
361
|
+
message: messageProxy,
|
|
362
|
+
reason: 'message_proxy',
|
|
363
|
+
path: 'message',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
message: {
|
|
367
|
+
content: '',
|
|
368
|
+
additional_kwargs: metadataProxy,
|
|
369
|
+
getType: () => 'tool',
|
|
370
|
+
} as unknown as ToolMessage,
|
|
371
|
+
reason: 'metadata_proxy',
|
|
372
|
+
path: 'additional_kwargs',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
message: {
|
|
376
|
+
content: '',
|
|
377
|
+
additional_kwargs: accessorMetadata,
|
|
378
|
+
getType: () => 'tool',
|
|
379
|
+
} as unknown as ToolMessage,
|
|
380
|
+
reason: 'metadata_accessor',
|
|
381
|
+
path: 'additional_kwargs.type',
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
for (const testCase of cases) {
|
|
386
|
+
try {
|
|
387
|
+
getTokenCountForMessage(testCase.message, (text) => text.length);
|
|
388
|
+
throw new Error('Expected token measurement to fail');
|
|
389
|
+
} catch (error) {
|
|
390
|
+
expect(error).toBeInstanceOf(UnsafeTokenMeasurementError);
|
|
391
|
+
expect(JSON.parse((error as Error).message)).toEqual({
|
|
392
|
+
type: 'unsafe_token_measurement',
|
|
393
|
+
reason: testCase.reason,
|
|
394
|
+
path: testCase.path,
|
|
395
|
+
});
|
|
396
|
+
expect((error as Error).message).not.toContain('secret payload');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
expect(getterCalls).toBe(0);
|
|
400
|
+
});
|
|
401
|
+
|
|
280
402
|
test('detects inherited accessors without invoking them', () => {
|
|
281
403
|
let accessorCalls = 0;
|
|
282
404
|
const prototype = {};
|
|
@@ -292,10 +414,32 @@ describe('getTokenCountForMessage', () => {
|
|
|
292
414
|
expect(hasUnsafeStructuredSerialization(args)).toBe(true);
|
|
293
415
|
expect(
|
|
294
416
|
getTokenCountForMessage(messageWithToolArgs(args), (text) => text.length)
|
|
295
|
-
).
|
|
417
|
+
).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
296
418
|
expect(accessorCalls).toBe(0);
|
|
297
419
|
});
|
|
298
420
|
|
|
421
|
+
test('counts safely serializable proxy tool-call history without invoking getters', () => {
|
|
422
|
+
let getCalls = 0;
|
|
423
|
+
const args = new Proxy(
|
|
424
|
+
{ query: 'select * from reports' },
|
|
425
|
+
{
|
|
426
|
+
get(target, property, receiver) {
|
|
427
|
+
getCalls++;
|
|
428
|
+
return Reflect.get(target, property, receiver);
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const count = getTokenCountForMessage(
|
|
434
|
+
messageWithToolArgs(args),
|
|
435
|
+
(text) => text.length
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(count).toBeGreaterThan(0);
|
|
439
|
+
expect(count).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
440
|
+
expect(getCalls).toBe(0);
|
|
441
|
+
});
|
|
442
|
+
|
|
299
443
|
test('counts tool_calls-only names and arguments', () => {
|
|
300
444
|
const message = new AIMessage({
|
|
301
445
|
content: '',
|
|
@@ -334,7 +478,8 @@ describe('getTokenCountForMessage', () => {
|
|
|
334
478
|
|
|
335
479
|
const count = getTokenCountForMessage(message, (text) => text.length);
|
|
336
480
|
|
|
337
|
-
expect(count).
|
|
481
|
+
expect(count).toBeLessThan(Number.MAX_SAFE_INTEGER);
|
|
482
|
+
expect(count).toBeGreaterThan(0);
|
|
338
483
|
expect(toJSONCalls).toBe(0);
|
|
339
484
|
});
|
|
340
485
|
|
|
@@ -1059,6 +1059,24 @@ describe('recency window — first-turn protection', () => {
|
|
|
1059
1059
|
expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm2' });
|
|
1060
1060
|
});
|
|
1061
1061
|
|
|
1062
|
+
it('anchors a derived retained message on its persisted source id', async () => {
|
|
1063
|
+
const summaryBlock = await runCompaction([
|
|
1064
|
+
new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
|
|
1065
|
+
new AIMessage({ content: 'pre-steer reply', id: 'm2' }),
|
|
1066
|
+
new HumanMessage({
|
|
1067
|
+
content: 'steer',
|
|
1068
|
+
id: 'reducer-uuid',
|
|
1069
|
+
additional_kwargs: {
|
|
1070
|
+
role: 'user',
|
|
1071
|
+
source: 'steer',
|
|
1072
|
+
sourceMessageId: 'm2',
|
|
1073
|
+
},
|
|
1074
|
+
}),
|
|
1075
|
+
]);
|
|
1076
|
+
|
|
1077
|
+
expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm2' });
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1062
1080
|
/** `formatAgentMessages` reconstructs skill bodies inside its payload loop
|
|
1063
1081
|
* and keeps processing payload entries after, so this unstamped entry — a
|
|
1064
1082
|
* reducer UUID by the time compaction sees it — precedes stamped messages.
|
|
@@ -1770,13 +1788,13 @@ describe('summarize node breaker capture', () => {
|
|
|
1770
1788
|
const entryBreaker = new AbortController();
|
|
1771
1789
|
/** The trip lands while ON_SUMMARIZE_START is awaited — after the
|
|
1772
1790
|
* entry check already passed. */
|
|
1773
|
-
jest
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1791
|
+
jest
|
|
1792
|
+
.spyOn(eventUtils, 'safeDispatchCustomEvent')
|
|
1793
|
+
.mockImplementation((async (...args: unknown[]) => {
|
|
1794
|
+
if (args[0] === GraphEvents.ON_SUMMARIZE_START) {
|
|
1795
|
+
entryBreaker.abort(trip);
|
|
1796
|
+
}
|
|
1797
|
+
}) as never);
|
|
1780
1798
|
const modelClassSpy = jest
|
|
1781
1799
|
.spyOn(providers, 'getChatModelClass')
|
|
1782
1800
|
.mockReturnValue(
|
|
@@ -1915,13 +1933,13 @@ describe('summarize node breaker capture', () => {
|
|
|
1915
1933
|
const entryBreaker = new AbortController();
|
|
1916
1934
|
const lateBreaker = new AbortController();
|
|
1917
1935
|
let started = false;
|
|
1918
|
-
jest
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1936
|
+
jest
|
|
1937
|
+
.spyOn(eventUtils, 'safeDispatchCustomEvent')
|
|
1938
|
+
.mockImplementation((async (...args: unknown[]) => {
|
|
1939
|
+
if (args[0] === GraphEvents.ON_SUMMARIZE_START) {
|
|
1940
|
+
started = true;
|
|
1941
|
+
}
|
|
1942
|
+
}) as never);
|
|
1925
1943
|
|
|
1926
1944
|
const capturedSignals: Array<AbortSignal | undefined> = [];
|
|
1927
1945
|
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
|
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
isComputerCallOutputMessage,
|
|
18
18
|
serializeToolContentBounded,
|
|
19
19
|
} from '@/utils/toolContent';
|
|
20
|
+
import {
|
|
21
|
+
enforceStreamLimitsForWireChunk,
|
|
22
|
+
StreamLimitExceededError,
|
|
23
|
+
STREAM_LIMIT_EPOCH_KEY,
|
|
24
|
+
} from '@/llm/streamLimits';
|
|
20
25
|
import {
|
|
21
26
|
addTailCacheControl,
|
|
22
27
|
resolvePromptCacheTtl,
|
|
@@ -34,11 +39,6 @@ import {
|
|
|
34
39
|
Providers,
|
|
35
40
|
} from '@/common';
|
|
36
41
|
import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
|
|
37
|
-
import {
|
|
38
|
-
enforceStreamLimitsForWireChunk,
|
|
39
|
-
StreamLimitExceededError,
|
|
40
|
-
STREAM_LIMIT_EPOCH_KEY,
|
|
41
|
-
} from '@/llm/streamLimits';
|
|
42
42
|
import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
|
|
43
43
|
import { calculateMaxToolResultChars } from '@/utils/truncation';
|
|
44
44
|
import { createRemoveAllMessage } from '@/messages/reducer';
|
|
@@ -418,8 +418,9 @@ function computeSummaryTokenCount(
|
|
|
418
418
|
* in `ToolNode` and `StandardGraph`, handoff cues, reconstructed skill bodies.
|
|
419
419
|
*
|
|
420
420
|
* `steer` is exempt from the `source` check because a replayed steer *is*
|
|
421
|
-
*
|
|
422
|
-
* exactly those retained steers. Injected steers are still caught, by
|
|
421
|
+
* correlated with its payload entry; rejecting every marked `source` once
|
|
422
|
+
* dropped exactly those retained steers. Injected steers are still caught, by
|
|
423
|
+
* `injected`.
|
|
423
424
|
*
|
|
424
425
|
* Known limitation: a payload entry that omits `messageId` is never stamped, so
|
|
425
426
|
* the reducer's UUID is recorded and cannot resolve on the next run. There is no
|
|
@@ -443,7 +444,10 @@ function resolveSummaryCoverage(
|
|
|
443
444
|
if (isSyntheticContext(message)) {
|
|
444
445
|
continue;
|
|
445
446
|
}
|
|
446
|
-
const
|
|
447
|
+
const sourceMessageId = message.additional_kwargs.sourceMessageId;
|
|
448
|
+
const sourceId =
|
|
449
|
+
typeof sourceMessageId === 'string' ? sourceMessageId.trim() : '';
|
|
450
|
+
const id = sourceId !== '' ? sourceId : message.id?.trim();
|
|
447
451
|
if (id != null && id !== '') {
|
|
448
452
|
return { retainedFromMessageId: id };
|
|
449
453
|
}
|
|
@@ -1244,9 +1248,9 @@ export function createSummarizeNode({
|
|
|
1244
1248
|
? { [Constants.INVOKED_MODEL]: clientConfig.modelName }
|
|
1245
1249
|
: {}),
|
|
1246
1250
|
/** Entry-captured breaker epoch: the wire consumer epoch-gates
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1251
|
+
* old-run summary chunks exactly like model-attempt chunks —
|
|
1252
|
+
* without the stamp, a straggling summary from a failed run
|
|
1253
|
+
* reads as current and could abort the next run's controller. */
|
|
1250
1254
|
...(entryBreakerEpoch != null
|
|
1251
1255
|
? { [STREAM_LIMIT_EPOCH_KEY]: entryBreakerEpoch }
|
|
1252
1256
|
: {}),
|