Skip to content

Commit edbec4e

Browse files
committed
feat: Add tool origin tracking to ToolCallItem and ToolCallOutputItem
- Add ToolOriginType enum and ToolOrigin dataclass - Add _tool_origin field to FunctionTool - Set tool_origin for MCP tools and agent-as-tool - Extract and set tool_origin in ToolCallItem and ToolCallOutputItem creation - Add comprehensive tests for tool origin tracking
1 parent b39ae9c commit edbec4e

File tree

8 files changed

+431
-4
lines changed

8 files changed

+431
-4
lines changed

src/agents/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
FunctionToolResult,
4646
Tool,
4747
ToolErrorFunction,
48+
ToolOrigin,
49+
ToolOriginType,
4850
_extract_tool_argument_json_error,
4951
default_tool_error_function,
5052
)
@@ -802,6 +804,11 @@ async def _run_agent_tool(context: ToolContext, input_json: str) -> Any:
802804
)
803805
run_agent_tool._is_agent_tool = True
804806
run_agent_tool._agent_instance = self
807+
# Set origin tracking on run_agent (the FunctionTool returned by @function_tool)
808+
run_agent_tool._tool_origin = ToolOrigin(
809+
type=ToolOriginType.AGENT_AS_TOOL,
810+
agent_as_tool=self,
811+
)
805812

806813
return run_agent_tool
807814

