crosscheck-mcp 0.1.0 → 0.1.2

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.
@@ -16,6 +16,7 @@ var __dirname = /* @__PURE__ */ getDirname();
16
16
  // src/entrypoints/node-stdio.ts
17
17
  import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
18
18
  import path9 from "path";
19
+ import { fileURLToPath as fileURLToPath3 } from "url";
19
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
21
 
21
22
  // src/adapters/storage/better-sqlite3.ts
@@ -1233,6 +1234,7 @@ function defaultTransient(kind) {
1233
1234
  // src/providers/anthropic.ts
1234
1235
  var ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
1235
1236
  var ANTHROPIC_VERSION_HEADER = "2023-06-01";
1237
+ var ANTHROPIC_STRUCTURED_TOOL_NAME = "structured_output";
1236
1238
  function buildAnthropicRequest(opts) {
1237
1239
  let system;
1238
1240
  const convo = [];
@@ -1257,6 +1259,17 @@ function buildAnthropicRequest(opts) {
1257
1259
  if (system !== void 0) {
1258
1260
  body.system = system;
1259
1261
  }
1262
+ if (opts.jsonSchema) {
1263
+ body.tools = [{
1264
+ name: ANTHROPIC_STRUCTURED_TOOL_NAME,
1265
+ description: "Emit a single JSON object matching the requested schema.",
1266
+ input_schema: opts.jsonSchema
1267
+ }];
1268
+ body.tool_choice = {
1269
+ type: "tool",
1270
+ name: ANTHROPIC_STRUCTURED_TOOL_NAME
1271
+ };
1272
+ }
1260
1273
  const headers = {
1261
1274
  "content-type": "application/json",
1262
1275
  "x-api-key": opts.apiKey,
@@ -1267,6 +1280,7 @@ function buildAnthropicRequest(opts) {
1267
1280
  function parseAnthropicResponse(opts) {
1268
1281
  const r = opts.resp ?? {};
1269
1282
  let text = "";
1283
+ let toolUseInput = void 0;
1270
1284
  const content = r["content"];
1271
1285
  if (!Array.isArray(content)) {
1272
1286
  throw new ProviderError(
@@ -1276,10 +1290,19 @@ function parseAnthropicResponse(opts) {
1276
1290
  }
1277
1291
  for (const block of content) {
1278
1292
  if (block && typeof block === "object") {
1279
- const t = block["text"];
1280
- if (typeof t === "string") text += t;
1293
+ const b = block;
1294
+ const type = b["type"];
1295
+ if (type === "tool_use" && b["name"] === ANTHROPIC_STRUCTURED_TOOL_NAME) {
1296
+ toolUseInput = b["input"];
1297
+ } else {
1298
+ const t = b["text"];
1299
+ if (typeof t === "string") text += t;
1300
+ }
1281
1301
  }
1282
1302
  }
1303
+ if (toolUseInput !== void 0) {
1304
+ text = JSON.stringify(toolUseInput);
1305
+ }
1283
1306
  const u = r["usage"] ?? {};
1284
1307
  const prompt = Math.trunc(Number(u["input_tokens"] ?? 0)) || 0;
1285
1308
  const cached = Math.trunc(Number(u["cache_read_input_tokens"] ?? 0)) || 0;
@@ -1324,7 +1347,8 @@ async function sendAnthropic(args) {
1324
1347
  apiKey: args.apiKey,
1325
1348
  messages: args.messages,
1326
1349
  maxTokens: args.maxTokens,
1327
- temperature: args.temperature
1350
+ temperature: args.temperature,
1351
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1328
1352
  });
1329
1353
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1330
1354
  const init = {
@@ -1369,6 +1393,45 @@ async function sendAnthropic(args) {
1369
1393
 
1370
1394
  // src/providers/gemini.ts
1371
1395
  var GEMINI_API_URL_BASE = "https://generativelanguage.googleapis.com/v1beta/models";
1396
+ function jsonSchemaToGeminiSchema(schema) {
1397
+ const out = {};
1398
+ const type = schema["type"];
1399
+ if (typeof type === "string") {
1400
+ out["type"] = type.toUpperCase();
1401
+ } else if (Array.isArray(type)) {
1402
+ const nonNull = type.find((t) => typeof t === "string" && t !== "null");
1403
+ const hasNull = type.includes("null");
1404
+ if (typeof nonNull === "string") out["type"] = nonNull.toUpperCase();
1405
+ if (hasNull) out["nullable"] = true;
1406
+ }
1407
+ const passthrough = [
1408
+ "description",
1409
+ "enum",
1410
+ "format",
1411
+ "nullable",
1412
+ "required",
1413
+ "minItems",
1414
+ "maxItems",
1415
+ "minimum",
1416
+ "maximum"
1417
+ ];
1418
+ for (const k of passthrough) {
1419
+ if (k in schema) out[k] = schema[k];
1420
+ }
1421
+ if (schema["properties"] && typeof schema["properties"] === "object") {
1422
+ const props = {};
1423
+ for (const [name, val] of Object.entries(schema["properties"])) {
1424
+ if (val && typeof val === "object") {
1425
+ props[name] = jsonSchemaToGeminiSchema(val);
1426
+ }
1427
+ }
1428
+ out["properties"] = props;
1429
+ }
1430
+ if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
1431
+ out["items"] = jsonSchemaToGeminiSchema(schema["items"]);
1432
+ }
1433
+ return out;
1434
+ }
1372
1435
  function buildGeminiRequest(opts) {
1373
1436
  const contents = [];
1374
1437
  let systemText = null;
@@ -1394,6 +1457,10 @@ function buildGeminiRequest(opts) {
1394
1457
  if (systemText !== null && systemText !== "") {
1395
1458
  body.systemInstruction = { parts: [{ text: systemText }] };
1396
1459
  }
1460
+ if (opts.jsonSchema) {
1461
+ body.generationConfig.responseMimeType = "application/json";
1462
+ body.generationConfig.responseSchema = jsonSchemaToGeminiSchema(opts.jsonSchema);
1463
+ }
1397
1464
  const url = `${GEMINI_API_URL_BASE}/${encodeURIComponent(opts.model)}:generateContent?key=${encodeURIComponent(opts.apiKey)}`;
1398
1465
  return { url, headers: {}, body };
1399
1466
  }
@@ -1462,7 +1529,8 @@ async function sendGemini(args) {
1462
1529
  apiKey: args.apiKey,
1463
1530
  messages: args.messages,
1464
1531
  maxTokens: args.maxTokens,
1465
- temperature: args.temperature
1532
+ temperature: args.temperature,
1533
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1466
1534
  });
1467
1535
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1468
1536
  const init = {
@@ -1513,6 +1581,17 @@ var OPENAI_COMPAT_DEFAULT_URLS = {
1513
1581
  groq: "https://api.groq.com/openai/v1/chat/completions",
1514
1582
  deepseek: "https://api.deepseek.com/v1/chat/completions"
1515
1583
  };
1584
+ var OPENAI_COMPAT_NATIVE_STRUCTURED = /* @__PURE__ */ new Set([
1585
+ "openai"
1586
+ ]);
1587
+ function supportsNativeJsonSchema(provider, model) {
1588
+ if (!OPENAI_COMPAT_NATIVE_STRUCTURED.has(provider.toLowerCase())) {
1589
+ return false;
1590
+ }
1591
+ const m = model.toLowerCase();
1592
+ if (/^o1(?:-|$)/.test(m)) return false;
1593
+ return true;
1594
+ }
1516
1595
  function buildOpenAICompatibleRequest(opts) {
1517
1596
  const msgs = opts.messages.map((m) => ({
1518
1597
  role: m.role,
@@ -1528,6 +1607,16 @@ function buildOpenAICompatibleRequest(opts) {
1528
1607
  } else {
1529
1608
  body.max_completion_tokens = opts.maxTokens;
1530
1609
  }
1610
+ if (opts.jsonSchema && supportsNativeJsonSchema(opts.provider, opts.model)) {
1611
+ body.response_format = {
1612
+ type: "json_schema",
1613
+ json_schema: {
1614
+ name: "structured_output",
1615
+ schema: opts.jsonSchema,
1616
+ strict: true
1617
+ }
1618
+ };
1619
+ }
1531
1620
  const headers = {
1532
1621
  "content-type": "application/json",
1533
1622
  Authorization: `Bearer ${opts.apiKey}`
@@ -1599,7 +1688,8 @@ async function sendOpenAICompatible(args) {
1599
1688
  url: args.url,
1600
1689
  messages: args.messages,
1601
1690
  maxTokens: args.maxTokens,
1602
- temperature: args.temperature
1691
+ temperature: args.temperature,
1692
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1603
1693
  });
1604
1694
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1605
1695
  const init = {
@@ -2075,11 +2165,13 @@ ${schemaText}`;
2075
2165
  content: "Your previous response failed validation:\n- " + lastErrs.slice(0, 5).join("\n- ") + "\nFix the issues and re-emit valid JSON only."
2076
2166
  });
2077
2167
  }
2168
+ const useNative = opts.useNativeStructured !== false;
2078
2169
  const ans = await askOne(provider, msgs, {
2079
2170
  maxTokens: opts.maxTokens,
2080
2171
  temperature: temp,
2081
2172
  purpose,
2082
- ...opts.signal ? { signal: opts.signal } : {}
2173
+ ...opts.signal ? { signal: opts.signal } : {},
2174
+ ...useNative ? { jsonSchema: schema } : {}
2083
2175
  });
2084
2176
  lastAnswer = ans;
2085
2177
  if (ans.error !== void 0) {
@@ -2114,7 +2206,8 @@ async function askOne(provider, messages, opts) {
2114
2206
  maxTokens: opts.maxTokens,
2115
2207
  temperature: opts.temperature,
2116
2208
  purpose: opts.purpose,
2117
- ...opts.signal ? { signal: opts.signal } : {}
2209
+ ...opts.signal ? { signal: opts.signal } : {},
2210
+ ...opts.jsonSchema ? { jsonSchema: opts.jsonSchema } : {}
2118
2211
  });
2119
2212
  const wallMs = Math.trunc(performance.now() - startedWall);
2120
2213
  const cpu = process.cpuUsage(startedCpu);
@@ -2357,7 +2450,7 @@ async function runAudit(args, opts) {
2357
2450
  const rubricOverride = args["rubric"];
2358
2451
  const producing = toStringArray(args["producing_panelists"]).map((s) => s.toLowerCase());
2359
2452
  const explicit = typeof args["auditor"] === "string" ? args["auditor"] : null;
2360
- const cheapMode = boolArg(args["cheap_mode"], true);
2453
+ const cheapMode = boolArg(args["cheap_mode"], opts.ctx?.cheap_mode ?? true);
2361
2454
  const allowSelf = boolArg(args["allow_self_audit"], false);
2362
2455
  let coalesce = boolArg(args["coalesce"], false);
2363
2456
  const strictMode = boolArg(args["strict_mode"], false);
@@ -3887,7 +3980,7 @@ async function runConfer(args, opts) {
3887
3980
  };
3888
3981
  }
3889
3982
  }
3890
- const { selected, unknown: unknownNames, blocked } = resolveProviders(
3983
+ let { selected, unknown: unknownNames, blocked } = resolveProviders(
3891
3984
  resolvedProviders,
3892
3985
  opts.providers,
3893
3986
  opts.allowlist ?? null
@@ -3904,6 +3997,49 @@ async function runConfer(args, opts) {
3904
3997
  }
3905
3998
  return { tool: "confer", error: "no active providers have API keys in .env" };
3906
3999
  }
4000
+ let cheapModePanelMeta = null;
4001
+ if (opts.ctx?.cheap_mode === true && !callerSuppliedProviders && selected.length > 1) {
4002
+ if (!opts.pricing) {
4003
+ cheapModePanelMeta = {
4004
+ before: selected.length,
4005
+ after: selected.length,
4006
+ picked: null,
4007
+ reason: "ctx.cheap_mode=true but no pricing doc wired; kept full panel"
4008
+ };
4009
+ } else {
4010
+ const availableSet = new Set(selected.map((p) => p.name.toLowerCase()));
4011
+ const cheapWeights = opts.storage ? await loadProviderWeights(opts.storage, selected.map((p) => p.name)) : {};
4012
+ const allowOnly = opts.allowlist && opts.allowlist.length > 0 ? opts.allowlist.map((s) => s.toLowerCase()) : void 0;
4013
+ const tierPick = selectForDifficulty({
4014
+ pricing: opts.pricing,
4015
+ tier: "low",
4016
+ availableProviders: availableSet,
4017
+ providerWeights: cheapWeights,
4018
+ ...allowOnly !== void 0 ? { allowOnly } : {}
4019
+ });
4020
+ if (tierPick.pick) {
4021
+ const baseProvider = opts.providers[tierPick.pick.provider.toLowerCase()];
4022
+ if (baseProvider) {
4023
+ const beforeCount = selected.length;
4024
+ const retargeted = retargetProvider(baseProvider, tierPick.pick.model);
4025
+ selected = [retargeted];
4026
+ cheapModePanelMeta = {
4027
+ before: beforeCount,
4028
+ after: 1,
4029
+ picked: { provider: tierPick.pick.provider, model: tierPick.pick.model },
4030
+ reason: "ctx.cheap_mode=true; narrowed to cheapest low-tier candidate"
4031
+ };
4032
+ }
4033
+ } else {
4034
+ cheapModePanelMeta = {
4035
+ before: selected.length,
4036
+ after: selected.length,
4037
+ picked: null,
4038
+ reason: `ctx.cheap_mode=true; ${tierPick.reason ?? "no candidate"}; kept full panel`
4039
+ };
4040
+ }
4041
+ }
4042
+ }
3907
4043
  const untrusted = Boolean(args["untrusted_input"]);
3908
4044
  const canary = untrusted ? mintCanary() : null;
3909
4045
  const baseSys = "You are part of a panel of LLMs consulted by an engineer working inside Claude Code. Answer directly, cite assumptions, and keep it crisp.";
@@ -4020,6 +4156,7 @@ ${ctxBody}` });
4020
4156
  if (claimsBlock !== null) result["claims"] = claimsBlock;
4021
4157
  if (leaks.length > 0) result["canary_leaks"] = leaks;
4022
4158
  if (autoPanelMeta !== null) result["auto_panel"] = autoPanelMeta;
4159
+ if (cheapModePanelMeta !== null) result["cheap_mode_panel"] = cheapModePanelMeta;
4023
4160
  if (requestedWorkerTools.length > 0) {
4024
4161
  result["worker_tools"] = {
4025
4162
  accepted: acceptedWorkerTools,
@@ -5198,6 +5335,40 @@ import path6 from "path";
5198
5335
 
5199
5336
  // src/tools/orchestrate.ts
5200
5337
  import { performance as performance3 } from "perf_hooks";
5338
+
5339
+ // src/core/dead-models.ts
5340
+ var DEAD_MODELS_TTL_MS = 5 * 60 * 1e3;
5341
+ var deadModels = /* @__PURE__ */ new Map();
5342
+ function makeKey(provider, model) {
5343
+ return `${provider.toLowerCase()}:${model.toLowerCase()}`;
5344
+ }
5345
+ function recordDeadModel(provider, model, reason, nowMs = Date.now()) {
5346
+ const key = makeKey(provider, model);
5347
+ deadModels.set(key, {
5348
+ provider: provider.toLowerCase(),
5349
+ model: model.toLowerCase(),
5350
+ reason: reason.slice(0, 200),
5351
+ expires: nowMs + DEAD_MODELS_TTL_MS
5352
+ });
5353
+ }
5354
+ function isModelDead(provider, model, nowMs = Date.now()) {
5355
+ const key = makeKey(provider, model);
5356
+ const entry = deadModels.get(key);
5357
+ if (!entry) return false;
5358
+ if (nowMs >= entry.expires) {
5359
+ deadModels.delete(key);
5360
+ return false;
5361
+ }
5362
+ return true;
5363
+ }
5364
+ function isDeadModelError(err) {
5365
+ if (typeof err.error !== "string") return false;
5366
+ if (err.error_kind !== "client") return false;
5367
+ return /HTTP\s*404|not\s*found|does not exist|deprecated|decommission/i.test(err.error);
5368
+ }
5369
+
5370
+ // src/tools/orchestrate.ts
5371
+ var F1B_MAX_TIER_RETRIES = 2;
5201
5372
  var DIFFICULTY_TIERS2 = ["low", "med", "high"];
5202
5373
  var EST_TOKENS = {
5203
5374
  low: [800, 400],
@@ -5248,7 +5419,7 @@ async function runOrchestrate(args, opts) {
5248
5419
  const moderator = opts.providers[moderatorName.toLowerCase()] ?? selected[0];
5249
5420
  const failFast = boolArg2(args["fail_fast"], false);
5250
5421
  const planOnly = boolArg2(args["plan_only"], false);
5251
- const cheapMode = boolArg2(args["cheap_mode"], false);
5422
+ const cheapMode = boolArg2(args["cheap_mode"], opts.ctx?.cheap_mode ?? false);
5252
5423
  const maxTokens = opts.maxTokens ?? 4096;
5253
5424
  const cheapAvailable = new Set(
5254
5425
  selected.map((p) => p.name.toLowerCase())
@@ -5334,12 +5505,51 @@ async function runOrchestrate(args, opts) {
5334
5505
  failedIds.add(nid);
5335
5506
  continue;
5336
5507
  }
5508
+ const depsRaw = Array.isArray(node["depends_on"]) ? node["depends_on"] : [];
5509
+ const upstreamBlocks = [];
5510
+ for (const d of depsRaw) {
5511
+ if (typeof d !== "string") continue;
5512
+ const ur = nodeResults[d];
5513
+ if (ur && ur.status === "ok") {
5514
+ upstreamBlocks.push(`[node ${d} output]
5515
+ ${ur.output ?? ""}`);
5516
+ } else if (ur && ur.status === "failed") {
5517
+ upstreamBlocks.push(`[node ${d}] [MISSING: failed \u2014 ${ur.error ?? ""}]`);
5518
+ }
5519
+ }
5520
+ const ctxBlock = upstreamBlocks.length > 0 ? upstreamBlocks.join("\n\n") : "(no upstream nodes)";
5521
+ const sysMsg = "You are a worker LLM in an orchestrated DAG. Complete the assigned task using outputs from upstream nodes when relevant. Be concise.";
5522
+ const role = typeof node["role"] === "string" ? node["role"] : "worker";
5523
+ const task = String(node["task"] ?? "");
5524
+ const difficulty = String(node["difficulty"] ?? "med");
5525
+ const msgs = [
5526
+ { role: "system", content: sysMsg },
5527
+ {
5528
+ role: "user",
5529
+ content: `ROLE: ${role}
5530
+ DIFFICULTY: ${difficulty}
5531
+
5532
+ UPSTREAM:
5533
+ ${ctxBlock}
5534
+
5535
+ TASK:
5536
+ ${task}`
5537
+ }
5538
+ ];
5337
5539
  let chosen;
5540
+ let ans;
5338
5541
  let cheapReason = null;
5542
+ const retryAttempts = [];
5339
5543
  const pinnedName = typeof node["provider"] === "string" ? node["provider"].toLowerCase() : null;
5340
5544
  const pinnedModel = typeof node["model"] === "string" ? node["model"] : null;
5545
+ const started = performance3.now();
5341
5546
  if (pinnedName && opts.providers[pinnedName]) {
5342
5547
  chosen = opts.providers[pinnedName];
5548
+ ans = await askOne(chosen, msgs, {
5549
+ maxTokens,
5550
+ temperature: 0.4,
5551
+ purpose: "worker"
5552
+ });
5343
5553
  } else if (cheapMode && !pinnedName && !pinnedModel && opts.pricing) {
5344
5554
  const tier = String(node["difficulty"] ?? "med");
5345
5555
  const pick = selectForDifficulty({
@@ -5349,21 +5559,78 @@ async function runOrchestrate(args, opts) {
5349
5559
  providerWeights: cheapWeights,
5350
5560
  ...cheapAllowOnly ? { allowOnly: cheapAllowOnly } : {}
5351
5561
  });
5352
- if (pick.pick) {
5353
- const base = opts.providers[pick.pick.provider];
5354
- if (base) chosen = retargetProvider(base, pick.pick.model);
5562
+ const aliveCandidates = pick.scored.filter(
5563
+ (c) => !isModelDead(c.provider, c.model)
5564
+ );
5565
+ if (aliveCandidates.length === 0) {
5566
+ cheapReason = pick.reason ?? "all tier candidates filtered by deadModels cache";
5355
5567
  } else {
5356
- cheapReason = pick.reason;
5568
+ const tryLimit = Math.min(F1B_MAX_TIER_RETRIES, aliveCandidates.length);
5569
+ for (let i = 0; i < tryLimit; i++) {
5570
+ const c = aliveCandidates[i];
5571
+ const base = opts.providers[c.provider];
5572
+ if (!base) {
5573
+ retryAttempts.push({
5574
+ provider: c.provider,
5575
+ model: c.model,
5576
+ reason: "provider not in active registry"
5577
+ });
5578
+ continue;
5579
+ }
5580
+ const candidateProvider = retargetProvider(base, c.model);
5581
+ const candidateAns = await askOne(candidateProvider, msgs, {
5582
+ maxTokens,
5583
+ temperature: 0.4,
5584
+ purpose: "worker"
5585
+ });
5586
+ if (candidateAns.error === void 0) {
5587
+ chosen = candidateProvider;
5588
+ ans = candidateAns;
5589
+ if (retryAttempts.length > 0) {
5590
+ cheapReason = `recovered after ${retryAttempts.length} dead-model retry(s): ` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ");
5591
+ }
5592
+ break;
5593
+ }
5594
+ if (isDeadModelError(candidateAns)) {
5595
+ recordDeadModel(c.provider, c.model, candidateAns.error ?? "");
5596
+ retryAttempts.push({
5597
+ provider: c.provider,
5598
+ model: c.model,
5599
+ reason: (candidateAns.error ?? "dead").slice(0, 100)
5600
+ });
5601
+ continue;
5602
+ }
5603
+ chosen = candidateProvider;
5604
+ ans = candidateAns;
5605
+ if (retryAttempts.length > 0) {
5606
+ cheapReason = `${retryAttempts.length} dead-model retry(s) before non-dead failure: ` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ");
5607
+ }
5608
+ break;
5609
+ }
5357
5610
  }
5358
5611
  if (!chosen) {
5359
5612
  const idx = djb2Hash(nid) % Math.max(1, selected.length);
5360
5613
  chosen = selected[idx];
5614
+ ans = await askOne(chosen, msgs, {
5615
+ maxTokens,
5616
+ temperature: 0.4,
5617
+ purpose: "worker"
5618
+ });
5619
+ if (retryAttempts.length > 0) {
5620
+ cheapReason = `cheap-mode tier exhausted (` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ") + `); fell back to id-hash rotation`;
5621
+ }
5361
5622
  }
5362
5623
  } else {
5363
5624
  const idx = djb2Hash(nid) % Math.max(1, selected.length);
5364
5625
  chosen = selected[idx];
5626
+ ans = await askOne(chosen, msgs, {
5627
+ maxTokens,
5628
+ temperature: 0.4,
5629
+ purpose: "worker"
5630
+ });
5365
5631
  }
5366
- if (!chosen) {
5632
+ const wallMs = Math.trunc(performance3.now() - started);
5633
+ if (!chosen || !ans) {
5367
5634
  nodeResults[nid] = {
5368
5635
  id: nid,
5369
5636
  status: "failed",
@@ -5371,49 +5638,12 @@ async function runOrchestrate(args, opts) {
5371
5638
  model: null,
5372
5639
  error: "no provider available",
5373
5640
  wall_ms: 0,
5374
- cpu_ms: 0
5641
+ cpu_ms: 0,
5642
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5375
5643
  };
5376
5644
  failedIds.add(nid);
5377
5645
  continue;
5378
5646
  }
5379
- const depsRaw = Array.isArray(node["depends_on"]) ? node["depends_on"] : [];
5380
- const upstreamBlocks = [];
5381
- for (const d of depsRaw) {
5382
- if (typeof d !== "string") continue;
5383
- const ur = nodeResults[d];
5384
- if (ur && ur.status === "ok") {
5385
- upstreamBlocks.push(`[node ${d} output]
5386
- ${ur.output ?? ""}`);
5387
- } else if (ur && ur.status === "failed") {
5388
- upstreamBlocks.push(`[node ${d}] [MISSING: failed \u2014 ${ur.error ?? ""}]`);
5389
- }
5390
- }
5391
- const ctxBlock = upstreamBlocks.length > 0 ? upstreamBlocks.join("\n\n") : "(no upstream nodes)";
5392
- const sysMsg = "You are a worker LLM in an orchestrated DAG. Complete the assigned task using outputs from upstream nodes when relevant. Be concise.";
5393
- const role = typeof node["role"] === "string" ? node["role"] : "worker";
5394
- const task = String(node["task"] ?? "");
5395
- const difficulty = String(node["difficulty"] ?? "med");
5396
- const msgs = [
5397
- { role: "system", content: sysMsg },
5398
- {
5399
- role: "user",
5400
- content: `ROLE: ${role}
5401
- DIFFICULTY: ${difficulty}
5402
-
5403
- UPSTREAM:
5404
- ${ctxBlock}
5405
-
5406
- TASK:
5407
- ${task}`
5408
- }
5409
- ];
5410
- const started = performance3.now();
5411
- const ans = await askOne(chosen, msgs, {
5412
- maxTokens,
5413
- temperature: 0.4,
5414
- purpose: "worker"
5415
- });
5416
- const wallMs = Math.trunc(performance3.now() - started);
5417
5647
  if (ans.error !== void 0) {
5418
5648
  nodeResults[nid] = {
5419
5649
  id: nid,
@@ -5423,7 +5653,8 @@ ${task}`
5423
5653
  error: ans.error,
5424
5654
  wall_ms: wallMs,
5425
5655
  cpu_ms: ans.cpu_ms,
5426
- ...cheapReason ? { cheap_fallback_reason: cheapReason } : {}
5656
+ ...cheapReason ? { cheap_fallback_reason: cheapReason } : {},
5657
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5427
5658
  };
5428
5659
  failedIds.add(nid);
5429
5660
  continue;
@@ -5436,7 +5667,8 @@ ${task}`
5436
5667
  output: ans.response ?? "",
5437
5668
  wall_ms: wallMs,
5438
5669
  cpu_ms: ans.cpu_ms,
5439
- ...cheapReason ? { cheap_fallback_reason: cheapReason } : {}
5670
+ ...cheapReason ? { cheap_fallback_reason: cheapReason } : {},
5671
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5440
5672
  };
5441
5673
  }
5442
5674
  const missing = Object.keys(nodesById).filter(
@@ -5463,11 +5695,43 @@ Synthesize the node outputs into a single coherent deliverable. Preserve any [MI
5463
5695
  { role: "system", content: "You are the orchestrator. Combine node outputs into the final result." },
5464
5696
  { role: "user", content: recombinePrompt }
5465
5697
  ];
5466
- const synthAns = await askOne(moderator, recMsgs, {
5698
+ let synthProvider = moderator;
5699
+ let synthModel = null;
5700
+ if (cheapMode && opts.pricing) {
5701
+ const pick = selectForDifficulty({
5702
+ pricing: opts.pricing,
5703
+ tier: "med",
5704
+ availableProviders: cheapAvailable,
5705
+ providerWeights: cheapWeights,
5706
+ ...cheapAllowOnly ? { allowOnly: cheapAllowOnly } : {}
5707
+ });
5708
+ for (const c of pick.scored) {
5709
+ if (isModelDead(c.provider, c.model)) continue;
5710
+ const base = opts.providers[c.provider];
5711
+ if (base) {
5712
+ synthProvider = retargetProvider(base, c.model);
5713
+ synthModel = `${c.provider}/${c.model}`;
5714
+ break;
5715
+ }
5716
+ }
5717
+ }
5718
+ let synthAns = await askOne(synthProvider, recMsgs, {
5467
5719
  maxTokens,
5468
5720
  temperature: 0.4,
5469
5721
  purpose: "synth"
5470
5722
  });
5723
+ if (synthAns.error !== void 0 && synthProvider !== moderator) {
5724
+ if (isDeadModelError(synthAns) && synthModel) {
5725
+ const [prov, mdl] = synthModel.split("/");
5726
+ if (prov && mdl) recordDeadModel(prov, mdl, synthAns.error ?? "");
5727
+ }
5728
+ synthAns = await askOne(moderator, recMsgs, {
5729
+ maxTokens,
5730
+ temperature: 0.4,
5731
+ purpose: "synth"
5732
+ });
5733
+ synthModel = null;
5734
+ }
5471
5735
  const finalText = synthAns.error !== void 0 ? "" : synthAns.response ?? "";
5472
5736
  const synthErr = synthAns.error;
5473
5737
  const publicNodes = Object.keys(nodesById).map(
@@ -5491,6 +5755,7 @@ Synthesize the node outputs into a single coherent deliverable. Preserve any [MI
5491
5755
  fail_fast: failFast,
5492
5756
  cheap_mode: cheapMode
5493
5757
  };
5758
+ if (synthModel) result["synth_model"] = synthModel;
5494
5759
  if (synthErr) result["synth_error"] = synthErr;
5495
5760
  if (plannerErrors.length > 0) result["planner_errors"] = plannerErrors;
5496
5761
  if (unknownNames.length > 0) result["skipped_unknown_providers"] = unknownNames;
@@ -6062,6 +6327,16 @@ function errorPayload(code, message, hint, kind = "client") {
6062
6327
  };
6063
6328
  }
6064
6329
 
6330
+ // src/core/call-context.ts
6331
+ function buildCallContext(args) {
6332
+ const ctx = {
6333
+ cheap_mode: args.cheapMode,
6334
+ session_id: args.sessionId
6335
+ };
6336
+ if (args.purpose) ctx.purpose = args.purpose;
6337
+ return ctx;
6338
+ }
6339
+
6065
6340
  // src/tools/create.ts
6066
6341
  var CREATE_DOC_MAX_BYTES = 32 * 1024;
6067
6342
  var CREATE_AUDIT_THRESHOLD = 0.7;
@@ -6090,6 +6365,10 @@ async function runCreate(args, opts) {
6090
6365
  const dryRun = boolArg3(args["dry_run"], false);
6091
6366
  const planOnly = boolArg3(args["plan_only"], false);
6092
6367
  const moderator = typeof args["moderator"] === "string" && args["moderator"] ? args["moderator"] : opts.moderator ?? "anthropic";
6368
+ const ctx = opts.ctx ?? buildCallContext({
6369
+ cheapMode,
6370
+ sessionId
6371
+ });
6093
6372
  const descriptors = await ingestDocuments(documents, sessionId, opts);
6094
6373
  const documentsPayload = formatDocumentsPayload(descriptors);
6095
6374
  const okCount = descriptors.filter((d) => d.status === "ok").length;
@@ -6107,7 +6386,8 @@ ${documentsPayload}`;
6107
6386
  }, {
6108
6387
  providers: opts.providers,
6109
6388
  allowlist: opts.allowlist ?? null,
6110
- ...opts.bridge ? { bridge: opts.bridge } : {}
6389
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6390
+ ctx
6111
6391
  });
6112
6392
  const scopeAnswer = Array.isArray(scope["answers"]) ? scope["answers"].filter((a) => typeof a["response"] === "string" && a["response"]).map((a) => `[${a["provider"]}]
6113
6393
  ${a["response"]}`).join("\n\n") : "";
@@ -6129,7 +6409,8 @@ ${scopeAnswer || "(none)"}
6129
6409
  providers: opts.providers,
6130
6410
  allowlist: opts.allowlist ?? null,
6131
6411
  moderator,
6132
- ...opts.bridge ? { bridge: opts.bridge } : {}
6412
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6413
+ ctx
6133
6414
  });
6134
6415
  let attempts = 1;
6135
6416
  if (planOnly) {
@@ -6163,7 +6444,8 @@ ${scopeAnswer || "(none)"}
6163
6444
  }, {
6164
6445
  providers: opts.providers,
6165
6446
  allowlist: opts.allowlist ?? null,
6166
- ...opts.bridge ? { bridge: opts.bridge } : {}
6447
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6448
+ ctx
6167
6449
  });
6168
6450
  }
6169
6451
  let auditEnvelope = null;
@@ -6185,7 +6467,8 @@ ${scopeAnswer || "(none)"}
6185
6467
  }, {
6186
6468
  providers: opts.providers,
6187
6469
  allowlist: opts.allowlist ?? null,
6188
- ...opts.bridge ? { bridge: opts.bridge } : {}
6470
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6471
+ ctx
6189
6472
  });
6190
6473
  let overallF = toNumberOrNull(auditEnvelope["overall_score"]);
6191
6474
  if (overallF !== null && overallF < auditThreshold && !cheapMode && !boolArg3(args["_no_retry"], false)) {
@@ -6209,7 +6492,8 @@ ${documentsPayload}`
6209
6492
  providers: opts.providers,
6210
6493
  allowlist: opts.allowlist ?? null,
6211
6494
  moderator,
6212
- ...opts.bridge ? { bridge: opts.bridge } : {}
6495
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6496
+ ctx
6213
6497
  }
6214
6498
  );
6215
6499
  orchestration = retryOrchestration;
@@ -6228,7 +6512,8 @@ ${documentsPayload}`
6228
6512
  }, {
6229
6513
  providers: opts.providers,
6230
6514
  allowlist: opts.allowlist ?? null,
6231
- ...opts.bridge ? { bridge: opts.bridge } : {}
6515
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6516
+ ctx
6232
6517
  });
6233
6518
  overallF = toNumberOrNull(auditEnvelope["overall_score"]);
6234
6519
  } else {
@@ -8079,7 +8364,7 @@ CRITERIA:
8079
8364
  ${criteriaBlock}`
8080
8365
  }
8081
8366
  ];
8082
- const maxTokens = opts.maxTokens ?? 2048;
8367
+ const maxTokens = opts.maxTokens ?? 4096;
8083
8368
  const scoresByProvider = {};
8084
8369
  const answersCollected = [];
8085
8370
  const scoringErrors = {};
@@ -8275,9 +8560,23 @@ function toArray(v) {
8275
8560
  }
8276
8561
 
8277
8562
  // src/tools/plan.ts
8563
+ var PLAN_MODE_PRESETS = {
8564
+ fast: { max_rounds: 2, early_stop: true, early_stop_threshold: 0.7 },
8565
+ thorough: { max_rounds: 5, early_stop: false, early_stop_threshold: 0.7 }
8566
+ };
8567
+ var DEFAULT_PLAN_MODE = "fast";
8278
8568
  async function runPlan(args, opts) {
8279
8569
  const goal = typeof args["goal"] === "string" ? args["goal"] : String(args["goal"] ?? "");
8280
8570
  const constraints = typeof args["constraints"] === "string" ? args["constraints"] : "";
8571
+ const modeArg = args["mode"];
8572
+ const mode = modeArg === "fast" || modeArg === "thorough" ? modeArg : DEFAULT_PLAN_MODE;
8573
+ const preset = PLAN_MODE_PRESETS[mode];
8574
+ const maxRoundsRaw = args["max_rounds"];
8575
+ const earlyStopRaw = args["early_stop"];
8576
+ const earlyStopThresholdRaw = args["early_stop_threshold"];
8577
+ const maxRounds = typeof maxRoundsRaw === "number" && Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? Math.trunc(maxRoundsRaw) : preset.max_rounds;
8578
+ const earlyStop = typeof earlyStopRaw === "boolean" ? earlyStopRaw : preset.early_stop;
8579
+ const earlyStopThreshold = typeof earlyStopThresholdRaw === "number" && Number.isFinite(earlyStopThresholdRaw) ? earlyStopThresholdRaw : preset.early_stop_threshold;
8281
8580
  const merged = `We need a step-by-step plan to achieve this goal.
8282
8581
 
8283
8582
  GOAL: ${goal}
@@ -8288,7 +8587,10 @@ Return: (1) the plan as numbered steps, (2) risks, (3) alternatives considered.`
8288
8587
  const debateArgs = {
8289
8588
  topic: merged,
8290
8589
  context: typeof args["context"] === "string" ? args["context"] : "",
8291
- structured: Boolean(args["structured"])
8590
+ structured: Boolean(args["structured"]),
8591
+ max_rounds: maxRounds,
8592
+ early_stop: earlyStop,
8593
+ early_stop_threshold: earlyStopThreshold
8292
8594
  };
8293
8595
  if (args["providers"] !== void 0) debateArgs["providers"] = args["providers"];
8294
8596
  if (args["moderator"] !== void 0) debateArgs["moderator"] = args["moderator"];
@@ -9998,7 +10300,7 @@ function critiqueTool(providers, allowlist, bridge) {
9998
10300
  function planTool(providers, allowlist, bridge) {
9999
10301
  return {
10000
10302
  name: "plan",
10001
- description: "Have an LLM panel debate a step-by-step plan for the stated goal under the given constraints. Returns the debate envelope with a moderator-synthesised plan. Use `structured: true` (bridge) for schema-validated synthesis.",
10303
+ description: 'Have an LLM panel debate a step-by-step plan for the stated goal under the given constraints. Returns the debate envelope with a moderator-synthesised plan. Defaults to `mode: "fast"` (2 rounds + early_stop) \u2014 set `mode: "thorough"` for 5 rounds without early_stop. Explicit `max_rounds` / `early_stop` / `early_stop_threshold` always override the mode preset. Use `structured: true` for schema-validated synthesis.',
10002
10304
  inputSchema: {
10003
10305
  type: "object",
10004
10306
  additionalProperties: true,
@@ -10009,7 +10311,11 @@ function planTool(providers, allowlist, bridge) {
10009
10311
  providers: { type: "array", items: { type: "string" } },
10010
10312
  moderator: { type: "string" },
10011
10313
  session_id: { type: "string" },
10012
- structured: { type: "boolean" }
10314
+ structured: { type: "boolean" },
10315
+ mode: { type: "string", enum: ["fast", "thorough"] },
10316
+ max_rounds: { type: "integer", minimum: 1 },
10317
+ early_stop: { type: "boolean" },
10318
+ early_stop_threshold: { type: "number", minimum: 0, maximum: 1 }
10013
10319
  },
10014
10320
  required: ["goal"]
10015
10321
  },
@@ -10402,8 +10708,19 @@ async function main() {
10402
10708
  );
10403
10709
  }
10404
10710
  installShutdownHandlers(bridge);
10405
- const pricingPath = process.env["CROSSCHECK_PRICING_PATH"] ?? resolveRepoFile("config/pricing.json");
10406
- const pricing = pricingPath && existsSync8(pricingPath) ? loadPricing(pricingPath) : {};
10711
+ const bundledPricing = (() => {
10712
+ try {
10713
+ return path9.join(path9.dirname(fileURLToPath3(import.meta.url)), "pricing.json");
10714
+ } catch {
10715
+ return void 0;
10716
+ }
10717
+ })();
10718
+ const pricingPath = [
10719
+ process.env["CROSSCHECK_PRICING_PATH"],
10720
+ resolveRepoFile("config/pricing.json"),
10721
+ bundledPricing
10722
+ ].find((p) => p && existsSync8(p));
10723
+ const pricing = pricingPath ? loadPricing(pricingPath) : {};
10407
10724
  const providers = buildProviders({ env: process.env, pricing });
10408
10725
  if (Object.keys(providers).length > 0) {
10409
10726
  process.stderr.write(