@librechat/agents 3.2.64 → 3.2.66
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 +18 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +15 -3
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +1 -2
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +51 -24
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +73 -9
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +49 -9
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +8 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +9 -8
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +9 -7
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/crw-search.cjs +105 -0
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
- package/dist/cjs/tools/search/search.cjs +4 -2
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +15 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +18 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +15 -3
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +1 -2
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +51 -24
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +73 -9
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +49 -9
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
- package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +8 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +9 -8
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +9 -7
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +163 -0
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/crw-search.mjs +103 -0
- package/dist/esm/tools/search/crw-search.mjs.map +1 -0
- package/dist/esm/tools/search/search.mjs +4 -2
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +15 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/langfuseTraceShaping.d.ts +12 -6
- package/dist/types/llm/anthropic/index.d.ts +2 -0
- package/dist/types/llm/anthropic/types.d.ts +2 -0
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
- package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
- package/dist/types/tools/BashExecutor.d.ts +7 -6
- package/dist/types/tools/CodeExecutor.d.ts +7 -5
- package/dist/types/tools/search/crw-scraper.d.ts +41 -0
- package/dist/types/tools/search/crw-search.d.ts +4 -0
- package/dist/types/tools/search/types.d.ts +88 -3
- package/dist/types/types/graph.d.ts +10 -3
- package/package.json +1 -1
- package/src/graphs/Graph.ts +36 -3
- package/src/instrumentation.ts +20 -0
- package/src/langfuseToolOutputTracing.ts +2 -4
- package/src/langfuseTraceShaping.ts +73 -20
- package/src/llm/anthropic/index.ts +134 -10
- package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
- package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
- package/src/llm/anthropic/llm.spec.ts +100 -16
- package/src/llm/anthropic/types.ts +3 -0
- package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +110 -0
- package/src/llm/anthropic/utils/message_inputs.ts +75 -3
- package/src/llm/anthropic/utils/message_outputs.ts +10 -2
- package/src/llm/anthropic/utils/stream_events.ts +471 -0
- package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +122 -0
- package/src/llm/bedrock/utils/message_inputs.ts +13 -0
- package/src/specs/langfuse-instrumentation.test.ts +64 -0
- package/src/specs/langfuse-routing.integration.test.ts +49 -4
- package/src/specs/langfuse-tool-output-tracing.test.ts +5 -4
- package/src/specs/langfuse-trace-shaping.test.ts +80 -9
- package/src/specs/subagent.test.ts +180 -0
- package/src/tools/BashExecutor.ts +9 -8
- package/src/tools/CodeExecutor.ts +9 -7
- package/src/tools/__tests__/BashExecutor.test.ts +16 -5
- package/src/tools/__tests__/CodeExecutor.stateful.test.ts +17 -6
- package/src/tools/search/crw-scraper.ts +244 -0
- package/src/tools/search/crw-search.ts +167 -0
- package/src/tools/search/crw.test.ts +836 -0
- package/src/tools/search/search.ts +7 -1
- package/src/tools/search/tool.ts +23 -3
- package/src/tools/search/types.ts +103 -3
- package/src/types/graph.ts +10 -3
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a raw Anthropic SSE event stream into LangChain ChatModelStreamEvents.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ChatModelStreamEvent,
|
|
9
|
+
ContentBlockDelta,
|
|
10
|
+
FinishReason,
|
|
11
|
+
} from '@langchain/core/language_models/event';
|
|
12
|
+
import type { ContentBlock, UsageMetadata } from '@langchain/core/messages';
|
|
13
|
+
import type {
|
|
14
|
+
AnthropicCompactionBlock,
|
|
15
|
+
AnthropicCompactionContentBlockDelta,
|
|
16
|
+
AnthropicMessageStreamEvent,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import type { AnthropicUsageData } from './message_outputs';
|
|
19
|
+
import { getAnthropicUsageMetadata } from './message_outputs';
|
|
20
|
+
|
|
21
|
+
type AnthropicContentBlock =
|
|
22
|
+
| Extract<
|
|
23
|
+
AnthropicMessageStreamEvent,
|
|
24
|
+
{ type: 'content_block_start' }
|
|
25
|
+
>['content_block']
|
|
26
|
+
| AnthropicCompactionBlock;
|
|
27
|
+
|
|
28
|
+
type AnthropicContentDelta =
|
|
29
|
+
| Extract<
|
|
30
|
+
AnthropicMessageStreamEvent,
|
|
31
|
+
{ type: 'content_block_delta' }
|
|
32
|
+
>['delta']
|
|
33
|
+
| AnthropicCompactionContentBlockDelta;
|
|
34
|
+
|
|
35
|
+
interface AnthropicStreamErrorEvent {
|
|
36
|
+
type: 'error';
|
|
37
|
+
error: {
|
|
38
|
+
type: string;
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
request_id?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type AnthropicStreamInputEvent =
|
|
45
|
+
| AnthropicMessageStreamEvent
|
|
46
|
+
| AnthropicStreamErrorEvent;
|
|
47
|
+
|
|
48
|
+
type BlockAccumulator = Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
interface AnthropicEventUsage {
|
|
51
|
+
inputTokens: number;
|
|
52
|
+
cacheCreationInputTokens: number;
|
|
53
|
+
cacheReadInputTokens: number;
|
|
54
|
+
outputTokens: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Public API ─────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export interface ConvertAnthropicStreamOptions {
|
|
60
|
+
streamUsage?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert an async iterable of raw Anthropic stream events into
|
|
65
|
+
* LangChain `ChatModelStreamEvent`s with typed deltas.
|
|
66
|
+
*/
|
|
67
|
+
export async function* convertAnthropicStream(
|
|
68
|
+
source: AsyncIterable<AnthropicStreamInputEvent>,
|
|
69
|
+
options: ConvertAnthropicStreamOptions = {}
|
|
70
|
+
): AsyncGenerator<ChatModelStreamEvent> {
|
|
71
|
+
const shouldStreamUsage = options.streamUsage ?? true;
|
|
72
|
+
|
|
73
|
+
// Track accumulated state per content block (for finalization)
|
|
74
|
+
const blockAccumulators = new Map<number, BlockAccumulator>();
|
|
75
|
+
let usageSnapshot: UsageMetadata | undefined;
|
|
76
|
+
let eventUsage: AnthropicEventUsage | undefined;
|
|
77
|
+
let stopReason: string | null = null;
|
|
78
|
+
|
|
79
|
+
for await (const data of source) {
|
|
80
|
+
switch (data.type) {
|
|
81
|
+
// ── Message lifecycle ──────────────────────────────────
|
|
82
|
+
case 'message_start': {
|
|
83
|
+
const { usage, id, model } = data.message;
|
|
84
|
+
if (shouldStreamUsage) {
|
|
85
|
+
eventUsage = {
|
|
86
|
+
inputTokens: usage.input_tokens,
|
|
87
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
|
|
88
|
+
cacheReadInputTokens: usage.cache_read_input_tokens ?? 0,
|
|
89
|
+
outputTokens: usage.output_tokens,
|
|
90
|
+
};
|
|
91
|
+
usageSnapshot = buildUsageSnapshot(eventUsage);
|
|
92
|
+
}
|
|
93
|
+
yield {
|
|
94
|
+
event: 'message-start' as const,
|
|
95
|
+
id,
|
|
96
|
+
...(usageSnapshot ? { usage: usageSnapshot } : {}),
|
|
97
|
+
};
|
|
98
|
+
yield {
|
|
99
|
+
event: 'provider' as const,
|
|
100
|
+
provider: 'anthropic',
|
|
101
|
+
name: 'message_start',
|
|
102
|
+
payload: { model, id },
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'message_delta': {
|
|
108
|
+
stopReason = data.delta.stop_reason;
|
|
109
|
+
if (shouldStreamUsage) {
|
|
110
|
+
eventUsage = updateEventUsage(eventUsage, data.usage);
|
|
111
|
+
usageSnapshot = buildUsageSnapshot(eventUsage);
|
|
112
|
+
yield { event: 'usage' as const, usage: usageSnapshot };
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
'context_management' in data.delta &&
|
|
116
|
+
data.delta.context_management != null
|
|
117
|
+
) {
|
|
118
|
+
yield {
|
|
119
|
+
event: 'provider' as const,
|
|
120
|
+
provider: 'anthropic',
|
|
121
|
+
name: 'context_management',
|
|
122
|
+
payload: data.delta.context_management,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'message_stop': {
|
|
129
|
+
const finishEvent = {
|
|
130
|
+
event: 'message-finish' as const,
|
|
131
|
+
reason: mapStopReason(stopReason),
|
|
132
|
+
...(usageSnapshot ? { usage: usageSnapshot } : {}),
|
|
133
|
+
responseMetadata: { model_provider: 'anthropic' },
|
|
134
|
+
};
|
|
135
|
+
yield finishEvent;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case 'error': {
|
|
140
|
+
const streamError = new Error(data.error.message);
|
|
141
|
+
streamError.name = data.error.type;
|
|
142
|
+
throw streamError;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Content block lifecycle ───────────────────────────
|
|
146
|
+
case 'content_block_start': {
|
|
147
|
+
const { index, content_block } = data;
|
|
148
|
+
const mapped = mapBlockToContentBlock(content_block, index);
|
|
149
|
+
blockAccumulators.set(index, { ...mapped });
|
|
150
|
+
yield {
|
|
151
|
+
event: 'content-block-start' as const,
|
|
152
|
+
index,
|
|
153
|
+
content: mapped as unknown as ContentBlock,
|
|
154
|
+
};
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'content_block_delta': {
|
|
159
|
+
const { index, delta } = data;
|
|
160
|
+
const acc = blockAccumulators.get(index);
|
|
161
|
+
if (!acc) break;
|
|
162
|
+
|
|
163
|
+
const { contentDelta, accumulated } = applyAnthropicDelta(acc, delta);
|
|
164
|
+
blockAccumulators.set(index, accumulated);
|
|
165
|
+
|
|
166
|
+
yield {
|
|
167
|
+
event: 'content-block-delta' as const,
|
|
168
|
+
index,
|
|
169
|
+
delta: contentDelta,
|
|
170
|
+
};
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'content_block_stop': {
|
|
175
|
+
const { index } = data;
|
|
176
|
+
const acc = blockAccumulators.get(index);
|
|
177
|
+
if (!acc) break;
|
|
178
|
+
|
|
179
|
+
const finalized = finalizeBlock(acc);
|
|
180
|
+
yield {
|
|
181
|
+
event: 'content-block-finish' as const,
|
|
182
|
+
index,
|
|
183
|
+
content: finalized,
|
|
184
|
+
};
|
|
185
|
+
blockAccumulators.delete(index);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Unhandled → provider passthrough ───────────────────
|
|
190
|
+
default: {
|
|
191
|
+
const providerData = data as AnthropicMessageStreamEvent;
|
|
192
|
+
yield {
|
|
193
|
+
event: 'provider' as const,
|
|
194
|
+
provider: 'anthropic',
|
|
195
|
+
name: providerData.type,
|
|
196
|
+
payload: providerData,
|
|
197
|
+
};
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Internal helpers ───────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function mapStopReason(stopReason: string | null | undefined): FinishReason {
|
|
207
|
+
switch (stopReason) {
|
|
208
|
+
case 'end_turn':
|
|
209
|
+
case 'stop_sequence':
|
|
210
|
+
return 'stop';
|
|
211
|
+
case 'tool_use':
|
|
212
|
+
return 'tool_use';
|
|
213
|
+
case 'max_tokens':
|
|
214
|
+
case 'model_context_window_exceeded':
|
|
215
|
+
return 'length';
|
|
216
|
+
case 'refusal':
|
|
217
|
+
return 'content_filter';
|
|
218
|
+
default:
|
|
219
|
+
return 'stop';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateEventUsage(
|
|
224
|
+
previous: AnthropicEventUsage | undefined,
|
|
225
|
+
current: AnthropicUsageData
|
|
226
|
+
): AnthropicEventUsage {
|
|
227
|
+
return {
|
|
228
|
+
inputTokens: getCumulativeUsageValue(
|
|
229
|
+
current.input_tokens,
|
|
230
|
+
previous?.inputTokens
|
|
231
|
+
),
|
|
232
|
+
cacheCreationInputTokens: getCumulativeUsageValue(
|
|
233
|
+
current.cache_creation_input_tokens,
|
|
234
|
+
previous?.cacheCreationInputTokens
|
|
235
|
+
),
|
|
236
|
+
cacheReadInputTokens: getCumulativeUsageValue(
|
|
237
|
+
current.cache_read_input_tokens,
|
|
238
|
+
previous?.cacheReadInputTokens
|
|
239
|
+
),
|
|
240
|
+
outputTokens: getCumulativeUsageValue(
|
|
241
|
+
current.output_tokens,
|
|
242
|
+
previous?.outputTokens
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getCumulativeUsageValue(
|
|
248
|
+
current: number | null | undefined,
|
|
249
|
+
previous: number | undefined
|
|
250
|
+
): number {
|
|
251
|
+
return Math.max(previous ?? 0, current ?? 0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildUsageSnapshot(usage: AnthropicEventUsage): UsageMetadata {
|
|
255
|
+
const metadata = getAnthropicUsageMetadata({
|
|
256
|
+
input_tokens: usage.inputTokens,
|
|
257
|
+
output_tokens: usage.outputTokens,
|
|
258
|
+
cache_creation_input_tokens: usage.cacheCreationInputTokens,
|
|
259
|
+
cache_read_input_tokens: usage.cacheReadInputTokens,
|
|
260
|
+
});
|
|
261
|
+
if (metadata == null) {
|
|
262
|
+
throw new Error('Anthropic usage metadata was not created');
|
|
263
|
+
}
|
|
264
|
+
return metadata;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getStringField(record: BlockAccumulator, key: string): string {
|
|
268
|
+
const value = record[key];
|
|
269
|
+
return typeof value === 'string' ? value : '';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getArrayField(record: BlockAccumulator, key: string): unknown[] {
|
|
273
|
+
const value = record[key];
|
|
274
|
+
return Array.isArray(value) ? value : [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mapBlockToContentBlock(
|
|
278
|
+
block: AnthropicContentBlock,
|
|
279
|
+
index: number
|
|
280
|
+
): BlockAccumulator {
|
|
281
|
+
switch (block.type) {
|
|
282
|
+
case 'text':
|
|
283
|
+
return {
|
|
284
|
+
type: 'text' as const,
|
|
285
|
+
text: block.text,
|
|
286
|
+
...(block.citations != null ? { citations: block.citations } : {}),
|
|
287
|
+
index,
|
|
288
|
+
};
|
|
289
|
+
case 'thinking':
|
|
290
|
+
return {
|
|
291
|
+
type: 'reasoning' as const,
|
|
292
|
+
reasoning: block.thinking,
|
|
293
|
+
index,
|
|
294
|
+
};
|
|
295
|
+
case 'redacted_thinking':
|
|
296
|
+
return { ...block, index };
|
|
297
|
+
case 'tool_use':
|
|
298
|
+
return {
|
|
299
|
+
type: 'tool_call_chunk' as const,
|
|
300
|
+
id: block.id,
|
|
301
|
+
name: block.name,
|
|
302
|
+
args: '',
|
|
303
|
+
index,
|
|
304
|
+
};
|
|
305
|
+
case 'server_tool_use':
|
|
306
|
+
return {
|
|
307
|
+
type: 'server_tool_call_chunk' as const,
|
|
308
|
+
id: block.id,
|
|
309
|
+
name: block.name,
|
|
310
|
+
args: '',
|
|
311
|
+
index,
|
|
312
|
+
};
|
|
313
|
+
case 'web_search_tool_result':
|
|
314
|
+
return { ...block, index };
|
|
315
|
+
case 'compaction':
|
|
316
|
+
return { ...block, index };
|
|
317
|
+
default:
|
|
318
|
+
return { type: 'non_standard' as const, value: { ...block }, index };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Map an Anthropic content_block_delta to a content block delta
|
|
324
|
+
* and update the accumulated state.
|
|
325
|
+
*/
|
|
326
|
+
function applyAnthropicDelta(
|
|
327
|
+
accumulated: BlockAccumulator,
|
|
328
|
+
delta: AnthropicContentDelta
|
|
329
|
+
): {
|
|
330
|
+
contentDelta: ContentBlockDelta;
|
|
331
|
+
accumulated: BlockAccumulator;
|
|
332
|
+
} {
|
|
333
|
+
const rawDelta = delta as unknown as Record<string, unknown>;
|
|
334
|
+
switch (delta.type) {
|
|
335
|
+
case 'text_delta':
|
|
336
|
+
return {
|
|
337
|
+
contentDelta: { type: 'text-delta' as const, text: delta.text },
|
|
338
|
+
accumulated: {
|
|
339
|
+
...accumulated,
|
|
340
|
+
text: getStringField(accumulated, 'text') + delta.text,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
case 'thinking_delta':
|
|
345
|
+
return {
|
|
346
|
+
contentDelta: {
|
|
347
|
+
type: 'reasoning-delta' as const,
|
|
348
|
+
reasoning: delta.thinking,
|
|
349
|
+
},
|
|
350
|
+
accumulated: {
|
|
351
|
+
...accumulated,
|
|
352
|
+
reasoning: getStringField(accumulated, 'reasoning') + delta.thinking,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
case 'input_json_delta': {
|
|
357
|
+
const newArgs = getStringField(accumulated, 'args') + delta.partial_json;
|
|
358
|
+
return {
|
|
359
|
+
contentDelta: {
|
|
360
|
+
type: 'block-delta' as const,
|
|
361
|
+
fields: {
|
|
362
|
+
type: getStringField(accumulated, 'type'),
|
|
363
|
+
args: newArgs,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
accumulated: { ...accumulated, args: newArgs },
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
case 'citations_delta': {
|
|
371
|
+
const citations = [
|
|
372
|
+
...getArrayField(accumulated, 'citations'),
|
|
373
|
+
delta.citation,
|
|
374
|
+
];
|
|
375
|
+
return {
|
|
376
|
+
contentDelta: {
|
|
377
|
+
type: 'block-delta' as const,
|
|
378
|
+
fields: {
|
|
379
|
+
type: getStringField(accumulated, 'type'),
|
|
380
|
+
citations,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
accumulated: {
|
|
384
|
+
...accumulated,
|
|
385
|
+
citations,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case 'signature_delta':
|
|
391
|
+
return {
|
|
392
|
+
contentDelta: {
|
|
393
|
+
type: 'block-delta' as const,
|
|
394
|
+
fields: {
|
|
395
|
+
type: getStringField(accumulated, 'type'),
|
|
396
|
+
signature: delta.signature,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
accumulated: { ...accumulated, signature: delta.signature },
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
case 'compaction_delta': {
|
|
403
|
+
const previousContent = accumulated.content;
|
|
404
|
+
const content =
|
|
405
|
+
delta.content == null
|
|
406
|
+
? (previousContent ?? null)
|
|
407
|
+
: getStringField(accumulated, 'content') + delta.content;
|
|
408
|
+
return {
|
|
409
|
+
contentDelta: {
|
|
410
|
+
type: 'block-delta' as const,
|
|
411
|
+
fields: {
|
|
412
|
+
type: 'compaction',
|
|
413
|
+
content,
|
|
414
|
+
encrypted_content: delta.encrypted_content,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
accumulated: {
|
|
418
|
+
...accumulated,
|
|
419
|
+
content,
|
|
420
|
+
encrypted_content: delta.encrypted_content,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
default:
|
|
426
|
+
return {
|
|
427
|
+
contentDelta: {
|
|
428
|
+
type: 'block-delta' as const,
|
|
429
|
+
fields: {
|
|
430
|
+
type: getStringField(accumulated, 'type'),
|
|
431
|
+
...rawDelta,
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
accumulated,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function finalizeBlock(accumulated: BlockAccumulator): ContentBlock {
|
|
440
|
+
if (
|
|
441
|
+
accumulated.type === 'tool_call_chunk' ||
|
|
442
|
+
accumulated.type === 'server_tool_call_chunk'
|
|
443
|
+
) {
|
|
444
|
+
const finalType =
|
|
445
|
+
accumulated.type === 'tool_call_chunk'
|
|
446
|
+
? ('tool_call' as const)
|
|
447
|
+
: ('server_tool_call' as const);
|
|
448
|
+
const args = getStringField(accumulated, 'args');
|
|
449
|
+
let parsedArgs: unknown;
|
|
450
|
+
try {
|
|
451
|
+
parsedArgs = JSON.parse(args || '{}');
|
|
452
|
+
} catch {
|
|
453
|
+
return {
|
|
454
|
+
type: 'invalid_tool_call' as const,
|
|
455
|
+
id: getStringField(accumulated, 'id'),
|
|
456
|
+
name: getStringField(accumulated, 'name'),
|
|
457
|
+
args,
|
|
458
|
+
error: 'Failed to parse tool call arguments as JSON',
|
|
459
|
+
} as ContentBlock.Tools.InvalidToolCall;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
type: finalType,
|
|
463
|
+
id: getStringField(accumulated, 'id'),
|
|
464
|
+
name: getStringField(accumulated, 'name'),
|
|
465
|
+
args: parsedArgs,
|
|
466
|
+
} as ContentBlock;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const { index: _index, ...rest } = accumulated;
|
|
470
|
+
return rest as ContentBlock;
|
|
471
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import { convertToConverseMessages } from './message_inputs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression for cross-provider agent handoffs (Google → Bedrock): a Gemini
|
|
7
|
+
* turn that used a server-side tool (URL context, Google Search) leaves
|
|
8
|
+
* `toolCall`/`toolResponse` content blocks in history. The Bedrock Converse
|
|
9
|
+
* converter has no branch for them and previously threw
|
|
10
|
+
* "Unsupported content block type: toolCall", crashing the handoff. Only
|
|
11
|
+
* Google can execute these blocks or validate their thought signatures, so
|
|
12
|
+
* they are dropped on assistant turns; any other unknown block still throws.
|
|
13
|
+
*/
|
|
14
|
+
type ConverseResult = ReturnType<typeof convertToConverseMessages>;
|
|
15
|
+
|
|
16
|
+
/** Minimal view of a converted Bedrock Converse content block the assertions read. */
|
|
17
|
+
interface ConverseBlock {
|
|
18
|
+
text?: string;
|
|
19
|
+
toolUse?: {
|
|
20
|
+
toolUseId?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
input?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const assistantContent = (result: ConverseResult): ConverseBlock[] => {
|
|
27
|
+
const msg = result.converseMessages.find((m) => m.role === 'assistant');
|
|
28
|
+
return (msg?.content ?? []) as ConverseBlock[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('convertToConverseMessages — Google server-side tool blocks (Google → Bedrock)', () => {
|
|
32
|
+
it('drops toolCall/toolResponse on an assistant turn, keeping text and tool calls', () => {
|
|
33
|
+
const messages: BaseMessage[] = [
|
|
34
|
+
new HumanMessage('summarize this article'),
|
|
35
|
+
new AIMessage({
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'toolCall',
|
|
39
|
+
thoughtSignature: 'google-signature-not-valid-for-bedrock',
|
|
40
|
+
toolCall: {
|
|
41
|
+
toolType: 'URL_CONTEXT',
|
|
42
|
+
args: { urls: ['https://example.com/report'] },
|
|
43
|
+
id: 'j7pfyr6k',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'toolResponse',
|
|
48
|
+
toolResponse: {
|
|
49
|
+
toolType: 'URL_CONTEXT',
|
|
50
|
+
id: 'j7pfyr6k',
|
|
51
|
+
result: { status: 'SUCCESS' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: 'The article argues for dollar-cost averaging.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
tool_calls: [
|
|
60
|
+
{
|
|
61
|
+
id: 'call_client_tool',
|
|
62
|
+
name: 'save_note',
|
|
63
|
+
args: { note: 'DCA summary' },
|
|
64
|
+
type: 'tool_call',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
expect(() => convertToConverseMessages(messages)).not.toThrow();
|
|
71
|
+
const content = assistantContent(convertToConverseMessages(messages));
|
|
72
|
+
|
|
73
|
+
const serialized = JSON.stringify(content);
|
|
74
|
+
expect(serialized).not.toContain('URL_CONTEXT');
|
|
75
|
+
expect(serialized).not.toContain('google-signature-not-valid-for-bedrock');
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
content.some(
|
|
79
|
+
(b) => b.text === 'The article argues for dollar-cost averaging.'
|
|
80
|
+
)
|
|
81
|
+
).toBe(true);
|
|
82
|
+
const toolUse = content.find((b) => b.toolUse != null);
|
|
83
|
+
expect(toolUse?.toolUse).toMatchObject({
|
|
84
|
+
toolUseId: 'call_client_tool',
|
|
85
|
+
name: 'save_note',
|
|
86
|
+
input: { note: 'DCA summary' },
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('emits a placeholder (not empty content) when a server-tool-only turn is fully dropped', () => {
|
|
91
|
+
const messages: BaseMessage[] = [
|
|
92
|
+
new HumanMessage('hi'),
|
|
93
|
+
new AIMessage({
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'toolCall',
|
|
97
|
+
toolCall: { toolType: 'URL_CONTEXT', args: { urls: [] }, id: 'x1' },
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
expect(() => convertToConverseMessages(messages)).not.toThrow();
|
|
103
|
+
const content = assistantContent(convertToConverseMessages(messages));
|
|
104
|
+
expect(content.length).toBeGreaterThan(0);
|
|
105
|
+
expect(content.every((b) => typeof b.text === 'string')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('still throws on a genuinely unknown assistant block', () => {
|
|
109
|
+
const messages: BaseMessage[] = [
|
|
110
|
+
new HumanMessage('run code'),
|
|
111
|
+
new AIMessage({
|
|
112
|
+
content: [
|
|
113
|
+
{ type: 'some_future_block_type', foo: 'bar' },
|
|
114
|
+
{ type: 'text', text: 'done' },
|
|
115
|
+
],
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
expect(() => convertToConverseMessages(messages)).toThrow(
|
|
119
|
+
'Unsupported content block type'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -41,6 +41,14 @@ const FOREIGN_REASONING_TYPES = [
|
|
|
41
41
|
'think',
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Google server-side tool blocks (`toolCall`/`toolResponse` parts from e.g.
|
|
46
|
+
* URL context or Google Search). Only Google can execute these and validate
|
|
47
|
+
* their thought signatures, so they are dropped on a cross-provider handoff
|
|
48
|
+
* (e.g. Google → Bedrock); the assistant's answer text is kept.
|
|
49
|
+
*/
|
|
50
|
+
const FOREIGN_SERVER_TOOL_TYPES = ['toolCall', 'toolResponse'];
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* Bedrock Converse rejects assistant messages with no content blocks. When
|
|
46
54
|
* filtering (e.g. dropping foreign reasoning) empties an assistant turn that
|
|
@@ -727,6 +735,11 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
|
727
735
|
// than crash. The Bedrock model produces its own reasoning. Anything
|
|
728
736
|
// else unknown still throws below — real content must be surfaced.
|
|
729
737
|
return;
|
|
738
|
+
} else if (FOREIGN_SERVER_TOOL_TYPES.some((t) => t === block.type)) {
|
|
739
|
+
// Google server-side tool call/response (e.g. URL context) — only
|
|
740
|
+
// Google can execute it or validate its thought signature; drop it
|
|
741
|
+
// on a cross-provider handoff rather than crash.
|
|
742
|
+
return;
|
|
730
743
|
} else {
|
|
731
744
|
const blockValues = Object.fromEntries(
|
|
732
745
|
Object.entries(block).filter(([key]) => key !== 'type')
|
|
@@ -184,6 +184,70 @@ describe('Langfuse instrumentation', () => {
|
|
|
184
184
|
expect(mockBasicTracerProvider).toHaveBeenCalledTimes(1);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
it('resolves environment from LANGFUSE_TRACING_ENVIRONMENT', async () => {
|
|
188
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT = 'staging';
|
|
189
|
+
|
|
190
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
191
|
+
initializeLangfuseTracing({
|
|
192
|
+
publicKey: 'pk-config',
|
|
193
|
+
secretKey: 'sk-config',
|
|
194
|
+
baseUrl: 'https://langfuse.config',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
|
|
198
|
+
expect.objectContaining({ environment: 'staging' })
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('falls back to NODE_ENV when LANGFUSE_TRACING_ENVIRONMENT is unset', async () => {
|
|
203
|
+
delete process.env.LANGFUSE_TRACING_ENVIRONMENT;
|
|
204
|
+
process.env.NODE_ENV = 'production';
|
|
205
|
+
|
|
206
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
207
|
+
initializeLangfuseTracing({
|
|
208
|
+
publicKey: 'pk-config',
|
|
209
|
+
secretKey: 'sk-config',
|
|
210
|
+
baseUrl: 'https://langfuse.config',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
|
|
214
|
+
expect.objectContaining({ environment: 'production' })
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('falls through blank environment overrides and trims the selected value', async () => {
|
|
219
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT = ' ';
|
|
220
|
+
process.env.NODE_ENV = ' production ';
|
|
221
|
+
|
|
222
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
223
|
+
initializeLangfuseTracing({
|
|
224
|
+
publicKey: 'pk-config',
|
|
225
|
+
secretKey: 'sk-config',
|
|
226
|
+
baseUrl: 'https://langfuse.config',
|
|
227
|
+
environment: '',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
|
|
231
|
+
expect.objectContaining({ environment: 'production' })
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('prefers an explicit config environment over environment variables', async () => {
|
|
236
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT = 'staging';
|
|
237
|
+
|
|
238
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
239
|
+
initializeLangfuseTracing({
|
|
240
|
+
publicKey: 'pk-config',
|
|
241
|
+
secretKey: 'sk-config',
|
|
242
|
+
baseUrl: 'https://langfuse.config',
|
|
243
|
+
environment: 'canary',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
|
|
247
|
+
expect.objectContaining({ environment: 'canary' })
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
187
251
|
it('does not replace the global provider when explicit credentials change', async () => {
|
|
188
252
|
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
189
253
|
initializeLangfuseTracing({
|