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
21 changes: 21 additions & 0 deletions src/agents/run_internal/session_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ async def save_result_to_session(
serialized_to_save_counts[serialized] -= 1
saved_run_items_count += 1

# Drop items the Conversations API cannot accept (counted above so the
# persisted-item counter advances correctly on the next retry/call).
if is_openai_conversation_session:
items_to_save = [
item for item in items_to_save
if not _is_unpersistable_for_openai_conversation(item)
]

if len(items_to_save) == 0:
if run_state:
run_state._current_turn_persisted_item_count = already_persisted + saved_run_items_count
Expand Down Expand Up @@ -571,6 +579,19 @@ def _sanitize_openai_conversation_item(item: TResponseInputItem) -> TResponseInp
return item


def _is_unpersistable_for_openai_conversation(item: TResponseInputItem) -> bool:
"""Return True for items the OpenAI Conversations API cannot accept.

The Conversations API rejects reasoning items with an empty summary list.
Callers should drop these items before calling ``session.add_items`` while
still counting them in the persisted-item counter so the retry logic
advances past them correctly.
"""
if not isinstance(item, dict):
return False
return item.get("type") == "reasoning" and item.get("summary") == []


def _sanitize_openai_conversation_history_items_for_model_input(
items: Sequence[TResponseInputItem],
history_indexes: set[int],
Expand Down
99 changes: 99 additions & 0 deletions tests/test_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2796,6 +2796,105 @@ async def test_save_result_to_session_omits_reasoning_ids_when_policy_is_omit()
assert "id" not in saved_reasoning


@pytest.mark.asyncio
async def test_save_result_to_session_drops_empty_reasoning_from_openai_conversations() -> None:
"""OpenAIConversationsSession must not receive reasoning items with summary=[]."""

class DummyOpenAIConversationsSession(OpenAIConversationsSession):
def __init__(self) -> None:
self.saved_items: list[TResponseInputItem] = []

async def _get_session_id(self) -> str:
return "conv_test"

async def add_items(self, items: list[TResponseInputItem]) -> None:
self.saved_items.extend(items)

async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
return []

async def pop_item(self) -> TResponseInputItem | None:
return None

async def clear_session(self) -> None:
return None

session = DummyOpenAIConversationsSession()
agent = Agent(name="agent", model=FakeModel())
run_state: RunState[Any] = RunState(
context=RunContextWrapper(context={}),
original_input="input",
starting_agent=agent,
max_turns=1,
)

empty_reasoning = ReasoningItem(
agent=agent,
raw_item=ResponseReasoningItem(type="reasoning", id="rs_empty", summary=[]),
)

saved_count = await save_result_to_session(
session,
[],
cast(list[RunItem], [empty_reasoning]),
run_state,
)

# The item is counted (so the retry counter advances) but not sent to add_items.
assert saved_count == 1
assert run_state._current_turn_persisted_item_count == 1
assert len(session.saved_items) == 0


@pytest.mark.asyncio
async def test_save_result_to_session_keeps_nonempty_reasoning_in_openai_conversations() -> None:
"""Non-empty reasoning items must still be persisted to OpenAIConversationsSession."""

class DummyOpenAIConversationsSession(OpenAIConversationsSession):
def __init__(self) -> None:
self.saved_items: list[TResponseInputItem] = []

async def _get_session_id(self) -> str:
return "conv_test"

async def add_items(self, items: list[TResponseInputItem]) -> None:
self.saved_items.extend(items)

async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
return []

async def pop_item(self) -> TResponseInputItem | None:
return None

async def clear_session(self) -> None:
return None

session = DummyOpenAIConversationsSession()
agent = Agent(name="agent", model=FakeModel())

nonempty_reasoning = ReasoningItem(
agent=agent,
raw_item=ResponseReasoningItem(
type="reasoning",
id="rs_full",
summary=[Summary(type="summary_text", text="I thought about it")],
),
)

saved_count = await save_result_to_session(
session,
[],
cast(list[RunItem], [nonempty_reasoning]),
None,
)

assert saved_count == 1
assert len(session.saved_items) == 1
saved = cast(dict[str, Any], session.saved_items[0])
assert saved.get("type") == "reasoning"
assert saved.get("summary") != []


@pytest.mark.asyncio
async def test_save_result_to_session_keeps_tool_call_payload_api_safe() -> None:
session = SimpleListSession()
Expand Down
Loading