cognitive-modules-cli 2.2.11 → 2.2.12

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.
@@ -9,10 +9,37 @@ import * as fs from 'node:fs/promises';
9
9
  import * as path from 'node:path';
10
10
  import { aggregateRisk, isV22Envelope } from '../types.js';
11
11
  import { readModuleProvenance, verifyModuleIntegrity } from '../provenance.js';
12
+ import { extractJsonCandidates } from './json-extract.js';
13
+ import { compactReason, formatPolicySummaryLine } from '../policy-summary.js';
12
14
  // =============================================================================
13
15
  // Schema Validation
14
16
  // =============================================================================
15
17
  const ajv = new Ajv({ allErrors: true, strict: false });
18
+ function safeSnippet(s, max = 500) {
19
+ const raw = String(s ?? '');
20
+ if (raw.length <= max)
21
+ return raw;
22
+ return raw.slice(0, max) + `…(+${raw.length - max} chars)`;
23
+ }
24
+ function parseJsonWithCandidates(raw) {
25
+ const candidates = extractJsonCandidates(raw);
26
+ const attempts = [];
27
+ for (const c of candidates) {
28
+ try {
29
+ const parsed = JSON.parse(c.json.trim());
30
+ return { parsed, extracted: c, attempts };
31
+ }
32
+ catch (e) {
33
+ attempts.push({ strategy: c.strategy, error: e.message });
34
+ }
35
+ }
36
+ const err = new Error(attempts[0]?.error ?? 'Unable to parse JSON');
37
+ err.details = {
38
+ parse_attempts: attempts,
39
+ raw_response_snippet: safeSnippet(raw, 500),
40
+ };
41
+ throw err;
42
+ }
16
43
  /**
17
44
  * Validate data against JSON schema. Returns list of errors.
18
45
  */
@@ -1061,7 +1088,7 @@ async function enforcePolicyGates(module, policy) {
1061
1088
  explain: 'Refused by execution policy.',
1062
1089
  confidence: 1.0,
1063
1090
  risk: 'none',
1064
- suggestion: 'Migrate the module to v2.2, or run with --profile strict/default.',
1091
+ suggestion: 'Migrate the module to v2.2, or rerun with --profile standard.',
1065
1092
  });
1066
1093
  }
1067
1094
  }
@@ -1088,7 +1115,7 @@ async function enforcePolicyGates(module, policy) {
1088
1115
  explain: 'Refused by execution policy.',
1089
1116
  confidence: 1.0,
1090
1117
  risk: 'none',
1091
- suggestion: 'Install the module via `cog add <name>` (registry tarball) or use --profile strict/default.',
1118
+ suggestion: 'Install the module via `cog add <name>` (registry tarball) or rerun with --profile standard.',
1092
1119
  });
1093
1120
  }
1094
1121
  }
@@ -1120,7 +1147,7 @@ async function enforcePolicyGates(module, policy) {
1120
1147
  explain: 'Refused by execution policy.',
1121
1148
  confidence: 1.0,
1122
1149
  risk: 'none',
1123
- suggestion: 'Reinstall the module from a registry tarball and retry, or use --profile strict/default.',
1150
+ suggestion: 'Reinstall the module from a registry tarball and retry, or rerun with --profile standard.',
1124
1151
  });
1125
1152
  }
1126
1153
  // Integrity check (tamper detection).
@@ -1142,11 +1169,209 @@ async function enforcePolicyGates(module, policy) {
1142
1169
  return null;
1143
1170
  return null;
1144
1171
  }
1172
+ function resolveValidationFlags(module, policy, validateInputOpt, validateOutputOpt) {
1173
+ // Explicit overrides win.
1174
+ if (typeof validateInputOpt === 'boolean' || typeof validateOutputOpt === 'boolean') {
1175
+ const validateInput = typeof validateInputOpt === 'boolean' ? validateInputOpt : true;
1176
+ const validateOutput = typeof validateOutputOpt === 'boolean' ? validateOutputOpt : true;
1177
+ return { validateInput, validateOutput, reason: 'explicit validateInput/validateOutput' };
1178
+ }
1179
+ if (!policy) {
1180
+ return { validateInput: true, validateOutput: true, reason: 'no policy (default on)' };
1181
+ }
1182
+ if (policy.validate === 'off') {
1183
+ return { validateInput: false, validateOutput: false, reason: 'policy.validate=off' };
1184
+ }
1185
+ if (policy.validate === 'on') {
1186
+ return { validateInput: true, validateOutput: true, reason: 'policy.validate=on' };
1187
+ }
1188
+ // auto: decide based on module intent.
1189
+ const tier = module.tier ?? 'decision';
1190
+ const strictness = module.schemaStrictness ?? 'medium';
1191
+ // Certified flows already set policy.validate=on in resolveExecutionPolicy().
1192
+ // Keep auto conservative for exec/decision.
1193
+ // For exploration: do not validate inputs by default, but still validate outputs post-hoc.
1194
+ // This preserves "5-minute path" ergonomics while keeping envelopes structurally reliable.
1195
+ if (strictness === 'high') {
1196
+ return { validateInput: true, validateOutput: true, reason: `auto: schema_strictness=high (tier=${tier})` };
1197
+ }
1198
+ if (tier === 'exploration') {
1199
+ return { validateInput: false, validateOutput: true, reason: 'auto: tier=exploration (post-hoc output only)' };
1200
+ }
1201
+ // exec + decision default to validation on.
1202
+ return { validateInput: true, validateOutput: true, reason: `auto: tier=${tier}` };
1203
+ }
1204
+ function getProviderCapabilities(provider) {
1205
+ const caps = provider.getCapabilities?.();
1206
+ if (caps)
1207
+ return caps;
1208
+ return {
1209
+ structuredOutput: 'prompt',
1210
+ streaming: provider.supportsStreaming?.() ?? false,
1211
+ };
1212
+ }
1213
+ function providerSupportsNativeStructuredOutput(caps) {
1214
+ return caps.structuredOutput === 'native';
1215
+ }
1216
+ function providerSupportsNativeJsonSchema(caps) {
1217
+ if (!providerSupportsNativeStructuredOutput(caps))
1218
+ return false;
1219
+ const dialect = caps.nativeSchemaDialect ?? 'json-schema';
1220
+ return dialect === 'json-schema';
1221
+ }
1222
+ function schemaByteLength(schema) {
1223
+ try {
1224
+ return Buffer.byteLength(JSON.stringify(schema), 'utf8');
1225
+ }
1226
+ catch {
1227
+ // If something is non-serializable, treat as huge and disable native.
1228
+ return Number.MAX_SAFE_INTEGER;
1229
+ }
1230
+ }
1231
+ function resolveJsonSchemaParams(module, provider, validateOutput, structured) {
1232
+ if (!validateOutput)
1233
+ return {};
1234
+ if (!module.outputSchema)
1235
+ return {};
1236
+ const pref = structured ?? 'auto';
1237
+ if (pref === 'off')
1238
+ return { policy: { requested: pref, resolved: 'off', reason: 'structured=off' } };
1239
+ if (pref === 'prompt') {
1240
+ return { jsonSchema: module.outputSchema, jsonSchemaMode: 'prompt', allowSchemaFallback: false, policy: { requested: pref, resolved: 'prompt', reason: 'structured=prompt' } };
1241
+ }
1242
+ const caps = getProviderCapabilities(provider);
1243
+ if (caps.structuredOutput === 'none')
1244
+ return {};
1245
+ if (pref === 'native') {
1246
+ // "native" means "prefer native", not "fail hard".
1247
+ // If the provider doesn't support native structured output at all, safely downgrade to prompt guidance.
1248
+ //
1249
+ // Important: "native" only covers JSON Schema-native providers. For providers whose schema dialect is
1250
+ // not JSON Schema (e.g. Gemini responseSchema), we do NOT attempt to translate arbitrary JSON Schema
1251
+ // into the provider dialect (too many subset incompatibilities).
1252
+ //
1253
+ // Instead, we safely downgrade to prompt-only JSON guidance and rely on post-validation. This avoids
1254
+ // user-visible 400s and keeps the protocol contract stable.
1255
+ if (!providerSupportsNativeStructuredOutput(caps)) {
1256
+ const dialect = caps.nativeSchemaDialect ?? 'unknown';
1257
+ return {
1258
+ jsonSchema: module.outputSchema,
1259
+ jsonSchemaMode: 'prompt',
1260
+ allowSchemaFallback: false,
1261
+ policy: {
1262
+ requested: pref,
1263
+ resolved: 'prompt',
1264
+ reason: `provider lacks native structured output (${caps.structuredOutput}); dialect=${dialect}`,
1265
+ },
1266
+ };
1267
+ }
1268
+ const dialect = caps.nativeSchemaDialect ?? 'json-schema';
1269
+ if (dialect !== 'json-schema') {
1270
+ return {
1271
+ jsonSchema: module.outputSchema,
1272
+ jsonSchemaMode: 'prompt',
1273
+ allowSchemaFallback: false,
1274
+ policy: {
1275
+ requested: pref,
1276
+ resolved: 'prompt',
1277
+ reason: `native schema dialect is not JSON Schema (${dialect}); using prompt-only guidance`,
1278
+ },
1279
+ };
1280
+ }
1281
+ const maxBytes = caps.maxNativeSchemaBytes;
1282
+ if (typeof maxBytes === 'number' && maxBytes > 0) {
1283
+ const bytes = schemaByteLength(module.outputSchema);
1284
+ if (bytes > maxBytes) {
1285
+ return {
1286
+ jsonSchema: module.outputSchema,
1287
+ jsonSchemaMode: 'prompt',
1288
+ allowSchemaFallback: false,
1289
+ policy: { requested: pref, resolved: 'prompt', reason: `native schema too large (${bytes}B > ${maxBytes}B)` },
1290
+ };
1291
+ }
1292
+ }
1293
+ // Some providers accept only a restricted schema subset and can reject otherwise-valid JSON Schema.
1294
+ // Retrying once in prompt mode yields a much better UX while still keeping the initial attempt "native".
1295
+ return {
1296
+ jsonSchema: module.outputSchema,
1297
+ jsonSchemaMode: 'native',
1298
+ allowSchemaFallback: true,
1299
+ policy: {
1300
+ requested: pref,
1301
+ resolved: 'native',
1302
+ reason: 'structured=native',
1303
+ },
1304
+ };
1305
+ }
1306
+ // auto: choose based on provider capabilities.
1307
+ let jsonSchemaMode = providerSupportsNativeJsonSchema(caps) ? 'native' : 'prompt';
1308
+ let sizeReason = null;
1309
+ if (jsonSchemaMode === 'native') {
1310
+ const maxBytes = caps.maxNativeSchemaBytes;
1311
+ if (typeof maxBytes === 'number' && maxBytes > 0) {
1312
+ const bytes = schemaByteLength(module.outputSchema);
1313
+ if (bytes > maxBytes) {
1314
+ jsonSchemaMode = 'prompt';
1315
+ sizeReason = `native schema too large (${bytes}B > ${maxBytes}B)`;
1316
+ }
1317
+ }
1318
+ }
1319
+ return {
1320
+ jsonSchema: module.outputSchema,
1321
+ jsonSchemaMode,
1322
+ allowSchemaFallback: true,
1323
+ policy: {
1324
+ requested: pref,
1325
+ resolved: jsonSchemaMode,
1326
+ reason: sizeReason
1327
+ ? `auto: ${sizeReason}`
1328
+ : providerSupportsNativeJsonSchema(caps)
1329
+ ? `auto: native JSON Schema supported`
1330
+ : caps.structuredOutput === 'native'
1331
+ ? `auto: native schema dialect is not JSON Schema (${caps.nativeSchemaDialect ?? 'unknown'})`
1332
+ : `auto: provider structuredOutput=${caps.structuredOutput}`,
1333
+ },
1334
+ };
1335
+ }
1336
+ function isSchemaCompatibilityError(e) {
1337
+ const msg = e instanceof Error ? e.message : String(e ?? '');
1338
+ return (msg.includes('responseSchema') ||
1339
+ msg.includes('response_schema') ||
1340
+ msg.includes('Invalid JSON payload') ||
1341
+ msg.includes('INVALID_ARGUMENT') ||
1342
+ msg.includes('Unknown name') ||
1343
+ msg.includes('should be non-empty for OBJECT type'));
1344
+ }
1345
+ function resolveStructuredSchemaPlan(module, provider, validateOutput, structuredPref, policy) {
1346
+ // If output validation is disabled, never pass schema hints to providers.
1347
+ if (!validateOutput)
1348
+ return {};
1349
+ if (!module.outputSchema)
1350
+ return {};
1351
+ const requested = structuredPref ?? 'auto';
1352
+ // Progressive Complexity trigger:
1353
+ // When validate is `auto` and tier=exploration, default to "post-hoc validation only":
1354
+ // do not enforce/guide with schemas at the provider layer unless the user explicitly opts in.
1355
+ const tier = module.tier ?? 'decision';
1356
+ const strictness = module.schemaStrictness ?? 'medium';
1357
+ if (requested === 'auto' && policy?.validate === 'auto' && tier === 'exploration' && strictness !== 'high') {
1358
+ return {
1359
+ policy: {
1360
+ requested,
1361
+ resolved: 'off',
1362
+ reason: 'auto: tier=exploration defaults to post-hoc validation (no provider schema hints)',
1363
+ },
1364
+ };
1365
+ }
1366
+ return resolveJsonSchemaParams(module, provider, validateOutput, requested);
1367
+ }
1145
1368
  // =============================================================================
1146
1369
  // Main Runner
1147
1370
  // =============================================================================
1148
1371
  export async function runModule(module, provider, options = {}) {
1149
- const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride, policy } = options;
1372
+ const { args, input, verbose = false, validateInput: validateInputOpt, validateOutput: validateOutputOpt, useEnvelope, useV22, enableRepair: enableRepairOpt, traceId, model: modelOverride, policy, structured: structuredOverride, } = options;
1373
+ const { validateInput, validateOutput, reason: validateReason } = resolveValidationFlags(module, policy, validateInputOpt, validateOutputOpt);
1374
+ const enableRepair = enableRepairOpt ?? policy?.enableRepair ?? true;
1150
1375
  const startTime = Date.now();
1151
1376
  const gate = await enforcePolicyGates(module, policy);
1152
1377
  if (gate) {
@@ -1204,11 +1429,34 @@ export async function runModule(module, provider, options = {}) {
1204
1429
  }
1205
1430
  // Build prompt with clean substitution
1206
1431
  const prompt = buildPrompt(module, inputData);
1432
+ const effectiveStructuredPref = structuredOverride ?? policy?.structured;
1433
+ const structuredPlan = resolveStructuredSchemaPlan(module, provider, validateOutput, effectiveStructuredPref, policy);
1207
1434
  if (verbose) {
1208
1435
  console.error('--- Module ---');
1209
1436
  console.error(`Name: ${module.name} (${module.format})`);
1210
1437
  console.error(`Responsibility: ${module.responsibility}`);
1211
1438
  console.error(`Envelope: ${shouldUseEnvelope}`);
1439
+ if (policy) {
1440
+ console.error('--- Policy ---');
1441
+ const requested = effectiveStructuredPref ?? 'auto';
1442
+ const applied = structuredPlan.jsonSchemaMode ?? 'off';
1443
+ const sReason = structuredPlan.policy?.reason ??
1444
+ (structuredPlan.jsonSchemaMode ? 'auto: schema hints enabled' : 'auto: schema hints disabled');
1445
+ console.error(formatPolicySummaryLine(policy, { validateInput, validateOutput, reason: validateReason }, { requested, applied, reason: sReason }, { enableRepair, requireV22: policy.requireV22 }));
1446
+ console.error(JSON.stringify({
1447
+ profile: policy.profile,
1448
+ validate: policy.validate,
1449
+ validateInput,
1450
+ validateOutput,
1451
+ validate_reason: validateReason,
1452
+ audit: policy.audit,
1453
+ enableRepair,
1454
+ structured: requested,
1455
+ structured_effective: applied,
1456
+ structured_reason: structuredPlan.policy?.reason ?? null,
1457
+ requireV22: policy.requireV22,
1458
+ }, null, 2));
1459
+ }
1212
1460
  console.error('--- Input ---');
1213
1461
  console.error(JSON.stringify(inputData, null, 2));
1214
1462
  console.error('--- Prompt ---');
@@ -1278,11 +1526,58 @@ export async function runModule(module, provider, options = {}) {
1278
1526
  ];
1279
1527
  try {
1280
1528
  // Invoke provider
1281
- const result = await provider.invoke({
1282
- messages,
1283
- jsonSchema: module.outputSchema,
1284
- temperature: 0.3,
1285
- });
1529
+ const { allowSchemaFallback, policy: structuredPolicy, ...invokeSchemaParams } = structuredPlan;
1530
+ const invokeParams = { ...invokeSchemaParams };
1531
+ const capsSnapshot = getProviderCapabilities(provider);
1532
+ const plannedSchemaMode = invokeParams.jsonSchema && typeof invokeParams.jsonSchema === 'object'
1533
+ ? (invokeParams.jsonSchemaMode ?? 'prompt')
1534
+ : 'off';
1535
+ let appliedSchemaMode = plannedSchemaMode;
1536
+ let schemaFallbackAttempted = false;
1537
+ let schemaFallbackReason = null;
1538
+ let result;
1539
+ try {
1540
+ result = await provider.invoke({
1541
+ messages,
1542
+ // Progressive Complexity: only enforce schema at the provider layer when validation is enabled.
1543
+ ...invokeParams,
1544
+ temperature: 0.3,
1545
+ });
1546
+ }
1547
+ catch (e) {
1548
+ // If the provider rejects native structured output schemas, retry once in prompt mode.
1549
+ if (allowSchemaFallback &&
1550
+ invokeParams.jsonSchema &&
1551
+ invokeParams.jsonSchemaMode === 'native' &&
1552
+ isSchemaCompatibilityError(e)) {
1553
+ schemaFallbackAttempted = true;
1554
+ schemaFallbackReason = compactReason(e instanceof Error ? e.message : String(e ?? ''), 180);
1555
+ invokeParams.jsonSchemaMode = 'prompt';
1556
+ appliedSchemaMode = 'prompt';
1557
+ result = await provider.invoke({
1558
+ messages,
1559
+ ...invokeParams,
1560
+ temperature: 0.3,
1561
+ });
1562
+ }
1563
+ else {
1564
+ throw e;
1565
+ }
1566
+ }
1567
+ const structuredPolicyMeta = structuredPolicy
1568
+ ? {
1569
+ ...structuredPolicy,
1570
+ planned: structuredPolicy.resolved,
1571
+ applied: appliedSchemaMode,
1572
+ downgraded: appliedSchemaMode !== structuredPolicy.resolved,
1573
+ fallback: schemaFallbackAttempted ? { attempted: true, reason: schemaFallbackReason ?? undefined } : { attempted: false },
1574
+ provider: {
1575
+ structuredOutput: capsSnapshot.structuredOutput,
1576
+ nativeSchemaDialect: capsSnapshot.nativeSchemaDialect,
1577
+ maxNativeSchemaBytes: capsSnapshot.maxNativeSchemaBytes,
1578
+ },
1579
+ }
1580
+ : undefined;
1286
1581
  if (verbose) {
1287
1582
  console.error('--- Response ---');
1288
1583
  console.error(result.content);
@@ -1292,21 +1587,87 @@ export async function runModule(module, provider, options = {}) {
1292
1587
  const latencyMs = Date.now() - startTime;
1293
1588
  // Parse response
1294
1589
  let parsed;
1590
+ let parseExtracted = null;
1591
+ let parseAttempts = [];
1592
+ let parseRetries = 0;
1295
1593
  try {
1296
- const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1297
- const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
1298
- parsed = JSON.parse(jsonStr.trim());
1594
+ const r = parseJsonWithCandidates(result.content);
1595
+ parsed = r.parsed;
1596
+ parseExtracted = r.extracted;
1597
+ parseAttempts = r.attempts;
1299
1598
  }
1300
1599
  catch (e) {
1301
- const errorResult = makeErrorResponse({
1302
- code: 'E1000', // PARSE_ERROR
1303
- message: `Failed to parse JSON response: ${e.message}`,
1304
- explain: 'Failed to parse LLM response as JSON.',
1305
- details: { raw_response: result.content.substring(0, 500) },
1306
- suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
1307
- });
1308
- _invokeErrorHooks(module.name, e, null);
1309
- return errorResult;
1600
+ const allowParseRetry = policy?.profile !== 'certified';
1601
+ const firstDetails = e?.details;
1602
+ if (firstDetails && typeof firstDetails === 'object' && Array.isArray(firstDetails.parse_attempts)) {
1603
+ parseAttempts = firstDetails.parse_attempts;
1604
+ }
1605
+ if (!allowParseRetry) {
1606
+ const details = typeof firstDetails === 'object' && firstDetails
1607
+ ? firstDetails
1608
+ : { raw_response_snippet: safeSnippet(result.content, 500) };
1609
+ const errorResult = makeErrorResponse({
1610
+ code: 'E1000', // PARSE_ERROR
1611
+ message: `Failed to parse JSON response: ${e.message}`,
1612
+ explain: 'Failed to parse LLM response as JSON.',
1613
+ details: {
1614
+ ...details,
1615
+ parse_retry: { attempted: false, reason: 'profile=certified (fail-fast)' },
1616
+ },
1617
+ suggestion: 'The LLM response was not valid JSON. Fix the module/provider output or switch provider.',
1618
+ });
1619
+ _invokeErrorHooks(module.name, e, null);
1620
+ return errorResult;
1621
+ }
1622
+ // Retry once with stronger formatting instructions (prompt-only enforcement).
1623
+ parseRetries = 1;
1624
+ const retryMessages = [
1625
+ ...messages,
1626
+ {
1627
+ role: 'user',
1628
+ content: 'Your previous response was not valid JSON.\n\nReturn ONLY a single valid JSON value (no markdown, no code fences, no commentary, no trailing text).',
1629
+ },
1630
+ ];
1631
+ try {
1632
+ const retryResult = await provider.invoke({
1633
+ messages: retryMessages,
1634
+ ...invokeParams,
1635
+ temperature: 0.3,
1636
+ });
1637
+ if (verbose) {
1638
+ console.error('--- Response (retry) ---');
1639
+ console.error(retryResult.content);
1640
+ console.error('--- End Response (retry) ---');
1641
+ }
1642
+ const r2 = parseJsonWithCandidates(retryResult.content);
1643
+ parsed = r2.parsed;
1644
+ parseExtracted = r2.extracted;
1645
+ parseAttempts = [...parseAttempts, ...r2.attempts];
1646
+ }
1647
+ catch (e2) {
1648
+ const combinedAttempts = [
1649
+ ...parseAttempts,
1650
+ ...((typeof e2?.details === 'object' && e2?.details && Array.isArray(e2.details.parse_attempts))
1651
+ ? e2.details.parse_attempts
1652
+ : []),
1653
+ ];
1654
+ const details = typeof e2?.details === 'object' && e2?.details
1655
+ ? e2.details
1656
+ : { raw_response_snippet: safeSnippet(result.content, 500) };
1657
+ const errorResult = makeErrorResponse({
1658
+ code: 'E1000', // PARSE_ERROR
1659
+ message: `Failed to parse JSON response: ${e2.message}`,
1660
+ explain: 'Failed to parse LLM response as JSON.',
1661
+ details: {
1662
+ ...details,
1663
+ parse_attempts: combinedAttempts.length ? combinedAttempts : undefined,
1664
+ parse_retry: { attempted: true, count: 1 },
1665
+ },
1666
+ suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
1667
+ });
1668
+ _invokeErrorHooks(module.name, e2, null);
1669
+ return errorResult;
1670
+ }
1310
1671
  }
1311
1672
  // Convert to v2.2 envelope
1312
1673
  let response;
@@ -1323,6 +1684,37 @@ export async function runModule(module, provider, options = {}) {
1323
1684
  response.version = ENVELOPE_VERSION;
1324
1685
  if (response.meta) {
1325
1686
  response.meta.latency_ms = latencyMs;
1687
+ if (structuredPolicyMeta) {
1688
+ // Publish-grade parity: record how structured output was applied for this run.
1689
+ // This is intentionally small and stable, so users can reason about provider differences.
1690
+ response.meta.policy = {
1691
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
1692
+ structured: structuredPolicyMeta,
1693
+ };
1694
+ }
1695
+ if (policy) {
1696
+ response.meta.policy = {
1697
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
1698
+ validation: {
1699
+ mode: policy.validate,
1700
+ input: validateInput,
1701
+ output: validateOutput,
1702
+ reason: validateReason,
1703
+ },
1704
+ audit: { enabled: policy.audit === true },
1705
+ repair: { enabled: enableRepair === true },
1706
+ };
1707
+ }
1708
+ // Record parse strategy and retry count for publish-grade diagnostics.
1709
+ const includeParseAttempts = verbose || policy?.profile !== 'core';
1710
+ response.meta.policy = {
1711
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
1712
+ parse: {
1713
+ strategy: parseExtracted?.strategy ?? null,
1714
+ retries: parseRetries,
1715
+ attempts: includeParseAttempts && parseAttempts.length ? parseAttempts : undefined,
1716
+ },
1717
+ };
1326
1718
  if (traceId) {
1327
1719
  response.meta.trace_id = traceId;
1328
1720
  }
@@ -1451,7 +1843,9 @@ export async function runModule(module, provider, options = {}) {
1451
1843
  * }
1452
1844
  */
1453
1845
  export async function* runModuleStream(module, provider, options = {}) {
1454
- const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model, policy } = options;
1846
+ const { input, args, validateInput: validateInputOpt, validateOutput: validateOutputOpt, useV22 = true, enableRepair: enableRepairOpt, traceId, model, policy, structured: structuredOverride, } = options;
1847
+ const { validateInput, validateOutput, reason: validateReason } = resolveValidationFlags(module, policy, validateInputOpt, validateOutputOpt);
1848
+ const enableRepair = enableRepairOpt ?? policy?.enableRepair ?? true;
1455
1849
  const startTime = Date.now();
1456
1850
  const moduleName = module.name;
1457
1851
  const providerName = provider?.name;
@@ -1485,6 +1879,13 @@ export async function* runModuleStream(module, provider, options = {}) {
1485
1879
  inputData.query = args;
1486
1880
  }
1487
1881
  }
1882
+ // Single-file core modules promise "missing fields are empty".
1883
+ if (typeof module.location === 'string' && /\.(md|markdown)$/i.test(module.location)) {
1884
+ if (inputData.query === undefined)
1885
+ inputData.query = '';
1886
+ if (inputData.code === undefined)
1887
+ inputData.code = '';
1888
+ }
1488
1889
  _invokeBeforeHooks(module.name, inputData, module);
1489
1890
  // Validate input if enabled
1490
1891
  if (validateInput && module.inputSchema && Object.keys(module.inputSchema).length > 0) {
@@ -1508,6 +1909,8 @@ export async function* runModuleStream(module, provider, options = {}) {
1508
1909
  const riskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
1509
1910
  // Build prompt
1510
1911
  const prompt = buildPrompt(module, inputData);
1912
+ const effectiveStructuredPref = structuredOverride ?? policy?.structured;
1913
+ const structuredPlan = resolveStructuredSchemaPlan(module, provider, validateOutput, effectiveStructuredPref, policy);
1511
1914
  // Build messages
1512
1915
  const systemParts = [
1513
1916
  `You are executing the "${module.name}" Cognitive Module.`,
@@ -1526,52 +1929,188 @@ export async function* runModuleStream(module, provider, options = {}) {
1526
1929
  ];
1527
1930
  // Invoke provider with streaming if supported
1528
1931
  let fullContent;
1932
+ const { allowSchemaFallback, policy: structuredPolicy, ...invokeSchemaParams } = structuredPlan;
1933
+ const invokeParams = { ...invokeSchemaParams };
1934
+ const capsSnapshot = getProviderCapabilities(provider);
1935
+ const plannedSchemaMode = invokeParams.jsonSchema && typeof invokeParams.jsonSchema === 'object'
1936
+ ? (invokeParams.jsonSchemaMode ?? 'prompt')
1937
+ : 'off';
1938
+ let appliedSchemaMode = plannedSchemaMode;
1939
+ let schemaFallbackAttempted = false;
1940
+ let schemaFallbackReason = null;
1529
1941
  if (provider.supportsStreaming?.() && provider.invokeStream) {
1530
1942
  // Use true streaming
1531
1943
  const stream = provider.invokeStream({
1532
1944
  messages,
1533
- jsonSchema: module.outputSchema,
1945
+ ...invokeParams,
1534
1946
  temperature: 0.3,
1535
1947
  });
1536
1948
  // Iterate through the async generator, yielding chunks as they arrive
1537
1949
  let streamResult;
1538
- while (!(streamResult = await stream.next()).done) {
1539
- const chunk = streamResult.value;
1540
- yield makeEvent('delta', { delta: chunk });
1950
+ try {
1951
+ while (!(streamResult = await stream.next()).done) {
1952
+ const chunk = streamResult.value;
1953
+ yield makeEvent('delta', { delta: chunk });
1954
+ }
1955
+ // Get the final result (returned from the generator)
1956
+ fullContent = streamResult.value.content;
1957
+ }
1958
+ catch (e) {
1959
+ // If streaming fails (e.g., schema rejected), retry once non-streaming in prompt mode.
1960
+ if (allowSchemaFallback &&
1961
+ invokeParams.jsonSchema &&
1962
+ invokeParams.jsonSchemaMode === 'native' &&
1963
+ isSchemaCompatibilityError(e)) {
1964
+ schemaFallbackAttempted = true;
1965
+ schemaFallbackReason = compactReason(e instanceof Error ? e.message : String(e ?? ''), 180);
1966
+ invokeParams.jsonSchemaMode = 'prompt';
1967
+ appliedSchemaMode = 'prompt';
1968
+ const result = await provider.invoke({
1969
+ messages,
1970
+ ...invokeParams,
1971
+ temperature: 0.3,
1972
+ });
1973
+ fullContent = result.content;
1974
+ yield makeEvent('delta', { delta: result.content });
1975
+ }
1976
+ else {
1977
+ throw e;
1978
+ }
1541
1979
  }
1542
- // Get the final result (returned from the generator)
1543
- fullContent = streamResult.value.content;
1544
1980
  }
1545
1981
  else {
1546
1982
  // Fallback to non-streaming invoke
1547
- const result = await provider.invoke({
1548
- messages,
1549
- jsonSchema: module.outputSchema,
1550
- temperature: 0.3,
1551
- });
1983
+ let result;
1984
+ try {
1985
+ result = await provider.invoke({
1986
+ messages,
1987
+ ...invokeParams,
1988
+ temperature: 0.3,
1989
+ });
1990
+ }
1991
+ catch (e) {
1992
+ if (allowSchemaFallback &&
1993
+ invokeParams.jsonSchema &&
1994
+ invokeParams.jsonSchemaMode === 'native' &&
1995
+ isSchemaCompatibilityError(e)) {
1996
+ schemaFallbackAttempted = true;
1997
+ schemaFallbackReason = compactReason(e instanceof Error ? e.message : String(e ?? ''), 180);
1998
+ invokeParams.jsonSchemaMode = 'prompt';
1999
+ appliedSchemaMode = 'prompt';
2000
+ result = await provider.invoke({
2001
+ messages,
2002
+ ...invokeParams,
2003
+ temperature: 0.3,
2004
+ });
2005
+ }
2006
+ else {
2007
+ throw e;
2008
+ }
2009
+ }
1552
2010
  fullContent = result.content;
1553
2011
  // Emit chunk event with full response
1554
2012
  yield makeEvent('delta', { delta: result.content });
1555
2013
  }
2014
+ const structuredPolicyMeta = structuredPolicy
2015
+ ? {
2016
+ ...structuredPolicy,
2017
+ planned: structuredPolicy.resolved,
2018
+ applied: appliedSchemaMode,
2019
+ downgraded: appliedSchemaMode !== structuredPolicy.resolved,
2020
+ fallback: schemaFallbackAttempted ? { attempted: true, reason: schemaFallbackReason ?? undefined } : { attempted: false },
2021
+ provider: {
2022
+ structuredOutput: capsSnapshot.structuredOutput,
2023
+ nativeSchemaDialect: capsSnapshot.nativeSchemaDialect,
2024
+ maxNativeSchemaBytes: capsSnapshot.maxNativeSchemaBytes,
2025
+ },
2026
+ }
2027
+ : undefined;
1556
2028
  // Parse response
1557
2029
  let parsed;
2030
+ let parseExtracted = null;
2031
+ let parseAttempts = [];
2032
+ let parseRetries = 0;
1558
2033
  try {
1559
- const jsonMatch = fullContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1560
- const jsonStr = jsonMatch ? jsonMatch[1] : fullContent;
1561
- parsed = JSON.parse(jsonStr.trim());
2034
+ const r = parseJsonWithCandidates(fullContent);
2035
+ parsed = r.parsed;
2036
+ parseExtracted = r.extracted;
2037
+ parseAttempts = r.attempts;
1562
2038
  }
1563
2039
  catch (e) {
1564
- const errorResult = makeErrorResponse({
1565
- code: 'E1000', // PARSE_ERROR
1566
- message: `Failed to parse JSON: ${e.message}`,
1567
- suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
1568
- });
1569
- _invokeErrorHooks(module.name, e, null);
1570
- // errorResult is always an error response from makeErrorResponse
1571
- const errorObj = errorResult.error;
1572
- yield makeEvent('error', { error: errorObj });
1573
- yield makeEvent('end', { result: errorResult });
1574
- return;
2040
+ const firstDetails = e?.details;
2041
+ if (firstDetails && typeof firstDetails === 'object' && Array.isArray(firstDetails.parse_attempts)) {
2042
+ parseAttempts = firstDetails.parse_attempts;
2043
+ }
2044
+ const allowParseRetry = policy?.profile !== 'certified';
2045
+ if (!allowParseRetry) {
2046
+ const details = typeof firstDetails === 'object' && firstDetails
2047
+ ? firstDetails
2048
+ : { raw_response_snippet: safeSnippet(fullContent, 500) };
2049
+ const errorResult = makeErrorResponse({
2050
+ code: 'E1000', // PARSE_ERROR
2051
+ message: `Failed to parse JSON: ${e.message}`,
2052
+ details: {
2053
+ ...details,
2054
+ parse_retry: { attempted: false, reason: 'profile=certified (fail-fast)' },
2055
+ },
2056
+ suggestion: 'The LLM response was not valid JSON. Fix the module/provider output or switch provider.',
2057
+ });
2058
+ _invokeErrorHooks(module.name, e, null);
2059
+ const errorObj = errorResult.error;
2060
+ yield makeEvent('error', { error: errorObj });
2061
+ yield makeEvent('end', { result: errorResult });
2062
+ return;
2063
+ }
2064
+ // Retry once (non-streaming) with stronger JSON-only instructions.
2065
+ parseRetries = 1;
2066
+ const retryMessages = [
2067
+ ...messages,
2068
+ {
2069
+ role: 'user',
2070
+ content: 'Your previous response was not valid JSON.\n\nReturn ONLY a single valid JSON value (no markdown, no code fences, no commentary, no trailing text).',
2071
+ },
2072
+ ];
2073
+ try {
2074
+ const retryResult = await provider.invoke({
2075
+ messages: retryMessages,
2076
+ ...invokeParams,
2077
+ temperature: 0.3,
2078
+ });
2079
+ fullContent = retryResult.content; // do not emit delta for retry; clients should rely on final envelope
2080
+ const r2 = parseJsonWithCandidates(fullContent);
2081
+ parsed = r2.parsed;
2082
+ parseExtracted = r2.extracted;
2083
+ parseAttempts = [...parseAttempts, ...r2.attempts];
2084
+ }
2085
+ catch (e2) {
2086
+ const combinedAttempts = [
2087
+ ...parseAttempts,
2088
+ ...((typeof e2?.details === 'object' &&
2089
+ e2?.details &&
2090
+ Array.isArray(e2.details.parse_attempts))
2091
+ ? e2.details.parse_attempts
2092
+ : []),
2093
+ ];
2094
+ const details = typeof e2?.details === 'object' && e2?.details
2095
+ ? e2.details
2096
+ : { raw_response_snippet: safeSnippet(fullContent, 500) };
2097
+ const errorResult = makeErrorResponse({
2098
+ code: 'E1000', // PARSE_ERROR
2099
+ message: `Failed to parse JSON: ${e2.message}`,
2100
+ details: {
2101
+ ...details,
2102
+ parse_attempts: combinedAttempts.length ? combinedAttempts : undefined,
2103
+ parse_retry: { attempted: true, count: 1 },
2104
+ },
2105
+ suggestion: 'The LLM response was not valid JSON. Try again or adjust the prompt.',
2106
+ });
2107
+ _invokeErrorHooks(module.name, e2, null);
2108
+ // errorResult is always an error response from makeErrorResponse
2109
+ const errorObj = errorResult.error;
2110
+ yield makeEvent('error', { error: errorObj });
2111
+ yield makeEvent('end', { result: errorResult });
2112
+ return;
2113
+ }
1575
2114
  }
1576
2115
  // Convert to v2.2 envelope
1577
2116
  let response;
@@ -1589,6 +2128,34 @@ export async function* runModuleStream(module, provider, options = {}) {
1589
2128
  const latencyMs = Date.now() - startTime;
1590
2129
  if (response.meta) {
1591
2130
  response.meta.latency_ms = latencyMs;
2131
+ if (structuredPolicyMeta) {
2132
+ response.meta.policy = {
2133
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
2134
+ structured: structuredPolicyMeta,
2135
+ };
2136
+ }
2137
+ if (policy) {
2138
+ response.meta.policy = {
2139
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
2140
+ validation: {
2141
+ mode: policy.validate,
2142
+ input: validateInput,
2143
+ output: validateOutput,
2144
+ reason: validateReason,
2145
+ },
2146
+ audit: { enabled: policy.audit === true },
2147
+ repair: { enabled: enableRepair === true },
2148
+ };
2149
+ }
2150
+ const includeParseAttempts = policy?.profile !== 'core';
2151
+ response.meta.policy = {
2152
+ ...(typeof response.meta.policy === 'object' && response.meta.policy ? response.meta.policy : {}),
2153
+ parse: {
2154
+ strategy: parseExtracted?.strategy ?? null,
2155
+ retries: parseRetries,
2156
+ attempts: includeParseAttempts && parseAttempts.length ? parseAttempts : undefined,
2157
+ },
2158
+ };
1592
2159
  if (traceId) {
1593
2160
  response.meta.trace_id = traceId;
1594
2161
  }