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/src/modules/runner.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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
6
|
|
|
7
|
+
import _Ajv from 'ajv';
|
|
8
|
+
const Ajv = _Ajv.default || _Ajv;
|
|
6
9
|
import type {
|
|
7
10
|
Provider,
|
|
8
11
|
CognitiveModule,
|
|
@@ -16,882 +19,1734 @@ import type {
|
|
|
16
19
|
EnvelopeMeta,
|
|
17
20
|
ModuleResultData,
|
|
18
21
|
RiskLevel,
|
|
19
|
-
RiskRule
|
|
20
|
-
// v2.5 types
|
|
21
|
-
StreamingChunk,
|
|
22
|
-
MetaChunk,
|
|
23
|
-
DeltaChunk,
|
|
24
|
-
FinalChunk,
|
|
25
|
-
ErrorChunk,
|
|
26
|
-
ProgressChunk,
|
|
27
|
-
StreamingSession,
|
|
28
|
-
MediaInput,
|
|
29
|
-
ProviderV25,
|
|
30
|
-
CognitiveModuleV25,
|
|
31
|
-
ModalityType,
|
|
32
|
-
RuntimeCapabilities
|
|
22
|
+
RiskRule
|
|
33
23
|
} from '../types.js';
|
|
34
|
-
import {
|
|
35
|
-
aggregateRisk,
|
|
36
|
-
isV22Envelope,
|
|
37
|
-
isProviderV25,
|
|
38
|
-
isModuleV25,
|
|
39
|
-
moduleSupportsStreaming,
|
|
40
|
-
moduleSupportsMultimodal,
|
|
41
|
-
getModuleInputModalities,
|
|
42
|
-
ErrorCodesV25,
|
|
43
|
-
DEFAULT_RUNTIME_CAPABILITIES
|
|
44
|
-
} from '../types.js';
|
|
45
|
-
import { randomUUID } from 'crypto';
|
|
46
|
-
import { readFile } from 'fs/promises';
|
|
47
|
-
import { existsSync } from 'fs';
|
|
48
|
-
import { extname } from 'path';
|
|
24
|
+
import { aggregateRisk, isV22Envelope } from '../types.js';
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
enableRepair?: boolean;
|
|
55
|
+
return errors;
|
|
68
56
|
}
|
|
69
57
|
|
|
70
58
|
// =============================================================================
|
|
71
|
-
//
|
|
59
|
+
// v2.2 Policy Enforcement
|
|
72
60
|
// =============================================================================
|
|
73
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
|
+
|
|
74
102
|
/**
|
|
75
|
-
*
|
|
103
|
+
* Check if a tool is allowed by the module's tools policy.
|
|
76
104
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* - Trim whitespace from string fields
|
|
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
|
|
81
108
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
109
|
+
* @example
|
|
110
|
+
* const result = checkToolPolicy('write_file', module);
|
|
111
|
+
* if (!result.allowed) {
|
|
112
|
+
* throw new Error(result.reason);
|
|
113
|
+
* }
|
|
84
114
|
*/
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const repaired = { ...response };
|
|
115
|
+
export function checkToolPolicy(
|
|
116
|
+
toolName: string,
|
|
117
|
+
module: CognitiveModule
|
|
118
|
+
): PolicyCheckResult {
|
|
119
|
+
const toolsPolicy = module.tools;
|
|
91
120
|
|
|
92
|
-
//
|
|
93
|
-
if (!
|
|
94
|
-
|
|
121
|
+
// No policy = allow all
|
|
122
|
+
if (!toolsPolicy) {
|
|
123
|
+
return { allowed: true };
|
|
95
124
|
}
|
|
96
125
|
|
|
97
|
-
const
|
|
98
|
-
const data = (repaired.data ?? {}) as Record<string, unknown>;
|
|
99
|
-
|
|
100
|
-
// Repair confidence
|
|
101
|
-
if (typeof meta.confidence !== 'number') {
|
|
102
|
-
meta.confidence = (data.confidence as number) ?? 0.5;
|
|
103
|
-
}
|
|
104
|
-
meta.confidence = Math.max(0, Math.min(1, meta.confidence as number));
|
|
126
|
+
const normalizedName = toolName.toLowerCase().replace(/[-\s]/g, '_');
|
|
105
127
|
|
|
106
|
-
//
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const validRisks = ['none', 'low', 'medium', 'high'];
|
|
114
|
-
meta.risk = validRisks.includes(trimmedRisk) ? trimmedRisk : 'medium';
|
|
115
|
-
} else {
|
|
116
|
-
meta.risk = 'medium'; // Default for invalid type
|
|
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
|
+
};
|
|
117
135
|
}
|
|
118
136
|
|
|
119
|
-
//
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
const builtMeta: EnvelopeMeta = {
|
|
133
|
-
confidence: meta.confidence as number,
|
|
134
|
-
risk: meta.risk as RiskLevel,
|
|
135
|
-
explain: meta.explain as string
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const result: EnvelopeResponseV22<unknown> = repaired.ok === false ? {
|
|
139
|
-
ok: false,
|
|
140
|
-
meta: builtMeta,
|
|
141
|
-
error: (repaired.error as { code: string; message: string }) ?? { code: 'UNKNOWN', message: 'Unknown error' },
|
|
142
|
-
partial_data: repaired.partial_data
|
|
143
|
-
} : {
|
|
144
|
-
ok: true,
|
|
145
|
-
meta: builtMeta,
|
|
146
|
-
data: repaired.data
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return result;
|
|
153
|
+
return { allowed: true };
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
/**
|
|
153
|
-
*
|
|
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
|
+
* }
|
|
154
168
|
*/
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
):
|
|
159
|
-
|
|
160
|
-
|
|
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 };
|
|
161
178
|
}
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const confidence = (data.confidence as number) ?? 0.5;
|
|
166
|
-
const rationale = (data.rationale as string) ?? '';
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
ok: true,
|
|
170
|
-
meta: {
|
|
171
|
-
confidence,
|
|
172
|
-
risk: aggregateRisk(data, riskRule),
|
|
173
|
-
explain: rationale.slice(0, 280) || 'No explanation provided'
|
|
174
|
-
},
|
|
175
|
-
data: data as ModuleResultData
|
|
176
|
-
};
|
|
177
|
-
} else {
|
|
178
|
-
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
180
|
+
// Check the specific policy
|
|
181
|
+
if (policies[action] === 'deny') {
|
|
179
182
|
return {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
risk: 'high',
|
|
184
|
-
explain: errorMsg.slice(0, 280)
|
|
185
|
-
},
|
|
186
|
-
error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
|
|
187
|
-
partial_data: response.partial_data
|
|
183
|
+
allowed: false,
|
|
184
|
+
reason: `Action '${action}' is denied by module policy`,
|
|
185
|
+
policy: `policies.${action}`
|
|
188
186
|
};
|
|
189
187
|
}
|
|
188
|
+
|
|
189
|
+
return { allowed: true };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
238
|
|
|
199
|
-
|
|
200
|
-
|
|
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[] = [];
|
|
201
252
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
for (const toolName of toolNames) {
|
|
254
|
+
const result = checkToolAllowed(toolName, module);
|
|
255
|
+
if (!result.allowed) {
|
|
256
|
+
violations.push(result);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
205
259
|
|
|
206
|
-
|
|
207
|
-
|
|
260
|
+
return violations;
|
|
261
|
+
}
|
|
208
262
|
|
|
209
|
-
|
|
210
|
-
|
|
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;
|
|
211
270
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
inputData.query = args;
|
|
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);
|
|
219
277
|
}
|
|
220
278
|
}
|
|
279
|
+
|
|
280
|
+
return denied;
|
|
281
|
+
}
|
|
221
282
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
289
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.error(JSON.stringify(inputData, null, 2));
|
|
232
|
-
console.error('--- Prompt ---');
|
|
233
|
-
console.error(prompt);
|
|
234
|
-
console.error('--- End ---');
|
|
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 || [];
|
|
235
296
|
}
|
|
297
|
+
return null; // null means "all allowed except denied list"
|
|
298
|
+
}
|
|
236
299
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'',
|
|
241
|
-
`RESPONSIBILITY: ${module.responsibility}`,
|
|
242
|
-
];
|
|
300
|
+
// =============================================================================
|
|
301
|
+
// Tool Call Interceptor
|
|
302
|
+
// =============================================================================
|
|
243
303
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
304
|
+
/** Tool call request from LLM */
|
|
305
|
+
export interface ToolCallRequest {
|
|
306
|
+
name: string;
|
|
307
|
+
arguments: Record<string, unknown>;
|
|
308
|
+
}
|
|
248
309
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
310
|
+
/** Tool call result */
|
|
311
|
+
export interface ToolCallResult {
|
|
312
|
+
success: boolean;
|
|
313
|
+
result?: unknown;
|
|
314
|
+
error?: {
|
|
315
|
+
code: string;
|
|
316
|
+
message: string;
|
|
317
|
+
};
|
|
318
|
+
}
|
|
256
319
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
|
|
260
|
-
systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
|
|
261
|
-
|
|
262
|
-
const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
|
|
263
|
-
systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
|
|
264
|
-
}
|
|
320
|
+
/** Tool executor function type */
|
|
321
|
+
export type ToolExecutor = (args: Record<string, unknown>) => Promise<unknown>;
|
|
265
322
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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;
|
|
292
353
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// Invoke provider
|
|
300
|
-
const result = await provider.invoke({
|
|
301
|
-
messages,
|
|
302
|
-
jsonSchema: module.outputSchema,
|
|
303
|
-
temperature: 0.3,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
if (verbose) {
|
|
307
|
-
console.error('--- Response ---');
|
|
308
|
-
console.error(result.content);
|
|
309
|
-
console.error('--- End Response ---');
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Register a tool executor.
|
|
357
|
+
*/
|
|
358
|
+
registerTool(name: string, executor: ToolExecutor): void {
|
|
359
|
+
this.tools.set(name.toLowerCase(), executor);
|
|
310
360
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
|
|
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
|
+
}
|
|
320
369
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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();
|
|
325
387
|
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
};
|
|
335
400
|
}
|
|
336
401
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
raw: result.content
|
|
348
|
-
} as ModuleResultV22;
|
|
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
|
+
};
|
|
349
412
|
}
|
|
350
413
|
|
|
351
|
-
|
|
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
|
+
}
|
|
352
428
|
}
|
|
353
|
-
|
|
354
|
-
// Handle legacy format (non-envelope)
|
|
355
|
-
const legacyResult = parseLegacyResponse(parsed, result.content);
|
|
356
429
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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 = [];
|
|
370
471
|
}
|
|
371
472
|
|
|
372
|
-
|
|
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
|
+
}
|
|
373
489
|
}
|
|
374
490
|
|
|
375
491
|
/**
|
|
376
|
-
*
|
|
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' });
|
|
377
501
|
*/
|
|
378
|
-
function
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
};
|
|
382
516
|
}
|
|
383
517
|
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// v2.2 Runtime Enforcement - Overflow & Enum
|
|
520
|
+
// =============================================================================
|
|
521
|
+
|
|
384
522
|
/**
|
|
385
|
-
*
|
|
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
|
|
386
528
|
*/
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
ok: false,
|
|
400
|
-
meta: response.meta,
|
|
401
|
-
error: response.error,
|
|
402
|
-
partial_data: response.partial_data,
|
|
403
|
-
raw,
|
|
404
|
-
} as ModuleResultV22;
|
|
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');
|
|
405
541
|
}
|
|
542
|
+
return errors;
|
|
406
543
|
}
|
|
407
544
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
} as ModuleResultV21;
|
|
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
|
+
}
|
|
428
564
|
}
|
|
565
|
+
|
|
566
|
+
return errors;
|
|
429
567
|
}
|
|
430
568
|
|
|
431
569
|
/**
|
|
432
|
-
*
|
|
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
|
|
433
576
|
*/
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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');
|
|
455
609
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
ok: true,
|
|
460
|
-
data: {
|
|
461
|
-
...outputObj,
|
|
462
|
-
confidence,
|
|
463
|
-
rationale,
|
|
464
|
-
behavior_equivalence: behaviorEquivalence,
|
|
465
|
-
},
|
|
466
|
-
raw,
|
|
467
|
-
} as ModuleResultV21;
|
|
610
|
+
|
|
611
|
+
return errors;
|
|
468
612
|
}
|
|
469
613
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
function buildPrompt(module: CognitiveModule, input: ModuleInput): string {
|
|
474
|
-
let prompt = module.prompt;
|
|
614
|
+
// =============================================================================
|
|
615
|
+
// Constants
|
|
616
|
+
// =============================================================================
|
|
475
617
|
|
|
476
|
-
|
|
477
|
-
for (const [key, value] of Object.entries(input)) {
|
|
478
|
-
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
479
|
-
prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
|
|
480
|
-
}
|
|
618
|
+
const ENVELOPE_VERSION = '2.2';
|
|
481
619
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
620
|
+
// =============================================================================
|
|
621
|
+
// Utility Functions
|
|
622
|
+
// =============================================================================
|
|
485
623
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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;
|
|
492
631
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (input.query) {
|
|
501
|
-
prompt += input.query + '\n';
|
|
502
|
-
}
|
|
503
|
-
if (input.language) {
|
|
504
|
-
prompt += `\nLanguage: ${input.language}\n`;
|
|
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]);
|
|
505
639
|
}
|
|
506
640
|
}
|
|
507
|
-
|
|
508
|
-
return prompt;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Heuristic to detect if input looks like code
|
|
513
|
-
*/
|
|
514
|
-
function looksLikeCode(str: string): boolean {
|
|
515
|
-
const codeIndicators = [
|
|
516
|
-
/^(def|function|class|const|let|var|import|export|public|private)\s/,
|
|
517
|
-
/[{};()]/,
|
|
518
|
-
/=>/,
|
|
519
|
-
/\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
|
|
520
|
-
];
|
|
521
|
-
return codeIndicators.some(re => re.test(str));
|
|
641
|
+
return cloned;
|
|
522
642
|
}
|
|
523
643
|
|
|
524
644
|
// =============================================================================
|
|
525
|
-
//
|
|
645
|
+
// Observability Hooks
|
|
526
646
|
// =============================================================================
|
|
527
647
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
onChunk?: (chunk: StreamingChunk) => void;
|
|
531
|
-
|
|
532
|
-
/** Callback for progress updates */
|
|
533
|
-
onProgress?: (percent: number, message?: string) => void;
|
|
534
|
-
|
|
535
|
-
/** Heartbeat interval in milliseconds (default: 15000) */
|
|
536
|
-
heartbeatInterval?: number;
|
|
537
|
-
|
|
538
|
-
/** Maximum stream duration in milliseconds (default: 300000) */
|
|
539
|
-
maxDuration?: number;
|
|
540
|
-
}
|
|
648
|
+
/** Hook called before module execution */
|
|
649
|
+
export type BeforeCallHook = (moduleName: string, inputData: ModuleInput, moduleConfig: CognitiveModule) => void;
|
|
541
650
|
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
accumulated_text: {}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
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[] = [];
|
|
555
661
|
|
|
556
662
|
/**
|
|
557
|
-
*
|
|
663
|
+
* Decorator to register a before-call hook.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* onBeforeCall((moduleName, inputData, config) => {
|
|
667
|
+
* console.log(`Calling ${moduleName} with`, inputData);
|
|
668
|
+
* });
|
|
558
669
|
*/
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
streaming: true,
|
|
563
|
-
session_id: session.session_id,
|
|
564
|
-
meta
|
|
565
|
-
};
|
|
670
|
+
export function onBeforeCall(hook: BeforeCallHook): BeforeCallHook {
|
|
671
|
+
_beforeCallHooks.push(hook);
|
|
672
|
+
return hook;
|
|
566
673
|
}
|
|
567
674
|
|
|
568
675
|
/**
|
|
569
|
-
*
|
|
570
|
-
*
|
|
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
|
+
* });
|
|
571
682
|
*/
|
|
572
|
-
function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
delta: string
|
|
576
|
-
): DeltaChunk {
|
|
577
|
-
session.chunks_sent++;
|
|
578
|
-
return {
|
|
579
|
-
chunk: {
|
|
580
|
-
seq: session.chunks_sent,
|
|
581
|
-
type: 'delta',
|
|
582
|
-
field,
|
|
583
|
-
delta
|
|
584
|
-
}
|
|
585
|
-
};
|
|
683
|
+
export function onAfterCall(hook: AfterCallHook): AfterCallHook {
|
|
684
|
+
_afterCallHooks.push(hook);
|
|
685
|
+
return hook;
|
|
586
686
|
}
|
|
587
687
|
|
|
588
688
|
/**
|
|
589
|
-
*
|
|
590
|
-
*
|
|
689
|
+
* Decorator to register an error hook.
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* onError((moduleName, error, partialResult) => {
|
|
693
|
+
* console.error(`Error in ${moduleName}:`, error);
|
|
694
|
+
* });
|
|
591
695
|
*/
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
stage?: string,
|
|
596
|
-
message?: string
|
|
597
|
-
): ProgressChunk {
|
|
598
|
-
return {
|
|
599
|
-
progress: {
|
|
600
|
-
percent,
|
|
601
|
-
stage,
|
|
602
|
-
message
|
|
603
|
-
}
|
|
604
|
-
};
|
|
696
|
+
export function onError(hook: ErrorHook): ErrorHook {
|
|
697
|
+
_errorHooks.push(hook);
|
|
698
|
+
return hook;
|
|
605
699
|
}
|
|
606
700
|
|
|
607
701
|
/**
|
|
608
|
-
*
|
|
609
|
-
* Note: Final chunks don't include session_id per v2.5 spec
|
|
702
|
+
* Register a hook programmatically.
|
|
610
703
|
*/
|
|
611
|
-
function
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
}
|
|
623
717
|
}
|
|
624
718
|
|
|
625
719
|
/**
|
|
626
|
-
*
|
|
720
|
+
* Unregister a hook. Returns true if found and removed.
|
|
627
721
|
*/
|
|
628
|
-
function
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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,
|
|
645
872
|
};
|
|
646
873
|
}
|
|
647
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
|
+
|
|
648
885
|
/**
|
|
649
|
-
*
|
|
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
|
|
650
963
|
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
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.
|
|
655
968
|
*/
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
):
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
onProgress,
|
|
664
|
-
heartbeatInterval = 15000,
|
|
665
|
-
maxDuration = 300000,
|
|
666
|
-
...runOptions
|
|
667
|
-
} = options;
|
|
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);
|
|
668
976
|
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
977
|
+
// Ensure meta exists
|
|
978
|
+
if (!repaired.meta || typeof repaired.meta !== 'object') {
|
|
979
|
+
repaired.meta = {};
|
|
980
|
+
}
|
|
672
981
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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 };
|
|
688
1094
|
}
|
|
689
|
-
|
|
690
|
-
return result;
|
|
1095
|
+
return response;
|
|
691
1096
|
}
|
|
692
1097
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const result = await runModule(module, provider, runOptions);
|
|
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) ?? '';
|
|
698
1102
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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);
|
|
708
1141
|
|
|
709
|
-
return
|
|
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
|
+
};
|
|
710
1170
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const {
|
|
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
|
|
723
1186
|
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
1187
|
+
|
|
1188
|
+
// Determine if we should use v2.2 format
|
|
724
1189
|
const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
|
|
725
1190
|
const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
|
|
726
|
-
const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
|
|
727
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)
|
|
728
1196
|
const inputData: ModuleInput = input || {};
|
|
1197
|
+
|
|
1198
|
+
// Map legacy --args to clean input
|
|
729
1199
|
if (args && !inputData.code && !inputData.query) {
|
|
1200
|
+
// Determine if args looks like code or natural language
|
|
730
1201
|
if (looksLikeCode(args)) {
|
|
731
1202
|
inputData.code = args;
|
|
732
1203
|
} else {
|
|
733
1204
|
inputData.query = args;
|
|
734
1205
|
}
|
|
735
1206
|
}
|
|
736
|
-
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
+
|
|
746
1300
|
const messages: Message[] = [
|
|
747
1301
|
{ role: 'system', content: systemParts.join('\n') },
|
|
748
1302
|
{ role: 'user', content: prompt },
|
|
749
1303
|
];
|
|
750
|
-
|
|
1304
|
+
|
|
751
1305
|
try {
|
|
752
|
-
//
|
|
753
|
-
const
|
|
1306
|
+
// Invoke provider
|
|
1307
|
+
const result = await provider.invoke({
|
|
754
1308
|
messages,
|
|
755
1309
|
jsonSchema: module.outputSchema,
|
|
756
1310
|
temperature: 0.3,
|
|
757
|
-
stream: true,
|
|
758
|
-
images: mediaInputs.images,
|
|
759
|
-
audio: mediaInputs.audio,
|
|
760
|
-
video: mediaInputs.video
|
|
761
1311
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
+
}
|
|
780
1395
|
}
|
|
781
1396
|
|
|
782
|
-
//
|
|
783
|
-
|
|
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
|
+
}
|
|
784
1410
|
|
|
785
|
-
//
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
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
|
+
}
|
|
789
1424
|
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
+
}
|
|
804
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;
|
|
805
1456
|
}
|
|
806
|
-
|
|
807
|
-
//
|
|
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
|
|
808
1613
|
let parsed: unknown;
|
|
809
1614
|
try {
|
|
810
|
-
const jsonMatch =
|
|
811
|
-
const jsonStr = jsonMatch ? jsonMatch[1] :
|
|
1615
|
+
const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1616
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
|
|
812
1617
|
parsed = JSON.parse(jsonStr.trim());
|
|
813
|
-
} catch {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
onChunk?.(errorChunk);
|
|
824
|
-
return undefined;
|
|
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;
|
|
825
1628
|
}
|
|
826
|
-
|
|
827
|
-
//
|
|
828
|
-
let
|
|
829
|
-
if (
|
|
830
|
-
|
|
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;
|
|
831
1659
|
|
|
832
|
-
if (
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
1685
|
}
|
|
843
|
-
} else {
|
|
844
|
-
result = parseLegacyResponseLocal(parsed, accumulatedContent);
|
|
845
1686
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
+
}
|
|
858
1710
|
}
|
|
1711
|
+
} else if (!response.ok && enableRepair) {
|
|
1712
|
+
response = repairErrorEnvelope(response as unknown as Record<string, unknown>);
|
|
1713
|
+
response.version = ENVELOPE_VERSION;
|
|
859
1714
|
}
|
|
860
|
-
|
|
861
|
-
// Emit
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
onChunk?.(finalChunk);
|
|
875
|
-
onProgress?.(100, 'Complete');
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
return result;
|
|
879
|
-
|
|
880
|
-
} catch (error) {
|
|
881
|
-
const errorChunk = createErrorChunk(
|
|
882
|
-
session,
|
|
883
|
-
ErrorCodesV25.STREAM_INTERRUPTED,
|
|
884
|
-
error instanceof Error ? error.message : 'Stream interrupted',
|
|
885
|
-
true
|
|
886
|
-
);
|
|
887
|
-
yield errorChunk;
|
|
888
|
-
onChunk?.(errorChunk);
|
|
889
|
-
return undefined;
|
|
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 });
|
|
890
1729
|
}
|
|
891
1730
|
}
|
|
892
1731
|
|
|
893
|
-
//
|
|
894
|
-
|
|
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)
|
|
895
1750
|
if (isV22Envelope(response)) {
|
|
896
1751
|
if (response.ok) {
|
|
897
1752
|
return {
|
|
@@ -911,6 +1766,7 @@ function parseEnvelopeResponseLocal(response: EnvelopeResponse<unknown>, raw: st
|
|
|
911
1766
|
}
|
|
912
1767
|
}
|
|
913
1768
|
|
|
1769
|
+
// v2.1 format
|
|
914
1770
|
if (response.ok) {
|
|
915
1771
|
const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
|
|
916
1772
|
return {
|
|
@@ -933,44 +1789,10 @@ function parseEnvelopeResponseLocal(response: EnvelopeResponse<unknown>, raw: st
|
|
|
933
1789
|
}
|
|
934
1790
|
}
|
|
935
1791
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (isV22Envelope(response)) {
|
|
941
|
-
return response;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (response.ok) {
|
|
945
|
-
const data = (response.data ?? {}) as Record<string, unknown>;
|
|
946
|
-
const confidence = (data.confidence as number) ?? 0.5;
|
|
947
|
-
const rationale = (data.rationale as string) ?? '';
|
|
948
|
-
|
|
949
|
-
return {
|
|
950
|
-
ok: true,
|
|
951
|
-
meta: {
|
|
952
|
-
confidence,
|
|
953
|
-
risk: aggregateRisk(data, riskRule),
|
|
954
|
-
explain: rationale.slice(0, 280) || 'No explanation provided'
|
|
955
|
-
},
|
|
956
|
-
data: data as ModuleResultData
|
|
957
|
-
};
|
|
958
|
-
} else {
|
|
959
|
-
const errorMsg = response.error?.message ?? 'Unknown error';
|
|
960
|
-
return {
|
|
961
|
-
ok: false,
|
|
962
|
-
meta: {
|
|
963
|
-
confidence: 0,
|
|
964
|
-
risk: 'high',
|
|
965
|
-
explain: errorMsg.slice(0, 280)
|
|
966
|
-
},
|
|
967
|
-
error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
|
|
968
|
-
partial_data: response.partial_data
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function parseLegacyResponseLocal(output: unknown, raw: string): ModuleResult {
|
|
1792
|
+
/**
|
|
1793
|
+
* Parse legacy (non-envelope) format response
|
|
1794
|
+
*/
|
|
1795
|
+
function parseLegacyResponse(output: unknown, raw: string): ModuleResult {
|
|
974
1796
|
const outputObj = output as Record<string, unknown>;
|
|
975
1797
|
const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
|
|
976
1798
|
const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
|
|
@@ -978,6 +1800,7 @@ function parseLegacyResponseLocal(output: unknown, raw: string): ModuleResult {
|
|
|
978
1800
|
? outputObj.behavior_equivalence
|
|
979
1801
|
: undefined;
|
|
980
1802
|
|
|
1803
|
+
// Check if this is an error response (has error.code)
|
|
981
1804
|
if (outputObj.error && typeof outputObj.error === 'object') {
|
|
982
1805
|
const errorObj = outputObj.error as Record<string, unknown>;
|
|
983
1806
|
if (typeof errorObj.code === 'string') {
|
|
@@ -992,6 +1815,7 @@ function parseLegacyResponseLocal(output: unknown, raw: string): ModuleResult {
|
|
|
992
1815
|
}
|
|
993
1816
|
}
|
|
994
1817
|
|
|
1818
|
+
// Return as v2.1 format (data includes confidence)
|
|
995
1819
|
return {
|
|
996
1820
|
ok: true,
|
|
997
1821
|
data: {
|
|
@@ -1004,286 +1828,156 @@ function parseLegacyResponseLocal(output: unknown, raw: string): ModuleResult {
|
|
|
1004
1828
|
} as ModuleResultV21;
|
|
1005
1829
|
}
|
|
1006
1830
|
|
|
1007
|
-
// =============================================================================
|
|
1008
|
-
// v2.5 Multimodal Support
|
|
1009
|
-
// =============================================================================
|
|
1010
|
-
|
|
1011
|
-
interface ExtractedMedia {
|
|
1012
|
-
images: MediaInput[];
|
|
1013
|
-
audio: MediaInput[];
|
|
1014
|
-
video: MediaInput[];
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Extract media inputs from module input data
|
|
1019
|
-
*/
|
|
1020
|
-
function extractMediaInputs(input: ModuleInput): ExtractedMedia {
|
|
1021
|
-
const images: MediaInput[] = [];
|
|
1022
|
-
const audio: MediaInput[] = [];
|
|
1023
|
-
const video: MediaInput[] = [];
|
|
1024
|
-
|
|
1025
|
-
// Check for images array
|
|
1026
|
-
if (Array.isArray(input.images)) {
|
|
1027
|
-
for (const img of input.images) {
|
|
1028
|
-
if (isValidMediaInput(img)) {
|
|
1029
|
-
images.push(img);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Check for audio array
|
|
1035
|
-
if (Array.isArray(input.audio)) {
|
|
1036
|
-
for (const aud of input.audio) {
|
|
1037
|
-
if (isValidMediaInput(aud)) {
|
|
1038
|
-
audio.push(aud);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Check for video array
|
|
1044
|
-
if (Array.isArray(input.video)) {
|
|
1045
|
-
for (const vid of input.video) {
|
|
1046
|
-
if (isValidMediaInput(vid)) {
|
|
1047
|
-
video.push(vid);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return { images, audio, video };
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
* Validate media input structure
|
|
1057
|
-
*/
|
|
1058
|
-
function isValidMediaInput(input: unknown): input is MediaInput {
|
|
1059
|
-
if (typeof input !== 'object' || input === null) return false;
|
|
1060
|
-
const obj = input as Record<string, unknown>;
|
|
1061
|
-
|
|
1062
|
-
if (obj.type === 'url' && typeof obj.url === 'string') return true;
|
|
1063
|
-
if (obj.type === 'base64' && typeof obj.data === 'string' && typeof obj.media_type === 'string') return true;
|
|
1064
|
-
if (obj.type === 'file' && typeof obj.path === 'string') return true;
|
|
1065
|
-
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Build prompt with media placeholders
|
|
1071
|
-
*/
|
|
1072
|
-
function buildPromptWithMedia(
|
|
1073
|
-
module: CognitiveModule,
|
|
1074
|
-
input: ModuleInput,
|
|
1075
|
-
media: ExtractedMedia
|
|
1076
|
-
): string {
|
|
1077
|
-
let prompt = buildPrompt(module, input);
|
|
1078
|
-
|
|
1079
|
-
// Replace $MEDIA_INPUTS placeholder
|
|
1080
|
-
if (prompt.includes('$MEDIA_INPUTS')) {
|
|
1081
|
-
const mediaSummary = buildMediaSummary(media);
|
|
1082
|
-
prompt = prompt.replace(/\$MEDIA_INPUTS/g, mediaSummary);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
return prompt;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Build summary of media inputs for prompt
|
|
1090
|
-
*/
|
|
1091
|
-
function buildMediaSummary(media: ExtractedMedia): string {
|
|
1092
|
-
const parts: string[] = [];
|
|
1093
|
-
|
|
1094
|
-
if (media.images.length > 0) {
|
|
1095
|
-
parts.push(`[${media.images.length} image(s) attached]`);
|
|
1096
|
-
}
|
|
1097
|
-
if (media.audio.length > 0) {
|
|
1098
|
-
parts.push(`[${media.audio.length} audio file(s) attached]`);
|
|
1099
|
-
}
|
|
1100
|
-
if (media.video.length > 0) {
|
|
1101
|
-
parts.push(`[${media.video.length} video file(s) attached]`);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
return parts.length > 0 ? parts.join('\n') : '[No media attached]';
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1831
|
/**
|
|
1108
|
-
* Build
|
|
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)
|
|
1109
1839
|
*/
|
|
1110
|
-
function
|
|
1111
|
-
module
|
|
1112
|
-
shouldUseEnvelope: boolean,
|
|
1113
|
-
shouldUseV22: boolean
|
|
1114
|
-
): string[] {
|
|
1115
|
-
const systemParts: string[] = [
|
|
1116
|
-
`You are executing the "${module.name}" Cognitive Module.`,
|
|
1117
|
-
'',
|
|
1118
|
-
`RESPONSIBILITY: ${module.responsibility}`,
|
|
1119
|
-
];
|
|
1840
|
+
function buildPrompt(module: CognitiveModule, input: ModuleInput): string {
|
|
1841
|
+
let prompt = module.prompt;
|
|
1120
1842
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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);
|
|
1124
1847
|
}
|
|
1125
1848
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
if (module.constraints.no_network) systemParts.push('- No network access');
|
|
1129
|
-
if (module.constraints.no_side_effects) systemParts.push('- No side effects');
|
|
1130
|
-
if (module.constraints.no_file_write) systemParts.push('- No file writes');
|
|
1131
|
-
if (module.constraints.no_inventing_data) systemParts.push('- Do not invent data');
|
|
1132
|
-
}
|
|
1849
|
+
// v1 compatibility: get args value
|
|
1850
|
+
const argsValue = input.code || input.query || '';
|
|
1133
1851
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
}
|
|
1141
1863
|
}
|
|
1142
1864
|
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
const inputModalities = getModuleInputModalities(module);
|
|
1146
|
-
systemParts.push('', 'MULTIMODAL INPUT:');
|
|
1147
|
-
systemParts.push(`- This module accepts: ${inputModalities.join(', ')}`);
|
|
1148
|
-
systemParts.push('- Analyze any attached media carefully');
|
|
1149
|
-
systemParts.push('- Reference specific elements from the media in your analysis');
|
|
1150
|
-
}
|
|
1865
|
+
// Replace $ARGUMENTS LAST (after indexed forms to avoid partial matches)
|
|
1866
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
|
|
1151
1867
|
|
|
1152
|
-
//
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
|
|
1158
|
-
systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
1159
|
-
systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
|
|
1160
|
-
systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
|
|
1161
|
-
} else {
|
|
1162
|
-
systemParts.push('', 'RESPONSE FORMAT (Envelope):');
|
|
1163
|
-
systemParts.push('- Wrap your response in the envelope format');
|
|
1164
|
-
systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
|
|
1165
|
-
systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
|
|
1166
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
|
|
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';
|
|
1167
1873
|
}
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1874
|
+
if (input.query) {
|
|
1875
|
+
prompt += input.query + '\n';
|
|
1170
1876
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
systemParts.push('- Respond with ONLY valid JSON');
|
|
1174
|
-
systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
|
|
1175
|
-
if (module.output?.require_behavior_equivalence) {
|
|
1176
|
-
systemParts.push('- Include "behavior_equivalence" (boolean) field');
|
|
1877
|
+
if (input.language) {
|
|
1878
|
+
prompt += `\nLanguage: ${input.language}\n`;
|
|
1177
1879
|
}
|
|
1178
1880
|
}
|
|
1179
1881
|
|
|
1180
|
-
return
|
|
1882
|
+
return prompt;
|
|
1181
1883
|
}
|
|
1182
1884
|
|
|
1183
1885
|
/**
|
|
1184
|
-
*
|
|
1886
|
+
* Heuristic to detect if input looks like code
|
|
1185
1887
|
*/
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const media_type = getMediaTypeFromExtension(extname(path));
|
|
1195
|
-
|
|
1196
|
-
return { data, media_type };
|
|
1197
|
-
} catch {
|
|
1198
|
-
return null;
|
|
1199
|
-
}
|
|
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));
|
|
1200
1896
|
}
|
|
1201
1897
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
'.gif': 'image/gif',
|
|
1211
|
-
'.webp': 'image/webp',
|
|
1212
|
-
'.mp3': 'audio/mpeg',
|
|
1213
|
-
'.wav': 'audio/wav',
|
|
1214
|
-
'.ogg': 'audio/ogg',
|
|
1215
|
-
'.webm': 'audio/webm',
|
|
1216
|
-
'.mp4': 'video/mp4',
|
|
1217
|
-
'.mov': 'video/quicktime',
|
|
1218
|
-
'.pdf': 'application/pdf'
|
|
1219
|
-
};
|
|
1220
|
-
|
|
1221
|
-
return mimeTypes[ext.toLowerCase()] ?? 'application/octet-stream';
|
|
1898
|
+
// =============================================================================
|
|
1899
|
+
// Legacy API (for backward compatibility)
|
|
1900
|
+
// =============================================================================
|
|
1901
|
+
|
|
1902
|
+
export interface RunModuleLegacyOptions {
|
|
1903
|
+
validateInput?: boolean;
|
|
1904
|
+
validateOutput?: boolean;
|
|
1905
|
+
model?: string;
|
|
1222
1906
|
}
|
|
1223
1907
|
|
|
1224
1908
|
/**
|
|
1225
|
-
*
|
|
1909
|
+
* Run a cognitive module (legacy API, returns raw output).
|
|
1910
|
+
* For backward compatibility. Throws on error instead of returning error envelope.
|
|
1226
1911
|
*/
|
|
1227
|
-
export function
|
|
1228
|
-
media: MediaInput,
|
|
1912
|
+
export async function runModuleLegacy(
|
|
1229
1913
|
module: CognitiveModule,
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
valid: false,
|
|
1236
|
-
error: 'Module does not support multimodal input',
|
|
1237
|
-
code: ErrorCodesV25.MULTIMODAL_NOT_SUPPORTED
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// Validate media type
|
|
1242
|
-
if (media.type === 'base64') {
|
|
1243
|
-
const mediaType = (media as { media_type: string }).media_type;
|
|
1244
|
-
if (!isValidMediaType(mediaType)) {
|
|
1245
|
-
return {
|
|
1246
|
-
valid: false,
|
|
1247
|
-
error: `Unsupported media type: ${mediaType}`,
|
|
1248
|
-
code: ErrorCodesV25.UNSUPPORTED_MEDIA_TYPE
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1914
|
+
provider: Provider,
|
|
1915
|
+
input: ModuleInput,
|
|
1916
|
+
options: RunModuleLegacyOptions = {}
|
|
1917
|
+
): Promise<unknown> {
|
|
1918
|
+
const { validateInput = true, validateOutput = true, model } = options;
|
|
1252
1919
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1920
|
+
const result = await runModule(module, provider, {
|
|
1921
|
+
input,
|
|
1922
|
+
validateInput,
|
|
1923
|
+
validateOutput,
|
|
1924
|
+
useEnvelope: false,
|
|
1925
|
+
useV22: false,
|
|
1926
|
+
model,
|
|
1927
|
+
});
|
|
1255
1928
|
|
|
1256
|
-
|
|
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
|
+
}
|
|
1257
1935
|
}
|
|
1258
1936
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
function isValidMediaType(mediaType: string): boolean {
|
|
1263
|
-
const supported = [
|
|
1264
|
-
'image/jpeg', 'image/png', 'image/webp', 'image/gif',
|
|
1265
|
-
'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm',
|
|
1266
|
-
'video/mp4', 'video/webm', 'video/quicktime',
|
|
1267
|
-
'application/pdf'
|
|
1268
|
-
];
|
|
1269
|
-
|
|
1270
|
-
return supported.includes(mediaType);
|
|
1271
|
-
}
|
|
1937
|
+
// =============================================================================
|
|
1938
|
+
// Convenience Functions
|
|
1939
|
+
// =============================================================================
|
|
1272
1940
|
|
|
1273
1941
|
/**
|
|
1274
|
-
*
|
|
1942
|
+
* Extract meta from v2.2 envelope for routing/logging.
|
|
1275
1943
|
*/
|
|
1276
|
-
export function
|
|
1277
|
-
return
|
|
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
|
+
};
|
|
1278
1950
|
}
|
|
1279
1951
|
|
|
1952
|
+
// Alias for backward compatibility
|
|
1953
|
+
export const extractMetaV22 = extractMeta;
|
|
1954
|
+
|
|
1280
1955
|
/**
|
|
1281
|
-
*
|
|
1956
|
+
* Determine if result should be escalated to human review based on meta.
|
|
1282
1957
|
*/
|
|
1283
|
-
export function
|
|
1284
|
-
|
|
1285
|
-
|
|
1958
|
+
export function shouldEscalate(
|
|
1959
|
+
result: EnvelopeResponseV22<unknown>,
|
|
1960
|
+
confidenceThreshold: number = 0.7
|
|
1286
1961
|
): boolean {
|
|
1287
|
-
const
|
|
1288
|
-
|
|
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;
|
|
1289
1980
|
}
|
|
1981
|
+
|
|
1982
|
+
// Alias for backward compatibility
|
|
1983
|
+
export const shouldEscalateV22 = shouldEscalate;
|