cognitive-modules-cli 2.2.1 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +519 -23
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +383 -16
  7. package/dist/commands/compose.js +60 -23
  8. package/dist/commands/index.d.ts +4 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/init.js +23 -1
  11. package/dist/commands/migrate.d.ts +30 -0
  12. package/dist/commands/migrate.js +650 -0
  13. package/dist/commands/pipe.d.ts +1 -0
  14. package/dist/commands/pipe.js +31 -11
  15. package/dist/commands/remove.js +33 -2
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +61 -28
  18. package/dist/commands/search.d.ts +28 -0
  19. package/dist/commands/search.js +143 -0
  20. package/dist/commands/test.d.ts +65 -0
  21. package/dist/commands/test.js +454 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +106 -14
  24. package/dist/commands/validate.d.ts +36 -0
  25. package/dist/commands/validate.js +97 -0
  26. package/dist/errors/index.d.ts +225 -0
  27. package/dist/errors/index.js +420 -0
  28. package/dist/mcp/server.js +84 -79
  29. package/dist/modules/composition.js +97 -32
  30. package/dist/modules/loader.js +4 -2
  31. package/dist/modules/runner.d.ts +72 -5
  32. package/dist/modules/runner.js +306 -59
  33. package/dist/modules/subagent.d.ts +6 -1
  34. package/dist/modules/subagent.js +18 -13
  35. package/dist/modules/validator.js +14 -6
  36. package/dist/providers/anthropic.d.ts +15 -0
  37. package/dist/providers/anthropic.js +147 -5
  38. package/dist/providers/base.d.ts +11 -0
  39. package/dist/providers/base.js +18 -0
  40. package/dist/providers/gemini.d.ts +15 -0
  41. package/dist/providers/gemini.js +122 -5
  42. package/dist/providers/ollama.d.ts +15 -0
  43. package/dist/providers/ollama.js +111 -3
  44. package/dist/providers/openai.d.ts +11 -0
  45. package/dist/providers/openai.js +133 -0
  46. package/dist/registry/client.d.ts +212 -0
  47. package/dist/registry/client.js +359 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/registry/tar.d.ts +8 -0
  51. package/dist/registry/tar.js +353 -0
  52. package/dist/server/http.js +301 -45
  53. package/dist/server/index.d.ts +2 -0
  54. package/dist/server/index.js +1 -0
  55. package/dist/server/sse.d.ts +13 -0
  56. package/dist/server/sse.js +22 -0
  57. package/dist/types.d.ts +32 -1
  58. package/dist/types.js +4 -1
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +4 -0
  61. package/package.json +31 -7
  62. package/dist/modules/composition.test.d.ts +0 -11
  63. package/dist/modules/composition.test.js +0 -450
  64. package/dist/modules/policy.test.d.ts +0 -10
  65. package/dist/modules/policy.test.js +0 -369
  66. package/src/cli.ts +0 -471
  67. package/src/commands/add.ts +0 -315
  68. package/src/commands/compose.ts +0 -185
  69. package/src/commands/index.ts +0 -13
  70. package/src/commands/init.ts +0 -94
  71. package/src/commands/list.ts +0 -33
  72. package/src/commands/pipe.ts +0 -76
  73. package/src/commands/remove.ts +0 -57
  74. package/src/commands/run.ts +0 -80
  75. package/src/commands/update.ts +0 -130
  76. package/src/commands/versions.ts +0 -79
  77. package/src/index.ts +0 -90
  78. package/src/mcp/index.ts +0 -5
  79. package/src/mcp/server.ts +0 -403
  80. package/src/modules/composition.test.ts +0 -558
  81. package/src/modules/composition.ts +0 -1674
  82. package/src/modules/index.ts +0 -9
  83. package/src/modules/loader.ts +0 -508
  84. package/src/modules/policy.test.ts +0 -455
  85. package/src/modules/runner.ts +0 -1983
  86. package/src/modules/subagent.ts +0 -277
  87. package/src/modules/validator.ts +0 -700
  88. package/src/providers/anthropic.ts +0 -89
  89. package/src/providers/base.ts +0 -29
  90. package/src/providers/deepseek.ts +0 -83
  91. package/src/providers/gemini.ts +0 -117
  92. package/src/providers/index.ts +0 -78
  93. package/src/providers/minimax.ts +0 -81
  94. package/src/providers/moonshot.ts +0 -82
  95. package/src/providers/ollama.ts +0 -83
  96. package/src/providers/openai.ts +0 -84
  97. package/src/providers/qwen.ts +0 -82
  98. package/src/server/http.ts +0 -316
  99. package/src/server/index.ts +0 -6
  100. package/src/types.ts +0 -599
  101. package/tsconfig.json +0 -17
