Skip to content

Python: (fix) Published HandoffRequest message had no cancellation token attached - #13130

Open
Audric Schiltknecht (audricschiltknecht) wants to merge 2 commits into
microsoft:mainfrom
audricschiltknecht:fix/python-handoff-cancellation-token
Open

Audric Schiltknecht (audricschiltknecht) wants to merge 2 commits into
microsoft:mainfrom
audricschiltknecht:fix/python-handoff-cancellation-token

Conversation

@audricschiltknecht

@audricschiltknecht Audric Schiltknecht (audricschiltknecht) commented Sep 16, 2025 •

Copy link
Copy Markdown

Motivation and Context

When a handoff occurs, the HandoffRequest was not passed the original cancellation token, leading to the creation of a separate token. This made the OrchestrationResult impossible to cancel/stop the runtime as the agent are testing for the wrong token in the HandoffAgentActor::_handle_request_message() method.

Description

Simply pass the cancellation token in the publish_message() call.

Contribution Checklist

@moonbox3 Evan Mattson (moonbox3) added the python Pull requests for the Python Semantic Kernel label Sep 16, 2025
@audricschiltknecht

Copy link
Copy Markdown
Author

I would need a bit of help to add testing (if possible at all). I was looking at inspiration from the test_invoke_with_handoff_function_call, but I'm not understanding how I could simulate that the agent does not complete the task.

@moonbox3 Evan Mattson (moonbox3) changed the title Python: Published HandoffRequest message had no cancellation token attached Python: (fix) Published HandoffRequest message had no cancellation token attached Apr 8, 2026
@moonbox3

Copy link
Copy Markdown
Collaborator

Audric Schiltknecht (@audricschiltknecht) please have a look at this test:

@pytest.mark.skipif(                                                                                                                                                                               
    sys.version_info < (3, 11),                                                                                                                                                                    
    reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)                                                                                                                                                                                                  
async def test_invoke_cancel_during_handoff():                  
    """Test that cancellation propagates through handoff requests.                                                                                                                                 
                                                                                                                                                                                                   
    When agent A hands off to agent B and the orchestration is cancelled,
    agent B should not process the handoff request because the cancellation                                                                                                                        
    token should be propagated with the HandoffRequestMessage.                                                                                                                                     
    """
    agent_b = MockAgent()                                                                                                                                                                          
    agent_a = MockAgentWithHandoffFunctionCall(agent_b)                                                                                                                                            
 
    with (                                                                                                                                                                                         
        patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
    ):                                                                                                                                                                                             
        runtime = InProcessRuntime()
        runtime.start()                                                                                                                                                                            
                                                                
        try:
            orchestration = HandoffOrchestration(
                members=[agent_a, agent_b],                                                                                                                                                        
                handoffs={
                    agent_a.name: {agent_b.name: "test"},                                                                                                                                          
                    agent_b.name: {agent_a.name: "test"},       
                },                                                                                                                                                                                 
            )                                                   
            orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)                                                                                                
                                                                                                                                                                                                   
            # Wait long enough for agent_a to complete its handoff (~0.1s)                                                                                                                         
            # but cancel before agent_b processes the request                                                                                                                                      
            await asyncio.sleep(0.15)                                                                                                                                                              
            orchestration_result.cancel()                       
        finally:                                                                                                                                                                                   
            await runtime.stop_when_idle()                      
                                                                                                                                                                                                   
        # Agent B should NOT have been invoked because the cancellation token
        # was propagated with the HandoffRequestMessage and the ActorBase                                                                                                                          
        # checks `ctx.cancellation_token.is_cancelled()` before processing.                                                                                                                        
        # Without the fix, agent B receives a fresh (non-cancelled) token
        # and processes the request anyway.                                                                                                                                                        
        assert mock_invoke_stream.call_count == 0

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.

Copilot review overview

🟡 Changes recommended

Add regression coverage verifying cancellation after a handoff is published.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes cancellation-token propagation for Python handoff requests.

Changes:

  • Reuses the orchestration cancellation token during handoffs.
  • Preserves cancellation behavior across agent transitions.
File Description
python/​semantic_kernel/​agents/​orchestration/​handoffs.py Propagates the cancellation token during handoff requests.

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

await self.publish_message(
HandoffRequestMessage(agent_name=self._handoff_agent_name),
TopicId(self._internal_topic_type, self.id.key),
cancellation_token=cts.cancellation_token,

This branch had an error being deployed

1 failed deployment
github-app-auth — 6e18dd95 Deployed Sep 28, 2026 by eavanvalkenburg via add_label #29078
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests for the Python Semantic Kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants