Skip to content

Commit c329e92

Browse files
authored
fix(core): redact streaming task exception tracebacks (#5121)
1 parent c641e39 commit c329e92

2 files changed

Lines changed: 87 additions & 20 deletions

File tree

‎src/agents/run.py‎

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2600,28 +2600,29 @@ def run_streamed(
26002600

26012601
# Kick off the actual agent loop in the background and return the streamed result object.
26022602
async def run_loop() -> None:
2603-
await _await_data_redacted_error_boundary(
2604-
lambda: start_streaming(
2605-
starting_input=input_for_result,
2606-
streamed_result=streamed_result,
2607-
starting_agent=starting_agent,
2608-
max_turns=max_turns,
2609-
hooks=hooks,
2610-
context_wrapper=context_wrapper,
2611-
run_config=run_config,
2612-
error_handlers=error_handlers,
2613-
previous_response_id=previous_response_id,
2614-
auto_previous_response_id=auto_previous_response_id,
2615-
conversation_id=conversation_id,
2616-
session=session,
2617-
run_state=run_state,
2618-
trace_workflow_name=trace_workflow_name,
2619-
is_resumed_state=is_resumed_state,
2620-
sandbox_runtime=sandbox_runtime,
2621-
)
2603+
await start_streaming(
2604+
starting_input=input_for_result,
2605+
streamed_result=streamed_result,
2606+
starting_agent=starting_agent,
2607+
max_turns=max_turns,
2608+
hooks=hooks,
2609+
context_wrapper=context_wrapper,
2610+
run_config=run_config,
2611+
error_handlers=error_handlers,
2612+
previous_response_id=previous_response_id,
2613+
auto_previous_response_id=auto_previous_response_id,
2614+
conversation_id=conversation_id,
2615+
session=session,
2616+
run_state=run_state,
2617+
trace_workflow_name=trace_workflow_name,
2618+
is_resumed_state=is_resumed_state,
2619+
sandbox_runtime=sandbox_runtime,
26222620
)
26232621

2624-
streamed_result.run_loop_task = asyncio.create_task(run_loop())
2622+
# Keep the outer task frame inside the boundary so it cannot retain run payloads.
2623+
streamed_result.run_loop_task = asyncio.create_task(
2624+
_await_data_redacted_error_boundary(run_loop)
2625+
)
26252626
if owns_model_provider:
26262627
model_provider = run_config.model_provider
26272628
streamed_result._ensure_model_provider_cleanup_on_completion(

‎tests/test_error_logging_redaction.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,72 @@ async def test_streamed_run_surfaces_redacted_output_validation_error(
14601460
_assert_secret_absent_from_agents_traceback(error, _MODEL_OUTPUT_SECRET)
14611461

14621462

1463+
@pytest.mark.parametrize("redacted", [False, True])
1464+
@pytest.mark.asyncio
1465+
async def test_streamed_task_observers_follow_model_data_policy(
1466+
monkeypatch: pytest.MonkeyPatch,
1467+
redacted: bool,
1468+
) -> None:
1469+
monkeypatch.setattr(_debug, "DONT_LOG_MODEL_DATA", redacted)
1470+
monkeypatch.setattr(_debug, "DONT_LOG_TOOL_DATA", False)
1471+
input_secret = "STREAM_TASK_INPUT_CANARY"
1472+
context = {"private": "STREAM_TASK_CONTEXT_CANARY"}
1473+
session = SimpleListSession(
1474+
session_id="stream-task-redaction",
1475+
history=[{"role": "user", "content": "STREAM_TASK_SESSION_CANARY"}],
1476+
)
1477+
model = ScriptedModel(steps=[[get_text_message(f'{{"answer": "{_MODEL_OUTPUT_SECRET}"}}')]])
1478+
agent = Agent(name="A", model=model, output_type=_RequiredOutput)
1479+
result = Runner.run_streamed(agent, input_secret, context=context, session=session)
1480+
task = result.run_loop_task
1481+
assert task is not None
1482+
observed = asyncio.get_running_loop().create_future()
1483+
1484+
def observe_task(completed: asyncio.Task[Any]) -> None:
1485+
error = completed.exception()
1486+
assert isinstance(error, ModelBehaviorError)
1487+
# Snapshot before a result accessor or stream consumer can sanitize the exception.
1488+
frames = [dict(frame) for frame in _agents_traceback_frame_locals(error)]
1489+
observed.set_result((error, frames, repr(frames)))
1490+
1491+
task.add_done_callback(observe_task)
1492+
error, callback_frames, callback_snapshot = await observed
1493+
with pytest.raises(ModelBehaviorError) as exc_info:
1494+
await task
1495+
assert exc_info.value is error
1496+
await_frames = _agents_traceback_frame_locals(error)
1497+
1498+
if redacted:
1499+
assert error.run_data is None
1500+
assert error.__cause__ is None
1501+
assert error.__context__ is None
1502+
for secret in (
1503+
input_secret,
1504+
context["private"],
1505+
"STREAM_TASK_SESSION_CANARY",
1506+
_MODEL_OUTPUT_SECRET,
1507+
):
1508+
assert secret not in callback_snapshot
1509+
_assert_secret_absent_from_agents_traceback(error, secret)
1510+
for frame in callback_frames + await_frames:
1511+
for value in frame.values():
1512+
assert value is not session
1513+
assert value is not result
1514+
assert value is not context
1515+
assert not isinstance(value, RunContextWrapper)
1516+
else:
1517+
assert _MODEL_OUTPUT_SECRET in str(error)
1518+
assert isinstance(error.__cause__, ValidationError)
1519+
assert input_secret in callback_snapshot
1520+
assert any(frame.get("session") is session for frame in callback_frames)
1521+
assert any(frame.get("session") is session for frame in await_frames)
1522+
1523+
with pytest.raises(ModelBehaviorError) as stream_exc_info:
1524+
async for _ in result.stream_events():
1525+
pass
1526+
assert stream_exc_info.value is error
1527+
1528+
14631529
@pytest.mark.parametrize("redacted", [False, True])
14641530
@pytest.mark.asyncio
14651531
async def test_streamed_run_loop_exception_follows_model_data_policy(

0 commit comments

Comments
 (0)