logancyang_obsidian-copilot/designdocs/BEDROCK_TOOL_CALLING.md
Logan Yang fff6b4efd0
chore: rename docs to designdocs and nest todo folder (#2252)
- Renamed docs/ to designdocs/
- Moved draft/todo docs into designdocs/todo/ subfolder
- Added OBSIDIAN_CLI_INTEGRATION.md from master's todo/ folder
- Updated all docs/ path references in CLAUDE.md, AGENTS.md, and designdocs

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:26:03 -08:00

4.9 KiB

BedrockChatModel Tool Calling Implementation

Status: IMPLEMENTED

Native tool/function calling support has been added to the custom BedrockChatModel class for Agent mode.


Summary

The BedrockChatModel now supports LangChain's native tool calling via model.bindTools(tools), enabling it to work with Agent mode just like ChatOpenAI, ChatAnthropic, and ChatGoogleGenerativeAI.


Implementation Details

1. bindTools() Method

bindTools(tools: StructuredToolInterface[]): BedrockChatModel {
  const bound = Object.create(this) as BedrockChatModel;
  bound.boundTools = tools;
  return bound;
}

Creates a new instance with tools bound, following LangChain's pattern.

2. Tool Format Conversion

private convertToolsToClaude(tools: StructuredToolInterface[]): any[] {
  return tools.map((tool) => {
    let inputSchema: any = { type: "object", properties: {} };
    if (tool.schema) {
      inputSchema = isInteropZodSchema(tool.schema)
        ? toJsonSchema(tool.schema)
        : tool.schema;
    }
    return {
      name: tool.name,
      description: tool.description || "",
      input_schema: inputSchema,
    };
  });
}

Uses LangChain's isInteropZodSchema and toJsonSchema for proper schema conversion.

3. Request Body with Tools

Tools are included in the request payload when bound:

if (this.boundTools && this.boundTools.length > 0) {
  payload.tools = this.convertToolsToClaude(this.boundTools);
}

4. ToolMessage Handling

buildRequestBody handles ToolMessage (tool results) as tool_result content blocks:

if (messageType === "tool") {
  const toolMessage = message as ToolMessage;
  conversation.push({
    role: "user",
    content: [
      {
        type: "tool_result",
        tool_use_id: toolMessage.tool_call_id,
        content: toolResultContent,
      },
    ],
  });
}

5. AIMessage with Tool Calls

buildRequestBody handles AIMessage with tool_calls as tool_use content blocks:

if (toolCalls && toolCalls.length > 0) {
  const contentBlocks: ContentBlock[] = [];
  // Add text if present
  // Add tool_use blocks for each tool call
  for (const tc of toolCalls) {
    contentBlocks.push({
      type: "tool_use",
      id: tc.id || `tool_${Date.now()}`,
      name: tc.name,
      input: tc.args as Record<string, unknown>,
    });
  }
}

6. Non-Streaming Tool Call Extraction

_generate extracts tool calls from Claude's response:

private extractToolCalls(data: any): any[] | undefined {
  if (!Array.isArray(data?.content)) return undefined;
  const toolUseBlocks = data.content.filter(
    (block: any) => block.type === "tool_use"
  );
  if (toolUseBlocks.length === 0) return undefined;
  return toolUseBlocks.map((block: any) => ({
    id: block.id,
    name: block.name,
    args: block.input || {},
    type: "tool_call" as const,
  }));
}

7. Streaming Tool Call Chunks

processStreamEvent emits tool_call_chunks for LangChain's concat mechanism:

private extractToolCallChunk(event: any): { id?: string; index: number; name?: string; args?: string } | null {
  // content_block_start with tool_use - initial tool call info
  if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
    return {
      id: event.content_block.id,
      index: event.index ?? 0,
      name: event.content_block.name,
      args: "",
    };
  }
  // content_block_delta with input_json_delta - partial tool args
  if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
    return {
      index: event.index ?? 0,
      args: event.delta.partial_json || "",
    };
  }
  return null;
}

Tool call chunks are emitted as AIMessageChunk with tool_call_chunks:

const toolCallChunk = this.extractToolCallChunk(innerEvent);
if (toolCallChunk) {
  const messageChunk = new AIMessageChunk({
    content: "",
    response_metadata: chunkMetadata,
    tool_call_chunks: [toolCallChunk],
  });
  deltaChunks.push(new ChatGenerationChunk({ message: messageChunk, text: "" }));
}

Testing

const model = new BedrockChatModel({
  modelId: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
  apiKey: "...",
  endpoint: "...",
  streamEndpoint: "...",
});

const tools = [
  {
    name: "get_weather",
    description: "Get weather for a location",
    schema: z.object({ location: z.string() }),
  },
];

const boundModel = model.bindTools(tools);
const response = await boundModel.invoke([new HumanMessage("What's the weather in Tokyo?")]);

console.log(response.tool_calls); // Should have tool call

Reference