Python: Propagate thread_id and forwarded_props through AG-UI to A2A context_id#5383
Python: Propagate thread_id and forwarded_props through AG-UI to A2A context_id#5383moonbox3 merged 3 commits intomicrosoft:mainfrom
Conversation
When A2AAgent is used behind the AG-UI protocol, the client thread_id is stored in session.service_session_id but was never forwarded as the A2A context_id. This broke session continuity across the AG-UI → A2A boundary. Add an optional context_id keyword argument to _prepare_message_for_a2a() and pass session.service_session_id from run(). The explicit message.additional_properties["context_id"] still takes precedence. Fixes microsoft#5345 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 91%
✓ Correctness
This PR correctly threads
session.service_session_idas a fallbackcontext_idinto_prepare_message_for_a2a, so that A2A messages get a context_id even when the message itself doesn't carry one explicitly. Theorfallback pattern at line 678 properly prefers message-level context_id over the session-level fallback.additional_propertiesdefaults to{}(confirmed at _types.py:1749-1751), so.get("context_id")safely returnsNonewhen absent, andNone or context_idcorrectly falls through. The two new tests cover both the fallback and precedence scenarios. The pyright ignore comments in the sample files are appropriate for an optional import inside try/except. No correctness issues found.
✓ Security Reliability
This PR adds a fallback
context_idparameter to_prepare_message_for_a2aso that when the message itself doesn't carry acontext_idinadditional_properties, the session'sservice_session_idis used instead. The logic is correct:message.additional_properties.get('context_id') or context_idcorrectly prefers the explicit message-level value over the fallback. The two sample file changes are trivial pyright suppression comments on an optional import already guarded by try/except. Tests cover both the fallback and the precedence behavior. No security, reliability, or resource-leak concerns were found.
✓ Test Coverage
The two new unit tests (
test_prepare_message_for_a2a_uses_fallback_context_idandtest_prepare_message_for_a2a_message_context_id_takes_precedence) adequately cover the core logic change in_prepare_message_for_a2a— both the fallback behavior and the precedence semantics. However, there is no integration-level test verifying thatsession.service_session_idis actually threaded through to the A2A message during arun()call. The existing integration tests (e.g.,test_run_invokes_context_providers) use aMockA2AClientwhosesend_messagedoes not capture the message argument, so the end-to-end wiring from session to context_id is untested. The sample-file pyright-ignore changes are non-functional and need no tests.
✓ Design Approach
The change is minimal and well-targeted. It maps
session.service_session_idto the A2Acontext_idas a fallback when the message has no explicitcontext_idinadditional_properties, which is the correct semantic mapping —service_session_idis how the framework tracks service-managed conversation identity across all providers (OpenAI, Anthropic, etc.), and A2Acontext_idserves the same role on the A2A side. The precedence logic (message.additional_properties.get("context_id") or context_id) correctly lets per-message overrides win. Tests cover both the fallback and the override cases. Thepyright: ignoreadditions in the sample files silence a known type-checker false positive for an optional import guarded by try/except, which is appropriate. No design concerns found.
Suggestions
- Consider adding an integration-level test that calls
agent.run("Hello", session=session)with a session carrying a knownservice_session_id, captures the A2A message passed toMockA2AClient.send_message, and assertsmessage.context_id == session.service_session_id. This would verify the end-to-end wiring at line 300 of_agent.py. CurrentlyMockA2AClient.send_messagediscards itsmessageargument, so the mock would need a small enhancement (e.g.,self.last_message = message) to support this assertion.
Automated review by moonbox3's agents
There was a problem hiding this comment.
Pull request overview
This PR improves session continuity across the Agent Framework → A2A boundary by ensuring an A2A context_id can be derived from the framework AgentSession.service_session_id when the inbound message doesn’t explicitly provide one, and it tightens typing/linting in related Python samples.
Changes:
- Add a kw-only
context_idfallback parameter toA2AAgent._prepare_message_for_a2a()and wire it up fromsession.service_session_idinA2AAgent.run(). - Add unit tests validating fallback
context_idbehavior and precedence rules. - Update optional
orjsonimports in conversation samples to satisfy Pyright strict mode.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| python/packages/a2a/agent_framework_a2a/_agent.py | Adds session-derived fallback context_id wiring when preparing A2A messages. |
| python/packages/a2a/tests/test_a2a_agent.py | Adds tests for fallback context_id and precedence over message-specified context_id. |
| python/samples/02-agents/conversations/file_history_provider.py | Adds Pyright missing-import suppression for optional orjson. |
| python/samples/02-agents/conversations/file_history_provider_conversation_persistence.py | Adds Pyright missing-import suppression for optional orjson. |
Comments suppressed due to low confidence (1)
python/packages/a2a/agent_framework_a2a/_agent.py:599
- The
_prepare_message_for_a2adocstring wasn’t updated to document the newcontext_idkw-only parameter and its precedence rules (message.additional_properties['context_id'] vs fallback). Please update the docstring so callers and future maintainers understand when/why the fallback is applied.
def _prepare_message_for_a2a(self, message: Message, *, context_id: str | None = None) -> A2AMessage:
"""Prepare a Message for the A2A protocol.
Transforms Agent Framework Message objects into A2A protocol Messages by:
- Converting all message contents to appropriate A2A Part types
- Mapping text content to TextPart objects
- Converting file references (URI/data/hosted_file) to FilePart objects
- Preserving metadata and additional properties from the original message
- Setting the role to 'user' as framework messages are treated as user input
"""
…ft#5345) - Enhance MockA2AClient.send_message to capture last_message for assertions - Add test_run_passes_session_service_session_id_as_context_id: verifies run() passes session.service_session_id through to A2A message context_id - Add test_run_message_context_id_takes_precedence_over_session: verifies explicit message context_id wins over session fallback - Update _prepare_message_for_a2a docstring to document context_id param and its precedence rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt passing of context_id / thread_id in A2A/AG-UI implementations
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 95% | Result: All clear
Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach
Automated review by moonbox3's agents
Motivation and Context
When using the AG-UI protocol to front an A2A agent, the client-supplied
thread_idwas never forwarded as the A2Acontext_id, andforwarded_propsfrom the AG-UI input payload were silently dropped. This broke session continuity and history tracing across the AG-UI → MAF → A2A boundary.Fixes #5345
Description
In
A2AAgent,_prepare_message_for_a2anow accepts an optionalcontext_idfallback that is populated fromsession.service_session_idwhen the message itself does not carry an explicitcontext_id. In the AG-UI layer, bothrun_agent_streamandrun_workflow_streamextractforwarded_props(or its camelCase variantforwardedProps) from the input payload and propagate it—via session metadata for the agent path and viafunction_invocation_kwargsfor the workflow path—with a signature check to safely skip workflows whoserun()method doesn't accept the extra kwarg. The_build_safe_metadatahelper was also hardened to drop oversized values instead of truncating them into invalid JSON.Contribution Checklist