From 0c2422950278b60f682e0988c9bde82b0feb5293 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Fri, 21 Aug 2026 14:37:32 -0500 Subject: [PATCH 1/2] fix(items): strip created_by when replaying RunItems as input ModelResponse.to_input_items already dropped output-only created_by, but RunItem.to_input_item still forwarded it on function calls and tool outputs used for sessions and to_input_list. --- src/agents/items.py | 10 ++------ tests/test_items_helpers.py | 40 ++++++++++++++++++++++++++++++++ tests/test_run_internal_items.py | 23 ++++++++++++++++++ 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/agents/items.py b/src/agents/items.py index a4da32fe97..f3fa5d8c67 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -150,14 +150,7 @@ def _get_agent_via_weakref(self, attr_name: str, ref_name: str) -> Any: def to_input_item(self) -> TResponseInputItem: """Converts this item into an input item suitable for passing to the model.""" - if isinstance(self.raw_item, dict): - # We know that input items are dicts, so we can ignore the type error - return self.raw_item # type: ignore - elif isinstance(self.raw_item, BaseModel): - # All output items are Pydantic models that can be converted to input items. - return self.raw_item.model_dump(exclude_unset=True) # type: ignore - else: - raise AgentsException(f"Unexpected raw item type: {type(self.raw_item)}") + return _output_item_to_input_item(self.raw_item) @dataclass @@ -491,6 +484,7 @@ def to_input_item(self) -> TResponseInputItem: if isinstance(outcome, dict): if outcome.get("type") == "exit": entry["outcome"] = outcome + payload.pop("created_by", None) return cast(TResponseInputItem, payload) return super().to_input_item() diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 264f631092..11ed51fa1c 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -621,6 +621,46 @@ def test_to_input_items_strips_created_by_for_non_tool_search_items() -> None: assert all("created_by" not in item for item in input_items) +def test_tool_call_item_to_input_item_strips_created_by() -> None: + """RunItem replay must strip output-only created_by, matching ModelResponse.to_input_items.""" + agent = Agent(name="A") + call = ResponseFunctionToolCall.model_validate( + { + "id": "fc_1", + "arguments": "{}", + "call_id": "call_1", + "name": "lookup", + "type": "function_call", + "created_by": "server", + } + ) + item = ToolCallItem(agent=agent, raw_item=call) + replayed = item.to_input_item() + assert isinstance(replayed, dict) + assert replayed["type"] == "function_call" + assert "created_by" not in replayed + assert getattr(call, "created_by", None) == "server" + + +def test_tool_call_output_item_to_input_item_strips_created_by() -> None: + agent = Agent(name="A") + item = ToolCallOutputItem( + agent=agent, + raw_item={ + "type": "function_call_output", + "call_id": "call_1", + "output": "ok", + "created_by": "server", + }, + output="ok", + ) + replayed = item.to_input_item() + assert isinstance(replayed, dict) + assert replayed["type"] == "function_call_output" + assert "created_by" not in replayed + assert item.raw_item["created_by"] == "server" + + def test_to_input_items_strips_nested_created_by_from_shell_call_output() -> None: """``shell_call_output`` carries ``created_by`` at the item level and inside each output chunk. diff --git a/tests/test_run_internal_items.py b/tests/test_run_internal_items.py index 2b991ea342..1cdc793213 100644 --- a/tests/test_run_internal_items.py +++ b/tests/test_run_internal_items.py @@ -898,6 +898,29 @@ def test_run_item_to_input_item_strips_tool_search_created_by() -> None: assert "created_by" not in converted_output +def test_run_item_to_input_item_strips_function_call_created_by() -> None: + agent = Agent(name="A") + tool_call = ToolCallItem( + agent=agent, + raw_item=ResponseFunctionToolCall.model_validate( + { + "id": "fc_1", + "arguments": "{}", + "call_id": "call_1", + "name": "lookup", + "type": "function_call", + "created_by": "server", + } + ), + ) + + converted = run_items.run_item_to_input_item(tool_call) + + assert isinstance(converted, dict) + assert converted["type"] == "function_call" + assert "created_by" not in converted + + def test_run_item_to_input_item_omits_tool_call_metadata() -> None: agent = Agent(name="A") tool_call = ToolCallItem( From 44e0d20460c95cf833f9f5cb23679cf58cb1c704 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Fri, 21 Aug 2026 23:39:48 -0500 Subject: [PATCH 2/2] fix(items): sanitize dict shell output replay --- src/agents/items.py | 3 +-- tests/test_items_helpers.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/agents/items.py b/src/agents/items.py index f3fa5d8c67..49f734b1d0 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -484,8 +484,7 @@ def to_input_item(self) -> TResponseInputItem: if isinstance(outcome, dict): if outcome.get("type") == "exit": entry["outcome"] = outcome - payload.pop("created_by", None) - return cast(TResponseInputItem, payload) + return _output_item_to_input_item(payload) return super().to_input_item() diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 11ed51fa1c..df96b65243 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -661,6 +661,45 @@ def test_tool_call_output_item_to_input_item_strips_created_by() -> None: assert item.raw_item["created_by"] == "server" +def test_dict_shell_call_output_item_to_input_item_sanitizes_without_mutation() -> None: + agent = Agent(name="A") + raw_item = { + "type": "shell_call_output", + "call_id": "call_1", + "status": "completed", + "shell_output": "legacy", + "provider_data": {"provider": "value"}, + "created_by": "server", + "output": [ + { + "stdout": "ok", + "stderr": "", + "outcome": {"type": "exit", "exit_code": 0}, + "created_by": "server", + } + ], + } + original_chunk = raw_item["output"][0] + item = ToolCallOutputItem(agent=agent, raw_item=raw_item, output="ok") + + replayed = item.to_input_item() + + assert replayed == { + "type": "shell_call_output", + "call_id": "call_1", + "output": [ + { + "stdout": "ok", + "stderr": "", + "outcome": {"type": "exit", "exit_code": 0}, + } + ], + } + assert raw_item["created_by"] == "server" + assert original_chunk["created_by"] == "server" + assert raw_item["output"][0] is original_chunk + + def test_to_input_items_strips_nested_created_by_from_shell_call_output() -> None: """``shell_call_output`` carries ``created_by`` at the item level and inside each output chunk.