Skip to content

Commit a776d80

Browse files
authored
Nest handoff history by default (#1996)
## Summary - add a `nest_handoff_history` flag to `RunConfig` and call a new helper that condenses the prior transcript into a developer-role summary when handing off - update the default handoff path, docs, and helper library so that the developer summary is produced automatically unless a custom filter overrides it - expand the handoff-focused tests to cover the new behavior (including helper unit tests) and update existing expectations ## Testing - `uv run pytest tests/test_extension_filters.py` - `uv run pytest tests/test_agent_runner.py -k handoff` - `uv run pytest tests/test_agent_runner_streamed.py -k handoff` ------ https://chatgpt.com/codex/tasks/task_i_68ff73bda0f4832496f3d1fa9103905f
1 parent 05dc79d commit a776d80

File tree

13 files changed

+795
-68
lines changed

13 files changed

+795
-68
lines changed

docs/handoffs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ handoff_obj = handoff(
8282

8383
When a handoff occurs, it's as though the new agent takes over the conversation, and gets to see the entire previous conversation history. If you want to change this, you can set an [`input_filter`][agents.handoffs.Handoff.input_filter]. An input filter is a function that receives the existing input via a [`HandoffInputData`][agents.handoffs.HandoffInputData], and must return a new `HandoffInputData`.
8484

85+
By default the runner now collapses the prior transcript into a single assistant summary message (see [`RunConfig.nest_handoff_history`][agents.run.RunConfig.nest_handoff_history]). The summary appears inside a `<CONVERSATION HISTORY>` block that keeps appending new turns when multiple handoffs happen during the same run. You can provide your own mapping function via [`RunConfig.handoff_history_mapper`][agents.run.RunConfig.handoff_history_mapper] to replace the generated message without writing a full `input_filter`. That default only applies when neither the handoff nor the run supplies an explicit `input_filter`, so existing code that already customizes the payload (including the examples in this repository) keeps its current behavior without changes. You can override the nesting behaviour for a single handoff by passing `nest_handoff_history=True` or `False` to [`handoff(...)`][agents.handoffs.handoff], which sets [`Handoff.nest_handoff_history`][agents.handoffs.Handoff.nest_handoff_history]. If you just need to change the wrapper text for the generated summary, call [`set_conversation_history_wrappers`][agents.handoffs.set_conversation_history_wrappers] (and optionally [`reset_conversation_history_wrappers`][agents.handoffs.reset_conversation_history_wrappers]) before running your agents.
86+
8587
There are some common patterns (for example removing all tool calls from the history), which are implemented for you in [`agents.extensions.handoff_filters`][]
8688

8789
```python

docs/release.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ We will increment `Z` for non-breaking changes:
1919

2020
## Breaking change changelog
2121

22+
### 0.6.0
23+
24+
In this version, the default handoff history is now packaged into a single assistant message instead of exposing the raw user/assistant turns, giving downstream agents a concise, predictable recap
25+
- The existing single-message handoff transcript now by default starts with "For context, here is the conversation so far between the user and the previous agent:" before the `<CONVERSATION HISTORY>` block, so downstream agents get a clearly labeled recap
26+
2227
### 0.5.0
2328

2429
This version doesn’t introduce any visible breaking changes, but it includes new features and a few significant updates under the hood:

docs/running_agents.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ The `run_config` parameter lets you configure some global settings for the agent
5151
- [`model_settings`][agents.run.RunConfig.model_settings]: Overrides agent-specific settings. For example, you can set a global `temperature` or `top_p`.
5252
- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]: A list of input or output guardrails to include on all runs.
5353
- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]: A global input filter to apply to all handoffs, if the handoff doesn't already have one. The input filter allows you to edit the inputs that are sent to the new agent. See the documentation in [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] for more details.
54+
- [`nest_handoff_history`][agents.run.RunConfig.nest_handoff_history]: When `True` (the default) the runner collapses the prior transcript into a single assistant message before invoking the next agent. The helper places the content inside a `<CONVERSATION HISTORY>` block that keeps appending new turns as subsequent handoffs occur. Set this to `False` or provide a custom handoff filter if you prefer to pass through the raw transcript. All [`Runner` methods](agents.run.Runner) automatically create a `RunConfig` when you do not pass one, so the quickstarts and examples pick up this default automatically, and any explicit [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] callbacks continue to override it. Individual handoffs can override this setting via [`Handoff.nest_handoff_history`][agents.handoffs.Handoff.nest_handoff_history].
55+
- [`handoff_history_mapper`][agents.run.RunConfig.handoff_history_mapper]: Optional callable that receives the normalized transcript (history + handoff items) whenever `nest_handoff_history` is `True`. It must return the exact list of input items to forward to the next agent, allowing you to replace the built-in summary without writing a full handoff filter.
5456
- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: Allows you to disable [tracing](tracing.md) for the entire run.
5557
- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]: Configures whether traces will include potentially sensitive data, such as LLM and tool call inputs/outputs.
5658
- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: Sets the tracing workflow name, trace ID and trace group ID for the run. We recommend at least setting `workflow_name`. The group ID is an optional field that lets you link traces across multiple runs.
5759
- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: Metadata to include on all traces.
5860

61+
By default, the SDK now nests prior turns inside a single assistant summary message whenever an agent hands off to another agent. This reduces repeated assistant messages and keeps the full transcript inside a single block that new agents can scan quickly. If you'd like to return to the legacy behavior, pass `RunConfig(nest_handoff_history=False)` or supply a `handoff_input_filter` (or `handoff_history_mapper`) that forwards the conversation exactly as you need. You can also opt out (or in) for a specific handoff by setting `handoff(..., nest_handoff_history=False)` or `True`. To change the wrapper text used in the generated summary without writing a custom mapper, call [`set_conversation_history_wrappers`][agents.handoffs.set_conversation_history_wrappers] (and [`reset_conversation_history_wrappers`][agents.handoffs.reset_conversation_history_wrappers] to restore the defaults).
62+
5963
## Conversations/chat threads
6064

6165
Calling any of the run methods can result in one or more agents running (and hence one or more LLM calls), but it represents a single logical turn in a chat conversation. For example:
@@ -200,4 +204,4 @@ The SDK raises exceptions in certain cases. The full list is in [`agents.excepti
200204
- Malformed JSON: When the model provides a malformed JSON structure for tool calls or in its direct output, especially if a specific `output_type` is defined.
201205
- Unexpected tool-related failures: When the model fails to use tools in an expected manner
202206
- [`UserError`][agents.exceptions.UserError]: This exception is raised when you (the person writing code using the SDK) make an error while using the SDK. This typically results from incorrect code implementation, invalid configuration, or misuse of the SDK's API.
203-
- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]: This exception is raised when the conditions of an input guardrail or output guardrail are met, respectively. Input guardrails check incoming messages before processing, while output guardrails check the agent's final response before delivery.
207+
- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]: This exception is raised when the conditions of an input guardrail or output guardrail are met, respectively. Input guardrails check incoming messages before processing, while output guardrails check the agent's final response before delivery.

