mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 13:38:43 +02:00
fix: align orchestration behavior with agent framework docs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+4
-2
@@ -92,10 +92,10 @@ sequenceDiagram
|
||||
M->>M: Create run record and mark session running
|
||||
M->>S: run-turn command
|
||||
S->>C: Execute workflow
|
||||
C-->>S: Partial output / tool activity / handoffs
|
||||
C-->>S: Partial output / tool activity / handoffs / input requests
|
||||
S-->>M: Stream deltas and activity events
|
||||
M-->>R: Push session events and workspace updates
|
||||
C-->>S: Final messages
|
||||
C-->>S: Final messages or turn boundary
|
||||
S-->>M: Completion or error
|
||||
M->>M: Finalize run and persist state
|
||||
M-->>R: Final workspace snapshot
|
||||
@@ -134,6 +134,8 @@ Patterns describe how agents collaborate. The architecture supports:
|
||||
- handoff flows
|
||||
- group chat style collaboration
|
||||
|
||||
Their runtime semantics follow the Agent Framework orchestration model: sequential and group chat preserve a visible shared conversation, concurrent aggregates multiple independent responses into one turn, and handoff turns can end once the active agent has responded and is waiting for the next user input.
|
||||
|
||||
Patterns are shared application data, not renderer-only configuration. That means the same pattern definition can drive validation, persistence, UI rendering, and sidecar execution.
|
||||
|
||||
### Sessions
|
||||
|
||||
@@ -36,10 +36,10 @@ Add a local folder when you want help that is grounded in your work. Eryx is des
|
||||
Eryx supports several ways of working:
|
||||
|
||||
- **Single** for direct one-agent help
|
||||
- **Sequential** for step-by-step specialist workflows
|
||||
- **Concurrent** for parallel exploration
|
||||
- **Handoff** for agent-to-agent delegation
|
||||
- **Group chat** for collaborative multi-agent discussion
|
||||
- **Sequential** for pipeline-style work where each agent sees the full conversation and appends its contribution
|
||||
- **Concurrent** for parallel exploration where the final turn aggregates multiple independent responses
|
||||
- **Handoff** for agent-to-agent delegation, with the next user turn continuing when a specialist needs more input
|
||||
- **Group chat** for round-robin collaborative refinement across multiple agent turns
|
||||
|
||||
### Add global MCPs and LSPs
|
||||
|
||||
|
||||
@@ -47,8 +47,12 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await HandleWorkflowEventAsync(command, evt, inputMessages, state, onDelta, onActivity)
|
||||
bool shouldEndTurn = await HandleWorkflowEventAsync(command, evt, inputMessages, state, onDelta, onActivity)
|
||||
.ConfigureAwait(false);
|
||||
if (shouldEndTurn)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return state.FinalizeCompletedMessages();
|
||||
@@ -61,7 +65,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
return _approvalCoordinator.ResolveApprovalAsync(command, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task HandleWorkflowEventAsync(
|
||||
private static async Task<bool> HandleWorkflowEventAsync(
|
||||
RunTurnCommandDto command,
|
||||
WorkflowEvent evt,
|
||||
IReadOnlyList<ChatMessage> inputMessages,
|
||||
@@ -76,7 +80,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
out AgentIdentity invokedAgent))
|
||||
{
|
||||
await state.EmitThinkingIfNeeded(invokedAgent, onActivity).ConfigureAwait(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evt is RequestInfoEvent requestInfo)
|
||||
@@ -89,18 +93,18 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
|
||||
if (activity is null)
|
||||
{
|
||||
return;
|
||||
return WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(command, requestInfo);
|
||||
}
|
||||
|
||||
state.ApplyActivity(activity);
|
||||
await onActivity(activity).ConfigureAwait(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evt is AgentResponseUpdateEvent update)
|
||||
{
|
||||
await HandleAgentResponseUpdateAsync(command, update, state, onDelta, onActivity).ConfigureAwait(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evt is ExecutorCompletedEvent completed
|
||||
@@ -111,7 +115,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
out AgentIdentity completedAgent))
|
||||
{
|
||||
state.ClearActiveAgentIfMatching(completedAgent);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evt is WorkflowOutputEvent outputEvent)
|
||||
@@ -119,6 +123,8 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
||||
state.UpdateCompletedMessages(allMessages, inputMessages);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task HandleAgentResponseUpdateAsync(
|
||||
|
||||
@@ -52,6 +52,23 @@ internal static class WorkflowRequestInfoInterpreter
|
||||
};
|
||||
}
|
||||
|
||||
public static bool RequiresUserInputTurnBoundary(
|
||||
RunTurnCommandDto command,
|
||||
RequestInfoEvent requestInfo)
|
||||
{
|
||||
if (!string.Equals(command.Pattern.Mode, "handoff", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryGetHandoffTarget(command.Pattern, requestInfo, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !TryGetToolRequestInfo(requestInfo, out _, out _);
|
||||
}
|
||||
|
||||
private static bool TryGetHandoffTarget(
|
||||
PatternDefinitionDto pattern,
|
||||
RequestInfoEvent requestInfo,
|
||||
|
||||
@@ -156,6 +156,143 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Equal("Hello", message.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCompletedMessages_PreservesSequentialConversationHistory()
|
||||
{
|
||||
RunTurnCommandDto command = new()
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "pattern-sequential",
|
||||
Name = "Sequential Trio Review",
|
||||
Mode = "sequential",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
CreateAgent(id: "agent-sequential-analyst", name: "Analyst"),
|
||||
CreateAgent(id: "agent-sequential-builder", name: "Builder"),
|
||||
CreateAgent(id: "agent-sequential-reviewer", name: "Reviewer"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
IReadOnlyList<ChatMessageDto> messages = WorkflowTranscriptProjector.ProjectCompletedMessages(
|
||||
command,
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Plan the approach.") { AuthorName = "Analyst" },
|
||||
new ChatMessage(ChatRole.Assistant, "Implement the change.") { AuthorName = "Builder" },
|
||||
new ChatMessage(ChatRole.Assistant, "Review the implementation.") { AuthorName = "Reviewer" },
|
||||
],
|
||||
[]);
|
||||
|
||||
Assert.Collection(
|
||||
messages,
|
||||
analyst =>
|
||||
{
|
||||
Assert.Equal("Analyst", analyst.AuthorName);
|
||||
Assert.Equal("Plan the approach.", analyst.Content);
|
||||
},
|
||||
builder =>
|
||||
{
|
||||
Assert.Equal("Builder", builder.AuthorName);
|
||||
Assert.Equal("Implement the change.", builder.Content);
|
||||
},
|
||||
reviewer =>
|
||||
{
|
||||
Assert.Equal("Reviewer", reviewer.AuthorName);
|
||||
Assert.Equal("Review the implementation.", reviewer.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCompletedMessages_PreservesConcurrentAggregatedResponses()
|
||||
{
|
||||
RunTurnCommandDto command = new()
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "pattern-concurrent",
|
||||
Name = "Concurrent Brainstorm",
|
||||
Mode = "concurrent",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
CreateAgent(id: "agent-concurrent-architect", name: "Architect"),
|
||||
CreateAgent(id: "agent-concurrent-product", name: "Product"),
|
||||
CreateAgent(id: "agent-concurrent-implementer", name: "Implementer"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
IReadOnlyList<ChatMessageDto> messages = WorkflowTranscriptProjector.ProjectCompletedMessages(
|
||||
command,
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Architecture concerns.") { AuthorName = "Architect" },
|
||||
new ChatMessage(ChatRole.Assistant, "Product trade-offs.") { AuthorName = "Product" },
|
||||
new ChatMessage(ChatRole.Assistant, "Implementation details.") { AuthorName = "Implementer" },
|
||||
],
|
||||
[]);
|
||||
|
||||
Assert.Collection(
|
||||
messages,
|
||||
architect => Assert.Equal("Architect", architect.AuthorName),
|
||||
product => Assert.Equal("Product", product.AuthorName),
|
||||
implementer => Assert.Equal("Implementer", implementer.AuthorName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCompletedMessages_PreservesGroupChatConversationHistory()
|
||||
{
|
||||
RunTurnCommandDto command = new()
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "pattern-group-chat",
|
||||
Name = "Collaborative Group Chat",
|
||||
Mode = "group-chat",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
CreateAgent(id: "agent-group-writer", name: "Writer"),
|
||||
CreateAgent(id: "agent-group-reviewer", name: "Reviewer"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
IReadOnlyList<ChatMessageDto> messages = WorkflowTranscriptProjector.ProjectCompletedMessages(
|
||||
command,
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Initial draft.") { AuthorName = "Writer" },
|
||||
new ChatMessage(ChatRole.Assistant, "Needs clearer examples.") { AuthorName = "Reviewer" },
|
||||
new ChatMessage(ChatRole.Assistant, "Revised draft with examples.") { AuthorName = "Writer" },
|
||||
],
|
||||
[]);
|
||||
|
||||
Assert.Collection(
|
||||
messages,
|
||||
writerDraft =>
|
||||
{
|
||||
Assert.Equal("Writer", writerDraft.AuthorName);
|
||||
Assert.Equal("Initial draft.", writerDraft.Content);
|
||||
},
|
||||
reviewer =>
|
||||
{
|
||||
Assert.Equal("Reviewer", reviewer.AuthorName);
|
||||
Assert.Equal("Needs clearer examples.", reviewer.Content);
|
||||
},
|
||||
writerRevision =>
|
||||
{
|
||||
Assert.Equal("Writer", writerRevision.AuthorName);
|
||||
Assert.Equal("Revised draft with examples.", writerRevision.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectCompletedMessages_UsesFallbackAgentForGenericAssistantOutput()
|
||||
{
|
||||
|
||||
@@ -109,6 +109,62 @@ public sealed class WorkflowRequestInfoInterpreterTests
|
||||
Assert.Empty(toolNamesByCallId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresUserInputTurnBoundary_ReturnsTrueForUnhandledHandoffRequests()
|
||||
{
|
||||
RequestInfoEvent requestInfo = CreateRequestInfoEvent(new
|
||||
{
|
||||
Prompt = "Please provide more detail.",
|
||||
});
|
||||
|
||||
bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(
|
||||
CreateHandoffCommand(),
|
||||
requestInfo);
|
||||
|
||||
Assert.True(requiresBoundary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresUserInputTurnBoundary_ReturnsFalseForExplicitHandoffs()
|
||||
{
|
||||
RequestInfoEvent requestInfo = CreateRequestInfoEvent(
|
||||
CreateHandoffTarget("agent-handoff-ux", "UX Specialist"));
|
||||
|
||||
bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(
|
||||
CreateHandoffCommand(),
|
||||
requestInfo);
|
||||
|
||||
Assert.False(requiresBoundary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresUserInputTurnBoundary_ReturnsFalseForToolRequests()
|
||||
{
|
||||
RequestInfoEvent requestInfo = CreateRequestInfoEvent(
|
||||
new FunctionCallContent("call-1", "view", new Dictionary<string, object?>()));
|
||||
|
||||
bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(
|
||||
CreateHandoffCommand(),
|
||||
requestInfo);
|
||||
|
||||
Assert.False(requiresBoundary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresUserInputTurnBoundary_ReturnsFalseOutsideHandoffMode()
|
||||
{
|
||||
RequestInfoEvent requestInfo = CreateRequestInfoEvent(new
|
||||
{
|
||||
Prompt = "Please provide more detail.",
|
||||
});
|
||||
|
||||
bool requiresBoundary = WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(
|
||||
CreateSingleAgentCommand(),
|
||||
requestInfo);
|
||||
|
||||
Assert.False(requiresBoundary);
|
||||
}
|
||||
|
||||
private static RunTurnCommandDto CreateSingleAgentCommand()
|
||||
{
|
||||
return new RunTurnCommandDto
|
||||
|
||||
@@ -90,7 +90,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
||||
{
|
||||
id: 'pattern-sequential-review',
|
||||
name: 'Sequential Trio Review',
|
||||
description: 'Three Copilot-backed agents execute in order, refining the answer each step.',
|
||||
description: 'Agents execute in order, each seeing the full conversation and appending to a shared transcript.',
|
||||
mode: 'sequential',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
@@ -126,7 +126,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
||||
{
|
||||
id: 'pattern-concurrent-brainstorm',
|
||||
name: 'Concurrent Brainstorm',
|
||||
description: 'Multiple agents respond in parallel for comparison or voting.',
|
||||
description: 'Agents work independently in parallel and the final conversation aggregates every response.',
|
||||
mode: 'concurrent',
|
||||
availability: 'available',
|
||||
maxIterations: 1,
|
||||
@@ -162,7 +162,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
||||
{
|
||||
id: 'pattern-handoff-support',
|
||||
name: 'Handoff Support Flow',
|
||||
description: 'A triage agent routes the task to specialists and can reclaim control.',
|
||||
description: 'A triage agent routes work to specialists, and the next user turn continues when more input is needed.',
|
||||
mode: 'handoff',
|
||||
availability: 'available',
|
||||
maxIterations: 4,
|
||||
@@ -201,7 +201,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
||||
{
|
||||
id: 'pattern-group-chat',
|
||||
name: 'Collaborative Group Chat',
|
||||
description: 'Two or more agents iterate together under a round-robin manager.',
|
||||
description: 'Agents take turns under a round-robin manager, iteratively refining a shared conversation.',
|
||||
mode: 'group-chat',
|
||||
availability: 'available',
|
||||
maxIterations: 5,
|
||||
|
||||
Reference in New Issue
Block a user