spring-projects / spring-projects/spring-ai

AssistantMessage mutation appends replacement content and retains cleared tool calls

Open
#7,010 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Bug description

After #6997, AssistantMessage.mutate().content(...) appends the replacement text to the original text, and mutate().toolCalls(List.of()) does not clear the original tool calls. These setters replaced the corresponding fields before the message-parts change.

This affects response post-processing (for example, replacing a formatted response before parsing it) and applications removing tool calls before replaying a message. It is a regression on main, not a claim about the latest stable 2.0.1 release.

Environment

  • Spring AI main at b6b5767eb62b61ceaaac78ad103a0ad96e8f8272, 2.1.0-SNAPSHOT
  • Introduced by 0c815f0bca7583a3b6e6e212d38442e288109f1e (#6997)
  • Windows, Liberica JDK 17.0.20.1+1; repository Maven wrapper
  • No model provider, API key or network request is needed by the reproducer.

Steps to reproduce

var original = AssistantMessage.builder()
    .content("original")
    .toolCalls(List.of(new AssistantMessage.ToolCall(
        "call-1", "function", "weather", "{}")))
    .build();

var rewritten = original.mutate().content("replacement").build();
var cleared = original.mutate().toolCalls(List.of()).build();

assertThat(rewritten.getText()).isEqualTo("replacement");
assertThat(cleared.getToolCalls()).isEmpty();

Actual results: rewritten.getText() is "originalreplacement"; cleared.getToolCalls() still contains call-1.

I added two focused JUnit tests locally and ran:

./mvnw -B -pl spring-ai-model -am -Dtest=AssistantMessageMutationRegressionTests -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.build.cache.enabled=false test

Result: 2 tests, 2 assertion failures, 0 errors, 0 skipped. A standalone Java program compiled against the current message sources reproduces the same results.

Expected behavior

The existing mutation setters should replace or clear the selected content, preserving unrelated parts and the original message. Explicit part(...) calls should remain append operations.

Root cause and proposed direction

mutate() now seeds a builder using parts(getParts()). The legacy setters populate separate fields, and buildParts() appends those fields after the seeded parts. An empty replacement list therefore removes nothing. The same mechanism also applies to media.

Keep the documented append behavior for a fresh parts builder, but make mutation setters replace their corresponding parts, preserving other parts and their order. UserMessage.mutate() already follows this replacement principle.

I searched open/closed reports and read #6997's discussion. #4888 discusses response mutation APIs more generally; this report is specifically about the new replacement regression. I am working on a focused regression fix.

Investigation and reproductions were prepared with Codex assistance; the outputs above were executed locally.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the spring-ai-model module with AssistantMessage mutation and compare its replacement behavior with UserMessage.mutate(). Run the focused AssistantMessageMutationRegressionTests command from the report. Done means replacement content no longer appends, an empty tool-call list clears existing calls, unrelated parts and ordering remain intact, and explicit part(...) appends still work.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.