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.
- package/README.md +58 -126
- package/dist/cli.js +35 -1
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +344 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/loader.js +77 -1
- package/dist/modules/runner.d.ts +3 -1
- package/dist/modules/runner.js +188 -11
- package/dist/modules/subagent.d.ts +65 -0
- package/dist/modules/subagent.js +185 -0
- package/dist/server/http.d.ts +20 -0
- package/dist/server/http.js +243 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +4 -0
- package/dist/types.d.ts +25 -5
- package/dist/types.js +34 -10
- package/package.json +4 -1
- package/src/cli.ts +36 -1
- package/src/mcp/index.ts +5 -0
- package/src/mcp/server.ts +403 -0
- package/src/modules/index.ts +1 -0
- package/src/modules/loader.ts +101 -2
- package/src/modules/runner.ts +230 -15
- package/src/modules/subagent.ts +275 -0
- package/src/server/http.ts +316 -0
- package/src/server/index.ts +6 -0
- package/src/types.ts +53 -10
package/src/modules/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Runner - Execute Cognitive Modules
|
|
3
|
-
* v2.
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|