@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and
974 lines (973 loc) • 48.5 kB
JavaScript
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand, } from "@aws-sdk/client-bedrock-runtime";
import { BedrockClient, ListFoundationModelsCommand, } from "@aws-sdk/client-bedrock";
import { BaseProvider } from "../core/baseProvider.js";
import { logger } from "../utils/logger.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export class AmazonBedrockProvider extends BaseProvider {
bedrockClient;
conversationHistory = [];
constructor(modelName, neurolink) {
super(modelName, "bedrock", neurolink);
logger.debug("[AmazonBedrockProvider] Starting constructor with extensive logging for debugging");
// Log environment variables for debugging
logger.debug(`[AmazonBedrockProvider] Environment check: AWS_REGION=${process.env.AWS_REGION || "undefined"}, AWS_ACCESS_KEY_ID=${process.env.AWS_ACCESS_KEY_ID ? "SET" : "undefined"}, AWS_SECRET_ACCESS_KEY=${process.env.AWS_SECRET_ACCESS_KEY ? "SET" : "undefined"}`);
try {
// Create BedrockRuntimeClient with clean configuration like working Bedrock-MCP-Connector
// Absolutely no proxy interference - let AWS SDK handle everything natively
logger.debug("[AmazonBedrockProvider] Creating BedrockRuntimeClient with clean configuration");
this.bedrockClient = new BedrockRuntimeClient({
region: process.env.AWS_REGION || "us-east-1",
// Clean configuration - AWS SDK will handle credentials via:
// 1. IAM roles (preferred in production)
// 2. Environment variables
// 3. AWS config files
// 4. Instance metadata
});
logger.debug(`[AmazonBedrockProvider] Successfully created BedrockRuntimeClient with model: ${this.modelName}, region: ${process.env.AWS_REGION || "us-east-1"}`);
// Immediate health check to catch credential issues early
this.performInitialHealthCheck();
}
catch (error) {
logger.error(`[AmazonBedrockProvider] CRITICAL: Failed to initialize BedrockRuntimeClient:`, error);
throw error;
}
}
/**
* Perform initial health check to catch credential/connectivity issues early
* This prevents the health check failure we saw in production logs
*/
async performInitialHealthCheck() {
const bedrockClient = new BedrockClient({
region: process.env.AWS_REGION || "us-east-1",
});
try {
logger.debug("[AmazonBedrockProvider] Starting initial health check to validate credentials and connectivity");
// Try to list foundation models as a lightweight health check
const command = new ListFoundationModelsCommand({});
const startTime = Date.now();
await bedrockClient.send(command);
const responseTime = Date.now() - startTime;
logger.debug(`[AmazonBedrockProvider] Health check PASSED - credentials valid, connectivity good, responseTime: ${responseTime}ms`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`[AmazonBedrockProvider] Health check FAILED - this will cause production failures:`, {
error: errorMessage,
errorType: error instanceof Error ? error.constructor.name : "Unknown",
region: process.env.AWS_REGION || "us-east-1",
hasAccessKey: !!process.env.AWS_ACCESS_KEY_ID,
hasSecretKey: !!process.env.AWS_SECRET_ACCESS_KEY,
});
// Don't throw here - let the actual usage fail with better context
}
finally {
try {
bedrockClient.destroy();
}
catch {
// Ignore destroy errors during cleanup
}
}
}
// Not using AI SDK approach in conversation management
getAISDKModel() {
throw new Error("AmazonBedrockProvider does not use AI SDK models");
}
getProviderName() {
return "bedrock";
}
getDefaultModel() {
return (process.env.BEDROCK_MODEL || "anthropic.claude-3-sonnet-20240229-v1:0");
}
// Override the main generate method to implement conversation management
async generate(optionsOrPrompt) {
logger.debug("[AmazonBedrockProvider] generate() called with conversation management");
const options = typeof optionsOrPrompt === "string"
? { prompt: optionsOrPrompt }
: optionsOrPrompt;
// Clear conversation history for new generation
this.conversationHistory = [];
// Add user message to conversation
const userMessage = {
role: "user",
content: [{ text: options.prompt }],
};
this.conversationHistory.push(userMessage);
logger.debug(`[AmazonBedrockProvider] Starting conversation with prompt: ${options.prompt}`);
// Start conversation loop and return enhanced result
const text = await this.conversationLoop(options);
return {
content: text, // CLI expects 'content' not 'text'
usage: { total: 0, input: 0, output: 0 },
model: this.modelName || this.getDefaultModel(),
provider: this.getProviderName(),
};
}
async conversationLoop(options) {
const maxIterations = 10; // Prevent infinite loops
let iteration = 0;
while (iteration < maxIterations) {
iteration++;
logger.debug(`[AmazonBedrockProvider] Conversation iteration ${iteration}`);
try {
logger.debug(`[AmazonBedrockProvider] About to call Bedrock API`);
const response = await this.callBedrock(options);
logger.debug(`[AmazonBedrockProvider] Received Bedrock response`, JSON.stringify(response, null, 2));
const result = await this.handleBedrockResponse(response);
logger.debug(`[AmazonBedrockProvider] Handle response result:`, result);
if (result.shouldContinue) {
logger.debug(`[AmazonBedrockProvider] Continuing conversation loop...`);
continue;
}
else {
logger.debug(`[AmazonBedrockProvider] Conversation completed with final text`);
logger.debug(`[AmazonBedrockProvider] Returning final text: "${result.text}"`);
return result.text || "";
}
}
catch (error) {
logger.error(`[AmazonBedrockProvider] Error in conversation loop:`, error);
throw this.handleProviderError(error);
}
}
throw new Error("Conversation loop exceeded maximum iterations");
}
async callBedrock(options) {
const startTime = Date.now();
logger.info(`🚀 [AmazonBedrockProvider] Starting Bedrock API call at ${new Date().toISOString()}`);
try {
// Pre-call validation and logging
const region = typeof this.bedrockClient.config.region === "function"
? await this.bedrockClient.config.region()
: this.bedrockClient.config.region;
logger.info(`🔧 [AmazonBedrockProvider] Client region: ${region}`);
logger.info(`🔧 [AmazonBedrockProvider] Model: ${this.modelName || this.getDefaultModel()}`);
logger.info(`🔧 [AmazonBedrockProvider] Conversation history length: ${this.conversationHistory.length}`);
// Get all available tools
const aiTools = await this.getAllTools();
const allTools = this.convertAISDKToolsToToolDefinitions(aiTools);
const toolConfig = this.formatToolsForBedrock(allTools);
const commandInput = {
modelId: this.modelName || this.getDefaultModel(),
messages: this.convertToAWSMessages(this.conversationHistory),
system: [
{
text: options.systemPrompt ||
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
},
],
inferenceConfig: {
maxTokens: options.maxTokens || 4096,
temperature: options.temperature || 0.7,
},
};
if (toolConfig) {
commandInput.toolConfig = toolConfig;
logger.info(`🛠️ [AmazonBedrockProvider] Tools configured: ${toolConfig.tools?.length || 0}`);
}
// Log command details for debugging
logger.info(`📋 [AmazonBedrockProvider] Command input summary:`);
logger.info(` - Model ID: ${commandInput.modelId}`);
logger.info(` - Messages count: ${commandInput.messages?.length || 0}`);
logger.info(` - System prompts: ${commandInput.system?.length || 0}`);
logger.info(` - Max tokens: ${commandInput.inferenceConfig?.maxTokens}`);
logger.info(` - Temperature: ${commandInput.inferenceConfig?.temperature}`);
logger.debug(`[AmazonBedrockProvider] Calling Bedrock with ${this.conversationHistory.length} messages and ${toolConfig?.tools?.length || 0} tools`);
// Create command and attempt API call
const command = new ConverseCommand(commandInput);
logger.info(`⏳ [AmazonBedrockProvider] Sending ConverseCommand to Bedrock...`);
const apiCallStartTime = Date.now();
const response = await this.bedrockClient.send(command);
const apiCallDuration = Date.now() - apiCallStartTime;
logger.info(`✅ [AmazonBedrockProvider] Bedrock API call successful!`);
logger.info(`⏱️ [AmazonBedrockProvider] API call duration: ${apiCallDuration}ms`);
logger.info(`📊 [AmazonBedrockProvider] Response metadata:`);
logger.info(` - Stop reason: ${response.stopReason}`);
logger.info(` - Usage tokens: ${JSON.stringify(response.usage || {})}`);
logger.info(` - Metrics: ${JSON.stringify(response.metrics || {})}`);
const totalDuration = Date.now() - startTime;
logger.info(`🎯 [AmazonBedrockProvider] Total callBedrock duration: ${totalDuration}ms`);
return response;
}
catch (error) {
const errorDuration = Date.now() - startTime;
logger.error(`❌ [AmazonBedrockProvider] Bedrock API call failed after ${errorDuration}ms`);
logger.error(`🔍 [AmazonBedrockProvider] Error details:`);
if (error instanceof Error) {
logger.error(` - Error name: ${error.name}`);
logger.error(` - Error message: ${error.message}`);
logger.error(` - Error stack: ${error.stack}`);
}
// Log AWS SDK specific error details
if (error && typeof error === "object") {
const awsError = error;
if (awsError.$metadata && typeof awsError.$metadata === "object") {
const metadata = awsError.$metadata;
logger.error(`🏭 [AmazonBedrockProvider] AWS SDK metadata:`);
logger.error(` - HTTP status: ${metadata.httpStatusCode}`);
logger.error(` - Request ID: ${metadata.requestId}`);
logger.error(` - Attempts: ${metadata.attempts}`);
logger.error(` - Total retry delay: ${metadata.totalRetryDelay}`);
}
if (awsError.Code) {
logger.error(` - AWS Error Code: ${awsError.Code}`);
}
if (awsError.Type) {
logger.error(` - AWS Error Type: ${awsError.Type}`);
}
if (awsError.Fault) {
logger.error(` - AWS Fault: ${awsError.Fault}`);
}
}
// Log environment details for debugging
logger.error(`🌍 [AmazonBedrockProvider] Environment diagnostics:`);
logger.error(` - AWS_REGION: ${process.env.AWS_REGION || "not set"}`);
logger.error(` - AWS_PROFILE: ${process.env.AWS_PROFILE || "not set"}`);
logger.error(` - AWS_ACCESS_KEY_ID: ${process.env.AWS_ACCESS_KEY_ID ? "set" : "not set"}`);
logger.error(` - AWS_SECRET_ACCESS_KEY: ${process.env.AWS_SECRET_ACCESS_KEY ? "set" : "not set"}`);
logger.error(` - AWS_SESSION_TOKEN: ${process.env.AWS_SESSION_TOKEN ? "set" : "not set"}`);
throw error;
}
}
async handleBedrockResponse(response) {
logger.debug(`[AmazonBedrockProvider] Received response with stopReason: ${response.stopReason}`);
if (!response.output || !response.output.message) {
throw new Error("Invalid response structure from Bedrock API");
}
const assistantMessage = response.output.message;
const stopReason = response.stopReason;
// Add assistant message to conversation history
const bedrockAssistantMessage = {
role: "assistant",
content: (assistantMessage.content || []).map((item) => {
const bedrockItem = {};
if ("text" in item && item.text) {
bedrockItem.text = item.text;
}
if ("toolUse" in item && item.toolUse) {
bedrockItem.toolUse = {
toolUseId: item.toolUse.toolUseId || "",
name: item.toolUse.name || "",
input: item.toolUse.input || {},
};
}
if ("toolResult" in item && item.toolResult) {
bedrockItem.toolResult = {
toolUseId: item.toolResult.toolUseId || "",
content: (item.toolResult.content || []).map((c) => ({
text: typeof c === "object" && "text" in c
? c.text || ""
: "",
})),
status: item.toolResult.status || "unknown",
};
}
return bedrockItem;
}),
};
this.conversationHistory.push(bedrockAssistantMessage);
if (stopReason === "end_turn" || stopReason === "stop_sequence") {
// Extract text from assistant message
const textContent = bedrockAssistantMessage.content
.filter((item) => item.text)
.map((item) => item.text)
.join(" ");
return { shouldContinue: false, text: textContent };
}
else if (stopReason === "tool_use") {
logger.debug(`[AmazonBedrockProvider] Tool use detected - executing tools immediately`);
// Execute all tool uses in the message
const toolResults = [];
for (const contentItem of bedrockAssistantMessage.content) {
if (contentItem.toolUse) {
logger.debug(`[AmazonBedrockProvider] Executing tool: ${contentItem.toolUse.name}`);
try {
// Execute tool using BaseProvider's tool execution
logger.debug(`[AmazonBedrockProvider] Debug toolUse.input:`, JSON.stringify(contentItem.toolUse.input, null, 2));
const toolResult = await this.executeSingleTool(contentItem.toolUse.name, contentItem.toolUse.input || {}, contentItem.toolUse.toolUseId);
logger.debug(`[AmazonBedrockProvider] Tool execution successful: ${contentItem.toolUse.name}`);
toolResults.push({
toolResult: {
toolUseId: contentItem.toolUse.toolUseId,
content: [{ text: String(toolResult) }],
status: "success",
},
});
}
catch (error) {
logger.error(`[AmazonBedrockProvider] Tool execution failed: ${contentItem.toolUse.name}`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
// Still create toolResult for failed tools to maintain 1:1 mapping with toolUse blocks
toolResults.push({
toolResult: {
toolUseId: contentItem.toolUse.toolUseId,
content: [
{
text: `Error executing tool ${contentItem.toolUse.name}: ${errorMessage}`,
},
],
status: "error",
},
});
}
}
}
// Add tool results as user message
if (toolResults.length > 0) {
const userMessageWithToolResults = {
role: "user",
content: toolResults,
};
this.conversationHistory.push(userMessageWithToolResults);
logger.debug(`[AmazonBedrockProvider] Added ${toolResults.length} tool results to conversation`);
}
return { shouldContinue: true };
}
else if (stopReason === "max_tokens") {
// Handle max tokens by continuing conversation
const userMessage = {
role: "user",
content: [{ text: "Please continue." }],
};
this.conversationHistory.push(userMessage);
return { shouldContinue: true };
}
else {
logger.warn(`[AmazonBedrockProvider] Unrecognized stop reason "${stopReason}", ending conversation.`);
return { shouldContinue: false, text: "" };
}
}
convertToAWSMessages(bedrockMessages) {
return bedrockMessages.map((msg) => ({
role: msg.role,
content: msg.content.map((item) => {
if (item.text) {
return {
text: item.text,
};
}
if (item.toolUse) {
return {
toolUse: {
toolUseId: item.toolUse.toolUseId,
name: item.toolUse.name,
input: item.toolUse.input,
},
};
}
if (item.toolResult) {
return {
toolResult: {
toolUseId: item.toolResult.toolUseId,
content: item.toolResult.content,
status: item.toolResult.status,
},
};
}
return { text: "" };
}),
}));
}
async executeSingleTool(toolName, args, _toolUseId) {
logger.debug(`[AmazonBedrockProvider] Executing single tool: ${toolName}`, {
args,
});
try {
// Use BaseProvider's tool execution mechanism
const aiTools = await this.getAllTools();
const tools = this.convertAISDKToolsToToolDefinitions(aiTools);
if (!tools[toolName]) {
throw new Error(`Tool not found: ${toolName}`);
}
const tool = tools[toolName];
if (!tool || !tool.execute) {
throw new Error(`Tool ${toolName} does not have execute method`);
}
// Apply robust parameter handling like Bedrock-MCP-Connector
// Bedrock toolUse.input already contains the correct parameter structure
const toolInput = args || {};
// Add default parameters for common tools that Claude might call without required params
if (toolName === "list_directory" && !toolInput.path) {
toolInput.path = ".";
logger.debug(`[AmazonBedrockProvider] Added default path '.' for list_directory tool`);
}
logger.debug(`[AmazonBedrockProvider] Tool input parameters:`, toolInput);
// Convert Record<string, unknown> to ToolArgs by filtering out non-JsonValue types
const toolArgs = {};
for (const [key, value] of Object.entries(toolInput)) {
// Only include values that are JsonValue compatible
if (value === null ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
(typeof value === "object" && value !== null)) {
toolArgs[key] = value;
}
}
const result = await tool.execute(toolArgs);
logger.debug(`[AmazonBedrockProvider] Tool execution result:`, {
toolName,
result,
});
// Handle ToolResult type
if (result && typeof result === "object" && "success" in result) {
if (result.success && result.data !== undefined) {
if (typeof result.data === "string") {
return result.data;
}
else if (typeof result.data === "object") {
return JSON.stringify(result.data, null, 2);
}
else {
return String(result.data);
}
}
else if (result.error) {
throw new Error(result.error.message || "Tool execution failed");
}
}
// Fallback for non-ToolResult return types
if (typeof result === "string") {
return result;
}
else if (typeof result === "object") {
return JSON.stringify(result, null, 2);
}
else {
return String(result);
}
}
catch (error) {
logger.error(`[AmazonBedrockProvider] Tool execution error:`, {
toolName,
error,
});
throw error;
}
}
convertAISDKToolsToToolDefinitions(aiTools) {
const result = {};
for (const [name, tool] of Object.entries(aiTools)) {
if ("description" in tool && tool.description) {
result[name] = {
description: tool.description,
parameters: "parameters" in tool ? tool.parameters : undefined,
execute: async (params) => {
if ("execute" in tool && tool.execute) {
const result = await tool.execute(params, {
toolCallId: `tool_${Date.now()}`,
messages: [],
});
return {
success: true,
data: result,
};
}
throw new Error(`Tool ${name} has no execute method`);
},
};
}
}
return result;
}
formatToolsForBedrock(tools) {
if (!tools || Object.keys(tools).length === 0) {
return null;
}
const bedrockTools = Object.entries(tools).map(([name, tool]) => {
// Handle Zod schema or plain object schema
let schema;
if (tool.parameters && typeof tool.parameters === "object") {
// Check if it's a Zod schema
if ("_def" in tool.parameters) {
// It's a Zod schema, convert to JSON schema
schema = zodToJsonSchema(tool.parameters);
}
else {
// It's already a plain object schema
schema = tool.parameters;
}
}
else {
schema = {
type: "object",
properties: {},
required: [],
};
}
// Ensure the schema always has type: "object" at the root level
if (!schema.type || schema.type !== "object") {
schema = {
type: "object",
properties: schema.properties || {},
required: schema.required || [],
};
}
const toolSpec = {
name,
description: tool.description,
inputSchema: {
json: schema,
},
};
return {
toolSpec,
};
});
logger.debug(`[AmazonBedrockProvider] Formatted ${bedrockTools.length} tools for Bedrock`);
return { tools: bedrockTools };
}
// Bedrock-MCP-Connector compatibility
getBedrockClient() {
return this.bedrockClient;
}
async executeStream(options) {
logger.debug("🟢 [TRACE] executeStream ENTRY - starting streaming attempt");
logger.info("🚀 [AmazonBedrockProvider] Attempting real streaming with ConverseStreamCommand");
try {
logger.debug("🟢 [TRACE] executeStream TRY block - about to call streamingConversationLoop");
// CRITICAL FIX: Initialize conversation history like generate() does
// Clear conversation history for new streaming session
this.conversationHistory = [];
// Add user message to conversation - exactly like generate() does
const userMessage = {
role: "user",
content: [{ text: options.input.text }],
};
this.conversationHistory.push(userMessage);
logger.debug(`[AmazonBedrockProvider] Starting streaming conversation with prompt: ${options.input.text}`);
// Call the actual streaming implementation that already exists
logger.debug("🟢 [TRACE] executeStream - calling streamingConversationLoop NOW");
const result = await this.streamingConversationLoop(options);
logger.debug("🟢 [TRACE] executeStream - streamingConversationLoop SUCCESS, returning result");
return result;
}
catch (error) {
logger.debug("🔴 [TRACE] executeStream CATCH - error caught from streamingConversationLoop");
const errorObj = error;
// Check if error is related to streaming permissions
const isPermissionError = errorObj?.name ===
"AccessDeniedException" ||
errorObj?.name ===
"UnauthorizedOperation" ||
errorObj?.message?.includes("bedrock:InvokeModelWithResponseStream") ||
errorObj?.message?.includes("streaming") ||
errorObj?.message?.includes("ConverseStream");
logger.debug("🔴 [TRACE] executeStream CATCH - checking if permission error");
logger.debug(`🔴 [TRACE] executeStream CATCH - isPermissionError=${isPermissionError}`);
if (isPermissionError) {
logger.debug("🟡 [TRACE] executeStream CATCH - PERMISSION ERROR DETECTED, starting fallback");
logger.warn(`[AmazonBedrockProvider] Streaming permissions not available, falling back to generate method: ${errorObj.message}`);
// Fallback to generate method and convert to streaming format
const generateResult = await this.generate({
prompt: options.input.text,
});
if (!generateResult) {
throw new Error("Generate method returned null result");
}
// Convert generate result to streaming format
const stream = new ReadableStream({
start(controller) {
// Split the response into chunks for pseudo-streaming
const responseText = generateResult.content || "";
const chunks = responseText.split(" ");
chunks.forEach((word, _index) => {
controller.enqueue({ content: word + " " });
});
controller.enqueue({ content: "" });
controller.close();
},
});
// Convert ReadableStream to AsyncIterable like streamingConversationLoop does
const asyncIterable = {
async *[Symbol.asyncIterator]() {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
yield value;
}
}
finally {
reader.releaseLock();
}
},
};
return {
stream: asyncIterable,
usage: { total: 0, input: 0, output: 0 },
model: this.modelName || this.getDefaultModel(),
provider: this.getProviderName(),
metadata: {
fallback: true,
},
};
}
// Re-throw non-permission errors
throw error;
}
}
async streamingConversationLoop(options) {
logger.debug("🟦 [TRACE] streamingConversationLoop ENTRY");
const maxIterations = 10;
let iteration = 0;
// The REAL issue: ReadableStream errors don't bubble up to the caller
// So we need to make the first streaming call synchronously to test permissions
try {
logger.debug("🟦 [TRACE] streamingConversationLoop - testing first streaming call");
const commandInput = await this.prepareStreamCommand(options);
const command = new ConverseStreamCommand(commandInput);
const response = await this.bedrockClient.send(command);
logger.debug("🟦 [TRACE] streamingConversationLoop - first streaming call SUCCESS");
// Process the first response immediately to avoid waste
const stream = new ReadableStream({
start: async (controller) => {
logger.debug("🟦 [TRACE] streamingConversationLoop - ReadableStream start() called");
try {
// Process the first response we already have
if (response.stream) {
for await (const chunk of response.stream) {
if (chunk.contentBlockDelta?.delta?.text) {
controller.enqueue({
content: chunk.contentBlockDelta.delta.text,
});
}
if (chunk.messageStop) {
controller.close();
return;
}
}
}
// Continue with normal iterations if needed
while (iteration < maxIterations) {
iteration++;
logger.debug(`[AmazonBedrockProvider] Streaming iteration ${iteration}`);
const commandInput = await this.prepareStreamCommand(options);
const { stopReason, assistantMessage } = await this.processStreamResponse(commandInput, controller);
const shouldContinue = await this.handleStreamStopReason(stopReason, assistantMessage, controller);
if (!shouldContinue) {
break;
}
}
if (iteration >= maxIterations) {
controller.error(new Error("Streaming conversation exceeded maximum iterations"));
}
}
catch (error) {
logger.debug("🔴 [TRACE] streamingConversationLoop - CATCH block hit in ReadableStream");
controller.error(error);
}
},
});
return {
stream: this.convertToAsyncIterable(stream),
usage: { total: 0, input: 0, output: 0 },
model: this.modelName || this.getDefaultModel(),
provider: this.getProviderName(),
};
}
catch (error) {
logger.debug("🔴 [TRACE] streamingConversationLoop - first streaming call FAILED, throwing");
throw error; // This will be caught by executeStream
}
}
convertToAsyncIterable(stream) {
return {
async *[Symbol.asyncIterator]() {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
yield value;
}
}
finally {
reader.releaseLock();
}
},
};
}
async prepareStreamCommand(options) {
// CRITICAL DEBUG: Log conversation history before conversion
logger.info(`🔍 [AmazonBedrockProvider] BEFORE conversion - conversationHistory length: ${this.conversationHistory.length}`);
this.conversationHistory.forEach((msg, index) => {
logger.info(`🔍 [AmazonBedrockProvider] Message ${index}: role=${msg.role}, content=${JSON.stringify(msg.content)}`);
});
// Get all available tools
const aiTools = await this.getAllTools();
const allTools = this.convertAISDKToolsToToolDefinitions(aiTools);
const toolConfig = this.formatToolsForBedrock(allTools);
const convertedMessages = this.convertToAWSMessages(this.conversationHistory);
logger.info(`🔍 [AmazonBedrockProvider] AFTER conversion - messages length: ${convertedMessages.length}`);
convertedMessages.forEach((msg, index) => {
logger.info(`🔍 [AmazonBedrockProvider] Converted Message ${index}: role=${msg.role}, content=${JSON.stringify(msg.content)}`);
});
const commandInput = {
modelId: this.modelName || this.getDefaultModel(),
messages: convertedMessages,
system: [
{
text: options.systemPrompt ||
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
},
],
inferenceConfig: {
maxTokens: options.maxTokens || 4096,
temperature: options.temperature || 0.7,
},
};
if (toolConfig) {
commandInput.toolConfig = toolConfig;
}
logger.debug(`[AmazonBedrockProvider] Calling Bedrock streaming with ${this.conversationHistory.length} messages`);
// DEBUG: Log exact conversation structure being sent to Bedrock
logger.debug(`[AmazonBedrockProvider] DEBUG - Conversation structure:`);
this.conversationHistory.forEach((msg, index) => {
logger.debug(` Message ${index} (${msg.role}): ${msg.content.length} content items`);
msg.content.forEach((item, itemIndex) => {
const keys = Object.keys(item);
logger.debug(` Content ${itemIndex}: ${keys.join(", ")}`);
});
});
return commandInput;
}
async processStreamResponse(commandInput, controller) {
const command = new ConverseStreamCommand(commandInput);
const response = await this.bedrockClient.send(command);
if (!response.stream) {
throw new Error("No stream returned from Bedrock");
}
const currentMessageContent = [];
let stopReason = "";
let currentText = "";
// Process streaming chunks
for await (const chunk of response.stream) {
if (chunk.contentBlockStart) {
// Starting a new content block
currentMessageContent.push({});
}
if (chunk.contentBlockDelta?.delta?.text) {
// Text delta - stream it to user
const textDelta = chunk.contentBlockDelta.delta.text;
currentText += textDelta;
controller.enqueue({
content: textDelta,
});
}
if (chunk.contentBlockStart?.start?.toolUse) {
// Tool use block starting - initialize tool information
const currentBlock = currentMessageContent[currentMessageContent.length - 1];
currentBlock.toolUse = {
name: chunk.contentBlockStart.start.toolUse.name || "",
input: {}, // Initialize empty - will be populated by delta chunks
toolUseId: chunk.contentBlockStart.start.toolUse.toolUseId ||
`tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
};
}
if (chunk.contentBlockDelta?.delta?.toolUse) {
// Tool use delta - accumulate tool information
const currentBlock = currentMessageContent[currentMessageContent.length - 1];
if (!currentBlock.toolUse) {
currentBlock.toolUse = {
name: "",
input: {},
toolUseId: `tool_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
};
}
// Use robust parameter merging like Bedrock-MCP-Connector
if (chunk.contentBlockDelta.delta.toolUse.input) {
// Merge parameters more robustly to avoid missing required parameters
const deltaInput = chunk.contentBlockDelta.delta.toolUse.input;
if (typeof deltaInput === "string") {
currentBlock.toolUse.input = { value: deltaInput };
}
else if (deltaInput &&
typeof deltaInput === "object" &&
!Array.isArray(deltaInput)) {
// Ensure both objects are properly typed before spreading
const currentInput = currentBlock.toolUse.input || {};
const newInput = deltaInput;
currentBlock.toolUse.input = {
...currentInput,
...newInput,
};
}
}
}
if (chunk.contentBlockStop) {
// Content block completed
const currentBlock = currentMessageContent[currentMessageContent.length - 1];
if (currentText && currentBlock && !currentBlock.toolUse) {
// Only add text to blocks that don't have toolUse
currentBlock.text = currentText;
}
currentText = "";
}
if (chunk.messageStop) {
stopReason = chunk.messageStop.stopReason || "end_turn";
break;
}
}
// Add assistant message to conversation history
const assistantMessage = {
role: "assistant",
content: currentMessageContent,
};
this.conversationHistory.push(assistantMessage);
return { stopReason, assistantMessage };
}
async handleStreamStopReason(stopReason, assistantMessage, controller) {
if (stopReason === "end_turn" || stopReason === "stop_sequence") {
// Conversation completed
controller.close();
return false;
}
else if (stopReason === "tool_use") {
logger.debug(`🛠️ [AmazonBedrockProvider] Tool use detected in streaming - executing tools`);
await this.executeStreamTools(assistantMessage.content);
return true; // Continue conversation loop
}
else if (stopReason === "max_tokens") {
// Handle max tokens by continuing conversation
const userMessage = {
role: "user",
content: [{ text: "Please continue." }],
};
this.conversationHistory.push(userMessage);
return true; // Continue conversation loop
}
else {
// Unknown stop reason - end conversation
controller.close();
return false;
}
}
async executeStreamTools(messageContent) {
// Execute all tool uses in the message - ensure 1:1 mapping like Bedrock-MCP-Connector
const toolResults = [];
let toolUseCount = 0;
// Count toolUse blocks first to ensure 1:1 mapping
for (const contentItem of messageContent) {
if (contentItem.toolUse) {
toolUseCount++;
}
}
logger.debug(`🔍 [AmazonBedrockProvider] Found ${toolUseCount} toolUse blocks in assistant message`);
for (const contentItem of messageContent) {
if (contentItem.toolUse) {
logger.debug(`🔧 [AmazonBedrockProvider] Executing tool: ${contentItem.toolUse.name}`);
try {
const toolResult = await this.executeSingleTool(contentItem.toolUse.name, contentItem.toolUse.input || {}, contentItem.toolUse.toolUseId);
logger.debug(`✅ [AmazonBedrockProvider] Tool execution successful: ${contentItem.toolUse.name}`);
// Ensure exact structure matching Bedrock-MCP-Connector
toolResults.push({
toolResult: {
toolUseId: contentItem.toolUse.toolUseId,
content: [{ text: String(toolResult) }],
status: "success",
},
});
}
catch (error) {
logger.error(`❌ [AmazonBedrockProvider] Tool execution failed: ${contentItem.toolUse.name}`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
toolResults.push({
toolResult: {
toolUseId: contentItem.toolUse.toolUseId,
content: [
{
text: `Error executing tool ${contentItem.toolUse.name}: ${errorMessage}`,
},
],
status: "error",
},
});
}
}
}
logger.debug(`📊 [AmazonBedrockProvider] Created ${toolResults.length} toolResult blocks for ${toolUseCount} toolUse blocks`);
// Validate 1:1 mapping before adding to conversation
if (toolResults.length !== toolUseCount) {
logger.error(`❌ [AmazonBedrockProvider] Mismatch: ${toolResults.length} toolResults vs ${toolUseCount} toolUse blocks`);
throw new Error(`Tool mapping mismatch: ${toolResults.length} toolResults for ${toolUseCount} toolUse blocks`);
}
// Add tool results as user message - exact structure like Bedrock-MCP-Connector
if (toolResults.length > 0) {
const userMessageWithToolResults = {
role: "user",
content: toolResults,
};
this.conversationHistory.push(userMessageWithToolResults);
logger.debug(`📤 [AmazonBedrockProvider] Added ${toolResults.length} tool results to conversation (1:1 mapping validated)`);
}
}
/**
* Health check for Amazon Bedrock service
* Uses ListFoundationModels API to validate connectivity and permissions
*/
async checkBedrockHealth() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
// Create a separate BedrockClient for health checks (not BedrockRuntimeClient)
// Use simple configuration like working example - no custom proxy handler
const healthCheckClient = new BedrockClient({
region: process.env.AWS_REGION || "us-east-1",
});
try {
logger.debug("🔍 [AmazonBedrockProvider] Starting health check...");
const command = new ListFoundationModelsCommand({});
const response = await healthCheckClient.send(command, {
abortSignal: controller.signal,
});
const models = response.modelSummaries || [];
const activeModels = models.filter((model) => model.modelLifecycle?.status === "ACTIVE");
logger.debug(`✅ [AmazonBedrockProvider] Health check passed - Found ${activeModels.length} active models out of ${models.length} total models`);
if (activeModels.length === 0) {
throw new Error("No active foundation models available in the region");
}
}
catch (error) {
clearTimeout(timeoutId);
const errorObj = error;
if (errorObj.name === "AbortError") {
throw new Error("Bedrock health check timed out after 10 seconds");
}
const errorMessage = typeof errorObj.message === "string" ? errorObj.message : "";
if (errorMessage.includes("UnauthorizedOperation") ||
errorMessage.includes("AccessDenied")) {
throw new Error("Bedrock access denied. Check your AWS credentials and IAM permissions for bedrock:ListFoundationModels");
}
if (errorObj.code === "ECONNREFUSED" || errorObj.code === "ENOTFOUND") {
throw new Error("Unable to connect to Bedrock service. Check your network connectivity and AWS region configuration");
}
logger.error("❌ [AmazonBedrockProvider] Health check failed:", error);
throw new Error(`Bedrock health check failed: ${errorMessage || "Unknown error"}`);
}
finally {
clearTimeout(timeoutId);
try {
healthCheckClient.destroy();
}
catch {
// Ignore destroy errors during cleanup
}
}
}
handleProviderError(error) {
// Handle AWS SDK specific errors
const message = error instanceof Error ? error.message : String(error);
if (message.includes("AccessDeniedException")) {
return new Error("AWS Bedrock access denied. Check your credentials and permissions.");
}
if (message.includes("ValidationException")) {
return new Error(`AWS Bedrock validation error: ${message}`);
}
return new Error(`AWS Bedrock error: ${message}`);
}
}