mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 05:13:58 +02:00
fix: only project handoff tool calls as FunctionCallContent
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>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<FunctionCallContent> 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();
|
||||
|
||||
Reference in New Issue
Block a user