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();