Repository navigation
Conversation
Hold the last complete text update for the final artifact, retaining earlier messages and metadata. Cover stream delivery and caller-session persistence with both plain replies and remote tool calls. Related: google#7247
|
Could someone rerun the triage job and approve the CI run? The triage job failed with a Gemini I reran the checks on my fork for |
There was a problem hiding this comment.
Code Review
This pull request updates the A2aAgentExecutor to prevent duplicate publishing of the final answer by holding the last complete text event as a pending event and only enqueuing it when a subsequent event arrives or during cleanup in an exception/cancellation block. It also propagates the pending event's metadata to the final artifact and includes comprehensive unit tests for these streaming and interruption behaviors. Feedback on the changes suggests using getattr when checking adk_event.partial to avoid potential AttributeErrors if the event object lacks this attribute.
| if ( | ||
| isinstance(e, TaskStatusUpdateEvent) | ||
| and task_result_aggregator.task_state == _compat.TS_WORKING | ||
| and not adk_event.partial |
There was a problem hiding this comment.
Using adk_event.partial directly can raise an AttributeError if adk_event is None or does not have the partial attribute (e.g., in certain test mocks or custom event implementations). It is safer to use getattr(adk_event, 'partial', False) to ensure robustness.
| and not adk_event.partial | |
| and not getattr(adk_event, "partial", False) |
There was a problem hiding this comment.
Runner.run_async() yields Event instances, and Event inherits partial: Optional[bool] = None from LlmResponse. The field is present even when unset, and the default converter rejects None before this branch. I'm keeping direct access here because it follows the event contract.
Link to Issue or Description of Change
Related to #7247. The proposed scope and timing are described on the issue.
The legacy executor sends the complete final text as both a WORKING message and a final artifact. The caller receives and stores both copies. This holds the latest complete text-only update until another converted event arrives or the run finishes, then delivers the last answer through the final artifact. Earlier messages keep their order and metadata; failures and cancellation flush pending text.
A complete progress message can now wait for the next event or completion. Partial chunks and tool messages remain immediate. Maintainer feedback on that timing tradeoff is still requested. Confirmation requests are covered separately by #7252.
Testing Plan
Added 13 regression cases covering duplicate delivery, distinct identical messages, state-only events, metadata, interrupted runs and real streaming/non-streaming clients. Eight duplicate-text cases fail without the fix; the remaining five pass on both versions.
The Linux validation passed all 16 jobs for
60863f0, including full tox on Python 3.11–3.14, all-files pre-commit, baseline mypy and A2A/MCP compatibility. Full suites passed 19,052 tests on 3.11 and 19,043 on each other version, with existing skips and expected-failure results recorded in the run. Mypy found no new diagnostics. The fork test environment excludeslancedb.Manual Runner E2E: A scripted model, in-process HTTP transport,
to_a2a()and a caller Runner. The setup and command below check plain replies and remote tool calls against legacy and forced-v2 servers. Source checks passed with a2a-sdk 1.2.2 and 0.3.25; the installed-wheel check passed with 1.2.2. Each answer was yielded and persisted once, with one tool call where configured:Standalone offline reproduction
Save as
repro.pyin a development checkout with the test dependencies installed:To check an installed wheel instead, run with that environment's Python and omit
--source src. It uses no model API or credentials.Checklist
Developed and tested with Codex.