diff --git a/src/agents/items.py b/src/agents/items.py index c2fcb16ddf..59ad02763f 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -362,6 +362,29 @@ class ToolCallItem(RunItemBase[Any]): tool_origin: ToolOrigin | None = None """Optional metadata describing the source of a function-tool-backed item.""" + @property + def tool_name(self) -> str | None: + """Return the tool name from the raw item if available. + + For function tools (e.g. ``ResponseFunctionToolCall``, ``McpCall``) this is the + function name. For hosted tools (computer-use, file-search, web-search …) the raw + item typically carries no ``name`` field, so ``None`` is returned. + """ + if isinstance(self.raw_item, dict): + candidate = self.raw_item.get("name") or self.raw_item.get("tool_name") + else: + candidate = getattr(self.raw_item, "name", None) or getattr( + self.raw_item, "tool_name", None + ) + return str(candidate) if candidate is not None else None + + @property + def call_id(self) -> str | None: + """Return the call identifier from the raw item if available.""" + if isinstance(self.raw_item, dict): + return self.raw_item.get("call_id") or self.raw_item.get("id") + return getattr(self.raw_item, "call_id", None) or getattr(self.raw_item, "id", None) + ToolCallOutputTypes: TypeAlias = ( FunctionCallOutput @@ -389,6 +412,17 @@ class ToolCallOutputItem(RunItemBase[Any]): tool_origin: ToolOrigin | None = None """Optional metadata describing the source of a function-tool-backed item.""" + @property + def call_id(self) -> str | None: + """Return the call identifier from the raw item if available. + + This matches the ``call_id`` on the corresponding :class:`ToolCallItem` and can be + used to correlate outputs with their originating tool calls without a manual join. + """ + if isinstance(self.raw_item, dict): + return self.raw_item.get("call_id") + return getattr(self.raw_item, "call_id", None) + def to_input_item(self) -> TResponseInputItem: """Converts the tool output into an input item for the next model turn. diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 4244dbd284..07549b20eb 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -615,3 +615,117 @@ def test_tool_call_item_to_input_item_keeps_payload_api_safe() -> None: assert result_dict["type"] == "function_call" assert "title" not in result_dict assert "description" not in result_dict + + +# --------------------------------------------------------------------------- +# ToolCallItem / ToolCallOutputItem convenience properties +# --------------------------------------------------------------------------- + + +def test_tool_call_item_tool_name_from_function_call() -> None: + """tool_name is extracted from ResponseFunctionToolCall.name.""" + agent = Agent(name="test", instructions="test") + raw_item = ResponseFunctionToolCall( + id="fc_1", + call_id="call_abc", + name="my_tool", + arguments="{}", + type="function_call", + status="completed", + ) + item = ToolCallItem(agent=agent, raw_item=raw_item) + assert item.tool_name == "my_tool" + + +def test_tool_call_item_tool_name_from_dict() -> None: + """tool_name is extracted from a raw dict with a 'name' key.""" + agent = Agent(name="test", instructions="test") + raw_item: dict[str, Any] = {"type": "function_call", "name": "dict_tool", "call_id": "cid_1"} + item = ToolCallItem(agent=agent, raw_item=raw_item) + assert item.tool_name == "dict_tool" + + +def test_tool_call_item_tool_name_none_for_computer_call() -> None: + """tool_name is None for tool types that carry no 'name' field.""" + from openai.types.responses.response_computer_tool_call import ResponseComputerToolCall + + agent = Agent(name="test", instructions="test") + raw_item = ResponseComputerToolCall( + id="cu_1", + call_id="call_cu", + type="computer_call", + status="completed", + action={"type": "screenshot"}, + actions=[], + pending_safety_checks=[], + ) + item = ToolCallItem(agent=agent, raw_item=raw_item) + assert item.tool_name is None + + +def test_tool_call_item_call_id_from_function_call() -> None: + """call_id is extracted from ResponseFunctionToolCall.call_id.""" + agent = Agent(name="test", instructions="test") + raw_item = ResponseFunctionToolCall( + id="fc_2", + call_id="call_xyz", + name="another_tool", + arguments="{}", + type="function_call", + status="completed", + ) + item = ToolCallItem(agent=agent, raw_item=raw_item) + assert item.call_id == "call_xyz" + + +def test_tool_call_item_call_id_from_dict() -> None: + """call_id is extracted from a raw dict with a 'call_id' key.""" + agent = Agent(name="test", instructions="test") + raw_item: dict[str, Any] = {"type": "function_call", "name": "t", "call_id": "cid_dict"} + item = ToolCallItem(agent=agent, raw_item=raw_item) + assert item.call_id == "cid_dict" + + +def test_tool_call_output_item_call_id_from_dict() -> None: + """ToolCallOutputItem.call_id is extracted from the raw dict (TypedDict) payload.""" + agent = Agent(name="test", instructions="test") + raw_item: dict[str, Any] = { + "type": "function_call_output", + "call_id": "call_out_1", + "output": "result", + } + item = ToolCallOutputItem(agent=agent, raw_item=raw_item, output="result") + assert item.call_id == "call_out_1" + + +def test_tool_call_output_item_call_id_none_when_missing() -> None: + """ToolCallOutputItem.call_id returns None when the raw item has no call_id.""" + agent = Agent(name="test", instructions="test") + raw_item: dict[str, Any] = {"type": "custom_output", "output": "result"} + item = ToolCallOutputItem(agent=agent, raw_item=raw_item, output="result") + assert item.call_id is None + + +def test_tool_call_items_can_be_joined_by_call_id() -> None: + """Demonstrates the motivating use-case: correlate outputs to calls via call_id.""" + agent = Agent(name="test", instructions="test") + + call = ToolCallItem( + agent=agent, + raw_item=ResponseFunctionToolCall( + id="fc_join", + call_id="call_join", + name="join_tool", + arguments="{}", + type="function_call", + status="completed", + ), + ) + output = ToolCallOutputItem( + agent=agent, + raw_item={"type": "function_call_output", "call_id": "call_join", "output": "done"}, + output="done", + ) + + assert call.call_id == output.call_id + assert call.tool_name == "join_tool"