cognitive-modules-cli 1.4.1 → 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 +348 -34
- package/dist/modules/runner.js +1263 -708
- package/dist/modules/subagent.js +2 -0
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +629 -0
- package/dist/providers/base.d.ts +1 -45
- package/dist/providers/base.js +0 -67
- package/dist/providers/openai.d.ts +3 -27
- package/dist/providers/openai.js +3 -175
- package/dist/types.d.ts +93 -316
- package/dist/types.js +1 -120
- 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 +1692 -998
- package/src/modules/subagent.ts +2 -0
- package/src/modules/validator.ts +700 -0
- package/src/providers/base.ts +1 -86
- package/src/providers/openai.ts +4 -226
- package/src/types.ts +113 -462
- package/tsconfig.json +1 -1
package/dist/modules/runner.js
CHANGED
|
@@ -1,28 +1,712 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Runner - Execute Cognitive Modules
|
|
3
|
-
* v2.
|
|
3
|
+
* v2.2: Envelope format with meta/data separation, risk_rule, repair pass
|
|
4
|
+
* v2.2.1: Version field, enhanced error taxonomy, observability hooks, streaming
|
|
4
5
|
*/
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import _Ajv from 'ajv';
|
|
7
|
+
const Ajv = _Ajv.default || _Ajv;
|
|
8
|
+
import { aggregateRisk, isV22Envelope } from '../types.js';
|
|
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
|
+
}
|
|
10
690
|
// =============================================================================
|
|
11
691
|
// Repair Pass (v2.2)
|
|
12
692
|
// =============================================================================
|
|
13
693
|
/**
|
|
14
694
|
* Attempt to repair envelope format issues without changing semantics.
|
|
15
695
|
*
|
|
16
|
-
* Repairs (lossless
|
|
696
|
+
* Repairs (mostly lossless, except explain truncation):
|
|
17
697
|
* - Missing meta fields (fill with conservative defaults)
|
|
18
698
|
* - Truncate explain if too long
|
|
19
699
|
* - Trim whitespace from string fields
|
|
700
|
+
* - Clamp confidence to [0, 1] range
|
|
20
701
|
*
|
|
21
702
|
* Does NOT repair:
|
|
22
703
|
* - Invalid enum values (treated as validation failure)
|
|
704
|
+
*
|
|
705
|
+
* Note: Returns a deep copy to avoid modifying the original data.
|
|
23
706
|
*/
|
|
24
707
|
function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLength = 280) {
|
|
25
|
-
|
|
708
|
+
// Deep clone to avoid mutation
|
|
709
|
+
const repaired = deepClone(response);
|
|
26
710
|
// Ensure meta exists
|
|
27
711
|
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
28
712
|
repaired.meta = {};
|
|
@@ -58,7 +742,7 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
58
742
|
if (meta.explain.length > maxExplainLength) {
|
|
59
743
|
meta.explain = meta.explain.slice(0, maxExplainLength - 3) + '...';
|
|
60
744
|
}
|
|
61
|
-
// Build proper v2.2 response
|
|
745
|
+
// Build proper v2.2 response with version
|
|
62
746
|
const builtMeta = {
|
|
63
747
|
confidence: meta.confidence,
|
|
64
748
|
risk: meta.risk,
|
|
@@ -66,21 +750,63 @@ function repairEnvelope(response, riskRule = 'max_changes_risk', maxExplainLengt
|
|
|
66
750
|
};
|
|
67
751
|
const result = repaired.ok === false ? {
|
|
68
752
|
ok: false,
|
|
753
|
+
version: ENVELOPE_VERSION,
|
|
69
754
|
meta: builtMeta,
|
|
70
755
|
error: repaired.error ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
71
756
|
partial_data: repaired.partial_data
|
|
72
757
|
} : {
|
|
73
758
|
ok: true,
|
|
759
|
+
version: ENVELOPE_VERSION,
|
|
74
760
|
meta: builtMeta,
|
|
75
761
|
data: repaired.data
|
|
76
762
|
};
|
|
77
763
|
return result;
|
|
78
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
|
+
}
|
|
79
801
|
/**
|
|
80
802
|
* Wrap v2.1 response to v2.2 format
|
|
81
803
|
*/
|
|
82
804
|
function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
83
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
|
+
}
|
|
84
810
|
return response;
|
|
85
811
|
}
|
|
86
812
|
if (response.ok) {
|
|
@@ -89,6 +815,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
89
815
|
const rationale = data.rationale ?? '';
|
|
90
816
|
return {
|
|
91
817
|
ok: true,
|
|
818
|
+
version: ENVELOPE_VERSION,
|
|
92
819
|
meta: {
|
|
93
820
|
confidence,
|
|
94
821
|
risk: aggregateRisk(data, riskRule),
|
|
@@ -101,6 +828,7 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
101
828
|
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
102
829
|
return {
|
|
103
830
|
ok: false,
|
|
831
|
+
version: ENVELOPE_VERSION,
|
|
104
832
|
meta: {
|
|
105
833
|
confidence: 0,
|
|
106
834
|
risk: 'high',
|
|
@@ -111,8 +839,51 @@ function wrapV21ToV22(response, riskRule = 'max_changes_risk') {
|
|
|
111
839
|
};
|
|
112
840
|
}
|
|
113
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
|
+
// =============================================================================
|
|
114
884
|
export async function runModule(module, provider, options = {}) {
|
|
115
|
-
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();
|
|
116
887
|
// Determine if we should use envelope format
|
|
117
888
|
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
118
889
|
// Determine if we should use v2.2 format
|
|
@@ -132,6 +903,24 @@ export async function runModule(module, provider, options = {}) {
|
|
|
132
903
|
inputData.query = args;
|
|
133
904
|
}
|
|
134
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
|
+
}
|
|
135
924
|
// Build prompt with clean substitution
|
|
136
925
|
const prompt = buildPrompt(module, inputData);
|
|
137
926
|
if (verbose) {
|
|
@@ -184,490 +973,400 @@ export async function runModule(module, provider, options = {}) {
|
|
|
184
973
|
systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
|
|
185
974
|
}
|
|
186
975
|
else {
|
|
187
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope):');
|
|
188
|
-
systemParts.push('- Wrap your response in the envelope format');
|
|
189
|
-
systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
|
|
190
|
-
systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
191
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
|
|
192
|
-
}
|
|
193
|
-
if (module.output?.require_behavior_equivalence) {
|
|
194
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) in data');
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
systemParts.push('', 'OUTPUT FORMAT:');
|
|
199
|
-
systemParts.push('- Respond with ONLY valid JSON');
|
|
200
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
|
|
201
|
-
if (module.output?.require_behavior_equivalence) {
|
|
202
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) field');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const messages = [
|
|
206
|
-
{ role: 'system', content: systemParts.join('\n') },
|
|
207
|
-
{ role: 'user', content: prompt },
|
|
208
|
-
];
|
|
209
|
-
// Invoke provider
|
|
210
|
-
const result = await provider.invoke({
|
|
211
|
-
messages,
|
|
212
|
-
jsonSchema: module.outputSchema,
|
|
213
|
-
temperature: 0.3,
|
|
214
|
-
});
|
|
215
|
-
if (verbose) {
|
|
216
|
-
console.error('--- Response ---');
|
|
217
|
-
console.error(result.content);
|
|
218
|
-
console.error('--- End Response ---');
|
|
219
|
-
}
|
|
220
|
-
// Parse response
|
|
221
|
-
let parsed;
|
|
222
|
-
try {
|
|
223
|
-
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
224
|
-
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
225
|
-
parsed = JSON.parse(jsonStr.trim());
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
|
|
229
|
-
}
|
|
230
|
-
// Handle envelope format
|
|
231
|
-
if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
|
|
232
|
-
let response = parseEnvelopeResponse(parsed, result.content);
|
|
233
|
-
// Upgrade to v2.2 if needed
|
|
234
|
-
if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
|
|
235
|
-
const upgraded = wrapV21ToV22(parsed, riskRule);
|
|
236
|
-
response = {
|
|
237
|
-
ok: true,
|
|
238
|
-
meta: upgraded.meta,
|
|
239
|
-
data: upgraded.data,
|
|
240
|
-
raw: result.content
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
// Apply repair pass if enabled and response needs it
|
|
244
|
-
if (enableRepair && response.ok && shouldUseV22) {
|
|
245
|
-
const repaired = repairEnvelope(response, riskRule);
|
|
246
|
-
response = {
|
|
247
|
-
ok: true,
|
|
248
|
-
meta: repaired.meta,
|
|
249
|
-
data: repaired.data,
|
|
250
|
-
raw: result.content
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
return response;
|
|
254
|
-
}
|
|
255
|
-
// Handle legacy format (non-envelope)
|
|
256
|
-
const legacyResult = parseLegacyResponse(parsed, result.content);
|
|
257
|
-
// Upgrade to v2.2 if requested
|
|
258
|
-
if (shouldUseV22 && legacyResult.ok) {
|
|
259
|
-
const data = (legacyResult.data ?? {});
|
|
260
|
-
return {
|
|
261
|
-
ok: true,
|
|
262
|
-
meta: {
|
|
263
|
-
confidence: data.confidence ?? 0.5,
|
|
264
|
-
risk: aggregateRisk(data, riskRule),
|
|
265
|
-
explain: (data.rationale ?? '').slice(0, 280) || 'No explanation provided'
|
|
266
|
-
},
|
|
267
|
-
data: legacyResult.data,
|
|
268
|
-
raw: result.content
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
return legacyResult;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Check if response is in envelope format
|
|
275
|
-
*/
|
|
276
|
-
function isEnvelopeResponse(obj) {
|
|
277
|
-
if (typeof obj !== 'object' || obj === null)
|
|
278
|
-
return false;
|
|
279
|
-
const o = obj;
|
|
280
|
-
return typeof o.ok === 'boolean';
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Parse envelope format response (supports both v2.1 and v2.2)
|
|
284
|
-
*/
|
|
285
|
-
function parseEnvelopeResponse(response, raw) {
|
|
286
|
-
// Check if v2.2 format (has meta)
|
|
287
|
-
if (isV22Envelope(response)) {
|
|
288
|
-
if (response.ok) {
|
|
289
|
-
return {
|
|
290
|
-
ok: true,
|
|
291
|
-
meta: response.meta,
|
|
292
|
-
data: response.data,
|
|
293
|
-
raw,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
return {
|
|
298
|
-
ok: false,
|
|
299
|
-
meta: response.meta,
|
|
300
|
-
error: response.error,
|
|
301
|
-
partial_data: response.partial_data,
|
|
302
|
-
raw,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// v2.1 format
|
|
307
|
-
if (response.ok) {
|
|
308
|
-
const data = (response.data ?? {});
|
|
309
|
-
return {
|
|
310
|
-
ok: true,
|
|
311
|
-
data: {
|
|
312
|
-
...data,
|
|
313
|
-
confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
|
|
314
|
-
rationale: typeof data.rationale === 'string' ? data.rationale : '',
|
|
315
|
-
behavior_equivalence: data.behavior_equivalence,
|
|
316
|
-
},
|
|
317
|
-
raw,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
return {
|
|
322
|
-
ok: false,
|
|
323
|
-
error: response.error,
|
|
324
|
-
partial_data: response.partial_data,
|
|
325
|
-
raw,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Parse legacy (non-envelope) format response
|
|
331
|
-
*/
|
|
332
|
-
function parseLegacyResponse(output, raw) {
|
|
333
|
-
const outputObj = output;
|
|
334
|
-
const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
|
|
335
|
-
const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
|
|
336
|
-
const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
|
|
337
|
-
? outputObj.behavior_equivalence
|
|
338
|
-
: undefined;
|
|
339
|
-
// Check if this is an error response (has error.code)
|
|
340
|
-
if (outputObj.error && typeof outputObj.error === 'object') {
|
|
341
|
-
const errorObj = outputObj.error;
|
|
342
|
-
if (typeof errorObj.code === 'string') {
|
|
343
|
-
return {
|
|
344
|
-
ok: false,
|
|
345
|
-
error: {
|
|
346
|
-
code: errorObj.code,
|
|
347
|
-
message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
|
|
348
|
-
},
|
|
349
|
-
raw,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Return as v2.1 format (data includes confidence)
|
|
354
|
-
return {
|
|
355
|
-
ok: true,
|
|
356
|
-
data: {
|
|
357
|
-
...outputObj,
|
|
358
|
-
confidence,
|
|
359
|
-
rationale,
|
|
360
|
-
behavior_equivalence: behaviorEquivalence,
|
|
361
|
-
},
|
|
362
|
-
raw,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Build prompt with clean variable substitution
|
|
367
|
-
*/
|
|
368
|
-
function buildPrompt(module, input) {
|
|
369
|
-
let prompt = module.prompt;
|
|
370
|
-
// v2 style: substitute ${variable} placeholders
|
|
371
|
-
for (const [key, value] of Object.entries(input)) {
|
|
372
|
-
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
373
|
-
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
374
|
-
}
|
|
375
|
-
// v1 compatibility: substitute $ARGUMENTS
|
|
376
|
-
const argsValue = input.code || input.query || '';
|
|
377
|
-
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
378
|
-
// Substitute $N placeholders (v1 compatibility)
|
|
379
|
-
if (typeof argsValue === 'string') {
|
|
380
|
-
const argsList = argsValue.split(/\s+/);
|
|
381
|
-
argsList.forEach((arg, i) => {
|
|
382
|
-
prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
// Append input summary if not already in prompt
|
|
386
|
-
if (!prompt.includes(argsValue) && argsValue) {
|
|
387
|
-
prompt += '\n\n## Input\n\n';
|
|
388
|
-
if (input.code) {
|
|
389
|
-
prompt += '```\n' + input.code + '\n```\n';
|
|
390
|
-
}
|
|
391
|
-
if (input.query) {
|
|
392
|
-
prompt += input.query + '\n';
|
|
393
|
-
}
|
|
394
|
-
if (input.language) {
|
|
395
|
-
prompt += `\nLanguage: ${input.language}\n`;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return prompt;
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Heuristic to detect if input looks like code
|
|
402
|
-
*/
|
|
403
|
-
function looksLikeCode(str) {
|
|
404
|
-
const codeIndicators = [
|
|
405
|
-
/^(def|function|class|const|let|var|import|export|public|private)\s/,
|
|
406
|
-
/[{};()]/,
|
|
407
|
-
/=>/,
|
|
408
|
-
/\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
|
|
409
|
-
];
|
|
410
|
-
return codeIndicators.some(re => re.test(str));
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Create a new streaming session
|
|
414
|
-
*/
|
|
415
|
-
function createStreamingSession(moduleName) {
|
|
416
|
-
return {
|
|
417
|
-
session_id: `sess_${randomUUID().slice(0, 12)}`,
|
|
418
|
-
module_name: moduleName,
|
|
419
|
-
started_at: Date.now(),
|
|
420
|
-
chunks_sent: 0,
|
|
421
|
-
accumulated_data: {},
|
|
422
|
-
accumulated_text: {}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Create meta chunk (initial streaming response)
|
|
427
|
-
*/
|
|
428
|
-
function createMetaChunk(session, meta) {
|
|
429
|
-
return {
|
|
430
|
-
ok: true,
|
|
431
|
-
streaming: true,
|
|
432
|
-
session_id: session.session_id,
|
|
433
|
-
meta
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Create delta chunk (incremental content)
|
|
438
|
-
* Note: Delta chunks don't include session_id per v2.5 spec
|
|
439
|
-
*/
|
|
440
|
-
function createDeltaChunk(session, field, delta) {
|
|
441
|
-
session.chunks_sent++;
|
|
442
|
-
return {
|
|
443
|
-
chunk: {
|
|
444
|
-
seq: session.chunks_sent,
|
|
445
|
-
type: 'delta',
|
|
446
|
-
field,
|
|
447
|
-
delta
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Create progress chunk
|
|
453
|
-
* Note: Progress chunks don't include session_id per v2.5 spec
|
|
454
|
-
*/
|
|
455
|
-
function createProgressChunk(_session, percent, stage, message) {
|
|
456
|
-
return {
|
|
457
|
-
progress: {
|
|
458
|
-
percent,
|
|
459
|
-
stage,
|
|
460
|
-
message
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Create final chunk (completion signal)
|
|
466
|
-
* Note: Final chunks don't include session_id per v2.5 spec
|
|
467
|
-
*/
|
|
468
|
-
function createFinalChunk(_session, meta, data, usage) {
|
|
469
|
-
return {
|
|
470
|
-
final: true,
|
|
471
|
-
meta,
|
|
472
|
-
data,
|
|
473
|
-
usage
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Create error chunk
|
|
478
|
-
*/
|
|
479
|
-
function createErrorChunk(session, code, message, recoverable = false, partialData) {
|
|
480
|
-
return {
|
|
481
|
-
ok: false,
|
|
482
|
-
streaming: true,
|
|
483
|
-
session_id: session.session_id,
|
|
484
|
-
error: {
|
|
485
|
-
code,
|
|
486
|
-
message,
|
|
487
|
-
recoverable
|
|
488
|
-
},
|
|
489
|
-
partial_data: partialData
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Run module with streaming response
|
|
494
|
-
*
|
|
495
|
-
* @param module - The cognitive module to execute
|
|
496
|
-
* @param provider - The LLM provider
|
|
497
|
-
* @param options - Run options including streaming callbacks
|
|
498
|
-
* @yields Streaming chunks
|
|
499
|
-
*/
|
|
500
|
-
export async function* runModuleStream(module, provider, options = {}) {
|
|
501
|
-
const { onChunk, onProgress, heartbeatInterval = 15000, maxDuration = 300000, ...runOptions } = options;
|
|
502
|
-
// Create streaming session
|
|
503
|
-
const session = createStreamingSession(module.name);
|
|
504
|
-
const startTime = Date.now();
|
|
505
|
-
// Check if module supports streaming
|
|
506
|
-
if (!moduleSupportsStreaming(module)) {
|
|
507
|
-
// Fallback to sync execution
|
|
508
|
-
const result = await runModule(module, provider, runOptions);
|
|
509
|
-
// Emit as single final chunk
|
|
510
|
-
if (result.ok && 'meta' in result) {
|
|
511
|
-
const finalChunk = createFinalChunk(session, result.meta, result.data);
|
|
512
|
-
yield finalChunk;
|
|
513
|
-
onChunk?.(finalChunk);
|
|
514
|
-
return result;
|
|
515
|
-
}
|
|
516
|
-
return result;
|
|
517
|
-
}
|
|
518
|
-
// Check if provider supports streaming
|
|
519
|
-
if (!isProviderV25(provider) || !provider.supportsStreaming?.()) {
|
|
520
|
-
// Fallback to sync with warning
|
|
521
|
-
console.warn('[cognitive] Provider does not support streaming, falling back to sync');
|
|
522
|
-
const result = await runModule(module, provider, runOptions);
|
|
523
|
-
if (result.ok && 'meta' in result) {
|
|
524
|
-
const finalChunk = createFinalChunk(session, result.meta, result.data);
|
|
525
|
-
yield finalChunk;
|
|
526
|
-
onChunk?.(finalChunk);
|
|
527
|
-
}
|
|
528
|
-
return result;
|
|
529
|
-
}
|
|
530
|
-
// Emit initial meta chunk
|
|
531
|
-
const metaChunk = createMetaChunk(session, {
|
|
532
|
-
confidence: undefined,
|
|
533
|
-
risk: 'low',
|
|
534
|
-
explain: 'Processing...'
|
|
535
|
-
});
|
|
536
|
-
yield metaChunk;
|
|
537
|
-
onChunk?.(metaChunk);
|
|
538
|
-
// Build prompt and messages (same as sync)
|
|
539
|
-
const { input, args, verbose = false, useEnvelope, useV22 } = runOptions;
|
|
540
|
-
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
541
|
-
const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
|
|
542
|
-
const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
|
|
543
|
-
const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
544
|
-
const inputData = input || {};
|
|
545
|
-
if (args && !inputData.code && !inputData.query) {
|
|
546
|
-
if (looksLikeCode(args)) {
|
|
547
|
-
inputData.code = args;
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
inputData.query = args;
|
|
976
|
+
systemParts.push('', 'RESPONSE FORMAT (Envelope):');
|
|
977
|
+
systemParts.push('- Wrap your response in the envelope format');
|
|
978
|
+
systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
|
|
979
|
+
systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
980
|
+
systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
|
|
981
|
+
}
|
|
982
|
+
if (module.output?.require_behavior_equivalence) {
|
|
983
|
+
systemParts.push('- Include "behavior_equivalence" (boolean) in data');
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
systemParts.push('', 'OUTPUT FORMAT:');
|
|
988
|
+
systemParts.push('- Respond with ONLY valid JSON');
|
|
989
|
+
systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
|
|
990
|
+
if (module.output?.require_behavior_equivalence) {
|
|
991
|
+
systemParts.push('- Include "behavior_equivalence" (boolean) field');
|
|
551
992
|
}
|
|
552
993
|
}
|
|
553
|
-
// Extract media from input
|
|
554
|
-
const mediaInputs = extractMediaInputs(inputData);
|
|
555
|
-
// Build prompt with media placeholders
|
|
556
|
-
const prompt = buildPromptWithMedia(module, inputData, mediaInputs);
|
|
557
|
-
// Build system message
|
|
558
|
-
const systemParts = buildSystemMessage(module, shouldUseEnvelope, shouldUseV22);
|
|
559
994
|
const messages = [
|
|
560
995
|
{ role: 'system', content: systemParts.join('\n') },
|
|
561
996
|
{ role: 'user', content: prompt },
|
|
562
997
|
];
|
|
563
998
|
try {
|
|
564
|
-
//
|
|
565
|
-
const
|
|
999
|
+
// Invoke provider
|
|
1000
|
+
const result = await provider.invoke({
|
|
566
1001
|
messages,
|
|
567
1002
|
jsonSchema: module.outputSchema,
|
|
568
1003
|
temperature: 0.3,
|
|
569
|
-
stream: true,
|
|
570
|
-
images: mediaInputs.images,
|
|
571
|
-
audio: mediaInputs.audio,
|
|
572
|
-
video: mediaInputs.video
|
|
573
1004
|
});
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// Check timeout
|
|
579
|
-
if (Date.now() - startTime > maxDuration) {
|
|
580
|
-
const errorChunk = createErrorChunk(session, ErrorCodesV25.STREAM_TIMEOUT, `Stream exceeded max duration of ${maxDuration}ms`, false, { partial_content: accumulatedContent });
|
|
581
|
-
yield errorChunk;
|
|
582
|
-
onChunk?.(errorChunk);
|
|
583
|
-
return undefined;
|
|
584
|
-
}
|
|
585
|
-
// Accumulate content
|
|
586
|
-
accumulatedContent += chunk;
|
|
587
|
-
// Emit delta chunk
|
|
588
|
-
const deltaChunk = createDeltaChunk(session, 'data.rationale', chunk);
|
|
589
|
-
yield deltaChunk;
|
|
590
|
-
onChunk?.(deltaChunk);
|
|
591
|
-
// Emit progress periodically
|
|
592
|
-
const now = Date.now();
|
|
593
|
-
if (now - lastProgressTime > 1000) {
|
|
594
|
-
const elapsed = now - startTime;
|
|
595
|
-
const estimatedPercent = Math.min(90, Math.floor(elapsed / maxDuration * 100));
|
|
596
|
-
const progressChunk = createProgressChunk(session, estimatedPercent, 'generating', 'Generating response...');
|
|
597
|
-
yield progressChunk;
|
|
598
|
-
onProgress?.(estimatedPercent, 'Generating response...');
|
|
599
|
-
lastProgressTime = now;
|
|
600
|
-
}
|
|
1005
|
+
if (verbose) {
|
|
1006
|
+
console.error('--- Response ---');
|
|
1007
|
+
console.error(result.content);
|
|
1008
|
+
console.error('--- End Response ---');
|
|
601
1009
|
}
|
|
602
|
-
//
|
|
1010
|
+
// Calculate latency
|
|
1011
|
+
const latencyMs = Date.now() - startTime;
|
|
1012
|
+
// Parse response
|
|
603
1013
|
let parsed;
|
|
604
1014
|
try {
|
|
605
|
-
const jsonMatch =
|
|
606
|
-
const jsonStr = jsonMatch ? jsonMatch[1] :
|
|
1015
|
+
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1016
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
607
1017
|
parsed = JSON.parse(jsonStr.trim());
|
|
608
1018
|
}
|
|
609
|
-
catch {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
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);
|
|
1135
|
+
return response;
|
|
1136
|
+
}
|
|
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 = {}) {
|
|
1176
|
+
return {
|
|
1177
|
+
type,
|
|
1178
|
+
timestamp_ms: Date.now() - startTime,
|
|
1179
|
+
module_name: moduleName,
|
|
1180
|
+
...extra,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
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;
|
|
628
1191
|
}
|
|
629
1192
|
else {
|
|
630
|
-
|
|
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;
|
|
631
1210
|
}
|
|
632
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
|
+
}
|
|
633
1266
|
else {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
};
|
|
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;
|
|
647
1279
|
}
|
|
1280
|
+
// Emit meta event early
|
|
1281
|
+
yield makeEvent('meta', { meta: response.meta });
|
|
648
1282
|
}
|
|
649
|
-
//
|
|
650
|
-
if (
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
}
|
|
659
1333
|
}
|
|
660
|
-
|
|
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 });
|
|
661
1340
|
}
|
|
662
|
-
catch (
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 });
|
|
667
1351
|
}
|
|
668
1352
|
}
|
|
669
|
-
//
|
|
670
|
-
|
|
1353
|
+
// =============================================================================
|
|
1354
|
+
// Helper Functions
|
|
1355
|
+
// =============================================================================
|
|
1356
|
+
/**
|
|
1357
|
+
* Check if response is in envelope format
|
|
1358
|
+
*/
|
|
1359
|
+
function isEnvelopeResponse(obj) {
|
|
1360
|
+
if (typeof obj !== 'object' || obj === null)
|
|
1361
|
+
return false;
|
|
1362
|
+
const o = obj;
|
|
1363
|
+
return typeof o.ok === 'boolean';
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Parse envelope format response (supports both v2.1 and v2.2)
|
|
1367
|
+
*/
|
|
1368
|
+
function parseEnvelopeResponse(response, raw) {
|
|
1369
|
+
// Check if v2.2 format (has meta)
|
|
671
1370
|
if (isV22Envelope(response)) {
|
|
672
1371
|
if (response.ok) {
|
|
673
1372
|
return {
|
|
@@ -687,6 +1386,7 @@ function parseEnvelopeResponseLocal(response, raw) {
|
|
|
687
1386
|
};
|
|
688
1387
|
}
|
|
689
1388
|
}
|
|
1389
|
+
// v2.1 format
|
|
690
1390
|
if (response.ok) {
|
|
691
1391
|
const data = (response.data ?? {});
|
|
692
1392
|
return {
|
|
@@ -709,45 +1409,17 @@ function parseEnvelopeResponseLocal(response, raw) {
|
|
|
709
1409
|
};
|
|
710
1410
|
}
|
|
711
1411
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (response.ok) {
|
|
717
|
-
const data = (response.data ?? {});
|
|
718
|
-
const confidence = data.confidence ?? 0.5;
|
|
719
|
-
const rationale = data.rationale ?? '';
|
|
720
|
-
return {
|
|
721
|
-
ok: true,
|
|
722
|
-
meta: {
|
|
723
|
-
confidence,
|
|
724
|
-
risk: aggregateRisk(data, riskRule),
|
|
725
|
-
explain: rationale.slice(0, 280) || 'No explanation provided'
|
|
726
|
-
},
|
|
727
|
-
data: data
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
732
|
-
return {
|
|
733
|
-
ok: false,
|
|
734
|
-
meta: {
|
|
735
|
-
confidence: 0,
|
|
736
|
-
risk: 'high',
|
|
737
|
-
explain: errorMsg.slice(0, 280)
|
|
738
|
-
},
|
|
739
|
-
error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
|
|
740
|
-
partial_data: response.partial_data
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function parseLegacyResponseLocal(output, raw) {
|
|
1412
|
+
/**
|
|
1413
|
+
* Parse legacy (non-envelope) format response
|
|
1414
|
+
*/
|
|
1415
|
+
function parseLegacyResponse(output, raw) {
|
|
745
1416
|
const outputObj = output;
|
|
746
1417
|
const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
|
|
747
1418
|
const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
|
|
748
1419
|
const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
|
|
749
1420
|
? outputObj.behavior_equivalence
|
|
750
1421
|
: undefined;
|
|
1422
|
+
// Check if this is an error response (has error.code)
|
|
751
1423
|
if (outputObj.error && typeof outputObj.error === 'object') {
|
|
752
1424
|
const errorObj = outputObj.error;
|
|
753
1425
|
if (typeof errorObj.code === 'string') {
|
|
@@ -761,6 +1433,7 @@ function parseLegacyResponseLocal(output, raw) {
|
|
|
761
1433
|
};
|
|
762
1434
|
}
|
|
763
1435
|
}
|
|
1436
|
+
// Return as v2.1 format (data includes confidence)
|
|
764
1437
|
return {
|
|
765
1438
|
ok: true,
|
|
766
1439
|
data: {
|
|
@@ -773,237 +1446,119 @@ function parseLegacyResponseLocal(output, raw) {
|
|
|
773
1446
|
};
|
|
774
1447
|
}
|
|
775
1448
|
/**
|
|
776
|
-
*
|
|
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)
|
|
777
1456
|
*/
|
|
778
|
-
function
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
for (const img of input.images) {
|
|
785
|
-
if (isValidMediaInput(img)) {
|
|
786
|
-
images.push(img);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
1457
|
+
function buildPrompt(module, input) {
|
|
1458
|
+
let prompt = module.prompt;
|
|
1459
|
+
// v2 style: substitute ${variable} placeholders
|
|
1460
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1461
|
+
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1462
|
+
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
789
1463
|
}
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1464
|
+
// v1 compatibility: get args value
|
|
1465
|
+
const argsValue = input.code || input.query || '';
|
|
1466
|
+
// Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
|
|
1467
|
+
// Process in descending order to avoid $1 replacing part of $10
|
|
1468
|
+
if (typeof argsValue === 'string') {
|
|
1469
|
+
const argsList = argsValue.split(/\s+/);
|
|
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
|
|
1475
|
+
prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
|
|
796
1476
|
}
|
|
797
1477
|
}
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1478
|
+
// Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
|
|
1479
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
1480
|
+
// Append input summary if not already in prompt
|
|
1481
|
+
if (!prompt.includes(argsValue) && argsValue) {
|
|
1482
|
+
prompt += '\n\n## Input\n\n';
|
|
1483
|
+
if (input.code) {
|
|
1484
|
+
prompt += '```\n' + input.code + '\n```\n';
|
|
1485
|
+
}
|
|
1486
|
+
if (input.query) {
|
|
1487
|
+
prompt += input.query + '\n';
|
|
1488
|
+
}
|
|
1489
|
+
if (input.language) {
|
|
1490
|
+
prompt += `\nLanguage: ${input.language}\n`;
|
|
804
1491
|
}
|
|
805
|
-
}
|
|
806
|
-
return { images, audio, video };
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Validate media input structure
|
|
810
|
-
*/
|
|
811
|
-
function isValidMediaInput(input) {
|
|
812
|
-
if (typeof input !== 'object' || input === null)
|
|
813
|
-
return false;
|
|
814
|
-
const obj = input;
|
|
815
|
-
if (obj.type === 'url' && typeof obj.url === 'string')
|
|
816
|
-
return true;
|
|
817
|
-
if (obj.type === 'base64' && typeof obj.data === 'string' && typeof obj.media_type === 'string')
|
|
818
|
-
return true;
|
|
819
|
-
if (obj.type === 'file' && typeof obj.path === 'string')
|
|
820
|
-
return true;
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Build prompt with media placeholders
|
|
825
|
-
*/
|
|
826
|
-
function buildPromptWithMedia(module, input, media) {
|
|
827
|
-
let prompt = buildPrompt(module, input);
|
|
828
|
-
// Replace $MEDIA_INPUTS placeholder
|
|
829
|
-
if (prompt.includes('$MEDIA_INPUTS')) {
|
|
830
|
-
const mediaSummary = buildMediaSummary(media);
|
|
831
|
-
prompt = prompt.replace(/\$MEDIA_INPUTS/g, mediaSummary);
|
|
832
1492
|
}
|
|
833
1493
|
return prompt;
|
|
834
1494
|
}
|
|
835
1495
|
/**
|
|
836
|
-
*
|
|
1496
|
+
* Heuristic to detect if input looks like code
|
|
837
1497
|
*/
|
|
838
|
-
function
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (media.video.length > 0) {
|
|
847
|
-
parts.push(`[${media.video.length} video file(s) attached]`);
|
|
848
|
-
}
|
|
849
|
-
return parts.length > 0 ? parts.join('\n') : '[No media attached]';
|
|
1498
|
+
function looksLikeCode(str) {
|
|
1499
|
+
const codeIndicators = [
|
|
1500
|
+
/^(def|function|class|const|let|var|import|export|public|private)\s/,
|
|
1501
|
+
/[{};()]/,
|
|
1502
|
+
/=>/,
|
|
1503
|
+
/\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
|
|
1504
|
+
];
|
|
1505
|
+
return codeIndicators.some(re => re.test(str));
|
|
850
1506
|
}
|
|
851
1507
|
/**
|
|
852
|
-
*
|
|
1508
|
+
* Run a cognitive module (legacy API, returns raw output).
|
|
1509
|
+
* For backward compatibility. Throws on error instead of returning error envelope.
|
|
853
1510
|
*/
|
|
854
|
-
function
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
if (module.constraints.no_network)
|
|
867
|
-
systemParts.push('- No network access');
|
|
868
|
-
if (module.constraints.no_side_effects)
|
|
869
|
-
systemParts.push('- No side effects');
|
|
870
|
-
if (module.constraints.no_file_write)
|
|
871
|
-
systemParts.push('- No file writes');
|
|
872
|
-
if (module.constraints.no_inventing_data)
|
|
873
|
-
systemParts.push('- Do not invent data');
|
|
874
|
-
}
|
|
875
|
-
if (module.output?.require_behavior_equivalence) {
|
|
876
|
-
systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
|
|
877
|
-
systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
|
|
878
|
-
systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
|
|
879
|
-
const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
|
|
880
|
-
systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
|
|
881
|
-
}
|
|
882
|
-
// Add multimodal instructions if module supports it
|
|
883
|
-
if (isModuleV25(module) && moduleSupportsMultimodal(module)) {
|
|
884
|
-
const inputModalities = getModuleInputModalities(module);
|
|
885
|
-
systemParts.push('', 'MULTIMODAL INPUT:');
|
|
886
|
-
systemParts.push(`- This module accepts: ${inputModalities.join(', ')}`);
|
|
887
|
-
systemParts.push('- Analyze any attached media carefully');
|
|
888
|
-
systemParts.push('- Reference specific elements from the media in your analysis');
|
|
889
|
-
}
|
|
890
|
-
// Add envelope format instructions
|
|
891
|
-
if (shouldUseEnvelope) {
|
|
892
|
-
if (shouldUseV22) {
|
|
893
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
|
|
894
|
-
systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
|
|
895
|
-
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
896
|
-
systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
897
|
-
systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
|
|
898
|
-
systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
|
|
899
|
-
}
|
|
900
|
-
else {
|
|
901
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope):');
|
|
902
|
-
systemParts.push('- Wrap your response in the envelope format');
|
|
903
|
-
systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
|
|
904
|
-
systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
905
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
|
|
906
|
-
}
|
|
907
|
-
if (module.output?.require_behavior_equivalence) {
|
|
908
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) in data');
|
|
909
|
-
}
|
|
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;
|
|
910
1523
|
}
|
|
911
1524
|
else {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
|
|
915
|
-
if (module.output?.require_behavior_equivalence) {
|
|
916
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) field');
|
|
917
|
-
}
|
|
1525
|
+
const error = 'error' in result ? result.error : { code: 'UNKNOWN', message: 'Unknown error' };
|
|
1526
|
+
throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
|
|
918
1527
|
}
|
|
919
|
-
return systemParts;
|
|
920
1528
|
}
|
|
1529
|
+
// =============================================================================
|
|
1530
|
+
// Convenience Functions
|
|
1531
|
+
// =============================================================================
|
|
921
1532
|
/**
|
|
922
|
-
*
|
|
1533
|
+
* Extract meta from v2.2 envelope for routing/logging.
|
|
923
1534
|
*/
|
|
924
|
-
export
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const buffer = await readFile(path);
|
|
930
|
-
const data = buffer.toString('base64');
|
|
931
|
-
const media_type = getMediaTypeFromExtension(extname(path));
|
|
932
|
-
return { data, media_type };
|
|
933
|
-
}
|
|
934
|
-
catch {
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Get MIME type from file extension
|
|
940
|
-
*/
|
|
941
|
-
function getMediaTypeFromExtension(ext) {
|
|
942
|
-
const mimeTypes = {
|
|
943
|
-
'.jpg': 'image/jpeg',
|
|
944
|
-
'.jpeg': 'image/jpeg',
|
|
945
|
-
'.png': 'image/png',
|
|
946
|
-
'.gif': 'image/gif',
|
|
947
|
-
'.webp': 'image/webp',
|
|
948
|
-
'.mp3': 'audio/mpeg',
|
|
949
|
-
'.wav': 'audio/wav',
|
|
950
|
-
'.ogg': 'audio/ogg',
|
|
951
|
-
'.webm': 'audio/webm',
|
|
952
|
-
'.mp4': 'video/mp4',
|
|
953
|
-
'.mov': 'video/quicktime',
|
|
954
|
-
'.pdf': 'application/pdf'
|
|
1535
|
+
export function extractMeta(result) {
|
|
1536
|
+
return result.meta ?? {
|
|
1537
|
+
confidence: 0.5,
|
|
1538
|
+
risk: 'medium',
|
|
1539
|
+
explain: 'No meta available',
|
|
955
1540
|
};
|
|
956
|
-
return mimeTypes[ext.toLowerCase()] ?? 'application/octet-stream';
|
|
957
1541
|
}
|
|
1542
|
+
// Alias for backward compatibility
|
|
1543
|
+
export const extractMetaV22 = extractMeta;
|
|
958
1544
|
/**
|
|
959
|
-
*
|
|
1545
|
+
* Determine if result should be escalated to human review based on meta.
|
|
960
1546
|
*/
|
|
961
|
-
export function
|
|
962
|
-
|
|
963
|
-
if
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
error: 'Module does not support multimodal input',
|
|
967
|
-
code: ErrorCodesV25.MULTIMODAL_NOT_SUPPORTED
|
|
968
|
-
};
|
|
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;
|
|
969
1552
|
}
|
|
970
|
-
//
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
if (!isValidMediaType(mediaType)) {
|
|
974
|
-
return {
|
|
975
|
-
valid: false,
|
|
976
|
-
error: `Unsupported media type: ${mediaType}`,
|
|
977
|
-
code: ErrorCodesV25.UNSUPPORTED_MEDIA_TYPE
|
|
978
|
-
};
|
|
979
|
-
}
|
|
1553
|
+
// Escalate if high risk
|
|
1554
|
+
if (meta.risk === 'high') {
|
|
1555
|
+
return true;
|
|
980
1556
|
}
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
* Check if media type is supported
|
|
987
|
-
*/
|
|
988
|
-
function isValidMediaType(mediaType) {
|
|
989
|
-
const supported = [
|
|
990
|
-
'image/jpeg', 'image/png', 'image/webp', 'image/gif',
|
|
991
|
-
'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm',
|
|
992
|
-
'video/mp4', 'video/webm', 'video/quicktime',
|
|
993
|
-
'application/pdf'
|
|
994
|
-
];
|
|
995
|
-
return supported.includes(mediaType);
|
|
996
|
-
}
|
|
997
|
-
/**
|
|
998
|
-
* Get runtime capabilities
|
|
999
|
-
*/
|
|
1000
|
-
export function getRuntimeCapabilities() {
|
|
1001
|
-
return { ...DEFAULT_RUNTIME_CAPABILITIES };
|
|
1002
|
-
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Check if runtime supports a specific modality
|
|
1005
|
-
*/
|
|
1006
|
-
export function runtimeSupportsModality(modality, direction = 'input') {
|
|
1007
|
-
const caps = getRuntimeCapabilities();
|
|
1008
|
-
return caps.multimodal[direction].includes(modality);
|
|
1557
|
+
// Escalate if error
|
|
1558
|
+
if (!result.ok) {
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1009
1562
|
}
|
|
1563
|
+
// Alias for backward compatibility
|
|
1564
|
+
export const shouldEscalateV22 = shouldEscalate;
|