cognitive-modules-cli 2.2.0 → 2.2.5

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 (94) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +572 -28
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +222 -13
  7. package/dist/commands/compose.d.ts +31 -0
  8. package/dist/commands/compose.js +185 -0
  9. package/dist/commands/index.d.ts +5 -0
  10. package/dist/commands/index.js +5 -0
  11. package/dist/commands/init.js +23 -1
  12. package/dist/commands/migrate.d.ts +30 -0
  13. package/dist/commands/migrate.js +650 -0
  14. package/dist/commands/pipe.d.ts +1 -0
  15. package/dist/commands/pipe.js +31 -11
  16. package/dist/commands/remove.js +33 -2
  17. package/dist/commands/run.d.ts +1 -0
  18. package/dist/commands/run.js +37 -27
  19. package/dist/commands/search.d.ts +28 -0
  20. package/dist/commands/search.js +143 -0
  21. package/dist/commands/test.d.ts +65 -0
  22. package/dist/commands/test.js +454 -0
  23. package/dist/commands/update.d.ts +1 -0
  24. package/dist/commands/update.js +106 -14
  25. package/dist/commands/validate.d.ts +36 -0
  26. package/dist/commands/validate.js +97 -0
  27. package/dist/errors/index.d.ts +218 -0
  28. package/dist/errors/index.js +412 -0
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +5 -1
  31. package/dist/mcp/server.js +84 -79
  32. package/dist/modules/composition.d.ts +251 -0
  33. package/dist/modules/composition.js +1330 -0
  34. package/dist/modules/index.d.ts +2 -0
  35. package/dist/modules/index.js +2 -0
  36. package/dist/modules/loader.d.ts +22 -2
  37. package/dist/modules/loader.js +171 -6
  38. package/dist/modules/runner.d.ts +422 -1
  39. package/dist/modules/runner.js +1472 -71
  40. package/dist/modules/subagent.d.ts +6 -1
  41. package/dist/modules/subagent.js +20 -13
  42. package/dist/modules/validator.d.ts +28 -0
  43. package/dist/modules/validator.js +637 -0
  44. package/dist/providers/anthropic.d.ts +15 -0
  45. package/dist/providers/anthropic.js +147 -5
  46. package/dist/providers/base.d.ts +11 -0
  47. package/dist/providers/base.js +18 -0
  48. package/dist/providers/gemini.d.ts +15 -0
  49. package/dist/providers/gemini.js +122 -5
  50. package/dist/providers/ollama.d.ts +15 -0
  51. package/dist/providers/ollama.js +111 -3
  52. package/dist/providers/openai.d.ts +11 -0
  53. package/dist/providers/openai.js +133 -0
  54. package/dist/registry/client.d.ts +204 -0
  55. package/dist/registry/client.js +356 -0
  56. package/dist/registry/index.d.ts +4 -0
  57. package/dist/registry/index.js +4 -0
  58. package/dist/server/http.js +173 -42
  59. package/dist/types.d.ts +123 -8
  60. package/dist/types.js +4 -1
  61. package/dist/version.d.ts +1 -0
  62. package/dist/version.js +4 -0
  63. package/package.json +32 -7
  64. package/src/cli.ts +0 -410
  65. package/src/commands/add.ts +0 -315
  66. package/src/commands/index.ts +0 -12
  67. package/src/commands/init.ts +0 -94
  68. package/src/commands/list.ts +0 -33
  69. package/src/commands/pipe.ts +0 -76
  70. package/src/commands/remove.ts +0 -57
  71. package/src/commands/run.ts +0 -80
  72. package/src/commands/update.ts +0 -130
  73. package/src/commands/versions.ts +0 -79
  74. package/src/index.ts +0 -55
  75. package/src/mcp/index.ts +0 -5
  76. package/src/mcp/server.ts +0 -403
  77. package/src/modules/index.ts +0 -7
  78. package/src/modules/loader.ts +0 -318
  79. package/src/modules/runner.ts +0 -495
  80. package/src/modules/subagent.ts +0 -275
  81. package/src/providers/anthropic.ts +0 -89
  82. package/src/providers/base.ts +0 -29
  83. package/src/providers/deepseek.ts +0 -83
  84. package/src/providers/gemini.ts +0 -117
  85. package/src/providers/index.ts +0 -78
  86. package/src/providers/minimax.ts +0 -81
  87. package/src/providers/moonshot.ts +0 -82
  88. package/src/providers/ollama.ts +0 -83
  89. package/src/providers/openai.ts +0 -84
  90. package/src/providers/qwen.ts +0 -82
  91. package/src/server/http.ts +0 -316
  92. package/src/server/index.ts +0 -6
  93. package/src/types.ts +0 -495
  94. package/tsconfig.json +0 -17
@@ -1,24 +1,877 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
3
  * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
+ * v2.2.1: Version field, enhanced error taxonomy, observability hooks, streaming
4
5
  */
6
+ import _Ajv from 'ajv';
7
+ const Ajv = _Ajv.default || _Ajv;
5
8
  import { aggregateRisk, isV22Envelope } from '../types.js';
6
9
  // =============================================================================
