From 154787c336e6b3feb960f2a0bc37a0213940c501 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Fri, 27 Mar 2026 21:12:42 +0100 Subject: [PATCH] fix: only project handoff tool calls as FunctionCallContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AryxCopilotAgent was converting ALL Copilot tool requests (ask_user, MCP tools, etc.) into FunctionCallContent. The Agent Framework's AIAgentHostExecutor tracks every FunctionCallContent as an outstanding request. Since SDK-handled tools like ask_user never produce a matching FunctionResultContent, HasOutstandingRequests stayed true and TurnToken was never emitted — stalling group-chat advancement after an ask_user interaction. Now only handoff_to_* tool requests are projected as FunctionCallContent. All other tool calls remain invisible to the Agent Framework executor, matching the upstream GitHubCopilotAgent behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/AryxCopilotAgent.cs | 19 +++++++++++++++++-- .../CopilotAgentBundleTests.cs | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/sidecar/src/Aryx.AgentHost/Services/AryxCopilotAgent.cs b/sidecar/src/Aryx.AgentHost/Services/AryxCopilotAgent.cs index 6ea74e1..aac1fcb 100644 --- a/sidecar/src/Aryx.AgentHost/Services/AryxCopilotAgent.cs +++ b/sidecar/src/Aryx.AgentHost/Services/AryxCopilotAgent.cs @@ -225,6 +225,16 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable continue; } + // Only project handoff tool calls as FunctionCallContent for the Agent Framework. + // Other tool calls (ask_user, MCP tools, etc.) are resolved by the Copilot SDK + // internally and must not be surfaced, because AIAgentHostExecutor tracks every + // FunctionCallContent as an outstanding request. An unmatched request prevents + // the executor from emitting a TurnToken, which stalls group-chat advancement. + if (!IsHandoffToolName(toolRequest.Name)) + { + continue; + } + contents.Add(new FunctionCallContent( toolRequest.ToolCallId, toolRequest.Name, @@ -234,6 +244,12 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable return contents; } + private static bool IsHandoffToolName(string? name) + { + return !string.IsNullOrWhiteSpace(name) + && name.StartsWith(HandoffToolPrefix, StringComparison.Ordinal); + } + private async Task EnsureClientStartedAsync(CancellationToken cancellationToken) { if (_copilotClient.State != ConnectionState.Connected) @@ -344,8 +360,7 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable private static bool IsHandoffDeclaration(AIFunctionDeclaration declaration) { - return !string.IsNullOrWhiteSpace(declaration.Name) - && declaration.Name.StartsWith(HandoffToolPrefix, StringComparison.Ordinal); + return IsHandoffToolName(declaration.Name); } private static AIFunction CreateInvokableHandoffFunction(AIFunctionDeclaration declaration) diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotAgentBundleTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotAgentBundleTests.cs index 2eafa6b..b6a3a8f 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/CopilotAgentBundleTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotAgentBundleTests.cs @@ -125,6 +125,24 @@ public sealed class CopilotAgentBundleTests Assert.Equal("frontend specialist", functionCall.Arguments["reasonForHandoff"]?.ToString()); } + [Fact] + public void ConvertToolRequestsToFunctionCalls_SkipsNonHandoffToolCalls() + { + AssistantMessageDataToolRequestsItem[] toolRequests = + { + new() { ToolCallId = "call-001", Name = "ask_user" }, + new() { ToolCallId = "call-002", Name = "web_fetch" }, + new() { ToolCallId = "call-003", Name = "handoff_to_reviewer" }, + new() { ToolCallId = "call-004", Name = "grep" }, + }; + + IReadOnlyList result = AryxCopilotAgent.ConvertToolRequestsToFunctionCalls(toolRequests); + + FunctionCallContent single = Assert.Single(result); + Assert.Equal("call-003", single.CallId); + Assert.Equal("handoff_to_reviewer", single.Name); + } + private static AIFunction CreateTool() { ToolTarget target = new();