Skip to content

fix(adk): keep Ollama tool property order stable across requests - #2693

Open
garlicKim21 wants to merge 6 commits into
kagent-dev:mainfrom
garlicKim21:fix/ollama-tool-property-order
Open

fix(adk): keep Ollama tool property order stable across requests#2693
garlicKim21 wants to merge 6 commits into
kagent-dev:mainfrom
garlicKim21:fix/ollama-tool-property-order

Conversation

@garlicKim21

Copy link
Copy Markdown
Contributor

Summary

convertGenaiToolsToOllama ranges over the Go map of parameter properties and
Sets each one into an api.ToolPropertiesMap:

for name, propAny := range props {        // range order is randomized
    ...
    params.Properties.Set(name, prop)     // insertion order is preserved
}

That type is order-preserving by design — // ToolPropertiesMap holds tool properties in insertion order (ollama/api/types.go), with a dedicated
TestToolPropertiesMap_OrderPreservation. So the randomized range order is
carried straight into the serialized request and the tool definitions come out
in a different order on every call.

Ollama servers cache on the prompt prefix, and tool definitions sit near the
front of the rendered prompt. A reordered tool block invalidates the prefix, so
the entire prompt is re-processed on every request.

Both branches of the conversion (ParametersJsonSchema and Parameters) have
the same problem. This sorts the property names before inserting.

Impact measured on a live agent

An agent with 9 tools bound, 8 of them with more than one property. Feeding the
real schemas through the current code path:

changes_by_entity      props=2  distinct serializations=2
check_stack            props=3  distinct=3
get_release            props=3  distinct=3
list_changes           props=5  distinct=5
k8s_get_resources      props=5  distinct=5
...
tool block combinations = 5400
two consecutive requests agree = 0.0185% of the time

(Small Go maps live in one bucket, so the range order is a rotation rather than
a full shuffle — hence 5 variants for 5 properties, not 5!.)

Against the inference server, over one 19-request session:

  • the server logged a full prompt re-process 19 times out of 19
  • prefill totalled 1,453s against 809s of generation — 64% of the session's
    wall clock
    spent re-reading a prompt that had barely changed
  • the longest common prefix with the cache broke at 206–1,400 tokens, well
    before the end of the (static, ~2,800 token) system instruction

The varying break point is itself a fingerprint of this bug: a static
difference would break at a constant offset, whereas here it breaks at
whichever tool first happens to differ.

Why the Python runtime was unaffected

The same agent, same config, on the Python ADK image hit the cache on every
turn. Python preserves dict insertion order deterministically, so the
equivalent conversion is stable there. Switching an agent's runtime from
python to go is what surfaced this.

Test

TestConvertGenaiToolsToOllamaPropertyOrderIsStable converts the same tool 100
times and asserts the property order is identical, then asserts the order is
sorted so it is stable across processes too.

Without the fix:

--- FAIL: TestConvertGenaiToolsToOllamaPropertyOrderIsStable
    ollama_test.go:242: property order changed between calls:
         first: [project family bucket since limit]
         call 0: [bucket since limit project family]

Environment

kagent 0.10.0-rc6, Go ADK runtime, declarative agent with an Ollama
ModelConfig, tools from two remote MCP servers.

convertGenaiToolsToOllama ranges over the Go map of parameter properties
and Sets each one into an api.ToolPropertiesMap. That type preserves
insertion order, so a randomized range order is carried straight into the
serialized request: the tool definitions come out in a different order on
every call.

Ollama servers cache on the prompt prefix. Tool definitions sit near the
front of the rendered prompt, so a reordered tool block invalidates the
prefix and the whole prompt is re-processed every request.

Measured against a live agent with 9 tools bound (8 of them with more
than one property): 5,400 distinct serializations of the tool block, so
two consecutive requests agree 0.02% of the time. In a 19-request session
the server logged a full re-process 19 times, and prefill accounted for
64% of the session's wall clock. The same agent on the Python runtime hit
the cache on every turn, because dict iteration order is deterministic
there.

Sort the property names before inserting. Both branches of the conversion
have the same problem, so both are sorted.

Signed-off-by: Golden Garlic <148346166+garlicKim21@users.noreply.github.com>
@github-actions github-actions Bot added the bug Something isn't working label Sep 4, 2026
@EItanya
EItanya enabled auto-merge (squash) September 4, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants