cognitive-modules-cli 2.2.1 → 2.2.7
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 +519 -23
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +383 -16
- package/dist/commands/compose.js +60 -23
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -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 +2 -0
- package/dist/commands/run.js +61 -28
- 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 +225 -0
- package/dist/errors/index.js +420 -0
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.js +97 -32
- package/dist/modules/loader.js +4 -2
- package/dist/modules/runner.d.ts +72 -5
- package/dist/modules/runner.js +306 -59
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +18 -13
- package/dist/modules/validator.js +14 -6
- 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 +212 -0
- package/dist/registry/client.js +359 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/tar.d.ts +8 -0
- package/dist/registry/tar.js +353 -0
- package/dist/server/http.js +301 -45
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sse.d.ts +13 -0
- package/dist/server/sse.js +22 -0
- package/dist/types.d.ts +32 -1
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +31 -7
- package/dist/modules/composition.test.d.ts +0 -11
- package/dist/modules/composition.test.js +0 -450
- package/dist/modules/policy.test.d.ts +0 -10
- package/dist/modules/policy.test.js +0 -369
- package/src/cli.ts +0 -471
- package/src/commands/add.ts +0 -315
- package/src/commands/compose.ts +0 -185
- package/src/commands/index.ts +0 -13
- 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 -90
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/composition.test.ts +0 -558
- package/src/modules/composition.ts +0 -1674
- package/src/modules/index.ts +0 -9
- package/src/modules/loader.ts +0 -508
- package/src/modules/policy.test.ts +0 -455
- package/src/modules/runner.ts +0 -1983
- package/src/modules/subagent.ts +0 -277
- package/src/modules/validator.ts +0 -700
- 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 -599
- package/tsconfig.json +0 -17
package/src/modules/runner.ts
DELETED
|
@@ -1,1983 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module Runner - Execute Cognitive Modules
|
|
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
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import _Ajv from 'ajv';
|
|
8
|
-
const Ajv = _Ajv.default || _Ajv;
|
|
9
|
-
import type {
|
|
10
|
-
Provider,
|
|
11
|
-
CognitiveModule,
|
|
12
|
-
ModuleResult,
|
|
13
|
-
ModuleResultV21,
|
|
14
|
-
ModuleResultV22,
|
|
15
|
-
Message,
|
|
16
|
-
ModuleInput,
|
|
17
|
-
EnvelopeResponse,
|
|
18
|
-
EnvelopeResponseV22,
|
|
19
|
-
EnvelopeMeta,
|
|
20
|
-
ModuleResultData,
|
|
21
|
-
RiskLevel,
|
|
22
|
-
RiskRule
|
|
23
|
-
} from '../types.js';
|
|
24
|
-
import { aggregateRisk, isV22Envelope } from '../types.js';
|
|
25
|
-
|
|
26
|
-
// =============================================================================
|
|
27
|
-
// Schema Validation
|
|
28
|
-
// =============================================================================
|
|
29
|
-
|
|
30
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Validate data against JSON schema. Returns list of errors.
|
|
34
|
-
*/
|
|
35
|
-
export function validateData(data: unknown, schema: object, label: string = 'Data'): string[] {
|
|
36
|
-
const errors: string[] = [];
|
|
37
|
-
if (!schema || Object.keys(schema).length === 0) {
|
|
38
|
-
return errors;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const validate = ajv.compile(schema);
|
|
43
|
-
const valid = validate(data);
|
|
44
|
-
|
|
45
|
-
if (!valid && validate.errors) {
|
|
46
|
-
for (const err of validate.errors) {
|
|
47
|
-
const path = err.instancePath || '/';
|
|
48
|
-
errors.push(`${label} validation error: ${err.message} at ${path}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} catch (e) {
|
|
52
|
-
errors.push(`Schema error: ${(e as Error).message}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return errors;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// =============================================================================
|
|
59
|
-
// v2.2 Policy Enforcement
|
|
60
|
-
// =============================================================================
|
|
61
|
-
|
|
62
|
-
/** Action types that can be checked against policies */
|
|
63
|
-
export type PolicyAction = 'network' | 'filesystem_write' | 'side_effects' | 'code_execution';
|
|
64
|
-
|
|
65
|
-
/** Tool categories for automatic policy mapping */
|
|
66
|
-
const TOOL_POLICY_MAPPING: Record<string, PolicyAction[]> = {
|
|
67
|
-
// Network tools
|
|
68
|
-
'fetch': ['network'],
|
|
69
|
-
'http': ['network'],
|
|
70
|
-
'request': ['network'],
|
|
71
|
-
'curl': ['network'],
|
|
72
|
-
'wget': ['network'],
|
|
73
|
-
'api_call': ['network'],
|
|
74
|
-
|
|
75
|
-
// Filesystem tools
|
|
76
|
-
'write_file': ['filesystem_write', 'side_effects'],
|
|
77
|
-
'create_file': ['filesystem_write', 'side_effects'],
|
|
78
|
-
'delete_file': ['filesystem_write', 'side_effects'],
|
|
79
|
-
'rename_file': ['filesystem_write', 'side_effects'],
|
|
80
|
-
'mkdir': ['filesystem_write', 'side_effects'],
|
|
81
|
-
'rmdir': ['filesystem_write', 'side_effects'],
|
|
82
|
-
|
|
83
|
-
// Code execution tools
|
|
84
|
-
'shell': ['code_execution', 'side_effects'],
|
|
85
|
-
'exec': ['code_execution', 'side_effects'],
|
|
86
|
-
'run_code': ['code_execution', 'side_effects'],
|
|
87
|
-
'code_interpreter': ['code_execution', 'side_effects'],
|
|
88
|
-
'eval': ['code_execution', 'side_effects'],
|
|
89
|
-
|
|
90
|
-
// Database tools
|
|
91
|
-
'sql_query': ['side_effects'],
|
|
92
|
-
'db_write': ['side_effects'],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** Result of a policy check */
|
|
96
|
-
export interface PolicyCheckResult {
|
|
97
|
-
allowed: boolean;
|
|
98
|
-
reason?: string;
|
|
99
|
-
policy?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Check if a tool is allowed by the module's tools policy.
|
|
104
|
-
*
|
|
105
|
-
* @param toolName The name of the tool to check
|
|
106
|
-
* @param module The cognitive module config
|
|
107
|
-
* @returns PolicyCheckResult indicating if the tool is allowed
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* const result = checkToolPolicy('write_file', module);
|
|
111
|
-
* if (!result.allowed) {
|
|
112
|
-
* throw new Error(result.reason);
|
|
113
|
-
* }
|
|
114
|
-
*/
|
|
115
|
-
export function checkToolPolicy(
|
|
116
|
-
toolName: string,
|
|
117
|
-
module: CognitiveModule
|
|
118
|
-
): PolicyCheckResult {
|
|
119
|
-
const toolsPolicy = module.tools;
|
|
120
|
-
|
|
121
|
-
// No policy = allow all
|
|
122
|
-
if (!toolsPolicy) {
|
|
123
|
-
return { allowed: true };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
|
|
127
|
-
|
|
128
|
-
// Check explicit denied list first
|
|
129
|
-
if (toolsPolicy.denied?.some(d => d.toLowerCase().replace(/[-\s]/g, '_') === normalizedName)) {
|
|
130
|
-
return {
|
|
131
|
-
allowed: false,
|
|
132
|
-
reason: `Tool '${toolName}' is explicitly denied by module tools policy`,
|
|
133
|
-
policy: 'tools.denied'
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check policy mode
|
|
138
|
-
if (toolsPolicy.policy === 'deny_by_default') {
|
|
139
|
-
// In deny_by_default mode, tool must be in allowed list
|
|
140
|
-
const isAllowed = toolsPolicy.allowed?.some(
|
|
141
|
-
a => a.toLowerCase().replace(/[-\s]/g, '_') === normalizedName
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (!isAllowed) {
|
|
145
|
-
return {
|
|
146
|
-
allowed: false,
|
|
147
|
-
reason: `Tool '${toolName}' not in allowed list (policy: deny_by_default)`,
|
|
148
|
-
policy: 'tools.policy'
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { allowed: true };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Check if an action is allowed by the module's policies.
|
|
158
|
-
*
|
|
159
|
-
* @param action The action to check (network, filesystem_write, etc.)
|
|
160
|
-
* @param module The cognitive module config
|
|
161
|
-
* @returns PolicyCheckResult indicating if the action is allowed
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* const result = checkPolicy('network', module);
|
|
165
|
-
* if (!result.allowed) {
|
|
166
|
-
* throw new Error(result.reason);
|
|
167
|
-
* }
|
|
168
|
-
*/
|
|
169
|
-
export function checkPolicy(
|
|
170
|
-
action: PolicyAction,
|
|
171
|
-
module: CognitiveModule
|
|
172
|
-
): PolicyCheckResult {
|
|
173
|
-
const policies = module.policies;
|
|
174
|
-
|
|
175
|
-
// No policies = allow all
|
|
176
|
-
if (!policies) {
|
|
177
|
-
return { allowed: true };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check the specific policy
|
|
181
|
-
if (policies[action] === 'deny') {
|
|
182
|
-
return {
|
|
183
|
-
allowed: false,
|
|
184
|
-
reason: `Action '${action}' is denied by module policy`,
|
|
185
|
-
policy: `policies.${action}`
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { allowed: true };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Check if a tool is allowed considering both tools policy and general policies.
|
|
194
|
-
* This performs a comprehensive check that:
|
|
195
|
-
* 1. Checks the tools policy (allowed/denied lists)
|
|
196
|
-
* 2. Maps the tool to policy actions and checks those
|
|
197
|
-
*
|
|
198
|
-
* @param toolName The name of the tool to check
|
|
199
|
-
* @param module The cognitive module config
|
|
200
|
-
* @returns PolicyCheckResult with detailed information
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* const result = checkToolAllowed('write_file', module);
|
|
204
|
-
* if (!result.allowed) {
|
|
205
|
-
* return makeErrorResponse({
|
|
206
|
-
* code: 'POLICY_VIOLATION',
|
|
207
|
-
* message: result.reason,
|
|
208
|
-
* });
|
|
209
|
-
* }
|
|
210
|
-
*/
|
|
211
|
-
export function checkToolAllowed(
|
|
212
|
-
toolName: string,
|
|
213
|
-
module: CognitiveModule
|
|
214
|
-
): PolicyCheckResult {
|
|
215
|
-
// First check explicit tools policy
|
|
216
|
-
const toolCheck = checkToolPolicy(toolName, module);
|
|
217
|
-
if (!toolCheck.allowed) {
|
|
218
|
-
return toolCheck;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Then check mapped policies
|
|
222
|
-
const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
|
|
223
|
-
const mappedActions = TOOL_POLICY_MAPPING[normalizedName] || [];
|
|
224
|
-
|
|
225
|
-
for (const action of mappedActions) {
|
|
226
|
-
const policyCheck = checkPolicy(action, module);
|
|
227
|
-
if (!policyCheck.allowed) {
|
|
228
|
-
return {
|
|
229
|
-
allowed: false,
|
|
230
|
-
reason: `Tool '${toolName}' requires '${action}' which is denied by policy`,
|
|
231
|
-
policy: policyCheck.policy
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return { allowed: true };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Validate that a list of tools are all allowed by the module's policies.
|
|
241
|
-
* Returns all violations found.
|
|
242
|
-
*
|
|
243
|
-
* @param toolNames List of tool names to check
|
|
244
|
-
* @param module The cognitive module config
|
|
245
|
-
* @returns Array of PolicyCheckResult for denied tools
|
|
246
|
-
*/
|
|
247
|
-
export function validateToolsAllowed(
|
|
248
|
-
toolNames: string[],
|
|
249
|
-
module: CognitiveModule
|
|
250
|
-
): PolicyCheckResult[] {
|
|
251
|
-
const violations: PolicyCheckResult[] = [];
|
|
252
|
-
|
|
253
|
-
for (const toolName of toolNames) {
|
|
254
|
-
const result = checkToolAllowed(toolName, module);
|
|
255
|
-
if (!result.allowed) {
|
|
256
|
-
violations.push(result);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return violations;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Get all denied actions for a module based on its policies.
|
|
265
|
-
* Useful for informing LLM about restrictions.
|
|
266
|
-
*/
|
|
267
|
-
export function getDeniedActions(module: CognitiveModule): PolicyAction[] {
|
|
268
|
-
const denied: PolicyAction[] = [];
|
|
269
|
-
const policies = module.policies;
|
|
270
|
-
|
|
271
|
-
if (!policies) return denied;
|
|
272
|
-
|
|
273
|
-
const actions: PolicyAction[] = ['network', 'filesystem_write', 'side_effects', 'code_execution'];
|
|
274
|
-
for (const action of actions) {
|
|
275
|
-
if (policies[action] === 'deny') {
|
|
276
|
-
denied.push(action);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return denied;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get all denied tools for a module based on its tools policy.
|
|
285
|
-
*/
|
|
286
|
-
export function getDeniedTools(module: CognitiveModule): string[] {
|
|
287
|
-
return module.tools?.denied || [];
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Get all allowed tools for a module (only meaningful in deny_by_default mode).
|
|
292
|
-
*/
|
|
293
|
-
export function getAllowedTools(module: CognitiveModule): string[] | null {
|
|
294
|
-
if (module.tools?.policy === 'deny_by_default') {
|
|
295
|
-
return module.tools.allowed || [];
|
|
296
|
-
}
|
|
297
|
-
return null; // null means "all allowed except denied list"
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// =============================================================================
|
|
301
|
-
// Tool Call Interceptor
|
|
302
|
-
// =============================================================================
|
|
303
|
-
|
|
304
|
-
/** Tool call request from LLM */
|
|
305
|
-
export interface ToolCallRequest {
|
|
306
|
-
name: string;
|
|
307
|
-
arguments: Record<string, unknown>;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Tool call result */
|
|
311
|
-
export interface ToolCallResult {
|
|
312
|
-
success: boolean;
|
|
313
|
-
result?: unknown;
|
|
314
|
-
error?: {
|
|
315
|
-
code: string;
|
|
316
|
-
message: string;
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Tool executor function type */
|
|
321
|
-
export type ToolExecutor = (args: Record<string, unknown>) => Promise<unknown>;
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* ToolCallInterceptor - Intercepts and validates tool calls against module policies.
|
|
325
|
-
*
|
|
326
|
-
* Use this class to wrap tool execution with policy enforcement:
|
|
327
|
-
*
|
|
328
|
-
* @example
|
|
329
|
-
* const interceptor = new ToolCallInterceptor(module);
|
|
330
|
-
*
|
|
331
|
-
* // Register tool executors
|
|
332
|
-
* interceptor.registerTool('read_file', async (args) => {
|
|
333
|
-
* return fs.readFile(args.path as string, 'utf-8');
|
|
334
|
-
* });
|
|
335
|
-
*
|
|
336
|
-
* // Execute tool with policy check
|
|
337
|
-
* const result = await interceptor.execute({
|
|
338
|
-
* name: 'write_file',
|
|
339
|
-
* arguments: { path: '/tmp/test.txt', content: 'hello' }
|
|
340
|
-
* });
|
|
341
|
-
*
|
|
342
|
-
* if (!result.success) {
|
|
343
|
-
* console.error('Tool blocked:', result.error);
|
|
344
|
-
* }
|
|
345
|
-
*/
|
|
346
|
-
export class ToolCallInterceptor {
|
|
347
|
-
private module: CognitiveModule;
|
|
348
|
-
private tools: Map<string, ToolExecutor> = new Map();
|
|
349
|
-
private callLog: Array<{ tool: string; allowed: boolean; timestamp: number }> = [];
|
|
350
|
-
|
|
351
|
-
constructor(module: CognitiveModule) {
|
|
352
|
-
this.module = module;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Register a tool executor.
|
|
357
|
-
*/
|
|
358
|
-
registerTool(name: string, executor: ToolExecutor): void {
|
|
359
|
-
this.tools.set(name.toLowerCase(), executor);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Register multiple tools at once.
|
|
364
|
-
*/
|
|
365
|
-
registerTools(tools: Record<string, ToolExecutor>): void {
|
|
366
|
-
for (const [name, executor] of Object.entries(tools)) {
|
|
367
|
-
this.registerTool(name, executor);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Check if a tool call is allowed without executing it.
|
|
373
|
-
*/
|
|
374
|
-
checkAllowed(toolName: string): PolicyCheckResult {
|
|
375
|
-
return checkToolAllowed(toolName, this.module);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Execute a tool call with policy enforcement.
|
|
380
|
-
*
|
|
381
|
-
* @param request The tool call request
|
|
382
|
-
* @returns ToolCallResult with success/error
|
|
383
|
-
*/
|
|
384
|
-
async execute(request: ToolCallRequest): Promise<ToolCallResult> {
|
|
385
|
-
const { name, arguments: args } = request;
|
|
386
|
-
const timestamp = Date.now();
|
|
387
|
-
|
|
388
|
-
// Check policy
|
|
389
|
-
const policyResult = checkToolAllowed(name, this.module);
|
|
390
|
-
|
|
391
|
-
if (!policyResult.allowed) {
|
|
392
|
-
this.callLog.push({ tool: name, allowed: false, timestamp });
|
|
393
|
-
return {
|
|
394
|
-
success: false,
|
|
395
|
-
error: {
|
|
396
|
-
code: 'TOOL_NOT_ALLOWED',
|
|
397
|
-
message: policyResult.reason || `Tool '${name}' is not allowed`,
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Find executor
|
|
403
|
-
const executor = this.tools.get(name.toLowerCase());
|
|
404
|
-
if (!executor) {
|
|
405
|
-
return {
|
|
406
|
-
success: false,
|
|
407
|
-
error: {
|
|
408
|
-
code: 'TOOL_NOT_FOUND',
|
|
409
|
-
message: `Tool '${name}' is not registered`,
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Execute
|
|
415
|
-
try {
|
|
416
|
-
this.callLog.push({ tool: name, allowed: true, timestamp });
|
|
417
|
-
const result = await executor(args);
|
|
418
|
-
return { success: true, result };
|
|
419
|
-
} catch (e) {
|
|
420
|
-
return {
|
|
421
|
-
success: false,
|
|
422
|
-
error: {
|
|
423
|
-
code: 'TOOL_EXECUTION_ERROR',
|
|
424
|
-
message: (e as Error).message,
|
|
425
|
-
},
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Execute multiple tool calls in sequence.
|
|
432
|
-
* Stops on first policy violation.
|
|
433
|
-
*/
|
|
434
|
-
async executeMany(requests: ToolCallRequest[]): Promise<ToolCallResult[]> {
|
|
435
|
-
const results: ToolCallResult[] = [];
|
|
436
|
-
|
|
437
|
-
for (const request of requests) {
|
|
438
|
-
const result = await this.execute(request);
|
|
439
|
-
results.push(result);
|
|
440
|
-
|
|
441
|
-
// Stop on policy violation (not execution error)
|
|
442
|
-
if (!result.success && result.error?.code === 'TOOL_NOT_ALLOWED') {
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return results;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Get the call log for auditing.
|
|
452
|
-
*/
|
|
453
|
-
getCallLog(): Array<{ tool: string; allowed: boolean; timestamp: number }> {
|
|
454
|
-
return [...this.callLog];
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Get summary of denied calls.
|
|
459
|
-
*/
|
|
460
|
-
getDeniedCalls(): Array<{ tool: string; timestamp: number }> {
|
|
461
|
-
return this.callLog
|
|
462
|
-
.filter(c => !c.allowed)
|
|
463
|
-
.map(({ tool, timestamp }) => ({ tool, timestamp }));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Clear the call log.
|
|
468
|
-
*/
|
|
469
|
-
clearLog(): void {
|
|
470
|
-
this.callLog = [];
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Get policy summary for this module.
|
|
475
|
-
*/
|
|
476
|
-
getPolicySummary(): {
|
|
477
|
-
deniedActions: PolicyAction[];
|
|
478
|
-
deniedTools: string[];
|
|
479
|
-
allowedTools: string[] | null;
|
|
480
|
-
toolsPolicy: 'allow_by_default' | 'deny_by_default' | undefined;
|
|
481
|
-
} {
|
|
482
|
-
return {
|
|
483
|
-
deniedActions: getDeniedActions(this.module),
|
|
484
|
-
deniedTools: getDeniedTools(this.module),
|
|
485
|
-
allowedTools: getAllowedTools(this.module),
|
|
486
|
-
toolsPolicy: this.module.tools?.policy,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Create a policy-aware tool executor wrapper.
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* const safeExecutor = createPolicyAwareExecutor(module, 'write_file', async (args) => {
|
|
496
|
-
* return fs.writeFile(args.path, args.content);
|
|
497
|
-
* });
|
|
498
|
-
*
|
|
499
|
-
* // This will throw if write_file is denied
|
|
500
|
-
* await safeExecutor({ path: '/tmp/test.txt', content: 'hello' });
|
|
501
|
-
*/
|
|
502
|
-
export function createPolicyAwareExecutor(
|
|
503
|
-
module: CognitiveModule,
|
|
504
|
-
toolName: string,
|
|
505
|
-
executor: ToolExecutor
|
|
506
|
-
): ToolExecutor {
|
|
507
|
-
return async (args: Record<string, unknown>) => {
|
|
508
|
-
const policyResult = checkToolAllowed(toolName, module);
|
|
509
|
-
|
|
510
|
-
if (!policyResult.allowed) {
|
|
511
|
-
throw new Error(`Policy violation: ${policyResult.reason}`);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return executor(args);
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// =============================================================================
|
|
519
|
-
// v2.2 Runtime Enforcement - Overflow & Enum
|
|
520
|
-
// =============================================================================
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Validate overflow.insights against module's max_items config.
|
|
524
|
-
*
|
|
525
|
-
* @param data The response data object
|
|
526
|
-
* @param module The cognitive module config
|
|
527
|
-
* @returns Array of errors if insights exceed limit
|
|
528
|
-
*/
|
|
529
|
-
export function validateOverflowLimits(
|
|
530
|
-
data: Record<string, unknown>,
|
|
531
|
-
module: CognitiveModule
|
|
532
|
-
): string[] {
|
|
533
|
-
const errors: string[] = [];
|
|
534
|
-
|
|
535
|
-
const overflowConfig = module.overflow;
|
|
536
|
-
if (!overflowConfig?.enabled) {
|
|
537
|
-
// If overflow disabled, insights should not exist
|
|
538
|
-
const extensions = data.extensions as Record<string, unknown> | undefined;
|
|
539
|
-
if (extensions?.insights && Array.isArray(extensions.insights) && extensions.insights.length > 0) {
|
|
540
|
-
errors.push('Overflow is disabled but extensions.insights contains data');
|
|
541
|
-
}
|
|
542
|
-
return errors;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const maxItems = overflowConfig.max_items ?? 5;
|
|
546
|
-
const extensions = data.extensions as Record<string, unknown> | undefined;
|
|
547
|
-
|
|
548
|
-
if (extensions?.insights && Array.isArray(extensions.insights)) {
|
|
549
|
-
const insights = extensions.insights as unknown[];
|
|
550
|
-
|
|
551
|
-
if (insights.length > maxItems) {
|
|
552
|
-
errors.push(`overflow.max_items exceeded: ${insights.length} > ${maxItems}`);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// Check require_suggested_mapping
|
|
556
|
-
if (overflowConfig.require_suggested_mapping) {
|
|
557
|
-
for (let i = 0; i < insights.length; i++) {
|
|
558
|
-
const insight = insights[i] as Record<string, unknown>;
|
|
559
|
-
if (!insight.suggested_mapping) {
|
|
560
|
-
errors.push(`insight[${i}] missing required suggested_mapping`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return errors;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Validate enum values against module's enum strategy.
|
|
571
|
-
* For strict mode, custom enum objects are not allowed.
|
|
572
|
-
*
|
|
573
|
-
* @param data The response data object
|
|
574
|
-
* @param module The cognitive module config
|
|
575
|
-
* @returns Array of errors if enum violations found
|
|
576
|
-
*/
|
|
577
|
-
export function validateEnumStrategy(
|
|
578
|
-
data: Record<string, unknown>,
|
|
579
|
-
module: CognitiveModule
|
|
580
|
-
): string[] {
|
|
581
|
-
const errors: string[] = [];
|
|
582
|
-
|
|
583
|
-
const enumStrategy = module.enums?.strategy ?? 'strict';
|
|
584
|
-
|
|
585
|
-
if (enumStrategy === 'strict') {
|
|
586
|
-
// In strict mode, custom enum objects (with 'custom' key) are not allowed
|
|
587
|
-
const checkForCustomEnums = (obj: unknown, path: string): void => {
|
|
588
|
-
if (obj === null || obj === undefined) return;
|
|
589
|
-
|
|
590
|
-
if (Array.isArray(obj)) {
|
|
591
|
-
obj.forEach((item, i) => checkForCustomEnums(item, `${path}[${i}]`));
|
|
592
|
-
} else if (typeof obj === 'object') {
|
|
593
|
-
const record = obj as Record<string, unknown>;
|
|
594
|
-
|
|
595
|
-
// Check if this is a custom enum object
|
|
596
|
-
if ('custom' in record && 'reason' in record && Object.keys(record).length === 2) {
|
|
597
|
-
errors.push(`Custom enum not allowed in strict mode at ${path}: { custom: "${record.custom}" }`);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Recurse into nested objects
|
|
602
|
-
for (const [key, value] of Object.entries(record)) {
|
|
603
|
-
checkForCustomEnums(value, `${path}.${key}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
checkForCustomEnums(data, 'data');
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return errors;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// =============================================================================
|
|
615
|
-
// Constants
|
|
616
|
-
// =============================================================================
|
|
617
|
-
|
|
618
|
-
const ENVELOPE_VERSION = '2.2';
|
|
619
|
-
|
|
620
|
-
// =============================================================================
|
|
621
|
-
// Utility Functions
|
|
622
|
-
// =============================================================================
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Deep clone an object to avoid mutation issues.
|
|
626
|
-
* Handles nested objects, arrays, and primitive values.
|
|
627
|
-
*/
|
|
628
|
-
function deepClone<T>(obj: T): T {
|
|
629
|
-
if (obj === null || typeof obj !== 'object') {
|
|
630
|
-
return obj;
|
|
631
|
-
}
|
|
632
|
-
if (Array.isArray(obj)) {
|
|
633
|
-
return obj.map(item => deepClone(item)) as T;
|
|
634
|
-
}
|
|
635
|
-
const cloned = {} as T;
|
|
636
|
-
for (const key in obj) {
|
|
637
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
638
|
-
cloned[key] = deepClone(obj[key]);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return cloned;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// =============================================================================
|
|
645
|
-
// Observability Hooks
|
|
646
|
-
// =============================================================================
|
|
647
|
-
|
|
648
|
-
/** Hook called before module execution */
|
|
649
|
-
export type BeforeCallHook = (moduleName: string, inputData: ModuleInput, moduleConfig: CognitiveModule) => void;
|
|
650
|
-
|
|
651
|
-
/** Hook called after successful module execution */
|
|
652
|
-
export type AfterCallHook = (moduleName: string, result: EnvelopeResponseV22<unknown>, latencyMs: number) => void;
|
|
653
|
-
|
|
654
|
-
/** Hook called when an error occurs */
|
|
655
|
-
export type ErrorHook = (moduleName: string, error: Error, partialResult: unknown | null) => void;
|
|
656
|
-
|
|
657
|
-
// Global hook registries
|
|
658
|
-
const _beforeCallHooks: BeforeCallHook[] = [];
|
|
659
|
-
const _afterCallHooks: AfterCallHook[] = [];
|
|
660
|
-
const _errorHooks: ErrorHook[] = [];
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Decorator to register a before-call hook.
|
|
664
|
-
*
|
|
665
|
-
* @example
|
|
666
|
-
* onBeforeCall((moduleName, inputData, config) => {
|
|
667
|
-
* console.log(`Calling ${moduleName} with`, inputData);
|
|
668
|
-
* });
|
|
669
|
-
*/
|
|
670
|
-
export function onBeforeCall(hook: BeforeCallHook): BeforeCallHook {
|
|
671
|
-
_beforeCallHooks.push(hook);
|
|
672
|
-
return hook;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Decorator to register an after-call hook.
|
|
677
|
-
*
|
|
678
|
-
* @example
|
|
679
|
-
* onAfterCall((moduleName, result, latencyMs) => {
|
|
680
|
-
* console.log(`${moduleName} completed in ${latencyMs}ms`);
|
|
681
|
-
* });
|
|
682
|
-
*/
|
|
683
|
-
export function onAfterCall(hook: AfterCallHook): AfterCallHook {
|
|
684
|
-
_afterCallHooks.push(hook);
|
|
685
|
-
return hook;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Decorator to register an error hook.
|
|
690
|
-
*
|
|
691
|
-
* @example
|
|
692
|
-
* onError((moduleName, error, partialResult) => {
|
|
693
|
-
* console.error(`Error in ${moduleName}:`, error);
|
|
694
|
-
* });
|
|
695
|
-
*/
|
|
696
|
-
export function onError(hook: ErrorHook): ErrorHook {
|
|
697
|
-
_errorHooks.push(hook);
|
|
698
|
-
return hook;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Register a hook programmatically.
|
|
703
|
-
*/
|
|
704
|
-
export function registerHook(
|
|
705
|
-
hookType: 'before_call' | 'after_call' | 'error',
|
|
706
|
-
hook: BeforeCallHook | AfterCallHook | ErrorHook
|
|
707
|
-
): void {
|
|
708
|
-
if (hookType === 'before_call') {
|
|
709
|
-
_beforeCallHooks.push(hook as BeforeCallHook);
|
|
710
|
-
} else if (hookType === 'after_call') {
|
|
711
|
-
_afterCallHooks.push(hook as AfterCallHook);
|
|
712
|
-
} else if (hookType === 'error') {
|
|
713
|
-
_errorHooks.push(hook as ErrorHook);
|
|
714
|
-
} else {
|
|
715
|
-
throw new Error(`Unknown hook type: ${hookType}`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Unregister a hook. Returns true if found and removed.
|
|
721
|
-
*/
|
|
722
|
-
export function unregisterHook(
|
|
723
|
-
hookType: 'before_call' | 'after_call' | 'error',
|
|
724
|
-
hook: BeforeCallHook | AfterCallHook | ErrorHook
|
|
725
|
-
): boolean {
|
|
726
|
-
let hooks: unknown[];
|
|
727
|
-
if (hookType === 'before_call') {
|
|
728
|
-
hooks = _beforeCallHooks;
|
|
729
|
-
} else if (hookType === 'after_call') {
|
|
730
|
-
hooks = _afterCallHooks;
|
|
731
|
-
} else if (hookType === 'error') {
|
|
732
|
-
hooks = _errorHooks;
|
|
733
|
-
} else {
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const index = hooks.indexOf(hook);
|
|
738
|
-
if (index !== -1) {
|
|
739
|
-
hooks.splice(index, 1);
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Clear all registered hooks.
|
|
747
|
-
*/
|
|
748
|
-
export function clearHooks(): void {
|
|
749
|
-
_beforeCallHooks.length = 0;
|
|
750
|
-
_afterCallHooks.length = 0;
|
|
751
|
-
_errorHooks.length = 0;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function _invokeBeforeHooks(moduleName: string, inputData: ModuleInput, moduleConfig: CognitiveModule): void {
|
|
755
|
-
for (const hook of _beforeCallHooks) {
|
|
756
|
-
try {
|
|
757
|
-
hook(moduleName, inputData, moduleConfig);
|
|
758
|
-
} catch {
|
|
759
|
-
// Hooks should not break the main flow
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function _invokeAfterHooks(moduleName: string, result: EnvelopeResponseV22<unknown>, latencyMs: number): void {
|
|
765
|
-
for (const hook of _afterCallHooks) {
|
|
766
|
-
try {
|
|
767
|
-
hook(moduleName, result, latencyMs);
|
|
768
|
-
} catch {
|
|
769
|
-
// Hooks should not break the main flow
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function _invokeErrorHooks(moduleName: string, error: Error, partialResult: unknown | null): void {
|
|
775
|
-
for (const hook of _errorHooks) {
|
|
776
|
-
try {
|
|
777
|
-
hook(moduleName, error, partialResult);
|
|
778
|
-
} catch {
|
|
779
|
-
// Hooks should not break the main flow
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// =============================================================================
|
|
785
|
-
// Error Response Builder
|
|
786
|
-
// =============================================================================
|
|
787
|
-
|
|
788
|
-
/** Error codes and their default properties */
|
|
789
|
-
export const ERROR_PROPERTIES: Record<string, { recoverable: boolean; retry_after_ms: number | null }> = {
|
|
790
|
-
MODULE_NOT_FOUND: { recoverable: false, retry_after_ms: null },
|
|
791
|
-
INVALID_INPUT: { recoverable: false, retry_after_ms: null },
|
|
792
|
-
PARSE_ERROR: { recoverable: true, retry_after_ms: 1000 },
|
|
793
|
-
SCHEMA_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
794
|
-
META_VALIDATION_FAILED: { recoverable: true, retry_after_ms: 1000 },
|
|
795
|
-
POLICY_VIOLATION: { recoverable: false, retry_after_ms: null },
|
|
796
|
-
TOOL_NOT_ALLOWED: { recoverable: false, retry_after_ms: null },
|
|
797
|
-
LLM_ERROR: { recoverable: true, retry_after_ms: 5000 },
|
|
798
|
-
RATE_LIMITED: { recoverable: true, retry_after_ms: 10000 },
|
|
799
|
-
TIMEOUT: { recoverable: true, retry_after_ms: 5000 },
|
|
800
|
-
UNKNOWN: { recoverable: false, retry_after_ms: null },
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
export interface MakeErrorResponseOptions {
|
|
804
|
-
code: string;
|
|
805
|
-
message: string;
|
|
806
|
-
explain?: string;
|
|
807
|
-
partialData?: unknown;
|
|
808
|
-
details?: Record<string, unknown>;
|
|
809
|
-
recoverable?: boolean;
|
|
810
|
-
retryAfterMs?: number;
|
|
811
|
-
confidence?: number;
|
|
812
|
-
risk?: RiskLevel;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Build a standardized error response with enhanced taxonomy.
|
|
817
|
-
*/
|
|
818
|
-
export function makeErrorResponse(options: MakeErrorResponseOptions): EnvelopeResponseV22<unknown> {
|
|
819
|
-
const {
|
|
820
|
-
code,
|
|
821
|
-
message,
|
|
822
|
-
explain,
|
|
823
|
-
partialData,
|
|
824
|
-
details,
|
|
825
|
-
recoverable,
|
|
826
|
-
retryAfterMs,
|
|
827
|
-
confidence = 0.0,
|
|
828
|
-
risk = 'high',
|
|
829
|
-
} = options;
|
|
830
|
-
|
|
831
|
-
// Get default properties from error code
|
|
832
|
-
const defaults = ERROR_PROPERTIES[code] || ERROR_PROPERTIES.UNKNOWN;
|
|
833
|
-
|
|
834
|
-
const errorObj: {
|
|
835
|
-
code: string;
|
|
836
|
-
message: string;
|
|
837
|
-
recoverable?: boolean;
|
|
838
|
-
retry_after_ms?: number;
|
|
839
|
-
details?: Record<string, unknown>;
|
|
840
|
-
} = {
|
|
841
|
-
code,
|
|
842
|
-
message,
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
// Add recoverable flag
|
|
846
|
-
const isRecoverable = recoverable ?? defaults.recoverable;
|
|
847
|
-
if (isRecoverable !== undefined) {
|
|
848
|
-
errorObj.recoverable = isRecoverable;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Add retry suggestion
|
|
852
|
-
const retryMs = retryAfterMs ?? defaults.retry_after_ms;
|
|
853
|
-
if (retryMs !== null) {
|
|
854
|
-
errorObj.retry_after_ms = retryMs;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// Add details if provided
|
|
858
|
-
if (details) {
|
|
859
|
-
errorObj.details = details;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return {
|
|
863
|
-
ok: false,
|
|
864
|
-
version: ENVELOPE_VERSION,
|
|
865
|
-
meta: {
|
|
866
|
-
confidence,
|
|
867
|
-
risk,
|
|
868
|
-
explain: (explain || message).slice(0, 280),
|
|
869
|
-
},
|
|
870
|
-
error: errorObj,
|
|
871
|
-
partial_data: partialData,
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
export interface MakeSuccessResponseOptions {
|
|
876
|
-
data: unknown;
|
|
877
|
-
confidence: number;
|
|
878
|
-
risk: RiskLevel;
|
|
879
|
-
explain: string;
|
|
880
|
-
latencyMs?: number;
|
|
881
|
-
model?: string;
|
|
882
|
-
traceId?: string;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Build a standardized success response.
|
|
887
|
-
*/
|
|
888
|
-
export function makeSuccessResponse(options: MakeSuccessResponseOptions): EnvelopeResponseV22<unknown> {
|
|
889
|
-
const { data, confidence, risk, explain, latencyMs, model, traceId } = options;
|
|
890
|
-
|
|
891
|
-
const meta: EnvelopeMeta = {
|
|
892
|
-
confidence: Math.max(0.0, Math.min(1.0, confidence)),
|
|
893
|
-
risk,
|
|
894
|
-
explain: explain ? explain.slice(0, 280) : 'No explanation provided',
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
if (latencyMs !== undefined) {
|
|
898
|
-
meta.latency_ms = latencyMs;
|
|
899
|
-
}
|
|
900
|
-
if (model) {
|
|
901
|
-
meta.model = model;
|
|
902
|
-
}
|
|
903
|
-
if (traceId) {
|
|
904
|
-
meta.trace_id = traceId;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return {
|
|
908
|
-
ok: true,
|
|
909
|
-
version: ENVELOPE_VERSION,
|
|
910
|
-
meta,
|
|
911
|
-
data,
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// =============================================================================
|
|
916
|
-
// Run Options
|
|
917
|
-
// =============================================================================
|
|
918
|
-
|
|
919
|
-
export interface RunOptions {
|
|
920
|
-
// Clean input (v2 style)
|
|
921
|
-
input?: ModuleInput;
|
|
922
|
-
|
|
923
|
-
// Legacy CLI args (v1 compatibility) - mapped to input.code or input.query
|
|
924
|
-
args?: string;
|
|
925
|
-
|
|
926
|
-
// Runtime options
|
|
927
|
-
verbose?: boolean;
|
|
928
|
-
|
|
929
|
-
// Whether to validate input against schema (default: true)
|
|
930
|
-
validateInput?: boolean;
|
|
931
|
-
|
|
932
|
-
// Whether to validate output against schema (default: true)
|
|
933
|
-
validateOutput?: boolean;
|
|
934
|
-
|
|
935
|
-
// Force envelope format (default: auto-detect from module.output.envelope)
|
|
936
|
-
useEnvelope?: boolean;
|
|
937
|
-
|
|
938
|
-
// Force v2.2 format (default: auto-detect from module.tier)
|
|
939
|
-
useV22?: boolean;
|
|
940
|
-
|
|
941
|
-
// Enable repair pass for validation failures (default: true)
|
|
942
|
-
enableRepair?: boolean;
|
|
943
|
-
|
|
944
|
-
// Trace ID for distributed tracing
|
|
945
|
-
traceId?: string;
|
|
946
|
-
|
|
947
|
-
// Model identifier (for meta.model tracking)
|
|
948
|
-
model?: string;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// =============================================================================
|
|
952
|
-
// Repair Pass (v2.2)
|
|
953
|
-
// =============================================================================
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Attempt to repair envelope format issues without changing semantics.
|
|
957
|
-
*
|
|
958
|
-
* Repairs (mostly lossless, except explain truncation):
|
|
959
|
-
* - Missing meta fields (fill with conservative defaults)
|
|
960
|
-
* - Truncate explain if too long
|
|
961
|
-
* - Trim whitespace from string fields
|
|
962
|
-
* - Clamp confidence to [0, 1] range
|
|
963
|
-
*
|
|
964
|
-
* Does NOT repair:
|
|
965
|
-
* - Invalid enum values (treated as validation failure)
|
|
966
|
-
*
|
|
967
|
-
* Note: Returns a deep copy to avoid modifying the original data.
|
|
968
|
-
*/
|
|
969
|
-
function repairEnvelope(
|
|
970
|
-
response: Record<string, unknown>,
|
|
971
|
-
riskRule: RiskRule = 'max_changes_risk',
|
|
972
|
-
maxExplainLength: number = 280
|
|
973
|
-
): EnvelopeResponseV22<unknown> {
|
|
974
|
-
// Deep clone to avoid mutation
|
|
975
|
-
const repaired = deepClone(response);
|
|
976
|
-
|
|
977
|
-
// Ensure meta exists
|
|
978
|
-
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
979
|
-
repaired.meta = {};
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const meta = repaired.meta as Record<string, unknown>;
|
|
983
|
-
const data = (repaired.data ?? {}) as Record<string, unknown>;
|
|
984
|
-
|
|
985
|
-
// Repair confidence
|
|
986
|
-
if (typeof meta.confidence !== 'number') {
|
|
987
|
-
meta.confidence = (data.confidence as number) ?? 0.5;
|
|
988
|
-
}
|
|
989
|
-
meta.confidence = Math.max(0, Math.min(1, meta.confidence as number));
|
|
990
|
-
|
|
991
|
-
// Repair risk using configurable aggregation rule
|
|
992
|
-
if (!meta.risk) {
|
|
993
|
-
meta.risk = aggregateRisk(data, riskRule);
|
|
994
|
-
}
|
|
995
|
-
// Trim whitespace only (lossless), validate is valid RiskLevel
|
|
996
|
-
if (typeof meta.risk === 'string') {
|
|
997
|
-
const trimmedRisk = meta.risk.trim().toLowerCase();
|
|
998
|
-
const validRisks = ['none', 'low', 'medium', 'high'];
|
|
999
|
-
meta.risk = validRisks.includes(trimmedRisk) ? trimmedRisk : 'medium';
|
|
1000
|
-
} else {
|
|
1001
|
-
meta.risk = 'medium'; // Default for invalid type
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Repair explain
|
|
1005
|
-
if (typeof meta.explain !== 'string') {
|
|
1006
|
-
const rationale = data.rationale as string | undefined;
|
|
1007
|
-
meta.explain = rationale ? String(rationale).slice(0, maxExplainLength) : 'No explanation provided';
|
|
1008
|
-
}
|
|
1009
|
-
// Trim whitespace (lossless)
|
|
1010
|
-
const explainStr = meta.explain as string;
|
|
1011
|
-
meta.explain = explainStr.trim();
|
|
1012
|
-
if ((meta.explain as string).length > maxExplainLength) {
|
|
1013
|
-
meta.explain = (meta.explain as string).slice(0, maxExplainLength - 3) + '...';
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Build proper v2.2 response with version
|
|
1017
|
-
const builtMeta: EnvelopeMeta = {
|
|
1018
|
-
confidence: meta.confidence as number,
|
|
1019
|
-
risk: meta.risk as RiskLevel,
|
|
1020
|
-
explain: meta.explain as string
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
const result: EnvelopeResponseV22<unknown> = repaired.ok === false ? {
|
|
1024
|
-
ok: false,
|
|
1025
|
-
version: ENVELOPE_VERSION,
|
|
1026
|
-
meta: builtMeta,
|
|
1027
|
-
error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
1028
|
-
partial_data: repaired.partial_data
|
|
1029
|
-
} : {
|
|
1030
|
-
ok: true,
|
|
1031
|
-
version: ENVELOPE_VERSION,
|
|
1032
|
-
meta: builtMeta,
|
|
1033
|
-
data: repaired.data
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
return result;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Repair error envelope format.
|
|
1041
|
-
*
|
|
1042
|
-
* Note: Returns a deep copy to avoid modifying the original data.
|
|
1043
|
-
*/
|
|
1044
|
-
function repairErrorEnvelope(
|
|
1045
|
-
data: Record<string, unknown>,
|
|
1046
|
-
maxExplainLength: number = 280
|
|
1047
|
-
): EnvelopeResponseV22<unknown> {
|
|
1048
|
-
// Deep clone to avoid mutation
|
|
1049
|
-
const repaired = deepClone(data);
|
|
1050
|
-
|
|
1051
|
-
// Ensure meta exists for errors
|
|
1052
|
-
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
1053
|
-
repaired.meta = {};
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const meta = repaired.meta as Record<string, unknown>;
|
|
1057
|
-
|
|
1058
|
-
// Set default meta for errors
|
|
1059
|
-
if (typeof meta.confidence !== 'number') {
|
|
1060
|
-
meta.confidence = 0.0;
|
|
1061
|
-
}
|
|
1062
|
-
if (!meta.risk) {
|
|
1063
|
-
meta.risk = 'high';
|
|
1064
|
-
}
|
|
1065
|
-
if (typeof meta.explain !== 'string') {
|
|
1066
|
-
const error = (repaired.error ?? {}) as Record<string, unknown>;
|
|
1067
|
-
meta.explain = ((error.message as string) ?? 'An error occurred').slice(0, maxExplainLength);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
return {
|
|
1071
|
-
ok: false,
|
|
1072
|
-
version: ENVELOPE_VERSION,
|
|
1073
|
-
meta: {
|
|
1074
|
-
confidence: meta.confidence as number,
|
|
1075
|
-
risk: meta.risk as RiskLevel,
|
|
1076
|
-
explain: meta.explain as string,
|
|
1077
|
-
},
|
|
1078
|
-
error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
1079
|
-
partial_data: repaired.partial_data,
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Wrap v2.1 response to v2.2 format
|
|
1085
|
-
*/
|
|
1086
|
-
function wrapV21ToV22(
|
|
1087
|
-
response: EnvelopeResponse<unknown>,
|
|
1088
|
-
riskRule: RiskRule = 'max_changes_risk'
|
|
1089
|
-
): EnvelopeResponseV22<unknown> {
|
|
1090
|
-
if (isV22Envelope(response)) {
|
|
1091
|
-
// Already v2.2, but ensure version field exists
|
|
1092
|
-
if (!('version' in response) || !response.version) {
|
|
1093
|
-
return { ...deepClone(response), version: ENVELOPE_VERSION };
|
|
1094
|
-
}
|
|
1095
|
-
return response;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (response.ok) {
|
|
1099
|
-
const data = (response.data ?? {}) as Record<string, unknown>;
|
|
1100
|
-
const confidence = (data.confidence as number) ?? 0.5;
|
|
1101
|
-
const rationale = (data.rationale as string) ?? '';
|
|
1102
|
-
|
|
1103
|
-
return {
|
|
1104
|
-
ok: true,
|
|
1105
|
-
version: ENVELOPE_VERSION,
|
|
1106
|
-
meta: {
|
|
1107
|
-
confidence,
|
|
1108
|
-
risk: aggregateRisk(data, riskRule),
|
|
1109
|
-
explain: rationale.slice(0, 280) || 'No explanation provided'
|
|
1110
|
-
},
|
|
1111
|
-
data: data as ModuleResultData
|
|
1112
|
-
};
|
|
1113
|
-
} else {
|
|
1114
|
-
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
1115
|
-
return {
|
|
1116
|
-
ok: false,
|
|
1117
|
-
version: ENVELOPE_VERSION,
|
|
1118
|
-
meta: {
|
|
1119
|
-
confidence: 0,
|
|
1120
|
-
risk: 'high',
|
|
1121
|
-
explain: errorMsg.slice(0, 280)
|
|
1122
|
-
},
|
|
1123
|
-
error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
|
|
1124
|
-
partial_data: response.partial_data
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* Convert legacy format (no envelope) to v2.2 envelope.
|
|
1131
|
-
*/
|
|
1132
|
-
function convertLegacyToEnvelope(
|
|
1133
|
-
data: Record<string, unknown>,
|
|
1134
|
-
isError: boolean = false
|
|
1135
|
-
): EnvelopeResponseV22<unknown> {
|
|
1136
|
-
if (isError || 'error' in data) {
|
|
1137
|
-
const error = (data.error ?? {}) as Record<string, unknown>;
|
|
1138
|
-
const errorMsg = typeof error === 'object'
|
|
1139
|
-
? ((error.message as string) ?? String(error))
|
|
1140
|
-
: String(error);
|
|
1141
|
-
|
|
1142
|
-
return {
|
|
1143
|
-
ok: false,
|
|
1144
|
-
version: ENVELOPE_VERSION,
|
|
1145
|
-
meta: {
|
|
1146
|
-
confidence: 0.0,
|
|
1147
|
-
risk: 'high',
|
|
1148
|
-
explain: errorMsg.slice(0, 280),
|
|
1149
|
-
},
|
|
1150
|
-
error: {
|
|
1151
|
-
code: (typeof error === 'object' ? (error.code as string) : undefined) ?? 'UNKNOWN',
|
|
1152
|
-
message: errorMsg,
|
|
1153
|
-
},
|
|
1154
|
-
partial_data: undefined,
|
|
1155
|
-
};
|
|
1156
|
-
} else {
|
|
1157
|
-
const confidence = (data.confidence as number) ?? 0.5;
|
|
1158
|
-
const rationale = (data.rationale as string) ?? '';
|
|
1159
|
-
|
|
1160
|
-
return {
|
|
1161
|
-
ok: true,
|
|
1162
|
-
version: ENVELOPE_VERSION,
|
|
1163
|
-
meta: {
|
|
1164
|
-
confidence,
|
|
1165
|
-
risk: aggregateRisk(data),
|
|
1166
|
-
explain: rationale.slice(0, 280) || 'No explanation provided',
|
|
1167
|
-
},
|
|
1168
|
-
data,
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// =============================================================================
|
|
1174
|
-
// Main Runner
|
|
1175
|
-
// =============================================================================
|
|
1176
|
-
|
|
1177
|
-
export async function runModule(
|
|
1178
|
-
module: CognitiveModule,
|
|
1179
|
-
provider: Provider,
|
|
1180
|
-
options: RunOptions = {}
|
|
1181
|
-
): Promise<ModuleResult> {
|
|
1182
|
-
const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
|
|
1183
|
-
const startTime = Date.now();
|
|
1184
|
-
|
|
1185
|
-
// Determine if we should use envelope format
|
|
1186
|
-
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
1187
|
-
|
|
1188
|
-
// Determine if we should use v2.2 format
|
|
1189
|
-
const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
|
|
1190
|
-
const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
|
|
1191
|
-
|
|
1192
|
-
// Get risk_rule from module config
|
|
1193
|
-
const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
1194
|
-
|
|
1195
|
-
// Build clean input data (v2 style: no $ARGUMENTS pollution)
|
|
1196
|
-
const inputData: ModuleInput = input || {};
|
|
1197
|
-
|
|
1198
|
-
// Map legacy --args to clean input
|
|
1199
|
-
if (args && !inputData.code && !inputData.query) {
|
|
1200
|
-
// Determine if args looks like code or natural language
|
|
1201
|
-
if (looksLikeCode(args)) {
|
|
1202
|
-
inputData.code = args;
|
|
1203
|
-
} else {
|
|
1204
|
-
inputData.query = args;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// Invoke before hooks
|
|
1209
|
-
_invokeBeforeHooks(module.name, inputData, module);
|
|
1210
|
-
|
|
1211
|
-
// Validate input against schema
|
|
1212
|
-
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
1213
|
-
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
1214
|
-
if (inputErrors.length > 0) {
|
|
1215
|
-
const errorResult = makeErrorResponse({
|
|
1216
|
-
code: 'INVALID_INPUT',
|
|
1217
|
-
message: inputErrors.join('; '),
|
|
1218
|
-
explain: 'Input validation failed.',
|
|
1219
|
-
confidence: 1.0,
|
|
1220
|
-
risk: 'none',
|
|
1221
|
-
details: { validation_errors: inputErrors },
|
|
1222
|
-
});
|
|
1223
|
-
_invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
|
|
1224
|
-
return errorResult as ModuleResult;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
// Build prompt with clean substitution
|
|
1229
|
-
const prompt = buildPrompt(module, inputData);
|
|
1230
|
-
|
|
1231
|
-
if (verbose) {
|
|
1232
|
-
console.error('--- Module ---');
|
|
1233
|
-
console.error(`Name: ${module.name} (${module.format})`);
|
|
1234
|
-
console.error(`Responsibility: ${module.responsibility}`);
|
|
1235
|
-
console.error(`Envelope: ${shouldUseEnvelope}`);
|
|
1236
|
-
console.error('--- Input ---');
|
|
1237
|
-
console.error(JSON.stringify(inputData, null, 2));
|
|
1238
|
-
console.error('--- Prompt ---');
|
|
1239
|
-
console.error(prompt);
|
|
1240
|
-
console.error('--- End ---');
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// Build system message based on module config
|
|
1244
|
-
const systemParts: string[] = [
|
|
1245
|
-
`You are executing the "${module.name}" Cognitive Module.`,
|
|
1246
|
-
'',
|
|
1247
|
-
`RESPONSIBILITY: ${module.responsibility}`,
|
|
1248
|
-
];
|
|
1249
|
-
|
|
1250
|
-
if (module.excludes.length > 0) {
|
|
1251
|
-
systemParts.push('', 'YOU MUST NOT:');
|
|
1252
|
-
module.excludes.forEach(e => systemParts.push(`- ${e}`));
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (module.constraints) {
|
|
1256
|
-
systemParts.push('', 'CONSTRAINTS:');
|
|
1257
|
-
if (module.constraints.no_network) systemParts.push('- No network access');
|
|
1258
|
-
if (module.constraints.no_side_effects) systemParts.push('- No side effects');
|
|
1259
|
-
if (module.constraints.no_file_write) systemParts.push('- No file writes');
|
|
1260
|
-
if (module.constraints.no_inventing_data) systemParts.push('- Do not invent data');
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
if (module.output?.require_behavior_equivalence) {
|
|
1264
|
-
systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
|
|
1265
|
-
systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
|
|
1266
|
-
systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
|
|
1267
|
-
|
|
1268
|
-
const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
|
|
1269
|
-
systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
// Add envelope format instructions
|
|
1273
|
-
if (shouldUseEnvelope) {
|
|
1274
|
-
if (shouldUseV22) {
|
|
1275
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
|
|
1276
|
-
systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
|
|
1277
|
-
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
1278
|
-
systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
1279
|
-
systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
|
|
1280
|
-
systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
|
|
1281
|
-
} else {
|
|
1282
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope):');
|
|
1283
|
-
systemParts.push('- Wrap your response in the envelope format');
|
|
1284
|
-
systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
|
|
1285
|
-
systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
1286
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
|
|
1287
|
-
}
|
|
1288
|
-
if (module.output?.require_behavior_equivalence) {
|
|
1289
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) in data');
|
|
1290
|
-
}
|
|
1291
|
-
} else {
|
|
1292
|
-
systemParts.push('', 'OUTPUT FORMAT:');
|
|
1293
|
-
systemParts.push('- Respond with ONLY valid JSON');
|
|
1294
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
|
|
1295
|
-
if (module.output?.require_behavior_equivalence) {
|
|
1296
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) field');
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
const messages: Message[] = [
|
|
1301
|
-
{ role: 'system', content: systemParts.join('\n') },
|
|
1302
|
-
{ role: 'user', content: prompt },
|
|
1303
|
-
];
|
|
1304
|
-
|
|
1305
|
-
try {
|
|
1306
|
-
// Invoke provider
|
|
1307
|
-
const result = await provider.invoke({
|
|
1308
|
-
messages,
|
|
1309
|
-
jsonSchema: module.outputSchema,
|
|
1310
|
-
temperature: 0.3,
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
if (verbose) {
|
|
1314
|
-
console.error('--- Response ---');
|
|
1315
|
-
console.error(result.content);
|
|
1316
|
-
console.error('--- End Response ---');
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// Calculate latency
|
|
1320
|
-
const latencyMs = Date.now() - startTime;
|
|
1321
|
-
|
|
1322
|
-
// Parse response
|
|
1323
|
-
let parsed: unknown;
|
|
1324
|
-
try {
|
|
1325
|
-
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1326
|
-
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
1327
|
-
parsed = JSON.parse(jsonStr.trim());
|
|
1328
|
-
} catch (e) {
|
|
1329
|
-
const errorResult = makeErrorResponse({
|
|
1330
|
-
code: 'PARSE_ERROR',
|
|
1331
|
-
message: `Failed to parse JSON response: ${(e as Error).message}`,
|
|
1332
|
-
explain: 'Failed to parse LLM response as JSON.',
|
|
1333
|
-
details: { raw_response: result.content.substring(0, 500) },
|
|
1334
|
-
});
|
|
1335
|
-
_invokeErrorHooks(module.name, e as Error, null);
|
|
1336
|
-
return errorResult as ModuleResult;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Convert to v2.2 envelope
|
|
1340
|
-
let response: EnvelopeResponseV22<unknown>;
|
|
1341
|
-
if (isV22Envelope(parsed as EnvelopeResponse<unknown>)) {
|
|
1342
|
-
response = parsed as EnvelopeResponseV22<unknown>;
|
|
1343
|
-
} else if (isEnvelopeResponse(parsed)) {
|
|
1344
|
-
response = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
|
|
1345
|
-
} else {
|
|
1346
|
-
response = convertLegacyToEnvelope(parsed as Record<string, unknown>);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// Add version and meta fields
|
|
1350
|
-
response.version = ENVELOPE_VERSION;
|
|
1351
|
-
if (response.meta) {
|
|
1352
|
-
response.meta.latency_ms = latencyMs;
|
|
1353
|
-
if (traceId) {
|
|
1354
|
-
response.meta.trace_id = traceId;
|
|
1355
|
-
}
|
|
1356
|
-
if (modelOverride) {
|
|
1357
|
-
response.meta.model = modelOverride;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
// Validate and potentially repair output
|
|
1362
|
-
if (response.ok && validateOutput) {
|
|
1363
|
-
// Get data schema (support both "data" and "output" aliases)
|
|
1364
|
-
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1365
|
-
const metaSchema = module.metaSchema;
|
|
1366
|
-
const dataToValidate = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
|
|
1367
|
-
|
|
1368
|
-
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1369
|
-
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1370
|
-
|
|
1371
|
-
if (dataErrors.length > 0 && enableRepair) {
|
|
1372
|
-
// Attempt repair pass
|
|
1373
|
-
response = repairEnvelope(
|
|
1374
|
-
response as unknown as Record<string, unknown>,
|
|
1375
|
-
riskRule
|
|
1376
|
-
);
|
|
1377
|
-
response.version = ENVELOPE_VERSION;
|
|
1378
|
-
|
|
1379
|
-
// Re-validate after repair
|
|
1380
|
-
const repairedData = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
|
|
1381
|
-
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
if (dataErrors.length > 0) {
|
|
1385
|
-
const errorResult = makeErrorResponse({
|
|
1386
|
-
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1387
|
-
message: dataErrors.join('; '),
|
|
1388
|
-
explain: 'Schema validation failed after repair attempt.',
|
|
1389
|
-
partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
|
|
1390
|
-
details: { validation_errors: dataErrors },
|
|
1391
|
-
});
|
|
1392
|
-
_invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data);
|
|
1393
|
-
return errorResult as ModuleResult;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// v2.2: Validate overflow limits
|
|
1398
|
-
const overflowErrors = validateOverflowLimits(dataToValidate as Record<string, unknown>, module);
|
|
1399
|
-
if (overflowErrors.length > 0) {
|
|
1400
|
-
const errorResult = makeErrorResponse({
|
|
1401
|
-
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1402
|
-
message: overflowErrors.join('; '),
|
|
1403
|
-
explain: 'Overflow validation failed.',
|
|
1404
|
-
partialData: dataToValidate,
|
|
1405
|
-
details: { overflow_errors: overflowErrors },
|
|
1406
|
-
});
|
|
1407
|
-
_invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
|
|
1408
|
-
return errorResult as ModuleResult;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
// v2.2: Validate enum strategy
|
|
1412
|
-
const enumErrors = validateEnumStrategy(dataToValidate as Record<string, unknown>, module);
|
|
1413
|
-
if (enumErrors.length > 0) {
|
|
1414
|
-
const errorResult = makeErrorResponse({
|
|
1415
|
-
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1416
|
-
message: enumErrors.join('; '),
|
|
1417
|
-
explain: 'Enum strategy validation failed.',
|
|
1418
|
-
partialData: dataToValidate,
|
|
1419
|
-
details: { enum_errors: enumErrors },
|
|
1420
|
-
});
|
|
1421
|
-
_invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
|
|
1422
|
-
return errorResult as ModuleResult;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Validate meta if schema exists
|
|
1426
|
-
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1427
|
-
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1428
|
-
|
|
1429
|
-
if (metaErrors.length > 0 && enableRepair) {
|
|
1430
|
-
response = repairEnvelope(
|
|
1431
|
-
response as unknown as Record<string, unknown>,
|
|
1432
|
-
riskRule
|
|
1433
|
-
);
|
|
1434
|
-
response.version = ENVELOPE_VERSION;
|
|
1435
|
-
|
|
1436
|
-
// Re-validate meta after repair
|
|
1437
|
-
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1438
|
-
|
|
1439
|
-
if (metaErrors.length > 0) {
|
|
1440
|
-
const errorResult = makeErrorResponse({
|
|
1441
|
-
code: 'META_VALIDATION_FAILED',
|
|
1442
|
-
message: metaErrors.join('; '),
|
|
1443
|
-
explain: 'Meta schema validation failed after repair attempt.',
|
|
1444
|
-
partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
|
|
1445
|
-
details: { validation_errors: metaErrors },
|
|
1446
|
-
});
|
|
1447
|
-
_invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data);
|
|
1448
|
-
return errorResult as ModuleResult;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
} else if (enableRepair) {
|
|
1453
|
-
// Repair error envelopes to ensure they have proper meta fields
|
|
1454
|
-
response = repairErrorEnvelope(response as unknown as Record<string, unknown>);
|
|
1455
|
-
response.version = ENVELOPE_VERSION;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
// Invoke after hooks
|
|
1459
|
-
const finalLatencyMs = Date.now() - startTime;
|
|
1460
|
-
_invokeAfterHooks(module.name, response, finalLatencyMs);
|
|
1461
|
-
|
|
1462
|
-
return response as ModuleResult;
|
|
1463
|
-
|
|
1464
|
-
} catch (e) {
|
|
1465
|
-
const latencyMs = Date.now() - startTime;
|
|
1466
|
-
const errorResult = makeErrorResponse({
|
|
1467
|
-
code: 'UNKNOWN',
|
|
1468
|
-
message: (e as Error).message,
|
|
1469
|
-
explain: `Unexpected error: ${(e as Error).name}`,
|
|
1470
|
-
details: { exception_type: (e as Error).name },
|
|
1471
|
-
});
|
|
1472
|
-
if (errorResult.meta) {
|
|
1473
|
-
errorResult.meta.latency_ms = latencyMs;
|
|
1474
|
-
}
|
|
1475
|
-
_invokeErrorHooks(module.name, e as Error, null);
|
|
1476
|
-
return errorResult as ModuleResult;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// =============================================================================
|
|
1481
|
-
// Streaming Support
|
|
1482
|
-
// =============================================================================
|
|
1483
|
-
|
|
1484
|
-
/** Event types emitted during streaming execution */
|
|
1485
|
-
export type StreamEventType = 'start' | 'chunk' | 'meta' | 'complete' | 'error';
|
|
1486
|
-
|
|
1487
|
-
/** Event emitted during streaming execution */
|
|
1488
|
-
export interface StreamEvent {
|
|
1489
|
-
type: StreamEventType;
|
|
1490
|
-
timestamp_ms: number;
|
|
1491
|
-
module_name: string;
|
|
1492
|
-
chunk?: string;
|
|
1493
|
-
meta?: EnvelopeMeta;
|
|
1494
|
-
result?: EnvelopeResponseV22<unknown>;
|
|
1495
|
-
error?: { code: string; message: string };
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
export interface StreamOptions {
|
|
1499
|
-
input?: ModuleInput;
|
|
1500
|
-
args?: string;
|
|
1501
|
-
validateInput?: boolean;
|
|
1502
|
-
validateOutput?: boolean;
|
|
1503
|
-
useV22?: boolean;
|
|
1504
|
-
enableRepair?: boolean;
|
|
1505
|
-
traceId?: string;
|
|
1506
|
-
model?: string; // Model identifier for meta.model
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
/**
|
|
1510
|
-
* Run a cognitive module with streaming output.
|
|
1511
|
-
*
|
|
1512
|
-
* Yields StreamEvent objects as the module executes:
|
|
1513
|
-
* - type="start": Module execution started
|
|
1514
|
-
* - type="chunk": Incremental data chunk (if LLM supports streaming)
|
|
1515
|
-
* - type="meta": Meta information available early
|
|
1516
|
-
* - type="complete": Final complete result
|
|
1517
|
-
* - type="error": Error occurred
|
|
1518
|
-
*
|
|
1519
|
-
* @example
|
|
1520
|
-
* for await (const event of runModuleStream(module, provider, options)) {
|
|
1521
|
-
* if (event.type === 'chunk') {
|
|
1522
|
-
* process.stdout.write(event.chunk);
|
|
1523
|
-
* } else if (event.type === 'complete') {
|
|
1524
|
-
* console.log('Result:', event.result);
|
|
1525
|
-
* }
|
|
1526
|
-
* }
|
|
1527
|
-
*/
|
|
1528
|
-
export async function* runModuleStream(
|
|
1529
|
-
module: CognitiveModule,
|
|
1530
|
-
provider: Provider,
|
|
1531
|
-
options: StreamOptions = {}
|
|
1532
|
-
): AsyncGenerator<StreamEvent> {
|
|
1533
|
-
const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
|
|
1534
|
-
const startTime = Date.now();
|
|
1535
|
-
const moduleName = module.name;
|
|
1536
|
-
|
|
1537
|
-
function makeEvent(type: StreamEventType, extra: Partial<StreamEvent> = {}): StreamEvent {
|
|
1538
|
-
return {
|
|
1539
|
-
type,
|
|
1540
|
-
timestamp_ms: Date.now() - startTime,
|
|
1541
|
-
module_name: moduleName,
|
|
1542
|
-
...extra,
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
try {
|
|
1547
|
-
// Emit start event
|
|
1548
|
-
yield makeEvent('start');
|
|
1549
|
-
|
|
1550
|
-
// Build input data
|
|
1551
|
-
const inputData: ModuleInput = input || {};
|
|
1552
|
-
if (args && !inputData.code && !inputData.query) {
|
|
1553
|
-
if (looksLikeCode(args)) {
|
|
1554
|
-
inputData.code = args;
|
|
1555
|
-
} else {
|
|
1556
|
-
inputData.query = args;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Validate input if enabled
|
|
1561
|
-
if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
|
|
1562
|
-
const inputErrors = validateData(inputData, module.inputSchema, 'Input');
|
|
1563
|
-
if (inputErrors.length > 0) {
|
|
1564
|
-
const errorResult = makeErrorResponse({
|
|
1565
|
-
code: 'INVALID_INPUT',
|
|
1566
|
-
message: inputErrors.join('; '),
|
|
1567
|
-
confidence: 1.0,
|
|
1568
|
-
risk: 'none',
|
|
1569
|
-
});
|
|
1570
|
-
const errorObj = (errorResult as { error: { code: string; message: string } }).error;
|
|
1571
|
-
yield makeEvent('error', { error: errorObj });
|
|
1572
|
-
yield makeEvent('complete', { result: errorResult });
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Get risk_rule from module config
|
|
1578
|
-
const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
1579
|
-
|
|
1580
|
-
// Build prompt
|
|
1581
|
-
const prompt = buildPrompt(module, inputData);
|
|
1582
|
-
|
|
1583
|
-
// Build messages
|
|
1584
|
-
const systemParts: string[] = [
|
|
1585
|
-
`You are executing the "${module.name}" Cognitive Module.`,
|
|
1586
|
-
'',
|
|
1587
|
-
`RESPONSIBILITY: ${module.responsibility}`,
|
|
1588
|
-
];
|
|
1589
|
-
|
|
1590
|
-
if (useV22) {
|
|
1591
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
|
|
1592
|
-
systemParts.push('- Wrap your response in the v2.2 envelope format');
|
|
1593
|
-
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
1594
|
-
systemParts.push('- Return ONLY valid JSON.');
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
const messages: Message[] = [
|
|
1598
|
-
{ role: 'system', content: systemParts.join('\n') },
|
|
1599
|
-
{ role: 'user', content: prompt },
|
|
1600
|
-
];
|
|
1601
|
-
|
|
1602
|
-
// Invoke provider (streaming not yet supported in provider interface, so we fallback)
|
|
1603
|
-
const result = await provider.invoke({
|
|
1604
|
-
messages,
|
|
1605
|
-
jsonSchema: module.outputSchema,
|
|
1606
|
-
temperature: 0.3,
|
|
1607
|
-
});
|
|
1608
|
-
|
|
1609
|
-
// Emit chunk event with full response
|
|
1610
|
-
yield makeEvent('chunk', { chunk: result.content });
|
|
1611
|
-
|
|
1612
|
-
// Parse response
|
|
1613
|
-
let parsed: unknown;
|
|
1614
|
-
try {
|
|
1615
|
-
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1616
|
-
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
1617
|
-
parsed = JSON.parse(jsonStr.trim());
|
|
1618
|
-
} catch (e) {
|
|
1619
|
-
const errorResult = makeErrorResponse({
|
|
1620
|
-
code: 'PARSE_ERROR',
|
|
1621
|
-
message: `Failed to parse JSON: ${(e as Error).message}`,
|
|
1622
|
-
});
|
|
1623
|
-
// errorResult is always an error response from makeErrorResponse
|
|
1624
|
-
const errorObj = (errorResult as { error: { code: string; message: string } }).error;
|
|
1625
|
-
yield makeEvent('error', { error: errorObj });
|
|
1626
|
-
yield makeEvent('complete', { result: errorResult });
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// Convert to v2.2 envelope
|
|
1631
|
-
let response: EnvelopeResponseV22<unknown>;
|
|
1632
|
-
if (isV22Envelope(parsed as EnvelopeResponse<unknown>)) {
|
|
1633
|
-
response = parsed as EnvelopeResponseV22<unknown>;
|
|
1634
|
-
} else if (isEnvelopeResponse(parsed)) {
|
|
1635
|
-
response = wrapV21ToV22(parsed as EnvelopeResponse<unknown>, riskRule);
|
|
1636
|
-
} else {
|
|
1637
|
-
response = convertLegacyToEnvelope(parsed as Record<string, unknown>);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
// Add version and meta
|
|
1641
|
-
response.version = ENVELOPE_VERSION;
|
|
1642
|
-
const latencyMs = Date.now() - startTime;
|
|
1643
|
-
if (response.meta) {
|
|
1644
|
-
response.meta.latency_ms = latencyMs;
|
|
1645
|
-
if (traceId) {
|
|
1646
|
-
response.meta.trace_id = traceId;
|
|
1647
|
-
}
|
|
1648
|
-
if (model) {
|
|
1649
|
-
response.meta.model = model;
|
|
1650
|
-
}
|
|
1651
|
-
// Emit meta event early
|
|
1652
|
-
yield makeEvent('meta', { meta: response.meta });
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// Validate and repair output
|
|
1656
|
-
if (response.ok && validateOutput) {
|
|
1657
|
-
const dataSchema = module.dataSchema || module.outputSchema;
|
|
1658
|
-
const metaSchema = module.metaSchema;
|
|
1659
|
-
|
|
1660
|
-
if (dataSchema && Object.keys(dataSchema).length > 0) {
|
|
1661
|
-
const dataToValidate = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
|
|
1662
|
-
let dataErrors = validateData(dataToValidate, dataSchema, 'Data');
|
|
1663
|
-
|
|
1664
|
-
if (dataErrors.length > 0 && enableRepair) {
|
|
1665
|
-
response = repairEnvelope(response as unknown as Record<string, unknown>, riskRule);
|
|
1666
|
-
response.version = ENVELOPE_VERSION;
|
|
1667
|
-
// Re-validate after repair
|
|
1668
|
-
const repairedData = (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data ?? {};
|
|
1669
|
-
dataErrors = validateData(repairedData, dataSchema, 'Data');
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (dataErrors.length > 0) {
|
|
1673
|
-
const errorResult = makeErrorResponse({
|
|
1674
|
-
code: 'SCHEMA_VALIDATION_FAILED',
|
|
1675
|
-
message: dataErrors.join('; '),
|
|
1676
|
-
explain: 'Schema validation failed after repair attempt.',
|
|
1677
|
-
partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
|
|
1678
|
-
details: { validation_errors: dataErrors },
|
|
1679
|
-
});
|
|
1680
|
-
const errorObj = (errorResult as { error: { code: string; message: string } }).error;
|
|
1681
|
-
yield makeEvent('error', { error: errorObj });
|
|
1682
|
-
yield makeEvent('complete', { result: errorResult });
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Validate meta if schema exists
|
|
1688
|
-
if (metaSchema && Object.keys(metaSchema).length > 0) {
|
|
1689
|
-
let metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1690
|
-
|
|
1691
|
-
if (metaErrors.length > 0 && enableRepair) {
|
|
1692
|
-
response = repairEnvelope(response as unknown as Record<string, unknown>, riskRule);
|
|
1693
|
-
response.version = ENVELOPE_VERSION;
|
|
1694
|
-
metaErrors = validateData(response.meta ?? {}, metaSchema, 'Meta');
|
|
1695
|
-
|
|
1696
|
-
if (metaErrors.length > 0) {
|
|
1697
|
-
const errorResult = makeErrorResponse({
|
|
1698
|
-
code: 'META_VALIDATION_FAILED',
|
|
1699
|
-
message: metaErrors.join('; '),
|
|
1700
|
-
explain: 'Meta validation failed after repair attempt.',
|
|
1701
|
-
partialData: (response as EnvelopeResponseV22<unknown> & { data?: unknown }).data,
|
|
1702
|
-
details: { validation_errors: metaErrors },
|
|
1703
|
-
});
|
|
1704
|
-
const errorObj = (errorResult as { error: { code: string; message: string } }).error;
|
|
1705
|
-
yield makeEvent('error', { error: errorObj });
|
|
1706
|
-
yield makeEvent('complete', { result: errorResult });
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
} else if (!response.ok && enableRepair) {
|
|
1712
|
-
response = repairErrorEnvelope(response as unknown as Record<string, unknown>);
|
|
1713
|
-
response.version = ENVELOPE_VERSION;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
// Emit complete event
|
|
1717
|
-
yield makeEvent('complete', { result: response });
|
|
1718
|
-
|
|
1719
|
-
} catch (e) {
|
|
1720
|
-
const errorResult = makeErrorResponse({
|
|
1721
|
-
code: 'UNKNOWN',
|
|
1722
|
-
message: (e as Error).message,
|
|
1723
|
-
explain: `Unexpected error: ${(e as Error).name}`,
|
|
1724
|
-
});
|
|
1725
|
-
// errorResult is always an error response from makeErrorResponse
|
|
1726
|
-
const errorObj = (errorResult as { error: { code: string; message: string } }).error;
|
|
1727
|
-
yield makeEvent('error', { error: errorObj });
|
|
1728
|
-
yield makeEvent('complete', { result: errorResult });
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// =============================================================================
|
|
1733
|
-
// Helper Functions
|
|
1734
|
-
// =============================================================================
|
|
1735
|
-
|
|
1736
|
-
/**
|
|
1737
|
-
* Check if response is in envelope format
|
|
1738
|
-
*/
|
|
1739
|
-
function isEnvelopeResponse(obj: unknown): obj is EnvelopeResponse {
|
|
1740
|
-
if (typeof obj !== 'object' || obj === null) return false;
|
|
1741
|
-
const o = obj as Record<string, unknown>;
|
|
1742
|
-
return typeof o.ok === 'boolean';
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
/**
|
|
1746
|
-
* Parse envelope format response (supports both v2.1 and v2.2)
|
|
1747
|
-
*/
|
|
1748
|
-
function parseEnvelopeResponse(response: EnvelopeResponse<unknown>, raw: string): ModuleResult {
|
|
1749
|
-
// Check if v2.2 format (has meta)
|
|
1750
|
-
if (isV22Envelope(response)) {
|
|
1751
|
-
if (response.ok) {
|
|
1752
|
-
return {
|
|
1753
|
-
ok: true,
|
|
1754
|
-
meta: response.meta,
|
|
1755
|
-
data: response.data as ModuleResultData,
|
|
1756
|
-
raw,
|
|
1757
|
-
} as ModuleResultV22;
|
|
1758
|
-
} else {
|
|
1759
|
-
return {
|
|
1760
|
-
ok: false,
|
|
1761
|
-
meta: response.meta,
|
|
1762
|
-
error: response.error,
|
|
1763
|
-
partial_data: response.partial_data,
|
|
1764
|
-
raw,
|
|
1765
|
-
} as ModuleResultV22;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// v2.1 format
|
|
1770
|
-
if (response.ok) {
|
|
1771
|
-
const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
|
|
1772
|
-
return {
|
|
1773
|
-
ok: true,
|
|
1774
|
-
data: {
|
|
1775
|
-
...data,
|
|
1776
|
-
confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
|
|
1777
|
-
rationale: typeof data.rationale === 'string' ? data.rationale : '',
|
|
1778
|
-
behavior_equivalence: data.behavior_equivalence,
|
|
1779
|
-
},
|
|
1780
|
-
raw,
|
|
1781
|
-
} as ModuleResultV21;
|
|
1782
|
-
} else {
|
|
1783
|
-
return {
|
|
1784
|
-
ok: false,
|
|
1785
|
-
error: response.error,
|
|
1786
|
-
partial_data: response.partial_data,
|
|
1787
|
-
raw,
|
|
1788
|
-
} as ModuleResultV21;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
/**
|
|
1793
|
-
* Parse legacy (non-envelope) format response
|
|
1794
|
-
*/
|
|
1795
|
-
function parseLegacyResponse(output: unknown, raw: string): ModuleResult {
|
|
1796
|
-
const outputObj = output as Record<string, unknown>;
|
|
1797
|
-
const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
|
|
1798
|
-
const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
|
|
1799
|
-
const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
|
|
1800
|
-
? outputObj.behavior_equivalence
|
|
1801
|
-
: undefined;
|
|
1802
|
-
|
|
1803
|
-
// Check if this is an error response (has error.code)
|
|
1804
|
-
if (outputObj.error && typeof outputObj.error === 'object') {
|
|
1805
|
-
const errorObj = outputObj.error as Record<string, unknown>;
|
|
1806
|
-
if (typeof errorObj.code === 'string') {
|
|
1807
|
-
return {
|
|
1808
|
-
ok: false,
|
|
1809
|
-
error: {
|
|
1810
|
-
code: errorObj.code,
|
|
1811
|
-
message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
|
|
1812
|
-
},
|
|
1813
|
-
raw,
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
// Return as v2.1 format (data includes confidence)
|
|
1819
|
-
return {
|
|
1820
|
-
ok: true,
|
|
1821
|
-
data: {
|
|
1822
|
-
...outputObj,
|
|
1823
|
-
confidence,
|
|
1824
|
-
rationale,
|
|
1825
|
-
behavior_equivalence: behaviorEquivalence,
|
|
1826
|
-
},
|
|
1827
|
-
raw,
|
|
1828
|
-
} as ModuleResultV21;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
/**
|
|
1832
|
-
* Build prompt with clean variable substitution
|
|
1833
|
-
*
|
|
1834
|
-
* Substitution order (important to avoid partial replacements):
|
|
1835
|
-
* 1. ${variable} - v2 style placeholders
|
|
1836
|
-
* 2. $ARGUMENTS[N] - indexed access (descending order to avoid $1 matching $10)
|
|
1837
|
-
* 3. $N - shorthand indexed access (descending order)
|
|
1838
|
-
* 4. $ARGUMENTS - full argument string (LAST to avoid partial matches)
|
|
1839
|
-
*/
|
|
1840
|
-
function buildPrompt(module: CognitiveModule, input: ModuleInput): string {
|
|
1841
|
-
let prompt = module.prompt;
|
|
1842
|
-
|
|
1843
|
-
// v2 style: substitute ${variable} placeholders
|
|
1844
|
-
for (const [key, value] of Object.entries(input)) {
|
|
1845
|
-
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1846
|
-
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
// v1 compatibility: get args value
|
|
1850
|
-
const argsValue = input.code || input.query || '';
|
|
1851
|
-
|
|
1852
|
-
// Substitute $ARGUMENTS[N] and $N placeholders FIRST (v1 compatibility)
|
|
1853
|
-
// Process in descending order to avoid $1 replacing part of $10
|
|
1854
|
-
if (typeof argsValue === 'string') {
|
|
1855
|
-
const argsList = argsValue.split(/\s+/);
|
|
1856
|
-
for (let i = argsList.length - 1; i >= 0; i--) {
|
|
1857
|
-
const arg = argsList[i];
|
|
1858
|
-
// Replace $ARGUMENTS[N] first
|
|
1859
|
-
prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
|
|
1860
|
-
// Replace $N shorthand
|
|
1861
|
-
prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
|
|
1866
|
-
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
1867
|
-
|
|
1868
|
-
// Append input summary if not already in prompt
|
|
1869
|
-
if (!prompt.includes(argsValue) && argsValue) {
|
|
1870
|
-
prompt += '\n\n## Input\n\n';
|
|
1871
|
-
if (input.code) {
|
|
1872
|
-
prompt += '```\n' + input.code + '\n```\n';
|
|
1873
|
-
}
|
|
1874
|
-
if (input.query) {
|
|
1875
|
-
prompt += input.query + '\n';
|
|
1876
|
-
}
|
|
1877
|
-
if (input.language) {
|
|
1878
|
-
prompt += `\nLanguage: ${input.language}\n`;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
return prompt;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
/**
|
|
1886
|
-
* Heuristic to detect if input looks like code
|
|
1887
|
-
*/
|
|
1888
|
-
function looksLikeCode(str: string): boolean {
|
|
1889
|
-
const codeIndicators = [
|
|
1890
|
-
/^(def|function|class|const|let|var|import|export|public|private)\s/,
|
|
1891
|
-
/[{};()]/,
|
|
1892
|
-
/=>/,
|
|
1893
|
-
/\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
|
|
1894
|
-
];
|
|
1895
|
-
return codeIndicators.some(re => re.test(str));
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// =============================================================================
|
|
1899
|
-
// Legacy API (for backward compatibility)
|
|
1900
|
-
// =============================================================================
|
|
1901
|
-
|
|
1902
|
-
export interface RunModuleLegacyOptions {
|
|
1903
|
-
validateInput?: boolean;
|
|
1904
|
-
validateOutput?: boolean;
|
|
1905
|
-
model?: string;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
/**
|
|
1909
|
-
* Run a cognitive module (legacy API, returns raw output).
|
|
1910
|
-
* For backward compatibility. Throws on error instead of returning error envelope.
|
|
1911
|
-
*/
|
|
1912
|
-
export async function runModuleLegacy(
|
|
1913
|
-
module: CognitiveModule,
|
|
1914
|
-
provider: Provider,
|
|
1915
|
-
input: ModuleInput,
|
|
1916
|
-
options: RunModuleLegacyOptions = {}
|
|
1917
|
-
): Promise<unknown> {
|
|
1918
|
-
const { validateInput = true, validateOutput = true, model } = options;
|
|
1919
|
-
|
|
1920
|
-
const result = await runModule(module, provider, {
|
|
1921
|
-
input,
|
|
1922
|
-
validateInput,
|
|
1923
|
-
validateOutput,
|
|
1924
|
-
useEnvelope: false,
|
|
1925
|
-
useV22: false,
|
|
1926
|
-
model,
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
if (result.ok && 'data' in result) {
|
|
1930
|
-
return result.data;
|
|
1931
|
-
} else {
|
|
1932
|
-
const error = 'error' in result ? result.error : { code: 'UNKNOWN', message: 'Unknown error' };
|
|
1933
|
-
throw new Error(`${error?.code ?? 'UNKNOWN'}: ${error?.message ?? 'Unknown error'}`);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
// =============================================================================
|
|
1938
|
-
// Convenience Functions
|
|
1939
|
-
// =============================================================================
|
|
1940
|
-
|
|
1941
|
-
/**
|
|
1942
|
-
* Extract meta from v2.2 envelope for routing/logging.
|
|
1943
|
-
*/
|
|
1944
|
-
export function extractMeta(result: EnvelopeResponseV22<unknown>): EnvelopeMeta {
|
|
1945
|
-
return result.meta ?? {
|
|
1946
|
-
confidence: 0.5,
|
|
1947
|
-
risk: 'medium',
|
|
1948
|
-
explain: 'No meta available',
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// Alias for backward compatibility
|
|
1953
|
-
export const extractMetaV22 = extractMeta;
|
|
1954
|
-
|
|
1955
|
-
/**
|
|
1956
|
-
* Determine if result should be escalated to human review based on meta.
|
|
1957
|
-
*/
|
|
1958
|
-
export function shouldEscalate(
|
|
1959
|
-
result: EnvelopeResponseV22<unknown>,
|
|
1960
|
-
confidenceThreshold: number = 0.7
|
|
1961
|
-
): boolean {
|
|
1962
|
-
const meta = extractMeta(result);
|
|
1963
|
-
|
|
1964
|
-
// Escalate if low confidence
|
|
1965
|
-
if (meta.confidence < confidenceThreshold) {
|
|
1966
|
-
return true;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// Escalate if high risk
|
|
1970
|
-
if (meta.risk === 'high') {
|
|
1971
|
-
return true;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
// Escalate if error
|
|
1975
|
-
if (!result.ok) {
|
|
1976
|
-
return true;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
return false;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// Alias for backward compatibility
|
|
1983
|
-
export const shouldEscalateV22 = shouldEscalate;
|