cognitive-modules-cli 2.2.0 → 2.2.5
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -29
- package/dist/cli.js +572 -28
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +222 -13
- package/dist/commands/compose.d.ts +31 -0
- package/dist/commands/compose.js +185 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +5 -0
- package/dist/commands/init.js +23 -1
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.js +650 -0
- package/dist/commands/pipe.d.ts +1 -0
- package/dist/commands/pipe.js +31 -11
- package/dist/commands/remove.js +33 -2
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +37 -27
- package/dist/commands/search.d.ts +28 -0
- package/dist/commands/search.js +143 -0
- package/dist/commands/test.d.ts +65 -0
- package/dist/commands/test.js +454 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +106 -14
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.js +97 -0
- package/dist/errors/index.d.ts +218 -0
- package/dist/errors/index.js +412 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.d.ts +251 -0
- package/dist/modules/composition.js +1330 -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 +171 -6
- package/dist/modules/runner.d.ts +422 -1
- package/dist/modules/runner.js +1472 -71
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +20 -13
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +637 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +147 -5
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.js +18 -0
- package/dist/providers/gemini.d.ts +15 -0
- package/dist/providers/gemini.js +122 -5
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +111 -3
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.js +133 -0
- package/dist/registry/client.d.ts +204 -0
- package/dist/registry/client.js +356 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +4 -0
- package/dist/server/http.js +173 -42
- package/dist/types.d.ts +123 -8
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +32 -7
- package/src/cli.ts +0 -410
- package/src/commands/add.ts +0 -315
- package/src/commands/index.ts +0 -12
- package/src/commands/init.ts +0 -94
- package/src/commands/list.ts +0 -33
- package/src/commands/pipe.ts +0 -76
- package/src/commands/remove.ts +0 -57
- package/src/commands/run.ts +0 -80
- package/src/commands/update.ts +0 -130
- package/src/commands/versions.ts +0 -79
- package/src/index.ts +0 -55
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/index.ts +0 -7
- package/src/modules/loader.ts +0 -318
- package/src/modules/runner.ts +0 -495
- package/src/modules/subagent.ts +0 -275
- package/src/providers/anthropic.ts +0 -89
- package/src/providers/base.ts +0 -29
- package/src/providers/deepseek.ts +0 -83
- package/src/providers/gemini.ts +0 -117
- package/src/providers/index.ts +0 -78
- package/src/providers/minimax.ts +0 -81
- package/src/providers/moonshot.ts +0 -82
- package/src/providers/ollama.ts +0 -83
- package/src/providers/openai.ts +0 -84
- package/src/providers/qwen.ts +0 -82
- package/src/server/http.ts +0 -316
- package/src/server/index.ts +0 -6
- package/src/types.ts +0 -495
- package/tsconfig.json +0 -17
package/dist/modules/runner.js
CHANGED
|
@@ -1,24 +1,877 @@
|
|
|
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 (any presence of 'custom' is disallowed in strict mode)
|
|
451
|
+
if ('custom' in record) {
|
|
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
|
+
/**
|
|
614
|
+
* Error code taxonomy following CONFORMANCE.md E1xxx-E4xxx structure.
|
|
615
|
+
*
|
|
616
|
+
* E1xxx: Input errors (caller errors, fixable by modifying input)
|
|
617
|
+
* E2xxx: Processing errors (module understood input but couldn't complete)
|
|
618
|
+
* E3xxx: Output errors (generated output doesn't meet requirements)
|
|
619
|
+
* E4xxx: Runtime errors (infrastructure/system-level failures)
|
|
620
|
+
*/
|
|
621
|
+
/** Standard error codes with E-format (as per ERROR-CODES.md) */
|
|
622
|
+
export const ERROR_CODES = {
|
|
623
|
+
// E1xxx: Input errors
|
|
624
|
+
E1000: 'PARSE_ERROR',
|
|
625
|
+
E1001: 'INVALID_INPUT',
|
|
626
|
+
E1002: 'MISSING_REQUIRED_FIELD',
|
|
627
|
+
E1003: 'TYPE_MISMATCH',
|
|
628
|
+
E1004: 'UNSUPPORTED_VALUE',
|
|
629
|
+
E1005: 'INPUT_TOO_LARGE',
|
|
630
|
+
E1006: 'INVALID_REFERENCE',
|
|
631
|
+
// E2xxx: Processing errors
|
|
632
|
+
E2001: 'LOW_CONFIDENCE',
|
|
633
|
+
E2002: 'TIMEOUT',
|
|
634
|
+
E2003: 'TOKEN_LIMIT',
|
|
635
|
+
E2004: 'NO_ACTION_POSSIBLE',
|
|
636
|
+
E2005: 'SEMANTIC_CONFLICT',
|
|
637
|
+
E2006: 'AMBIGUOUS_INPUT',
|
|
638
|
+
E2007: 'INSUFFICIENT_CONTEXT',
|
|
639
|
+
// E3xxx: Output errors
|
|
640
|
+
E3001: 'OUTPUT_SCHEMA_VIOLATION',
|
|
641
|
+
E3002: 'PARTIAL_RESULT',
|
|
642
|
+
E3003: 'MISSING_RATIONALE',
|
|
643
|
+
E3004: 'OVERFLOW_LIMIT',
|
|
644
|
+
E3005: 'INVALID_ENUM',
|
|
645
|
+
E3006: 'CONSTRAINT_VIOLATION',
|
|
646
|
+
// E4xxx: Runtime errors
|
|
647
|
+
E4000: 'INTERNAL_ERROR',
|
|
648
|
+
E4001: 'PROVIDER_UNAVAILABLE',
|
|
649
|
+
E4002: 'RATE_LIMITED',
|
|
650
|
+
E4003: 'CONTEXT_OVERFLOW',
|
|
651
|
+
E4004: 'CIRCULAR_DEPENDENCY',
|
|
652
|
+
E4005: 'MAX_DEPTH_EXCEEDED',
|
|
653
|
+
E4006: 'MODULE_NOT_FOUND',
|
|
654
|
+
E4007: 'PERMISSION_DENIED',
|
|
655
|
+
};
|
|
656
|
+
/** Reverse mapping: legacy code -> E-format code */
|
|
657
|
+
export const LEGACY_TO_E_CODE = {
|
|
658
|
+
PARSE_ERROR: 'E1000',
|
|
659
|
+
INVALID_INPUT: 'E1001',
|
|
660
|
+
MISSING_REQUIRED_FIELD: 'E1002',
|
|
661
|
+
TYPE_MISMATCH: 'E1003',
|
|
662
|
+
UNSUPPORTED_VALUE: 'E1004',
|
|
663
|
+
INPUT_TOO_LARGE: 'E1005',
|
|
664
|
+
INVALID_REFERENCE: 'E1006',
|
|
665
|
+
LOW_CONFIDENCE: 'E2001',
|
|
666
|
+
TIMEOUT: 'E2002',
|
|
667
|
+
TOKEN_LIMIT: 'E2003',
|
|
668
|
+
NO_ACTION_POSSIBLE: 'E2004',
|
|
669
|
+
SEMANTIC_CONFLICT: 'E2005',
|
|
670
|
+
AMBIGUOUS_INPUT: 'E2006',
|
|
671
|
+
INSUFFICIENT_CONTEXT: 'E2007',
|
|
672
|
+
OUTPUT_SCHEMA_VIOLATION: 'E3001',
|
|
673
|
+
SCHEMA_VALIDATION_FAILED: 'E3001', // Alias
|
|
674
|
+
PARTIAL_RESULT: 'E3002',
|
|
675
|
+
MISSING_RATIONALE: 'E3003',
|
|
676
|
+
OVERFLOW_LIMIT: 'E3004',
|
|
677
|
+
INVALID_ENUM: 'E3005',
|
|
678
|
+
CONSTRAINT_VIOLATION: 'E3006',
|
|
679
|
+
META_VALIDATION_FAILED: 'E3001', // Alias (output validation)
|
|
680
|
+
INTERNAL_ERROR: 'E4000',
|
|
681
|
+
PROVIDER_UNAVAILABLE: 'E4001',
|
|
682
|
+
LLM_ERROR: 'E4001', // Alias
|
|
683
|
+
RATE_LIMITED: 'E4002',
|
|
684
|
+
CONTEXT_OVERFLOW: 'E4003',
|
|
685
|
+
CIRCULAR_DEPENDENCY: 'E4004',
|
|
686
|
+
MAX_DEPTH_EXCEEDED: 'E4005',
|
|
687
|
+
MODULE_NOT_FOUND: 'E4006',
|
|
688
|
+
PERMISSION_DENIED: 'E4007',
|
|
689
|
+
POLICY_VIOLATION: 'E4007', // Alias
|
|
690
|
+
TOOL_NOT_ALLOWED: 'E4007', // Alias
|
|
691
|
+
UNKNOWN: 'E4000', // Fallback to internal error
|
|
692
|
+
};
|
|
693
|
+
/** Error codes and their default properties */
|
|
694
|
+
export const ERROR_PROPERTIES = {
|
|
695
|
+
// E1xxx: Input errors (mostly recoverable by fixing input)
|
|
696
|
+
E1000: { recoverable: false, retry_after_ms: null }, // PARSE_ERROR
|
|
697
|
+
E1001: { recoverable: true, retry_after_ms: null }, // INVALID_INPUT
|
|
698
|
+
E1002: { recoverable: true, retry_after_ms: null }, // MISSING_REQUIRED_FIELD
|
|
699
|
+
E1003: { recoverable: true, retry_after_ms: null }, // TYPE_MISMATCH
|
|
700
|
+
E1004: { recoverable: false, retry_after_ms: null }, // UNSUPPORTED_VALUE
|
|
701
|
+
E1005: { recoverable: true, retry_after_ms: null }, // INPUT_TOO_LARGE
|
|
702
|
+
E1006: { recoverable: true, retry_after_ms: null }, // INVALID_REFERENCE
|
|
703
|
+
// E2xxx: Processing errors (may have partial results)
|
|
704
|
+
E2001: { recoverable: true, retry_after_ms: null }, // LOW_CONFIDENCE
|
|
705
|
+
E2002: { recoverable: true, retry_after_ms: 5000 }, // TIMEOUT
|
|
706
|
+
E2003: { recoverable: true, retry_after_ms: null }, // TOKEN_LIMIT
|
|
707
|
+
E2004: { recoverable: false, retry_after_ms: null }, // NO_ACTION_POSSIBLE
|
|
708
|
+
E2005: { recoverable: false, retry_after_ms: null }, // SEMANTIC_CONFLICT
|
|
709
|
+
E2006: { recoverable: true, retry_after_ms: null }, // AMBIGUOUS_INPUT
|
|
710
|
+
E2007: { recoverable: true, retry_after_ms: null }, // INSUFFICIENT_CONTEXT
|
|
711
|
+
// E3xxx: Output errors (schema violations)
|
|
712
|
+
E3001: { recoverable: true, retry_after_ms: 1000 }, // OUTPUT_SCHEMA_VIOLATION
|
|
713
|
+
E3002: { recoverable: true, retry_after_ms: null }, // PARTIAL_RESULT
|
|
714
|
+
E3003: { recoverable: false, retry_after_ms: null }, // MISSING_RATIONALE
|
|
715
|
+
E3004: { recoverable: false, retry_after_ms: null }, // OVERFLOW_LIMIT
|
|
716
|
+
E3005: { recoverable: false, retry_after_ms: null }, // INVALID_ENUM
|
|
717
|
+
E3006: { recoverable: false, retry_after_ms: null }, // CONSTRAINT_VIOLATION
|
|
718
|
+
// E4xxx: Runtime errors (infrastructure failures)
|
|
719
|
+
E4000: { recoverable: false, retry_after_ms: null }, // INTERNAL_ERROR
|
|
720
|
+
E4001: { recoverable: true, retry_after_ms: 5000 }, // PROVIDER_UNAVAILABLE
|
|
721
|
+
E4002: { recoverable: true, retry_after_ms: 10000 }, // RATE_LIMITED
|
|
722
|
+
E4003: { recoverable: false, retry_after_ms: null }, // CONTEXT_OVERFLOW
|
|
723
|
+
E4004: { recoverable: false, retry_after_ms: null }, // CIRCULAR_DEPENDENCY
|
|
724
|
+
E4005: { recoverable: false, retry_after_ms: null }, // MAX_DEPTH_EXCEEDED
|
|
725
|
+
E4006: { recoverable: true, retry_after_ms: null }, // MODULE_NOT_FOUND
|
|
726
|
+
E4007: { recoverable: false, retry_after_ms: null }, // PERMISSION_DENIED
|
|
727
|
+
// Legacy codes (for backward compatibility)
|
|
728
|
+
MODULE_NOT_FOUND: { recoverable: true, retry_after_ms: null },
|
|
729
|
+
INVALID_INPUT: { recoverable: true, retry_after_ms: null },
|
|
730
|
+
PARSE_ERROR: { recoverable: false, retry_after_ms: null },
|
|
731
|
+
SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
732
|
+
META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
733
|
+
POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
|
|
734
|
+
TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
|
|
735
|
+
LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
|
|
736
|
+
RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
|
|
737
|
+
TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
|
|
738
|
+
UNKNOWN: { recoverable: false, retry_after_ms: null },
|
|
739
|
+
};
|
|
740
|
+
/**
|
|
741
|
+
* Normalize error code to E-format.
|
|
742
|
+
* Accepts both E-format (E1001) and legacy format (INVALID_INPUT).
|
|
743
|
+
*
|
|
744
|
+
* @param code Error code in any format
|
|
745
|
+
* @returns E-format code (e.g., "E1001")
|
|
746
|
+
*/
|
|
747
|
+
export function normalizeErrorCode(code) {
|
|
748
|
+
// Already E-format
|
|
749
|
+
if (/^E\d{4}$/.test(code)) {
|
|
750
|
+
return code;
|
|
751
|
+
}
|
|
752
|
+
// Map legacy to E-format
|
|
753
|
+
const eCode = LEGACY_TO_E_CODE[code];
|
|
754
|
+
return eCode || 'E4000'; // Default to INTERNAL_ERROR
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Get error category from E-format code.
|
|
758
|
+
*
|
|
759
|
+
* @param code E-format error code (e.g., "E1001")
|
|
760
|
+
* @returns Category: 'input' | 'processing' | 'output' | 'runtime'
|
|
761
|
+
*/
|
|
762
|
+
export function getErrorCategory(code) {
|
|
763
|
+
const normalized = normalizeErrorCode(code);
|
|
764
|
+
const category = normalized.charAt(1);
|
|
765
|
+
switch (category) {
|
|
766
|
+
case '1': return 'input';
|
|
767
|
+
case '2': return 'processing';
|
|
768
|
+
case '3': return 'output';
|
|
769
|
+
case '4': return 'runtime';
|
|
770
|
+
default: return 'runtime';
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Build a standardized error response with enhanced taxonomy.
|
|
775
|
+
* Supports both E-format (E1001) and legacy format (INVALID_INPUT) error codes.
|
|
776
|
+
*
|
|
777
|
+
* @param options Error response options
|
|
778
|
+
* @returns Standardized error envelope
|
|
779
|
+
*/
|
|
780
|
+
export function makeErrorResponse(options) {
|
|
781
|
+
const { code, message, explain, partialData, details, recoverable, retryAfterMs, confidence = 0.0, risk = 'high', suggestion, useEFormat = true, } = options;
|
|
782
|
+
// Normalize error code to E-format if requested
|
|
783
|
+
const normalizedCode = useEFormat ? normalizeErrorCode(code) : code;
|
|
784
|
+
// Get default properties from error code (try normalized first, then original)
|
|
785
|
+
const defaults = ERROR_PROPERTIES[normalizedCode] || ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN || { recoverable: false, retry_after_ms: null };
|
|
786
|
+
const errorObj = {
|
|
787
|
+
code: normalizedCode,
|
|
788
|
+
message,
|
|
789
|
+
};
|
|
790
|
+
// Add recoverable flag
|
|
791
|
+
const isRecoverable = recoverable ?? defaults.recoverable;
|
|
792
|
+
if (isRecoverable !== undefined) {
|
|
793
|
+
errorObj.recoverable = isRecoverable;
|
|
794
|
+
}
|
|
795
|
+
// Add retry suggestion
|
|
796
|
+
const retryMs = retryAfterMs ?? defaults.retry_after_ms;
|
|
797
|
+
if (retryMs !== null && retryMs !== undefined) {
|
|
798
|
+
errorObj.retry_after_ms = retryMs;
|
|
799
|
+
}
|
|
800
|
+
// Add suggestion if provided
|
|
801
|
+
if (suggestion) {
|
|
802
|
+
errorObj.suggestion = suggestion;
|
|
803
|
+
}
|
|
804
|
+
// Add details if provided
|
|
805
|
+
if (details) {
|
|
806
|
+
errorObj.details = details;
|
|
807
|
+
}
|
|
808
|
+
// Determine confidence based on error category (if not explicitly provided)
|
|
809
|
+
let finalConfidence = confidence;
|
|
810
|
+
if (confidence === 0.0 && partialData) {
|
|
811
|
+
// If we have partial data, may have some confidence
|
|
812
|
+
const category = getErrorCategory(normalizedCode);
|
|
813
|
+
if (category === 'processing') {
|
|
814
|
+
finalConfidence = 0.3; // Some partial understanding
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
ok: false,
|
|
819
|
+
version: ENVELOPE_VERSION,
|
|
820
|
+
meta: {
|
|
821
|
+
confidence: finalConfidence,
|
|
822
|
+
risk,
|
|
823
|
+
explain: (explain || message).slice(0, 280),
|
|
824
|
+
},
|
|
825
|
+
error: errorObj,
|
|
826
|
+
partial_data: partialData,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Build a standardized success response.
|
|
831
|
+
*/
|
|
832
|
+
export function makeSuccessResponse(options) {
|
|
833
|
+
const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
|
|
834
|
+
const meta = {
|
|
835
|
+
confidence: Math.max(0.0, Math.min(1.0, confidence)),
|
|
836
|
+
risk,
|
|
837
|
+
explain: explain ? explain.slice(0, 280) : 'No explanation provided',
|
|
838
|
+
};
|
|
839
|
+
if (latencyMs !== undefined) {
|
|
840
|
+
meta.latency_ms = latencyMs;
|
|
841
|
+
}
|
|
842
|
+
if (model) {
|
|
843
|
+
meta.model = model;
|
|
844
|
+
}
|
|
845
|
+
if (traceId) {
|
|
846
|
+
meta.trace_id = traceId;
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
ok: true,
|
|
850
|
+
version: ENVELOPE_VERSION,
|
|
851
|
+
meta,
|
|
852
|
+
data,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
// =============================================================================
|
|
7
856
|
// Repair Pass (v2.2)
|
|
8
857
|
// =============================================================================
|
|
9
858
|
/**
|
|
10
859
|
* Attempt to repair envelope format issues without changing semantics.
|
|
11
860
|
*
|
|
12
|
-
* Repairs (lossless
|
|
861
|
+
* Repairs (mostly lossless, except explain truncation):
|
|
13
862
|
* - Missing meta fields (fill with conservative defaults)
|
|
14
863
|
* - Truncate explain if too long
|
|
15
864
|
* - Trim whitespace from string fields
|
|
865
|
+
* - Clamp confidence to [0, 1] range
|
|
16
866
|
*
|
|
17
867
|
* Does NOT repair:
|
|
18
868
|
* - Invalid enum values (treated as validation failure)
|
|
869
|
+
*
|
|
870
|
+
* Note: Returns a deep copy to avoid modifying the original data.
|
|
19
871
|
*/
|
|
20
872
|
function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
|
|
21
|
-
|
|
873
|
+
// Deep clone to avoid mutation
|
|
874
|
+
const repaired = deepClone(response);
|
|
22
875
|
// Ensure meta exists
|
|
23
876
|
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
24
877
|
repaired.meta = {};
|
|
@@ -34,14 +887,13 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
34
887
|
if (!meta.risk) {
|
|
35
888
|
meta.risk = aggregateRisk(data, riskRule);
|
|
36
889
|
}
|
|
37
|
-
// Trim whitespace only (lossless)
|
|
890
|
+
// Trim whitespace only (lossless). Do NOT repair invalid enum values.
|
|
38
891
|
if (typeof meta.risk === 'string') {
|
|
39
892
|
const trimmedRisk = meta.risk.trim().toLowerCase();
|
|
40
893
|
const validRisks = ['none', 'low', 'medium', 'high'];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
meta.risk = 'medium'; // Default for invalid type
|
|
894
|
+
if (validRisks.includes(trimmedRisk)) {
|
|
895
|
+
meta.risk = trimmedRisk;
|
|
896
|
+
}
|
|
45
897
|
}
|
|
46
898
|
// Repair explain
|
|
47
899
|
if (typeof meta.explain !== 'string') {
|
|
@@ -54,7 +906,7 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
54
906
|
if (meta.explain.length > maxExplainLength) {
|
|
55
907
|
meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
|
|
56
908
|
}
|
|
57
|
-
// Build proper v2.2 response
|
|
909
|
+
// Build proper v2.2 response with version
|
|
58
910
|
const builtMeta = {
|
|
59
911
|
confidence: meta.confidence,
|
|
60
912
|
risk: meta.risk,
|
|
@@ -62,21 +914,65 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
62
914
|
};
|
|
63
915
|
const result = repaired.ok === false ? {
|
|
64
916
|
ok: false,
|
|
917
|
+
version: ENVELOPE_VERSION,
|
|
65
918
|
meta: builtMeta,
|
|
66
|
-
|
|
919
|
+
// E4000 is an internal/runtime error fallback (should rarely happen after repair).
|
|
920
|
+
error: repaired.error ?? { code: 'E4000', message: 'Unknown error' },
|
|
67
921
|
partial_data: repaired.partial_data
|
|
68
922
|
} : {
|
|
69
923
|
ok: true,
|
|
924
|
+
version: ENVELOPE_VERSION,
|
|
70
925
|
meta: builtMeta,
|
|
71
926
|
data: repaired.data
|
|
72
927
|
};
|
|
73
928
|
return result;
|
|
74
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Repair error envelope format.
|
|
932
|
+
*
|
|
933
|
+
* Note: Returns a deep copy to avoid modifying the original data.
|
|
934
|
+
*/
|
|
935
|
+
function repairErrorEnvelope(data, maxExplainLength = 280) {
|
|
936
|
+
// Deep clone to avoid mutation
|
|
937
|
+
const repaired = deepClone(data);
|
|
938
|
+
// Ensure meta exists for errors
|
|
939
|
+
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
940
|
+
repaired.meta = {};
|
|
941
|
+
}
|
|
942
|
+
const meta = repaired.meta;
|
|
943
|
+
// Set default meta for errors
|
|
944
|
+
if (typeof meta.confidence !== 'number') {
|
|
945
|
+
meta.confidence = 0.0;
|
|
946
|
+
}
|
|
947
|
+
if (!meta.risk) {
|
|
948
|
+
meta.risk = 'high';
|
|
949
|
+
}
|
|
950
|
+
if (typeof meta.explain !== 'string') {
|
|
951
|
+
const error = (repaired.error ?? {});
|
|
952
|
+
meta.explain = (error.message ?? 'An error occurred').slice(0, maxExplainLength);
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
ok: false,
|
|
956
|
+
version: ENVELOPE_VERSION,
|
|
957
|
+
meta: {
|
|
958
|
+
confidence: meta.confidence,
|
|
959
|
+
risk: meta.risk,
|
|
960
|
+
explain: meta.explain,
|
|
961
|
+
},
|
|
962
|
+
// E4000 is an internal/runtime error fallback (should rarely happen after repair).
|
|
963
|
+
error: repaired.error ?? { code: 'E4000', message: 'Unknown error' },
|
|
964
|
+
partial_data: repaired.partial_data,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
75
967
|
/**
|
|
76
968
|
* Wrap v2.1 response to v2.2 format
|
|
77
969
|
*/
|
|
78
970
|
function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
79
971
|
if (isV22Envelope(response)) {
|
|
972
|
+
// Already v2.2, but ensure version field exists
|
|
973
|
+
if (!('version' in response) || !response.version) {
|
|
974
|
+
return { ...deepClone(response), version: ENVELOPE_VERSION };
|
|
975
|
+
}
|
|
80
976
|
return response;
|
|
81
977
|
}
|
|
82
978
|
if (response.ok) {
|
|
@@ -85,6 +981,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
85
981
|
const rationale = data.rationale ?? '';
|
|
86
982
|
return {
|
|
87
983
|
ok: true,
|
|
984
|
+
version: ENVELOPE_VERSION,
|
|
88
985
|
meta: {
|
|
89
986
|
confidence,
|
|
90
987
|
risk: aggregateRisk(data, riskRule),
|
|
@@ -97,18 +994,64 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
97
994
|
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
98
995
|
return {
|
|
99
996
|
ok: false,
|
|
997
|
+
version: ENVELOPE_VERSION,
|
|
100
998
|
meta: {
|
|
101
999
|
confidence: 0,
|
|
102
1000
|
risk: 'high',
|
|
103
1001
|
explain: errorMsg.slice(0, 280)
|
|
104
1002
|
},
|
|
105
|
-
error: response.error ?? { code: '
|
|
1003
|
+
error: response.error ?? { code: 'E4000', message: errorMsg }, // INTERNAL_ERROR fallback
|
|
106
1004
|
partial_data: response.partial_data
|
|
107
1005
|
};
|
|
108
1006
|
}
|
|
109
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Convert legacy format (no envelope) to v2.2 envelope.
|
|
1010
|
+
*/
|
|
1011
|
+
function convertLegacyToEnvelope(data, isError = false) {
|
|
1012
|
+
const isPlainObject = typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
1013
|
+
const dataObj = isPlainObject ? data : { result: data };
|
|
1014
|
+
if (isError || (isPlainObject && 'error' in dataObj)) {
|
|
1015
|
+
const error = (dataObj.error ?? {});
|
|
1016
|
+
const errorMsg = typeof error === 'object'
|
|
1017
|
+
? (error.message ?? String(error))
|
|
1018
|
+
: String(error);
|
|
1019
|
+
return {
|
|
1020
|
+
ok: false,
|
|
1021
|
+
version: ENVELOPE_VERSION,
|
|
1022
|
+
meta: {
|
|
1023
|
+
confidence: 0.0,
|
|
1024
|
+
risk: 'high',
|
|
1025
|
+
explain: errorMsg.slice(0, 280),
|
|
1026
|
+
},
|
|
1027
|
+
error: {
|
|
1028
|
+
code: (typeof error === 'object' ? error.code : undefined) ?? 'UNKNOWN',
|
|
1029
|
+
message: errorMsg,
|
|
1030
|
+
},
|
|
1031
|
+
partial_data: undefined,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
const confidence = dataObj.confidence ?? 0.5;
|
|
1036
|
+
const rationale = dataObj.rationale ?? '';
|
|
1037
|
+
return {
|
|
1038
|
+
ok: true,
|
|
1039
|
+
version: ENVELOPE_VERSION,
|
|
1040
|
+
meta: {
|
|
1041
|
+
confidence,
|
|
1042
|
+
risk: aggregateRisk(dataObj),
|
|
1043
|
+
explain: rationale.slice(0, 280) || 'No explanation provided',
|
|
1044
|
+
},
|
|
1045
|
+
data: dataObj,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// =============================================================================
|
|
1050
|
+
// Main Runner
|
|
1051
|
+
// =============================================================================
|
|
110
1052
|
export async function runModule(module, provider, options = {}) {
|
|
111
|
-
const { args, input, verbose = false, useEnvelope, useV22, enableRepair = true } = options;
|
|
1053
|
+
const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
|
|
1054
|
+
const startTime = Date.now();
|
|
112
1055
|
// Determine if we should use envelope format
|
|
113
1056
|
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
114
1057
|
// Determine if we should use v2.2 format
|
|
@@ -128,6 +1071,25 @@ export async function runModule(module, provider, options = {}) {
|
|
|
128
1071
|
inputData.query = args;
|
|
129
1072
|
}
|
|
130
1073
|
}
|
|
1074
|
+
// Invoke before hooks
|
|
1075
|
+
_invokeBeforeHooks(module.name, inputData, module);
|
|
1076
|
+
// Validate input against schema
|
|
1077
|
+
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
1078
|
+
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
1079
|
+
if (inputErrors.length > 0) {
|
|
1080
|
+
const errorResult = makeErrorResponse({
|
|
1081
|
+
code: 'E1001', // INVALID_INPUT
|
|
1082
|
+
message: inputErrors.join('; '),
|
|
1083
|
+
explain: 'Input validation failed.',
|
|
1084
|
+
confidence: 1.0,
|
|
1085
|
+
risk: 'none',
|
|
1086
|
+
details: { validation_errors: inputErrors },
|
|
1087
|
+
suggestion: 'Check input against the module schema and fix validation errors.',
|
|
1088
|
+
});
|
|
1089
|
+
_invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
|
|
1090
|
+
return errorResult;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
131
1093
|
// Build prompt with clean substitution
|
|
132
1094
|
const prompt = buildPrompt(module, inputData);
|
|
133
1095
|
if (verbose) {
|
|
@@ -202,70 +1164,427 @@ export async function runModule(module, provider, options = {}) {
|
|
|
202
1164
|
{ role: 'system', content: systemParts.join('\n') },
|
|
203
1165
|
{ role: 'user', content: prompt },
|
|
204
1166
|
];
|
|
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
1167
|
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
|
-
};
|
|
1168
|
+
// Invoke provider
|
|
1169
|
+
const result = await provider.invoke({
|
|
1170
|
+
messages,
|
|
1171
|
+
jsonSchema: module.outputSchema,
|
|
1172
|
+
temperature: 0.3,
|
|
1173
|
+
});
|
|
1174
|
+
if (verbose) {
|
|
1175
|
+
console.error('--- Response ---');
|
|
1176
|
+
console.error(result.content);
|
|
1177
|
+
console.error('--- End Response ---');
|
|
238
1178
|
}
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
1179
|
+
// Calculate latency
|
|
1180
|
+
const latencyMs = Date.now() - startTime;
|
|
1181
|
+
// Parse response
|
|
1182
|
+
let parsed;
|
|
1183
|
+
try {
|
|
1184
|
+
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1185
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
1186
|
+
parsed = JSON.parse(jsonStr.trim());
|
|
1187
|
+
}
|
|
1188
|
+
catch (e) {
|
|
1189
|
+
const errorResult = makeErrorResponse({
|
|
1190
|
+
code: 'E1000', // PARSE_ERROR
|
|
1191
|
+
message: `Failed to parse JSON response: ${e.message}`,
|
|
1192
|
+
explain: 'Failed to parse LLM response as JSON.',
|
|
1193
|
+
details: { raw_response: result.content.substring(0, 500) },
|
|
1194
|
+
suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
|
|
1195
|
+
});
|
|
1196
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1197
|
+
return errorResult;
|
|
1198
|
+
}
|
|
1199
|
+
// Convert to v2.2 envelope
|
|
1200
|
+
let response;
|
|
1201
|
+
if (isV22Envelope(parsed)) {
|
|
1202
|
+
response = parsed;
|
|
1203
|
+
}
|
|
1204
|
+
else if (isEnvelopeResponse(parsed)) {
|
|
1205
|
+
response = wrapV21ToV22(parsed, riskRule);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
response = convertLegacyToEnvelope(parsed);
|
|
248
1209
|
}
|
|
1210
|
+
// Add version and meta fields
|
|
1211
|
+
response.version = ENVELOPE_VERSION;
|
|
1212
|
+
if (response.meta) {
|
|
1213
|
+
response.meta.latency_ms = latencyMs;
|
|
1214
|
+
if (traceId) {
|
|
1215
|
+
response.meta.trace_id = traceId;
|
|
1216
|
+
}
|
|
1217
|
+
if (modelOverride) {
|
|
1218
|
+
response.meta.model = modelOverride;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// Validate and potentially repair output
|
|
1222
|
+
if (response.ok && validateOutput) {
|
|
1223
|
+
// Get data schema (support both "data" and "output" aliases)
|
|
1224
|
+
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1225
|
+
const metaSchema = module.metaSchema;
|
|
1226
|
+
const dataToValidate = response.data ?? {};
|
|
1227
|
+
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1228
|
+
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1229
|
+
if (dataErrors.length > 0 && enableRepair) {
|
|
1230
|
+
// Attempt repair pass
|
|
1231
|
+
response = repairEnvelope(response, riskRule);
|
|
1232
|
+
response.version = ENVELOPE_VERSION;
|
|
1233
|
+
// Re-validate after repair
|
|
1234
|
+
const repairedData = response.data ?? {};
|
|
1235
|
+
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1236
|
+
}
|
|
1237
|
+
if (dataErrors.length > 0) {
|
|
1238
|
+
const errorResult = makeErrorResponse({
|
|
1239
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1240
|
+
message: dataErrors.join('; '),
|
|
1241
|
+
explain: 'Schema validation failed after repair attempt.',
|
|
1242
|
+
partialData: response.data,
|
|
1243
|
+
details: { validation_errors: dataErrors },
|
|
1244
|
+
});
|
|
1245
|
+
_invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
|
|
1246
|
+
return errorResult;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// v2.2: Validate overflow limits
|
|
1250
|
+
const overflowErrors = validateOverflowLimits(dataToValidate, module);
|
|
1251
|
+
if (overflowErrors.length > 0) {
|
|
1252
|
+
const errorResult = makeErrorResponse({
|
|
1253
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1254
|
+
message: overflowErrors.join('; '),
|
|
1255
|
+
explain: 'Overflow validation failed.',
|
|
1256
|
+
partialData: dataToValidate,
|
|
1257
|
+
details: { overflow_errors: overflowErrors },
|
|
1258
|
+
});
|
|
1259
|
+
_invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
|
|
1260
|
+
return errorResult;
|
|
1261
|
+
}
|
|
1262
|
+
// v2.2: Validate enum strategy
|
|
1263
|
+
const enumErrors = validateEnumStrategy(dataToValidate, module);
|
|
1264
|
+
if (enumErrors.length > 0) {
|
|
1265
|
+
const errorResult = makeErrorResponse({
|
|
1266
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1267
|
+
message: enumErrors.join('; '),
|
|
1268
|
+
explain: 'Enum strategy validation failed.',
|
|
1269
|
+
partialData: dataToValidate,
|
|
1270
|
+
details: { enum_errors: enumErrors },
|
|
1271
|
+
});
|
|
1272
|
+
_invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
|
|
1273
|
+
return errorResult;
|
|
1274
|
+
}
|
|
1275
|
+
// Validate meta if schema exists
|
|
1276
|
+
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1277
|
+
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1278
|
+
if (metaErrors.length > 0 && enableRepair) {
|
|
1279
|
+
response = repairEnvelope(response, riskRule);
|
|
1280
|
+
response.version = ENVELOPE_VERSION;
|
|
1281
|
+
// Re-validate meta after repair
|
|
1282
|
+
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1283
|
+
if (metaErrors.length > 0) {
|
|
1284
|
+
const errorResult = makeErrorResponse({
|
|
1285
|
+
code: 'E3001', // META_VALIDATION_FAILED (maps to OUTPUT_SCHEMA_VIOLATION)
|
|
1286
|
+
message: metaErrors.join('; '),
|
|
1287
|
+
explain: 'Meta schema validation failed after repair attempt.',
|
|
1288
|
+
partialData: response.data,
|
|
1289
|
+
details: { validation_errors: metaErrors },
|
|
1290
|
+
});
|
|
1291
|
+
_invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
|
|
1292
|
+
return errorResult;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
else if (enableRepair) {
|
|
1298
|
+
// Repair error envelopes to ensure they have proper meta fields
|
|
1299
|
+
response = repairErrorEnvelope(response);
|
|
1300
|
+
response.version = ENVELOPE_VERSION;
|
|
1301
|
+
}
|
|
1302
|
+
// Invoke after hooks
|
|
1303
|
+
const finalLatencyMs = Date.now() - startTime;
|
|
1304
|
+
_invokeAfterHooks(module.name, response, finalLatencyMs);
|
|
249
1305
|
return response;
|
|
250
1306
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1307
|
+
catch (e) {
|
|
1308
|
+
const latencyMs = Date.now() - startTime;
|
|
1309
|
+
const errorResult = makeErrorResponse({
|
|
1310
|
+
code: 'E4000', // INTERNAL_ERROR
|
|
1311
|
+
message: e.message,
|
|
1312
|
+
explain: `Unexpected error: ${e.name}`,
|
|
1313
|
+
details: { exception_type: e.name },
|
|
1314
|
+
});
|
|
1315
|
+
if (errorResult.meta) {
|
|
1316
|
+
errorResult.meta.latency_ms = latencyMs;
|
|
1317
|
+
}
|
|
1318
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1319
|
+
return errorResult;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Run a cognitive module with streaming output.
|
|
1324
|
+
*
|
|
1325
|
+
* Yields StreamEvent objects as the module executes:
|
|
1326
|
+
* - type="start": Module execution started
|
|
1327
|
+
* - type="chunk": Incremental data chunk (if LLM supports streaming)
|
|
1328
|
+
* - type="meta": Meta information available early
|
|
1329
|
+
* - type="complete": Final complete result
|
|
1330
|
+
* - type="error": Error occurred
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* for await (const event of runModuleStream(module, provider, options)) {
|
|
1334
|
+
* if (event.type === 'chunk') {
|
|
1335
|
+
* process.stdout.write(event.chunk);
|
|
1336
|
+
* } else if (event.type === 'complete') {
|
|
1337
|
+
* console.log('Result:', event.result);
|
|
1338
|
+
* }
|
|
1339
|
+
* }
|
|
1340
|
+
*/
|
|
1341
|
+
export async function* runModuleStream(module, provider, options = {}) {
|
|
1342
|
+
const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
|
|
1343
|
+
const startTime = Date.now();
|
|
1344
|
+
const moduleName = module.name;
|
|
1345
|
+
function makeEvent(type, extra = {}) {
|
|
256
1346
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
|
|
262
|
-
},
|
|
263
|
-
data: legacyResult.data,
|
|
264
|
-
raw: result.content
|
|
1347
|
+
type,
|
|
1348
|
+
timestamp_ms: Date.now() - startTime,
|
|
1349
|
+
module_name: moduleName,
|
|
1350
|
+
...extra,
|
|
265
1351
|
};
|
|
266
1352
|
}
|
|
267
|
-
|
|
1353
|
+
try {
|
|
1354
|
+
// Emit start event
|
|
1355
|
+
yield makeEvent('start');
|
|
1356
|
+
// Build input data
|
|
1357
|
+
const inputData = input || {};
|
|
1358
|
+
if (args && !inputData.code && !inputData.query) {
|
|
1359
|
+
if (looksLikeCode(args)) {
|
|
1360
|
+
inputData.code = args;
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
inputData.query = args;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
_invokeBeforeHooks(module.name, inputData, module);
|
|
1367
|
+
// Validate input if enabled
|
|
1368
|
+
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
1369
|
+
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
1370
|
+
if (inputErrors.length > 0) {
|
|
1371
|
+
const errorResult = makeErrorResponse({
|
|
1372
|
+
code: 'E1001', // INVALID_INPUT
|
|
1373
|
+
message: inputErrors.join('; '),
|
|
1374
|
+
confidence: 1.0,
|
|
1375
|
+
risk: 'none',
|
|
1376
|
+
suggestion: 'Check input against the module schema and fix validation errors.',
|
|
1377
|
+
});
|
|
1378
|
+
_invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
|
|
1379
|
+
const errorObj = errorResult.error;
|
|
1380
|
+
yield makeEvent('error', { error: errorObj });
|
|
1381
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// Get risk_rule from module config
|
|
1386
|
+
const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
1387
|
+
// Build prompt
|
|
1388
|
+
const prompt = buildPrompt(module, inputData);
|
|
1389
|
+
// Build messages
|
|
1390
|
+
const systemParts = [
|
|
1391
|
+
`You are executing the "${module.name}" Cognitive Module.`,
|
|
1392
|
+
'',
|
|
1393
|
+
`RESPONSIBILITY: ${module.responsibility}`,
|
|
1394
|
+
];
|
|
1395
|
+
if (useV22) {
|
|
1396
|
+
systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
|
|
1397
|
+
systemParts.push('- Wrap your response in the v2.2 envelope format');
|
|
1398
|
+
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
1399
|
+
systemParts.push('- Return ONLY valid JSON.');
|
|
1400
|
+
}
|
|
1401
|
+
const messages = [
|
|
1402
|
+
{ role: 'system', content: systemParts.join('\n') },
|
|
1403
|
+
{ role: 'user', content: prompt },
|
|
1404
|
+
];
|
|
1405
|
+
// Invoke provider with streaming if supported
|
|
1406
|
+
let fullContent;
|
|
1407
|
+
if (provider.supportsStreaming?.() && provider.invokeStream) {
|
|
1408
|
+
// Use true streaming
|
|
1409
|
+
const stream = provider.invokeStream({
|
|
1410
|
+
messages,
|
|
1411
|
+
jsonSchema: module.outputSchema,
|
|
1412
|
+
temperature: 0.3,
|
|
1413
|
+
});
|
|
1414
|
+
// Iterate through the async generator, yielding chunks as they arrive
|
|
1415
|
+
let streamResult;
|
|
1416
|
+
while (!(streamResult = await stream.next()).done) {
|
|
1417
|
+
const chunk = streamResult.value;
|
|
1418
|
+
yield makeEvent('chunk', { chunk });
|
|
1419
|
+
}
|
|
1420
|
+
// Get the final result (returned from the generator)
|
|
1421
|
+
fullContent = streamResult.value.content;
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
// Fallback to non-streaming invoke
|
|
1425
|
+
const result = await provider.invoke({
|
|
1426
|
+
messages,
|
|
1427
|
+
jsonSchema: module.outputSchema,
|
|
1428
|
+
temperature: 0.3,
|
|
1429
|
+
});
|
|
1430
|
+
fullContent = result.content;
|
|
1431
|
+
// Emit chunk event with full response
|
|
1432
|
+
yield makeEvent('chunk', { chunk: result.content });
|
|
1433
|
+
}
|
|
1434
|
+
// Parse response
|
|
1435
|
+
let parsed;
|
|
1436
|
+
try {
|
|
1437
|
+
const jsonMatch = fullContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1438
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : fullContent;
|
|
1439
|
+
parsed = JSON.parse(jsonStr.trim());
|
|
1440
|
+
}
|
|
1441
|
+
catch (e) {
|
|
1442
|
+
const errorResult = makeErrorResponse({
|
|
1443
|
+
code: 'E1000', // PARSE_ERROR
|
|
1444
|
+
message: `Failed to parse JSON: ${e.message}`,
|
|
1445
|
+
suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
|
|
1446
|
+
});
|
|
1447
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1448
|
+
// errorResult is always an error response from makeErrorResponse
|
|
1449
|
+
const errorObj = errorResult.error;
|
|
1450
|
+
yield makeEvent('error', { error: errorObj });
|
|
1451
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
// Convert to v2.2 envelope
|
|
1455
|
+
let response;
|
|
1456
|
+
if (isV22Envelope(parsed)) {
|
|
1457
|
+
response = parsed;
|
|
1458
|
+
}
|
|
1459
|
+
else if (isEnvelopeResponse(parsed)) {
|
|
1460
|
+
response = wrapV21ToV22(parsed, riskRule);
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
response = convertLegacyToEnvelope(parsed);
|
|
1464
|
+
}
|
|
1465
|
+
// Add version and meta
|
|
1466
|
+
response.version = ENVELOPE_VERSION;
|
|
1467
|
+
const latencyMs = Date.now() - startTime;
|
|
1468
|
+
if (response.meta) {
|
|
1469
|
+
response.meta.latency_ms = latencyMs;
|
|
1470
|
+
if (traceId) {
|
|
1471
|
+
response.meta.trace_id = traceId;
|
|
1472
|
+
}
|
|
1473
|
+
if (model) {
|
|
1474
|
+
response.meta.model = model;
|
|
1475
|
+
}
|
|
1476
|
+
// Emit meta event early
|
|
1477
|
+
yield makeEvent('meta', { meta: response.meta });
|
|
1478
|
+
}
|
|
1479
|
+
// Validate and repair output
|
|
1480
|
+
if (response.ok && validateOutput) {
|
|
1481
|
+
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1482
|
+
const metaSchema = module.metaSchema;
|
|
1483
|
+
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1484
|
+
let dataToValidate = response.data ?? {};
|
|
1485
|
+
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1486
|
+
if (dataErrors.length > 0 && enableRepair) {
|
|
1487
|
+
response = repairEnvelope(response, riskRule);
|
|
1488
|
+
response.version = ENVELOPE_VERSION;
|
|
1489
|
+
// Re-validate after repair
|
|
1490
|
+
const repairedData = response.data ?? {};
|
|
1491
|
+
dataToValidate = repairedData;
|
|
1492
|
+
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1493
|
+
}
|
|
1494
|
+
if (dataErrors.length > 0) {
|
|
1495
|
+
const errorResult = makeErrorResponse({
|
|
1496
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1497
|
+
message: dataErrors.join('; '),
|
|
1498
|
+
explain: 'Schema validation failed after repair attempt.',
|
|
1499
|
+
partialData: response.data,
|
|
1500
|
+
details: { validation_errors: dataErrors },
|
|
1501
|
+
});
|
|
1502
|
+
_invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
|
|
1503
|
+
const errorObj = errorResult.error;
|
|
1504
|
+
yield makeEvent('error', { error: errorObj });
|
|
1505
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const overflowErrors = validateOverflowLimits(dataToValidate, module);
|
|
1509
|
+
if (overflowErrors.length > 0) {
|
|
1510
|
+
const errorResult = makeErrorResponse({
|
|
1511
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1512
|
+
message: overflowErrors.join('; '),
|
|
1513
|
+
explain: 'Overflow validation failed.',
|
|
1514
|
+
partialData: dataToValidate,
|
|
1515
|
+
details: { overflow_errors: overflowErrors },
|
|
1516
|
+
});
|
|
1517
|
+
_invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
|
|
1518
|
+
const errorObj = errorResult.error;
|
|
1519
|
+
yield makeEvent('error', { error: errorObj });
|
|
1520
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const enumErrors = validateEnumStrategy(dataToValidate, module);
|
|
1524
|
+
if (enumErrors.length > 0) {
|
|
1525
|
+
const errorResult = makeErrorResponse({
|
|
1526
|
+
code: 'E3001', // OUTPUT_SCHEMA_VIOLATION
|
|
1527
|
+
message: enumErrors.join('; '),
|
|
1528
|
+
explain: 'Enum strategy validation failed.',
|
|
1529
|
+
partialData: dataToValidate,
|
|
1530
|
+
details: { enum_errors: enumErrors },
|
|
1531
|
+
});
|
|
1532
|
+
_invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
|
|
1533
|
+
const errorObj = errorResult.error;
|
|
1534
|
+
yield makeEvent('error', { error: errorObj });
|
|
1535
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// Validate meta if schema exists
|
|
1540
|
+
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1541
|
+
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1542
|
+
if (metaErrors.length > 0 && enableRepair) {
|
|
1543
|
+
response = repairEnvelope(response, riskRule);
|
|
1544
|
+
response.version = ENVELOPE_VERSION;
|
|
1545
|
+
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1546
|
+
if (metaErrors.length > 0) {
|
|
1547
|
+
const errorResult = makeErrorResponse({
|
|
1548
|
+
code: 'E3001', // META_VALIDATION_FAILED (maps to OUTPUT_SCHEMA_VIOLATION)
|
|
1549
|
+
message: metaErrors.join('; '),
|
|
1550
|
+
explain: 'Meta validation failed after repair attempt.',
|
|
1551
|
+
partialData: response.data,
|
|
1552
|
+
details: { validation_errors: metaErrors },
|
|
1553
|
+
});
|
|
1554
|
+
_invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
|
|
1555
|
+
const errorObj = errorResult.error;
|
|
1556
|
+
yield makeEvent('error', { error: errorObj });
|
|
1557
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
else if (!response.ok && enableRepair) {
|
|
1564
|
+
response = repairErrorEnvelope(response);
|
|
1565
|
+
response.version = ENVELOPE_VERSION;
|
|
1566
|
+
}
|
|
1567
|
+
const finalLatencyMs = Date.now() - startTime;
|
|
1568
|
+
_invokeAfterHooks(module.name, response, finalLatencyMs);
|
|
1569
|
+
// Emit complete event
|
|
1570
|
+
yield makeEvent('complete', { result: response });
|
|
1571
|
+
}
|
|
1572
|
+
catch (e) {
|
|
1573
|
+
_invokeErrorHooks(module.name, e, null);
|
|
1574
|
+
const errorResult = makeErrorResponse({
|
|
1575
|
+
code: 'E4000', // INTERNAL_ERROR
|
|
1576
|
+
message: e.message,
|
|
1577
|
+
explain: `Unexpected error: ${e.name}`,
|
|
1578
|
+
});
|
|
1579
|
+
// errorResult is always an error response from makeErrorResponse
|
|
1580
|
+
const errorObj = errorResult.error;
|
|
1581
|
+
yield makeEvent('error', { error: errorObj });
|
|
1582
|
+
yield makeEvent('complete', { result: errorResult });
|
|
1583
|
+
}
|
|
268
1584
|
}
|
|
1585
|
+
// =============================================================================
|
|
1586
|
+
// Helper Functions
|
|
1587
|
+
// =============================================================================
|
|
269
1588
|
/**
|
|
270
1589
|
* Check if response is in envelope format
|
|
271
1590
|
*/
|
|
@@ -326,6 +1645,18 @@ function parseEnvelopeResponse(response, raw) {
|
|
|
326
1645
|
* Parse legacy (non-envelope) format response
|
|
327
1646
|
*/
|
|
328
1647
|
function parseLegacyResponse(output, raw) {
|
|
1648
|
+
const isPlainObject = typeof output === 'object' && output !== null && !Array.isArray(output);
|
|
1649
|
+
if (!isPlainObject) {
|
|
1650
|
+
return {
|
|
1651
|
+
ok: true,
|
|
1652
|
+
data: {
|
|
1653
|
+
result: output,
|
|
1654
|
+
confidence: 0.5,
|
|
1655
|
+
rationale: '',
|
|
1656
|
+
},
|
|
1657
|
+
raw,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
329
1660
|
const outputObj = output;
|
|
330
1661
|
const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
|
|
331
1662
|
const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
|
|
@@ -360,6 +1691,12 @@ function parseLegacyResponse(output, raw) {
|
|
|
360
1691
|
}
|
|
361
1692
|
/**
|
|
362
1693
|
* Build prompt with clean variable substitution
|
|
1694
|
+
*
|
|
1695
|
+
* Substitution order (important to avoid partial replacements):
|
|
1696
|
+
* 1. ${variable} - v2 style placeholders
|
|
1697
|
+
* 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
|
|
1698
|
+
* 3. $N - shorthand indexed access (descending order)
|
|
1699
|
+
* 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
|
|
363
1700
|
*/
|
|
364
1701
|
function buildPrompt(module, input) {
|
|
365
1702
|
let prompt = module.prompt;
|
|
@@ -368,16 +1705,22 @@ function buildPrompt(module, input) {
|
|
|
368
1705
|
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
369
1706
|
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
370
1707
|
}
|
|
371
|
-
// v1 compatibility:
|
|
1708
|
+
// v1 compatibility: get args value
|
|
372
1709
|
const argsValue = input.code || input.query || '';
|
|
373
|
-
|
|
374
|
-
//
|
|
1710
|
+
// Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
|
|
1711
|
+
// Process in descending order to avoid $1 replacing part of $10
|
|
375
1712
|
if (typeof argsValue === 'string') {
|
|
376
1713
|
const argsList = argsValue.split(/\s+/);
|
|
377
|
-
argsList.
|
|
1714
|
+
for (let i = argsList.length - 1; i >= 0; i--) {
|
|
1715
|
+
const arg = argsList[i];
|
|
1716
|
+
// Replace $ARGUMENTS[N] first
|
|
1717
|
+
prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
|
|
1718
|
+
// Replace $N shorthand
|
|
378
1719
|
prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
|
|
379
|
-
}
|
|
1720
|
+
}
|
|
380
1721
|
}
|
|
1722
|
+
// Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
|
|
1723
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
381
1724
|
// Append input summary if not already in prompt
|
|
382
1725
|
if (!prompt.includes(argsValue) && argsValue) {
|
|
383
1726
|
prompt += '\n\n## Input\n\n';
|
|
@@ -405,3 +1748,61 @@ function looksLikeCode(str) {
|
|
|
405
1748
|
];
|
|
406
1749
|
return codeIndicators.some(re => re.test(str));
|
|
407
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Run a cognitive module (legacy API, returns raw output).
|
|
1753
|
+
* For backward compatibility. Throws on error instead of returning error envelope.
|
|
1754
|
+
*/
|
|
1755
|
+
export async function runModuleLegacy(module, provider, input, options = {}) {
|
|
1756
|
+
const { validateInput = true, validateOutput = true, model } = options;
|
|
1757
|
+
const result = await runModule(module, provider, {
|
|
1758
|
+
input,
|
|
1759
|
+
validateInput,
|
|
1760
|
+
validateOutput,
|
|
1761
|
+
useEnvelope: false,
|
|
1762
|
+
useV22: false,
|
|
1763
|
+
model,
|
|
1764
|
+
});
|
|
1765
|
+
if (result.ok && 'data' in result) {
|
|
1766
|
+
return result.data;
|
|
1767
|
+
}
|
|
1768
|
+
else {
|
|
1769
|
+
const error = 'error' in result ? result.error : { code: 'E4000', message: 'Unknown error' }; // INTERNAL_ERROR fallback
|
|
1770
|
+
throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
// =============================================================================
|
|
1774
|
+
// Convenience Functions
|
|
1775
|
+
// =============================================================================
|
|
1776
|
+
/**
|
|
1777
|
+
* Extract meta from v2.2 envelope for routing/logging.
|
|
1778
|
+
*/
|
|
1779
|
+
export function extractMeta(result) {
|
|
1780
|
+
return result.meta ?? {
|
|
1781
|
+
confidence: 0.5,
|
|
1782
|
+
risk: 'medium',
|
|
1783
|
+
explain: 'No meta available',
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
// Alias for backward compatibility
|
|
1787
|
+
export const extractMetaV22 = extractMeta;
|
|
1788
|
+
/**
|
|
1789
|
+
* Determine if result should be escalated to human review based on meta.
|
|
1790
|
+
*/
|
|
1791
|
+
export function shouldEscalate(result, confidenceThreshold = 0.7) {
|
|
1792
|
+
const meta = extractMeta(result);
|
|
1793
|
+
// Escalate if low confidence
|
|
1794
|
+
if (meta.confidence < confidenceThreshold) {
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
// Escalate if high risk
|
|
1798
|
+
if (meta.risk === 'high') {
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
// Escalate if error
|
|
1802
|
+
if (!result.ok) {
|
|
1803
|
+
return true;
|
|
1804
|
+
}
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
// Alias for backward compatibility
|
|
1808
|
+
export const shouldEscalateV22 = shouldEscalate;
|