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.
@@ -48,6 +48,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
48
48
  // src/entrypoints/node-stdio.ts
49
49
  var import_node_fs11 = require("fs");
50
50
  var import_node_path11 = __toESM(require("path"), 1);
51
+ var import_node_url2 = require("url");
51
52
  var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
52
53
 
53
54
  // src/adapters/storage/better-sqlite3.ts
@@ -1265,6 +1266,7 @@ function defaultTransient(kind) {
1265
1266
  // src/providers/anthropic.ts
1266
1267
  var ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
1267
1268
  var ANTHROPIC_VERSION_HEADER = "2023-06-01";
1269
+ var ANTHROPIC_STRUCTURED_TOOL_NAME = "structured_output";
1268
1270
  function buildAnthropicRequest(opts) {
1269
1271
  let system;
1270
1272
  const convo = [];
@@ -1289,6 +1291,17 @@ function buildAnthropicRequest(opts) {
1289
1291
  if (system !== void 0) {
1290
1292
  body.system = system;
1291
1293
  }
1294
+ if (opts.jsonSchema) {
1295
+ body.tools = [{
1296
+ name: ANTHROPIC_STRUCTURED_TOOL_NAME,
1297
+ description: "Emit a single JSON object matching the requested schema.",
1298
+ input_schema: opts.jsonSchema
1299
+ }];
1300
+ body.tool_choice = {
1301
+ type: "tool",
1302
+ name: ANTHROPIC_STRUCTURED_TOOL_NAME
1303
+ };
1304
+ }
1292
1305
  const headers = {
1293
1306
  "content-type": "application/json",
1294
1307
  "x-api-key": opts.apiKey,
@@ -1299,6 +1312,7 @@ function buildAnthropicRequest(opts) {
1299
1312
  function parseAnthropicResponse(opts) {
1300
1313
  const r = opts.resp ?? {};
1301
1314
  let text = "";
1315
+ let toolUseInput = void 0;
1302
1316
  const content = r["content"];
1303
1317
  if (!Array.isArray(content)) {
1304
1318
  throw new ProviderError(
@@ -1308,10 +1322,19 @@ function parseAnthropicResponse(opts) {
1308
1322
  }
1309
1323
  for (const block of content) {
1310
1324
  if (block && typeof block === "object") {
1311
- const t = block["text"];
1312
- if (typeof t === "string") text += t;
1325
+ const b = block;
1326
+ const type = b["type"];
1327
+ if (type === "tool_use" && b["name"] === ANTHROPIC_STRUCTURED_TOOL_NAME) {
1328
+ toolUseInput = b["input"];
1329
+ } else {
1330
+ const t = b["text"];
1331
+ if (typeof t === "string") text += t;
1332
+ }
1313
1333
  }
1314
1334
  }
1335
+ if (toolUseInput !== void 0) {
1336
+ text = JSON.stringify(toolUseInput);
1337
+ }
1315
1338
  const u = r["usage"] ?? {};
1316
1339
  const prompt = Math.trunc(Number(u["input_tokens"] ?? 0)) || 0;
1317
1340
  const cached = Math.trunc(Number(u["cache_read_input_tokens"] ?? 0)) || 0;
@@ -1356,7 +1379,8 @@ async function sendAnthropic(args) {
1356
1379
  apiKey: args.apiKey,
1357
1380
  messages: args.messages,
1358
1381
  maxTokens: args.maxTokens,
1359
- temperature: args.temperature
1382
+ temperature: args.temperature,
1383
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1360
1384
  });
1361
1385
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1362
1386
  const init = {
@@ -1401,6 +1425,45 @@ async function sendAnthropic(args) {
1401
1425
 
1402
1426
  // src/providers/gemini.ts
1403
1427
  var GEMINI_API_URL_BASE = "https://generativelanguage.googleapis.com/v1beta/models";
1428
+ function jsonSchemaToGeminiSchema(schema) {
1429
+ const out = {};
1430
+ const type = schema["type"];
1431
+ if (typeof type === "string") {
1432
+ out["type"] = type.toUpperCase();
1433
+ } else if (Array.isArray(type)) {
1434
+ const nonNull = type.find((t) => typeof t === "string" && t !== "null");
1435
+ const hasNull = type.includes("null");
1436
+ if (typeof nonNull === "string") out["type"] = nonNull.toUpperCase();
1437
+ if (hasNull) out["nullable"] = true;
1438
+ }
1439
+ const passthrough = [
1440
+ "description",
1441
+ "enum",
1442
+ "format",
1443
+ "nullable",
1444
+ "required",
1445
+ "minItems",
1446
+ "maxItems",
1447
+ "minimum",
1448
+ "maximum"
1449
+ ];
1450
+ for (const k of passthrough) {
1451
+ if (k in schema) out[k] = schema[k];
1452
+ }
1453
+ if (schema["properties"] && typeof schema["properties"] === "object") {
1454
+ const props = {};
1455
+ for (const [name, val] of Object.entries(schema["properties"])) {
1456
+ if (val && typeof val === "object") {
1457
+ props[name] = jsonSchemaToGeminiSchema(val);
1458
+ }
1459
+ }
1460
+ out["properties"] = props;
1461
+ }
1462
+ if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
1463
+ out["items"] = jsonSchemaToGeminiSchema(schema["items"]);
1464
+ }
1465
+ return out;
1466
+ }
1404
1467
  function buildGeminiRequest(opts) {
1405
1468
  const contents = [];
1406
1469
  let systemText = null;
@@ -1426,6 +1489,10 @@ function buildGeminiRequest(opts) {
1426
1489
  if (systemText !== null && systemText !== "") {
1427
1490
  body.systemInstruction = { parts: [{ text: systemText }] };
1428
1491
  }
1492
+ if (opts.jsonSchema) {
1493
+ body.generationConfig.responseMimeType = "application/json";
1494
+ body.generationConfig.responseSchema = jsonSchemaToGeminiSchema(opts.jsonSchema);
1495
+ }
1429
1496
  const url = `${GEMINI_API_URL_BASE}/${encodeURIComponent(opts.model)}:generateContent?key=${encodeURIComponent(opts.apiKey)}`;
1430
1497
  return { url, headers: {}, body };
1431
1498
  }
@@ -1494,7 +1561,8 @@ async function sendGemini(args) {
1494
1561
  apiKey: args.apiKey,
1495
1562
  messages: args.messages,
1496
1563
  maxTokens: args.maxTokens,
1497
- temperature: args.temperature
1564
+ temperature: args.temperature,
1565
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1498
1566
  });
1499
1567
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1500
1568
  const init = {
@@ -1545,6 +1613,17 @@ var OPENAI_COMPAT_DEFAULT_URLS = {
1545
1613
  groq: "https://api.groq.com/openai/v1/chat/completions",
1546
1614
  deepseek: "https://api.deepseek.com/v1/chat/completions"
1547
1615
  };
1616
+ var OPENAI_COMPAT_NATIVE_STRUCTURED = /* @__PURE__ */ new Set([
1617
+ "openai"
1618
+ ]);
1619
+ function supportsNativeJsonSchema(provider, model) {
1620
+ if (!OPENAI_COMPAT_NATIVE_STRUCTURED.has(provider.toLowerCase())) {
1621
+ return false;
1622
+ }
1623
+ const m = model.toLowerCase();
1624
+ if (/^o1(?:-|$)/.test(m)) return false;
1625
+ return true;
1626
+ }
1548
1627
  function buildOpenAICompatibleRequest(opts) {
1549
1628
  const msgs = opts.messages.map((m) => ({
1550
1629
  role: m.role,
@@ -1560,6 +1639,16 @@ function buildOpenAICompatibleRequest(opts) {
1560
1639
  } else {
1561
1640
  body.max_completion_tokens = opts.maxTokens;
1562
1641
  }
1642
+ if (opts.jsonSchema && supportsNativeJsonSchema(opts.provider, opts.model)) {
1643
+ body.response_format = {
1644
+ type: "json_schema",
1645
+ json_schema: {
1646
+ name: "structured_output",
1647
+ schema: opts.jsonSchema,
1648
+ strict: true
1649
+ }
1650
+ };
1651
+ }
1563
1652
  const headers = {
1564
1653
  "content-type": "application/json",
1565
1654
  Authorization: `Bearer ${opts.apiKey}`
@@ -1631,7 +1720,8 @@ async function sendOpenAICompatible(args) {
1631
1720
  url: args.url,
1632
1721
  messages: args.messages,
1633
1722
  maxTokens: args.maxTokens,
1634
- temperature: args.temperature
1723
+ temperature: args.temperature,
1724
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1635
1725
  });
1636
1726
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1637
1727
  const init = {
@@ -2104,11 +2194,13 @@ ${schemaText}`;
2104
2194
  content: "Your previous response failed validation:\n- " + lastErrs.slice(0, 5).join("\n- ") + "\nFix the issues and re-emit valid JSON only."
2105
2195
  });
2106
2196
  }
2197
+ const useNative = opts.useNativeStructured !== false;
2107
2198
  const ans = await askOne(provider, msgs, {
2108
2199
  maxTokens: opts.maxTokens,
2109
2200
  temperature: temp,
2110
2201
  purpose,
2111
- ...opts.signal ? { signal: opts.signal } : {}
2202
+ ...opts.signal ? { signal: opts.signal } : {},
2203
+ ...useNative ? { jsonSchema: schema } : {}
2112
2204
  });
2113
2205
  lastAnswer = ans;
2114
2206
  if (ans.error !== void 0) {
@@ -2143,7 +2235,8 @@ async function askOne(provider, messages, opts) {
2143
2235
  maxTokens: opts.maxTokens,
2144
2236
  temperature: opts.temperature,
2145
2237
  purpose: opts.purpose,
2146
- ...opts.signal ? { signal: opts.signal } : {}
2238
+ ...opts.signal ? { signal: opts.signal } : {},
2239
+ ...opts.jsonSchema ? { jsonSchema: opts.jsonSchema } : {}
2147
2240
  });
2148
2241
  const wallMs = Math.trunc(performance.now() - startedWall);
2149
2242
  const cpu = process.cpuUsage(startedCpu);
@@ -2386,7 +2479,7 @@ async function runAudit(args, opts) {
2386
2479
  const rubricOverride = args["rubric"];
2387
2480
  const producing = toStringArray(args["producing_panelists"]).map((s) => s.toLowerCase());
2388
2481
  const explicit = typeof args["auditor"] === "string" ? args["auditor"] : null;
2389
- const cheapMode = boolArg(args["cheap_mode"], true);
2482
+ const cheapMode = boolArg(args["cheap_mode"], opts.ctx?.cheap_mode ?? true);
2390
2483
  const allowSelf = boolArg(args["allow_self_audit"], false);
2391
2484
  let coalesce = boolArg(args["coalesce"], false);
2392
2485
  const strictMode = boolArg(args["strict_mode"], false);
@@ -3916,7 +4009,7 @@ async function runConfer(args, opts) {
3916
4009
  };
3917
4010
  }
3918
4011
  }
3919
- const { selected, unknown: unknownNames, blocked } = resolveProviders(
4012
+ let { selected, unknown: unknownNames, blocked } = resolveProviders(
3920
4013
  resolvedProviders,
3921
4014
  opts.providers,
3922
4015
  opts.allowlist ?? null
@@ -3933,6 +4026,49 @@ async function runConfer(args, opts) {
3933
4026
  }
3934
4027
  return { tool: "confer", error: "no active providers have API keys in .env" };
3935
4028
  }
4029
+ let cheapModePanelMeta = null;
4030
+ if (opts.ctx?.cheap_mode === true && !callerSuppliedProviders && selected.length > 1) {
4031
+ if (!opts.pricing) {
4032
+ cheapModePanelMeta = {
4033
+ before: selected.length,
4034
+ after: selected.length,
4035
+ picked: null,
4036
+ reason: "ctx.cheap_mode=true but no pricing doc wired; kept full panel"
4037
+ };
4038
+ } else {
4039
+ const availableSet = new Set(selected.map((p) => p.name.toLowerCase()));
4040
+ const cheapWeights = opts.storage ? await loadProviderWeights(opts.storage, selected.map((p) => p.name)) : {};
4041
+ const allowOnly = opts.allowlist && opts.allowlist.length > 0 ? opts.allowlist.map((s) => s.toLowerCase()) : void 0;
4042
+ const tierPick = selectForDifficulty({
4043
+ pricing: opts.pricing,
4044
+ tier: "low",
4045
+ availableProviders: availableSet,
4046
+ providerWeights: cheapWeights,
4047
+ ...allowOnly !== void 0 ? { allowOnly } : {}
4048
+ });
4049
+ if (tierPick.pick) {
4050
+ const baseProvider = opts.providers[tierPick.pick.provider.toLowerCase()];
4051
+ if (baseProvider) {
4052
+ const beforeCount = selected.length;
4053
+ const retargeted = retargetProvider(baseProvider, tierPick.pick.model);
4054
+ selected = [retargeted];
4055
+ cheapModePanelMeta = {
4056
+ before: beforeCount,
4057
+ after: 1,
4058
+ picked: { provider: tierPick.pick.provider, model: tierPick.pick.model },
4059
+ reason: "ctx.cheap_mode=true; narrowed to cheapest low-tier candidate"
4060
+ };
4061
+ }
4062
+ } else {
4063
+ cheapModePanelMeta = {
4064
+ before: selected.length,
4065
+ after: selected.length,
4066
+ picked: null,
4067
+ reason: `ctx.cheap_mode=true; ${tierPick.reason ?? "no candidate"}; kept full panel`
4068
+ };
4069
+ }
4070
+ }
4071
+ }
3936
4072
  const untrusted = Boolean(args["untrusted_input"]);
3937
4073
  const canary = untrusted ? mintCanary() : null;
3938
4074
  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.";
@@ -4049,6 +4185,7 @@ ${ctxBody}` });
4049
4185
  if (claimsBlock !== null) result["claims"] = claimsBlock;
4050
4186
  if (leaks.length > 0) result["canary_leaks"] = leaks;
4051
4187
  if (autoPanelMeta !== null) result["auto_panel"] = autoPanelMeta;
4188
+ if (cheapModePanelMeta !== null) result["cheap_mode_panel"] = cheapModePanelMeta;
4052
4189
  if (requestedWorkerTools.length > 0) {
4053
4190
  result["worker_tools"] = {
4054
4191
  accepted: acceptedWorkerTools,
@@ -5220,6 +5357,40 @@ var import_node_path8 = __toESM(require("path"), 1);
5220
5357
 
5221
5358
  // src/tools/orchestrate.ts
5222
5359
  var import_node_perf_hooks2 = require("perf_hooks");
5360
+
5361
+ // src/core/dead-models.ts
5362
+ var DEAD_MODELS_TTL_MS = 5 * 60 * 1e3;
5363
+ var deadModels = /* @__PURE__ */ new Map();
5364
+ function makeKey(provider, model) {
5365
+ return `${provider.toLowerCase()}:${model.toLowerCase()}`;
5366
+ }
5367
+ function recordDeadModel(provider, model, reason, nowMs = Date.now()) {
5368
+ const key = makeKey(provider, model);
5369
+ deadModels.set(key, {
5370
+ provider: provider.toLowerCase(),
5371
+ model: model.toLowerCase(),
5372
+ reason: reason.slice(0, 200),
5373
+ expires: nowMs + DEAD_MODELS_TTL_MS
5374
+ });
5375
+ }
5376
+ function isModelDead(provider, model, nowMs = Date.now()) {
5377
+ const key = makeKey(provider, model);
5378
+ const entry = deadModels.get(key);
5379
+ if (!entry) return false;
5380
+ if (nowMs >= entry.expires) {
5381
+ deadModels.delete(key);
5382
+ return false;
5383
+ }
5384
+ return true;
5385
+ }
5386
+ function isDeadModelError(err) {
5387
+ if (typeof err.error !== "string") return false;
5388
+ if (err.error_kind !== "client") return false;
5389
+ return /HTTP\s*404|not\s*found|does not exist|deprecated|decommission/i.test(err.error);
5390
+ }
5391
+
5392
+ // src/tools/orchestrate.ts
5393
+ var F1B_MAX_TIER_RETRIES = 2;
5223
5394
  var DIFFICULTY_TIERS2 = ["low", "med", "high"];
5224
5395
  var EST_TOKENS = {
5225
5396
  low: [800, 400],
@@ -5270,7 +5441,7 @@ async function runOrchestrate(args, opts) {
5270
5441
  const moderator = opts.providers[moderatorName.toLowerCase()] ?? selected[0];
5271
5442
  const failFast = boolArg2(args["fail_fast"], false);
5272
5443
  const planOnly = boolArg2(args["plan_only"], false);
5273
- const cheapMode = boolArg2(args["cheap_mode"], false);
5444
+ const cheapMode = boolArg2(args["cheap_mode"], opts.ctx?.cheap_mode ?? false);
5274
5445
  const maxTokens = opts.maxTokens ?? 4096;
5275
5446
  const cheapAvailable = new Set(
5276
5447
  selected.map((p) => p.name.toLowerCase())
@@ -5356,12 +5527,51 @@ async function runOrchestrate(args, opts) {
5356
5527
  failedIds.add(nid);
5357
5528
  continue;
5358
5529
  }
5530
+ const depsRaw = Array.isArray(node["depends_on"]) ? node["depends_on"] : [];
5531
+ const upstreamBlocks = [];
5532
+ for (const d of depsRaw) {
5533
+ if (typeof d !== "string") continue;
5534
+ const ur = nodeResults[d];
5535
+ if (ur && ur.status === "ok") {
5536
+ upstreamBlocks.push(`[node ${d} output]
5537
+ ${ur.output ?? ""}`);
5538
+ } else if (ur && ur.status === "failed") {
5539
+ upstreamBlocks.push(`[node ${d}] [MISSING: failed \u2014 ${ur.error ?? ""}]`);
5540
+ }
5541
+ }
5542
+ const ctxBlock = upstreamBlocks.length > 0 ? upstreamBlocks.join("\n\n") : "(no upstream nodes)";
5543
+ const sysMsg = "You are a worker LLM in an orchestrated DAG. Complete the assigned task using outputs from upstream nodes when relevant. Be concise.";
5544
+ const role = typeof node["role"] === "string" ? node["role"] : "worker";
5545
+ const task = String(node["task"] ?? "");
5546
+ const difficulty = String(node["difficulty"] ?? "med");
5547
+ const msgs = [
5548
+ { role: "system", content: sysMsg },
5549
+ {
5550
+ role: "user",
5551
+ content: `ROLE: ${role}
5552
+ DIFFICULTY: ${difficulty}
5553
+
5554
+ UPSTREAM:
5555
+ ${ctxBlock}
5556
+
5557
+ TASK:
5558
+ ${task}`
5559
+ }
5560
+ ];
5359
5561
  let chosen;
5562
+ let ans;
5360
5563
  let cheapReason = null;
5564
+ const retryAttempts = [];
5361
5565
  const pinnedName = typeof node["provider"] === "string" ? node["provider"].toLowerCase() : null;
5362
5566
  const pinnedModel = typeof node["model"] === "string" ? node["model"] : null;
5567
+ const started = import_node_perf_hooks2.performance.now();
5363
5568
  if (pinnedName && opts.providers[pinnedName]) {
5364
5569
  chosen = opts.providers[pinnedName];
5570
+ ans = await askOne(chosen, msgs, {
5571
+ maxTokens,
5572
+ temperature: 0.4,
5573
+ purpose: "worker"
5574
+ });
5365
5575
  } else if (cheapMode && !pinnedName && !pinnedModel && opts.pricing) {
5366
5576
  const tier = String(node["difficulty"] ?? "med");
5367
5577
  const pick = selectForDifficulty({
@@ -5371,21 +5581,78 @@ async function runOrchestrate(args, opts) {
5371
5581
  providerWeights: cheapWeights,
5372
5582
  ...cheapAllowOnly ? { allowOnly: cheapAllowOnly } : {}
5373
5583
  });
5374
- if (pick.pick) {
5375
- const base = opts.providers[pick.pick.provider];
5376
- if (base) chosen = retargetProvider(base, pick.pick.model);
5584
+ const aliveCandidates = pick.scored.filter(
5585
+ (c) => !isModelDead(c.provider, c.model)
5586
+ );
5587
+ if (aliveCandidates.length === 0) {
5588
+ cheapReason = pick.reason ?? "all tier candidates filtered by deadModels cache";
5377
5589
  } else {
5378
- cheapReason = pick.reason;
5590
+ const tryLimit = Math.min(F1B_MAX_TIER_RETRIES, aliveCandidates.length);
5591
+ for (let i = 0; i < tryLimit; i++) {
5592
+ const c = aliveCandidates[i];
5593
+ const base = opts.providers[c.provider];
5594
+ if (!base) {
5595
+ retryAttempts.push({
5596
+ provider: c.provider,
5597
+ model: c.model,
5598
+ reason: "provider not in active registry"
5599
+ });
5600
+ continue;
5601
+ }
5602
+ const candidateProvider = retargetProvider(base, c.model);
5603
+ const candidateAns = await askOne(candidateProvider, msgs, {
5604
+ maxTokens,
5605
+ temperature: 0.4,
5606
+ purpose: "worker"
5607
+ });
5608
+ if (candidateAns.error === void 0) {
5609
+ chosen = candidateProvider;
5610
+ ans = candidateAns;
5611
+ if (retryAttempts.length > 0) {
5612
+ cheapReason = `recovered after ${retryAttempts.length} dead-model retry(s): ` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ");
5613
+ }
5614
+ break;
5615
+ }
5616
+ if (isDeadModelError(candidateAns)) {
5617
+ recordDeadModel(c.provider, c.model, candidateAns.error ?? "");
5618
+ retryAttempts.push({
5619
+ provider: c.provider,
5620
+ model: c.model,
5621
+ reason: (candidateAns.error ?? "dead").slice(0, 100)
5622
+ });
5623
+ continue;
5624
+ }
5625
+ chosen = candidateProvider;
5626
+ ans = candidateAns;
5627
+ if (retryAttempts.length > 0) {
5628
+ cheapReason = `${retryAttempts.length} dead-model retry(s) before non-dead failure: ` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ");
5629
+ }
5630
+ break;
5631
+ }
5379
5632
  }
5380
5633
  if (!chosen) {
5381
5634
  const idx = djb2Hash(nid) % Math.max(1, selected.length);
5382
5635
  chosen = selected[idx];
5636
+ ans = await askOne(chosen, msgs, {
5637
+ maxTokens,
5638
+ temperature: 0.4,
5639
+ purpose: "worker"
5640
+ });
5641
+ if (retryAttempts.length > 0) {
5642
+ cheapReason = `cheap-mode tier exhausted (` + retryAttempts.map((a) => `${a.provider}/${a.model}`).join(", ") + `); fell back to id-hash rotation`;
5643
+ }
5383
5644
  }
5384
5645
  } else {
5385
5646
  const idx = djb2Hash(nid) % Math.max(1, selected.length);
5386
5647
  chosen = selected[idx];
5648
+ ans = await askOne(chosen, msgs, {
5649
+ maxTokens,
5650
+ temperature: 0.4,
5651
+ purpose: "worker"
5652
+ });
5387
5653
  }
5388
- if (!chosen) {
5654
+ const wallMs = Math.trunc(import_node_perf_hooks2.performance.now() - started);
5655
+ if (!chosen || !ans) {
5389
5656
  nodeResults[nid] = {
5390
5657
  id: nid,
5391
5658
  status: "failed",
@@ -5393,49 +5660,12 @@ async function runOrchestrate(args, opts) {
5393
5660
  model: null,
5394
5661
  error: "no provider available",
5395
5662
  wall_ms: 0,
5396
- cpu_ms: 0
5663
+ cpu_ms: 0,
5664
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5397
5665
  };
5398
5666
  failedIds.add(nid);
5399
5667
  continue;
5400
5668
  }
5401
- const depsRaw = Array.isArray(node["depends_on"]) ? node["depends_on"] : [];
5402
- const upstreamBlocks = [];
5403
- for (const d of depsRaw) {
5404
- if (typeof d !== "string") continue;
5405
- const ur = nodeResults[d];
5406
- if (ur && ur.status === "ok") {
5407
- upstreamBlocks.push(`[node ${d} output]
5408
- ${ur.output ?? ""}`);
5409
- } else if (ur && ur.status === "failed") {
5410
- upstreamBlocks.push(`[node ${d}] [MISSING: failed \u2014 ${ur.error ?? ""}]`);
5411
- }
5412
- }
5413
- const ctxBlock = upstreamBlocks.length > 0 ? upstreamBlocks.join("\n\n") : "(no upstream nodes)";
5414
- const sysMsg = "You are a worker LLM in an orchestrated DAG. Complete the assigned task using outputs from upstream nodes when relevant. Be concise.";
5415
- const role = typeof node["role"] === "string" ? node["role"] : "worker";
5416
- const task = String(node["task"] ?? "");
5417
- const difficulty = String(node["difficulty"] ?? "med");
5418
- const msgs = [
5419
- { role: "system", content: sysMsg },
5420
- {
5421
- role: "user",
5422
- content: `ROLE: ${role}
5423
- DIFFICULTY: ${difficulty}
5424
-
5425
- UPSTREAM:
5426
- ${ctxBlock}
5427
-
5428
- TASK:
5429
- ${task}`
5430
- }
5431
- ];
5432
- const started = import_node_perf_hooks2.performance.now();
5433
- const ans = await askOne(chosen, msgs, {
5434
- maxTokens,
5435
- temperature: 0.4,
5436
- purpose: "worker"
5437
- });
5438
- const wallMs = Math.trunc(import_node_perf_hooks2.performance.now() - started);
5439
5669
  if (ans.error !== void 0) {
5440
5670
  nodeResults[nid] = {
5441
5671
  id: nid,
@@ -5445,7 +5675,8 @@ ${task}`
5445
5675
  error: ans.error,
5446
5676
  wall_ms: wallMs,
5447
5677
  cpu_ms: ans.cpu_ms,
5448
- ...cheapReason ? { cheap_fallback_reason: cheapReason } : {}
5678
+ ...cheapReason ? { cheap_fallback_reason: cheapReason } : {},
5679
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5449
5680
  };
5450
5681
  failedIds.add(nid);
5451
5682
  continue;
@@ -5458,7 +5689,8 @@ ${task}`
5458
5689
  output: ans.response ?? "",
5459
5690
  wall_ms: wallMs,
5460
5691
  cpu_ms: ans.cpu_ms,
5461
- ...cheapReason ? { cheap_fallback_reason: cheapReason } : {}
5692
+ ...cheapReason ? { cheap_fallback_reason: cheapReason } : {},
5693
+ ...retryAttempts.length > 0 ? { retry_attempts: retryAttempts } : {}
5462
5694
  };
5463
5695
  }
5464
5696
  const missing = Object.keys(nodesById).filter(
@@ -5485,11 +5717,43 @@ Synthesize the node outputs into a single coherent deliverable. Preserve any [MI
5485
5717
  { role: "system", content: "You are the orchestrator. Combine node outputs into the final result." },
5486
5718
  { role: "user", content: recombinePrompt }
5487
5719
  ];
5488
- const synthAns = await askOne(moderator, recMsgs, {
5720
+ let synthProvider = moderator;
5721
+ let synthModel = null;
5722
+ if (cheapMode && opts.pricing) {
5723
+ const pick = selectForDifficulty({
5724
+ pricing: opts.pricing,
5725
+ tier: "med",
5726
+ availableProviders: cheapAvailable,
5727
+ providerWeights: cheapWeights,
5728
+ ...cheapAllowOnly ? { allowOnly: cheapAllowOnly } : {}
5729
+ });
5730
+ for (const c of pick.scored) {
5731
+ if (isModelDead(c.provider, c.model)) continue;
5732
+ const base = opts.providers[c.provider];
5733
+ if (base) {
5734
+ synthProvider = retargetProvider(base, c.model);
5735
+ synthModel = `${c.provider}/${c.model}`;
5736
+ break;
5737
+ }
5738
+ }
5739
+ }
5740
+ let synthAns = await askOne(synthProvider, recMsgs, {
5489
5741
  maxTokens,
5490
5742
  temperature: 0.4,
5491
5743
  purpose: "synth"
5492
5744
  });
5745
+ if (synthAns.error !== void 0 && synthProvider !== moderator) {
5746
+ if (isDeadModelError(synthAns) && synthModel) {
5747
+ const [prov, mdl] = synthModel.split("/");
5748
+ if (prov && mdl) recordDeadModel(prov, mdl, synthAns.error ?? "");
5749
+ }
5750
+ synthAns = await askOne(moderator, recMsgs, {
5751
+ maxTokens,
5752
+ temperature: 0.4,
5753
+ purpose: "synth"
5754
+ });
5755
+ synthModel = null;
5756
+ }
5493
5757
  const finalText = synthAns.error !== void 0 ? "" : synthAns.response ?? "";
5494
5758
  const synthErr = synthAns.error;
5495
5759
  const publicNodes = Object.keys(nodesById).map(
@@ -5513,6 +5777,7 @@ Synthesize the node outputs into a single coherent deliverable. Preserve any [MI
5513
5777
  fail_fast: failFast,
5514
5778
  cheap_mode: cheapMode
5515
5779
  };
5780
+ if (synthModel) result["synth_model"] = synthModel;
5516
5781
  if (synthErr) result["synth_error"] = synthErr;
5517
5782
  if (plannerErrors.length > 0) result["planner_errors"] = plannerErrors;
5518
5783
  if (unknownNames.length > 0) result["skipped_unknown_providers"] = unknownNames;
@@ -6084,6 +6349,16 @@ function errorPayload(code, message, hint, kind = "client") {
6084
6349
  };
6085
6350
  }
6086
6351
 
6352
+ // src/core/call-context.ts
6353
+ function buildCallContext(args) {
6354
+ const ctx = {
6355
+ cheap_mode: args.cheapMode,
6356
+ session_id: args.sessionId
6357
+ };
6358
+ if (args.purpose) ctx.purpose = args.purpose;
6359
+ return ctx;
6360
+ }
6361
+
6087
6362
  // src/tools/create.ts
6088
6363
  var CREATE_DOC_MAX_BYTES = 32 * 1024;
6089
6364
  var CREATE_AUDIT_THRESHOLD = 0.7;
@@ -6112,6 +6387,10 @@ async function runCreate(args, opts) {
6112
6387
  const dryRun = boolArg3(args["dry_run"], false);
6113
6388
  const planOnly = boolArg3(args["plan_only"], false);
6114
6389
  const moderator = typeof args["moderator"] === "string" && args["moderator"] ? args["moderator"] : opts.moderator ?? "anthropic";
6390
+ const ctx = opts.ctx ?? buildCallContext({
6391
+ cheapMode,
6392
+ sessionId
6393
+ });
6115
6394
  const descriptors = await ingestDocuments(documents, sessionId, opts);
6116
6395
  const documentsPayload = formatDocumentsPayload(descriptors);
6117
6396
  const okCount = descriptors.filter((d) => d.status === "ok").length;
@@ -6129,7 +6408,8 @@ ${documentsPayload}`;
6129
6408
  }, {
6130
6409
  providers: opts.providers,
6131
6410
  allowlist: opts.allowlist ?? null,
6132
- ...opts.bridge ? { bridge: opts.bridge } : {}
6411
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6412
+ ctx
6133
6413
  });
6134
6414
  const scopeAnswer = Array.isArray(scope["answers"]) ? scope["answers"].filter((a) => typeof a["response"] === "string" && a["response"]).map((a) => `[${a["provider"]}]
6135
6415
  ${a["response"]}`).join("\n\n") : "";
@@ -6151,7 +6431,8 @@ ${scopeAnswer || "(none)"}
6151
6431
  providers: opts.providers,
6152
6432
  allowlist: opts.allowlist ?? null,
6153
6433
  moderator,
6154
- ...opts.bridge ? { bridge: opts.bridge } : {}
6434
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6435
+ ctx
6155
6436
  });
6156
6437
  let attempts = 1;
6157
6438
  if (planOnly) {
@@ -6185,7 +6466,8 @@ ${scopeAnswer || "(none)"}
6185
6466
  }, {
6186
6467
  providers: opts.providers,
6187
6468
  allowlist: opts.allowlist ?? null,
6188
- ...opts.bridge ? { bridge: opts.bridge } : {}
6469
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6470
+ ctx
6189
6471
  });
6190
6472
  }
6191
6473
  let auditEnvelope = null;
@@ -6207,7 +6489,8 @@ ${scopeAnswer || "(none)"}
6207
6489
  }, {
6208
6490
  providers: opts.providers,
6209
6491
  allowlist: opts.allowlist ?? null,
6210
- ...opts.bridge ? { bridge: opts.bridge } : {}
6492
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6493
+ ctx
6211
6494
  });
6212
6495
  let overallF = toNumberOrNull(auditEnvelope["overall_score"]);
6213
6496
  if (overallF !== null && overallF < auditThreshold && !cheapMode && !boolArg3(args["_no_retry"], false)) {
@@ -6231,7 +6514,8 @@ ${documentsPayload}`
6231
6514
  providers: opts.providers,
6232
6515
  allowlist: opts.allowlist ?? null,
6233
6516
  moderator,
6234
- ...opts.bridge ? { bridge: opts.bridge } : {}
6517
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6518
+ ctx
6235
6519
  }
6236
6520
  );
6237
6521
  orchestration = retryOrchestration;
@@ -6250,7 +6534,8 @@ ${documentsPayload}`
6250
6534
  }, {
6251
6535
  providers: opts.providers,
6252
6536
  allowlist: opts.allowlist ?? null,
6253
- ...opts.bridge ? { bridge: opts.bridge } : {}
6537
+ ...opts.bridge ? { bridge: opts.bridge } : {},
6538
+ ctx
6254
6539
  });
6255
6540
  overallF = toNumberOrNull(auditEnvelope["overall_score"]);
6256
6541
  } else {
@@ -8101,7 +8386,7 @@ CRITERIA:
8101
8386
  ${criteriaBlock}`
8102
8387
  }
8103
8388
  ];
8104
- const maxTokens = opts.maxTokens ?? 2048;
8389
+ const maxTokens = opts.maxTokens ?? 4096;
8105
8390
  const scoresByProvider = {};
8106
8391
  const answersCollected = [];
8107
8392
  const scoringErrors = {};
@@ -8297,9 +8582,23 @@ function toArray(v) {
8297
8582
  }
8298
8583
 
8299
8584
  // src/tools/plan.ts
8585
+ var PLAN_MODE_PRESETS = {
8586
+ fast: { max_rounds: 2, early_stop: true, early_stop_threshold: 0.7 },
8587
+ thorough: { max_rounds: 5, early_stop: false, early_stop_threshold: 0.7 }
8588
+ };
8589
+ var DEFAULT_PLAN_MODE = "fast";
8300
8590
  async function runPlan(args, opts) {
8301
8591
  const goal = typeof args["goal"] === "string" ? args["goal"] : String(args["goal"] ?? "");
8302
8592
  const constraints = typeof args["constraints"] === "string" ? args["constraints"] : "";
8593
+ const modeArg = args["mode"];
8594
+ const mode = modeArg === "fast" || modeArg === "thorough" ? modeArg : DEFAULT_PLAN_MODE;
8595
+ const preset = PLAN_MODE_PRESETS[mode];
8596
+ const maxRoundsRaw = args["max_rounds"];
8597
+ const earlyStopRaw = args["early_stop"];
8598
+ const earlyStopThresholdRaw = args["early_stop_threshold"];
8599
+ const maxRounds = typeof maxRoundsRaw === "number" && Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? Math.trunc(maxRoundsRaw) : preset.max_rounds;
8600
+ const earlyStop = typeof earlyStopRaw === "boolean" ? earlyStopRaw : preset.early_stop;
8601
+ const earlyStopThreshold = typeof earlyStopThresholdRaw === "number" && Number.isFinite(earlyStopThresholdRaw) ? earlyStopThresholdRaw : preset.early_stop_threshold;
8303
8602
  const merged = `We need a step-by-step plan to achieve this goal.
8304
8603
 
8305
8604
  GOAL: ${goal}
@@ -8310,7 +8609,10 @@ Return: (1) the plan as numbered steps, (2) risks, (3) alternatives considered.`
8310
8609
  const debateArgs = {
8311
8610
  topic: merged,
8312
8611
  context: typeof args["context"] === "string" ? args["context"] : "",
8313
- structured: Boolean(args["structured"])
8612
+ structured: Boolean(args["structured"]),
8613
+ max_rounds: maxRounds,
8614
+ early_stop: earlyStop,
8615
+ early_stop_threshold: earlyStopThreshold
8314
8616
  };
8315
8617
  if (args["providers"] !== void 0) debateArgs["providers"] = args["providers"];
8316
8618
  if (args["moderator"] !== void 0) debateArgs["moderator"] = args["moderator"];
@@ -10020,7 +10322,7 @@ function critiqueTool(providers, allowlist, bridge) {
10020
10322
  function planTool(providers, allowlist, bridge) {
10021
10323
  return {
10022
10324
  name: "plan",
10023
- 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.",
10325
+ 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.',
10024
10326
  inputSchema: {
10025
10327
  type: "object",
10026
10328
  additionalProperties: true,
@@ -10031,7 +10333,11 @@ function planTool(providers, allowlist, bridge) {
10031
10333
  providers: { type: "array", items: { type: "string" } },
10032
10334
  moderator: { type: "string" },
10033
10335
  session_id: { type: "string" },
10034
- structured: { type: "boolean" }
10336
+ structured: { type: "boolean" },
10337
+ mode: { type: "string", enum: ["fast", "thorough"] },
10338
+ max_rounds: { type: "integer", minimum: 1 },
10339
+ early_stop: { type: "boolean" },
10340
+ early_stop_threshold: { type: "number", minimum: 0, maximum: 1 }
10035
10341
  },
10036
10342
  required: ["goal"]
10037
10343
  },
@@ -10424,8 +10730,19 @@ async function main() {
10424
10730
  );
10425
10731
  }
10426
10732
  installShutdownHandlers(bridge);
10427
- const pricingPath = process.env["CROSSCHECK_PRICING_PATH"] ?? resolveRepoFile("config/pricing.json");
10428
- const pricing = pricingPath && (0, import_node_fs11.existsSync)(pricingPath) ? loadPricing(pricingPath) : {};
10733
+ const bundledPricing = (() => {
10734
+ try {
10735
+ return import_node_path11.default.join(import_node_path11.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl)), "pricing.json");
10736
+ } catch {
10737
+ return void 0;
10738
+ }
10739
+ })();
10740
+ const pricingPath = [
10741
+ process.env["CROSSCHECK_PRICING_PATH"],
10742
+ resolveRepoFile("config/pricing.json"),
10743
+ bundledPricing
10744
+ ].find((p) => p && (0, import_node_fs11.existsSync)(p));
10745
+ const pricing = pricingPath ? loadPricing(pricingPath) : {};
10429
10746
  const providers = buildProviders({ env: process.env, pricing });
10430
10747
  if (Object.keys(providers).length > 0) {
10431
10748
  process.stderr.write(