@librechat/agents 3.2.54 → 3.2.56
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/llm/invoke.cjs +3 -0
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/truncation.cjs +110 -0
- package/dist/cjs/llm/truncation.cjs.map +1 -0
- package/dist/cjs/stream.cjs +7 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +3 -0
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/truncation.mjs +110 -0
- package/dist/esm/llm/truncation.mjs.map +1 -0
- package/dist/esm/stream.mjs +7 -2
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/types/llm/truncation.d.ts +45 -0
- package/dist/types/types/tools.d.ts +10 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +100 -0
- package/src/llm/invoke.ts +3 -0
- package/src/llm/truncation.test.ts +242 -0
- package/src/llm/truncation.ts +178 -0
- package/src/specs/bedrock-truncation.live.test.ts +191 -0
- package/src/stream.ts +19 -1
- package/src/types/tools.ts +10 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Bedrock tool-call truncation verification.
|
|
3
|
+
*
|
|
4
|
+
* Reproduces the production failure where a large tool argument (`content`)
|
|
5
|
+
* gets cut off by the max output token limit, leaving the handler with a
|
|
6
|
+
* well-formed-but-incomplete tool call and no truncation signal.
|
|
7
|
+
*
|
|
8
|
+
* Run with:
|
|
9
|
+
* RUN_BEDROCK_TRUNCATION_LIVE=1 npm test -- bedrock-truncation.live.test.ts --runInBand
|
|
10
|
+
*/
|
|
11
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
12
|
+
dotenvConfig();
|
|
13
|
+
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
16
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
17
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
18
|
+
import type * as t from '@/types';
|
|
19
|
+
import { GraphEvents, Providers } from '@/common';
|
|
20
|
+
import { ChatModelStreamHandler } from '@/stream';
|
|
21
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
22
|
+
import { Run } from '@/run';
|
|
23
|
+
|
|
24
|
+
const shouldRunLive =
|
|
25
|
+
process.env.RUN_BEDROCK_TRUNCATION_LIVE === '1' &&
|
|
26
|
+
(process.env.BEDROCK_AWS_ACCESS_KEY_ID ?? '') !== '' &&
|
|
27
|
+
(process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ?? '') !== '';
|
|
28
|
+
|
|
29
|
+
const describeIfLive = shouldRunLive ? describe : describe.skip;
|
|
30
|
+
|
|
31
|
+
const REGION =
|
|
32
|
+
[
|
|
33
|
+
process.env.BEDROCK_AWS_REGION,
|
|
34
|
+
process.env.BEDROCK_AWS_DEFAULT_REGION,
|
|
35
|
+
process.env.AWS_DEFAULT_REGION,
|
|
36
|
+
].find(
|
|
37
|
+
(value): value is string => typeof value === 'string' && value !== ''
|
|
38
|
+
) ?? 'us-east-1';
|
|
39
|
+
const MODEL = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0';
|
|
40
|
+
const MAX_TOKENS = Number(process.env.REPRO_MAX_TOKENS ?? 350);
|
|
41
|
+
|
|
42
|
+
const schema = z.object({
|
|
43
|
+
path: z.string().describe('Path to write.'),
|
|
44
|
+
content: z.string().describe('Complete file contents.'),
|
|
45
|
+
overwrite: z.boolean().optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describeIfLive('Bedrock tool-call truncation (live)', () => {
|
|
49
|
+
jest.setTimeout(120_000);
|
|
50
|
+
|
|
51
|
+
it('fails fast with a truncation error instead of looping', async () => {
|
|
52
|
+
const received: Array<{ keys: string[]; contentLen: number | null }> = [];
|
|
53
|
+
const stepToolCalls: string[][] = [];
|
|
54
|
+
let invocations = 0;
|
|
55
|
+
|
|
56
|
+
const createFile = new DynamicStructuredTool({
|
|
57
|
+
name: 'create_file',
|
|
58
|
+
description: 'Create a new file. Requires path and content.',
|
|
59
|
+
schema,
|
|
60
|
+
func: async (input: z.infer<typeof schema>) => {
|
|
61
|
+
invocations += 1;
|
|
62
|
+
const hasContent =
|
|
63
|
+
typeof input.content === 'string' && input.content.length > 0;
|
|
64
|
+
received.push({
|
|
65
|
+
keys: Object.keys(input as Record<string, unknown>),
|
|
66
|
+
contentLen: hasContent ? input.content.length : null,
|
|
67
|
+
});
|
|
68
|
+
if (!hasContent) {
|
|
69
|
+
return 'Error: content is required\n Please fix your mistakes.';
|
|
70
|
+
}
|
|
71
|
+
return `Wrote ${input.content.length} bytes to ${input.path}`;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const llmConfig = {
|
|
76
|
+
provider: Providers.BEDROCK,
|
|
77
|
+
model: MODEL,
|
|
78
|
+
region: REGION,
|
|
79
|
+
credentials: {
|
|
80
|
+
accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
|
|
81
|
+
secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
|
|
82
|
+
},
|
|
83
|
+
maxTokens: MAX_TOKENS,
|
|
84
|
+
streaming: true,
|
|
85
|
+
streamUsage: true,
|
|
86
|
+
} as t.LLMConfig;
|
|
87
|
+
|
|
88
|
+
const run = await Run.create<t.IState>({
|
|
89
|
+
runId: 'bedrock-truncation-live',
|
|
90
|
+
graphConfig: {
|
|
91
|
+
type: 'standard',
|
|
92
|
+
llmConfig,
|
|
93
|
+
tools: [createFile],
|
|
94
|
+
instructions: 'You are a ClickHouse assistant. Use tools when asked.',
|
|
95
|
+
maxContextTokens: 89000,
|
|
96
|
+
},
|
|
97
|
+
returnContent: true,
|
|
98
|
+
skipCleanup: true,
|
|
99
|
+
customHandlers: {
|
|
100
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(
|
|
101
|
+
async (toolEndData: t.ToolEndData) => {
|
|
102
|
+
const out = toolEndData.output as
|
|
103
|
+
| { content?: unknown; name?: string; status?: string }
|
|
104
|
+
| undefined;
|
|
105
|
+
const content =
|
|
106
|
+
typeof out?.content === 'string'
|
|
107
|
+
? out.content.slice(0, 120)
|
|
108
|
+
: JSON.stringify(out?.content).slice(0, 120);
|
|
109
|
+
// eslint-disable-next-line no-console
|
|
110
|
+
console.log(
|
|
111
|
+
`TOOL_END name=${out?.name} status=${out?.status} content=${content}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
116
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
117
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
118
|
+
handle: (_event: string, data: t.StreamEventData): void => {
|
|
119
|
+
const step = data as unknown as {
|
|
120
|
+
stepDetails?: {
|
|
121
|
+
type?: string;
|
|
122
|
+
tool_calls?: Array<{ name?: string; args?: unknown }>;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const tcs = step.stepDetails?.tool_calls;
|
|
126
|
+
if ((tcs?.length ?? 0) > 0 && tcs != null) {
|
|
127
|
+
for (const tc of tcs) {
|
|
128
|
+
if (tc.name === 'create_file') {
|
|
129
|
+
stepToolCalls.push(
|
|
130
|
+
typeof tc.args === 'string'
|
|
131
|
+
? ['<raw-string>']
|
|
132
|
+
: Object.keys((tc.args ?? {}) as Record<string, unknown>)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line no-console
|
|
137
|
+
console.log(
|
|
138
|
+
'ON_RUN_STEP tool_calls=',
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
tcs.map((tc) => ({
|
|
141
|
+
name: tc.name,
|
|
142
|
+
args:
|
|
143
|
+
typeof tc.args === 'string'
|
|
144
|
+
? tc.args.slice(0, 80)
|
|
145
|
+
: Object.keys(
|
|
146
|
+
(tc.args ?? {}) as Record<string, unknown>
|
|
147
|
+
),
|
|
148
|
+
}))
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const prompt =
|
|
158
|
+
'Call create_file once: path="skills/clickhouse-demo/SKILL.md", content=a complete ' +
|
|
159
|
+
'3000-word ClickHouse guide (schema design, MergeTree, partitioning, 10 example queries). ' +
|
|
160
|
+
'Put the entire guide in the content argument.';
|
|
161
|
+
|
|
162
|
+
let loopError: string | null = null;
|
|
163
|
+
try {
|
|
164
|
+
await run.processStream(
|
|
165
|
+
{ messages: [new HumanMessage(prompt)] },
|
|
166
|
+
{
|
|
167
|
+
configurable: { provider: Providers.BEDROCK, thread_id: 'trunc' },
|
|
168
|
+
version: 'v2',
|
|
169
|
+
recursionLimit: 8,
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
loopError = err instanceof Error ? err.message : String(err);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log('LIVE RESULT', {
|
|
178
|
+
invocations,
|
|
179
|
+
received,
|
|
180
|
+
stepToolCalls,
|
|
181
|
+
loopError,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// The run fails fast with the actionable truncation error...
|
|
185
|
+
expect(loopError).toMatch(/truncated at the maximum output token limit/i);
|
|
186
|
+
// ...instead of looping until the recursion limit.
|
|
187
|
+
expect(loopError).not.toMatch(/Recursion limit/i);
|
|
188
|
+
// No truncated create_file call ever carried a usable `content` arg.
|
|
189
|
+
expect(stepToolCalls.every((keys) => !keys.includes('content'))).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
package/src/stream.ts
CHANGED
|
@@ -127,6 +127,17 @@ function hasToolOutputReference(value: unknown): boolean {
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function isEagerExecutionExcludedTool(
|
|
131
|
+
name: string,
|
|
132
|
+
graph: StandardGraph
|
|
133
|
+
): boolean {
|
|
134
|
+
if (name === '') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const excluded = graph.eagerEventToolExecution?.excludeToolNames;
|
|
138
|
+
return excluded != null && excluded.includes(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
function isDirectGraphTool(
|
|
131
142
|
name: string,
|
|
132
143
|
agentContext: AgentContext | undefined
|
|
@@ -615,7 +626,7 @@ function createEagerToolExecutionPlan(args: {
|
|
|
615
626
|
return undefined;
|
|
616
627
|
}
|
|
617
628
|
|
|
618
|
-
const
|
|
629
|
+
const unstartedToolCalls = skipExisting
|
|
619
630
|
? toolCalls.filter((toolCall) => {
|
|
620
631
|
if (toolCall.id == null || toolCall.id === '') {
|
|
621
632
|
return true;
|
|
@@ -623,6 +634,13 @@ function createEagerToolExecutionPlan(args: {
|
|
|
623
634
|
return !graph.eagerEventToolExecutions.has(toolCall.id);
|
|
624
635
|
})
|
|
625
636
|
: toolCalls;
|
|
637
|
+
// Drop host-excluded tools only AFTER the batch-level guards above have run
|
|
638
|
+
// against the full batch, so excluding a call never hides a sibling direct
|
|
639
|
+
// tool from `hasDirectToolCallInBatch`. Excluded calls fall through to normal
|
|
640
|
+
// ToolNode execution; siblings may still eager-execute.
|
|
641
|
+
const candidateToolCalls = unstartedToolCalls.filter(
|
|
642
|
+
(toolCall) => !isEagerExecutionExcludedTool(toolCall.name, graph)
|
|
643
|
+
);
|
|
626
644
|
if (candidateToolCalls.length === 0) {
|
|
627
645
|
return [];
|
|
628
646
|
}
|
package/src/types/tools.ts
CHANGED
|
@@ -38,6 +38,16 @@ export type EagerEventToolExecutionConfig = {
|
|
|
38
38
|
* ToolMessages so provider message ordering is preserved.
|
|
39
39
|
*/
|
|
40
40
|
enabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Tool names that must never be started eagerly. Eager execution
|
|
43
|
+
* speculates on tool args before the model turn commits, so
|
|
44
|
+
* side-effecting tools (e.g. file writes) should opt out: a
|
|
45
|
+
* speculative write can land even if the turn is superseded, and a
|
|
46
|
+
* later arg revision would otherwise trip the "changed after eager
|
|
47
|
+
* execution" guard. Excluded calls fall through to normal ToolNode
|
|
48
|
+
* execution with final args.
|
|
49
|
+
*/
|
|
50
|
+
excludeToolNames?: string[];
|
|
41
51
|
};
|
|
42
52
|
|
|
43
53
|
export type EagerEventToolExecutionOutcome =
|