Python: Add long-running agents and background responses support (#3808)

* Python: Add long-running agents and background responses support

- Add ContinuationToken TypedDict to core types
- Add continuation_token field to ChatResponse, ChatResponseUpdate,
  AgentResponse, and AgentResponseUpdate
- Add background and continuation_token options to OpenAIResponsesOptions
- Implement polling via responses.retrieve() and streaming resumption
  in RawOpenAIResponsesClient
- Propagate continuation tokens through agent run() and
  map_chat_to_agent_update
- Fix streaming telemetry 'Failed to detach context' error in both
  ChatTelemetryLayer and AgentTelemetryLayer by avoiding
  trace.use_span() context attachment for async-managed spans
- Add 14 unit tests for continuation token types and background flows
- Add background_responses sample showing polling and stream resumption

Fixes #2478

* Python: Add A2A long-running task support via ContinuationToken

- Make ContinuationToken provider-agnostic (total=False, optional task_id/context_id fields)
- Add background param to A2AAgent.run() controlling token emission
- Add poll_task() for single-request task state retrieval
- Add resubscribe support via continuation_token param on run()
- Extract _updates_from_task() and _map_a2a_stream() for cleaner code
- Streamline run()/streaming by removing intermediate _stream_updates wrapper
- Update A2A sample to show background=False (default) with link to background_responses sample
- Remove stale BareAgent from __all__
- Add 12 new A2A continuation token tests

* fix logic for overriding continuation token when done

* refactored ContinuationToken setup
This commit is contained in:
Eduard van Valkenburg
2026-02-10 21:37:43 +01:00
committed by GitHub
Unverified
parent 32ba81e990
commit 35097d8c75
12 changed files with 916 additions and 127 deletions
@@ -0,0 +1,139 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIResponsesClient
"""Background Responses Sample.
This sample demonstrates long-running agent operations using the OpenAI
Responses API ``background`` option. Two patterns are shown:
1. **Non-streaming polling** start a background run, then poll with the
``continuation_token`` until the operation completes.
2. **Streaming with resumption** start a background streaming run, simulate
an interruption, and resume from the last ``continuation_token``.
Prerequisites:
- Set the ``OPENAI_API_KEY`` environment variable.
- A model that benefits from background execution (e.g. ``o3``).
"""
# 1. Create the agent with an OpenAI Responses client.
agent = ChatAgent(
name="researcher",
instructions="You are a helpful research assistant. Be concise.",
chat_client=OpenAIResponsesClient(model_id="o3"),
)
async def non_streaming_polling() -> None:
"""Demonstrate non-streaming background run with polling."""
print("=== Non-Streaming Polling ===\n")
thread = agent.get_new_thread()
# 2. Start a background run — returns immediately.
response = await agent.run(
messages="Briefly explain the theory of relativity in two sentences.",
thread=thread,
options={"background": True},
)
print(f"Initial status: continuation_token={'set' if response.continuation_token else 'None'}")
# 3. Poll until the operation completes.
poll_count = 0
while response.continuation_token is not None:
poll_count += 1
await asyncio.sleep(2)
response = await agent.run(
thread=thread,
options={"continuation_token": response.continuation_token},
)
print(f" Poll {poll_count}: continuation_token={'set' if response.continuation_token else 'None'}")
# 4. Done — print the final result.
print(f"\nResult ({poll_count} poll(s)):\n{response.text}\n")
async def streaming_with_resumption() -> None:
"""Demonstrate streaming background run with simulated interruption and resumption."""
print("=== Streaming with Resumption ===\n")
thread = agent.get_new_thread()
# 2. Start a streaming background run.
last_token = None
stream = agent.run(
messages="Briefly list three benefits of exercise.",
stream=True,
thread=thread,
options={"background": True},
)
# 3. Read some chunks, then simulate an interruption.
chunk_count = 0
print("First stream (before interruption):")
async for update in stream:
last_token = update.continuation_token
if update.text:
print(update.text, end="", flush=True)
chunk_count += 1
if chunk_count >= 3:
print("\n [simulated interruption]")
break
# 4. Resume from the last continuation token.
if last_token is not None:
print("Resumed stream:")
stream = agent.run(
stream=True,
thread=thread,
options={"continuation_token": last_token},
)
async for update in stream:
if update.text:
print(update.text, end="", flush=True)
print("\n")
async def main() -> None:
await non_streaming_polling()
await streaming_with_resumption()
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== Non-Streaming Polling ===
Initial status: continuation_token=set
Poll 1: continuation_token=set
Poll 2: continuation_token=None
Result (2 poll(s)):
The theory of relativity, developed by Albert Einstein, consists of special
relativity (1905), which shows that the laws of physics are the same for all
non-accelerating observers and that the speed of light is constant, and general
relativity (1915), which describes gravity as the curvature of spacetime caused
by mass and energy.
=== Streaming with Resumption ===
First stream (before interruption):
Here are three
[simulated interruption]
Resumed stream:
key benefits of regular exercise:
1. **Improved cardiovascular health** ...
2. **Better mental health** ...
3. **Stronger muscles and bones** ...
"""
@@ -2,12 +2,15 @@
This folder contains examples demonstrating how to create and use agents with the A2A (Agent2Agent) protocol from the `agent_framework` package to communicate with remote A2A agents.
By default the A2AAgent waits for the remote agent to finish before returning (`background=False`), so long-running A2A tasks are handled transparently. For advanced scenarios where you need to poll or resubscribe to in-progress tasks using continuation tokens, see the [background responses sample](../../../concepts/background_responses.py).
For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/
## Examples
| File | Description |
|------|-------------|
| [`agent_with_a2a.py`](agent_with_a2a.py) | The simplest way to connect to and use a single A2A agent. Demonstrates agent discovery via agent cards and basic message exchange using the A2A protocol. |
| [`agent_with_a2a.py`](agent_with_a2a.py) | Demonstrates agent discovery, non-streaming and streaming responses using the A2A protocol. |
## Environment Variables
@@ -15,13 +15,18 @@ the A2A protocol. A2A is a standardized communication protocol that enables inte
between different agent systems, allowing agents built with different frameworks and
technologies to communicate seamlessly.
By default the A2AAgent waits for the remote agent to finish before returning (background=False).
This means long-running A2A tasks are handled transparently — the caller simply awaits the result.
For advanced scenarios where you need to poll or resubscribe to in-progress tasks, see the
background_responses sample: samples/concepts/background_responses.py
For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/
Key concepts demonstrated:
- Discovering A2A-compliant agents using AgentCard resolution
- Creating A2AAgent instances to wrap external A2A endpoints
- Converting Agent Framework messages to A2A protocol format
- Handling A2A responses (Messages and Tasks) back to framework types
- Non-streaming request/response
- Streaming responses to receive incremental updates via SSE
To run this sample:
1. Set the A2A_AGENT_HOST environment variable to point to an A2A-compliant agent endpoint
@@ -29,50 +34,75 @@ To run this sample:
2. Ensure the target agent exposes its AgentCard at /.well-known/agent.json
3. Run: uv run python agent_with_a2a.py
The sample will:
- Connect to the specified A2A agent endpoint
- Retrieve and parse the agent's capabilities via its AgentCard
- Send a message using the A2A protocol
- Display the agent's response
Visit the README.md for more details on setting up and running A2A agents.
"""
async def main():
"""Demonstrates connecting to and communicating with an A2A-compliant agent."""
# Get A2A agent host from environment
# 1. Get A2A agent host from environment.
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
print(f"Connecting to A2A agent at: {a2a_agent_host}")
# Initialize A2ACardResolver
# 2. Resolve the agent card to discover capabilities.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
# Get agent card
agent_card = await resolver.get_agent_card()
print(f"Found agent: {agent_card.name} - {agent_card.description}")
# Create A2A agent instance
agent = A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
)
# Invoke the agent and output the result
print("\nSending message to A2A agent...")
# 3. Create A2A agent instance.
async with A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
) as agent:
# 4. Simple request/response — the agent waits for completion internally.
# Even if the remote agent takes a while, background=False (the default)
# means the call blocks until a terminal state is reached.
print("\n--- Non-streaming response ---")
response = await agent.run("What are your capabilities?")
# Print the response
print("\nAgent Response:")
print("Agent Response:")
for message in response.messages:
print(message.text)
print(f" {message.text}")
# 5. Stream a response — the natural model for A2A.
# Updates arrive as Server-Sent Events, letting you observe
# progress in real time as the remote agent works.
print("\n--- Streaming response ---")
async with agent.run("Tell me about yourself", stream=True) as stream:
async for update in stream:
for content in update.contents:
if content.text:
print(f" {content.text}")
response = await stream.get_final_response()
print(f"\nFinal response ({len(response.messages)} message(s)):")
for message in response.messages:
print(f" {message.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Connecting to A2A agent at: http://localhost:5001/
Found agent: MyAgent - A helpful AI assistant
--- Non-streaming response ---
Agent Response:
I can help with code generation, analysis, and general Q&A.
--- Streaming response ---
I am an AI assistant built to help with various tasks.
Final response (1 message(s)):
I am an AI assistant built to help with various tasks.
"""