feat(workflows): add phase 4 backend executor support

Add MVP code-executor and function-executor runtime support, request-port
bridging through Aryx user input, state-scope helpers, lockstep execution
mode handling, and validation/tests for new workflow node kinds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-05 20:22:20 +02:00
co-authored by Copilot
parent 7a32c9c0a3
commit 69ac454f29
11 changed files with 1943 additions and 76 deletions
@@ -1,11 +1,13 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
using GitHub.Copilot.SDK;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.InProc;
using Microsoft.Extensions.AI;
namespace Aryx.AgentHost.Tests;
@@ -799,6 +801,73 @@ public sealed class CopilotWorkflowRunnerTests
});
}
[Fact]
public void CreateExecutionEnvironment_UsesLockstepWhenRequested()
{
RunTurnCommandDto command = new()
{
Workflow = new WorkflowDefinitionDto
{
Settings = new WorkflowSettingsDto
{
ExecutionMode = "lockstep",
Checkpointing = new WorkflowCheckpointSettingsDto(),
},
},
};
InProcessExecutionEnvironment environment = CopilotWorkflowRunner.CreateExecutionEnvironment(command, checkpointManager: null);
Assert.Same(InProcessExecution.Lockstep, environment);
}
[Fact]
public void CoerceRequestPortResponse_ParsesSupportedResponseTypes()
{
Assert.Equal("hello", CopilotWorkflowRunner.CoerceRequestPortResponse("string", "hello"));
Assert.Equal(true, CopilotWorkflowRunner.CoerceRequestPortResponse("bool", "yes"));
Assert.Equal(12.5d, CopilotWorkflowRunner.CoerceRequestPortResponse("number", "12.5"));
JsonElement json = Assert.IsType<JsonElement>(CopilotWorkflowRunner.CoerceRequestPortResponse("json", "{\"ok\":true}"));
Assert.True(json.GetProperty("ok").GetBoolean());
}
[Fact]
public async Task RunTurnAsync_RequestPortWorkflowUsesUserInputBridge()
{
CopilotWorkflowRunner runner = new(new PatternValidator());
List<UserInputRequestedEventDto> requests = [];
IReadOnlyList<ChatMessageDto> messages = await runner.RunTurnAsync(
CreateRequestPortCommand(),
_ => Task.CompletedTask,
_ => Task.CompletedTask,
_ => Task.CompletedTask,
async request =>
{
requests.Add(request);
await runner.ResolveUserInputAsync(
new ResolveUserInputCommandDto
{
UserInputId = request.UserInputId,
Answer = "approved",
WasFreeform = true,
},
CancellationToken.None);
},
_ => Task.CompletedTask,
_ => Task.CompletedTask,
CancellationToken.None);
UserInputRequestedEventDto requestEvent = Assert.Single(requests);
Assert.Equal("Needs approval?", requestEvent.Question);
Assert.Null(requestEvent.AgentId);
ChatMessageDto message = Assert.Single(messages);
Assert.Equal("Workflow", message.AuthorName);
Assert.Equal("approved", message.Content);
}
[Fact]
public async Task HandleWorkflowEventAsync_FallsBackToActiveAgentForUnresolvedStreamingUpdates()
{
@@ -2212,6 +2281,96 @@ public sealed class CopilotWorkflowRunnerTests
};
}
private static RunTurnCommandDto CreateRequestPortCommand()
{
return new RunTurnCommandDto
{
RequestId = "turn-request-port",
SessionId = "session-request-port",
ProjectPath = "c:\\workspace\\personal\\projects\\aryx.worktrees\\copilot-powerful-vulture",
Pattern = new PatternDefinitionDto
{
Id = "pattern-request-port",
Name = "Request Port Pattern",
Mode = "single",
Availability = "available",
Agents = [],
},
Messages =
[
new ChatMessageDto
{
Id = "message-1",
Role = "user",
AuthorName = "User",
Content = "Please continue.",
CreatedAt = "2026-04-05T00:00:00.000Z",
},
],
Workflow = new WorkflowDefinitionDto
{
Id = "workflow-request-port",
Name = "Request Port Workflow",
Graph = new WorkflowGraphDto
{
Nodes =
[
new WorkflowNodeDto
{
Id = "start",
Kind = "start",
Label = "Start",
Config = new WorkflowNodeConfigDto { Kind = "start" },
},
new WorkflowNodeDto
{
Id = "approval-port",
Kind = "request-port",
Label = "Approval",
Config = new WorkflowNodeConfigDto
{
Kind = "request-port",
PortId = "approval",
RequestType = "Question",
ResponseType = "string",
Prompt = "Needs approval?",
},
},
new WorkflowNodeDto
{
Id = "end",
Kind = "end",
Label = "End",
Config = new WorkflowNodeConfigDto { Kind = "end" },
},
],
Edges =
[
new WorkflowEdgeDto
{
Id = "edge-start-port",
Source = "start",
Target = "approval-port",
Kind = "direct",
},
new WorkflowEdgeDto
{
Id = "edge-port-end",
Source = "approval-port",
Target = "end",
Kind = "direct",
},
],
},
Settings = new WorkflowSettingsDto
{
Checkpointing = new WorkflowCheckpointSettingsDto(),
ExecutionMode = "lockstep",
},
},
};
}
private static RunTurnMcpServerConfigDto CreateMcpServerConfig(string serverName)
=> new()
{