internal/llminternal: streamed function-call args misparse RFC 9535 array and quoted JSON paths
Describe the bug
streamingResponseAggregator rebuilds streamed function-call arguments by
stripping a "$." prefix and splitting each PartialArg.JsonPath on ".".
But partial-arg paths are RFC 9535 JSON paths — the genai field's own example
is "$.foo.bar[0].data" — so $.items[0] addresses an array element and
$.a["b.c"] a member whose name contains a dot. The split turns the first
into a literal "items[0]" map key and the second into the bogus keys
a["b / c"], so a tool whose schema declares an array or a
bracket-addressed member receives malformed arguments.
adk-python's StreamingResponseAggregator parses the same paths into
string/int components and builds real arrays
(src/google/adk/utils/streaming_utils.py, _parse_json_path,
_get_value_by_json_path, _set_value_by_json_path); the Go port predates
that implementation and never gained it.
Where
internal/llminternal/stream_aggregator.go—setValueByJSONPathsplits the path on".";getValueFromPartialArgrepeats the same split to look up an existing string to append to. Both sit underprocessStreamingFunctionCallPart.- Reachable whenever a Gemini/Vertex model streams function-call arguments
(
functionCallingConfig.streamFunctionCallArguments) — the aggregator backsmodel/gemini's streaming path.
Impact
Any function call whose arguments arrive via partialArgs and whose schema
contains an array, a nested object under an array element, or a member name
needing brackets is reconstructed wrong. {"items": ["a", "b"]} comes out
as {"items[0]": "a", "items[1]": "b"} — the declared parameter is absent
and junk keys appear in its place, so the dispatched tool fails argument
validation or silently reads nothing for the real parameter.
How to reproduce
TestStreamingFunctionCallArgsWithArrayJSONPath in
internal/llminternal/stream_aggregator_test.go streams PartialArgs at
$.items[0] (two chunks, exercising string concatenation), $.items[2].name,
$.filter["tag.list"][0], $['x.y'], and $["a\nb"], then asserts the
aggregated FunctionCall.Args. On the unfixed code it fails — the args are
{"items[0]": "ab", "items[2]": {"name": "x"}, "filter[\"tag": ...} — where
the fix produces {"items": ["ab", nil, {"name": "x"}], "filter": {"tag.list": ["y"]}, "x.y": "n", "a\nb": true}.
Suggested fix
Parse each JsonPath into typed components — string member names, int array
indices — the way _parse_json_path does in adk-python, then navigate or
create map[string]any vs []any by component type, padding sparse indices
up to a bound (_MAX_ARRAY_INDEX = 10000 in Python, mirrored) and rejecting
indices past it and paths that conflict with an earlier scalar. In Go,
append can reallocate a slice, so the setter must return the container and
each level write the child's result back.
Source: google/adk-go