refactor: decouple turn execution state from Copilot SDK events

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-08 11:12:40 +02:00
co-authored by Copilot
parent 2c165e453f
commit e85906669f
6 changed files with 449 additions and 146 deletions
@@ -0,0 +1,110 @@
namespace Aryx.AgentHost.Contracts;
internal abstract record ProviderSessionEvent;
internal sealed record ProviderAssistantMessageDeltaEvent(string MessageId) : ProviderSessionEvent;
internal sealed record ProviderAssistantMessageEvent(
string MessageId,
bool HasToolRequests) : ProviderSessionEvent;
internal sealed record ProviderToolExecutionStartEvent(
string ToolCallId,
string ToolName,
IReadOnlyDictionary<string, object?>? ToolArguments) : ProviderSessionEvent;
internal sealed record ProviderAssistantIntentEvent(string? Intent) : ProviderSessionEvent;
internal sealed record ProviderAssistantReasoningDeltaEvent(
string? ReasoningId,
string? DeltaContent) : ProviderSessionEvent;
internal sealed record ProviderSubagentStartedEvent(
string? ToolCallId,
string? AgentName,
string? AgentDisplayName,
string? AgentDescription) : ProviderSessionEvent;
internal sealed record ProviderSubagentCompletedEvent(
string? ToolCallId,
string? AgentName,
string? AgentDisplayName) : ProviderSessionEvent;
internal sealed record ProviderSubagentFailedEvent(
string? ToolCallId,
string? AgentName,
string? AgentDisplayName,
string? Error) : ProviderSessionEvent;
internal sealed record ProviderSubagentSelectedEvent(
string? AgentName,
string? AgentDisplayName,
IReadOnlyList<string>? Tools) : ProviderSessionEvent;
internal sealed record ProviderSubagentDeselectedEvent() : ProviderSessionEvent;
internal sealed record ProviderSkillInvokedEvent(
string SkillName,
string Path,
string Content,
IReadOnlyList<string>? AllowedTools,
string? PluginName,
string? PluginVersion) : ProviderSessionEvent;
internal sealed record ProviderHookStartEvent(
string HookInvocationId,
string HookType,
object? Input) : ProviderSessionEvent;
internal sealed record ProviderHookEndEvent(
string HookInvocationId,
string HookType,
bool? Success,
object? Output,
string? Error) : ProviderSessionEvent;
internal sealed record ProviderAssistantUsageEvent(
string Model,
double? InputTokens,
double? OutputTokens,
double? CacheReadTokens,
double? CacheWriteTokens,
double? Cost,
double? Duration,
double? TotalNanoAiu,
Dictionary<string, QuotaSnapshotDto>? QuotaSnapshots) : ProviderSessionEvent;
internal sealed record ProviderSessionUsageEvent(
double TokenLimit,
double CurrentTokens,
double MessagesLength,
double? SystemTokens,
double? ConversationTokens,
double? ToolDefinitionsTokens,
bool? IsInitial) : ProviderSessionEvent;
internal sealed record ProviderSessionCompactionStartEvent(
double? SystemTokens,
double? ConversationTokens,
double? ToolDefinitionsTokens) : ProviderSessionEvent;
internal sealed record ProviderSessionCompactionCompleteEvent(
bool? Success,
string? Error,
double? SystemTokens,
double? ConversationTokens,
double? ToolDefinitionsTokens,
double? PreCompactionTokens,
double? PostCompactionTokens,
double? PreCompactionMessagesLength,
double? MessagesRemoved,
double? TokensRemoved,
string? SummaryContent,
double? CheckpointNumber,
string? CheckpointPath) : ProviderSessionEvent;
internal sealed record ProviderPendingMessagesModifiedEvent() : ProviderSessionEvent;
internal sealed record ProviderMcpOauthRequiredEvent() : ProviderSessionEvent;
internal sealed record ProviderExitPlanModeRequestedEvent() : ProviderSessionEvent;
@@ -0,0 +1,157 @@
using Aryx.AgentHost.Contracts;
using GitHub.Copilot.SDK;
namespace Aryx.AgentHost.Services;
internal sealed class CopilotEventAdapter : IProviderEventAdapter
{
public ProviderSessionEvent? TryAdapt(object rawEvent)
{
return rawEvent switch
{
AssistantMessageDeltaEvent messageDelta
when NormalizeRequiredString(messageDelta.Data?.MessageId) is { } messageId =>
new ProviderAssistantMessageDeltaEvent(messageId),
AssistantMessageEvent assistantMessage
when NormalizeRequiredString(assistantMessage.Data?.MessageId) is { } messageId =>
new ProviderAssistantMessageEvent(
messageId,
assistantMessage.Data?.ToolRequests is { Length: > 0 }),
ToolExecutionStartEvent toolExecutionStart
when NormalizeRequiredString(toolExecutionStart.Data?.ToolCallId) is { } toolCallId
&& NormalizeRequiredString(toolExecutionStart.Data?.ToolName) is { } toolName =>
new ProviderToolExecutionStartEvent(
toolCallId,
toolName,
WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(toolExecutionStart.Data?.Arguments)),
AssistantIntentEvent intentEvent =>
new ProviderAssistantIntentEvent(NormalizeOptionalString(intentEvent.Data?.Intent)),
AssistantReasoningDeltaEvent reasoningDelta =>
new ProviderAssistantReasoningDeltaEvent(
NormalizeOptionalString(reasoningDelta.Data?.ReasoningId),
reasoningDelta.Data?.DeltaContent),
SubagentStartedEvent started =>
new ProviderSubagentStartedEvent(
started.Data?.ToolCallId,
started.Data?.AgentName,
started.Data?.AgentDisplayName,
started.Data?.AgentDescription),
SubagentCompletedEvent completed =>
new ProviderSubagentCompletedEvent(
completed.Data?.ToolCallId,
completed.Data?.AgentName,
completed.Data?.AgentDisplayName),
SubagentFailedEvent failed =>
new ProviderSubagentFailedEvent(
failed.Data?.ToolCallId,
failed.Data?.AgentName,
failed.Data?.AgentDisplayName,
failed.Data?.Error),
SubagentSelectedEvent selected =>
new ProviderSubagentSelectedEvent(
selected.Data?.AgentName,
selected.Data?.AgentDisplayName,
selected.Data?.Tools),
SubagentDeselectedEvent =>
new ProviderSubagentDeselectedEvent(),
SkillInvokedEvent skillInvoked =>
new ProviderSkillInvokedEvent(
skillInvoked.Data?.Name ?? string.Empty,
skillInvoked.Data?.Path ?? string.Empty,
skillInvoked.Data?.Content ?? string.Empty,
skillInvoked.Data?.AllowedTools,
skillInvoked.Data?.PluginName,
skillInvoked.Data?.PluginVersion),
HookStartEvent hookStart =>
new ProviderHookStartEvent(
hookStart.Data?.HookInvocationId ?? string.Empty,
hookStart.Data?.HookType ?? string.Empty,
hookStart.Data?.Input),
HookEndEvent hookEnd =>
new ProviderHookEndEvent(
hookEnd.Data?.HookInvocationId ?? string.Empty,
hookEnd.Data?.HookType ?? string.Empty,
hookEnd.Data?.Success,
hookEnd.Data?.Output,
hookEnd.Data?.Error?.Message),
AssistantUsageEvent assistantUsage =>
new ProviderAssistantUsageEvent(
assistantUsage.Data?.Model ?? string.Empty,
assistantUsage.Data?.InputTokens,
assistantUsage.Data?.OutputTokens,
assistantUsage.Data?.CacheReadTokens,
assistantUsage.Data?.CacheWriteTokens,
assistantUsage.Data?.Cost,
assistantUsage.Data?.Duration,
assistantUsage.Data?.CopilotUsage?.TotalNanoAiu,
QuotaSnapshotMapper.MapOrNull(assistantUsage.Data?.QuotaSnapshots)),
SessionUsageInfoEvent usageInfo =>
new ProviderSessionUsageEvent(
usageInfo.Data?.TokenLimit ?? 0,
usageInfo.Data?.CurrentTokens ?? 0,
usageInfo.Data?.MessagesLength ?? 0,
usageInfo.Data?.SystemTokens,
usageInfo.Data?.ConversationTokens,
usageInfo.Data?.ToolDefinitionsTokens,
usageInfo.Data?.IsInitial),
SessionCompactionStartEvent compactionStart =>
new ProviderSessionCompactionStartEvent(
compactionStart.Data?.SystemTokens,
compactionStart.Data?.ConversationTokens,
compactionStart.Data?.ToolDefinitionsTokens),
SessionCompactionCompleteEvent compactionComplete =>
new ProviderSessionCompactionCompleteEvent(
compactionComplete.Data?.Success,
compactionComplete.Data?.Error,
compactionComplete.Data?.SystemTokens,
compactionComplete.Data?.ConversationTokens,
compactionComplete.Data?.ToolDefinitionsTokens,
compactionComplete.Data?.PreCompactionTokens,
compactionComplete.Data?.PostCompactionTokens,
compactionComplete.Data?.PreCompactionMessagesLength,
compactionComplete.Data?.MessagesRemoved,
compactionComplete.Data?.TokensRemoved,
compactionComplete.Data?.SummaryContent,
compactionComplete.Data?.CheckpointNumber,
compactionComplete.Data?.CheckpointPath),
PendingMessagesModifiedEvent =>
new ProviderPendingMessagesModifiedEvent(),
McpOauthRequiredEvent =>
new ProviderMcpOauthRequiredEvent(),
ExitPlanModeRequestedEvent =>
new ProviderExitPlanModeRequestedEvent(),
_ => null,
};
}
private static string? NormalizeRequiredString(string? value)
{
string? normalized = NormalizeOptionalString(value);
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
}
private static string? NormalizeOptionalString(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
@@ -1,6 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Aryx.AgentHost.Contracts; using Aryx.AgentHost.Contracts;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
namespace Aryx.AgentHost.Services; namespace Aryx.AgentHost.Services;
@@ -70,7 +69,7 @@ internal sealed class CopilotTurnExecutionState
} }
} }
public void ObserveSessionEvent(WorkflowNodeDto agentDefinition, SessionEvent sessionEvent) public void ObserveSessionEvent(WorkflowNodeDto agentDefinition, ProviderSessionEvent sessionEvent)
{ {
AgentIdentity agent = AgentIdentityResolver.ResolveAgentIdentity( AgentIdentity agent = AgentIdentityResolver.ResolveAgentIdentity(
_command.Workflow, _command.Workflow,
@@ -79,27 +78,25 @@ internal sealed class CopilotTurnExecutionState
switch (sessionEvent) switch (sessionEvent)
{ {
case AssistantMessageDeltaEvent messageDelta when !string.IsNullOrWhiteSpace(messageDelta.Data?.MessageId): case ProviderAssistantMessageDeltaEvent messageDelta:
RecordObservedAgentForMessage(agent, messageDelta.Data!.MessageId); RecordObservedAgentForMessage(agent, messageDelta.MessageId);
QueueThinkingIfNeeded(agent); QueueThinkingIfNeeded(agent);
break; break;
case AssistantMessageEvent assistantMessage when !string.IsNullOrWhiteSpace(assistantMessage.Data?.MessageId): case ProviderAssistantMessageEvent assistantMessage:
RecordObservedAgentForMessage(agent, assistantMessage.Data!.MessageId); RecordObservedAgentForMessage(agent, assistantMessage.MessageId);
QueueThinkingIfNeeded(agent); QueueThinkingIfNeeded(agent);
if (assistantMessage.Data?.ToolRequests is { Length: > 0 }) if (assistantMessage.HasToolRequests)
{ {
QueueMessageReclassifiedIfNeeded(assistantMessage.Data.MessageId); QueueMessageReclassifiedIfNeeded(assistantMessage.MessageId);
} }
break; break;
case ToolExecutionStartEvent toolExecutionStart case ProviderToolExecutionStartEvent toolExecutionStart:
when !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolCallId) string toolCallId = toolExecutionStart.ToolCallId;
&& !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolName): string toolName = toolExecutionStart.ToolName;
string toolCallId = toolExecutionStart.Data.ToolCallId.Trim();
string toolName = toolExecutionStart.Data.ToolName.Trim();
ToolNamesByCallId[toolCallId] = toolName; ToolNamesByCallId[toolCallId] = toolName;
ActiveAgent = agent; ActiveAgent = agent;
AgentActivityEventDto? toolActivity = CreateToolCallingActivity( AgentActivityEventDto? toolActivity = CreateToolCallingActivity(
agent, toolName, toolCallId, toolExecutionStart.Data.Arguments); agent, toolName, toolCallId, toolExecutionStart.ToolArguments);
if (toolActivity is not null) if (toolActivity is not null)
{ {
_pendingEvents.Enqueue(toolActivity); _pendingEvents.Enqueue(toolActivity);
@@ -107,86 +104,101 @@ internal sealed class CopilotTurnExecutionState
QueueMessageReclassifiedIfNeeded(_lastObservedMessageId); QueueMessageReclassifiedIfNeeded(_lastObservedMessageId);
break; break;
case AssistantIntentEvent intentEvent: case ProviderAssistantIntentEvent intentEvent:
ActiveAgent = agent; ActiveAgent = agent;
QueueThinkingIfNeeded(agent); QueueThinkingIfNeeded(agent);
AssistantIntentEventDto? assistantIntent = CreateAssistantIntentEvent(agent, intentEvent.Data); AssistantIntentEventDto? assistantIntent = CreateAssistantIntentEvent(agent, intentEvent.Intent);
if (assistantIntent is not null) if (assistantIntent is not null)
{ {
_pendingEvents.Enqueue(assistantIntent); _pendingEvents.Enqueue(assistantIntent);
} }
break; break;
case AssistantReasoningDeltaEvent reasoningDelta: case ProviderAssistantReasoningDeltaEvent reasoningDelta:
ActiveAgent = agent; ActiveAgent = agent;
QueueThinkingIfNeeded(agent); QueueThinkingIfNeeded(agent);
ReasoningDeltaEventDto? reasoningDeltaEvent = CreateReasoningDeltaEvent(agent, reasoningDelta.Data); ReasoningDeltaEventDto? reasoningDeltaEvent = CreateReasoningDeltaEvent(
agent,
reasoningDelta.ReasoningId,
reasoningDelta.DeltaContent);
if (reasoningDeltaEvent is not null) if (reasoningDeltaEvent is not null)
{ {
_pendingEvents.Enqueue(reasoningDeltaEvent); _pendingEvents.Enqueue(reasoningDeltaEvent);
} }
break; break;
case SubagentStartedEvent started: case ProviderSubagentStartedEvent started:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSubagentEvent(agent, "started", started.Data)); _pendingEvents.Enqueue(CreateSubagentStartedEvent(agent, started));
break; break;
case SubagentCompletedEvent completed: case ProviderSubagentCompletedEvent completed:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSubagentCompletedEvent(agent, completed.Data)); _pendingEvents.Enqueue(CreateSubagentCompletedEvent(agent, completed));
break; break;
case SubagentFailedEvent failed: case ProviderSubagentFailedEvent failed:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSubagentFailedEvent(agent, failed.Data)); _pendingEvents.Enqueue(CreateSubagentFailedEvent(agent, failed));
break; break;
case SubagentSelectedEvent selected: case ProviderSubagentSelectedEvent selected:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSubagentSelectedEvent(agent, selected.Data)); _pendingEvents.Enqueue(CreateSubagentSelectedEvent(agent, selected));
break; break;
case SubagentDeselectedEvent: case ProviderSubagentDeselectedEvent:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSubagentDeselectedEvent(agent)); _pendingEvents.Enqueue(CreateSubagentDeselectedEvent(agent));
break; break;
case SkillInvokedEvent skillInvoked: case ProviderSkillInvokedEvent skillInvoked:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateSkillInvokedEvent(agent, skillInvoked.Data)); _pendingEvents.Enqueue(CreateSkillInvokedEvent(agent, skillInvoked));
break; break;
case HookStartEvent hookStart: case ProviderHookStartEvent hookStart:
ActiveAgent = agent; ActiveAgent = agent;
if (!SuppressHookLifecycleEvents) if (!SuppressHookLifecycleEvents)
{ {
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "start", hookStart.Data)); _pendingEvents.Enqueue(CreateHookLifecycleEvent(
agent,
"start",
hookStart.HookInvocationId,
hookStart.HookType,
input: hookStart.Input));
} }
break; break;
case HookEndEvent hookEnd: case ProviderHookEndEvent hookEnd:
ActiveAgent = agent; ActiveAgent = agent;
if (!SuppressHookLifecycleEvents) if (!SuppressHookLifecycleEvents)
{ {
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "end", hookEnd.Data)); _pendingEvents.Enqueue(CreateHookLifecycleEvent(
agent,
"end",
hookEnd.HookInvocationId,
hookEnd.HookType,
success: hookEnd.Success,
output: hookEnd.Output,
error: hookEnd.Error));
} }
break; break;
case AssistantUsageEvent assistantUsage: case ProviderAssistantUsageEvent assistantUsage:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateAssistantUsageEvent(agent, assistantUsage.Data)); _pendingEvents.Enqueue(CreateAssistantUsageEvent(agent, assistantUsage));
break; break;
case SessionUsageInfoEvent usageInfo: case ProviderSessionUsageEvent usageInfo:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateUsageEvent(agent, usageInfo.Data)); _pendingEvents.Enqueue(CreateUsageEvent(agent, usageInfo));
break; break;
case SessionCompactionStartEvent compactionStart: case ProviderSessionCompactionStartEvent compactionStart:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateCompactionStartEvent(agent, compactionStart.Data)); _pendingEvents.Enqueue(CreateCompactionStartEvent(agent, compactionStart));
break; break;
case SessionCompactionCompleteEvent compactionComplete: case ProviderSessionCompactionCompleteEvent compactionComplete:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreateCompactionCompleteEvent(agent, compactionComplete.Data)); _pendingEvents.Enqueue(CreateCompactionCompleteEvent(agent, compactionComplete));
break; break;
case PendingMessagesModifiedEvent: case ProviderPendingMessagesModifiedEvent:
ActiveAgent = agent; ActiveAgent = agent;
_pendingEvents.Enqueue(CreatePendingMessagesModifiedEvent(agent)); _pendingEvents.Enqueue(CreatePendingMessagesModifiedEvent(agent));
break; break;
case McpOauthRequiredEvent: case ProviderMcpOauthRequiredEvent:
ActiveAgent = agent; ActiveAgent = agent;
break; break;
case ExitPlanModeRequestedEvent: case ProviderExitPlanModeRequestedEvent:
HasPendingExitPlanModeRequest = true; HasPendingExitPlanModeRequest = true;
ActiveAgent = agent; ActiveAgent = agent;
break; break;
@@ -297,7 +309,7 @@ internal sealed class CopilotTurnExecutionState
AgentIdentity agent, AgentIdentity agent,
string toolName, string toolName,
string toolCallId, string toolCallId,
object? rawArguments = null) IReadOnlyDictionary<string, object?>? toolArguments = null)
{ {
if (toolName.StartsWith("handoff_to_", StringComparison.Ordinal)) if (toolName.StartsWith("handoff_to_", StringComparison.Ordinal))
{ {
@@ -314,7 +326,7 @@ internal sealed class CopilotTurnExecutionState
AgentName = agent.AgentName, AgentName = agent.AgentName,
ToolName = toolName, ToolName = toolName,
ToolCallId = toolCallId, ToolCallId = toolCallId,
ToolArguments = WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(rawArguments), ToolArguments = toolArguments,
}; };
} }
@@ -377,29 +389,28 @@ internal sealed class CopilotTurnExecutionState
return CompletedMessages; return CompletedMessages;
} }
private SubagentEventDto CreateSubagentEvent( private SubagentEventDto CreateSubagentStartedEvent(
AgentIdentity agent, AgentIdentity agent,
string eventKind, ProviderSubagentStartedEvent data)
SubagentStartedData? data)
{ {
return new SubagentEventDto return new SubagentEventDto
{ {
Type = "subagent-event", Type = "subagent-event",
RequestId = _command.RequestId, RequestId = _command.RequestId,
SessionId = _command.SessionId, SessionId = _command.SessionId,
EventKind = eventKind, EventKind = "started",
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
ToolCallId = data?.ToolCallId, ToolCallId = data.ToolCallId,
CustomAgentName = data?.AgentName, CustomAgentName = data.AgentName,
CustomAgentDisplayName = data?.AgentDisplayName, CustomAgentDisplayName = data.AgentDisplayName,
CustomAgentDescription = data?.AgentDescription, CustomAgentDescription = data.AgentDescription,
}; };
} }
private SubagentEventDto CreateSubagentCompletedEvent( private SubagentEventDto CreateSubagentCompletedEvent(
AgentIdentity agent, AgentIdentity agent,
SubagentCompletedData? data) ProviderSubagentCompletedEvent data)
{ {
return new SubagentEventDto return new SubagentEventDto
{ {
@@ -409,15 +420,15 @@ internal sealed class CopilotTurnExecutionState
EventKind = "completed", EventKind = "completed",
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
ToolCallId = data?.ToolCallId, ToolCallId = data.ToolCallId,
CustomAgentName = data?.AgentName, CustomAgentName = data.AgentName,
CustomAgentDisplayName = data?.AgentDisplayName, CustomAgentDisplayName = data.AgentDisplayName,
}; };
} }
private SubagentEventDto CreateSubagentFailedEvent( private SubagentEventDto CreateSubagentFailedEvent(
AgentIdentity agent, AgentIdentity agent,
SubagentFailedData? data) ProviderSubagentFailedEvent data)
{ {
return new SubagentEventDto return new SubagentEventDto
{ {
@@ -427,16 +438,16 @@ internal sealed class CopilotTurnExecutionState
EventKind = "failed", EventKind = "failed",
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
ToolCallId = data?.ToolCallId, ToolCallId = data.ToolCallId,
CustomAgentName = data?.AgentName, CustomAgentName = data.AgentName,
CustomAgentDisplayName = data?.AgentDisplayName, CustomAgentDisplayName = data.AgentDisplayName,
Error = data?.Error, Error = data.Error,
}; };
} }
private SubagentEventDto CreateSubagentSelectedEvent( private SubagentEventDto CreateSubagentSelectedEvent(
AgentIdentity agent, AgentIdentity agent,
SubagentSelectedData? data) ProviderSubagentSelectedEvent data)
{ {
return new SubagentEventDto return new SubagentEventDto
{ {
@@ -446,9 +457,9 @@ internal sealed class CopilotTurnExecutionState
EventKind = "selected", EventKind = "selected",
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
CustomAgentName = data?.AgentName, CustomAgentName = data.AgentName,
CustomAgentDisplayName = data?.AgentDisplayName, CustomAgentDisplayName = data.AgentDisplayName,
Tools = data?.Tools, Tools = data.Tools,
}; };
} }
@@ -467,10 +478,10 @@ internal sealed class CopilotTurnExecutionState
private AssistantIntentEventDto? CreateAssistantIntentEvent( private AssistantIntentEventDto? CreateAssistantIntentEvent(
AgentIdentity agent, AgentIdentity agent,
AssistantIntentData? data) string? intent)
{ {
string? intent = data?.Intent?.Trim(); string? normalizedIntent = intent?.Trim();
if (string.IsNullOrWhiteSpace(intent)) if (string.IsNullOrWhiteSpace(normalizedIntent))
{ {
return null; return null;
} }
@@ -482,17 +493,17 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
Intent = intent, Intent = normalizedIntent,
}; };
} }
private ReasoningDeltaEventDto? CreateReasoningDeltaEvent( private ReasoningDeltaEventDto? CreateReasoningDeltaEvent(
AgentIdentity agent, AgentIdentity agent,
AssistantReasoningDeltaData? data) string? reasoningId,
string? deltaContent)
{ {
if (data is null if (string.IsNullOrWhiteSpace(reasoningId)
|| string.IsNullOrWhiteSpace(data.ReasoningId) || string.IsNullOrEmpty(deltaContent))
|| string.IsNullOrEmpty(data.DeltaContent))
{ {
return null; return null;
} }
@@ -504,14 +515,14 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
ReasoningId = data.ReasoningId, ReasoningId = reasoningId,
ContentDelta = data.DeltaContent, ContentDelta = deltaContent,
}; };
} }
private SkillInvokedEventDto CreateSkillInvokedEvent( private SkillInvokedEventDto CreateSkillInvokedEvent(
AgentIdentity agent, AgentIdentity agent,
SkillInvokedData? data) ProviderSkillInvokedEvent data)
{ {
return new SkillInvokedEventDto return new SkillInvokedEventDto
{ {
@@ -520,19 +531,24 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
SkillName = data?.Name ?? string.Empty, SkillName = data.SkillName,
Path = data?.Path ?? string.Empty, Path = data.Path,
Content = data?.Content ?? string.Empty, Content = data.Content,
AllowedTools = data?.AllowedTools, AllowedTools = data.AllowedTools,
PluginName = data?.PluginName, PluginName = data.PluginName,
PluginVersion = data?.PluginVersion, PluginVersion = data.PluginVersion,
}; };
} }
private HookLifecycleEventDto CreateHookLifecycleEvent( private HookLifecycleEventDto CreateHookLifecycleEvent(
AgentIdentity agent, AgentIdentity agent,
string phase, string phase,
HookStartData? data) string hookInvocationId,
string hookType,
object? input = null,
bool? success = null,
object? output = null,
string? error = null)
{ {
return new HookLifecycleEventDto return new HookLifecycleEventDto
{ {
@@ -541,37 +557,19 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
HookInvocationId = data?.HookInvocationId ?? string.Empty, HookInvocationId = hookInvocationId,
HookType = data?.HookType ?? string.Empty, HookType = hookType,
Phase = phase, Phase = phase,
Input = data?.Input, Input = input,
}; Success = success,
} Output = output,
Error = error,
private HookLifecycleEventDto CreateHookLifecycleEvent(
AgentIdentity agent,
string phase,
HookEndData? data)
{
return new HookLifecycleEventDto
{
Type = "hook-lifecycle",
RequestId = _command.RequestId,
SessionId = _command.SessionId,
AgentId = agent.AgentId,
AgentName = agent.AgentName,
HookInvocationId = data?.HookInvocationId ?? string.Empty,
HookType = data?.HookType ?? string.Empty,
Phase = phase,
Success = data?.Success,
Output = data?.Output,
Error = data?.Error?.Message,
}; };
} }
private AssistantUsageEventDto CreateAssistantUsageEvent( private AssistantUsageEventDto CreateAssistantUsageEvent(
AgentIdentity agent, AgentIdentity agent,
AssistantUsageData? data) ProviderAssistantUsageEvent data)
{ {
return new AssistantUsageEventDto return new AssistantUsageEventDto
{ {
@@ -580,19 +578,19 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
Model = data?.Model ?? string.Empty, Model = data.Model,
InputTokens = data?.InputTokens, InputTokens = data.InputTokens,
OutputTokens = data?.OutputTokens, OutputTokens = data.OutputTokens,
CacheReadTokens = data?.CacheReadTokens, CacheReadTokens = data.CacheReadTokens,
CacheWriteTokens = data?.CacheWriteTokens, CacheWriteTokens = data.CacheWriteTokens,
Cost = data?.Cost, Cost = data.Cost,
Duration = data?.Duration, Duration = data.Duration,
TotalNanoAiu = data?.CopilotUsage?.TotalNanoAiu, TotalNanoAiu = data.TotalNanoAiu,
QuotaSnapshots = QuotaSnapshotMapper.MapOrNull(data?.QuotaSnapshots), QuotaSnapshots = data.QuotaSnapshots,
}; };
} }
private SessionUsageEventDto CreateUsageEvent(AgentIdentity agent, SessionUsageInfoData? data) private SessionUsageEventDto CreateUsageEvent(AgentIdentity agent, ProviderSessionUsageEvent data)
{ {
return new SessionUsageEventDto return new SessionUsageEventDto
{ {
@@ -601,19 +599,19 @@ internal sealed class CopilotTurnExecutionState
SessionId = _command.SessionId, SessionId = _command.SessionId,
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
TokenLimit = data?.TokenLimit ?? 0, TokenLimit = data.TokenLimit,
CurrentTokens = data?.CurrentTokens ?? 0, CurrentTokens = data.CurrentTokens,
MessagesLength = data?.MessagesLength ?? 0, MessagesLength = data.MessagesLength,
SystemTokens = data?.SystemTokens, SystemTokens = data.SystemTokens,
ConversationTokens = data?.ConversationTokens, ConversationTokens = data.ConversationTokens,
ToolDefinitionsTokens = data?.ToolDefinitionsTokens, ToolDefinitionsTokens = data.ToolDefinitionsTokens,
IsInitial = data?.IsInitial, IsInitial = data.IsInitial,
}; };
} }
private SessionCompactionEventDto CreateCompactionStartEvent( private SessionCompactionEventDto CreateCompactionStartEvent(
AgentIdentity agent, AgentIdentity agent,
SessionCompactionStartData? data) ProviderSessionCompactionStartEvent data)
{ {
return new SessionCompactionEventDto return new SessionCompactionEventDto
{ {
@@ -623,15 +621,15 @@ internal sealed class CopilotTurnExecutionState
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
Phase = "start", Phase = "start",
SystemTokens = data?.SystemTokens, SystemTokens = data.SystemTokens,
ConversationTokens = data?.ConversationTokens, ConversationTokens = data.ConversationTokens,
ToolDefinitionsTokens = data?.ToolDefinitionsTokens, ToolDefinitionsTokens = data.ToolDefinitionsTokens,
}; };
} }
private SessionCompactionEventDto CreateCompactionCompleteEvent( private SessionCompactionEventDto CreateCompactionCompleteEvent(
AgentIdentity agent, AgentIdentity agent,
SessionCompactionCompleteData? data) ProviderSessionCompactionCompleteEvent data)
{ {
return new SessionCompactionEventDto return new SessionCompactionEventDto
{ {
@@ -641,19 +639,19 @@ internal sealed class CopilotTurnExecutionState
AgentId = agent.AgentId, AgentId = agent.AgentId,
AgentName = agent.AgentName, AgentName = agent.AgentName,
Phase = "complete", Phase = "complete",
Success = data?.Success, Success = data.Success,
Error = data?.Error, Error = data.Error,
SystemTokens = data?.SystemTokens, SystemTokens = data.SystemTokens,
ConversationTokens = data?.ConversationTokens, ConversationTokens = data.ConversationTokens,
ToolDefinitionsTokens = data?.ToolDefinitionsTokens, ToolDefinitionsTokens = data.ToolDefinitionsTokens,
PreCompactionTokens = data?.PreCompactionTokens, PreCompactionTokens = data.PreCompactionTokens,
PostCompactionTokens = data?.PostCompactionTokens, PostCompactionTokens = data.PostCompactionTokens,
PreCompactionMessagesLength = data?.PreCompactionMessagesLength, PreCompactionMessagesLength = data.PreCompactionMessagesLength,
MessagesRemoved = data?.MessagesRemoved, MessagesRemoved = data.MessagesRemoved,
TokensRemoved = data?.TokensRemoved, TokensRemoved = data.TokensRemoved,
SummaryContent = data?.SummaryContent, SummaryContent = data.SummaryContent,
CheckpointNumber = data?.CheckpointNumber, CheckpointNumber = data.CheckpointNumber,
CheckpointPath = data?.CheckpointPath, CheckpointPath = data.CheckpointPath,
}; };
} }
@@ -44,6 +44,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
throw new InvalidOperationException(validationError); throw new InvalidOperationException(validationError);
} }
IProviderEventAdapter providerEventAdapter = new CopilotEventAdapter();
CopilotTurnExecutionState state = new(command); CopilotTurnExecutionState state = new(command);
using CancellationTokenSource runCancellation = using CancellationTokenSource runCancellation =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -70,7 +71,11 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
runCancellation.Token), runCancellation.Token),
(agent, sessionEvent) => (agent, sessionEvent) =>
{ {
state.ObserveSessionEvent(agent, sessionEvent); if (providerEventAdapter.TryAdapt(sessionEvent) is { } providerEvent)
{
state.ObserveSessionEvent(agent, providerEvent);
}
if (sessionEvent is McpOauthRequiredEvent mcpOauthRequired) if (sessionEvent is McpOauthRequiredEvent mcpOauthRequired)
{ {
state.EnqueuePendingMcpOauthRequest( state.EnqueuePendingMcpOauthRequest(
@@ -0,0 +1,8 @@
using Aryx.AgentHost.Contracts;
namespace Aryx.AgentHost.Services;
internal interface IProviderEventAdapter
{
ProviderSessionEvent? TryAdapt(object rawEvent);
}
@@ -0,0 +1,25 @@
using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
using GitHub.Copilot.SDK;
namespace Aryx.AgentHost.Tests;
internal static class SessionEventTestExtensions
{
private static readonly IProviderEventAdapter ProviderEventAdapter = new CopilotEventAdapter();
public static void ObserveSessionEvent(
this CopilotTurnExecutionState state,
WorkflowNodeDto agentDefinition,
SessionEvent sessionEvent)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(agentDefinition);
ArgumentNullException.ThrowIfNull(sessionEvent);
ProviderSessionEvent providerEvent = Assert.IsAssignableFrom<ProviderSessionEvent>(
ProviderEventAdapter.TryAdapt(sessionEvent));
state.ObserveSessionEvent(agentDefinition, providerEvent);
}
}