spring-projects / spring-projects/spring-ai
AssistantMessage mutation appends replacement content and retains cleared tool calls
Nobody has claimed this yet.
- 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
mainatb6b5767eb62b61ceaaac78ad103a0ad96e8f8272,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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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