Skip to content

contrib/spring-ai: tool results are dropped, making the module non-functional for tool-using agents on Spring AI 2.0.0 #1429

Description

@akbarkanso

🔴 Required Information

Describe the Bug:

MessageConverter in contrib/spring-ai never converts ADK functionResponse parts into Spring AI ToolResponseMessage.

Before Spring AI 2.0.0, this was masked because ChatModel implementations could execute tools internally, so ADK did not need to send tool results back through the model. Spring AI 2.0.0 removed internal tool execution at the ChatModel level, so ADK now owns the tool loop and the tool result it appends is silently discarded by the converter.

The result is a request containing an assistant message with tool_calls but no matching role:"tool" message. The model never receives the tool output. No exception is thrown, and nothing in the logs indicates a problem; the agent may simply answer as if the tool returned nothing.

Each change is survivable on its own. Together, they make contrib/spring-ai non-functional for tool-using agents.

Steps to Reproduce:

  1. Depend on com.google.adk:google-adk-spring-ai:1.7.1 and org.springframework.ai:spring-ai-bom:2.0.0, with spring-ai-starter-model-openai.
  2. Build an LlmAgent whose model is SpringAI wrapping an OpenAiChatModel, with exactly one FunctionTool.
  3. Point the OpenAI client at any OpenAI-compatible endpoint and ask the agent a question that requires the tool.
  4. Capture the outbound HTTP request bodies.
  5. Observe the second request: it contains the assistant message with tool_calls, but no role:"tool" message carrying the result.

Expected Behavior:

After ADK executes the tool and appends Content(role=user, functionResponse=...), the follow-up request should contain a corresponding tool message with role:"tool" and the matching tool_call_id, allowing the model to consume the tool result.

This is already how the corresponding tool-result handling works in Claude and chat.ChatCompletionsRequest in core/.

Observed Behavior:

The functionResponse part is skipped during conversion and never reaches the wire.

The follow-up request therefore contains tool_calls with no matching tool message. Depending on the OpenAI-compatible endpoint, the request may be rejected or the model may re-request the same call. In either case, the agent cannot reliably consume the tool output.

Environment Details:

  • ADK Library Version: com.google.adk:google-adk:1.7.1, com.google.adk:google-adk-spring-ai:1.7.1
  • Spring AI: org.springframework.ai:spring-ai-bom:2.0.0 (GA), with spring-ai-starter-model-openai
  • Java: OpenJDK 21 (Temurin 21.0.3)
  • OS: macOS and Linux
  • TS Version: N/A, Java

Model Information:

An OpenAI-compatible gateway (LiteLLM) fronting open-weight chat models.

The defect is in message conversion, so it should be model-independent. It affects any ChatModel reached through SpringAI.


🟡 Optional Information

Regression:

No. This conversion path never worked.

MessageConverter has no ToolResponseMessage support in any released version. The missing conversion became a functional blocker with Spring AI 2.0.0 because Spring AI removed the internal tool execution that previously meant ADK did not have to send tool results back through the ChatModel.

Therefore, the trigger is the Spring AI 2.0.0 change rather than a regression in ADK's tool loop.

Logs:

The decisive evidence is the outbound request body rather than a stack trace, because the failure is silent.

Shape of the second request:

messages: [
  { role: "system",    ... },
  { role: "user",      ... },
  { role: "assistant", tool_calls: [
      { id: "call_...", function: { name: "...", arguments: "..." } }
    ] }
]

Expected but absent:

{ role: "tool", tool_call_id: "call_...", content: "<result>" }

Additional Context:

Spring AI 2.0.0 removed internal tool execution.

The Spring AI 2.0.0 upgrade notes state that internalToolExecutionEnabled was removed and that per-model internal tool execution was removed from all ChatModel implementations, with no replacement at the ChatModel level.

This is also reflected in the artifacts: executeToolCalls appears in ToolCallingAdvisor (a ChatClient advisor) and ToolCallingManager, while OpenAiChatModel calls only resolveToolDefinitions.

ADK's SpringAI integration uses ChatModel, not ChatClient, so it does not receive tool execution from Spring AI at that layer.

The conversion gap in google-adk-spring-ai:1.7.1:

MessageConverter.handleUserContent detects functionResponse, but skips it:

  99: invokevirtual  Part.functionResponse()
 102: invokevirtual  Optional.isPresent()
 105: ifeq   111
 108: goto   354          <- skips to the next part; the tool result is discarded
 ...
 394: aload_3             <- "toolResponseMessages", which nothing ever adds to
 395: invokeinterface List.addAll

There are zero references to org.springframework.ai.chat.messages.ToolResponseMessage in the artifact, while handleAssistantContent does emit AssistantMessage$ToolCall.

The published 1.7.0 javadoc also states that media attachments and tool responses are currently not supported.

This appears scoped to contrib/, not to ADK core.

Connector Location Tool-result handling
Gemini core/ Native google-genai types; no translation required
Claude core/ functionResponseToolResultBlockParam
chat.ChatCompletionsRequest core/ functionResponsetoolCallId, role:"tool"
springai.MessageConverter contrib/ No ToolResponseMessage conversion

ADK's own tool loop is healthy. flows/llmflows/Functions references FunctionResponse 17 times. The missing behavior is in the Spring AI bridge.

PR #733:

PR #733 appears to be the only previous fix attempt. It is open, mergeable: dirty, and has been stale since 2026-04-07.

Its proposed mechanism, internalToolExecutionEnabled(false), no longer compiles against Spring AI 2.0.0, so it cannot be revived as-is.

What we are asking for:

Either of the following would address the issue:

  1. Fix handleUserContent to convert functionResponse parts into ToolResponseMessage.ToolResponse, keyed by the originating call ID, and add them to the toolResponseMessages list that is already assembled but never populated.

    Claude and chat.ChatCompletionsRequest provide working references for the expected tool-result handling.

  2. If contrib/spring-ai is not intended to be maintained, make that limitation explicit in the module documentation.

    The current javadoc describes tool responses as unsupported, but on Spring AI 2.0.0 and later this means tool-using agents can silently proceed without receiving their tool results. A note pointing users toward com.google.adk.models.chat.ChatCompletionsHttpClient would help prevent others from losing time debugging this behavior.

We spent several days isolating this because each component behaves correctly in isolation and the primary symptom is a silently incorrect model response.

We have since migrated to core's ChatCompletionsHttpClient behind a small BaseLlm subclass, and we now assert the presence of the role:"tool" follow-up message in CI so this failure cannot pass silently.

Related: #1295 (fixed in 1.7.0) and #1426.

Minimal Reproduction Code:

The essential wiring is a SpringAI model plus a single function tool:

OpenAiChatModel chatModel = /* configured against any OpenAI-compatible endpoint */;

LlmAgent agent = LlmAgent.builder()
        .name("repro-agent")
        .model(new SpringAI(chatModel))
        .instruction("Use the tool to answer.")
        .tools(FunctionTool.create(MyTools.class, "lookup"))
        .build();

// Ask something that requires "lookup", then inspect the outbound request bodies.
// The second request has assistant.tool_calls but no role:"tool" message.

How often has this issue occurred?:

  • Always (100%)
  • Often (50%+)
  • Intermittently (<50%)
  • Once / Rare

Metadata

Metadata

Assignees

Labels

waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions