using System.Collections.Concurrent; using System.Runtime.CompilerServices; using Aryx.AgentHost.Contracts; using Aryx.AgentHost.Services; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; namespace Aryx.AgentHost.Tests; public sealed class WorkflowRequestInfoInterpreterTests { [Fact] public void TryCreateActivityFromRequest_ReturnsToolCallingActivityForFunctionCalls() { ConcurrentDictionary toolNamesByCallId = new(StringComparer.Ordinal); RequestInfoEvent requestInfo = CreateRequestInfoEvent( new FunctionCallContent("call-1", "view", new Dictionary())); AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( CreateSingleAgentCommand(), requestInfo, new AgentIdentity("agent-1", "Primary"), toolNamesByCallId); Assert.NotNull(activity); Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("agent-1", activity.AgentId); Assert.Equal("Primary", activity.AgentName); Assert.Equal("view", activity.ToolName); Assert.Equal("view", toolNamesByCallId["call-1"]); } [Fact] public void TryCreateActivityFromRequest_MapsMcpToolCalls() { ConcurrentDictionary toolNamesByCallId = new(StringComparer.Ordinal); RequestInfoEvent requestInfo = CreateRequestInfoEvent( CreateMcpToolCall("call-1", "git.status", "Git MCP")); AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( CreateSingleAgentCommand(), requestInfo, new AgentIdentity("agent-1", "Primary"), toolNamesByCallId); Assert.NotNull(activity); Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("git.status", activity.ToolName); Assert.Equal("git.status", toolNamesByCallId["call-1"]); } [Fact] public void TryCreateActivityFromRequest_MapsCodeInterpreterCallsToSyntheticToolName() { ConcurrentDictionary toolNamesByCallId = new(StringComparer.Ordinal); RequestInfoEvent requestInfo = CreateRequestInfoEvent(CreateCodeInterpreterToolCall("call-1")); AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( CreateSingleAgentCommand(), requestInfo, new AgentIdentity("agent-1", "Primary"), toolNamesByCallId); Assert.NotNull(activity); Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("code interpreter", activity.ToolName); Assert.Equal("code interpreter", toolNamesByCallId["call-1"]); } [Fact] public void TryCreateActivityFromRequest_MapsImageGenerationCallsWithoutTrackingCallId() { ConcurrentDictionary toolNamesByCallId = new(StringComparer.Ordinal); RequestInfoEvent requestInfo = CreateRequestInfoEvent(CreateImageGenerationToolCall()); AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( CreateSingleAgentCommand(), requestInfo, new AgentIdentity("agent-1", "Primary"), toolNamesByCallId); Assert.NotNull(activity); Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("image generation", activity.ToolName); Assert.Empty(toolNamesByCallId); } [Fact] public void TryCreateActivityFromRequest_ReturnsHandoffActivityForKnownTargets() { ConcurrentDictionary toolNamesByCallId = new(StringComparer.Ordinal); RequestInfoEvent requestInfo = CreateRequestInfoEvent( CreateHandoffTarget("agent-handoff-ux", "UX Specialist")); AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( CreateHandoffCommand(), requestInfo, new AgentIdentity("agent-handoff-triage", "Triage"), toolNamesByCallId); Assert.NotNull(activity); Assert.Equal("handoff", activity.ActivityType); Assert.Equal("agent-handoff-ux", activity.AgentId); Assert.Equal("UX Specialist", activity.AgentName); Assert.Equal("agent-handoff-triage", activity.SourceAgentId); Assert.Equal("Triage", activity.SourceAgentName); Assert.Null(activity.ToolName); Assert.Empty(toolNamesByCallId); } [Fact] public void RequiresUserInputTurnBoundary_ReturnsTrueForUnhandledHandoffRequests() { RequestInfoEvent requestInfo = CreateRequestInfoEvent(new { Prompt = "Please provide more detail.", }); bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary( CreateHandoffCommand(), requestInfo); Assert.True(requiresBoundary); } [Fact] public void RequiresUserInputTurnBoundary_ReturnsFalseForExplicitHandoffs() { RequestInfoEvent requestInfo = CreateRequestInfoEvent( CreateHandoffTarget("agent-handoff-ux", "UX Specialist")); bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary( CreateHandoffCommand(), requestInfo); Assert.False(requiresBoundary); } [Fact] public void RequiresUserInputTurnBoundary_ReturnsFalseForToolRequests() { RequestInfoEvent requestInfo = CreateRequestInfoEvent( new FunctionCallContent("call-1", "view", new Dictionary())); bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary( CreateHandoffCommand(), requestInfo); Assert.False(requiresBoundary); } [Fact] public void RequiresUserInputTurnBoundary_ReturnsFalseOutsideHandoffMode() { RequestInfoEvent requestInfo = CreateRequestInfoEvent(new { Prompt = "Please provide more detail.", }); bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary( CreateSingleAgentCommand(), requestInfo); Assert.False(requiresBoundary); } private static RunTurnCommandDto CreateSingleAgentCommand() { return new RunTurnCommandDto { RequestId = "turn-1", SessionId = "session-1", Pattern = new PatternDefinitionDto { Id = "pattern-single", Name = "Single Agent", Mode = "single", Availability = "available", Agents = [ CreateAgent("agent-1", "Primary"), ], }, }; } private static RunTurnCommandDto CreateHandoffCommand() { return new RunTurnCommandDto { RequestId = "turn-1", SessionId = "session-1", Pattern = new PatternDefinitionDto { Id = "pattern-handoff", Name = "Handoff Flow", Mode = "handoff", Availability = "available", Agents = [ CreateAgent("agent-handoff-triage", "Triage"), CreateAgent("agent-handoff-ux", "UX Specialist"), ], }, }; } 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 RequestInfoEvent CreateRequestInfoEvent(object payload) { RequestPort port = RequestPort.Create("test-port"); ExternalRequest request = ExternalRequest.Create(port, payload, "request-1"); return new RequestInfoEvent(request); } private static object CreateCodeInterpreterToolCall(string callId) { Type type = Type.GetType( "Microsoft.Extensions.AI.CodeInterpreterToolCallContent, Microsoft.Extensions.AI.Abstractions", throwOnError: true)!; object instance = Activator.CreateInstance(type)!; type.GetProperty("CallId")!.SetValue(instance, callId); return instance; } private static object CreateMcpToolCall(string callId, string toolName, string serverName) { Type type = Type.GetType( "Microsoft.Extensions.AI.McpServerToolCallContent, Microsoft.Extensions.AI.Abstractions", throwOnError: true)!; return Activator.CreateInstance(type, callId, toolName, serverName)!; } private static object CreateImageGenerationToolCall() { Type type = Type.GetType( "Microsoft.Extensions.AI.ImageGenerationToolCallContent, Microsoft.Extensions.AI.Abstractions", throwOnError: true)!; return Activator.CreateInstance(type)!; } private static object CreateHandoffTarget(string id, string name) { Type type = Type.GetType( "Microsoft.Agents.AI.Workflows.Specialized.HandoffTarget, Microsoft.Agents.AI.Workflows", throwOnError: true)!; return Activator.CreateInstance(type, CreateChatClientAgent(id, name), "Handle the UX work.")!; } private static ChatClientAgent CreateChatClientAgent(string id, string name) { return new ChatClientAgent( new StubChatClient(), id, name, "Stub agent for handoff tests.", [], null!, null!); } private sealed class StubChatClient : IChatClient { public Task GetResponseAsync( IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken) { throw new NotSupportedException(); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) { yield break; } public object? GetService(Type serviceType, object? serviceKey) { return null; } public void Dispose() { } } }