elsabro 2.3.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +668 -20
  2. package/bin/install.js +0 -0
  3. package/flows/development-flow.json +452 -0
  4. package/flows/quick-flow.json +118 -0
  5. package/package.json +3 -2
  6. package/references/SYSTEM_INDEX.md +379 -5
  7. package/references/agent-marketplace.md +2274 -0
  8. package/references/agent-protocol.md +1126 -0
  9. package/references/ai-code-suggestions.md +2413 -0
  10. package/references/checkpointing.md +595 -0
  11. package/references/collaboration-patterns.md +851 -0
  12. package/references/collaborative-sessions.md +1081 -0
  13. package/references/configuration-management.md +1810 -0
  14. package/references/cost-tracking.md +1095 -0
  15. package/references/enterprise-sso.md +2001 -0
  16. package/references/error-contracts-v2.md +968 -0
  17. package/references/event-driven.md +1031 -0
  18. package/references/flow-orchestration.md +940 -0
  19. package/references/flow-visualization.md +1557 -0
  20. package/references/ide-integrations.md +3513 -0
  21. package/references/interrupt-system.md +681 -0
  22. package/references/kubernetes-deployment.md +3099 -0
  23. package/references/memory-system.md +683 -0
  24. package/references/mobile-companion.md +3236 -0
  25. package/references/multi-llm-providers.md +2494 -0
  26. package/references/multi-project-memory.md +1182 -0
  27. package/references/observability.md +793 -0
  28. package/references/output-schemas.md +858 -0
  29. package/references/performance-profiler.md +955 -0
  30. package/references/plugin-system.md +1526 -0
  31. package/references/prompt-management.md +292 -0
  32. package/references/sandbox-execution.md +303 -0
  33. package/references/security-system.md +1253 -0
  34. package/references/streaming.md +696 -0
  35. package/references/testing-framework.md +1151 -0
  36. package/references/time-travel.md +802 -0
  37. package/references/tool-registry.md +886 -0
  38. package/references/voice-commands.md +3296 -0
  39. package/templates/agent-marketplace-config.json +220 -0
  40. package/templates/agent-protocol-config.json +136 -0
  41. package/templates/ai-suggestions-config.json +100 -0
  42. package/templates/checkpoint-state.json +61 -0
  43. package/templates/collaboration-config.json +157 -0
  44. package/templates/collaborative-sessions-config.json +153 -0
  45. package/templates/configuration-config.json +245 -0
  46. package/templates/cost-tracking-config.json +148 -0
  47. package/templates/enterprise-sso-config.json +438 -0
  48. package/templates/events-config.json +148 -0
  49. package/templates/flow-visualization-config.json +196 -0
  50. package/templates/ide-integrations-config.json +442 -0
  51. package/templates/kubernetes-config.json +764 -0
  52. package/templates/memory-state.json +84 -0
  53. package/templates/mobile-companion-config.json +600 -0
  54. package/templates/multi-llm-config.json +544 -0
  55. package/templates/multi-project-memory-config.json +145 -0
  56. package/templates/observability-config.json +109 -0
  57. package/templates/performance-profiler-config.json +125 -0
  58. package/templates/plugin-config.json +170 -0
  59. package/templates/prompt-management-config.json +86 -0
  60. package/templates/sandbox-config.json +185 -0
  61. package/templates/schemas-config.json +65 -0
  62. package/templates/security-config.json +120 -0
  63. package/templates/streaming-config.json +72 -0
  64. package/templates/testing-config.json +81 -0
  65. package/templates/timetravel-config.json +62 -0
  66. package/templates/tool-registry-config.json +109 -0
  67. package/templates/voice-commands-config.json +658 -0