src/agents/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,17 @@
3636
input_guardrail,
3737
output_guardrail,
3838
)
39-
from .handoffs import Handoff, HandoffInputData, HandoffInputFilter, handoff
39+
from .handoffs import (
40+
Handoff,
41+
HandoffInputData,
42+
HandoffInputFilter,
43+
default_handoff_history_mapper,
44+
get_conversation_history_wrappers,
45+
handoff,
46+
nest_handoff_history,
47+
reset_conversation_history_wrappers,
48+
set_conversation_history_wrappers,
49+
)
4050
from .items import (
4151
HandoffCallItem,
4252
HandoffOutputItem,
@@ -207,6 +217,11 @@ def enable_verbose_stdout_logging():
207217
"StopAtTools",
208218
"ToolsToFinalOutputFunction",
209219
"ToolsToFinalOutputResult",
220+
"default_handoff_history_mapper",
221+
"get_conversation_history_wrappers",
222+
"nest_handoff_history",
223+
"reset_conversation_history_wrappers",
224+
"set_conversation_history_wrappers",
210225
"Runner",
211226
"apply_diff",
212227
"run_demo_loop",

src/agents/_run_impl.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
UserError,
5656
)
5757
from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult
58-
from .handoffs import Handoff, HandoffInputData
58+
from .handoffs import Handoff, HandoffInputData, nest_handoff_history
5959
from .items import (
6060
HandoffCallItem,
6161
HandoffOutputItem,
@@ -1212,8 +1212,14 @@ async def execute_handoffs(
12121212
input_filter = handoff.input_filter or (
12131213
run_config.handoff_input_filter if run_config else None
12141214
)
1215-
if input_filter:
1216-
logger.debug("Filtering inputs for handoff")
1215+
handoff_nest_setting = handoff.nest_handoff_history
1216+
should_nest_history = (
1217+
handoff_nest_setting
1218+
if handoff_nest_setting is not None
1219+
else run_config.nest_handoff_history
1220+
)
1221+
handoff_input_data: HandoffInputData | None = None
1222+
if input_filter or should_nest_history:
12171223
handoff_input_data = HandoffInputData(
12181224
input_history=tuple(original_input)
12191225
if isinstance(original_input, list)
@@ -1222,6 +1228,17 @@ async def execute_handoffs(
12221228
new_items=tuple(new_step_items),
12231229
run_context=context_wrapper,
12241230
)
1231+
1232+
if input_filter and handoff_input_data is not None:
1233+
filter_name = getattr(input_filter, "__qualname__", repr(input_filter))
1234+
from_agent = getattr(agent, "name", agent.__class__.__name__)
1235+
to_agent = getattr(new_agent, "name", new_agent.__class__.__name__)
1236+
logger.debug(
1237+
"Filtering handoff inputs with %s for %s -> %s",
1238+
filter_name,
1239+
from_agent,
1240+
to_agent,
1241+
)
12251242
if not callable(input_filter):
12261243
_error_tracing.attach_error_to_span(
12271244
span_handoff,
@@ -1251,6 +1268,18 @@ async def execute_handoffs(
12511268
)
12521269
pre_step_items = list(filtered.pre_handoff_items)
12531270
new_step_items = list(filtered.new_items)
1271+
elif should_nest_history and handoff_input_data is not None:
1272+
nested = nest_handoff_history(
1273+
handoff_input_data,
1274+
history_mapper=run_config.handoff_history_mapper,
1275+
)
1276+
original_input = (
1277+
nested.input_history
1278+
if isinstance(nested.input_history, str)
1279+
else list(nested.input_history)
1280+
)
1281+
pre_step_items = list(nested.pre_handoff_items)
1282+
new_step_items = list(nested.new_items)
12541283

12551284
return SingleStepResult(
12561285
original_input=original_input,

src/agents/extensions/handoff_filters.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from __future__ import annotations
22

3-
from ..handoffs import HandoffInputData
3+
from ..handoffs import (
4+
HandoffInputData,
5+
default_handoff_history_mapper,
6+
nest_handoff_history,
7+
)
48
from ..items import (
59
HandoffCallItem,
610
HandoffOutputItem,
@@ -13,6 +17,12 @@
1317

1418
"""Contains common handoff input filters, for convenience. """
1519

20+
__all__ = [
21+
"remove_all_tools",
22+
"nest_handoff_history",
23+
"default_handoff_history_mapper",
24+
]
25+
1626

1727
def remove_all_tools(handoff_input_data: HandoffInputData) -> HandoffInputData:
1828
"""Filters out all tool items: file search, web search and function calls+output."""

0 commit comments

Comments
 (0)