Files
aryx/src/shared/domain/pattern.ts
T
David KayaandCopilot 794722673b fix: strengthen group chat collaboration guidance
Audit confirmed that single, sequential, concurrent, and handoff are mapped to distinct workflow builders as intended. Group-chat was also using the correct round-robin manager, but the agents only had one-shot role prompts, so the reviewer could behave like a fresh assistant instead of refining the draft in progress. Add explicit runtime guidance for group-chat participants plus clearer built-in Writer/Reviewer instructions and regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 23:22:09 +01:00

329 lines
11 KiB
TypeScript

import type { ChatMessageRecord } from '@shared/domain/session';
export type OrchestrationMode =
| 'single'
| 'sequential'
| 'concurrent'
| 'handoff'
| 'group-chat'
| 'magentic';
export type PatternAvailability = 'available' | 'preview' | 'unavailable';
export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
export const reasoningEffortOptions: ReadonlyArray<{ value: ReasoningEffort; label: string }> = [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'xhigh', label: 'Maximum' },
];
export interface PatternAgentDefinition {
id: string;
name: string;
description: string;
instructions: string;
model: string;
reasoningEffort?: ReasoningEffort;
}
export interface PatternDefinition {
id: string;
name: string;
description: string;
isFavorite?: boolean;
mode: OrchestrationMode;
availability: PatternAvailability;
unavailabilityReason?: string;
maxIterations: number;
agents: PatternAgentDefinition[];
createdAt: string;
updatedAt: string;
}
export interface PatternValidationIssue {
level: 'error' | 'warning';
field?: string;
message: string;
}
const defaultModels = {
claude: 'claude-opus-4.5',
gpt54: 'gpt-5.4',
gpt53: 'gpt-5.3-codex',
} as const;
const reasoningEffortSet = new Set<ReasoningEffort>(reasoningEffortOptions.map((option) => option.value));
export function isReasoningEffort(value: string | undefined): value is ReasoningEffort {
return value !== undefined && reasoningEffortSet.has(value as ReasoningEffort);
}
export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
return [
{
id: 'pattern-single-chat',
name: '1-on-1 Copilot Chat',
description: 'Direct human-agent conversation for a selected project.',
mode: 'single',
availability: 'available',
maxIterations: 1,
agents: [
{
id: 'agent-single-primary',
name: 'Primary Agent',
description: 'General-purpose project assistant.',
instructions: 'You are a helpful coding assistant working inside the selected project.',
model: defaultModels.gpt54,
reasoningEffort: 'high',
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
{
id: 'pattern-sequential-review',
name: 'Sequential Trio Review',
description: 'Three Copilot-backed agents execute in order, refining the answer each step.',
mode: 'sequential',
availability: 'available',
maxIterations: 1,
agents: [
{
id: 'agent-sequential-analyst',
name: 'Analyst',
description: 'Breaks the task down and captures risks.',
instructions: 'Analyze the request, identify constraints, and produce a short working plan.',
model: defaultModels.gpt54,
reasoningEffort: 'high',
},
{
id: 'agent-sequential-builder',
name: 'Builder',
description: 'Translates the plan into a practical implementation.',
instructions: 'Use the prior context to propose a concrete implementation.',
model: defaultModels.gpt53,
reasoningEffort: 'medium',
},
{
id: 'agent-sequential-reviewer',
name: 'Reviewer',
description: 'Checks the proposal for gaps and edge cases.',
instructions: 'Review the previous answer, tighten it, and call out any missing edge cases.',
model: defaultModels.claude,
reasoningEffort: 'medium',
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
{
id: 'pattern-concurrent-brainstorm',
name: 'Concurrent Brainstorm',
description: 'Multiple agents respond in parallel for comparison or voting.',
mode: 'concurrent',
availability: 'available',
maxIterations: 1,
agents: [
{
id: 'agent-concurrent-architect',
name: 'Architect',
description: 'Focuses on architecture and boundaries.',
instructions: 'Answer from an architecture-first perspective.',
model: defaultModels.gpt54,
reasoningEffort: 'high',
},
{
id: 'agent-concurrent-product',
name: 'Product',
description: 'Focuses on UX and scope.',
instructions: 'Answer from a product and UX perspective.',
model: defaultModels.claude,
reasoningEffort: 'medium',
},
{
id: 'agent-concurrent-implementer',
name: 'Implementer',
description: 'Focuses on practical delivery.',
instructions: 'Answer from an implementation and testing perspective.',
model: defaultModels.gpt53,
reasoningEffort: 'medium',
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
{
id: 'pattern-handoff-support',
name: 'Handoff Support Flow',
description: 'A triage agent routes the task to specialists and can reclaim control.',
mode: 'handoff',
availability: 'available',
maxIterations: 4,
agents: [
{
id: 'agent-handoff-triage',
name: 'Triage',
description: 'Routes the request to the right specialist.',
instructions:
'You triage requests and must hand them off to the most appropriate specialist. Do not do the specialist work yourself once the right owner is clear.',
model: defaultModels.gpt54,
reasoningEffort: 'medium',
},
{
id: 'agent-handoff-ux',
name: 'UX Specialist',
description: 'Handles user experience questions.',
instructions:
'You focus on navigation, UX, and interaction details. Once triage hands work to you, you own the substantive answer.',
model: defaultModels.claude,
reasoningEffort: 'medium',
},
{
id: 'agent-handoff-runtime',
name: 'Runtime Specialist',
description: 'Handles backend and execution details.',
instructions:
'You focus on runtime, orchestration, and backend integration details. Once triage hands work to you, you own the substantive answer.',
model: defaultModels.gpt53,
reasoningEffort: 'medium',
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
{
id: 'pattern-group-chat',
name: 'Collaborative Group Chat',
description: 'Two or more agents iterate together under a round-robin manager.',
mode: 'group-chat',
availability: 'available',
maxIterations: 5,
agents: [
{
id: 'agent-group-writer',
name: 'Writer',
description: 'Produces candidate answers.',
instructions:
'You draft a concise, useful answer for the task. On later turns, refine your earlier draft based on peer feedback rather than restarting.',
model: defaultModels.gpt54,
reasoningEffort: 'medium',
},
{
id: 'agent-group-reviewer',
name: 'Reviewer',
description: 'Critiques and refines the answer.',
instructions:
'You review the latest draft and offer specific improvements. Focus on critique and refinement instead of restarting the conversation.',
model: defaultModels.claude,
reasoningEffort: 'medium',
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
{
id: 'pattern-magentic',
name: 'Magentic Planning',
description: 'Reserved for future .NET support when Magentic becomes available in C#.',
mode: 'magentic',
availability: 'unavailable',
unavailabilityReason: 'Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.',
maxIterations: 0,
agents: [
{
id: 'agent-magentic-manager',
name: 'Manager',
description: 'Future manager agent.',
instructions: 'Reserved until the .NET runtime supports Magentic orchestration.',
model: defaultModels.gpt54,
},
{
id: 'agent-magentic-specialist',
name: 'Specialist',
description: 'Future specialist agent.',
instructions: 'Reserved until the .NET runtime supports Magentic orchestration.',
model: defaultModels.claude,
},
],
createdAt: timestamp,
updatedAt: timestamp,
},
];
}
export function validatePatternDefinition(pattern: PatternDefinition): PatternValidationIssue[] {
const issues: PatternValidationIssue[] = [];
if (!pattern.name.trim()) {
issues.push({ level: 'error', field: 'name', message: 'Pattern name is required.' });
}
if (pattern.availability === 'unavailable') {
issues.push({
level: 'error',
field: 'availability',
message: pattern.unavailabilityReason ?? 'This orchestration mode is currently unavailable.',
});
}
if (pattern.agents.length === 0) {
issues.push({ level: 'error', field: 'agents', message: 'At least one agent is required.' });
}
if (pattern.mode === 'single' && pattern.agents.length !== 1) {
issues.push({ level: 'error', field: 'agents', message: 'Single-agent chat requires exactly one agent.' });
}
if (pattern.mode === 'handoff' && pattern.agents.length < 2) {
issues.push({ level: 'error', field: 'agents', message: 'Handoff orchestration requires at least two agents.' });
}
if (pattern.mode === 'group-chat' && pattern.agents.length < 2) {
issues.push({ level: 'error', field: 'agents', message: 'Group chat requires at least two agents.' });
}
if (pattern.mode === 'magentic') {
issues.push({
level: 'error',
field: 'mode',
message:
pattern.unavailabilityReason ??
'Magentic orchestration is currently documented as unsupported in the .NET Agent Framework.',
});
}
for (const agent of pattern.agents) {
if (!agent.name.trim()) {
issues.push({ level: 'error', field: 'agents.name', message: 'Every agent needs a name.' });
}
if (!agent.instructions.trim()) {
issues.push({
level: 'warning',
field: 'agents.instructions',
message: `Agent "${agent.name || agent.id}" should have instructions.`,
});
}
if (!agent.model.trim()) {
issues.push({
level: 'error',
field: 'agents.model',
message: `Agent "${agent.name || agent.id}" requires a model identifier.`,
});
}
}
return issues;
}
export function buildSessionTitle(pattern: PatternDefinition, messages: ChatMessageRecord[]): string {
const firstUserMessage = messages.find((message) => message.role === 'user');
if (!firstUserMessage) {
return pattern.name;
}
return firstUserMessage.content.slice(0, 48).trim() || pattern.name;
}