From d49a1d8b843d7121f6d815813ddab72542d95339 Mon Sep 17 00:00:00 2001 From: Saksham Goyal Date: Mon, 20 Jul 2026 14:39:32 +0530 Subject: [PATCH 1/2] fix: suppress expected lazy skill read errors in traces --- src/agents/sandbox/capabilities/skills.py | 8 +++-- src/agents/sandbox/session/sandbox_session.py | 33 +++++++++++++++---- tests/sandbox/test_session_sinks.py | 23 +++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/agents/sandbox/capabilities/skills.py b/src/agents/sandbox/capabilities/skills.py index 5d58c6703f..5812f592d0 100644 --- a/src/agents/sandbox/capabilities/skills.py +++ b/src/agents/sandbox/capabilities/skills.py @@ -12,9 +12,10 @@ from ...tool import FunctionTool, Tool from ..entries import BaseEntry, Dir, File, LocalDir, LocalFile -from ..errors import LocalDirReadError, SkillsConfigError +from ..errors import LocalDirReadError, SkillsConfigError, WorkspaceReadNotFoundError from ..manifest import Manifest from ..session.base_sandbox_session import BaseSandboxSession +from ..session.sandbox_session import expected_sandbox_error from ..types import User from ..workspace_paths import ( SandboxPathGrant, @@ -236,8 +237,9 @@ async def load_skill( skill_dest = workspace_root / metadata.path skill_md_path = skill_dest / "SKILL.md" try: - handle = await session.read(skill_md_path, user=user) - except Exception: + with expected_sandbox_error(WorkspaceReadNotFoundError): + handle = await session.read(skill_md_path, user=user) + except (FileNotFoundError, WorkspaceReadNotFoundError): handle = None if handle is not None: handle.close() diff --git a/src/agents/sandbox/session/sandbox_session.py b/src/agents/sandbox/session/sandbox_session.py index 1d41bc02fb..91ace87894 100644 --- a/src/agents/sandbox/session/sandbox_session.py +++ b/src/agents/sandbox/session/sandbox_session.py @@ -4,8 +4,9 @@ import ipaddress import time import uuid -from collections.abc import Callable, Coroutine -from contextlib import nullcontext +from collections.abc import Callable, Coroutine, Iterator +from contextlib import contextmanager, nullcontext +from contextvars import ContextVar from functools import wraps from pathlib import Path from typing import Any, TypeVar, cast @@ -28,6 +29,25 @@ T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Coroutine[object, object, object]]) +_expected_sandbox_errors: ContextVar[tuple[type[BaseException], ...]] = ContextVar( + "expected_sandbox_errors", + default=(), +) + + +@contextmanager +def expected_sandbox_error(error_type: type[BaseException]) -> Iterator[None]: + """Mark one sandbox operation error as expected by its caller.""" + + token = _expected_sandbox_errors.set((*_expected_sandbox_errors.get(), error_type)) + try: + yield + finally: + _expected_sandbox_errors.reset(token) + + +def _is_expected_sandbox_error(error: BaseException) -> bool: + return isinstance(error, _expected_sandbox_errors.get()) def instrumented_op( @@ -403,12 +423,13 @@ async def _annotate( value = await run() except Exception as e: duration_ms = (time.monotonic() - t0) * 1000.0 + expected_error = _is_expected_sandbox_error(e) self._apply_trace_finish_data( span=trace_span, op=op, - ok=False, + ok=expected_error, data=start_data, - exc=e, + exc=None if expected_error else e, ) await self._emit_finish_event( op=op, @@ -416,8 +437,8 @@ async def _annotate( parent_span_id=parent_span_id, trace_id=trace_id, duration_ms=duration_ms, - ok=False, - exc=e, + ok=expected_error, + exc=None if expected_error else e, data=start_data, stdout=None, stderr=None, diff --git a/tests/sandbox/test_session_sinks.py b/tests/sandbox/test_session_sinks.py index 09efbd83ca..c234b54406 100644 --- a/tests/sandbox/test_session_sinks.py +++ b/tests/sandbox/test_session_sinks.py @@ -32,6 +32,7 @@ WorkspaceJsonlSink, ) from agents.sandbox.session.base_sandbox_session import BaseSandboxSession +from agents.sandbox.session.sandbox_session import expected_sandbox_error from agents.sandbox.snapshot import LocalSnapshot from agents.tracing import custom_span, trace from tests.testing_processor import fetch_normalized_spans, fetch_ordered_spans @@ -455,6 +456,28 @@ async def test_sandbox_session_error_events_and_traces_include_retryability( assert error_payload["error_retryable"] is False +@pytest.mark.asyncio +async def test_sandbox_session_expected_read_not_found_is_not_an_error_event( + tmp_path: Path, +) -> None: + events: list[SandboxSessionEvent] = [] + instrumentation = Instrumentation( + sinks=[CallbackSink(lambda e, _sess: events.append(e), mode="sync")] + ) + inner = _build_unix_local_session(tmp_path) + + async with SandboxSession(inner, instrumentation=instrumentation) as session: + with expected_sandbox_error(WorkspaceReadNotFoundError): + with pytest.raises(WorkspaceReadNotFoundError): + await session.read(Path("missing.txt")) + + read_finish = [event for event in events if event.op == "read" and event.phase == "finish"][0] + assert isinstance(read_finish, SandboxSessionFinishEvent) + assert read_finish.ok is True + assert read_finish.error_type is None + assert read_finish.error_code is None + + @pytest.mark.asyncio async def test_sandbox_session_ops_nest_under_sdk_trace_and_events_carry_trace_ids( tmp_path: Path, From f84572c874f7562557eb8b09154bab852a2eb90e Mon Sep 17 00:00:00 2001 From: Saksham Goyal Date: Mon, 20 Jul 2026 20:20:09 +0530 Subject: [PATCH 2/2] fix: suppress raw lazy skill probe errors --- src/agents/sandbox/capabilities/skills.py | 5 ++++- tests/sandbox/test_session_sinks.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/agents/sandbox/capabilities/skills.py b/src/agents/sandbox/capabilities/skills.py index 5812f592d0..bb90116f56 100644 --- a/src/agents/sandbox/capabilities/skills.py +++ b/src/agents/sandbox/capabilities/skills.py @@ -237,7 +237,10 @@ async def load_skill( skill_dest = workspace_root / metadata.path skill_md_path = skill_dest / "SKILL.md" try: - with expected_sandbox_error(WorkspaceReadNotFoundError): + with ( + expected_sandbox_error(FileNotFoundError), + expected_sandbox_error(WorkspaceReadNotFoundError), + ): handle = await session.read(skill_md_path, user=user) except (FileNotFoundError, WorkspaceReadNotFoundError): handle = None diff --git a/tests/sandbox/test_session_sinks.py b/tests/sandbox/test_session_sinks.py index c234b54406..6ba24fb29e 100644 --- a/tests/sandbox/test_session_sinks.py +++ b/tests/sandbox/test_session_sinks.py @@ -459,6 +459,7 @@ async def test_sandbox_session_error_events_and_traces_include_retryability( @pytest.mark.asyncio async def test_sandbox_session_expected_read_not_found_is_not_an_error_event( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: events: list[SandboxSessionEvent] = [] instrumentation = Instrumentation( @@ -466,9 +467,15 @@ async def test_sandbox_session_expected_read_not_found_is_not_an_error_event( ) inner = _build_unix_local_session(tmp_path) + async def raw_missing_read(path: Path, *, user: object = None) -> io.IOBase: + _ = (path, user) + raise FileNotFoundError("missing.txt") + + monkeypatch.setattr(inner, "read", raw_missing_read) + async with SandboxSession(inner, instrumentation=instrumentation) as session: - with expected_sandbox_error(WorkspaceReadNotFoundError): - with pytest.raises(WorkspaceReadNotFoundError): + with expected_sandbox_error(FileNotFoundError): + with pytest.raises(FileNotFoundError): await session.read(Path("missing.txt")) read_finish = [event for event in events if event.op == "read" and event.phase == "finish"][0]