Skip to content

Python: Align HandoffBuilder participant type signatures with runtime Agent-only requirement#4302

Merged
moonbox3 merged 1 commit intomicrosoft:mainfrom
moonbox3:agent/fix-4301-1
Feb 26, 2026
Merged

Python: Align HandoffBuilder participant type signatures with runtime Agent-only requirement#4302
moonbox3 merged 1 commit intomicrosoft:mainfrom
moonbox3:agent/fix-4301-1

Conversation

@moonbox3
Copy link
Contributor

Motivation and Context

HandoffBuilder accepted SupportsAgentRun in its type signatures (e.g. participants(), add_handoff(), with_start_agent()), but _prepare_agent_with_handoffs raised a TypeError at runtime if the participant was not an Agent instance. This mismatch caused confusing late failures and incorrect API documentation.

Fixes #4301

Description

The root cause was that the public API surface of HandoffBuilder used SupportsAgentRun as the participant type, while the internal implementation relied on Agent-specific capabilities (cloning, tool injection, middleware) and enforced this with a runtime isinstance check buried in _prepare_agent_with_handoffs. The fix narrows all public type annotations from SupportsAgentRun to Agent, moves the validation into participants() with a clear error message, removes the redundant runtime check in _prepare_agent_with_handoffs, and adds a test confirming that non-Agent SupportsAgentRun implementors are rejected early with a descriptive TypeError.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Note: PR autogenerated by moonbox3's agent

