Python: feat(a2a): use non-streaming transport and return_immediately for background ops (#5963)

* feat(a2a): use non-streaming transport and return_immediately for background ops

When stream=False, use a client configured with streaming=False so the
SDK sends a single HTTP POST to message/send instead of opening an SSE
connection via message/stream. This matches the A2A protocol's design:
non-streaming calls use direct request/response, streaming calls use
Server-Sent Events.

Also sets return_immediately=background on SendMessageConfiguration so
the server respects the caller's intent for background operations.

Changes:
- Create separate streaming and non-streaming internal clients (sharing
  the same httpx connection pool) to match protocol transport semantics
- Select non-streaming client for run(stream=False) calls
- Add SendMessageConfiguration with return_immediately=background
- Fallback to streaming client when non-streaming unavailable (e.g. user
  provides their own client via constructor)
- Add tests for client selection and return_immediately behavior

Resolves microsoft/agent-framework#5936

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR review feedback

- Initialize last_request in MockA2AClient.__init__ for explicit state
- Use 'is not None' instead of truthiness for _non_streaming_client check
- Assert return_immediately propagates through non-streaming client path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: only set configuration when background=True

Only attach SendMessageConfiguration to the request when background=True,
keeping requests minimal and preserving server-side defaults for normal
(foreground) operations. This follows the framework pattern of only
setting optional fields when they have meaningful values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: only set return_immediately for non-streaming background ops

Per the A2A spec, return_immediately only applies to message/send
(non-streaming). It has no effect on streaming operations. Only set
the configuration field when both background=True and stream=False.

Adds test verifying streaming+background does not set return_immediately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-05-21 08:04:56 -07:00
committed by GitHub
Unverified
parent 46326b6b93
commit 289cafcf36
2 changed files with 128 additions and 7 deletions
@@ -44,6 +44,7 @@ class MockA2AClient:
self.subscribe_responses: list[StreamResponse] = []
self.get_task_response: Task | None = None
self.last_message: Any = None
self.last_request: Any = None
def add_message_response(self, message_id: str, text: str, role: str = "agent") -> None:
"""Add a mock Message response."""
@@ -91,6 +92,7 @@ class MockA2AClient:
async def send_message(self, request: Any) -> AsyncIterator[StreamResponse]:
"""Mock send_message method that yields responses."""
self.last_request = request
self.last_message = getattr(request, "message", request)
self.call_count += 1
@@ -745,6 +747,96 @@ async def test_working_task_no_token_without_background(a2a_agent: A2AAgent, moc
assert response.continuation_token is None
async def test_background_sets_return_immediately_on_request(
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
) -> None:
"""Test that background=True sets return_immediately=True on SendMessageRequest configuration."""
mock_a2a_client.add_in_progress_task_response("task-bg", state=TaskState.TASK_STATE_WORKING)
await a2a_agent.run("Background task", background=True)
assert mock_a2a_client.last_request.configuration.return_immediately is True
async def test_foreground_does_not_set_return_immediately(
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
) -> None:
"""Test that background=False (default) does not set configuration on SendMessageRequest."""
mock_a2a_client.add_task_response("task-fg2", [{"id": "art-1", "content": "Done"}])
await a2a_agent.run("Foreground task")
assert mock_a2a_client.last_request.HasField("configuration") is False
async def test_streaming_background_does_not_set_return_immediately(
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
) -> None:
"""Test that background=True with stream=True does not set return_immediately.
Per A2A spec, return_immediately only applies to non-streaming (message/send).
"""
mock_a2a_client.add_task_response("task-sb", [{"id": "art-1", "content": "Streaming bg"}])
updates: list[AgentResponseUpdate] = []
async for update in a2a_agent.run("Stream background", stream=True, background=True):
updates.append(update)
assert mock_a2a_client.last_request.HasField("configuration") is False
async def test_non_streaming_run_uses_non_streaming_client() -> None:
"""Test that stream=False uses the non-streaming client when available."""
streaming_client = MockA2AClient()
non_streaming_client = MockA2AClient()
non_streaming_client.add_task_response("task-ns", [{"id": "art-1", "content": "Non-streaming result"}])
agent = A2AAgent(name="Test Agent", id="test-ns", client=streaming_client, http_client=None)
agent._non_streaming_client = non_streaming_client # type: ignore[assignment]
response = await agent.run("Hello")
# Non-streaming client should have been called
assert non_streaming_client.call_count == 1
assert streaming_client.call_count == 0
assert response.messages[0].text == "Non-streaming result"
assert non_streaming_client.last_request.HasField("configuration") is False
async def test_streaming_run_uses_streaming_client() -> None:
"""Test that stream=True always uses the streaming client."""
streaming_client = MockA2AClient()
non_streaming_client = MockA2AClient()
streaming_client.add_task_response("task-s", [{"id": "art-1", "content": "Streaming result"}])
agent = A2AAgent(name="Test Agent", id="test-s", client=streaming_client, http_client=None)
agent._non_streaming_client = non_streaming_client # type: ignore[assignment]
updates: list[AgentResponseUpdate] = []
async for update in agent.run("Hello", stream=True):
updates.append(update)
# Streaming client should have been called
assert streaming_client.call_count == 1
assert non_streaming_client.call_count == 0
assert updates[0].contents[0].text == "Streaming result"
async def test_non_streaming_client_fallback_when_not_available(
a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient
) -> None:
"""Test that stream=False falls back to streaming client when non-streaming client is unavailable."""
mock_a2a_client.add_task_response("task-fb", [{"id": "art-1", "content": "Fallback result"}])
# a2a_agent is created with client= param so _non_streaming_client is None
assert a2a_agent._non_streaming_client is None
response = await a2a_agent.run("Hello")
assert mock_a2a_client.call_count == 1
assert response.messages[0].text == "Fallback result"
async def test_completed_task_has_no_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None:
"""Test that a completed task does not set a continuation token."""
mock_a2a_client.add_task_response("task-done", [{"id": "art-1", "content": "Result"}])