Skip to content

Commit 8ab7335

Browse files
seratchhabema
andcommitted
feat: #2228 persist tool origin metadata in run items
Co-authored-by: Hassan Abu Alhaj <136383052+habema@users.noreply.github.com>
1 parent 3a52673 commit 8ab7335

13 files changed

Lines changed: 520 additions & 15 deletions

File tree

src/agents/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@
159159
ShellToolLocalSkill,
160160
ShellToolSkillReference,
161161
Tool,
162+
ToolOrigin,
163+
ToolOriginType,
162164
ToolOutputFileContent,
163165
ToolOutputFileContentDict,
164166
ToolOutputImage,
@@ -358,6 +360,8 @@ def enable_verbose_stdout_logging():
358360
"MCPApprovalResponseItem",
359361
"ToolCallItem",
360362
"ToolCallOutputItem",
363+
"ToolOrigin",
364+
"ToolOriginType",
361365
"ReasoningItem",
362366
"ItemHelpers",
363367
"RunHooks",

src/agents/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
FunctionToolResult,
4747
Tool,
4848
ToolErrorFunction,
49+
ToolOrigin,
50+
ToolOriginType,
4951
_build_handled_function_tool_error_handler,
5052
_build_wrapped_function_tool,
5153
_log_function_tool_invocation,
@@ -854,6 +856,11 @@ async def dispatch_stream_events() -> None:
854856
strict_json_schema=True,
855857
is_enabled=is_enabled,
856858
needs_approval=needs_approval,
859+
tool_origin=ToolOrigin(
860+
type=ToolOriginType.AGENT_AS_TOOL,
861+
agent_name=self.name,
862+
agent_tool_name=tool_name_resolved,
863+
),
857864
)
858865
run_agent_tool._is_agent_tool = True
859866
run_agent_tool._agent_instance = self

