Repository navigation
fix(core): report the tool return value on ToolResultEndEvent - #3331
King52HerTz wants to merge 2 commits into
Conversation
ToolEmitter documents that emitted chunks do not affect what the LLM receives; only the tool method's return value does. The event stream disagrees: once a tool has emitted chunks, ReActAgent records its id in chunkedToolIds and emitToolResultDelta then skips the return value, and ToolResultEndEvent carries no result either. A consumer reconstructing the tool message from the stream therefore gets the concatenated progress text. For AG-UI that means TOOL_CALL_RESULT holds progress text while the model saw the return value, so the two histories diverge: a client resending full history feeds progress text back as the tool result, anything emitted only for the UI leaks into it, and the UI shows the wrong outcome. Put the return value on the end event as an explicit field and let AguiStreamContext prefer it over the delta buffer. It is not stored under AgentEvent.getMetadata(), which passes the tool result's own metadata through unchanged; ReActAgentNewLoopE2ETest asserts that map exactly, so adding a framework key there would be a contract change for unrelated consumers. Producers that report no return value keep the previous behaviour.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This comment was marked as abuse.
This comment was marked as abuse.
Review follow-up for agentscope-ai#3331. null on getFinalResultText() meant two different things: "the producer reports nothing" and "the tool returned content with no text in it". The second read as the first, so an image-only or blank return value fell back to the delta buffer and put progress text straight back into TOOL_CALL_RESULT — the leak this field exists to close. A result that carries content now reports its joined text even when that join is empty; only a result with no content blocks at all stays unreported. finalResultText joins text blocks, while an image or structured part reaches a client as ToolResultDataDeltaEvent, so replacing the whole buffer dropped it. AguiStreamContext now keeps the non-text fragments beside the buffer and re-appends them after the return value. The fallback path is untouched, including the arrival-order rendering the existing tests pin. service-dataplane was the second first-party consumer and still persisted the buffer, so the two disagreed about what a tool returned. SessionEventMapper applies the same rule with the same fallback, reuses the shared payload cap, and re-appends non-text fragments the way it already glued them. Also closes the two Info points: JSON round-trip for the new @JsonCreator property (the value survives polymorphic decode, "" decodes as "" rather than null, and a payload written before the field existed decodes to null), and the ReActAgent population path end to end — with an assertion that the progress chunk really reached the stream, so it cannot pass by the tool never emitting. Checked by mutation: reverting the empty-vs-null rule fails one test, never populating the field fails two, dropping the re-append in the AG-UI adapter fails two, reverting the dataplane rule fails two. Full reactor mvn test green, spotless:check green.
|
All three warnings are addressed in 0e94997, along with both 1.
2.
3.
One limit I did not resolve. A streaming tool that emits progress as image/data blocks will still have those blocks appear in the result. Evidence. Four mutations each fail exactly the tests that guard them: reverting the empty-vs-null rule → 1; never populating the field → 2; dropping the re-append in the AG-UI adapter → 2; reverting the dataplane rule → 2. Full reactor |
|
从可维护性角度来说,我们是否可以新增一个 |
Problem
ToolEmitterdocuments that "the emitted chunks do NOT affect what the LLM receives - only the tool method's return value is sent to the LLM." The event stream disagrees. Once a tool has emitted chunks its id lands inchunkedToolIds,emitToolResultDeltathen skips the return value, andToolResultEndEventcarries no result either — so a consumer rebuilding the tool message from the stream sees the concatenated progress text.For AG-UI that means
TOOL_CALL_RESULTholds progress text while the model received the return value:Reproduced from the issue with the reporter's scenario in
AguiAgentAdapterV2Test.Change
ToolResultEndEventgainsfinalResultText, populated byReActAgentfrom the tool result's text blocks, andAguiStreamContext.endToolResultprefers it over the accumulated delta buffer. Progress chunks still stream as deltas, so UIs that want them can render them — the difference is that the closed result is now the value the model saw.Why a field and not metadata. My first attempt put this under
AgentEvent.getMetadata(). That map passes the tool result's own metadata through unchanged, andReActAgentNewLoopE2ETest.toolResultMetadataPropagatesToEventsasserts it by exact equality — injecting a framework key there breaks it. That test failed on the first draft, so the value now travels on a dedicated field and the metadata map is untouched.The
@JsonCreatorgained an optionalfinalResultTextproperty; payloads without it deserialize tonull, and the two existing constructors delegate unchanged. When no return value is reported the previous buffer behaviour is preserved, so other producers are unaffected.Scope
Results with no text blocks yield
nulland fall back to the delta buffer rather than reporting an empty result.Testing
Two cases added: progress deltas plus a reported return value yields the return value; a bare
ToolResultEndEventstill yields the buffered deltas.Partially addresses #3312