cognitive-modules-cli 1.4.1 → 2.2.1

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 (45) hide show
  1. package/dist/cli.js +65 -12
  2. package/dist/commands/compose.d.ts +31 -0
  3. package/dist/commands/compose.js +148 -0
  4. package/dist/commands/index.d.ts +1 -0
  5. package/dist/commands/index.js +1 -0
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +5 -1
  8. package/dist/modules/composition.d.ts +251 -0
  9. package/dist/modules/composition.js +1265 -0
  10. package/dist/modules/composition.test.d.ts +11 -0
  11. package/dist/modules/composition.test.js +450 -0
  12. package/dist/modules/index.d.ts +2 -0
  13. package/dist/modules/index.js +2 -0
  14. package/dist/modules/loader.d.ts +22 -2
  15. package/dist/modules/loader.js +167 -4
  16. package/dist/modules/policy.test.d.ts +10 -0
  17. package/dist/modules/policy.test.js +369 -0
  18. package/dist/modules/runner.d.ts +348 -34
  19. package/dist/modules/runner.js +1263 -708
  20. package/dist/modules/subagent.js +2 -0
  21. package/dist/modules/validator.d.ts +28 -0
  22. package/dist/modules/validator.js +629 -0
  23. package/dist/providers/base.d.ts +1 -45
  24. package/dist/providers/base.js +0 -67
  25. package/dist/providers/openai.d.ts +3 -27
  26. package/dist/providers/openai.js +3 -175
  27. package/dist/types.d.ts +93 -316
  28. package/dist/types.js +1 -120
  29. package/package.json +2 -1
  30. package/src/cli.ts +73 -12
  31. package/src/commands/compose.ts +185 -0
  32. package/src/commands/index.ts +1 -0
  33. package/src/index.ts +35 -0
  34. package/src/modules/composition.test.ts +558 -0
  35. package/src/modules/composition.ts +1674 -0
  36. package/src/modules/index.ts +2 -0
  37. package/src/modules/loader.ts +196 -6
  38. package/src/modules/policy.test.ts +455 -0
  39. package/src/modules/runner.ts +1692 -998
  40. package/src/modules/subagent.ts +2 -0
  41. package/src/modules/validator.ts +700 -0
  42. package/src/providers/base.ts +1 -86
  43. package/src/providers/openai.ts +4 -226
  44. package/src/types.ts +113 -462
  45. package/tsconfig.json +1 -1
@@ -1,28 +1,712 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.5: Streaming response and multimodal support
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
  */