@@ -1,1983 +0,0 @@
1
- /**
2
- * Module Runner - Execute Cognitive Modules
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
5
- */
6
-
7
- import _Ajv from 'ajv';
8
- const Ajv = _Ajv.default || _Ajv;
9
- import type {
10
- Provider,
11
- CognitiveModule,
12
- ModuleResult,
13
- ModuleResultV21,
14
- ModuleResultV22,
15
- Message,
16
- ModuleInput,
17
- EnvelopeResponse,
18
- EnvelopeResponseV22,
19
- EnvelopeMeta,
20
- ModuleResultData,
21
- RiskLevel,
22
- RiskRule
23
- } from '../types.js';
24
- import { aggregateRisk, isV22Envelope } from '../types.js';
25
-
26
- // =============================================================================
27
- // Schema Validation
28
- // =============================================================================
29
-
30
- const ajv = new Ajv({ allErrors: true, strict: false });
31
-
32
- /**
33
- * Validate data against JSON schema. Returns list of errors.
34
- */
35
- export function validateData(data: unknown, schema: object, label: string = 'Data'): string[] {
36
- const errors: string[] = [];
37
- if (!schema || Object.keys(schema).length === 0) {
38
- return errors;
39
- }
40
-
41
- try {
42
- const validate = ajv.compile(schema);
43
- const valid = validate(data);
44
-
45
- if (!valid && validate.errors) {
46
- for (const err of validate.errors) {
47
- const path = err.instancePath || '/';
48
- errors.push(`${label} validation error: ${err.message} at ${path}`);
49
- }
50
- }
51
- } catch (e) {
52
- errors.push(`Schema error: ${(e as Error).message}`);
53
- }
54
-
55
- return errors;
56
- }
57
-
58
- // =============================================================================
59
- // v2.2 Policy Enforcement
60
- // =============================================================================
61
-
62
- /** Action types that can be checked against policies */
63
- export type PolicyAction = 'network' | 'filesystem_write' | 'side_effects' | 'code_execution';
64
-
65
- /** Tool categories for automatic policy mapping */
66
- const TOOL_POLICY_MAPPING: Record<string, PolicyAction[]> = {
67
- // Network tools
68
- 'fetch': ['network'],
69
- 'http': ['network'],
70
- 'request': ['network'],
71
- 'curl': ['network'],
72
- 'wget': ['network'],
73
- 'api_call': ['network'],
74
-
75
- // Filesystem tools
76
- 'write_file': ['filesystem_write', 'side_effects'],
77
- 'create_file': ['filesystem_write', 'side_effects'],
78
- 'delete_file': ['filesystem_write', 'side_effects'],
79
- 'rename_file': ['filesystem_write', 'side_effects'],
80
- 'mkdir': ['filesystem_write', 'side_effects'],
81
- 'rmdir': ['filesystem_write', 'side_effects'],
82
-
83
- // Code execution tools
84
- 'shell': ['code_execution', 'side_effects'],
85
- 'exec': ['code_execution', 'side_effects'],
86
- 'run_code': ['code_execution', 'side_effects'],
87
- 'code_interpreter': ['code_execution', 'side_effects'],
88
- 'eval': ['code_execution', 'side_effects'],
89
-
90
- // Database tools
91
- 'sql_query': ['side_effects'],
92
- 'db_write': ['side_effects'],
93
- };
94
-
95
- /** Result of a policy check */
96
- export interface PolicyCheckResult {
97
- allowed: boolean;
98
- reason?: string;
99
- policy?: string;
100
- }
101
-
102
- /**
103
- * Check if a tool is allowed by the module's tools policy.
104
- *
105
- * @param toolName The name of the tool to check
106
- * @param module The cognitive module config
107
- * @returns PolicyCheckResult indicating if the tool is allowed
108
- *
109
- * @example
110
- * const result = checkToolPolicy('write_file', module);
111
- * if (!result.allowed) {
112
- * throw new Error(result.reason);
113
- * }
114
- */
115
- export function checkToolPolicy(
116
- toolName: string,
117
- module: CognitiveModule
118
- ): PolicyCheckResult {
119
- const toolsPolicy = module.tools;
120
-
121
- // No policy = allow all
122
- if (!toolsPolicy) {
123
- return { allowed: true };
124
- }
125
-
126
- const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
127
-
128
- // Check explicit denied list first
129
- if (toolsPolicy.denied?.some(d => d.toLowerCase().replace(/[-\s]/g, '_') === normalizedName)) {
130
- return {
131
- allowed: false,
132
- reason: `Tool '${toolName}' is explicitly denied by module tools policy`,
133
- policy: 'tools.denied'
134
- };
135
- }
136
-
137
- // Check policy mode
138
- if (toolsPolicy.policy === 'deny_by_default') {
139
- // In deny_by_default mode, tool must be in allowed list
140
- const isAllowed = toolsPolicy.allowed?.some(
141
- a => a.toLowerCase().replace(/[-\s]/g, '_') === normalizedName
142
- );
143
-
144
- if (!isAllowed) {
145
- return {
146
- allowed: false,
147
- reason: `Tool '${toolName}' not in allowed list (policy: deny_by_default)`,
148
- policy: 'tools.policy'
149
- };
150
- }
151
- }
152
-
153
- return { allowed: true };
154
- }
155
-
156
- /**
157
- * Check if an action is allowed by the module's policies.
158
- *
159
- * @param action The action to check (network, filesystem_write, etc.)
160
- * @param module The cognitive module config
161
- * @returns PolicyCheckResult indicating if the action is allowed
162
- *
163
- * @example
164
- * const result = checkPolicy('network', module);
165
- * if (!result.allowed) {
166
- * throw new Error(result.reason);
167
- * }
168
- */
169
- export function checkPolicy(
170
- action: PolicyAction,
171
- module: CognitiveModule
172
- ): PolicyCheckResult {
173
- const policies = module.policies;
174
-
175
- // No policies = allow all
176
- if (!policies) {
177
- return { allowed: true };
178
- }
179
-
180
- // Check the specific policy
181
- if (policies[action] === 'deny') {
182
- return {
183
- allowed: false,
184
- reason: `Action '${action}' is denied by module policy`,
185
- policy: `policies.${action}`
186
- };
187
- }
188
-
189
- return { allowed: true };
190
- }
191
-
192
- /**
193
- * Check if a tool is allowed considering both tools policy and general policies.
194
- * This performs a comprehensive check that:
195
- * 1. Checks the tools policy (allowed/denied lists)
196
- * 2. Maps the tool to policy actions and checks those
197
- *
198
- * @param toolName The name of the tool to check
199
- * @param module The cognitive module config
200
- * @returns PolicyCheckResult with detailed information
201
- *
202
- * @example
203
- * const result = checkToolAllowed('write_file', module);
204
- * if (!result.allowed) {
205
- * return makeErrorResponse({
206
- * code: 'POLICY_VIOLATION',
207
- * message: result.reason,
208
- * });
209
- * }
210
- */
211
- export function checkToolAllowed(
212
- toolName: string,
213
- module: CognitiveModule
214
- ): PolicyCheckResult {
215
- // First check explicit tools policy
216
- const toolCheck = checkToolPolicy(toolName, module);
217
- if (!toolCheck.allowed) {
218
- return toolCheck;
219
- }
220
-
221
- // Then check mapped policies
222
- const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
223
- const mappedActions = TOOL_POLICY_MAPPING[normalizedName] || [];
224
-
225
- for (const action of mappedActions) {
226
- const policyCheck = checkPolicy(action, module);
227
- if (!policyCheck.allowed) {
228
- return {
229
- allowed: false,
230
- reason: `Tool '${toolName}' requires '${action}' which is denied by policy`,
231
- policy: policyCheck.policy
232
- };
233
- }
234
- }
235
-
236
- return { allowed: true };
237
- }
238
-
239
- /**
240
- * Validate that a list of tools are all allowed by the module's policies.
241
- * Returns all violations found.
242
- *
243
- * @param toolNames List of tool names to check
244
- * @param module The cognitive module config
245
- * @returns Array of PolicyCheckResult for denied tools
246
- */
247
- export function validateToolsAllowed(
248
- toolNames: string[],
249
- module: CognitiveModule
250
- ): PolicyCheckResult[] {
251
- const violations: PolicyCheckResult[] = [];
252
-
253
- for (const toolName of toolNames) {
254
- const result = checkToolAllowed(toolName, module);
255
- if (!result.allowed) {
256
- violations.push(result);
257
- }
258
- }
259
-
260
- return violations;
261
- }
262
-
263
- /**
264
- * Get all denied actions for a module based on its policies.
265
- * Useful for informing LLM about restrictions.
266
- */
267
- export function getDeniedActions(module: CognitiveModule): PolicyAction[] {
268
- const denied: PolicyAction[] = [];
269
- const policies = module.policies;
270
-
271
- if (!policies) return denied;
272
-
273
- const actions: PolicyAction[] = ['network', 'filesystem_write', 'side_effects', 'code_execution'];
274
- for (const action of actions) {
275
- if (policies[action] === 'deny') {
276
- denied.push(action);
277
- }
278
- }
279
-
280
- return denied;
281
- }
282
-
283
- /**
284
- * Get all denied tools for a module based on its tools policy.
285
- */
286
- export function getDeniedTools(module: CognitiveModule): string[] {
287
- return module.tools?.denied || [];
288
- }
289
-
290
- /**
291
- * Get all allowed tools for a module (only meaningful in deny_by_default mode).
292
- */
293
- export function getAllowedTools(module: CognitiveModule): string[] | null {
294
- if (module.tools?.policy === 'deny_by_default') {
295
- return module.tools.allowed || [];
296
- }
297
- return null; // null means "all allowed except denied list"
298
- }
299
-
300
- // =============================================================================
301
- // Tool Call Interceptor
302
- // =============================================================================
303
-
304
- /** Tool call request from LLM */
305
- export interface ToolCallRequest {
306
- name: string;
307
- arguments: Record<string, unknown>;
308
- }
309
-
310
- /** Tool call result */
311
- export interface ToolCallResult {
312
- success: boolean;
313
- result?: unknown;
314
- error?: {
315
- code: string;
316
- message: string;
317
- };
318
- }
319
-
320
- /** Tool executor function type */
321
- export type ToolExecutor = (args: Record<string, unknown>) => Promise<unknown>;
322
-
323
- /**
324
- * ToolCallInterceptor - Intercepts and validates tool calls against module policies.
325
- *
326
- * Use this class to wrap tool execution with policy enforcement:
327
- *
328
- * @example
329
- * const interceptor = new ToolCallInterceptor(module);
330
- *
331
- * // Register tool executors
332
- * interceptor.registerTool('read_file', async (args) => {
333
- * return fs.readFile(args.path as string, 'utf-8');
334
- * });
335
- *
336
- * // Execute tool with policy check
337
- * const result = await interceptor.execute({
338
- * name: 'write_file',
339
- * arguments: { path: '/tmp/test.txt', content: 'hello' }
340
- * });
341
- *
342
- * if (!result.success) {
343
- * console.error('Tool blocked:', result.error);
344
- * }
345
- */
346
- export class ToolCallInterceptor {
347
- private module: CognitiveModule;
348
- private tools: Map<string, ToolExecutor> = new Map();
349
- private callLog: Array<{ tool: string; allowed: boolean; timestamp: number }> = [];
350
-
351
- constructor(module: CognitiveModule) {
352
- this.module = module;
353
- }
354
-
355
- /**
356
- * Register a tool executor.
357
- */
358
- registerTool(name: string, executor: ToolExecutor): void {
359
- this.tools.set(name.toLowerCase(), executor);
360
- }
361
-
362
- /**
363
- * Register multiple tools at once.
364
- */
365
- registerTools(tools: Record<string, ToolExecutor>): void {
366
- for (const [name, executor] of Object.entries(tools)) {
367
- this.registerTool(name, executor);
368
- }
369
- }
370
-
371
- /**
372
- * Check if a tool call is allowed without executing it.
373
- */
374
- checkAllowed(toolName: string): PolicyCheckResult {
375
- return checkToolAllowed(toolName, this.module);
376
- }
377
-
378
- /**
379
- * Execute a tool call with policy enforcement.
380
- *
381
- * @param request The tool call request
382
- * @returns ToolCallResult with success/error
383
- */
384
- async execute(request: ToolCallRequest): Promise<ToolCallResult> {
385
- const { name, arguments: args } = request;
386
- const timestamp = Date.now();
387
-
388
- // Check policy
389
- const policyResult = checkToolAllowed(name, this.module);
390
-
391
- if (!policyResult.allowed) {
392
- this.callLog.push({ tool: name, allowed: false, timestamp });
393
- return {
394
- success: false,
395
- error: {
396
- code: 'TOOL_NOT_ALLOWED',
397
- message: policyResult.reason || `Tool '${name}' is not allowed`,
398
- },
399
- };
400
- }
401
-
402
- // Find executor
403
- const executor = this.tools.get(name.toLowerCase());
404
- if (!executor) {
405
- return {
406
- success: false,
407
- error: {
408
- code: 'TOOL_NOT_FOUND',
409
- message: `Tool '${name}' is not registered`,
410
- },
411
- };
412
- }
413
-
414
- // Execute
415
- try {
416
- this.callLog.push({ tool: name, allowed: true, timestamp });
417
- const result = await executor(args);
418
- return { success: true, result };
419
- } catch (e) {
420
- return {
421
- success: false,
422
- error: {
423
- code: 'TOOL_EXECUTION_ERROR',
424
- message: (e as Error).message,
425
- },
426
- };
427
- }
428
- }
429
-
430
- /**
431
- * Execute multiple tool calls in sequence.
432
- * Stops on first policy violation.
433
- */
434
- async executeMany(requests: ToolCallRequest[]): Promise<ToolCallResult[]> {
435
- const results: ToolCallResult[] = [];
436
-
437
- for (const request of requests) {
438
- const result = await this.execute(request);
439
- results.push(result);
440
-
441
- // Stop on policy violation (not execution error)
442
- if (!result.success && result.error?.code === 'TOOL_NOT_ALLOWED') {
443
- break;
444
- }
445
- }
446
-
447
- return results;
448
- }
449
-
450
- /**
451
- * Get the call log for auditing.
452
- */
453
- getCallLog(): Array<{ tool: string; allowed: boolean; timestamp: number }> {
454
- return [...this.callLog];
455
- }
456
-
457
- /**
458
- * Get summary of denied calls.
459
- */
460
- getDeniedCalls(): Array<{ tool: string; timestamp: number }> {
461
- return this.callLog
462
- .filter(c => !c.allowed)
463
- .map(({ tool, timestamp }) => ({ tool, timestamp }));
464
- }
465
-
466
- /**
467
- * Clear the call log.
468
- */
469
- clearLog(): void {
470
- this.callLog = [];
471
- }
472
-
473
- /**
474
- * Get policy summary for this module.
475
- */
476
- getPolicySummary(): {
477
- deniedActions: PolicyAction[];
478
- deniedTools: string[];
479
- allowedTools: string[] | null;
480
- toolsPolicy: 'allow_by_default' | 'deny_by_default' | undefined;
481
- } {
482
- return {
483
- deniedActions: getDeniedActions(this.module),
484
- deniedTools: getDeniedTools(this.module),
485
- allowedTools: getAllowedTools(this.module),
486
- toolsPolicy: this.module.tools?.policy,
487
- };
488
- }
489
- }
490
-
491
- /**
492
- * Create a policy-aware tool executor wrapper.
493
- *
494
- * @example
495
- * const safeExecutor = createPolicyAwareExecutor(module, 'write_file', async (args) => {
496
- * return fs.writeFile(args.path, args.content);
497
- * });
498
- *
499
- * // This will throw if write_file is denied
500
- * await safeExecutor({ path: '/tmp/test.txt', content: 'hello' });
501
- */
502
- export function createPolicyAwareExecutor(
503
- module: CognitiveModule,
504
- toolName: string,
505
- executor: ToolExecutor
506
- ): ToolExecutor {
507
- return async (args: Record<string, unknown>) => {
508
- const policyResult = checkToolAllowed(toolName, module);
509
-
510
- if (!policyResult.allowed) {
511
- throw new Error(`Policy violation: ${policyResult.reason}`);
512
- }
513
-
514
- return executor(args);
515
- };
516
- }
517
-
518
- // =============================================================================
519
- // v2.2 Runtime Enforcement - Overflow & Enum
520
- // =============================================================================
521
-
522
- /**
523
- * Validate overflow.insights against module's max_items config.
524
- *
525
- * @param data The response data object
526
- * @param module The cognitive module config
527
- * @returns Array of errors if insights exceed limit
528
- */
529
- export function validateOverflowLimits(
530
- data: Record<string, unknown>,
531
- module: CognitiveModule
532
- ): string[] {
533
- const errors: string[] = [];
534
-
535
- const overflowConfig = module.overflow;
536
- if (!overflowConfig?.enabled) {
537
- // If overflow disabled, insights should not exist
538
- const extensions = data.extensions as Record<string, unknown> | undefined;
539
- if (extensions?.insights && Array.isArray(extensions.insights) && extensions.insights.length > 0) {
540
- errors.push('Overflow is disabled but extensions.insights contains data');
541
- }
542
- return errors;
543
- }
544
-
545
- const maxItems = overflowConfig.max_items ?? 5;
546
- const extensions = data.extensions as Record<string, unknown> | undefined;
547
-
548
- if (extensions?.insights && Array.isArray(extensions.insights)) {
549
- const insights = extensions.insights as unknown[];
550
-
551
- if (insights.length > maxItems) {
552
- errors.push(`overflow.max_items exceeded: ${insights.length} > ${maxItems}`);
553
- }
554
-
555
- // Check require_suggested_mapping
556
- if (overflowConfig.require_suggested_mapping) {
557
- for (let i = 0; i < insights.length; i++) {
558
- const insight = insights[i] as Record<string, unknown>;
559
- if (!insight.suggested_mapping) {
560
- errors.push(`insight[${i}] missing required suggested_mapping`);
561
- }
562
- }
563
- }
564
- }
565
-
566
- return errors;
567
- }
568
-
569
- /**
570
- * Validate enum values against module's enum strategy.
571
- * For strict mode, custom enum objects are not allowed.
572
- *
573
- * @param data The response data object
574
- * @param module The cognitive module config
575
- * @returns Array of errors if enum violations found
576
- */
577
- export function validateEnumStrategy(
578
- data: Record<string, unknown>,
579
- module: CognitiveModule
580
- ): string[] {
581
- const errors: string[] = [];
582
-
583
- const enumStrategy = module.enums?.strategy ?? 'strict';
584
-
585
- if (enumStrategy === 'strict') {
586
- // In strict mode, custom enum objects (with 'custom' key) are not allowed
587
- const checkForCustomEnums = (obj: unknown, path: string): void => {
588
- if (obj === null || obj === undefined) return;
589
-
590
- if (Array.isArray(obj)) {
591
- obj.forEach((item, i) => checkForCustomEnums(item, `${path}[${i}]`));
592
- } else if (typeof obj === 'object') {
593
- const record = obj as Record<string, unknown>;
594
-
595
- // Check if this is a custom enum object
596
- if ('custom' in record && 'reason' in record && Object.keys(record).length === 2) {
597
- errors.push(`Custom enum not allowed in strict mode at ${path}: { custom: "${record.custom}" }`);
598
- return;
599
- }
600
-
601
- // Recurse into nested objects
602
- for (const [key, value] of Object.entries(record)) {
603
- checkForCustomEnums(value, `${path}.${key}`);
604
- }
605
- }
606
- };
607
-
608
- checkForCustomEnums(data, 'data');
609
- }
610
-
611
- return errors;
612
- }
613
-
614
- // =============================================================================
615
- // Constants
616
- // =============================================================================
617
-
618
- const ENVELOPE_VERSION = '2.2';
619
-
620
- // =============================================================================
621
- // Utility Functions
622
- // =============================================================================
623
-
624
- /**
625
- * Deep clone an object to avoid mutation issues.
626
- * Handles nested objects, arrays, and primitive values.
627
- */
628
- function deepClone<T>(obj: T): T {
629
- if (obj === null || typeof obj !== 'object') {
630
- return obj;
631
- }
632
- if (Array.isArray(obj)) {
633
- return obj.map(item => deepClone(item)) as T;
634
- }
635
- const cloned = {} as T;
636
- for (const key in obj) {
637
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
638
- cloned[key] = deepClone(obj[key]);
639
- }
640
- }
641
- return cloned;
642
- }
643
-
644
- // =============================================================================
645
- // Observability Hooks
646
- // =============================================================================
647
-
648
- /** Hook called before module execution */
649
- export type BeforeCallHook = (moduleName: string, inputData: ModuleInput, moduleConfig: CognitiveModule) => void;
650
-
651
- /** Hook called after successful module execution */
652
- export type AfterCallHook = (moduleName: string, result: EnvelopeResponseV22<unknown>, latencyMs: number) => void;
653
-
654
- /** Hook called when an error occurs */
655
- export type ErrorHook = (moduleName: string, error: Error, partialResult: unknown | null) => void;
656
-
657
- // Global hook registries
658
- const _beforeCallHooks: BeforeCallHook[] = [];
659
- const _afterCallHooks: AfterCallHook[] = [];
660
- const _errorHooks: ErrorHook[] = [];
661
-
662
- /**
663
- * Decorator to register a before-call hook.
664
- *
665
- * @example
666
- * onBeforeCall((moduleName, inputData, config) => {
667
- * console.log(`Calling ${moduleName} with`, inputData);
668
- * });
669
- */
670
- export function onBeforeCall(hook: BeforeCallHook): BeforeCallHook {
671
- _beforeCallHooks.push(hook);
672
- return hook;
673
- }
674
-
675
- /**
676
- * Decorator to register an after-call hook.
677
- *
678
- * @example
679
- * onAfterCall((moduleName, result, latencyMs) => {
680
- * console.log(`${moduleName} completed in ${latencyMs}ms`);
681
- * });
682
- */
683
- export function onAfterCall(hook: AfterCallHook): AfterCallHook {
684
- _afterCallHooks.push(hook);
685
- return hook;
686
- }
687
-
688
- /**
689
- * Decorator to register an error hook.
690
- *
691
- * @example
692
- * onError((moduleName, error, partialResult) => {
693
- * console.error(`Error in ${moduleName}:`, error);
694
- * });
695
- */
696
- export function onError(hook: ErrorHook): ErrorHook {
697
- _errorHooks.push(hook);
698
- return hook;
699
- }
700
-
701
- /**
702
- * Register a hook programmatically.
703
- */
704
- export function registerHook(
705
- hookType: 'before_call' | 'after_call' | 'error',
706
- hook: BeforeCallHook | AfterCallHook | ErrorHook
707
- ): void {
708
- if (hookType === 'before_call') {
709
- _beforeCallHooks.push(hook as BeforeCallHook);
710
- } else if (hookType === 'after_call') {
711
- _afterCallHooks.push(hook as AfterCallHook);
712
- } else if (hookType === 'error') {
713
- _errorHooks.push(hook as ErrorHook);
714
- } else {
715
- throw new Error(`Unknown hook type: ${hookType}`);
716
- }
717
- }
718
-
719
- /**
720
- * Unregister a hook. Returns true if found and removed.
721
- */
722
- export function unregisterHook(
723
- hookType: 'before_call' | 'after_call' | 'error',
724
- hook: BeforeCallHook | AfterCallHook | ErrorHook
725
- ): boolean {
726
- let hooks: unknown[];
727
- if (hookType === 'before_call') {
728
- hooks = _beforeCallHooks;
729
- } else if (hookType === 'after_call') {
730
- hooks = _afterCallHooks;
731
- } else if (hookType === 'error') {
732
- hooks = _errorHooks;
733
- } else {
734
- return false;
735
- }
736
-
737
- const index = hooks.indexOf(hook);
738
- if (index !== -1) {
739
- hooks.splice(index, 1);
740
- return true;
741
- }
742
- return false;
743
- }
744
-
745
- /**
746
- * Clear all registered hooks.
747
- */
748
- export function clearHooks(): void {
749
- _beforeCallHooks.length = 0;
750
- _afterCallHooks.length = 0;
751
- _errorHooks.length = 0;
752
- }
753
-
754
- function _invokeBeforeHooks(moduleName: string, inputData: ModuleInput, moduleConfig: CognitiveModule): void {
755
- for (const hook of _beforeCallHooks) {
756
- try {
757
- hook(moduleName, inputData, moduleConfig);
758
- } catch {
759
- // Hooks should not break the main flow
760
- }
761
- }
762
- }
763
-
764
- function _invokeAfterHooks(moduleName: string, result: EnvelopeResponseV22<unknown>, latencyMs: number): void {
765
- for (const hook of _afterCallHooks) {
766
- try {
767
- hook(moduleName, result, latencyMs);
768
- } catch {
769
- // Hooks should not break the main flow
770
- }
771
- }
772
- }
773
-
774
- function _invokeErrorHooks(moduleName: string, error: Error, partialResult: unknown | null): void {
775
- for (const hook of _errorHooks) {
776
- try {
777
- hook(moduleName, error, partialResult);
778
- } catch {
779
- // Hooks should not break the main flow
780
- }
781
- }
782
- }
783
-
784
- // =============================================================================
785
- // Error Response Builder
786
- // =============================================================================
787
-
788
- /** Error codes and their default properties */
789
- export const ERROR_PROPERTIES: Record<string, { recoverable: boolean; retry_after_ms: number | null }> = {
790
- MODULE_NOT_FOUND: { recoverable: false, retry_after_ms: null },
791
- INVALID_INPUT: { recoverable: false, retry_after_ms: null },
792
- PARSE_ERROR: { recoverable: true, retry_after_ms: 1000 },
793
- SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
794
- META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
795
- POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
796
- TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
797
- LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
798
- RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
799
- TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
800
- UNKNOWN: { recoverable: false, retry_after_ms: null },
801
- };
802
-
803
- export interface MakeErrorResponseOptions {
804
- code: string;
805
- message: string;
806
- explain?: string;
807
- partialData?: unknown;
808
- details?: Record<string, unknown>;
809
- recoverable?: boolean;
810
- retryAfterMs?: number;
811
- confidence?: number;
812
- risk?: RiskLevel;
813
- }
814
-
815
- /**
816
- * Build a standardized error response with enhanced taxonomy.
817
- */
818
- export function makeErrorResponse(options: MakeErrorResponseOptions): EnvelopeResponseV22<unknown> {
819
- const {
820
- code,
821
- message,
822
- explain,
823
- partialData,
824
- details,
825
- recoverable,
826
- retryAfterMs,
827
- confidence = 0.0,
828
- risk = 'high',
829
- } = options;
830
-
831
- // Get default properties from error code
832
- const defaults = ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN;
833
-
834
- const errorObj: {
835
- code: string;
836
- message: string;
837
- recoverable?: boolean;
838
- retry_after_ms?: number;
839
- details?: Record<string, unknown>;
840
- } = {
841
- code,
842
- message,
843
- };
844
-
845
- // Add recoverable flag
846
- const isRecoverable = recoverable ?? defaults.recoverable;
847
- if (isRecoverable !== undefined) {
848
- errorObj.recoverable = isRecoverable;
849
- }
850
-
851
- // Add retry suggestion
852
- const retryMs = retryAfterMs ?? defaults.retry_after_ms;
853
- if (retryMs !== null) {
854
- errorObj.retry_after_ms = retryMs;
855
- }
856
-
857
- // Add details if provided
858
- if (details) {
859
- errorObj.details = details;
860
- }
861
-
862
- return {
863
- ok: false,
864
- version: ENVELOPE_VERSION,
865
- meta: {
866
- confidence,
867
- risk,
868
- explain: (explain || message).slice(0, 280),
869
- },
870
- error: errorObj,
871
- partial_data: partialData,
872
- };
873
- }
874
-
875
- export interface MakeSuccessResponseOptions {
876
- data: unknown;
877
- confidence: number;
878
- risk: RiskLevel;
879
- explain: string;
880
- latencyMs?: number;
881
- model?: string;
882
- traceId?: string;
883
- }
884
-
885
- /**
886
- * Build a standardized success response.
887
- */
888
- export function makeSuccessResponse(options: MakeSuccessResponseOptions): EnvelopeResponseV22<unknown> {
889
- const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
890
-
891
- const meta: EnvelopeMeta = {
892
- confidence: Math.max(0.0, Math.min(1.0, confidence)),
893
- risk,
894
- explain: explain ? explain.slice(0, 280) : 'No explanation provided',
895
- };
896
-
897
- if (latencyMs !== undefined) {
898
- meta.latency_ms = latencyMs;
899
- }
900
- if (model) {
901
- meta.model = model;
902
- }
903
- if (traceId) {
904
- meta.trace_id = traceId;
905
- }
906
-
907
- return {
908
- ok: true,
909
- version: ENVELOPE_VERSION,
910
- meta,
911
- data,
912
- };
913
- }
914
-
915
- // =============================================================================
916
- // Run Options
917
- // =============================================================================
918
-
919
- export interface RunOptions {
920
- // Clean input (v2 style)
921
- input?: ModuleInput;
922
-
923
- // Legacy CLI args (v1 compatibility) - mapped to input.code or input.query
924
- args?: string;
925
-
926
- // Runtime options
927
- verbose?: boolean;
928
-
929
- // Whether to validate input against schema (default: true)
930
- validateInput?: boolean;
931
-
932
- // Whether to validate output against schema (default: true)
933
- validateOutput?: boolean;
934
-
935
- // Force envelope format (default: auto-detect from module.output.envelope)
936
- useEnvelope?: boolean;
937
-
938
- // Force v2.2 format (default: auto-detect from module.tier)
939
- useV22?: boolean;
940
-
941
- // Enable repair pass for validation failures (default: true)
942
- enableRepair?: boolean;
943
-
944
- // Trace ID for distributed tracing
945
- traceId?: string;
946
-
947
- // Model identifier (for meta.model tracking)
948
- model?: string;
949
- }
950
-
951
- // =============================================================================
952
- // Repair Pass (v2.2)
953
- // =============================================================================
954
-
955
- /**
956
- * Attempt to repair envelope format issues without changing semantics.
957
- *
958
- * Repairs (mostly lossless, except explain truncation):
959
- * - Missing meta fields (fill with conservative defaults)
960
- * - Truncate explain if too long
961
- * - Trim whitespace from string fields
962
- * - Clamp confidence to [0, 1] range
963
- *
964
- * Does NOT repair:
965
- * - Invalid enum values (treated as validation failure)
966
- *
967
- * Note: Returns a deep copy to avoid modifying the original data.
968
- */
969
- function repairEnvelope(
970
- response: Record<string, unknown>,
971
- riskRule: RiskRule = 'max_changes_risk',
972
- maxExplainLength: number = 280
973
- ): EnvelopeResponseV22<unknown> {
974
- // Deep clone to avoid mutation
975
- const repaired = deepClone(response);
976
-
977
- // Ensure meta exists
978
- if (!repaired.meta || typeof repaired.meta !== 'object') {
979
- repaired.meta = {};
980
- }
981
-
982
- const meta = repaired.meta as Record<string, unknown>;
983
- const data = (repaired.data ?? {}) as Record<string, unknown>;
984
-
985
- // Repair confidence
986
- if (typeof meta.confidence !== 'number') {
987
- meta.confidence = (data.confidence as number) ?? 0.5;
988
- }
989
- meta.confidence = Math.max(0, Math.min(1, meta.confidence as number));
990
-
991
- // Repair risk using configurable aggregation rule
992
- if (!meta.risk) {
993
- meta.risk = aggregateRisk(data, riskRule);
994
- }
995
- // Trim whitespace only (lossless), validate is valid RiskLevel
996
- if (typeof meta.risk === 'string') {
997
- const trimmedRisk = meta.risk.trim().toLowerCase();
998
- const validRisks = ['none', 'low', 'medium', 'high'];
999
- meta.risk = validRisks.includes(trimmedRisk) ? trimmedRisk : 'medium';
1000
- } else {
1001
- meta.risk = 'medium'; // Default for invalid type
1002
- }
1003
-
1004
- // Repair explain
1005
- if (typeof meta.explain !== 'string') {
1006
- const rationale = data.rationale as string | undefined;
1007
- meta.explain = rationale ? String(rationale).slice(0, maxExplainLength) : 'No explanation provided';
1008
- }
1009
- // Trim whitespace (lossless)
1010
- const explainStr = meta.explain as string;
1011
- meta.explain = explainStr.trim();
1012
- if ((meta.explain as string).length > maxExplainLength) {
1013
- meta.explain = (meta.explain as string).slice(0, maxExplainLength - 3) + '...';
1014
- }
1015
-
1016
- // Build proper v2.2 response with version
1017
- const builtMeta: EnvelopeMeta = {
1018
- confidence: meta.confidence as number,
1019
- risk: meta.risk as RiskLevel,
1020
- explain: meta.explain as string
1021
- };
1022
-
1023
- const result: EnvelopeResponseV22<unknown> = repaired.ok === false ? {
1024
- ok: false,
1025
- version: ENVELOPE_VERSION,
1026
- meta: builtMeta,
1027
- error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
1028
- partial_data: repaired.partial_data
1029
- } : {
1030
- ok: true,
1031
- version: ENVELOPE_VERSION,
1032
- meta: builtMeta,
1033
- data: repaired.data
1034
- };
1035
-
1036
- return result;
1037
- }
1038
-
1039
- /**
1040
- * Repair error envelope format.
1041
- *
1042
- * Note: Returns a deep copy to avoid modifying the original data.
1043
- */
1044
- function repairErrorEnvelope(
1045
- data: Record<string, unknown>,
1046
- maxExplainLength: number = 280
1047
- ): EnvelopeResponseV22<unknown> {
1048
- // Deep clone to avoid mutation
1049
- const repaired = deepClone(data);
1050
-
1051
- // Ensure meta exists for errors
1052
- if (!repaired.meta || typeof repaired.meta !== 'object') {
1053
- repaired.meta = {};
1054
- }
1055
-
1056
- const meta = repaired.meta as Record<string, unknown>;
1057
-
1058
- // Set default meta for errors
1059
- if (typeof meta.confidence !== 'number') {
1060
- meta.confidence = 0.0;
1061
- }
1062
- if (!meta.risk) {
1063
- meta.risk = 'high';
1064
- }
1065
- if (typeof meta.explain !== 'string') {
1066
- const error = (repaired.error ?? {}) as Record<string, unknown>;
1067
- meta.explain = ((error.message as string) ?? 'An error occurred').slice(0, maxExplainLength);
1068
- }
1069
-
1070
- return {
1071
- ok: false,
1072
- version: ENVELOPE_VERSION,
1073
- meta: {
1074
- confidence: meta.confidence as number,
1075
- risk: meta.risk as RiskLevel,
1076
- explain: meta.explain as string,
1077
- },
1078
- error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
1079
- partial_data: repaired.partial_data,
1080
- };
1081
- }
1082
-
1083
- /**
1084
- * Wrap v2.1 response to v2.2 format
1085
- */
1086
- function wrapV21ToV22(
1087
- response: EnvelopeResponse<unknown>,
1088
- riskRule: RiskRule = 'max_changes_risk'
1089
- ): EnvelopeResponseV22<unknown> {
1090
- if (isV22Envelope(response)) {
1091
- // Already v2.2, but ensure version field exists
1092
- if (!('version' in response) || !response.version) {
1093
- return { ...deepClone(response), version: ENVELOPE_VERSION };
1094
- }
1095
- return response;
1096
- }
1097
-
1098
- if (response.ok) {
1099
- const data = (response.data ?? {}) as Record<string, unknown>;
1100
- const confidence = (data.confidence as number) ?? 0.5;
1101
- const rationale = (data.rationale as string) ?? '';
1102
-
1103
- return {
1104
- ok: true,
1105
- version: ENVELOPE_VERSION,
1106
- meta: {
1107
- confidence,
1108
- risk: aggregateRisk(data, riskRule),
1109
- explain: rationale.slice(0, 280) || 'No explanation provided'
1110
- },
1111
- data: data as ModuleResultData
1112
- };
1113
- } else {
1114
- const errorMsg = response.error?.message ?? 'Unknown error';
1115
- return {
1116
- ok: false,
1117
- version: ENVELOPE_VERSION,
1118
- meta: {
1119
- confidence: 0,
1120
- risk: 'high',
1121
- explain: errorMsg.slice(0, 280)
1122
- },
1123
- error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
1124
- partial_data: response.partial_data
1125
- };
1126
- }
1127
- }
1128
-
1129
- /**
1130
- * Convert legacy format (no envelope) to v2.2 envelope.
1131
- */
1132
- function convertLegacyToEnvelope(
1133
- data: Record<string, unknown>,
1134
- isError: boolean = false
1135
- ): EnvelopeResponseV22<unknown> {
1136
- if (isError || 'error' in data) {
1137
- const error = (data.error ?? {}) as Record<string, unknown>;
1138
- const errorMsg = typeof error === 'object'
1139
- ? ((error.message as string) ?? String(error))
1140
- : String(error);
1141
-
1142
- return {
1143
- ok: false,
1144
- version: ENVELOPE_VERSION,
1145
- meta: {
1146
- confidence: 0.0,
1147
- risk: 'high',
1148
- explain: errorMsg.slice(0, 280),
1149
- },
1150
- error: {
1151
- code: (typeof error === 'object' ? (error.code as string) : undefined) ?? 'UNKNOWN',
1152
- message: errorMsg,
1153
- },
1154
- partial_data: undefined,
1155
- };
1156
- } else {
1157
- const confidence = (data.confidence as number) ?? 0.5;
1158
- const rationale = (data.rationale as string) ?? '';
1159
-
1160
- return {
1161
- ok: true,
1162
- version: ENVELOPE_VERSION,
1163
- meta: {
1164
- confidence,
1165
- risk: aggregateRisk(data),
1166
- explain: rationale.slice(0, 280) || 'No explanation provided',
1167
- },
1168
- data,
1169
- };
1170
- }
1171
- }
1172
-
1173
- // =============================================================================
1174
- // Main Runner
1175
- // =============================================================================
1176
-
1177
- export async function runModule(
1178
- module: CognitiveModule,
1179
- provider: Provider,
1180
- options: RunOptions = {}
1181
- ): Promise<ModuleResult> {
1182
- const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
1183
- const startTime = Date.now();
1184
-
1185
- // Determine if we should use envelope format
1186
- const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
1187
-
1188
- // Determine if we should use v2.2 format
1189
- const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
1190
- const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
1191
-
1192
- // Get risk_rule from module config
1193
- const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
1194
-
1195
- // Build clean input data (v2 style: no $ARGUMENTS pollution)
1196
- const inputData: ModuleInput = input || {};
1197
-
1198
- // Map legacy --args to clean input
1199
- if (args && !inputData.code && !inputData.query) {
1200
- // Determine if args looks like code or natural language
1201
- if (looksLikeCode(args)) {
1202
- inputData.code = args;
1203
- } else {
1204
- inputData.query = args;
1205
- }
1206
- }
1207
-
1208
- // Invoke before hooks
1209
- _invokeBeforeHooks(module.name, inputData, module);
1210
-
1211
- // Validate input against schema
1212
- if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
1213
- const inputErrors = validateData(inputData, module.inputSchema, 'Input');
1214
- if (inputErrors.length > 0) {
1215
- const errorResult = makeErrorResponse({
1216
- code: 'INVALID_INPUT',
1217
- message: inputErrors.join('; '),
1218
- explain: 'Input validation failed.',
1219
- confidence: 1.0,
1220
- risk: 'none',
1221
- details: { validation_errors: inputErrors },
1222
- });
1223
- _invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
1224
- return errorResult as ModuleResult;
1225
- }
1226
- }
1227
-
1228
- // Build prompt with clean substitution
1229
- const prompt = buildPrompt(module, inputData);
1230
-
1231
- if (verbose) {
1232
- console.error('--- Module ---');
1233
- console.error(`Name: ${module.name} (${module.format})`);
1234
- console.error(`Responsibility: ${module.responsibility}`);
1235
- console.error(`Envelope: ${shouldUseEnvelope}`);
1236
- console.error('--- Input ---');
1237
- console.error(JSON.stringify(inputData, null, 2));
1238
- console.error('--- Prompt ---');
1239
- console.error(prompt);
1240
- console.error('--- End ---');
1241
- }
1242
-
1243
- // Build system message based on module config
1244
- const systemParts: string[] = [
1245
- `You are executing the "${module.name}" Cognitive Module.`,
1246
- '',
1247
- `RESPONSIBILITY: ${module.responsibility}`,
1248
- ];
1249
-
1250
- if (module.excludes.length > 0) {
1251
- systemParts.push('', 'YOU MUST NOT:');
1252
- module.excludes.forEach(e => systemParts.push(`- ${e}`));
1253
- }
1254
-
1255
- if (module.constraints) {
1256
- systemParts.push('', 'CONSTRAINTS:');
1257
- if (module.constraints.no_network) systemParts.push('- No network access');
1258
- if (module.constraints.no_side_effects) systemParts.push('- No side effects');
1259
- if (module.constraints.no_file_write) systemParts.push('- No file writes');
1260
- if (module.constraints.no_inventing_data) systemParts.push('- Do not invent data');
1261
- }
1262
-
1263
- if (module.output?.require_behavior_equivalence) {
1264
- systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
1265
- systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
1266
- systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
1267
-
1268
- const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
1269
- systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
1270
- }
1271
-
1272
- // Add envelope format instructions
1273
- if (shouldUseEnvelope) {
1274
- if (shouldUseV22) {
1275
- systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
1276
- systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
1277
- systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
1278
- systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
1279
- systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
1280
- systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
1281
- } else {
1282
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
1283
- systemParts.push('- Wrap your response in the envelope format');
1284
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
1285
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
1286
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
1287
- }
1288
- if (module.output?.require_behavior_equivalence) {
1289
- systemParts.push('- Include "behavior_equivalence" (boolean) in data');
1290
- }
1291
- } else {
1292
- systemParts.push('', 'OUTPUT FORMAT:');
1293
- systemParts.push('- Respond with ONLY valid JSON');
1294
- systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
1295
- if (module.output?.require_behavior_equivalence) {
1296
- systemParts.push('- Include "behavior_equivalence" (boolean) field');
1297
- }
1298
- }
1299
-
1300
- const messages: Message[] = [
1301
- { role: 'system', content: systemParts.join('\n') },
1302
- { role: 'user', content: prompt },
1303
- ];
1304
-
1305
- try {
1306
- // Invoke provider
1307
- const result = await provider.invoke({
1308
- messages,
1309
- jsonSchema: module.outputSchema,
1310
- temperature: 0.3,
1311
- });
1312
-
1313
- if (verbose) {
1314
- console.error('--- Response ---');
1315
- console.error(result.content);
1316
- console.error('--- End Response ---');
1317
- }
1318
-
1319
- // Calculate latency
1320
- const latencyMs = Date.now() - startTime;
1321
-
1322
- // Parse response
1323
- let parsed: unknown;
1324
- try {
1325
- const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1326
- const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
1327
- parsed = JSON.parse(jsonStr.trim());
1328
- } catch (e) {
1329
- const errorResult = makeErrorResponse({
1330
- code: 'PARSE_ERROR',
1331
- message: `Failed to parse JSON response: ${(e as Error).message}`,
1332
- explain: 'Failed to parse LLM response as JSON.',
1333
- details: { raw_response: result.content.substring(0, 500) },
1334
- });
1335
- _invokeErrorHooks(module.name, e as Error, null);
1336
- return errorResult as ModuleResult;
1337
- }
1338
-
1339
- // Convert to v2.2 envelope
1340
- let response: EnvelopeResponseV22<unknown>;
1341
- if (isV22Envelope(parsed as EnvelopeResponse<unknown>)) {
1342
- response = parsed as EnvelopeResponseV22<unknown>;
1343
- } else if (isEnvelopeResponse(parsed)) {
1344
- response = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
1345
- } else {
1346
- response = convertLegacyToEnvelope(parsed as Record<string, unknown>);
1347
- }
1348
-
1349
- // Add version and meta fields
1350
- response.version = ENVELOPE_VERSION;
1351
- if (response.meta) {
1352
- response.meta.latency_ms = latencyMs;
1353
- if (traceId) {
1354
- response.meta.trace_id = traceId;
1355
- }
1356
- if (modelOverride) {
1357
- response.meta.model = modelOverride;
1358
- }
1359
- }
1360
-
1361
- // Validate and potentially repair output
1362
- if (response.ok && validateOutput) {
1363
- // Get data schema (support both "data" and "output" aliases)
1364
- const dataSchema = module.dataSchema || module.outputSchema;
1365
- const metaSchema = module.metaSchema;
1366
- const dataToValidate = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
1367
-
1368
- if (dataSchema && Object.keys(dataSchema).length > 0) {
1369
- let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1370
-
1371
- if (dataErrors.length > 0 && enableRepair) {
1372
- // Attempt repair pass
1373
- response = repairEnvelope(
1374
- response as unknown as Record<string, unknown>,
1375
- riskRule
1376
- );
1377
- response.version = ENVELOPE_VERSION;
1378
-
1379
- // Re-validate after repair
1380
- const repairedData = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
1381
- dataErrors = validateData(repairedData, dataSchema, 'Data');
1382
- }
1383
-
1384
- if (dataErrors.length > 0) {
1385
- const errorResult = makeErrorResponse({
1386
- code: 'SCHEMA_VALIDATION_FAILED',
1387
- message: dataErrors.join('; '),
1388
- explain: 'Schema validation failed after repair attempt.',
1389
- partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
1390
- details: { validation_errors: dataErrors },
1391
- });
1392
- _invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data);
1393
- return errorResult as ModuleResult;
1394
- }
1395
- }
1396
-
1397
- // v2.2: Validate overflow limits
1398
- const overflowErrors = validateOverflowLimits(dataToValidate as Record<string, unknown>, module);
1399
- if (overflowErrors.length > 0) {
1400
- const errorResult = makeErrorResponse({
1401
- code: 'SCHEMA_VALIDATION_FAILED',
1402
- message: overflowErrors.join('; '),
1403
- explain: 'Overflow validation failed.',
1404
- partialData: dataToValidate,
1405
- details: { overflow_errors: overflowErrors },
1406
- });
1407
- _invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
1408
- return errorResult as ModuleResult;
1409
- }
1410
-
1411
- // v2.2: Validate enum strategy
1412
- const enumErrors = validateEnumStrategy(dataToValidate as Record<string, unknown>, module);
1413
- if (enumErrors.length > 0) {
1414
- const errorResult = makeErrorResponse({
1415
- code: 'SCHEMA_VALIDATION_FAILED',
1416
- message: enumErrors.join('; '),
1417
- explain: 'Enum strategy validation failed.',
1418
- partialData: dataToValidate,
1419
- details: { enum_errors: enumErrors },
1420
- });
1421
- _invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
1422
- return errorResult as ModuleResult;
1423
- }
1424
-
1425
- // Validate meta if schema exists
1426
- if (metaSchema && Object.keys(metaSchema).length > 0) {
1427
- let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1428
-
1429
- if (metaErrors.length > 0 && enableRepair) {
1430
- response = repairEnvelope(
1431
- response as unknown as Record<string, unknown>,
1432
- riskRule
1433
- );
1434
- response.version = ENVELOPE_VERSION;
1435
-
1436
- // Re-validate meta after repair
1437
- metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1438
-
1439
- if (metaErrors.length > 0) {
1440
- const errorResult = makeErrorResponse({
1441
- code: 'META_VALIDATION_FAILED',
1442
- message: metaErrors.join('; '),
1443
- explain: 'Meta schema validation failed after repair attempt.',
1444
- partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
1445
- details: { validation_errors: metaErrors },
1446
- });
1447
- _invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data);
1448
- return errorResult as ModuleResult;
1449
- }
1450
- }
1451
- }
1452
- } else if (enableRepair) {
1453
- // Repair error envelopes to ensure they have proper meta fields
1454
- response = repairErrorEnvelope(response as unknown as Record<string, unknown>);
1455
- response.version = ENVELOPE_VERSION;
1456
- }
1457
-
1458
- // Invoke after hooks
1459
- const finalLatencyMs = Date.now() - startTime;
1460
- _invokeAfterHooks(module.name, response, finalLatencyMs);
1461
-
1462
- return response as ModuleResult;
1463
-
1464
- } catch (e) {
1465
- const latencyMs = Date.now() - startTime;
1466
- const errorResult = makeErrorResponse({
1467
- code: 'UNKNOWN',
1468
- message: (e as Error).message,
1469
- explain: `Unexpected error: ${(e as Error).name}`,
1470
- details: { exception_type: (e as Error).name },
1471
- });
1472
- if (errorResult.meta) {
1473
- errorResult.meta.latency_ms = latencyMs;
1474
- }
1475
- _invokeErrorHooks(module.name, e as Error, null);
1476
- return errorResult as ModuleResult;
1477
- }
1478
- }
1479
-
1480
- // =============================================================================
1481
- // Streaming Support
1482
- // =============================================================================
1483
-
1484
- /** Event types emitted during streaming execution */
1485
- export type StreamEventType = 'start' | 'chunk' | 'meta' | 'complete' | 'error';
1486
-
1487
- /** Event emitted during streaming execution */
1488
- export interface StreamEvent {
1489
- type: StreamEventType;
1490
- timestamp_ms: number;
1491
- module_name: string;
1492
- chunk?: string;
1493
- meta?: EnvelopeMeta;
1494
- result?: EnvelopeResponseV22<unknown>;
1495
- error?: { code: string; message: string };
1496
- }
1497
-
1498
- export interface StreamOptions {
1499
- input?: ModuleInput;
1500
- args?: string;
1501
- validateInput?: boolean;
1502
- validateOutput?: boolean;
1503
- useV22?: boolean;
1504
- enableRepair?: boolean;
1505
- traceId?: string;
1506
- model?: string; // Model identifier for meta.model
1507
- }
1508
-
1509
- /**
1510
- * Run a cognitive module with streaming output.
1511
- *
1512
- * Yields StreamEvent objects as the module executes:
1513
- * - type="start": Module execution started
1514
- * - type="chunk": Incremental data chunk (if LLM supports streaming)
1515
- * - type="meta": Meta information available early
1516
- * - type="complete": Final complete result
1517
- * - type="error": Error occurred
1518
- *
1519
- * @example
1520
- * for await (const event of runModuleStream(module, provider, options)) {
1521
- * if (event.type === 'chunk') {
1522
- * process.stdout.write(event.chunk);
1523
- * } else if (event.type === 'complete') {
1524
- * console.log('Result:', event.result);
1525
- * }
1526
- * }
1527
- */
1528
- export async function* runModuleStream(
1529
- module: CognitiveModule,
1530
- provider: Provider,
1531
- options: StreamOptions = {}
1532
- ): AsyncGenerator<StreamEvent> {
1533
- const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
1534
- const startTime = Date.now();
1535
- const moduleName = module.name;
1536
-
1537
- function makeEvent(type: StreamEventType, extra: Partial<StreamEvent> = {}): StreamEvent {
1538
- return {
1539
- type,
1540
- timestamp_ms: Date.now() - startTime,
1541
- module_name: moduleName,
1542
- ...extra,
1543
- };
1544
- }
1545
-
1546
- try {
1547
- // Emit start event
1548
- yield makeEvent('start');
1549
-
1550
- // Build input data
1551
- const inputData: ModuleInput = input || {};
1552
- if (args && !inputData.code && !inputData.query) {
1553
- if (looksLikeCode(args)) {
1554
- inputData.code = args;
1555
- } else {
1556
- inputData.query = args;
1557
- }
1558
- }
1559
-
1560
- // Validate input if enabled
1561
- if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
1562
- const inputErrors = validateData(inputData, module.inputSchema, 'Input');
1563
- if (inputErrors.length > 0) {
1564
- const errorResult = makeErrorResponse({
1565
- code: 'INVALID_INPUT',
1566
- message: inputErrors.join('; '),
1567
- confidence: 1.0,
1568
- risk: 'none',
1569
- });
1570
- const errorObj = (errorResult as { error: { code: string; message: string } }).error;
1571
- yield makeEvent('error', { error: errorObj });
1572
- yield makeEvent('complete', { result: errorResult });
1573
- return;
1574
- }
1575
- }
1576
-
1577
- // Get risk_rule from module config
1578
- const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
1579
-
1580
- // Build prompt
1581
- const prompt = buildPrompt(module, inputData);
1582
-
1583
- // Build messages
1584
- const systemParts: string[] = [
1585
- `You are executing the "${module.name}" Cognitive Module.`,
1586
- '',
1587
- `RESPONSIBILITY: ${module.responsibility}`,
1588
- ];
1589
-
1590
- if (useV22) {
1591
- systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
1592
- systemParts.push('- Wrap your response in the v2.2 envelope format');
1593
- systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
1594
- systemParts.push('- Return ONLY valid JSON.');
1595
- }
1596
-
1597
- const messages: Message[] = [
1598
- { role: 'system', content: systemParts.join('\n') },
1599
- { role: 'user', content: prompt },
1600
- ];
1601
-
1602
- // Invoke provider (streaming not yet supported in provider interface, so we fallback)
1603
- const result = await provider.invoke({
1604
- messages,
1605
- jsonSchema: module.outputSchema,
1606
- temperature: 0.3,
1607
- });
1608
-
1609
- // Emit chunk event with full response
1610
- yield makeEvent('chunk', { chunk: result.content });
1611
-
1612
- // Parse response
1613
- let parsed: unknown;
1614
- try {
1615
- const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1616
- const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
1617
- parsed = JSON.parse(jsonStr.trim());
1618
- } catch (e) {
1619
- const errorResult = makeErrorResponse({
1620
- code: 'PARSE_ERROR',
1621
- message: `Failed to parse JSON: ${(e as Error).message}`,
1622
- });
1623
- // errorResult is always an error response from makeErrorResponse
1624
- const errorObj = (errorResult as { error: { code: string; message: string } }).error;
1625
- yield makeEvent('error', { error: errorObj });
1626
- yield makeEvent('complete', { result: errorResult });
1627
- return;
1628
- }
1629
-
1630
- // Convert to v2.2 envelope
1631
- let response: EnvelopeResponseV22<unknown>;
1632
- if (isV22Envelope(parsed as EnvelopeResponse<unknown>)) {
1633
- response = parsed as EnvelopeResponseV22<unknown>;
1634
- } else if (isEnvelopeResponse(parsed)) {
1635
- response = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
1636
- } else {
1637
- response = convertLegacyToEnvelope(parsed as Record<string, unknown>);
1638
- }
1639
-
1640
- // Add version and meta
1641
- response.version = ENVELOPE_VERSION;
1642
- const latencyMs = Date.now() - startTime;
1643
- if (response.meta) {
1644
- response.meta.latency_ms = latencyMs;
1645
- if (traceId) {
1646
- response.meta.trace_id = traceId;
1647
- }
1648
- if (model) {
1649
- response.meta.model = model;
1650
- }
1651
- // Emit meta event early
1652
- yield makeEvent('meta', { meta: response.meta });
1653
- }
1654
-
1655
- // Validate and repair output
1656
- if (response.ok && validateOutput) {
1657
- const dataSchema = module.dataSchema || module.outputSchema;
1658
- const metaSchema = module.metaSchema;
1659
-
1660
- if (dataSchema && Object.keys(dataSchema).length > 0) {
1661
- const dataToValidate = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
1662
- let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
1663
-
1664
- if (dataErrors.length > 0 && enableRepair) {
1665
- response = repairEnvelope(response as unknown as Record<string, unknown>, riskRule);
1666
- response.version = ENVELOPE_VERSION;
1667
- // Re-validate after repair
1668
- const repairedData = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
1669
- dataErrors = validateData(repairedData, dataSchema, 'Data');
1670
- }
1671
-
1672
- if (dataErrors.length > 0) {
1673
- const errorResult = makeErrorResponse({
1674
- code: 'SCHEMA_VALIDATION_FAILED',
1675
- message: dataErrors.join('; '),
1676
- explain: 'Schema validation failed after repair attempt.',
1677
- partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
1678
- details: { validation_errors: dataErrors },
1679
- });
1680
- const errorObj = (errorResult as { error: { code: string; message: string } }).error;
1681
- yield makeEvent('error', { error: errorObj });
1682
- yield makeEvent('complete', { result: errorResult });
1683
- return;
1684
- }
1685
- }
1686
-
1687
- // Validate meta if schema exists
1688
- if (metaSchema && Object.keys(metaSchema).length > 0) {
1689
- let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1690
-
1691
- if (metaErrors.length > 0 && enableRepair) {
1692
- response = repairEnvelope(response as unknown as Record<string, unknown>, riskRule);
1693
- response.version = ENVELOPE_VERSION;
1694
- metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
1695
-
1696
- if (metaErrors.length > 0) {
1697
- const errorResult = makeErrorResponse({
1698
- code: 'META_VALIDATION_FAILED',
1699
- message: metaErrors.join('; '),
1700
- explain: 'Meta validation failed after repair attempt.',
1701
- partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
1702
- details: { validation_errors: metaErrors },
1703
- });
1704
- const errorObj = (errorResult as { error: { code: string; message: string } }).error;
1705
- yield makeEvent('error', { error: errorObj });
1706
- yield makeEvent('complete', { result: errorResult });
1707
- return;
1708
- }
1709
- }
1710
- }
1711
- } else if (!response.ok && enableRepair) {
1712
- response = repairErrorEnvelope(response as unknown as Record<string, unknown>);
1713
- response.version = ENVELOPE_VERSION;
1714
- }
1715
-
1716
- // Emit complete event
1717
- yield makeEvent('complete', { result: response });
1718
-
1719
- } catch (e) {
1720
- const errorResult = makeErrorResponse({
1721
- code: 'UNKNOWN',
1722
- message: (e as Error).message,
1723
- explain: `Unexpected error: ${(e as Error).name}`,
1724
- });
1725
- // errorResult is always an error response from makeErrorResponse
1726
- const errorObj = (errorResult as { error: { code: string; message: string } }).error;
1727
- yield makeEvent('error', { error: errorObj });
1728
- yield makeEvent('complete', { result: errorResult });
1729
- }
1730
- }
1731
-
1732
- // =============================================================================
1733
- // Helper Functions
1734
- // =============================================================================
1735
-
1736
- /**
1737
- * Check if response is in envelope format
1738
- */
1739
- function isEnvelopeResponse(obj: unknown): obj is EnvelopeResponse {
1740
- if (typeof obj !== 'object' || obj === null) return false;
1741
- const o = obj as Record<string, unknown>;
1742
- return typeof o.ok === 'boolean';
1743
- }
1744
-
1745
- /**
1746
- * Parse envelope format response (supports both v2.1 and v2.2)
1747
- */
1748
- function parseEnvelopeResponse(response: EnvelopeResponse<unknown>, raw: string): ModuleResult {
1749
- // Check if v2.2 format (has meta)
1750
- if (isV22Envelope(response)) {
1751
- if (response.ok) {
1752
- return {
1753
- ok: true,
1754
- meta: response.meta,
1755
- data: response.data as ModuleResultData,
1756
- raw,
1757
- } as ModuleResultV22;
1758
- } else {
1759
- return {
1760
- ok: false,
1761
- meta: response.meta,
1762
- error: response.error,
1763
- partial_data: response.partial_data,
1764
- raw,
1765
- } as ModuleResultV22;
1766
- }
1767
- }
1768
-
1769
- // v2.1 format
1770
- if (response.ok) {
1771
- const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
1772
- return {
1773
- ok: true,
1774
- data: {
1775
- ...data,
1776
- confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
1777
- rationale: typeof data.rationale === 'string' ? data.rationale : '',
1778
- behavior_equivalence: data.behavior_equivalence,
1779
- },
1780
- raw,
1781
- } as ModuleResultV21;
1782
- } else {
1783
- return {
1784
- ok: false,
1785
- error: response.error,
1786
- partial_data: response.partial_data,
1787
- raw,
1788
- } as ModuleResultV21;
1789
- }
1790
- }
1791
-
1792
- /**
1793
- * Parse legacy (non-envelope) format response
1794
- */
1795
- function parseLegacyResponse(output: unknown, raw: string): ModuleResult {
1796
- const outputObj = output as Record<string, unknown>;
1797
- const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
1798
- const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
1799
- const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
1800
- ? outputObj.behavior_equivalence
1801
- : undefined;
1802
-
1803
- // Check if this is an error response (has error.code)
1804
- if (outputObj.error && typeof outputObj.error === 'object') {
1805
- const errorObj = outputObj.error as Record<string, unknown>;
1806
- if (typeof errorObj.code === 'string') {
1807
- return {
1808
- ok: false,
1809
- error: {
1810
- code: errorObj.code,
1811
- message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
1812
- },
1813
- raw,
1814
- };
1815
- }
1816
- }
1817
-
1818
- // Return as v2.1 format (data includes confidence)
1819
- return {
1820
- ok: true,
1821
- data: {
1822
- ...outputObj,
1823
- confidence,
1824
- rationale,
1825
- behavior_equivalence: behaviorEquivalence,
1826
- },
1827
- raw,
1828
- } as ModuleResultV21;
1829
- }
1830
-
1831
- /**
1832
- * Build prompt with clean variable substitution
1833
- *
1834
- * Substitution order (important to avoid partial replacements):
1835
- * 1. ${variable} - v2 style placeholders
1836
- * 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
1837
- * 3. $N - shorthand indexed access (descending order)
1838
- * 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
1839
- */
1840
- function buildPrompt(module: CognitiveModule, input: ModuleInput): string {
1841
- let prompt = module.prompt;
1842
-
1843
- // v2 style: substitute ${variable} placeholders
1844
- for (const [key, value] of Object.entries(input)) {
1845
- const strValue = typeof value === 'string' ? value : JSON.stringify(value);
1846
- prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
1847
- }
1848
-
1849
- // v1 compatibility: get args value
1850
- const argsValue = input.code || input.query || '';
1851
-
1852
- // Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
1853
- // Process in descending order to avoid $1 replacing part of $10
1854
- if (typeof argsValue === 'string') {
1855
- const argsList = argsValue.split(/\s+/);
1856
- for (let i = argsList.length - 1; i >= 0; i--) {
1857
- const arg = argsList[i];
1858
- // Replace $ARGUMENTS[N] first
1859
- prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
1860
- // Replace $N shorthand
1861
- prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
1862
- }
1863
- }
1864
-
1865
- // Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
1866
- prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
1867
-
1868
- // Append input summary if not already in prompt
1869
- if (!prompt.includes(argsValue) && argsValue) {
1870
- prompt += '\n\n## Input\n\n';
1871
- if (input.code) {
1872
- prompt += '```\n' + input.code + '\n```\n';
1873
- }
1874
- if (input.query) {
1875
- prompt += input.query + '\n';
1876
- }
1877
- if (input.language) {
1878
- prompt += `\nLanguage: ${input.language}\n`;
1879
- }
1880
- }
1881
-
1882
- return prompt;
1883
- }
1884
-
1885
- /**
1886
- * Heuristic to detect if input looks like code
1887
- */
1888
- function looksLikeCode(str: string): boolean {
1889
- const codeIndicators = [
1890
- /^(def|function|class|const|let|var|import|export|public|private)\s/,
1891
- /[{};()]/,
1892
- /=>/,
1893
- /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
1894
- ];
1895
- return codeIndicators.some(re => re.test(str));
1896
- }
1897
-
1898
- // =============================================================================
1899
- // Legacy API (for backward compatibility)
1900
- // =============================================================================
1901
-
1902
- export interface RunModuleLegacyOptions {
1903
- validateInput?: boolean;
1904
- validateOutput?: boolean;
1905
- model?: string;
1906
- }
1907
-
1908
- /**
1909
- * Run a cognitive module (legacy API, returns raw output).
1910
- * For backward compatibility. Throws on error instead of returning error envelope.
1911
- */
1912
- export async function runModuleLegacy(
1913
- module: CognitiveModule,
1914
- provider: Provider,
1915
- input: ModuleInput,
1916
- options: RunModuleLegacyOptions = {}
1917
- ): Promise<unknown> {
1918
- const { validateInput = true, validateOutput = true, model } = options;
1919
-
1920
- const result = await runModule(module, provider, {
1921
- input,
1922
- validateInput,
1923
- validateOutput,
1924
- useEnvelope: false,
1925
- useV22: false,
1926
- model,
1927
- });
1928
-
1929
- if (result.ok && 'data' in result) {
1930
- return result.data;
1931
- } else {
1932
- const error = 'error' in result ? result.error : { code: 'UNKNOWN', message: 'Unknown error' };
1933
- throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
1934
- }
1935
- }
1936
-
1937
- // =============================================================================
1938
- // Convenience Functions
1939
- // =============================================================================
1940
-
1941
- /**
1942
- * Extract meta from v2.2 envelope for routing/logging.
1943
- */
1944
- export function extractMeta(result: EnvelopeResponseV22<unknown>): EnvelopeMeta {
1945
- return result.meta ?? {
1946
- confidence: 0.5,
1947
- risk: 'medium',
1948
- explain: 'No meta available',
1949
- };
1950
- }
1951
-
1952
- // Alias for backward compatibility
1953
- export const extractMetaV22 = extractMeta;
1954
-
1955
- /**
1956
- * Determine if result should be escalated to human review based on meta.
1957
- */
1958
- export function shouldEscalate(
1959
- result: EnvelopeResponseV22<unknown>,
1960
- confidenceThreshold: number = 0.7
1961
- ): boolean {
1962
- const meta = extractMeta(result);
1963
-
1964
- // Escalate if low confidence
1965
- if (meta.confidence < confidenceThreshold) {
1966
- return true;
1967
- }
1968
-
1969
- // Escalate if high risk
1970
- if (meta.risk === 'high') {
1971
- return true;
1972
- }
1973
-
1974
- // Escalate if error
1975
- if (!result.ok) {
1976
- return true;
1977
- }
1978
-
1979
- return false;
1980
- }
1981
-
1982
- // Alias for backward compatibility
1983
- export const shouldEscalateV22 = shouldEscalate;