Skip to content

Python: keep agent compaction configuration when HandoffBuilder clones participants - #8329

Merged
Evan Mattson (moonbox3) merged 4 commits into
microsoft:mainfrom
manjunathshiva:python-handoff-clone-keeps-compaction-8320
Sep 17, 2026
Merged

Evan Mattson (moonbox3) merged 4 commits into
microsoft:mainfrom
manjunathshiva:python-handoff-clone-keeps-compaction-8320

Conversation

@manjunathshiva

@manjunathshiva Manjunath Janardhan (manjunathshiva) commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

HandoffAgentExecutor._clone_chat_agent rebuilds each participant through Agent(...) to inject
handoff tools and middleware, listing constructor arguments by hand. compaction_strategy and
tokenizer live outside default_options and were missing from that list, so the clone got None
for both:

compaction_strategy   original=ToolResultCompactionStrategy   clone=None   *** DROPPED ***
tokenizer             original=CharacterEstimatorTokenizer    clone=None   *** DROPPED ***
instructions          KEPT          name  KEPT

Nothing raises. The participant simply runs with no compaction, so a long handoff conversation grows
unbounded until it trips the model's context limit — surfacing as cost and latency well before it
surfaces as an error.

One note on the issue text: it says "the cloned executor agent gets None for both" and "forward
both fields", but only ever names compaction_strategy. The second field is tokenizer. A fix
written from the title alone would close half the bug.

Description & Review Guide

  • What are the major changes? Two arguments forwarded in _clone_chat_agent, plus two tests.
    No API change, no change to compaction behaviour itself.

  • Why by reference and not deepcopy. Both are forwarded as-is, matching context_providers
    and middleware on the lines above. I checked that the strategies hold only immutable
    configuration — ToolResultCompactionStrategy stores one threshold, ContextWindowCompactionStrategy
    ten — and CharacterEstimatorTokenizer is stateless, so clones cannot interfere through them.
    additional_properties stays deep-copied because it is a mutable mapping the clone does own.

  • Why not replace the hand-rolled constructor with a copy. Agent inherits __copy__ and
    __deepcopy__ from SerializationMixin, which copy every instance field and would have fixed this
    class of bug permanently — so I checked whether they could be used here. They cannot:
    Agent.__init__ re-separates MCP tools from regular tools, which is exactly why this method
    recombines agent.mcp_tools into the tools list before calling it. Copying the instance would skip
    that separation, and copy would additionally share default_options with the original, so
    mutating allow_multiple_tool_calls on the clone would reach back into the caller's agent.
    Rebuilding through the constructor is deliberate and stays.

  • The second test is the point. Every parameter added to Agent.__init__ has to be added to this
    call or it is silently dropped, and the test_handoff_clone_preserves_* tests directly above were
    each written after that already happened — this is at least the fourth field to go missing.
    test_handoff_clone_forwards_every_agent_constructor_field reads the keyword names off the
    Agent(...) call with ast and asserts every constructor parameter is either forwarded or named in
    handled_elsewhere with a reason. The next new parameter fails there instead of in a user's
    workflow. Reading the AST rather than substring-matching the source is deliberate: I verified that a
    commented-out argument still fails the guard, which a substring check would have passed.

  • What is the impact of these changes? Compaction configured on a handoff participant now takes
    effect. That is a behaviour change in the sense that previously-ignored configuration starts
    working, so a long-running handoff conversation will begin compacting where it did not before. I
    have not marked it a breaking change because it makes documented configuration behave as
    documented, but say if you would rather label it.

  • What do you want reviewers to focus on? Whether by-reference is the sharing you want for these
    two, and whether the drift guard is welcome — it is a little unusual for this suite, and I would
    rather drop it than have it be a maintenance burden. _clone_chat_agent is the only
    clone-by-reconstruct site in the repo, so there is no sibling instance to fix.

    Validation: orchestrations 211 passing with poe check -P orchestrations clean across all five
    type checkers, and core 5329 passing unchanged. Both new tests fail against the unfixed tree.

Related Issue

Fixes #8320

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

…s participants

