fix(runner): build a new message when saving input blobs, instead of writing into the caller's Content - #1378
Conversation
|
Hi @svetanis, thank you for your contribution! We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team, we will keep you posted if any additional information is required. thank you. |
4f140c7 to
7abdc1c
Compare
MiloszSobczyk
left a comment
There was a problem hiding this comment.
Hi @svetanis, thank you for your contribution. Please look into the comments I left within my review.
7abdc1c to
d43aea5
Compare
|
Thanks for the review @MiloszSobczyk.
The branch has also been rebased against the latest |
|
Thank you for addressing my comments @svetanis. I have two more suggestions regarding the test suite. Please consider following them.
|
…riting into the caller's Content
d43aea5 to
512047c
Compare
|
Thanks for the review, @MiloszSobczyk. Both applied. Changes in the
|
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
With
saveInputBlobsAsArtifacts(true),Runner.appendNewMessageToSessionreplaces each inline blob witha placeholder by writing into the parts list of the
Contentthe caller passed torunAsync(
Runner.java:376-382).That list is never copied on the way in, so for an immutable one the
setthrowsUnsupportedOperationException— before the model is called, and before anything is appended:builder().parts(List.of(text, blob))List.of, uncopiedbuilder().parts(textBuilder, blobBuilder)ImmutableListContent.fromParts(text, blob)Arrays.asListSolution:
Rewrite a copy of the parts list and build a new
Contentfor the event. The loop moves into ahelper returning both the prepared message and the saves it scheduled:
so
appendNewMessageToSessionbecomes one decision:The functional change is one line —
new ArrayList<>(...), which always acceptsset. Documentedbehaviour is unchanged: the persisted message still carries the placeholder, the blob is still saved, and
the model still never sees it.
Testing Plan
Unit Tests:
Nine tests added. Five fail without the fix (regression tests); four pass in both states (parity tests
pinning behaviour that must not change):
..._immutablePartsList_savesArtifactAndCompletes..._partBuilderPartsList_savesArtifactAndCompletes..._appendedEventReplacesBlobWithPlaceholder..._doesNotModifyCallerMessage..._storesBlobVerbatim..._fromPartsConstruction_savesArtifactAndCompletes..._disabled_keepsBlobAndSavesNothing..._textOnlyMessage_passesThroughUnchanged..._disabledWithTextOnlyMessage_passesThroughUnchangedThe last two cover ordinary traffic: a text-only prompt with the option on (the runner still walks the
parts and, after the fix, copies them), and the default path with the option off (the rewrite is skipped
entirely and the caller's message passes through uncopied).
Manual End-to-End (E2E) Tests:
A demo drives one real agent turn per row through
InMemoryRunner(no custom wiring) againstgemini-2.5-flash, varying only the construction and the flag, and reads back both the artifact storeand the session afterwards.
Before:
After (same demo):
On every row where the offload ran, the payload came back out of storage verbatim, the model was never
shown the blob, and the model was shown the placeholder — so this is not merely "stopped crashing".
Checklist