Skip to content

Commit 2956373

Browse files
committed
Redact handled function tool trace errors
1 parent 8e015e7 commit 2956373

4 files changed

Lines changed: 141 additions & 24 deletions

File tree

src/agents/run_internal/tool_execution.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
from ..tracing import Span, SpanError, function_span, get_current_trace
8888
from ..util import _coro, _error_tracing
8989
from ..util._approvals import evaluate_needs_approval_setting
90+
from ..util._tool_errors import get_trace_tool_error
9091
from ..util._types import MaybeAwaitable
9192
from ._asyncio_progress import get_function_tool_task_progress_deadline
9293
from .agent_bindings import AgentBindings, bind_public_agent
@@ -152,7 +153,6 @@
152153
"execute_approved_tools",
153154
]
154155

155-
REDACTED_TOOL_ERROR_MESSAGE = "Tool execution failed. Error details are redacted."
156156
TToolSpanResult = TypeVar("TToolSpanResult")
157157
_FUNCTION_TOOL_CANCELLED_DRAIN_SECONDS = 0.25
158158
_FUNCTION_TOOL_CANCELLED_IMMEDIATE_STEP_LIMIT = 64
@@ -1013,11 +1013,6 @@ def format_shell_error(error: Exception | BaseException | Any) -> str:
10131013
return repr(error)
10141014

10151015

