Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/agents/sandbox/memory/rollouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"mcp_approval_request",
"mcp_approval_response",
"mcp_call",
"program",
"program_output",
"shell_call",
"shell_call_output",
"tool_search_call",
Expand Down
118 changes: 118 additions & 0 deletions tests/sandbox/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import pytest
from openai.types.responses import ResponseCustomToolCall, ResponseFunctionToolCall
from openai.types.responses.response_function_tool_call import CallerProgram
from openai.types.responses.response_output_item import Program, ProgramOutput
from openai.types.responses.response_output_message import ResponseOutputMessage
from openai.types.responses.response_reasoning_item import ResponseReasoningItem

Expand All @@ -33,6 +35,8 @@
CompactionItem,
MessageOutputItem,
ToolApprovalItem,
ToolCallItem,
ToolCallOutputItem,
TResponseOutputItem,
)
from agents.result import RunResult, RunResultStreaming
Expand Down Expand Up @@ -279,6 +283,120 @@ def test_build_rollout_payload_filters_developer_and_noisy_items() -> None:
assert payload["final_output"] == "done"


def test_build_rollout_payload_keeps_programmatic_tool_calling_items() -> None:
agent = Agent(name="test")
program = Program(
id="program_item",
call_id="call_prog_1",
code='lookup_inventory(sku="A-1")',
fingerprint="fingerprint",
type="program",
)
function_call = ResponseFunctionToolCall(
id="function_item",
call_id="call_fn_1",
name="lookup_inventory",
arguments='{"sku":"A-1"}',
caller=CallerProgram(type="program", caller_id="call_prog_1"),
type="function_call",
)
function_call_output = cast(
TResponseInputItem,
{
"type": "function_call_output",
"call_id": "call_fn_1",
"output": '{"available_units":42}',
},
)
program_output = ProgramOutput(
id="program_output_item",
call_id="call_prog_1",
result='{"sku":"A-1","available_units":42}',
status="completed",
type="program_output",
)

payload = build_rollout_payload(
input="what is in stock?",
new_items=[
ToolCallItem(agent=agent, raw_item=program),
ToolCallItem(agent=agent, raw_item=function_call),
ToolCallOutputItem(agent=agent, raw_item=function_call_output, output="42"),
ToolCallOutputItem(agent=agent, raw_item=program_output, output="42"),
],
final_output="done",
interruptions=[],
terminal_metadata=RolloutTerminalMetadata(
terminal_state="completed",
has_final_output=True,
),
)

generated_items = payload["generated_items"]
assert [item["type"] for item in generated_items] == [
"program",
"function_call",
"function_call_output",
"program_output",
]
# The retained function call points back at the program that issued it, so the program
# it names has to survive alongside it.
assert generated_items[1]["caller"] == {"type": "program", "caller_id": "call_prog_1"}
assert generated_items[0]["call_id"] == "call_prog_1"
assert generated_items[0]["code"] == 'lookup_inventory(sku="A-1")'
assert generated_items[3]["call_id"] == "call_prog_1"
assert generated_items[3]["result"] == '{"sku":"A-1","available_units":42}'


def test_build_rollout_payload_keeps_program_items_from_input() -> None:
payload = build_rollout_payload(
input=[
cast(
TResponseInputItem,
{
"type": "program",
"call_id": "call_prog_1",
"code": 'lookup_inventory(sku="A-1")',
"fingerprint": "fingerprint",
},
),
cast(
TResponseInputItem,
{
"type": "program_output",
"call_id": "call_prog_1",
"result": '{"available_units":42}',
"status": "completed",
},
),
],
new_items=[],
final_output=None,
interruptions=[],
terminal_metadata=RolloutTerminalMetadata(terminal_state="completed"),
)

assert [item["type"] for item in payload["input"]] == ["program", "program_output"]


def test_build_rollout_payload_still_drops_hosted_items_outside_the_included_set() -> None:
"""Program items are included because every other call/output pair is; hosted tool calls
with no output half stay out."""
payload = build_rollout_payload(
input=[
cast(TResponseInputItem, {"type": "file_search_call", "id": "fs_1", "queries": []}),
cast(TResponseInputItem, {"type": "image_generation_call", "id": "ig_1"}),
cast(TResponseInputItem, {"type": "program", "call_id": "call_prog_1", "code": "x()"}),
],
new_items=[],
final_output=None,
interruptions=[],
terminal_metadata=RolloutTerminalMetadata(terminal_state="completed"),
)

assert [item["type"] for item in payload["input"]] == ["program"]


def test_build_rollout_payload_serializes_model_interruptions_as_dicts() -> None:
agent = Agent(name="test")
raw = ResponseFunctionToolCall(
Expand Down
Loading