mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-27 06:58:44 +02:00
feat: scaffold electron orchestrator foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export const ipcChannels = {
|
||||
loadWorkspace: 'workspace:load',
|
||||
addProject: 'workspace:add-project',
|
||||
removeProject: 'workspace:remove-project',
|
||||
savePattern: 'patterns:save',
|
||||
deletePattern: 'patterns:delete',
|
||||
createSession: 'sessions:create',
|
||||
sendSessionMessage: 'sessions:send-message',
|
||||
selectProject: 'selection:project',
|
||||
selectPattern: 'selection:pattern',
|
||||
selectSession: 'selection:session',
|
||||
workspaceUpdated: 'workspace:updated',
|
||||
sessionEvent: 'sessions:event',
|
||||
} as const;
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
export interface CreateSessionInput {
|
||||
projectId: string;
|
||||
patternId: string;
|
||||
}
|
||||
|
||||
export interface SavePatternInput {
|
||||
pattern: PatternDefinition;
|
||||
}
|
||||
|
||||
export interface SendSessionMessageInput {
|
||||
sessionId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ElectronApi {
|
||||
loadWorkspace(): Promise<WorkspaceState>;
|
||||
addProject(): Promise<WorkspaceState>;
|
||||
removeProject(projectId: string): Promise<WorkspaceState>;
|
||||
savePattern(input: SavePatternInput): Promise<WorkspaceState>;
|
||||
deletePattern(patternId: string): Promise<WorkspaceState>;
|
||||
createSession(input: CreateSessionInput): Promise<WorkspaceState>;
|
||||
sendSessionMessage(input: SendSessionMessageInput): Promise<void>;
|
||||
selectProject(projectId?: string): Promise<WorkspaceState>;
|
||||
selectPattern(patternId?: string): Promise<WorkspaceState>;
|
||||
selectSession(sessionId?: string): Promise<WorkspaceState>;
|
||||
onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void;
|
||||
onSessionEvent(listener: (event: SessionEventRecord) => void): () => void;
|
||||
}
|
||||
|
||||
export interface RendererSelectionState {
|
||||
selectedProject?: ProjectRecord;
|
||||
selectedPattern?: PatternDefinition;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { PatternDefinition, PatternValidationIssue } from '@shared/domain/pattern';
|
||||
import type { ChatMessageRecord } from '@shared/domain/session';
|
||||
|
||||
export interface SidecarModeCapability {
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface SidecarCapabilities {
|
||||
runtime: 'dotnet-maf';
|
||||
modes: Record<PatternDefinition['mode'], SidecarModeCapability>;
|
||||
}
|
||||
|
||||
export interface DescribeCapabilitiesCommand {
|
||||
type: 'describe-capabilities';
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface ValidatePatternCommand {
|
||||
type: 'validate-pattern';
|
||||
requestId: string;
|
||||
pattern: PatternDefinition;
|
||||
}
|
||||
|
||||
export interface RunTurnCommand {
|
||||
type: 'run-turn';
|
||||
requestId: string;
|
||||
sessionId: string;
|
||||
projectPath: string;
|
||||
pattern: PatternDefinition;
|
||||
messages: ChatMessageRecord[];
|
||||
}
|
||||
|
||||
export type SidecarCommand = DescribeCapabilitiesCommand | ValidatePatternCommand | RunTurnCommand;
|
||||
|
||||
export interface CapabilitiesEvent {
|
||||
type: 'capabilities';
|
||||
requestId: string;
|
||||
capabilities: SidecarCapabilities;
|
||||
}
|
||||
|
||||
export interface PatternValidationEvent {
|
||||
type: 'pattern-validation';
|
||||
requestId: string;
|
||||
issues: PatternValidationIssue[];
|
||||
}
|
||||
|
||||
export interface TurnDeltaEvent {
|
||||
type: 'turn-delta';
|
||||
requestId: string;
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
authorName: string;
|
||||
contentDelta: string;
|
||||
}
|
||||
|
||||
export interface TurnCompleteEvent {
|
||||
type: 'turn-complete';
|
||||
requestId: string;
|
||||
sessionId: string;
|
||||
messages: ChatMessageRecord[];
|
||||
}
|
||||
|
||||
export interface CommandErrorEvent {
|
||||
type: 'command-error';
|
||||
requestId: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CommandCompleteEvent {
|
||||
type: 'command-complete';
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export type SidecarEvent =
|
||||
| CapabilitiesEvent
|
||||
| PatternValidationEvent
|
||||
| TurnDeltaEvent
|
||||
| TurnCompleteEvent
|
||||
| CommandErrorEvent
|
||||
| CommandCompleteEvent;
|
||||
@@ -0,0 +1,12 @@
|
||||
export type SessionEventKind = 'status' | 'message-delta' | 'message-complete' | 'error';
|
||||
|
||||
export interface SessionEventRecord {
|
||||
sessionId: string;
|
||||
kind: SessionEventKind;
|
||||
occurredAt: string;
|
||||
status?: 'idle' | 'running' | 'error';
|
||||
messageId?: string;
|
||||
authorName?: string;
|
||||
contentDelta?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
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 interface PatternAgentDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
model: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
export interface PatternDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
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;
|
||||
|
||||
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.',
|
||||
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.',
|
||||
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.',
|
||||
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.',
|
||||
model: defaultModels.gpt54,
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
{
|
||||
id: 'agent-group-reviewer',
|
||||
name: 'Reviewer',
|
||||
description: 'Critiques and refines the answer.',
|
||||
instructions: 'You review the latest answer and improve it when needed.',
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ProjectRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
addedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export type ChatRole = 'system' | 'user' | 'assistant';
|
||||
export type SessionStatus = 'idle' | 'running' | 'error';
|
||||
|
||||
export interface ChatMessageRecord {
|
||||
id: string;
|
||||
role: ChatRole;
|
||||
authorName: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
pending?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionRecord {
|
||||
id: string;
|
||||
projectId: string;
|
||||
patternId: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: SessionStatus;
|
||||
messages: ChatMessageRecord[];
|
||||
lastError?: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import { createBuiltinPatterns } from '@shared/domain/pattern';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { SessionRecord } from '@shared/domain/session';
|
||||
import { nowIso } from '@shared/utils/ids';
|
||||
|
||||
export interface WorkspaceState {
|
||||
projects: ProjectRecord[];
|
||||
patterns: PatternDefinition[];
|
||||
sessions: SessionRecord[];
|
||||
selectedProjectId?: string;
|
||||
selectedPatternId?: string;
|
||||
selectedSessionId?: string;
|
||||
lastUpdatedAt: string;
|
||||
}
|
||||
|
||||
export function createWorkspaceSeed(): WorkspaceState {
|
||||
const timestamp = nowIso();
|
||||
return {
|
||||
projects: [],
|
||||
patterns: createBuiltinPatterns(timestamp),
|
||||
sessions: [],
|
||||
lastUpdatedAt: timestamp,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function createId(prefix: string): string {
|
||||
const random = Math.random().toString(36).slice(2, 10);
|
||||
return `${prefix}-${random}`;
|
||||
}
|
||||
|
||||
export function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
Reference in New Issue
Block a user