Skip to content

Nest handoff history by default - #1996

Merged
jhills20 merged 22 commits into
mainfrom
codex/add-nest_handoff_history-field-and-functionality
Nov 17, 2025
Merged

Nest handoff history by default#1996
jhills20 merged 22 commits into
mainfrom
codex/add-nest_handoff_history-field-and-functionality

Conversation

@jhills20

Copy link
Copy Markdown
Contributor

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

@seratch seratch added feature:core enhancement New feature or request labels Oct 28, 2025
@seratch
seratch requested a review from rm-openai October 28, 2025 04:38
Comment thread src/agents/_run_impl.py Outdated
Comment thread src/agents/_run_impl.py Outdated

@rm-openai rm-openai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of kaz's feedback seems right. In addition, I'd say two major things:

  1. This setting should be on the Handoff object
  2. Since you're updating the default, this is a breaking change, so please update the changelog.

@seratch seratch mentioned this pull request Oct 30, 2025
@seratch seratch added this to the 0.6.x milestone Nov 5, 2025
@jhills20 jhills20 closed this Nov 7, 2025
@jhills20 jhills20 reopened this Nov 8, 2025

@jhills20 jhills20 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to Handoff, added handoff history .py file for non extension parts of handoff filter, added changelog

Comment thread docs/release.md Outdated
Comment on lines +28 to +29
- 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 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we;ve release 0.5.0 so you'll need to move this to a 0.6.0 section

Comment thread src/agents/_run_impl.py
Comment on lines +1019 to +1027
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,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird syntax - also can you use f-strings and get rid of getattr?

