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
4 changes: 4 additions & 0 deletions docs/handoffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ handoff_obj = handoff(

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`.

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.

If you are using server-managed conversations (`conversation_id` or `previous_response_id`), the SDK keeps the server's transcript authoritative: handoff input filters are rejected and nested history is disabled to avoid desynchronizing the stored conversation.

There are some common patterns (for example removing all tool calls from the history), which are implemented for you in [`agents.extensions.handoff_filters`][]

```python
Expand Down
3 changes: 3 additions & 0 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ This version doesn’t introduce any visible breaking changes, but it includes n

- Added support for `RealtimeRunner` to handle [SIP protocol connections](https://platform.openai.com/docs/guides/realtime-sip)
- Significantly revised the internal logic of `Runner#run_sync` for Python 3.14 compatibility
- By 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
- The existing single-message handoff transcript now 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
- The existing single-message handoff transcript now 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

### 0.4.0

Expand Down
6 changes: 5 additions & 1 deletion docs/running_agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ The `run_config` parameter lets you configure some global settings for the agent
- [`model_settings`][agents.run.RunConfig.model_settings]: Overrides agent-specific settings. For example, you can set a global `temperature` or `top_p`.
- [`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.
- [`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.
- [`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].
- [`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.
- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: Allows you to disable [tracing](tracing.md) for the entire run.
- [`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.
- [`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.
- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: Metadata to include on all traces.

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).

## Conversations/chat threads

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:
Expand Down Expand Up @@ -200,4 +204,4 @@ The SDK raises exceptions in certain cases. The full list is in [`agents.excepti
- 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.
- Unexpected tool-related failures: When the model fails to use tools in an expected manner
- [`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.
- [`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.
- [`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.
17 changes: 16 additions & 1 deletion src/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@
input_guardrail,
output_guardrail,
)
from .handoffs import Handoff, HandoffInputData, HandoffInputFilter, handoff
from .handoffs import (
Handoff,
HandoffInputData,
HandoffInputFilter,
default_handoff_history_mapper,
get_conversation_history_wrappers,
handoff,
nest_handoff_history,
reset_conversation_history_wrappers,
set_conversation_history_wrappers,
)
from .items import (
HandoffCallItem,
HandoffOutputItem,
Expand Down Expand Up @@ -191,6 +201,11 @@ def enable_verbose_stdout_logging():
"StopAtTools",
"ToolsToFinalOutputFunction",
"ToolsToFinalOutputResult",
"default_handoff_history_mapper",
"get_conversation_history_wrappers",
"nest_handoff_history",
"reset_conversation_history_wrappers",
"set_conversation_history_wrappers",
"Runner",
"run_demo_loop",
"Model",
Expand Down
59 changes: 56 additions & 3 deletions src/agents/_run_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
UserError,
)
from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult
from .handoffs import Handoff, HandoffInputData
from .handoffs import Handoff, HandoffInputData, nest_handoff_history
from .items import (
HandoffCallItem,
HandoffOutputItem,
Expand Down Expand Up @@ -226,6 +226,10 @@ class SingleStepResult:
tool_output_guardrail_results: list[ToolOutputGuardrailResult]
"""Tool output guardrail results from this step."""

reset_conversation_items: bool = False
"""When True, omit previously generated items from the next model call (e.g. after nesting
handoff history) while still keeping them in run results."""

@property
def generated_items(self) -> list[RunItem]:
"""Items generated during the agent run (i.e. everything generated after
Expand Down Expand Up @@ -260,6 +264,7 @@ async def execute_tools_and_side_effects(
hooks: RunHooks[TContext],
context_wrapper: RunContextWrapper[TContext],
run_config: RunConfig,
server_conversation_mode: bool = False,
) -> SingleStepResult:
# Make a copy of the generated items
pre_step_items = list(pre_step_items)
Expand Down Expand Up @@ -320,6 +325,7 @@ async def execute_tools_and_side_effects(
hooks=hooks,
context_wrapper=context_wrapper,
run_config=run_config,
server_conversation_mode=server_conversation_mode,
)

# Next, we'll check if the tool use should result in a final output
Expand Down Expand Up @@ -927,6 +933,7 @@ async def execute_handoffs(
hooks: RunHooks[TContext],
context_wrapper: RunContextWrapper[TContext],
run_config: RunConfig,
server_conversation_mode: bool = False,
) -> SingleStepResult:
# If there is more than one handoff, add tool responses that reject those handoffs
multiple_handoffs = len(run_handoffs) > 1
Expand Down Expand Up @@ -998,8 +1005,29 @@ async def execute_handoffs(
input_filter = handoff.input_filter or (
run_config.handoff_input_filter if run_config else None
)
if input_filter:
logger.debug("Filtering inputs for handoff")
handoff_nest_setting = handoff.nest_handoff_history
should_nest_history = (
handoff_nest_setting
if handoff_nest_setting is not None
else run_config.nest_handoff_history
)
reset_conversation_items = False
if server_conversation_mode:
if input_filter:
raise UserError(
"Handoff input filters are not supported when using server-managed "
"conversations (conversation_id or previous_response_id). Remove the "
"input_filter or disable server-managed conversation state."
)
if should_nest_history:
logger.warning(
"Disabling nest_handoff_history because server-managed conversation state "
"is enabled via conversation_id or previous_response_id."
)
input_filter = None
should_nest_history = False
handoff_input_data: HandoffInputData | None = None
if input_filter or should_nest_history:
handoff_input_data = HandoffInputData(
input_history=tuple(original_input)
if isinstance(original_input, list)
Expand All @@ -1008,6 +1036,17 @@ async def execute_handoffs(
new_items=tuple(new_step_items),
run_context=context_wrapper,
)

if input_filter and handoff_input_data is not None:
filter_name = getattr(input_filter, "__qualname__", repr(input_filter))
from_agent = getattr(agent, "name", agent.__class__.__name__)
to_agent = getattr(new_agent, "name", new_agent.__class__.__name__)
logger.debug(
"Filtering handoff inputs with %s for %s -> %s",
filter_name,
from_agent,
to_agent,
)
if not callable(input_filter):
_error_tracing.attach_error_to_span(
span_handoff,
Expand Down Expand Up @@ -1037,6 +1076,19 @@ async def execute_handoffs(
)
pre_step_items = list(filtered.pre_handoff_items)
new_step_items = list(filtered.new_items)
elif should_nest_history and handoff_input_data is not None:
nested = nest_handoff_history(
handoff_input_data,
history_mapper=run_config.handoff_history_mapper,
)
reset_conversation_items = True
original_input = (
nested.input_history
if isinstance(nested.input_history, str)
else list(nested.input_history)
)
pre_step_items = list(nested.pre_handoff_items)
new_step_items = list(nested.new_items)

return SingleStepResult(
original_input=original_input,
Expand All @@ -1046,6 +1098,7 @@ async def execute_handoffs(
next_step=NextStepHandoff(new_agent),
tool_input_guardrail_results=[],
tool_output_guardrail_results=[],
reset_conversation_items=reset_conversation_items,
)

@classmethod
Expand Down
12 changes: 11 additions & 1 deletion src/agents/extensions/handoff_filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from __future__ import annotations

from ..handoffs import HandoffInputData
from ..handoffs import (
HandoffInputData,
default_handoff_history_mapper,
nest_handoff_history,
)
from ..items import (
HandoffCallItem,
HandoffOutputItem,
Expand All @@ -13,6 +17,12 @@

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

__all__ = [
"remove_all_tools",
"nest_handoff_history",
"default_handoff_history_mapper",
]


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