.NET: feat: Refactor Handoff Orchestration and add HITL support#5174
Open
.NET: feat: Refactor Handoff Orchestration and add HITL support#5174
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Refactors the .NET handoff orchestration executors to use a stateful “ContinueTurn” pattern and adds HITL support for unserviced function calls and tool approval requests, aligning behavior with the existing AIAgentHostExecutor flow.
Changes:
- Refactors
HandoffAgentExecutorto be stateful, factory-instantiated, and able to surface/continue on pendingFunctionCallContent+ToolApprovalRequestContent. - Extracts shared “unserviced request” collection/submission logic into
AIAgentUnservicedRequestsCollector. - Fixes
StatefulExecutorcaching behavior to properly load state from checkpoint on first access.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffAgentExecutorTests.cs | Updates executor construction to new signature. |
| dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs | Adds HITL handoff tests and updates checkpointed run helpers to surface pending requests. |
| dotnet/src/Microsoft.Agents.AI.Workflows/StatefulExecutor.cs | Fixes initial cache load from checkpoint; refactors protocol configuration path. |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffsStartExecutor.cs | Renames constants/class to “Handoff…” and propagates “previous agent” tracking. |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffsEndExecutor.cs | Renames to HandoffEndExecutor and updates persisted agent tracking field. |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffState.cs | Renames state fields to clarify semantics (requested target vs previous agent). |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs | Major refactor to stateful + HITL support, plus checkpoint persistence for pending requests. |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentUnservicedRequestsCollector.cs | New helper to collect/submit pending external requests from agent outputs. |
| dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs | Uses new collector; improves synthetic message metadata on resume. |
| dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs | Switches to factory-based executor bindings and updates routing to renamed state fields. |
dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentUnservicedRequestsCollector.cs
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentUnservicedRequestsCollector.cs
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
Outdated
Show resolved
Hide resolved
* Change HandoffAgentExecutor to use factory-based instantiation * Extract shared request collection logic in AIAgentUnservicedRequestsCollector * Refactor HandoffAgentExecutor to use the "ContinueTurn" pattern as in AIAgentHostExecutor
ccbfe66 to
c3f0999
Compare
SergeyMenshykh
approved these changes
Apr 9, 2026
| { | ||
| if (this._userInputRequests.ContainsKey(userInputRequest.RequestId)) | ||
| { | ||
| throw new InvalidOperationException($"ToolApprovalRequestContent with duplicate RequestId: ${userInputRequest.RequestId}"); |
Member
There was a problem hiding this comment.
Suggested change
| throw new InvalidOperationException($"ToolApprovalRequestContent with duplicate RequestId: ${userInputRequest.RequestId}"); | |
| throw new InvalidOperationException($"ToolApprovalRequestContent with duplicate RequestId: {userInputRequest.RequestId}"); |
| { | ||
| if (this._functionCalls.ContainsKey(functionCall.CallId)) | ||
| { | ||
| throw new InvalidOperationException($"FunctionCallContent with duplicate CallId: ${functionCall.CallId}"); |
Member
There was a problem hiding this comment.
Suggested change
| throw new InvalidOperationException($"FunctionCallContent with duplicate CallId: ${functionCall.CallId}"); | |
| throw new InvalidOperationException($"FunctionCallContent with duplicate CallId: {functionCall.CallId}"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
Due to needing special-case logic, the Handoff orchestration pattern implementation relies on a custom
AIAgenthosting executor (HandoffAgentExecutor). While support for turn-interruption due to unserviced function calls (AIFunctionDeclaration-only) or tool approval demands was added toAIAgentHostExecutor, it was not implemented forHandoffAgentExecutor.Description
FunctionCallContentandToolApprovalRequestContentcoming out ofAIAgentsused in Handoff workflowsHandoffAgentExecutordoes not emitRequestInfoEventforToolApprovalRequestContent—ApprovalRequiredAIFunctionunusable in handoff workflows #5035HandoffAgentExecutorto be stateful, and switch to using executor factory pattern for instantiationStatefulExecutorwhere first access to cacheable state (skipCache = false, non-concurrent) will never read the state from the checkpoint, causing errorsContribution Checklist
[ ] Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.