@warmdrift/kgauto-compiler 2.0.0-alpha.75 → 2.0.0-alpha.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,7 +25,10 @@ __export(index_exports, {
25
25
  ALL_ARCHETYPES: () => ALL_ARCHETYPES,
26
26
  ARCHETYPE_FAMILY_FITS: () => ARCHETYPE_FAMILY_FITS,
27
27
  ARCHETYPE_FLOOR_DEFAULT: () => ARCHETYPE_FLOOR_DEFAULT,
28
+ COACH_CFG: () => COACH_CFG,
28
29
  CallError: () => CallError,
30
+ DECOMPOSITION_TEMPLATES: () => DECOMPOSITION_TEMPLATES,
31
+ DECOMPOSITION_TEMPLATES_VERSION: () => DECOMPOSITION_TEMPLATES_VERSION,
29
32
  DEFAULT_FINDINGS_ENDPOINT: () => DEFAULT_FINDINGS_ENDPOINT,
30
33
  DEFAULT_MEASURED_FAILURE_ENDPOINT: () => DEFAULT_MEASURED_FAILURE_ENDPOINT,
31
34
  DEFAULT_PROMOTIONS_ENDPOINT: () => DEFAULT_PROMOTIONS_ENDPOINT,
@@ -125,6 +128,7 @@ __export(index_exports, {
125
128
  parseGoldenCaptureRate: () => parseGoldenCaptureRate,
126
129
  parseJudgeVerdict: () => parseJudgeVerdict,
127
130
  peekBrainDeadLetter: () => peekBrainDeadLetter,
131
+ planDecomposition: () => planDecomposition,
128
132
  prefetchMeasuredFailure: () => prefetchMeasuredFailure,
129
133
  probeShadow: () => probeShadow,
130
134
  profileToRow: () => profileToRow,
@@ -6422,8 +6426,33 @@ function isAuthSignatureBody(body, message) {
6422
6426
  const m = message.toLowerCase();
6423
6427
  return m.includes("api key not valid") || m.includes("invalid api key") || m.includes("invalid x-api-key") || m.includes("incorrect api key");
6424
6428
  }
6429
+ function isBillingExhaustedBody(status, body, message) {
6430
+ if (status === 402) return true;
6431
+ const m = message.toLowerCase();
6432
+ if (m.includes("free_tier") || m.includes("free tier")) return false;
6433
+ if (m.includes("credit balance is too low")) return true;
6434
+ if (m.includes("insufficient balance")) return true;
6435
+ if (m.includes("billingnotenabled") || m.includes("billing not enabled")) return true;
6436
+ if (body && typeof body === "object") {
6437
+ const err = body.error;
6438
+ if (err && typeof err === "object") {
6439
+ const e = err;
6440
+ if (e.code === "insufficient_quota" || e.type === "insufficient_quota") return true;
6441
+ if (Array.isArray(e.details)) {
6442
+ for (const d of e.details) {
6443
+ if (d && typeof d === "object" && d.reason === "BILLING_DISABLED") {
6444
+ return true;
6445
+ }
6446
+ }
6447
+ }
6448
+ }
6449
+ }
6450
+ return false;
6451
+ }
6425
6452
  function classifyHttpError(status, body) {
6426
6453
  const message = extractErrorMessage(body) ?? `HTTP ${status}`;
6454
+ if (isBillingExhaustedBody(status, body, message))
6455
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
6427
6456
  if (status === 429)
6428
6457
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
6429
6458
  if (status === 408)
@@ -6687,6 +6716,9 @@ function applyOverrides(request, overrides) {
6687
6716
  }
6688
6717
  function classifyHttpError2(status, body) {
6689
6718
  const message = extractErrorMessage2(body) ?? `HTTP ${status}`;
6719
+ if (isBillingExhaustedBody(status, body, message)) {
6720
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
6721
+ }
6690
6722
  if (status === 429) {
6691
6723
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
6692
6724
  }
@@ -7529,18 +7561,19 @@ async function call(ir, opts = {}) {
7529
7561
  }
7530
7562
  let activeCompile = initial;
7531
7563
  let lastErr;
7532
- const failedProviders = /* @__PURE__ */ new Set();
7564
+ const failedProviders = /* @__PURE__ */ new Map();
7533
7565
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
7534
7566
  let retriedSameModel = false;
7535
7567
  for (let i = 0; i < targetsToTry.length; i++) {
7536
7568
  const targetModel = targetsToTry[i];
7537
7569
  const targetProfile = tryGetProfile(targetModel);
7538
- if (targetProfile && failedProviders.has(targetProfile.provider) && !opts.noFallback) {
7570
+ const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
7571
+ if (targetProfile && providerFailReason && !opts.noFallback) {
7539
7572
  attempts.push({
7540
7573
  model: targetModel,
7541
7574
  status: "terminal",
7542
- errorCode: "auth_inferred",
7543
- message: `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
7575
+ errorCode: `${providerFailReason}_inferred`,
7576
+ message: providerFailReason === "billing_exhausted" ? `Skipped \u2014 provider ${targetProfile.provider} is out of credits (seen earlier in this call); a billing failure is account-wide, so this attempt cannot succeed until a human funds the account` : `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
7544
7577
  });
7545
7578
  continue;
7546
7579
  }
@@ -7722,15 +7755,18 @@ async function call(ir, opts = {}) {
7722
7755
  });
7723
7756
  lastErr = validated;
7724
7757
  if (validated.errorType === "terminal" || opts.noFallback) {
7725
- if (validated.errorCode === "auth" && !opts.noFallback && activeCompile.provider) {
7726
- failedProviders.add(activeCompile.provider);
7758
+ if ((validated.errorCode === "auth" || validated.errorCode === "billing_exhausted") && !opts.noFallback && activeCompile.provider) {
7759
+ failedProviders.set(
7760
+ activeCompile.provider,
7761
+ validated.errorCode === "billing_exhausted" ? "billing_exhausted" : "auth"
7762
+ );
7727
7763
  continue;
7728
7764
  }
7729
7765
  break;
7730
7766
  }
7731
7767
  }
7732
7768
  const lastAttempted = [...attempts].reverse().find(
7733
- (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "compile_error"
7769
+ (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "billing_exhausted_inferred" && a.errorCode !== "compile_error"
7734
7770
  );
7735
7771
  const latencyMs = Date.now() - start;
7736
7772
  await record({
@@ -8052,6 +8088,9 @@ function normalizeFallbackReason(attempts) {
8052
8088
  }
8053
8089
  if (code === "cost_cap_exceeded") return "cost_cap";
8054
8090
  if (code === "auth" || code === "auth_inferred") return "provider_auth_failed";
8091
+ if (code === "billing_exhausted" || code === "billing_exhausted_inferred") {
8092
+ return "provider_billing_exhausted";
8093
+ }
8055
8094
  return "provider_error";
8056
8095
  }
8057
8096
  function generateTraceId() {
@@ -9039,7 +9078,7 @@ function createBrainForwardRoutes(config) {
9039
9078
  }
9040
9079
 
9041
9080
  // src/version.ts
9042
- var LIBRARY_VERSION = "2.0.0-alpha.75";
9081
+ var LIBRARY_VERSION = "2.0.0-alpha.77";
9043
9082
 
9044
9083
  // src/key-health.ts
9045
9084
  var JSON_HEADERS2 = { "Content-Type": "application/json" };
@@ -9657,6 +9696,211 @@ async function markExclusionFindingHandled(opts) {
9657
9696
  return { ok: true };
9658
9697
  }
9659
9698
 
9699
+ // src/decomposition.ts
9700
+ var DECOMPOSITION_TEMPLATES_VERSION = "decomposition-templates-v1";
9701
+ var DECOMPOSITION_TEMPLATES = {
9702
+ summarize: {
9703
+ archetype: "summarize",
9704
+ version: DECOMPOSITION_TEMPLATES_VERSION,
9705
+ rationale: "A long-input summarize is mostly reading. A cheap extractor reads the full payload and emits compressed notes; the incumbent composes the summary from the notes \u2014 frontier tokens are spent only on the part that needs frontier judgment.",
9706
+ steps: [
9707
+ {
9708
+ role: "chunk-extract",
9709
+ archetype: "extract",
9710
+ tier: "delegate",
9711
+ inputShare: 1,
9712
+ emitsShareOfInput: 0.15,
9713
+ outputShare: 0
9714
+ },
9715
+ {
9716
+ role: "compose",
9717
+ archetype: "summarize",
9718
+ tier: "anchor",
9719
+ inputShare: 0.15,
9720
+ // reads the notes, not the raw payload
9721
+ emitsShareOfInput: 0,
9722
+ outputShare: 1
9723
+ }
9724
+ ]
9725
+ },
9726
+ hunt: {
9727
+ archetype: "hunt",
9728
+ version: DECOMPOSITION_TEMPLATES_VERSION,
9729
+ rationale: "Hunt decomposes into breadth (search sweeps), mechanical harvesting (extraction), and judgment (dedupe + compose). The sweeps and the harvest are grunt work; the composition anchors on the incumbent.",
9730
+ steps: [
9731
+ {
9732
+ role: "search-sweep",
9733
+ archetype: "hunt",
9734
+ tier: "delegate",
9735
+ inputShare: 0.5,
9736
+ emitsShareOfInput: 0.2,
9737
+ outputShare: 0
9738
+ },
9739
+ {
9740
+ role: "harvest",
9741
+ archetype: "extract",
9742
+ tier: "delegate",
9743
+ inputShare: 0.35,
9744
+ emitsShareOfInput: 0.1,
9745
+ outputShare: 0
9746
+ },
9747
+ {
9748
+ role: "dedupe-compose",
9749
+ archetype: "judge",
9750
+ tier: "anchor",
9751
+ inputShare: 0.3,
9752
+ // sweep notes + harvest notes
9753
+ emitsShareOfInput: 0,
9754
+ outputShare: 1
9755
+ }
9756
+ ]
9757
+ },
9758
+ plan: {
9759
+ archetype: "plan",
9760
+ version: DECOMPOSITION_TEMPLATES_VERSION,
9761
+ rationale: "Planning splits into context-gathering (mechanical reading) and the plan itself (judgment). The gatherer reads the corpus and briefs; the incumbent plans from the brief.",
9762
+ steps: [
9763
+ {
9764
+ role: "gather-brief",
9765
+ archetype: "extract",
9766
+ tier: "delegate",
9767
+ inputShare: 1,
9768
+ emitsShareOfInput: 0.2,
9769
+ outputShare: 0
9770
+ },
9771
+ {
9772
+ role: "draft-plan",
9773
+ archetype: "plan",
9774
+ tier: "anchor",
9775
+ inputShare: 0.2,
9776
+ emitsShareOfInput: 0,
9777
+ outputShare: 1
9778
+ }
9779
+ ]
9780
+ }
9781
+ };
9782
+ var COACH_CFG = {
9783
+ /** Same felt-utility floor as promotions (alpha.67 family). */
9784
+ minMonthlySavingUsd: 5,
9785
+ /** Executor must clear this archetypePerf on the step's archetype —
9786
+ * same floor as the translator/advisor (ARCHETYPE_FLOOR_DEFAULT). */
9787
+ executorPerfFloor: 6,
9788
+ daysPerMonth: 30
9789
+ };
9790
+ function planDecomposition(args) {
9791
+ const { stats, incumbentPricing, pickExecutor } = args;
9792
+ const cfg = { ...COACH_CFG, ...args.cfg ?? {} };
9793
+ const template = DECOMPOSITION_TEMPLATES[stats.archetype];
9794
+ if (!template) return void 0;
9795
+ const perCall = (pricing, tokensIn, tokensOut) => tokensIn / 1e6 * pricing.costInputPer1m + tokensOut / 1e6 * pricing.costOutputPer1m;
9796
+ const monolithCostPerCallUsd = perCall(
9797
+ incumbentPricing,
9798
+ stats.avgTokensIn,
9799
+ stats.avgTokensOut
9800
+ );
9801
+ const assumptions = [
9802
+ `per-step token shares are ${template.version} JUDGMENT numbers \u2014 no fan-out traffic exists to measure them from yet; they graduate per surface when branch traffic lands`
9803
+ ];
9804
+ const steps = [];
9805
+ let splitCostPerCallUsd = 0;
9806
+ for (const step of template.steps) {
9807
+ const stepTokensIn = stats.avgTokensIn * step.inputShare;
9808
+ const stepTokensOut = stats.avgTokensIn * step.emitsShareOfInput + stats.avgTokensOut * step.outputShare;
9809
+ if (step.tier === "anchor") {
9810
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
9811
+ splitCostPerCallUsd += cost2;
9812
+ steps.push({
9813
+ ...step,
9814
+ executorModel: stats.incumbentModel,
9815
+ executorGrounding: "measured",
9816
+ // the incumbent IS the measured baseline
9817
+ executorPerfScore: null,
9818
+ projectedCostPerCallUsd: cost2
9819
+ });
9820
+ continue;
9821
+ }
9822
+ const candidate = pickExecutor(step.archetype);
9823
+ if (candidate && candidate.modelId === stats.incumbentModel) {
9824
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
9825
+ splitCostPerCallUsd += cost2;
9826
+ steps.push({
9827
+ ...step,
9828
+ tier: "anchor",
9829
+ executorModel: stats.incumbentModel,
9830
+ executorGrounding: candidate.grounding,
9831
+ executorPerfScore: candidate.perfScore,
9832
+ projectedCostPerCallUsd: cost2
9833
+ });
9834
+ assumptions.push(
9835
+ `step '${step.role}': the cheapest qualified executor IS the incumbent \u2014 nothing to delegate to`
9836
+ );
9837
+ continue;
9838
+ }
9839
+ if (!candidate || candidate.perfScore < cfg.executorPerfFloor) {
9840
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
9841
+ splitCostPerCallUsd += cost2;
9842
+ steps.push({
9843
+ ...step,
9844
+ tier: "anchor",
9845
+ executorModel: stats.incumbentModel,
9846
+ executorGrounding: "measured",
9847
+ executorPerfScore: candidate?.perfScore ?? null,
9848
+ projectedCostPerCallUsd: cost2
9849
+ });
9850
+ assumptions.push(
9851
+ `step '${step.role}' (${step.archetype}): no executor clears the perf floor ${cfg.executorPerfFloor} \u2014 kept on the incumbent`
9852
+ );
9853
+ continue;
9854
+ }
9855
+ const cost = perCall(candidate, stepTokensIn, stepTokensOut);
9856
+ splitCostPerCallUsd += cost;
9857
+ steps.push({
9858
+ ...step,
9859
+ executorModel: candidate.modelId,
9860
+ executorGrounding: candidate.grounding,
9861
+ executorPerfScore: candidate.perfScore,
9862
+ projectedCostPerCallUsd: cost
9863
+ });
9864
+ if (candidate.grounding === "judgment") {
9865
+ assumptions.push(
9866
+ `step '${step.role}': ${candidate.modelId} perf ${candidate.perfScore}/10 on ${step.archetype} is JUDGMENT-grounded (no measured portfolio outcomes for the tuple yet)`
9867
+ );
9868
+ }
9869
+ }
9870
+ const savingPerCallUsd = monolithCostPerCallUsd - splitCostPerCallUsd;
9871
+ const monthlyCalls = stats.nCalls / stats.windowDays * cfg.daysPerMonth;
9872
+ const projectedMonthlySavingUsd = savingPerCallUsd * monthlyCalls;
9873
+ const breakEvenMonthlyCalls = savingPerCallUsd > 0 ? cfg.minMonthlySavingUsd / savingPerCallUsd : null;
9874
+ let verdict;
9875
+ let verdictReason;
9876
+ if (savingPerCallUsd <= 0) {
9877
+ verdict = "keep-monolith";
9878
+ verdictReason = "the split costs MORE per call than the monolith at current pricing \u2014 no volume makes it pay";
9879
+ } else if (projectedMonthlySavingUsd < cfg.minMonthlySavingUsd) {
9880
+ verdict = "keep-monolith";
9881
+ verdictReason = `the split pays $${projectedMonthlySavingUsd.toFixed(2)}/mo at your ~${Math.round(monthlyCalls)} calls/mo \u2014 below the $${cfg.minMonthlySavingUsd} felt-utility floor. Break-even is ~${Math.ceil(breakEvenMonthlyCalls ?? 0)} calls/mo; revisit when volume gets there. Decomposition adds moving parts, and a saving you can't feel doesn't buy them`;
9882
+ } else {
9883
+ verdict = "split-pays";
9884
+ verdictReason = `projected $${projectedMonthlySavingUsd.toFixed(2)}/mo saving at ~${Math.round(monthlyCalls)} calls/mo (monolith $${monolithCostPerCallUsd.toFixed(4)}/call \u2192 split $${splitCostPerCallUsd.toFixed(4)}/call)`;
9885
+ }
9886
+ return {
9887
+ appId: stats.appId,
9888
+ archetype: String(stats.archetype),
9889
+ incumbentModel: stats.incumbentModel,
9890
+ template,
9891
+ steps,
9892
+ monolithCostPerCallUsd,
9893
+ splitCostPerCallUsd,
9894
+ savingPerCallUsd,
9895
+ monthlyCalls,
9896
+ projectedMonthlySavingUsd,
9897
+ breakEvenMonthlyCalls,
9898
+ verdict,
9899
+ verdictReason,
9900
+ assumptions
9901
+ };
9902
+ }
9903
+
9660
9904
  // src/index.ts
9661
9905
  function compile2(ir, opts) {
9662
9906
  const result = compile(ir, opts);
@@ -9670,7 +9914,10 @@ function compile2(ir, opts) {
9670
9914
  ALL_ARCHETYPES,
9671
9915
  ARCHETYPE_FAMILY_FITS,
9672
9916
  ARCHETYPE_FLOOR_DEFAULT,
9917
+ COACH_CFG,
9673
9918
  CallError,
9919
+ DECOMPOSITION_TEMPLATES,
9920
+ DECOMPOSITION_TEMPLATES_VERSION,
9674
9921
  DEFAULT_FINDINGS_ENDPOINT,
9675
9922
  DEFAULT_MEASURED_FAILURE_ENDPOINT,
9676
9923
  DEFAULT_PROMOTIONS_ENDPOINT,
@@ -9770,6 +10017,7 @@ function compile2(ir, opts) {
9770
10017
  parseGoldenCaptureRate,
9771
10018
  parseJudgeVerdict,
9772
10019
  peekBrainDeadLetter,
10020
+ planDecomposition,
9773
10021
  prefetchMeasuredFailure,
9774
10022
  probeShadow,
9775
10023
  profileToRow,
package/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  LIBRARY_VERSION,
18
18
  createKeyHealthRoute
19
- } from "./chunk-WP22F3CX.mjs";
19
+ } from "./chunk-GB7VJQ6C.mjs";
20
20
  import {
21
21
  ABSOLUTE_FLOOR,
22
22
  ARCHETYPE_FLOOR_DEFAULT,
@@ -4222,8 +4222,33 @@ function isAuthSignatureBody(body, message) {
4222
4222
  const m = message.toLowerCase();
4223
4223
  return m.includes("api key not valid") || m.includes("invalid api key") || m.includes("invalid x-api-key") || m.includes("incorrect api key");
4224
4224
  }
4225
+ function isBillingExhaustedBody(status, body, message) {
4226
+ if (status === 402) return true;
4227
+ const m = message.toLowerCase();
4228
+ if (m.includes("free_tier") || m.includes("free tier")) return false;
4229
+ if (m.includes("credit balance is too low")) return true;
4230
+ if (m.includes("insufficient balance")) return true;
4231
+ if (m.includes("billingnotenabled") || m.includes("billing not enabled")) return true;
4232
+ if (body && typeof body === "object") {
4233
+ const err = body.error;
4234
+ if (err && typeof err === "object") {
4235
+ const e = err;
4236
+ if (e.code === "insufficient_quota" || e.type === "insufficient_quota") return true;
4237
+ if (Array.isArray(e.details)) {
4238
+ for (const d of e.details) {
4239
+ if (d && typeof d === "object" && d.reason === "BILLING_DISABLED") {
4240
+ return true;
4241
+ }
4242
+ }
4243
+ }
4244
+ }
4245
+ }
4246
+ return false;
4247
+ }
4225
4248
  function classifyHttpError(status, body) {
4226
4249
  const message = extractErrorMessage(body) ?? `HTTP ${status}`;
4250
+ if (isBillingExhaustedBody(status, body, message))
4251
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
4227
4252
  if (status === 429)
4228
4253
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
4229
4254
  if (status === 408)
@@ -4487,6 +4512,9 @@ function applyOverrides(request, overrides) {
4487
4512
  }
4488
4513
  function classifyHttpError2(status, body) {
4489
4514
  const message = extractErrorMessage2(body) ?? `HTTP ${status}`;
4515
+ if (isBillingExhaustedBody(status, body, message)) {
4516
+ return { ok: false, status, errorType: "terminal", errorCode: "billing_exhausted", message, raw: body };
4517
+ }
4490
4518
  if (status === 429) {
4491
4519
  return { ok: false, status, errorType: "retryable", errorCode: "rate_limit", message, raw: body };
4492
4520
  }
@@ -4675,18 +4703,19 @@ async function call(ir, opts = {}) {
4675
4703
  }
4676
4704
  let activeCompile = initial;
4677
4705
  let lastErr;
4678
- const failedProviders = /* @__PURE__ */ new Set();
4706
+ const failedProviders = /* @__PURE__ */ new Map();
4679
4707
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
4680
4708
  let retriedSameModel = false;
4681
4709
  for (let i = 0; i < targetsToTry.length; i++) {
4682
4710
  const targetModel = targetsToTry[i];
4683
4711
  const targetProfile = tryGetProfile(targetModel);
4684
- if (targetProfile && failedProviders.has(targetProfile.provider) && !opts.noFallback) {
4712
+ const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
4713
+ if (targetProfile && providerFailReason && !opts.noFallback) {
4685
4714
  attempts.push({
4686
4715
  model: targetModel,
4687
4716
  status: "terminal",
4688
- errorCode: "auth_inferred",
4689
- message: `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
4717
+ errorCode: `${providerFailReason}_inferred`,
4718
+ message: providerFailReason === "billing_exhausted" ? `Skipped \u2014 provider ${targetProfile.provider} is out of credits (seen earlier in this call); a billing failure is account-wide, so this attempt cannot succeed until a human funds the account` : `Skipped \u2014 provider ${targetProfile.provider} returned 401/403 earlier in this call; same key inferred to fail`
4690
4719
  });
4691
4720
  continue;
4692
4721
  }
@@ -4868,15 +4897,18 @@ async function call(ir, opts = {}) {
4868
4897
  });
4869
4898
  lastErr = validated;
4870
4899
  if (validated.errorType === "terminal" || opts.noFallback) {
4871
- if (validated.errorCode === "auth" && !opts.noFallback && activeCompile.provider) {
4872
- failedProviders.add(activeCompile.provider);
4900
+ if ((validated.errorCode === "auth" || validated.errorCode === "billing_exhausted") && !opts.noFallback && activeCompile.provider) {
4901
+ failedProviders.set(
4902
+ activeCompile.provider,
4903
+ validated.errorCode === "billing_exhausted" ? "billing_exhausted" : "auth"
4904
+ );
4873
4905
  continue;
4874
4906
  }
4875
4907
  break;
4876
4908
  }
4877
4909
  }
4878
4910
  const lastAttempted = [...attempts].reverse().find(
4879
- (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "compile_error"
4911
+ (a) => a.status !== "success" && a.errorCode !== "auth_inferred" && a.errorCode !== "billing_exhausted_inferred" && a.errorCode !== "compile_error"
4880
4912
  );
4881
4913
  const latencyMs = Date.now() - start;
4882
4914
  await record({
@@ -5198,6 +5230,9 @@ function normalizeFallbackReason(attempts) {
5198
5230
  }
5199
5231
  if (code === "cost_cap_exceeded") return "cost_cap";
5200
5232
  if (code === "auth" || code === "auth_inferred") return "provider_auth_failed";
5233
+ if (code === "billing_exhausted" || code === "billing_exhausted_inferred") {
5234
+ return "provider_billing_exhausted";
5235
+ }
5201
5236
  return "provider_error";
5202
5237
  }
5203
5238
  function generateTraceId() {
@@ -6438,6 +6473,211 @@ async function markExclusionFindingHandled(opts) {
6438
6473
  return { ok: true };
6439
6474
  }
6440
6475
 
6476
+ // src/decomposition.ts
6477
+ var DECOMPOSITION_TEMPLATES_VERSION = "decomposition-templates-v1";
6478
+ var DECOMPOSITION_TEMPLATES = {
6479
+ summarize: {
6480
+ archetype: "summarize",
6481
+ version: DECOMPOSITION_TEMPLATES_VERSION,
6482
+ rationale: "A long-input summarize is mostly reading. A cheap extractor reads the full payload and emits compressed notes; the incumbent composes the summary from the notes \u2014 frontier tokens are spent only on the part that needs frontier judgment.",
6483
+ steps: [
6484
+ {
6485
+ role: "chunk-extract",
6486
+ archetype: "extract",
6487
+ tier: "delegate",
6488
+ inputShare: 1,
6489
+ emitsShareOfInput: 0.15,
6490
+ outputShare: 0
6491
+ },
6492
+ {
6493
+ role: "compose",
6494
+ archetype: "summarize",
6495
+ tier: "anchor",
6496
+ inputShare: 0.15,
6497
+ // reads the notes, not the raw payload
6498
+ emitsShareOfInput: 0,
6499
+ outputShare: 1
6500
+ }
6501
+ ]
6502
+ },
6503
+ hunt: {
6504
+ archetype: "hunt",
6505
+ version: DECOMPOSITION_TEMPLATES_VERSION,
6506
+ rationale: "Hunt decomposes into breadth (search sweeps), mechanical harvesting (extraction), and judgment (dedupe + compose). The sweeps and the harvest are grunt work; the composition anchors on the incumbent.",
6507
+ steps: [
6508
+ {
6509
+ role: "search-sweep",
6510
+ archetype: "hunt",
6511
+ tier: "delegate",
6512
+ inputShare: 0.5,
6513
+ emitsShareOfInput: 0.2,
6514
+ outputShare: 0
6515
+ },
6516
+ {
6517
+ role: "harvest",
6518
+ archetype: "extract",
6519
+ tier: "delegate",
6520
+ inputShare: 0.35,
6521
+ emitsShareOfInput: 0.1,
6522
+ outputShare: 0
6523
+ },
6524
+ {
6525
+ role: "dedupe-compose",
6526
+ archetype: "judge",
6527
+ tier: "anchor",
6528
+ inputShare: 0.3,
6529
+ // sweep notes + harvest notes
6530
+ emitsShareOfInput: 0,
6531
+ outputShare: 1
6532
+ }
6533
+ ]
6534
+ },
6535
+ plan: {
6536
+ archetype: "plan",
6537
+ version: DECOMPOSITION_TEMPLATES_VERSION,
6538
+ rationale: "Planning splits into context-gathering (mechanical reading) and the plan itself (judgment). The gatherer reads the corpus and briefs; the incumbent plans from the brief.",
6539
+ steps: [
6540
+ {
6541
+ role: "gather-brief",
6542
+ archetype: "extract",
6543
+ tier: "delegate",
6544
+ inputShare: 1,
6545
+ emitsShareOfInput: 0.2,
6546
+ outputShare: 0
6547
+ },
6548
+ {
6549
+ role: "draft-plan",
6550
+ archetype: "plan",
6551
+ tier: "anchor",
6552
+ inputShare: 0.2,
6553
+ emitsShareOfInput: 0,
6554
+ outputShare: 1
6555
+ }
6556
+ ]
6557
+ }
6558
+ };
6559
+ var COACH_CFG = {
6560
+ /** Same felt-utility floor as promotions (alpha.67 family). */
6561
+ minMonthlySavingUsd: 5,
6562
+ /** Executor must clear this archetypePerf on the step's archetype —
6563
+ * same floor as the translator/advisor (ARCHETYPE_FLOOR_DEFAULT). */
6564
+ executorPerfFloor: 6,
6565
+ daysPerMonth: 30
6566
+ };
6567
+ function planDecomposition(args) {
6568
+ const { stats, incumbentPricing, pickExecutor } = args;
6569
+ const cfg = { ...COACH_CFG, ...args.cfg ?? {} };
6570
+ const template = DECOMPOSITION_TEMPLATES[stats.archetype];
6571
+ if (!template) return void 0;
6572
+ const perCall = (pricing, tokensIn, tokensOut) => tokensIn / 1e6 * pricing.costInputPer1m + tokensOut / 1e6 * pricing.costOutputPer1m;
6573
+ const monolithCostPerCallUsd = perCall(
6574
+ incumbentPricing,
6575
+ stats.avgTokensIn,
6576
+ stats.avgTokensOut
6577
+ );
6578
+ const assumptions = [
6579
+ `per-step token shares are ${template.version} JUDGMENT numbers \u2014 no fan-out traffic exists to measure them from yet; they graduate per surface when branch traffic lands`
6580
+ ];
6581
+ const steps = [];
6582
+ let splitCostPerCallUsd = 0;
6583
+ for (const step of template.steps) {
6584
+ const stepTokensIn = stats.avgTokensIn * step.inputShare;
6585
+ const stepTokensOut = stats.avgTokensIn * step.emitsShareOfInput + stats.avgTokensOut * step.outputShare;
6586
+ if (step.tier === "anchor") {
6587
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
6588
+ splitCostPerCallUsd += cost2;
6589
+ steps.push({
6590
+ ...step,
6591
+ executorModel: stats.incumbentModel,
6592
+ executorGrounding: "measured",
6593
+ // the incumbent IS the measured baseline
6594
+ executorPerfScore: null,
6595
+ projectedCostPerCallUsd: cost2
6596
+ });
6597
+ continue;
6598
+ }
6599
+ const candidate = pickExecutor(step.archetype);
6600
+ if (candidate && candidate.modelId === stats.incumbentModel) {
6601
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
6602
+ splitCostPerCallUsd += cost2;
6603
+ steps.push({
6604
+ ...step,
6605
+ tier: "anchor",
6606
+ executorModel: stats.incumbentModel,
6607
+ executorGrounding: candidate.grounding,
6608
+ executorPerfScore: candidate.perfScore,
6609
+ projectedCostPerCallUsd: cost2
6610
+ });
6611
+ assumptions.push(
6612
+ `step '${step.role}': the cheapest qualified executor IS the incumbent \u2014 nothing to delegate to`
6613
+ );
6614
+ continue;
6615
+ }
6616
+ if (!candidate || candidate.perfScore < cfg.executorPerfFloor) {
6617
+ const cost2 = perCall(incumbentPricing, stepTokensIn, stepTokensOut);
6618
+ splitCostPerCallUsd += cost2;
6619
+ steps.push({
6620
+ ...step,
6621
+ tier: "anchor",
6622
+ executorModel: stats.incumbentModel,
6623
+ executorGrounding: "measured",
6624
+ executorPerfScore: candidate?.perfScore ?? null,
6625
+ projectedCostPerCallUsd: cost2
6626
+ });
6627
+ assumptions.push(
6628
+ `step '${step.role}' (${step.archetype}): no executor clears the perf floor ${cfg.executorPerfFloor} \u2014 kept on the incumbent`
6629
+ );
6630
+ continue;
6631
+ }
6632
+ const cost = perCall(candidate, stepTokensIn, stepTokensOut);
6633
+ splitCostPerCallUsd += cost;
6634
+ steps.push({
6635
+ ...step,
6636
+ executorModel: candidate.modelId,
6637
+ executorGrounding: candidate.grounding,
6638
+ executorPerfScore: candidate.perfScore,
6639
+ projectedCostPerCallUsd: cost
6640
+ });
6641
+ if (candidate.grounding === "judgment") {
6642
+ assumptions.push(
6643
+ `step '${step.role}': ${candidate.modelId} perf ${candidate.perfScore}/10 on ${step.archetype} is JUDGMENT-grounded (no measured portfolio outcomes for the tuple yet)`
6644
+ );
6645
+ }
6646
+ }
6647
+ const savingPerCallUsd = monolithCostPerCallUsd - splitCostPerCallUsd;
6648
+ const monthlyCalls = stats.nCalls / stats.windowDays * cfg.daysPerMonth;
6649
+ const projectedMonthlySavingUsd = savingPerCallUsd * monthlyCalls;
6650
+ const breakEvenMonthlyCalls = savingPerCallUsd > 0 ? cfg.minMonthlySavingUsd / savingPerCallUsd : null;
6651
+ let verdict;
6652
+ let verdictReason;
6653
+ if (savingPerCallUsd <= 0) {
6654
+ verdict = "keep-monolith";
6655
+ verdictReason = "the split costs MORE per call than the monolith at current pricing \u2014 no volume makes it pay";
6656
+ } else if (projectedMonthlySavingUsd < cfg.minMonthlySavingUsd) {
6657
+ verdict = "keep-monolith";
6658
+ verdictReason = `the split pays $${projectedMonthlySavingUsd.toFixed(2)}/mo at your ~${Math.round(monthlyCalls)} calls/mo \u2014 below the $${cfg.minMonthlySavingUsd} felt-utility floor. Break-even is ~${Math.ceil(breakEvenMonthlyCalls ?? 0)} calls/mo; revisit when volume gets there. Decomposition adds moving parts, and a saving you can't feel doesn't buy them`;
6659
+ } else {
6660
+ verdict = "split-pays";
6661
+ verdictReason = `projected $${projectedMonthlySavingUsd.toFixed(2)}/mo saving at ~${Math.round(monthlyCalls)} calls/mo (monolith $${monolithCostPerCallUsd.toFixed(4)}/call \u2192 split $${splitCostPerCallUsd.toFixed(4)}/call)`;
6662
+ }
6663
+ return {
6664
+ appId: stats.appId,
6665
+ archetype: String(stats.archetype),
6666
+ incumbentModel: stats.incumbentModel,
6667
+ template,
6668
+ steps,
6669
+ monolithCostPerCallUsd,
6670
+ splitCostPerCallUsd,
6671
+ savingPerCallUsd,
6672
+ monthlyCalls,
6673
+ projectedMonthlySavingUsd,
6674
+ breakEvenMonthlyCalls,
6675
+ verdict,
6676
+ verdictReason,
6677
+ assumptions
6678
+ };
6679
+ }
6680
+
6441
6681
  // src/index.ts
6442
6682
  function compile2(ir, opts) {
6443
6683
  const result = compile(ir, opts);
@@ -6450,7 +6690,10 @@ export {
6450
6690
  ALL_ARCHETYPES,
6451
6691
  ARCHETYPE_FAMILY_FITS,
6452
6692
  ARCHETYPE_FLOOR_DEFAULT,
6693
+ COACH_CFG,
6453
6694
  CallError,
6695
+ DECOMPOSITION_TEMPLATES,
6696
+ DECOMPOSITION_TEMPLATES_VERSION,
6454
6697
  DEFAULT_FINDINGS_ENDPOINT,
6455
6698
  DEFAULT_MEASURED_FAILURE_ENDPOINT,
6456
6699
  DEFAULT_PROMOTIONS_ENDPOINT,
@@ -6550,6 +6793,7 @@ export {
6550
6793
  parseGoldenCaptureRate,
6551
6794
  parseJudgeVerdict,
6552
6795
  peekBrainDeadLetter,
6796
+ planDecomposition,
6553
6797
  prefetchMeasuredFailure,
6554
6798
  probeShadow,
6555
6799
  profileToRow,