Python: Fix A2AAgent to invoke context providers before and after run#4757
Conversation
A2AAgent.run() bypassed the context provider lifecycle (before_run/after_run) that BaseAgent defines as a contract for all agents. This caused A2AAgent to violate the semantic definition of BaseAgent, resulting in inconsistency with other agent implementations. The fix follows the same pattern used by WorkflowAgent: - Create SessionContext and run before_run on all context providers before processing the A2A stream - Collect response updates and run after_run on all context providers after the stream is fully consumed - Auto-create a session when context providers are configured but no session is explicitly passed Fixes microsoft#4754 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 95%
✓ Correctness
This diff deletes a REPRODUCTION_REPORT.md file from the repository root. The file was a temporary investigation artifact containing internal paths, timestamps, and debug notes from reproducing issue #4754. Removing it is correct — such files should not be committed to the repository. No code changes, no correctness concerns.
✓ Security Reliability
This diff deletes a REPRODUCTION_REPORT.md file that was accidentally committed to the repository. The file contained internal debugging notes, local filesystem paths, and investigation artifacts that should not be part of the codebase. The deletion is correct and desirable — the file leaked internal worktree paths and investigation workflow details. No security or reliability issues with this deletion; it is purely cleanup of a file that should never have been committed.
✓ Test Coverage
This diff only deletes a REPRODUCTION_REPORT.md file that was used as an internal investigation artifact. It contains no code changes, no test changes, and no behavioral modifications. There is nothing to review from a test coverage perspective.
✓ Design Approach
The diff deletes REPRODUCTION_REPORT.md, a temporary investigation artifact that should never have been committed to the repository. This is a cleanup-only change with no functional impact. There are no design concerns to flag.
Suggestions
- Consider adding REPRODUCTION_REPORT.md to .gitignore to prevent accidental re-commits of investigation artifacts.
Automated review by eavanvalkenburg's agents
There was a problem hiding this comment.
Pull request overview
This PR fixes A2AAgent.run() to honor the BaseAgent context provider lifecycle by invoking before_run()/after_run() around A2A streaming/non-streaming execution, and adds tests to validate that behavior (including session auto-creation when needed).
Changes:
- Invoke context providers’
before_run()at the start of A2A streaming andafter_run()once the stream is consumed. - Construct and populate a
SessionContext(including response attachment forafter_run()). - Add unit tests covering provider invocation for both streaming and non-streaming runs.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| python/packages/a2a/agent_framework_a2a/_agent.py | Adds provider lifecycle hook invocation and SessionContext plumbing to A2AAgent run/stream mapping. |
| python/packages/a2a/tests/test_a2a_agent.py | Adds tests ensuring context providers are invoked and receive input/response context. |
You can also share your feedback on Copilot code review. Take the survey.
- Validate messages when no continuation_token: raise ValueError if normalized_messages is empty, preventing IndexError on messages[-1] - Import BaseContextProvider/SessionContext from public agent_framework package instead of internal agent_framework._sessions module - Add test for ValueError on run(None) without continuation_token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 92%
✓ Correctness
The diff adds a guard to raise a ValueError when run() is called with no messages and no continuation_token, preventing an IndexError on
normalized_messages[-1]. The test correctly verifies this behavior by passing None. The import refactoring in the test file moves BaseContextProvider and SessionContext from a private module to the public agent_framework namespace, which is correct since both are exported from init.py. All changes are logically sound.
✓ Security Reliability
The change adds an early validation check in A2AAgent.run() to raise a ValueError when no messages are provided and there is no continuation_token. This prevents a downstream IndexError on
normalized_messages[-1](empty list access) and makes the failure mode explicit. The test correctly validates this behavior. The import refactor in the test file switches from a private module path to the public API, which is a healthy cleanup. No security or reliability issues found.
✗ Test Coverage
The new guard clause correctly raises ValueError when no messages and no continuation_token are provided, and the test covers the None-input case with a meaningful assertion (matching the error message). However, the test only checks
Noneinput — the empty-list case (await agent.run([])) also triggers the guard but is not tested. Additionally, there is no test verifying that the guard does NOT fire when a continuation_token is present with no messages, which is the complementary valid path.
✓ Design Approach
The change correctly adds a precondition guard before accessing
normalized_messages[-1], converting a confusingIndexErrorinto a descriptiveValueError. The validation is placed after normalization and inside the correct branch where the message is actually required, so it accurately captures the contract: messages are optional only when a continuation_token is provided. The test import refactor (movingBaseContextProviderandSessionContextto the publicagent_frameworkimport) is a clean, unrelated improvement. No design issues were found — the fix targets the root cause and is not overly narrow.
Suggestions
- Add a test case for
await agent.run([])(empty list) without continuation_token, sincenormalize_messages([])also produces an empty list and hits the same guard. Consider parameterizing the existing test to cover bothNoneand[]. - Add a test verifying that
run(None, continuation_token={...})does NOT raise, confirming the guard is correctly scoped to the no-continuation-token branch.
Automated review by eavanvalkenburg's agents
…soft#4754) - Parameterize test to cover both messages=None and messages=[] inputs - Add test verifying run(None, continuation_token=...) does not raise Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 91%
✓ Correctness
The changes are correct. The parametrize decorator properly tests both None and empty-list cases for the ValueError path. The new continuation_token test correctly sets up the mock with a terminal-state task in resubscribe_responses matching the expected (Task, event) tuple format consumed by _map_a2a_stream. The pytest.raises match string is a valid substring of the actual error message. No logical issues found.
✓ Security Reliability
This diff modifies a test file only, adding parametrization to an existing test (covering both None and empty list inputs) and adding a new test for the continuation_token path. The changes are straightforward test improvements with no security or reliability concerns — no production code is modified, no secrets are introduced, no unsafe patterns are used, and the mock setup correctly matches the production code's resubscribe interface.
✓ Test Coverage
The diff improves test coverage by parametrizing the 'no messages' validation test to cover both None and empty-list inputs, and adds a new test verifying that a continuation_token bypasses the message requirement. Both changes are well-structured. However, the new continuation_token test (test_run_with_continuation_token_does_not_require_messages) only asserts
response is not None, which is a weak assertion—especially since a very similar existing test (test_resume_via_continuation_token at line 775) already validates response content in detail. The new test adds value by verifying the no-raise path specifically, but its assertions could be stronger.
✓ Design Approach
The diff adds two things: (1) a parametrize decorator to cover the empty-list case alongside None in the existing validation test, and (2) a new test confirming that a continuation_token bypasses the message requirement. Both additions are correct and well-targeted. The parametrized test properly covers the
[]branch ofnot normalized_messages, and the continuation-token test faithfully exercises theresubscribepath. The only minor design concern is that the new test name (test_run_with_continuation_token_does_not_require_messages) only coversmessages=None, notmessages=[]; the empty-list case with a continuation token is equally valid and untested, but this is a coverage gap rather than a fundamentally wrong approach.
Suggestions
- The assertion
assert response is not Nonein test_run_with_continuation_token_does_not_require_messages is weak. Consider additionally asserting onisinstance(response, AgentResponse)and verifyingmock_a2a_client.call_countto confirm the resubscribe path was actually exercised, similar to how test_resume_via_continuation_token validates the response content. - The new continuation-token test only passes
messages=None. Consider also parametrizing it with[](empty list), since the production guard checksnot normalized_messages, making both values equivalent — the same gap that motivated the parametrize fix in the first test.
Automated review by eavanvalkenburg's agents
Motivation and Context
A2AAgent's
run()method did not callbefore_run/after_runon configured context providers, making it inconsistent with BaseAgent's contract and silently ignoring any context providers passed to it.Fixes #4754
Description
The root cause was that
A2AAgent.run()and its internal_map_a2a_stream()method never invoked the context provider lifecycle hooks thatBaseAgentdefines. The fix adds calls to each provider'sbefore_run()at the start of_map_a2a_stream()and delegates to_run_after_providers()after the stream is fully consumed, matching the pattern used by other agent implementations. ASessionContextis constructed with input messages and populated with the response before calling after-run hooks, and anAgentSessionis auto-created when providers are configured but no session is explicitly passed.Contribution Checklist