10
+ // Schema Validation
11
+ // =============================================================================
12
+ const ajv = new Ajv({ allErrors: true, strict: false });
13
+ /**
14
+ * Validate data against JSON schema. Returns list of errors.
15
+ */
16
+ export function validateData(data, schema, label = 'Data') {
17
+ const errors = [];
18
+ if (!schema || Object.keys(schema).length === 0) {
19
+ return errors;
20
+ }
21
+ try {
22
+ const validate = ajv.compile(schema);
23
+ const valid = validate(data);
24
+ if (!valid && validate.errors) {
25
+ for (const err of validate.errors) {
26
+ const path = err.instancePath || '/';
27
+ errors.push(`${label} validation error: ${err.message} at ${path}`);
28
+ }
29
+ }
30
+ }
31
+ catch (e) {
32
+ errors.push(`Schema error: ${e.message}`);
33
+ }
34
+ return errors;
35
+ }
36
+ /** Tool categories for automatic policy mapping */
37
+ const TOOL_POLICY_MAPPING = {
38
+ // Network tools
39
+ 'fetch': ['network'],
40
+ 'http': ['network'],
41
+ 'request': ['network'],
42
+ 'curl': ['network'],
43
+ 'wget': ['network'],
44
+ 'api_call': ['network'],
45
+ // Filesystem tools
46
+ 'write_file': ['filesystem_write', 'side_effects'],
47
+ 'create_file': ['filesystem_write', 'side_effects'],
48
+ 'delete_file': ['filesystem_write', 'side_effects'],
49
+ 'rename_file': ['filesystem_write', 'side_effects'],
50
+ 'mkdir': ['filesystem_write', 'side_effects'],
51
+ 'rmdir': ['filesystem_write', 'side_effects'],
52
+ // Code execution tools
53
+ 'shell': ['code_execution', 'side_effects'],
54
+ 'exec': ['code_execution', 'side_effects'],
55
+ 'run_code': ['code_execution', 'side_effects'],
56
+ 'code_interpreter': ['code_execution', 'side_effects'],
57
+ 'eval': ['code_execution', 'side_effects'],
58
+ // Database tools
59
+ 'sql_query': ['side_effects'],
60
+ 'db_write': ['side_effects'],
61
+ };
62
+ /**
63
+ * Check if a tool is allowed by the module's tools policy.
64
+ *
65
+ * @param toolName The name of the tool to check
66
+ * @param module The cognitive module config
67
+ * @returns PolicyCheckResult indicating if the tool is allowed
68
+ *
69
+ * @example
70
+ * const result = checkToolPolicy('write_file', module);
71
+ * if (!result.allowed) {
72
+ * throw new Error(result.reason);
73
+ * }
74
+ */
75
+ export function checkToolPolicy(toolName, module) {
76
+ const toolsPolicy = module.tools;
77
+ // No policy = allow all
78
+ if (!toolsPolicy) {
79
+ return { allowed: true };
80
+ }
81
+ const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
82
+ // Check explicit denied list first
83
+ if (toolsPolicy.denied?.some(d => d.toLowerCase().replace(/[-\s]/g, '_') === normalizedName)) {
84
+ return {
85
+ allowed: false,
86
+ reason: `Tool '${toolName}' is explicitly denied by module tools policy`,
87
+ policy: 'tools.denied'
88
+ };
89
+ }
90
+ // Check policy mode
91
+ if (toolsPolicy.policy === 'deny_by_default') {
92
+ // In deny_by_default mode, tool must be in allowed list
93
+ const isAllowed = toolsPolicy.allowed?.some(a => a.toLowerCase().replace(/[-\s]/g, '_') === normalizedName);
94
+ if (!isAllowed) {
95
+ return {
96
+ allowed: false,
97
+ reason: `Tool '${toolName}' not in allowed list (policy: deny_by_default)`,
98
+ policy: 'tools.policy'
99
+ };
100
+ }
101
+ }
102
+ return { allowed: true };
103
+ }
104
+ /**
105
+ * Check if an action is allowed by the module's policies.
106
+ *
107
+ * @param action The action to check (network, filesystem_write, etc.)
108
+ * @param module The cognitive module config
109
+ * @returns PolicyCheckResult indicating if the action is allowed
110
+ *
111
+ * @example
112
+ * const result = checkPolicy('network', module);
113
+ * if (!result.allowed) {
114
+ * throw new Error(result.reason);
115
+ * }
116
+ */
117
+ export function checkPolicy(action, module) {
118
+ const policies = module.policies;
119
+ // No policies = allow all
120
+ if (!policies) {
121
+ return { allowed: true };
122
+ }
123
+ // Check the specific policy
124
+ if (policies[action] === 'deny') {
125
+ return {
126
+ allowed: false,
127
+ reason: `Action '${action}' is denied by module policy`,
128
+ policy: `policies.${action}`
129
+ };
130
+ }
131
+ return { allowed: true };
132
+ }
133
+ /**
134
+ * Check if a tool is allowed considering both tools policy and general policies.
135
+ * This performs a comprehensive check that:
136
+ * 1. Checks the tools policy (allowed/denied lists)
137
+ * 2. Maps the tool to policy actions and checks those
138
+ *
139
+ * @param toolName The name of the tool to check
140
+ * @param module The cognitive module config
141
+ * @returns PolicyCheckResult with detailed information
142
+ *
143
+ * @example
144
+ * const result = checkToolAllowed('write_file', module);
145
+ * if (!result.allowed) {
146
+ * return makeErrorResponse({
147
+ * code: 'POLICY_VIOLATION',
148
+ * message: result.reason,
149
+ * });
150
+ * }
151
+ */
152
+ export function checkToolAllowed(toolName, module) {
153
+ // First check explicit tools policy
154
+ const toolCheck = checkToolPolicy(toolName, module);
155
+ if (!toolCheck.allowed) {
156
+ return toolCheck;
157
+ }
158
+ // Then check mapped policies
159
+ const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
160
+ const mappedActions = TOOL_POLICY_MAPPING[normalizedName] || [];
161
+ for (const action of mappedActions) {
162
+ const policyCheck = checkPolicy(action, module);
163
+ if (!policyCheck.allowed) {
164
+ return {
165
+ allowed: false,
166
+ reason: `Tool '${toolName}' requires '${action}' which is denied by policy`,
167
+ policy: policyCheck.policy
168
+ };
169
+ }
170
+ }
171
+ return { allowed: true };
172
+ }
173
+ /**
174
+ * Validate that a list of tools are all allowed by the module's policies.
175
+ * Returns all violations found.
176
+ *
177
+ * @param toolNames List of tool names to check
178
+ * @param module The cognitive module config
179
+ * @returns Array of PolicyCheckResult for denied tools
180
+ */
181
+ export function validateToolsAllowed(toolNames, module) {
182
+ const violations = [];
183
+ for (const toolName of toolNames) {
184
+ const result = checkToolAllowed(toolName, module);
185
+ if (!result.allowed) {
186
+ violations.push(result);
187
+ }
188
+ }
189
+ return violations;
190
+ }
191
+ /**
192
+ * Get all denied actions for a module based on its policies.
193
+ * Useful for informing LLM about restrictions.
194
+ */
195
+ export function getDeniedActions(module) {
196
+ const denied = [];
197
+ const policies = module.policies;
198
+ if (!policies)
199
+ return denied;
200
+ const actions = ['network', 'filesystem_write', 'side_effects', 'code_execution'];
201
+ for (const action of actions) {
202
+ if (policies[action] === 'deny') {
203
+ denied.push(action);
204
+ }
205
+ }
206
+ return denied;
207
+ }
208
+ /**
209
+ * Get all denied tools for a module based on its tools policy.
210
+ */
211
+ export function getDeniedTools(module) {
212
+ return module.tools?.denied || [];
213
+ }
214
+ /**
215
+ * Get all allowed tools for a module (only meaningful in deny_by_default mode).
216
+ */
217
+ export function getAllowedTools(module) {
218
+ if (module.tools?.policy === 'deny_by_default') {
219
+ return module.tools.allowed || [];
220
+ }
221
+ return null; // null means "all allowed except denied list"
222
+ }
223
+ /**
224
+ * ToolCallInterceptor - Intercepts and validates tool calls against module policies.
225
+ *
226
+ * Use this class to wrap tool execution with policy enforcement:
227
+ *
228
+ * @example
229
+ * const interceptor = new ToolCallInterceptor(module);
230
+ *
231
+ * // Register tool executors
232
+ * interceptor.registerTool('read_file', async (args) => {
233
+ * return fs.readFile(args.path as string, 'utf-8');
234
+ * });
235
+ *
236
+ * // Execute tool with policy check
237
+ * const result = await interceptor.execute({
238
+ * name: 'write_file',
239
+ * arguments: { path: '/tmp/test.txt', content: 'hello' }
240
+ * });
241
+ *
242
+ * if (!result.success) {
243
+ * console.error('Tool blocked:', result.error);
244
+ * }
245
+ */
246
+ export class ToolCallInterceptor {
247
+ module;
248
+ tools = new Map();
249
+ callLog = [];
250
+ constructor(module) {
251
+ this.module = module;
252
+ }
253
+ /**
254
+ * Register a tool executor.
255
+ */
256
+ registerTool(name, executor) {
257
+ this.tools.set(name.toLowerCase(), executor);
258
+ }
259
+ /**
260
+ * Register multiple tools at once.
261
+ */
262
+ registerTools(tools) {
263
+ for (const [name, executor] of Object.entries(tools)) {
264
+ this.registerTool(name, executor);
265
+ }
266
+ }
267
+ /**
268
+ * Check if a tool call is allowed without executing it.
269
+ */
270
+ checkAllowed(toolName) {
271
+ return checkToolAllowed(toolName, this.module);
272
+ }
273
+ /**
274
+ * Execute a tool call with policy enforcement.
275
+ *
276
+ * @param request The tool call request
277
+ * @returns ToolCallResult with success/error
278
+ */
279
+ async execute(request) {
280
+ const { name, arguments: args } = request;
281
+ const timestamp = Date.now();
282
+ // Check policy
283
+ const policyResult = checkToolAllowed(name, this.module);
284
+ if (!policyResult.allowed) {
285
+ this.callLog.push({ tool: name, allowed: false, timestamp });
286
+ return {
287
+ success: false,
288
+ error: {
289
+ code: 'TOOL_NOT_ALLOWED',
290
+ message: policyResult.reason || `Tool '${name}' is not allowed`,
291
+ },
292
+ };
293
+ }
294
+ // Find executor
295
+ const executor = this.tools.get(name.toLowerCase());
296
+ if (!executor) {
297
+ return {
298
+ success: false,
299
+ error: {
300
+ code: 'TOOL_NOT_FOUND',
301
+ message: `Tool '${name}' is not registered`,
302
+ },
303
+ };
304
+ }
305
+ // Execute
306
+ try {
307
+ this.callLog.push({ tool: name, allowed: true, timestamp });
308
+ const result = await executor(args);
309
+ return { success: true, result };
310
+ }
311
+ catch (e) {
312
+ return {
313
+ success: false,
314
+ error: {
315
+ code: 'TOOL_EXECUTION_ERROR',
316
+ message: e.message,
317
+ },
318
+ };
319
+ }
320
+ }
321
+ /**
322
+ * Execute multiple tool calls in sequence.
323
+ * Stops on first policy violation.
324
+ */
325
+ async executeMany(requests) {
326
+ const results = [];
327
+ for (const request of requests) {
328
+ const result = await this.execute(request);
329
+ results.push(result);
330
+ // Stop on policy violation (not execution error)
331
+ if (!result.success && result.error?.code === 'TOOL_NOT_ALLOWED') {
332
+ break;
333
+ }
334
+ }
335
+ return results;
336
+ }
337
+ /**
338
+ * Get the call log for auditing.
339
+ */
340
+ getCallLog() {
341
+ return [...this.callLog];
342
+ }
343
+ /**
344
+ * Get summary of denied calls.
345
+ */
346
+ getDeniedCalls() {
347
+ return this.callLog
348
+ .filter(c => !c.allowed)
349
+ .map(({ tool, timestamp }) => ({ tool, timestamp }));
350
+ }
351
+ /**
352
+ * Clear the call log.
353
+ */
354
+ clearLog() {
355
+ this.callLog = [];
356
+ }
357
+ /**
358
+ * Get policy summary for this module.
359
+ */
360
+ getPolicySummary() {
361
+ return {
362
+ deniedActions: getDeniedActions(this.module),
363
+ deniedTools: getDeniedTools(this.module),
364
+ allowedTools: getAllowedTools(this.module),
365
+ toolsPolicy: this.module.tools?.policy,
366
+ };
367
+ }
368
+ }
369
+ /**
370
+ * Create a policy-aware tool executor wrapper.
371
+ *
372
+ * @example
373
+ * const safeExecutor = createPolicyAwareExecutor(module, 'write_file', async (args) => {
374
+ * return fs.writeFile(args.path, args.content);
375
+ * });
376
+ *
377
+ * // This will throw if write_file is denied
378
+ * await safeExecutor({ path: '/tmp/test.txt', content: 'hello' });
379
+ */
380
+ export function createPolicyAwareExecutor(module, toolName, executor) {
381
+ return async (args) => {
382
+ const policyResult = checkToolAllowed(toolName, module);
383
+ if (!policyResult.allowed) {
384
+ throw new Error(`Policy violation: ${policyResult.reason}`);
385
+ }
386
+ return executor(args);
387
+ };
388
+ }
389
+ // =============================================================================
390
+ // v2.2 Runtime Enforcement - Overflow & Enum
391
+ // =============================================================================
392
+ /**
393
+ * Validate overflow.insights against module's max_items config.
394
+ *
395
+ * @param data The response data object
396
+ * @param module The cognitive module config
397
+ * @returns Array of errors if insights exceed limit
398
+ */
399
+ export function validateOverflowLimits(data, module) {
400
+ const errors = [];
401
+ const overflowConfig = module.overflow;
402
+ if (!overflowConfig?.enabled) {
403
+ // If overflow disabled, insights should not exist
404
+ const extensions = data.extensions;
405
+ if (extensions?.insights && Array.isArray(extensions.insights) && extensions.insights.length > 0) {
406
+ errors.push('Overflow is disabled but extensions.insights contains data');
407
+ }
408
+ return errors;
409
+ }
410
+ const maxItems = overflowConfig.max_items ?? 5;
411
+ const extensions = data.extensions;
412
+ if (extensions?.insights && Array.isArray(extensions.insights)) {
413
+ const insights = extensions.insights;
414
+ if (insights.length > maxItems) {
415
+ errors.push(`overflow.max_items exceeded: ${insights.length} > ${maxItems}`);
416
+ }
417
+ // Check require_suggested_mapping
418
+ if (overflowConfig.require_suggested_mapping) {
419
+ for (let i = 0; i < insights.length; i++) {
420
+ const insight = insights[i];
421
+ if (!insight.suggested_mapping) {
422
+ errors.push(`insight[${i}] missing required suggested_mapping`);
423
+ }
424
+ }
425
+ }
426
+ }
427
+ return errors;
428
+ }
429
+ /**
430
+ * Validate enum values against module's enum strategy.
431
+ * For strict mode, custom enum objects are not allowed.
432
+ *
433
+ * @param data The response data object
434
+ * @param module The cognitive module config
435
+ * @returns Array of errors if enum violations found
436
+ */
437
+ export function validateEnumStrategy(data, module) {
438
+ const errors = [];
439
+ const enumStrategy = module.enums?.strategy ?? 'strict';
440
+ if (enumStrategy === 'strict') {
441
+ // In strict mode, custom enum objects (with 'custom' key) are not allowed
442
+ const checkForCustomEnums = (obj, path) => {
443
+ if (obj === null || obj === undefined)
444
+ return;
445
+ if (Array.isArray(obj)) {
446
+ obj.forEach((item, i) => checkForCustomEnums(item, `${path}[${i}]`));
447
+ }
448
+ else if (typeof obj === 'object') {
449
+ const record = obj;
450
+ // Check if this is a custom enum object (any presence of 'custom' is disallowed in strict mode)
451
+ if ('custom' in record) {
452
+ errors.push(`Custom enum not allowed in strict mode at ${path}: { custom: "${record.custom}" }`);
453
+ return;
454
+ }
455
+ // Recurse into nested objects
456
+ for (const [key, value] of Object.entries(record)) {
457
+ checkForCustomEnums(value, `${path}.${key}`);
458
+ }
459
+ }
460
+ };
461
+ checkForCustomEnums(data, 'data');
462
+ }
463
+ return errors;
464
+ }
465
+ // =============================================================================
466
+ // Constants
467
+ // =============================================================================
468
+ const ENVELOPE_VERSION = '2.2';
469
+ // =============================================================================
470
+ // Utility Functions
471
+ // =============================================================================
472
+ /**
473
+ * Deep clone an object to avoid mutation issues.
474
+ * Handles nested objects, arrays, and primitive values.
475
+ */
476
+ function deepClone(obj) {
477
+ if (obj === null || typeof obj !== 'object') {
478
+ return obj;
479
+ }
480
+ if (Array.isArray(obj)) {
481
+ return obj.map(item => deepClone(item));
482
+ }
483
+ const cloned = {};
484
+ for (const key in obj) {
485
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
486
+ cloned[key] = deepClone(obj[key]);
487
+ }
488
+ }
489
+ return cloned;
490
+ }
491
+ // Global hook registries
492
+ const _beforeCallHooks = [];
493
+ const _afterCallHooks = [];
494
+ const _errorHooks = [];
495
+ /**
496
+ * Decorator to register a before-call hook.
497
+ *
498
+ * @example
499
+ * onBeforeCall((moduleName, inputData, config) => {
500
+ * console.log(`Calling ${moduleName} with`, inputData);
501
+ * });
502
+ */
503
+ export function onBeforeCall(hook) {
504
+ _beforeCallHooks.push(hook);
505
+ return hook;
506
+ }
507
+ /**
508
+ * Decorator to register an after-call hook.
509
+ *
510
+ * @example
511
+ * onAfterCall((moduleName, result, latencyMs) => {
512
+ * console.log(`${moduleName} completed in ${latencyMs}ms`);
513
+ * });
514
+ */
515
+ export function onAfterCall(hook) {
516
+ _afterCallHooks.push(hook);
517
+ return hook;
518
+ }
519
+ /**
520
+ * Decorator to register an error hook.
521
+ *
522
+ * @example
523
+ * onError((moduleName, error, partialResult) => {
524
+ * console.error(`Error in ${moduleName}:`, error);
525
+ * });
526
+ */
527
+ export function onError(hook) {
528
+ _errorHooks.push(hook);
529
+ return hook;
530
+ }
531
+ /**
532
+ * Register a hook programmatically.
533
+ */
534
+ export function registerHook(hookType, hook) {
535
+ if (hookType === 'before_call') {
536
+ _beforeCallHooks.push(hook);
537
+ }
538
+ else if (hookType === 'after_call') {
539
+ _afterCallHooks.push(hook);
540
+ }
541
+ else if (hookType === 'error') {
542
+ _errorHooks.push(hook);
543
+ }
544
+ else {
545
+ throw new Error(`Unknown hook type: ${hookType}`);
546
+ }
547
+ }
548
+ /**
549
+ * Unregister a hook. Returns true if found and removed.
550
+ */
551
+ export function unregisterHook(hookType, hook) {
552
+ let hooks;
553
+ if (hookType === 'before_call') {
554
+ hooks = _beforeCallHooks;
555
+ }
556
+ else if (hookType === 'after_call') {
557
+ hooks = _afterCallHooks;
558
+ }
559
+ else if (hookType === 'error') {
560
+ hooks = _errorHooks;
561
+ }
562
+ else {
563
+ return false;
564
+ }
565
+ const index = hooks.indexOf(hook);
566
+ if (index !== -1) {
567
+ hooks.splice(index, 1);
568
+ return true;
569
+ }
570
+ return false;
571
+ }
572
+ /**
573
+ * Clear all registered hooks.
574
+ */
575
+ export function clearHooks() {
576
+ _beforeCallHooks.length = 0;
577
+ _afterCallHooks.length = 0;
578
+ _errorHooks.length = 0;
579
+ }
580
+ function _invokeBeforeHooks(moduleName, inputData, moduleConfig) {
581
+ for (const hook of _beforeCallHooks) {
582
+ try {
583
+ hook(moduleName, inputData, moduleConfig);
584
+ }
585
+ catch {
586
+ // Hooks should not break the main flow
587
+ }
588
+ }
589
+ }
590
+ function _invokeAfterHooks(moduleName, result, latencyMs) {
591
+ for (const hook of _afterCallHooks) {
592
+ try {
593
+ hook(moduleName, result, latencyMs);
594
+ }
595
+ catch {
596
+ // Hooks should not break the main flow
597
+ }
598
+ }
599
+ }
600
+ function _invokeErrorHooks(moduleName, error, partialResult) {
601
+ for (const hook of _errorHooks) {
602
+ try {
603
+ hook(moduleName, error, partialResult);
604
+ }
605
+ catch {
606
+ // Hooks should not break the main flow
607
+ }
608
+ }
609
+ }
610
+ // =============================================================================
611
+ // Error Response Builder
612
+ // =============================================================================
613
+ /**
614
+ * Error code taxonomy following CONFORMANCE.md E1xxx-E4xxx structure.
615
+ *
616
+ * E1xxx: Input errors (caller errors, fixable by modifying input)
617
+ * E2xxx: Processing errors (module understood input but couldn't complete)
618
+ * E3xxx: Output errors (generated output doesn't meet requirements)
619
+ * E4xxx: Runtime errors (infrastructure/system-level failures)
620
+ */
621
+ /** Standard error codes with E-format (as per ERROR-CODES.md) */
622
+ export const ERROR_CODES = {
623
+ // E1xxx: Input errors
624
+ E1000: 'PARSE_ERROR',
625
+ E1001: 'INVALID_INPUT',
626
+ E1002: 'MISSING_REQUIRED_FIELD',
627
+ E1003: 'TYPE_MISMATCH',
628
+ E1004: 'UNSUPPORTED_VALUE',
629
+ E1005: 'INPUT_TOO_LARGE',
630
+ E1006: 'INVALID_REFERENCE',
631
+ // E2xxx: Processing errors
632
+ E2001: 'LOW_CONFIDENCE',
633
+ E2002: 'TIMEOUT',
634
+ E2003: 'TOKEN_LIMIT',
635
+ E2004: 'NO_ACTION_POSSIBLE',
636
+ E2005: 'SEMANTIC_CONFLICT',
637
+ E2006: 'AMBIGUOUS_INPUT',
638
+ E2007: 'INSUFFICIENT_CONTEXT',
639
+ // E3xxx: Output errors
640
+ E3001: 'OUTPUT_SCHEMA_VIOLATION',
641
+ E3002: 'PARTIAL_RESULT',
642
+ E3003: 'MISSING_RATIONALE',
643
+ E3004: 'OVERFLOW_LIMIT',
644
+ E3005: 'INVALID_ENUM',
645
+ E3006: 'CONSTRAINT_VIOLATION',
646
+ // E4xxx: Runtime errors
647
+ E4000: 'INTERNAL_ERROR',
648
+ E4001: 'PROVIDER_UNAVAILABLE',
649
+ E4002: 'RATE_LIMITED',
650
+ E4003: 'CONTEXT_OVERFLOW',
651
+ E4004: 'CIRCULAR_DEPENDENCY',
652
+ E4005: 'MAX_DEPTH_EXCEEDED',
653
+ E4006: 'MODULE_NOT_FOUND',
654
+ E4007: 'PERMISSION_DENIED',
655
+ };
656
+ /** Reverse mapping: legacy code -> E-format code */
657
+ export const LEGACY_TO_E_CODE = {
658
+ PARSE_ERROR: 'E1000',
659
+ INVALID_INPUT: 'E1001',
660
+ MISSING_REQUIRED_FIELD: 'E1002',
661
+ TYPE_MISMATCH: 'E1003',
662
+ UNSUPPORTED_VALUE: 'E1004',
663
+ INPUT_TOO_LARGE: 'E1005',
664
+ INVALID_REFERENCE: 'E1006',
665
+ LOW_CONFIDENCE: 'E2001',
666
+ TIMEOUT: 'E2002',
667
+ TOKEN_LIMIT: 'E2003',
668
+ NO_ACTION_POSSIBLE: 'E2004',
669
+ SEMANTIC_CONFLICT: 'E2005',
670
+ AMBIGUOUS_INPUT: 'E2006',
671
+ INSUFFICIENT_CONTEXT: 'E2007',
672
+ OUTPUT_SCHEMA_VIOLATION: 'E3001',
673
+ SCHEMA_VALIDATION_FAILED: 'E3001', // Alias
674
+ PARTIAL_RESULT: 'E3002',
675
+ MISSING_RATIONALE: 'E3003',
676
+ OVERFLOW_LIMIT: 'E3004',
677
+ INVALID_ENUM: 'E3005',
678
+ CONSTRAINT_VIOLATION: 'E3006',
679
+ META_VALIDATION_FAILED: 'E3001', // Alias (output validation)
680
+ INTERNAL_ERROR: 'E4000',
681
+ PROVIDER_UNAVAILABLE: 'E4001',
682
+ LLM_ERROR: 'E4001', // Alias
683
+ RATE_LIMITED: 'E4002',
684
+ CONTEXT_OVERFLOW: 'E4003',
685
+ CIRCULAR_DEPENDENCY: 'E4004',
686
+ MAX_DEPTH_EXCEEDED: 'E4005',
687
+ MODULE_NOT_FOUND: 'E4006',
688
+ PERMISSION_DENIED: 'E4007',
689
+ POLICY_VIOLATION: 'E4007', // Alias
690
+ TOOL_NOT_ALLOWED: 'E4007', // Alias
691
+ UNKNOWN: 'E4000', // Fallback to internal error
692
+ };
693
+ /** Error codes and their default properties */
694
+ export const ERROR_PROPERTIES = {
695
+ // E1xxx: Input errors (mostly recoverable by fixing input)
696
+ E1000: { recoverable: false, retry_after_ms: null }, // PARSE_ERROR
697
+ E1001: { recoverable: true, retry_after_ms: null }, // INVALID_INPUT
698
+ E1002: { recoverable: true, retry_after_ms: null }, // MISSING_REQUIRED_FIELD
699
+ E1003: { recoverable: true, retry_after_ms: null }, // TYPE_MISMATCH
700
+ E1004: { recoverable: false, retry_after_ms: null }, // UNSUPPORTED_VALUE
701
+ E1005: { recoverable: true, retry_after_ms: null }, // INPUT_TOO_LARGE
702
+ E1006: { recoverable: true, retry_after_ms: null }, // INVALID_REFERENCE
703
+ // E2xxx: Processing errors (may have partial results)
704
+ E2001: { recoverable: true, retry_after_ms: null }, // LOW_CONFIDENCE
705
+ E2002: { recoverable: true, retry_after_ms: 5000 }, // TIMEOUT
706
+ E2003: { recoverable: true, retry_after_ms: null }, // TOKEN_LIMIT
707
+ E2004: { recoverable: false, retry_after_ms: null }, // NO_ACTION_POSSIBLE
708
+ E2005: { recoverable: false, retry_after_ms: null }, // SEMANTIC_CONFLICT
709
+ E2006: { recoverable: true, retry_after_ms: null }, // AMBIGUOUS_INPUT
710
+ E2007: { recoverable: true, retry_after_ms: null }, // INSUFFICIENT_CONTEXT
711
+ // E3xxx: Output errors (schema violations)
712
+ E3001: { recoverable: true, retry_after_ms: 1000 }, // OUTPUT_SCHEMA_VIOLATION
713
+ E3002: { recoverable: true, retry_after_ms: null }, // PARTIAL_RESULT
714
+ E3003: { recoverable: false, retry_after_ms: null }, // MISSING_RATIONALE
715
+ E3004: { recoverable: false, retry_after_ms: null }, // OVERFLOW_LIMIT
716
+ E3005: { recoverable: false, retry_after_ms: null }, // INVALID_ENUM
717
+ E3006: { recoverable: false, retry_after_ms: null }, // CONSTRAINT_VIOLATION
718
+ // E4xxx: Runtime errors (infrastructure failures)
719
+ E4000: { recoverable: false, retry_after_ms: null }, // INTERNAL_ERROR
720
+ E4001: { recoverable: true, retry_after_ms: 5000 }, // PROVIDER_UNAVAILABLE
721
+ E4002: { recoverable: true, retry_after_ms: 10000 }, // RATE_LIMITED
722
+ E4003: { recoverable: false, retry_after_ms: null }, // CONTEXT_OVERFLOW
723
+ E4004: { recoverable: false, retry_after_ms: null }, // CIRCULAR_DEPENDENCY
724
+ E4005: { recoverable: false, retry_after_ms: null }, // MAX_DEPTH_EXCEEDED
725
+ E4006: { recoverable: true, retry_after_ms: null }, // MODULE_NOT_FOUND
726
+ E4007: { recoverable: false, retry_after_ms: null }, // PERMISSION_DENIED
727
+ // Legacy codes (for backward compatibility)
728
+ MODULE_NOT_FOUND: { recoverable: true, retry_after_ms: null },
729
+ INVALID_INPUT: { recoverable: true, retry_after_ms: null },
730
+ PARSE_ERROR: { recoverable: false, retry_after_ms: null },
731
+ SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
732
+ META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
733
+ POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
734
+ TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
735
+ LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
736
+ RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
737
+ TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
738
+ UNKNOWN: { recoverable: false, retry_after_ms: null },
739
+ };
740
+ /**
741
+ * Normalize error code to E-format.
742
+ * Accepts both E-format (E1001) and legacy format (INVALID_INPUT).
743
+ *
744
+ * @param code Error code in any format
745
+ * @returns E-format code (e.g., "E1001")
746
+ */
747
+ export function normalizeErrorCode(code) {
748
+ // Already E-format
749
+ if (/^E\d{4}$/.test(code)) {
750
+ return code;
751
+ }
752
+ // Map legacy to E-format
753
+ const eCode = LEGACY_TO_E_CODE[code];
754
+ return eCode || 'E4000'; // Default to INTERNAL_ERROR
755
+ }
756
+ /**
757
+ * Get error category from E-format code.
758
+ *
759
+ * @param code E-format error code (e.g., "E1001")
760
+ * @returns Category: 'input' | 'processing' | 'output' | 'runtime'
761
+ */
762
+ export function getErrorCategory(code) {
763
+ const normalized = normalizeErrorCode(code);
764
+ const category = normalized.charAt(1);
765
+ switch (category) {
766
+ case '1': return 'input';
767
+ case '2': return 'processing';
768
+ case '3': return 'output';
769
+ case '4': return 'runtime';
770
+ default: return 'runtime';
771
+ }
772
+ }
773
+ /**
774
+ * Build a standardized error response with enhanced taxonomy.
775
+ * Supports both E-format (E1001) and legacy format (INVALID_INPUT) error codes.
776
+ *
777
+ * @param options Error response options
778
+ * @returns Standardized error envelope
779
+ */
780
+ export function makeErrorResponse(options) {
781
+ const { code, message, explain, partialData, details, recoverable, retryAfterMs, confidence = 0.0, risk = 'high', suggestion, useEFormat = true, } = options;
782
+ // Normalize error code to E-format if requested
783
+ const normalizedCode = useEFormat ? normalizeErrorCode(code) : code;
784
+ // Get default properties from error code (try normalized first, then original)
785
+ const defaults = ERROR_PROPERTIES[normalizedCode] || ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN || { recoverable: false, retry_after_ms: null };
786
+ const errorObj = {
787
+ code: normalizedCode,
788
+ message,
789
+ };
790
+ // Add recoverable flag
791
+ const isRecoverable = recoverable ?? defaults.recoverable;
792
+ if (isRecoverable !== undefined) {
793
+ errorObj.recoverable = isRecoverable;
794
+ }
795
+ // Add retry suggestion
796
+ const retryMs = retryAfterMs ?? defaults.retry_after_ms;
797
+ if (retryMs !== null && retryMs !== undefined) {
798
+ errorObj.retry_after_ms = retryMs;
799
+ }
800
+ // Add suggestion if provided
801
+ if (suggestion) {
802
+ errorObj.suggestion = suggestion;
803
+ }
804
+ // Add details if provided
805
+ if (details) {
806
+ errorObj.details = details;
807
+ }
808
+ // Determine confidence based on error category (if not explicitly provided)
809
+ let finalConfidence = confidence;
810
+ if (confidence === 0.0 && partialData) {
811
+ // If we have partial data, may have some confidence
812
+ const category = getErrorCategory(normalizedCode);
813
+ if (category === 'processing') {
814
+ finalConfidence = 0.3; // Some partial understanding
815
+ }
816
+ }
817
+ return {
818
+ ok: false,
819
+ version: ENVELOPE_VERSION,
820
+ meta: {
821
+ confidence: finalConfidence,
822
+ risk,
823
+ explain: (explain || message).slice(0, 280),
824
+ },
825
+ error: errorObj,
826
+ partial_data: partialData,
827
+ };
828
+ }
829
+ /**
830
+ * Build a standardized success response.
831
+ */
832
+ export function makeSuccessResponse(options) {
833
+ const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
834
+ const meta = {
835
+ confidence: Math.max(0.0, Math.min(1.0, confidence)),
836
+ risk,
837
+ explain: explain ? explain.slice(0, 280) : 'No explanation provided',
838
+ };
839
+ if (latencyMs !== undefined) {
840
+ meta.latency_ms = latencyMs;
841
+ }
842
+ if (model) {
843
+ meta.model = model;
844
+ }
845
+ if (traceId) {
846
+ meta.trace_id = traceId;
847
+ }
848
+ return {
849
+ ok: true,
850
+ version: ENVELOPE_VERSION,
851
+ meta,
852
+ data,
853
+ };
854
+ }
855
+ // =============================================================================
7
856
  // Repair Pass (v2.2)