@@ -0,0 +1,1557 @@
1
+ # ELSABRO Flow Visualization System
2
+
3
+ > Sistema de visualización y edición visual de flows con graph rendering interactivo.
4
+
5
+ ## Arquitectura General
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────────┐
9
+ │ Flow Visualization System │
10
+ ├─────────────────────────────────────────────────────────────────────────┤
11
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
12
+ │ │ FlowVisualizer │ │ GraphRenderer │ │ NodeEditor │ │
13
+ │ │ ───────────── │ │ ───────────── │ │ ───────────── │ │
14
+ │ │ • Parse flows │ │ • Layout algo │ │ • Node config │ │
15
+ │ │ • State sync │ │ • ASCII render │ │ • Validation │ │
16
+ │ │ • Export/Import │ │ • SVG export │ │ • Templates │ │
17
+ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
18
+ │ │ │
19
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
20
+ │ │ ConnectionManager │ │
21
+ │ │ • Edge routing • Validation • Auto-connect • Cycles │ │
22
+ │ └────────────────────────────────────────────────────────────┘ │
23
+ │ │ │
24
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
25
+ │ │ InteractiveCanvas │ │
26
+ │ │ • Pan/Zoom • Selection • Drag & Drop • Keyboard nav │ │
27
+ │ └────────────────────────────────────────────────────────────┘ │
28
+ └─────────────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 1. FlowVisualizer
34
+
35
+ ### Propósito
36
+ Componente principal que coordina la visualización y edición de flows.
37
+
38
+ ### Interfaz
39
+
40
+ ```typescript
41
+ interface FlowVisualizerOptions {
42
+ flow: Flow;
43
+ mode: 'view' | 'edit';
44
+ renderer: 'ascii' | 'svg' | 'html';
45
+ theme: VisualizerTheme;
46
+ layout: LayoutAlgorithm;
47
+ showMinimap?: boolean;
48
+ showToolbar?: boolean;
49
+ readonly?: boolean;
50
+ }
51
+
52
+ interface VisualizerTheme {
53
+ background: string;
54
+ nodeColors: Record<NodeType, string>;
55
+ edgeColor: string;
56
+ selectedColor: string;
57
+ errorColor: string;
58
+ font: string;
59
+ }
60
+
61
+ type LayoutAlgorithm =
62
+ | 'dagre' // Directed acyclic graph (default)
63
+ | 'elk' // Eclipse Layout Kernel
64
+ | 'force' // Force-directed
65
+ | 'tree' // Hierarchical tree
66
+ | 'radial' // Radial layout
67
+ | 'manual'; // User-positioned
68
+
69
+ interface FlowVisualizer {
70
+ // Core
71
+ render(): string | HTMLElement;
72
+ update(flow: Flow): void;
73
+ refresh(): void;
74
+
75
+ // Navigation
76
+ fitToView(): void;
77
+ zoomIn(): void;
78
+ zoomOut(): void;
79
+ zoomTo(level: number): void;
80
+ panTo(nodeId: string): void;
81
+ center(): void;
82
+
83
+ // Selection
84
+ select(nodeId: string): void;
85
+ selectMultiple(nodeIds: string[]): void;
86
+ clearSelection(): void;
87
+ getSelected(): string[];
88
+
89
+ // Editing (when mode='edit')
90
+ addNode(type: NodeType, position?: Position): string;
91
+ removeNode(nodeId: string): void;
92
+ updateNode(nodeId: string, config: Partial<NodeConfig>): void;
93
+ connect(sourceId: string, targetId: string, label?: string): void;
94
+ disconnect(sourceId: string, targetId: string): void;
95
+
96
+ // Export/Import
97
+ exportFlow(): Flow;
98
+ exportImage(format: 'svg' | 'png'): Promise<Blob>;
99
+ exportASCII(): string;
100
+ importFlow(flow: Flow): void;
101
+
102
+ // Events
103
+ on(event: VisualizerEvent, callback: Function): () => void;
104
+ off(event: VisualizerEvent, callback: Function): void;
105
+ }
106
+
107
+ type VisualizerEvent =
108
+ | 'node:click'
109
+ | 'node:dblclick'
110
+ | 'node:select'
111
+ | 'node:move'
112
+ | 'edge:click'
113
+ | 'edge:select'
114
+ | 'canvas:click'
115
+ | 'flow:change'
116
+ | 'zoom:change'
117
+ | 'selection:change';
118
+ ```
119
+
120
+ ### Implementación
121
+
122
+ ```typescript
123
+ class FlowVisualizerImpl implements FlowVisualizer {
124
+ private flow: Flow;
125
+ private options: FlowVisualizerOptions;
126
+ private renderer: GraphRenderer;
127
+ private layout: LayoutEngine;
128
+ private selection: Set<string> = new Set();
129
+ private eventEmitter: EventEmitter = new EventEmitter();
130
+ private zoom: number = 1;
131
+ private pan: Position = { x: 0, y: 0 };
132
+
133
+ constructor(options: FlowVisualizerOptions) {
134
+ this.options = {
135
+ showMinimap: true,
136
+ showToolbar: true,
137
+ readonly: false,
138
+ ...options
139
+ };
140
+ this.flow = options.flow;
141
+ this.renderer = this.createRenderer(options.renderer);
142
+ this.layout = this.createLayout(options.layout);
143
+ }
144
+
145
+ private createRenderer(type: string): GraphRenderer {
146
+ switch (type) {
147
+ case 'ascii':
148
+ return new ASCIIRenderer(this.options.theme);
149
+ case 'svg':
150
+ return new SVGRenderer(this.options.theme);
151
+ case 'html':
152
+ return new HTMLRenderer(this.options.theme);
153
+ default:
154
+ return new ASCIIRenderer(this.options.theme);
155
+ }
156
+ }
157
+
158
+ private createLayout(algorithm: LayoutAlgorithm): LayoutEngine {
159
+ switch (algorithm) {
160
+ case 'dagre':
161
+ return new DagreLayout();
162
+ case 'elk':
163
+ return new ELKLayout();
164
+ case 'force':
165
+ return new ForceDirectedLayout();
166
+ case 'tree':
167
+ return new TreeLayout();
168
+ case 'radial':
169
+ return new RadialLayout();
170
+ case 'manual':
171
+ return new ManualLayout();
172
+ default:
173
+ return new DagreLayout();
174
+ }
175
+ }
176
+
177
+ render(): string | HTMLElement {
178
+ // Calculate layout positions
179
+ const positions = this.layout.calculate(this.flow);
180
+
181
+ // Apply positions to flow
182
+ const layoutedFlow = this.applyPositions(this.flow, positions);
183
+
184
+ // Render with current state
185
+ return this.renderer.render(layoutedFlow, {
186
+ zoom: this.zoom,
187
+ pan: this.pan,
188
+ selection: this.selection,
189
+ showMinimap: this.options.showMinimap,
190
+ showToolbar: this.options.showToolbar,
191
+ mode: this.options.mode
192
+ });
193
+ }
194
+
195
+ update(flow: Flow): void {
196
+ this.flow = flow;
197
+ this.eventEmitter.emit('flow:change', flow);
198
+ }
199
+
200
+ // Selection
201
+ select(nodeId: string): void {
202
+ this.selection.clear();
203
+ this.selection.add(nodeId);
204
+ this.eventEmitter.emit('selection:change', Array.from(this.selection));
205
+ this.eventEmitter.emit('node:select', { nodeId });
206
+ }
207
+
208
+ selectMultiple(nodeIds: string[]): void {
209
+ this.selection = new Set(nodeIds);
210
+ this.eventEmitter.emit('selection:change', nodeIds);
211
+ }
212
+
213
+ clearSelection(): void {
214
+ this.selection.clear();
215
+ this.eventEmitter.emit('selection:change', []);
216
+ }
217
+
218
+ getSelected(): string[] {
219
+ return Array.from(this.selection);
220
+ }
221
+
222
+ // Editing
223
+ addNode(type: NodeType, position?: Position): string {
224
+ if (this.options.readonly) {
225
+ throw new Error('Cannot edit in readonly mode');
226
+ }
227
+
228
+ const nodeId = `node_${Date.now()}`;
229
+ const newNode: FlowNode = {
230
+ id: nodeId,
231
+ type,
232
+ config: this.getDefaultConfig(type),
233
+ position: position || this.getNextPosition()
234
+ };
235
+
236
+ this.flow.nodes.push(newNode);
237
+ this.eventEmitter.emit('flow:change', this.flow);
238
+ return nodeId;
239
+ }
240
+
241
+ removeNode(nodeId: string): void {
242
+ if (this.options.readonly) {
243
+ throw new Error('Cannot edit in readonly mode');
244
+ }
245
+
246
+ // Remove node
247
+ this.flow.nodes = this.flow.nodes.filter(n => n.id !== nodeId);
248
+
249
+ // Remove connected edges
250
+ this.flow.edges = this.flow.edges.filter(
251
+ e => e.source !== nodeId && e.target !== nodeId
252
+ );
253
+
254
+ this.selection.delete(nodeId);
255
+ this.eventEmitter.emit('flow:change', this.flow);
256
+ }
257
+
258
+ connect(sourceId: string, targetId: string, label?: string): void {
259
+ if (this.options.readonly) {
260
+ throw new Error('Cannot edit in readonly mode');
261
+ }
262
+
263
+ // Validate connection
264
+ const validation = this.validateConnection(sourceId, targetId);
265
+ if (!validation.valid) {
266
+ throw new Error(validation.error);
267
+ }
268
+
269
+ const edge: FlowEdge = {
270
+ id: `edge_${sourceId}_${targetId}`,
271
+ source: sourceId,
272
+ target: targetId,
273
+ label
274
+ };
275
+
276
+ this.flow.edges.push(edge);
277
+ this.eventEmitter.emit('flow:change', this.flow);
278
+ }
279
+
280
+ disconnect(sourceId: string, targetId: string): void {
281
+ if (this.options.readonly) {
282
+ throw new Error('Cannot edit in readonly mode');
283
+ }
284
+
285
+ this.flow.edges = this.flow.edges.filter(
286
+ e => !(e.source === sourceId && e.target === targetId)
287
+ );
288
+ this.eventEmitter.emit('flow:change', this.flow);
289
+ }
290
+
291
+ private validateConnection(sourceId: string, targetId: string): { valid: boolean; error?: string } {
292
+ // Check nodes exist
293
+ const sourceNode = this.flow.nodes.find(n => n.id === sourceId);
294
+ const targetNode = this.flow.nodes.find(n => n.id === targetId);
295
+
296
+ if (!sourceNode || !targetNode) {
297
+ return { valid: false, error: 'Node not found' };
298
+ }
299
+
300
+ // Check for self-connection
301
+ if (sourceId === targetId) {
302
+ return { valid: false, error: 'Cannot connect node to itself' };
303
+ }
304
+
305
+ // Check for duplicate connection
306
+ const exists = this.flow.edges.some(
307
+ e => e.source === sourceId && e.target === targetId
308
+ );
309
+ if (exists) {
310
+ return { valid: false, error: 'Connection already exists' };
311
+ }
312
+
313
+ // Check for cycles (if DAG required)
314
+ if (this.wouldCreateCycle(sourceId, targetId)) {
315
+ return { valid: false, error: 'Connection would create a cycle' };
316
+ }
317
+
318
+ return { valid: true };
319
+ }
320
+
321
+ private wouldCreateCycle(sourceId: string, targetId: string): boolean {
322
+ // DFS to check if adding edge would create cycle
323
+ const visited = new Set<string>();
324
+ const stack = [sourceId];
325
+
326
+ // First, check if target can reach source
327
+ const reverseStack = [targetId];
328
+ while (reverseStack.length > 0) {
329
+ const current = reverseStack.pop()!;
330
+ if (current === sourceId) {
331
+ return true; // Would create cycle
332
+ }
333
+ if (visited.has(current)) continue;
334
+ visited.add(current);
335
+
336
+ // Get outgoing edges from current
337
+ const outgoing = this.flow.edges
338
+ .filter(e => e.source === current)
339
+ .map(e => e.target);
340
+ reverseStack.push(...outgoing);
341
+ }
342
+
343
+ return false;
344
+ }
345
+
346
+ // Export
347
+ exportFlow(): Flow {
348
+ return JSON.parse(JSON.stringify(this.flow));
349
+ }
350
+
351
+ async exportImage(format: 'svg' | 'png'): Promise<Blob> {
352
+ if (this.renderer instanceof SVGRenderer) {
353
+ const svg = this.renderer.renderToString(this.flow);
354
+
355
+ if (format === 'svg') {
356
+ return new Blob([svg], { type: 'image/svg+xml' });
357
+ } else {
358
+ // Convert SVG to PNG
359
+ return this.svgToPng(svg);
360
+ }
361
+ }
362
+ throw new Error('Image export only supported with SVG renderer');
363
+ }
364
+
365
+ exportASCII(): string {
366
+ const asciiRenderer = new ASCIIRenderer(this.options.theme);
367
+ const positions = this.layout.calculate(this.flow);
368
+ const layoutedFlow = this.applyPositions(this.flow, positions);
369
+ return asciiRenderer.renderToString(layoutedFlow);
370
+ }
371
+
372
+ // Events
373
+ on(event: VisualizerEvent, callback: Function): () => void {
374
+ this.eventEmitter.on(event, callback);
375
+ return () => this.eventEmitter.off(event, callback);
376
+ }
377
+
378
+ off(event: VisualizerEvent, callback: Function): void {
379
+ this.eventEmitter.off(event, callback);
380
+ }
381
+
382
+ private getDefaultConfig(type: NodeType): NodeConfig {
383
+ const defaults: Record<NodeType, NodeConfig> = {
384
+ entry: { name: 'Start' },
385
+ exit: { name: 'End' },
386
+ agent: { name: 'Agent', agentType: 'general-purpose' },
387
+ parallel: { name: 'Parallel', branches: [] },
388
+ sequence: { name: 'Sequence', steps: [] },
389
+ condition: { name: 'Condition', expression: 'true' },
390
+ router: { name: 'Router', routes: [] },
391
+ loop: { name: 'Loop', maxIterations: 10 },
392
+ interrupt: { name: 'Interrupt', type: 'approval' },
393
+ subflow: { name: 'Subflow', flowId: '' }
394
+ };
395
+ return defaults[type] || { name: type };
396
+ }
397
+
398
+ private getNextPosition(): Position {
399
+ // Find rightmost node and place new node to its right
400
+ let maxX = 0;
401
+ for (const node of this.flow.nodes) {
402
+ if (node.position && node.position.x > maxX) {
403
+ maxX = node.position.x;
404
+ }
405
+ }
406
+ return { x: maxX + 200, y: 100 };
407
+ }
408
+
409
+ private applyPositions(flow: Flow, positions: Map<string, Position>): Flow {
410
+ return {
411
+ ...flow,
412
+ nodes: flow.nodes.map(node => ({
413
+ ...node,
414
+ position: positions.get(node.id) || node.position || { x: 0, y: 0 }
415
+ }))
416
+ };
417
+ }
418
+ }
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 2. GraphRenderer
424
+
425
+ ### Propósito
426
+ Renderiza el grafo del flow en diferentes formatos (ASCII, SVG, HTML).
427
+
428
+ ### ASCII Renderer
429
+
430
+ ```typescript
431
+ interface ASCIIRendererOptions {
432
+ width?: number;
433
+ height?: number;
434
+ nodeWidth?: number;
435
+ nodeHeight?: number;
436
+ spacing?: number;
437
+ charset?: 'unicode' | 'ascii';
438
+ }
439
+
440
+ class ASCIIRenderer implements GraphRenderer {
441
+ private options: Required<ASCIIRendererOptions>;
442
+ private theme: VisualizerTheme;
443
+
444
+ // Unicode box drawing characters
445
+ private chars = {
446
+ unicode: {
447
+ horizontal: '─',
448
+ vertical: '│',
449
+ topLeft: '┌',
450
+ topRight: '┐',
451
+ bottomLeft: '└',
452
+ bottomRight: '┘',
453
+ cross: '┼',
454
+ teeDown: '┬',
455
+ teeUp: '┴',
456
+ teeRight: '├',
457
+ teeLeft: '┤',
458
+ arrowRight: '→',
459
+ arrowDown: '↓',
460
+ arrowLeft: '←',
461
+ arrowUp: '↑',
462
+ bullet: '●',
463
+ diamond: '◆',
464
+ square: '■'
465
+ },
466
+ ascii: {
467
+ horizontal: '-',
468
+ vertical: '|',
469
+ topLeft: '+',
470
+ topRight: '+',
471
+ bottomLeft: '+',
472
+ bottomRight: '+',
473
+ cross: '+',
474
+ teeDown: '+',
475
+ teeUp: '+',
476
+ teeRight: '+',
477
+ teeLeft: '+',
478
+ arrowRight: '>',
479
+ arrowDown: 'v',
480
+ arrowLeft: '<',
481
+ arrowUp: '^',
482
+ bullet: '*',
483
+ diamond: '*',
484
+ square: '#'
485
+ }
486
+ };
487
+
488
+ constructor(theme: VisualizerTheme, options?: ASCIIRendererOptions) {
489
+ this.theme = theme;
490
+ this.options = {
491
+ width: 120,
492
+ height: 40,
493
+ nodeWidth: 20,
494
+ nodeHeight: 5,
495
+ spacing: 4,
496
+ charset: 'unicode',
497
+ ...options
498
+ };
499
+ }
500
+
501
+ render(flow: Flow, state: RenderState): string {
502
+ const canvas = this.createCanvas();
503
+ const c = this.chars[this.options.charset];
504
+
505
+ // Render edges first (behind nodes)
506
+ for (const edge of flow.edges) {
507
+ this.renderEdge(canvas, edge, flow, c);
508
+ }
509
+
510
+ // Render nodes
511
+ for (const node of flow.nodes) {
512
+ const isSelected = state.selection.has(node.id);
513
+ this.renderNode(canvas, node, isSelected, c);
514
+ }
515
+
516
+ // Add title
517
+ this.renderTitle(canvas, flow.id || 'Untitled Flow');
518
+
519
+ // Add legend if in edit mode
520
+ if (state.mode === 'edit') {
521
+ this.renderLegend(canvas, c);
522
+ }
523
+
524
+ return this.canvasToString(canvas);
525
+ }
526
+
527
+ private createCanvas(): string[][] {
528
+ return Array(this.options.height)
529
+ .fill(null)
530
+ .map(() => Array(this.options.width).fill(' '));
531
+ }
532
+
533
+ private renderNode(
534
+ canvas: string[][],
535
+ node: FlowNode,
536
+ isSelected: boolean,
537
+ c: typeof this.chars.unicode
538
+ ): void {
539
+ const { x, y } = node.position || { x: 0, y: 0 };
540
+ const w = this.options.nodeWidth;
541
+ const h = this.options.nodeHeight;
542
+
543
+ // Scale positions to canvas
544
+ const cx = Math.floor(x / 10);
545
+ const cy = Math.floor(y / 10);
546
+
547
+ // Draw box
548
+ const borderChar = isSelected ? '═' : c.horizontal;
549
+ const vertChar = isSelected ? '║' : c.vertical;
550
+
551
+ // Top border
552
+ this.drawText(canvas, cx, cy, c.topLeft + borderChar.repeat(w - 2) + c.topRight);
553
+
554
+ // Sides and content
555
+ for (let i = 1; i < h - 1; i++) {
556
+ this.drawText(canvas, cx, cy + i, vertChar);
557
+ this.drawText(canvas, cx + w - 1, cy + i, vertChar);
558
+ }
559
+
560
+ // Bottom border
561
+ this.drawText(canvas, cx, cy + h - 1, c.bottomLeft + borderChar.repeat(w - 2) + c.bottomRight);
562
+
563
+ // Node icon based on type
564
+ const icon = this.getNodeIcon(node.type, c);
565
+ this.drawText(canvas, cx + 2, cy + 1, icon);
566
+
567
+ // Node name (truncated)
568
+ const name = (node.config?.name || node.type).slice(0, w - 6);
569
+ this.drawText(canvas, cx + 4, cy + 1, name);
570
+
571
+ // Node type
572
+ const typeLabel = `[${node.type}]`.slice(0, w - 4);
573
+ this.drawText(canvas, cx + 2, cy + 2, typeLabel);
574
+
575
+ // Status indicator if running
576
+ if (node._status === 'running') {
577
+ this.drawText(canvas, cx + w - 3, cy + 1, '⟳');
578
+ } else if (node._status === 'completed') {
579
+ this.drawText(canvas, cx + w - 3, cy + 1, '✓');
580
+ } else if (node._status === 'error') {
581
+ this.drawText(canvas, cx + w - 3, cy + 1, '✗');
582
+ }
583
+ }
584
+
585
+ private renderEdge(
586
+ canvas: string[][],
587
+ edge: FlowEdge,
588
+ flow: Flow,
589
+ c: typeof this.chars.unicode
590
+ ): void {
591
+ const sourceNode = flow.nodes.find(n => n.id === edge.source);
592
+ const targetNode = flow.nodes.find(n => n.id === edge.target);
593
+
594
+ if (!sourceNode?.position || !targetNode?.position) return;
595
+
596
+ const w = this.options.nodeWidth;
597
+ const h = this.options.nodeHeight;
598
+
599
+ // Calculate connection points
600
+ const sx = Math.floor(sourceNode.position.x / 10) + w;
601
+ const sy = Math.floor(sourceNode.position.y / 10) + Math.floor(h / 2);
602
+ const tx = Math.floor(targetNode.position.x / 10);
603
+ const ty = Math.floor(targetNode.position.y / 10) + Math.floor(h / 2);
604
+
605
+ // Draw edge (simple right-angle routing)
606
+ const midX = Math.floor((sx + tx) / 2);
607
+
608
+ // Horizontal from source
609
+ for (let x = sx; x <= midX; x++) {
610
+ this.drawChar(canvas, x, sy, c.horizontal);
611
+ }
612
+
613
+ // Vertical segment
614
+ const [minY, maxY] = sy < ty ? [sy, ty] : [ty, sy];
615
+ for (let y = minY; y <= maxY; y++) {
616
+ this.drawChar(canvas, midX, y, c.vertical);
617
+ }
618
+
619
+ // Horizontal to target
620
+ for (let x = midX; x < tx; x++) {
621
+ this.drawChar(canvas, x, ty, c.horizontal);
622
+ }
623
+
624
+ // Arrow at target
625
+ this.drawChar(canvas, tx - 1, ty, c.arrowRight);
626
+
627
+ // Corner characters
628
+ if (sy !== ty) {
629
+ if (sy < ty) {
630
+ this.drawChar(canvas, midX, sy, c.topLeft);
631
+ this.drawChar(canvas, midX, ty, c.bottomRight);
632
+ } else {
633
+ this.drawChar(canvas, midX, sy, c.bottomLeft);
634
+ this.drawChar(canvas, midX, ty, c.topRight);
635
+ }
636
+ }
637
+
638
+ // Edge label
639
+ if (edge.label) {
640
+ const labelX = midX - Math.floor(edge.label.length / 2);
641
+ const labelY = Math.floor((sy + ty) / 2);
642
+ this.drawText(canvas, labelX, labelY, `[${edge.label}]`);
643
+ }
644
+ }
645
+
646
+ private getNodeIcon(type: NodeType, c: typeof this.chars.unicode): string {
647
+ const icons: Record<NodeType, string> = {
648
+ entry: '▶',
649
+ exit: '■',
650
+ agent: '◉',
651
+ parallel: '⫴',
652
+ sequence: '→',
653
+ condition: '◇',
654
+ router: '⋔',
655
+ loop: '↻',
656
+ interrupt: '⏸',
657
+ subflow: '⊞'
658
+ };
659
+ return icons[type] || c.bullet;
660
+ }
661
+
662
+ private renderTitle(canvas: string[][], title: string): void {
663
+ const centeredX = Math.floor((this.options.width - title.length - 4) / 2);
664
+ this.drawText(canvas, centeredX, 0, `╔${'═'.repeat(title.length + 2)}╗`);
665
+ this.drawText(canvas, centeredX, 1, `║ ${title} ║`);
666
+ this.drawText(canvas, centeredX, 2, `╚${'═'.repeat(title.length + 2)}╝`);
667
+ }
668
+
669
+ private renderLegend(canvas: string[][], c: typeof this.chars.unicode): void {
670
+ const y = this.options.height - 3;
671
+ const legend = [
672
+ `${this.getNodeIcon('entry', c)} Entry`,
673
+ `${this.getNodeIcon('agent', c)} Agent`,
674
+ `${this.getNodeIcon('parallel', c)} Parallel`,
675
+ `${this.getNodeIcon('condition', c)} Condition`,
676
+ `${this.getNodeIcon('exit', c)} Exit`
677
+ ].join(' │ ');
678
+
679
+ this.drawText(canvas, 2, y, `Legend: ${legend}`);
680
+ }
681
+
682
+ private drawText(canvas: string[][], x: number, y: number, text: string): void {
683
+ if (y < 0 || y >= canvas.length) return;
684
+ for (let i = 0; i < text.length; i++) {
685
+ if (x + i >= 0 && x + i < canvas[0].length) {
686
+ canvas[y][x + i] = text[i];
687
+ }
688
+ }
689
+ }
690
+
691
+ private drawChar(canvas: string[][], x: number, y: number, char: string): void {
692
+ if (y >= 0 && y < canvas.length && x >= 0 && x < canvas[0].length) {
693
+ // Don't overwrite nodes
694
+ if (canvas[y][x] === ' ' || canvas[y][x] === '─' || canvas[y][x] === '│') {
695
+ canvas[y][x] = char;
696
+ }
697
+ }
698
+ }
699
+
700
+ private canvasToString(canvas: string[][]): string {
701
+ return canvas.map(row => row.join('')).join('\n');
702
+ }
703
+
704
+ renderToString(flow: Flow): string {
705
+ return this.render(flow, {
706
+ zoom: 1,
707
+ pan: { x: 0, y: 0 },
708
+ selection: new Set(),
709
+ showMinimap: false,
710
+ showToolbar: false,
711
+ mode: 'view'
712
+ });
713
+ }
714
+ }
715
+ ```
716
+
717
+ ### Visualización de Ejemplo
718
+
719
+ ```
720
+ ╔════════════════════════════════════════════════════════════════════════════════╗
721
+ ║ development_flow ║
722
+ ╚════════════════════════════════════════════════════════════════════════════════╝
723
+
724
+ ┌──────────────────┐ ┌──────────────────┐
725
+ │ ▶ Start │ │ ⫴ Analyze │
726
+ │ [entry] │───────────────────→│ [parallel] │
727
+ │ │ │ branches: 3 │
728
+ └──────────────────┘ └──────────────────┘
729
+
730
+
731
+
732
+ ┌──────────────────┐ ┌──────────────────┐
733
+ │ ◇ Has Errors? │ │ ◉ Implement │
734
+ │ [condition] │←───────────────────│ [agent] │
735
+ │ │ │ type: executor │
736
+ └──────────────────┘ └──────────────────┘
737
+
738
+ ┌────┴────┐
739
+ │ yes no │
740
+ ↓ ↓
741
+ ┌──────────────────┐ ┌──────────────────┐
742
+ │ ↻ Fix Loop │ │ ⫴ Review │
743
+ │ [loop] │ │ [parallel] │
744
+ │ max: 3 │ │ branches: 3 │
745
+ └──────────────────┘ └──────────────────┘
746
+
747
+
748
+ ┌──────────────────┐
749
+ │ ■ Complete │
750
+ │ [exit] │
751
+ │ │
752
+ └──────────────────┘
753
+
754
+ Legend: ▶ Entry │ ◉ Agent │ ⫴ Parallel │ ◇ Condition │ ■ Exit
755
+ ```
756
+
757
+ ---
758
+
759
+ ## 3. NodeEditor
760
+
761
+ ### Propósito
762
+ Editor visual para configurar nodos individuales.
763
+
764
+ ### Interfaz
765
+
766
+ ```typescript
767
+ interface NodeEditorOptions {
768
+ node: FlowNode;
769
+ availableAgents: AgentDefinition[];
770
+ availableFlows: Flow[];
771
+ customValidators?: Record<string, NodeValidator>;
772
+ }
773
+
774
+ interface NodeEditor {
775
+ // Core
776
+ render(): string;
777
+ getValue(): NodeConfig;
778
+ setValue(config: NodeConfig): void;
779
+
780
+ // Validation
781
+ validate(): ValidationResult;
782
+ getErrors(): string[];
783
+
784
+ // Templates
785
+ applyTemplate(templateId: string): void;
786
+ getTemplates(nodeType: NodeType): NodeTemplate[];
787
+
788
+ // Events
789
+ onChange(callback: (config: NodeConfig) => void): () => void;
790
+ }
791
+
792
+ interface NodeTemplate {
793
+ id: string;
794
+ name: string;
795
+ description: string;
796
+ nodeType: NodeType;
797
+ config: Partial<NodeConfig>;
798
+ }
799
+ ```
800
+
801
+ ### Implementación
802
+
803
+ ```typescript
804
+ class NodeEditorImpl implements NodeEditor {
805
+ private node: FlowNode;
806
+ private config: NodeConfig;
807
+ private options: NodeEditorOptions;
808
+ private changeCallbacks: Set<Function> = new Set();
809
+
810
+ constructor(options: NodeEditorOptions) {
811
+ this.options = options;
812
+ this.node = options.node;
813
+ this.config = { ...options.node.config };
814
+ }
815
+
816
+ render(): string {
817
+ const lines: string[] = [];
818
+ const c = {
819
+ h: '─', v: '│', tl: '┌', tr: '┐', bl: '└', br: '┘'
820
+ };
821
+
822
+ // Header
823
+ lines.push(`${c.tl}${'─'.repeat(50)}${c.tr}`);
824
+ lines.push(`${c.v} Node Editor: ${this.node.type.padEnd(33)}${c.v}`);
825
+ lines.push(`${c.v}${'─'.repeat(50)}${c.v}`);
826
+
827
+ // Fields based on node type
828
+ const fields = this.getFieldsForType(this.node.type);
829
+
830
+ for (const field of fields) {
831
+ const value = this.config[field.key] ?? field.default ?? '';
832
+ const displayValue = this.formatValue(value, field.type);
833
+ const label = field.label.padEnd(15);
834
+
835
+ if (field.type === 'select') {
836
+ lines.push(`${c.v} ${label}: [${displayValue}] ▼`.padEnd(51) + c.v);
837
+ } else if (field.type === 'boolean') {
838
+ const checkbox = value ? '[✓]' : '[ ]';
839
+ lines.push(`${c.v} ${label}: ${checkbox}`.padEnd(51) + c.v);
840
+ } else if (field.type === 'array') {
841
+ lines.push(`${c.v} ${label}:`.padEnd(51) + c.v);
842
+ const items = Array.isArray(value) ? value : [];
843
+ for (const item of items.slice(0, 3)) {
844
+ lines.push(`${c.v} • ${String(item).slice(0, 40)}`.padEnd(51) + c.v);
845
+ }
846
+ if (items.length > 3) {
847
+ lines.push(`${c.v} ... and ${items.length - 3} more`.padEnd(51) + c.v);
848
+ }
849
+ } else {
850
+ lines.push(`${c.v} ${label}: ${displayValue.slice(0, 30)}`.padEnd(51) + c.v);
851
+ }
852
+ }
853
+
854
+ // Validation status
855
+ const validation = this.validate();
856
+ lines.push(`${c.v}${'─'.repeat(50)}${c.v}`);
857
+ if (validation.valid) {
858
+ lines.push(`${c.v} Status: ✓ Valid`.padEnd(51) + c.v);
859
+ } else {
860
+ lines.push(`${c.v} Status: ✗ ${validation.errors.length} error(s)`.padEnd(51) + c.v);
861
+ for (const error of validation.errors.slice(0, 2)) {
862
+ lines.push(`${c.v} • ${error.slice(0, 42)}`.padEnd(51) + c.v);
863
+ }
864
+ }
865
+
866
+ // Footer
867
+ lines.push(`${c.bl}${'─'.repeat(50)}${c.br}`);
868
+
869
+ return lines.join('\n');
870
+ }
871
+
872
+ private getFieldsForType(type: NodeType): FieldDefinition[] {
873
+ const commonFields: FieldDefinition[] = [
874
+ { key: 'name', label: 'Name', type: 'string', required: true }
875
+ ];
876
+
877
+ const typeSpecificFields: Record<NodeType, FieldDefinition[]> = {
878
+ entry: [],
879
+ exit: [
880
+ { key: 'outputMapping', label: 'Output Map', type: 'object' }
881
+ ],
882
+ agent: [
883
+ { key: 'agentType', label: 'Agent Type', type: 'select', options: this.getAgentOptions() },
884
+ { key: 'model', label: 'Model', type: 'select', options: ['haiku', 'sonnet', 'opus'] },
885
+ { key: 'prompt', label: 'Prompt', type: 'text' },
886
+ { key: 'timeout', label: 'Timeout (ms)', type: 'number', default: 30000 }
887
+ ],
888
+ parallel: [
889
+ { key: 'branches', label: 'Branches', type: 'array' },
890
+ { key: 'joinType', label: 'Join Type', type: 'select', options: ['all', 'any', 'quorum', 'first_success'] },
891
+ { key: 'quorumSize', label: 'Quorum Size', type: 'number', default: 2 }
892
+ ],
893
+ sequence: [
894
+ { key: 'steps', label: 'Steps', type: 'array' },
895
+ { key: 'stopOnError', label: 'Stop on Error', type: 'boolean', default: true }
896
+ ],
897
+ condition: [
898
+ { key: 'expression', label: 'Expression', type: 'string', required: true },
899
+ { key: 'trueBranch', label: 'True Branch', type: 'string' },
900
+ { key: 'falseBranch', label: 'False Branch', type: 'string' }
901
+ ],
902
+ router: [
903
+ { key: 'routes', label: 'Routes', type: 'array' },
904
+ { key: 'defaultRoute', label: 'Default Route', type: 'string' }
905
+ ],
906
+ loop: [
907
+ { key: 'maxIterations', label: 'Max Iterations', type: 'number', default: 10 },
908
+ { key: 'condition', label: 'Condition', type: 'string' },
909
+ { key: 'body', label: 'Loop Body', type: 'string' }
910
+ ],
911
+ interrupt: [
912
+ { key: 'type', label: 'Type', type: 'select', options: ['approval', 'decision', 'input', 'confirmation'] },
913
+ { key: 'message', label: 'Message', type: 'text' },
914
+ { key: 'timeout', label: 'Timeout (ms)', type: 'number' },
915
+ { key: 'defaultAction', label: 'Default Action', type: 'string' }
916
+ ],
917
+ subflow: [
918
+ { key: 'flowId', label: 'Flow ID', type: 'select', options: this.getFlowOptions() },
919
+ { key: 'inputMapping', label: 'Input Map', type: 'object' },
920
+ { key: 'outputMapping', label: 'Output Map', type: 'object' }
921
+ ]
922
+ };
923
+
924
+ return [...commonFields, ...(typeSpecificFields[type] || [])];
925
+ }
926
+
927
+ private getAgentOptions(): string[] {
928
+ return this.options.availableAgents.map(a => a.name);
929
+ }
930
+
931
+ private getFlowOptions(): string[] {
932
+ return this.options.availableFlows.map(f => f.id);
933
+ }
934
+
935
+ validate(): ValidationResult {
936
+ const errors: string[] = [];
937
+ const fields = this.getFieldsForType(this.node.type);
938
+
939
+ for (const field of fields) {
940
+ const value = this.config[field.key];
941
+
942
+ // Required check
943
+ if (field.required && (value === undefined || value === null || value === '')) {
944
+ errors.push(`${field.label} is required`);
945
+ }
946
+
947
+ // Type validation
948
+ if (value !== undefined && value !== null) {
949
+ switch (field.type) {
950
+ case 'number':
951
+ if (typeof value !== 'number' || isNaN(value)) {
952
+ errors.push(`${field.label} must be a number`);
953
+ }
954
+ break;
955
+ case 'boolean':
956
+ if (typeof value !== 'boolean') {
957
+ errors.push(`${field.label} must be a boolean`);
958
+ }
959
+ break;
960
+ case 'array':
961
+ if (!Array.isArray(value)) {
962
+ errors.push(`${field.label} must be an array`);
963
+ }
964
+ break;
965
+ }
966
+ }
967
+ }
968
+
969
+ // Custom validators
970
+ if (this.options.customValidators?.[this.node.type]) {
971
+ const customErrors = this.options.customValidators[this.node.type](this.config);
972
+ errors.push(...customErrors);
973
+ }
974
+
975
+ return { valid: errors.length === 0, errors };
976
+ }
977
+
978
+ getValue(): NodeConfig {
979
+ return { ...this.config };
980
+ }
981
+
982
+ setValue(config: NodeConfig): void {
983
+ this.config = { ...config };
984
+ this.notifyChange();
985
+ }
986
+
987
+ getTemplates(nodeType: NodeType): NodeTemplate[] {
988
+ const templates: Record<NodeType, NodeTemplate[]> = {
989
+ agent: [
990
+ {
991
+ id: 'explore-agent',
992
+ name: 'Exploration Agent',
993
+ description: 'Fast agent for codebase exploration',
994
+ nodeType: 'agent',
995
+ config: { agentType: 'Explore', model: 'haiku', timeout: 30000 }
996
+ },
997
+ {
998
+ id: 'implement-agent',
999
+ name: 'Implementation Agent',
1000
+ description: 'High-quality agent for code implementation',
1001
+ nodeType: 'agent',
1002
+ config: { agentType: 'elsabro-executor', model: 'opus', timeout: 120000 }
1003
+ },
1004
+ {
1005
+ id: 'review-agent',
1006
+ name: 'Code Review Agent',
1007
+ description: 'Agent for thorough code review',
1008
+ nodeType: 'agent',
1009
+ config: { agentType: 'pr-review-toolkit:code-reviewer', model: 'opus', timeout: 60000 }
1010
+ }
1011
+ ],
1012
+ parallel: [
1013
+ {
1014
+ id: 'explore-parallel',
1015
+ name: 'Exploration Wave',
1016
+ description: '4 parallel exploration agents',
1017
+ nodeType: 'parallel',
1018
+ config: {
1019
+ branches: [
1020
+ { agent: 'Explore' },
1021
+ { agent: 'Plan' },
1022
+ { agent: 'feature-dev:code-explorer' },
1023
+ { agent: 'general-purpose' }
1024
+ ],
1025
+ joinType: 'all'
1026
+ }
1027
+ },
1028
+ {
1029
+ id: 'review-parallel',
1030
+ name: 'Review Wave',
1031
+ description: '3 parallel review agents',
1032
+ nodeType: 'parallel',
1033
+ config: {
1034
+ branches: [
1035
+ { agent: 'pr-review-toolkit:code-reviewer' },
1036
+ { agent: 'pr-review-toolkit:silent-failure-hunter' },
1037
+ { agent: 'elsabro-verifier' }
1038
+ ],
1039
+ joinType: 'all'
1040
+ }
1041
+ }
1042
+ ],
1043
+ // ... other node types
1044
+ } as Record<NodeType, NodeTemplate[]>;
1045
+
1046
+ return templates[nodeType] || [];
1047
+ }
1048
+
1049
+ applyTemplate(templateId: string): void {
1050
+ const templates = this.getTemplates(this.node.type);
1051
+ const template = templates.find(t => t.id === templateId);
1052
+
1053
+ if (template) {
1054
+ this.config = { ...this.config, ...template.config };
1055
+ this.notifyChange();
1056
+ }
1057
+ }
1058
+
1059
+ onChange(callback: Function): () => void {
1060
+ this.changeCallbacks.add(callback);
1061
+ return () => this.changeCallbacks.delete(callback);
1062
+ }
1063
+
1064
+ private notifyChange(): void {
1065
+ this.changeCallbacks.forEach(cb => cb(this.config));
1066
+ }
1067
+
1068
+ private formatValue(value: unknown, type: string): string {
1069
+ if (value === undefined || value === null) return '';
1070
+ if (type === 'array') return `${(value as unknown[]).length} items`;
1071
+ if (type === 'object') return '{...}';
1072
+ return String(value);
1073
+ }
1074
+ }
1075
+
1076
+ interface FieldDefinition {
1077
+ key: string;
1078
+ label: string;
1079
+ type: 'string' | 'number' | 'boolean' | 'select' | 'text' | 'array' | 'object';
1080
+ required?: boolean;
1081
+ default?: unknown;
1082
+ options?: string[];
1083
+ }
1084
+ ```
1085
+
1086
+ ---
1087
+
1088
+ ## 4. ConnectionManager
1089
+
1090
+ ### Propósito
1091
+ Gestiona las conexiones entre nodos con validación y auto-routing.
1092
+
1093
+ ### Interfaz
1094
+
1095
+ ```typescript
1096
+ interface ConnectionManager {
1097
+ // Validation
1098
+ canConnect(sourceId: string, targetId: string): boolean;
1099
+ validateConnection(edge: FlowEdge): ValidationResult;
1100
+ validateAllConnections(flow: Flow): ValidationResult[];
1101
+
1102
+ // Operations
1103
+ suggestConnections(nodeId: string, flow: Flow): SuggestedConnection[];
1104
+ autoConnect(flow: Flow): FlowEdge[];
1105
+ optimizeRouting(flow: Flow): Flow;
1106
+
1107
+ // Analysis
1108
+ findCycles(flow: Flow): string[][];
1109
+ getUnconnectedNodes(flow: Flow): string[];
1110
+ getDeadEnds(flow: Flow): string[];
1111
+ getOrphanedNodes(flow: Flow): string[];
1112
+ }
1113
+
1114
+ interface SuggestedConnection {
1115
+ sourceId: string;
1116
+ targetId: string;
1117
+ confidence: number;
1118
+ reason: string;
1119
+ }
1120
+ ```
1121
+
1122
+ ### Implementación
1123
+
1124
+ ```typescript
1125
+ class ConnectionManagerImpl implements ConnectionManager {
1126
+ private nodeRules: Map<NodeType, ConnectionRules> = new Map();
1127
+
1128
+ constructor() {
1129
+ this.initializeRules();
1130
+ }
1131
+
1132
+ private initializeRules(): void {
1133
+ // Define valid connections for each node type
1134
+ this.nodeRules.set('entry', {
1135
+ maxOutgoing: 1,
1136
+ maxIncoming: 0,
1137
+ allowedTargets: ['agent', 'parallel', 'sequence', 'condition', 'router', 'subflow']
1138
+ });
1139
+
1140
+ this.nodeRules.set('exit', {
1141
+ maxOutgoing: 0,
1142
+ maxIncoming: Infinity,
1143
+ allowedSources: ['agent', 'parallel', 'sequence', 'condition', 'router', 'loop', 'subflow']
1144
+ });
1145
+
1146
+ this.nodeRules.set('agent', {
1147
+ maxOutgoing: 1,
1148
+ maxIncoming: Infinity,
1149
+ allowedTargets: ['agent', 'parallel', 'sequence', 'condition', 'router', 'exit', 'loop', 'interrupt', 'subflow'],
1150
+ allowedSources: ['entry', 'agent', 'parallel', 'sequence', 'condition', 'router', 'loop', 'subflow']
1151
+ });
1152
+
1153
+ this.nodeRules.set('parallel', {
1154
+ maxOutgoing: 1,
1155
+ maxIncoming: Infinity,
1156
+ allowedTargets: ['agent', 'condition', 'router', 'exit', 'parallel', 'sequence'],
1157
+ allowedSources: ['entry', 'agent', 'condition', 'router', 'loop', 'sequence']
1158
+ });
1159
+
1160
+ this.nodeRules.set('condition', {
1161
+ maxOutgoing: 2, // true and false branches
1162
+ maxIncoming: Infinity,
1163
+ allowedTargets: ['agent', 'parallel', 'sequence', 'exit', 'loop', 'subflow'],
1164
+ allowedSources: ['agent', 'parallel', 'sequence', 'loop', 'entry']
1165
+ });
1166
+
1167
+ this.nodeRules.set('loop', {
1168
+ maxOutgoing: 2, // continue and exit
1169
+ maxIncoming: Infinity,
1170
+ allowedTargets: ['agent', 'parallel', 'condition', 'exit'],
1171
+ allowedSources: ['agent', 'parallel', 'condition']
1172
+ });
1173
+ }
1174
+
1175
+ canConnect(sourceId: string, targetId: string, flow: Flow): boolean {
1176
+ const result = this.validateConnectionAttempt(sourceId, targetId, flow);
1177
+ return result.valid;
1178
+ }
1179
+
1180
+ private validateConnectionAttempt(
1181
+ sourceId: string,
1182
+ targetId: string,
1183
+ flow: Flow
1184
+ ): ValidationResult {
1185
+ const errors: string[] = [];
1186
+
1187
+ const sourceNode = flow.nodes.find(n => n.id === sourceId);
1188
+ const targetNode = flow.nodes.find(n => n.id === targetId);
1189
+
1190
+ if (!sourceNode) {
1191
+ errors.push(`Source node not found: ${sourceId}`);
1192
+ return { valid: false, errors };
1193
+ }
1194
+
1195
+ if (!targetNode) {
1196
+ errors.push(`Target node not found: ${targetId}`);
1197
+ return { valid: false, errors };
1198
+ }
1199
+
1200
+ // Self-connection
1201
+ if (sourceId === targetId) {
1202
+ errors.push('Cannot connect node to itself');
1203
+ return { valid: false, errors };
1204
+ }
1205
+
1206
+ // Get rules
1207
+ const sourceRules = this.nodeRules.get(sourceNode.type);
1208
+ const targetRules = this.nodeRules.get(targetNode.type);
1209
+
1210
+ // Check outgoing limit
1211
+ if (sourceRules) {
1212
+ const outgoingCount = flow.edges.filter(e => e.source === sourceId).length;
1213
+ if (outgoingCount >= sourceRules.maxOutgoing) {
1214
+ errors.push(`${sourceNode.type} node has reached maximum outgoing connections`);
1215
+ }
1216
+
1217
+ // Check allowed targets
1218
+ if (sourceRules.allowedTargets && !sourceRules.allowedTargets.includes(targetNode.type)) {
1219
+ errors.push(`${sourceNode.type} cannot connect to ${targetNode.type}`);
1220
+ }
1221
+ }
1222
+
1223
+ // Check incoming limit
1224
+ if (targetRules) {
1225
+ const incomingCount = flow.edges.filter(e => e.target === targetId).length;
1226
+ if (incomingCount >= targetRules.maxIncoming) {
1227
+ errors.push(`${targetNode.type} node has reached maximum incoming connections`);
1228
+ }
1229
+
1230
+ // Check allowed sources
1231
+ if (targetRules.allowedSources && !targetRules.allowedSources.includes(sourceNode.type)) {
1232
+ errors.push(`${targetNode.type} cannot receive connection from ${sourceNode.type}`);
1233
+ }
1234
+ }
1235
+
1236
+ // Check for duplicate edge
1237
+ const exists = flow.edges.some(
1238
+ e => e.source === sourceId && e.target === targetId
1239
+ );
1240
+ if (exists) {
1241
+ errors.push('Connection already exists');
1242
+ }
1243
+
1244
+ // Check for cycles
1245
+ if (this.wouldCreateCycle(sourceId, targetId, flow)) {
1246
+ errors.push('Connection would create a cycle');
1247
+ }
1248
+
1249
+ return { valid: errors.length === 0, errors };
1250
+ }
1251
+
1252
+ private wouldCreateCycle(sourceId: string, targetId: string, flow: Flow): boolean {
1253
+ // Check if there's already a path from target to source
1254
+ const visited = new Set<string>();
1255
+ const stack = [targetId];
1256
+
1257
+ while (stack.length > 0) {
1258
+ const current = stack.pop()!;
1259
+ if (current === sourceId) return true;
1260
+ if (visited.has(current)) continue;
1261
+ visited.add(current);
1262
+
1263
+ const outgoing = flow.edges
1264
+ .filter(e => e.source === current)
1265
+ .map(e => e.target);
1266
+ stack.push(...outgoing);
1267
+ }
1268
+
1269
+ return false;
1270
+ }
1271
+
1272
+ suggestConnections(nodeId: string, flow: Flow): SuggestedConnection[] {
1273
+ const node = flow.nodes.find(n => n.id === nodeId);
1274
+ if (!node) return [];
1275
+
1276
+ const suggestions: SuggestedConnection[] = [];
1277
+ const rules = this.nodeRules.get(node.type);
1278
+
1279
+ // Current connections
1280
+ const outgoing = new Set(flow.edges.filter(e => e.source === nodeId).map(e => e.target));
1281
+ const incoming = new Set(flow.edges.filter(e => e.target === nodeId).map(e => e.source));
1282
+
1283
+ // Suggest outgoing connections
1284
+ if (rules && outgoing.size < rules.maxOutgoing) {
1285
+ for (const targetNode of flow.nodes) {
1286
+ if (targetNode.id === nodeId) continue;
1287
+ if (outgoing.has(targetNode.id)) continue;
1288
+
1289
+ if (this.canConnect(nodeId, targetNode.id, flow)) {
1290
+ suggestions.push({
1291
+ sourceId: nodeId,
1292
+ targetId: targetNode.id,
1293
+ confidence: this.calculateConfidence(node, targetNode, flow),
1294
+ reason: this.getConnectionReason(node, targetNode)
1295
+ });
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ // Sort by confidence
1301
+ return suggestions.sort((a, b) => b.confidence - a.confidence);
1302
+ }
1303
+
1304
+ private calculateConfidence(source: FlowNode, target: FlowNode, flow: Flow): number {
1305
+ let confidence = 0.5;
1306
+
1307
+ // Higher confidence for natural flow (entry -> agent -> exit)
1308
+ const naturalOrder: NodeType[] = ['entry', 'agent', 'parallel', 'condition', 'agent', 'exit'];
1309
+ const sourceIndex = naturalOrder.indexOf(source.type);
1310
+ const targetIndex = naturalOrder.indexOf(target.type);
1311
+ if (sourceIndex >= 0 && targetIndex > sourceIndex) {
1312
+ confidence += 0.3;
1313
+ }
1314
+
1315
+ // Lower confidence if target already has incoming connections
1316
+ const targetIncoming = flow.edges.filter(e => e.target === target.id).length;
1317
+ if (targetIncoming > 0) {
1318
+ confidence -= 0.1 * targetIncoming;
1319
+ }
1320
+
1321
+ return Math.max(0, Math.min(1, confidence));
1322
+ }
1323
+
1324
+ private getConnectionReason(source: FlowNode, target: FlowNode): string {
1325
+ if (source.type === 'entry') return 'Start flow execution';
1326
+ if (target.type === 'exit') return 'Complete flow execution';
1327
+ if (source.type === 'condition') return 'Branch based on condition';
1328
+ if (target.type === 'parallel') return 'Execute in parallel';
1329
+ return 'Continue flow execution';
1330
+ }
1331
+
1332
+ findCycles(flow: Flow): string[][] {
1333
+ const cycles: string[][] = [];
1334
+ const visited = new Set<string>();
1335
+ const recStack = new Set<string>();
1336
+ const path: string[] = [];
1337
+
1338
+ const dfs = (nodeId: string): void => {
1339
+ visited.add(nodeId);
1340
+ recStack.add(nodeId);
1341
+ path.push(nodeId);
1342
+
1343
+ const outgoing = flow.edges
1344
+ .filter(e => e.source === nodeId)
1345
+ .map(e => e.target);
1346
+
1347
+ for (const neighbor of outgoing) {
1348
+ if (!visited.has(neighbor)) {
1349
+ dfs(neighbor);
1350
+ } else if (recStack.has(neighbor)) {
1351
+ // Found cycle
1352
+ const cycleStart = path.indexOf(neighbor);
1353
+ cycles.push(path.slice(cycleStart));
1354
+ }
1355
+ }
1356
+
1357
+ path.pop();
1358
+ recStack.delete(nodeId);
1359
+ };
1360
+
1361
+ for (const node of flow.nodes) {
1362
+ if (!visited.has(node.id)) {
1363
+ dfs(node.id);
1364
+ }
1365
+ }
1366
+
1367
+ return cycles;
1368
+ }
1369
+
1370
+ getUnconnectedNodes(flow: Flow): string[] {
1371
+ const connectedNodes = new Set<string>();
1372
+
1373
+ for (const edge of flow.edges) {
1374
+ connectedNodes.add(edge.source);
1375
+ connectedNodes.add(edge.target);
1376
+ }
1377
+
1378
+ return flow.nodes
1379
+ .filter(n => !connectedNodes.has(n.id))
1380
+ .map(n => n.id);
1381
+ }
1382
+
1383
+ getDeadEnds(flow: Flow): string[] {
1384
+ // Nodes with incoming but no outgoing (except exit nodes)
1385
+ return flow.nodes
1386
+ .filter(node => {
1387
+ if (node.type === 'exit') return false;
1388
+ const hasIncoming = flow.edges.some(e => e.target === node.id);
1389
+ const hasOutgoing = flow.edges.some(e => e.source === node.id);
1390
+ return hasIncoming && !hasOutgoing;
1391
+ })
1392
+ .map(n => n.id);
1393
+ }
1394
+
1395
+ getOrphanedNodes(flow: Flow): string[] {
1396
+ // Nodes with no incoming connections (except entry nodes)
1397
+ return flow.nodes
1398
+ .filter(node => {
1399
+ if (node.type === 'entry') return false;
1400
+ return !flow.edges.some(e => e.target === node.id);
1401
+ })
1402
+ .map(n => n.id);
1403
+ }
1404
+ }
1405
+
1406
+ interface ConnectionRules {
1407
+ maxOutgoing: number;
1408
+ maxIncoming: number;
1409
+ allowedTargets?: NodeType[];
1410
+ allowedSources?: NodeType[];
1411
+ }
1412
+ ```
1413
+
1414
+ ---
1415
+
1416
+ ## 5. Comandos CLI
1417
+
1418
+ ### /elsabro:flow visualize
1419
+
1420
+ ```bash
1421
+ # Ver flow actual
1422
+ /elsabro:flow visualize
1423
+
1424
+ # Ver flow específico
1425
+ /elsabro:flow visualize development_flow
1426
+
1427
+ # Exportar a SVG
1428
+ /elsabro:flow visualize --export svg --output flow.svg
1429
+
1430
+ # Modo editor interactivo
1431
+ /elsabro:flow edit development_flow
1432
+ ```
1433
+
1434
+ ### Output de Visualización
1435
+
1436
+ ```
1437
+ ╔══════════════════════════════════════════════════════════════════════════════╗
1438
+ ║ Flow: development_flow ║
1439
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1440
+ ║ Status: Active │ Nodes: 8 │ Edges: 9 │ Cycles: 0 ║
1441
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1442
+
1443
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
1444
+ │ ▶ start │─────────→│ ⫴ explore │─────────→│ ◉ implement │
1445
+ │ [entry] │ │ [parallel] │ │ [agent] │
1446
+ └─────────────┘ │ 4 branches │ │ opus │
1447
+ └─────────────┘ └──────┬──────┘
1448
+
1449
+ ┌────────────────────────┘
1450
+
1451
+ ┌─────────────┐ ┌─────────────┐
1452
+ │ ◇ hasErrors │─── no ──→│ ⫴ review │
1453
+ │ [condition] │ │ [parallel] │
1454
+ └──────┬──────┘ │ 3 branches │
1455
+ │ yes └──────┬──────┘
1456
+ ↓ │
1457
+ ┌─────────────┐ │
1458
+ │ ↻ fixLoop │ │
1459
+ │ [loop] │←────────────────┘
1460
+ │ max: 3 │ (if errors)
1461
+ └─────────────┘
1462
+
1463
+ ↓ (success)
1464
+ ┌─────────────┐
1465
+ │ ■ complete │
1466
+ │ [exit] │
1467
+ └─────────────┘
1468
+
1469
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1470
+ ║ Legend: ▶Entry ■Exit ◉Agent ⫴Parallel ◇Condition ↻Loop ⊞Subflow ║
1471
+ ╚══════════════════════════════════════════════════════════════════════════════╝
1472
+ ```
1473
+
1474
+ ---
1475
+
1476
+ ## 6. Integración con ELSABRO
1477
+
1478
+ ### Inicialización
1479
+
1480
+ ```typescript
1481
+ // visualization/index.ts
1482
+ import { FlowVisualizer } from './FlowVisualizer';
1483
+ import { ASCIIRenderer, SVGRenderer } from './renderers';
1484
+ import { NodeEditor } from './NodeEditor';
1485
+ import { ConnectionManager } from './ConnectionManager';
1486
+
1487
+ export function createFlowVisualization(flow: Flow, options?: Partial<FlowVisualizerOptions>): FlowVisualizer {
1488
+ return new FlowVisualizer({
1489
+ flow,
1490
+ mode: 'view',
1491
+ renderer: 'ascii',
1492
+ layout: 'dagre',
1493
+ theme: defaultTheme,
1494
+ ...options
1495
+ });
1496
+ }
1497
+
1498
+ export function visualizeFlowASCII(flow: Flow): string {
1499
+ const visualizer = createFlowVisualization(flow, { renderer: 'ascii' });
1500
+ return visualizer.exportASCII();
1501
+ }
1502
+
1503
+ export function editFlow(flow: Flow): FlowVisualizer {
1504
+ return createFlowVisualization(flow, {
1505
+ mode: 'edit',
1506
+ showToolbar: true,
1507
+ showMinimap: true
1508
+ });
1509
+ }
1510
+ ```
1511
+
1512
+ ### Uso en Comandos
1513
+
1514
+ ```typescript
1515
+ // commands/flow-visualize.ts
1516
+ export async function handleFlowVisualize(args: string[]): Promise<void> {
1517
+ const flowId = args[0] || getCurrentFlowId();
1518
+ const flow = await loadFlow(flowId);
1519
+
1520
+ if (!flow) {
1521
+ console.error(`Flow not found: ${flowId}`);
1522
+ return;
1523
+ }
1524
+
1525
+ const visualizer = createFlowVisualization(flow);
1526
+ const ascii = visualizer.exportASCII();
1527
+
1528
+ console.log(ascii);
1529
+
1530
+ // Show validation
1531
+ const connectionManager = new ConnectionManager();
1532
+ const unconnected = connectionManager.getUnconnectedNodes(flow);
1533
+ const deadEnds = connectionManager.getDeadEnds(flow);
1534
+ const cycles = connectionManager.findCycles(flow);
1535
+
1536
+ if (unconnected.length > 0 || deadEnds.length > 0 || cycles.length > 0) {
1537
+ console.log('\n⚠️ Flow Issues:');
1538
+ if (unconnected.length > 0) {
1539
+ console.log(` • Unconnected nodes: ${unconnected.join(', ')}`);
1540
+ }
1541
+ if (deadEnds.length > 0) {
1542
+ console.log(` • Dead ends: ${deadEnds.join(', ')}`);
1543
+ }
1544
+ if (cycles.length > 0) {
1545
+ console.log(` • Cycles detected: ${cycles.length}`);
1546
+ }
1547
+ }
1548
+ }
1549
+ ```
1550
+
1551
+ ---
1552
+
1553
+ ## Referencias
1554
+
1555
+ - **REF-010**: Flow Orchestration
1556
+ - **REF-016**: Time-Travel Debugging
1557
+ - **REF-028**: Esta referencia (Flow Visualization)