Skip to content

Commit 3364202

Browse files
committed
fixes
1 parent 6d7f9e4 commit 3364202

File tree

9 files changed

+484
-314
lines changed

9 files changed

+484
-314
lines changed

docs/handoffs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ 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.
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.
8686

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

docs/running_agents.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ 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.
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].
5555
- [`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.
5656
- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: Allows you to disable [tracing](tracing.md) for the entire run.
5757
- [`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.
5858
- [`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.
5959
- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: Metadata to include on all traces.
6060

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

6363
## Conversations/chat threads
6464

@@ -204,4 +204,4 @@ The SDK raises exceptions in certain cases. The full list is in [`agents.excepti
204204
- 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.
205205
- Unexpected tool-related failures: When the model fails to use tools in an expected manner
206206
- [`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.
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.
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
@@ -34,7 +34,17 @@
3434
input_guardrail,
3535
output_guardrail,
3636
)
37-
from .handoffs import Handoff, HandoffInputData, HandoffInputFilter, handoff
37+
from .handoffs import (
38+
Handoff,
39+
HandoffInputData,
40+
HandoffInputFilter,
41+
default_handoff_history_mapper,
42+
get_conversation_history_wrappers,
43+
handoff,
44+
nest_handoff_history,
45+
reset_conversation_history_wrappers,
46+
set_conversation_history_wrappers,
47+
)
3848
from .items import (
3949
HandoffCallItem,
4050
HandoffOutputItem,
@@ -191,6 +201,11 @@ def enable_verbose_stdout_logging():
191201
"StopAtTools",
192202
"ToolsToFinalOutputFunction",
193203
"ToolsToFinalOutputResult",
204+
"default_handoff_history_mapper",
205+
"get_conversation_history_wrappers",
206+
"nest_handoff_history",
207+
"reset_conversation_history_wrappers",
208+
"set_conversation_history_wrappers",
194209
"Runner",
195210
"run_demo_loop",
196211
"Model",

src/agents/_run_impl.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,8 @@
5151
ToolOutputGuardrailTripwireTriggered,
5252
UserError,
5353
)
54-
from .extensions.handoff_filters import nest_handoff_history
5554
from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult
56-
from .handoffs import Handoff, HandoffInputData
55+
from .handoffs import Handoff, HandoffInputData, nest_handoff_history
5756
from .items import (
5857
HandoffCallItem,
5958
HandoffOutputItem,
@@ -999,8 +998,14 @@ async def execute_handoffs(
999998
input_filter = handoff.input_filter or (
1000999
run_config.handoff_input_filter if run_config else None
10011000
)
1001+
handoff_nest_setting = handoff.nest_handoff_history
1002+
should_nest_history = (
1003+
handoff_nest_setting
1004+
if handoff_nest_setting is not None
1005+
else run_config.nest_handoff_history
1006+
)
10021007
handoff_input_data: HandoffInputData | None = None
1003-
if input_filter or run_config.nest_handoff_history:
1008+
if input_filter or should_nest_history:
10041009
handoff_input_data = HandoffInputData(
10051010
input_history=tuple(original_input)
10061011
if isinstance(original_input, list)
@@ -1011,7 +1016,15 @@ async def execute_handoffs(
10111016
)
10121017

10131018
if input_filter and handoff_input_data is not None:
1014-
logger.debug("Filtering inputs for handoff")
1019+
filter_name = getattr(input_filter, "__qualname__", repr(input_filter))
1020+
from_agent = getattr(agent, "name", agent.__class__.__name__)
1021+
to_agent = getattr(new_agent, "name", new_agent.__class__.__name__)
1022+
logger.debug(
1023+
"Filtering handoff inputs with %s for %s -> %s",
1024+
filter_name,
1025+
from_agent,
1026+
to_agent,
1027+
)
10151028
if not callable(input_filter):
10161029
_error_tracing.attach_error_to_span(
10171030
span_handoff,
@@ -1041,7 +1054,7 @@ async def execute_handoffs(
10411054
)
10421055
pre_step_items = list(filtered.pre_handoff_items)
10431056
new_step_items = list(filtered.new_items)
1044-
elif run_config.nest_handoff_history and handoff_input_data is not None:
1057+
elif should_nest_history and handoff_input_data is not None:
10451058
nested = nest_handoff_history(
10461059
handoff_input_data,
10471060
history_mapper=run_config.handoff_history_mapper,

src/agents/extensions/handoff_filters.py

Lines changed: 11 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from __future__ import annotations
22

3-
import json
4-
from copy import deepcopy
5-
from typing import Any, cast
6-
7-
from ..handoffs import HandoffHistoryMapper, HandoffInputData
3+
from ..handoffs import (
4+
HandoffInputData,
5+
default_handoff_history_mapper,
6+
nest_handoff_history,
7+
)
88
from ..items import (
99
HandoffCallItem,
1010
HandoffOutputItem,
11-
ItemHelpers,
1211
ReasoningItem,
1312
RunItem,
1413
ToolCallItem,
@@ -18,6 +17,12 @@
1817

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

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

2227
def remove_all_tools(handoff_input_data: HandoffInputData) -> HandoffInputData:
2328
"""Filters out all tool items: file search, web search and function calls+output."""
@@ -39,181 +44,6 @@ def remove_all_tools(handoff_input_data: HandoffInputData) -> HandoffInputData:
3944
)
4045

4146

42-
_CONVERSATION_HISTORY_START = "<CONVERSATION HISTORY>"
43-
_CONVERSATION_HISTORY_END = "</CONVERSATION HISTORY>"
44-
45-
46-
def nest_handoff_history(
47-
handoff_input_data: HandoffInputData,
48-
*,
49-
history_mapper: HandoffHistoryMapper | None = None,
50-
) -> HandoffInputData:
51-
"""Summarizes the previous transcript for the next agent."""
52-
53-
normalized_history = _normalize_input_history(handoff_input_data.input_history)
54-
flattened_history = _flatten_nested_history_messages(normalized_history)
55-
pre_items_as_inputs = [
56-
_run_item_to_plain_input(item) for item in handoff_input_data.pre_handoff_items
57-
]
58-
new_items_as_inputs = [_run_item_to_plain_input(item) for item in handoff_input_data.new_items]
59-
transcript = flattened_history + pre_items_as_inputs + new_items_as_inputs
60-
61-
mapper = history_mapper or default_handoff_history_mapper
62-
history_items = mapper(transcript)
63-
filtered_pre_items = tuple(
64-
item
65-
for item in handoff_input_data.pre_handoff_items
66-
if _get_run_item_role(item) != "assistant"
67-
)
68-
69-
return handoff_input_data.clone(
70-
input_history=tuple(deepcopy(item) for item in history_items),
71-
pre_handoff_items=filtered_pre_items,
72-
)
73-
74-
75-
def default_handoff_history_mapper(
76-
transcript: list[TResponseInputItem],
77-
) -> list[TResponseInputItem]:
78-
"""Returns a single assistant message summarizing the transcript."""
79-
80-
summary_message = _build_summary_message(transcript)
81-
return [summary_message]
82-
83-
84-
def _normalize_input_history(
85-
input_history: str | tuple[TResponseInputItem, ...],
86-
) -> list[TResponseInputItem]:
87-
if isinstance(input_history, str):
88-
return ItemHelpers.input_to_new_input_list(input_history)
89-
return [deepcopy(item) for item in input_history]
90-
91-
92-
def _run_item_to_plain_input(run_item: RunItem) -> TResponseInputItem:
93-
return deepcopy(run_item.to_input_item())
94-
95-
96-
def _build_summary_message(transcript: list[TResponseInputItem]) -> TResponseInputItem:
97-
transcript_copy = [deepcopy(item) for item in transcript]
98-
if transcript_copy:
99-
summary_lines = [
100-
f"{idx + 1}. {_format_transcript_item(item)}"
101-
for idx, item in enumerate(transcript_copy)
102-
]
103-
else:
104-
summary_lines = ["(no previous turns recorded)"]
105-
106-
content_lines = [_CONVERSATION_HISTORY_START, *summary_lines, _CONVERSATION_HISTORY_END]
107-
content = "\n".join(content_lines)
108-
assistant_message: dict[str, Any] = {
109-
"role": "assistant",
110-
"content": content,
111-
}
112-
return cast(TResponseInputItem, assistant_message)
113-
114-
115-
def _format_transcript_item(item: TResponseInputItem) -> str:
116-
role = item.get("role")
117-
if isinstance(role, str):
118-
prefix = role
119-
name = item.get("name")
120-
if isinstance(name, str) and name:
121-
prefix = f"{prefix} ({name})"
122-
content_str = _stringify_content(item.get("content"))
123-
return f"{prefix}: {content_str}" if content_str else prefix
124-
125-
item_type = item.get("type", "item")
126-
rest = {k: v for k, v in item.items() if k != "type"}
127-
try:
128-
serialized = json.dumps(rest, ensure_ascii=False, default=str)
129-
except TypeError:
130-
serialized = str(rest)
131-
return f"{item_type}: {serialized}" if serialized else str(item_type)
132-
133-
134-
def _stringify_content(content: Any) -> str:
135-
if content is None:
136-
return ""
137-
if isinstance(content, str):
138-
return content
139-
try:
140-
return json.dumps(content, ensure_ascii=False, default=str)
141-
except TypeError:
142-
return str(content)
143-
144-
145-
def _flatten_nested_history_messages(
146-
items: list[TResponseInputItem],
147-
) -> list[TResponseInputItem]:
148-
flattened: list[TResponseInputItem] = []
149-
for item in items:
150-
nested_transcript = _extract_nested_history_transcript(item)
151-
if nested_transcript is not None:
152-
flattened.extend(nested_transcript)
153-
continue
154-
flattened.append(deepcopy(item))
155-
return flattened
156-
157-
158-
def _extract_nested_history_transcript(
159-
item: TResponseInputItem,
160-
) -> list[TResponseInputItem] | None:
161-
content = item.get("content")
162-
if not isinstance(content, str):
163-
return None
164-
start_idx = content.find(_CONVERSATION_HISTORY_START)
165-
end_idx = content.find(_CONVERSATION_HISTORY_END)
166-
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
167-
return None
168-
start_idx += len(_CONVERSATION_HISTORY_START)
169-
body = content[start_idx:end_idx]
170-
lines = [line.strip() for line in body.splitlines() if line.strip()]
171-
parsed: list[TResponseInputItem] = []
172-
for line in lines:
173-
parsed_item = _parse_summary_line(line)
174-
if parsed_item is not None:
175-
parsed.append(parsed_item)
176-
return parsed
177-
178-
179-
def _parse_summary_line(line: str) -> TResponseInputItem | None:
180-
stripped = line.strip()
181-
if not stripped:
182-
return None
183-
dot_index = stripped.find(".")
184-
if dot_index != -1 and stripped[:dot_index].isdigit():
185-
stripped = stripped[dot_index + 1 :].lstrip()
186-
role_part, sep, remainder = stripped.partition(":")
187-
if not sep:
188-
return None
189-
role_text = role_part.strip()
190-
if not role_text:
191-
return None
192-
role, name = _split_role_and_name(role_text)
193-
reconstructed: dict[str, Any] = {"role": role}
194-
if name:
195-
reconstructed["name"] = name
196-
content = remainder.strip()
197-
if content:
198-
reconstructed["content"] = content
199-
return cast(TResponseInputItem, reconstructed)
200-
201-
202-
def _split_role_and_name(role_text: str) -> tuple[str, str | None]:
203-
if role_text.endswith(")") and "(" in role_text:
204-
open_idx = role_text.rfind("(")
205-
possible_name = role_text[open_idx + 1 : -1].strip()
206-
role_candidate = role_text[:open_idx].strip()
207-
if possible_name:
208-
return (role_candidate or "developer", possible_name)
209-
return (role_text or "developer", None)
210-
211-
212-
def _get_run_item_role(run_item: RunItem) -> str | None:
213-
role_candidate = run_item.to_input_item().get("role")
214-
return role_candidate if isinstance(role_candidate, str) else None
215-
216-
21747
def _remove_tools_from_items(items: tuple[RunItem, ...]) -> tuple[RunItem, ...]:
21848
filtered_items = []
21949
for item in items:

0 commit comments

Comments
 (0)