679 lines
22 KiB
Python
679 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from openai.types.responses import (
|
|
ResponseFunctionToolCall,
|
|
ResponseToolSearchCall,
|
|
ResponseToolSearchOutputItem,
|
|
)
|
|
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
|
|
from agents import Agent
|
|
from agents.exceptions import AgentsException
|
|
from agents.items import (
|
|
ReasoningItem,
|
|
ToolCallItem,
|
|
ToolSearchCallItem,
|
|
ToolSearchOutputItem,
|
|
TResponseInputItem,
|
|
coerce_tool_search_output_raw_item,
|
|
)
|
|
from agents.models.fake_id import FAKE_RESPONSES_ID
|
|
from agents.result import RunResult
|
|
from agents.run_context import RunContextWrapper
|
|
from agents.run_internal import items as run_items
|
|
|
|
|
|
def test_drop_orphan_function_calls_preserves_non_mapping_entries() -> None:
|
|
payload: list[Any] = [
|
|
cast(TResponseInputItem, "plain-text-input"),
|
|
cast(TResponseInputItem, {"type": "message", "role": "user", "content": "hello"}),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "orphan_call",
|
|
"name": "orphan",
|
|
"arguments": "{}",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "paired_call",
|
|
"name": "paired",
|
|
"arguments": "{}",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{"type": "function_call_output", "call_id": "paired_call", "output": "ok"},
|
|
),
|
|
cast(TResponseInputItem, {"call_id": "not-a-tool-call"}),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
filtered_values = cast(list[Any], filtered)
|
|
assert "plain-text-input" in filtered_values
|
|
assert cast(dict[str, Any], filtered[1])["type"] == "message"
|
|
assert any(
|
|
isinstance(entry, dict)
|
|
and entry.get("type") == "function_call"
|
|
and entry.get("call_id") == "paired_call"
|
|
for entry in filtered
|
|
)
|
|
assert not any(
|
|
isinstance(entry, dict)
|
|
and entry.get("type") == "function_call"
|
|
and entry.get("call_id") == "orphan_call"
|
|
for entry in filtered
|
|
)
|
|
|
|
|
|
def test_drop_orphan_function_calls_drops_reasoning_preceding_dropped_tool_call() -> None:
|
|
# Regression: reasoning items tied to a now-dropped orphan tool call would otherwise be
|
|
# forwarded to the API and trigger
|
|
# ``Item 'rs_...' of type 'reasoning' was provided without its required following item``.
|
|
payload: list[Any] = [
|
|
cast(TResponseInputItem, {"role": "user", "content": "hi"}),
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_orphan_a", "summary": []}),
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_orphan_b", "summary": []}),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "orphan_call",
|
|
"name": "orphan",
|
|
"arguments": "{}",
|
|
},
|
|
),
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_paired", "summary": []}),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "paired_call",
|
|
"name": "paired",
|
|
"arguments": "{}",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{"type": "function_call_output", "call_id": "paired_call", "output": "ok"},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
reasoning_ids = [
|
|
entry.get("id")
|
|
for entry in filtered
|
|
if isinstance(entry, dict) and entry.get("type") == "reasoning"
|
|
]
|
|
assert reasoning_ids == ["rs_paired"]
|
|
assert not any(
|
|
isinstance(entry, dict)
|
|
and entry.get("type") == "function_call"
|
|
and entry.get("call_id") == "orphan_call"
|
|
for entry in filtered
|
|
)
|
|
|
|
|
|
def test_drop_orphan_function_calls_keeps_lone_reasoning_when_no_tool_calls_dropped() -> None:
|
|
# Server-managed conversations (or compaction) may forward standalone reasoning items whose
|
|
# required following item lives in the server-side conversation. We must not drop those.
|
|
payload: list[Any] = [
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_lone", "summary": []}),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert filtered == payload
|
|
|
|
|
|
def test_drop_orphan_function_calls_handles_tool_search_calls() -> None:
|
|
payload: list[Any] = [
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": "tool_search_orphan",
|
|
"arguments": {"query": "orphan"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": "tool_search_keep",
|
|
"arguments": {"query": "keep"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_output",
|
|
"call_id": "tool_search_keep",
|
|
"execution": "server",
|
|
"status": "completed",
|
|
"tools": [],
|
|
},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert any(
|
|
isinstance(entry, dict)
|
|
and entry.get("type") == "tool_search_call"
|
|
and entry.get("call_id") == "tool_search_keep"
|
|
for entry in filtered
|
|
)
|
|
assert not any(
|
|
isinstance(entry, dict)
|
|
and entry.get("type") == "tool_search_call"
|
|
and entry.get("call_id") == "tool_search_orphan"
|
|
for entry in filtered
|
|
)
|
|
|
|
|
|
def test_drop_orphan_function_calls_preserves_hosted_tool_search_pairs_without_call_ids() -> None:
|
|
payload: list[Any] = [
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": None,
|
|
"arguments": {"query": "keep"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_output",
|
|
"call_id": None,
|
|
"execution": "server",
|
|
"status": "completed",
|
|
"tools": [],
|
|
},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert len(filtered) == 2
|
|
assert cast(dict[str, Any], filtered[0])["type"] == "tool_search_call"
|
|
assert cast(dict[str, Any], filtered[1])["type"] == "tool_search_output"
|
|
|
|
|
|
def test_drop_orphan_function_calls_matches_latest_anonymous_tool_search_call() -> None:
|
|
payload: list[Any] = [
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": None,
|
|
"arguments": {"query": "orphan"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": None,
|
|
"arguments": {"query": "paired"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_output",
|
|
"call_id": None,
|
|
"execution": "server",
|
|
"status": "completed",
|
|
"tools": [],
|
|
},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert [cast(dict[str, Any], item)["type"] for item in filtered] == [
|
|
"tool_search_call",
|
|
"tool_search_output",
|
|
]
|
|
assert cast(dict[str, Any], filtered[0])["arguments"] == {"query": "paired"}
|
|
|
|
|
|
def test_drop_orphan_function_calls_does_not_pair_named_tool_search_with_anonymous_output() -> None:
|
|
payload: list[Any] = [
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_call",
|
|
"call_id": "orphan_search",
|
|
"arguments": {"query": "keep"},
|
|
"execution": "server",
|
|
"status": "completed",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "tool_search_output",
|
|
"call_id": None,
|
|
"execution": "server",
|
|
"status": "completed",
|
|
"tools": [],
|
|
},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert [cast(dict[str, Any], item)["type"] for item in filtered] == ["tool_search_output"]
|
|
|
|
|
|
def test_drop_orphan_function_calls_keeps_reasoning_chain_before_non_dropped_item() -> None:
|
|
payload: list[Any] = [
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_1", "summary": []}),
|
|
cast(TResponseInputItem, {"type": "reasoning", "id": "rs_2", "summary": []}),
|
|
cast(TResponseInputItem, {"type": "message", "role": "assistant", "content": []}),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "orphan_call",
|
|
"name": "orphan",
|
|
"arguments": "{}",
|
|
},
|
|
),
|
|
]
|
|
|
|
filtered = run_items.drop_orphan_function_calls(cast(list[TResponseInputItem], payload))
|
|
|
|
assert [cast(dict[str, Any], item)["id"] for item in filtered[:2]] == ["rs_1", "rs_2"]
|
|
assert [cast(dict[str, Any], item)["type"] for item in filtered] == [
|
|
"reasoning",
|
|
"reasoning",
|
|
"message",
|
|
]
|
|
|
|
|
|
def test_normalize_and_ensure_input_item_format_keep_non_dict_entries() -> None:
|
|
item = cast(TResponseInputItem, "raw-item")
|
|
assert run_items.ensure_input_item_format(item) == item
|
|
assert run_items.normalize_input_items_for_api([item]) == [item]
|
|
|
|
|
|
def test_fingerprint_input_item_handles_edge_cases(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
assert run_items.fingerprint_input_item(None) is None
|
|
|
|
fingerprint = run_items.fingerprint_input_item(
|
|
cast(
|
|
TResponseInputItem, {"id": "id-1", "type": "message", "role": "user", "content": "hi"}
|
|
),
|
|
ignore_ids_for_matching=True,
|
|
)
|
|
assert fingerprint is not None
|
|
assert '"id"' not in fingerprint
|
|
|
|
class _BrokenModelDump:
|
|
def model_dump(self, *_args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
if "warnings" in kwargs:
|
|
raise TypeError("warnings arg unsupported")
|
|
raise RuntimeError("still broken")
|
|
|
|
assert run_items.fingerprint_input_item(_BrokenModelDump()) is None
|
|
assert run_items._model_dump_without_warnings(object()) is None
|
|
|
|
class _Opaque:
|
|
pass
|
|
|
|
monkeypatch.setattr(
|
|
run_items,
|
|
"ensure_input_item_format",
|
|
lambda _item: {"id": "internal-id", "type": "message", "role": "user", "content": "x"},
|
|
)
|
|
opaque_fingerprint = run_items.fingerprint_input_item(_Opaque(), ignore_ids_for_matching=True)
|
|
assert opaque_fingerprint is not None
|
|
assert '"id"' not in opaque_fingerprint
|
|
|
|
|
|
def test_fingerprint_input_item_returns_none_when_serialization_fails(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def _raise_json_error(*_args: Any, **_kwargs: Any) -> str:
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(cast(Any, run_items).json, "dumps", _raise_json_error)
|
|
|
|
assert run_items.fingerprint_input_item({"type": "message", "role": "user"}) is None
|
|
|
|
|
|
def test_strip_metadata_and_reasoning_id_helpers_keep_non_matching_items() -> None:
|
|
raw = cast(TResponseInputItem, "raw-item")
|
|
non_reasoning = cast(TResponseInputItem, {"type": "message", "id": "msg_1"})
|
|
reasoning_without_id = cast(TResponseInputItem, {"type": "reasoning", "summary": []})
|
|
|
|
assert run_items.strip_internal_input_item_metadata(raw) == raw
|
|
assert run_items._without_reasoning_item_id(raw) == raw
|
|
assert run_items._without_reasoning_item_id(non_reasoning) == non_reasoning
|
|
assert run_items._without_reasoning_item_id(reasoning_without_id) == reasoning_without_id
|
|
|
|
|
|
def test_deduplicate_input_items_handles_fake_ids_and_approval_request_ids() -> None:
|
|
items: list[Any] = [
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call_output",
|
|
"id": FAKE_RESPONSES_ID,
|
|
"call_id": "call-1",
|
|
"output": "first",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call_output",
|
|
"id": FAKE_RESPONSES_ID,
|
|
"call_id": "call-1",
|
|
"output": "latest",
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "mcp_approval_response",
|
|
"approval_request_id": "req-1",
|
|
"approve": True,
|
|
},
|
|
),
|
|
cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "mcp_approval_response",
|
|
"approval_request_id": "req-1",
|
|
"approve": False,
|
|
},
|
|
),
|
|
cast(TResponseInputItem, "plain"),
|
|
]
|
|
|
|
deduplicated = run_items.deduplicate_input_items(cast(list[TResponseInputItem], items))
|
|
assert len(deduplicated) == 3
|
|
assert cast(list[Any], deduplicated)[-1] == "plain"
|
|
|
|
latest = run_items.deduplicate_input_items_preferring_latest(
|
|
cast(list[TResponseInputItem], items[:2])
|
|
)
|
|
assert len(latest) == 1
|
|
latest_output = cast(dict[str, Any], latest[0])
|
|
assert latest_output["output"] == "latest"
|
|
|
|
|
|
def test_extract_mcp_request_id_supports_dicts_and_objects() -> None:
|
|
assert (
|
|
run_items.extract_mcp_request_id(
|
|
{"provider_data": {"id": "provider-id"}, "id": "fallback-id"}
|
|
)
|
|
== "provider-id"
|
|
)
|
|
assert run_items.extract_mcp_request_id({"call_id": "call-id"}) == "call-id"
|
|
|
|
class _WithProviderData:
|
|
provider_data = {"id": "from-provider"}
|
|
|
|
assert run_items.extract_mcp_request_id(_WithProviderData()) == "from-provider"
|
|
|
|
class _BrokenObject:
|
|
@property
|
|
def provider_data(self) -> dict[str, Any]:
|
|
raise RuntimeError("boom")
|
|
|
|
def __getattr__(self, _name: str) -> Any:
|
|
raise RuntimeError("boom")
|
|
|
|
assert run_items.extract_mcp_request_id(_BrokenObject()) is None
|
|
|
|
|
|
def test_extract_mcp_request_id_from_run_variants() -> None:
|
|
class _Run:
|
|
def __init__(self, request_item: Any = None, requestItem: Any = None) -> None:
|
|
self.request_item = request_item
|
|
self.requestItem = requestItem
|
|
|
|
class _RequestObject:
|
|
provider_data = {"id": "provider-object"}
|
|
id = "object-id"
|
|
call_id = "object-call-id"
|
|
|
|
assert (
|
|
run_items.extract_mcp_request_id_from_run(
|
|
_Run(request_item={"provider_data": {"id": "provider-dict"}, "id": "fallback"})
|
|
)
|
|
== "provider-dict"
|
|
)
|
|
assert (
|
|
run_items.extract_mcp_request_id_from_run(_Run(request_item={"id": "dict-id"})) == "dict-id"
|
|
)
|
|
assert (
|
|
run_items.extract_mcp_request_id_from_run(_Run(request_item=_RequestObject()))
|
|
== "provider-object"
|
|
)
|
|
assert (
|
|
run_items.extract_mcp_request_id_from_run(_Run(requestItem={"call_id": "camel-call"}))
|
|
== "camel-call"
|
|
)
|
|
|
|
|
|
def test_run_item_to_input_item_preserves_reasoning_item_ids_by_default() -> None:
|
|
agent = Agent(name="A")
|
|
reasoning = ReasoningItem(
|
|
agent=agent,
|
|
raw_item=ResponseReasoningItem(
|
|
type="reasoning",
|
|
id="rs_123",
|
|
summary=[],
|
|
),
|
|
)
|
|
|
|
result = run_items.run_item_to_input_item(reasoning)
|
|
|
|
assert isinstance(result, dict)
|
|
assert result.get("type") == "reasoning"
|
|
assert result.get("id") == "rs_123"
|
|
|
|
|
|
def test_run_item_to_input_item_omits_reasoning_item_ids_when_configured() -> None:
|
|
agent = Agent(name="A")
|
|
reasoning = ReasoningItem(
|
|
agent=agent,
|
|
raw_item=ResponseReasoningItem(
|
|
type="reasoning",
|
|
id="rs_456",
|
|
summary=[],
|
|
),
|
|
)
|
|
|
|
result = run_items.run_item_to_input_item(reasoning, "omit")
|
|
|
|
assert isinstance(result, dict)
|
|
assert result.get("type") == "reasoning"
|
|
assert "id" not in result
|
|
|
|
|
|
def test_run_item_to_input_item_preserves_tool_search_items() -> None:
|
|
agent = Agent(name="A")
|
|
tool_search_call = ToolSearchCallItem(
|
|
agent=agent,
|
|
raw_item={"type": "tool_search_call", "queries": [{"search_term": "profile"}]},
|
|
)
|
|
tool_search_output = ToolSearchOutputItem(
|
|
agent=agent,
|
|
raw_item={"type": "tool_search_output", "results": [{"text": "Customer profile"}]},
|
|
)
|
|
|
|
converted_call = run_items.run_item_to_input_item(tool_search_call)
|
|
converted_output = run_items.run_item_to_input_item(tool_search_output)
|
|
|
|
assert isinstance(converted_call, dict)
|
|
assert converted_call["type"] == "tool_search_call"
|
|
assert isinstance(converted_output, dict)
|
|
assert converted_output["type"] == "tool_search_output"
|
|
|
|
|
|
def test_run_item_to_input_item_strips_tool_search_created_by() -> None:
|
|
agent = Agent(name="A")
|
|
tool_search_call = ToolSearchCallItem(
|
|
agent=agent,
|
|
raw_item=ResponseToolSearchCall(
|
|
id="tsc_123",
|
|
type="tool_search_call",
|
|
arguments={"query": "profile"},
|
|
execution="client",
|
|
status="completed",
|
|
created_by="server",
|
|
),
|
|
)
|
|
tool_search_output = ToolSearchOutputItem(
|
|
agent=agent,
|
|
raw_item=ResponseToolSearchOutputItem(
|
|
id="tso_123",
|
|
type="tool_search_output",
|
|
execution="client",
|
|
status="completed",
|
|
tools=[],
|
|
created_by="server",
|
|
),
|
|
)
|
|
|
|
converted_call = run_items.run_item_to_input_item(tool_search_call)
|
|
converted_output = run_items.run_item_to_input_item(tool_search_output)
|
|
|
|
assert isinstance(converted_call, dict)
|
|
assert converted_call["type"] == "tool_search_call"
|
|
assert "created_by" not in converted_call
|
|
assert isinstance(converted_output, dict)
|
|
assert converted_output["type"] == "tool_search_output"
|
|
assert "created_by" not in converted_output
|
|
|
|
|
|
def test_run_item_to_input_item_omits_tool_call_metadata() -> None:
|
|
agent = Agent(name="A")
|
|
tool_call = ToolCallItem(
|
|
agent=agent,
|
|
raw_item=ResponseFunctionToolCall(
|
|
id="fc_123",
|
|
call_id="call_123",
|
|
name="lookup_account",
|
|
arguments="{}",
|
|
type="function_call",
|
|
status="completed",
|
|
),
|
|
description="Lookup customer records.",
|
|
title="Lookup Account",
|
|
)
|
|
|
|
result = run_items.run_item_to_input_item(tool_call)
|
|
result_dict = cast(dict[str, Any], result)
|
|
|
|
assert isinstance(result, dict)
|
|
assert result_dict["type"] == "function_call"
|
|
assert "description" not in result_dict
|
|
assert "title" not in result_dict
|
|
|
|
|
|
def test_normalize_input_items_for_api_strips_internal_tool_call_metadata() -> None:
|
|
item = cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_123",
|
|
"name": "lookup_account",
|
|
"arguments": "{}",
|
|
run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.",
|
|
run_items.TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account",
|
|
},
|
|
)
|
|
|
|
normalized = run_items.normalize_input_items_for_api([item])
|
|
normalized_item = cast(dict[str, Any], normalized[0])
|
|
|
|
assert run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY not in normalized_item
|
|
assert run_items.TOOL_CALL_SESSION_TITLE_KEY not in normalized_item
|
|
|
|
|
|
def test_fingerprint_input_item_ignores_internal_tool_call_metadata() -> None:
|
|
base_item = cast(
|
|
TResponseInputItem,
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_123",
|
|
"name": "lookup_account",
|
|
"arguments": "{}",
|
|
},
|
|
)
|
|
with_metadata = cast(
|
|
TResponseInputItem,
|
|
{
|
|
**cast(dict[str, Any], base_item),
|
|
run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.",
|
|
run_items.TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account",
|
|
},
|
|
)
|
|
|
|
assert run_items.fingerprint_input_item(base_item) == run_items.fingerprint_input_item(
|
|
with_metadata
|
|
)
|
|
|
|
|
|
def test_run_result_to_input_list_preserves_tool_search_items() -> None:
|
|
agent = Agent(name="A")
|
|
result = RunResult(
|
|
input="Find CRM tools",
|
|
new_items=[
|
|
ToolSearchCallItem(
|
|
agent=agent,
|
|
raw_item={"type": "tool_search_call", "queries": [{"search_term": "profile"}]},
|
|
),
|
|
ToolSearchOutputItem(
|
|
agent=agent,
|
|
raw_item={"type": "tool_search_output", "results": [{"text": "Customer profile"}]},
|
|
),
|
|
],
|
|
raw_responses=[],
|
|
final_output="done",
|
|
input_guardrail_results=[],
|
|
output_guardrail_results=[],
|
|
tool_input_guardrail_results=[],
|
|
tool_output_guardrail_results=[],
|
|
context_wrapper=RunContextWrapper(context=None),
|
|
_last_agent=agent,
|
|
)
|
|
|
|
input_items = result.to_input_list()
|
|
|
|
assert len(input_items) == 3
|
|
assert cast(dict[str, Any], input_items[1])["type"] == "tool_search_call"
|
|
assert cast(dict[str, Any], input_items[2])["type"] == "tool_search_output"
|
|
|
|
|
|
def test_coerce_tool_search_output_raw_item_rejects_legacy_type() -> None:
|
|
with pytest.raises(AgentsException, match="Unexpected tool search output item type"):
|
|
coerce_tool_search_output_raw_item({"type": "tool_search_result", "results": []})
|