1016-
def get_trace_tool_error(*, trace_include_sensitive_data: bool, error_message: str) -> str:
1017-
"""Return a trace-safe tool error string based on the sensitive-data setting."""
1018-
return error_message if trace_include_sensitive_data else REDACTED_TOOL_ERROR_MESSAGE
1019-
1020-
10211016
async def with_tool_function_span(
10221017
*,
10231018
config: RunConfig,
@@ -1736,10 +1731,14 @@ async def _invoke_tool_and_run_post_invoke(
17361731
if result is None:
17371732
raise
17381733

1734+
trace_error = get_trace_tool_error(
1735+
trace_include_sensitive_data=self.config.trace_include_sensitive_data,
1736+
error_message=str(e),
1737+
)
17391738
_error_tracing.attach_error_to_current_span(
17401739
SpanError(
17411740
message="Tool execution cancelled",
1742-
data={"tool_name": func_tool.name, "error": str(e)},
1741+
data={"tool_name": func_tool.name, "error": trace_error},
17431742
)
17441743
)
17451744
real_result = result

src/agents/tool.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from .tool_guardrails import ToolInputGuardrail, ToolOutputGuardrail
6161
from .tracing import SpanError
6262
from .util import _error_tracing
63+
from .util._tool_errors import get_trace_tool_error
6364
from .util._types import MaybeAwaitable
6465

6566
if TYPE_CHECKING:
@@ -422,7 +423,7 @@ class _FailureHandlingFunctionToolInvoker:
422423
def __init__(
423424
self,
424425
invoke_tool_impl: Callable[[ToolContext[Any], str], Awaitable[Any]],
425-
on_handled_error: Callable[[FunctionTool, Exception, str], None],
426+
on_handled_error: Callable[[FunctionTool, Exception, str, ToolContext[Any]], None],
426427
*,
427428
function_tool: FunctionTool | None = None,
428429
) -> None:
@@ -457,7 +458,7 @@ async def __call__(self, ctx: ToolContext[Any], input: str) -> Any:
457458
if result is None:
458459
raise
459460

460-
self._on_handled_error(self._function_tool, e, input)
461+
self._on_handled_error(self._function_tool, e, input, ctx)
461462
return result
462463

463464

@@ -466,6 +467,26 @@ def with_function_tool_failure_error_handler(
466467
on_handled_error: Callable[[FunctionTool, Exception, str], None],
467468
) -> Callable[[ToolContext[Any], str], Awaitable[Any]]:
468469
"""Wrap a tool invoker so copied FunctionTools resolve failure policy against themselves."""
470+
471+
def _on_handled_error_with_context(
472+
function_tool: FunctionTool,
473+
error: Exception,
474+
input_json: str,
475+
_context: ToolContext[Any],
476+
) -> None:
477+
on_handled_error(function_tool, error, input_json)
478+
479+
return _with_context_function_tool_failure_error_handler(
480+
invoke_tool_impl,
481+
_on_handled_error_with_context,
482+
)
483+
484+
485+
def _with_context_function_tool_failure_error_handler(
486+
invoke_tool_impl: Callable[[ToolContext[Any], str], Awaitable[Any]],
487+
on_handled_error: Callable[[FunctionTool, Exception, str, ToolContext[Any]], None],
488+
) -> Callable[[ToolContext[Any], str], Awaitable[Any]]:
489+
"""Wrap a tool invoker with context-aware handled-error reporting."""
469490
return _FailureHandlingFunctionToolInvoker(invoke_tool_impl, on_handled_error)
470491

471492

@@ -475,7 +496,7 @@ def _build_wrapped_function_tool(
475496
description: str,
476497
params_json_schema: dict[str, Any],
477498
invoke_tool_impl: Callable[[ToolContext[Any], str], Awaitable[Any]],
478-
on_handled_error: Callable[[FunctionTool, Exception, str], None],
499+
on_handled_error: Callable[[FunctionTool, Exception, str, ToolContext[Any]], None],
479500
failure_error_function: ToolErrorFunction | None | object = _UNSET_FAILURE_ERROR_FUNCTION,
480501
strict_json_schema: bool = True,
481502
is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
@@ -493,7 +514,7 @@ def _build_wrapped_function_tool(
493514
tool_origin: ToolOrigin | None = None,
494515
) -> FunctionTool:
495516
"""Create a FunctionTool with copied-tool-aware failure handling bound in one place."""
496-
on_invoke_tool = with_function_tool_failure_error_handler(
517+
on_invoke_tool = _with_context_function_tool_failure_error_handler(
497518
invoke_tool_impl,
498519
on_handled_error,
499520
)
@@ -1377,24 +1398,36 @@ def _build_handled_function_tool_error_handler(
13771398
span_message_for_json_decode_error: str | None = None,
13781399
include_input_json_in_logs: bool = True,
13791400
include_tool_name_in_log_messages: bool = True,
1380-
) -> Callable[[FunctionTool, Exception, str], None]:
1401+
) -> Callable[[FunctionTool, Exception, str, ToolContext[Any]], None]:
13811402
"""Create a consistent handled-error reporter for wrapped FunctionTools."""
13821403

1383-
def _on_handled_error(function_tool: FunctionTool, error: Exception, input_json: str) -> None:
1404+
def _on_handled_error(
1405+
function_tool: FunctionTool,
1406+
error: Exception,
1407+
input_json: str,
1408+
context: ToolContext[Any],
1409+
) -> None:
13841410
json_decode_error = _extract_tool_argument_json_error(error)
13851411
if json_decode_error is not None and span_message_for_json_decode_error is not None:
13861412
resolved_span_message = span_message_for_json_decode_error
13871413
span_error_detail = str(json_decode_error)
13881414
else:
13891415
resolved_span_message = span_message
13901416
span_error_detail = str(error)
1417+
trace_include_sensitive_data = (
1418+
context.run_config is None or context.run_config.trace_include_sensitive_data
1419+
)
1420+
trace_error = get_trace_tool_error(
1421+
trace_include_sensitive_data=trace_include_sensitive_data,
1422+
error_message=span_error_detail,
1423+
)
13911424

13921425
_error_tracing.attach_error_to_current_span(
13931426
SpanError(
13941427
message=resolved_span_message,
13951428
data={
13961429
"tool_name": function_tool.name,
1397-
"error": span_error_detail,
1430+
"error": trace_error,
13981431
},
13991432
)
14001433
)

src/agents/util/_tool_errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Helpers for rendering tool errors in trace-safe form."""
2+
3+
REDACTED_TOOL_ERROR_MESSAGE = "Tool execution failed. Error details are redacted."
4+
5+
6+
def get_trace_tool_error(*, trace_include_sensitive_data: bool, error_message: str) -> str:
7+
"""Return a trace-safe tool error string based on the sensitive-data setting."""
8+
return error_message if trace_include_sensitive_data else REDACTED_TOOL_ERROR_MESSAGE

tests/test_run_step_execution.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@
9393
)
9494

9595

96-
def _function_span_names() -> list[str]:
97-
names: list[str] = []
96+
def _function_spans() -> list[dict[str, Any]]:
97+
function_spans: list[dict[str, Any]] = []
9898
for span in SPAN_PROCESSOR_TESTING.get_ordered_spans(including_empty=True):
9999
exported = span.export()
100100
if not exported:
@@ -104,6 +104,16 @@ def _function_span_names() -> list[str]:
104104
continue
105105
if span_data.get("type") != "function":
106106
continue
107+
function_spans.append(exported)
108+
return function_spans
109+
110+
111+
def _function_span_names() -> list[str]:
112+
names: list[str] = []
113+
for exported in _function_spans():
114+
span_data = exported.get("span_data")
115+
if not isinstance(span_data, dict):
116+
continue
107117
name = span_data.get("name")
108118
if isinstance(name, str):
109119
names.append(name)
@@ -534,14 +544,7 @@ async def _error_tool() -> str:
534544
run_config=RunConfig(trace_include_sensitive_data=False),
535545
)
536546

537-
function_spans = []
538-
for span in SPAN_PROCESSOR_TESTING.get_ordered_spans(including_empty=True):
539-
exported = span.export()
540-
if not exported:
541-
continue
542-
span_data = exported.get("span_data")
543-
if isinstance(span_data, dict) and span_data.get("type") == "function":
544-
function_spans.append(exported)
547+
function_spans = _function_spans()
545548

546549
assert len(function_spans) == 1
547550
error = function_spans[0]["error"]
@@ -551,6 +554,43 @@ async def _error_tool() -> str:
551554
assert "secret-token-123" not in str(error)
552555

553556

557+
@pytest.mark.asyncio
558+
async def test_default_function_tool_error_trace_respects_sensitive_data_setting():
559+
async def _error_tool() -> str:
560+
raise ValueError("secret-token-123")
561+
562+
error_tool = function_tool(_error_tool, name_override="error_tool")
563+
agent = Agent(name="test", tools=[error_tool])
564+
response = ModelResponse(
565+
output=[get_function_tool_call("error_tool", "{}", call_id="1")],
566+
usage=Usage(),
567+
response_id=None,
568+
)
569+
570+
with trace("test"):
571+
result = await get_execute_result(
572+
agent,
573+
response,
574+
run_config=RunConfig(trace_include_sensitive_data=False),
575+
)
576+
577+
assert len(result.generated_items) == 2
578+
assert isinstance(result.next_step, NextStepRunAgain)
579+
assert_item_is_function_tool_call_output(
580+
result.generated_items[1],
581+
"An error occurred while running the tool. Please try again. Error: secret-token-123",
582+
)
583+
584+
function_spans = _function_spans()
585+
586+
assert len(function_spans) == 1
587+
error = function_spans[0]["error"]
588+
assert error["message"] == "Error running tool (non-fatal)"
589+
assert error["data"]["tool_name"] == "error_tool"
590+
assert error["data"]["error"] == "Tool execution failed. Error details are redacted."
591+
assert "secret-token-123" not in str(error)
592+
593+
554594
@pytest.mark.asyncio
555595
async def test_multiple_tool_calls_still_raise_when_sibling_cancelled():
556596
async def _ok_tool() -> str:
@@ -812,6 +852,43 @@ async def _cancel_tool() -> str:
812852
)
813853

814854

855+
@pytest.mark.asyncio
856+
async def test_cancelled_function_tool_error_trace_respects_sensitive_data_setting():
857+
async def _cancel_tool() -> str:
858+
raise asyncio.CancelledError("secret-token-123")
859+
860+
cancel_tool = function_tool(_cancel_tool, name_override="cancel_tool")
861+
agent = Agent(name="test", tools=[cancel_tool])
862+
response = ModelResponse(
863+
output=[get_function_tool_call("cancel_tool", "{}", call_id="1")],
864+
usage=Usage(),
865+
response_id=None,
866+
)
867+
868+
with trace("test"):
869+
result = await get_execute_result(
870+
agent,
871+
response,
872+
run_config=RunConfig(trace_include_sensitive_data=False),
873+
)
874+
875+
assert len(result.generated_items) == 2
876+
assert isinstance(result.next_step, NextStepRunAgain)
877+
assert_item_is_function_tool_call_output(
878+
result.generated_items[1],
879+
"An error occurred while running the tool. Please try again. Error: secret-token-123",
880+
)
881+
882+
function_spans = _function_spans()
883+
884+
assert len(function_spans) == 1
885+
error = function_spans[0]["error"]
886+
assert error["message"] == "Tool execution cancelled"
887+
assert error["data"]["tool_name"] == "cancel_tool"
888+
assert error["data"]["error"] == "Tool execution failed. Error details are redacted."
889+
assert "secret-token-123" not in str(error)
890+
891+
815892
@pytest.mark.asyncio
816893
async def test_multiple_tool_calls_surface_hook_failure_over_sibling_cancellation():
817894
hook_started = asyncio.Event()

0 commit comments

Comments
 (0)