Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@
ShellToolLocalSkill,
ShellToolSkillReference,
Tool,
ToolOrigin,
ToolOriginType,
ToolOutputFileContent,
ToolOutputFileContentDict,
ToolOutputImage,
Expand Down Expand Up @@ -358,6 +360,8 @@ def enable_verbose_stdout_logging():
"MCPApprovalResponseItem",
"ToolCallItem",
"ToolCallOutputItem",
"ToolOrigin",
"ToolOriginType",
"ReasoningItem",
"ItemHelpers",
"RunHooks",
Expand Down
7 changes: 7 additions & 0 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
FunctionToolResult,
Tool,
ToolErrorFunction,
ToolOrigin,
ToolOriginType,
_build_handled_function_tool_error_handler,
_build_wrapped_function_tool,
_log_function_tool_invocation,
Expand Down Expand Up @@ -854,6 +856,11 @@ async def dispatch_stream_events() -> None:
strict_json_schema=True,
is_enabled=is_enabled,
needs_approval=needs_approval,
tool_origin=ToolOrigin(
type=ToolOriginType.AGENT_AS_TOOL,
agent_name=self.name,
agent_tool_name=tool_name_resolved,
),
)
run_agent_tool._is_agent_tool = True
run_agent_tool._agent_instance = self
Expand Down
10 changes: 10 additions & 0 deletions src/agents/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from .exceptions import AgentsException, ModelBehaviorError
from .logger import logger
from .tool import (
ToolOrigin,
ToolOutputFileContent,
ToolOutputImage,
ToolOutputText,
Expand Down Expand Up @@ -358,6 +359,9 @@ class ToolCallItem(RunItemBase[Any]):
title: str | None = None
"""Optional short display label if known at item creation time."""

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


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

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

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

def to_input_item(self) -> TResponseInputItem:
"""Converts the tool output into an input item for the next model turn.

Expand Down Expand Up @@ -493,6 +500,9 @@ class ToolApprovalItem(RunItemBase[Any]):
tool_namespace: str | None = None
"""Optional Responses API namespace for function-tool approvals."""

tool_origin: ToolOrigin | None = None
"""Optional metadata describing where the approved tool call came from."""

tool_lookup_key: FunctionToolLookupKey | None = field(
default=None,
kw_only=True,
Expand Down
6 changes: 6 additions & 0 deletions src/agents/mcp/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
FunctionTool,
Tool,
ToolErrorFunction,
ToolOrigin,
ToolOriginType,
ToolOutputImageDict,
ToolOutputTextDict,
_build_handled_function_tool_error_handler,
Expand Down Expand Up @@ -284,6 +286,10 @@ def to_function_tool(
strict_json_schema=is_strict,
needs_approval=needs_approval,
mcp_title=resolve_mcp_tool_title(tool),
tool_origin=ToolOrigin(
type=ToolOriginType.MCP,
mcp_server_name=server.name,
),
)
return function_tool

Expand Down
3 changes: 3 additions & 0 deletions src/agents/run_internal/approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from ..agent import Agent
from ..items import ItemHelpers, RunItem, ToolApprovalItem, ToolCallOutputItem, TResponseInputItem
from ..tool import ToolOrigin
from .items import ReasoningItemIdPolicy, run_item_to_input_item

# --------------------------
Expand All @@ -28,6 +29,7 @@ def append_approval_error_output(
tool_name: str,
call_id: str | None,
message: str,
tool_origin: ToolOrigin | None = None,
) -> None:
"""Emit a synthetic tool output so users see why an approval failed."""
error_tool_call = _build_function_tool_call_for_approval_error(tool_call, tool_name, call_id)
Expand All @@ -36,6 +38,7 @@ def append_approval_error_output(
output=message,
raw_item=ItemHelpers.tool_call_output_item(error_tool_call, message),
agent=agent,
tool_origin=tool_origin,
)
)

Expand Down
2 changes: 2 additions & 0 deletions src/agents/run_internal/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def function_rejection_item(
*,
rejection_message: str = REJECTION_MESSAGE,
scope_id: str | None = None,
tool_origin: Any = None,
) -> ToolCallOutputItem:
"""Build a ToolCallOutputItem representing a rejected function tool call."""
if isinstance(tool_call, ResponseFunctionToolCall):
Expand All @@ -264,6 +265,7 @@ def function_rejection_item(
output=rejection_message,
raw_item=ItemHelpers.tool_call_output_item(tool_call, rejection_message),
agent=agent,
tool_origin=tool_origin,
)


Expand Down
31 changes: 29 additions & 2 deletions src/agents/run_internal/run_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
from collections.abc import Awaitable, Callable, Mapping
from typing import Any, TypeVar, cast

from openai.types.responses import Response, ResponseCompletedEvent, ResponseOutputItemDoneEvent
from openai.types.responses import (
Response,
ResponseCompletedEvent,
ResponseFunctionToolCall,
ResponseOutputItemDoneEvent,
)
from openai.types.responses.response_output_item import McpCall, McpListTools
from openai.types.responses.response_prompt_param import ResponsePromptParam
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
Expand Down Expand Up @@ -62,7 +67,14 @@
RawResponsesStreamEvent,
RunItemStreamEvent,
)
from ..tool import FunctionTool, Tool, dispose_resolved_computers
from ..tool import (
FunctionTool,
Tool,
ToolOrigin,
ToolOriginType,
dispose_resolved_computers,
get_function_tool_origin,
)
from ..tracing import Span, SpanError, agent_span, get_current_trace
from ..tracing.model_tracing import get_model_tracing_impl
from ..tracing.span_data import AgentSpanData
Expand Down Expand Up @@ -131,6 +143,7 @@
from .streaming import stream_step_items_to_queue, stream_step_result_to_queue
from .tool_actions import ApplyPatchAction, ComputerAction, LocalShellAction, ShellAction
from .tool_execution import (
build_litellm_json_tool_call,
coerce_shell_call,
execute_apply_patch_calls,
execute_computer_actions,
Expand Down Expand Up @@ -1351,24 +1364,38 @@ async def rewind_model_request() -> None:
matched_tool = (
tool_map.get(tool_lookup_key) if tool_lookup_key is not None else None
)
if (
matched_tool is None
and output_schema is not None
and isinstance(output_item, ResponseFunctionToolCall)
and output_item.name == "json_tool_call"
):
matched_tool = build_litellm_json_tool_call(output_item)
tool_description: str | None = None
tool_title: str | None = None
tool_origin = None
if isinstance(output_item, McpCall):
metadata = hosted_mcp_tool_metadata.get(
(output_item.server_label, output_item.name)
)
if metadata is not None:
tool_description = metadata.description
tool_title = metadata.title
tool_origin = ToolOrigin(
type=ToolOriginType.MCP,
mcp_server_name=output_item.server_label,
)
elif matched_tool is not None:
tool_description = getattr(matched_tool, "description", None)
tool_title = getattr(matched_tool, "_mcp_title", None)
tool_origin = get_function_tool_origin(matched_tool)

tool_item = ToolCallItem(
raw_item=cast(ToolCallItemTypes, output_item),
agent=agent,
description=tool_description,
title=tool_title,
tool_origin=tool_origin,
)
streamed_result._event_queue.put_nowait(
RunItemStreamEvent(item=tool_item, name="tool_called")
Expand Down
46 changes: 39 additions & 7 deletions src/agents/run_internal/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
ShellCallOutcome,
ShellCommandOutput,
Tool,
ToolOrigin,
get_function_tool_origin,
invoke_function_tool,
maybe_invoke_function_tool_failure_error_function,
resolve_computer,
Expand Down Expand Up @@ -980,6 +982,7 @@ async def on_invoke_tool(_ctx: ToolContext[Any], value: Any) -> Any:
on_invoke_tool=on_invoke_tool,
strict_json_schema=True,
is_enabled=True,
_emit_tool_origin=False,
)


Expand All @@ -992,6 +995,7 @@ async def resolve_approval_status(
context_wrapper: RunContextWrapper[Any],
tool_namespace: str | None = None,
tool_lookup_key: FunctionToolLookupKey | None = None,
tool_origin: ToolOrigin | None = None,
on_approval: Callable[[RunContextWrapper[Any], ToolApprovalItem], Any] | None = None,
) -> tuple[bool | None, ToolApprovalItem]:
"""Build approval item, run on_approval hook if needed, and return latest approval status."""
Expand All @@ -1000,6 +1004,7 @@ async def resolve_approval_status(
raw_item=raw_item,
tool_name=tool_name,
tool_namespace=tool_namespace,
tool_origin=tool_origin,
tool_lookup_key=tool_lookup_key,
)
approval_status = context_wrapper.get_approval_status(
Expand Down Expand Up @@ -1503,6 +1508,7 @@ async def _maybe_execute_tool_approval(
raw_item=raw_tool_call,
tool_name=func_tool.name,
tool_namespace=tool_namespace,
tool_origin=get_function_tool_origin(func_tool),
tool_lookup_key=tool_lookup_key,
_allow_bare_name_alias=should_allow_bare_name_approval_alias(
func_tool,
Expand Down Expand Up @@ -1541,6 +1547,7 @@ async def _maybe_execute_tool_approval(
tool_call,
rejection_message=rejection_message,
scope_id=self.tool_state_scope_id,
tool_origin=get_function_tool_origin(func_tool),
),
)

Expand Down Expand Up @@ -1735,6 +1742,7 @@ def _build_function_tool_results(self) -> list[FunctionToolResult]:
output=result,
raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, result),
agent=self.agent,
tool_origin=get_function_tool_origin(tool_run.function_tool),
)
else:
# Skip tool output until nested interruptions are resolved.
Expand Down Expand Up @@ -1926,14 +1934,22 @@ async def execute_approved_tools(
if isinstance(tool_name, str) and tool_name:
tool_map[tool_name] = tool

def _append_error(message: str, *, tool_call: Any, tool_name: str, call_id: str) -> None:
def _append_error(
message: str,
*,
tool_call: Any,
tool_name: str,
call_id: str,
tool_origin: ToolOrigin | None = None,
) -> None:
append_approval_error_output(
message=message,
tool_call=tool_call,
tool_name=tool_name,
call_id=call_id,
generated_items=generated_items,
agent=agent,
tool_origin=tool_origin,
)

async def _resolve_tool_run(
Expand Down Expand Up @@ -1961,14 +1977,25 @@ async def _resolve_tool_run(

call_id = extract_tool_call_id(tool_call)
if not call_id:
resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
if resolved_tool is None and tool_namespace is None:
resolved_tool = tool_map.get(tool_name)
_append_error(
message="Tool approval item missing call ID.",
tool_call=tool_call,
tool_name=tool_name,
call_id="unknown",
tool_origin=(
get_function_tool_origin(resolved_tool)
if isinstance(resolved_tool, FunctionTool)
else None
),
)
return None

resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
if resolved_tool is None and tool_namespace is None:
resolved_tool = tool_map.get(tool_name)
approval_status = context_wrapper.get_approval_status(
tool_name,
call_id,
Expand All @@ -1977,9 +2004,6 @@ async def _resolve_tool_run(
tool_lookup_key=tool_lookup_key,
)
if approval_status is False:
resolved_tool = tool_map.get(approval_key) if approval_key is not None else None
if resolved_tool is None and tool_namespace is None:
resolved_tool = tool_map.get(tool_name)
message = REJECTION_MESSAGE
if isinstance(resolved_tool, FunctionTool):
message = await resolve_approval_rejection_message(
Expand All @@ -1994,6 +2018,11 @@ async def _resolve_tool_run(
tool_call=tool_call,
tool_name=tool_name,
call_id=call_id,
tool_origin=(
get_function_tool_origin(resolved_tool)
if isinstance(resolved_tool, FunctionTool)
else None
),
)
return None

Expand All @@ -2003,12 +2032,15 @@ async def _resolve_tool_run(
tool_call=tool_call,
tool_name=tool_name,
call_id=call_id,
tool_origin=(
get_function_tool_origin(resolved_tool)
if isinstance(resolved_tool, FunctionTool)
else None
),
)
return None

tool = tool_map.get(approval_key) if approval_key is not None else None
if tool is None and tool_namespace is None:
tool = tool_map.get(tool_name)
tool = resolved_tool
if tool is None:
_append_error(
message=f"Tool '{display_tool_name}' not found.",
Expand Down
8 changes: 7 additions & 1 deletion src/agents/run_internal/tool_planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
ToolCallOutputItem,
)
from ..run_context import RunContextWrapper
from ..tool import FunctionTool, MCPToolApprovalRequest
from ..tool import FunctionTool, MCPToolApprovalRequest, get_function_tool_origin
from ..tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
from .run_steps import (
ToolRunApplyPatchCall,
Expand Down Expand Up @@ -410,11 +410,17 @@ async def _collect_runs_by_approval(
if approval_status is True:
approved_runs.append(run)
else:
function_tool = get_mapping_or_attr(run, "function_tool")
pending_item = existing_pending or ToolApprovalItem(
agent=agent,
raw_item=get_mapping_or_attr(run, "tool_call"),
tool_name=tool_name,
tool_namespace=get_tool_call_namespace(get_mapping_or_attr(run, "tool_call")),
tool_origin=(
get_function_tool_origin(function_tool)
if isinstance(function_tool, FunctionTool)
else None
),
tool_lookup_key=get_function_tool_lookup_key_for_call(
get_mapping_or_attr(run, "tool_call")
),
Expand Down
Loading
Loading