mirror of
https://github.com/davidkaya/aryx.git
synced 2026-09-11 04:11:47 +02:00
feat: show sub-workflow agents and lifecycle in the Activity panel
Deep agent resolution in the sidecar now walks sub-workflow nodes so nested agents carry subworkflowNodeId and subworkflowName on activity events. New subworkflow-started / subworkflow-completed activity types let the frontend track sub-workflow lifecycle. The Activity panel groups nested agents under collapsible sub-workflow cards with status badges, accent-colored left borders, and smooth expand/collapse transitions. Cards auto-expand when a sub-workflow starts running. Workflows without sub-workflow nodes render identically to before. Extracted AgentRow, SubWorkflowGroup, and shared accent constants to a new components/activity/ feature directory. Added resolveWorkflowAgentHierarchy and buildGroupedActivityRows for hierarchical activity grouping with dynamic fallback for unresolved sub-workflow agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -452,6 +452,8 @@ public sealed class AgentActivityEventDto : SidecarEventDto
|
||||
public string ActivityType { get; init; } = string.Empty;
|
||||
public string? AgentId { get; init; }
|
||||
public string? AgentName { get; init; }
|
||||
public string? SubworkflowNodeId { get; init; }
|
||||
public string? SubworkflowName { get; init; }
|
||||
public string? SourceAgentId { get; init; }
|
||||
public string? SourceAgentName { get; init; }
|
||||
public string? ToolName { get; init; }
|
||||
|
||||
@@ -3,7 +3,12 @@ using Aryx.AgentHost.Contracts;
|
||||
|
||||
namespace Aryx.AgentHost.Services;
|
||||
|
||||
internal readonly record struct AgentIdentity(string AgentId, string AgentName);
|
||||
internal readonly record struct SubworkflowContext(string SubworkflowNodeId, string SubworkflowName);
|
||||
|
||||
internal readonly record struct AgentIdentity(
|
||||
string AgentId,
|
||||
string AgentName,
|
||||
SubworkflowContext? Subworkflow = null);
|
||||
|
||||
internal static class AgentIdentityResolver
|
||||
{
|
||||
@@ -13,17 +18,69 @@ internal static class AgentIdentityResolver
|
||||
WorkflowDefinitionDto workflow,
|
||||
string? agentIdentifier,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
return TryResolveKnownAgentIdentity(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(null),
|
||||
agentIdentifier,
|
||||
agentSubworkflowIndex: null,
|
||||
out agent);
|
||||
}
|
||||
|
||||
public static bool TryResolveKnownAgentIdentity(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyList<WorkflowDefinitionDto>? workflowLibrary,
|
||||
string? agentIdentifier,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
return TryResolveKnownAgentIdentity(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(workflowLibrary),
|
||||
agentIdentifier,
|
||||
agentSubworkflowIndex: null,
|
||||
out agent);
|
||||
}
|
||||
|
||||
internal static bool TryResolveKnownAgentIdentity(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
string? agentIdentifier,
|
||||
IReadOnlyDictionary<string, SubworkflowContext>? agentSubworkflowIndex,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
agent = default;
|
||||
|
||||
WorkflowNodeDto? match = FindKnownAgent(workflow, agentIdentifier)
|
||||
?? ResolveSingleAgentAssistantAlias(workflow, agentIdentifier);
|
||||
if (match is null)
|
||||
WorkflowNodeDto? shallowMatch = FindKnownAgent(workflow.GetAgentNodes(), agentIdentifier);
|
||||
if (shallowMatch is not null)
|
||||
{
|
||||
agent = ToAgentIdentity(shallowMatch);
|
||||
return true;
|
||||
}
|
||||
|
||||
WorkflowNodeDto? deepMatch = FindKnownAgent(workflow.GetAllAgentNodes(workflowLibrary), agentIdentifier);
|
||||
if (deepMatch is not null)
|
||||
{
|
||||
IReadOnlyDictionary<string, SubworkflowContext> subworkflowIndex = agentSubworkflowIndex
|
||||
?? BuildAgentSubworkflowIndex(workflow, workflowLibrary);
|
||||
agent = ToAgentIdentity(deepMatch, subworkflowIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
WorkflowNodeDto? aliasMatch = ResolveSingleAgentAssistantAlias(workflow, workflowLibrary, agentIdentifier);
|
||||
if (aliasMatch is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
agent = ToAgentIdentity(match);
|
||||
if (workflow.GetAgentNodes().Contains(aliasMatch))
|
||||
{
|
||||
agent = ToAgentIdentity(aliasMatch);
|
||||
return true;
|
||||
}
|
||||
|
||||
IReadOnlyDictionary<string, SubworkflowContext> aliasSubworkflowIndex = agentSubworkflowIndex
|
||||
?? BuildAgentSubworkflowIndex(workflow, workflowLibrary);
|
||||
agent = ToAgentIdentity(aliasMatch, aliasSubworkflowIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -33,7 +90,24 @@ internal static class AgentIdentityResolver
|
||||
AgentIdentity? fallbackAgent,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
if (TryResolveKnownAgentIdentity(workflow, agentIdentifier, out agent))
|
||||
return TryResolveObservedAgentIdentity(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(null),
|
||||
agentIdentifier,
|
||||
fallbackAgent,
|
||||
agentSubworkflowIndex: null,
|
||||
out agent);
|
||||
}
|
||||
|
||||
internal static bool TryResolveObservedAgentIdentity(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
string? agentIdentifier,
|
||||
AgentIdentity? fallbackAgent,
|
||||
IReadOnlyDictionary<string, SubworkflowContext>? agentSubworkflowIndex,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
if (TryResolveKnownAgentIdentity(workflow, workflowLibrary, agentIdentifier, agentSubworkflowIndex, out agent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -53,13 +127,46 @@ internal static class AgentIdentityResolver
|
||||
string? agentId,
|
||||
string? agentName)
|
||||
{
|
||||
WorkflowNodeDto? match = FindKnownAgent(workflow, agentId)
|
||||
?? FindKnownAgent(workflow, agentName)
|
||||
?? ResolveSingleAgentAssistantAlias(workflow, agentId, agentName);
|
||||
return ResolveAgentIdentity(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(null),
|
||||
agentId,
|
||||
agentName,
|
||||
agentSubworkflowIndex: null);
|
||||
}
|
||||
|
||||
return match is not null
|
||||
? ToAgentIdentity(match)
|
||||
: CreateFallbackIdentity(agentId, agentName);
|
||||
public static AgentIdentity ResolveAgentIdentity(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyList<WorkflowDefinitionDto>? workflowLibrary,
|
||||
string? agentId,
|
||||
string? agentName)
|
||||
{
|
||||
return ResolveAgentIdentity(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(workflowLibrary),
|
||||
agentId,
|
||||
agentName,
|
||||
agentSubworkflowIndex: null);
|
||||
}
|
||||
|
||||
internal static AgentIdentity ResolveAgentIdentity(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
string? agentId,
|
||||
string? agentName,
|
||||
IReadOnlyDictionary<string, SubworkflowContext>? agentSubworkflowIndex)
|
||||
{
|
||||
if (TryResolveKnownAgentIdentity(workflow, workflowLibrary, agentId, agentSubworkflowIndex, out AgentIdentity resolvedById))
|
||||
{
|
||||
return resolvedById;
|
||||
}
|
||||
|
||||
if (TryResolveKnownAgentIdentity(workflow, workflowLibrary, agentName, agentSubworkflowIndex, out AgentIdentity resolvedByName))
|
||||
{
|
||||
return resolvedByName;
|
||||
}
|
||||
|
||||
return CreateFallbackIdentity(agentId, agentName, agentSubworkflowIndex);
|
||||
}
|
||||
|
||||
public static string ResolveDisplayAuthorName(
|
||||
@@ -90,6 +197,53 @@ internal static class AgentIdentityResolver
|
||||
return GenericAssistantIdentifier;
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<string, SubworkflowContext> BuildAgentSubworkflowIndex(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyList<WorkflowDefinitionDto>? workflowLibrary = null)
|
||||
{
|
||||
return BuildAgentSubworkflowIndex(
|
||||
workflow,
|
||||
WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(workflowLibrary));
|
||||
}
|
||||
|
||||
internal static IReadOnlyDictionary<string, SubworkflowContext> BuildAgentSubworkflowIndex(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
ArgumentNullException.ThrowIfNull(workflowLibrary);
|
||||
|
||||
Dictionary<string, SubworkflowContext> index = new(StringComparer.Ordinal);
|
||||
CollectAgentSubworkflowContexts(
|
||||
workflow,
|
||||
workflowLibrary,
|
||||
currentSubworkflow: null,
|
||||
index,
|
||||
new HashSet<string>(StringComparer.Ordinal),
|
||||
new HashSet<WorkflowDefinitionDto>(ReferenceEqualityComparer.Instance));
|
||||
return index;
|
||||
}
|
||||
|
||||
internal static bool TryResolveSubworkflowContext(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
string? subworkflowNodeId,
|
||||
out SubworkflowContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
ArgumentNullException.ThrowIfNull(workflowLibrary);
|
||||
|
||||
context = default;
|
||||
WorkflowNodeDto? node = workflow.FindSubWorkflowNode(subworkflowNodeId, workflowLibrary);
|
||||
if (node is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context = CreateSubworkflowContext(node, workflowLibrary);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsGenericAssistantIdentifier(string? candidate)
|
||||
{
|
||||
return string.Equals(
|
||||
@@ -100,34 +254,120 @@ internal static class AgentIdentityResolver
|
||||
|
||||
private static WorkflowNodeDto? ResolveSingleAgentAssistantAlias(
|
||||
WorkflowDefinitionDto workflow,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
params string?[] agentIdentifiers)
|
||||
{
|
||||
IReadOnlyList<WorkflowNodeDto> agentNodes = workflow.GetAgentNodes();
|
||||
return agentNodes.Count == 1 && agentIdentifiers.Any(IsGenericAssistantIdentifier)
|
||||
? agentNodes[0]
|
||||
: null;
|
||||
if (!agentIdentifiers.Any(IsGenericAssistantIdentifier))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IReadOnlyList<WorkflowNodeDto> topLevelAgents = workflow.GetAgentNodes();
|
||||
if (topLevelAgents.Count == 1)
|
||||
{
|
||||
return topLevelAgents[0];
|
||||
}
|
||||
|
||||
IReadOnlyList<WorkflowNodeDto> allAgents = workflow.GetAllAgentNodes(workflowLibrary);
|
||||
return allAgents.Count == 1 ? allAgents[0] : null;
|
||||
}
|
||||
|
||||
private static WorkflowNodeDto? FindKnownAgent(WorkflowDefinitionDto workflow, string? candidate)
|
||||
private static WorkflowNodeDto? FindKnownAgent(
|
||||
IEnumerable<WorkflowNodeDto> agents,
|
||||
string? candidate)
|
||||
{
|
||||
return workflow.GetAgentNodes().FirstOrDefault(agent => MatchesAgent(agent, candidate));
|
||||
return agents.FirstOrDefault(agent => MatchesAgent(agent, candidate));
|
||||
}
|
||||
|
||||
private static AgentIdentity ToAgentIdentity(WorkflowNodeDto agent)
|
||||
=> new(agent.GetAgentId(), agent.GetAgentName());
|
||||
|
||||
private static AgentIdentity CreateFallbackIdentity(string? agentId, string? agentName)
|
||||
private static AgentIdentity ToAgentIdentity(
|
||||
WorkflowNodeDto agent,
|
||||
IReadOnlyDictionary<string, SubworkflowContext> agentSubworkflowIndex)
|
||||
{
|
||||
string resolvedAgentId = !string.IsNullOrWhiteSpace(agentId)
|
||||
? agentId
|
||||
: agentName ?? "agent";
|
||||
string resolvedAgentName = !string.IsNullOrWhiteSpace(agentName)
|
||||
? agentName
|
||||
: resolvedAgentId;
|
||||
string agentId = agent.GetAgentId();
|
||||
return agentSubworkflowIndex.TryGetValue(agentId, out SubworkflowContext subworkflow)
|
||||
? new AgentIdentity(agentId, agent.GetAgentName(), subworkflow)
|
||||
: new AgentIdentity(agentId, agent.GetAgentName());
|
||||
}
|
||||
|
||||
private static AgentIdentity CreateFallbackIdentity(
|
||||
string? agentId,
|
||||
string? agentName,
|
||||
IReadOnlyDictionary<string, SubworkflowContext>? agentSubworkflowIndex)
|
||||
{
|
||||
string resolvedAgentId = NormalizeOptionalString(agentId)
|
||||
?? NormalizeOptionalString(agentName)
|
||||
?? "agent";
|
||||
string resolvedAgentName = NormalizeOptionalString(agentName)
|
||||
?? resolvedAgentId;
|
||||
|
||||
if (agentSubworkflowIndex is not null
|
||||
&& agentSubworkflowIndex.TryGetValue(resolvedAgentId, out SubworkflowContext subworkflow))
|
||||
{
|
||||
return new AgentIdentity(resolvedAgentId, resolvedAgentName, subworkflow);
|
||||
}
|
||||
|
||||
return new AgentIdentity(resolvedAgentId, resolvedAgentName);
|
||||
}
|
||||
|
||||
private static void CollectAgentSubworkflowContexts(
|
||||
WorkflowDefinitionDto workflowDefinition,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
SubworkflowContext? currentSubworkflow,
|
||||
Dictionary<string, SubworkflowContext> index,
|
||||
ISet<string> visitedWorkflowIds,
|
||||
ISet<WorkflowDefinitionDto> visitedAnonymousWorkflows)
|
||||
{
|
||||
string? workflowId = NormalizeOptionalString(workflowDefinition.Id);
|
||||
if (workflowId is not null)
|
||||
{
|
||||
if (!visitedWorkflowIds.Add(workflowId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!visitedAnonymousWorkflows.Add(workflowDefinition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (WorkflowNodeDto node in workflowDefinition.Graph.Nodes)
|
||||
{
|
||||
if (node.IsAgentNode())
|
||||
{
|
||||
if (currentSubworkflow.HasValue)
|
||||
{
|
||||
index[node.GetAgentId()] = currentSubworkflow.Value;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!node.IsSubWorkflowNode())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
WorkflowDefinitionDto subWorkflow = node.ResolveSubWorkflowDefinition(workflowLibrary);
|
||||
CollectAgentSubworkflowContexts(
|
||||
subWorkflow,
|
||||
workflowLibrary,
|
||||
CreateSubworkflowContext(node, workflowLibrary),
|
||||
index,
|
||||
visitedWorkflowIds,
|
||||
visitedAnonymousWorkflows);
|
||||
}
|
||||
}
|
||||
|
||||
private static SubworkflowContext CreateSubworkflowContext(
|
||||
WorkflowNodeDto node,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary)
|
||||
{
|
||||
return new SubworkflowContext(node.Id, node.GetSubworkflowDisplayName(workflowLibrary));
|
||||
}
|
||||
|
||||
private static bool MatchesAgent(WorkflowNodeDto agent, string? candidate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
@@ -164,6 +404,9 @@ internal static class AgentIdentityResolver
|
||||
&& normalizedCandidate.Contains(normalizedName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string? NormalizeOptionalString(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static string NormalizeComparisonKey(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@@ -297,14 +297,21 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
|
||||
{
|
||||
if (evt is ExecutorInvokedEvent invoked)
|
||||
{
|
||||
if (AgentIdentityResolver.TryResolveKnownAgentIdentity(
|
||||
command.Workflow,
|
||||
invoked.ExecutorId,
|
||||
out AgentIdentity invokedAgent))
|
||||
if (state.TryResolveKnownAgentIdentity(invoked.ExecutorId, out AgentIdentity invokedAgent))
|
||||
{
|
||||
TraceHandoff(command, $"Executor invoked: {invoked.ExecutorId} -> {invokedAgent.AgentName} ({invokedAgent.AgentId}).");
|
||||
await state.EmitThinkingIfNeeded(invokedAgent, onEvent).ConfigureAwait(false);
|
||||
}
|
||||
else if (state.TryCreateSubworkflowLifecycleActivity(
|
||||
"subworkflow-started",
|
||||
invoked.ExecutorId,
|
||||
out AgentActivityEventDto subworkflowStarted))
|
||||
{
|
||||
TraceHandoff(
|
||||
command,
|
||||
$"Sub-workflow executor invoked: {invoked.ExecutorId} -> {subworkflowStarted.SubworkflowName ?? subworkflowStarted.SubworkflowNodeId ?? "<unknown>"}.");
|
||||
await EmitActivityAsync(command, state, subworkflowStarted, onEvent).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceHandoff(command, $"Executor invoked without a known agent match: {invoked.ExecutorId}.");
|
||||
@@ -355,16 +362,22 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
|
||||
|
||||
if (evt is ExecutorCompletedEvent completed)
|
||||
{
|
||||
if (AgentIdentityResolver.TryResolveObservedAgentIdentity(
|
||||
command.Workflow,
|
||||
completed.ExecutorId,
|
||||
state.ActiveAgent,
|
||||
out AgentIdentity completedAgent))
|
||||
if (state.TryResolveObservedAgentIdentity(completed.ExecutorId, state.ActiveAgent, out AgentIdentity completedAgent))
|
||||
{
|
||||
TraceHandoff(command, $"Executor completed: {completed.ExecutorId} -> {completedAgent.AgentName} ({completedAgent.AgentId}).");
|
||||
state.QueueCompletedActivity(completedAgent);
|
||||
state.ClearActiveAgentIfMatching(completedAgent);
|
||||
}
|
||||
else if (state.TryCreateSubworkflowLifecycleActivity(
|
||||
"subworkflow-completed",
|
||||
completed.ExecutorId,
|
||||
out AgentActivityEventDto subworkflowCompleted))
|
||||
{
|
||||
TraceHandoff(
|
||||
command,
|
||||
$"Sub-workflow executor completed: {completed.ExecutorId} -> {subworkflowCompleted.SubworkflowName ?? subworkflowCompleted.SubworkflowNodeId ?? "<unknown>"}.");
|
||||
await EmitActivityAsync(command, state, subworkflowCompleted, onEvent).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceHandoff(command, $"Executor completed without a known agent match: {completed.ExecutorId}.");
|
||||
@@ -470,11 +483,10 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
|
||||
updateAgent = observedMessageAgent;
|
||||
authorName = observedMessageAgent.AgentName;
|
||||
}
|
||||
else if (AgentIdentityResolver.TryResolveObservedAgentIdentity(
|
||||
command.Workflow,
|
||||
update.ExecutorId,
|
||||
state.ActiveAgent,
|
||||
out AgentIdentity resolvedUpdateAgent))
|
||||
else if (state.TryResolveObservedAgentIdentity(
|
||||
update.ExecutorId,
|
||||
state.ActiveAgent,
|
||||
out AgentIdentity resolvedUpdateAgent))
|
||||
{
|
||||
updateAgent = resolvedUpdateAgent;
|
||||
authorName = resolvedUpdateAgent.AgentName;
|
||||
@@ -545,11 +557,12 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
|
||||
&& !string.IsNullOrWhiteSpace(activity.AgentId)
|
||||
&& !string.IsNullOrWhiteSpace(activity.AgentName))
|
||||
{
|
||||
AgentIdentity promotedAgent = state.ResolveAgentIdentity(activity.AgentId, activity.AgentName);
|
||||
TraceHandoff(
|
||||
command,
|
||||
$"Promoting handoff target to thinking: {activity.AgentName} ({activity.AgentId}).");
|
||||
$"Promoting handoff target to thinking: {promotedAgent.AgentName} ({promotedAgent.AgentId}).");
|
||||
await state.EmitThinkingIfNeeded(
|
||||
new AgentIdentity(activity.AgentId, activity.AgentName),
|
||||
promotedAgent,
|
||||
onEvent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -593,8 +606,7 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
|
||||
{
|
||||
case ExecutorFailedEvent executorFailed:
|
||||
{
|
||||
AgentIdentity? agent = AgentIdentityResolver.TryResolveObservedAgentIdentity(
|
||||
command.Workflow,
|
||||
AgentIdentity? agent = state.TryResolveObservedAgentIdentity(
|
||||
executorFailed.ExecutorId,
|
||||
state.ActiveAgent,
|
||||
out AgentIdentity resolvedAgent)
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Aryx.AgentHost.Services;
|
||||
internal class TurnExecutionState
|
||||
{
|
||||
private readonly RunTurnCommandDto _command;
|
||||
private readonly IReadOnlyDictionary<string, WorkflowDefinitionDto> _workflowLibrary;
|
||||
private readonly IReadOnlyDictionary<string, SubworkflowContext> _agentSubworkflowIndex;
|
||||
private readonly HashSet<string> _startedAgents = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _reclassifiedMessageIds = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentQueue<SidecarEventDto> _pendingEvents = new();
|
||||
@@ -19,6 +21,8 @@ internal class TurnExecutionState
|
||||
public TurnExecutionState(RunTurnCommandDto command)
|
||||
{
|
||||
_command = command;
|
||||
_workflowLibrary = WorkflowDefinitionExtensions.CreateWorkflowLibraryMap(command.WorkflowLibrary);
|
||||
_agentSubworkflowIndex = AgentIdentityResolver.BuildAgentSubworkflowIndex(command.Workflow, _workflowLibrary);
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<string, string> ToolNamesByCallId { get; } = new(StringComparer.Ordinal);
|
||||
@@ -33,6 +37,59 @@ internal class TurnExecutionState
|
||||
|
||||
public bool SuppressHookLifecycleEvents { get; set; }
|
||||
|
||||
public AgentIdentity ResolveAgentIdentity(string? agentId, string? agentName)
|
||||
{
|
||||
return AgentIdentityResolver.ResolveAgentIdentity(
|
||||
_command.Workflow,
|
||||
_workflowLibrary,
|
||||
agentId,
|
||||
agentName,
|
||||
_agentSubworkflowIndex);
|
||||
}
|
||||
|
||||
public bool TryResolveKnownAgentIdentity(string? agentIdentifier, out AgentIdentity agent)
|
||||
{
|
||||
return AgentIdentityResolver.TryResolveKnownAgentIdentity(
|
||||
_command.Workflow,
|
||||
_workflowLibrary,
|
||||
agentIdentifier,
|
||||
_agentSubworkflowIndex,
|
||||
out agent);
|
||||
}
|
||||
|
||||
public bool TryResolveObservedAgentIdentity(
|
||||
string? agentIdentifier,
|
||||
AgentIdentity? fallbackAgent,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
return AgentIdentityResolver.TryResolveObservedAgentIdentity(
|
||||
_command.Workflow,
|
||||
_workflowLibrary,
|
||||
agentIdentifier,
|
||||
fallbackAgent,
|
||||
_agentSubworkflowIndex,
|
||||
out agent);
|
||||
}
|
||||
|
||||
public bool TryCreateSubworkflowLifecycleActivity(
|
||||
string activityType,
|
||||
string? executorId,
|
||||
out AgentActivityEventDto activity)
|
||||
{
|
||||
activity = default!;
|
||||
if (!AgentIdentityResolver.TryResolveSubworkflowContext(
|
||||
_command.Workflow,
|
||||
_workflowLibrary,
|
||||
executorId,
|
||||
out SubworkflowContext subworkflow))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activity = CreateSubworkflowActivity(activityType, subworkflow);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task EmitThinkingIfNeeded(
|
||||
AgentIdentity agent,
|
||||
Func<SidecarEventDto, Task> onEvent)
|
||||
@@ -67,14 +124,13 @@ internal class TurnExecutionState
|
||||
&& !string.IsNullOrWhiteSpace(activity.AgentId)
|
||||
&& !string.IsNullOrWhiteSpace(activity.AgentName))
|
||||
{
|
||||
ActiveAgent = new AgentIdentity(activity.AgentId, activity.AgentName);
|
||||
ActiveAgent = ResolveAgentIdentity(activity.AgentId, activity.AgentName);
|
||||
}
|
||||
}
|
||||
|
||||
public void ObserveSessionEvent(WorkflowNodeDto agentDefinition, ProviderSessionEvent sessionEvent)
|
||||
{
|
||||
AgentIdentity agent = AgentIdentityResolver.ResolveAgentIdentity(
|
||||
_command.Workflow,
|
||||
AgentIdentity agent = ResolveAgentIdentity(
|
||||
agentDefinition.GetAgentId(),
|
||||
agentDefinition.GetAgentName());
|
||||
|
||||
@@ -313,6 +369,8 @@ internal class TurnExecutionState
|
||||
ActivityType = "thinking",
|
||||
AgentId = agent.AgentId,
|
||||
AgentName = agent.AgentName,
|
||||
SubworkflowNodeId = agent.Subworkflow?.SubworkflowNodeId,
|
||||
SubworkflowName = agent.Subworkflow?.SubworkflowName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -335,6 +393,8 @@ internal class TurnExecutionState
|
||||
ActivityType = "tool-calling",
|
||||
AgentId = agent.AgentId,
|
||||
AgentName = agent.AgentName,
|
||||
SubworkflowNodeId = agent.Subworkflow?.SubworkflowNodeId,
|
||||
SubworkflowName = agent.Subworkflow?.SubworkflowName,
|
||||
ToolName = toolName,
|
||||
ToolCallId = toolCallId,
|
||||
ToolArguments = toolArguments,
|
||||
@@ -351,6 +411,23 @@ internal class TurnExecutionState
|
||||
ActivityType = "completed",
|
||||
AgentId = agent.AgentId,
|
||||
AgentName = agent.AgentName,
|
||||
SubworkflowNodeId = agent.Subworkflow?.SubworkflowNodeId,
|
||||
SubworkflowName = agent.Subworkflow?.SubworkflowName,
|
||||
};
|
||||
}
|
||||
|
||||
private AgentActivityEventDto CreateSubworkflowActivity(
|
||||
string activityType,
|
||||
SubworkflowContext subworkflow)
|
||||
{
|
||||
return new AgentActivityEventDto
|
||||
{
|
||||
Type = "agent-activity",
|
||||
RequestId = _command.RequestId,
|
||||
SessionId = _command.SessionId,
|
||||
ActivityType = activityType,
|
||||
SubworkflowNodeId = subworkflow.SubworkflowNodeId,
|
||||
SubworkflowName = subworkflow.SubworkflowName,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ internal static class WorkflowDefinitionExtensions
|
||||
return string.Equals(node.Kind, "agent", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsSubWorkflowNode(this WorkflowNodeDto node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
return string.Equals(node.Kind, "sub-workflow", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetAgentId(this WorkflowNodeDto node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
@@ -87,6 +93,67 @@ internal static class WorkflowDefinitionExtensions
|
||||
return string.Equals(workflow.Settings.OrchestrationMode, mode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static WorkflowNodeDto? FindSubWorkflowNode(
|
||||
this WorkflowDefinitionDto workflow,
|
||||
string? nodeId,
|
||||
IReadOnlyList<WorkflowDefinitionDto>? workflowLibrary = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
string? normalizedNodeId = NormalizeOptionalString(nodeId);
|
||||
if (normalizedNodeId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindSubWorkflowNode(
|
||||
workflow,
|
||||
normalizedNodeId,
|
||||
CreateWorkflowLibraryMap(workflowLibrary),
|
||||
new HashSet<string>(StringComparer.Ordinal),
|
||||
new HashSet<WorkflowDefinitionDto>(ReferenceEqualityComparer.Instance));
|
||||
}
|
||||
|
||||
internal static WorkflowNodeDto? FindSubWorkflowNode(
|
||||
this WorkflowDefinitionDto workflow,
|
||||
string? nodeId,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto>? workflowLibrary)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
string? normalizedNodeId = NormalizeOptionalString(nodeId);
|
||||
if (normalizedNodeId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindSubWorkflowNode(
|
||||
workflow,
|
||||
normalizedNodeId,
|
||||
workflowLibrary ?? EmptyWorkflowLibrary,
|
||||
new HashSet<string>(StringComparer.Ordinal),
|
||||
new HashSet<WorkflowDefinitionDto>(ReferenceEqualityComparer.Instance));
|
||||
}
|
||||
|
||||
internal static string GetSubworkflowDisplayName(
|
||||
this WorkflowNodeDto node,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto>? workflowLibrary)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
WorkflowDefinitionDto? resolvedWorkflow = null;
|
||||
if (node.Config.InlineWorkflow is not null)
|
||||
{
|
||||
resolvedWorkflow = node.Config.InlineWorkflow;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(node.Config.WorkflowId)
|
||||
&& workflowLibrary is not null
|
||||
&& workflowLibrary.TryGetValue(node.Config.WorkflowId, out WorkflowDefinitionDto? workflow))
|
||||
{
|
||||
resolvedWorkflow = workflow;
|
||||
}
|
||||
|
||||
return FirstNonBlank(node.Label, resolvedWorkflow?.Name, node.Config.WorkflowId, node.Id) ?? "sub-workflow";
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, WorkflowDefinitionDto> EmptyWorkflowLibrary =
|
||||
new Dictionary<string, WorkflowDefinitionDto>(StringComparer.Ordinal);
|
||||
|
||||
@@ -128,7 +195,55 @@ internal static class WorkflowDefinitionExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, WorkflowDefinitionDto> CreateWorkflowLibraryMap(
|
||||
private static WorkflowNodeDto? FindSubWorkflowNode(
|
||||
WorkflowDefinitionDto workflowDefinition,
|
||||
string nodeId,
|
||||
IReadOnlyDictionary<string, WorkflowDefinitionDto> workflowLibrary,
|
||||
ISet<string> visitedWorkflowIds,
|
||||
ISet<WorkflowDefinitionDto> visitedAnonymousWorkflows)
|
||||
{
|
||||
string? workflowId = NormalizeOptionalString(workflowDefinition.Id);
|
||||
if (workflowId is not null)
|
||||
{
|
||||
if (!visitedWorkflowIds.Add(workflowId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (!visitedAnonymousWorkflows.Add(workflowDefinition))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (WorkflowNodeDto node in workflowDefinition.Graph.Nodes)
|
||||
{
|
||||
if (!node.IsSubWorkflowNode())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(node.Id, nodeId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
WorkflowDefinitionDto subWorkflow = node.ResolveSubWorkflowDefinition(workflowLibrary);
|
||||
WorkflowNodeDto? match = FindSubWorkflowNode(
|
||||
subWorkflow,
|
||||
nodeId,
|
||||
workflowLibrary,
|
||||
visitedWorkflowIds,
|
||||
visitedAnonymousWorkflows);
|
||||
if (match is not null)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, WorkflowDefinitionDto> CreateWorkflowLibraryMap(
|
||||
IReadOnlyList<WorkflowDefinitionDto>? workflowLibrary)
|
||||
{
|
||||
return workflowLibrary?
|
||||
|
||||
@@ -23,7 +23,7 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
ConcurrentDictionary<string, string> toolNamesByCallId,
|
||||
ConcurrentDictionary<string, bool> toolCallHasArgumentsById)
|
||||
{
|
||||
RequestInterpretation interpretation = InterpretRequest(command.Workflow, requestInfo);
|
||||
RequestInterpretation interpretation = InterpretRequest(command, requestInfo);
|
||||
return interpretation switch
|
||||
{
|
||||
HandoffRequestInterpretation handoff =>
|
||||
@@ -39,7 +39,7 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
RequestInfoEvent requestInfo)
|
||||
{
|
||||
return command.Workflow.IsOrchestrationMode("handoff")
|
||||
&& InterpretRequest(command.Workflow, requestInfo) is UnknownRequestInterpretation;
|
||||
&& InterpretRequest(command, requestInfo) is UnknownRequestInterpretation;
|
||||
}
|
||||
|
||||
private static AgentActivityEventDto CreateHandoffActivity(
|
||||
@@ -55,6 +55,8 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
ActivityType = HandoffActivityType,
|
||||
AgentId = handoffAgent.AgentId,
|
||||
AgentName = handoffAgent.AgentName,
|
||||
SubworkflowNodeId = handoffAgent.Subworkflow?.SubworkflowNodeId,
|
||||
SubworkflowName = handoffAgent.Subworkflow?.SubworkflowName,
|
||||
SourceAgentId = activeAgent?.AgentId,
|
||||
SourceAgentName = activeAgent?.AgentName,
|
||||
};
|
||||
@@ -88,6 +90,8 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
ActivityType = ToolCallingActivityType,
|
||||
AgentId = activeAgent.AgentId,
|
||||
AgentName = activeAgent.AgentName,
|
||||
SubworkflowNodeId = activeAgent.Subworkflow?.SubworkflowNodeId,
|
||||
SubworkflowName = activeAgent.Subworkflow?.SubworkflowName,
|
||||
ToolName = tool.ToolName,
|
||||
ToolCallId = tool.ToolCallId,
|
||||
ToolArguments = tool.ToolArguments,
|
||||
@@ -109,10 +113,10 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
}
|
||||
|
||||
private static RequestInterpretation InterpretRequest(
|
||||
WorkflowDefinitionDto workflow,
|
||||
RunTurnCommandDto command,
|
||||
RequestInfoEvent requestInfo)
|
||||
{
|
||||
if (TryGetHandoffTarget(workflow, requestInfo, out AgentIdentity handoffAgent))
|
||||
if (TryGetHandoffTarget(command, requestInfo, out AgentIdentity handoffAgent))
|
||||
{
|
||||
return new HandoffRequestInterpretation(handoffAgent);
|
||||
}
|
||||
@@ -123,7 +127,7 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
}
|
||||
|
||||
private static bool TryGetHandoffTarget(
|
||||
WorkflowDefinitionDto workflow,
|
||||
RunTurnCommandDto command,
|
||||
RequestInfoEvent requestInfo,
|
||||
out AgentIdentity agent)
|
||||
{
|
||||
@@ -142,7 +146,8 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
}
|
||||
|
||||
agent = AgentIdentityResolver.ResolveAgentIdentity(
|
||||
workflow,
|
||||
command.Workflow,
|
||||
command.WorkflowLibrary,
|
||||
target.Id,
|
||||
target.Name);
|
||||
return !string.IsNullOrWhiteSpace(agent.AgentName);
|
||||
|
||||
Reference in New Issue
Block a user