a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

pre-enter MCP servers before agent.run() to fix cancel scope error

fresh instances per run alone isn't enough — pydantic-ai runs parallel
tool calls via asyncio.create_task, and each does async with self: on
the MCP server. the first task opens the connection (anyio task group),
the last to finish closes it in a different task → RuntimeError.

fix: enter MCP servers in the calling task via AsyncExitStack before
agent.run(). parallel tool calls then only bump the reference count
(1→2→...→1) without ever hitting 0, so the connection open/close
both happen in the same task.

ref: https://github.com/pydantic/pydantic-ai/issues/2818

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 83eea801 947094d5

+10 -1
+10 -1
src/bot/agent.py
··· 1 1 """MCP-enabled agent for phi with structured memory.""" 2 2 3 3 import asyncio 4 + import contextlib 4 5 import ipaddress 5 6 import logging 6 7 import os ··· 408 409 memory=self.memory, 409 410 thread_uri=thread_uri, 410 411 ) 411 - result = await self.agent.run(user_prompt, deps=deps, toolsets=self._mcp_toolsets()) 412 + # Enter MCP servers before agent.run() so the connection is opened 413 + # in this task. Parallel tool calls inside agent.run() then just bump 414 + # the reference count instead of opening/closing across tasks. 415 + # https://github.com/pydantic/pydantic-ai/issues/2818 416 + toolsets = self._mcp_toolsets() 417 + async with contextlib.AsyncExitStack() as stack: 418 + for ts in toolsets: 419 + await stack.enter_async_context(ts) 420 + result = await self.agent.run(user_prompt, deps=deps, toolsets=toolsets) 412 421 logger.info(f"agent decided: {result.output.action}" + (f" - {result.output.text[:80]}" if result.output.text else "") + (f" ({result.output.reason})" if result.output.reason else "")) 413 422 414 423 # Store interaction and extract observations