From f8c6320cb9001f3da43651a3f1f5848b8cc6ea1c Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Fri, 8 May 2026 13:13:35 -0400 Subject: [PATCH] fix: Synthesized Handoff FunctionResult is never sent to agent When we receive a handoff request from the agent, we need to service it outside of the Agent Loop to terminate the loop. What this means is that we take ownership of terminating the call by feeding the result back into the agent on a subsequent invocation. When we refactored Handoff to support HITL and make use of AgentSession, we inadvertantly removed this step, causing subsequent invocations to the Handoff agent to fail (first works, but breaks the state). The fix is to be more precise about the agent's bookmark when concatenating the result of agent invocation to the shared conversation history. --- .../Specialized/HandoffAgentExecutor.cs | 75 +++++++++++-------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs index 576c749a90..6ee4c9c098 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs @@ -266,7 +266,33 @@ internal sealed class HandoffAgentExecutor : sharedState.Conversation.AddMessages(incomingMessages); } - newConversationBookmark = sharedState.Conversation.AddMessages(result.Response.Messages); + if (result.IsHandoffRequested) + { + int preHandoffMessageCount = result.Response.Messages.Count - 1; + newConversationBookmark = sharedState.Conversation.AddMessages(result.Response.Messages.Take(preHandoffMessageCount)); + + // The following message contains the Handoff FunctionCallResult which should be added to the conversation history with + // the caveat that we need to get it back next time _this_ agent is invoked because we need to feed the FunctionCallResult + // back to the agent. So ignore the bookmark update. + ChatMessage handoffCallResultMessage = result.Response.Messages[preHandoffMessageCount]; + + if (handoffCallResultMessage.Role != ChatRole.Tool) + { + throw new InvalidOperationException("The last message in a handoff response must be a Tool message containing the Handoff FunctionCallResult."); + } + + if (handoffCallResultMessage.Contents.Count != 1 || + handoffCallResultMessage.Contents[0] is not FunctionResultContent) + { + throw new InvalidOperationException("The Tool message in a handoff response must contain exactly one content item of type FunctionResultContent."); + } + + _ = sharedState.Conversation.AddMessage(handoffCallResultMessage); + } + else + { + newConversationBookmark = sharedState.Conversation.AddMessages(result.Response.Messages); + } return new ValueTask(); }, @@ -376,39 +402,28 @@ internal sealed class HandoffAgentExecutor : List updates = []; List candidateRequests = []; - await this.InvokeWithStateAsync( - async (state, ctx, ct) => + this._session ??= await this._agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false); + + IAsyncEnumerable agentStream = + this._agent.RunStreamingAsync(messages, this._session, this._agentOptions, cancellationToken); + + await foreach (AgentResponseUpdate update in agentStream.ConfigureAwait(false)) + { + await AddUpdateAsync(update, cancellationToken).ConfigureAwait(false); + + collector.ProcessAgentResponseUpdate(update, CollectHandoffRequestsFilter); + + bool CollectHandoffRequestsFilter(FunctionCallContent candidateHandoffRequest) { - this._session ??= await this._agent.CreateSessionAsync(ct).ConfigureAwait(false); - - IAsyncEnumerable agentStream = - this._agent.RunStreamingAsync(messages, - this._session, - options: this._agentOptions, - cancellationToken: ct); - - await foreach (AgentResponseUpdate update in agentStream.ConfigureAwait(false)) + bool isHandoffRequest = this._handoffFunctionNames.Contains(candidateHandoffRequest.Name); + if (isHandoffRequest) { - await AddUpdateAsync(update, ct).ConfigureAwait(false); - - collector.ProcessAgentResponseUpdate(update, CollectHandoffRequestsFilter); - - bool CollectHandoffRequestsFilter(FunctionCallContent candidateHandoffRequest) - { - bool isHandoffRequest = this._handoffFunctionNames.Contains(candidateHandoffRequest.Name); - if (isHandoffRequest) - { - candidateRequests.Add(candidateHandoffRequest); - } - - return !isHandoffRequest; - } + candidateRequests.Add(candidateHandoffRequest); } - return state; - }, - context, - cancellationToken: cancellationToken).ConfigureAwait(false); + return !isHandoffRequest; + } + } if (candidateRequests.Count > 1) {