mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-28 05:43:57 +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->>M: Create run record and mark session running
|
||||||
M->>S: run-turn command
|
M->>S: run-turn command
|
||||||
S->>C: Execute workflow
|
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
|
S-->>M: Stream deltas and activity events
|
||||||
M-->>R: Push session events and workspace updates
|
M-->>R: Push session events and workspace updates
|
||||||
C-->>S: Final messages
|
C-->>S: Final messages or turn boundary
|
||||||
S-->>M: Completion or error
|
S-->>M: Completion or error
|
||||||
M->>M: Finalize run and persist state
|
M->>M: Finalize run and persist state
|
||||||
M-->>R: Final workspace snapshot
|
M-->>R: Final workspace snapshot
|
||||||
@@ -134,6 +134,8 @@ Patterns describe how agents collaborate. The architecture supports:
|
|||||||
- handoff flows
|
- handoff flows
|
||||||
- group chat style collaboration
|
- 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.
|
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
|
### 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:
|
Eryx supports several ways of working:
|
||||||
|
|
||||||
- **Single** for direct one-agent help
|
- **Single** for direct one-agent help
|
||||||
- **Sequential** for step-by-step specialist workflows
|
- **Sequential** for pipeline-style work where each agent sees the full conversation and appends its contribution
|
||||||
- **Concurrent** for parallel exploration
|
- **Concurrent** for parallel exploration where the final turn aggregates multiple independent responses
|
||||||
- **Handoff** for agent-to-agent delegation
|
- **Handoff** for agent-to-agent delegation, with the next user turn continuing when a specialist needs more input
|
||||||
- **Group chat** for collaborative multi-agent discussion
|
- **Group chat** for round-robin collaborative refinement across multiple agent turns
|
||||||
|
|
||||||
### Add global MCPs and LSPs
|
### 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 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);
|
.ConfigureAwait(false);
|
||||||
|
if (shouldEndTurn)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.FinalizeCompletedMessages();
|
return state.FinalizeCompletedMessages();
|
||||||
@@ -61,7 +65,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
return _approvalCoordinator.ResolveApprovalAsync(command, cancellationToken);
|
return _approvalCoordinator.ResolveApprovalAsync(command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleWorkflowEventAsync(
|
private static async Task<bool> HandleWorkflowEventAsync(
|
||||||
RunTurnCommandDto command,
|
RunTurnCommandDto command,
|
||||||
WorkflowEvent evt,
|
WorkflowEvent evt,
|
||||||
IReadOnlyList<ChatMessage> inputMessages,
|
IReadOnlyList<ChatMessage> inputMessages,
|
||||||
@@ -76,7 +80,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
out AgentIdentity invokedAgent))
|
out AgentIdentity invokedAgent))
|
||||||
{
|
{
|
||||||
await state.EmitThinkingIfNeeded(invokedAgent, onActivity).ConfigureAwait(false);
|
await state.EmitThinkingIfNeeded(invokedAgent, onActivity).ConfigureAwait(false);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt is RequestInfoEvent requestInfo)
|
if (evt is RequestInfoEvent requestInfo)
|
||||||
@@ -89,18 +93,18 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
|
|
||||||
if (activity is null)
|
if (activity is null)
|
||||||
{
|
{
|
||||||
return;
|
return WorkflowRequestInfoInterpreter.RequiresUserInputTurnBoundary(command, requestInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ApplyActivity(activity);
|
state.ApplyActivity(activity);
|
||||||
await onActivity(activity).ConfigureAwait(false);
|
await onActivity(activity).ConfigureAwait(false);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt is AgentResponseUpdateEvent update)
|
if (evt is AgentResponseUpdateEvent update)
|
||||||
{
|
{
|
||||||
await HandleAgentResponseUpdateAsync(command, update, state, onDelta, onActivity).ConfigureAwait(false);
|
await HandleAgentResponseUpdateAsync(command, update, state, onDelta, onActivity).ConfigureAwait(false);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt is ExecutorCompletedEvent completed
|
if (evt is ExecutorCompletedEvent completed
|
||||||
@@ -111,7 +115,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
out AgentIdentity completedAgent))
|
out AgentIdentity completedAgent))
|
||||||
{
|
{
|
||||||
state.ClearActiveAgentIfMatching(completedAgent);
|
state.ClearActiveAgentIfMatching(completedAgent);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt is WorkflowOutputEvent outputEvent)
|
if (evt is WorkflowOutputEvent outputEvent)
|
||||||
@@ -119,6 +123,8 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
||||||
state.UpdateCompletedMessages(allMessages, inputMessages);
|
state.UpdateCompletedMessages(allMessages, inputMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleAgentResponseUpdateAsync(
|
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(
|
private static bool TryGetHandoffTarget(
|
||||||
PatternDefinitionDto pattern,
|
PatternDefinitionDto pattern,
|
||||||
RequestInfoEvent requestInfo,
|
RequestInfoEvent requestInfo,
|
||||||
|
|||||||
@@ -156,6 +156,143 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
Assert.Equal("Hello", message.Content);
|
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]
|
[Fact]
|
||||||
public void ProjectCompletedMessages_UsesFallbackAgentForGenericAssistantOutput()
|
public void ProjectCompletedMessages_UsesFallbackAgentForGenericAssistantOutput()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,6 +109,62 @@ public sealed class WorkflowRequestInfoInterpreterTests
|
|||||||
Assert.Empty(toolNamesByCallId);
|
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()
|
private static RunTurnCommandDto CreateSingleAgentCommand()
|
||||||
{
|
{
|
||||||
return new RunTurnCommandDto
|
return new RunTurnCommandDto
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
|||||||
{
|
{
|
||||||
id: 'pattern-sequential-review',
|
id: 'pattern-sequential-review',
|
||||||
name: 'Sequential Trio 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',
|
mode: 'sequential',
|
||||||
availability: 'available',
|
availability: 'available',
|
||||||
maxIterations: 1,
|
maxIterations: 1,
|
||||||
@@ -126,7 +126,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
|||||||
{
|
{
|
||||||
id: 'pattern-concurrent-brainstorm',
|
id: 'pattern-concurrent-brainstorm',
|
||||||
name: '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',
|
mode: 'concurrent',
|
||||||
availability: 'available',
|
availability: 'available',
|
||||||
maxIterations: 1,
|
maxIterations: 1,
|
||||||
@@ -162,7 +162,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
|||||||
{
|
{
|
||||||
id: 'pattern-handoff-support',
|
id: 'pattern-handoff-support',
|
||||||
name: 'Handoff Support Flow',
|
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',
|
mode: 'handoff',
|
||||||
availability: 'available',
|
availability: 'available',
|
||||||
maxIterations: 4,
|
maxIterations: 4,
|
||||||
@@ -201,7 +201,7 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
|||||||
{
|
{
|
||||||
id: 'pattern-group-chat',
|
id: 'pattern-group-chat',
|
||||||
name: 'Collaborative 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',
|
mode: 'group-chat',
|
||||||
availability: 'available',
|
availability: 'available',
|
||||||
maxIterations: 5,
|
maxIterations: 5,
|
||||||
|
|||||||
Reference in New Issue
Block a user