8
857
  // =============================================================================
9
858
  /**
10
859
  * Attempt to repair envelope format issues without changing semantics.
11
860
  *
12
- * Repairs (lossless only):
861
+ * Repairs (mostly lossless, except explain truncation):
13
862
  * - Missing meta fields (fill with conservative defaults)
14
863
  * - Truncate explain if too long
15
864
  * - Trim whitespace from string fields
865
+ * - Clamp confidence to [0, 1] range
16
866
  *
17
867
  * Does NOT repair:
18
868
  * - Invalid enum values (treated as validation failure)
869
+ *
870
+ * Note: Returns a deep copy to avoid modifying the original data.
19
871
  */
20
872
  function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
21
- const repaired = { ...response };
873
+ // Deep clone to avoid mutation
874
+ const repaired = deepClone(response);
22
875
  // Ensure meta exists
23
876
  if (!repaired.meta || typeof repaired.meta !== 'object') {
24
877
  repaired.meta = {};
@@ -34,14 +887,13 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
34
887
  if (!meta.risk) {
35
888
  meta.risk = aggregateRisk(data, riskRule);
36
889
  }
37
- // Trim whitespace only (lossless), validate is valid RiskLevel
890
+ // Trim whitespace only (lossless). Do NOT repair invalid enum values.
38
891
  if (typeof meta.risk === 'string') {
39
892
  const trimmedRisk = meta.risk.trim().toLowerCase();
40
893
  const validRisks = ['none', 'low', 'medium', 'high'];
41
- meta.risk = validRisks.includes(trimmedRisk) ? trimmedRisk : 'medium';
42
- }
43
- else {
44
- meta.risk = 'medium'; // Default for invalid type
894
+ if (validRisks.includes(trimmedRisk)) {
895
+ meta.risk = trimmedRisk;
896
+ }
45
897
  }
46
898
  // Repair explain
47
899
  if (typeof meta.explain !== 'string') {
@@ -54,7 +906,7 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
54
906
  if (meta.explain.length > maxExplainLength) {
55
907
  meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
56
908
  }
57
- // Build proper v2.2 response
909
+ // Build proper v2.2 response with version
58
910
  const builtMeta = {
59
911
  confidence: meta.confidence,
60
912
  risk: meta.risk,
@@ -62,21 +914,65 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
62
914
  };
63
915
  const result = repaired.ok === false ? {
64
916
  ok: false,
917
+ version: ENVELOPE_VERSION,
65
918
  meta: builtMeta,
66
- error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
919
+ // E4000 is an internal/runtime error fallback (should rarely happen after repair).
920
+ error: repaired.error ?? { code: 'E4000', message: 'Unknown error' },
67
921
  partial_data: repaired.partial_data
68
922
  } : {
69
923
  ok: true,
924
+ version: ENVELOPE_VERSION,
70
925
  meta: builtMeta,
71
926
  data: repaired.data
72
927
  };
73
928
  return result;
74
929
  }
930
+ /**
931
+ * Repair error envelope format.
932
+ *
933
+ * Note: Returns a deep copy to avoid modifying the original data.
934
+ */
935
+ function repairErrorEnvelope(data, maxExplainLength = 280) {
936
+ // Deep clone to avoid mutation
937
+ const repaired = deepClone(data);
938
+ // Ensure meta exists for errors
939
+ if (!repaired.meta || typeof repaired.meta !== 'object') {
940
+ repaired.meta = {};
941
+ }
942
+ const meta = repaired.meta;
943
+ // Set default meta for errors
944
+ if (typeof meta.confidence !== 'number') {
945
+ meta.confidence = 0.0;
946
+ }
947
+ if (!meta.risk) {
948
+ meta.risk = 'high';
949
+ }
950
+ if (typeof meta.explain !== 'string') {
951
+ const error = (repaired.error ?? {});
952
+ meta.explain = (error.message ?? 'An error occurred').slice(0, maxExplainLength);
953
+ }
954
+ return {
955
+ ok: false,
956
+ version: ENVELOPE_VERSION,
957
+ meta: {
958
+ confidence: meta.confidence,
959
+ risk: meta.risk,
960
+ explain: meta.explain,
961
+ },
962
+ // E4000 is an internal/runtime error fallback (should rarely happen after repair).
963
+ error: repaired.error ?? { code: 'E4000', message: 'Unknown error' },
964
+ partial_data: repaired.partial_data,
965
+ };
966
+ }
75
967
  /**
76
968
  * Wrap v2.1 response to v2.2 format
77
969
  */
78
970
  function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
79
971
  if (isV22Envelope(response)) {
972
+ // Already v2.2, but ensure version field exists
973
+ if (!('version' in response) || !response.version) {
974
+ return { ...deepClone(response), version: ENVELOPE_VERSION };
975
+ }
80
976
  return response;
81
977
  }
82
978
  if (response.ok) {
@@ -85,6 +981,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
85
981
  const rationale = data.rationale ?? '';
86
982
  return {
87
983
  ok: true,
984
+ version: ENVELOPE_VERSION,
88
985
  meta: {
89
986
  confidence,
90
987
  risk: aggregateRisk(data, riskRule),
@@ -97,18 +994,64 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
97
994
  const errorMsg = response.error?.message ?? 'Unknown error';
98
995
  return {
99
996
  ok: false,
997
+ version: ENVELOPE_VERSION,
100
998
  meta: {
101
999
  confidence: 0,
102
1000
  risk: 'high',
103
1001
  explain: errorMsg.slice(0, 280)
104
1002
  },
105
- error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
1003
+ error: response.error ?? { code: 'E4000', message: errorMsg }, // INTERNAL_ERROR fallback
106
1004
  partial_data: response.partial_data
107
1005
  };
108
1006
  }
109
1007
  }
1008
+ /**
1009
+ * Convert legacy format (no envelope) to v2.2 envelope.
1010
+ */
1011
+ function convertLegacyToEnvelope(data, isError = false) {
1012
+ const isPlainObject = typeof data === 'object' && data !== null && !Array.isArray(data);
1013
+ const dataObj = isPlainObject ? data : { result: data };
1014
+ if (isError || (isPlainObject && 'error' in dataObj)) {
1015
+ const error = (dataObj.error ?? {});
1016
+ const errorMsg = typeof error === 'object'
1017
+ ? (error.message ?? String(error))
1018
+ : String(error);
1019
+ return {
1020
+ ok: false,
1021
+ version: ENVELOPE_VERSION,
1022
+ meta: {
1023
+ confidence: 0.0,
1024
+ risk: 'high',
1025
+ explain: errorMsg.slice(0, 280),
1026
+ },
1027
+ error: {
1028
+ code: (typeof error === 'object' ? error.code : undefined) ?? 'UNKNOWN',
1029
+ message: errorMsg,
1030
+ },
1031
+ partial_data: undefined,
1032
+ };
1033
+ }
1034
+ else {
1035
+ const confidence = dataObj.confidence ?? 0.5;
1036
+ const rationale = dataObj.rationale ?? '';
1037
+ return {
1038
+ ok: true,
1039
+ version: ENVELOPE_VERSION,
1040
+ meta: {
1041
+ confidence,
1042
+ risk: aggregateRisk(dataObj),
1043
+ explain: rationale.slice(0, 280) || 'No explanation provided',
1044
+ },
1045
+ data: dataObj,
1046
+ };
1047
+ }
1048
+ }
1049
+ // =============================================================================
1050
+ // Main Runner
1051
+ // =============================================================================
110
1052
  export async function runModule(module, provider, options = {}) {
111
- const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
1053
+ const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
1054
+ const startTime = Date.now();
112
1055
  // Determine if we should use envelope format
113
1056
  const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
114
1057
  // Determine if we should use v2.2 format
@@ -128,6 +1071,25 @@ export async function runModule(module, provider, options = {}) {
128
1071
  inputData.query = args;
129
1072
  }
130
1073
  }
1074
+ // Invoke before hooks
1075
+ _invokeBeforeHooks(module.name, inputData, module);
1076
+ // Validate input against schema
1077
+ if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
1078
+ const inputErrors = validateData(inputData, module.inputSchema, 'Input');
1079
+ if (inputErrors.length > 0) {
1080
+ const errorResult = makeErrorResponse({
1081
+ code: 'E1001', // INVALID_INPUT
1082
+ message: inputErrors.join('; '),
1083
+ explain: 'Input validation failed.',
1084
+ confidence: 1.0,
1085
+ risk: 'none',
1086
+ details: { validation_errors: inputErrors },
1087
+ suggestion: 'Check input against the module schema and fix validation errors.',
1088
+ });
1089
+ _invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
1090
+ return errorResult;
1091
+ }
1092
+ }
131
1093
  // Build prompt with clean substitution
132
1094
  const prompt = buildPrompt(module, inputData);
133
1095
  if (verbose) {
@@ -202,70 +1164,427 @@ export async function runModule(module, provider, options = {}) {
202
1164
  { role: 'system', content: systemParts.join('\n') },
203
1165
  { role: 'user', content: prompt },
204
1166
  ];
205
- // Invoke provider
206
- const result = await provider.invoke({
207
- messages,
208
- jsonSchema: module.outputSchema,
209
- temperature: 0.3,
210
- });
211
- if (verbose) {
212
- console.error('--- Response ---');
213
- console.error(result.content);
214
- console.error('--- End Response ---');
215
- }
216
- // Parse response
217
- let parsed;
218
1167
  try {
219
- const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
220
- const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
221
- parsed = JSON.parse(jsonStr.trim());
222
- }
223
- catch {
224
- throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
225
- }
226
- // Handle envelope format
227
- if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
228
- let response = parseEnvelopeResponse(parsed, result.content);
229
- // Upgrade to v2.2 if needed
230
- if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
231
- const upgraded = wrapV21ToV22(parsed, riskRule);
232
- response = {
233
- ok: true,
234
- meta: upgraded.meta,
235
- data: upgraded.data,
236
- raw: result.content
237
- };
1168
+ // Invoke provider
1169
+ const result = await provider.invoke({
1170
+ messages,
1171
+ jsonSchema: module.outputSchema,
1172
+ temperature: 0.3,
1173
+ });
1174
+ if (verbose) {
1175
+ console.error('--- Response ---');
1176
+ console.error(result.content);
1177
+ console.error('--- End Response ---');
238
1178
  }
239
- // Apply repair pass if enabled and response needs it
240
- if (enableRepair && response.ok && shouldUseV22) {
241
- const repaired = repairEnvelope(response, riskRule);
242
- response = {
243
- ok: true,
244
- meta: repaired.meta,
245
- data: repaired.data,
246
- raw: result.content
247
- };
1179
+ // Calculate latency
1180
+ const latencyMs = Date.now() - startTime;
1181
+ // Parse response
1182
+ let parsed;
1183
+ try {
1184
+ const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1185
+ const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
1186
+ parsed = JSON.parse(jsonStr.trim());
1187
+ }
1188
+ catch (e) {
1189
+ const errorResult = makeErrorResponse({
1190
+ code: 'E1000', // PARSE_ERROR
1191
+ message: `Failed to parse JSON response: ${e.message}`,
1192
+ explain: 'Failed to parse LLM response as JSON.',
1193
+ details: { raw_response: result.content.substring(0, 500) },
1194
+ suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
1195
+ });
1196
+ _invokeErrorHooks(module.name, e, null);
1197
+ return errorResult;
1198
+ }
1199
+ // Convert to v2.2 envelope
1200
+ let response;
1201
+ if (isV22Envelope(parsed)) {
1202
+ response = parsed;
1203
+ }
1204
+ else if (isEnvelopeResponse(parsed)) {
1205
+ response = wrapV21ToV22(parsed, riskRule);
1206
+ }
1207
+ else {
1208
+ response = convertLegacyToEnvelope(parsed);
248
1209
  }
1210
+ // Add version and meta fields
1211
+ response.version = ENVELOPE_VERSION;
1212
+ if (response.meta) {
1213
+ response.meta.latency_ms = latencyMs;
1214
+ if (traceId) {
1215
+ response.meta.trace_id = traceId;
1216
+ }
1217
+ if (modelOverride) {
1218
+ response.meta.model = modelOverride;
1219
+ }
1220
+ }
1221
+ // Validate and potentially repair output
1222
+ if (response.ok && validateOutput) {
1223
+ // Get data schema (support both "data" and "output" aliases)
1224
+ const dataSchema = module.dataSchema || module.outputSchema;
1225
+ const metaSchema = module.metaSchema;
1226
+ const dataToValidate = response.data ?? {};
1227
+ if (dataSchema && Object.keys(dataSchema).length > 0) {
1228
+ let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1229
+ if (dataErrors.length > 0 && enableRepair) {
1230
+ // Attempt repair pass
1231
+ response = repairEnvelope(response, riskRule);
1232
+ response.version = ENVELOPE_VERSION;
1233
+ // Re-validate after repair
1234
+ const repairedData = response.data ?? {};
1235
+ dataErrors = validateData(repairedData, dataSchema, 'Data');
1236
+ }
1237
+ if (dataErrors.length > 0) {
1238
+ const errorResult = makeErrorResponse({
1239
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1240
+ message: dataErrors.join('; '),
1241
+ explain: 'Schema validation failed after repair attempt.',
1242
+ partialData: response.data,
1243
+ details: { validation_errors: dataErrors },
1244
+ });
1245
+ _invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
1246
+ return errorResult;
1247
+ }
1248
+ }
1249
+ // v2.2: Validate overflow limits
1250
+ const overflowErrors = validateOverflowLimits(dataToValidate, module);
1251
+ if (overflowErrors.length > 0) {
1252
+ const errorResult = makeErrorResponse({
1253
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1254
+ message: overflowErrors.join('; '),
1255
+ explain: 'Overflow validation failed.',
1256
+ partialData: dataToValidate,
1257
+ details: { overflow_errors: overflowErrors },
1258
+ });
1259
+ _invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
1260
+ return errorResult;
1261
+ }
1262
+ // v2.2: Validate enum strategy
1263
+ const enumErrors = validateEnumStrategy(dataToValidate, module);
1264
+ if (enumErrors.length > 0) {
1265
+ const errorResult = makeErrorResponse({
1266
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1267
+ message: enumErrors.join('; '),
1268
+ explain: 'Enum strategy validation failed.',
1269
+ partialData: dataToValidate,
1270
+ details: { enum_errors: enumErrors },
1271
+ });
1272
+ _invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
1273
+ return errorResult;
1274
+ }
1275
+ // Validate meta if schema exists
1276
+ if (metaSchema && Object.keys(metaSchema).length > 0) {
1277
+ let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1278
+ if (metaErrors.length > 0 && enableRepair) {
1279
+ response = repairEnvelope(response, riskRule);
1280
+ response.version = ENVELOPE_VERSION;
1281
+ // Re-validate meta after repair
1282
+ metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1283
+ if (metaErrors.length > 0) {
1284
+ const errorResult = makeErrorResponse({
1285
+ code: 'E3001', // META_VALIDATION_FAILED (maps to OUTPUT_SCHEMA_VIOLATION)
1286
+ message: metaErrors.join('; '),
1287
+ explain: 'Meta schema validation failed after repair attempt.',
1288
+ partialData: response.data,
1289
+ details: { validation_errors: metaErrors },
1290
+ });
1291
+ _invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
1292
+ return errorResult;
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+ else if (enableRepair) {
1298
+ // Repair error envelopes to ensure they have proper meta fields
1299
+ response = repairErrorEnvelope(response);
1300
+ response.version = ENVELOPE_VERSION;
1301
+ }
1302
+ // Invoke after hooks
1303
+ const finalLatencyMs = Date.now() - startTime;
1304
+ _invokeAfterHooks(module.name, response, finalLatencyMs);
249
1305
  return response;
250
1306
  }
251
- // Handle legacy format (non-envelope)
252
- const legacyResult = parseLegacyResponse(parsed, result.content);
253
- // Upgrade to v2.2 if requested
254
- if (shouldUseV22 && legacyResult.ok) {
255
- const data = (legacyResult.data ?? {});
1307
+ catch (e) {
1308
+ const latencyMs = Date.now() - startTime;
1309
+ const errorResult = makeErrorResponse({
1310
+ code: 'E4000', // INTERNAL_ERROR
1311
+ message: e.message,
1312
+ explain: `Unexpected error: ${e.name}`,
1313
+ details: { exception_type: e.name },
1314
+ });
1315
+ if (errorResult.meta) {
1316
+ errorResult.meta.latency_ms = latencyMs;
1317
+ }
1318
+ _invokeErrorHooks(module.name, e, null);
1319
+ return errorResult;
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Run a cognitive module with streaming output.
1324
+ *
1325
+ * Yields StreamEvent objects as the module executes:
1326
+ * - type="start": Module execution started
1327
+ * - type="chunk": Incremental data chunk (if LLM supports streaming)
1328
+ * - type="meta": Meta information available early
1329
+ * - type="complete": Final complete result
1330
+ * - type="error": Error occurred
1331
+ *
1332
+ * @example
1333
+ * for await (const event of runModuleStream(module, provider, options)) {
1334
+ * if (event.type === 'chunk') {
1335
+ * process.stdout.write(event.chunk);
1336
+ * } else if (event.type === 'complete') {
1337
+ * console.log('Result:', event.result);
1338
+ * }
1339
+ * }
1340
+ */
1341
+ export async function* runModuleStream(module, provider, options = {}) {
1342
+ const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
1343
+ const startTime = Date.now();
1344
+ const moduleName = module.name;
1345
+ function makeEvent(type, extra = {}) {
256
1346
  return {
257
- ok: true,
258
- meta: {
259
- confidence: data.confidence ?? 0.5,
260
- risk: aggregateRisk(data, riskRule),
261
- explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
262
- },
263
- data: legacyResult.data,
264
- raw: result.content
1347
+ type,
1348
+ timestamp_ms: Date.now() - startTime,
1349
+ module_name: moduleName,
1350
+ ...extra,
265
1351
  };
266
1352
  }
267
- return legacyResult;
1353
+ try {
1354
+ // Emit start event
1355
+ yield makeEvent('start');
1356
+ // Build input data
1357
+ const inputData = input || {};
1358
+ if (args && !inputData.code && !inputData.query) {
1359
+ if (looksLikeCode(args)) {
1360
+ inputData.code = args;
1361
+ }
1362
+ else {
1363
+ inputData.query = args;
1364
+ }
1365
+ }
1366
+ _invokeBeforeHooks(module.name, inputData, module);
1367
+ // Validate input if enabled
1368
+ if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
1369
+ const inputErrors = validateData(inputData, module.inputSchema, 'Input');
1370
+ if (inputErrors.length > 0) {
1371
+ const errorResult = makeErrorResponse({
1372
+ code: 'E1001', // INVALID_INPUT
1373
+ message: inputErrors.join('; '),
1374
+ confidence: 1.0,
1375
+ risk: 'none',
1376
+ suggestion: 'Check input against the module schema and fix validation errors.',
1377
+ });
1378
+ _invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
1379
+ const errorObj = errorResult.error;
1380
+ yield makeEvent('error', { error: errorObj });
1381
+ yield makeEvent('complete', { result: errorResult });
1382
+ return;
1383
+ }
1384
+ }
1385
+ // Get risk_rule from module config
1386
+ const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
1387
+ // Build prompt
1388
+ const prompt = buildPrompt(module, inputData);
1389
+ // Build messages
1390
+ const systemParts = [
1391
+ `You are executing the "${module.name}" Cognitive Module.`,
1392
+ '',
1393
+ `RESPONSIBILITY: ${module.responsibility}`,
1394
+ ];
1395
+ if (useV22) {
1396
+ systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
1397
+ systemParts.push('- Wrap your response in the v2.2 envelope format');
1398
+ systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
1399
+ systemParts.push('- Return ONLY valid JSON.');
1400
+ }
1401
+ const messages = [
1402
+ { role: 'system', content: systemParts.join('\n') },
1403
+ { role: 'user', content: prompt },
1404
+ ];
1405
+ // Invoke provider with streaming if supported
1406
+ let fullContent;
1407
+ if (provider.supportsStreaming?.() && provider.invokeStream) {
1408
+ // Use true streaming
1409
+ const stream = provider.invokeStream({
1410
+ messages,
1411
+ jsonSchema: module.outputSchema,
1412
+ temperature: 0.3,
1413
+ });
1414
+ // Iterate through the async generator, yielding chunks as they arrive
1415
+ let streamResult;
1416
+ while (!(streamResult = await stream.next()).done) {
1417
+ const chunk = streamResult.value;
1418
+ yield makeEvent('chunk', { chunk });
1419
+ }
1420
+ // Get the final result (returned from the generator)
1421
+ fullContent = streamResult.value.content;
1422
+ }
1423
+ else {
1424
+ // Fallback to non-streaming invoke
1425
+ const result = await provider.invoke({
1426
+ messages,
1427
+ jsonSchema: module.outputSchema,
1428
+ temperature: 0.3,
1429
+ });
1430
+ fullContent = result.content;
1431
+ // Emit chunk event with full response
1432
+ yield makeEvent('chunk', { chunk: result.content });
1433
+ }
1434
+ // Parse response
1435
+ let parsed;
1436
+ try {
1437
+ const jsonMatch = fullContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1438
+ const jsonStr = jsonMatch ? jsonMatch[1] : fullContent;
1439
+ parsed = JSON.parse(jsonStr.trim());
1440
+ }
1441
+ catch (e) {
1442
+ const errorResult = makeErrorResponse({
1443
+ code: 'E1000', // PARSE_ERROR
1444
+ message: `Failed to parse JSON: ${e.message}`,
1445
+ suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
1446
+ });
1447
+ _invokeErrorHooks(module.name, e, null);
1448
+ // errorResult is always an error response from makeErrorResponse
1449
+ const errorObj = errorResult.error;
1450
+ yield makeEvent('error', { error: errorObj });
1451
+ yield makeEvent('complete', { result: errorResult });
1452
+ return;
1453
+ }
1454
+ // Convert to v2.2 envelope
1455
+ let response;
1456
+ if (isV22Envelope(parsed)) {
1457
+ response = parsed;
1458
+ }
1459
+ else if (isEnvelopeResponse(parsed)) {
1460
+ response = wrapV21ToV22(parsed, riskRule);
1461
+ }
1462
+ else {
1463
+ response = convertLegacyToEnvelope(parsed);
1464
+ }
1465
+ // Add version and meta
1466
+ response.version = ENVELOPE_VERSION;
1467
+ const latencyMs = Date.now() - startTime;
1468
+ if (response.meta) {
1469
+ response.meta.latency_ms = latencyMs;
1470
+ if (traceId) {
1471
+ response.meta.trace_id = traceId;
1472
+ }
1473
+ if (model) {
1474
+ response.meta.model = model;
1475
+ }
1476
+ // Emit meta event early
1477
+ yield makeEvent('meta', { meta: response.meta });
1478
+ }
1479
+ // Validate and repair output
1480
+ if (response.ok && validateOutput) {
1481
+ const dataSchema = module.dataSchema || module.outputSchema;
1482
+ const metaSchema = module.metaSchema;
1483
+ if (dataSchema && Object.keys(dataSchema).length > 0) {
1484
+ let dataToValidate = response.data ?? {};
1485
+ let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1486
+ if (dataErrors.length > 0 && enableRepair) {
1487
+ response = repairEnvelope(response, riskRule);
1488
+ response.version = ENVELOPE_VERSION;
1489
+ // Re-validate after repair
1490
+ const repairedData = response.data ?? {};
1491
+ dataToValidate = repairedData;
1492
+ dataErrors = validateData(repairedData, dataSchema, 'Data');
1493
+ }
1494
+ if (dataErrors.length > 0) {
1495
+ const errorResult = makeErrorResponse({
1496
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1497
+ message: dataErrors.join('; '),
1498
+ explain: 'Schema validation failed after repair attempt.',
1499
+ partialData: response.data,
1500
+ details: { validation_errors: dataErrors },
1501
+ });
1502
+ _invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
1503
+ const errorObj = errorResult.error;
1504
+ yield makeEvent('error', { error: errorObj });
1505
+ yield makeEvent('complete', { result: errorResult });
1506
+ return;
1507
+ }
1508
+ const overflowErrors = validateOverflowLimits(dataToValidate, module);
1509
+ if (overflowErrors.length > 0) {
1510
+ const errorResult = makeErrorResponse({
1511
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1512
+ message: overflowErrors.join('; '),
1513
+ explain: 'Overflow validation failed.',
1514
+ partialData: dataToValidate,
1515
+ details: { overflow_errors: overflowErrors },
1516
+ });
1517
+ _invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
1518
+ const errorObj = errorResult.error;
1519
+ yield makeEvent('error', { error: errorObj });
1520
+ yield makeEvent('complete', { result: errorResult });
1521
+ return;
1522
+ }
1523
+ const enumErrors = validateEnumStrategy(dataToValidate, module);
1524
+ if (enumErrors.length > 0) {
1525
+ const errorResult = makeErrorResponse({
1526
+ code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
1527
+ message: enumErrors.join('; '),
1528
+ explain: 'Enum strategy validation failed.',
1529
+ partialData: dataToValidate,
1530
+ details: { enum_errors: enumErrors },
1531
+ });
1532
+ _invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
1533
+ const errorObj = errorResult.error;
1534
+ yield makeEvent('error', { error: errorObj });
1535
+ yield makeEvent('complete', { result: errorResult });
1536
+ return;
1537
+ }
1538
+ }
1539
+ // Validate meta if schema exists
1540
+ if (metaSchema && Object.keys(metaSchema).length > 0) {
1541
+ let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1542
+ if (metaErrors.length > 0 && enableRepair) {
1543
+ response = repairEnvelope(response, riskRule);
1544
+ response.version = ENVELOPE_VERSION;
1545
+ metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1546
+ if (metaErrors.length > 0) {
1547
+ const errorResult = makeErrorResponse({
1548
+ code: 'E3001', // META_VALIDATION_FAILED (maps to OUTPUT_SCHEMA_VIOLATION)
1549
+ message: metaErrors.join('; '),
1550
+ explain: 'Meta validation failed after repair attempt.',
1551
+ partialData: response.data,
1552
+ details: { validation_errors: metaErrors },
1553
+ });
1554
+ _invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
1555
+ const errorObj = errorResult.error;
1556
+ yield makeEvent('error', { error: errorObj });
1557
+ yield makeEvent('complete', { result: errorResult });
1558
+ return;
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ else if (!response.ok && enableRepair) {
1564
+ response = repairErrorEnvelope(response);
1565
+ response.version = ENVELOPE_VERSION;
1566
+ }
1567
+ const finalLatencyMs = Date.now() - startTime;
1568
+ _invokeAfterHooks(module.name, response, finalLatencyMs);
1569
+ // Emit complete event
1570
+ yield makeEvent('complete', { result: response });
1571
+ }
1572
+ catch (e) {
1573
+ _invokeErrorHooks(module.name, e, null);
1574
+ const errorResult = makeErrorResponse({
1575
+ code: 'E4000', // INTERNAL_ERROR
1576
+ message: e.message,
1577
+ explain: `Unexpected error: ${e.name}`,
1578
+ });
1579
+ // errorResult is always an error response from makeErrorResponse
1580
+ const errorObj = errorResult.error;
1581
+ yield makeEvent('error', { error: errorObj });
1582
+ yield makeEvent('complete', { result: errorResult });
1583
+ }
268
1584
  }
1585
+ // =============================================================================
1586
+ // Helper Functions
1587
+ // =============================================================================
269
1588
  /**
270
1589
  * Check if response is in envelope format
271
1590
  */
@@ -326,6 +1645,18 @@ function parseEnvelopeResponse(response, raw) {
326
1645
  * Parse legacy (non-envelope) format response
327
1646
  */
328
1647
  function parseLegacyResponse(output, raw) {
1648
+ const isPlainObject = typeof output === 'object' && output !== null && !Array.isArray(output);
1649
+ if (!isPlainObject) {
1650
+ return {
1651
+ ok: true,
1652
+ data: {
1653
+ result: output,
1654
+ confidence: 0.5,
1655
+ rationale: '',
1656
+ },
1657
+ raw,
1658
+ };
1659
+ }
329
1660
  const outputObj = output;
330
1661
  const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
331
1662
  const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
@@ -360,6 +1691,12 @@ function parseLegacyResponse(output, raw) {
360
1691
  }
361
1692
  /**
362
1693
  * Build prompt with clean variable substitution
1694
+ *
1695
+ * Substitution order (important to avoid partial replacements):
1696
+ * 1. ${variable} - v2 style placeholders
1697
+ * 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
1698
+ * 3. $N - shorthand indexed access (descending order)
1699
+ * 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
363
1700
  */
364
1701
  function buildPrompt(module, input) {
365
1702
  let prompt = module.prompt;
@@ -368,16 +1705,22 @@ function buildPrompt(module, input) {
368
1705
  const strValue = typeof value === 'string' ? value : JSON.stringify(value);
369
1706
  prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
370
1707
  }
371
- // v1 compatibility: substitute $ARGUMENTS
1708
+ // v1 compatibility: get args value
372
1709
  const argsValue = input.code || input.query || '';
373
- prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
374
- // Substitute $N placeholders (v1 compatibility)
1710
+ // Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
1711
+ // Process in descending order to avoid $1 replacing part of $10
375
1712
  if (typeof argsValue === 'string') {
376
1713
  const argsList = argsValue.split(/\s+/);
377
- argsList.forEach((arg, i) => {
1714
+ for (let i = argsList.length - 1; i >= 0; i--) {
1715
+ const arg = argsList[i];
1716
+ // Replace $ARGUMENTS[N] first
1717
+ prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
1718
+ // Replace $N shorthand
378
1719
  prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
379
- });
1720
+ }
380
1721
  }
1722
+ // Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
1723
+ prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
381
1724
  // Append input summary if not already in prompt
382
1725
  if (!prompt.includes(argsValue) && argsValue) {
383
1726
  prompt += '\n\n## Input\n\n';
@@ -405,3 +1748,61 @@ function looksLikeCode(str) {
405
1748
  ];
406
1749
  return codeIndicators.some(re => re.test(str));
407
1750
  }
1751
+ /**
1752
+ * Run a cognitive module (legacy API, returns raw output).
1753
+ * For backward compatibility. Throws on error instead of returning error envelope.
1754
+ */
1755
+ export async function runModuleLegacy(module, provider, input, options = {}) {
1756
+ const { validateInput = true, validateOutput = true, model } = options;
1757
+ const result = await runModule(module, provider, {
1758
+ input,
1759
+ validateInput,
1760
+ validateOutput,
1761
+ useEnvelope: false,
1762
+ useV22: false,
1763
+ model,
1764
+ });
1765
+ if (result.ok && 'data' in result) {
1766
+ return result.data;
1767
+ }
1768
+ else {
1769
+ const error = 'error' in result ? result.error : { code: 'E4000', message: 'Unknown error' }; // INTERNAL_ERROR fallback
1770
+ throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
1771
+ }
1772
+ }
1773
+ // =============================================================================
1774
+ // Convenience Functions
1775
+ // =============================================================================
1776
+ /**
1777
+ * Extract meta from v2.2 envelope for routing/logging.
1778
+ */
1779
+ export function extractMeta(result) {
1780
+ return result.meta ?? {
1781
+ confidence: 0.5,
1782
+ risk: 'medium',
1783
+ explain: 'No meta available',
1784
+ };
1785
+ }
1786
+ // Alias for backward compatibility
1787
+ export const extractMetaV22 = extractMeta;
1788
+ /**
1789
+ * Determine if result should be escalated to human review based on meta.
1790
+ */
1791
+ export function shouldEscalate(result, confidenceThreshold = 0.7) {
1792
+ const meta = extractMeta(result);
1793
+ // Escalate if low confidence
1794
+ if (meta.confidence < confidenceThreshold) {
1795
+ return true;
1796
+ }
1797
+ // Escalate if high risk
1798
+ if (meta.risk === 'high') {
1799
+ return true;
1800
+ }
1801
+ // Escalate if error
1802
+ if (!result.ok) {
1803
+ return true;
1804
+ }
1805
+ return false;
1806
+ }
1807
+ // Alias for backward compatibility
1808
+ export const shouldEscalateV22 = shouldEscalate;