5
- import { aggregateRisk, isV22Envelope, isProviderV25, isModuleV25, moduleSupportsStreaming, moduleSupportsMultimodal, getModuleInputModalities, ErrorCodesV25, DEFAULT_RUNTIME_CAPABILITIES } from '../types.js';
6
- import { randomUUID } from 'crypto';
7
- import { readFile } from 'fs/promises';
8
- import { existsSync } from 'fs';
9
- import { extname } from 'path';
6
+ import _Ajv from 'ajv';
7
+ const Ajv = _Ajv.default || _Ajv;
8
+ import { aggregateRisk, isV22Envelope } from '../types.js';
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
451
+ if ('custom' in record && 'reason' in record && Object.keys(record).length === 2) {
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
+ /** Error codes and their default properties */
614
+ export const ERROR_PROPERTIES = {
615
+ MODULE_NOT_FOUND: { recoverable: false, retry_after_ms: null },
616
+ INVALID_INPUT: { recoverable: false, retry_after_ms: null },
617
+ PARSE_ERROR: { recoverable: true, retry_after_ms: 1000 },
618
+ SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
619
+ META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
620
+ POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
621
+ TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
622
+ LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
623
+ RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
624
+ TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
625
+ UNKNOWN: { recoverable: false, retry_after_ms: null },
626
+ };
627
+ /**
628
+ * Build a standardized error response with enhanced taxonomy.
629
+ */
630
+ export function makeErrorResponse(options) {
631
+ const { code, message, explain, partialData, details, recoverable, retryAfterMs, confidence = 0.0, risk = 'high', } = options;
632
+ // Get default properties from error code
633
+ const defaults = ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN;
634
+ const errorObj = {
635
+ code,
636
+ message,
637
+ };
638
+ // Add recoverable flag
639
+ const isRecoverable = recoverable ?? defaults.recoverable;
640
+ if (isRecoverable !== undefined) {
641
+ errorObj.recoverable = isRecoverable;
642
+ }
643
+ // Add retry suggestion
644
+ const retryMs = retryAfterMs ?? defaults.retry_after_ms;
645
+ if (retryMs !== null) {
646
+ errorObj.retry_after_ms = retryMs;
647
+ }
648
+ // Add details if provided
649
+ if (details) {
650
+ errorObj.details = details;
651
+ }
652
+ return {
653
+ ok: false,
654
+ version: ENVELOPE_VERSION,
655
+ meta: {
656
+ confidence,
657
+ risk,
658
+ explain: (explain || message).slice(0, 280),
659
+ },
660
+ error: errorObj,
661
+ partial_data: partialData,
662
+ };
663
+ }
664
+ /**
665
+ * Build a standardized success response.
666
+ */
667
+ export function makeSuccessResponse(options) {
668
+ const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
669
+ const meta = {
670
+ confidence: Math.max(0.0, Math.min(1.0, confidence)),
671
+ risk,
672
+ explain: explain ? explain.slice(0, 280) : 'No explanation provided',
673
+ };
674
+ if (latencyMs !== undefined) {
675
+ meta.latency_ms = latencyMs;
676
+ }
677
+ if (model) {
678
+ meta.model = model;
679
+ }
680
+ if (traceId) {
681
+ meta.trace_id = traceId;
682
+ }
683
+ return {
684
+ ok: true,
685
+ version: ENVELOPE_VERSION,
686
+ meta,
687
+ data,
688
+ };
689
+ }
10
690
  // =============================================================================
11
691
  // Repair Pass (v2.2)
12
692
  // =============================================================================
13
693
  /**
14
694
  * Attempt to repair envelope format issues without changing semantics.
15
695
  *
16
- * Repairs (lossless only):
696
+ * Repairs (mostly lossless, except explain truncation):
17
697
  * - Missing meta fields (fill with conservative defaults)
18
698
  * - Truncate explain if too long
19
699
  * - Trim whitespace from string fields
700
+ * - Clamp confidence to [0, 1] range
20
701
  *
21
702
  * Does NOT repair:
22
703
  * - Invalid enum values (treated as validation failure)
704
+ *
705
+ * Note: Returns a deep copy to avoid modifying the original data.
23
706
  */
24
707
  function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
25
- const repaired = { ...response };
708
+ // Deep clone to avoid mutation
709
+ const repaired = deepClone(response);
26
710
  // Ensure meta exists
27
711
  if (!repaired.meta || typeof repaired.meta !== 'object') {
28
712
  repaired.meta = {};
@@ -58,7 +742,7 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
58
742
  if (meta.explain.length > maxExplainLength) {
59
743
  meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
60
744
  }
61
- // Build proper v2.2 response
745
+ // Build proper v2.2 response with version
62
746
  const builtMeta = {
63
747
  confidence: meta.confidence,
64
748
  risk: meta.risk,
@@ -66,21 +750,63 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
66
750
  };
67
751
  const result = repaired.ok === false ? {
68
752
  ok: false,
753
+ version: ENVELOPE_VERSION,
69
754
  meta: builtMeta,
70
755
  error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
71
756
  partial_data: repaired.partial_data
72
757
  } : {
73
758
  ok: true,
759
+ version: ENVELOPE_VERSION,
74
760
  meta: builtMeta,
75
761
  data: repaired.data
76
762
  };
77
763
  return result;
78
764
  }
765
+ /**
766
+ * Repair error envelope format.
767
+ *
768
+ * Note: Returns a deep copy to avoid modifying the original data.
769
+ */
770
+ function repairErrorEnvelope(data, maxExplainLength = 280) {
771
+ // Deep clone to avoid mutation
772
+ const repaired = deepClone(data);
773
+ // Ensure meta exists for errors
774
+ if (!repaired.meta || typeof repaired.meta !== 'object') {
775
+ repaired.meta = {};
776
+ }
777
+ const meta = repaired.meta;
778
+ // Set default meta for errors
779
+ if (typeof meta.confidence !== 'number') {
780
+ meta.confidence = 0.0;
781
+ }
782
+ if (!meta.risk) {
783
+ meta.risk = 'high';
784
+ }
785
+ if (typeof meta.explain !== 'string') {
786
+ const error = (repaired.error ?? {});
787
+ meta.explain = (error.message ?? 'An error occurred').slice(0, maxExplainLength);
788
+ }
789
+ return {
790
+ ok: false,
791
+ version: ENVELOPE_VERSION,
792
+ meta: {
793
+ confidence: meta.confidence,
794
+ risk: meta.risk,
795
+ explain: meta.explain,
796
+ },
797
+ error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
798
+ partial_data: repaired.partial_data,
799
+ };
800
+ }
79
801
  /**
80
802
  * Wrap v2.1 response to v2.2 format
81
803
  */
82
804
  function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
83
805
  if (isV22Envelope(response)) {
806
+ // Already v2.2, but ensure version field exists
807
+ if (!('version' in response) || !response.version) {
808
+ return { ...deepClone(response), version: ENVELOPE_VERSION };
809
+ }
84
810
  return response;
85
811
  }
86
812
  if (response.ok) {
@@ -89,6 +815,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
89
815
  const rationale = data.rationale ?? '';
90
816
  return {
91
817
  ok: true,
818
+ version: ENVELOPE_VERSION,
92
819
  meta: {
93
820
  confidence,
94
821
  risk: aggregateRisk(data, riskRule),
@@ -101,6 +828,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
101
828
  const errorMsg = response.error?.message ?? 'Unknown error';
102
829
  return {
103
830
  ok: false,
831
+ version: ENVELOPE_VERSION,
104
832
  meta: {
105
833
  confidence: 0,
106
834
  risk: 'high',
@@ -111,8 +839,51 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
111
839
  };
112
840
  }
113
841
  }
842
+ /**
843
+ * Convert legacy format (no envelope) to v2.2 envelope.
844
+ */
845
+ function convertLegacyToEnvelope(data, isError = false) {
846
+ if (isError || 'error' in data) {
847
+ const error = (data.error ?? {});
848
+ const errorMsg = typeof error === 'object'
849
+ ? (error.message ?? String(error))
850
+ : String(error);
851
+ return {
852
+ ok: false,
853
+ version: ENVELOPE_VERSION,
854
+ meta: {
855
+ confidence: 0.0,
856
+ risk: 'high',
857
+ explain: errorMsg.slice(0, 280),
858
+ },
859
+ error: {
860
+ code: (typeof error === 'object' ? error.code : undefined) ?? 'UNKNOWN',
861
+ message: errorMsg,
862
+ },
863
+ partial_data: undefined,
864
+ };
865
+ }
866
+ else {
867
+ const confidence = data.confidence ?? 0.5;
868
+ const rationale = data.rationale ?? '';
869
+ return {
870
+ ok: true,
871
+ version: ENVELOPE_VERSION,
872
+ meta: {
873
+ confidence,
874
+ risk: aggregateRisk(data),
875
+ explain: rationale.slice(0, 280) || 'No explanation provided',
876
+ },
877
+ data,
878
+ };
879
+ }
880
+ }
881
+ // =============================================================================
882
+ // Main Runner
883
+ // =============================================================================
114
884
  export async function runModule(module, provider, options = {}) {
115
- const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
885
+ const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
886
+ const startTime = Date.now();
116
887
  // Determine if we should use envelope format
117
888
  const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
118
889
  // Determine if we should use v2.2 format
@@ -132,6 +903,24 @@ export async function runModule(module, provider, options = {}) {
132
903
  inputData.query = args;
133
904
  }
134
905
  }
906
+ // Invoke before hooks
907
+ _invokeBeforeHooks(module.name, inputData, module);
908
+ // Validate input against schema
909
+ if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
910
+ const inputErrors = validateData(inputData, module.inputSchema, 'Input');
911
+ if (inputErrors.length > 0) {
912
+ const errorResult = makeErrorResponse({
913
+ code: 'INVALID_INPUT',
914
+ message: inputErrors.join('; '),
915
+ explain: 'Input validation failed.',
916
+ confidence: 1.0,
917
+ risk: 'none',
918
+ details: { validation_errors: inputErrors },
919
+ });
920
+ _invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
921
+ return errorResult;
922
+ }
923
+ }
135
924
  // Build prompt with clean substitution
136
925
  const prompt = buildPrompt(module, inputData);
137
926
  if (verbose) {
@@ -184,490 +973,400 @@ export async function runModule(module, provider, options = {}) {
184
973
  systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
185
974
  }
186
975
  else {
187
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
188
- systemParts.push('- Wrap your response in the envelope format');
189
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
190
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
191
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
192
- }
193
- if (module.output?.require_behavior_equivalence) {
194
- systemParts.push('- Include "behavior_equivalence" (boolean) in data');
195
- }
196
- }
197
- else {
198
- systemParts.push('', 'OUTPUT FORMAT:');
199
- systemParts.push('- Respond with ONLY valid JSON');
200
- systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
201
- if (module.output?.require_behavior_equivalence) {
202
- systemParts.push('- Include "behavior_equivalence" (boolean) field');
203
- }
204
- }
205
- const messages = [
206
- { role: 'system', content: systemParts.join('\n') },
207
- { role: 'user', content: prompt },
208
- ];
209
- // Invoke provider
210
- const result = await provider.invoke({
211
- messages,
212
- jsonSchema: module.outputSchema,
213
- temperature: 0.3,
214
- });
215
- if (verbose) {
216
- console.error('--- Response ---');
217
- console.error(result.content);
218
- console.error('--- End Response ---');
219
- }
220
- // Parse response
221
- let parsed;
222
- try {
223
- const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
224
- const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
225
- parsed = JSON.parse(jsonStr.trim());
226
- }
227
- catch {
228
- throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
229
- }
230
- // Handle envelope format
231
- if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
232
- let response = parseEnvelopeResponse(parsed, result.content);
233
- // Upgrade to v2.2 if needed
234
- if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
235
- const upgraded = wrapV21ToV22(parsed, riskRule);
236
- response = {
237
- ok: true,
238
- meta: upgraded.meta,
239
- data: upgraded.data,
240
- raw: result.content
241
- };
242
- }
243
- // Apply repair pass if enabled and response needs it
244
- if (enableRepair && response.ok && shouldUseV22) {
245
- const repaired = repairEnvelope(response, riskRule);
246
- response = {
247
- ok: true,
248
- meta: repaired.meta,
249
- data: repaired.data,
250
- raw: result.content
251
- };
252
- }
253
- return response;
254
- }
255
- // Handle legacy format (non-envelope)
256
- const legacyResult = parseLegacyResponse(parsed, result.content);
257
- // Upgrade to v2.2 if requested
258
- if (shouldUseV22 && legacyResult.ok) {
259
- const data = (legacyResult.data ?? {});
260
- return {
261
- ok: true,
262
- meta: {
263
- confidence: data.confidence ?? 0.5,
264
- risk: aggregateRisk(data, riskRule),
265
- explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
266
- },
267
- data: legacyResult.data,
268
- raw: result.content
269
- };
270
- }
271
- return legacyResult;
272
- }
273
- /**
274
- * Check if response is in envelope format
275
- */
276
- function isEnvelopeResponse(obj) {
277
- if (typeof obj !== 'object' || obj === null)
278
- return false;
279
- const o = obj;
280
- return typeof o.ok === 'boolean';
281
- }
282
- /**
283
- * Parse envelope format response (supports both v2.1 and v2.2)
284
- */
285
- function parseEnvelopeResponse(response, raw) {
286
- // Check if v2.2 format (has meta)
287
- if (isV22Envelope(response)) {
288
- if (response.ok) {
289
- return {
290
- ok: true,
291
- meta: response.meta,
292
- data: response.data,
293
- raw,
294
- };
295
- }
296
- else {
297
- return {
298
- ok: false,
299
- meta: response.meta,
300
- error: response.error,
301
- partial_data: response.partial_data,
302
- raw,
303
- };
304
- }
305
- }
306
- // v2.1 format
307
- if (response.ok) {
308
- const data = (response.data ?? {});
309
- return {
310
- ok: true,
311
- data: {
312
- ...data,
313
- confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
314
- rationale: typeof data.rationale === 'string' ? data.rationale : '',
315
- behavior_equivalence: data.behavior_equivalence,
316
- },
317
- raw,
318
- };
319
- }
320
- else {
321
- return {
322
- ok: false,
323
- error: response.error,
324
- partial_data: response.partial_data,
325
- raw,
326
- };
327
- }
328
- }
329
- /**
330
- * Parse legacy (non-envelope) format response
331
- */
332
- function parseLegacyResponse(output, raw) {
333
- const outputObj = output;
334
- const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
335
- const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
336
- const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
337
- ? outputObj.behavior_equivalence
338
- : undefined;
339
- // Check if this is an error response (has error.code)
340
- if (outputObj.error && typeof outputObj.error === 'object') {
341
- const errorObj = outputObj.error;
342
- if (typeof errorObj.code === 'string') {
343
- return {
344
- ok: false,
345
- error: {
346
- code: errorObj.code,
347
- message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
348
- },
349
- raw,
350
- };
351
- }
352
- }
353
- // Return as v2.1 format (data includes confidence)
354
- return {
355
- ok: true,
356
- data: {
357
- ...outputObj,
358
- confidence,
359
- rationale,
360
- behavior_equivalence: behaviorEquivalence,
361
- },
362
- raw,
363
- };
364
- }
365
- /**
366
- * Build prompt with clean variable substitution
367
- */
368
- function buildPrompt(module, input) {
369
- let prompt = module.prompt;
370
- // v2 style: substitute ${variable} placeholders
371
- for (const [key, value] of Object.entries(input)) {
372
- const strValue = typeof value === 'string' ? value : JSON.stringify(value);
373
- prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
374
- }
375
- // v1 compatibility: substitute $ARGUMENTS
376
- const argsValue = input.code || input.query || '';
377
- prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
378
- // Substitute $N placeholders (v1 compatibility)
379
- if (typeof argsValue === 'string') {
380
- const argsList = argsValue.split(/\s+/);
381
- argsList.forEach((arg, i) => {
382
- prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
383
- });
384
- }
385
- // Append input summary if not already in prompt
386
- if (!prompt.includes(argsValue) && argsValue) {
387
- prompt += '\n\n## Input\n\n';
388
- if (input.code) {
389
- prompt += '```\n' + input.code + '\n```\n';
390
- }
391
- if (input.query) {
392
- prompt += input.query + '\n';
393
- }
394
- if (input.language) {
395
- prompt += `\nLanguage: ${input.language}\n`;
396
- }
397
- }
398
- return prompt;
399
- }
400
- /**
401
- * Heuristic to detect if input looks like code
402
- */
403
- function looksLikeCode(str) {
404
- const codeIndicators = [
405
- /^(def|function|class|const|let|var|import|export|public|private)\s/,
406
- /[{};()]/,
407
- /=>/,
408
- /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
409
- ];
410
- return codeIndicators.some(re => re.test(str));
411
- }
412
- /**
413
- * Create a new streaming session
414
- */
415
- function createStreamingSession(moduleName) {
416
- return {
417
- session_id: `sess_${randomUUID().slice(0, 12)}`,
418
- module_name: moduleName,
419
- started_at: Date.now(),
420
- chunks_sent: 0,
421
- accumulated_data: {},
422
- accumulated_text: {}
423
- };
424
- }
425
- /**
426
- * Create meta chunk (initial streaming response)
427
- */
428
- function createMetaChunk(session, meta) {
429
- return {
430
- ok: true,
431
- streaming: true,
432
- session_id: session.session_id,
433
- meta
434
- };
435
- }
436
- /**
437
- * Create delta chunk (incremental content)
438
- * Note: Delta chunks don't include session_id per v2.5 spec
439
- */
440
- function createDeltaChunk(session, field, delta) {
441
- session.chunks_sent++;
442
- return {
443
- chunk: {
444
- seq: session.chunks_sent,
445
- type: 'delta',
446
- field,
447
- delta
448
- }
449
- };
450
- }
451
- /**
452
- * Create progress chunk
453
- * Note: Progress chunks don't include session_id per v2.5 spec
454
- */
455
- function createProgressChunk(_session, percent, stage, message) {
456
- return {
457
- progress: {
458
- percent,
459
- stage,
460
- message
461
- }
462
- };
463
- }
464
- /**
465
- * Create final chunk (completion signal)
466
- * Note: Final chunks don't include session_id per v2.5 spec
467
- */
468
- function createFinalChunk(_session, meta, data, usage) {
469
- return {
470
- final: true,
471
- meta,
472
- data,
473
- usage
474
- };
475
- }
476
- /**
477
- * Create error chunk
478
- */
479
- function createErrorChunk(session, code, message, recoverable = false, partialData) {
480
- return {
481
- ok: false,
482
- streaming: true,
483
- session_id: session.session_id,
484
- error: {
485
- code,
486
- message,
487
- recoverable
488
- },
489
- partial_data: partialData
490
- };
491
- }
492
- /**
493
- * Run module with streaming response
494
- *
495
- * @param module - The cognitive module to execute
496
- * @param provider - The LLM provider
497
- * @param options - Run options including streaming callbacks
498
- * @yields Streaming chunks
499
- */
500
- export async function* runModuleStream(module, provider, options = {}) {
501
- const { onChunk, onProgress, heartbeatInterval = 15000, maxDuration = 300000, ...runOptions } = options;
502
- // Create streaming session
503
- const session = createStreamingSession(module.name);
504
- const startTime = Date.now();
505
- // Check if module supports streaming
506
- if (!moduleSupportsStreaming(module)) {
507
- // Fallback to sync execution
508
- const result = await runModule(module, provider, runOptions);
509
- // Emit as single final chunk
510
- if (result.ok && 'meta' in result) {
511
- const finalChunk = createFinalChunk(session, result.meta, result.data);
512
- yield finalChunk;
513
- onChunk?.(finalChunk);
514
- return result;
515
- }
516
- return result;
517
- }
518
- // Check if provider supports streaming
519
- if (!isProviderV25(provider) || !provider.supportsStreaming?.()) {
520
- // Fallback to sync with warning
521
- console.warn('[cognitive] Provider does not support streaming, falling back to sync');
522
- const result = await runModule(module, provider, runOptions);
523
- if (result.ok && 'meta' in result) {
524
- const finalChunk = createFinalChunk(session, result.meta, result.data);
525
- yield finalChunk;
526
- onChunk?.(finalChunk);
527
- }
528
- return result;
529
- }
530
- // Emit initial meta chunk
531
- const metaChunk = createMetaChunk(session, {
532
- confidence: undefined,
533
- risk: 'low',
534
- explain: 'Processing...'
535
- });
536
- yield metaChunk;
537
- onChunk?.(metaChunk);
538
- // Build prompt and messages (same as sync)
539
- const { input, args, verbose = false, useEnvelope, useV22 } = runOptions;
540
- const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
541
- const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
542
- const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
543
- const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
544
- const inputData = input || {};
545
- if (args && !inputData.code && !inputData.query) {
546
- if (looksLikeCode(args)) {
547
- inputData.code = args;
548
- }
549
- else {
550
- inputData.query = args;
976
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
977
+ systemParts.push('- Wrap your response in the envelope format');
978
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
979
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
980
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
981
+ }
982
+ if (module.output?.require_behavior_equivalence) {
983
+ systemParts.push('- Include "behavior_equivalence" (boolean) in data');
984
+ }
985
+ }
986
+ else {
987
+ systemParts.push('', 'OUTPUT FORMAT:');
988
+ systemParts.push('- Respond with ONLY valid JSON');
989
+ systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
990
+ if (module.output?.require_behavior_equivalence) {
991
+ systemParts.push('- Include "behavior_equivalence" (boolean) field');
551
992
  }
552
993
  }
553
- // Extract media from input
554
- const mediaInputs = extractMediaInputs(inputData);
555
- // Build prompt with media placeholders
556
- const prompt = buildPromptWithMedia(module, inputData, mediaInputs);
557
- // Build system message
558
- const systemParts = buildSystemMessage(module, shouldUseEnvelope, shouldUseV22);
559
994
  const messages = [
560
995
  { role: 'system', content: systemParts.join('\n') },
561
996
  { role: 'user', content: prompt },
562
997
  ];
563
998
  try {
564
- // Start streaming invocation
565
- const streamResult = await provider.invokeStream({
999
+ // Invoke provider
1000
+ const result = await provider.invoke({
566
1001
  messages,
567
1002
  jsonSchema: module.outputSchema,
568
1003
  temperature: 0.3,
569
- stream: true,
570
- images: mediaInputs.images,
571
- audio: mediaInputs.audio,
572
- video: mediaInputs.video
573
1004
  });
574
- let accumulatedContent = '';
575
- let lastProgressTime = Date.now();
576
- // Process stream
577
- for await (const chunk of streamResult.stream) {
578
- // Check timeout
579
- if (Date.now() - startTime > maxDuration) {
580
- const errorChunk = createErrorChunk(session, ErrorCodesV25.STREAM_TIMEOUT, `Stream exceeded max duration of ${maxDuration}ms`, false, { partial_content: accumulatedContent });
581
- yield errorChunk;
582
- onChunk?.(errorChunk);
583
- return undefined;
584
- }
585
- // Accumulate content
586
- accumulatedContent += chunk;
587
- // Emit delta chunk
588
- const deltaChunk = createDeltaChunk(session, 'data.rationale', chunk);
589
- yield deltaChunk;
590
- onChunk?.(deltaChunk);
591
- // Emit progress periodically
592
- const now = Date.now();
593
- if (now - lastProgressTime > 1000) {
594
- const elapsed = now - startTime;
595
- const estimatedPercent = Math.min(90, Math.floor(elapsed / maxDuration * 100));
596
- const progressChunk = createProgressChunk(session, estimatedPercent, 'generating', 'Generating response...');
597
- yield progressChunk;
598
- onProgress?.(estimatedPercent, 'Generating response...');
599
- lastProgressTime = now;
600
- }
1005
+ if (verbose) {
1006
+ console.error('--- Response ---');
1007
+ console.error(result.content);
1008
+ console.error('--- End Response ---');
601
1009
  }
602
- // Parse accumulated response
1010
+ // Calculate latency
1011
+ const latencyMs = Date.now() - startTime;
1012
+ // Parse response
603
1013
  let parsed;
604
1014
  try {
605
- const jsonMatch = accumulatedContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
606
- const jsonStr = jsonMatch ? jsonMatch[1] : accumulatedContent;
1015
+ const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1016
+ const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
607
1017
  parsed = JSON.parse(jsonStr.trim());
608
1018
  }
609
- catch {
610
- // Try to extract partial JSON
611
- const errorChunk = createErrorChunk(session, 'E3001', `Failed to parse JSON response`, false, { raw: accumulatedContent });
612
- yield errorChunk;
613
- onChunk?.(errorChunk);
614
- return undefined;
615
- }
616
- // Process parsed response
617
- let result;
618
- if (shouldUseEnvelope && typeof parsed === 'object' && parsed !== null && 'ok' in parsed) {
619
- const response = parseEnvelopeResponseLocal(parsed, accumulatedContent);
620
- if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
621
- const upgraded = wrapV21ToV22Local(parsed, riskRule);
622
- result = {
623
- ok: true,
624
- meta: upgraded.meta,
625
- data: upgraded.data,
626
- raw: accumulatedContent
627
- };
1019
+ catch (e) {
1020
+ const errorResult = makeErrorResponse({
1021
+ code: 'PARSE_ERROR',
1022
+ message: `Failed to parse JSON response: ${e.message}`,
1023
+ explain: 'Failed to parse LLM response as JSON.',
1024
+ details: { raw_response: result.content.substring(0, 500) },
1025
+ });
1026
+ _invokeErrorHooks(module.name, e, null);
1027
+ return errorResult;
1028
+ }
1029
+ // Convert to v2.2 envelope
1030
+ let response;
1031
+ if (isV22Envelope(parsed)) {
1032
+ response = parsed;
1033
+ }
1034
+ else if (isEnvelopeResponse(parsed)) {
1035
+ response = wrapV21ToV22(parsed, riskRule);
1036
+ }
1037
+ else {
1038
+ response = convertLegacyToEnvelope(parsed);
1039
+ }
1040
+ // Add version and meta fields
1041
+ response.version = ENVELOPE_VERSION;
1042
+ if (response.meta) {
1043
+ response.meta.latency_ms = latencyMs;
1044
+ if (traceId) {
1045
+ response.meta.trace_id = traceId;
1046
+ }
1047
+ if (modelOverride) {
1048
+ response.meta.model = modelOverride;
1049
+ }
1050
+ }
1051
+ // Validate and potentially repair output
1052
+ if (response.ok && validateOutput) {
1053
+ // Get data schema (support both "data" and "output" aliases)
1054
+ const dataSchema = module.dataSchema || module.outputSchema;
1055
+ const metaSchema = module.metaSchema;
1056
+ const dataToValidate = response.data ?? {};
1057
+ if (dataSchema && Object.keys(dataSchema).length > 0) {
1058
+ let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1059
+ if (dataErrors.length > 0 && enableRepair) {
1060
+ // Attempt repair pass
1061
+ response = repairEnvelope(response, riskRule);
1062
+ response.version = ENVELOPE_VERSION;
1063
+ // Re-validate after repair
1064
+ const repairedData = response.data ?? {};
1065
+ dataErrors = validateData(repairedData, dataSchema, 'Data');
1066
+ }
1067
+ if (dataErrors.length > 0) {
1068
+ const errorResult = makeErrorResponse({
1069
+ code: 'SCHEMA_VALIDATION_FAILED',
1070
+ message: dataErrors.join('; '),
1071
+ explain: 'Schema validation failed after repair attempt.',
1072
+ partialData: response.data,
1073
+ details: { validation_errors: dataErrors },
1074
+ });
1075
+ _invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
1076
+ return errorResult;
1077
+ }
1078
+ }
1079
+ // v2.2: Validate overflow limits
1080
+ const overflowErrors = validateOverflowLimits(dataToValidate, module);
1081
+ if (overflowErrors.length > 0) {
1082
+ const errorResult = makeErrorResponse({
1083
+ code: 'SCHEMA_VALIDATION_FAILED',
1084
+ message: overflowErrors.join('; '),
1085
+ explain: 'Overflow validation failed.',
1086
+ partialData: dataToValidate,
1087
+ details: { overflow_errors: overflowErrors },
1088
+ });
1089
+ _invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
1090
+ return errorResult;
1091
+ }
1092
+ // v2.2: Validate enum strategy
1093
+ const enumErrors = validateEnumStrategy(dataToValidate, module);
1094
+ if (enumErrors.length > 0) {
1095
+ const errorResult = makeErrorResponse({
1096
+ code: 'SCHEMA_VALIDATION_FAILED',
1097
+ message: enumErrors.join('; '),
1098
+ explain: 'Enum strategy validation failed.',
1099
+ partialData: dataToValidate,
1100
+ details: { enum_errors: enumErrors },
1101
+ });
1102
+ _invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
1103
+ return errorResult;
1104
+ }
1105
+ // Validate meta if schema exists
1106
+ if (metaSchema && Object.keys(metaSchema).length > 0) {
1107
+ let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1108
+ if (metaErrors.length > 0 && enableRepair) {
1109
+ response = repairEnvelope(response, riskRule);
1110
+ response.version = ENVELOPE_VERSION;
1111
+ // Re-validate meta after repair
1112
+ metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1113
+ if (metaErrors.length > 0) {
1114
+ const errorResult = makeErrorResponse({
1115
+ code: 'META_VALIDATION_FAILED',
1116
+ message: metaErrors.join('; '),
1117
+ explain: 'Meta schema validation failed after repair attempt.',
1118
+ partialData: response.data,
1119
+ details: { validation_errors: metaErrors },
1120
+ });
1121
+ _invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
1122
+ return errorResult;
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ else if (enableRepair) {
1128
+ // Repair error envelopes to ensure they have proper meta fields
1129
+ response = repairErrorEnvelope(response);
1130
+ response.version = ENVELOPE_VERSION;
1131
+ }
1132
+ // Invoke after hooks
1133
+ const finalLatencyMs = Date.now() - startTime;
1134
+ _invokeAfterHooks(module.name, response, finalLatencyMs);
1135
+ return response;
1136
+ }
1137
+ catch (e) {
1138
+ const latencyMs = Date.now() - startTime;
1139
+ const errorResult = makeErrorResponse({
1140
+ code: 'UNKNOWN',
1141
+ message: e.message,
1142
+ explain: `Unexpected error: ${e.name}`,
1143
+ details: { exception_type: e.name },
1144
+ });
1145
+ if (errorResult.meta) {
1146
+ errorResult.meta.latency_ms = latencyMs;
1147
+ }
1148
+ _invokeErrorHooks(module.name, e, null);
1149
+ return errorResult;
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Run a cognitive module with streaming output.
1154
+ *
1155
+ * Yields StreamEvent objects as the module executes:
1156
+ * - type="start": Module execution started
1157
+ * - type="chunk": Incremental data chunk (if LLM supports streaming)
1158
+ * - type="meta": Meta information available early
1159
+ * - type="complete": Final complete result
1160
+ * - type="error": Error occurred
1161
+ *
1162
+ * @example
1163
+ * for await (const event of runModuleStream(module, provider, options)) {
1164
+ * if (event.type === 'chunk') {
1165
+ * process.stdout.write(event.chunk);
1166
+ * } else if (event.type === 'complete') {
1167
+ * console.log('Result:', event.result);
1168
+ * }
1169
+ * }
1170
+ */
1171
+ export async function* runModuleStream(module, provider, options = {}) {
1172
+ const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
1173
+ const startTime = Date.now();
1174
+ const moduleName = module.name;
1175
+ function makeEvent(type, extra = {}) {
1176
+ return {
1177
+ type,
1178
+ timestamp_ms: Date.now() - startTime,
1179
+ module_name: moduleName,
1180
+ ...extra,
1181
+ };
1182
+ }
1183
+ try {
1184
+ // Emit start event
1185
+ yield makeEvent('start');
1186
+ // Build input data
1187
+ const inputData = input || {};
1188
+ if (args && !inputData.code && !inputData.query) {
1189
+ if (looksLikeCode(args)) {
1190
+ inputData.code = args;
628
1191
  }
629
1192
  else {
630
- result = response;
1193
+ inputData.query = args;
1194
+ }
1195
+ }
1196
+ // Validate input if enabled
1197
+ if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
1198
+ const inputErrors = validateData(inputData, module.inputSchema, 'Input');
1199
+ if (inputErrors.length > 0) {
1200
+ const errorResult = makeErrorResponse({
1201
+ code: 'INVALID_INPUT',
1202
+ message: inputErrors.join('; '),
1203
+ confidence: 1.0,
1204
+ risk: 'none',
1205
+ });
1206
+ const errorObj = errorResult.error;
1207
+ yield makeEvent('error', { error: errorObj });
1208
+ yield makeEvent('complete', { result: errorResult });
1209
+ return;
631
1210
  }
632
1211
  }
1212
+ // Get risk_rule from module config
1213
+ const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
1214
+ // Build prompt
1215
+ const prompt = buildPrompt(module, inputData);
1216
+ // Build messages
1217
+ const systemParts = [
1218
+ `You are executing the "${module.name}" Cognitive Module.`,
1219
+ '',
1220
+ `RESPONSIBILITY: ${module.responsibility}`,
1221
+ ];
1222
+ if (useV22) {
1223
+ systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
1224
+ systemParts.push('- Wrap your response in the v2.2 envelope format');
1225
+ systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
1226
+ systemParts.push('- Return ONLY valid JSON.');
1227
+ }
1228
+ const messages = [
1229
+ { role: 'system', content: systemParts.join('\n') },
1230
+ { role: 'user', content: prompt },
1231
+ ];
1232
+ // Invoke provider (streaming not yet supported in provider interface, so we fallback)
1233
+ const result = await provider.invoke({
1234
+ messages,
1235
+ jsonSchema: module.outputSchema,
1236
+ temperature: 0.3,
1237
+ });
1238
+ // Emit chunk event with full response
1239
+ yield makeEvent('chunk', { chunk: result.content });
1240
+ // Parse response
1241
+ let parsed;
1242
+ try {
1243
+ const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1244
+ const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
1245
+ parsed = JSON.parse(jsonStr.trim());
1246
+ }
1247
+ catch (e) {
1248
+ const errorResult = makeErrorResponse({
1249
+ code: 'PARSE_ERROR',
1250
+ message: `Failed to parse JSON: ${e.message}`,
1251
+ });
1252
+ // errorResult is always an error response from makeErrorResponse
1253
+ const errorObj = errorResult.error;
1254
+ yield makeEvent('error', { error: errorObj });
1255
+ yield makeEvent('complete', { result: errorResult });
1256
+ return;
1257
+ }
1258
+ // Convert to v2.2 envelope
1259
+ let response;
1260
+ if (isV22Envelope(parsed)) {
1261
+ response = parsed;
1262
+ }
1263
+ else if (isEnvelopeResponse(parsed)) {
1264
+ response = wrapV21ToV22(parsed, riskRule);
1265
+ }
633
1266
  else {
634
- result = parseLegacyResponseLocal(parsed, accumulatedContent);
635
- if (shouldUseV22 && result.ok) {
636
- const data = (result.data ?? {});
637
- result = {
638
- ok: true,
639
- meta: {
640
- confidence: data.confidence ?? 0.5,
641
- risk: aggregateRisk(data, riskRule),
642
- explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
643
- },
644
- data: result.data,
645
- raw: accumulatedContent
646
- };
1267
+ response = convertLegacyToEnvelope(parsed);
1268
+ }
1269
+ // Add version and meta
1270
+ response.version = ENVELOPE_VERSION;
1271
+ const latencyMs = Date.now() - startTime;
1272
+ if (response.meta) {
1273
+ response.meta.latency_ms = latencyMs;
1274
+ if (traceId) {
1275
+ response.meta.trace_id = traceId;
1276
+ }
1277
+ if (model) {
1278
+ response.meta.model = model;
647
1279
  }
1280
+ // Emit meta event early
1281
+ yield makeEvent('meta', { meta: response.meta });
648
1282
  }
649
- // Emit final chunk
650
- if (result.ok && 'meta' in result) {
651
- const finalChunk = createFinalChunk(session, result.meta, result.data, streamResult.usage ? {
652
- input_tokens: streamResult.usage.promptTokens,
653
- output_tokens: streamResult.usage.completionTokens,
654
- total_tokens: streamResult.usage.totalTokens
655
- } : undefined);
656
- yield finalChunk;
657
- onChunk?.(finalChunk);
658
- onProgress?.(100, 'Complete');
1283
+ // Validate and repair output
1284
+ if (response.ok && validateOutput) {
1285
+ const dataSchema = module.dataSchema || module.outputSchema;
1286
+ const metaSchema = module.metaSchema;
1287
+ if (dataSchema && Object.keys(dataSchema).length > 0) {
1288
+ const dataToValidate = response.data ?? {};
1289
+ let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1290
+ if (dataErrors.length > 0 && enableRepair) {
1291
+ response = repairEnvelope(response, riskRule);
1292
+ response.version = ENVELOPE_VERSION;
1293
+ // Re-validate after repair
1294
+ const repairedData = response.data ?? {};
1295
+ dataErrors = validateData(repairedData, dataSchema, 'Data');
1296
+ }
1297
+ if (dataErrors.length > 0) {
1298
+ const errorResult = makeErrorResponse({
1299
+ code: 'SCHEMA_VALIDATION_FAILED',
1300
+ message: dataErrors.join('; '),
1301
+ explain: 'Schema validation failed after repair attempt.',
1302
+ partialData: response.data,
1303
+ details: { validation_errors: dataErrors },
1304
+ });
1305
+ const errorObj = errorResult.error;
1306
+ yield makeEvent('error', { error: errorObj });
1307
+ yield makeEvent('complete', { result: errorResult });
1308
+ return;
1309
+ }
1310
+ }
1311
+ // Validate meta if schema exists
1312
+ if (metaSchema && Object.keys(metaSchema).length > 0) {
1313
+ let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1314
+ if (metaErrors.length > 0 && enableRepair) {
1315
+ response = repairEnvelope(response, riskRule);
1316
+ response.version = ENVELOPE_VERSION;
1317
+ metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1318
+ if (metaErrors.length > 0) {
1319
+ const errorResult = makeErrorResponse({
1320
+ code: 'META_VALIDATION_FAILED',
1321
+ message: metaErrors.join('; '),
1322
+ explain: 'Meta validation failed after repair attempt.',
1323
+ partialData: response.data,
1324
+ details: { validation_errors: metaErrors },
1325
+ });
1326
+ const errorObj = errorResult.error;
1327
+ yield makeEvent('error', { error: errorObj });
1328
+ yield makeEvent('complete', { result: errorResult });
1329
+ return;
1330
+ }
1331
+ }
1332
+ }
659
1333
  }
660
- return result;
1334
+ else if (!response.ok && enableRepair) {
1335
+ response = repairErrorEnvelope(response);
1336
+ response.version = ENVELOPE_VERSION;
1337
+ }
1338
+ // Emit complete event
1339
+ yield makeEvent('complete', { result: response });
661
1340
  }
662
- catch (error) {
663
- const errorChunk = createErrorChunk(session, ErrorCodesV25.STREAM_INTERRUPTED, error instanceof Error ? error.message : 'Stream interrupted', true);
664
- yield errorChunk;
665
- onChunk?.(errorChunk);
666
- return undefined;
1341
+ catch (e) {
1342
+ const errorResult = makeErrorResponse({
1343
+ code: 'UNKNOWN',
1344
+ message: e.message,
1345
+ explain: `Unexpected error: ${e.name}`,
1346
+ });
1347
+ // errorResult is always an error response from makeErrorResponse
1348
+ const errorObj = errorResult.error;
1349
+ yield makeEvent('error', { error: errorObj });
1350
+ yield makeEvent('complete', { result: errorResult });
667
1351
  }
668
1352
  }
669
- // Local versions of helper functions to avoid circular issues
670
- function parseEnvelopeResponseLocal(response, raw) {
1353
+ // =============================================================================
1354
+ // Helper Functions
1355
+ // =============================================================================
1356
+ /**
1357
+ * Check if response is in envelope format
1358
+ */
1359
+ function isEnvelopeResponse(obj) {
1360
+ if (typeof obj !== 'object' || obj === null)
1361
+ return false;
1362
+ const o = obj;
1363
+ return typeof o.ok === 'boolean';
1364
+ }
1365
+ /**
1366
+ * Parse envelope format response (supports both v2.1 and v2.2)
1367
+ */
1368
+ function parseEnvelopeResponse(response, raw) {
1369
+ // Check if v2.2 format (has meta)
671
1370
  if (isV22Envelope(response)) {
672
1371
  if (response.ok) {
673
1372
  return {
@@ -687,6 +1386,7 @@ function parseEnvelopeResponseLocal(response, raw) {
687
1386
  };
688
1387
  }
689
1388
  }
1389
+ // v2.1 format
690
1390
  if (response.ok) {
691
1391
  const data = (response.data ?? {});
692
1392
  return {
@@ -709,45 +1409,17 @@ function parseEnvelopeResponseLocal(response, raw) {
709
1409
  };
710
1410
  }
711
1411
  }
712
- function wrapV21ToV22Local(response, riskRule = 'max_changes_risk') {
713
- if (isV22Envelope(response)) {
714
- return response;
715
- }
716
- if (response.ok) {
717
- const data = (response.data ?? {});
718
- const confidence = data.confidence ?? 0.5;
719
- const rationale = data.rationale ?? '';
720
- return {
721
- ok: true,
722
- meta: {
723
- confidence,
724
- risk: aggregateRisk(data, riskRule),
725
- explain: rationale.slice(0, 280) || 'No explanation provided'
726
- },
727
- data: data
728
- };
729
- }
730
- else {
731
- const errorMsg = response.error?.message ?? 'Unknown error';
732
- return {
733
- ok: false,
734
- meta: {
735
- confidence: 0,
736
- risk: 'high',
737
- explain: errorMsg.slice(0, 280)
738
- },
739
- error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
740
- partial_data: response.partial_data
741
- };
742
- }
743
- }
744
- function parseLegacyResponseLocal(output, raw) {
1412
+ /**
1413
+ * Parse legacy (non-envelope) format response
1414
+ */
1415
+ function parseLegacyResponse(output, raw) {
745
1416
  const outputObj = output;
746
1417
  const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
747
1418
  const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
748
1419
  const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
749
1420
  ? outputObj.behavior_equivalence
750
1421
  : undefined;
1422
+ // Check if this is an error response (has error.code)
751
1423
  if (outputObj.error && typeof outputObj.error === 'object') {
752
1424
  const errorObj = outputObj.error;
753
1425
  if (typeof errorObj.code === 'string') {
@@ -761,6 +1433,7 @@ function parseLegacyResponseLocal(output, raw) {
761
1433
  };
762
1434
  }
763
1435
  }
1436
+ // Return as v2.1 format (data includes confidence)
764
1437
  return {
765
1438
  ok: true,
766
1439
  data: {
@@ -773,237 +1446,119 @@ function parseLegacyResponseLocal(output, raw) {
773
1446
  };
774
1447
  }
775
1448
  /**
776
- * Extract media inputs from module input data
1449
+ * Build prompt with clean variable substitution
1450
+ *
1451
+ * Substitution order (important to avoid partial replacements):
1452
+ * 1. ${variable} - v2 style placeholders
1453
+ * 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
1454
+ * 3. $N - shorthand indexed access (descending order)
1455
+ * 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
777
1456
  */
778
- function extractMediaInputs(input) {
779
- const images = [];
780
- const audio = [];
781
- const video = [];
782
- // Check for images array
783
- if (Array.isArray(input.images)) {
784
- for (const img of input.images) {
785
- if (isValidMediaInput(img)) {
786
- images.push(img);
787
- }
788
- }
1457
+ function buildPrompt(module, input) {
1458
+ let prompt = module.prompt;
1459
+ // v2 style: substitute ${variable} placeholders
1460
+ for (const [key, value] of Object.entries(input)) {
1461
+ const strValue = typeof value === 'string' ? value : JSON.stringify(value);
1462
+ prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
789
1463
  }
790
- // Check for audio array
791
- if (Array.isArray(input.audio)) {
792
- for (const aud of input.audio) {
793
- if (isValidMediaInput(aud)) {
794
- audio.push(aud);
795
- }
1464
+ // v1 compatibility: get args value
1465
+ const argsValue = input.code || input.query || '';
1466
+ // Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
1467
+ // Process in descending order to avoid $1 replacing part of $10
1468
+ if (typeof argsValue === 'string') {
1469
+ const argsList = argsValue.split(/\s+/);
1470
+ for (let i = argsList.length - 1; i >= 0; i--) {
1471
+ const arg = argsList[i];
1472
+ // Replace $ARGUMENTS[N] first
1473
+ prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
1474
+ // Replace $N shorthand
1475
+ prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
796
1476
  }
797
1477
  }
798
- // Check for video array
799
- if (Array.isArray(input.video)) {
800
- for (const vid of input.video) {
801
- if (isValidMediaInput(vid)) {
802
- video.push(vid);
803
- }
1478
+ // Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
1479
+ prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
1480
+ // Append input summary if not already in prompt
1481
+ if (!prompt.includes(argsValue) && argsValue) {
1482
+ prompt += '\n\n## Input\n\n';
1483
+ if (input.code) {
1484
+ prompt += '```\n' + input.code + '\n```\n';
1485
+ }
1486
+ if (input.query) {
1487
+ prompt += input.query + '\n';
1488
+ }
1489
+ if (input.language) {
1490
+ prompt += `\nLanguage: ${input.language}\n`;
804
1491
  }
805
- }
806
- return { images, audio, video };
807
- }
808
- /**
809
- * Validate media input structure
810
- */
811
- function isValidMediaInput(input) {
812
- if (typeof input !== 'object' || input === null)
813
- return false;
814
- const obj = input;
815
- if (obj.type === 'url' && typeof obj.url === 'string')
816
- return true;
817
- if (obj.type === 'base64' && typeof obj.data === 'string' && typeof obj.media_type === 'string')
818
- return true;
819
- if (obj.type === 'file' && typeof obj.path === 'string')
820
- return true;
821
- return false;
822
- }
823
- /**
824
- * Build prompt with media placeholders
825
- */
826
- function buildPromptWithMedia(module, input, media) {
827
- let prompt = buildPrompt(module, input);
828
- // Replace $MEDIA_INPUTS placeholder
829
- if (prompt.includes('$MEDIA_INPUTS')) {
830
- const mediaSummary = buildMediaSummary(media);
831
- prompt = prompt.replace(/\$MEDIA_INPUTS/g, mediaSummary);
832
1492
  }
833
1493
  return prompt;
834
1494
  }
835
1495
  /**
836
- * Build summary of media inputs for prompt
1496
+ * Heuristic to detect if input looks like code
837
1497
  */
838
- function buildMediaSummary(media) {
839
- const parts = [];
840
- if (media.images.length > 0) {
841
- parts.push(`[${media.images.length} image(s) attached]`);
842
- }
843
- if (media.audio.length > 0) {
844
- parts.push(`[${media.audio.length} audio file(s) attached]`);
845
- }
846
- if (media.video.length > 0) {
847
- parts.push(`[${media.video.length} video file(s) attached]`);
848
- }
849
- return parts.length > 0 ? parts.join('\n') : '[No media attached]';
1498
+ function looksLikeCode(str) {
1499
+ const codeIndicators = [
1500
+ /^(def|function|class|const|let|var|import|export|public|private)\s/,
1501
+ /[{};()]/,
1502
+ /=>/,
1503
+ /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
1504
+ ];
1505
+ return codeIndicators.some(re => re.test(str));
850
1506
  }
851
1507
  /**
852
- * Build system message for module execution
1508
+ * Run a cognitive module (legacy API, returns raw output).
1509
+ * For backward compatibility. Throws on error instead of returning error envelope.
853
1510
  */
854
- function buildSystemMessage(module, shouldUseEnvelope, shouldUseV22) {
855
- const systemParts = [
856
- `You are executing the "${module.name}" Cognitive Module.`,
857
- '',
858
- `RESPONSIBILITY: ${module.responsibility}`,
859
- ];
860
- if (module.excludes.length > 0) {
861
- systemParts.push('', 'YOU MUST NOT:');
862
- module.excludes.forEach(e => systemParts.push(`- ${e}`));
863
- }
864
- if (module.constraints) {
865
- systemParts.push('', 'CONSTRAINTS:');
866
- if (module.constraints.no_network)
867
- systemParts.push('- No network access');
868
- if (module.constraints.no_side_effects)
869
- systemParts.push('- No side effects');
870
- if (module.constraints.no_file_write)
871
- systemParts.push('- No file writes');
872
- if (module.constraints.no_inventing_data)
873
- systemParts.push('- Do not invent data');
874
- }
875
- if (module.output?.require_behavior_equivalence) {
876
- systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
877
- systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
878
- systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
879
- const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
880
- systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
881
- }
882
- // Add multimodal instructions if module supports it
883
- if (isModuleV25(module) && moduleSupportsMultimodal(module)) {
884
- const inputModalities = getModuleInputModalities(module);
885
- systemParts.push('', 'MULTIMODAL INPUT:');
886
- systemParts.push(`- This module accepts: ${inputModalities.join(', ')}`);
887
- systemParts.push('- Analyze any attached media carefully');
888
- systemParts.push('- Reference specific elements from the media in your analysis');
889
- }
890
- // Add envelope format instructions
891
- if (shouldUseEnvelope) {
892
- if (shouldUseV22) {
893
- systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
894
- systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
895
- systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
896
- systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
897
- systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
898
- systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
899
- }
900
- else {
901
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
902
- systemParts.push('- Wrap your response in the envelope format');
903
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
904
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
905
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
906
- }
907
- if (module.output?.require_behavior_equivalence) {
908
- systemParts.push('- Include "behavior_equivalence" (boolean) in data');
909
- }
1511
+ export async function runModuleLegacy(module, provider, input, options = {}) {
1512
+ const { validateInput = true, validateOutput = true, model } = options;
1513
+ const result = await runModule(module, provider, {
1514
+ input,
1515
+ validateInput,
1516
+ validateOutput,
1517
+ useEnvelope: false,
1518
+ useV22: false,
1519
+ model,
1520
+ });
1521
+ if (result.ok && 'data' in result) {
1522
+ return result.data;
910
1523
  }
911
1524
  else {
912
- systemParts.push('', 'OUTPUT FORMAT:');
913
- systemParts.push('- Respond with ONLY valid JSON');
914
- systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
915
- if (module.output?.require_behavior_equivalence) {
916
- systemParts.push('- Include "behavior_equivalence" (boolean) field');
917
- }
1525
+ const error = 'error' in result ? result.error : { code: 'UNKNOWN', message: 'Unknown error' };
1526
+ throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
918
1527
  }
919
- return systemParts;
920
1528
  }
1529
+ // =============================================================================
1530
+ // Convenience Functions
1531
+ // =============================================================================
921
1532
  /**
922
- * Load media file as base64
1533
+ * Extract meta from v2.2 envelope for routing/logging.
923
1534
  */
924
- export async function loadMediaAsBase64(path) {
925
- try {
926
- if (!existsSync(path)) {
927
- return null;
928
- }
929
- const buffer = await readFile(path);
930
- const data = buffer.toString('base64');
931
- const media_type = getMediaTypeFromExtension(extname(path));
932
- return { data, media_type };
933
- }
934
- catch {
935
- return null;
936
- }
937
- }
938
- /**
939
- * Get MIME type from file extension
940
- */
941
- function getMediaTypeFromExtension(ext) {
942
- const mimeTypes = {
943
- '.jpg': 'image/jpeg',
944
- '.jpeg': 'image/jpeg',
945
- '.png': 'image/png',
946
- '.gif': 'image/gif',
947
- '.webp': 'image/webp',
948
- '.mp3': 'audio/mpeg',
949
- '.wav': 'audio/wav',
950
- '.ogg': 'audio/ogg',
951
- '.webm': 'audio/webm',
952
- '.mp4': 'video/mp4',
953
- '.mov': 'video/quicktime',
954
- '.pdf': 'application/pdf'
1535
+ export function extractMeta(result) {
1536
+ return result.meta ?? {
1537
+ confidence: 0.5,
1538
+ risk: 'medium',
1539
+ explain: 'No meta available',
955
1540
  };
956
- return mimeTypes[ext.toLowerCase()] ?? 'application/octet-stream';
957
1541
  }
1542
+ // Alias for backward compatibility
1543
+ export const extractMetaV22 = extractMeta;
958
1544
  /**
959
- * Validate media input against module constraints
1545
+ * Determine if result should be escalated to human review based on meta.
960
1546
  */
961
- export function validateMediaInput(media, module, maxSizeMb = 20) {
962
- // Check if module supports multimodal
963
- if (!moduleSupportsMultimodal(module)) {
964
- return {
965
- valid: false,
966
- error: 'Module does not support multimodal input',
967
- code: ErrorCodesV25.MULTIMODAL_NOT_SUPPORTED
968
- };
1547
+ export function shouldEscalate(result, confidenceThreshold = 0.7) {
1548
+ const meta = extractMeta(result);
1549
+ // Escalate if low confidence
1550
+ if (meta.confidence < confidenceThreshold) {
1551
+ return true;
969
1552
  }
970
- // Validate media type
971
- if (media.type === 'base64') {
972
- const mediaType = media.media_type;
973
- if (!isValidMediaType(mediaType)) {
974
- return {
975
- valid: false,
976
- error: `Unsupported media type: ${mediaType}`,
977
- code: ErrorCodesV25.UNSUPPORTED_MEDIA_TYPE
978
- };
979
- }
1553
+ // Escalate if high risk
1554
+ if (meta.risk === 'high') {
1555
+ return true;
980
1556
  }
981
- // Size validation would require fetching/checking actual data
982
- // This is a placeholder for the check
983
- return { valid: true };
984
- }
985
- /**
986
- * Check if media type is supported
987
- */
988
- function isValidMediaType(mediaType) {
989
- const supported = [
990
- 'image/jpeg', 'image/png', 'image/webp', 'image/gif',
991
- 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm',
992
- 'video/mp4', 'video/webm', 'video/quicktime',
993
- 'application/pdf'
994
- ];
995
- return supported.includes(mediaType);
996
- }
997
- /**
998
- * Get runtime capabilities
999
- */
1000
- export function getRuntimeCapabilities() {
1001
- return { ...DEFAULT_RUNTIME_CAPABILITIES };
1002
- }
1003
- /**
1004
- * Check if runtime supports a specific modality
1005
- */
1006
- export function runtimeSupportsModality(modality, direction = 'input') {
1007
- const caps = getRuntimeCapabilities();
1008
- return caps.multimodal[direction].includes(modality);
1557
+ // Escalate if error
1558
+ if (!result.ok) {
1559
+ return true;
1560
+ }
1561
+ return false;
1009
1562
  }
1563
+ // Alias for backward compatibility
1564
+ export const shouldEscalateV22 = shouldEscalate;