feat(workflows): Phase 1 backend — WorkflowDefinition model, persistence, IPC, sidecar execution

Introduce WorkflowDefinition as a first-class entity alongside PatternDefinition.

Shared domain:
- src/shared/domain/workflow.ts: new WorkflowDefinition, WorkflowGraph,
  WorkflowNode/Edge/Config types, normalization, TS-side validation, and
  buildWorkflowExecutionPattern (synthetic pattern bridge for session plumbing)
- workspace.ts: add workflows[] and selectedWorkflowId
- session.ts: add optional workflowId to SessionRecord
- runTimeline.ts: add optional workflowId/workflowName to run records
- sessionLibrary.ts: workflow-aware session search

IPC / main process:
- contracts/channels.ts, contracts/ipc.ts: workflow CRUD channels
- preload/index.ts: expose saveWorkflow, deleteWorkflow, createWorkflowSession
- ipc/registerIpcHandlers.ts: register workflow handlers
- persistence/workspaceRepository.ts: load/save/normalize workflows
- AryxAppService.ts: saveWorkflow, deleteWorkflow, createWorkflowSession,
  resolveSessionExecutionDefinition, workflow-aware turn/run plumbing

Sidecar protocol:
- contracts/sidecar.ts: ValidateWorkflowCommand, WorkflowValidationEvent,
  optional workflow on RunTurnCommand
- sidecarProcess.ts: validate-workflow command dispatch and event handling
- Contracts/ProtocolModels.cs: workflow DTOs and validate-workflow DTOs
- Services/SidecarProtocolHost.cs: validate-workflow command handler
- Services/CopilotWorkflowRunner.cs: workflow-aware build and checkpoint path
- Services/WorkflowValidator.cs: graph validation (connectivity, start/end,
  fan-out/in arity, path reachability)
- Services/WorkflowRunner.cs: WorkflowDefinitionDto -> Agent Framework Workflow
  builder (direct, fan-out, fan-in edges; agent executor binding)

Project: bump both csproj from net9.0 to net10.0

Tests:
- tests/shared/workflow.test.ts: TS workflow validation and pattern synthesis
- tests/main/appServiceWorkflow.test.ts: saveWorkflow / createWorkflowSession
- tests/Aryx.AgentHost.Tests/SidecarProtocolHostTests.cs: ValidateWorkflowCommand
- Fix two stale workspace fixtures missing workflows field

Validation: tsc clean, bun test 367/367 pass, dotnet test 244/244 pass, bun run build succeeds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-05 17:29:03 +02:00
co-authored by Copilot
parent 4c3198a550
commit 19a764d297
25 changed files with 1720 additions and 32 deletions
+28
View File
@@ -14,6 +14,7 @@ import type {
McpOauthRequiredEvent,
ExitPlanModeRequestedEvent,
ValidatePatternCommand,
ValidateWorkflowCommand,
RunTurnCommand,
CopilotSessionListFilter,
CopilotSessionInfo,
@@ -46,6 +47,12 @@ type PendingCommand =
resolve: (issues: ValidatePatternCommand['pattern'] extends never ? never : unknown) => void;
reject: (error: Error) => void;
})
| ({
processId: number;
kind: 'validate-workflow';
resolve: (issues: ValidateWorkflowCommand['workflow'] extends never ? never : unknown) => void;
reject: (error: Error) => void;
})
| ({
processId: number;
kind: 'resolve-approval';
@@ -127,6 +134,14 @@ export class SidecarClient {
});
}
async validateWorkflow(workflow: ValidateWorkflowCommand['workflow']): Promise<unknown> {
return this.dispatch<unknown>({
type: 'validate-workflow',
requestId: `validate-workflow-${Date.now()}`,
workflow,
});
}
async runTurn(
command: RunTurnCommand,
onDelta: (event: TurnDeltaEvent) => void | Promise<void>,
@@ -317,6 +332,13 @@ export class SidecarClient {
resolve: resolve as (issues: unknown) => void,
reject,
});
} else if (command.type === 'validate-workflow') {
this.pending.set(command.requestId, {
processId: state.id,
kind: 'validate-workflow',
resolve: resolve as (issues: unknown) => void,
reject,
});
} else if (command.type === 'resolve-approval') {
this.pending.set(command.requestId, {
processId: state.id,
@@ -413,6 +435,12 @@ export class SidecarClient {
this.pending.delete(event.requestId);
}
return;
case 'workflow-validation':
if (pending.kind === 'validate-workflow') {
pending.resolve(event.issues);
this.pending.delete(event.requestId);
}
return;
case 'turn-delta':
if (pending.kind === 'run-turn' && shouldHandleRunTurnEvent(pending)) {
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onDelta(event));