Skip to content
Draft
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
34 changes: 34 additions & 0 deletions src/agents/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +422 to +424
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Support id fallback in ToolCallOutputItem.call_id

ToolCallOutputItem.call_id only reads raw_item["call_id"], but the SDK already treats id as a fallback identifier for tool payloads (for example in extract_tool_call_id and output indexing logic), so id-only provider payloads are intentionally handled elsewhere. In that supported scenario, ToolCallItem.call_id resolves to id while ToolCallOutputItem.call_id resolves to None, which breaks the new convenience join path and contradicts the property’s stated correlation behavior.

Useful? React with 👍 / 👎.


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

Expand Down
114 changes: 114 additions & 0 deletions tests/test_items_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading