Skip to content

Commit 4de6a06

Browse files
fix: budget isolated session setup failures
Co-authored-by: Codex <noreply@openai.com>
1 parent a075f12 commit 4de6a06

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

src/agents/mcp/server.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,14 +1213,25 @@ async def _call_tool_with_isolated_retry(
12131213
try:
12141214
return await asyncio.shield(request_task), False
12151215
except _SharedSessionRequestNeedsIsolation:
1216-
async with self._isolated_client_session() as session:
1216+
exit_stack = AsyncExitStack()
1217+
try:
1218+
session = await exit_stack.enter_async_context(self._isolated_client_session())
1219+
except asyncio.CancelledError:
1220+
await exit_stack.aclose()
1221+
raise
1222+
except BaseException as exc:
1223+
await exit_stack.aclose()
1224+
raise _IsolatedSessionRetryFailed() from exc
1225+
try:
12171226
try:
12181227
result = await self._call_tool_with_session(session, tool_name, arguments, meta)
12191228
return result, True
12201229
except asyncio.CancelledError:
12211230
raise
12221231
except BaseException as exc:
12231232
raise _IsolatedSessionRetryFailed() from exc
1233+
finally:
1234+
await exit_stack.aclose()
12241235
except asyncio.CancelledError:
12251236
if not request_task.done():
12261237
request_task.cancel()

tests/mcp/test_client_session_retries.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,13 @@ async def call_tool(self, tool_name, arguments, meta=None):
209209

210210

211211
class TimeoutSession:
212-
def __init__(self):
212+
def __init__(self, message: str = "timed out"):
213213
self.call_tool_attempts = 0
214+
self.message = message
214215

215216
async def call_tool(self, tool_name, arguments, meta=None):
216217
self.call_tool_attempts += 1
217-
raise httpx.TimeoutException("timed out")
218+
raise httpx.TimeoutException(self.message)
218219

219220

220221
class IsolatedRetrySession:
@@ -255,6 +256,29 @@ async def get_prompt(self, name, arguments=None):
255256
raise NotImplementedError
256257

257258

259+
class IsolatedSessionEnterFailure:
260+
def __init__(self, server: "EnterFailingStreamableHttpServer", message: str):
261+
self.server = server
262+
self.message = message
263+
264+
async def __aenter__(self):
265+
self.server.isolated_enter_attempts += 1
266+
raise httpx.TimeoutException(self.message)
267+
268+
async def __aexit__(self, exc_type, exc, tb):
269+
return False
270+
271+
272+
class EnterFailingStreamableHttpServer(DummyStreamableHttpServer):
273+
def __init__(self, shared_session: object, *, isolated_message: str):
274+
super().__init__(shared_session, IsolatedRetrySession())
275+
self.isolated_enter_attempts = 0
276+
self._isolated_message = isolated_message
277+
278+
def _isolated_client_session(self):
279+
return IsolatedSessionEnterFailure(self, self._isolated_message)
280+
281+
258282
@pytest.mark.asyncio
259283
async def test_streamable_http_retries_cancelled_request_on_isolated_session():
260284
shared_session = CancelledToolSession()
@@ -305,18 +329,34 @@ async def test_streamable_http_does_not_isolated_retry_without_retry_budget():
305329

306330
@pytest.mark.asyncio
307331
async def test_streamable_http_counts_isolated_retry_against_retry_budget():
308-
shared_session = TimeoutSession()
309-
isolated_session = TimeoutSession()
332+
shared_session = TimeoutSession("shared timed out")
333+
isolated_session = TimeoutSession("isolated timed out")
310334
server = DummyStreamableHttpServer(shared_session, isolated_session)
311335
server.max_retry_attempts = 2
312336

313-
with pytest.raises(httpx.TimeoutException, match="timed out"):
337+
with pytest.raises(httpx.TimeoutException, match="shared timed out"):
314338
await server.call_tool("tool", None)
315339

316340
assert shared_session.call_tool_attempts == 2
317341
assert isolated_session.call_tool_attempts == 1
318342

319343

344+
@pytest.mark.asyncio
345+
async def test_streamable_http_counts_isolated_session_setup_failure_against_retry_budget():
346+
shared_session = TimeoutSession("shared timed out")
347+
server = EnterFailingStreamableHttpServer(
348+
shared_session,
349+
isolated_message="isolated setup timed out",
350+
)
351+
server.max_retry_attempts = 2
352+
353+
with pytest.raises(httpx.TimeoutException, match="shared timed out"):
354+
await server.call_tool("tool", None)
355+
356+
assert shared_session.call_tool_attempts == 2
357+
assert server.isolated_enter_attempts == 1
358+
359+
320360
@pytest.mark.asyncio
321361
async def test_streamable_http_does_not_retry_mixed_exception_groups():
322362
isolated_session = IsolatedRetrySession()

0 commit comments

Comments
 (0)