Comment on lines +20 to +24
__all__ = [
"remove_all_tools",
"nest_handoff_history",
"default_handoff_history_mapper",
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove this, not super useful

Comment thread docs/handoffs.md
@jhills20
jhills20 merged commit a776d80 into main Nov 17, 2025
9 checks passed
@jhills20
jhills20 deleted the codex/add-nest_handoff_history-field-and-functionality branch November 17, 2025 21:44
@alexmojaki

Copy link
Copy Markdown
Contributor

Are you aware that this is duplicating everything by default? For example this code:

import asyncio

from agents import Agent, Runner, function_tool


@function_tool
def random_number() -> int:
    return 4


agent2 = Agent(name='agent2', instructions='Return double the number')
agent1 = Agent(name='agent1', tools=[random_number], handoffs=[agent2])


asyncio.run(Runner.run(agent1, input='Generate a random number then, hand off to agent2.'))

sends all of this:

[
  {
    "role": "assistant",
    "content": "For context, here is the conversation so far between the user and the previous agent:\n<CONVERSATION HISTORY>\n1. user: Generate a random number then, hand off to agent2.\n2. function_call: {\"arguments\": \"{}\", \"call_id\": \"call_Wsu7AtBXA186HIw3qOLST3KJ\", \"name\": \"random_number\", \"id\": \"fc_0001f0f3cb0a689000691db920bd54819d805de7a682ee3d08\", \"status\": \"completed\"}\n3. function_call_output: {\"call_id\": \"call_Wsu7AtBXA186HIw3qOLST3KJ\", \"output\": \"4\"}\n4. function_call: {\"arguments\": \"{}\", \"call_id\": \"call_9xPh03L6GqirBstlCgFE9Oqn\", \"name\": \"transfer_to_agent2\", \"id\": \"fc_0001f0f3cb0a689000691db922b564819dbf86d6da3014bb36\", \"status\": \"completed\"}\n5. function_call_output: {\"call_id\": \"call_9xPh03L6GqirBstlCgFE9Oqn\", \"output\": \"{\\\"assistant\\\": \\\"agent2\\\"}\"}\n</CONVERSATION HISTORY>"
  },
  {
    "arguments": "{}",
    "call_id": "call_Wsu7AtBXA186HIw3qOLST3KJ",
    "name": "random_number",
    "type": "function_call",
    "id": "fc_0001f0f3cb0a689000691db920bd54819d805de7a682ee3d08",
    "status": "completed"
  },
  {
    "call_id": "call_Wsu7AtBXA186HIw3qOLST3KJ",
    "output": "4",
    "type": "function_call_output"
  },
  {
    "arguments": "{}",
    "call_id": "call_9xPh03L6GqirBstlCgFE9Oqn",
    "name": "transfer_to_agent2",
    "type": "function_call",
    "id": "fc_0001f0f3cb0a689000691db922b564819dbf86d6da3014bb36",
    "status": "completed"
  },
  {
    "call_id": "call_9xPh03L6GqirBstlCgFE9Oqn",
    "output": "{\"assistant\": \"agent2\"}",
    "type": "function_call_output"
  }
]

Is it better in other cases?

@giuliohome

giuliohome commented Nov 19, 2025

Copy link
Copy Markdown

I've tested this in our environment and it looks good to me.
I noticed the context size has increased — assuming that's intentional, it should have a positive impact overall.

@KthProg

KthProg commented Nov 28, 2025

Copy link
Copy Markdown

Ever since this change, I keep getting this error:
No tool output found for function call call_NAwYOrJ0yPljsKVLSjKC9W8v..

When I check traces, the tool calls are still there but with empty responses. Any idea what gives? Does the remove_all_tools input filter need to be updated?

For reference this is my setup:

      file_search_tool = FileSearchTool(
        max_num_results=5,
        vector_store_ids=[vector_store_id],
      )

  agent_1= Agent(name=f"Agent 1", model="gpt-4o-mini",   instructions=prompt_with_handoff_instructions(agent_1_instructions), tools=[
        file_search_tool
      ], handoff_description="Agent 1...")

      agent_2= Agent(
        name=f"Agent 2", model="gpt-4o-mini", instructions=prompt_with_handoff_instructions(agent_2_instructions), tools=[
          file_search_tool
        ],
        mcp_servers=[mcp_server],
        handoff_description="Agent 2...")

      agent_1_handoff= handoff(
        agent=agent_1,
        on_handoff=on_handoff_agent_1,
        nest_handoff_history=False,
        input_filter=remove_all_tools
      )

      agent_2_handoff = handoff(
        agent=agent_2,
        on_handoff=agent_2,
        nest_handoff_history=False,
        input_filter=remove_all_tools
      )

      triage_agent = Agent(
        name=f"Triage Agent",
        model="gpt-4o-mini",
        instructions=prompt_with_handoff_instructions(triage_instructions),
        handoffs=[agent_1_handoff, agent_2_handoff],
        handoff_description="An agent that triages customer inquiries and hands off to the appropriate agent",
      )

  result = Runner.run_streamed(
    agent,
    user_message,
    previous_response_id=previous_response_id,
  )

The error occurs only when a previous response id is provided.

@Sourav-Nandy-ai

Sourav-Nandy-ai commented Jul 11, 2026

Copy link
Copy Markdown

Hi James,

This PR is a good example of a "threshold evals still pass, behavior distribution shifts" change. Making handoff-history nesting the default is the right call architecturally, but it changes what every downstream agent sees as context on every handoff.

I actually ran this end-to-end against the real SDK on both sides. The agents.RunConfig() on version 0.5.1 has no nest_handoff_history field at all, whereas on 0.6.0 it defaults to True. I then built a real triage-to-specialist handoff with a manufactured 3-turn prior conversation and called Runner.run() for real (using one gpt-4o-mini call per side).

Pre-fix (0.5.1): The result.to_input_list() keeps the 3 prior turns as 3 separate flat items.

Post-fix (0.6.0): Those same 3 turns collapse into a single role: "assistant" item with a summary block. They are genuinely nested rather than just reordered.
openai.github.io

The final output was identical on both sides, so this is a clean, isolated internal-representation change exactly as advertised.

As a minor aside, your merge commit calls it a "developer-role" summary, but the shipped code and what I actually observed both use role: "assistant". This is probably just stale commit-message wording.

That is the exact class of change agent-eval (agent_regress) is built to catch at scale. Instead of asserting individual outputs pass or fail, it runs an agent N times before and after a change and performs a real statistical comparison for a REGRESSED, STABLE, or IMPROVED verdict on the distribution. This would be useful here since two people (alexmojaki and KthProg) already hit real downstream regressions from this default flip before reaching for nest_handoff_history=False.

My OpenAI Agents SDK integration used to be flatly broken. It called a nonexistent agent.run() and crashed on the first call. That is fixed now, and I verified it against the real installed SDK for this check. I am genuinely curious whether a distributional before-and-after comparison on a handoff-nesting toggle specifically would have caught anything useful pre-merge. Let me know what you think if you get a chance to try it.

You can find the repository here: https://github.com/RudrenduPaul/agent-eval

Best,
Sourav

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants