cognitive-modules-cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.1: Envelope format support, clean input mapping
3
+ * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
4
  */
5
5
 
6
6
  import type {
@@ -8,11 +8,17 @@ import type {
8
8
  CognitiveModule,
9
9
  ModuleResult,
10
10
  ModuleResultV21,
11
+ ModuleResultV22,
11
12
  Message,
12
13
  ModuleInput,
13
14
  EnvelopeResponse,
14
- ModuleResultData
15
+ EnvelopeResponseV22,
16
+ EnvelopeMeta,
17
+ ModuleResultData,
18
+ RiskLevel,
19
+ RiskRule
15
20
  } from '../types.js';
21
+ import { aggregateRisk, isV22Envelope } from '../types.js';
16
22
 
17
23
  export interface RunOptions {
18
24
  // Clean input (v2 style)
@@ -26,6 +32,134 @@ export interface RunOptions {
26
32
 
27
33
  // Force envelope format (default: auto-detect from module.output.envelope)
28
34
  useEnvelope?: boolean;
35
+
36
+ // Force v2.2 format (default: auto-detect from module.tier)
37
+ useV22?: boolean;
38
+
39
+ // Enable repair pass for validation failures (default: true)
40
+ enableRepair?: boolean;
41
+ }
42
+
43
+ // =============================================================================
44
+ // Repair Pass (v2.2)
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Attempt to repair envelope format issues without changing semantics.
49
+ *
50
+ * Repairs (lossless only):
51
+ * - Missing meta fields (fill with conservative defaults)
52
+ * - Truncate explain if too long
53
+ * - Trim whitespace from string fields
54
+ *
55
+ * Does NOT repair:
56
+ * - Invalid enum values (treated as validation failure)
57
+ */
58
+ function repairEnvelope(
59
+ response: Record<string, unknown>,
60
+ riskRule: RiskRule = 'max_changes_risk',
61
+ maxExplainLength: number = 280
62
+ ): EnvelopeResponseV22<unknown> {
63
+ const repaired = { ...response };
64
+
65
+ // Ensure meta exists
66
+ if (!repaired.meta || typeof repaired.meta !== 'object') {
67
+ repaired.meta = {};
68
+ }
69
+
70
+ const meta = repaired.meta as Record<string, unknown>;
71
+ const data = (repaired.data ?? {}) as Record<string, unknown>;
72
+
73
+ // Repair confidence
74
+ if (typeof meta.confidence !== 'number') {
75
+ meta.confidence = (data.confidence as number) ?? 0.5;
76
+ }
77
+ meta.confidence = Math.max(0, Math.min(1, meta.confidence as number));
78
+
79
+ // Repair risk using configurable aggregation rule
80
+ if (!meta.risk) {
81
+ meta.risk = aggregateRisk(data, riskRule);
82
+ }
83
+ // Trim whitespace only (lossless), validate is valid RiskLevel
84
+ if (typeof meta.risk === 'string') {
85
+ const trimmedRisk = meta.risk.trim().toLowerCase();
86
+ const validRisks = ['none', 'low', 'medium', 'high'];
87
+ meta.risk = validRisks.includes(trimmedRisk) ? trimmedRisk : 'medium';
88
+ } else {
89
+ meta.risk = 'medium'; // Default for invalid type
90
+ }
91
+
92
+ // Repair explain
93
+ if (typeof meta.explain !== 'string') {
94
+ const rationale = data.rationale as string | undefined;
95
+ meta.explain = rationale ? String(rationale).slice(0, maxExplainLength) : 'No explanation provided';
96
+ }
97
+ // Trim whitespace (lossless)
98
+ const explainStr = meta.explain as string;
99
+ meta.explain = explainStr.trim();
100
+ if ((meta.explain as string).length > maxExplainLength) {
101
+ meta.explain = (meta.explain as string).slice(0, maxExplainLength - 3) + '...';
102
+ }
103
+
104
+ // Build proper v2.2 response
105
+ const builtMeta: EnvelopeMeta = {
106
+ confidence: meta.confidence as number,
107
+ risk: meta.risk as RiskLevel,
108
+ explain: meta.explain as string
109
+ };
110
+
111
+ const result: EnvelopeResponseV22<unknown> = repaired.ok === false ? {
112
+ ok: false,
113
+ meta: builtMeta,
114
+ error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
115
+ partial_data: repaired.partial_data
116
+ } : {
117
+ ok: true,
118
+ meta: builtMeta,
119
+ data: repaired.data
120
+ };
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Wrap v2.1 response to v2.2 format
127
+ */
128
+ function wrapV21ToV22(
129
+ response: EnvelopeResponse<unknown>,
130
+ riskRule: RiskRule = 'max_changes_risk'
131
+ ): EnvelopeResponseV22<unknown> {
132
+ if (isV22Envelope(response)) {
133
+ return response;
134
+ }
135
+
136
+ if (response.ok) {
137
+ const data = (response.data ?? {}) as Record<string, unknown>;
138
+ const confidence = (data.confidence as number) ?? 0.5;
139
+ const rationale = (data.rationale as string) ?? '';
140
+
141
+ return {
142
+ ok: true,
143
+ meta: {
144
+ confidence,
145
+ risk: aggregateRisk(data, riskRule),
146
+ explain: rationale.slice(0, 280) || 'No explanation provided'
147
+ },
148
+ data: data as ModuleResultData
149
+ };
150
+ } else {
151
+ const errorMsg = response.error?.message ?? 'Unknown error';
152
+ return {
153
+ ok: false,
154
+ meta: {
155
+ confidence: 0,
156
+ risk: 'high',
157
+ explain: errorMsg.slice(0, 280)
158
+ },
159
+ error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
160
+ partial_data: response.partial_data
161
+ };
162
+ }
29
163
  }
30
164
 
31
165
  export async function runModule(
@@ -33,10 +167,17 @@ export async function runModule(
33
167
  provider: Provider,
34
168
  options: RunOptions = {}
35
169
  ): Promise<ModuleResult> {
36
- const { args, input, verbose = false, useEnvelope } = options;
170
+ const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
37
171
 
38
172
  // Determine if we should use envelope format
39
173
  const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
174
+
175
+ // Determine if we should use v2.2 format
176
+ const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
177
+ const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
178
+
179
+ // Get risk_rule from module config
180
+ const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
40
181
 
41
182
  // Build clean input data (v2 style: no $ARGUMENTS pollution)
42
183
  const inputData: ModuleInput = input || {};
@@ -97,11 +238,20 @@ export async function runModule(
97
238
 
98
239
  // Add envelope format instructions
99
240
  if (shouldUseEnvelope) {
100
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
101
- systemParts.push('- Wrap your response in the envelope format');
102
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
103
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
104
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
241
+ if (shouldUseV22) {
242
+ systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
243
+ systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
244
+ systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
245
+ systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
246
+ systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
247
+ systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
248
+ } else {
249
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
250
+ systemParts.push('- Wrap your response in the envelope format');
251
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
252
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
253
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
254
+ }
105
255
  if (module.output?.require_behavior_equivalence) {
106
256
  systemParts.push('- Include "behavior_equivalence" (boolean) in data');
107
257
  }
@@ -144,11 +294,55 @@ export async function runModule(
144
294
 
145
295
  // Handle envelope format
146
296
  if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
147
- return parseEnvelopeResponse(parsed, result.content);
297
+ let response = parseEnvelopeResponse(parsed, result.content);
298
+
299
+ // Upgrade to v2.2 if needed
300
+ if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
301
+ const upgraded = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
302
+ response = {
303
+ ok: true,
304
+ meta: upgraded.meta as EnvelopeMeta,
305
+ data: (upgraded as { data?: ModuleResultData }).data,
306
+ raw: result.content
307
+ } as ModuleResultV22;
308
+ }
309
+
310
+ // Apply repair pass if enabled and response needs it
311
+ if (enableRepair && response.ok && shouldUseV22) {
312
+ const repaired = repairEnvelope(
313
+ response as unknown as Record<string, unknown>,
314
+ riskRule
315
+ );
316
+ response = {
317
+ ok: true,
318
+ meta: repaired.meta as EnvelopeMeta,
319
+ data: (repaired as { data?: ModuleResultData }).data,
320
+ raw: result.content
321
+ } as ModuleResultV22;
322
+ }
323
+
324
+ return response;
148
325
  }
149
326
 
150
327
  // Handle legacy format (non-envelope)
151
- return parseLegacyResponse(parsed, result.content);
328
+ const legacyResult = parseLegacyResponse(parsed, result.content);
329
+
330
+ // Upgrade to v2.2 if requested
331
+ if (shouldUseV22 && legacyResult.ok) {
332
+ const data = (legacyResult.data ?? {}) as Record<string, unknown>;
333
+ return {
334
+ ok: true,
335
+ meta: {
336
+ confidence: (data.confidence as number) ?? 0.5,
337
+ risk: aggregateRisk(data, riskRule),
338
+ explain: ((data.rationale as string) ?? '').slice(0, 280) || 'No explanation provided'
339
+ },
340
+ data: legacyResult.data,
341
+ raw: result.content
342
+ } as ModuleResultV22;
343
+ }
344
+
345
+ return legacyResult;
152
346
  }
153
347
 
154
348
  /**
@@ -161,11 +355,32 @@ function isEnvelopeResponse(obj: unknown): obj is EnvelopeResponse {
161
355
  }
162
356
 
163
357
  /**
164
- * Parse envelope format response
358
+ * Parse envelope format response (supports both v2.1 and v2.2)
165
359
  */
166
- function parseEnvelopeResponse(response: EnvelopeResponse, raw: string): ModuleResult {
360
+ function parseEnvelopeResponse(response: EnvelopeResponse<unknown>, raw: string): ModuleResult {
361
+ // Check if v2.2 format (has meta)
362
+ if (isV22Envelope(response)) {
363
+ if (response.ok) {
364
+ return {
365
+ ok: true,
366
+ meta: response.meta,
367
+ data: response.data as ModuleResultData,
368
+ raw,
369
+ } as ModuleResultV22;
370
+ } else {
371
+ return {
372
+ ok: false,
373
+ meta: response.meta,
374
+ error: response.error,
375
+ partial_data: response.partial_data,
376
+ raw,
377
+ } as ModuleResultV22;
378
+ }
379
+ }
380
+
381
+ // v2.1 format
167
382
  if (response.ok) {
168
- const data = response.data as ModuleResultData;
383
+ const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
169
384
  return {
170
385
  ok: true,
171
386
  data: {
@@ -175,14 +390,14 @@ function parseEnvelopeResponse(response: EnvelopeResponse, raw: string): ModuleR
175
390
  behavior_equivalence: data.behavior_equivalence,
176
391
  },
177
392
  raw,
178
- };
393
+ } as ModuleResultV21;
179
394
  } else {
180
395
  return {
181
396
  ok: false,
182
397
  error: response.error,
183
398
  partial_data: response.partial_data,
184
399
  raw,
185
- };
400
+ } as ModuleResultV21;
186
401
  }
187
402
  }
188
403
 
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Subagent - Orchestrate module calls with isolated execution contexts.
3
+ *
4
+ * Supports:
5
+ * - @call:module-name - Call another module
6
+ * - @call:module-name(args) - Call with arguments
7
+ * - context: fork - Isolated execution (no shared state)
8
+ * - context: main - Shared execution (default)
9
+ */
10
+
11
+ import type {
12
+ CognitiveModule,
13
+ ModuleResult,
14
+ ModuleInput,
15
+ Provider,
16
+ EnvelopeResponseV22
17
+ } from '../types.js';
18
+ import { loadModule, findModule, getDefaultSearchPaths } from './loader.js';
19
+ import { runModule } from './runner.js';
20
+
21
+ // =============================================================================
22
+ // Types
23
+ // =============================================================================
24
+
25
+ export interface SubagentContext {
26
+ parentId: string | null;
27
+ depth: number;
28
+ maxDepth: number;
29
+ results: Record<string, unknown>;
30
+ isolated: boolean;
31
+ }
32
+
33
+ export interface CallDirective {
34
+ module: string;
35
+ args: string;
36
+ match: string;
37
+ }
38
+
39
+ export interface SubagentRunOptions {
40
+ input?: ModuleInput;
41
+ validateInput?: boolean;
42
+ validateOutput?: boolean;
43
+ maxDepth?: number;
44
+ }
45
+
46
+ // =============================================================================
47
+ // Context Management
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Create a new root context
52
+ */
53
+ export function createContext(maxDepth: number = 5): SubagentContext {
54
+ return {
55
+ parentId: null,
56
+ depth: 0,
57
+ maxDepth,
58
+ results: {},
59
+ isolated: false
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Fork context (isolated - no inherited results)
65
+ */
66
+ export function forkContext(ctx: SubagentContext, moduleName: string): SubagentContext {
67
+ return {
68
+ parentId: moduleName,
69
+ depth: ctx.depth + 1,
70
+ maxDepth: ctx.maxDepth,
71
+ results: {},
72
+ isolated: true
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Extend context (shared - inherits results)
78
+ */
79
+ export function extendContext(ctx: SubagentContext, moduleName: string): SubagentContext {
80
+ return {
81
+ parentId: moduleName,
82
+ depth: ctx.depth + 1,
83
+ maxDepth: ctx.maxDepth,
84
+ results: { ...ctx.results },
85
+ isolated: false
86
+ };
87
+ }
88
+
89
+ // =============================================================================
90
+ // Call Parsing
91
+ // =============================================================================
92
+
93
+ // Pattern to match @call:module-name or @call:module-name(args)
94
+ const CALL_PATTERN = /@call:([a-zA-Z0-9_-]+)(?:\(([^)]*)\))?/g;
95
+
96
+ /**
97
+ * Parse @call directives from text
98
+ */
99
+ export function parseCalls(text: string): CallDirective[] {
100
+ const calls: CallDirective[] = [];
101
+ let match: RegExpExecArray | null;
102
+
103
+ // Reset regex state
104
+ CALL_PATTERN.lastIndex = 0;
105
+
106
+ while ((match = CALL_PATTERN.exec(text)) !== null) {
107
+ calls.push({
108
+ module: match[1],
109
+ args: match[2] || '',
110
+ match: match[0]
111
+ });
112
+ }
113
+
114
+ return calls;
115
+ }
116
+
117
+ /**
118
+ * Replace @call directives with their results
119
+ */
120
+ export function substituteCallResults(
121
+ text: string,
122
+ callResults: Record<string, unknown>
123
+ ): string {
124
+ let result = text;
125
+
126
+ for (const [callStr, callResult] of Object.entries(callResults)) {
127
+ const resultStr = typeof callResult === 'object'
128
+ ? JSON.stringify(callResult, null, 2)
129
+ : String(callResult);
130
+
131
+ result = result.replace(callStr, `[Result from ${callStr}]:\n${resultStr}`);
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ // =============================================================================
138
+ // Orchestrator
139
+ // =============================================================================
140
+
141
+ export class SubagentOrchestrator {
142
+ private provider: Provider;
143
+ private running: Set<string> = new Set();
144
+ private cwd: string;
145
+
146
+ constructor(provider: Provider, cwd: string = process.cwd()) {
147
+ this.provider = provider;
148
+ this.cwd = cwd;
149
+ }
150
+
151
+ /**
152
+ * Run a module with subagent support.
153
+ * Recursively resolves @call directives before final execution.
154
+ */
155
+ async run(
156
+ moduleName: string,
157
+ options: SubagentRunOptions = {},
158
+ context?: SubagentContext
159
+ ): Promise<ModuleResult> {
160
+ const {
161
+ input = {},
162
+ validateInput = true,
163
+ validateOutput = true,
164
+ maxDepth = 5
165
+ } = options;
166
+
167
+ // Initialize context
168
+ const ctx = context ?? createContext(maxDepth);
169
+
170
+ // Check depth limit
171
+ if (ctx.depth > ctx.maxDepth) {
172
+ throw new Error(
173
+ `Max subagent depth (${ctx.maxDepth}) exceeded. Check for circular calls.`
174
+ );
175
+ }
176
+
177
+ // Prevent circular calls
178
+ if (this.running.has(moduleName)) {
179
+ throw new Error(`Circular call detected: ${moduleName}`);
180
+ }
181
+
182
+ this.running.add(moduleName);
183
+
184
+ try {
185
+ // Find and load module
186
+ const searchPaths = getDefaultSearchPaths(this.cwd);
187
+ const module = await findModule(moduleName, searchPaths);
188
+
189
+ if (!module) {
190
+ throw new Error(`Module not found: ${moduleName}`);
191
+ }
192
+
193
+ // Check if this module wants isolated execution
194
+ const moduleContextMode = module.context ?? 'main';
195
+
196
+ // Parse @call directives from prompt
197
+ const calls = parseCalls(module.prompt);
198
+ const callResults: Record<string, unknown> = {};
199
+
200
+ // Resolve each @call directive
201
+ for (const call of calls) {
202
+ const childModule = call.module;
203
+ const childArgs = call.args;
204
+
205
+ // Prepare child input
206
+ const childInput: ModuleInput = childArgs
207
+ ? { query: childArgs, code: childArgs }
208
+ : { ...input };
209
+
210
+ // Determine child context
211
+ const childContext = moduleContextMode === 'fork'
212
+ ? forkContext(ctx, moduleName)
213
+ : extendContext(ctx, moduleName);
214
+
215
+ // Recursively run child module
216
+ const childResult = await this.run(
217
+ childModule,
218
+ {
219
+ input: childInput,
220
+ validateInput: false, // Skip validation for @call args
221
+ validateOutput
222
+ },
223
+ childContext
224
+ );
225
+
226
+ // Store result
227
+ if (childResult.ok && 'data' in childResult) {
228
+ callResults[call.match] = childResult.data;
229
+ } else if ('error' in childResult) {
230
+ callResults[call.match] = { error: childResult.error };
231
+ }
232
+ }
233
+
234
+ // Substitute call results into prompt
235
+ let modifiedModule = module;
236
+ if (Object.keys(callResults).length > 0) {
237
+ const modifiedPrompt = substituteCallResults(module.prompt, callResults);
238
+ modifiedModule = {
239
+ ...module,
240
+ prompt: modifiedPrompt + '\n\n## Subagent Results Available\nThe @call results have been injected above. Use them in your response.\n'
241
+ };
242
+ }
243
+
244
+ // Run the module
245
+ const result = await runModule(modifiedModule, this.provider, {
246
+ input,
247
+ verbose: false,
248
+ useV22: true
249
+ });
250
+
251
+ // Store result in context
252
+ if (result.ok && 'data' in result) {
253
+ ctx.results[moduleName] = result.data;
254
+ }
255
+
256
+ return result;
257
+
258
+ } finally {
259
+ this.running.delete(moduleName);
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Convenience function to run a module with subagent support
266
+ */
267
+ export async function runWithSubagents(
268
+ moduleName: string,
269
+ provider: Provider,
270
+ options: SubagentRunOptions & { cwd?: string } = {}
271
+ ): Promise<ModuleResult> {
272
+ const { cwd = process.cwd(), ...runOptions } = options;
273
+ const orchestrator = new SubagentOrchestrator(provider, cwd);
274
+ return orchestrator.run(moduleName, runOptions);
275
+ }