Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/running_agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ async def main():
# California
```

For the first turn where no real `previous_response_id` exists, you can set `previous_response_id="bootstrap"` to force-enable response chaining for internal function calls inside the first turn.

## Long running agents & human-in-the-loop

Expand Down
8 changes: 7 additions & 1 deletion src/agents/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ class CallModelData(Generic[TContext]):
@dataclass
class _ServerConversationTracker:
"""Tracks server-side conversation state for either conversation_id or
previous_response_id modes."""
previous_response_id modes.

Special value 'bootstrap' for previous_response_id enables chaining for internal
function calls in the first turn, even when there's no actual previous response ID yet.
"""

conversation_id: str | None = None
previous_response_id: str | None = None
Expand Down Expand Up @@ -1360,6 +1364,7 @@ async def _run_single_turn_streamed(
previous_response_id = (
server_conversation_tracker.previous_response_id
if server_conversation_tracker
and server_conversation_tracker.previous_response_id != "bootstrap"
else None
)
conversation_id = (
Expand Down Expand Up @@ -1798,6 +1803,7 @@ async def _get_new_response(
previous_response_id = (
server_conversation_tracker.previous_response_id
if server_conversation_tracker
and server_conversation_tracker.previous_response_id != "bootstrap"
else None
)
conversation_id = (
Expand Down
113 changes: 113 additions & 0 deletions tests/test_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,119 @@ async def test_default_send_all_items_streamed():
assert function_result.get("call_id") is not None


@pytest.mark.asyncio
async def test_bootstrap_mode_multi_turn():
"""Test that bootstrap mode (previous_response_id='bootstrap') enables
chaining from the first internal turn."""
model = FakeModel()
agent = Agent(
name="test",
model=model,
tools=[get_function_tool("test_func", "tool_result")],
)

model.add_multiple_turn_outputs(
[
# First turn: a message and tool call
[get_text_message("a_message"), get_function_tool_call("test_func", '{"arg": "foo"}')],
# Second turn: final text message
[get_text_message("done")],
]
)

result = await Runner.run(agent, input="user_message", previous_response_id="bootstrap")
assert result.final_output == "done"

# Check the first call
assert model.first_turn_args is not None
first_input = model.first_turn_args["input"]

# First call should include the original user input
assert isinstance(first_input, list)
assert len(first_input) == 1 # Should contain the user message

# The input should be the user message
user_message = first_input[0]
assert user_message.get("role") == "user"
assert user_message.get("content") == "user_message"

# In bootstrap mode, first call should NOT have previous_response_id
assert model.first_turn_args.get("previous_response_id") is None

# Check the input from the second turn (after function execution)
last_input = model.last_turn_args["input"]

# In bootstrap mode, the second turn should only contain the tool output
assert isinstance(last_input, list)
assert len(last_input) == 1 # Only the function result

# The single item should be a tool result
tool_result_item = last_input[0]
assert tool_result_item.get("type") == "function_call_output"
assert tool_result_item.get("call_id") is not None

# In bootstrap mode, second call should have previous_response_id set to the first response
assert model.last_turn_args.get("previous_response_id") == "resp-789"


@pytest.mark.asyncio
async def test_bootstrap_mode_multi_turn_streamed():
"""Test that bootstrap mode (previous_response_id='bootstrap') enables
chaining from the first internal turn (streamed mode)."""
model = FakeModel()
agent = Agent(
name="test",
model=model,
tools=[get_function_tool("test_func", "tool_result")],
)

model.add_multiple_turn_outputs(
[
# First turn: a message and tool call
[get_text_message("a_message"), get_function_tool_call("test_func", '{"arg": "foo"}')],
# Second turn: final text message
[get_text_message("done")],
]
)

result = Runner.run_streamed(agent, input="user_message", previous_response_id="bootstrap")
async for _ in result.stream_events():
pass

assert result.final_output == "done"

# Check the first call
assert model.first_turn_args is not None
first_input = model.first_turn_args["input"]

# First call should include the original user input
assert isinstance(first_input, list)
assert len(first_input) == 1 # Should contain the user message

# The input should be the user message
user_message = first_input[0]
assert user_message.get("role") == "user"
assert user_message.get("content") == "user_message"

# In bootstrap mode, first call should NOT have previous_response_id
assert model.first_turn_args.get("previous_response_id") is None

# Check the input from the second turn (after function execution)
last_input = model.last_turn_args["input"]

# In bootstrap mode, the second turn should only contain the tool output
assert isinstance(last_input, list)
assert len(last_input) == 1 # Only the function result

# The single item should be a tool result
tool_result_item = last_input[0]
assert tool_result_item.get("type") == "function_call_output"
assert tool_result_item.get("call_id") is not None

# In bootstrap mode, second call should have previous_response_id set to the first response
assert model.last_turn_args.get("previous_response_id") == "resp-789"


@pytest.mark.asyncio
async def test_dynamic_tool_addition_run() -> None:
"""Test that tools can be added to an agent during a run."""
Expand Down