#1585·adk-go

tool/functiontool: raw-value validation rejects marshalable results and accepts unrepresentable ones

Author: TrainingcqyCreated Sep 12, 2026Updated Sep 18, 2026
Labelsbug

Description

functionTool.Run converts a tool result into map[string]any, and falls back to wrapping a non-map result into {"result": v} when that conversion fails, at tool/functiontool/function.go:231-246:

go
	resp, err := typeutil.ConvertToWithJSONSchema[TResults, map[string]any](output, f.outputSchema)
	if err == nil { // all good
		return resp, nil
	}

	// Specs requires the result to be a map (dict in python). python impl allows basic types when building response event
	// functions.py __build_response_event does the following
	// if not isinstance(function_result, dict):
	// 		function_result = {'result': function_result}
	if f.outputSchema != nil {
		if err1 := f.outputSchema.Validate(output); err1 != nil {
			return resp, err // if it fails propagate original err.
		}
	}
	wrappedOutput := map[string]any{"result": output}
	return wrappedOutput, nil

The guard at :241 decides between wrapping the result and propagating the original error. What it validates is the raw Go value. Resolved.Validate walks that value through reflect.ValueOf and never marshals it: a reflect.Struct is refused outright at jsonschema/validate.go:449-451, and at jsonschema/util.go:262-266 a float is sorted into integer or number by whether its fractional part is zero, so NaN and ±Inf pass as number. For a schema inferred from TResults, what the guard detects is a Go struct on the value's path, which is unrelated to whether the result can be sent.

The guard is wrong in both directions.

Valid results are rejected. A tool returning []SomeStruct produces the shape the fallback exists for, yet Run returns json: cannot unmarshal array into Go value of type map[string]interface {}. At internal/llminternal/base_flow.go:1416 that error is written as {"error": ...}, so the model receives it instead of the data. time.Time is rejected the same way, since it is a named struct. The same tool returning []float64 is wrapped into {"result": [...]} as expected.

Unrepresentable results are let through. When a tool returns map[string]any with one value set to math.NaN(), the guard lets it through and Run returns the wrapped result with a nil error. google.golang.org/genai then calls InternalDeepMarshal at models.go:4472 without receiving the error it returns, leaving parameterMap empty, so InternalFormatMap at models.go:4506 renders the URL as :generateContent, the request body is empty, and the run ends in a 404 whose Message is empty. Also measured as let through: float64 returning +Inf or -Inf, float32 returning NaN, and map[string]any holding a chan or a func.

Suggested fix

Decide the fallback on whether encoding/json can represent the result rather than on schema validation: marshal once, return that error when it fails, and wrap otherwise.

Enforce only a schema the caller supplied through Config.OutputSchema. The inferred schema describes TResults, not what encoding/json emits for it, so validating the JSON form against it would start rejecting []byte, json.RawMessage and a *SomeStruct returning nil, all three of which work on main.

Environment

The first direction reproduces without a model call: build a tool returning []SomeStruct inside the tool/functiontool package and call Run to get the error above. Both directions were run against Gemini API and Vertex AI, with the same behavior.

  • ADK Library Version: main @ 11521a4
  • OS: Windows
  • Go Version: 1.26.6
  • Model Information: gemini-3.8-flash, Gemini API and Vertex AI