feat: scaffold electron orchestrator foundation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot CLI
2026-03-21 09:27:28 +01:00
co-authored by Copilot
parent 1ed3d3f652
commit 9e509593d6
46 changed files with 3870 additions and 13 deletions
+14
View File
@@ -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;
+38
View File
@@ -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;
}
+81
View File
@@ -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;
+12
View File
@@ -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;
}
+309
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
export interface ProjectRecord {
id: string;
name: string;
path: string;
addedAt: string;
}
+23
View File
@@ -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;
}
+25
View File
@@ -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,
};
}
+8
View File
@@ -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();
}