@librechat/agents 3.3.2 → 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/langfuse.cjs +6 -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 +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/langfuse.mjs +6 -0
- 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 +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 +13 -9
- 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/langfuse.ts +15 -0
- 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/langfuse-callbacks.test.ts +203 -1
- 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
|
@@ -29,7 +29,7 @@ function createAIMessageWithToolCalls(
|
|
|
29
29
|
type CompletionEvent = {
|
|
30
30
|
result: {
|
|
31
31
|
id: string;
|
|
32
|
-
tool_call: { id: string; output: string };
|
|
32
|
+
tool_call: { id: string; output: string; args?: string };
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -124,6 +124,153 @@ describe('ToolNode per-call onResult completion emission', () => {
|
|
|
124
124
|
expect(result.messages.map((m) => m.content)).toEqual(['sunny', '42']);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('serializes bigint output before early and batch completion paths', async () => {
|
|
128
|
+
const completions: CompletionEvent[] = [];
|
|
129
|
+
const structuredOutput = [{ rowsRead: BigInt(42), status: 'complete' }];
|
|
130
|
+
|
|
131
|
+
jest
|
|
132
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
133
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
134
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
135
|
+
completions.push(data as CompletionEvent);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
142
|
+
batch.onResult?.({
|
|
143
|
+
toolCallId: 'call_query',
|
|
144
|
+
status: 'success',
|
|
145
|
+
content: structuredOutput,
|
|
146
|
+
});
|
|
147
|
+
await flushAsync();
|
|
148
|
+
batch.resolve([
|
|
149
|
+
{
|
|
150
|
+
toolCallId: 'call_query',
|
|
151
|
+
status: 'success',
|
|
152
|
+
content: structuredOutput,
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const toolNode = new ToolNode({
|
|
158
|
+
tools: [createDummyTool('query')],
|
|
159
|
+
eventDrivenMode: true,
|
|
160
|
+
toolCallStepIds: new Map([['call_query', 'step_query']]),
|
|
161
|
+
});
|
|
162
|
+
const result = (await toolNode.invoke({
|
|
163
|
+
messages: [
|
|
164
|
+
createAIMessageWithToolCalls([
|
|
165
|
+
{ id: 'call_query', name: 'query', args: {} },
|
|
166
|
+
]),
|
|
167
|
+
],
|
|
168
|
+
})) as { messages: ToolMessage[] };
|
|
169
|
+
|
|
170
|
+
const serialized = '[{"rowsRead":"42","status":"complete"}]';
|
|
171
|
+
expect(completions).toHaveLength(1);
|
|
172
|
+
expect(completions[0].result.tool_call.output).toBe(serialized);
|
|
173
|
+
expect(result.messages[0].content).toBe(serialized);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('omits native computer screenshots from completion events', async () => {
|
|
177
|
+
const completions: CompletionEvent[] = [];
|
|
178
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
179
|
+
const computerOutput = new ToolMessage({
|
|
180
|
+
content: screenshot,
|
|
181
|
+
tool_call_id: 'call_computer',
|
|
182
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
183
|
+
});
|
|
184
|
+
const computer = createDummyTool('computer_use');
|
|
185
|
+
(
|
|
186
|
+
computer as unknown as {
|
|
187
|
+
invoke: () => Promise<ToolMessage>;
|
|
188
|
+
}
|
|
189
|
+
).invoke = async () => computerOutput;
|
|
190
|
+
jest
|
|
191
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
192
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
193
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
194
|
+
completions.push(data as CompletionEvent);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const toolNode = new ToolNode({
|
|
198
|
+
tools: [computer],
|
|
199
|
+
eventDrivenMode: true,
|
|
200
|
+
directToolNames: new Set(['computer_use']),
|
|
201
|
+
toolCallStepIds: new Map([['call_computer', 'step_computer']]),
|
|
202
|
+
maxToolResultChars: 80,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = (await toolNode.invoke({
|
|
206
|
+
messages: [
|
|
207
|
+
createAIMessageWithToolCalls([
|
|
208
|
+
{ id: 'call_computer', name: 'computer_use', args: {} },
|
|
209
|
+
]),
|
|
210
|
+
],
|
|
211
|
+
})) as { messages: ToolMessage[] };
|
|
212
|
+
|
|
213
|
+
expect(result.messages[0]).toBe(computerOutput);
|
|
214
|
+
expect(result.messages[0].content).toBe(screenshot);
|
|
215
|
+
expect(completions).toHaveLength(1);
|
|
216
|
+
expect(completions[0].result.tool_call.output).toContain(
|
|
217
|
+
'Computer screenshot omitted'
|
|
218
|
+
);
|
|
219
|
+
expect(completions[0].result.tool_call.output.length).toBeLessThanOrEqual(
|
|
220
|
+
80
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('bounds cyclic tool args without losing the completion event', async () => {
|
|
225
|
+
const completions: CompletionEvent[] = [];
|
|
226
|
+
const cyclicArgs: Record<string, unknown> = { city: 'NYC' };
|
|
227
|
+
cyclicArgs.self = cyclicArgs;
|
|
228
|
+
|
|
229
|
+
jest
|
|
230
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
231
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
232
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
233
|
+
completions.push(data as CompletionEvent);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
240
|
+
batch.onResult?.({
|
|
241
|
+
toolCallId: 'call_weather',
|
|
242
|
+
status: 'success',
|
|
243
|
+
content: 'sunny',
|
|
244
|
+
});
|
|
245
|
+
await flushAsync();
|
|
246
|
+
batch.resolve([
|
|
247
|
+
{
|
|
248
|
+
toolCallId: 'call_weather',
|
|
249
|
+
status: 'success',
|
|
250
|
+
content: 'sunny',
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const toolNode = new ToolNode({
|
|
256
|
+
tools: [createDummyTool('weather')],
|
|
257
|
+
eventDrivenMode: true,
|
|
258
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await toolNode.invoke({
|
|
262
|
+
messages: [
|
|
263
|
+
createAIMessageWithToolCalls([
|
|
264
|
+
{ id: 'call_weather', name: 'weather', args: cyclicArgs },
|
|
265
|
+
]),
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(completions).toHaveLength(1);
|
|
270
|
+
expect(completions[0].result.tool_call.args).toContain('[Circular]');
|
|
271
|
+
expect(completions[0].result.tool_call.output).toBe('sunny');
|
|
272
|
+
});
|
|
273
|
+
|
|
127
274
|
it('ignores duplicate and unknown onResult reports', async () => {
|
|
128
275
|
const completions: CompletionEvent[] = [];
|
|
129
276
|
|
|
@@ -113,6 +113,54 @@ describe('ToolNode tool output references', () => {
|
|
|
113
113
|
|
|
114
114
|
expect(capturedArgs).toEqual(['raw {{tool0turn0}}']);
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it('bounds structured ToolMessage output before its first model call', async () => {
|
|
118
|
+
const structuredTool = tool(
|
|
119
|
+
async () =>
|
|
120
|
+
new ToolMessage({
|
|
121
|
+
status: 'success',
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'json',
|
|
125
|
+
rows: Array.from({ length: 20 }, (_, index) => ({
|
|
126
|
+
id: index,
|
|
127
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
128
|
+
})),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
artifact: { source: 'clickhouse' },
|
|
132
|
+
metadata: { requestId: 'request-1' },
|
|
133
|
+
name: 'run_select_query',
|
|
134
|
+
tool_call_id: 'c1',
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
name: 'run_select_query',
|
|
138
|
+
description: 'returns structured rows',
|
|
139
|
+
schema: z.object({ command: z.string() }),
|
|
140
|
+
}
|
|
141
|
+
) as unknown as StructuredToolInterface;
|
|
142
|
+
const node = new ToolNode({
|
|
143
|
+
tools: [structuredTool],
|
|
144
|
+
maxToolResultChars: 200,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const [msg] = await invokeBatch(node, [
|
|
148
|
+
{
|
|
149
|
+
id: 'c1',
|
|
150
|
+
name: 'run_select_query',
|
|
151
|
+
command: 'SELECT * FROM events',
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(typeof msg.content).toBe('string');
|
|
156
|
+
expect(msg.content).toContain('truncated');
|
|
157
|
+
expect((msg.content as string).length).toBeLessThanOrEqual(200);
|
|
158
|
+
expect(msg.tool_call_id).toBe('c1');
|
|
159
|
+
expect(msg.name).toBe('run_select_query');
|
|
160
|
+
expect(msg.status).toBe('success');
|
|
161
|
+
expect(msg.artifact).toEqual({ source: 'clickhouse' });
|
|
162
|
+
expect(msg.metadata).toEqual({ requestId: 'request-1' });
|
|
163
|
+
});
|
|
116
164
|
});
|
|
117
165
|
|
|
118
166
|
describe('enabled', () => {
|
|
@@ -364,6 +412,67 @@ describe('ToolNode tool output references', () => {
|
|
|
364
412
|
).toBe(raw);
|
|
365
413
|
});
|
|
366
414
|
|
|
415
|
+
it('preserves structured output exactly up to the registry limit', async () => {
|
|
416
|
+
const structured = {
|
|
417
|
+
rows: Array.from({ length: 10 }, (_, index) => ({
|
|
418
|
+
id: index,
|
|
419
|
+
value: `row-${index}-${'x'.repeat(100)}`,
|
|
420
|
+
})),
|
|
421
|
+
};
|
|
422
|
+
const exact = JSON.stringify(structured);
|
|
423
|
+
const capturedArgs: string[] = [];
|
|
424
|
+
let callCount = 0;
|
|
425
|
+
const structuredTool = createEchoTool({
|
|
426
|
+
capturedArgs: [],
|
|
427
|
+
outputs: ['unused'],
|
|
428
|
+
name: 'structured',
|
|
429
|
+
});
|
|
430
|
+
(
|
|
431
|
+
structuredTool as unknown as {
|
|
432
|
+
invoke: (input: {
|
|
433
|
+
args: { command: string };
|
|
434
|
+
}) => Promise<typeof structured | string>;
|
|
435
|
+
}
|
|
436
|
+
).invoke = async (input) => {
|
|
437
|
+
capturedArgs.push(input.args.command);
|
|
438
|
+
return callCount++ === 0 ? structured : 'done';
|
|
439
|
+
};
|
|
440
|
+
const node = new ToolNode({
|
|
441
|
+
tools: [structuredTool],
|
|
442
|
+
maxToolResultChars: 100,
|
|
443
|
+
toolOutputReferences: {
|
|
444
|
+
enabled: true,
|
|
445
|
+
maxOutputSize: exact.length,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const [first] = await invokeBatch(
|
|
450
|
+
node,
|
|
451
|
+
[{ id: 'c1', name: 'structured', command: 'first' }],
|
|
452
|
+
'structured-raw-preservation'
|
|
453
|
+
);
|
|
454
|
+
await invokeBatch(
|
|
455
|
+
node,
|
|
456
|
+
[
|
|
457
|
+
{
|
|
458
|
+
id: 'c2',
|
|
459
|
+
name: 'structured',
|
|
460
|
+
command: '{{tool0turn0}}',
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
'structured-raw-preservation'
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect((first.content as string).length).toBeLessThanOrEqual(100);
|
|
467
|
+
expect(first.content).toContain('truncated');
|
|
468
|
+
expect(capturedArgs[1]).toBe(exact);
|
|
469
|
+
expect(
|
|
470
|
+
node
|
|
471
|
+
._unsafeGetToolOutputRegistry()!
|
|
472
|
+
.get('structured-raw-preservation', 'tool0turn0')
|
|
473
|
+
).toBe(exact);
|
|
474
|
+
});
|
|
475
|
+
|
|
367
476
|
it('uses each batch\'s own turn when ToolNode is invoked concurrently within a run', async () => {
|
|
368
477
|
const gates: Record<string, () => void> = {};
|
|
369
478
|
const slowTool = tool(
|
|
@@ -823,7 +932,7 @@ describe('ToolNode tool output references', () => {
|
|
|
823
932
|
expect(JSON.parse(stepCompletedArgs[1]).command).toBe('echo STORED');
|
|
824
933
|
});
|
|
825
934
|
|
|
826
|
-
it('records unresolved refs
|
|
935
|
+
it('records unresolved refs on multipart ToolMessage content within the cap', async () => {
|
|
827
936
|
const complexTool = tool(
|
|
828
937
|
async () =>
|
|
829
938
|
new ToolMessage({
|
|
@@ -855,8 +964,8 @@ describe('ToolNode tool output references', () => {
|
|
|
855
964
|
|
|
856
965
|
expect(Array.isArray(msg.content)).toBe(true);
|
|
857
966
|
const blocks = msg.content as Array<{ type: string; text?: string }>;
|
|
858
|
-
//
|
|
859
|
-
//
|
|
967
|
+
// In-budget multi-part content stays intact; the lazy transform handles
|
|
968
|
+
// the unresolved-refs warning at request time.
|
|
860
969
|
expect(blocks).toHaveLength(2);
|
|
861
970
|
expect(blocks[0].type).toBe('text');
|
|
862
971
|
expect(blocks[0].text).toBe('data');
|
|
@@ -90,6 +90,38 @@ describe('annotateMessagesForLLM', () => {
|
|
|
90
90
|
expect(out[0]).not.toBe(tm);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it('does not annotate native computer screenshot strings', () => {
|
|
94
|
+
const registry = new ToolOutputReferenceRegistry();
|
|
95
|
+
registry.set('r1', 'tool0turn0', 'stored-raw');
|
|
96
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
97
|
+
const tm = makeToolMessage({
|
|
98
|
+
content: screenshot,
|
|
99
|
+
additional_kwargs: {
|
|
100
|
+
type: 'computer_call_output',
|
|
101
|
+
_refKey: 'tool0turn0',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const out = annotateMessagesForLLM([tm], registry, 'r1');
|
|
106
|
+
|
|
107
|
+
expect(out[0].content).toBe(screenshot);
|
|
108
|
+
expect((out[0] as ToolMessage).additional_kwargs.type).toBe(
|
|
109
|
+
'computer_call_output'
|
|
110
|
+
);
|
|
111
|
+
expect((out[0] as ToolMessage).additional_kwargs._refKey).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves native computer screenshots reference-equal without ref metadata', () => {
|
|
115
|
+
const registry = new ToolOutputReferenceRegistry();
|
|
116
|
+
const tm = makeToolMessage({
|
|
117
|
+
content: 'data:image/png;base64,AAAA',
|
|
118
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
119
|
+
});
|
|
120
|
+
const messages = [tm];
|
|
121
|
+
|
|
122
|
+
expect(annotateMessagesForLLM(messages, registry, 'r1')).toBe(messages);
|
|
123
|
+
});
|
|
124
|
+
|
|
93
125
|
it('leaves content untouched but strips framework metadata when _refKey is stale', () => {
|
|
94
126
|
/**
|
|
95
127
|
* Stale `_refKey` (not in registry) doesn't trigger annotation,
|
|
@@ -20,7 +20,7 @@ import { ToolNode } from '../ToolNode';
|
|
|
20
20
|
*/
|
|
21
21
|
function createDirectTool(
|
|
22
22
|
name: string,
|
|
23
|
-
impl: (args: Record<string, unknown>) =>
|
|
23
|
+
impl: (args: Record<string, unknown>) => unknown | Promise<unknown>
|
|
24
24
|
): StructuredToolInterface {
|
|
25
25
|
return tool(async (args: Record<string, unknown>) => impl(args), {
|
|
26
26
|
name,
|
|
@@ -147,6 +147,217 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
147
147
|
expect(String(message.content)).toBe('ran:ls');
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
it('serializes bigint values in direct structured tool output', async () => {
|
|
151
|
+
const query = createDirectTool('query', () => 'unused');
|
|
152
|
+
(
|
|
153
|
+
query as unknown as {
|
|
154
|
+
invoke: () => Promise<Array<Record<string, unknown>>>;
|
|
155
|
+
}
|
|
156
|
+
).invoke = async () => [{ rowsRead: BigInt(42), status: 'complete' }];
|
|
157
|
+
const node = new ToolNode({
|
|
158
|
+
tools: [query],
|
|
159
|
+
eventDrivenMode: true,
|
|
160
|
+
directToolNames: new Set(['query']),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await node.invoke({
|
|
164
|
+
messages: [aiCall('call_bigint', 'query', {})],
|
|
165
|
+
});
|
|
166
|
+
const [message] = toolMessages(result);
|
|
167
|
+
|
|
168
|
+
expect(message.status).toBe('success');
|
|
169
|
+
expect(message.content).toBe('[{"rowsRead":"42","status":"complete"}]');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('bounds a large structured result without invoking toJSON', async () => {
|
|
173
|
+
let toJSONCalls = 0;
|
|
174
|
+
const structuredOutput = {
|
|
175
|
+
rows: [{ value: 'x'.repeat(20_000) }],
|
|
176
|
+
toJSON() {
|
|
177
|
+
toJSONCalls++;
|
|
178
|
+
return 'y'.repeat(100_000);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
const query = createDirectTool('large_query', () => 'unused');
|
|
182
|
+
(
|
|
183
|
+
query as unknown as {
|
|
184
|
+
invoke: () => Promise<typeof structuredOutput>;
|
|
185
|
+
}
|
|
186
|
+
).invoke = async () => structuredOutput;
|
|
187
|
+
const node = new ToolNode({
|
|
188
|
+
tools: [query],
|
|
189
|
+
eventDrivenMode: true,
|
|
190
|
+
directToolNames: new Set(['large_query']),
|
|
191
|
+
maxToolResultChars: 200,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await node.invoke({
|
|
195
|
+
messages: [aiCall('call_large', 'large_query', {})],
|
|
196
|
+
});
|
|
197
|
+
const [message] = toolMessages(result);
|
|
198
|
+
|
|
199
|
+
expect(toJSONCalls).toBe(0);
|
|
200
|
+
expect(typeof message.content).toBe('string');
|
|
201
|
+
expect((message.content as string).length).toBeLessThanOrEqual(200);
|
|
202
|
+
expect(message.content).toContain('truncated');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('keeps native computer screenshots intact despite the generic result cap', async () => {
|
|
206
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
207
|
+
const contents: ToolMessage['content'][] = [
|
|
208
|
+
screenshot,
|
|
209
|
+
[{ type: 'computer_screenshot', image_url: screenshot }],
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < contents.length; i++) {
|
|
213
|
+
const name = `computer_output_${i}`;
|
|
214
|
+
const computerOutput = new ToolMessage({
|
|
215
|
+
content: contents[i],
|
|
216
|
+
tool_call_id: `computer-call-${i}`,
|
|
217
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
218
|
+
});
|
|
219
|
+
const computer = createDirectTool(name, () => 'unused');
|
|
220
|
+
(
|
|
221
|
+
computer as unknown as {
|
|
222
|
+
invoke: () => Promise<ToolMessage>;
|
|
223
|
+
}
|
|
224
|
+
).invoke = async () => computerOutput;
|
|
225
|
+
const node = new ToolNode({
|
|
226
|
+
tools: [computer],
|
|
227
|
+
eventDrivenMode: true,
|
|
228
|
+
directToolNames: new Set([name]),
|
|
229
|
+
maxToolResultChars: 80,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await node.invoke({
|
|
233
|
+
messages: [aiCall(`computer-call-${i}`, name, {})],
|
|
234
|
+
});
|
|
235
|
+
const [message] = toolMessages(result);
|
|
236
|
+
|
|
237
|
+
expect(message).toBe(computerOutput);
|
|
238
|
+
expect(message.content).toBe(contents[i]);
|
|
239
|
+
expect(message.additional_kwargs.type).toBe('computer_call_output');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('preserves a valid PostToolUse computer screenshot replacement', async () => {
|
|
244
|
+
const original = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
245
|
+
const replacement = `data:image/png;base64,${'B'.repeat(2_000)}`;
|
|
246
|
+
const computerOutput = new ToolMessage({
|
|
247
|
+
content: original,
|
|
248
|
+
tool_call_id: 'computer-hook-valid',
|
|
249
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
250
|
+
});
|
|
251
|
+
const computer = createDirectTool('computer_hook_valid', () => 'unused');
|
|
252
|
+
(
|
|
253
|
+
computer as unknown as {
|
|
254
|
+
invoke: () => Promise<ToolMessage>;
|
|
255
|
+
}
|
|
256
|
+
).invoke = async () => computerOutput;
|
|
257
|
+
const registry = new HookRegistry();
|
|
258
|
+
registry.register('PostToolUse', {
|
|
259
|
+
hooks: [
|
|
260
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
261
|
+
updatedOutput: replacement,
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
const node = new ToolNode({
|
|
266
|
+
tools: [computer],
|
|
267
|
+
eventDrivenMode: true,
|
|
268
|
+
hookRegistry: registry,
|
|
269
|
+
directToolNames: new Set(['computer_hook_valid']),
|
|
270
|
+
maxToolResultChars: 80,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = await node.invoke({
|
|
274
|
+
messages: [aiCall('computer-hook-valid', 'computer_hook_valid', {})],
|
|
275
|
+
});
|
|
276
|
+
const [message] = toolMessages(result);
|
|
277
|
+
|
|
278
|
+
expect(message.content).toBe(replacement);
|
|
279
|
+
expect(message.additional_kwargs.type).toBe('computer_call_output');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('rejects an invalid PostToolUse computer output replacement', async () => {
|
|
283
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
284
|
+
const computerOutput = new ToolMessage({
|
|
285
|
+
content: screenshot,
|
|
286
|
+
tool_call_id: 'computer-hook-invalid',
|
|
287
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
288
|
+
});
|
|
289
|
+
const computer = createDirectTool('computer_hook_invalid', () => 'unused');
|
|
290
|
+
(
|
|
291
|
+
computer as unknown as {
|
|
292
|
+
invoke: () => Promise<ToolMessage>;
|
|
293
|
+
}
|
|
294
|
+
).invoke = async () => computerOutput;
|
|
295
|
+
const registry = new HookRegistry();
|
|
296
|
+
registry.register('PostToolUse', {
|
|
297
|
+
hooks: [
|
|
298
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
299
|
+
updatedOutput: 'redacted screenshot',
|
|
300
|
+
}),
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
const node = new ToolNode({
|
|
304
|
+
tools: [computer],
|
|
305
|
+
eventDrivenMode: true,
|
|
306
|
+
hookRegistry: registry,
|
|
307
|
+
directToolNames: new Set(['computer_hook_invalid']),
|
|
308
|
+
maxToolResultChars: 80,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await expect(
|
|
312
|
+
node.invoke({
|
|
313
|
+
messages: [
|
|
314
|
+
aiCall('computer-hook-invalid', 'computer_hook_invalid', {}),
|
|
315
|
+
],
|
|
316
|
+
})
|
|
317
|
+
).rejects.toThrow(
|
|
318
|
+
'PostToolUse updatedOutput for a computer call must be a valid screenshot'
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('caps returned and thrown direct-tool errors before storing them', async () => {
|
|
323
|
+
const returnedError = createDirectTool(
|
|
324
|
+
'returned_error',
|
|
325
|
+
() =>
|
|
326
|
+
new ToolMessage({
|
|
327
|
+
status: 'error',
|
|
328
|
+
content: `returned:${'x'.repeat(2_000)}`,
|
|
329
|
+
tool_call_id: 'returned',
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
const thrownError = createDirectTool('thrown_error', () => {
|
|
333
|
+
throw new Error(`thrown:${'y'.repeat(2_000)}`);
|
|
334
|
+
});
|
|
335
|
+
const node = new ToolNode({
|
|
336
|
+
tools: [returnedError, thrownError],
|
|
337
|
+
eventDrivenMode: true,
|
|
338
|
+
directToolNames: new Set(['returned_error', 'thrown_error']),
|
|
339
|
+
maxToolResultChars: 200,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const returned = toolMessages(
|
|
343
|
+
await node.invoke({
|
|
344
|
+
messages: [aiCall('returned', 'returned_error', {})],
|
|
345
|
+
})
|
|
346
|
+
)[0];
|
|
347
|
+
const thrown = toolMessages(
|
|
348
|
+
await node.invoke({
|
|
349
|
+
messages: [aiCall('thrown', 'thrown_error', {})],
|
|
350
|
+
})
|
|
351
|
+
)[0];
|
|
352
|
+
|
|
353
|
+
expect(returned.status).toBe('error');
|
|
354
|
+
expect(String(returned.content).length).toBeLessThanOrEqual(200);
|
|
355
|
+
expect(returned.content).toContain('[truncated:');
|
|
356
|
+
expect(thrown.status).toBe('error');
|
|
357
|
+
expect(String(thrown.content).length).toBeLessThanOrEqual(200);
|
|
358
|
+
expect(thrown.content).toContain('[truncated:');
|
|
359
|
+
});
|
|
360
|
+
|
|
150
361
|
it('executingAgentId defaults to agentId for direct callers, and an explicit value wins', async () => {
|
|
151
362
|
const echo = createDirectTool('echo', (args) => `ran:${args.command}`);
|
|
152
363
|
const captured: Array<string | undefined> = [];
|
|
@@ -247,6 +458,37 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
247
458
|
expect(message.status).toBe('success');
|
|
248
459
|
});
|
|
249
460
|
|
|
461
|
+
it('caps PostToolUse updatedOutput after the hook rewrite', async () => {
|
|
462
|
+
const echo = createDirectTool('echo', () => 'ORIGINAL');
|
|
463
|
+
const replacement = 'R'.repeat(500);
|
|
464
|
+
|
|
465
|
+
const registry = new HookRegistry();
|
|
466
|
+
registry.register('PostToolUse', {
|
|
467
|
+
hooks: [
|
|
468
|
+
async (): Promise<PostToolUseHookOutput> => ({
|
|
469
|
+
updatedOutput: replacement,
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const node = new ToolNode({
|
|
475
|
+
tools: [echo],
|
|
476
|
+
eventDrivenMode: true,
|
|
477
|
+
hookRegistry: registry,
|
|
478
|
+
directToolNames: new Set(['echo']),
|
|
479
|
+
maxToolResultChars: 50,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const result = await node.invoke({
|
|
483
|
+
messages: [aiCall('call_5_capped', 'echo', { command: 'x' })],
|
|
484
|
+
});
|
|
485
|
+
const [message] = toolMessages(result);
|
|
486
|
+
|
|
487
|
+
expect(typeof message.content).toBe('string');
|
|
488
|
+
expect((message.content as string).length).toBeLessThanOrEqual(50);
|
|
489
|
+
expect(message.content).not.toBe(replacement);
|
|
490
|
+
});
|
|
491
|
+
|
|
250
492
|
it('PostToolUseFailure observes errors thrown by the tool', async () => {
|
|
251
493
|
const failing = createDirectTool('boom', () => {
|
|
252
494
|
throw new Error('kaboom');
|
|
@@ -376,11 +618,16 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
376
618
|
|
|
377
619
|
// Patch the tool's `func` to record the turn the body sees via the
|
|
378
620
|
// standard LangChain config.toolCall.turn channel.
|
|
379
|
-
const originalFunc = (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
621
|
+
const originalFunc = (
|
|
622
|
+
echo as unknown as {
|
|
623
|
+
func: (input: unknown, config: unknown) => Promise<string>;
|
|
624
|
+
}
|
|
625
|
+
).func;
|
|
626
|
+
(
|
|
627
|
+
echo as unknown as {
|
|
628
|
+
func: (input: unknown, config: unknown) => Promise<string>;
|
|
629
|
+
}
|
|
630
|
+
).func = async (input, config): Promise<string> => {
|
|
384
631
|
const t = (config as { toolCall?: { turn?: number } } | undefined)
|
|
385
632
|
?.toolCall?.turn;
|
|
386
633
|
if (typeof t === 'number') bodyTurns.push(t);
|
|
@@ -424,7 +671,8 @@ describe('Direct-path lifecycle hooks (in-process tools)', () => {
|
|
|
424
671
|
hooks: [
|
|
425
672
|
async (): Promise<PreToolUseHookOutput> => ({
|
|
426
673
|
decision: 'allow',
|
|
427
|
-
additionalContext:
|
|
674
|
+
additionalContext:
|
|
675
|
+
'POLICY-NOTE: writes here require approval next time',
|
|
428
676
|
}),
|
|
429
677
|
],
|
|
430
678
|
});
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
HARD_MAX_TOOL_RESULT_CHARS,
|
|
30
30
|
HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
|
|
31
31
|
} from '@/utils/truncation';
|
|
32
|
+
import { isComputerCallOutputMessage } from '@/utils/toolContent';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.
|
|
@@ -656,6 +657,11 @@ export function annotateMessagesForLLM(
|
|
|
656
657
|
const hasRefScope = '_refScope' in meta;
|
|
657
658
|
const hasUnresolvedField = '_unresolvedRefs' in meta;
|
|
658
659
|
if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;
|
|
660
|
+
if (isComputerCallOutputMessage(m)) {
|
|
661
|
+
out ??= messages.slice();
|
|
662
|
+
out[i] = cloneToolMessageWithContent(m as ToolMessage, m.content);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
659
665
|
|
|
660
666
|
const refKey = readRefKey(meta);
|
|
661
667
|
const unresolved = readUnresolvedRefs(meta);
|
package/src/types/stream.ts
CHANGED
|
@@ -336,7 +336,7 @@ export type ToolCallPart = {
|
|
|
336
336
|
/** If provided, an identifier associated with the tool call */
|
|
337
337
|
id?: string;
|
|
338
338
|
/** If provided, the output of the tool call */
|
|
339
|
-
output?:
|
|
339
|
+
output?: ToolResultContent['content'];
|
|
340
340
|
/** Auth URL */
|
|
341
341
|
auth?: string;
|
|
342
342
|
/** Expiration time */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isAnthropicLike } from './llm';
|
|
2
|
+
import { Providers } from '@/common';
|
|
3
|
+
|
|
4
|
+
describe('isAnthropicLike', () => {
|
|
5
|
+
it('treats the default Bedrock model as Claude', () => {
|
|
6
|
+
expect(isAnthropicLike(Providers.BEDROCK)).toBe(true);
|
|
7
|
+
expect(
|
|
8
|
+
isAnthropicLike(Providers.BEDROCK, {
|
|
9
|
+
model: 'anthropic.claude-sonnet-4-5',
|
|
10
|
+
})
|
|
11
|
+
).toBe(true);
|
|
12
|
+
expect(
|
|
13
|
+
isAnthropicLike(Providers.BEDROCK, {
|
|
14
|
+
model: 'amazon.nova-pro-v1:0',
|
|
15
|
+
})
|
|
16
|
+
).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|