src/agents/items.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from .exceptions import AgentsException, ModelBehaviorError
5555
from .logger import logger
5656
from .tool import (
57+
ToolOrigin,
5758
ToolOutputFileContent,
5859
ToolOutputImage,
5960
ToolOutputText,
@@ -358,6 +359,9 @@ class ToolCallItem(RunItemBase[Any]):
358359
title: str | None = None
359360
"""Optional short display label if known at item creation time."""
360361

362+
tool_origin: ToolOrigin | None = None
363+
"""Optional metadata describing the source of a function-tool-backed item."""
364+
361365

362366
ToolCallOutputTypes: TypeAlias = Union[
363367
FunctionCallOutput,
@@ -382,6 +386,9 @@ class ToolCallOutputItem(RunItemBase[Any]):
382386

383387
type: Literal["tool_call_output_item"] = "tool_call_output_item"
384388

389+
tool_origin: ToolOrigin | None = None
390+
"""Optional metadata describing the source of a function-tool-backed item."""
391+
385392
def to_input_item(self) -> TResponseInputItem:
386393
"""Converts the tool output into an input item for the next model turn.
387394

src/agents/mcp/util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
FunctionTool,
2727
Tool,
2828
ToolErrorFunction,
29+
ToolOrigin,
30+
ToolOriginType,
2931
ToolOutputImageDict,
3032
ToolOutputTextDict,
3133
_build_handled_function_tool_error_handler,
@@ -284,6 +286,10 @@ def to_function_tool(
284286
strict_json_schema=is_strict,
285287
needs_approval=needs_approval,
286288
mcp_title=resolve_mcp_tool_title(tool),
289+
tool_origin=ToolOrigin(
290+
type=ToolOriginType.MCP,
291+
mcp_server_name=server.name,
292+
),
287293
)
288294
return function_tool
289295

src/agents/run_internal/approvals.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ..agent import Agent
1515
from ..items import ItemHelpers, RunItem, ToolApprovalItem, ToolCallOutputItem, TResponseInputItem
16+
from ..tool import ToolOrigin
1617
from .items import ReasoningItemIdPolicy, run_item_to_input_item
1718

1819
# --------------------------
@@ -28,6 +29,7 @@ def append_approval_error_output(
2829
tool_name: str,
2930
call_id: str | None,
3031
message: str,
32+
tool_origin: ToolOrigin | None = None,
3133
) -> None:
3234
"""Emit a synthetic tool output so users see why an approval failed."""
3335
error_tool_call = _build_function_tool_call_for_approval_error(tool_call, tool_name, call_id)
@@ -36,6 +38,7 @@ def append_approval_error_output(
3638
output=message,
3739
raw_item=ItemHelpers.tool_call_output_item(error_tool_call, message),
3840
agent=agent,
41+
tool_origin=tool_origin,
3942
)
4043
)
4144

src/agents/run_internal/items.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def function_rejection_item(
256256
*,
257257
rejection_message: str = REJECTION_MESSAGE,
258258
scope_id: str | None = None,
259+
tool_origin: Any = None,
259260
) -> ToolCallOutputItem:
260261
"""Build a ToolCallOutputItem representing a rejected function tool call."""
261262
if isinstance(tool_call, ResponseFunctionToolCall):
@@ -264,6 +265,7 @@ def function_rejection_item(
264265
output=rejection_message,
265266
raw_item=ItemHelpers.tool_call_output_item(tool_call, rejection_message),
266267
agent=agent,
268+
tool_origin=tool_origin,
267269
)
268270

269271

src/agents/run_internal/run_loop.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from collections.abc import Awaitable, Callable, Mapping
1212
from typing import Any, TypeVar, cast
1313

14-
from openai.types.responses import Response, ResponseCompletedEvent, ResponseOutputItemDoneEvent
14+
from openai.types.responses import (
15+
Response,
16+
ResponseCompletedEvent,
17+
ResponseFunctionToolCall,
18+
ResponseOutputItemDoneEvent,
19+
)
1520
from openai.types.responses.response_output_item import McpCall, McpListTools
1621
from openai.types.responses.response_prompt_param import ResponsePromptParam
1722
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
@@ -62,7 +67,7 @@
6267
RawResponsesStreamEvent,
6368
RunItemStreamEvent,
6469
)
65-
from ..tool import FunctionTool, Tool, dispose_resolved_computers
70+
from ..tool import FunctionTool, Tool, dispose_resolved_computers, get_function_tool_origin
6671
from ..tracing import Span, SpanError, agent_span, get_current_trace
6772
from ..tracing.model_tracing import get_model_tracing_impl
6873
from ..tracing.span_data import AgentSpanData
@@ -131,6 +136,7 @@
131136
from .streaming import stream_step_items_to_queue, stream_step_result_to_queue
132137
from .tool_actions import ApplyPatchAction, ComputerAction, LocalShellAction, ShellAction
133138
from .tool_execution import (
139+
build_litellm_json_tool_call,
134140
coerce_shell_call,
135141
execute_apply_patch_calls,
136142
execute_computer_actions,
@@ -1351,8 +1357,16 @@ async def rewind_model_request() -> None:
13511357
matched_tool = (
13521358
tool_map.get(tool_lookup_key) if tool_lookup_key is not None else None
13531359
)
1360+
if (
1361+
matched_tool is None
1362+
and output_schema is not None
1363+
and isinstance(output_item, ResponseFunctionToolCall)
1364+
and output_item.name == "json_tool_call"
1365+
):
1366+
matched_tool = build_litellm_json_tool_call(output_item)
13541367
tool_description: str | None = None
13551368
tool_title: str | None = None
1369+
tool_origin = None
13561370
if isinstance(output_item, McpCall):
13571371
metadata = hosted_mcp_tool_metadata.get(
13581372
(output_item.server_label, output_item.name)
@@ -1363,12 +1377,14 @@ async def rewind_model_request() -> None:
13631377
elif matched_tool is not None:
13641378
tool_description = getattr(matched_tool, "description", None)
13651379
tool_title = getattr(matched_tool, "_mcp_title", None)
1380+
tool_origin = get_function_tool_origin(matched_tool)
13661381

13671382
tool_item = ToolCallItem(
13681383
raw_item=cast(ToolCallItemTypes, output_item),
13691384
agent=agent,
13701385
description=tool_description,
13711386
title=tool_title,
1387+
tool_origin=tool_origin,
13721388
)
13731389
streamed_result._event_queue.put_nowait(
13741390
RunItemStreamEvent(item=tool_item, name="tool_called")

src/agents/run_internal/tool_execution.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
ShellCallOutcome,
7272
ShellCommandOutput,
7373
Tool,
74+
ToolOrigin,
75+
get_function_tool_origin,
7476
invoke_function_tool,
7577
maybe_invoke_function_tool_failure_error_function,
7678
resolve_computer,
@@ -980,6 +982,7 @@ async def on_invoke_tool(_ctx: ToolContext[Any], value: Any) -> Any:
980982
on_invoke_tool=on_invoke_tool,
981983
strict_json_schema=True,
982984
is_enabled=True,
985+
_emit_tool_origin=False,
983986
)
984987

985988

@@ -1541,6 +1544,7 @@ async def _maybe_execute_tool_approval(
15411544
tool_call,
15421545
rejection_message=rejection_message,
15431546
scope_id=self.tool_state_scope_id,
1547+
tool_origin=get_function_tool_origin(func_tool),
15441548
),
15451549
)
15461550

@@ -1735,6 +1739,7 @@ def _build_function_tool_results(self) -> list[FunctionToolResult]:
17351739
output=result,
17361740
raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, result),
17371741
agent=self.agent,
1742+
tool_origin=get_function_tool_origin(tool_run.function_tool),
17381743
)
17391744
else:
17401745
# Skip tool output until nested interruptions are resolved.
@@ -1926,14 +1931,22 @@ async def execute_approved_tools(
19261931
if isinstance(tool_name, str) and tool_name:
19271932
tool_map[tool_name] = tool
19281933

1929-
def _append_error(message: str, *, tool_call: Any, tool_name: str, call_id: str) -> None:
1934+
def _append_error(
1935+
message: str,
1936+
*,
1937+
tool_call: Any,
1938+
tool_name: str,
1939+
call_id: str,
1940+
tool_origin: ToolOrigin | None = None,
1941+
) -> None:
19301942
append_approval_error_output(
19311943
message=message,
19321944
tool_call=tool_call,
19331945
tool_name=tool_name,
19341946
call_id=call_id,
19351947
generated_items=generated_items,
19361948
agent=agent,
1949+
tool_origin=tool_origin,
19371950
)
19381951

19391952
async def _resolve_tool_run(
@@ -1961,14 +1974,25 @@ async def _resolve_tool_run(
19611974

19621975
call_id = extract_tool_call_id(tool_call)
19631976
if not call_id:
1977+
resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
1978+
if resolved_tool is None and tool_namespace is None:
1979+
resolved_tool = tool_map.get(tool_name)
19641980
_append_error(
19651981
message="Tool approval item missing call ID.",
19661982
tool_call=tool_call,
19671983
tool_name=tool_name,
19681984
call_id="unknown",
1985+
tool_origin=(
1986+
get_function_tool_origin(resolved_tool)
1987+
if isinstance(resolved_tool, FunctionTool)
1988+
else None
1989+
),
19691990
)
19701991
return None
19711992

1993+
resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
1994+
if resolved_tool is None and tool_namespace is None:
1995+
resolved_tool = tool_map.get(tool_name)
19721996
approval_status = context_wrapper.get_approval_status(
19731997
tool_name,
19741998
call_id,
@@ -1977,9 +2001,6 @@ async def _resolve_tool_run(
19772001
tool_lookup_key=tool_lookup_key,
19782002
)
19792003
if approval_status is False:
1980-
resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
1981-
if resolved_tool is None and tool_namespace is None:
1982-
resolved_tool = tool_map.get(tool_name)
19832004
message = REJECTION_MESSAGE
19842005
if isinstance(resolved_tool, FunctionTool):
19852006
message = await resolve_approval_rejection_message(
@@ -1994,6 +2015,11 @@ async def _resolve_tool_run(
19942015
tool_call=tool_call,
19952016
tool_name=tool_name,
19962017
call_id=call_id,
2018+
tool_origin=(
2019+
get_function_tool_origin(resolved_tool)
2020+
if isinstance(resolved_tool, FunctionTool)
2021+
else None
2022+
),
19972023
)
19982024
return None
19992025

@@ -2003,12 +2029,15 @@ async def _resolve_tool_run(
20032029
tool_call=tool_call,
20042030
tool_name=tool_name,
20052031
call_id=call_id,
2032+
tool_origin=(
2033+
get_function_tool_origin(resolved_tool)
2034+
if isinstance(resolved_tool, FunctionTool)
2035+
else None
2036+
),
20062037
)
20072038
return None
20082039

2009-
tool = tool_map.get(approval_key) if approval_key is not None else None
2010-
if tool is None and tool_namespace is None:
2011-
tool = tool_map.get(tool_name)
2040+
tool = resolved_tool
20122041
if tool is None:
20132042
_append_error(
20142043
message=f"Tool '{display_tool_name}' not found.",

src/agents/run_internal/turn_resolution.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
LocalShellTool,
7979
ShellTool,
8080
Tool,
81+
get_function_tool_origin,
8182
)
8283
from ..tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
8384
from ..tracing import SpanError, handoff_span
@@ -718,6 +719,7 @@ async def _record_function_rejection(
718719
tool_call,
719720
rejection_message=rejection_message,
720721
scope_id=tool_state_scope_id,
722+
tool_origin=get_function_tool_origin(function_tool),
721723
)
722724
)
723725
if isinstance(call_id, str):
@@ -1629,11 +1631,19 @@ def _dump_output_item(raw_item: Any) -> dict[str, Any]:
16291631
func_tool = function_map.get(lookup_key) if lookup_key is not None else None
16301632
if func_tool is None:
16311633
if output_schema is not None and output.name == "json_tool_call":
1632-
items.append(ToolCallItem(raw_item=output, agent=agent))
1634+
synthetic_tool = build_litellm_json_tool_call(output)
1635+
items.append(
1636+
ToolCallItem(
1637+
raw_item=output,
1638+
agent=agent,
1639+
description=synthetic_tool.description,
1640+
tool_origin=get_function_tool_origin(synthetic_tool),
1641+
)
1642+
)
16331643
functions.append(
16341644
ToolRunFunction(
16351645
tool_call=output,
1636-
function_tool=build_litellm_json_tool_call(output),
1646+
function_tool=synthetic_tool,
16371647
)
16381648
)
16391649
continue
@@ -1654,6 +1664,7 @@ def _dump_output_item(raw_item: Any) -> dict[str, Any]:
16541664
agent=agent,
16551665
description=func_tool.description,
16561666
title=func_tool._mcp_title,
1667+
tool_origin=get_function_tool_origin(func_tool),
16571668
)
16581669
)
16591670
functions.append(

0 commit comments

Comments
 (0)