diff --git a/go/adk/pkg/models/bedrock.go b/go/adk/pkg/models/bedrock.go index 950a5778c6..9050833830 100644 --- a/go/adk/pkg/models/bedrock.go +++ b/go/adk/pkg/models/bedrock.go @@ -647,10 +647,22 @@ func convertGenaiContentsToBedrockMessages(contents []*genai.Content, nameMap ma if sanitized, ok := nameMap[callName]; ok { callName = sanitized } + // Bedrock requires toolUse.input to be a JSON object. A tool call + // with no arguments arrives here with a nil Args map (genai's + // FunctionCall.Args is `json:"args,omitempty"`, so an empty map is + // dropped when the event is persisted to the session store and + // reloaded as nil). NewLazyDocument(nil) serializes to `null`, which + // Bedrock rejects with "ValidationException: Malformed input request" + // ("The value at messages.N.content.M.toolUse.input is empty"). + // Coerce nil to an empty object so no-argument tool calls round-trip. + args := part.FunctionCall.Args + if args == nil { + args = map[string]any{} + } toolUse := types.ToolUseBlock{ ToolUseId: aws.String(sanitizeBedrockToolID(part.FunctionCall.ID, idMap, &idCounter)), Name: aws.String(callName), - Input: document.NewLazyDocument(part.FunctionCall.Args), + Input: document.NewLazyDocument(args), } contentBlocks = append(contentBlocks, &types.ContentBlockMemberToolUse{ Value: toolUse, diff --git a/go/adk/pkg/models/bedrock_test.go b/go/adk/pkg/models/bedrock_test.go index 73f87bcb33..fe3e507b54 100644 --- a/go/adk/pkg/models/bedrock_test.go +++ b/go/adk/pkg/models/bedrock_test.go @@ -187,6 +187,44 @@ func TestConvertGenaiContentsToBedrockMessages(t *testing.T) { } } +// TestConvertGenaiContentsNoArgToolInput verifies that a tool call with no +// arguments serializes toolUse.input as an empty JSON object rather than null. +// A no-argument FunctionCall arrives here with a nil Args map because genai's +// FunctionCall.Args is `json:"args,omitempty"`, so the empty map is dropped when +// the event is persisted to the session store and reloaded. Bedrock Converse +// rejects a null input with "ValidationException: Malformed input request" +// ("The value at messages.N.content.M.toolUse.input is empty"). +func TestConvertGenaiContentsNoArgToolInput(t *testing.T) { + for _, args := range []map[string]any{nil, {}} { + contents := []*genai.Content{ + { + Role: "model", + Parts: []*genai.Part{ + {FunctionCall: &genai.FunctionCall{ID: "call_noargs", Name: "datetime_get_current_time", Args: args}}, + }, + }, + } + msgs, _ := convertGenaiContentsToBedrockMessages(contents, nil, nil) + if len(msgs) != 1 || len(msgs[0].Content) != 1 { + t.Fatalf("args=%v: expected 1 message with 1 content block, got %d messages", args, len(msgs)) + } + tu, ok := msgs[0].Content[0].(*types.ContentBlockMemberToolUse) + if !ok { + t.Fatalf("args=%v: want *ContentBlockMemberToolUse, got %T", args, msgs[0].Content[0]) + } + if tu.Value.Input == nil { + t.Fatalf("args=%v: toolUse.Input is nil; Bedrock requires a JSON object", args) + } + got := documentToMap(tu.Value.Input) + if got == nil { + t.Fatalf("args=%v: toolUse.Input serialized to null; want an empty JSON object {}", args) + } + if len(got) != 0 { + t.Errorf("args=%v: expected empty input object, got %v", args, got) + } + } +} + // TestConvertGenaiToolsToBedrock verifies schema conversion for all three tool // sources: genai.Schema (declaration-based), map[string]any (MCP), and // *jsonschema.Schema (functiontool.New).