mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 10:28:43 +02:00
feat: scaffold electron orchestrator foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kopaya.AgentHost.Contracts;
|
||||
|
||||
public sealed class PatternAgentDefinitionDto
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Instructions { get; init; } = string.Empty;
|
||||
public string Model { get; init; } = string.Empty;
|
||||
public string? ReasoningEffort { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PatternDefinitionDto
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Mode { get; init; } = string.Empty;
|
||||
public string Availability { get; init; } = "available";
|
||||
public string? UnavailabilityReason { get; init; }
|
||||
public int MaxIterations { get; init; }
|
||||
public IReadOnlyList<PatternAgentDefinitionDto> Agents { get; init; } = [];
|
||||
public string CreatedAt { get; init; } = string.Empty;
|
||||
public string UpdatedAt { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ChatMessageDto
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Role { get; init; } = string.Empty;
|
||||
public string AuthorName { get; init; } = string.Empty;
|
||||
public string Content { get; init; } = string.Empty;
|
||||
public string CreatedAt { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class PatternValidationIssueDto
|
||||
{
|
||||
public string Level { get; init; } = "error";
|
||||
public string? Field { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SidecarModeCapabilityDto
|
||||
{
|
||||
public bool Available { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
public sealed class SidecarCapabilitiesDto
|
||||
{
|
||||
public string Runtime { get; init; } = "dotnet-maf";
|
||||
public Dictionary<string, SidecarModeCapabilityDto> Modes { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public class SidecarCommandEnvelope
|
||||
{
|
||||
public string Type { get; init; } = string.Empty;
|
||||
public string RequestId { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class DescribeCapabilitiesCommandDto : SidecarCommandEnvelope;
|
||||
|
||||
public sealed class ValidatePatternCommandDto : SidecarCommandEnvelope
|
||||
{
|
||||
public PatternDefinitionDto Pattern { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class RunTurnCommandDto : SidecarCommandEnvelope
|
||||
{
|
||||
public string SessionId { get; init; } = string.Empty;
|
||||
public string ProjectPath { get; init; } = string.Empty;
|
||||
public PatternDefinitionDto Pattern { get; init; } = new();
|
||||
public IReadOnlyList<ChatMessageDto> Messages { get; init; } = [];
|
||||
}
|
||||
|
||||
public abstract class SidecarEventDto
|
||||
{
|
||||
public string Type { get; init; } = string.Empty;
|
||||
public string RequestId { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CapabilitiesEventDto : SidecarEventDto
|
||||
{
|
||||
public SidecarCapabilitiesDto Capabilities { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class PatternValidationEventDto : SidecarEventDto
|
||||
{
|
||||
public IReadOnlyList<PatternValidationIssueDto> Issues { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class TurnDeltaEventDto : SidecarEventDto
|
||||
{
|
||||
public string SessionId { get; init; } = string.Empty;
|
||||
public string MessageId { get; init; } = string.Empty;
|
||||
public string AuthorName { get; init; } = string.Empty;
|
||||
public string ContentDelta { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class TurnCompleteEventDto : SidecarEventDto
|
||||
{
|
||||
public string SessionId { get; init; } = string.Empty;
|
||||
public IReadOnlyList<ChatMessageDto> Messages { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class CommandErrorEventDto : SidecarEventDto
|
||||
{
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CommandCompleteEventDto : SidecarEventDto;
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc4" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.0.0-preview.260311.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
using Kopaya.AgentHost.Services;
|
||||
|
||||
if (!args.Contains("--stdio", StringComparer.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine("Kopaya.AgentHost expects the --stdio flag.");
|
||||
return;
|
||||
}
|
||||
|
||||
SidecarProtocolHost host = new();
|
||||
await host.RunAsync(Console.In, Console.Out, CancellationToken.None);
|
||||
@@ -0,0 +1,269 @@
|
||||
using System.Text;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Kopaya.AgentHost.Contracts;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.GitHub.Copilot;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Kopaya.AgentHost.Services;
|
||||
|
||||
public sealed class CopilotWorkflowRunner
|
||||
{
|
||||
private readonly PatternValidator _patternValidator;
|
||||
|
||||
public CopilotWorkflowRunner(PatternValidator patternValidator)
|
||||
{
|
||||
_patternValidator = patternValidator;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ChatMessageDto>> RunTurnAsync(
|
||||
RunTurnCommandDto command,
|
||||
Func<TurnDeltaEventDto, Task> onDelta,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PatternValidationIssueDto? validationError = _patternValidator.Validate(command.Pattern).FirstOrDefault();
|
||||
if (validationError is not null)
|
||||
{
|
||||
throw new InvalidOperationException(validationError.Message);
|
||||
}
|
||||
|
||||
await using AgentBundle bundle = await AgentBundle.CreateAsync(command.Pattern, command.ProjectPath, cancellationToken);
|
||||
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
|
||||
List<ChatMessage> inputMessages = command.Messages.Select(ToChatMessage).ToList();
|
||||
|
||||
List<StreamingSegment> segments = [];
|
||||
int fallbackMessageIndex = 0;
|
||||
List<ChatMessageDto> completedMessages = [];
|
||||
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, inputMessages).ConfigureAwait(false);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true)).ConfigureAwait(false);
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (evt is AgentResponseUpdateEvent update && !string.IsNullOrEmpty(update.Update.Text))
|
||||
{
|
||||
string messageId = update.Update.MessageId ?? $"{command.RequestId}-delta-{fallbackMessageIndex++}";
|
||||
StreamingSegment segment = GetOrCreateSegment(segments, messageId, update.ExecutorId);
|
||||
segment.Content.Append(update.Update.Text);
|
||||
|
||||
await onDelta(new TurnDeltaEventDto
|
||||
{
|
||||
Type = "turn-delta",
|
||||
RequestId = command.RequestId,
|
||||
SessionId = command.SessionId,
|
||||
MessageId = messageId,
|
||||
AuthorName = update.ExecutorId,
|
||||
ContentDelta = update.Update.Text,
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else if (evt is WorkflowOutputEvent outputEvent)
|
||||
{
|
||||
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
||||
List<ChatMessage> newMessages = allMessages.Skip(inputMessages.Count).ToList();
|
||||
completedMessages = ConvertOutputMessages(command, newMessages, segments);
|
||||
}
|
||||
}
|
||||
|
||||
return completedMessages;
|
||||
}
|
||||
|
||||
private static StreamingSegment GetOrCreateSegment(List<StreamingSegment> segments, string messageId, string authorName)
|
||||
{
|
||||
StreamingSegment? existing = segments.LastOrDefault(segment => segment.MessageId == messageId);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
StreamingSegment created = new(messageId, authorName);
|
||||
segments.Add(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private static List<ChatMessageDto> ConvertOutputMessages(
|
||||
RunTurnCommandDto command,
|
||||
IReadOnlyList<ChatMessage> newMessages,
|
||||
IReadOnlyList<StreamingSegment> segments)
|
||||
{
|
||||
List<ChatMessageDto> mapped = [];
|
||||
int segmentIndex = 0;
|
||||
|
||||
foreach (ChatMessage message in newMessages.Where(message => message.Role != ChatRole.User))
|
||||
{
|
||||
StreamingSegment? segment = segmentIndex < segments.Count ? segments[segmentIndex] : null;
|
||||
segmentIndex++;
|
||||
|
||||
mapped.Add(new ChatMessageDto
|
||||
{
|
||||
Id = segment?.MessageId ?? $"{command.RequestId}-final-{segmentIndex}",
|
||||
Role = message.Role == ChatRole.System ? "system" : "assistant",
|
||||
AuthorName = message.AuthorName ?? segment?.AuthorName ?? "assistant",
|
||||
Content = message.Text ?? segment?.Content.ToString() ?? string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
});
|
||||
}
|
||||
|
||||
if (mapped.Count == 0 && segments.Count > 0)
|
||||
{
|
||||
mapped.AddRange(segments.Select(segment => new ChatMessageDto
|
||||
{
|
||||
Id = segment.MessageId,
|
||||
Role = "assistant",
|
||||
AuthorName = segment.AuthorName,
|
||||
Content = segment.Content.ToString(),
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
}));
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static ChatMessage ToChatMessage(ChatMessageDto message)
|
||||
{
|
||||
ChatMessage mapped = new(message.Role switch
|
||||
{
|
||||
"user" => ChatRole.User,
|
||||
"system" => ChatRole.System,
|
||||
_ => ChatRole.Assistant,
|
||||
}, message.Content);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(message.AuthorName))
|
||||
{
|
||||
mapped.AuthorName = message.AuthorName;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private sealed class StreamingSegment
|
||||
{
|
||||
public StreamingSegment(string messageId, string authorName)
|
||||
{
|
||||
MessageId = messageId;
|
||||
AuthorName = authorName;
|
||||
}
|
||||
|
||||
public string MessageId { get; }
|
||||
|
||||
public string AuthorName { get; }
|
||||
|
||||
public StringBuilder Content { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class AgentBundle : IAsyncDisposable
|
||||
{
|
||||
private readonly List<IAsyncDisposable> _disposables = [];
|
||||
|
||||
private AgentBundle(IReadOnlyList<AIAgent> agents)
|
||||
{
|
||||
Agents = agents;
|
||||
}
|
||||
|
||||
public IReadOnlyList<AIAgent> Agents { get; }
|
||||
|
||||
public static async Task<AgentBundle> CreateAsync(
|
||||
PatternDefinitionDto pattern,
|
||||
string projectPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<IAsyncDisposable> disposables = [];
|
||||
List<AIAgent> agents = [];
|
||||
|
||||
foreach (PatternAgentDefinitionDto definition in pattern.Agents)
|
||||
{
|
||||
CopilotClient client = new();
|
||||
await client.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
Model = definition.Model,
|
||||
ReasoningEffort = definition.ReasoningEffort,
|
||||
SystemMessage = new SystemMessageConfig
|
||||
{
|
||||
Content = definition.Instructions,
|
||||
},
|
||||
WorkingDirectory = projectPath,
|
||||
OnPermissionRequest = ApprovePermissionAsync,
|
||||
Streaming = true,
|
||||
};
|
||||
|
||||
GitHubCopilotAgent agent = new(
|
||||
client,
|
||||
sessionConfig,
|
||||
ownsClient: true,
|
||||
id: definition.Id,
|
||||
name: definition.Name,
|
||||
description: definition.Description);
|
||||
|
||||
agents.Add(agent);
|
||||
disposables.Add(agent);
|
||||
}
|
||||
|
||||
AgentBundle bundle = new(agents);
|
||||
bundle._disposables.AddRange(disposables);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public Workflow BuildWorkflow(PatternDefinitionDto pattern)
|
||||
{
|
||||
return pattern.Mode switch
|
||||
{
|
||||
"single" => AgentWorkflowBuilder.BuildSequential(pattern.Name, Agents),
|
||||
"sequential" => AgentWorkflowBuilder.BuildSequential(pattern.Name, Agents),
|
||||
"concurrent" => AgentWorkflowBuilder.BuildConcurrent(pattern.Name, Agents),
|
||||
"handoff" => BuildHandoffWorkflow(pattern),
|
||||
"group-chat" => BuildGroupChatWorkflow(pattern),
|
||||
"magentic" => throw new NotSupportedException(
|
||||
pattern.UnavailabilityReason
|
||||
?? "Magentic orchestration is not yet supported in the .NET Agent Framework."),
|
||||
_ => throw new NotSupportedException($"Unsupported orchestration mode '{pattern.Mode}'."),
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (IAsyncDisposable disposable in _disposables)
|
||||
{
|
||||
await disposable.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Workflow BuildHandoffWorkflow(PatternDefinitionDto pattern)
|
||||
{
|
||||
AIAgent firstAgent = Agents[0];
|
||||
IReadOnlyList<AIAgent> specialists = Agents.Skip(1).ToList();
|
||||
|
||||
HandoffsWorkflowBuilder builder = AgentWorkflowBuilder.CreateHandoffBuilderWith(firstAgent)
|
||||
.WithHandoffs(firstAgent, specialists)
|
||||
.WithHandoffs(specialists, firstAgent);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private Workflow BuildGroupChatWorkflow(PatternDefinitionDto pattern)
|
||||
{
|
||||
int maximumIterations = pattern.MaxIterations <= 0 ? 5 : pattern.MaxIterations;
|
||||
|
||||
return AgentWorkflowBuilder
|
||||
.CreateGroupChatBuilderWith(agents =>
|
||||
new RoundRobinGroupChatManager(agents)
|
||||
{
|
||||
MaximumIterationCount = maximumIterations,
|
||||
})
|
||||
.AddParticipants(Agents.ToArray())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static Task<PermissionRequestResult> ApprovePermissionAsync(
|
||||
PermissionRequest request,
|
||||
PermissionInvocation invocation)
|
||||
{
|
||||
return Task.FromResult(new PermissionRequestResult
|
||||
{
|
||||
Kind = "approved",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Kopaya.AgentHost.Contracts;
|
||||
|
||||
namespace Kopaya.AgentHost.Services;
|
||||
|
||||
public sealed class PatternValidator
|
||||
{
|
||||
public IReadOnlyList<PatternValidationIssueDto> Validate(PatternDefinitionDto pattern)
|
||||
{
|
||||
List<PatternValidationIssueDto> issues = [];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pattern.Name))
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "name",
|
||||
Message = "Pattern name is required.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(pattern.Availability, "unavailable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "availability",
|
||||
Message = pattern.UnavailabilityReason ?? "This orchestration mode is currently unavailable.",
|
||||
});
|
||||
}
|
||||
|
||||
if (pattern.Agents.Count == 0)
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents",
|
||||
Message = "At least one agent is required.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(pattern.Mode, "single", StringComparison.OrdinalIgnoreCase) && pattern.Agents.Count != 1)
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents",
|
||||
Message = "Single-agent chat requires exactly one agent.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(pattern.Mode, "handoff", StringComparison.OrdinalIgnoreCase) && pattern.Agents.Count < 2)
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents",
|
||||
Message = "Handoff orchestration requires at least two agents.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(pattern.Mode, "group-chat", StringComparison.OrdinalIgnoreCase) && pattern.Agents.Count < 2)
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents",
|
||||
Message = "Group chat requires at least two agents.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(pattern.Mode, "magentic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "mode",
|
||||
Message = pattern.UnavailabilityReason
|
||||
?? "Magentic orchestration is currently documented as unsupported in the .NET Agent Framework.",
|
||||
});
|
||||
}
|
||||
|
||||
foreach (PatternAgentDefinitionDto agent in pattern.Agents)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(agent.Name))
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents.name",
|
||||
Message = "Every agent needs a name.",
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(agent.Model))
|
||||
{
|
||||
issues.Add(new PatternValidationIssueDto
|
||||
{
|
||||
Field = "agents.model",
|
||||
Message = $"Agent \"{agent.Name}\" requires a model identifier.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kopaya.AgentHost.Contracts;
|
||||
|
||||
namespace Kopaya.AgentHost.Services;
|
||||
|
||||
public sealed class SidecarProtocolHost
|
||||
{
|
||||
private readonly PatternValidator _patternValidator;
|
||||
private readonly CopilotWorkflowRunner _workflowRunner;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private readonly ConcurrentDictionary<string, Task> _inFlight = new(StringComparer.Ordinal);
|
||||
|
||||
public SidecarProtocolHost()
|
||||
{
|
||||
_patternValidator = new PatternValidator();
|
||||
_workflowRunner = new CopilotWorkflowRunner(_patternValidator);
|
||||
_jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task RunAsync(TextReader input, TextWriter output, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
string? line = await input.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (line is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SidecarCommandEnvelope envelope = DeserializeEnvelope(line);
|
||||
Task task = HandleCommandAsync(line, envelope, output, cancellationToken);
|
||||
_inFlight[envelope.RequestId] = task;
|
||||
_ = task.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
_inFlight.TryRemove(envelope.RequestId, out Task? removedTask);
|
||||
return removedTask is not null;
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.None,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
await Task.WhenAll(_inFlight.Values).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private SidecarCommandEnvelope DeserializeEnvelope(string line)
|
||||
{
|
||||
return JsonSerializer.Deserialize<SidecarCommandEnvelope>(line, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Could not deserialize sidecar command envelope.");
|
||||
}
|
||||
|
||||
private async Task HandleCommandAsync(
|
||||
string rawCommand,
|
||||
SidecarCommandEnvelope envelope,
|
||||
TextWriter output,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (envelope.Type)
|
||||
{
|
||||
case "describe-capabilities":
|
||||
await WriteAsync(output, new CapabilitiesEventDto
|
||||
{
|
||||
Type = "capabilities",
|
||||
RequestId = envelope.RequestId,
|
||||
Capabilities = BuildCapabilities(),
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case "validate-pattern":
|
||||
ValidatePatternCommandDto validateCommand =
|
||||
JsonSerializer.Deserialize<ValidatePatternCommandDto>(rawCommand, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Could not deserialize validate-pattern command.");
|
||||
|
||||
await WriteAsync(output, new PatternValidationEventDto
|
||||
{
|
||||
Type = "pattern-validation",
|
||||
RequestId = envelope.RequestId,
|
||||
Issues = _patternValidator.Validate(validateCommand.Pattern),
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case "run-turn":
|
||||
RunTurnCommandDto runTurnCommand =
|
||||
JsonSerializer.Deserialize<RunTurnCommandDto>(rawCommand, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Could not deserialize run-turn command.");
|
||||
|
||||
IReadOnlyList<ChatMessageDto> messages = await _workflowRunner.RunTurnAsync(
|
||||
runTurnCommand,
|
||||
delta => WriteAsync(output, delta, cancellationToken),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await WriteAsync(output, new TurnCompleteEventDto
|
||||
{
|
||||
Type = "turn-complete",
|
||||
RequestId = envelope.RequestId,
|
||||
SessionId = runTurnCommand.SessionId,
|
||||
Messages = messages,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unknown sidecar command type '{envelope.Type}'.");
|
||||
}
|
||||
|
||||
await WriteAsync(output, new CommandCompleteEventDto
|
||||
{
|
||||
Type = "command-complete",
|
||||
RequestId = envelope.RequestId,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await WriteAsync(output, new CommandErrorEventDto
|
||||
{
|
||||
Type = "command-error",
|
||||
RequestId = envelope.RequestId,
|
||||
Message = ex.Message,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteAsync(TextWriter output, object payload, CancellationToken cancellationToken)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(payload, _jsonOptions);
|
||||
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await output.WriteLineAsync(json).ConfigureAwait(false);
|
||||
await output.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static SidecarCapabilitiesDto BuildCapabilities()
|
||||
{
|
||||
return new SidecarCapabilitiesDto
|
||||
{
|
||||
Modes = new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["single"] = new() { Available = true },
|
||||
["sequential"] = new() { Available = true },
|
||||
["concurrent"] = new() { Available = true },
|
||||
["handoff"] = new() { Available = true },
|
||||
["group-chat"] = new() { Available = true },
|
||||
["magentic"] = new()
|
||||
{
|
||||
Available = false,
|
||||
Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user