src/agents/items.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from .exceptions import AgentsException, ModelBehaviorError
5050
from .logger import logger
5151
from .tool import (
52+
ToolOrigin,
5253
ToolOutputFileContent,
5354
ToolOutputImage,
5455
ToolOutputText,
@@ -248,6 +249,9 @@ class ToolCallItem(RunItemBase[Any]):
248249
description: str | None = None
249250
"""Optional tool description if known at item creation time."""
250251

252+
tool_origin: ToolOrigin | None = field(default=None, repr=False)
253+
"""Information about the origin/source of the tool call. Only set for FunctionTool calls."""
254+
251255

252256
ToolCallOutputTypes: TypeAlias = Union[
253257
FunctionCallOutput,
@@ -271,6 +275,9 @@ class ToolCallOutputItem(RunItemBase[Any]):
271275

272276
type: Literal["tool_call_output_item"] = "tool_call_output_item"
273277

278+
tool_origin: ToolOrigin | None = field(default=None, repr=False)
279+
"""Information about the origin/source of the tool call. Only set for FunctionTool calls."""
280+
274281
def to_input_item(self) -> TResponseInputItem:
275282
"""Converts the tool output into an input item for the next model turn.
276283

src/agents/mcp/util.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
FunctionTool,
2121
Tool,
2222
ToolErrorFunction,
23+
ToolOrigin,
24+
ToolOriginType,
2325
ToolOutputImageDict,
2426
ToolOutputTextDict,
2527
default_tool_error_function,
@@ -301,14 +303,19 @@ async def invoke_func(ctx: ToolContext[Any], input_json: str) -> ToolOutput:
301303
bool | Callable[[RunContextWrapper[Any], dict[str, Any], str], Awaitable[bool]]
302304
) = server._get_needs_approval_for_tool(tool, agent)
303305

304-
return FunctionTool(
306+
function_tool = FunctionTool(
305307
name=tool.name,
306308
description=tool.description or "",
307309
params_json_schema=schema,
308310
on_invoke_tool=invoke_func,
309311
strict_json_schema=is_strict,
310312
needs_approval=needs_approval,
311313
)
314+
function_tool._tool_origin = ToolOrigin(
315+
type=ToolOriginType.MCP,
316+
mcp_server=server,
317+
)
318+
return function_tool
312319

313320
@staticmethod
314321
def _merge_mcp_meta(

src/agents/run_internal/run_loop.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
RawResponsesStreamEvent,
5050
RunItemStreamEvent,
5151
)
52-
from ..tool import Tool, dispose_resolved_computers
52+
from ..tool import FunctionTool, Tool, _get_tool_origin_info, dispose_resolved_computers
5353
from ..tracing import Span, SpanError, agent_span, get_current_trace
5454
from ..tracing.model_tracing import get_model_tracing_impl
5555
from ..tracing.span_data import AgentSpanData
@@ -1216,13 +1216,18 @@ async def run_single_turn_streamed(
12161216
# execution behavior in process_model_response).
12171217
tool_name = getattr(output_item, "name", None)
12181218
tool_description: str | None = None
1219+
tool_origin = None
12191220
if isinstance(tool_name, str) and tool_name in tool_map:
1220-
tool_description = getattr(tool_map[tool_name], "description", None)
1221+
tool = tool_map[tool_name]
1222+
tool_description = getattr(tool, "description", None)
1223+
if isinstance(tool, FunctionTool):
1224+
tool_origin = _get_tool_origin_info(tool)
12211225

12221226
tool_item = ToolCallItem(
12231227
raw_item=cast(ToolCallItemTypes, output_item),
12241228
agent=agent,
12251229
description=tool_description,
1230+
tool_origin=tool_origin,
12261231
)
12271232
streamed_result._event_queue.put_nowait(
12281233
RunItemStreamEvent(item=tool_item, name="tool_called")

src/agents/run_internal/tool_execution.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
ShellCallOutcome,
5353
ShellCommandOutput,
5454
Tool,
55+
_get_tool_origin_info,
5556
resolve_computer,
5657
)
5758
from ..tool_context import ToolContext
@@ -973,10 +974,12 @@ async def run_single_tool(func_tool: FunctionTool, tool_call: ResponseFunctionTo
973974

974975
run_item: RunItem | None = None
975976
if not nested_interruptions:
977+
tool_origin = _get_tool_origin_info(tool_run.function_tool)
976978
run_item = ToolCallOutputItem(
977979
output=result,
978980
raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, result),
979981
agent=agent,
982+
tool_origin=tool_origin,
980983
)
981984
else:
982985
# Skip tool output until nested interruptions are resolved.

src/agents/run_internal/turn_resolution.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
LocalShellTool,
6363
ShellTool,
6464
Tool,
65+
_get_tool_origin_info,
6566
)
6667
from ..tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
6768
from ..tracing import SpanError, handoff_span
@@ -1473,8 +1474,14 @@ def process_model_response(
14731474
raise ModelBehaviorError(error)
14741475

14751476
func_tool = function_map[output.name]
1477+
tool_origin = _get_tool_origin_info(func_tool)
14761478
items.append(
1477-
ToolCallItem(raw_item=output, agent=agent, description=func_tool.description)
1479+
ToolCallItem(
1480+
raw_item=output,
1481+
agent=agent,
1482+
description=func_tool.description,
1483+
tool_origin=tool_origin,
1484+
)
14781485
)
14791486
functions.append(
14801487
ToolRunFunction(

src/agents/tool.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import enum
45
import inspect
56
import json
67
import weakref
@@ -48,6 +49,7 @@
4849
if TYPE_CHECKING:
4950
from .agent import Agent, AgentBase
5051
from .items import RunItem, ToolApprovalItem
52+
from .mcp.server import MCPServer
5153

5254

5355
ToolParams = ParamSpec("ToolParams")
@@ -182,6 +184,59 @@ class ComputerProvider(Generic[ComputerT]):
182184
]
183185

184186

187+
class ToolOriginType(str, enum.Enum):
188+
"""The type of tool origin."""
189+
190+
FUNCTION = "function"
191+
"""Regular Python function tool created via @function_tool decorator."""
192+
193+
MCP = "mcp"
194+
"""MCP server tool converted via MCPUtil.to_function_tool()."""
195+
196+
AGENT_AS_TOOL = "agent_as_tool"
197+
"""Agent converted to tool via agent.as_tool()."""
198+
199+
200+
@dataclass
201+
class ToolOrigin:
202+
"""Information about the origin/source of a function tool."""
203+
204+
type: ToolOriginType
205+
"""The type of tool origin."""
206+
207+
mcp_server: MCPServer | None = None
208+
"""The MCP server object. Only set when type is MCP."""
209+
210+
agent_as_tool: Agent[Any] | None = None
211+
"""The agent object. Only set when type is AGENT_AS_TOOL."""
212+
213+
def __repr__(self) -> str:
214+
"""Custom repr that only includes relevant fields."""
215+
parts = [f"type={self.type.value!r}"]
216+
if self.mcp_server is not None:
217+
parts.append(f"mcp_server_name={self.mcp_server.name!r}")
218+
if self.agent_as_tool is not None:
219+
parts.append(f"agent_as_tool_name={self.agent_as_tool.name!r}")
220+
return f"ToolOrigin({', '.join(parts)})"
221+
222+
223+
def _get_tool_origin_info(function_tool: FunctionTool) -> ToolOrigin | None:
224+
"""Extract origin information from a FunctionTool.
225+
226+
Args:
227+
function_tool: The function tool to extract origin info from.
228+
229+
Returns:
230+
ToolOrigin object if origin is set, otherwise None (defaults to FUNCTION type).
231+
"""
232+
origin = function_tool._tool_origin
233+
if origin is None:
234+
# Default to FUNCTION if not explicitly set
235+
return ToolOrigin(type=ToolOriginType.FUNCTION)
236+
237+
return origin
238+
239+
185240
@dataclass
186241
class FunctionToolResult:
187242
tool: FunctionTool
@@ -264,6 +319,9 @@ class FunctionTool:
264319
_agent_instance: Any = field(default=None, init=False, repr=False)
265320
"""Internal reference to the agent instance if this is an agent-as-tool."""
266321

322+
_tool_origin: ToolOrigin | None = field(default=None, init=False, repr=False)
323+
"""Internal field tracking the origin of this tool (FUNCTION, MCP, or AGENT_AS_TOOL)."""
324+
267325
def __post_init__(self):
268326
if self.strict_json_schema:
269327
self.params_json_schema = ensure_strict_json_schema(self.params_json_schema)

0 commit comments

Comments
 (0)