cognitive-modules-cli 2.2.1 → 2.2.7

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 (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +519 -23
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +383 -16
  7. package/dist/commands/compose.js +60 -23
  8. package/dist/commands/index.d.ts +4 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/init.js +23 -1
  11. package/dist/commands/migrate.d.ts +30 -0
  12. package/dist/commands/migrate.js +650 -0
  13. package/dist/commands/pipe.d.ts +1 -0
  14. package/dist/commands/pipe.js +31 -11
  15. package/dist/commands/remove.js +33 -2
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +61 -28
  18. package/dist/commands/search.d.ts +28 -0
  19. package/dist/commands/search.js +143 -0
  20. package/dist/commands/test.d.ts +65 -0
  21. package/dist/commands/test.js +454 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +106 -14
  24. package/dist/commands/validate.d.ts +36 -0
  25. package/dist/commands/validate.js +97 -0
  26. package/dist/errors/index.d.ts +225 -0
  27. package/dist/errors/index.js +420 -0
  28. package/dist/mcp/server.js +84 -79
  29. package/dist/modules/composition.js +97 -32
  30. package/dist/modules/loader.js +4 -2
  31. package/dist/modules/runner.d.ts +72 -5
  32. package/dist/modules/runner.js +306 -59
  33. package/dist/modules/subagent.d.ts +6 -1
  34. package/dist/modules/subagent.js +18 -13
  35. package/dist/modules/validator.js +14 -6
  36. package/dist/providers/anthropic.d.ts +15 -0
  37. package/dist/providers/anthropic.js +147 -5
  38. package/dist/providers/base.d.ts +11 -0
  39. package/dist/providers/base.js +18 -0
  40. package/dist/providers/gemini.d.ts +15 -0
  41. package/dist/providers/gemini.js +122 -5
  42. package/dist/providers/ollama.d.ts +15 -0
  43. package/dist/providers/ollama.js +111 -3
  44. package/dist/providers/openai.d.ts +11 -0
  45. package/dist/providers/openai.js +133 -0
  46. package/dist/registry/client.d.ts +212 -0
  47. package/dist/registry/client.js +359 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/registry/tar.d.ts +8 -0
  51. package/dist/registry/tar.js +353 -0
  52. package/dist/server/http.js +301 -45
  53. package/dist/server/index.d.ts +2 -0
  54. package/dist/server/index.js +1 -0
  55. package/dist/server/sse.d.ts +13 -0
  56. package/dist/server/sse.js +22 -0
  57. package/dist/types.d.ts +32 -1
  58. package/dist/types.js +4 -1
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +4 -0
  61. package/package.json +31 -7
  62. package/dist/modules/composition.test.d.ts +0 -11
  63. package/dist/modules/composition.test.js +0 -450
  64. package/dist/modules/policy.test.d.ts +0 -10
  65. package/dist/modules/policy.test.js +0 -369
  66. package/src/cli.ts +0 -471
  67. package/src/commands/add.ts +0 -315
  68. package/src/commands/compose.ts +0 -185
  69. package/src/commands/index.ts +0 -13
  70. package/src/commands/init.ts +0 -94
  71. package/src/commands/list.ts +0 -33
  72. package/src/commands/pipe.ts +0 -76
  73. package/src/commands/remove.ts +0 -57
  74. package/src/commands/run.ts +0 -80
  75. package/src/commands/update.ts +0 -130
  76. package/src/commands/versions.ts +0 -79
  77. package/src/index.ts +0 -90
  78. package/src/mcp/index.ts +0 -5
  79. package/src/mcp/server.ts +0 -403
  80. package/src/modules/composition.test.ts +0 -558
  81. package/src/modules/composition.ts +0 -1674
  82. package/src/modules/index.ts +0 -9
  83. package/src/modules/loader.ts +0 -508
  84. package/src/modules/policy.test.ts +0 -455
  85. package/src/modules/runner.ts +0 -1983
  86. package/src/modules/subagent.ts +0 -277
  87. package/src/modules/validator.ts +0 -700
  88. package/src/providers/anthropic.ts +0 -89
  89. package/src/providers/base.ts +0 -29
  90. package/src/providers/deepseek.ts +0 -83
  91. package/src/providers/gemini.ts +0 -117
  92. package/src/providers/index.ts +0 -78
  93. package/src/providers/minimax.ts +0 -81
  94. package/src/providers/moonshot.ts +0 -82
  95. package/src/providers/ollama.ts +0 -83
  96. package/src/providers/openai.ts +0 -84
  97. package/src/providers/qwen.ts +0 -82
  98. package/src/server/http.ts +0 -316
  99. package/src/server/index.ts +0 -6
  100. package/src/types.ts +0 -599
  101. package/tsconfig.json +0 -17
@@ -1,1674 +0,0 @@
1
- /**
2
- * Composition Engine - Module Composition and Orchestration
3
- *
4
- * Implements COMPOSITION.md specification:
5
- * - Sequential Composition: A → B → C
6
- * - Parallel Composition: A → [B, C, D] → Aggregator
7
- * - Conditional Composition: A → (condition) → B or C
8
- * - Iterative Composition: A → (check) → A → ... → Done
9
- * - Dataflow Mapping: JSONPath-like expressions
10
- * - Aggregation Strategies: merge, array, first, custom
11
- * - Dependency Resolution with fallbacks
12
- * - Timeout handling
13
- * - Circular dependency detection
14
- */
15
-
16
- import type {
17
- CognitiveModule,
18
- ModuleResult,
19
- ModuleInput,
20
- Provider,
21
- EnvelopeResponseV22,
22
- EnvelopeMeta,
23
- RiskLevel
24
- } from '../types.js';
25
- import { loadModule, findModule, getDefaultSearchPaths } from './loader.js';
26
- import { runModule } from './runner.js';
27
- import { isV22Envelope, aggregateRisk } from '../types.js';
28
-
29
- // =============================================================================
30
- // Composition Types
31
- // =============================================================================
32
-
33
- /** Composition pattern types */
34
- export type CompositionPattern = 'sequential' | 'parallel' | 'conditional' | 'iterative';
35
-
36
- /** Aggregation strategy for combining multiple outputs */
37
- export type AggregationStrategy = 'merge' | 'array' | 'first' | 'custom';
38
-
39
- /** Semver-like version matching pattern */
40
- export type VersionPattern = string; // e.g., ">=1.0.0", "^1.0.0", "~1.0.0", "*"
41
-
42
- /** Dependency declaration for composition.requires */
43
- export interface DependencyDeclaration {
44
- /** Module name */
45
- name: string;
46
- /** Semver version pattern */
47
- version?: VersionPattern;
48
- /** Whether dependency is optional */
49
- optional?: boolean;
50
- /** Fallback module if unavailable */
51
- fallback?: string | null;
52
- /** Per-module timeout (ms) */
53
- timeout_ms?: number;
54
- }
55
-
56
- /** Dataflow mapping expression */
57
- export interface DataflowMapping {
58
- [key: string]: string; // target_field: "$.source.path"
59
- }
60
-
61
- /** Condition expression for routing */
62
- export interface ConditionExpression {
63
- expression: string; // e.g., "$.meta.confidence > 0.7"
64
- }
65
-
66
- /** Dataflow step configuration */
67
- export interface DataflowStep {
68
- /** Source of data: 'input' or 'module-name.output' */
69
- from: string | string[];
70
- /** Destination: module name or 'output' */
71
- to: string | string[];
72
- /** Field mapping expressions */
73
- mapping?: DataflowMapping;
74
- /** Condition for execution */
75
- condition?: string;
76
- /** Aggregation strategy when from is an array */
77
- aggregate?: AggregationStrategy;
78
- /** Custom aggregation function name */
79
- aggregator?: string;
80
- }
81
-
82
- /** Conditional routing rule */
83
- export interface RoutingRule {
84
- /** Condition expression */
85
- condition: string;
86
- /** Next module to execute (null means use current result) */
87
- next: string | null;
88
- }
89
-
90
- /** Full composition configuration (from module.yaml) */
91
- export interface CompositionConfig {
92
- /** Composition pattern */
93
- pattern: CompositionPattern;
94
- /** Required dependencies */
95
- requires?: DependencyDeclaration[];
96
- /** Dataflow configuration */
97
- dataflow?: DataflowStep[];
98
- /** Conditional routing rules */
99
- routing?: RoutingRule[];
100
- /** Maximum composition depth */
101
- max_depth?: number;
102
- /** Total timeout for composition (ms) */
103
- timeout_ms?: number;
104
- /** Iteration configuration */
105
- iteration?: {
106
- /** Maximum iterations */
107
- max_iterations?: number;
108
- /** Condition to continue iterating */
109
- continue_condition?: string;
110
- /** Condition to stop iterating */
111
- stop_condition?: string;
112
- };
113
- }
114
-
115
- /** Execution context for composition */
116
- export interface CompositionContext {
117
- /** Current execution depth */
118
- depth: number;
119
- /** Maximum allowed depth */
120
- maxDepth: number;
121
- /** Results from completed modules */
122
- results: Record<string, ModuleResult>;
123
- /** Original input data */
124
- input: ModuleInput;
125
- /** Currently running modules (for circular detection) */
126
- running: Set<string>;
127
- /** Start time for timeout tracking */
128
- startTime: number;
129
- /** Total timeout (ms) */
130
- timeoutMs?: number;
131
- /** Iteration count (for iterative composition) */
132
- iterationCount: number;
133
- }
134
-
135
- /** Result of composition execution */
136
- export interface CompositionResult {
137
- /** Whether composition succeeded */
138
- ok: boolean;
139
- /** Final aggregated result */
140
- result?: ModuleResult;
141
- /** Results from all executed modules */
142
- moduleResults: Record<string, ModuleResult>;
143
- /** Execution trace for debugging */
144
- trace: ExecutionTrace[];
145
- /** Total execution time (ms) */
146
- totalTimeMs: number;
147
- /** Error if composition failed */
148
- error?: {
149
- code: string;
150
- message: string;
151
- module?: string;
152
- };
153
- }
154
-
155
- /** Execution trace entry */
156
- export interface ExecutionTrace {
157
- module: string;
158
- startTime: number;
159
- endTime: number;
160
- durationMs: number;
161
- success: boolean;
162
- skipped?: boolean;
163
- reason?: string;
164
- }
165
-
166
- // =============================================================================
167
- // Error Codes for Composition
168
- // =============================================================================
169
-
170
- export const COMPOSITION_ERRORS = {
171
- E4004: 'CIRCULAR_DEPENDENCY',
172
- E4005: 'MAX_DEPTH_EXCEEDED',
173
- E4008: 'COMPOSITION_TIMEOUT',
174
- E4009: 'DEPENDENCY_NOT_FOUND',
175
- E4010: 'DATAFLOW_ERROR',
176
- E4011: 'CONDITION_EVAL_ERROR',
177
- E4012: 'AGGREGATION_ERROR',
178
- E4013: 'ITERATION_LIMIT_EXCEEDED',
179
- } as const;
180
-
181
- // =============================================================================
182
- // JSONPath-like Expression Parser
183
- // =============================================================================
184
-
185
- /**
186
- * Parse and evaluate JSONPath-like expressions.
187
- *
188
- * Supported syntax:
189
- * - $.field - Root field access
190
- * - $.nested.field - Nested access
191
- * - $.array[0] - Array index
192
- * - $.array[*].field - Array map
193
- * - $ - Entire object
194
- */
195
- export function evaluateJsonPath(expression: string, data: unknown): unknown {
196
- if (!expression.startsWith('$')) {
197
- return expression; // Literal value
198
- }
199
-
200
- if (expression === '$') {
201
- return data;
202
- }
203
-
204
- const path = expression.slice(1); // Remove leading $
205
- const segments = parsePathSegments(path);
206
-
207
- let current: unknown = data;
208
-
209
- for (const segment of segments) {
210
- if (current === null || current === undefined) {
211
- return undefined;
212
- }
213
-
214
- if (segment.type === 'field' && segment.name !== undefined) {
215
- if (typeof current !== 'object') {
216
- return undefined;
217
- }
218
- current = (current as Record<string, unknown>)[segment.name];
219
- } else if (segment.type === 'index' && segment.index !== undefined) {
220
- if (!Array.isArray(current)) {
221
- return undefined;
222
- }
223
- current = current[segment.index];
224
- } else if (segment.type === 'wildcard') {
225
- if (!Array.isArray(current)) {
226
- return undefined;
227
- }
228
- // Map over array
229
- const remainingSegments = segment.remaining ?? [];
230
- current = current.map(item => {
231
- let result: unknown = item;
232
- for (const remainingSegment of remainingSegments) {
233
- if (result === null || result === undefined) {
234
- return undefined;
235
- }
236
- if (remainingSegment.type === 'field' && remainingSegment.name !== undefined) {
237
- result = (result as Record<string, unknown>)[remainingSegment.name];
238
- }
239
- }
240
- return result;
241
- });
242
- break; // Wildcard consumes remaining path
243
- }
244
- }
245
-
246
- return current;
247
- }
248
-
249
- interface PathSegment {
250
- type: 'field' | 'index' | 'wildcard';
251
- name?: string;
252
- index?: number;
253
- remaining?: PathSegment[];
254
- }
255
-
256
- function parsePathSegments(path: string): PathSegment[] {
257
- const segments: PathSegment[] = [];
258
- let remaining = path;
259
-
260
- while (remaining.length > 0) {
261
- // Remove leading dot
262
- if (remaining.startsWith('.')) {
263
- remaining = remaining.slice(1);
264
- }
265
-
266
- // Array index: [0]
267
- const indexMatch = remaining.match(/^\[(\d+)\]/);
268
- if (indexMatch) {
269
- segments.push({ type: 'index', index: parseInt(indexMatch[1], 10) });
270
- remaining = remaining.slice(indexMatch[0].length);
271
- continue;
272
- }
273
-
274
- // Array wildcard: [*]
275
- const wildcardMatch = remaining.match(/^\[\*\]/);
276
- if (wildcardMatch) {
277
- remaining = remaining.slice(wildcardMatch[0].length);
278
- // Parse remaining path for wildcard
279
- const remainingSegments = parsePathSegments(remaining);
280
- segments.push({ type: 'wildcard', remaining: remainingSegments });
281
- break; // Wildcard consumes the rest
282
- }
283
-
284
- // Field name (support hyphens in field names like quick-check)
285
- const fieldMatch = remaining.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/);
286
- if (fieldMatch) {
287
- segments.push({ type: 'field', name: fieldMatch[1] });
288
- remaining = remaining.slice(fieldMatch[0].length);
289
- continue;
290
- }
291
-
292
- // Unknown segment, break
293
- break;
294
- }
295
-
296
- return segments;
297
- }
298
-
299
- /**
300
- * Apply dataflow mapping to transform data
301
- */
302
- export function applyMapping(
303
- mapping: DataflowMapping,
304
- sourceData: unknown
305
- ): Record<string, unknown> {
306
- const result: Record<string, unknown> = {};
307
-
308
- for (const [targetField, sourceExpr] of Object.entries(mapping)) {
309
- result[targetField] = evaluateJsonPath(sourceExpr, sourceData);
310
- }
311
-
312
- return result;
313
- }
314
-
315
- // =============================================================================
316
- // Condition Expression Evaluator
317
- // =============================================================================
318
-
319
- /**
320
- * Evaluate condition expressions.
321
- *
322
- * Supported operators:
323
- * - Comparison: ==, !=, >, <, >=, <=
324
- * - Logical: &&, ||, !
325
- * - Existence: exists($.field)
326
- * - String: contains($.field, "value")
327
- */
328
- export function evaluateCondition(expression: string, data: unknown): boolean {
329
- try {
330
- // Handle exists() function
331
- const existsMatch = expression.match(/exists\(([^)]+)\)/);
332
- if (existsMatch) {
333
- const value = evaluateJsonPath(existsMatch[1].trim(), data);
334
- const exists = value !== undefined && value !== null;
335
- const remainingExpr = expression.replace(existsMatch[0], exists ? 'true' : 'false');
336
- if (remainingExpr.trim() === 'true' || remainingExpr.trim() === 'false') {
337
- return remainingExpr.trim() === 'true';
338
- }
339
- return evaluateCondition(remainingExpr, data);
340
- }
341
-
342
- // Handle contains() function - supports both string and array
343
- const containsMatch = expression.match(/contains\(([^,]+),\s*["']([^"']+)["']\)/);
344
- if (containsMatch) {
345
- const value = evaluateJsonPath(containsMatch[1].trim(), data);
346
- const search = containsMatch[2];
347
- let contains = false;
348
- if (typeof value === 'string') {
349
- contains = value.includes(search);
350
- } else if (Array.isArray(value)) {
351
- contains = value.includes(search);
352
- }
353
- const remainingExpr = expression.replace(containsMatch[0], contains ? 'true' : 'false');
354
- return evaluateCondition(remainingExpr, data);
355
- }
356
-
357
- // Handle length property (support hyphens in field names)
358
- const lengthMatch = expression.match(/(\$[a-zA-Z0-9._\[\]*-]+)\.length/g);
359
- if (lengthMatch) {
360
- let processedExpr = expression;
361
- for (const match of lengthMatch) {
362
- const pathPart = match.replace('.length', '');
363
- const value = evaluateJsonPath(pathPart, data);
364
- const length = Array.isArray(value) ? value.length :
365
- typeof value === 'string' ? value.length : 0;
366
- processedExpr = processedExpr.replace(match, String(length));
367
- }
368
- expression = processedExpr;
369
- }
370
-
371
- // Replace JSONPath expressions with values (support hyphens in field names)
372
- const jsonPathMatches = expression.match(/\$[a-zA-Z0-9._\[\]*-]+/g);
373
- if (jsonPathMatches) {
374
- let processedExpr = expression;
375
- for (const match of jsonPathMatches) {
376
- const value = evaluateJsonPath(match, data);
377
- let replacement: string;
378
-
379
- if (value === undefined || value === null) {
380
- replacement = 'null';
381
- } else if (typeof value === 'string') {
382
- replacement = `"${value}"`;
383
- } else if (typeof value === 'boolean') {
384
- replacement = value ? 'true' : 'false';
385
- } else if (typeof value === 'number') {
386
- replacement = String(value);
387
- } else {
388
- replacement = JSON.stringify(value);
389
- }
390
-
391
- processedExpr = processedExpr.replace(match, replacement);
392
- }
393
- expression = processedExpr;
394
- }
395
-
396
- // Evaluate the expression safely
397
- return safeEval(expression);
398
- } catch (error) {
399
- console.error(`Failed to evaluate condition: ${expression}`, error);
400
- return false;
401
- }
402
- }
403
-
404
- /**
405
- * Safe expression evaluator (no eval())
406
- */
407
- function safeEval(expression: string): boolean {
408
- // Remove whitespace
409
- expression = expression.trim();
410
-
411
- // Handle logical operators (lowest precedence)
412
- // Handle || first
413
- const orParts = splitByOperator(expression, '||');
414
- if (orParts.length > 1) {
415
- return orParts.some(part => safeEval(part));
416
- }
417
-
418
- // Handle &&
419
- const andParts = splitByOperator(expression, '&&');
420
- if (andParts.length > 1) {
421
- return andParts.every(part => safeEval(part));
422
- }
423
-
424
- // Handle ! (not)
425
- if (expression.startsWith('!')) {
426
- return !safeEval(expression.slice(1));
427
- }
428
-
429
- // Handle parentheses
430
- if (expression.startsWith('(') && expression.endsWith(')')) {
431
- return safeEval(expression.slice(1, -1));
432
- }
433
-
434
- // Handle comparison operators
435
- const comparisonOps = ['!==', '===', '!=', '==', '>=', '<=', '>', '<'];
436
- for (const op of comparisonOps) {
437
- const opIndex = expression.indexOf(op);
438
- if (opIndex !== -1) {
439
- const left = parseValue(expression.slice(0, opIndex).trim());
440
- const right = parseValue(expression.slice(opIndex + op.length).trim());
441
-
442
- switch (op) {
443
- case '===':
444
- case '==':
445
- return left === right;
446
- case '!==':
447
- case '!=':
448
- return left !== right;
449
- case '>':
450
- return (left as number) > (right as number);
451
- case '<':
452
- return (left as number) < (right as number);
453
- case '>=':
454
- return (left as number) >= (right as number);
455
- case '<=':
456
- return (left as number) <= (right as number);
457
- }
458
- }
459
- }
460
-
461
- // Handle boolean literals
462
- if (expression === 'true') return true;
463
- if (expression === 'false') return false;
464
-
465
- // Handle truthy/falsy
466
- const value = parseValue(expression);
467
- return Boolean(value);
468
- }
469
-
470
- function splitByOperator(expression: string, operator: string): string[] {
471
- const parts: string[] = [];
472
- let current = '';
473
- let depth = 0;
474
- let inString = false;
475
- let stringChar = '';
476
-
477
- for (let i = 0; i < expression.length; i++) {
478
- const char = expression[i];
479
-
480
- // Handle string literals
481
- if ((char === '"' || char === "'") && expression[i - 1] !== '\\') {
482
- if (!inString) {
483
- inString = true;
484
- stringChar = char;
485
- } else if (char === stringChar) {
486
- inString = false;
487
- }
488
- }
489
-
490
- // Handle parentheses depth
491
- if (!inString) {
492
- if (char === '(') depth++;
493
- if (char === ')') depth--;
494
- }
495
-
496
- // Check for operator
497
- if (!inString && depth === 0 && expression.slice(i, i + operator.length) === operator) {
498
- parts.push(current.trim());
499
- current = '';
500
- i += operator.length - 1;
501
- continue;
502
- }
503
-
504
- current += char;
505
- }
506
-
507
- if (current.trim()) {
508
- parts.push(current.trim());
509
- }
510
-
511
- return parts;
512
- }
513
-
514
- function parseValue(str: string): unknown {
515
- str = str.trim();
516
-
517
- // Null
518
- if (str === 'null') return null;
519
-
520
- // Boolean
521
- if (str === 'true') return true;
522
- if (str === 'false') return false;
523
-
524
- // String (quoted)
525
- if ((str.startsWith('"') && str.endsWith('"')) ||
526
- (str.startsWith("'") && str.endsWith("'"))) {
527
- return str.slice(1, -1);
528
- }
529
-
530
- // Number
531
- const num = Number(str);
532
- if (!isNaN(num)) return num;
533
-
534
- // Return as string
535
- return str;
536
- }
537
-
538
- // =============================================================================
539
- // Aggregation Strategies
540
- // =============================================================================
541
-
542
- /**
543
- * Deep merge two objects (later wins on conflict)
544
- */
545
- function deepMerge(target: unknown, source: unknown): unknown {
546
- if (source === null || source === undefined) {
547
- return target;
548
- }
549
-
550
- if (typeof source !== 'object' || typeof target !== 'object') {
551
- return source;
552
- }
553
-
554
- if (Array.isArray(source)) {
555
- return source;
556
- }
557
-
558
- const result: Record<string, unknown> = { ...(target as Record<string, unknown>) };
559
-
560
- for (const [key, value] of Object.entries(source as Record<string, unknown>)) {
561
- if (key in result && typeof result[key] === 'object' && typeof value === 'object') {
562
- result[key] = deepMerge(result[key], value);
563
- } else {
564
- result[key] = value;
565
- }
566
- }
567
-
568
- return result;
569
- }
570
-
571
- /**
572
- * Aggregate multiple results using specified strategy
573
- */
574
- export function aggregateResults(
575
- results: ModuleResult[],
576
- strategy: AggregationStrategy
577
- ): ModuleResult {
578
- if (results.length === 0) {
579
- return {
580
- ok: false,
581
- meta: { confidence: 0, risk: 'high', explain: 'No results to aggregate' },
582
- error: { code: 'E4012', message: 'No results to aggregate' }
583
- } as ModuleResult;
584
- }
585
-
586
- if (results.length === 1) {
587
- return results[0];
588
- }
589
-
590
- switch (strategy) {
591
- case 'first': {
592
- // Return first non-null successful result
593
- const firstSuccess = results.find(r => r.ok);
594
- return firstSuccess ?? results[0];
595
- }
596
-
597
- case 'array': {
598
- // Collect all results into an array
599
- const allData = results
600
- .filter(r => r.ok && 'data' in r)
601
- .map(r => (r as { data: unknown }).data);
602
-
603
- const allMeta = results
604
- .filter(r => 'meta' in r)
605
- .map(r => (r as { meta: EnvelopeMeta }).meta);
606
-
607
- // Compute aggregate meta
608
- const avgConfidence = allMeta.length > 0
609
- ? allMeta.reduce((sum, m) => sum + m.confidence, 0) / allMeta.length
610
- : 0.5;
611
-
612
- const maxRisk = allMeta.length > 0
613
- ? (['none', 'low', 'medium', 'high'] as RiskLevel[])[
614
- Math.max(...allMeta.map(m =>
615
- ['none', 'low', 'medium', 'high'].indexOf(m.risk)
616
- ))
617
- ]
618
- : 'medium';
619
-
620
- return {
621
- ok: true,
622
- meta: {
623
- confidence: avgConfidence,
624
- risk: maxRisk,
625
- explain: `Aggregated ${allData.length} results`
626
- },
627
- data: {
628
- results: allData,
629
- rationale: `Combined ${allData.length} module outputs into array`
630
- }
631
- } as ModuleResult;
632
- }
633
-
634
- case 'merge':
635
- default: {
636
- // Deep merge all results (later wins)
637
- let mergedData: unknown = {};
638
- let mergedMeta: EnvelopeMeta = {
639
- confidence: 0.5,
640
- risk: 'medium',
641
- explain: ''
642
- };
643
-
644
- const explains: string[] = [];
645
- let totalConfidence = 0;
646
- let maxRiskLevel = 0;
647
- const riskLevels: Record<RiskLevel, number> = { none: 0, low: 1, medium: 2, high: 3 };
648
- const riskNames: RiskLevel[] = ['none', 'low', 'medium', 'high'];
649
-
650
- for (const result of results) {
651
- if (result.ok && 'data' in result) {
652
- mergedData = deepMerge(mergedData, (result as { data: unknown }).data);
653
- }
654
-
655
- if ('meta' in result) {
656
- const meta = (result as { meta: EnvelopeMeta }).meta;
657
- totalConfidence += meta.confidence;
658
- maxRiskLevel = Math.max(maxRiskLevel, riskLevels[meta.risk] ?? 2);
659
- if (meta.explain) {
660
- explains.push(meta.explain);
661
- }
662
- }
663
- }
664
-
665
- mergedMeta = {
666
- confidence: totalConfidence / results.length,
667
- risk: riskNames[maxRiskLevel],
668
- explain: explains.join('; ').slice(0, 280)
669
- };
670
-
671
- return {
672
- ok: true,
673
- meta: mergedMeta,
674
- data: {
675
- ...(mergedData as Record<string, unknown>),
676
- rationale: `Merged ${results.length} module outputs`
677
- }
678
- } as ModuleResult;
679
- }
680
- }
681
- }
682
-
683
- // =============================================================================
684
- // Dependency Resolution
685
- // =============================================================================
686
-
687
- /**
688
- * Check if version matches pattern (simplified semver)
689
- */
690
- export function versionMatches(version: string, pattern: string): boolean {
691
- if (!pattern || pattern === '*') {
692
- return true;
693
- }
694
-
695
- // Parse version into parts
696
- const parseVersion = (v: string): number[] => {
697
- return v.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
698
- };
699
-
700
- const vParts = parseVersion(version);
701
-
702
- // Exact match
703
- if (!pattern.startsWith('^') && !pattern.startsWith('~') && !pattern.startsWith('>') && !pattern.startsWith('<')) {
704
- const pParts = parseVersion(pattern);
705
- return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] === pParts[2];
706
- }
707
-
708
- // >= match
709
- if (pattern.startsWith('>=')) {
710
- const pParts = parseVersion(pattern.slice(2));
711
- for (let i = 0; i < 3; i++) {
712
- if (vParts[i] > pParts[i]) return true;
713
- if (vParts[i] < pParts[i]) return false;
714
- }
715
- return true;
716
- }
717
-
718
- // > match
719
- if (pattern.startsWith('>') && !pattern.startsWith('>=')) {
720
- const pParts = parseVersion(pattern.slice(1));
721
- for (let i = 0; i < 3; i++) {
722
- if (vParts[i] > pParts[i]) return true;
723
- if (vParts[i] < pParts[i]) return false;
724
- }
725
- return false;
726
- }
727
-
728
- // ^ (compatible) - same major
729
- if (pattern.startsWith('^')) {
730
- const pParts = parseVersion(pattern.slice(1));
731
- return vParts[0] === pParts[0] &&
732
- (vParts[1] > pParts[1] || (vParts[1] === pParts[1] && vParts[2] >= pParts[2]));
733
- }
734
-
735
- // ~ (patch only) - same major.minor
736
- if (pattern.startsWith('~')) {
737
- const pParts = parseVersion(pattern.slice(1));
738
- return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] >= pParts[2];
739
- }
740
-
741
- return true;
742
- }
743
-
744
- /**
745
- * Resolve a dependency, checking version and trying fallbacks
746
- */
747
- export async function resolveDependency(
748
- dep: DependencyDeclaration,
749
- searchPaths: string[]
750
- ): Promise<CognitiveModule | null> {
751
- // Try primary module
752
- const module = await findModule(dep.name, searchPaths);
753
-
754
- if (module) {
755
- // Check version if specified
756
- if (dep.version && !versionMatches(module.version, dep.version)) {
757
- console.warn(`Module ${dep.name} version ${module.version} does not match ${dep.version}`);
758
- if (!dep.optional) {
759
- // Try fallback
760
- if (dep.fallback) {
761
- return findModule(dep.fallback, searchPaths);
762
- }
763
- return null;
764
- }
765
- }
766
- return module;
767
- }
768
-
769
- // Try fallback
770
- if (dep.fallback) {
771
- return findModule(dep.fallback, searchPaths);
772
- }
773
-
774
- // Optional dependency not found
775
- if (dep.optional) {
776
- return null;
777
- }
778
-
779
- throw new Error(`Required dependency not found: ${dep.name}`);
780
- }
781
-
782
- // =============================================================================
783
- // Composition Orchestrator
784
- // =============================================================================
785
-
786
- export class CompositionOrchestrator {
787
- private provider: Provider;
788
- private cwd: string;
789
- private searchPaths: string[];
790
-
791
- constructor(provider: Provider, cwd: string = process.cwd()) {
792
- this.provider = provider;
793
- this.cwd = cwd;
794
- this.searchPaths = getDefaultSearchPaths(cwd);
795
- }
796
-
797
- /**
798
- * Execute a composed module workflow
799
- */
800
- async execute(
801
- moduleName: string,
802
- input: ModuleInput,
803
- options: {
804
- maxDepth?: number;
805
- timeoutMs?: number;
806
- } = {}
807
- ): Promise<CompositionResult> {
808
- const startTime = Date.now();
809
- const trace: ExecutionTrace[] = [];
810
- const moduleResults: Record<string, ModuleResult> = {};
811
-
812
- // Create context
813
- const context: CompositionContext = {
814
- depth: 0,
815
- maxDepth: options.maxDepth ?? 5,
816
- results: {},
817
- input,
818
- running: new Set(),
819
- startTime,
820
- timeoutMs: options.timeoutMs,
821
- iterationCount: 0
822
- };
823
-
824
- try {
825
- // Load the main module
826
- const module = await findModule(moduleName, this.searchPaths);
827
- if (!module) {
828
- return {
829
- ok: false,
830
- moduleResults: {},
831
- trace: [],
832
- totalTimeMs: Date.now() - startTime,
833
- error: {
834
- code: COMPOSITION_ERRORS.E4009,
835
- message: `Module not found: ${moduleName}`
836
- }
837
- };
838
- }
839
-
840
- // Check if module has composition config
841
- const composition = this.getCompositionConfig(module);
842
-
843
- let result: ModuleResult;
844
-
845
- if (composition) {
846
- // Execute composition workflow
847
- result = await this.executeComposition(module, composition, context, trace);
848
- } else {
849
- // Simple execution (no composition)
850
- result = await this.executeModule(module, input, context, trace);
851
- }
852
-
853
- // Collect all results
854
- for (const [name, res] of Object.entries(context.results)) {
855
- moduleResults[name] = res as ModuleResult;
856
- }
857
- moduleResults[moduleName] = result;
858
-
859
- return {
860
- ok: result.ok,
861
- result,
862
- moduleResults,
863
- trace,
864
- totalTimeMs: Date.now() - startTime
865
- };
866
-
867
- } catch (error) {
868
- return {
869
- ok: false,
870
- moduleResults,
871
- trace,
872
- totalTimeMs: Date.now() - startTime,
873
- error: {
874
- code: 'E4000',
875
- message: (error as Error).message
876
- }
877
- };
878
- }
879
- }
880
-
881
- /**
882
- * Get composition config from module (if exists)
883
- */
884
- private getCompositionConfig(module: CognitiveModule): CompositionConfig | null {
885
- // Check if module has composition in its metadata
886
- const raw = (module as unknown as { composition?: CompositionConfig }).composition;
887
- return raw ?? null;
888
- }
889
-
890
- /**
891
- * Execute composition based on pattern
892
- */
893
- private async executeComposition(
894
- module: CognitiveModule,
895
- composition: CompositionConfig,
896
- context: CompositionContext,
897
- trace: ExecutionTrace[]
898
- ): Promise<ModuleResult> {
899
- // Check timeout
900
- if (this.isTimedOut(context)) {
901
- return this.timeoutError(context);
902
- }
903
-
904
- // Check depth
905
- if (context.depth > context.maxDepth) {
906
- return {
907
- ok: false,
908
- meta: { confidence: 0, risk: 'high', explain: 'Max composition depth exceeded' },
909
- error: { code: COMPOSITION_ERRORS.E4005, message: `Maximum composition depth (${context.maxDepth}) exceeded` }
910
- } as ModuleResult;
911
- }
912
-
913
- // Resolve dependencies first
914
- if (composition.requires) {
915
- for (const dep of composition.requires) {
916
- const resolved = await resolveDependency(dep, this.searchPaths);
917
- if (!resolved && !dep.optional) {
918
- return {
919
- ok: false,
920
- meta: { confidence: 0, risk: 'high', explain: `Dependency not found: ${dep.name}` },
921
- error: { code: COMPOSITION_ERRORS.E4009, message: `Required dependency not found: ${dep.name}` }
922
- } as ModuleResult;
923
- }
924
- }
925
- }
926
-
927
- // Execute based on pattern
928
- switch (composition.pattern) {
929
- case 'sequential':
930
- return this.executeSequential(module, composition, context, trace);
931
-
932
- case 'parallel':
933
- return this.executeParallel(module, composition, context, trace);
934
-
935
- case 'conditional':
936
- return this.executeConditional(module, composition, context, trace);
937
-
938
- case 'iterative':
939
- return this.executeIterative(module, composition, context, trace);
940
-
941
- default:
942
- // Default to sequential
943
- return this.executeSequential(module, composition, context, trace);
944
- }
945
- }
946
-
947
- /**
948
- * Execute sequential composition: A → B → C
949
- */
950
- private async executeSequential(
951
- module: CognitiveModule,
952
- composition: CompositionConfig,
953
- context: CompositionContext,
954
- trace: ExecutionTrace[]
955
- ): Promise<ModuleResult> {
956
- const dataflow = composition.dataflow ?? [];
957
- let currentData: unknown = context.input;
958
- let lastResult: ModuleResult | null = null;
959
-
960
- for (const step of dataflow) {
961
- // Check timeout
962
- if (this.isTimedOut(context)) {
963
- return this.timeoutError(context);
964
- }
965
-
966
- // Check condition
967
- if (step.condition) {
968
- const conditionData = {
969
- input: context.input,
970
- ...context.results,
971
- current: currentData
972
- };
973
-
974
- if (!evaluateCondition(step.condition, conditionData)) {
975
- trace.push({
976
- module: Array.isArray(step.to) ? step.to.join(',') : step.to,
977
- startTime: Date.now(),
978
- endTime: Date.now(),
979
- durationMs: 0,
980
- success: true,
981
- skipped: true,
982
- reason: `Condition not met: ${step.condition}`
983
- });
984
- continue;
985
- }
986
- }
987
-
988
- // Get source data
989
- const sources = Array.isArray(step.from) ? step.from : [step.from];
990
- const sourceDataArray: unknown[] = [];
991
-
992
- for (const source of sources) {
993
- if (source === 'input') {
994
- sourceDataArray.push(context.input);
995
- } else if (source.endsWith('.output')) {
996
- const moduleName = source.replace('.output', '');
997
- const moduleResult = context.results[moduleName];
998
- if (moduleResult && 'data' in moduleResult) {
999
- sourceDataArray.push((moduleResult as { data: unknown }).data);
1000
- }
1001
- } else {
1002
- // Assume it's a module name
1003
- const moduleResult = context.results[source];
1004
- if (moduleResult && 'data' in moduleResult) {
1005
- sourceDataArray.push((moduleResult as { data: unknown }).data);
1006
- }
1007
- }
1008
- }
1009
-
1010
- // Aggregate sources if multiple
1011
- let sourceData: unknown;
1012
- if (sourceDataArray.length === 1) {
1013
- sourceData = sourceDataArray[0];
1014
- } else if (sourceDataArray.length > 1) {
1015
- sourceData = { sources: sourceDataArray };
1016
- } else {
1017
- sourceData = currentData;
1018
- }
1019
-
1020
- // Apply mapping
1021
- if (step.mapping) {
1022
- currentData = applyMapping(step.mapping, sourceData);
1023
- } else {
1024
- currentData = sourceData;
1025
- }
1026
-
1027
- // Execute target(s)
1028
- const targets = Array.isArray(step.to) ? step.to : [step.to];
1029
-
1030
- if (targets.length === 1 && targets[0] === 'output') {
1031
- // Final output, no module to execute
1032
- continue;
1033
- }
1034
-
1035
- for (const target of targets) {
1036
- if (target === 'output') continue;
1037
-
1038
- // Find and execute target module
1039
- const targetModule = await findModule(target, this.searchPaths);
1040
- if (!targetModule) {
1041
- return {
1042
- ok: false,
1043
- meta: { confidence: 0, risk: 'high', explain: `Target module not found: ${target}` },
1044
- error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
1045
- } as ModuleResult;
1046
- }
1047
-
1048
- // Get timeout for this dependency
1049
- const depConfig = composition.requires?.find(d => d.name === target);
1050
- const depTimeout = depConfig?.timeout_ms;
1051
-
1052
- // Execute with timeout
1053
- lastResult = await this.executeModuleWithTimeout(
1054
- targetModule,
1055
- currentData as ModuleInput,
1056
- context,
1057
- trace,
1058
- depTimeout
1059
- );
1060
-
1061
- // Store result
1062
- context.results[target] = lastResult;
1063
-
1064
- if (!lastResult.ok) {
1065
- // Check if we should use fallback
1066
- if (depConfig?.fallback) {
1067
- const fallbackModule = await findModule(depConfig.fallback, this.searchPaths);
1068
- if (fallbackModule) {
1069
- lastResult = await this.executeModuleWithTimeout(
1070
- fallbackModule,
1071
- currentData as ModuleInput,
1072
- context,
1073
- trace,
1074
- depTimeout
1075
- );
1076
- context.results[depConfig.fallback] = lastResult;
1077
- }
1078
- }
1079
-
1080
- if (!lastResult.ok && !depConfig?.optional) {
1081
- return lastResult;
1082
- }
1083
- }
1084
-
1085
- // Update current data with result
1086
- if (lastResult.ok && 'data' in lastResult) {
1087
- currentData = (lastResult as { data: unknown }).data;
1088
- }
1089
- }
1090
- }
1091
-
1092
- // Return last result or execute main module
1093
- if (lastResult) {
1094
- return lastResult;
1095
- }
1096
-
1097
- // Execute the main module with composed input
1098
- return this.executeModule(module, currentData as ModuleInput, context, trace);
1099
- }
1100
-
1101
- /**
1102
- * Execute parallel composition: A → [B, C, D] → Aggregator
1103
- */
1104
- private async executeParallel(
1105
- module: CognitiveModule,
1106
- composition: CompositionConfig,
1107
- context: CompositionContext,
1108
- trace: ExecutionTrace[]
1109
- ): Promise<ModuleResult> {
1110
- const dataflow = composition.dataflow ?? [];
1111
-
1112
- // Find parallel execution steps (where 'to' is an array)
1113
- for (const step of dataflow) {
1114
- if (this.isTimedOut(context)) {
1115
- return this.timeoutError(context);
1116
- }
1117
-
1118
- // Get source data
1119
- let sourceData: unknown = context.input;
1120
- if (step.from) {
1121
- const sources = Array.isArray(step.from) ? step.from : [step.from];
1122
- for (const source of sources) {
1123
- if (source === 'input') {
1124
- sourceData = context.input;
1125
- } else {
1126
- const moduleName = source.replace('.output', '');
1127
- const moduleResult = context.results[moduleName];
1128
- if (moduleResult && 'data' in moduleResult) {
1129
- sourceData = (moduleResult as { data: unknown }).data;
1130
- }
1131
- }
1132
- }
1133
- }
1134
-
1135
- // Apply mapping
1136
- if (step.mapping) {
1137
- sourceData = applyMapping(step.mapping, sourceData);
1138
- }
1139
-
1140
- // Get targets
1141
- const targets = Array.isArray(step.to) ? step.to : [step.to];
1142
-
1143
- if (targets.every(t => t === 'output')) {
1144
- continue;
1145
- }
1146
-
1147
- // Execute targets in parallel
1148
- const parallelPromises: Promise<ModuleResult>[] = [];
1149
- const parallelModules: string[] = [];
1150
-
1151
- for (const target of targets) {
1152
- if (target === 'output') continue;
1153
-
1154
- const targetModule = await findModule(target, this.searchPaths);
1155
- if (!targetModule) {
1156
- if (!composition.requires?.find(d => d.name === target)?.optional) {
1157
- return {
1158
- ok: false,
1159
- meta: { confidence: 0, risk: 'high', explain: `Module not found: ${target}` },
1160
- error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
1161
- } as ModuleResult;
1162
- }
1163
- continue;
1164
- }
1165
-
1166
- const depConfig = composition.requires?.find(d => d.name === target);
1167
- const depTimeout = depConfig?.timeout_ms;
1168
-
1169
- parallelModules.push(target);
1170
- parallelPromises.push(
1171
- this.executeModuleWithTimeout(
1172
- targetModule,
1173
- sourceData as ModuleInput,
1174
- { ...context, running: new Set(context.running) },
1175
- trace,
1176
- depTimeout
1177
- )
1178
- );
1179
- }
1180
-
1181
- // Wait for all parallel executions
1182
- const parallelResults = await Promise.all(parallelPromises);
1183
-
1184
- // Store results
1185
- for (let i = 0; i < parallelModules.length; i++) {
1186
- context.results[parallelModules[i]] = parallelResults[i];
1187
- }
1188
-
1189
- // Check for failures
1190
- const failures = parallelResults.filter(r => !r.ok);
1191
- if (failures.length > 0) {
1192
- // Check if all failed modules are optional
1193
- const allOptional = parallelModules.every((name, i) => {
1194
- if (parallelResults[i].ok) return true;
1195
- return composition.requires?.find(d => d.name === name)?.optional;
1196
- });
1197
-
1198
- if (!allOptional) {
1199
- return failures[0];
1200
- }
1201
- }
1202
- }
1203
-
1204
- // Aggregate results
1205
- const aggregateStep = dataflow.find(s => {
1206
- const targets = Array.isArray(s.to) ? s.to : [s.to];
1207
- return targets.includes('output');
1208
- });
1209
-
1210
- if (aggregateStep && Array.isArray(aggregateStep.from)) {
1211
- const resultsToAggregate: ModuleResult[] = [];
1212
-
1213
- for (const source of aggregateStep.from) {
1214
- const moduleName = source.replace('.output', '');
1215
- const result = context.results[moduleName];
1216
- if (result) {
1217
- resultsToAggregate.push(result);
1218
- }
1219
- }
1220
-
1221
- const strategy = aggregateStep.aggregate ?? 'merge';
1222
- return aggregateResults(resultsToAggregate, strategy);
1223
- }
1224
-
1225
- // Execute main module with all results
1226
- return this.executeModule(module, context.input, context, trace);
1227
- }
1228
-
1229
- /**
1230
- * Execute conditional composition: A → (condition) → B or C
1231
- */
1232
- private async executeConditional(
1233
- module: CognitiveModule,
1234
- composition: CompositionConfig,
1235
- context: CompositionContext,
1236
- trace: ExecutionTrace[]
1237
- ): Promise<ModuleResult> {
1238
- const routing = composition.routing ?? [];
1239
-
1240
- // First, execute the initial module to get data for conditions
1241
- const dataflow = composition.dataflow ?? [];
1242
- let conditionData: unknown = context.input;
1243
-
1244
- // Execute initial steps
1245
- for (const step of dataflow) {
1246
- if (this.isTimedOut(context)) {
1247
- return this.timeoutError(context);
1248
- }
1249
-
1250
- const targets = Array.isArray(step.to) ? step.to : [step.to];
1251
-
1252
- // Execute non-routing targets
1253
- for (const target of targets) {
1254
- if (target === 'output') continue;
1255
-
1256
- // Check if this target is involved in routing
1257
- const isRoutingTarget = routing.some(r => r.next === target);
1258
- if (isRoutingTarget) continue;
1259
-
1260
- const targetModule = await findModule(target, this.searchPaths);
1261
- if (!targetModule) continue;
1262
-
1263
- // Get source data
1264
- let sourceData = context.input;
1265
- if (step.from) {
1266
- const source = Array.isArray(step.from) ? step.from[0] : step.from;
1267
- if (source !== 'input') {
1268
- const moduleName = source.replace('.output', '');
1269
- const result = context.results[moduleName];
1270
- if (result && 'data' in result) {
1271
- sourceData = (result as { data: unknown }).data as ModuleInput;
1272
- }
1273
- }
1274
- }
1275
-
1276
- if (step.mapping) {
1277
- sourceData = applyMapping(step.mapping, sourceData) as ModuleInput;
1278
- }
1279
-
1280
- const result = await this.executeModule(targetModule, sourceData, context, trace);
1281
- context.results[target] = result;
1282
-
1283
- // Update condition data - include full result (meta + data) for routing conditions
1284
- conditionData = {
1285
- input: context.input,
1286
- ...Object.fromEntries(
1287
- Object.entries(context.results).map(([k, v]) => [
1288
- k,
1289
- v // Keep full result including meta and data
1290
- ])
1291
- )
1292
- };
1293
- }
1294
- }
1295
-
1296
- // Evaluate routing conditions
1297
- // Build condition data with proper structure for accessing $.module-name.meta.confidence
1298
- const routingConditionData = {
1299
- input: context.input,
1300
- ...Object.fromEntries(
1301
- Object.entries(context.results).map(([k, v]) => [k, v])
1302
- )
1303
- };
1304
-
1305
- for (const rule of routing) {
1306
- const matches = evaluateCondition(rule.condition, routingConditionData);
1307
-
1308
- if (matches) {
1309
- if (rule.next === null) {
1310
- // Use current result directly
1311
- const lastResult = Object.values(context.results).pop();
1312
- return lastResult ?? this.executeModule(module, context.input, context, trace);
1313
- }
1314
-
1315
- // Execute the next module
1316
- const nextModule = await findModule(rule.next, this.searchPaths);
1317
- if (!nextModule) {
1318
- return {
1319
- ok: false,
1320
- meta: { confidence: 0, risk: 'high', explain: `Routing target not found: ${rule.next}` },
1321
- error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${rule.next}` }
1322
- } as ModuleResult;
1323
- }
1324
-
1325
- // Pass through the data
1326
- let nextInput = context.input;
1327
- const lastDataflowStep = dataflow[dataflow.length - 1];
1328
- if (lastDataflowStep?.mapping) {
1329
- nextInput = applyMapping(lastDataflowStep.mapping, conditionData) as ModuleInput;
1330
- }
1331
-
1332
- return this.executeModule(nextModule, nextInput, context, trace);
1333
- }
1334
- }
1335
-
1336
- // No routing matched, execute main module
1337
- return this.executeModule(module, context.input, context, trace);
1338
- }
1339
-
1340
- /**
1341
- * Execute iterative composition: A → (check) → A → ... → Done
1342
- */
1343
- private async executeIterative(
1344
- module: CognitiveModule,
1345
- composition: CompositionConfig,
1346
- context: CompositionContext,
1347
- trace: ExecutionTrace[]
1348
- ): Promise<ModuleResult> {
1349
- const maxIterations = composition.iteration?.max_iterations ?? 10;
1350
- const continueCondition = composition.iteration?.continue_condition;
1351
- const stopCondition = composition.iteration?.stop_condition;
1352
-
1353
- let currentInput = context.input;
1354
- let lastResult: ModuleResult | null = null;
1355
- let iteration = 0;
1356
-
1357
- while (iteration < maxIterations) {
1358
- if (this.isTimedOut(context)) {
1359
- return this.timeoutError(context);
1360
- }
1361
-
1362
- // Execute the module
1363
- lastResult = await this.executeModule(module, currentInput, context, trace);
1364
- context.iterationCount = iteration + 1;
1365
-
1366
- // Store result with iteration number
1367
- context.results[`${module.name}_iteration_${iteration}`] = lastResult;
1368
-
1369
- // Check stop condition
1370
- if (stopCondition) {
1371
- const stopData = {
1372
- input: context.input,
1373
- current: lastResult,
1374
- iteration,
1375
- meta: lastResult && 'meta' in lastResult ? (lastResult as { meta: unknown }).meta : null,
1376
- data: lastResult && 'data' in lastResult ? (lastResult as { data: unknown }).data : null
1377
- };
1378
-
1379
- if (evaluateCondition(stopCondition, stopData)) {
1380
- break;
1381
- }
1382
- }
1383
-
1384
- // Check continue condition
1385
- if (continueCondition) {
1386
- const continueData = {
1387
- input: context.input,
1388
- current: lastResult,
1389
- iteration,
1390
- meta: lastResult && 'meta' in lastResult ? (lastResult as { meta: unknown }).meta : null,
1391
- data: lastResult && 'data' in lastResult ? (lastResult as { data: unknown }).data : null
1392
- };
1393
-
1394
- if (!evaluateCondition(continueCondition, continueData)) {
1395
- break;
1396
- }
1397
- } else {
1398
- // No continue condition and no stop condition - only run once
1399
- break;
1400
- }
1401
-
1402
- // Update input for next iteration
1403
- if (lastResult.ok && 'data' in lastResult) {
1404
- currentInput = (lastResult as { data: ModuleInput }).data;
1405
- }
1406
-
1407
- iteration++;
1408
- }
1409
-
1410
- // Check if we hit iteration limit
1411
- if (iteration >= maxIterations && continueCondition) {
1412
- return {
1413
- ok: false,
1414
- meta: {
1415
- confidence: 0.5,
1416
- risk: 'medium',
1417
- explain: `Iteration limit (${maxIterations}) reached`
1418
- },
1419
- error: {
1420
- code: COMPOSITION_ERRORS.E4013,
1421
- message: `Maximum iterations (${maxIterations}) exceeded`
1422
- },
1423
- partial_data: lastResult && 'data' in lastResult
1424
- ? (lastResult as { data: unknown }).data
1425
- : undefined
1426
- } as ModuleResult;
1427
- }
1428
-
1429
- return lastResult ?? {
1430
- ok: false,
1431
- meta: { confidence: 0, risk: 'high', explain: 'No iteration executed' },
1432
- error: { code: 'E4000', message: 'Iterative composition produced no result' }
1433
- } as ModuleResult;
1434
- }
1435
-
1436
- /**
1437
- * Execute a single module
1438
- */
1439
- private async executeModule(
1440
- module: CognitiveModule,
1441
- input: ModuleInput,
1442
- context: CompositionContext,
1443
- trace: ExecutionTrace[]
1444
- ): Promise<ModuleResult> {
1445
- // Check depth limit
1446
- if (context.depth >= context.maxDepth) {
1447
- return {
1448
- ok: false,
1449
- meta: { confidence: 0, risk: 'high', explain: `Max depth exceeded at ${module.name}` },
1450
- error: {
1451
- code: COMPOSITION_ERRORS.E4005,
1452
- message: `Maximum composition depth (${context.maxDepth}) exceeded at module: ${module.name}`
1453
- }
1454
- } as ModuleResult;
1455
- }
1456
-
1457
- // Check for circular dependency
1458
- if (context.running.has(module.name)) {
1459
- trace.push({
1460
- module: module.name,
1461
- startTime: Date.now(),
1462
- endTime: Date.now(),
1463
- durationMs: 0,
1464
- success: false,
1465
- reason: 'Circular dependency detected'
1466
- });
1467
-
1468
- return {
1469
- ok: false,
1470
- meta: { confidence: 0, risk: 'high', explain: `Circular dependency: ${module.name}` },
1471
- error: {
1472
- code: COMPOSITION_ERRORS.E4004,
1473
- message: `Circular dependency detected: ${module.name}`
1474
- }
1475
- } as ModuleResult;
1476
- }
1477
-
1478
- context.running.add(module.name);
1479
- context.depth++; // Increment depth when entering module
1480
- const startTime = Date.now();
1481
-
1482
- try {
1483
- const result = await runModule(module, this.provider, {
1484
- input,
1485
- validateInput: true,
1486
- validateOutput: true,
1487
- useV22: true
1488
- });
1489
-
1490
- const endTime = Date.now();
1491
- trace.push({
1492
- module: module.name,
1493
- startTime,
1494
- endTime,
1495
- durationMs: endTime - startTime,
1496
- success: result.ok
1497
- });
1498
-
1499
- return result;
1500
-
1501
- } catch (error) {
1502
- const endTime = Date.now();
1503
- trace.push({
1504
- module: module.name,
1505
- startTime,
1506
- endTime,
1507
- durationMs: endTime - startTime,
1508
- success: false,
1509
- reason: (error as Error).message
1510
- });
1511
-
1512
- return {
1513
- ok: false,
1514
- meta: { confidence: 0, risk: 'high', explain: (error as Error).message.slice(0, 280) },
1515
- error: { code: 'E4000', message: (error as Error).message }
1516
- } as ModuleResult;
1517
-
1518
- } finally {
1519
- context.running.delete(module.name);
1520
- context.depth--; // Decrement depth when exiting module
1521
- }
1522
- }
1523
-
1524
- /**
1525
- * Execute module with timeout
1526
- */
1527
- private async executeModuleWithTimeout(
1528
- module: CognitiveModule,
1529
- input: ModuleInput,
1530
- context: CompositionContext,
1531
- trace: ExecutionTrace[],
1532
- timeoutMs?: number
1533
- ): Promise<ModuleResult> {
1534
- const timeout = timeoutMs ?? context.timeoutMs;
1535
-
1536
- if (!timeout) {
1537
- return this.executeModule(module, input, context, trace);
1538
- }
1539
-
1540
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
1541
-
1542
- const timeoutPromise = new Promise<ModuleResult>((_, reject) => {
1543
- timeoutId = setTimeout(() => {
1544
- reject(new Error(`Module ${module.name} timed out after ${timeout}ms`));
1545
- }, timeout);
1546
- });
1547
-
1548
- try {
1549
- const result = await Promise.race([
1550
- this.executeModule(module, input, context, trace),
1551
- timeoutPromise
1552
- ]);
1553
- // Clear timeout on success to prevent memory leak
1554
- if (timeoutId) clearTimeout(timeoutId);
1555
- return result;
1556
- } catch (error) {
1557
- // Clear timeout on error too
1558
- if (timeoutId) clearTimeout(timeoutId);
1559
- return {
1560
- ok: false,
1561
- meta: { confidence: 0, risk: 'high', explain: `Timeout: ${module.name}` },
1562
- error: {
1563
- code: COMPOSITION_ERRORS.E4008,
1564
- message: (error as Error).message
1565
- }
1566
- } as ModuleResult;
1567
- }
1568
- }
1569
-
1570
- /**
1571
- * Check if composition has timed out
1572
- */
1573
- private isTimedOut(context: CompositionContext): boolean {
1574
- if (!context.timeoutMs) return false;
1575
- return Date.now() - context.startTime > context.timeoutMs;
1576
- }
1577
-
1578
- /**
1579
- * Create timeout error response
1580
- */
1581
- private timeoutError(context: CompositionContext): ModuleResult {
1582
- const elapsed = Date.now() - context.startTime;
1583
- return {
1584
- ok: false,
1585
- meta: {
1586
- confidence: 0,
1587
- risk: 'high',
1588
- explain: `Composition timed out after ${elapsed}ms`
1589
- },
1590
- error: {
1591
- code: COMPOSITION_ERRORS.E4008,
1592
- message: `Composition timeout: ${elapsed}ms exceeded ${context.timeoutMs}ms limit`
1593
- }
1594
- } as ModuleResult;
1595
- }
1596
- }
1597
-
1598
- // =============================================================================
1599
- // Convenience Functions
1600
- // =============================================================================
1601
-
1602
- /**
1603
- * Execute a composed module workflow
1604
- */
1605
- export async function executeComposition(
1606
- moduleName: string,
1607
- input: ModuleInput,
1608
- provider: Provider,
1609
- options: {
1610
- cwd?: string;
1611
- maxDepth?: number;
1612
- timeoutMs?: number;
1613
- } = {}
1614
- ): Promise<CompositionResult> {
1615
- const { cwd = process.cwd(), ...execOptions } = options;
1616
- const orchestrator = new CompositionOrchestrator(provider, cwd);
1617
- return orchestrator.execute(moduleName, input, execOptions);
1618
- }
1619
-
1620
- /**
1621
- * Validate composition configuration
1622
- */
1623
- export function validateCompositionConfig(config: CompositionConfig): {
1624
- valid: boolean;
1625
- errors: string[]
1626
- } {
1627
- const errors: string[] = [];
1628
-
1629
- // Check pattern
1630
- const validPatterns: CompositionPattern[] = ['sequential', 'parallel', 'conditional', 'iterative'];
1631
- if (!validPatterns.includes(config.pattern)) {
1632
- errors.push(`Invalid pattern: ${config.pattern}. Must be one of: ${validPatterns.join(', ')}`);
1633
- }
1634
-
1635
- // Check dataflow steps
1636
- if (config.dataflow) {
1637
- for (let i = 0; i < config.dataflow.length; i++) {
1638
- const step = config.dataflow[i];
1639
- if (!step.from) {
1640
- errors.push(`Dataflow step ${i}: missing 'from' field`);
1641
- }
1642
- if (!step.to) {
1643
- errors.push(`Dataflow step ${i}: missing 'to' field`);
1644
- }
1645
- }
1646
- }
1647
-
1648
- // Check routing rules for conditional pattern
1649
- if (config.pattern === 'conditional' && (!config.routing || config.routing.length === 0)) {
1650
- errors.push('Conditional pattern requires routing rules');
1651
- }
1652
-
1653
- // Check iteration config for iterative pattern
1654
- if (config.pattern === 'iterative') {
1655
- const iter = config.iteration;
1656
- if (!iter?.continue_condition && !iter?.stop_condition) {
1657
- errors.push('Iterative pattern requires either continue_condition or stop_condition');
1658
- }
1659
- }
1660
-
1661
- // Check dependencies
1662
- if (config.requires) {
1663
- for (const dep of config.requires) {
1664
- if (!dep.name) {
1665
- errors.push('Dependency missing name');
1666
- }
1667
- }
1668
- }
1669
-
1670
- return {
1671
- valid: errors.length === 0,
1672
- errors
1673
- };
1674
- }