cognitive-modules-cli 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +65 -12
- package/dist/commands/compose.d.ts +31 -0
- package/dist/commands/compose.js +148 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/modules/composition.d.ts +251 -0
- package/dist/modules/composition.js +1265 -0
- package/dist/modules/composition.test.d.ts +11 -0
- package/dist/modules/composition.test.js +450 -0
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +2 -0
- package/dist/modules/loader.d.ts +22 -2
- package/dist/modules/loader.js +167 -4
- package/dist/modules/policy.test.d.ts +10 -0
- package/dist/modules/policy.test.js +369 -0
- package/dist/modules/runner.d.ts +357 -1
- package/dist/modules/runner.js +1221 -64
- package/dist/modules/subagent.js +2 -0
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +629 -0
- package/dist/types.d.ts +92 -8
- package/package.json +2 -1
- package/src/cli.ts +73 -12
- package/src/commands/compose.ts +185 -0
- package/src/commands/index.ts +1 -0
- package/src/index.ts +35 -0
- package/src/modules/composition.test.ts +558 -0
- package/src/modules/composition.ts +1674 -0
- package/src/modules/index.ts +2 -0
- package/src/modules/loader.ts +196 -6
- package/src/modules/policy.test.ts +455 -0
- package/src/modules/runner.ts +1562 -74
- package/src/modules/subagent.ts +2 -0
- package/src/modules/validator.ts +700 -0
- package/src/types.ts +112 -8
- package/tsconfig.json +1 -1
package/dist/modules/runner.js
CHANGED
|
@@ -1,24 +1,712 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Runner - Execute Cognitive Modules
|
|
3
3
|
* v2.2: Envelope format with meta/data separation, risk_rule, repair pass
|
|
4
|
+
* v2.2.1: Version field, enhanced error taxonomy, observability hooks, streaming
|
|
4
5
|
*/
|
|
6
|
+
import _Ajv from 'ajv';
|
|
7
|
+
const Ajv = _Ajv.default || _Ajv;
|
|
5
8
|
import { aggregateRisk, isV22Envelope } from '../types.js';
|
|
6
9
|
// =============================================================================
|
|
10
|
+
// Schema Validation
|
|
11
|
+
// =============================================================================
|
|
12
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
13
|
+
/**
|
|
14
|
+
* Validate data against JSON schema. Returns list of errors.
|
|
15
|
+
*/
|
|
16
|
+
export function validateData(data, schema, label = 'Data') {
|
|
17
|
+
const errors = [];
|
|
18
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
19
|
+
return errors;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const validate = ajv.compile(schema);
|
|
23
|
+
const valid = validate(data);
|
|
24
|
+
if (!valid && validate.errors) {
|
|
25
|
+
for (const err of validate.errors) {
|
|
26
|
+
const path = err.instancePath || '/';
|
|
27
|
+
errors.push(`${label} validation error: ${err.message} at ${path}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
errors.push(`Schema error: ${e.message}`);
|
|
33
|
+
}
|
|
34
|
+
return errors;
|
|
35
|
+
}
|
|
36
|
+
/** Tool categories for automatic policy mapping */
|
|
37
|
+
const TOOL_POLICY_MAPPING = {
|
|
38
|
+
// Network tools
|
|
39
|
+
'fetch': ['network'],
|
|
40
|
+
'http': ['network'],
|
|
41
|
+
'request': ['network'],
|
|
42
|
+
'curl': ['network'],
|
|
43
|
+
'wget': ['network'],
|
|
44
|
+
'api_call': ['network'],
|
|
45
|
+
// Filesystem tools
|
|
46
|
+
'write_file': ['filesystem_write', 'side_effects'],
|
|
47
|
+
'create_file': ['filesystem_write', 'side_effects'],
|
|
48
|
+
'delete_file': ['filesystem_write', 'side_effects'],
|
|
49
|
+
'rename_file': ['filesystem_write', 'side_effects'],
|
|
50
|
+
'mkdir': ['filesystem_write', 'side_effects'],
|
|
51
|
+
'rmdir': ['filesystem_write', 'side_effects'],
|
|
52
|
+
// Code execution tools
|
|
53
|
+
'shell': ['code_execution', 'side_effects'],
|
|
54
|
+
'exec': ['code_execution', 'side_effects'],
|
|
55
|
+
'run_code': ['code_execution', 'side_effects'],
|
|
56
|
+
'code_interpreter': ['code_execution', 'side_effects'],
|
|
57
|
+
'eval': ['code_execution', 'side_effects'],
|
|
58
|
+
// Database tools
|
|
59
|
+
'sql_query': ['side_effects'],
|
|
60
|
+
'db_write': ['side_effects'],
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Check if a tool is allowed by the module's tools policy.
|
|
64
|
+
*
|
|
65
|
+
* @param toolName The name of the tool to check
|
|
66
|
+
* @param module The cognitive module config
|
|
67
|
+
* @returns PolicyCheckResult indicating if the tool is allowed
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const result = checkToolPolicy('write_file', module);
|
|
71
|
+
* if (!result.allowed) {
|
|
72
|
+
* throw new Error(result.reason);
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
export function checkToolPolicy(toolName, module) {
|
|
76
|
+
const toolsPolicy = module.tools;
|
|
77
|
+
// No policy = allow all
|
|
78
|
+
if (!toolsPolicy) {
|
|
79
|
+
return { allowed: true };
|
|
80
|
+
}
|
|
81
|
+
const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
|
|
82
|
+
// Check explicit denied list first
|
|
83
|
+
if (toolsPolicy.denied?.some(d => d.toLowerCase().replace(/[-\s]/g, '_') === normalizedName)) {
|
|
84
|
+
return {
|
|
85
|
+
allowed: false,
|
|
86
|
+
reason: `Tool '${toolName}' is explicitly denied by module tools policy`,
|
|
87
|
+
policy: 'tools.denied'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Check policy mode
|
|
91
|
+
if (toolsPolicy.policy === 'deny_by_default') {
|
|
92
|
+
// In deny_by_default mode, tool must be in allowed list
|
|
93
|
+
const isAllowed = toolsPolicy.allowed?.some(a => a.toLowerCase().replace(/[-\s]/g, '_') === normalizedName);
|
|
94
|
+
if (!isAllowed) {
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
reason: `Tool '${toolName}' not in allowed list (policy: deny_by_default)`,
|
|
98
|
+
policy: 'tools.policy'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { allowed: true };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if an action is allowed by the module's policies.
|
|
106
|
+
*
|
|
107
|
+
* @param action The action to check (network, filesystem_write, etc.)
|
|
108
|
+
* @param module The cognitive module config
|
|
109
|
+
* @returns PolicyCheckResult indicating if the action is allowed
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* const result = checkPolicy('network', module);
|
|
113
|
+
* if (!result.allowed) {
|
|
114
|
+
* throw new Error(result.reason);
|
|
115
|
+
* }
|
|
116
|
+
*/
|
|
117
|
+
export function checkPolicy(action, module) {
|
|
118
|
+
const policies = module.policies;
|
|
119
|
+
// No policies = allow all
|
|
120
|
+
if (!policies) {
|
|
121
|
+
return { allowed: true };
|
|
122
|
+
}
|
|
123
|
+
// Check the specific policy
|
|
124
|
+
if (policies[action] === 'deny') {
|
|
125
|
+
return {
|
|
126
|
+
allowed: false,
|
|
127
|
+
reason: `Action '${action}' is denied by module policy`,
|
|
128
|
+
policy: `policies.${action}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { allowed: true };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if a tool is allowed considering both tools policy and general policies.
|
|
135
|
+
* This performs a comprehensive check that:
|
|
136
|
+
* 1. Checks the tools policy (allowed/denied lists)
|
|
137
|
+
* 2. Maps the tool to policy actions and checks those
|
|
138
|
+
*
|
|
139
|
+
* @param toolName The name of the tool to check
|
|
140
|
+
* @param module The cognitive module config
|
|
141
|
+
* @returns PolicyCheckResult with detailed information
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const result = checkToolAllowed('write_file', module);
|
|
145
|
+
* if (!result.allowed) {
|
|
146
|
+
* return makeErrorResponse({
|
|
147
|
+
* code: 'POLICY_VIOLATION',
|
|
148
|
+
* message: result.reason,
|
|
149
|
+
* });
|
|
150
|
+
* }
|
|
151
|
+
*/
|
|
152
|
+
export function checkToolAllowed(toolName, module) {
|
|
153
|
+
// First check explicit tools policy
|
|
154
|
+
const toolCheck = checkToolPolicy(toolName, module);
|
|
155
|
+
if (!toolCheck.allowed) {
|
|
156
|
+
return toolCheck;
|
|
157
|
+
}
|
|
158
|
+
// Then check mapped policies
|
|
159
|
+
const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
|
|
160
|
+
const mappedActions = TOOL_POLICY_MAPPING[normalizedName] || [];
|
|
161
|
+
for (const action of mappedActions) {
|
|
162
|
+
const policyCheck = checkPolicy(action, module);
|
|
163
|
+
if (!policyCheck.allowed) {
|
|
164
|
+
return {
|
|
165
|
+
allowed: false,
|
|
166
|
+
reason: `Tool '${toolName}' requires '${action}' which is denied by policy`,
|
|
167
|
+
policy: policyCheck.policy
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { allowed: true };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate that a list of tools are all allowed by the module's policies.
|
|
175
|
+
* Returns all violations found.
|
|
176
|
+
*
|
|
177
|
+
* @param toolNames List of tool names to check
|
|
178
|
+
* @param module The cognitive module config
|
|
179
|
+
* @returns Array of PolicyCheckResult for denied tools
|
|
180
|
+
*/
|
|
181
|
+
export function validateToolsAllowed(toolNames, module) {
|
|
182
|
+
const violations = [];
|
|
183
|
+
for (const toolName of toolNames) {
|
|
184
|
+
const result = checkToolAllowed(toolName, module);
|
|
185
|
+
if (!result.allowed) {
|
|
186
|
+
violations.push(result);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return violations;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get all denied actions for a module based on its policies.
|
|
193
|
+
* Useful for informing LLM about restrictions.
|
|
194
|
+
*/
|
|
195
|
+
export function getDeniedActions(module) {
|
|
196
|
+
const denied = [];
|
|
197
|
+
const policies = module.policies;
|
|
198
|
+
if (!policies)
|
|
199
|
+
return denied;
|
|
200
|
+
const actions = ['network', 'filesystem_write', 'side_effects', 'code_execution'];
|
|
201
|
+
for (const action of actions) {
|
|
202
|
+
if (policies[action] === 'deny') {
|
|
203
|
+
denied.push(action);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return denied;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get all denied tools for a module based on its tools policy.
|
|
210
|
+
*/
|
|
211
|
+
export function getDeniedTools(module) {
|
|
212
|
+
return module.tools?.denied || [];
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get all allowed tools for a module (only meaningful in deny_by_default mode).
|
|
216
|
+
*/
|
|
217
|
+
export function getAllowedTools(module) {
|
|
218
|
+
if (module.tools?.policy === 'deny_by_default') {
|
|
219
|
+
return module.tools.allowed || [];
|
|
220
|
+
}
|
|
221
|
+
return null; // null means "all allowed except denied list"
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* ToolCallInterceptor - Intercepts and validates tool calls against module policies.
|
|
225
|
+
*
|
|
226
|
+
* Use this class to wrap tool execution with policy enforcement:
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* const interceptor = new ToolCallInterceptor(module);
|
|
230
|
+
*
|
|
231
|
+
* // Register tool executors
|
|
232
|
+
* interceptor.registerTool('read_file', async (args) => {
|
|
233
|
+
* return fs.readFile(args.path as string, 'utf-8');
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* // Execute tool with policy check
|
|
237
|
+
* const result = await interceptor.execute({
|
|
238
|
+
* name: 'write_file',
|
|
239
|
+
* arguments: { path: '/tmp/test.txt', content: 'hello' }
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* if (!result.success) {
|
|
243
|
+
* console.error('Tool blocked:', result.error);
|
|
244
|
+
* }
|
|
245
|
+
*/
|
|
246
|
+
export class ToolCallInterceptor {
|
|
247
|
+
module;
|
|
248
|
+
tools = new Map();
|
|
249
|
+
callLog = [];
|
|
250
|
+
constructor(module) {
|
|
251
|
+
this.module = module;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Register a tool executor.
|
|
255
|
+
*/
|
|
256
|
+
registerTool(name, executor) {
|
|
257
|
+
this.tools.set(name.toLowerCase(), executor);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Register multiple tools at once.
|
|
261
|
+
*/
|
|
262
|
+
registerTools(tools) {
|
|
263
|
+
for (const [name, executor] of Object.entries(tools)) {
|
|
264
|
+
this.registerTool(name, executor);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Check if a tool call is allowed without executing it.
|
|
269
|
+
*/
|
|
270
|
+
checkAllowed(toolName) {
|
|
271
|
+
return checkToolAllowed(toolName, this.module);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Execute a tool call with policy enforcement.
|
|
275
|
+
*
|
|
276
|
+
* @param request The tool call request
|
|
277
|
+
* @returns ToolCallResult with success/error
|
|
278
|
+
*/
|
|
279
|
+
async execute(request) {
|
|
280
|
+
const { name, arguments: args } = request;
|
|
281
|
+
const timestamp = Date.now();
|
|
282
|
+
// Check policy
|
|
283
|
+
const policyResult = checkToolAllowed(name, this.module);
|
|
284
|
+
if (!policyResult.allowed) {
|
|
285
|
+
this.callLog.push({ tool: name, allowed: false, timestamp });
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error: {
|
|
289
|
+
code: 'TOOL_NOT_ALLOWED',
|
|
290
|
+
message: policyResult.reason || `Tool '${name}' is not allowed`,
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// Find executor
|
|
295
|
+
const executor = this.tools.get(name.toLowerCase());
|
|
296
|
+
if (!executor) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
error: {
|
|
300
|
+
code: 'TOOL_NOT_FOUND',
|
|
301
|
+
message: `Tool '${name}' is not registered`,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// Execute
|
|
306
|
+
try {
|
|
307
|
+
this.callLog.push({ tool: name, allowed: true, timestamp });
|
|
308
|
+
const result = await executor(args);
|
|
309
|
+
return { success: true, result };
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
error: {
|
|
315
|
+
code: 'TOOL_EXECUTION_ERROR',
|
|
316
|
+
message: e.message,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Execute multiple tool calls in sequence.
|
|
323
|
+
* Stops on first policy violation.
|
|
324
|
+
*/
|
|
325
|
+
async executeMany(requests) {
|
|
326
|
+
const results = [];
|
|
327
|
+
for (const request of requests) {
|
|
328
|
+
const result = await this.execute(request);
|
|
329
|
+
results.push(result);
|
|
330
|
+
// Stop on policy violation (not execution error)
|
|
331
|
+
if (!result.success && result.error?.code === 'TOOL_NOT_ALLOWED') {
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return results;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get the call log for auditing.
|
|
339
|
+
*/
|
|
340
|
+
getCallLog() {
|
|
341
|
+
return [...this.callLog];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get summary of denied calls.
|
|
345
|
+
*/
|
|
346
|
+
getDeniedCalls() {
|
|
347
|
+
return this.callLog
|
|
348
|
+
.filter(c => !c.allowed)
|
|
349
|
+
.map(({ tool, timestamp }) => ({ tool, timestamp }));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Clear the call log.
|
|
353
|
+
*/
|
|
354
|
+
clearLog() {
|
|
355
|
+
this.callLog = [];
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get policy summary for this module.
|
|
359
|
+
*/
|
|
360
|
+
getPolicySummary() {
|
|
361
|
+
return {
|
|
362
|
+
deniedActions: getDeniedActions(this.module),
|
|
363
|
+
deniedTools: getDeniedTools(this.module),
|
|
364
|
+
allowedTools: getAllowedTools(this.module),
|
|
365
|
+
toolsPolicy: this.module.tools?.policy,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Create a policy-aware tool executor wrapper.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* const safeExecutor = createPolicyAwareExecutor(module, 'write_file', async (args) => {
|
|
374
|
+
* return fs.writeFile(args.path, args.content);
|
|
375
|
+
* });
|
|
376
|
+
*
|
|
377
|
+
* // This will throw if write_file is denied
|
|
378
|
+
* await safeExecutor({ path: '/tmp/test.txt', content: 'hello' });
|
|
379
|
+
*/
|
|
380
|
+
export function createPolicyAwareExecutor(module, toolName, executor) {
|
|
381
|
+
return async (args) => {
|
|
382
|
+
const policyResult = checkToolAllowed(toolName, module);
|
|
383
|
+
if (!policyResult.allowed) {
|
|
384
|
+
throw new Error(`Policy violation: ${policyResult.reason}`);
|
|
385
|
+
}
|
|
386
|
+
return executor(args);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// v2.2 Runtime Enforcement - Overflow & Enum
|
|
391
|
+
// =============================================================================
|
|
392
|
+
/**
|
|
393
|
+
* Validate overflow.insights against module's max_items config.
|
|
394
|
+
*
|
|
395
|
+
* @param data The response data object
|
|
396
|
+
* @param module The cognitive module config
|
|
397
|
+
* @returns Array of errors if insights exceed limit
|
|
398
|
+
*/
|
|
399
|
+
export function validateOverflowLimits(data, module) {
|
|
400
|
+
const errors = [];
|
|
401
|
+
const overflowConfig = module.overflow;
|
|
402
|
+
if (!overflowConfig?.enabled) {
|
|
403
|
+
// If overflow disabled, insights should not exist
|
|
404
|
+
const extensions = data.extensions;
|
|
405
|
+
if (extensions?.insights && Array.isArray(extensions.insights) && extensions.insights.length > 0) {
|
|
406
|
+
errors.push('Overflow is disabled but extensions.insights contains data');
|
|
407
|
+
}
|
|
408
|
+
return errors;
|
|
409
|
+
}
|
|
410
|
+
const maxItems = overflowConfig.max_items ?? 5;
|
|
411
|
+
const extensions = data.extensions;
|
|
412
|
+
if (extensions?.insights && Array.isArray(extensions.insights)) {
|
|
413
|
+
const insights = extensions.insights;
|
|
414
|
+
if (insights.length > maxItems) {
|
|
415
|
+
errors.push(`overflow.max_items exceeded: ${insights.length} > ${maxItems}`);
|
|
416
|
+
}
|
|
417
|
+
// Check require_suggested_mapping
|
|
418
|
+
if (overflowConfig.require_suggested_mapping) {
|
|
419
|
+
for (let i = 0; i < insights.length; i++) {
|
|
420
|
+
const insight = insights[i];
|
|
421
|
+
if (!insight.suggested_mapping) {
|
|
422
|
+
errors.push(`insight[${i}] missing required suggested_mapping`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return errors;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Validate enum values against module's enum strategy.
|
|
431
|
+
* For strict mode, custom enum objects are not allowed.
|
|
432
|
+
*
|
|
433
|
+
* @param data The response data object
|
|
434
|
+
* @param module The cognitive module config
|
|
435
|
+
* @returns Array of errors if enum violations found
|
|
436
|
+
*/
|
|
437
|
+
export function validateEnumStrategy(data, module) {
|
|
438
|
+
const errors = [];
|
|
439
|
+
const enumStrategy = module.enums?.strategy ?? 'strict';
|
|
440
|
+
if (enumStrategy === 'strict') {
|
|
441
|
+
// In strict mode, custom enum objects (with 'custom' key) are not allowed
|
|
442
|
+
const checkForCustomEnums = (obj, path) => {
|
|
443
|
+
if (obj === null || obj === undefined)
|
|
444
|
+
return;
|
|
445
|
+
if (Array.isArray(obj)) {
|
|
446
|
+
obj.forEach((item, i) => checkForCustomEnums(item, `${path}[${i}]`));
|
|
447
|
+
}
|
|
448
|
+
else if (typeof obj === 'object') {
|
|
449
|
+
const record = obj;
|
|
450
|
+
// Check if this is a custom enum object
|
|
451
|
+
if ('custom' in record && 'reason' in record && Object.keys(record).length === 2) {
|
|
452
|
+
errors.push(`Custom enum not allowed in strict mode at ${path}: { custom: "${record.custom}" }`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Recurse into nested objects
|
|
456
|
+
for (const [key, value] of Object.entries(record)) {
|
|
457
|
+
checkForCustomEnums(value, `${path}.${key}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
checkForCustomEnums(data, 'data');
|
|
462
|
+
}
|
|
463
|
+
return errors;
|
|
464
|
+
}
|
|
465
|
+
// =============================================================================
|
|
466
|
+
// Constants
|
|
467
|
+
// =============================================================================
|
|
468
|
+
const ENVELOPE_VERSION = '2.2';
|
|
469
|
+
// =============================================================================
|
|
470
|
+
// Utility Functions
|
|
471
|
+
// =============================================================================
|
|
472
|
+
/**
|
|
473
|
+
* Deep clone an object to avoid mutation issues.
|
|
474
|
+
* Handles nested objects, arrays, and primitive values.
|
|
475
|
+
*/
|
|
476
|
+
function deepClone(obj) {
|
|
477
|
+
if (obj === null || typeof obj !== 'object') {
|
|
478
|
+
return obj;
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(obj)) {
|
|
481
|
+
return obj.map(item => deepClone(item));
|
|
482
|
+
}
|
|
483
|
+
const cloned = {};
|
|
484
|
+
for (const key in obj) {
|
|
485
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
486
|
+
cloned[key] = deepClone(obj[key]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return cloned;
|
|
490
|
+
}
|
|
491
|
+
// Global hook registries
|
|
492
|
+
const _beforeCallHooks = [];
|
|
493
|
+
const _afterCallHooks = [];
|
|
494
|
+
const _errorHooks = [];
|
|
495
|
+
/**
|
|
496
|
+
* Decorator to register a before-call hook.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* onBeforeCall((moduleName, inputData, config) => {
|
|
500
|
+
* console.log(`Calling ${moduleName} with`, inputData);
|
|
501
|
+
* });
|
|
502
|
+
*/
|
|
503
|
+
export function onBeforeCall(hook) {
|
|
504
|
+
_beforeCallHooks.push(hook);
|
|
505
|
+
return hook;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Decorator to register an after-call hook.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* onAfterCall((moduleName, result, latencyMs) => {
|
|
512
|
+
* console.log(`${moduleName} completed in ${latencyMs}ms`);
|
|
513
|
+
* });
|
|
514
|
+
*/
|
|
515
|
+
export function onAfterCall(hook) {
|
|
516
|
+
_afterCallHooks.push(hook);
|
|
517
|
+
return hook;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Decorator to register an error hook.
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* onError((moduleName, error, partialResult) => {
|
|
524
|
+
* console.error(`Error in ${moduleName}:`, error);
|
|
525
|
+
* });
|
|
526
|
+
*/
|
|
527
|
+
export function onError(hook) {
|
|
528
|
+
_errorHooks.push(hook);
|
|
529
|
+
return hook;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Register a hook programmatically.
|
|
533
|
+
*/
|
|
534
|
+
export function registerHook(hookType, hook) {
|
|
535
|
+
if (hookType === 'before_call') {
|
|
536
|
+
_beforeCallHooks.push(hook);
|
|
537
|
+
}
|
|
538
|
+
else if (hookType === 'after_call') {
|
|
539
|
+
_afterCallHooks.push(hook);
|
|
540
|
+
}
|
|
541
|
+
else if (hookType === 'error') {
|
|
542
|
+
_errorHooks.push(hook);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
throw new Error(`Unknown hook type: ${hookType}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Unregister a hook. Returns true if found and removed.
|
|
550
|
+
*/
|
|
551
|
+
export function unregisterHook(hookType, hook) {
|
|
552
|
+
let hooks;
|
|
553
|
+
if (hookType === 'before_call') {
|
|
554
|
+
hooks = _beforeCallHooks;
|
|
555
|
+
}
|
|
556
|
+
else if (hookType === 'after_call') {
|
|
557
|
+
hooks = _afterCallHooks;
|
|
558
|
+
}
|
|
559
|
+
else if (hookType === 'error') {
|
|
560
|
+
hooks = _errorHooks;
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const index = hooks.indexOf(hook);
|
|
566
|
+
if (index !== -1) {
|
|
567
|
+
hooks.splice(index, 1);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Clear all registered hooks.
|
|
574
|
+
*/
|
|
575
|
+
export function clearHooks() {
|
|
576
|
+
_beforeCallHooks.length = 0;
|
|
577
|
+
_afterCallHooks.length = 0;
|
|
578
|
+
_errorHooks.length = 0;
|
|
579
|
+
}
|
|
580
|
+
function _invokeBeforeHooks(moduleName, inputData, moduleConfig) {
|
|
581
|
+
for (const hook of _beforeCallHooks) {
|
|
582
|
+
try {
|
|
583
|
+
hook(moduleName, inputData, moduleConfig);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Hooks should not break the main flow
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function _invokeAfterHooks(moduleName, result, latencyMs) {
|
|
591
|
+
for (const hook of _afterCallHooks) {
|
|
592
|
+
try {
|
|
593
|
+
hook(moduleName, result, latencyMs);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Hooks should not break the main flow
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function _invokeErrorHooks(moduleName, error, partialResult) {
|
|
601
|
+
for (const hook of _errorHooks) {
|
|
602
|
+
try {
|
|
603
|
+
hook(moduleName, error, partialResult);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// Hooks should not break the main flow
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// =============================================================================
|
|
611
|
+
// Error Response Builder
|
|
612
|
+
// =============================================================================
|
|
613
|
+
/** Error codes and their default properties */
|
|
614
|
+
export const ERROR_PROPERTIES = {
|
|
615
|
+
MODULE_NOT_FOUND: { recoverable: false, retry_after_ms: null },
|
|
616
|
+
INVALID_INPUT: { recoverable: false, retry_after_ms: null },
|
|
617
|
+
PARSE_ERROR: { recoverable: true, retry_after_ms: 1000 },
|
|
618
|
+
SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
619
|
+
META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
620
|
+
POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
|
|
621
|
+
TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
|
|
622
|
+
LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
|
|
623
|
+
RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
|
|
624
|
+
TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
|
|
625
|
+
UNKNOWN: { recoverable: false, retry_after_ms: null },
|
|
626
|
+
};
|
|
627
|
+
/**
|
|
628
|
+
* Build a standardized error response with enhanced taxonomy.
|
|
629
|
+
*/
|
|
630
|
+
export function makeErrorResponse(options) {
|
|
631
|
+
const { code, message, explain, partialData, details, recoverable, retryAfterMs, confidence = 0.0, risk = 'high', } = options;
|
|
632
|
+
// Get default properties from error code
|
|
633
|
+
const defaults = ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN;
|
|
634
|
+
const errorObj = {
|
|
635
|
+
code,
|
|
636
|
+
message,
|
|
637
|
+
};
|
|
638
|
+
// Add recoverable flag
|
|
639
|
+
const isRecoverable = recoverable ?? defaults.recoverable;
|
|
640
|
+
if (isRecoverable !== undefined) {
|
|
641
|
+
errorObj.recoverable = isRecoverable;
|
|
642
|
+
}
|
|
643
|
+
// Add retry suggestion
|
|
644
|
+
const retryMs = retryAfterMs ?? defaults.retry_after_ms;
|
|
645
|
+
if (retryMs !== null) {
|
|
646
|
+
errorObj.retry_after_ms = retryMs;
|
|
647
|
+
}
|
|
648
|
+
// Add details if provided
|
|
649
|
+
if (details) {
|
|
650
|
+
errorObj.details = details;
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
version: ENVELOPE_VERSION,
|
|
655
|
+
meta: {
|
|
656
|
+
confidence,
|
|
657
|
+
risk,
|
|
658
|
+
explain: (explain || message).slice(0, 280),
|
|
659
|
+
},
|
|
660
|
+
error: errorObj,
|
|
661
|
+
partial_data: partialData,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Build a standardized success response.
|
|
666
|
+
*/
|
|
667
|
+
export function makeSuccessResponse(options) {
|
|
668
|
+
const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
|
|
669
|
+
const meta = {
|
|
670
|
+
confidence: Math.max(0.0, Math.min(1.0, confidence)),
|
|
671
|
+
risk,
|
|
672
|
+
explain: explain ? explain.slice(0, 280) : 'No explanation provided',
|
|
673
|
+
};
|
|
674
|
+
if (latencyMs !== undefined) {
|
|
675
|
+
meta.latency_ms = latencyMs;
|
|
676
|
+
}
|
|
677
|
+
if (model) {
|
|
678
|
+
meta.model = model;
|
|
679
|
+
}
|
|
680
|
+
if (traceId) {
|
|
681
|
+
meta.trace_id = traceId;
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
ok: true,
|
|
685
|
+
version: ENVELOPE_VERSION,
|
|
686
|
+
meta,
|
|
687
|
+
data,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// =============================================================================
|
|
7
691
|
// Repair Pass (v2.2)
|
|
8
692
|
// =============================================================================
|
|
9
693
|
/**
|
|
10
694
|
* Attempt to repair envelope format issues without changing semantics.
|
|
11
695
|
*
|
|
12
|
-
* Repairs (lossless
|
|
696
|
+
* Repairs (mostly lossless, except explain truncation):
|
|
13
697
|
* - Missing meta fields (fill with conservative defaults)
|
|
14
698
|
* - Truncate explain if too long
|
|
15
699
|
* - Trim whitespace from string fields
|
|
700
|
+
* - Clamp confidence to [0, 1] range
|
|
16
701
|
*
|
|
17
702
|
* Does NOT repair:
|
|
18
703
|
* - Invalid enum values (treated as validation failure)
|
|
704
|
+
*
|
|
705
|
+
* Note: Returns a deep copy to avoid modifying the original data.
|
|
19
706
|
*/
|
|
20
707
|
function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
|
|
21
|
-
|
|
708
|
+
// Deep clone to avoid mutation
|
|
709
|
+
const repaired = deepClone(response);
|
|
22
710
|
// Ensure meta exists
|
|
23
711
|
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
24
712
|
repaired.meta = {};
|
|
@@ -54,7 +742,7 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
54
742
|
if (meta.explain.length > maxExplainLength) {
|
|
55
743
|
meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
|
|
56
744
|
}
|
|
57
|
-
// Build proper v2.2 response
|
|
745
|
+
// Build proper v2.2 response with version
|
|
58
746
|
const builtMeta = {
|
|
59
747
|
confidence: meta.confidence,
|
|
60
748
|
risk: meta.risk,
|
|
@@ -62,21 +750,63 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
62
750
|
};
|
|
63
751
|
const result = repaired.ok === false ? {
|
|
64
752
|
ok: false,
|
|
753
|
+
version: ENVELOPE_VERSION,
|
|
65
754
|
meta: builtMeta,
|
|
66
755
|
error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
67
756
|
partial_data: repaired.partial_data
|
|
68
757
|
} : {
|
|
69
758
|
ok: true,
|
|
759
|
+
version: ENVELOPE_VERSION,
|
|
70
760
|
meta: builtMeta,
|
|
71
761
|
data: repaired.data
|
|
72
762
|
};
|
|
73
763
|
return result;
|
|
74
764
|
}
|
|
765
|
+
/**
|
|
766
|
+
* Repair error envelope format.
|
|
767
|
+
*
|
|
768
|
+
* Note: Returns a deep copy to avoid modifying the original data.
|
|
769
|
+
*/
|
|
770
|
+
function repairErrorEnvelope(data, maxExplainLength = 280) {
|
|
771
|
+
// Deep clone to avoid mutation
|
|
772
|
+
const repaired = deepClone(data);
|
|
773
|
+
// Ensure meta exists for errors
|
|
774
|
+
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
775
|
+
repaired.meta = {};
|
|
776
|
+
}
|
|
777
|
+
const meta = repaired.meta;
|
|
778
|
+
// Set default meta for errors
|
|
779
|
+
if (typeof meta.confidence !== 'number') {
|
|
780
|
+
meta.confidence = 0.0;
|
|
781
|
+
}
|
|
782
|
+
if (!meta.risk) {
|
|
783
|
+
meta.risk = 'high';
|
|
784
|
+
}
|
|
785
|
+
if (typeof meta.explain !== 'string') {
|
|
786
|
+
const error = (repaired.error ?? {});
|
|
787
|
+
meta.explain = (error.message ?? 'An error occurred').slice(0, maxExplainLength);
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
version: ENVELOPE_VERSION,
|
|
792
|
+
meta: {
|
|
793
|
+
confidence: meta.confidence,
|
|
794
|
+
risk: meta.risk,
|
|
795
|
+
explain: meta.explain,
|
|
796
|
+
},
|
|
797
|
+
error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
798
|
+
partial_data: repaired.partial_data,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
75
801
|
/**
|
|
76
802
|
* Wrap v2.1 response to v2.2 format
|
|
77
803
|
*/
|
|
78
804
|
function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
79
805
|
if (isV22Envelope(response)) {
|
|
806
|
+
// Already v2.2, but ensure version field exists
|
|
807
|
+
if (!('version' in response) || !response.version) {
|
|
808
|
+
return { ...deepClone(response), version: ENVELOPE_VERSION };
|
|
809
|
+
}
|
|
80
810
|
return response;
|
|
81
811
|
}
|
|
82
812
|
if (response.ok) {
|
|
@@ -85,6 +815,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
85
815
|
const rationale = data.rationale ?? '';
|
|
86
816
|
return {
|
|
87
817
|
ok: true,
|
|
818
|
+
version: ENVELOPE_VERSION,
|
|
88
819
|
meta: {
|
|
89
820
|
confidence,
|
|
90
821
|
risk: aggregateRisk(data, riskRule),
|
|
@@ -97,6 +828,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
97
828
|
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
98
829
|
return {
|
|
99
830
|
ok: false,
|
|
831
|
+
version: ENVELOPE_VERSION,
|
|
100
832
|
meta: {
|
|
101
833
|
confidence: 0,
|
|
102
834
|
risk: 'high',
|
|
@@ -107,8 +839,51 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
107
839
|
};
|
|
108
840
|
}
|
|
109
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Convert legacy format (no envelope) to v2.2 envelope.
|
|
844
|
+
*/
|
|
845
|
+
function convertLegacyToEnvelope(data, isError = false) {
|
|
846
|
+
if (isError || 'error' in data) {
|
|
847
|
+
const error = (data.error ?? {});
|
|
848
|
+
const errorMsg = typeof error === 'object'
|
|
849
|
+
? (error.message ?? String(error))
|
|
850
|
+
: String(error);
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
version: ENVELOPE_VERSION,
|
|
854
|
+
meta: {
|
|
855
|
+
confidence: 0.0,
|
|
856
|
+
risk: 'high',
|
|
857
|
+
explain: errorMsg.slice(0, 280),
|
|
858
|
+
},
|
|
859
|
+
error: {
|
|
860
|
+
code: (typeof error === 'object' ? error.code : undefined) ?? 'UNKNOWN',
|
|
861
|
+
message: errorMsg,
|
|
862
|
+
},
|
|
863
|
+
partial_data: undefined,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
const confidence = data.confidence ?? 0.5;
|
|
868
|
+
const rationale = data.rationale ?? '';
|
|
869
|
+
return {
|
|
870
|
+
ok: true,
|
|
871
|
+
version: ENVELOPE_VERSION,
|
|
872
|
+
meta: {
|
|
873
|
+
confidence,
|
|
874
|
+
risk: aggregateRisk(data),
|
|
875
|
+
explain: rationale.slice(0, 280) || 'No explanation provided',
|
|
876
|
+
},
|
|
877
|
+
data,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
// =============================================================================
|
|
882
|
+
// Main Runner
|
|
883
|
+
// =============================================================================
|
|
110
884
|
export async function runModule(module, provider, options = {}) {
|
|
111
|
-
const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
|
|
885
|
+
const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
|
|
886
|
+
const startTime = Date.now();
|
|
112
887
|
// Determine if we should use envelope format
|
|
113
888
|
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
114
889
|
// Determine if we should use v2.2 format
|
|
@@ -128,6 +903,24 @@ export async function runModule(module, provider, options = {}) {
|
|
|
128
903
|
inputData.query = args;
|
|
129
904
|
}
|
|
130
905
|
}
|
|
906
|
+
// Invoke before hooks
|
|
907
|
+
_invokeBeforeHooks(module.name, inputData, module);
|
|
908
|
+
// Validate input against schema
|
|
909
|
+
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
910
|
+
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
911
|
+
if (inputErrors.length > 0) {
|
|
912
|
+
const errorResult = makeErrorResponse({
|
|
913
|
+
code: 'INVALID_INPUT',
|
|
914
|
+
message: inputErrors.join('; '),
|
|
915
|
+
explain: 'Input validation failed.',
|
|
916
|
+
confidence: 1.0,
|
|
917
|
+
risk: 'none',
|
|
918
|
+
details: { validation_errors: inputErrors },
|
|
919
|
+
});
|
|
920
|
+
_invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
|
|
921
|
+
return errorResult;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
131
924
|
// Build prompt with clean substitution
|
|
132
925
|
const prompt = buildPrompt(module, inputData);
|
|
133
926
|
if (verbose) {
|
|
@@ -202,70 +995,364 @@ export async function runModule(module, provider, options = {}) {
|
|
|
202
995
|
{ role: 'system', content: systemParts.join('\n') },
|
|
203
996
|
{ role: 'user', content: prompt },
|
|
204
997
|
];
|
|
205
|
-
// Invoke provider
|
|
206
|
-
const result = await provider.invoke({
|
|
207
|
-
messages,
|
|
208
|
-
jsonSchema: module.outputSchema,
|
|
209
|
-
temperature: 0.3,
|
|
210
|
-
});
|
|
211
|
-
if (verbose) {
|
|
212
|
-
console.error('--- Response ---');
|
|
213
|
-
console.error(result.content);
|
|
214
|
-
console.error('--- End Response ---');
|
|
215
|
-
}
|
|
216
|
-
// Parse response
|
|
217
|
-
let parsed;
|
|
218
998
|
try {
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Upgrade to v2.2 if needed
|
|
230
|
-
if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
|
|
231
|
-
const upgraded = wrapV21ToV22(parsed, riskRule);
|
|
232
|
-
response = {
|
|
233
|
-
ok: true,
|
|
234
|
-
meta: upgraded.meta,
|
|
235
|
-
data: upgraded.data,
|
|
236
|
-
raw: result.content
|
|
237
|
-
};
|
|
999
|
+
// Invoke provider
|
|
1000
|
+
const result = await provider.invoke({
|
|
1001
|
+
messages,
|
|
1002
|
+
jsonSchema: module.outputSchema,
|
|
1003
|
+
temperature: 0.3,
|
|
1004
|
+
});
|
|
1005
|
+
if (verbose) {
|
|
1006
|
+
console.error('--- Response ---');
|
|
1007
|
+
console.error(result.content);
|
|
1008
|
+
console.error('--- End Response ---');
|
|
238
1009
|
}
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
1010
|
+
// Calculate latency
|
|
1011
|
+
const latencyMs = Date.now() - startTime;
|
|
1012
|
+
// Parse response
|
|
1013
|
+
let parsed;
|
|
1014
|
+
try {
|
|
1015
|
+
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1016
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
1017
|
+
parsed = JSON.parse(jsonStr.trim());
|
|
1018
|
+
}
|
|
1019
|
+
catch (e) {
|
|
1020
|
+
const errorResult = makeErrorResponse({
|
|
1021
|
+
code: 'PARSE_ERROR',
|
|
1022
|
+
message: `Failed to parse JSON response: ${e.message}`,
|
|
1023
|
+
explain: 'Failed to parse LLM response as JSON.',
|
|
1024
|
+
details: { raw_response: result.content.substring(0, 500) },
|
|
1025
|
+
});
|
|
1026
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1027
|
+
return errorResult;
|
|
1028
|
+
}
|
|
1029
|
+
// Convert to v2.2 envelope
|
|
1030
|
+
let response;
|
|
1031
|
+
if (isV22Envelope(parsed)) {
|
|
1032
|
+
response = parsed;
|
|
1033
|
+
}
|
|
1034
|
+
else if (isEnvelopeResponse(parsed)) {
|
|
1035
|
+
response = wrapV21ToV22(parsed, riskRule);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
response = convertLegacyToEnvelope(parsed);
|
|
248
1039
|
}
|
|
1040
|
+
// Add version and meta fields
|
|
1041
|
+
response.version = ENVELOPE_VERSION;
|
|
1042
|
+
if (response.meta) {
|
|
1043
|
+
response.meta.latency_ms = latencyMs;
|
|
1044
|
+
if (traceId) {
|
|
1045
|
+
response.meta.trace_id = traceId;
|
|
1046
|
+
}
|
|
1047
|
+
if (modelOverride) {
|
|
1048
|
+
response.meta.model = modelOverride;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
// Validate and potentially repair output
|
|
1052
|
+
if (response.ok && validateOutput) {
|
|
1053
|
+
// Get data schema (support both "data" and "output" aliases)
|
|
1054
|
+
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1055
|
+
const metaSchema = module.metaSchema;
|
|
1056
|
+
const dataToValidate = response.data ?? {};
|
|
1057
|
+
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1058
|
+
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1059
|
+
if (dataErrors.length > 0 && enableRepair) {
|
|
1060
|
+
// Attempt repair pass
|
|
1061
|
+
response = repairEnvelope(response, riskRule);
|
|
1062
|
+
response.version = ENVELOPE_VERSION;
|
|
1063
|
+
// Re-validate after repair
|
|
1064
|
+
const repairedData = response.data ?? {};
|
|
1065
|
+
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1066
|
+
}
|
|
1067
|
+
if (dataErrors.length > 0) {
|
|
1068
|
+
const errorResult = makeErrorResponse({
|
|
1069
|
+
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1070
|
+
message: dataErrors.join('; '),
|
|
1071
|
+
explain: 'Schema validation failed after repair attempt.',
|
|
1072
|
+
partialData: response.data,
|
|
1073
|
+
details: { validation_errors: dataErrors },
|
|
1074
|
+
});
|
|
1075
|
+
_invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
|
|
1076
|
+
return errorResult;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// v2.2: Validate overflow limits
|
|
1080
|
+
const overflowErrors = validateOverflowLimits(dataToValidate, module);
|
|
1081
|
+
if (overflowErrors.length > 0) {
|
|
1082
|
+
const errorResult = makeErrorResponse({
|
|
1083
|
+
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1084
|
+
message: overflowErrors.join('; '),
|
|
1085
|
+
explain: 'Overflow validation failed.',
|
|
1086
|
+
partialData: dataToValidate,
|
|
1087
|
+
details: { overflow_errors: overflowErrors },
|
|
1088
|
+
});
|
|
1089
|
+
_invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
|
|
1090
|
+
return errorResult;
|
|
1091
|
+
}
|
|
1092
|
+
// v2.2: Validate enum strategy
|
|
1093
|
+
const enumErrors = validateEnumStrategy(dataToValidate, module);
|
|
1094
|
+
if (enumErrors.length > 0) {
|
|
1095
|
+
const errorResult = makeErrorResponse({
|
|
1096
|
+
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1097
|
+
message: enumErrors.join('; '),
|
|
1098
|
+
explain: 'Enum strategy validation failed.',
|
|
1099
|
+
partialData: dataToValidate,
|
|
1100
|
+
details: { enum_errors: enumErrors },
|
|
1101
|
+
});
|
|
1102
|
+
_invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
|
|
1103
|
+
return errorResult;
|
|
1104
|
+
}
|
|
1105
|
+
// Validate meta if schema exists
|
|
1106
|
+
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1107
|
+
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1108
|
+
if (metaErrors.length > 0 && enableRepair) {
|
|
1109
|
+
response = repairEnvelope(response, riskRule);
|
|
1110
|
+
response.version = ENVELOPE_VERSION;
|
|
1111
|
+
// Re-validate meta after repair
|
|
1112
|
+
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1113
|
+
if (metaErrors.length > 0) {
|
|
1114
|
+
const errorResult = makeErrorResponse({
|
|
1115
|
+
code: 'META_VALIDATION_FAILED',
|
|
1116
|
+
message: metaErrors.join('; '),
|
|
1117
|
+
explain: 'Meta schema validation failed after repair attempt.',
|
|
1118
|
+
partialData: response.data,
|
|
1119
|
+
details: { validation_errors: metaErrors },
|
|
1120
|
+
});
|
|
1121
|
+
_invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
|
|
1122
|
+
return errorResult;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else if (enableRepair) {
|
|
1128
|
+
// Repair error envelopes to ensure they have proper meta fields
|
|
1129
|
+
response = repairErrorEnvelope(response);
|
|
1130
|
+
response.version = ENVELOPE_VERSION;
|
|
1131
|
+
}
|
|
1132
|
+
// Invoke after hooks
|
|
1133
|
+
const finalLatencyMs = Date.now() - startTime;
|
|
1134
|
+
_invokeAfterHooks(module.name, response, finalLatencyMs);
|
|
249
1135
|
return response;
|
|
250
1136
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
const latencyMs = Date.now() - startTime;
|
|
1139
|
+
const errorResult = makeErrorResponse({
|
|
1140
|
+
code: 'UNKNOWN',
|
|
1141
|
+
message: e.message,
|
|
1142
|
+
explain: `Unexpected error: ${e.name}`,
|
|
1143
|
+
details: { exception_type: e.name },
|
|
1144
|
+
});
|
|
1145
|
+
if (errorResult.meta) {
|
|
1146
|
+
errorResult.meta.latency_ms = latencyMs;
|
|
1147
|
+
}
|
|
1148
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1149
|
+
return errorResult;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Run a cognitive module with streaming output.
|
|
1154
|
+
*
|
|
1155
|
+
* Yields StreamEvent objects as the module executes:
|
|
1156
|
+
* - type="start": Module execution started
|
|
1157
|
+
* - type="chunk": Incremental data chunk (if LLM supports streaming)
|
|
1158
|
+
* - type="meta": Meta information available early
|
|
1159
|
+
* - type="complete": Final complete result
|
|
1160
|
+
* - type="error": Error occurred
|
|
1161
|
+
*
|
|
1162
|
+
* @example
|
|
1163
|
+
* for await (const event of runModuleStream(module, provider, options)) {
|
|
1164
|
+
* if (event.type === 'chunk') {
|
|
1165
|
+
* process.stdout.write(event.chunk);
|
|
1166
|
+
* } else if (event.type === 'complete') {
|
|
1167
|
+
* console.log('Result:', event.result);
|
|
1168
|
+
* }
|
|
1169
|
+
* }
|
|
1170
|
+
*/
|
|
1171
|
+
export async function* runModuleStream(module, provider, options = {}) {
|
|
1172
|
+
const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
|
|
1173
|
+
const startTime = Date.now();
|
|
1174
|
+
const moduleName = module.name;
|
|
1175
|
+
function makeEvent(type, extra = {}) {
|
|
256
1176
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
|
|
262
|
-
},
|
|
263
|
-
data: legacyResult.data,
|
|
264
|
-
raw: result.content
|
|
1177
|
+
type,
|
|
1178
|
+
timestamp_ms: Date.now() - startTime,
|
|
1179
|
+
module_name: moduleName,
|
|
1180
|
+
...extra,
|
|
265
1181
|
};
|
|
266
1182
|
}
|
|
267
|
-
|
|
1183
|
+
try {
|
|
1184
|
+
// Emit start event
|
|
1185
|
+
yield makeEvent('start');
|
|
1186
|
+
// Build input data
|
|
1187
|
+
const inputData = input || {};
|
|
1188
|
+
if (args && !inputData.code && !inputData.query) {
|
|
1189
|
+
if (looksLikeCode(args)) {
|
|
1190
|
+
inputData.code = args;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
inputData.query = args;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
// Validate input if enabled
|
|
1197
|
+
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
1198
|
+
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
1199
|
+
if (inputErrors.length > 0) {
|
|
1200
|
+
const errorResult = makeErrorResponse({
|
|
1201
|
+
code: 'INVALID_INPUT',
|
|
1202
|
+
message: inputErrors.join('; '),
|
|
1203
|
+
confidence: 1.0,
|
|
1204
|
+
risk: 'none',
|
|
1205
|
+
});
|
|
1206
|
+
const errorObj = errorResult.error;
|
|
1207
|
+
yield makeEvent('error', { error: errorObj });
|
|
1208
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
// Get risk_rule from module config
|
|
1213
|
+
const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
1214
|
+
// Build prompt
|
|
1215
|
+
const prompt = buildPrompt(module, inputData);
|
|
1216
|
+
// Build messages
|
|
1217
|
+
const systemParts = [
|
|
1218
|
+
`You are executing the "${module.name}" Cognitive Module.`,
|
|
1219
|
+
'',
|
|
1220
|
+
`RESPONSIBILITY: ${module.responsibility}`,
|
|
1221
|
+
];
|
|
1222
|
+
if (useV22) {
|
|
1223
|
+
systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
|
|
1224
|
+
systemParts.push('- Wrap your response in the v2.2 envelope format');
|
|
1225
|
+
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
1226
|
+
systemParts.push('- Return ONLY valid JSON.');
|
|
1227
|
+
}
|
|
1228
|
+
const messages = [
|
|
1229
|
+
{ role: 'system', content: systemParts.join('\n') },
|
|
1230
|
+
{ role: 'user', content: prompt },
|
|
1231
|
+
];
|
|
1232
|
+
// Invoke provider (streaming not yet supported in provider interface, so we fallback)
|
|
1233
|
+
const result = await provider.invoke({
|
|
1234
|
+
messages,
|
|
1235
|
+
jsonSchema: module.outputSchema,
|
|
1236
|
+
temperature: 0.3,
|
|
1237
|
+
});
|
|
1238
|
+
// Emit chunk event with full response
|
|
1239
|
+
yield makeEvent('chunk', { chunk: result.content });
|
|
1240
|
+
// Parse response
|
|
1241
|
+
let parsed;
|
|
1242
|
+
try {
|
|
1243
|
+
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1244
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
1245
|
+
parsed = JSON.parse(jsonStr.trim());
|
|
1246
|
+
}
|
|
1247
|
+
catch (e) {
|
|
1248
|
+
const errorResult = makeErrorResponse({
|
|
1249
|
+
code: 'PARSE_ERROR',
|
|
1250
|
+
message: `Failed to parse JSON: ${e.message}`,
|
|
1251
|
+
});
|
|
1252
|
+
// errorResult is always an error response from makeErrorResponse
|
|
1253
|
+
const errorObj = errorResult.error;
|
|
1254
|
+
yield makeEvent('error', { error: errorObj });
|
|
1255
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
// Convert to v2.2 envelope
|
|
1259
|
+
let response;
|
|
1260
|
+
if (isV22Envelope(parsed)) {
|
|
1261
|
+
response = parsed;
|
|
1262
|
+
}
|
|
1263
|
+
else if (isEnvelopeResponse(parsed)) {
|
|
1264
|
+
response = wrapV21ToV22(parsed, riskRule);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
response = convertLegacyToEnvelope(parsed);
|
|
1268
|
+
}
|
|
1269
|
+
// Add version and meta
|
|
1270
|
+
response.version = ENVELOPE_VERSION;
|
|
1271
|
+
const latencyMs = Date.now() - startTime;
|
|
1272
|
+
if (response.meta) {
|
|
1273
|
+
response.meta.latency_ms = latencyMs;
|
|
1274
|
+
if (traceId) {
|
|
1275
|
+
response.meta.trace_id = traceId;
|
|
1276
|
+
}
|
|
1277
|
+
if (model) {
|
|
1278
|
+
response.meta.model = model;
|
|
1279
|
+
}
|
|
1280
|
+
// Emit meta event early
|
|
1281
|
+
yield makeEvent('meta', { meta: response.meta });
|
|
1282
|
+
}
|
|
1283
|
+
// Validate and repair output
|
|
1284
|
+
if (response.ok && validateOutput) {
|
|
1285
|
+
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1286
|
+
const metaSchema = module.metaSchema;
|
|
1287
|
+
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1288
|
+
const dataToValidate = response.data ?? {};
|
|
1289
|
+
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1290
|
+
if (dataErrors.length > 0 && enableRepair) {
|
|
1291
|
+
response = repairEnvelope(response, riskRule);
|
|
1292
|
+
response.version = ENVELOPE_VERSION;
|
|
1293
|
+
// Re-validate after repair
|
|
1294
|
+
const repairedData = response.data ?? {};
|
|
1295
|
+
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1296
|
+
}
|
|
1297
|
+
if (dataErrors.length > 0) {
|
|
1298
|
+
const errorResult = makeErrorResponse({
|
|
1299
|
+
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1300
|
+
message: dataErrors.join('; '),
|
|
1301
|
+
explain: 'Schema validation failed after repair attempt.',
|
|
1302
|
+
partialData: response.data,
|
|
1303
|
+
details: { validation_errors: dataErrors },
|
|
1304
|
+
});
|
|
1305
|
+
const errorObj = errorResult.error;
|
|
1306
|
+
yield makeEvent('error', { error: errorObj });
|
|
1307
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// Validate meta if schema exists
|
|
1312
|
+
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1313
|
+
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1314
|
+
if (metaErrors.length > 0 && enableRepair) {
|
|
1315
|
+
response = repairEnvelope(response, riskRule);
|
|
1316
|
+
response.version = ENVELOPE_VERSION;
|
|
1317
|
+
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1318
|
+
if (metaErrors.length > 0) {
|
|
1319
|
+
const errorResult = makeErrorResponse({
|
|
1320
|
+
code: 'META_VALIDATION_FAILED',
|
|
1321
|
+
message: metaErrors.join('; '),
|
|
1322
|
+
explain: 'Meta validation failed after repair attempt.',
|
|
1323
|
+
partialData: response.data,
|
|
1324
|
+
details: { validation_errors: metaErrors },
|
|
1325
|
+
});
|
|
1326
|
+
const errorObj = errorResult.error;
|
|
1327
|
+
yield makeEvent('error', { error: errorObj });
|
|
1328
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
else if (!response.ok && enableRepair) {
|
|
1335
|
+
response = repairErrorEnvelope(response);
|
|
1336
|
+
response.version = ENVELOPE_VERSION;
|
|
1337
|
+
}
|
|
1338
|
+
// Emit complete event
|
|
1339
|
+
yield makeEvent('complete', { result: response });
|
|
1340
|
+
}
|
|
1341
|
+
catch (e) {
|
|
1342
|
+
const errorResult = makeErrorResponse({
|
|
1343
|
+
code: 'UNKNOWN',
|
|
1344
|
+
message: e.message,
|
|
1345
|
+
explain: `Unexpected error: ${e.name}`,
|
|
1346
|
+
});
|
|
1347
|
+
// errorResult is always an error response from makeErrorResponse
|
|
1348
|
+
const errorObj = errorResult.error;
|
|
1349
|
+
yield makeEvent('error', { error: errorObj });
|
|
1350
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1351
|
+
}
|
|
268
1352
|
}
|
|
1353
|
+
// =============================================================================
|
|
1354
|
+
// Helper Functions
|
|
1355
|
+
// =============================================================================
|
|
269
1356
|
/**
|
|
270
1357
|
* Check if response is in envelope format
|
|
271
1358
|
*/
|
|
@@ -360,6 +1447,12 @@ function parseLegacyResponse(output, raw) {
|
|
|
360
1447
|
}
|
|
361
1448
|
/**
|
|
362
1449
|
* Build prompt with clean variable substitution
|
|
1450
|
+
*
|
|
1451
|
+
* Substitution order (important to avoid partial replacements):
|
|
1452
|
+
* 1. ${variable} - v2 style placeholders
|
|
1453
|
+
* 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
|
|
1454
|
+
* 3. $N - shorthand indexed access (descending order)
|
|
1455
|
+
* 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
|
|
363
1456
|
*/
|
|
364
1457
|
function buildPrompt(module, input) {
|
|
365
1458
|
let prompt = module.prompt;
|
|
@@ -368,16 +1461,22 @@ function buildPrompt(module, input) {
|
|
|
368
1461
|
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
369
1462
|
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
370
1463
|
}
|
|
371
|
-
// v1 compatibility:
|
|
1464
|
+
// v1 compatibility: get args value
|
|
372
1465
|
const argsValue = input.code || input.query || '';
|
|
373
|
-
|
|
374
|
-
//
|
|
1466
|
+
// Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
|
|
1467
|
+
// Process in descending order to avoid $1 replacing part of $10
|
|
375
1468
|
if (typeof argsValue === 'string') {
|
|
376
1469
|
const argsList = argsValue.split(/\s+/);
|
|
377
|
-
argsList.
|
|
1470
|
+
for (let i = argsList.length - 1; i >= 0; i--) {
|
|
1471
|
+
const arg = argsList[i];
|
|
1472
|
+
// Replace $ARGUMENTS[N] first
|
|
1473
|
+
prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
|
|
1474
|
+
// Replace $N shorthand
|
|
378
1475
|
prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
|
|
379
|
-
}
|
|
1476
|
+
}
|
|
380
1477
|
}
|
|
1478
|
+
// Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
|
|
1479
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
381
1480
|
// Append input summary if not already in prompt
|
|
382
1481
|
if (!prompt.includes(argsValue) && argsValue) {
|
|
383
1482
|
prompt += '\n\n## Input\n\n';
|
|
@@ -405,3 +1504,61 @@ function looksLikeCode(str) {
|
|
|
405
1504
|
];
|
|
406
1505
|
return codeIndicators.some(re => re.test(str));
|
|
407
1506
|
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Run a cognitive module (legacy API, returns raw output).
|
|
1509
|
+
* For backward compatibility. Throws on error instead of returning error envelope.
|
|
1510
|
+
*/
|
|
1511
|
+
export async function runModuleLegacy(module, provider, input, options = {}) {
|
|
1512
|
+
const { validateInput = true, validateOutput = true, model } = options;
|
|
1513
|
+
const result = await runModule(module, provider, {
|
|
1514
|
+
input,
|
|
1515
|
+
validateInput,
|
|
1516
|
+
validateOutput,
|
|
1517
|
+
useEnvelope: false,
|
|
1518
|
+
useV22: false,
|
|
1519
|
+
model,
|
|
1520
|
+
});
|
|
1521
|
+
if (result.ok && 'data' in result) {
|
|
1522
|
+
return result.data;
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
const error = 'error' in result ? result.error : { code: 'UNKNOWN', message: 'Unknown error' };
|
|
1526
|
+
throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
// =============================================================================
|
|
1530
|
+
// Convenience Functions
|
|
1531
|
+
// =============================================================================
|
|
1532
|
+
/**
|
|
1533
|
+
* Extract meta from v2.2 envelope for routing/logging.
|
|
1534
|
+
*/
|
|
1535
|
+
export function extractMeta(result) {
|
|
1536
|
+
return result.meta ?? {
|
|
1537
|
+
confidence: 0.5,
|
|
1538
|
+
risk: 'medium',
|
|
1539
|
+
explain: 'No meta available',
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
// Alias for backward compatibility
|
|
1543
|
+
export const extractMetaV22 = extractMeta;
|
|
1544
|
+
/**
|
|
1545
|
+
* Determine if result should be escalated to human review based on meta.
|
|
1546
|
+
*/
|
|
1547
|
+
export function shouldEscalate(result, confidenceThreshold = 0.7) {
|
|
1548
|
+
const meta = extractMeta(result);
|
|
1549
|
+
// Escalate if low confidence
|
|
1550
|
+
if (meta.confidence < confidenceThreshold) {
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
// Escalate if high risk
|
|
1554
|
+
if (meta.risk === 'high') {
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
// Escalate if error
|
|
1558
|
+
if (!result.ok) {
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
// Alias for backward compatibility
|
|
1564
|
+
export const shouldEscalateV22 = shouldEscalate;
|