[Bug]: DefaultRequestHandler runs follow-up messages on a task with the first request's contextvars
Maintainer thường phản hồi trong vòng 2 ngày
Đánh giá
Issue này chưa được đánh giá.
Mô tả
What happened?
With DefaultRequestHandler (v2), a follow-up message on an existing task runs AgentExecutor.execute() with the contextvars of the request that created the task, not the request that sent the follow-up. A common case is a reply after the task went input-required.
Any per-request contextvar set by ASGI middleware or by the ServerCallContextBuilder comes through stale: auth tokens, tenant or caller identity, request-scoped tracing. ServerCallContext.state is correct per message. Only contextvars are wrong.
Reproduced on 1.1.2 and 1.2.2, for both SendMessage and SendStreamingMessage. 0.3.x is not affected because its handler started a new producer per message.
Cause
ActiveTask.start() creates the producer with asyncio.create_task(self._run_producer()) during the first request, so the producer copies that request's context once. Later requests call ActiveTask.enqueue_request(), which only puts the RequestContext on _request_queue. The producer then calls execute() from the first request's context. A task parked in input-required keeps its ActiveTask (#1296), so a reply hours later still sees the first request's values, which may by then be an expired token.
Reproduction
import asyncio, contextvars, uuid
import httpx
from a2a.helpers import new_task_from_user_message
from a2a.server.agent_execution import AgentExecutor
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import AgentCapabilities, AgentCard
from starlette.applications import Starlette
REQUEST_TAG = contextvars.ContextVar("request_tag", default="unset")
class TagMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
token = REQUEST_TAG.set(dict(scope.get("headers", [])).get(b"x-tag", b"unset").decode())
try:
await self.app(scope, receive, send)
finally:
REQUEST_TAG.reset(token)
class Executor(AgentExecutor):
async def execute(self, context, event_queue):
print(f"execute({context.get_user_input()!r}) sees request_tag={REQUEST_TAG.get()!r}")
task = context.current_task or new_task_from_user_message(context.message)
updater = TaskUpdater(event_queue, task.id, task.context_id)
if context.current_task:
await updater.complete()
else:
await event_queue.enqueue_event(task)
await updater.requires_input()
async def cancel(self, context, event_queue):
pass
card = AgentCard(name="a", description="a", version="1", capabilities=AgentCapabilities())
handler = DefaultRequestHandler(agent_executor=Executor(), task_store=InMemoryTaskStore(), agent_card=card)
app = Starlette(routes=create_jsonrpc_routes(request_handler=handler, rpc_url="/"))
app.add_middleware(TagMiddleware)
async def send(client, tag, text, task=None):
message = {"messageId": str(uuid.uuid4()), "role": "ROLE_USER", "parts": [{"text": text}]}
if task:
message |= {"taskId": task["id"], "contextId": task["contextId"]}
body = {"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": {"message": message}}
resp = await client.post("/", json=body, headers={"x-tag": tag, "A2A-Version": "1.0"})
return resp.json()["result"]["task"]
async def main():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://t") as client:
task = await send(client, "request-1", "book a flight")
await send(client, "request-2", "Friday", task)
asyncio.run(main())
Actual:
execute('book a flight') sees request_tag='request-1'
execute('Friday') sees request_tag='request-1'
Expected:
execute('book a flight') sees request_tag='request-1'
execute('Friday') sees request_tag='request-2'
Proposed fix
Capture the sender's context in enqueue_request() and run execute() in it:
async def enqueue_request(self, request_context):
request_id = uuid.uuid4()
await self._request_queue.put((request_context, request_id, contextvars.copy_context()))
return request_id
# in _run_producer()
request_context, request_id, sender_context = await self._request_queue.get()
...
await sender_context.run(
asyncio.create_task,
self._agent_executor.execute(request_context, self._event_queue_agent),
)
I applied this patch to 1.2.2 locally and the reproduction prints request-2 for the follow-up, for both SendMessage and SendStreamingMessage. Context.run(asyncio.create_task, ...) works on Python 3.10. Awaiting the child task keeps cancellation working, because cancelling the producer cancels the child. The MCP Python SDK fixed the same problem this way in modelcontextprotocol/python-sdk#2298.
Downstream workaround
bedrock-agentcore works around this today without touching a2a internals. It wraps SimpleRequestContextBuilder to store contextvars.copy_context() in call_context.state, and wraps the executor to run execute() in that copy (aws/bedrock-agentcore-sdk-python#690). A fix here would let downstream projects delete that code, and it would also cover users who build DefaultRequestHandler directly.
Relevant log output
No response
Code of Conduct
- I agree to follow this project's Code of Conduct
- Ngôn ngữ chính
- Python
- Star
- 2.2k
- Fork
- 509
- Merge trung bình
- 3 ngày 18 giờ
- Pull request đã merge (30 ngày)
- 44
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của a2aproject/a2a-python
-
[Bug]: REST task/request id sanitizationCó thể đã có người làm @Linux2010 đã nhận 102 ngày trước. Đang mởmaintainers-only
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
a2aproject/a2a-python#805 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
[Feat]: Cluster mode: detect and recover tasks abandoned by a crashed replica (heartbeat/lease)Có thể đã có người làm @rohityan đã nhận 1 ngày trước. Đang mở
a2aproject/a2a-python#1324 · 2 người được giao ·
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: Cluster mode: SubscribeToTask on a replica holding a paused ActiveTask returns a stale INPUT_REQUIRED snapshot and closesCó thể đã có người làm @rohityan đã nhận 1 ngày trước. Đang mở
a2aproject/a2a-python#1323 · 2 người được giao ·
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: After a streamed task is cancelled, its background producer never finishesCó thể đã có người làm @rohityan đã nhận 1 ngày trước. Đang mở
a2aproject/a2a-python#1322 · 2 người được giao ·
Maintainer thường phản hồi trong vòng 2 ngày
-
v0.3 gRPC and REST SendMessage without configuration run non-blockingCó thể đã có người làm @rohityan đã nhận 2 ngày trước. Đang mở
a2aproject/a2a-python#1321 · 2 người được giao ·
Maintainer thường phản hồi trong vòng 2 ngày
Tất cả issue của a2aproject/a2a-python
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
UKGovernmentBEIS/inspect_ai#5781 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Bump .cicd to wamp-cicd 4c2f9ac: `just land` refuses open A18 decisions, `just where` lists themĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 82/100
crossbario/cfxdb#139 ·
-
Bump .cicd to wamp-cicd 4c2f9ac: `just land` refuses open A18 decisions, `just where` lists themĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 84/100
crossbario/txaio#241 ·
-
UX
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
mediajunkie/piper-morgan-product#1963 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100