feat: add ask_user support to sidecar

Implement the Copilot SDK user input round-trip in the backend:
- add user-input-requested and resolve-user-input protocol DTOs
- add a CopilotUserInputCoordinator that mirrors approval flow with
  pending TaskCompletionSource state and explicit resolution
- wire SessionConfig.OnUserInputRequest through CopilotAgentBundle
  and CopilotWorkflowRunner
- extend SidecarProtocolHost to emit user input events, accept
  resolve-user-input commands, and filter ask_user from runtime
  approval tools
- add regression tests for the new coordinator and protocol flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-26 17:34:49 +01:00
co-authored by Copilot
parent 4c01d8b1a7
commit 2ae4bd01b4
8 changed files with 482 additions and 8 deletions
@@ -177,6 +177,13 @@ public sealed class ResolveApprovalCommandDto : SidecarCommandEnvelope
public string Decision { get; init; } = string.Empty;
}
public sealed class ResolveUserInputCommandDto : SidecarCommandEnvelope
{
public string UserInputId { get; init; } = string.Empty;
public string Answer { get; init; } = string.Empty;
public bool WasFreeform { get; init; }
}
public sealed class RunTurnToolingConfigDto
{
public IReadOnlyList<RunTurnMcpServerConfigDto> McpServers { get; init; } = [];
@@ -290,6 +297,17 @@ public sealed class ApprovalRequestedEventDto : SidecarEventDto
public PermissionDetailDto? PermissionDetail { get; init; }
}
public sealed class UserInputRequestedEventDto : SidecarEventDto
{
public string SessionId { get; init; } = string.Empty;
public string UserInputId { get; init; } = string.Empty;
public string? AgentId { get; init; }
public string? AgentName { get; init; }
public string Question { get; init; } = string.Empty;
public IReadOnlyList<string>? Choices { get; init; }
public bool? AllowFreeform { get; init; }
}
public sealed class CommandErrorEventDto : SidecarEventDto
{
public string Message { get; init; } = string.Empty;
@@ -22,6 +22,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
public static async Task<CopilotAgentBundle> CreateAsync(
RunTurnCommandDto command,
Func<PatternAgentDefinitionDto, PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> onPermissionRequest,
Func<PatternAgentDefinitionDto, UserInputRequest, UserInputInvocation, Task<UserInputResponse>> onUserInputRequest,
Action<PatternAgentDefinitionDto, SessionEvent>? onSessionEvent,
CancellationToken cancellationToken)
{
@@ -53,6 +54,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
},
WorkingDirectory = command.ProjectPath,
OnPermissionRequest = (request, invocation) => onPermissionRequest(definition, request, invocation),
OnUserInputRequest = (request, invocation) => onUserInputRequest(definition, request, invocation),
OnEvent = evt => onSessionEvent?.Invoke(definition, evt),
Streaming = true,
};
@@ -0,0 +1,153 @@
using System.Collections.Concurrent;
using Aryx.AgentHost.Contracts;
using GitHub.Copilot.SDK;
namespace Aryx.AgentHost.Services;
internal sealed class CopilotUserInputCoordinator
{
private readonly ConcurrentDictionary<string, PendingUserInputRequest> _pendingUserInputs = new(StringComparer.Ordinal);
public Task ResolveUserInputAsync(
ResolveUserInputCommandDto command,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
string userInputId = RequireUserInputId(command.UserInputId);
PendingUserInputRequest pending = GetPendingUserInput(userInputId);
UserInputResponse response = new()
{
Answer = command.Answer ?? string.Empty,
WasFreeform = command.WasFreeform,
};
if (!pending.Response.TrySetResult(response))
{
throw new InvalidOperationException($"User input request \"{userInputId}\" is no longer pending.");
}
return Task.CompletedTask;
}
public async Task<UserInputResponse> RequestUserInputAsync(
RunTurnCommandDto command,
PatternAgentDefinitionDto agent,
UserInputRequest request,
UserInputInvocation invocation,
Func<UserInputRequestedEventDto, Task> onUserInput,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(agent);
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(invocation);
ArgumentNullException.ThrowIfNull(onUserInput);
PendingUserInputRequest pending = CreatePendingUserInput(command);
if (!_pendingUserInputs.TryAdd(pending.UserInputId, pending))
{
throw new InvalidOperationException($"User input request \"{pending.UserInputId}\" is already pending.");
}
try
{
await onUserInput(BuildUserInputRequestedEvent(command, agent, request, pending.UserInputId))
.ConfigureAwait(false);
using CancellationTokenRegistration registration = cancellationToken.Register(
static state =>
{
((TaskCompletionSource<UserInputResponse>)state!)
.TrySetCanceled();
},
pending.Response);
return await pending.Response.Task.ConfigureAwait(false);
}
finally
{
_pendingUserInputs.TryRemove(pending.UserInputId, out _);
}
}
internal static UserInputRequestedEventDto BuildUserInputRequestedEvent(
RunTurnCommandDto command,
PatternAgentDefinitionDto agent,
UserInputRequest request,
string userInputId)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(agent);
ArgumentNullException.ThrowIfNull(request);
string? normalizedAgentId = NormalizeOptionalString(agent.Id);
string? normalizedAgentName = NormalizeOptionalString(agent.Name) ?? normalizedAgentId;
return new UserInputRequestedEventDto
{
Type = "user-input-requested",
RequestId = command.RequestId,
SessionId = command.SessionId,
UserInputId = userInputId,
AgentId = normalizedAgentId,
AgentName = normalizedAgentName,
Question = NormalizeOptionalString(request.Question) ?? string.Empty,
Choices = NormalizeOptionalStringList(request.Choices ?? []),
AllowFreeform = request.AllowFreeform,
};
}
private static PendingUserInputRequest CreatePendingUserInput(RunTurnCommandDto command)
{
return new PendingUserInputRequest(
command.RequestId,
command.SessionId,
CreateUserInputRequestId(),
new TaskCompletionSource<UserInputResponse>(TaskCreationOptions.RunContinuationsAsynchronously));
}
private PendingUserInputRequest GetPendingUserInput(string userInputId)
{
if (_pendingUserInputs.TryGetValue(userInputId, out PendingUserInputRequest? pending))
{
return pending;
}
throw new InvalidOperationException($"User input request \"{userInputId}\" is not pending.");
}
private static string RequireUserInputId(string? userInputId)
{
string? normalizedUserInputId = NormalizeOptionalString(userInputId);
return normalizedUserInputId
?? throw new InvalidOperationException("User input ID is required.");
}
private static string CreateUserInputRequestId()
{
return $"user-input-{Guid.NewGuid():N}";
}
private static string? NormalizeOptionalString(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static IReadOnlyList<string>? NormalizeOptionalStringList(IEnumerable<string?> values)
{
List<string> normalized = values
.Select(NormalizeOptionalString)
.Where(static value => value is not null)
.Cast<string>()
.ToList();
return normalized.Count > 0 ? normalized : null;
}
private sealed record PendingUserInputRequest(
string RequestId,
string SessionId,
string UserInputId,
TaskCompletionSource<UserInputResponse> Response);
}
@@ -8,6 +8,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
{
private readonly PatternValidator _patternValidator;
private readonly CopilotApprovalCoordinator _approvalCoordinator = new();
private readonly CopilotUserInputCoordinator _userInputCoordinator = new();
public CopilotWorkflowRunner(PatternValidator patternValidator)
{
@@ -19,6 +20,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
Func<TurnDeltaEventDto, Task> onDelta,
Func<AgentActivityEventDto, Task> onActivity,
Func<ApprovalRequestedEventDto, Task> onApproval,
Func<UserInputRequestedEventDto, Task> onUserInput,
CancellationToken cancellationToken)
{
PatternValidationIssueDto? validationError = _patternValidator.Validate(command.Pattern).FirstOrDefault();
@@ -38,6 +40,13 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
state.ToolNamesByCallId,
onApproval,
cancellationToken),
(agent, request, invocation) => _userInputCoordinator.RequestUserInputAsync(
command,
agent,
request,
invocation,
onUserInput,
cancellationToken),
(agent, sessionEvent) => state.ObserveSessionEvent(agent, sessionEvent),
cancellationToken);
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
@@ -66,6 +75,13 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
return _approvalCoordinator.ResolveApprovalAsync(command, cancellationToken);
}
public Task ResolveUserInputAsync(
ResolveUserInputCommandDto command,
CancellationToken cancellationToken)
{
return _userInputCoordinator.ResolveUserInputAsync(command, cancellationToken);
}
private static async Task<bool> HandleWorkflowEventAsync(
RunTurnCommandDto command,
WorkflowEvent evt,
@@ -9,9 +9,14 @@ public interface ITurnWorkflowRunner
Func<TurnDeltaEventDto, Task> onDelta,
Func<AgentActivityEventDto, Task> onActivity,
Func<ApprovalRequestedEventDto, Task> onApproval,
Func<UserInputRequestedEventDto, Task> onUserInput,
CancellationToken cancellationToken);
Task ResolveApprovalAsync(
ResolveApprovalCommandDto command,
CancellationToken cancellationToken);
Task ResolveUserInputAsync(
ResolveUserInputCommandDto command,
CancellationToken cancellationToken);
}
@@ -14,6 +14,8 @@ public sealed class SidecarProtocolHost
private const string RunTurnCommandType = "run-turn";
private const string CancelTurnCommandType = "cancel-turn";
private const string ResolveApprovalCommandType = "resolve-approval";
private const string ResolveUserInputCommandType = "resolve-user-input";
private const string AskUserToolName = "ask_user";
private static readonly string[] AuthenticationErrorIndicators =
[
@@ -62,6 +64,7 @@ public sealed class SidecarProtocolHost
[RunTurnCommandType] = HandleRunTurnAsync,
[CancelTurnCommandType] = HandleCancelTurnAsync,
[ResolveApprovalCommandType] = HandleResolveApprovalAsync,
[ResolveUserInputCommandType] = HandleResolveUserInputAsync,
};
}
@@ -178,6 +181,7 @@ public sealed class SidecarProtocolHost
delta => WriteAsync(context.Output, delta, turnCancellation.Token),
activity => WriteAsync(context.Output, activity, turnCancellation.Token),
approval => WriteAsync(context.Output, approval, turnCancellation.Token),
userInput => WriteAsync(context.Output, userInput, turnCancellation.Token),
turnCancellation.Token)
.ConfigureAwait(false);
@@ -231,6 +235,12 @@ public sealed class SidecarProtocolHost
await _workflowRunner.ResolveApprovalAsync(command, context.CancellationToken).ConfigureAwait(false);
}
private async Task HandleResolveUserInputAsync(CommandContext context)
{
ResolveUserInputCommandDto command = DeserializeCommand<ResolveUserInputCommandDto>(context);
await _workflowRunner.ResolveUserInputAsync(command, context.CancellationToken).ConfigureAwait(false);
}
private TCommand DeserializeCommand<TCommand>(CommandContext context)
where TCommand : SidecarCommandEnvelope
{
@@ -427,7 +437,13 @@ public sealed class SidecarProtocolHost
CancellationToken cancellationToken)
{
ToolsListResult result = await client.Rpc.Tools.ListAsync(null!, cancellationToken).ConfigureAwait(false);
return result.Tools
return MapRuntimeTools(result.Tools);
}
internal static IReadOnlyList<SidecarRuntimeToolDto> MapRuntimeTools(IEnumerable<Tool> tools)
{
return tools
.Where(ShouldIncludeRuntimeTool)
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
.Select(tool => new SidecarRuntimeToolDto
{
@@ -440,6 +456,13 @@ public sealed class SidecarProtocolHost
.ToList();
}
private static bool ShouldIncludeRuntimeTool(Tool tool)
{
string? toolName = string.IsNullOrWhiteSpace(tool.Name) ? null : tool.Name.Trim();
return toolName is not null
&& !string.Equals(toolName, AskUserToolName, StringComparison.OrdinalIgnoreCase);
}
private static bool IsReasoningEffort(string? value)
{
return value is "low" or "medium" or "high" or "xhigh";
@@ -0,0 +1,109 @@
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
using GitHub.Copilot.SDK;
namespace Aryx.AgentHost.Tests;
public sealed class CopilotUserInputCoordinatorTests
{
[Fact]
public async Task RequestUserInputAsync_RaisesUserInputEventAndCompletesAfterResolution()
{
CopilotUserInputCoordinator coordinator = new();
UserInputRequestedEventDto? observedEvent = null;
RunTurnCommandDto command = CreateUserInputCommand();
Task<UserInputResponse> pending = coordinator.RequestUserInputAsync(
command,
command.Pattern.Agents[0],
new UserInputRequest
{
Question = "How should I proceed?",
Choices = ["Continue", "Stop"],
AllowFreeform = true,
},
new UserInputInvocation
{
SessionId = "copilot-session-1",
},
userInputEvent =>
{
observedEvent = userInputEvent;
return Task.CompletedTask;
},
CancellationToken.None);
Assert.False(pending.IsCompleted);
Assert.NotNull(observedEvent);
Assert.Equal("user-input-requested", observedEvent!.Type);
Assert.Equal("turn-1", observedEvent.RequestId);
Assert.Equal("session-1", observedEvent.SessionId);
Assert.Equal("agent-1", observedEvent.AgentId);
Assert.Equal("Primary", observedEvent.AgentName);
Assert.Equal("How should I proceed?", observedEvent.Question);
Assert.Equal(["Continue", "Stop"], observedEvent.Choices);
Assert.True(observedEvent.AllowFreeform);
await coordinator.ResolveUserInputAsync(
new ResolveUserInputCommandDto
{
UserInputId = observedEvent.UserInputId,
Answer = "Continue",
WasFreeform = false,
},
CancellationToken.None);
UserInputResponse response = await pending;
Assert.Equal("Continue", response.Answer);
Assert.False(response.WasFreeform);
}
[Fact]
public async Task ResolveUserInputAsync_RejectsUnknownUserInputIds()
{
CopilotUserInputCoordinator coordinator = new();
InvalidOperationException error = await Assert.ThrowsAsync<InvalidOperationException>(() =>
coordinator.ResolveUserInputAsync(
new ResolveUserInputCommandDto
{
UserInputId = "user-input-missing",
Answer = "Continue",
WasFreeform = false,
},
CancellationToken.None));
Assert.Contains("is not pending", error.Message);
}
private static PatternAgentDefinitionDto CreateAgent(string id, string name)
{
return new PatternAgentDefinitionDto
{
Id = id,
Name = name,
Model = "gpt-5.4",
Instructions = "Help with the request.",
};
}
private static RunTurnCommandDto CreateUserInputCommand()
{
return new RunTurnCommandDto
{
RequestId = "turn-1",
SessionId = "session-1",
Pattern = new PatternDefinitionDto
{
Id = "pattern-1",
Name = "User Input Pattern",
Mode = "single",
Availability = "available",
Agents =
[
CreateAgent("agent-1", "Primary"),
],
},
};
}
}
@@ -1,6 +1,7 @@
using System.Text.Json;
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
using GitHub.Copilot.SDK.Rpc;
namespace Aryx.AgentHost.Tests;
@@ -112,7 +113,7 @@ public sealed class SidecarProtocolHostTests
{
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, cancellationToken) =>
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) =>
{
await onActivity(new AgentActivityEventDto
{
@@ -236,7 +237,7 @@ public sealed class SidecarProtocolHostTests
{
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, cancellationToken) =>
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) =>
{
await onApproval(new ApprovalRequestedEventDto
{
@@ -309,12 +310,85 @@ public sealed class SidecarProtocolHostTests
});
}
[Fact]
public async Task RunTurnCommand_ReturnsUserInputEvents()
{
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) =>
{
await onUserInput(new UserInputRequestedEventDto
{
Type = "user-input-requested",
RequestId = command.RequestId,
SessionId = command.SessionId,
UserInputId = "user-input-1",
AgentId = "agent-1",
AgentName = "Primary",
Question = "What should I do next?",
Choices = ["Continue", "Stop"],
AllowFreeform = true,
});
return [];
}));
IReadOnlyList<JsonElement> events = await RunHostAsync(
new RunTurnCommandDto
{
Type = "run-turn",
RequestId = "turn-user-input",
SessionId = "session-1",
ProjectPath = "C:\\workspace\\project",
Pattern = new PatternDefinitionDto
{
Id = "pattern-1",
Name = "Single Agent",
Mode = "single",
Availability = "available",
Agents =
[
CreateAgent(name: "Primary"),
],
},
},
host);
Assert.Collection(
events,
userInputEvent =>
{
Assert.Equal("user-input-requested", userInputEvent.GetProperty("type").GetString());
Assert.Equal("turn-user-input", userInputEvent.GetProperty("requestId").GetString());
Assert.Equal("session-1", userInputEvent.GetProperty("sessionId").GetString());
Assert.Equal("user-input-1", userInputEvent.GetProperty("userInputId").GetString());
Assert.Equal("Primary", userInputEvent.GetProperty("agentName").GetString());
Assert.Equal("What should I do next?", userInputEvent.GetProperty("question").GetString());
string[] choices = userInputEvent.GetProperty("choices")
.EnumerateArray()
.Select(choice => choice.GetString() ?? string.Empty)
.ToArray();
Assert.Equal(["Continue", "Stop"], choices);
Assert.True(userInputEvent.GetProperty("allowFreeform").GetBoolean());
},
completionEvent =>
{
Assert.Equal("turn-complete", completionEvent.GetProperty("type").GetString());
Assert.False(completionEvent.GetProperty("cancelled").GetBoolean());
},
commandCompleteEvent =>
{
Assert.Equal("command-complete", commandCompleteEvent.GetProperty("type").GetString());
Assert.Equal("turn-user-input", commandCompleteEvent.GetProperty("requestId").GetString());
});
}
[Fact]
public async Task CancelTurnCommand_CancelsInProgressTurnAndCompletesBothCommands()
{
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, cancellationToken) =>
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) =>
{
await Task.Delay(Timeout.Infinite, cancellationToken);
return [];
@@ -362,7 +436,7 @@ public sealed class SidecarProtocolHostTests
{
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, cancellationToken) => []));
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) => []));
await RunHostAsync(CreateRunTurnCommand(requestId: "turn-completed"), host);
@@ -385,7 +459,7 @@ public sealed class SidecarProtocolHostTests
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(
handler: async (command, onDelta, onActivity, onApproval, cancellationToken) => [],
handler: async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) => [],
resolveApprovalHandler: (command, cancellationToken) =>
{
captured = command;
@@ -409,6 +483,67 @@ public sealed class SidecarProtocolHostTests
Assert.Equal("approved", captured?.Decision);
}
[Fact]
public async Task ResolveUserInputCommand_DelegatesToWorkflowRunnerAndCompletes()
{
ResolveUserInputCommandDto? captured = null;
SidecarProtocolHost host = new(
new PatternValidator(),
new FakeWorkflowRunner(
handler: async (command, onDelta, onActivity, onApproval, onUserInput, cancellationToken) => [],
resolveUserInputHandler: (command, cancellationToken) =>
{
captured = command;
return Task.CompletedTask;
}));
IReadOnlyList<JsonElement> events = await RunHostAsync(
new ResolveUserInputCommandDto
{
Type = "resolve-user-input",
RequestId = "user-input-command-1",
UserInputId = "user-input-1",
Answer = "Continue",
WasFreeform = false,
},
host);
JsonElement completionEvent = Assert.Single(events);
Assert.Equal("command-complete", completionEvent.GetProperty("type").GetString());
Assert.Equal("user-input-command-1", completionEvent.GetProperty("requestId").GetString());
Assert.Equal("user-input-1", captured?.UserInputId);
Assert.Equal("Continue", captured?.Answer);
Assert.False(captured?.WasFreeform);
}
[Fact]
public void MapRuntimeTools_ExcludesAskUserAndDeduplicatesByName()
{
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = SidecarProtocolHost.MapRuntimeTools(
[
new Tool
{
Name = "ask_user",
Description = "Ask the user a question.",
},
new Tool
{
Name = " web_fetch ",
Description = " Fetch content from the web. ",
},
new Tool
{
Name = "WEB_FETCH",
Description = "Duplicate entry",
},
]);
SidecarRuntimeToolDto runtimeTool = Assert.Single(runtimeTools);
Assert.Equal("web_fetch", runtimeTool.Id);
Assert.Equal("web_fetch", runtimeTool.Label);
Assert.Equal("Fetch content from the web.", runtimeTool.Description);
}
[Fact]
public void ClassifyConnectionStatus_ReturnsAuthRequiredForLoginFailures()
{
@@ -619,9 +754,11 @@ public sealed class SidecarProtocolHostTests
Func<TurnDeltaEventDto, Task>,
Func<AgentActivityEventDto, Task>,
Func<ApprovalRequestedEventDto, Task>,
Func<UserInputRequestedEventDto, Task>,
CancellationToken,
Task<IReadOnlyList<ChatMessageDto>>> _handler;
private readonly Func<ResolveApprovalCommandDto, CancellationToken, Task> _resolveApprovalHandler;
private readonly Func<ResolveUserInputCommandDto, CancellationToken, Task> _resolveUserInputHandler;
public FakeWorkflowRunner(
Func<
@@ -629,12 +766,15 @@ public sealed class SidecarProtocolHostTests
Func<TurnDeltaEventDto, Task>,
Func<AgentActivityEventDto, Task>,
Func<ApprovalRequestedEventDto, Task>,
Func<UserInputRequestedEventDto, Task>,
CancellationToken,
Task<IReadOnlyList<ChatMessageDto>>> handler,
Func<ResolveApprovalCommandDto, CancellationToken, Task>? resolveApprovalHandler = null)
Func<ResolveApprovalCommandDto, CancellationToken, Task>? resolveApprovalHandler = null,
Func<ResolveUserInputCommandDto, CancellationToken, Task>? resolveUserInputHandler = null)
{
_handler = handler;
_resolveApprovalHandler = resolveApprovalHandler ?? ((_, _) => Task.CompletedTask);
_resolveUserInputHandler = resolveUserInputHandler ?? ((_, _) => Task.CompletedTask);
}
public Task<IReadOnlyList<ChatMessageDto>> RunTurnAsync(
@@ -642,9 +782,10 @@ public sealed class SidecarProtocolHostTests
Func<TurnDeltaEventDto, Task> onDelta,
Func<AgentActivityEventDto, Task> onActivity,
Func<ApprovalRequestedEventDto, Task> onApproval,
Func<UserInputRequestedEventDto, Task> onUserInput,
CancellationToken cancellationToken)
{
return _handler(command, onDelta, onActivity, onApproval, cancellationToken);
return _handler(command, onDelta, onActivity, onApproval, onUserInput, cancellationToken);
}
public Task ResolveApprovalAsync(
@@ -653,5 +794,12 @@ public sealed class SidecarProtocolHostTests
{
return _resolveApprovalHandler(command, cancellationToken);
}
public Task ResolveUserInputAsync(
ResolveUserInputCommandDto command,
CancellationToken cancellationToken)
{
return _resolveUserInputHandler(command, cancellationToken);
}
}
}