`_clone_chat_agent` rebuilds each participant through `Agent(...)` to inject the
handoff tools and middleware, listing constructor arguments by hand.
`compaction_strategy` and `tokenizer` live outside `default_options` and were not
in that list, so the clone silently got `None` for both and the participant ran
with no compaction at all. Nothing raises: a long handoff conversation simply
grows unbounded until it trips the model's context limit, after showing up as
cost and latency first.

Both are forwarded by reference rather than deep-copied, matching
`context_providers` and `middleware`. They hold immutable configuration the clone
never mutates, and a tokenizer can carry a vocabulary that is expensive or unsafe
to copy.

Rebuilding through the constructor is deliberate and stays: `Agent.__init__`
re-separates MCP tools from regular ones, which is why the method recombines
`agent.mcp_tools` into the tools list first. Copying the instance instead would
skip that.

The second test is the one that matters beyond this bug. Every parameter added to
`Agent.__init__` has to be added to this call or it is silently dropped, and the
`test_handoff_clone_preserves_*` tests above were each written after that already
happened. The guard reads the keyword names off the `Agent(...)` call with `ast`
and asserts every constructor parameter is either forwarded or named as
deliberately handled elsewhere, so the next missing field fails here rather than
in a user's workflow. Reading the AST rather than substring-matching the source
means a commented-out argument cannot satisfy it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused fix correctly preserves both omitted fields and includes appropriate regression protection.

Pull request overview

Preserves agent-level compaction settings when handoff participants are cloned.

Changes:

  • Forwards compaction_strategy and tokenizer to cloned agents.
  • Adds regression and constructor-field drift tests.
File summaries
File Description
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py Preserves compaction configuration during cloning.
python/packages/orchestrations/tests/test_handoff.py Tests configuration retention and future constructor coverage.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Thanks for the update. Before this is ready, could you please:

  • get the non-green checks passing: merge-gatekeeper (FAILURE), Python Tests (3.10, ubuntu-latest) (CANCELLED), Python Tests (3.10, windows-latest) (CANCELLED), Python Tests (3.11, ubuntu-latest) (FAILURE), Python Tests (3.11, windows-latest) (CANCELLED), Python Tests (3.12, ubuntu-latest) (CANCELLED), Python Tests (3.12, windows-latest) (CANCELLED), Python Tests (3.13, windows-latest) (CANCELLED), Python Tests (3.14, windows-latest) (CANCELLED)

Once those are addressed, please re-request review. Thanks!

@eavanvalkenburg

Copy link
Copy Markdown
Member

Thanks for the contribution. The approval-gated workflows completed, but the current head is not green: Python - Tests, Python - Test Coverage, and Merge Gatekeeper failed, with additional cancellations across the Python test matrix. Could you please get the checks passing, then re-request review?

@manjunathshiva

Copy link
Copy Markdown
Contributor Author

Green now, in 906d9680b — the failure was not in this change.

The run was 1 failed / 12546 passed, and the single failure was
test_execute_code_tool_clears_output_after_rejection in packages/hyperlight/, on
output_root.iterdir(). This PR only touches packages/orchestrations/.

Evan Mattson (@moonbox3)'s #8380, "stabilize Hyperlight output cleanup test", fixes it; this branch simply sat
below that commit. I have merged main to pick it up and confirmed the test passes here. The
cancellations across the rest of the matrix were the gatekeeper reacting to that one failure rather
than separate problems.

Re-verified after the merge rather than assuming, since main moved 42 commits: orchestrations 211
passing with poe check -P orchestrations clean across all five type checkers, core 5329 unchanged,
and both new tests still fail against the unfixed tree. Agent.__init__ gained no parameter in
those 42 commits, so the drift guard still reflects the current constructor -- and would have failed
here if it had.

No change to this PR's own code was needed.

On re-requesting review: you are still in the pending-reviewer list here -- your last message was a PR comment rather than a submitted review -- so there is no re-request for me to make. The one thing that would unblock it is releasing the approval-gated workflows on this head: they queue ataction_required on every push, so the checks cannot go green until someone runs them. Happy to re-request the moment there is something to re-request.

@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Sep 17, 2026
Merged via the queue into microsoft:main with commit ab8299b Sep 17, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: HandoffBuilder drops agent-level compaction_strategy when cloning participants

4 participants