@juspay/neurolink 11.6.0 → 11.7.0
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/CHANGELOG.md +2 -6
- package/dist/browser/neurolink.min.js +392 -392
- package/dist/core/loopEngine.js +21 -2
- package/dist/lib/core/loopEngine.js +21 -2
- package/dist/lib/providers/amazonBedrock/client.d.ts +23 -7
- package/dist/lib/providers/amazonBedrock/client.js +276 -950
- package/dist/lib/providers/amazonBedrock/loopAdapter.d.ts +36 -0
- package/dist/lib/providers/amazonBedrock/loopAdapter.js +278 -0
- package/dist/lib/types/loopEngine.d.ts +52 -0
- package/dist/lib/types/providers.d.ts +9 -0
- package/dist/providers/amazonBedrock/client.d.ts +23 -7
- package/dist/providers/amazonBedrock/client.js +276 -950
- package/dist/providers/amazonBedrock/loopAdapter.d.ts +36 -0
- package/dist/providers/amazonBedrock/loopAdapter.js +277 -0
- package/dist/types/loopEngine.d.ts +52 -0
- package/dist/types/providers.d.ts +9 -0
- package/package.json +3 -2
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { BedrockRuntimeClient,
|
|
1
|
+
import { BedrockRuntimeClient, ImageFormat, } from "@aws-sdk/client-bedrock-runtime";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { createAnalytics } from "../../core/analytics.js";
|
|
4
4
|
import { BaseProvider } from "../../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../../core/constants.js";
|
|
6
|
-
import {
|
|
6
|
+
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
7
|
+
import { createBedrockLoopAdapter } from "./loopAdapter.js";
|
|
7
8
|
import { AuthenticationError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
8
9
|
import { classifyProviderError } from "../../utils/errorClassifier.js";
|
|
9
10
|
import { isAbortError, withTimeout } from "../../utils/errorHandling.js";
|
|
10
|
-
import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
import { resolveSamplingParams } from "../../models/modelRegistry.js";
|
|
13
|
-
import { calculateCost } from "../../utils/pricing.js";
|
|
14
13
|
import { buildMultimodalMessagesArray } from "../../utils/messageBuilder.js";
|
|
15
14
|
import { buildMultimodalOptions } from "../../utils/multimodalOptionsBuilder.js";
|
|
16
15
|
import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
|
|
@@ -196,8 +195,10 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
196
195
|
let text;
|
|
197
196
|
let usage;
|
|
198
197
|
let finishReason;
|
|
198
|
+
let rawFinishReason;
|
|
199
199
|
try {
|
|
200
|
-
({ text, usage, finishReason } =
|
|
200
|
+
({ text, usage, finishReason, rawFinishReason } =
|
|
201
|
+
await this.conversationLoop(options));
|
|
201
202
|
}
|
|
202
203
|
catch (error) {
|
|
203
204
|
// Emit failure generation:end so Pipeline B records the failed generation
|
|
@@ -244,344 +245,78 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
244
245
|
usage,
|
|
245
246
|
model: this.modelName || this.getDefaultModel(),
|
|
246
247
|
provider: this.getProviderName(),
|
|
248
|
+
...(finishReason !== undefined && { finishReason }),
|
|
249
|
+
...(rawFinishReason !== undefined && { rawFinishReason }),
|
|
247
250
|
};
|
|
248
251
|
}
|
|
249
252
|
async conversationLoop(options) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (response.stopReason) {
|
|
273
|
-
lastFinishReason = response.stopReason;
|
|
274
|
-
}
|
|
275
|
-
const result = await this.handleBedrockResponse(response);
|
|
276
|
-
logger.debug(`[AmazonBedrockProvider] Handle response result:`, result);
|
|
277
|
-
if (result.shouldContinue) {
|
|
278
|
-
logger.debug(`[AmazonBedrockProvider] Continuing conversation loop...`);
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
logger.debug(`[AmazonBedrockProvider] Conversation completed with final text`);
|
|
282
|
-
logger.debug(`[AmazonBedrockProvider] Returning final text: "${result.text}"`);
|
|
283
|
-
return {
|
|
284
|
-
text: result.text || "",
|
|
285
|
-
usage: {
|
|
286
|
-
input: totalInputTokens,
|
|
287
|
-
output: totalOutputTokens,
|
|
288
|
-
// Cache reads/writes are billed tokens reported separately
|
|
289
|
-
// from inputTokens — the total must include them.
|
|
290
|
-
total: totalInputTokens +
|
|
291
|
-
totalCacheReadTokens +
|
|
292
|
-
totalCacheWriteTokens +
|
|
293
|
-
totalOutputTokens,
|
|
294
|
-
...(totalCacheReadTokens > 0 && {
|
|
295
|
-
cacheReadTokens: totalCacheReadTokens,
|
|
296
|
-
}),
|
|
297
|
-
...(totalCacheWriteTokens > 0 && {
|
|
298
|
-
cacheCreationTokens: totalCacheWriteTokens,
|
|
299
|
-
}),
|
|
300
|
-
},
|
|
301
|
-
finishReason: lastFinishReason,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
logger.error(`[AmazonBedrockProvider] Error in conversation loop:`, error);
|
|
307
|
-
throw this.handleProviderError(error);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
throw new Error("Conversation loop exceeded maximum iterations");
|
|
311
|
-
}
|
|
312
|
-
async callBedrock(options) {
|
|
313
|
-
const startTime = Date.now();
|
|
314
|
-
return bedrockTracer.startActiveSpan("bedrock.generate", {
|
|
315
|
-
kind: SpanKind.CLIENT,
|
|
316
|
-
attributes: {
|
|
317
|
-
"gen_ai.system": "aws.bedrock",
|
|
318
|
-
"gen_ai.request.model": this.modelName || this.getDefaultModel(),
|
|
319
|
-
"gen_ai.operation.name": "chat",
|
|
320
|
-
},
|
|
321
|
-
}, async (generateSpan) => {
|
|
322
|
-
logger.info(`[AmazonBedrockProvider] Starting Bedrock API call at ${new Date().toISOString()}`);
|
|
323
|
-
try {
|
|
324
|
-
// Pre-call validation and logging
|
|
325
|
-
let region = "unknown";
|
|
326
|
-
try {
|
|
327
|
-
region =
|
|
328
|
-
typeof this.bedrockClient.config.region === "function"
|
|
329
|
-
? await this.bedrockClient.config.region()
|
|
330
|
-
: (this.bedrockClient.config.region ?? "unknown");
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
// Region lookup failed — not critical, only used for logging
|
|
334
|
-
}
|
|
335
|
-
logger.info(`[AmazonBedrockProvider] Client region: ${region}`);
|
|
336
|
-
logger.info(`[AmazonBedrockProvider] Model: ${this.modelName || this.getDefaultModel()}`);
|
|
337
|
-
logger.info(`[AmazonBedrockProvider] Conversation history length: ${this.conversationHistory.length}`);
|
|
338
|
-
// Get all available tools
|
|
339
|
-
const aiTools = await this.getAllTools();
|
|
340
|
-
const allTools = this.convertAISDKToolsToToolDefinitions(aiTools);
|
|
341
|
-
const toolConfig = this.formatToolsForBedrock(allTools);
|
|
342
|
-
// Registry-driven strip: models that reject sampling params
|
|
343
|
-
// (Sonnet 5 / Opus 4.7+ / Fable 5 Claude families on Bedrock)
|
|
344
|
-
// must not receive the legacy 0.7 default temperature.
|
|
345
|
-
const generateSampling = resolveSamplingParams(this.providerName, this.modelName || this.getDefaultModel(), { temperature: options.temperature ?? 0.7 }, "bedrock.converse");
|
|
346
|
-
const commandInput = {
|
|
347
|
-
modelId: this.modelName || this.getDefaultModel(),
|
|
348
|
-
messages: this.convertToAWSMessages(this.conversationHistory),
|
|
349
|
-
system: [
|
|
350
|
-
{
|
|
351
|
-
text: options.systemPrompt ||
|
|
352
|
-
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
inferenceConfig: {
|
|
356
|
-
maxTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
357
|
-
...(generateSampling.temperature !== undefined && {
|
|
358
|
-
temperature: generateSampling.temperature,
|
|
359
|
-
}),
|
|
253
|
+
// The step cap is now the same `maxSteps || DEFAULT_MAX_STEPS` the
|
|
254
|
+
// streaming path has always used. It used to be a hardcoded 10 that
|
|
255
|
+
// ignored the caller's maxSteps entirely, and reaching it threw — which
|
|
256
|
+
// then propagated into NeuroLink's provider-level retry and ran the whole
|
|
257
|
+
// turn twice more, so a runaway tool loop cost thirty billed calls. The
|
|
258
|
+
// engine stops at the cap and returns what the turn produced.
|
|
259
|
+
const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
260
|
+
const tools = await this.resolveTurnTools(options.tools, options.disableTools);
|
|
261
|
+
const toolConfig = this.formatToolsForBedrock(tools);
|
|
262
|
+
const sampling = resolveSamplingParams(this.providerName, this.modelName || this.getDefaultModel(), { temperature: options.temperature ?? 0.7 }, "bedrock.converse");
|
|
263
|
+
const adapter = createBedrockLoopAdapter({
|
|
264
|
+
client: this.bedrockClient,
|
|
265
|
+
streaming: false,
|
|
266
|
+
region: this.region,
|
|
267
|
+
maxSteps,
|
|
268
|
+
buildCommandInput: (conversation) => ({
|
|
269
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
270
|
+
messages: this.convertToAWSMessages(conversation),
|
|
271
|
+
system: [
|
|
272
|
+
{
|
|
273
|
+
text: options.systemPrompt ||
|
|
274
|
+
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
|
|
360
275
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Log command details for debugging
|
|
367
|
-
logger.info(`[AmazonBedrockProvider] Command input summary:`);
|
|
368
|
-
logger.info(` - Model ID: ${commandInput.modelId}`);
|
|
369
|
-
logger.info(` - Messages count: ${commandInput.messages?.length || 0}`);
|
|
370
|
-
logger.info(` - System prompts: ${commandInput.system?.length || 0}`);
|
|
371
|
-
logger.info(` - Max tokens: ${commandInput.inferenceConfig?.maxTokens}`);
|
|
372
|
-
logger.info(` - Temperature: ${commandInput.inferenceConfig?.temperature}`);
|
|
373
|
-
logger.debug(`[AmazonBedrockProvider] Calling Bedrock with ${this.conversationHistory.length} messages and ${toolConfig?.tools?.length || 0} tools`);
|
|
374
|
-
logger.debug("[Observability] Bedrock API request", {
|
|
375
|
-
model: commandInput.modelId,
|
|
376
|
-
region: region,
|
|
377
|
-
messageCount: commandInput.messages?.length || 0,
|
|
378
|
-
toolCount: commandInput.toolConfig?.tools?.length || 0,
|
|
379
|
-
maxTokens: commandInput.inferenceConfig?.maxTokens,
|
|
380
|
-
});
|
|
381
|
-
const apiCallStartTime = Date.now();
|
|
382
|
-
const response = await withInferenceProfileFallback(commandInput.modelId ?? "",
|
|
383
|
-
// `this.region`, not the local `region` above: that one is
|
|
384
|
-
// best-effort for logging and stays "unknown" if the lookup
|
|
385
|
-
// throws. It must also match the id the streaming path keys its
|
|
386
|
-
// cache by, or a resolution found here is never reused there.
|
|
387
|
-
this.region, (effectiveModelId) => withTimeout(this.bedrockClient.send(new ConverseCommand({
|
|
388
|
-
...commandInput,
|
|
389
|
-
modelId: effectiveModelId,
|
|
390
|
-
})), 120_000, new Error("Bedrock API call timed out")));
|
|
391
|
-
const apiCallDuration = Date.now() - apiCallStartTime;
|
|
392
|
-
logger.debug("[Observability] Bedrock API response", {
|
|
393
|
-
model: commandInput.modelId,
|
|
394
|
-
durationMs: apiCallDuration,
|
|
395
|
-
hasContent: !!response.output?.message?.content?.length,
|
|
396
|
-
stopReason: response.stopReason,
|
|
397
|
-
usage: response.usage
|
|
398
|
-
? {
|
|
399
|
-
inputTokens: response.usage.inputTokens,
|
|
400
|
-
outputTokens: response.usage.outputTokens,
|
|
401
|
-
totalTokens: (response.usage.inputTokens || 0) +
|
|
402
|
-
(response.usage.outputTokens || 0),
|
|
403
|
-
}
|
|
404
|
-
: undefined,
|
|
405
|
-
});
|
|
406
|
-
logger.info(`[AmazonBedrockProvider] Bedrock API call successful`);
|
|
407
|
-
logger.info(`[AmazonBedrockProvider] API call duration: ${apiCallDuration}ms`);
|
|
408
|
-
const totalDuration = Date.now() - startTime;
|
|
409
|
-
logger.info(`[AmazonBedrockProvider] Total callBedrock duration: ${totalDuration}ms`);
|
|
410
|
-
generateSpan.setAttribute("gen_ai.response.stop_reason", response.stopReason ?? "");
|
|
411
|
-
const spanCacheRead = response.usage?.cacheReadInputTokens ?? 0;
|
|
412
|
-
const spanCacheWrite = response.usage?.cacheWriteInputTokens ?? 0;
|
|
413
|
-
// Converse's inputTokens is only the UNCACHED remainder — the span
|
|
414
|
-
// attribute reports the FULL prompt (uncached + cache read/write)
|
|
415
|
-
// so telemetry matches the cache-inclusive pricing inputs below.
|
|
416
|
-
generateSpan.setAttribute("gen_ai.usage.input_tokens", (response.usage?.inputTokens ?? 0) + spanCacheRead + spanCacheWrite);
|
|
417
|
-
generateSpan.setAttribute("gen_ai.usage.cache_read_input_tokens", spanCacheRead);
|
|
418
|
-
generateSpan.setAttribute("gen_ai.usage.cache_creation_input_tokens", spanCacheWrite);
|
|
419
|
-
generateSpan.setAttribute("gen_ai.usage.output_tokens", response.usage?.outputTokens ?? 0);
|
|
420
|
-
const cost = calculateCost(this.providerName, this.modelName, {
|
|
421
|
-
input: response.usage?.inputTokens ?? 0,
|
|
422
|
-
output: response.usage?.outputTokens ?? 0,
|
|
423
|
-
total: (response.usage?.inputTokens ?? 0) +
|
|
424
|
-
spanCacheRead +
|
|
425
|
-
spanCacheWrite +
|
|
426
|
-
(response.usage?.outputTokens ?? 0),
|
|
427
|
-
...(spanCacheRead > 0 && { cacheReadTokens: spanCacheRead }),
|
|
428
|
-
...(spanCacheWrite > 0 && {
|
|
429
|
-
cacheCreationTokens: spanCacheWrite,
|
|
276
|
+
],
|
|
277
|
+
inferenceConfig: {
|
|
278
|
+
maxTokens: options.maxTokens,
|
|
279
|
+
...(sampling.temperature !== undefined && {
|
|
280
|
+
temperature: sampling.temperature,
|
|
430
281
|
}),
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
generateSpan.setAttribute("neurolink.cost", cost);
|
|
434
|
-
}
|
|
435
|
-
generateSpan.setStatus({ code: SpanStatusCode.OK });
|
|
436
|
-
generateSpan.end();
|
|
437
|
-
return response;
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
const errorDuration = Date.now() - startTime;
|
|
441
|
-
// Extract AWS metadata for structured logging
|
|
442
|
-
const awsError = error && typeof error === "object"
|
|
443
|
-
? error
|
|
444
|
-
: null;
|
|
445
|
-
const metadata = awsError?.$metadata && typeof awsError.$metadata === "object"
|
|
446
|
-
? awsError.$metadata
|
|
447
|
-
: null;
|
|
448
|
-
logger.debug("[Observability] Bedrock API request failed", {
|
|
449
|
-
model: this.modelName || this.getDefaultModel(),
|
|
450
|
-
durationMs: errorDuration,
|
|
451
|
-
error: error instanceof Error ? error.message : String(error),
|
|
452
|
-
errorName: error instanceof Error ? error.name : undefined,
|
|
453
|
-
httpStatus: metadata?.httpStatusCode,
|
|
454
|
-
awsRequestId: metadata?.requestId,
|
|
455
|
-
awsErrorCode: awsError?.Code,
|
|
456
|
-
});
|
|
457
|
-
logger.error(`[AmazonBedrockProvider] Bedrock API call failed after ${errorDuration}ms`);
|
|
458
|
-
if (error instanceof Error) {
|
|
459
|
-
logger.error(`[AmazonBedrockProvider] Error: ${error.name} - ${error.message}`);
|
|
460
|
-
}
|
|
461
|
-
if (metadata) {
|
|
462
|
-
logger.error(`[AmazonBedrockProvider] AWS SDK metadata`, {
|
|
463
|
-
httpStatus: metadata.httpStatusCode,
|
|
464
|
-
requestId: metadata.requestId,
|
|
465
|
-
attempts: metadata.attempts,
|
|
466
|
-
totalRetryDelay: metadata.totalRetryDelay,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
generateSpan.setStatus({
|
|
470
|
-
code: SpanStatusCode.ERROR,
|
|
471
|
-
message: error instanceof Error ? error.message : String(error),
|
|
472
|
-
});
|
|
473
|
-
generateSpan.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
474
|
-
generateSpan.end();
|
|
475
|
-
throw error;
|
|
476
|
-
}
|
|
477
|
-
}); // end bedrockTracer.startActiveSpan('bedrock.generate')
|
|
478
|
-
}
|
|
479
|
-
async handleBedrockResponse(response) {
|
|
480
|
-
logger.debug(`[AmazonBedrockProvider] Received response with stopReason: ${response.stopReason}`);
|
|
481
|
-
if (!response.output || !response.output.message) {
|
|
482
|
-
throw new Error("Invalid response structure from Bedrock API");
|
|
483
|
-
}
|
|
484
|
-
const assistantMessage = response.output.message;
|
|
485
|
-
const stopReason = response.stopReason;
|
|
486
|
-
// Add assistant message to conversation history
|
|
487
|
-
const bedrockAssistantMessage = {
|
|
488
|
-
role: "assistant",
|
|
489
|
-
content: (assistantMessage.content || []).map((item) => {
|
|
490
|
-
const bedrockItem = {};
|
|
491
|
-
if ("text" in item && item.text) {
|
|
492
|
-
bedrockItem.text = item.text;
|
|
493
|
-
}
|
|
494
|
-
if ("toolUse" in item && item.toolUse) {
|
|
495
|
-
bedrockItem.toolUse = {
|
|
496
|
-
toolUseId: item.toolUse.toolUseId || "",
|
|
497
|
-
name: item.toolUse.name || "",
|
|
498
|
-
input: item.toolUse.input || {},
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if ("toolResult" in item && item.toolResult) {
|
|
502
|
-
bedrockItem.toolResult = {
|
|
503
|
-
toolUseId: item.toolResult.toolUseId || "",
|
|
504
|
-
content: (item.toolResult.content || []).map((c) => ({
|
|
505
|
-
text: typeof c === "object" && "text" in c
|
|
506
|
-
? c.text || ""
|
|
507
|
-
: "",
|
|
508
|
-
})),
|
|
509
|
-
status: item.toolResult.status || "unknown",
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
return bedrockItem;
|
|
282
|
+
},
|
|
283
|
+
...(toolConfig ? { toolConfig } : {}),
|
|
513
284
|
}),
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
logger.error(`[AmazonBedrockProvider] Tool execution failed: ${contentItem.toolUse.name}`, error);
|
|
546
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
547
|
-
// Still create toolResult for failed tools to maintain 1:1 mapping with toolUse blocks
|
|
548
|
-
toolResults.push({
|
|
549
|
-
toolResult: {
|
|
550
|
-
toolUseId: contentItem.toolUse.toolUseId,
|
|
551
|
-
content: [
|
|
552
|
-
{
|
|
553
|
-
text: `Error executing tool ${contentItem.toolUse.name}: ${errorMessage}`,
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
status: "error",
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// Add tool results as user message
|
|
563
|
-
if (toolResults.length > 0) {
|
|
564
|
-
const userMessageWithToolResults = {
|
|
565
|
-
role: "user",
|
|
566
|
-
content: toolResults,
|
|
567
|
-
};
|
|
568
|
-
this.conversationHistory.push(userMessageWithToolResults);
|
|
569
|
-
logger.debug(`[AmazonBedrockProvider] Added ${toolResults.length} tool results to conversation`);
|
|
570
|
-
}
|
|
571
|
-
return { shouldContinue: true };
|
|
572
|
-
}
|
|
573
|
-
else if (stopReason === "max_tokens") {
|
|
574
|
-
// Max tokens reached — return what we have rather than continuing,
|
|
575
|
-
// since the model hit the configured limit.
|
|
576
|
-
const textContent = bedrockAssistantMessage.content
|
|
577
|
-
.filter((item) => item.text)
|
|
578
|
-
.map((item) => item.text)
|
|
579
|
-
.join(" ");
|
|
580
|
-
return { shouldContinue: false, text: textContent };
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
const { resultPromise } = runAgenticLoop(adapter, this.conversationHistory, {
|
|
288
|
+
tools: this.toEngineTools(tools),
|
|
289
|
+
abortSignal: options.abortSignal,
|
|
290
|
+
});
|
|
291
|
+
const result = await resultPromise;
|
|
292
|
+
this.conversationHistory = result.conversation;
|
|
293
|
+
const input = result.usage.inputTokens;
|
|
294
|
+
const output = result.usage.outputTokens;
|
|
295
|
+
const cacheRead = result.usage.cacheReadTokens ?? 0;
|
|
296
|
+
const cacheWrite = result.usage.cacheWriteTokens ?? 0;
|
|
297
|
+
return {
|
|
298
|
+
text: result.text || "",
|
|
299
|
+
usage: {
|
|
300
|
+
input,
|
|
301
|
+
output,
|
|
302
|
+
// Cache reads/writes are billed tokens reported separately from
|
|
303
|
+
// inputTokens — the total must include them.
|
|
304
|
+
total: input + cacheRead + cacheWrite + output,
|
|
305
|
+
...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
|
|
306
|
+
...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
|
|
307
|
+
},
|
|
308
|
+
// The MAPPED reason, matching the streaming path and every AI-SDK
|
|
309
|
+
// backed provider. This path previously surfaced Bedrock's raw value,
|
|
310
|
+
// so a turn stopped at the step cap reported "tool_use" here while
|
|
311
|
+
// the same turn reported "tool-calls" when streamed. The raw value is
|
|
312
|
+
// kept alongside rather than dropped.
|
|
313
|
+
finishReason: result.finishReason,
|
|
314
|
+
rawFinishReason: result.rawStopReason,
|
|
315
|
+
};
|
|
581
316
|
}
|
|
582
|
-
|
|
583
|
-
logger.
|
|
584
|
-
|
|
317
|
+
catch (error) {
|
|
318
|
+
logger.error(`[AmazonBedrockProvider] Error in conversation loop:`, error);
|
|
319
|
+
throw this.handleProviderError(error);
|
|
585
320
|
}
|
|
586
321
|
}
|
|
587
322
|
convertToAWSMessages(bedrockMessages) {
|
|
@@ -625,7 +360,16 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
625
360
|
}),
|
|
626
361
|
}));
|
|
627
362
|
}
|
|
628
|
-
|
|
363
|
+
/**
|
|
364
|
+
* `tools` is passed in rather than re-resolved from `getAllTools()`. That
|
|
365
|
+
* call returns only the provider's own registry, so resolving here meant a
|
|
366
|
+
* tool the caller passed to generate/stream could never execute — the
|
|
367
|
+
* streaming path advertised it to the model and then failed every call to
|
|
368
|
+
* it with "Tool not found", and the generate path never advertised it at
|
|
369
|
+
* all. The turn's full merged tool set is resolved once by the caller and
|
|
370
|
+
* handed down.
|
|
371
|
+
*/
|
|
372
|
+
async executeSingleTool(tools, toolName, args, _toolUseId) {
|
|
629
373
|
return bedrockTracer.startActiveSpan("bedrock.tool.execute", {
|
|
630
374
|
kind: SpanKind.CLIENT,
|
|
631
375
|
attributes: {
|
|
@@ -637,9 +381,6 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
637
381
|
logger.debug(`[AmazonBedrockProvider] Executing single tool: ${toolName}`, {
|
|
638
382
|
args,
|
|
639
383
|
});
|
|
640
|
-
// Use BaseProvider's tool execution mechanism
|
|
641
|
-
const aiTools = await this.getAllTools();
|
|
642
|
-
const tools = this.convertAISDKToolsToToolDefinitions(aiTools);
|
|
643
384
|
if (!tools[toolName]) {
|
|
644
385
|
throw new Error(`Tool not found: ${toolName}`);
|
|
645
386
|
}
|
|
@@ -727,6 +468,39 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
727
468
|
}
|
|
728
469
|
});
|
|
729
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Resolve the turn's tools once: whatever the caller passed, else the
|
|
473
|
+
* provider's own registry. `BaseProvider.stream()` has already merged base
|
|
474
|
+
* tools into `options.tools` by the time it reaches the streaming path;
|
|
475
|
+
* the generate path has no such pre-merge, so it falls back here.
|
|
476
|
+
*/
|
|
477
|
+
async resolveTurnTools(optionTools, disableTools) {
|
|
478
|
+
// `generate()` reaches conversationLoop() without going through
|
|
479
|
+
// BaseProvider's tool preparation, so `options.tools` is undefined there
|
|
480
|
+
// and the getAllTools() fallback would hand the model the whole registry
|
|
481
|
+
// even when the caller asked for no tools at all.
|
|
482
|
+
if (disableTools) {
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
const aiTools = optionTools ??
|
|
486
|
+
(await this.getAllTools());
|
|
487
|
+
return this.convertAISDKToolsToToolDefinitions(aiTools);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Present the resolved tools in the shape `runAgenticLoop` dispatches
|
|
491
|
+
* through. Execution still goes through `executeSingleTool`, so the tool
|
|
492
|
+
* span, the parameter defaults and the ToolResult unwrapping are unchanged
|
|
493
|
+
* — only which tools are reachable changes.
|
|
494
|
+
*/
|
|
495
|
+
toEngineTools(tools) {
|
|
496
|
+
const engineTools = {};
|
|
497
|
+
for (const name of Object.keys(tools)) {
|
|
498
|
+
engineTools[name] = {
|
|
499
|
+
execute: (args) => this.executeSingleTool(tools, name, args),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return engineTools;
|
|
503
|
+
}
|
|
730
504
|
convertAISDKToolsToToolDefinitions(aiTools) {
|
|
731
505
|
const result = {};
|
|
732
506
|
for (const [name, tool] of Object.entries(aiTools)) {
|
|
@@ -1052,631 +826,183 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1052
826
|
});
|
|
1053
827
|
}
|
|
1054
828
|
async streamingConversationLoop(options, streamSpan) {
|
|
1055
|
-
logger.debug("[TRACE] streamingConversationLoop ENTRY");
|
|
1056
829
|
const startTime = Date.now();
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const response = await withInferenceProfileFallback(commandInput.modelId ?? "", this.region, (effectiveModelId) => withTimeout(this.bedrockClient.send(new ConverseStreamCommand({
|
|
1083
|
-
...commandInput,
|
|
1084
|
-
modelId: effectiveModelId,
|
|
1085
|
-
})), 120_000, new Error("Bedrock streaming API call timed out")));
|
|
1086
|
-
logger.debug("[Observability] Bedrock streaming API connection established", {
|
|
1087
|
-
model: commandInput.modelId,
|
|
1088
|
-
durationMs: Date.now() - streamStartTime,
|
|
1089
|
-
hasStream: !!response.stream,
|
|
1090
|
-
});
|
|
1091
|
-
// Process the first response immediately to avoid waste
|
|
1092
|
-
const stream = new ReadableStream({
|
|
1093
|
-
start: async (controller) => {
|
|
1094
|
-
logger.debug("[TRACE] streamingConversationLoop - ReadableStream start() called");
|
|
1095
|
-
try {
|
|
1096
|
-
// Process the first response we already have, tracking all event types
|
|
1097
|
-
let firstStopReason = "";
|
|
1098
|
-
if (response.stream) {
|
|
1099
|
-
const firstMessageContent = [];
|
|
1100
|
-
let firstText = "";
|
|
1101
|
-
for await (const chunk of response.stream) {
|
|
1102
|
-
if (chunk.contentBlockStart) {
|
|
1103
|
-
firstMessageContent.push({});
|
|
1104
|
-
}
|
|
1105
|
-
if (chunk.contentBlockDelta?.delta?.text) {
|
|
1106
|
-
const textDelta = chunk.contentBlockDelta.delta.text;
|
|
1107
|
-
firstText += textDelta;
|
|
1108
|
-
controller.enqueue({ content: textDelta });
|
|
1109
|
-
}
|
|
1110
|
-
if (chunk.contentBlockStart?.start?.toolUse) {
|
|
1111
|
-
const currentBlock = firstMessageContent[firstMessageContent.length - 1];
|
|
1112
|
-
currentBlock.toolUse = {
|
|
1113
|
-
name: chunk.contentBlockStart.start.toolUse.name || "",
|
|
1114
|
-
input: {},
|
|
1115
|
-
toolUseId: chunk.contentBlockStart.start.toolUse.toolUseId ||
|
|
1116
|
-
`tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
if (chunk.contentBlockDelta?.delta?.toolUse) {
|
|
1120
|
-
const currentBlock = firstMessageContent[firstMessageContent.length - 1];
|
|
1121
|
-
if (!currentBlock.toolUse) {
|
|
1122
|
-
currentBlock.toolUse = {
|
|
1123
|
-
name: "",
|
|
1124
|
-
input: {},
|
|
1125
|
-
toolUseId: `tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
const deltaInput = chunk.contentBlockDelta.delta.toolUse.input;
|
|
1129
|
-
if (!deltaInput) {
|
|
1130
|
-
// no input delta
|
|
1131
|
-
}
|
|
1132
|
-
else if (typeof deltaInput === "string") {
|
|
1133
|
-
currentBlock._inputBuffer =
|
|
1134
|
-
(currentBlock._inputBuffer || "") + deltaInput;
|
|
1135
|
-
}
|
|
1136
|
-
else if (typeof deltaInput === "object" &&
|
|
1137
|
-
!Array.isArray(deltaInput)) {
|
|
1138
|
-
const currentInput = currentBlock.toolUse.input || {};
|
|
1139
|
-
currentBlock.toolUse.input = {
|
|
1140
|
-
...currentInput,
|
|
1141
|
-
...deltaInput,
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
if (chunk.contentBlockStop) {
|
|
1146
|
-
const currentBlock = firstMessageContent[firstMessageContent.length - 1];
|
|
1147
|
-
if (currentBlock?.toolUse && currentBlock._inputBuffer) {
|
|
1148
|
-
try {
|
|
1149
|
-
currentBlock.toolUse.input = JSON.parse(currentBlock._inputBuffer);
|
|
1150
|
-
}
|
|
1151
|
-
catch {
|
|
1152
|
-
currentBlock.toolUse.input = {};
|
|
1153
|
-
}
|
|
1154
|
-
delete currentBlock._inputBuffer;
|
|
1155
|
-
}
|
|
1156
|
-
if (firstText && currentBlock && !currentBlock.toolUse) {
|
|
1157
|
-
currentBlock.text = firstText;
|
|
1158
|
-
}
|
|
1159
|
-
firstText = "";
|
|
1160
|
-
}
|
|
1161
|
-
if (chunk.messageStop) {
|
|
1162
|
-
firstStopReason = chunk.messageStop.stopReason || "end_turn";
|
|
1163
|
-
// Don't break — metadata chunk with usage comes after messageStop
|
|
1164
|
-
continue;
|
|
1165
|
-
}
|
|
1166
|
-
// Accumulate usage from Bedrock metadata chunk for Pipeline B.
|
|
1167
|
-
// The metadata chunk is emitted after messageStop with aggregate usage.
|
|
1168
|
-
if (chunk.metadata?.usage) {
|
|
1169
|
-
streamTotalInputTokens +=
|
|
1170
|
-
chunk.metadata.usage.inputTokens ?? 0;
|
|
1171
|
-
streamTotalOutputTokens +=
|
|
1172
|
-
chunk.metadata.usage.outputTokens ?? 0;
|
|
1173
|
-
// inputTokens excludes cache reads/writes (Converse follows
|
|
1174
|
-
// the Anthropic additive convention) — track them too.
|
|
1175
|
-
streamTotalCacheReadTokens +=
|
|
1176
|
-
chunk.metadata.usage.cacheReadInputTokens ?? 0;
|
|
1177
|
-
streamTotalCacheWriteTokens +=
|
|
1178
|
-
chunk.metadata.usage.cacheWriteInputTokens ?? 0;
|
|
1179
|
-
// Stream is effectively complete after metadata chunk
|
|
1180
|
-
break;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
if (firstStopReason) {
|
|
1184
|
-
streamLastStopReason = firstStopReason;
|
|
1185
|
-
}
|
|
1186
|
-
// Add first assistant message to conversation history
|
|
1187
|
-
const firstAssistantMessage = {
|
|
1188
|
-
role: "assistant",
|
|
1189
|
-
content: firstMessageContent,
|
|
1190
|
-
};
|
|
1191
|
-
this.conversationHistory.push(firstAssistantMessage);
|
|
1192
|
-
streamSpan.addEvent("stream.turn_complete", {
|
|
1193
|
-
iteration: 0,
|
|
1194
|
-
stop_reason: firstStopReason,
|
|
1195
|
-
});
|
|
1196
|
-
if (firstStopReason === "tool_use") {
|
|
1197
|
-
const toolNames = firstMessageContent
|
|
1198
|
-
.flatMap((b) => (b.toolUse?.name ? [b.toolUse.name] : []))
|
|
1199
|
-
.join(", ");
|
|
1200
|
-
streamSpan.addEvent("stream.tool_use", {
|
|
1201
|
-
iteration: 0,
|
|
1202
|
-
tool_names: toolNames,
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
// Handle the stop reason from the first response
|
|
1206
|
-
const shouldContinue = await this.handleStreamStopReason(firstStopReason, firstAssistantMessage, controller, options);
|
|
1207
|
-
if (!shouldContinue) {
|
|
1208
|
-
streamSpan.setAttribute("gen_ai.response.stop_reason", firstStopReason);
|
|
1209
|
-
// Close the controller so downstream `for await` exits;
|
|
1210
|
-
// see the close() comment near the bottom of this start()
|
|
1211
|
-
// function for the spec rationale.
|
|
1212
|
-
controller.close();
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
// Continue with normal iterations if needed
|
|
1217
|
-
while (iteration < maxIterations) {
|
|
1218
|
-
iteration++;
|
|
1219
|
-
logger.debug(`[AmazonBedrockProvider] Streaming iteration ${iteration}`);
|
|
1220
|
-
const commandInput = await this.prepareStreamCommand(options);
|
|
1221
|
-
const { stopReason, assistantMessage, usage } = await this.processStreamResponse(commandInput, controller);
|
|
1222
|
-
// Accumulate real usage from Bedrock metadata chunks.
|
|
1223
|
-
if (usage) {
|
|
1224
|
-
streamTotalInputTokens += usage.input;
|
|
1225
|
-
streamTotalOutputTokens += usage.output;
|
|
1226
|
-
streamTotalCacheReadTokens += usage.cacheReadTokens ?? 0;
|
|
1227
|
-
streamTotalCacheWriteTokens += usage.cacheCreationTokens ?? 0;
|
|
1228
|
-
}
|
|
1229
|
-
if (stopReason) {
|
|
1230
|
-
streamLastStopReason = stopReason;
|
|
1231
|
-
}
|
|
1232
|
-
streamSpan.addEvent("stream.turn_complete", {
|
|
1233
|
-
iteration,
|
|
1234
|
-
stop_reason: stopReason,
|
|
1235
|
-
});
|
|
1236
|
-
if (stopReason === "tool_use") {
|
|
1237
|
-
const toolNames = assistantMessage.content
|
|
1238
|
-
.flatMap((b) => (b.toolUse?.name ? [b.toolUse.name] : []))
|
|
1239
|
-
.join(", ");
|
|
1240
|
-
streamSpan.addEvent("stream.tool_use", {
|
|
1241
|
-
iteration,
|
|
1242
|
-
tool_names: toolNames,
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
const shouldContinue = await this.handleStreamStopReason(stopReason, assistantMessage, controller, options);
|
|
1246
|
-
if (!shouldContinue) {
|
|
1247
|
-
streamSpan.setAttribute("gen_ai.response.stop_reason", stopReason);
|
|
1248
|
-
break;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
if (iteration >= maxIterations) {
|
|
1252
|
-
streamSpan.setAttribute("gen_ai.response.stop_reason", "max_iterations");
|
|
1253
|
-
controller.error(new Error("Streaming conversation exceeded maximum iterations"));
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
// CRITICAL: ReadableStream's start() returning does NOT auto-close
|
|
1257
|
-
// the controller per the WHATWG Streams spec. Without this, the
|
|
1258
|
-
// downstream `for await (const chunk of stream)` in
|
|
1259
|
-
// convertToAsyncIterable never sees `done: true` and the
|
|
1260
|
-
// consumer hangs forever — manifested as a 240s harness
|
|
1261
|
-
// PER_TEST_TIMEOUT_SKIP for `[bedrock] stream tokens`. The first-
|
|
1262
|
-
// iteration `return` path and the while-loop natural `break`
|
|
1263
|
-
// path both reach here, so closing once at the bottom covers
|
|
1264
|
-
// every non-error exit.
|
|
1265
|
-
controller.close();
|
|
1266
|
-
}
|
|
1267
|
-
catch (error) {
|
|
1268
|
-
logger.debug("[TRACE] streamingConversationLoop - CATCH block hit in ReadableStream");
|
|
1269
|
-
controller.error(error);
|
|
1270
|
-
}
|
|
1271
|
-
},
|
|
1272
|
-
});
|
|
1273
|
-
// Emit generation:end after the stream completes so Pipeline B (Langfuse)
|
|
1274
|
-
// creates a GENERATION observation. Bedrock bypasses the Vercel AI SDK so
|
|
1275
|
-
// experimental_telemetry is never injected; we emit the event manually.
|
|
1276
|
-
const streamEmitter = this.neurolink?.getEventEmitter();
|
|
1277
|
-
const streamAsyncIterable = this.convertToAsyncIterable(stream);
|
|
1278
|
-
const self = this;
|
|
1279
|
-
// Defer analytics resolution until the stream completes so we have
|
|
1280
|
-
// real token counts aggregated from Bedrock metadata chunks.
|
|
1281
|
-
let resolveAnalytics;
|
|
1282
|
-
const analyticsPromise = new Promise((resolve) => {
|
|
1283
|
-
resolveAnalytics = resolve;
|
|
1284
|
-
});
|
|
1285
|
-
const wrappedStreamIterable = {
|
|
1286
|
-
async *[Symbol.asyncIterator]() {
|
|
1287
|
-
let streamErrored = false;
|
|
1288
|
-
try {
|
|
1289
|
-
yield* streamAsyncIterable;
|
|
1290
|
-
}
|
|
1291
|
-
catch (error) {
|
|
1292
|
-
streamErrored = true;
|
|
1293
|
-
throw error;
|
|
1294
|
-
}
|
|
1295
|
-
finally {
|
|
1296
|
-
const aggregatedUsage = {
|
|
1297
|
-
input: streamTotalInputTokens,
|
|
1298
|
-
output: streamTotalOutputTokens,
|
|
1299
|
-
// Cache reads/writes are billed tokens reported separately
|
|
1300
|
-
// from inputTokens — the total must include them.
|
|
1301
|
-
total: streamTotalInputTokens +
|
|
1302
|
-
streamTotalCacheReadTokens +
|
|
1303
|
-
streamTotalCacheWriteTokens +
|
|
1304
|
-
streamTotalOutputTokens,
|
|
1305
|
-
...(streamTotalCacheReadTokens > 0 && {
|
|
1306
|
-
cacheReadTokens: streamTotalCacheReadTokens,
|
|
1307
|
-
}),
|
|
1308
|
-
...(streamTotalCacheWriteTokens > 0 && {
|
|
1309
|
-
cacheCreationTokens: streamTotalCacheWriteTokens,
|
|
1310
|
-
}),
|
|
1311
|
-
};
|
|
1312
|
-
// Resolve analytics with accumulated token counts from Bedrock
|
|
1313
|
-
// metadata chunks so Pipeline A also reports real usage.
|
|
1314
|
-
resolveAnalytics(createAnalytics(self.providerName, self.modelName || self.getDefaultModel(), { usage: aggregatedUsage }, Date.now() - startTime, {
|
|
1315
|
-
requestId: `bedrock-stream-${Date.now()}`,
|
|
1316
|
-
streamingMode: true,
|
|
1317
|
-
}));
|
|
1318
|
-
if (streamEmitter) {
|
|
1319
|
-
streamEmitter.emit("generation:end", {
|
|
1320
|
-
provider: self.providerName,
|
|
1321
|
-
responseTime: Date.now() - startTime,
|
|
1322
|
-
timestamp: Date.now(),
|
|
1323
|
-
result: {
|
|
1324
|
-
content: "",
|
|
1325
|
-
usage: aggregatedUsage,
|
|
1326
|
-
model: self.modelName || self.getDefaultModel(),
|
|
1327
|
-
provider: self.providerName,
|
|
1328
|
-
finishReason: streamErrored ? "error" : streamLastStopReason,
|
|
1329
|
-
},
|
|
1330
|
-
success: !streamErrored,
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
},
|
|
1335
|
-
};
|
|
1336
|
-
return {
|
|
1337
|
-
stream: wrappedStreamIterable,
|
|
1338
|
-
// No usage key here on purpose: the real aggregate resolves through
|
|
1339
|
-
// `analytics` after the stream drains. A literal zero object is
|
|
1340
|
-
// truthy and would block every downstream usage fallback.
|
|
1341
|
-
model: this.modelName || this.getDefaultModel(),
|
|
1342
|
-
provider: this.getProviderName(),
|
|
1343
|
-
analytics: analyticsPromise,
|
|
1344
|
-
metadata: {
|
|
1345
|
-
startTime,
|
|
1346
|
-
streamId: `bedrock-${Date.now()}`,
|
|
830
|
+
const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
831
|
+
// Resolved once for the whole turn. Bedrock has no mid-turn tool
|
|
832
|
+
// discovery, so nothing here would need re-resolving per step.
|
|
833
|
+
const tools = await this.resolveTurnTools(options.tools, options.disableTools);
|
|
834
|
+
const toolConfig = this.formatToolsForBedrock(tools);
|
|
835
|
+
const sampling = resolveSamplingParams(this.providerName, this.modelName || this.getDefaultModel(), { temperature: options.temperature ?? 0.7 }, "bedrock.converseStream");
|
|
836
|
+
const baseAdapter = createBedrockLoopAdapter({
|
|
837
|
+
client: this.bedrockClient,
|
|
838
|
+
streaming: true,
|
|
839
|
+
region: this.region,
|
|
840
|
+
maxSteps,
|
|
841
|
+
buildCommandInput: (conversation) => ({
|
|
842
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
843
|
+
messages: this.convertToAWSMessages(conversation),
|
|
844
|
+
system: [
|
|
845
|
+
{
|
|
846
|
+
text: options.systemPrompt ||
|
|
847
|
+
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
|
|
848
|
+
},
|
|
849
|
+
],
|
|
850
|
+
inferenceConfig: {
|
|
851
|
+
maxTokens: options.maxTokens,
|
|
852
|
+
...(sampling.temperature !== undefined && {
|
|
853
|
+
temperature: sampling.temperature,
|
|
854
|
+
}),
|
|
1347
855
|
},
|
|
856
|
+
...(toolConfig ? { toolConfig } : {}),
|
|
857
|
+
}),
|
|
858
|
+
});
|
|
859
|
+
// executeStream() falls back to non-streaming generate() when the first
|
|
860
|
+
// call fails on permissions, and that only works if the failure reaches it
|
|
861
|
+
// synchronously. The engine runs the turn in the background, so the first
|
|
862
|
+
// successful send reports separately.
|
|
863
|
+
//
|
|
864
|
+
// Success is the ONLY thing signalled here. The engine wraps every
|
|
865
|
+
// executeStep in withProviderRetry, so rejecting on a failed attempt
|
|
866
|
+
// would settle this promise on attempt one and never un-settle it: a
|
|
867
|
+
// retryable 429 would be reported as a dead turn even though the engine's
|
|
868
|
+
// own retry went on to succeed, and this method would throw while that
|
|
869
|
+
// retried — and billed — turn kept running in the background with its
|
|
870
|
+
// output discarded, with the fallback generate() billed on top. A
|
|
871
|
+
// terminal failure is taken from the turn's settled outcome instead,
|
|
872
|
+
// which by definition is reached only after the engine has stopped
|
|
873
|
+
// retrying.
|
|
874
|
+
let firstStepSucceeded = false;
|
|
875
|
+
let firstStepSent;
|
|
876
|
+
const firstStep = new Promise((resolve) => {
|
|
877
|
+
firstStepSent = () => {
|
|
878
|
+
firstStepSucceeded = true;
|
|
879
|
+
resolve();
|
|
1348
880
|
};
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
return {
|
|
1357
|
-
async *[Symbol.asyncIterator]() {
|
|
1358
|
-
const reader = stream.getReader();
|
|
1359
|
-
try {
|
|
1360
|
-
while (true) {
|
|
1361
|
-
const { done, value } = await reader.read();
|
|
1362
|
-
if (done) {
|
|
1363
|
-
break;
|
|
1364
|
-
}
|
|
1365
|
-
yield value;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
finally {
|
|
1369
|
-
reader.releaseLock();
|
|
1370
|
-
}
|
|
881
|
+
});
|
|
882
|
+
const adapter = {
|
|
883
|
+
...baseAdapter,
|
|
884
|
+
executeStep: async (request, channel, signal) => {
|
|
885
|
+
const stepResult = await baseAdapter.executeStep(request, channel, signal);
|
|
886
|
+
firstStepSent();
|
|
887
|
+
return stepResult;
|
|
1371
888
|
},
|
|
1372
889
|
};
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
//
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
890
|
+
streamSpan.addEvent("stream.api_call", {
|
|
891
|
+
"bedrock.tool_count": toolConfig?.tools?.length ?? 0,
|
|
892
|
+
});
|
|
893
|
+
const { stream, resultPromise } = runAgenticLoop(adapter, this.conversationHistory, {
|
|
894
|
+
tools: this.toEngineTools(tools),
|
|
895
|
+
abortSignal: options.abortSignal,
|
|
896
|
+
});
|
|
897
|
+
// The stream surfaces the same failure, so this settled view exists only
|
|
898
|
+
// so the turn's outcome can be read without a second unhandled rejection.
|
|
899
|
+
const settled = resultPromise.then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error }));
|
|
900
|
+
// A turn that ends without ever completing a step — an abort before the
|
|
901
|
+
// first request, or a failure the engine gave up retrying — would leave
|
|
902
|
+
// `firstStep` pending forever, so the settled outcome releases the wait
|
|
903
|
+
// too.
|
|
904
|
+
await Promise.race([firstStep, settled]);
|
|
905
|
+
if (!firstStepSucceeded) {
|
|
906
|
+
// The turn ended before any send succeeded. Surface its error here so
|
|
907
|
+
// executeStream's permission fallback still sees it synchronously.
|
|
908
|
+
const outcome = await settled;
|
|
909
|
+
if (outcome.error) {
|
|
910
|
+
throw outcome.error;
|
|
911
|
+
}
|
|
1393
912
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
modelId: this.modelName || this.getDefaultModel(),
|
|
1400
|
-
messages: convertedMessages,
|
|
1401
|
-
system: [
|
|
1402
|
-
{
|
|
1403
|
-
text: options.systemPrompt ||
|
|
1404
|
-
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
|
|
1405
|
-
},
|
|
1406
|
-
],
|
|
1407
|
-
inferenceConfig: {
|
|
1408
|
-
maxTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
1409
|
-
...(streamSampling.temperature !== undefined && {
|
|
1410
|
-
temperature: streamSampling.temperature,
|
|
1411
|
-
}),
|
|
1412
|
-
},
|
|
913
|
+
const streamEmitter = this.neurolink?.getEventEmitter();
|
|
914
|
+
const self = this;
|
|
915
|
+
const metadata = {
|
|
916
|
+
startTime,
|
|
917
|
+
streamId: `bedrock-${Date.now()}`,
|
|
1413
918
|
};
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
logger.debug(`[AmazonBedrockProvider] Calling Bedrock streaming with ${this.conversationHistory.length} messages`);
|
|
1418
|
-
// DEBUG: Log exact conversation structure being sent to Bedrock
|
|
1419
|
-
logger.debug(`[AmazonBedrockProvider] DEBUG - Conversation structure:`);
|
|
1420
|
-
this.conversationHistory.forEach((msg, index) => {
|
|
1421
|
-
logger.debug(` Message ${index} (${msg.role}): ${msg.content.length} content items`);
|
|
1422
|
-
msg.content.forEach((item, itemIndex) => {
|
|
1423
|
-
const keys = Object.keys(item);
|
|
1424
|
-
logger.debug(` Content ${itemIndex}: ${keys.join(", ")}`);
|
|
1425
|
-
});
|
|
1426
|
-
});
|
|
1427
|
-
return commandInput;
|
|
1428
|
-
}
|
|
1429
|
-
async processStreamResponse(commandInput, controller) {
|
|
1430
|
-
const command = new ConverseStreamCommand(commandInput);
|
|
1431
|
-
logger.debug("[Observability] Bedrock streaming API request (continuation)", {
|
|
1432
|
-
model: commandInput.modelId,
|
|
1433
|
-
messageCount: commandInput.messages?.length || 0,
|
|
919
|
+
let resolveAnalytics;
|
|
920
|
+
const analyticsPromise = new Promise((resolve) => {
|
|
921
|
+
resolveAnalytics = resolve;
|
|
1434
922
|
});
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
923
|
+
const usageFromOutcome = (usage) => {
|
|
924
|
+
const input = usage?.inputTokens ?? 0;
|
|
925
|
+
const output = usage?.outputTokens ?? 0;
|
|
926
|
+
const cacheRead = usage?.cacheReadTokens ?? 0;
|
|
927
|
+
const cacheWrite = usage?.cacheWriteTokens ?? 0;
|
|
928
|
+
return {
|
|
929
|
+
input,
|
|
930
|
+
output,
|
|
931
|
+
// Cache reads/writes are billed tokens reported separately from
|
|
932
|
+
// inputTokens — the total must include them.
|
|
933
|
+
total: input + cacheRead + cacheWrite + output,
|
|
934
|
+
...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
|
|
935
|
+
...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
|
|
936
|
+
};
|
|
937
|
+
};
|
|
938
|
+
// Bind analytics to the turn ending, not to the consumer draining the
|
|
939
|
+
// stream. A caller that awaits `result.analytics` without iterating
|
|
940
|
+
// `result.stream` would otherwise wait forever, because the generator
|
|
941
|
+
// body — and its finally block — never runs. Resolving twice is
|
|
942
|
+
// harmless; the first call wins.
|
|
943
|
+
void settled.then((outcome) => {
|
|
944
|
+
resolveAnalytics(createAnalytics(this.providerName, this.modelName || this.getDefaultModel(), { usage: usageFromOutcome(outcome.result?.usage) }, Date.now() - startTime, {
|
|
945
|
+
requestId: `bedrock-stream-${Date.now()}`,
|
|
946
|
+
streamingMode: true,
|
|
947
|
+
}));
|
|
1440
948
|
});
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
let currentText = "";
|
|
1447
|
-
let streamUsage;
|
|
1448
|
-
// Process streaming chunks
|
|
1449
|
-
for await (const chunk of response.stream) {
|
|
1450
|
-
if (chunk.contentBlockStart) {
|
|
1451
|
-
// Starting a new content block
|
|
1452
|
-
currentMessageContent.push({});
|
|
1453
|
-
}
|
|
1454
|
-
if (chunk.contentBlockDelta?.delta?.text) {
|
|
1455
|
-
// Text delta - stream it to user
|
|
1456
|
-
const textDelta = chunk.contentBlockDelta.delta.text;
|
|
1457
|
-
currentText += textDelta;
|
|
1458
|
-
controller.enqueue({
|
|
1459
|
-
content: textDelta,
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
if (chunk.contentBlockStart?.start?.toolUse) {
|
|
1463
|
-
// Tool use block starting - initialize tool information
|
|
1464
|
-
const currentBlock = currentMessageContent[currentMessageContent.length - 1];
|
|
1465
|
-
currentBlock.toolUse = {
|
|
1466
|
-
name: chunk.contentBlockStart.start.toolUse.name || "",
|
|
1467
|
-
input: {}, // Initialize empty - will be populated by delta chunks
|
|
1468
|
-
toolUseId: chunk.contentBlockStart.start.toolUse.toolUseId ||
|
|
1469
|
-
`tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
if (chunk.contentBlockDelta?.delta?.toolUse) {
|
|
1473
|
-
// Tool use delta - accumulate tool information
|
|
1474
|
-
const currentBlock = currentMessageContent[currentMessageContent.length - 1];
|
|
1475
|
-
if (!currentBlock.toolUse) {
|
|
1476
|
-
currentBlock.toolUse = {
|
|
1477
|
-
name: "",
|
|
1478
|
-
input: {},
|
|
1479
|
-
toolUseId: `tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
1480
|
-
};
|
|
949
|
+
const wrappedStreamIterable = {
|
|
950
|
+
async *[Symbol.asyncIterator]() {
|
|
951
|
+
let streamErrored = false;
|
|
952
|
+
try {
|
|
953
|
+
yield* stream;
|
|
1481
954
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (chunk.contentBlockDelta.delta.toolUse.input) {
|
|
1486
|
-
const deltaInput = chunk.contentBlockDelta.delta.toolUse.input;
|
|
1487
|
-
if (typeof deltaInput === "string") {
|
|
1488
|
-
currentBlock._inputBuffer =
|
|
1489
|
-
(currentBlock._inputBuffer || "") + deltaInput;
|
|
1490
|
-
}
|
|
1491
|
-
else if (deltaInput &&
|
|
1492
|
-
typeof deltaInput === "object" &&
|
|
1493
|
-
!Array.isArray(deltaInput)) {
|
|
1494
|
-
// Some SDK versions may deliver pre-parsed objects; merge directly
|
|
1495
|
-
const currentInput = currentBlock.toolUse.input || {};
|
|
1496
|
-
currentBlock.toolUse.input = {
|
|
1497
|
-
...currentInput,
|
|
1498
|
-
...deltaInput,
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
streamErrored = true;
|
|
957
|
+
throw error;
|
|
1501
958
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
// Parse accumulated JSON input buffer for tool-use blocks
|
|
1507
|
-
if (currentBlock?.toolUse && currentBlock._inputBuffer) {
|
|
1508
|
-
try {
|
|
1509
|
-
currentBlock.toolUse.input = JSON.parse(currentBlock._inputBuffer);
|
|
959
|
+
finally {
|
|
960
|
+
const outcome = await settled;
|
|
961
|
+
if (outcome.error) {
|
|
962
|
+
streamErrored = true;
|
|
1510
963
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
964
|
+
const aggregatedUsage = usageFromOutcome(outcome.result?.usage);
|
|
965
|
+
if (outcome.result) {
|
|
966
|
+
self.conversationHistory = outcome.result.conversation;
|
|
967
|
+
metadata.finishReason = outcome.result.finishReason;
|
|
968
|
+
metadata.rawFinishReason = outcome.result.rawStopReason;
|
|
969
|
+
streamSpan.setAttribute("gen_ai.response.stop_reason", outcome.result.rawStopReason ?? "unknown");
|
|
970
|
+
}
|
|
971
|
+
// Analytics is resolved off `settled` above, so it lands whether or
|
|
972
|
+
// not anyone drains the stream.
|
|
973
|
+
// Bedrock bypasses the Vercel AI SDK, so experimental_telemetry is
|
|
974
|
+
// never injected and generation:end is emitted by hand for
|
|
975
|
+
// Pipeline B (Langfuse).
|
|
976
|
+
if (streamEmitter) {
|
|
977
|
+
streamEmitter.emit("generation:end", {
|
|
978
|
+
provider: self.providerName,
|
|
979
|
+
responseTime: Date.now() - startTime,
|
|
980
|
+
timestamp: Date.now(),
|
|
981
|
+
result: {
|
|
982
|
+
content: "",
|
|
983
|
+
usage: aggregatedUsage,
|
|
984
|
+
model: self.modelName || self.getDefaultModel(),
|
|
985
|
+
provider: self.providerName,
|
|
986
|
+
finishReason: streamErrored
|
|
987
|
+
? "error"
|
|
988
|
+
: outcome.result?.rawStopReason,
|
|
989
|
+
},
|
|
990
|
+
success: !streamErrored,
|
|
991
|
+
});
|
|
1513
992
|
}
|
|
1514
|
-
delete currentBlock._inputBuffer;
|
|
1515
|
-
}
|
|
1516
|
-
if (currentText && currentBlock && !currentBlock.toolUse) {
|
|
1517
|
-
// Only add text to blocks that don't have toolUse
|
|
1518
|
-
currentBlock.text = currentText;
|
|
1519
993
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const output = chunk.metadata.usage.outputTokens ?? 0;
|
|
1532
|
-
const cacheRead = chunk.metadata.usage.cacheReadInputTokens ?? 0;
|
|
1533
|
-
const cacheWrite = chunk.metadata.usage.cacheWriteInputTokens ?? 0;
|
|
1534
|
-
streamUsage = {
|
|
1535
|
-
input,
|
|
1536
|
-
output,
|
|
1537
|
-
// Computed rather than trusting totalTokens: inputTokens excludes
|
|
1538
|
-
// cache reads/writes (additive convention), and the total must
|
|
1539
|
-
// count every billed component.
|
|
1540
|
-
total: input + cacheRead + cacheWrite + output,
|
|
1541
|
-
...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
|
|
1542
|
-
...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
|
|
1543
|
-
};
|
|
1544
|
-
// Stream is effectively complete after metadata chunk
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
// Add assistant message to conversation history
|
|
1549
|
-
const assistantMessage = {
|
|
1550
|
-
role: "assistant",
|
|
1551
|
-
content: currentMessageContent,
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
stream: wrappedStreamIterable,
|
|
998
|
+
// No usage key here on purpose: the real aggregate resolves through
|
|
999
|
+
// `analytics` after the stream drains. A literal zero object is truthy
|
|
1000
|
+
// and would block every downstream usage fallback.
|
|
1001
|
+
model: this.modelName || this.getDefaultModel(),
|
|
1002
|
+
provider: this.getProviderName(),
|
|
1003
|
+
analytics: analyticsPromise,
|
|
1004
|
+
metadata,
|
|
1552
1005
|
};
|
|
1553
|
-
this.conversationHistory.push(assistantMessage);
|
|
1554
|
-
return { stopReason, assistantMessage, usage: streamUsage };
|
|
1555
|
-
}
|
|
1556
|
-
async handleStreamStopReason(stopReason, assistantMessage, controller, options) {
|
|
1557
|
-
if (stopReason === "end_turn" || stopReason === "stop_sequence") {
|
|
1558
|
-
// Conversation completed
|
|
1559
|
-
controller.close();
|
|
1560
|
-
return false;
|
|
1561
|
-
}
|
|
1562
|
-
else if (stopReason === "tool_use") {
|
|
1563
|
-
logger.debug(`[AmazonBedrockProvider] Tool use detected in streaming - executing tools`);
|
|
1564
|
-
await this.executeStreamTools(assistantMessage.content, options);
|
|
1565
|
-
return true; // Continue conversation loop
|
|
1566
|
-
}
|
|
1567
|
-
else if (stopReason === "max_tokens") {
|
|
1568
|
-
// Max tokens reached — close the stream rather than continuing,
|
|
1569
|
-
// since the model hit the configured limit.
|
|
1570
|
-
controller.close();
|
|
1571
|
-
return false;
|
|
1572
|
-
}
|
|
1573
|
-
else {
|
|
1574
|
-
// Unknown stop reason - end conversation
|
|
1575
|
-
controller.close();
|
|
1576
|
-
return false;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
async executeStreamTools(messageContent, options) {
|
|
1580
|
-
// Execute all tool uses in the message - ensure 1:1 mapping like Bedrock-MCP-Connector
|
|
1581
|
-
const toolResults = [];
|
|
1582
|
-
let toolUseCount = 0;
|
|
1583
|
-
// Track tool calls and results for storage (similar to Vertex onStepFinish)
|
|
1584
|
-
const toolCalls = [];
|
|
1585
|
-
const toolResultsForStorage = [];
|
|
1586
|
-
// Count toolUse blocks first to ensure 1:1 mapping
|
|
1587
|
-
for (const contentItem of messageContent) {
|
|
1588
|
-
if (contentItem.toolUse) {
|
|
1589
|
-
toolUseCount++;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
logger.debug(`[AmazonBedrockProvider] Found ${toolUseCount} toolUse blocks in assistant message`);
|
|
1593
|
-
for (const contentItem of messageContent) {
|
|
1594
|
-
if (contentItem.toolUse) {
|
|
1595
|
-
logger.debug(`[AmazonBedrockProvider] Executing tool: ${contentItem.toolUse.name}`);
|
|
1596
|
-
// Track tool call
|
|
1597
|
-
toolCalls.push({
|
|
1598
|
-
type: "tool-call",
|
|
1599
|
-
toolCallId: contentItem.toolUse.toolUseId,
|
|
1600
|
-
toolName: contentItem.toolUse.name,
|
|
1601
|
-
args: contentItem.toolUse.input || {},
|
|
1602
|
-
});
|
|
1603
|
-
try {
|
|
1604
|
-
const toolResult = await this.executeSingleTool(contentItem.toolUse.name, contentItem.toolUse.input || {}, contentItem.toolUse.toolUseId);
|
|
1605
|
-
logger.debug(`[AmazonBedrockProvider] Tool execution successful: ${contentItem.toolUse.name}`);
|
|
1606
|
-
// Track tool result for storage
|
|
1607
|
-
toolResultsForStorage.push({
|
|
1608
|
-
type: "tool-result",
|
|
1609
|
-
toolCallId: contentItem.toolUse.toolUseId,
|
|
1610
|
-
toolName: contentItem.toolUse.name,
|
|
1611
|
-
result: toolResult,
|
|
1612
|
-
});
|
|
1613
|
-
// Ensure exact structure matching Bedrock-MCP-Connector
|
|
1614
|
-
toolResults.push({
|
|
1615
|
-
toolResult: {
|
|
1616
|
-
toolUseId: contentItem.toolUse.toolUseId,
|
|
1617
|
-
content: [{ text: String(toolResult) }],
|
|
1618
|
-
status: "success",
|
|
1619
|
-
},
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
catch (error) {
|
|
1623
|
-
logger.error(`[AmazonBedrockProvider] Tool execution failed: ${contentItem.toolUse.name}`, error);
|
|
1624
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1625
|
-
// Track failed tool result
|
|
1626
|
-
toolResultsForStorage.push({
|
|
1627
|
-
type: "tool-result",
|
|
1628
|
-
toolCallId: contentItem.toolUse.toolUseId,
|
|
1629
|
-
toolName: contentItem.toolUse.name,
|
|
1630
|
-
result: { error: errorMessage },
|
|
1631
|
-
});
|
|
1632
|
-
toolResults.push({
|
|
1633
|
-
toolResult: {
|
|
1634
|
-
toolUseId: contentItem.toolUse.toolUseId,
|
|
1635
|
-
content: [
|
|
1636
|
-
{
|
|
1637
|
-
text: `Error executing tool ${contentItem.toolUse.name}: ${errorMessage}`,
|
|
1638
|
-
},
|
|
1639
|
-
],
|
|
1640
|
-
status: "error",
|
|
1641
|
-
},
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
logger.debug(`[AmazonBedrockProvider] Created ${toolResults.length} toolResult blocks for ${toolUseCount} toolUse blocks`);
|
|
1647
|
-
// Validate 1:1 mapping before adding to conversation
|
|
1648
|
-
if (toolResults.length !== toolUseCount) {
|
|
1649
|
-
logger.error(`[AmazonBedrockProvider] Mismatch: ${toolResults.length} toolResults vs ${toolUseCount} toolUse blocks`);
|
|
1650
|
-
throw new Error(`Tool mapping mismatch: ${toolResults.length} toolResults for ${toolUseCount} toolUse blocks`);
|
|
1651
|
-
}
|
|
1652
|
-
// Add tool results as user message - exact structure like Bedrock-MCP-Connector
|
|
1653
|
-
if (toolResults.length > 0) {
|
|
1654
|
-
const userMessageWithToolResults = {
|
|
1655
|
-
role: "user",
|
|
1656
|
-
content: toolResults,
|
|
1657
|
-
};
|
|
1658
|
-
this.conversationHistory.push(userMessageWithToolResults);
|
|
1659
|
-
logger.debug(`[AmazonBedrockProvider] Added ${toolResults.length} tool results to conversation (1:1 mapping validated)`);
|
|
1660
|
-
// Emit tool:end for each completed tool result so Pipeline B
|
|
1661
|
-
// captures telemetry for Bedrock-driven tool calls (gap S2).
|
|
1662
|
-
emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), toolResultsForStorage.map((tr) => {
|
|
1663
|
-
const hasError = tr.result && typeof tr.result === "object" && "error" in tr.result;
|
|
1664
|
-
return {
|
|
1665
|
-
toolName: tr.toolName,
|
|
1666
|
-
result: tr.result,
|
|
1667
|
-
error: hasError
|
|
1668
|
-
? String(tr.result.error)
|
|
1669
|
-
: undefined,
|
|
1670
|
-
};
|
|
1671
|
-
}));
|
|
1672
|
-
// Store tool execution for analytics and debugging (similar to Vertex onStepFinish)
|
|
1673
|
-
this.handleToolExecutionStorage(toolCalls, toolResultsForStorage, options, new Date()).catch((error) => {
|
|
1674
|
-
logger.warn("[AmazonBedrockProvider] Failed to store tool executions", {
|
|
1675
|
-
provider: this.providerName,
|
|
1676
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1677
|
-
});
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
1006
|
}
|
|
1681
1007
|
/**
|
|
1682
1008
|
* Health check for Amazon Bedrock service
|