…entRun (microsoft#4301)

HandoffBuilder.participants() accepted SupportsAgentRun by API contract,
but build() failed at runtime because _prepare_agent_with_handoffs()
requires Agent instances for cloning, tool injection, and middleware.

Fix: Update all public type hints, docstrings, and validation in
HandoffBuilder and HandoffAgentExecutor to require Agent explicitly.
The isinstance check is now performed early in participants() with a
clear error message explaining why Agent is required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 26, 2026 09:10
Copy link
Contributor Author

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 3 | Confidence: 85%

✓ Correctness

This is a clean type-narrowing refactor that replaces the broader SupportsAgentRun protocol with the concrete Agent class throughout the handoff module. The runtime isinstance check in participants() was correctly updated, and a new test validates the stricter behavior. The only notable issue is an inconsistent docstring in with_autonomous_mode that now mentions both SupportsAgentRun and Agent despite the type having been narrowed to Agent only. No correctness bugs were found.

✓ Security Reliability

This diff narrows the accepted type from the SupportsAgentRun protocol to the concrete Agent class throughout the handoff system. The public entry point (HandoffBuilder.participants) retains a runtime isinstance check, but the runtime guard inside _prepare_agent_with_handoffs was removed. Since HandoffAgentExecutor is a public class whose init now accepts agent: Agent without any runtime validation, a caller bypassing HandoffBuilder could pass a non-Agent object and hit undefined behavior in _clone_chat_agent. This is a defense-in-depth gap, not a critical vulnerability. Otherwise the change is straightforward and the new test correctly validates the public-API guard.

✓ Test Coverage

The diff narrows type constraints from SupportsAgentRun to Agent across the handoff module and adds one test validating that participants() rejects non-Agent SupportsAgentRun instances. The test is well-structured with a meaningful assertion (TypeError match). However, coverage gaps exist: the runtime guard removed from _prepare_agent_with_handoffs has no replacement test, and other public API methods that changed signatures (add_handoff, with_start_agent) lack rejection tests. The constructor's participants= kwarg path is implicitly covered since it delegates to participants(), but that's not explicitly verified.

Suggestions

  • In with_autonomous_mode, the docstring on line 947 was changed to say SupportsAgentRun / Agent instances but the type annotation was narrowed to Sequence[Agent]. This should just say Agent instances to be consistent with the rest of the refactor.
  • The runtime isinstance(agent, Agent) check was removed from _prepare_agent_with_handoffs (the old lines 253-254). While the builder's participants() validates inputs, HandoffAgentExecutor can still be constructed directly without the builder, so callers bypassing the builder lose the runtime safety net. Consider keeping a runtime guard in __init__ or _prepare_agent_with_handoffs for defense-in-depth.
  • Consider keeping a lightweight runtime isinstance check (or assert) at the top of HandoffAgentExecutor.init or _prepare_agent_with_handoffs. The old TypeError guard was removed (around old line 253), but HandoffAgentExecutor is a public class — callers that construct it directly bypass HandoffBuilder.participants() validation, and Python type hints are not enforced at runtime.
  • Add a test that passes a non-Agent SupportsAgentRun to add_handoff(source=...) and add_handoff(targets=[...]) to verify those entry points also reject non-Agent instances consistently.
  • Add a test that passes a non-Agent to with_start_agent() to verify the narrowed type constraint at that entry point.
  • Consider adding a test for HandoffBuilder(participants=[fake]) (constructor kwarg path) to explicitly verify it delegates to the validated participants() method, even though it's implicitly covered.
  • The runtime TypeError guard was removed from _prepare_agent_with_handoffs (old lines 253-254). If a non-Agent somehow bypasses the participants() check (e.g., via internal misuse), the error will now surface as an opaque failure in _clone_chat_agent. Consider adding a regression test or restoring a guard with a clear error message in _prepare_agent_with_handoffs.

Automated review by moonbox3's agents

@markwallace-microsoft
Copy link
Member

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/orchestrations/agent_framework_orchestrations
   _handoff.py3855884%105–106, 108, 163–173, 175, 177, 179, 184, 279, 285, 317, 340, 362, 418, 443, 501, 533, 591–592, 624, 632, 636–637, 675–677, 682–684, 802, 805, 818, 880, 885, 892, 902, 904, 923, 925, 1007–1008, 1040–1041, 1123, 1130, 1202–1203, 1205
TOTAL22172276187% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4673 247 💤 0 ❌ 0 🔥 1m 16s ⏱️

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a type mismatch in HandoffBuilder where the API accepted SupportsAgentRun in type signatures but enforced Agent-only at runtime, causing late failures with confusing error messages. The fix narrows all public type annotations from SupportsAgentRun to Agent, moves validation into the participants() method with a clear error message, removes the redundant runtime check in _prepare_agent_with_handoffs, and adds a test confirming early rejection of non-Agent SupportsAgentRun implementors.

Changes:

  • Updated type signatures to require Agent instead of SupportsAgentRun throughout HandoffBuilder and HandoffAgentExecutor
  • Moved isinstance(participant, Agent) validation from _prepare_agent_with_handoffs to participants() with improved error messaging
  • Added test to verify non-Agent SupportsAgentRun implementations are rejected early with a descriptive error

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py Updated type signatures from SupportsAgentRun to Agent in HandoffBuilder and HandoffAgentExecutor; moved validation to participants() method; updated documentation to clarify Agent-only requirement
python/packages/orchestrations/tests/test_handoff.py Added test test_handoff_builder_rejects_non_agent_supports_agent_run() to verify early rejection of non-Agent SupportsAgentRun implementations

@moonbox3 moonbox3 self-assigned this Feb 26, 2026
@moonbox3 moonbox3 moved this to In Review in Agent Framework Feb 26, 2026
@moonbox3 moonbox3 enabled auto-merge February 26, 2026 10:39
@eavanvalkenburg
Copy link
Member

my only question is what happens with things like ClaudeAgent and CopilotAgent with this? are those not possible for handoffs?

@moonbox3 moonbox3 added this pull request to the merge queue Feb 26, 2026
Merged via the queue into microsoft:main with commit 97b2499 Feb 26, 2026
34 checks passed
@github-project-automation github-project-automation bot moved this from In Review to Done in Agent Framework Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Python: HandoffBuilder participants contract conflicts with runtime Agent-only enforcement

5 participants