Files
openai--openai-agents-python/tests/test_server_conversation_tracker.py
T
2026-07-13 12:39:17 +08:00

1029 lines
33 KiB
Python

from types import SimpleNamespace
from typing import Any, cast
import pytest
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses.response_output_item import McpCall, McpListTools, McpListToolsTool
from agents import Agent, HostedMCPTool
from agents.items import (
MCPListToolsItem,
ModelResponse,
RunItem,
ToolApprovalItem,
ToolCallItem,
ToolCallOutputItem,
TResponseInputItem,
)
from agents.lifecycle import RunHooks
from agents.models.fake_id import FAKE_RESPONSES_ID
from agents.result import RunResultStreaming
from agents.run_config import ModelInputData, RunConfig
from agents.run_context import RunContextWrapper
from agents.run_internal.agent_bindings import bind_public_agent
from agents.run_internal.agent_runner_helpers import get_unsent_tool_call_ids_for_interrupted_state
from agents.run_internal.oai_conversation import OpenAIServerConversationTracker
from agents.run_internal.run_loop import get_new_response, run_single_turn_streamed
from agents.run_internal.run_steps import NextStepInterruption
from agents.run_internal.tool_use_tracker import AgentToolUseTracker
from agents.stream_events import RunItemStreamEvent
from agents.usage import Usage
from .fake_model import FakeModel
from .test_responses import get_text_message
class DummyRunItem:
"""Minimal stand-in for RunItem with the attributes used by OpenAIServerConversationTracker."""
def __init__(self, raw_item: dict[str, Any], type: str = "message") -> None:
self.raw_item = raw_item
self.type = type
def _make_hosted_mcp_list_tools(server_label: str, tool_name: str) -> McpListTools:
return McpListTools(
id=f"list_{server_label}",
server_label=server_label,
tools=[
McpListToolsTool(
name=tool_name,
input_schema={},
description="Search the docs.",
annotations={"title": "Search Docs"},
)
],
type="mcp_list_tools",
)
def test_prepare_input_filters_items_seen_by_server_and_tool_calls() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv", previous_response_id=None)
original_input: list[TResponseInputItem] = [
cast(TResponseInputItem, {"id": "input-1", "type": "message"}),
cast(TResponseInputItem, {"id": "input-2", "type": "message"}),
]
new_raw_item = {"type": "message", "content": "hello"}
generated_items = [
DummyRunItem({"id": "server-echo", "type": "message"}),
DummyRunItem(new_raw_item),
DummyRunItem({"call_id": "call-1", "output": "done"}, type="function_call_output_item"),
]
model_response = object.__new__(ModelResponse)
model_response.output = [
cast(Any, {"call_id": "call-1", "output": "prior", "type": "function_call_output"})
]
model_response.usage = Usage()
model_response.response_id = "resp-1"
session_items: list[TResponseInputItem] = [
cast(TResponseInputItem, {"id": "session-1", "type": "message"})
]
tracker.hydrate_from_state(
original_input=original_input,
generated_items=cast(list[Any], generated_items),
model_responses=[model_response],
session_items=session_items,
)
prepared = tracker.prepare_input(
original_input=original_input,
generated_items=cast(list[Any], generated_items),
)
assert prepared == [new_raw_item]
assert tracker.sent_initial_input is True
assert tracker.remaining_initial_input is None
def test_hydrate_from_state_preserves_unsent_outputs_from_interrupted_turn() -> None:
agent = Agent(name="test")
cleanup1_call = ResponseFunctionToolCall(
id="fc_001",
type="function_call",
call_id="call_CLEANUP1",
name="run_cleanup",
arguments='{"target": "temp_files"}',
status="completed",
)
diagnostic_call = ResponseFunctionToolCall(
id="fc_002",
type="function_call",
call_id="call_DIAG",
name="run_diagnostic",
arguments='{"check_name": "thermal"}',
status="completed",
)
cleanup2_call = ResponseFunctionToolCall(
id="fc_003",
type="function_call",
call_id="call_CLEANUP2",
name="run_cleanup",
arguments='{"target": "winsxs_cache"}',
status="completed",
)
model_response = ModelResponse(
output=[cleanup1_call, diagnostic_call, cleanup2_call],
usage=Usage(),
response_id="resp_002",
)
diagnostic_output = ToolCallOutputItem(
agent=agent,
raw_item={
"type": "function_call_output",
"call_id": "call_DIAG",
"output": "Diagnostic completed.",
},
output="Diagnostic completed.",
)
generated_items: list[RunItem] = [
ToolCallItem(agent=agent, raw_item=cleanup1_call),
ToolCallItem(agent=agent, raw_item=diagnostic_call),
ToolCallItem(agent=agent, raw_item=cleanup2_call),
diagnostic_output,
ToolApprovalItem(agent=agent, raw_item=cleanup1_call, tool_name="run_cleanup"),
ToolApprovalItem(agent=agent, raw_item=cleanup2_call, tool_name="run_cleanup"),
]
interrupted_state = SimpleNamespace(
_current_step=NextStepInterruption(interruptions=[]),
_last_processed_response=SimpleNamespace(
handoffs=[],
functions=[
SimpleNamespace(tool_call=cleanup1_call),
SimpleNamespace(tool_call=diagnostic_call),
SimpleNamespace(tool_call=cleanup2_call),
],
computer_actions=[],
custom_tool_calls=[],
local_shell_calls=[],
shell_calls=[],
apply_patch_calls=[],
),
)
tracker = OpenAIServerConversationTracker(previous_response_id="resp_002")
tracker.hydrate_from_state(
original_input="Run cleanup, diagnostics, and cleanup.",
generated_items=generated_items,
model_responses=[model_response],
unsent_tool_call_ids=get_unsent_tool_call_ids_for_interrupted_state(
cast(Any, interrupted_state)
),
)
assert "call_DIAG" not in tracker.server_tool_call_ids
prepared = tracker.prepare_input(
"Run cleanup, diagnostics, and cleanup.",
[
ToolCallItem(agent=agent, raw_item=cleanup1_call),
ToolCallItem(agent=agent, raw_item=diagnostic_call),
ToolCallItem(agent=agent, raw_item=cleanup2_call),
diagnostic_output,
ToolCallOutputItem(
agent=agent,
raw_item={
"type": "function_call_output",
"call_id": "call_CLEANUP1",
"output": "Tool call not approved.",
},
output="Tool call not approved.",
),
ToolCallOutputItem(
agent=agent,
raw_item={
"type": "function_call_output",
"call_id": "call_CLEANUP2",
"output": "Tool call not approved.",
},
output="Tool call not approved.",
),
],
)
assert [
item.get("call_id")
for item in prepared
if isinstance(item, dict) and item.get("type") == "function_call_output"
] == ["call_DIAG", "call_CLEANUP1", "call_CLEANUP2"]
def test_hydrate_from_state_does_not_track_string_initial_input_by_object_identity() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv-init-string", previous_response_id=None
)
tracker.hydrate_from_state(
original_input="hello",
generated_items=[],
model_responses=[],
)
assert tracker.sent_items == []
assert tracker.sent_initial_input is True
assert tracker.remaining_initial_input is None
assert len(tracker.sent_item_fingerprints) == 1
def test_hydrate_from_state_does_not_track_list_initial_input_by_object_identity() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv-init-list", previous_response_id=None
)
original_input = [cast(TResponseInputItem, {"role": "user", "content": "hello"})]
tracker.hydrate_from_state(
original_input=original_input,
generated_items=[],
model_responses=[],
)
assert tracker.sent_items == []
assert tracker.sent_initial_input is True
assert tracker.remaining_initial_input is None
assert len(tracker.sent_item_fingerprints) == 1
def test_mark_input_as_sent_and_rewind_input_respects_remaining_initial_input() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv2", previous_response_id=None)
pending_1: TResponseInputItem = cast(TResponseInputItem, {"id": "p-1", "type": "message"})
pending_2: TResponseInputItem = cast(TResponseInputItem, {"id": "p-2", "type": "message"})
tracker.remaining_initial_input = [pending_1, pending_2]
tracker.mark_input_as_sent(
[pending_1, cast(TResponseInputItem, {"id": "p-2", "type": "message"})]
)
assert tracker.remaining_initial_input is None
tracker.rewind_input([pending_1])
assert tracker.remaining_initial_input == [pending_1]
def test_mark_input_as_sent_uses_raw_generated_source_for_rebuilt_filtered_item() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv2b", previous_response_id=None)
raw_generated_item = {
"type": "function_call_output",
"call_id": "call-2b",
"output": "done",
}
generated_items = [
DummyRunItem(raw_generated_item, type="function_call_output_item"),
]
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
rebuilt_filtered_item = cast(TResponseInputItem, dict(cast(dict[str, Any], prepared[0])))
tracker.mark_input_as_sent([rebuilt_filtered_item])
assert any(item is raw_generated_item for item in tracker.sent_items)
assert all(item is not rebuilt_filtered_item for item in tracker.sent_items)
prepared_again = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert prepared_again == []
def test_hydrate_from_state_skips_restored_tool_search_items_by_object_identity() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv2c", previous_response_id=None)
tool_search_call = {
"type": "tool_search_call",
"queries": [{"search_term": "account balance"}],
}
tool_search_result = {
"type": "tool_search_output",
"results": [{"text": "Balance lookup docs"}],
}
hydrated_items = [
DummyRunItem(tool_search_call, type="tool_search_call_item"),
DummyRunItem(tool_search_result, type="tool_search_output_item"),
]
tracker.hydrate_from_state(
original_input=[],
generated_items=cast(list[Any], hydrated_items),
model_responses=[],
)
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], hydrated_items),
)
assert prepared == []
def test_hydrate_from_state_skips_restored_tool_search_items_by_fingerprint() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv2d", previous_response_id=None)
tool_search_call = {
"type": "tool_search_call",
"queries": [{"search_term": "account balance"}],
}
tool_search_result = {
"type": "tool_search_output",
"results": [{"text": "Balance lookup docs"}],
}
hydrated_items = [
DummyRunItem(tool_search_call, type="tool_search_call_item"),
DummyRunItem(tool_search_result, type="tool_search_output_item"),
]
rebuilt_items = [
DummyRunItem(dict(tool_search_call), type="tool_search_call_item"),
DummyRunItem(dict(tool_search_result), type="tool_search_output_item"),
]
tracker.hydrate_from_state(
original_input=[],
generated_items=cast(list[Any], hydrated_items),
model_responses=[],
)
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], rebuilt_items),
)
assert prepared == []
def test_hydrate_from_state_skips_restored_tool_search_items_when_created_by_is_stripped() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv2d-created-by", previous_response_id=None
)
session_items = [
cast(
TResponseInputItem,
{
"type": "tool_search_call",
"call_id": "tool_search_call_1",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
"created_by": "server",
},
),
cast(
TResponseInputItem,
{
"type": "tool_search_output",
"call_id": "tool_search_call_1",
"execution": "server",
"status": "completed",
"tools": [],
"created_by": "server",
},
),
]
tracker.hydrate_from_state(
original_input=[],
generated_items=[],
model_responses=[],
session_items=session_items,
)
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(
list[RunItem],
[
DummyRunItem(
{
"type": "tool_search_call",
"call_id": "tool_search_call_1",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
type="tool_search_call_item",
),
DummyRunItem(
{
"type": "tool_search_output",
"call_id": "tool_search_call_1",
"execution": "server",
"status": "completed",
"tools": [],
},
type="tool_search_output_item",
),
],
),
)
assert prepared == []
def test_hydrate_from_state_skips_restored_tool_search_items_when_only_ids_differ() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv2d-ids-only", previous_response_id=None
)
session_items = [
cast(
TResponseInputItem,
{
"type": "tool_search_call",
"id": "tool_search_call_saved",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
),
cast(
TResponseInputItem,
{
"type": "tool_search_output",
"id": "tool_search_output_saved",
"execution": "server",
"status": "completed",
"tools": [],
},
),
]
tracker.hydrate_from_state(
original_input=[],
generated_items=[],
model_responses=[],
session_items=session_items,
)
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(
list[RunItem],
[
DummyRunItem(
{
"type": "tool_search_call",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
type="tool_search_call_item",
),
DummyRunItem(
{
"type": "tool_search_output",
"execution": "server",
"status": "completed",
"tools": [],
},
type="tool_search_output_item",
),
],
),
)
assert prepared == []
def test_prepare_input_keeps_repeated_tool_search_items_with_new_ids() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv2d-repeated-search", previous_response_id=None
)
prior_response = object.__new__(ModelResponse)
prior_response.output = [
cast(
Any,
{
"type": "tool_search_call",
"id": "tool_search_call_saved",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
"created_by": "server",
},
),
cast(
Any,
{
"type": "tool_search_output",
"id": "tool_search_output_saved",
"execution": "server",
"status": "completed",
"tools": [],
"created_by": "server",
},
),
]
prior_response.usage = Usage()
prior_response.response_id = "resp-tool-search-repeat-1"
tracker.track_server_items(prior_response)
repeated_items = [
DummyRunItem(
{
"type": "tool_search_call",
"id": "tool_search_call_repeat",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
type="tool_search_call_item",
),
DummyRunItem(
{
"type": "tool_search_output",
"id": "tool_search_output_repeat",
"execution": "server",
"status": "completed",
"tools": [],
},
type="tool_search_output_item",
),
]
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], repeated_items),
)
assert prepared == [
cast(
TResponseInputItem,
{
"type": "tool_search_call",
"id": "tool_search_call_repeat",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
),
cast(
TResponseInputItem,
{
"type": "tool_search_output",
"id": "tool_search_output_repeat",
"execution": "server",
"status": "completed",
"tools": [],
},
),
]
def test_track_server_items_skips_live_tool_search_items_on_next_prepare() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv2e", previous_response_id=None)
tool_search_call = cast(
Any,
{
"type": "tool_search_call",
"call_id": "tool_search_call_live",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
"created_by": "server",
},
)
tool_search_result = cast(
Any,
{
"type": "tool_search_output",
"call_id": "tool_search_call_live",
"execution": "server",
"status": "completed",
"tools": [],
"created_by": "server",
},
)
model_response = object.__new__(ModelResponse)
model_response.output = [tool_search_call, tool_search_result]
model_response.usage = Usage()
model_response.response_id = "resp-tool-search"
tracker.track_server_items(model_response)
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(
list[RunItem],
[
DummyRunItem(
{
"type": "tool_search_call",
"call_id": "tool_search_call_live",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
type="tool_search_call_item",
),
DummyRunItem(
{
"type": "tool_search_output",
"call_id": "tool_search_call_live",
"execution": "server",
"status": "completed",
"tools": [],
},
type="tool_search_output_item",
),
],
),
)
assert prepared == []
def test_track_server_items_filters_pending_tool_search_by_sanitized_fingerprint() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv2e-pending", previous_response_id=None
)
tracker.remaining_initial_input = [
cast(
TResponseInputItem,
{
"type": "tool_search_call",
"call_id": "tool_search_pending",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
},
),
cast(TResponseInputItem, {"id": "keep-me", "type": "message"}),
]
model_response = object.__new__(ModelResponse)
model_response.output = [
cast(
Any,
{
"type": "tool_search_call",
"call_id": "tool_search_pending",
"arguments": {"query": "account balance"},
"execution": "server",
"status": "completed",
"created_by": "server",
},
)
]
model_response.usage = Usage()
model_response.response_id = "resp-tool-search-pending"
tracker.track_server_items(model_response)
assert tracker.remaining_initial_input == [
cast(TResponseInputItem, {"id": "keep-me", "type": "message"})
]
def test_track_server_items_filters_remaining_initial_input_by_fingerprint() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv3", previous_response_id=None)
pending_kept: TResponseInputItem = cast(
TResponseInputItem, {"id": "keep-me", "type": "message"}
)
pending_filtered: TResponseInputItem = cast(
TResponseInputItem,
{"type": "function_call_output", "call_id": "call-2", "output": "x"},
)
tracker.remaining_initial_input = [pending_kept, pending_filtered]
model_response = object.__new__(ModelResponse)
model_response.output = [
cast(Any, {"type": "function_call_output", "call_id": "call-2", "output": "x"})
]
model_response.usage = Usage()
model_response.response_id = "resp-2"
tracker.track_server_items(model_response)
assert tracker.remaining_initial_input == [pending_kept]
def test_prepare_input_does_not_skip_fake_response_ids() -> None:
tracker = OpenAIServerConversationTracker(conversation_id="conv5", previous_response_id=None)
model_response = object.__new__(ModelResponse)
model_response.output = [cast(Any, {"id": FAKE_RESPONSES_ID, "type": "message"})]
model_response.usage = Usage()
model_response.response_id = "resp-3"
tracker.track_server_items(model_response)
raw_item = {"id": FAKE_RESPONSES_ID, "type": "message", "content": "hello"}
generated_items = [DummyRunItem(raw_item)]
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert prepared == [raw_item]
def test_prepare_input_applies_reasoning_item_id_policy_for_generated_items() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv7",
previous_response_id=None,
reasoning_item_id_policy="omit",
)
generated_items = [
DummyRunItem(
{
"type": "reasoning",
"id": "rs_turn_input",
"content": [{"type": "input_text", "text": "reasoning trace"}],
},
type="reasoning_item",
)
]
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert prepared == [
cast(
TResponseInputItem,
{"type": "reasoning", "content": [{"type": "input_text", "text": "reasoning trace"}]},
)
]
def test_prepare_input_does_not_resend_reasoning_item_after_marking_omitted_id_as_sent() -> None:
tracker = OpenAIServerConversationTracker(
conversation_id="conv8",
previous_response_id=None,
reasoning_item_id_policy="omit",
)
generated_items = [
DummyRunItem(
{
"type": "reasoning",
"id": "rs_turn_input",
"content": [{"type": "input_text", "text": "reasoning trace"}],
},
type="reasoning_item",
)
]
first_prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert first_prepared == [
cast(
TResponseInputItem,
{"type": "reasoning", "content": [{"type": "input_text", "text": "reasoning trace"}]},
)
]
tracker.mark_input_as_sent(first_prepared)
second_prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert second_prepared == []
@pytest.mark.asyncio
async def test_get_new_response_marks_filtered_input_as_sent() -> None:
model = FakeModel()
model.set_next_output([get_text_message("ok")])
agent = Agent(name="test", model=model)
tracker = OpenAIServerConversationTracker(conversation_id="conv4", previous_response_id=None)
context_wrapper: RunContextWrapper[dict[str, Any]] = RunContextWrapper(context={})
tool_use_tracker = AgentToolUseTracker()
item_1: TResponseInputItem = cast(TResponseInputItem, {"role": "user", "content": "first"})
item_2: TResponseInputItem = cast(TResponseInputItem, {"role": "user", "content": "second"})
def _filter_input(payload: Any) -> ModelInputData:
return ModelInputData(
input=[payload.model_data.input[0]],
instructions=payload.model_data.instructions,
)
run_config = RunConfig(call_model_input_filter=_filter_input)
await get_new_response(
bind_public_agent(agent),
None,
[item_1, item_2],
None,
[],
[],
RunHooks(),
context_wrapper,
run_config,
tool_use_tracker,
tracker,
None,
)
assert model.last_turn_args["input"] == [item_1]
assert any(item is item_1 for item in tracker.sent_items)
assert all(item is not item_2 for item in tracker.sent_items)
@pytest.mark.asyncio
async def test_run_single_turn_streamed_marks_filtered_input_as_sent() -> None:
model = FakeModel()
model.set_next_output([get_text_message("ok")])
agent = Agent(name="test", model=model)
tracker = OpenAIServerConversationTracker(conversation_id="conv6", previous_response_id=None)
context_wrapper: RunContextWrapper[dict[str, Any]] = RunContextWrapper(context={})
tool_use_tracker = AgentToolUseTracker()
item_1: TResponseInputItem = cast(TResponseInputItem, {"role": "user", "content": "first"})
item_2: TResponseInputItem = cast(TResponseInputItem, {"role": "user", "content": "second"})
def _filter_input(payload: Any) -> ModelInputData:
return ModelInputData(
input=[payload.model_data.input[0]],
instructions=payload.model_data.instructions,
)
run_config = RunConfig(call_model_input_filter=_filter_input)
streamed_result = RunResultStreaming(
input=[item_1, item_2],
new_items=[],
raw_responses=[],
final_output=None,
input_guardrail_results=[],
output_guardrail_results=[],
tool_input_guardrail_results=[],
tool_output_guardrail_results=[],
context_wrapper=context_wrapper,
current_agent=agent,
current_turn=0,
max_turns=1,
_current_agent_output_schema=None,
trace=None,
interruptions=[],
)
await run_single_turn_streamed(
streamed_result,
bind_public_agent(agent),
RunHooks(),
context_wrapper,
run_config,
should_run_agent_start_hooks=False,
tool_use_tracker=tool_use_tracker,
all_tools=[],
server_conversation_tracker=tracker,
)
assert model.last_turn_args["input"] == [item_1]
assert tracker.remaining_initial_input == [item_2]
@pytest.mark.asyncio
async def test_run_single_turn_streamed_seeds_hosted_mcp_metadata_from_pre_step_items() -> None:
model = FakeModel()
mcp_call = McpCall(
id="mcp_call_1",
arguments="{}",
name="search_docs",
server_label="docs_server",
type="mcp_call",
status="completed",
)
model.set_next_output([mcp_call])
agent = Agent(name="test", model=model)
hosted_tool = HostedMCPTool(
tool_config=cast(
Any,
{
"type": "mcp",
"server_label": "docs_server",
"server_url": "https://example.com/mcp",
},
)
)
context_wrapper: RunContextWrapper[dict[str, Any]] = RunContextWrapper(context={})
tool_use_tracker = AgentToolUseTracker()
item_1: TResponseInputItem = cast(TResponseInputItem, {"role": "user", "content": "first"})
pre_step_item = MCPListToolsItem(
agent=agent,
raw_item=_make_hosted_mcp_list_tools("docs_server", "search_docs"),
)
def _filter_input(payload: Any) -> ModelInputData:
return ModelInputData(
input=[payload.model_data.input[0]],
instructions=payload.model_data.instructions,
)
run_config = RunConfig(call_model_input_filter=_filter_input)
streamed_result = RunResultStreaming(
input=[item_1],
new_items=[],
raw_responses=[],
final_output=None,
input_guardrail_results=[],
output_guardrail_results=[],
tool_input_guardrail_results=[],
tool_output_guardrail_results=[],
context_wrapper=context_wrapper,
current_agent=agent,
current_turn=1,
max_turns=2,
_current_agent_output_schema=None,
trace=None,
interruptions=[],
)
streamed_result._model_input_items = [pre_step_item]
await run_single_turn_streamed(
streamed_result,
bind_public_agent(agent),
RunHooks(),
context_wrapper,
run_config,
should_run_agent_start_hooks=False,
tool_use_tracker=tool_use_tracker,
all_tools=[hosted_tool],
)
assert model.last_turn_args["input"] == [item_1]
tool_call_events: list[ToolCallItem] = []
while not streamed_result._event_queue.empty():
queued_event = streamed_result._event_queue.get_nowait()
streamed_result._event_queue.task_done()
if (
isinstance(queued_event, RunItemStreamEvent)
and queued_event.name == "tool_called"
and isinstance(queued_event.item, ToolCallItem)
):
tool_call_events.append(queued_event.item)
assert len(tool_call_events) == 1
assert tool_call_events[0].description == "Search the docs."
assert tool_call_events[0].title == "Search Docs"
@pytest.mark.parametrize("stale_collection_name", ["sent_items", "server_items"])
def test_prepare_input_keeps_fresh_tool_output_when_stale_identity_matches(
stale_collection_name: str,
) -> None:
"""Tracked object identity must not become a stale address-based dedupe key."""
tracker = OpenAIServerConversationTracker(previous_response_id="resp-1")
output_raw_item: dict[str, Any] = {
"type": "function_call_output",
"call_id": "call_FRESH",
"output": "42",
}
tracked_items = getattr(tracker, stale_collection_name)
if isinstance(tracked_items, set):
tracked_items.add(id(output_raw_item))
else:
old_item = {"type": "message", "content": "already tracked"}
tracked_items.append(old_item)
generated_items = [DummyRunItem(output_raw_item, type="function_call_output_item")]
prepared = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
prepared_output_call_ids = [
item.get("call_id")
for item in prepared
if isinstance(item, dict) and item.get("type") == "function_call_output"
]
assert "call_FRESH" in prepared_output_call_ids
def test_prepare_input_dedupes_same_delivered_tool_output_object() -> None:
"""Identity dedupe still skips the exact source object after it is delivered."""
tracker = OpenAIServerConversationTracker(previous_response_id="resp-1")
output_raw_item: dict[str, Any] = {
"type": "function_call_output",
"call_id": "call_X",
"output": "42",
}
generated_items = [DummyRunItem(output_raw_item, type="function_call_output_item")]
first = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert any(isinstance(item, dict) and item.get("call_id") == "call_X" for item in first)
tracker.mark_input_as_sent(first)
assert any(item is output_raw_item for item in tracker.sent_items)
second = tracker.prepare_input(
original_input=[],
generated_items=cast(list[Any], generated_items),
)
assert all(not (isinstance(item, dict) and item.get("call_id") == "call_X") for item in second)