@mindot/will 0.7.0 → 0.8.0

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.
Files changed (35) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +1 -1
  3. package/dist/channels/whatsapp.d.ts +1 -1
  4. package/dist/cli.js +10823 -10454
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +562 -226
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/effectors.d.ts +1 -1
  10. package/dist/{will-DAW0l-lY.d.ts → will-cS6k4uiJ.d.ts} +470 -84
  11. package/package.json +1 -1
  12. package/src/cognition/agency/engines/action.selector.ts +2 -1
  13. package/src/cognition/agency/engines/reafference.engine.ts +12 -2
  14. package/src/cognition/agency/reconcile.learning.ts +16 -2
  15. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  16. package/src/cognition/config.mirror.entities.ts +1 -1
  17. package/src/cognition/faculties/executive.engine/engine.ts +136 -58
  18. package/src/cognition/faculties/executive.engine/facet.ts +10 -2
  19. package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
  20. package/src/cognition/index.ts +4 -0
  21. package/src/cognition/memory/vector.embedder.ts +9 -5
  22. package/src/cognition/utilities/token.tracker.ts +191 -96
  23. package/src/host/boot.ts +78 -22
  24. package/src/index.ts +35 -0
  25. package/src/llm/index.ts +397 -96
  26. package/src/llm/routing.ts +198 -0
  27. package/src/llm/summarizer.ts +5 -1
  28. package/src/runners/thin-shim.runner.ts +18 -6
  29. package/src/sdk/will.ts +82 -16
  30. package/src/stem/guards/identity.coherence.ts +17 -6
  31. package/src/stem/index.ts +3 -3
  32. package/src/stem/mind.ts +155 -24
  33. package/src/stem/policy/arbiter.ts +49 -14
  34. package/src/stem/policy/rule.table.ts +2 -2
  35. package/src/stem/tracts/effector.controller.ts +56 -9
package/dist/index.js CHANGED
@@ -2800,62 +2800,26 @@ var AsyncEngine = class {
2800
2800
  }
2801
2801
  }
2802
2802
  };
2803
- var MODEL_PRICING = {
2804
- // OpenAI
2805
- "openai/gpt-4o": { input: 2.5, output: 10 },
2806
- "openai/gpt-4o-mini": { input: 0.15, output: 0.6 },
2807
- "openai/gpt-4-turbo": { input: 10, output: 30 },
2808
- "openai/gpt-3.5-turbo": { input: 0.5, output: 1.5 },
2809
- // Anthropic (Claude 4.x family — prices per 1M tokens)
2810
- "anthropic/claude-haiku-4-5": { input: 1, output: 5 },
2811
- "anthropic/claude-sonnet-4-5": { input: 3, output: 15 },
2812
- "anthropic/claude-sonnet-4-6": { input: 3, output: 15 },
2813
- "anthropic/claude-opus-4-7": { input: 5, output: 25 },
2814
- // Legacy aliases kept for backward compat
2815
- "anthropic/claude-haiku-4": { input: 1, output: 5 },
2816
- "anthropic/claude-opus-4": { input: 5, output: 25 },
2817
- // Z.ai (GLM-5 family). `glm-5.2[1m]` is the same model asking for its 1M
2818
- // context window — same rate, so it gets its own row rather than relying on
2819
- // the normalizer (a future long-context tier would price differently).
2820
- "glm/glm-5.2": { input: 1.4, output: 4.4 },
2821
- "glm/glm-5.2[1m]": { input: 1.4, output: 4.4 },
2822
- // Google
2823
- "google/gemini-2.0-flash": { input: 0.1, output: 0.4 },
2824
- "google/gemini-2.0-pro": { input: 1.25, output: 5 },
2825
- // Meta (via Groq/Replicate)
2826
- "meta/llama-3.3-70b": { input: 0.59, output: 0.79 },
2827
- "meta/llama-4-maverick": { input: 0.2, output: 0.6 },
2828
- // DeepSeek
2829
- "deepseek/deepseek-v3": { input: 0.27, output: 1.1 },
2830
- "deepseek/deepseek-r1": { input: 0.55, output: 2.19 },
2831
- // Embeddings (input-only — no completion tokens). Priced per 1M input tokens.
2832
- "openai/text-embedding-3-small": { input: 0.02, output: 0 },
2833
- "openai/text-embedding-3-large": { input: 0.13, output: 0 },
2834
- "google/text-embedding-004": { input: 0, output: 0 },
2835
- // free tier
2836
- "google/gemini-embedding-001": { input: 0, output: 0 },
2837
- // free tier
2838
- // Fallback for unknown models
2839
- "__default__": { input: 3, output: 15 }
2840
- };
2841
2803
  var CACHE_READ_MULT = 0.1;
2842
2804
  var CACHE_WRITE_MULT = 1.25;
2843
2805
  function normalizeModelKey(model) {
2844
2806
  let m = model.toLowerCase().trim();
2845
2807
  const slash = m.lastIndexOf("/");
2846
2808
  if (slash >= 0) m = m.slice(slash + 1);
2809
+ m = m.replace(/\[[^\]]*\]$/, "");
2847
2810
  return m.replace(/[-@]\d{6,8}$/, "");
2848
2811
  }
2849
- var PRICING_BY_NORM = (() => {
2850
- const out = {};
2851
- for (const [key, price] of Object.entries(MODEL_PRICING)) {
2852
- if (key === "__default__") continue;
2853
- out[normalizeModelKey(key)] = price;
2812
+ var _unpricedWarned = /* @__PURE__ */ new Set();
2813
+ function resolvePricing(model, hostPrices) {
2814
+ if (!hostPrices) return null;
2815
+ const exact = hostPrices[model];
2816
+ if (exact) return exact;
2817
+ const norm = normalizeModelKey(model);
2818
+ if (hostPrices[norm]) return hostPrices[norm];
2819
+ for (const [key, price] of Object.entries(hostPrices)) {
2820
+ if (normalizeModelKey(key) === norm) return price;
2854
2821
  }
2855
- return out;
2856
- })();
2857
- function resolvePricing(model) {
2858
- return PRICING_BY_NORM[normalizeModelKey(model)] ?? MODEL_PRICING[model] ?? MODEL_PRICING["__default__"];
2822
+ return null;
2859
2823
  }
2860
2824
  function composeLabel(m) {
2861
2825
  const base = `${m.category}/${m.attribute}/${m.function}`;
@@ -2865,6 +2829,7 @@ var TokenTracker = class {
2865
2829
  name = "token-tracker";
2866
2830
  _emitCostEvents;
2867
2831
  _costWarningThreshold;
2832
+ _prices;
2868
2833
  // All recorded usage for the simulation run
2869
2834
  _usageLog = [];
2870
2835
  // Running totals
@@ -2876,6 +2841,11 @@ var TokenTracker = class {
2876
2841
  _categoryTokens = /* @__PURE__ */ new Map();
2877
2842
  _functionCosts = /* @__PURE__ */ new Map();
2878
2843
  _functionTokens = /* @__PURE__ */ new Map();
2844
+ // Per-provider spend. The axis a host actually reconciles against invoices —
2845
+ // "which vendor did we pay?" is not answerable from the model id once routing
2846
+ // can reach one model through several of them.
2847
+ _providerCosts = /* @__PURE__ */ new Map();
2848
+ _providerTokens = /* @__PURE__ */ new Map();
2879
2849
  // Per-tick costs (for spike detection)
2880
2850
  _tickCosts = [];
2881
2851
  _maxTickCostSamples = 1e3;
@@ -2891,6 +2861,7 @@ var TokenTracker = class {
2891
2861
  constructor(config = {}) {
2892
2862
  this._emitCostEvents = config.emitCostEvents ?? true;
2893
2863
  this._costWarningThreshold = config.costWarningThresholdUsd ?? 0.05;
2864
+ this._prices = config.prices;
2894
2865
  this._ledgerPath = config.writeLedger && config.willId ? `./data/wills/${config.willId}/debug/token-report.jsonl` : null;
2895
2866
  }
2896
2867
  // ── Public API: record usage ──────────────────────────────
@@ -2899,13 +2870,20 @@ var TokenTracker = class {
2899
2870
  * Called by LLMDirector.call after each completion (src/llm/index.ts).
2900
2871
  */
2901
2872
  recordUsage(usage) {
2902
- const pricing = resolvePricing(usage.model);
2873
+ const pricing = resolvePricing(usage.model, this._prices);
2903
2874
  const cacheRead = usage.cacheReadTokens ?? 0;
2904
2875
  const cacheWrite = usage.cacheWriteTokens ?? 0;
2905
- const costUsd = usage.promptTokens / 1e6 * pricing.input + usage.completionTokens / 1e6 * pricing.output + cacheRead / 1e6 * pricing.input * CACHE_READ_MULT + cacheWrite / 1e6 * pricing.input * CACHE_WRITE_MULT;
2876
+ if (!pricing && !_unpricedWarned.has(usage.model)) {
2877
+ _unpricedWarned.add(usage.model);
2878
+ logger.warn(
2879
+ `[tokens] no price for "${usage.model}" \u2014 reporting cost 0 for it. Supply one via llm.providers.<provider>.prices to get cost telemetry.`
2880
+ );
2881
+ }
2882
+ const costUsd = pricing ? usage.promptTokens / 1e6 * pricing.input + usage.completionTokens / 1e6 * pricing.output + cacheRead / 1e6 * pricing.input * CACHE_READ_MULT + cacheWrite / 1e6 * pricing.input * CACHE_WRITE_MULT : 0;
2906
2883
  const full = {
2907
2884
  ...usage,
2908
2885
  label: usage.label ?? composeLabel(usage),
2886
+ priced: pricing !== null,
2909
2887
  estimatedCostUsd: Math.round(costUsd * 1e6) / 1e6
2910
2888
  // round to micro-dollars
2911
2889
  };
@@ -2915,6 +2893,7 @@ var TokenTracker = class {
2915
2893
  this._totalCost += full.estimatedCostUsd;
2916
2894
  this._accumulate(this._categoryCosts, this._categoryTokens, full.category, full);
2917
2895
  this._accumulate(this._functionCosts, this._functionTokens, full.function, full);
2896
+ this._accumulate(this._providerCosts, this._providerTokens, full.provider ?? "unattributed", full);
2918
2897
  this._emitLedger(full);
2919
2898
  }
2920
2899
  /**
@@ -2935,6 +2914,7 @@ var TokenTracker = class {
2935
2914
  ts: new Date(wallClock()).toISOString(),
2936
2915
  // determinism-ok: ledger timestamp is telemetry, never replay state
2937
2916
  model: full.model,
2917
+ provider: full.provider,
2938
2918
  category: full.category,
2939
2919
  attribute: full.attribute,
2940
2920
  function: full.function,
@@ -2946,6 +2926,10 @@ var TokenTracker = class {
2946
2926
  cacheWriteTok: full.cacheWriteTokens ?? 0,
2947
2927
  estPromptTok: full.estPromptTokens,
2948
2928
  costUsd: full.estimatedCostUsd,
2929
+ // Whether costUsd came from a real price. False ⇒ 0 because nothing
2930
+ // priced this model, NOT because the call was free — a consumer summing
2931
+ // spend must not fold unpriced calls in as zero.
2932
+ priced: full.priced,
2949
2933
  latencyMs: full.latencyMs
2950
2934
  };
2951
2935
  for (const fn of this._recordListeners) {
@@ -2984,23 +2968,14 @@ var TokenTracker = class {
2984
2968
  commands.metrics.push(
2985
2969
  ["llm.prompt_tokens_total", this._totalPromptTokens],
2986
2970
  ["llm.completion_tokens_total", this._totalCompletionTokens],
2987
- ["llm.cost_total_usd", this._totalCost],
2988
- ["llm.cost_this_tick_usd", tickCost],
2989
- ["llm.cost_avg_per_tick_usd", this._averageTickCost()],
2990
2971
  ["llm.total_calls", this._usageLog.length]
2991
2972
  );
2992
- for (const [cat, cost] of this._categoryCosts) {
2993
- commands.metrics.push([`llm.cost.${cat}`, cost]);
2994
- }
2995
2973
  for (const [cat, tok] of this._categoryTokens) {
2996
2974
  commands.metrics.push(
2997
2975
  [`llm.prompt_tokens.${cat}`, tok.prompt],
2998
2976
  [`llm.completion_tokens.${cat}`, tok.completion]
2999
2977
  );
3000
2978
  }
3001
- for (const [fn, cost] of this._functionCosts) {
3002
- commands.metrics.push([`llm.cost.fn.${fn}`, cost]);
3003
- }
3004
2979
  if (tickCost > this._costWarningThreshold && this._emitCostEvents && tick - this._lastCostWarningTick > 50) {
3005
2980
  this._lastCostWarningTick = tick;
3006
2981
  events.push({
@@ -3044,6 +3019,22 @@ var TokenTracker = class {
3044
3019
  get functionTokenBreakdown() {
3045
3020
  return this._functionTokens;
3046
3021
  }
3022
+ /**
3023
+ * Cost broken down by provider ('anthropic' | 'glm' | 'moonshot' | …), plus
3024
+ * an `unattributed` bucket for usage recorded without one.
3025
+ *
3026
+ * This is the axis a host reconciles against vendor invoices. Calls whose
3027
+ * model went unpriced contribute 0 here, so compare against
3028
+ * `getUsageLog()`'s `priced` flag before treating a small number as a small
3029
+ * bill.
3030
+ */
3031
+ get providerBreakdown() {
3032
+ return this._providerCosts;
3033
+ }
3034
+ /** Token counts (prompt + completion) broken down by provider. */
3035
+ get providerTokenBreakdown() {
3036
+ return this._providerTokens;
3037
+ }
3047
3038
  /** Cost per call average */
3048
3039
  get averageCostPerCall() {
3049
3040
  if (this._usageLog.length === 0) return 0;
@@ -3073,6 +3064,8 @@ var TokenTracker = class {
3073
3064
  this._categoryTokens.clear();
3074
3065
  this._functionCosts.clear();
3075
3066
  this._functionTokens.clear();
3067
+ this._providerCosts.clear();
3068
+ this._providerTokens.clear();
3076
3069
  this._tickCosts = [];
3077
3070
  }
3078
3071
  // ── Internal ─────────────────────────────────────────────
@@ -11567,7 +11560,7 @@ ${this._facetReasoningHistory.join("\n")}` : "";
11567
11560
  ideationUserMessage,
11568
11561
  tick: currentState.tick,
11569
11562
  proposeTemperature,
11570
- meta: { category: "executive", attribute: "facet", function: this._currentFocus?.function ?? "ideation", scope: this.facetId }
11563
+ meta: { category: "executive", attribute: "facet", function: this._currentFocus?.function ?? "ideation", scope: this.facetId, demand: processSelection.effortScore }
11571
11564
  });
11572
11565
  logger.info(
11573
11566
  `[executive.facet] ${this.facetId} \u25C6 deliberate propose tick=${currentState.tick} candidates=${ideationCandidates?.length ?? 0} temp=${proposeTemperature.toFixed(2)}`
@@ -11602,8 +11595,13 @@ ${this._facetReasoningHistory.join("\n")}` : "";
11602
11595
  const facetMeta = {
11603
11596
  category: "executive",
11604
11597
  attribute: "facet",
11605
- function: this._currentFocus?.function ?? "facet",
11606
- scope: this.facetId
11598
+ // A focus that declares no function is making its decision call, the
11599
+ // facet's analogue of master's 'decision'. This previously fell back to
11600
+ // 'facet' — an *attribute* value, which quietly created a bogus bucket
11601
+ // in the by-function cost breakdown. The typed axes caught it.
11602
+ function: this._currentFocus?.function ?? "decision",
11603
+ scope: this.facetId,
11604
+ demand: processSelection.effortScore
11607
11605
  };
11608
11606
  const result = this._chunkHandler ? await this._llmDirector.callStream(systemPrompt, userMessage, currentState.tick, this._chunkHandler, void 0, facetMeta) : await this._llmDirector.call(systemPrompt, userMessage, currentState.tick, void 0, facetMeta);
11609
11607
  output = parseResponse(result.text, currentState, []);
@@ -11877,6 +11875,67 @@ function updateGatingState(gs, state, _tick, didActivate, cleanedBuffer) {
11877
11875
  gs.lastExecutiveTick = _tick;
11878
11876
  }
11879
11877
 
11878
+ // src/llm/routing.ts
11879
+ var NULL_ROUTER = {
11880
+ name: "null",
11881
+ route() {
11882
+ return null;
11883
+ }
11884
+ };
11885
+ function isNullRouter(router) {
11886
+ return !router || router === NULL_ROUTER;
11887
+ }
11888
+ var TableRouter = class {
11889
+ name;
11890
+ _rules;
11891
+ constructor(rules, name = "table") {
11892
+ this._rules = [...rules];
11893
+ this.name = name;
11894
+ }
11895
+ route(meta) {
11896
+ for (const rule of this._rules) {
11897
+ if (matches(rule, meta)) return rule.route;
11898
+ }
11899
+ return null;
11900
+ }
11901
+ };
11902
+ function chainRouters(...routers) {
11903
+ const chain = routers.filter((r) => !isNullRouter(r));
11904
+ if (chain.length === 0) return NULL_ROUTER;
11905
+ if (chain.length === 1) return chain[0];
11906
+ const warned = /* @__PURE__ */ new Set();
11907
+ return {
11908
+ name: chain.map((r) => r.name).join(">"),
11909
+ route(meta) {
11910
+ for (const router of chain) {
11911
+ try {
11912
+ const hit = router.route(meta);
11913
+ if (hit) return hit;
11914
+ } catch (err) {
11915
+ if (!warned.has(router.name)) {
11916
+ warned.add(router.name);
11917
+ logger.warn(`[llm.routing] router "${router.name}" threw \u2014 skipping it: ${err.message}`);
11918
+ }
11919
+ }
11920
+ }
11921
+ return null;
11922
+ }
11923
+ };
11924
+ }
11925
+ function matches(rule, meta) {
11926
+ if (rule.category !== void 0 && rule.category !== meta.category) return false;
11927
+ if (rule.attribute !== void 0 && rule.attribute !== meta.attribute) return false;
11928
+ if (rule.function !== void 0 && rule.function !== meta.function) return false;
11929
+ const bounded = rule.minDemand !== void 0 || rule.maxDemand !== void 0;
11930
+ if (bounded) {
11931
+ const d = meta.demand;
11932
+ if (typeof d !== "number" || Number.isNaN(d)) return false;
11933
+ if (rule.minDemand !== void 0 && d < rule.minDemand) return false;
11934
+ if (rule.maxDemand !== void 0 && d >= rule.maxDemand) return false;
11935
+ }
11936
+ return true;
11937
+ }
11938
+
11880
11939
  // src/llm/gate.ts
11881
11940
  var MAX_CONCURRENT = parseInt(process.env.WILL_LLM_CONCURRENCY ?? "2");
11882
11941
  var maxRetries = () => parseInt(process.env.WILL_LLM_MAX_RETRIES ?? "4");
@@ -11947,39 +12006,92 @@ async function withGate(fn, label, gate = llmGate) {
11947
12006
  }
11948
12007
 
11949
12008
  // src/llm/index.ts
11950
- var ANTHROPIC_WIRE = /* @__PURE__ */ new Set(["anthropic", "glm"]);
11951
- function speaksAnthropicWire(provider) {
11952
- return ANTHROPIC_WIRE.has(provider);
12009
+ var MOCK_PROVIDER = "mock";
12010
+ var MOCK_MODEL = "mock";
12011
+ var KNOWN_PROVIDERS = {
12012
+ // Never dialled — present so a test-mode Will resolves an endpoint without
12013
+ // demanding a provider the run will never use.
12014
+ [MOCK_PROVIDER]: { wire: "anthropic", baseUrl: "http://mock.invalid/v1" },
12015
+ // ── Anthropic wire ──────────────────────────────────────────
12016
+ anthropic: { wire: "anthropic", baseUrl: "https://api.anthropic.com/v1" },
12017
+ // Z.ai documents the base as `…/api/anthropic` because the Anthropic SDK
12018
+ // appends `/v1/messages`; this client appends `/messages`, so the version
12019
+ // segment belongs here — verified against the live endpoint.
12020
+ glm: { wire: "anthropic", baseUrl: "https://api.z.ai/api/anthropic/v1" },
12021
+ // ── OpenAI wire ─────────────────────────────────────────────
12022
+ openai: { wire: "openai", baseUrl: "https://api.openai.com/v1" },
12023
+ deepseek: { wire: "openai", baseUrl: "https://api.deepseek.com/v1" },
12024
+ moonshot: { wire: "openai", baseUrl: "https://api.moonshot.ai/v1" },
12025
+ qwen: { wire: "openai", baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" },
12026
+ xai: { wire: "openai", baseUrl: "https://api.x.ai/v1" },
12027
+ minimax: { wire: "openai", baseUrl: "https://api.minimax.io/v1" },
12028
+ mistral: { wire: "openai", baseUrl: "https://api.mistral.ai/v1" },
12029
+ // Local runtimes. The port is the project default; a host that moved it sets
12030
+ // `baseUrl`. Both still want an `apiKey` — any non-empty string will do,
12031
+ // since neither checks it.
12032
+ ollama: { wire: "openai", baseUrl: "http://localhost:11434/v1" },
12033
+ vllm: { wire: "openai", baseUrl: "http://localhost:8000/v1" },
12034
+ // ── Google wire ─────────────────────────────────────────────
12035
+ // Gemini also exposes an OpenAI-compatible surface; this client speaks the
12036
+ // native one, which is where its caching and multimodal parts actually live.
12037
+ google: { wire: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta" }
12038
+ };
12039
+ var PROVIDER_KEY_ENV = {
12040
+ anthropic: "ANTHROPIC_API_KEY",
12041
+ glm: "ZAI_API_KEY",
12042
+ openai: "OPENAI_API_KEY",
12043
+ google: "GOOGLE_API_KEY",
12044
+ deepseek: "DEEPSEEK_API_KEY",
12045
+ moonshot: "MOONSHOT_API_KEY",
12046
+ qwen: "DASHSCOPE_API_KEY",
12047
+ xai: "XAI_API_KEY",
12048
+ minimax: "MINIMAX_API_KEY",
12049
+ mistral: "MISTRAL_API_KEY"
12050
+ };
12051
+ function providerKeyFromEnv(provider) {
12052
+ const name = PROVIDER_KEY_ENV[provider];
12053
+ if (!name) return void 0;
12054
+ const value = nonBlank(process.env[name]) ?? (provider === "google" ? nonBlank(process.env["GEMINI_API_KEY"]) : void 0);
12055
+ return value;
11953
12056
  }
11954
- function defaultBaseFor(provider) {
11955
- switch (provider) {
11956
- case "anthropic":
11957
- return "https://api.anthropic.com/v1";
11958
- // Z.ai documents the base as `…/api/anthropic` because the Anthropic SDK
11959
- // appends `/v1/messages`; this client appends `/messages`, so the version
11960
- // segment belongs here — verified against the live endpoint.
11961
- case "glm":
11962
- return "https://api.z.ai/api/anthropic/v1";
11963
- case "openai":
11964
- return "https://api.openai.com/v1";
11965
- case "deepseek":
11966
- return "https://api.deepseek.com/v1";
11967
- case "google":
11968
- return "https://generativelanguage.googleapis.com/v1beta";
11969
- }
12057
+ var nonBlank = (v) => v && v.trim() ? v : void 0;
12058
+ function knownWireFor(provider) {
12059
+ return KNOWN_PROVIDERS[provider]?.wire;
11970
12060
  }
11971
- function defaultModelFor(provider) {
11972
- return provider === "glm" ? "glm-5.2" : "claude-sonnet-4-5-20250929";
12061
+ function defaultBaseFor(provider) {
12062
+ return KNOWN_PROVIDERS[provider]?.baseUrl;
11973
12063
  }
11974
12064
  function anthropicWireHeaders(provider, apiKey) {
11975
12065
  return {
11976
12066
  "Content-Type": "application/json",
11977
12067
  "anthropic-version": "2023-06-01",
11978
12068
  "x-api-key": apiKey,
11979
- ...provider === "glm" ? { Authorization: `Bearer ${apiKey}` } : {}
12069
+ ...provider === "anthropic" ? {} : { Authorization: `Bearer ${apiKey}` }
11980
12070
  };
11981
12071
  }
12072
+ var BACKGROUND_DEMAND = 0.1;
12073
+ var ESCALATION_DEMAND = 0.7;
11982
12074
  var DEFAULT_CALL_META = { category: "executive", attribute: "master", function: "decision" };
12075
+ function resolveEndpoint(spec) {
12076
+ const wire = spec.wire ?? knownWireFor(spec.provider);
12077
+ if (!wire)
12078
+ throw new Error(
12079
+ `LLM provider "${spec.provider}" has no known wire. Declare it: llm.providers['${spec.provider}'].wire = 'anthropic' | 'openai' | 'google'.`
12080
+ );
12081
+ const baseUrl = spec.baseUrl ?? defaultBaseFor(spec.provider);
12082
+ if (!baseUrl)
12083
+ throw new Error(
12084
+ `LLM provider "${spec.provider}" has no known base URL. Declare it: llm.providers['${spec.provider}'].baseUrl.`
12085
+ );
12086
+ return {
12087
+ provider: spec.provider,
12088
+ wire,
12089
+ model: spec.model,
12090
+ apiKey: spec.apiKey,
12091
+ baseUrl,
12092
+ maxOutputTokens: spec.maxOutputTokens
12093
+ };
12094
+ }
11983
12095
  var LLMDirector = class {
11984
12096
  _willId;
11985
12097
  _model;
@@ -11991,6 +12103,12 @@ var LLMDirector = class {
11991
12103
  _baseUrl;
11992
12104
  _timeoutMs;
11993
12105
  _tokenTracker;
12106
+ _router;
12107
+ _credentials;
12108
+ /** Default endpoint — what every call used before the routing seam existed. */
12109
+ _defaultEndpoint;
12110
+ /** Routes already warned about (missing credential / bad provider) — log once. */
12111
+ _routeWarned = /* @__PURE__ */ new Set();
11994
12112
  constructor(config) {
11995
12113
  this._willId = config.willId;
11996
12114
  this._model = config.model;
@@ -12002,6 +12120,67 @@ var LLMDirector = class {
12002
12120
  this._baseUrl = config.baseUrl ?? null;
12003
12121
  this._timeoutMs = config.timeoutMs ?? 9e4;
12004
12122
  this._tokenTracker = config.tokenTracker ?? null;
12123
+ this._router = config.router ?? null;
12124
+ this._credentials = config.credentials ?? {};
12125
+ this._defaultEndpoint = resolveEndpoint({
12126
+ provider: this._provider,
12127
+ model: this._model,
12128
+ apiKey: this._apiKey,
12129
+ baseUrl: this._baseUrl ?? void 0,
12130
+ wire: config.wire,
12131
+ maxOutputTokens: this._maxOutputTokens
12132
+ });
12133
+ }
12134
+ /**
12135
+ * Resolve which model serves this call. Falls back to the default endpoint
12136
+ * whenever the router has no opinion, throws, or names a provider we hold no
12137
+ * credential for — degrade, never crash.
12138
+ */
12139
+ _resolveEndpoint(meta) {
12140
+ if (isNullRouter(this._router)) return this._defaultEndpoint;
12141
+ let route;
12142
+ try {
12143
+ route = this._router.route(meta);
12144
+ } catch (err) {
12145
+ this._warnRouteOnce(
12146
+ `throw:${this._router.name}`,
12147
+ `router "${this._router.name}" threw \u2014 using the default model`,
12148
+ err
12149
+ );
12150
+ return this._defaultEndpoint;
12151
+ }
12152
+ if (!route) return this._defaultEndpoint;
12153
+ const provider = route.provider ?? this._defaultEndpoint.provider;
12154
+ const cred = provider === this._defaultEndpoint.provider ? { apiKey: this._defaultEndpoint.apiKey, baseUrl: this._defaultEndpoint.baseUrl, wire: this._defaultEndpoint.wire } : this._credentials[provider];
12155
+ if (!cred?.apiKey) {
12156
+ this._warnRouteOnce(
12157
+ `cred:${provider}`,
12158
+ `no credential for routed provider "${provider}" \u2014 using the default model`
12159
+ );
12160
+ return this._defaultEndpoint;
12161
+ }
12162
+ try {
12163
+ return resolveEndpoint({
12164
+ provider,
12165
+ model: route.model,
12166
+ apiKey: cred.apiKey,
12167
+ baseUrl: route.baseUrl ?? cred.baseUrl,
12168
+ wire: cred.wire,
12169
+ maxOutputTokens: route.maxOutputTokens ?? this._defaultEndpoint.maxOutputTokens
12170
+ });
12171
+ } catch (err) {
12172
+ this._warnRouteOnce(
12173
+ `resolve:${provider}`,
12174
+ `cannot reach routed provider "${provider}" \u2014 using the default model`,
12175
+ err
12176
+ );
12177
+ return this._defaultEndpoint;
12178
+ }
12179
+ }
12180
+ _warnRouteOnce(key, message, err) {
12181
+ if (this._routeWarned.has(key)) return;
12182
+ this._routeWarned.add(key);
12183
+ logger.warn(`[llm.routing] ${message}`, err instanceof Error ? err.message : "");
12005
12184
  }
12006
12185
  // ── Mock response (test mode) ────────────────────────────
12007
12186
  /**
@@ -12076,18 +12255,19 @@ var LLMDirector = class {
12076
12255
  if (!replay.mock) onChunk(replay.text);
12077
12256
  return { text: replay.text, inputTok: replay.inputTok, outputTok: replay.outputTok };
12078
12257
  }
12258
+ const ep = this._resolveEndpoint(meta);
12079
12259
  if (this._mock) {
12080
12260
  const result2 = this._mockResponse(tick, userMessage);
12081
- this._recordCompletion(systemPrompt, userMessage, tick, result2, Date.now() - start, true);
12261
+ this._recordCompletion(systemPrompt, userMessage, tick, result2, Date.now() - start, true, ep);
12082
12262
  return result2;
12083
12263
  }
12084
- const result = speaksAnthropicWire(this._provider) ? await this._callAnthropicStream(systemPrompt, userMessage, onChunk, temperature) : await (async () => {
12085
- const r = await this._callProvider(systemPrompt, userMessage, temperature);
12264
+ const result = ep.wire === "anthropic" ? await this._callAnthropicStream(ep, systemPrompt, userMessage, onChunk, temperature) : await (async () => {
12265
+ const r = await this._callProvider(ep, systemPrompt, userMessage, temperature);
12086
12266
  onChunk(r.text);
12087
12267
  return r;
12088
12268
  })();
12089
- this._track(result, meta, tick, Date.now() - start, this._estPromptTokens(systemPrompt, userMessage));
12090
- this._recordCompletion(systemPrompt, userMessage, tick, result, Date.now() - start, false);
12269
+ this._track(result, meta, tick, Date.now() - start, this._estPromptTokens(systemPrompt, userMessage), ep);
12270
+ this._recordCompletion(systemPrompt, userMessage, tick, result, Date.now() - start, false, ep);
12091
12271
  return result;
12092
12272
  }
12093
12273
  /**
@@ -12096,9 +12276,14 @@ var LLMDirector = class {
12096
12276
  * mock/replay directors, so the call is simply skipped. Cache read/write tokens
12097
12277
  * are forwarded so the tracker prices them at 0.1× / 1.25× input.
12098
12278
  */
12099
- _track(result, meta, tick, latencyMs, estPromptTokens) {
12279
+ _track(result, meta, tick, latencyMs, estPromptTokens, ep = this._defaultEndpoint) {
12100
12280
  this._tokenTracker?.recordUsage({
12101
- model: this._model,
12281
+ // The endpoint that actually served this call — routed or default.
12282
+ // Pricing must follow the real model, or routed spend is attributed
12283
+ // wrongly; the provider rides along because the same model id can be
12284
+ // reached from several vendors at very different prices.
12285
+ model: ep.model,
12286
+ provider: ep.provider,
12102
12287
  promptTokens: result.inputTok,
12103
12288
  completionTokens: result.outputTok,
12104
12289
  totalTokens: result.inputTok + result.outputTok,
@@ -12124,14 +12309,16 @@ var LLMDirector = class {
12124
12309
  * The LLM is the non-deterministic oracle; recording its input+output is the
12125
12310
  * prerequisite for deterministic re-execution (REORIENT R2, deferred).
12126
12311
  */
12127
- _recordCompletion(systemPrompt, userMessage, tick, result, latencyMs, mock) {
12312
+ _recordCompletion(systemPrompt, userMessage, tick, result, latencyMs, mock, ep = this._defaultEndpoint) {
12128
12313
  try {
12129
12314
  getCompletionRecorder(this._willId)?.recordCompletion({
12130
12315
  tick,
12131
12316
  willId: this._willId,
12132
- provider: this._provider,
12133
- model: this._model,
12134
- maxOutputTokens: this._maxOutputTokens,
12317
+ // Record the endpoint that actually served the call: the tape is what
12318
+ // replay re-feeds, so it must say which model produced this text.
12319
+ provider: ep.provider,
12320
+ model: ep.model,
12321
+ maxOutputTokens: ep.maxOutputTokens,
12135
12322
  systemPrompt,
12136
12323
  userMessage,
12137
12324
  text: result.text,
@@ -12154,17 +12341,17 @@ var LLMDirector = class {
12154
12341
  _replayCompletion(systemPrompt, userMessage, tick) {
12155
12342
  return getCompletionSource(this._willId)?.nextCompletion(tick, systemPrompt, userMessage);
12156
12343
  }
12157
- async _callAnthropicStream(systemPrompt, userMessage, onChunk, temperature) {
12344
+ async _callAnthropicStream(ep, systemPrompt, userMessage, onChunk, temperature) {
12158
12345
  const controller = new AbortController();
12159
12346
  const timer = setTimeout(() => controller.abort(), this._timeoutMs);
12160
12347
  let res;
12161
12348
  try {
12162
- res = await fetch(`${this._resolvedBase()}/messages`, {
12349
+ res = await fetch(`${this._resolvedBase(ep)}/messages`, {
12163
12350
  method: "POST",
12164
- headers: anthropicWireHeaders(this._provider, this._apiKey),
12351
+ headers: anthropicWireHeaders(ep.provider, ep.apiKey),
12165
12352
  body: JSON.stringify({
12166
- model: this._model,
12167
- max_tokens: this._maxOutputTokens,
12353
+ model: ep.model,
12354
+ max_tokens: ep.maxOutputTokens,
12168
12355
  ...temperature !== void 0 ? { temperature } : {},
12169
12356
  stream: true,
12170
12357
  system: this._systemField(systemPrompt),
@@ -12175,7 +12362,7 @@ var LLMDirector = class {
12175
12362
  } catch (err) {
12176
12363
  clearTimeout(timer);
12177
12364
  if (controller.signal.aborted)
12178
- throw new Error(`LLM stream to ${this._provider} timed out after ${this._timeoutMs}ms (no response)`);
12365
+ throw new Error(`LLM stream to ${ep.provider} timed out after ${this._timeoutMs}ms (no response)`);
12179
12366
  throw err;
12180
12367
  }
12181
12368
  clearTimeout(timer);
@@ -12230,43 +12417,36 @@ var LLMDirector = class {
12230
12417
  const replay = this._replayCompletion(systemPrompt, userMessage, tick);
12231
12418
  if (replay)
12232
12419
  return { text: replay.text, inputTok: replay.inputTok, outputTok: replay.outputTok };
12420
+ const ep = this._resolveEndpoint(meta);
12233
12421
  if (this._mock) {
12234
12422
  const result2 = this._mockResponse(tick, userMessage);
12235
- this._recordCompletion(systemPrompt, userMessage, tick, result2, Date.now() - llmStart, true);
12423
+ this._recordCompletion(systemPrompt, userMessage, tick, result2, Date.now() - llmStart, true, ep);
12236
12424
  return result2;
12237
12425
  }
12238
12426
  const result = await withGate(
12239
- () => speaksAnthropicWire(this._provider) ? this._callAnthropicStream(systemPrompt, userMessage, () => {
12240
- }, temperature) : this._callProvider(systemPrompt, userMessage, temperature),
12427
+ () => ep.wire === "anthropic" ? this._callAnthropicStream(ep, systemPrompt, userMessage, () => {
12428
+ }, temperature) : this._callProvider(ep, systemPrompt, userMessage, temperature),
12241
12429
  "executive/direct"
12242
12430
  );
12243
- this._track(result, meta, tick, Date.now() - llmStart, this._estPromptTokens(systemPrompt, userMessage));
12244
- this._recordCompletion(systemPrompt, userMessage, tick, result, Date.now() - llmStart, false);
12431
+ this._track(result, meta, tick, Date.now() - llmStart, this._estPromptTokens(systemPrompt, userMessage), ep);
12432
+ this._recordCompletion(systemPrompt, userMessage, tick, result, Date.now() - llmStart, false, ep);
12245
12433
  return result;
12246
12434
  }
12247
- _callProvider(systemPrompt, userMessage, temperature) {
12248
- switch (this._provider) {
12435
+ _callProvider(ep, systemPrompt, userMessage, temperature) {
12436
+ switch (ep.wire) {
12249
12437
  case "anthropic":
12250
- return this._callAnthropic(systemPrompt, userMessage, temperature);
12251
- case "glm":
12252
- return this._callAnthropic(systemPrompt, userMessage, temperature);
12253
- case "deepseek":
12254
- return this._callOpenAI(systemPrompt, userMessage, temperature);
12438
+ return this._callAnthropic(ep, systemPrompt, userMessage, temperature);
12255
12439
  case "openai":
12256
- return this._callOpenAI(systemPrompt, userMessage, temperature);
12440
+ return this._callOpenAI(ep, systemPrompt, userMessage, temperature);
12257
12441
  case "google":
12258
- return this._callGoogle(systemPrompt, userMessage, temperature);
12442
+ return this._callGoogle(ep, systemPrompt, userMessage, temperature);
12259
12443
  default:
12260
- throw new Error(`Unknown LLM provider: ${this._provider}`);
12444
+ throw new Error(`Unknown LLM wire: ${ep.wire}`);
12261
12445
  }
12262
12446
  }
12263
- /** Default API base URL (including version segment) for a provider. */
12264
- _baseFor(provider) {
12265
- return defaultBaseFor(provider);
12266
- }
12267
12447
  /** Resolved API base: explicit override wins, else the provider default. */
12268
- _resolvedBase() {
12269
- return this._baseUrl ?? this._baseFor(this._provider);
12448
+ _resolvedBase(ep) {
12449
+ return ep.baseUrl;
12270
12450
  }
12271
12451
  /**
12272
12452
  * fetch() with a hard per-request deadline. A hung connection is aborted
@@ -12292,21 +12472,21 @@ var LLMDirector = class {
12292
12472
  _systemField(systemPrompt) {
12293
12473
  return [{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } }];
12294
12474
  }
12295
- async _callAnthropic(systemPrompt, userMessage, temperature) {
12475
+ async _callAnthropic(ep, systemPrompt, userMessage, temperature) {
12296
12476
  const body = {
12297
- model: this._model,
12298
- max_tokens: this._maxOutputTokens,
12477
+ model: ep.model,
12478
+ max_tokens: ep.maxOutputTokens,
12299
12479
  ...temperature !== void 0 ? { temperature } : {},
12300
12480
  system: this._systemField(systemPrompt),
12301
12481
  messages: [{ role: "user", content: userMessage }]
12302
12482
  };
12303
- const res = await this._fetchWithTimeout(`${this._resolvedBase()}/messages`, {
12483
+ const res = await this._fetchWithTimeout(`${this._resolvedBase(ep)}/messages`, {
12304
12484
  method: "POST",
12305
- headers: anthropicWireHeaders(this._provider, this._apiKey),
12485
+ headers: anthropicWireHeaders(ep.provider, ep.apiKey),
12306
12486
  body: JSON.stringify(body)
12307
12487
  });
12308
12488
  if (!res.ok)
12309
- throw new Error(`${this._provider} API ${res.status}: ${(await res.text()).slice(0, 300)}`);
12489
+ throw new Error(`${ep.provider} API ${res.status}: ${(await res.text()).slice(0, 300)}`);
12310
12490
  const data = await res.json(), text = data.content.find((b) => b.type === "text")?.text ?? "";
12311
12491
  return {
12312
12492
  text,
@@ -12316,21 +12496,21 @@ var LLMDirector = class {
12316
12496
  cacheWriteTok: data.usage.cache_creation_input_tokens ?? 0
12317
12497
  };
12318
12498
  }
12319
- async _callOpenAI(systemPrompt, userMessage, temperature) {
12499
+ async _callOpenAI(ep, systemPrompt, userMessage, temperature) {
12320
12500
  const body = {
12321
- model: this._model,
12322
- max_completion_tokens: this._maxOutputTokens,
12501
+ model: ep.model,
12502
+ max_completion_tokens: ep.maxOutputTokens,
12323
12503
  ...temperature !== void 0 ? { temperature } : {},
12324
12504
  messages: [
12325
12505
  { role: "system", content: systemPrompt },
12326
12506
  { role: "user", content: userMessage }
12327
12507
  ]
12328
12508
  };
12329
- const res = await this._fetchWithTimeout(`${this._resolvedBase()}/chat/completions`, {
12509
+ const res = await this._fetchWithTimeout(`${this._resolvedBase(ep)}/chat/completions`, {
12330
12510
  method: "POST",
12331
12511
  headers: {
12332
12512
  "Content-Type": "application/json",
12333
- "Authorization": `Bearer ${this._apiKey}`
12513
+ "Authorization": `Bearer ${ep.apiKey}`
12334
12514
  },
12335
12515
  body: JSON.stringify(body)
12336
12516
  });
@@ -12343,22 +12523,22 @@ var LLMDirector = class {
12343
12523
  outputTok: data.usage.completion_tokens
12344
12524
  };
12345
12525
  }
12346
- async _callGoogle(systemPrompt, userMessage, temperature) {
12526
+ async _callGoogle(ep, systemPrompt, userMessage, temperature) {
12347
12527
  const body = {
12348
12528
  systemInstruction: { parts: [{ text: systemPrompt }] },
12349
12529
  contents: [{ role: "user", parts: [{ text: userMessage }] }],
12350
12530
  generationConfig: {
12351
- maxOutputTokens: this._maxOutputTokens,
12531
+ maxOutputTokens: ep.maxOutputTokens,
12352
12532
  ...temperature !== void 0 ? { temperature } : {}
12353
12533
  }
12354
12534
  };
12355
12535
  const res = await this._fetchWithTimeout(
12356
- `${this._resolvedBase()}/models/${this._model}:generateContent`,
12536
+ `${this._resolvedBase(ep)}/models/${ep.model}:generateContent`,
12357
12537
  {
12358
12538
  method: "POST",
12359
12539
  headers: {
12360
12540
  "Content-Type": "application/json",
12361
- "x-goog-api-key": this._apiKey
12541
+ "x-goog-api-key": ep.apiKey
12362
12542
  },
12363
12543
  body: JSON.stringify(body)
12364
12544
  }
@@ -12836,13 +13016,14 @@ var ExecutiveEngine = class extends AsyncEngine {
12836
13016
  _lastExecutiveTick = -100;
12837
13017
  // ── Injected dependencies ──────────────────────────────────
12838
13018
  _willId = null;
12839
- /** Per-Will, per-role model ids (config.model, resolved in mind.ts). */
12840
- _models = { executive: null, summarizer: null, deliberation: null, conversation: null };
13019
+ /**
13020
+ * The Will's default model (config.model's `executive` role, resolved in
13021
+ * mind.ts). Every other role reaches its model through the router — see
13022
+ * `compileRoleRouter`.
13023
+ */
13024
+ _modelId = null;
12841
13025
  /** Per-Will LLM transport overrides (config.llm) — env fallbacks apply per field. */
12842
13026
  _llm = null;
12843
- /** One director per distinct model — same config, different model. Shared
12844
- * tracker/recorder/willId, so ledger attribution and replay hold per role. */
12845
- _directorCache = /* @__PURE__ */ new Map();
12846
13027
  _workingMemory = null;
12847
13028
  _goalManager = null;
12848
13029
  _episodicConsolidator = null;
@@ -12955,21 +13136,23 @@ var ExecutiveEngine = class extends AsyncEngine {
12955
13136
  set willId(willId) {
12956
13137
  this._willId = willId;
12957
13138
  }
12958
- /** Per-Will role models (config.model, resolved). Set before the first tick. */
12959
- set models(m) {
12960
- this._models = m;
13139
+ /**
13140
+ * The Will's default model. Set before the first tick.
13141
+ *
13142
+ * This replaced a four-role map (W7): the other roles are routing rules now,
13143
+ * compiled in mind.ts, so the engine holds one model and one router rather
13144
+ * than a model per role plus a router.
13145
+ */
13146
+ set modelId(id) {
13147
+ this._modelId = id;
12961
13148
  }
12962
- get models() {
12963
- return this._models;
13149
+ get modelId() {
13150
+ return this._modelId;
12964
13151
  }
12965
13152
  /** Per-Will LLM transport overrides (config.llm). Set before the first tick. */
12966
13153
  set llm(c) {
12967
13154
  this._llm = c;
12968
13155
  }
12969
- /** The executive-role model id (back-compat read). */
12970
- get modelId() {
12971
- return this._models.executive;
12972
- }
12973
13156
  // ── Public surface ─────────────────────────────────────────
12974
13157
  get latestOutput() {
12975
13158
  return this._lastExecutiveOutput;
@@ -12991,36 +13174,86 @@ var ExecutiveEngine = class extends AsyncEngine {
12991
13174
  * and subscribe() to receive facet decisions.
12992
13175
  */
12993
13176
  /** Get-or-create the director for a model id (shared config, per-Will). */
12994
- _directorFor(model) {
12995
- let d = this._directorCache.get(model);
12996
- if (!d) {
12997
- d = new LLMDirector({
12998
- willId: this._willId,
12999
- model,
13000
- maxOutputTokens: this._llm?.maxOutputTokens ?? parseInt(process.env.WILL_MAX_OUTPUT_TOKENS ?? "8096"),
13001
- // Provider-agnostic key; falls back to ANTHROPIC_API_KEY for back-compat.
13002
- apiKey: this._llm?.apiKey ?? process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? "",
13003
- provider: this._llm?.provider ?? process.env.WILL_LLM_PROVIDER ?? "anthropic",
13004
- // Optional base-URL override (e.g. Ollama / Azure / self-hosted). Unset →
13005
- // the director uses the provider's official endpoint.
13006
- baseUrl: this._llm?.baseUrl ?? process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
13007
- timeoutMs: this._llm?.timeoutMs ?? (process.env.WILL_LLM_TIMEOUT_MS ? parseInt(process.env.WILL_LLM_TIMEOUT_MS) : void 0),
13008
- sessionLogger: this._sessionLogger,
13009
- mock: this._testMode,
13010
- // Inject the per-Will tracker (R4) so live calls record usage here, not
13011
- // through a process global. null is fine the director skips recording.
13012
- tokenTracker: this._tokenTracker
13013
- });
13014
- this._directorCache.set(model, d);
13015
- }
13016
- return d;
13177
+ /**
13178
+ * The provider, from config or environment — never guessed.
13179
+ *
13180
+ * This used to default to 'anthropic', which is how a Will configured for one
13181
+ * vendor could quietly talk to another. An unset provider is a configuration
13182
+ * error, and saying so at construction is far cheaper than a 401 mid-tick.
13183
+ */
13184
+ _requireProvider() {
13185
+ const provider = this._llm?.provider ?? process.env.WILL_LLM_PROVIDER ?? (this._noLiveCalls() ? MOCK_PROVIDER : void 0);
13186
+ if (!provider)
13187
+ throw new Error(
13188
+ "No LLM provider configured. Set one on the Will (llm.provider) or in the environment (WILL_LLM_PROVIDER) \u2014 the engine carries no default."
13189
+ );
13190
+ return provider;
13191
+ }
13192
+ /**
13193
+ * True when this Will cannot make a live call, so provider/model are not
13194
+ * required: mock mode, or a replay re-feeding recorded completions.
13195
+ */
13196
+ _noLiveCalls() {
13197
+ return this._testMode || !!this._willId && !!getCompletionSource(this._willId);
13198
+ }
13199
+ /**
13200
+ * Build this Will's one and only director.
13201
+ *
13202
+ * There used to be a cache of them, keyed by model, because the per-role
13203
+ * model map had no other way to make a role use a different model. Routing
13204
+ * gave it one — the role map now compiles to rules (see `compileRoleRouter`)
13205
+ * and a single director resolves every call's endpoint per call. That is also
13206
+ * strictly more faithful: a facet follows the work it is doing rather than
13207
+ * whatever role it happened to be spawned under.
13208
+ */
13209
+ _buildDirector(model) {
13210
+ const provider = this._requireProvider();
13211
+ return new LLMDirector({
13212
+ willId: this._willId,
13213
+ model,
13214
+ maxOutputTokens: this._llm?.maxOutputTokens ?? parseInt(process.env.WILL_MAX_OUTPUT_TOKENS ?? "8096"),
13215
+ // Config, then the provider-agnostic env, then THIS provider's own env.
13216
+ // The last step is not the fallback W9 removed: that one ended at
13217
+ // ANTHROPIC_API_KEY for every provider, so a Will pointed elsewhere sent
13218
+ // Anthropic's key to a stranger. This one can only ever read the key
13219
+ // belonging to the provider actually configured.
13220
+ apiKey: this._llm?.apiKey ?? process.env.WILL_LLM_API_KEY ?? providerKeyFromEnv(provider) ?? "",
13221
+ provider,
13222
+ // Optional base-URL override (e.g. Ollama / Azure / self-hosted). Unset →
13223
+ // the director uses the provider's official endpoint.
13224
+ baseUrl: this._llm?.baseUrl ?? process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
13225
+ timeoutMs: this._llm?.timeoutMs ?? (process.env.WILL_LLM_TIMEOUT_MS ? parseInt(process.env.WILL_LLM_TIMEOUT_MS) : void 0),
13226
+ sessionLogger: this._sessionLogger,
13227
+ mock: this._testMode,
13228
+ // Inject the per-Will tracker (R4) so live calls record usage here, not
13229
+ // through a process global. null is fine — the director skips recording.
13230
+ tokenTracker: this._tokenTracker,
13231
+ // Credentials for routed calls, narrowed by the stem from the host's
13232
+ // per-provider map. Prices from that same map ride to the TokenTracker
13233
+ // instead, so nothing carries pricing into the call path.
13234
+ ...this._llm?.credentials ? { credentials: this._llm.credentials } : {},
13235
+ // Per-call model selection — the host's router chained with the rules
13236
+ // compiled from the per-role model map.
13237
+ ...this._llm?.router ? { router: this._llm.router } : {},
13238
+ // Dialect for the default provider — required for anything outside the
13239
+ // known set, so the engine never guesses how to talk to an endpoint.
13240
+ ...this._llm?.wire ? { wire: this._llm.wire } : {}
13241
+ });
13017
13242
  }
13243
+ /**
13244
+ * Spawn a facet.
13245
+ *
13246
+ * `role` declares the facet's intent at the call site. It no longer selects a
13247
+ * model: that used to happen here, pinning a facet for life to whatever role
13248
+ * it was spawned under, and it now happens per call from the focus function
13249
+ * the caller sets immediately afterwards (W7). The two always agreed — every
13250
+ * spawn site sets a focus whose `function` matches its role — so the routed
13251
+ * answer is the same one, decided later and from the work itself.
13252
+ */
13018
13253
  spawnFacet(role) {
13019
- const roleModel = role === "deliberation" ? this._models.deliberation : role === "conversation" || role === "outreach" ? this._models.conversation : null;
13020
- const director = roleModel && this._llmDirector ? this._directorFor(roleModel) : this._llmDirector;
13021
13254
  return this._facetSupervisor.spawn({
13022
13255
  bus: this._bus,
13023
- llmDirector: director,
13256
+ llmDirector: this._llmDirector,
13024
13257
  stateRef: this._lastStateRef,
13025
13258
  willId: this._willId,
13026
13259
  inbox: this._inbox,
@@ -13086,9 +13319,13 @@ var ExecutiveEngine = class extends AsyncEngine {
13086
13319
  this._gatingState.executiveInterval = rtConfig.executiveInterval;
13087
13320
  this._gatingState.cooldownTicks = rtConfig.cooldownTicks;
13088
13321
  if (!this._llmDirector && this._willId) {
13089
- const execModel = this._models.executive ?? process.env.WILL_LLM_MODEL ?? defaultModelFor(this._llm?.provider ?? process.env.WILL_LLM_PROVIDER ?? "anthropic");
13090
- this._llmDirector = this._directorFor(execModel);
13091
- this._summarizer?.attachLLMDirector(this._directorFor(this._models.summarizer ?? execModel));
13322
+ const defaultModel = this._modelId ?? process.env.WILL_LLM_MODEL ?? (this._noLiveCalls() ? MOCK_MODEL : void 0);
13323
+ if (!defaultModel)
13324
+ throw new Error(
13325
+ "No LLM model configured. Set one on the Will (llm.model) or in the environment (WILL_LLM_MODEL) \u2014 the engine carries no default."
13326
+ );
13327
+ this._llmDirector = this._buildDirector(defaultModel);
13328
+ this._summarizer?.attachLLMDirector(this._llmDirector);
13092
13329
  }
13093
13330
  const gatingDeps = {
13094
13331
  generativeModel: this._generativeModel,
@@ -13187,7 +13424,7 @@ var ExecutiveEngine = class extends AsyncEngine {
13187
13424
  ideationUserMessage,
13188
13425
  tick: state.tick,
13189
13426
  proposeTemperature,
13190
- meta: { category: "executive", attribute: "master", function: "ideation" }
13427
+ meta: { category: "executive", attribute: "master", function: "ideation", demand: processSelection.effortScore }
13191
13428
  });
13192
13429
  logger.info(
13193
13430
  `[executive] \u25C6 deliberate propose tick=${state.tick} candidates=${ideationCandidates?.length ?? 0} temp=${proposeTemperature.toFixed(2)} latency=${wallClock() - ideationStart}ms`
@@ -13229,7 +13466,7 @@ var ExecutiveEngine = class extends AsyncEngine {
13229
13466
  const llmStart = wallClock();
13230
13467
  let executiveOutput;
13231
13468
  try {
13232
- const masterMeta = { category: "executive", attribute: "master", function: "decision" };
13469
+ const masterMeta = { category: "executive", attribute: "master", function: "decision", demand: processSelection.effortScore };
13233
13470
  const result = this._chunkBroadcaster ? await this._llmDirector.callStream(systemPrompt, userMessage, state.tick, this._chunkBroadcaster, void 0, masterMeta) : await this._llmDirector.call(systemPrompt, userMessage, state.tick, void 0, masterMeta);
13234
13471
  logger.info(
13235
13472
  `[executive] \u2713 tick=${state.tick} in=${result.inputTok} tok out=${result.outputTok} tok latency=${wallClock() - llmStart}ms`
@@ -19023,6 +19260,24 @@ function clamp016(n) {
19023
19260
  return n < 0 ? 0 : n > 1 ? 1 : n;
19024
19261
  }
19025
19262
 
19263
+ // src/stem/policy/arbiter.ts
19264
+ var ALLOW = Object.freeze({ decision: "allow" });
19265
+ var NULL_ARBITER = {
19266
+ name: "null",
19267
+ evaluate() {
19268
+ return ALLOW;
19269
+ }
19270
+ };
19271
+ function isNullArbiter(arbiter) {
19272
+ return !arbiter || arbiter === NULL_ARBITER;
19273
+ }
19274
+ function finalityOf(verdict) {
19275
+ return asFinality(verdict.finality);
19276
+ }
19277
+ function asFinality(raw) {
19278
+ return raw === "class" ? "class" : raw === "context" ? "context" : "parameter";
19279
+ }
19280
+
19026
19281
  // src/cognition/agency/engines/action.selector.ts
19027
19282
  var MARGIN_THRESHOLD = 0.06;
19028
19283
  var BASE_STAKES_THRESHOLD = 0.6;
@@ -19422,7 +19677,7 @@ function refusedClassSchemas(state) {
19422
19677
  for (const e of state.entities.values()) {
19423
19678
  if (e.type !== "agency.outcome") continue;
19424
19679
  const m = e.metadata;
19425
- if (m?.["refused"] !== true || str2(m?.["finality"]) !== "class") continue;
19680
+ if (m?.["refused"] !== true || asFinality(m?.["finality"]) !== "class") continue;
19426
19681
  const schema = str2(m?.["schema"]);
19427
19682
  if (schema) out.add(schema);
19428
19683
  }
@@ -20228,7 +20483,7 @@ var IDLE_TICKS = 200;
20228
20483
  var DECAY_RATE = 0.02;
20229
20484
  var DROP_HABIT = 0.05;
20230
20485
  var AVAIL_DROP_CLASS = 0.5;
20231
- var AVAIL_DROP_INSTANCE = 0.12;
20486
+ var AVAIL_DROP_PARAMETER = 0.12;
20232
20487
  var AVAIL_FLOOR = 0.05;
20233
20488
  var AVAIL_RECOVERY = 0.02;
20234
20489
  var AVAIL_RECOVERED = 0.999;
@@ -20287,13 +20542,19 @@ var SchemaRepertoire = class {
20287
20542
  }
20288
20543
  /**
20289
20544
  * Fold a policy refusal into the availability layer (NOT competence). A
20290
- * `class` refusal cuts availability hard; an `instance` refusal dents it
20545
+ * `class` refusal cuts availability hard; a `parameter` refusal dents it
20291
20546
  * lightly. Multiplicative so repeated refusals compound toward — but never
20292
20547
  * reach — zero, keeping re-probe alive.
20548
+ *
20549
+ * `context` is EXCLUDED FROM THE SIGNATURE, not handled inside: a refusal
20550
+ * that was not about the action must never reach the availability layer at
20551
+ * all, and making that a type error rather than a convention means a future
20552
+ * caller cannot quietly re-introduce the dent. The routing decision lives in
20553
+ * the ReafferenceEngine's refused branch (P5).
20293
20554
  */
20294
20555
  recordRefusal(schema, finality, tick) {
20295
20556
  const prev = this._availability.get(schema)?.value ?? 1;
20296
- const drop = finality === "class" ? AVAIL_DROP_CLASS : AVAIL_DROP_INSTANCE;
20557
+ const drop = finality === "class" ? AVAIL_DROP_CLASS : AVAIL_DROP_PARAMETER;
20297
20558
  const value = Math.max(AVAIL_FLOOR, prev * (1 - drop));
20298
20559
  this._availability.set(schema, { value, lastRefusedTick: tick });
20299
20560
  return value;
@@ -20608,8 +20869,9 @@ var ReafferenceEngine = class {
20608
20869
  continue;
20609
20870
  }
20610
20871
  if (m["refused"] === true) {
20611
- const finality = str6(m["finality"]) === "class" ? "class" : "instance";
20612
- this._repertoire.recordRefusal(schema, finality, tick);
20872
+ const finality = asFinality(m["finality"]);
20873
+ if (finality !== "context")
20874
+ this._repertoire.recordRefusal(schema, finality, tick);
20613
20875
  if (fromState) del.push(id);
20614
20876
  const refusedIntent = str6(m["intentId"]);
20615
20877
  if (refusedIntent) del.push(refusedIntent);
@@ -21766,7 +22028,10 @@ ${r}`).join("\n\n---\n\n");
21766
22028
  userMessage,
21767
22029
  this._callCount,
21768
22030
  void 0,
21769
- { category: "summarizer", attribute: "memory", function: "consolidation" }
22031
+ // MODEL_ROUTING W0 compression is background work at a constant low
22032
+ // demand: distilling excerpts is the same job whether the mind is calm
22033
+ // or in crisis, so there is no honest per-tick measure to forward here.
22034
+ { category: "summarizer", attribute: "memory", function: "consolidation", demand: BACKGROUND_DEMAND }
21770
22035
  );
21771
22036
  if (result.text) {
21772
22037
  this._summary = result.text.trim();
@@ -22844,7 +23109,7 @@ function buildEngineConfigEntities(config, executiveInterval) {
22844
23109
  engine: "system",
22845
23110
  params: {
22846
23111
  anatomy: config.anatomy ?? "mind",
22847
- model: config.model ?? "",
23112
+ model: config.llm?.model ?? "",
22848
23113
  tickIntervalMs: config.tickIntervalMs ?? 1e3
22849
23114
  }
22850
23115
  },
@@ -23279,6 +23544,41 @@ function buildEngineConfigEntities(config, executiveInterval) {
23279
23544
  }
23280
23545
 
23281
23546
  // src/stem/mind.ts
23547
+ function mergeProviderPrices(providers) {
23548
+ if (!providers) return void 0;
23549
+ const out = {};
23550
+ for (const entry of Object.values(providers)) {
23551
+ for (const [model, price] of Object.entries(entry?.prices ?? {})) {
23552
+ if (!(model in out)) out[model] = price;
23553
+ }
23554
+ }
23555
+ return Object.keys(out).length > 0 ? out : void 0;
23556
+ }
23557
+ function providerCredentials(providers) {
23558
+ const out = {};
23559
+ for (const [name, entry] of Object.entries(providers)) {
23560
+ if (!entry?.apiKey) continue;
23561
+ out[name] = {
23562
+ apiKey: entry.apiKey,
23563
+ ...entry.baseUrl ? { baseUrl: entry.baseUrl } : {}
23564
+ };
23565
+ }
23566
+ return out;
23567
+ }
23568
+ function compileRoleRouter(roles) {
23569
+ const { executive, summarizer, deliberation, conversation } = roles;
23570
+ const distinct = (model) => !!model && model !== executive;
23571
+ const rules = [];
23572
+ if (distinct(summarizer))
23573
+ rules.push({ category: "summarizer", route: { model: summarizer } });
23574
+ if (distinct(deliberation))
23575
+ rules.push({ function: "deliberation", route: { model: deliberation } });
23576
+ if (distinct(conversation)) {
23577
+ rules.push({ function: "conversation", route: { model: conversation } });
23578
+ rules.push({ function: "outreach", route: { model: conversation } });
23579
+ }
23580
+ return rules.length > 0 ? new TableRouter(rules, "role-map") : null;
23581
+ }
23282
23582
  function resolveModelRoles(model) {
23283
23583
  const map = typeof model === "string" ? { executive: model } : model ?? {};
23284
23584
  const pin = process.env.WILL_LLM_MODEL;
@@ -23294,7 +23594,7 @@ function resolveModelRoles(model) {
23294
23594
  };
23295
23595
  }
23296
23596
  var EXECUTIVE_CADENCE = {
23297
- // Sonnetpremium/Enterprise; opt in via executiveInterval
23597
+ // most attentive, highest spend — opt in via executiveInterval
23298
23598
  balanced: 60};
23299
23599
  function _resolveVectorMemory(willId, seed, overrideAdapter, disable, tokenTracker, testMode, embeddingModel) {
23300
23600
  if (overrideAdapter) return { embedder: null, vectorMemory: overrideAdapter };
@@ -23424,6 +23724,10 @@ function _buildSimulation(willId, config, randomSeed) {
23424
23724
  function _constructCognition({ simulation, willId, config, randomSeed, executiveInterval, profile }) {
23425
23725
  const anatomy = config.anatomy ?? "mind";
23426
23726
  const tokenTracker = new TokenTracker({
23727
+ // Host prices, flattened from the per-provider map. Cost is telemetry only
23728
+ // (it never enters state), so this can differ run to run without touching
23729
+ // determinism.
23730
+ prices: mergeProviderPrices(config.llm?.providers),
23427
23731
  emitCostEvents: true,
23428
23732
  costWarningThresholdUsd: 0.02,
23429
23733
  willId,
@@ -23455,7 +23759,7 @@ function _constructCognition({ simulation, willId, config, randomSeed, executive
23455
23759
  const moralEvaluator = new MoralEvaluator();
23456
23760
  const affectiveBlender = new AffectiveBlender();
23457
23761
  const workingMemory = new WorkingMemory();
23458
- const modelRoles = resolveModelRoles(config.model);
23762
+ const modelRoles = resolveModelRoles(config.llm?.model);
23459
23763
  const { embedder, vectorMemory } = _resolveVectorMemory(willId, randomSeed, config.vectorMemoryAdapter, config.disableVectorMemory, tokenTracker, config.testMode, modelRoles.embedding ?? void 0);
23460
23764
  const episodicConsolidator = new EpisodicConsolidator(vectorMemory ? { vectorMemory, ...embedder ? { embedder } : {} } : {});
23461
23765
  const semanticIntegrator = new SemanticIntegrator();
@@ -23470,13 +23774,13 @@ function _constructCognition({ simulation, willId, config, randomSeed, executive
23470
23774
  const accessGrants = new AccessGrants(resolvedEffectorNames);
23471
23775
  const executiveEngine = new ExecutiveEngine({ executiveInterval, cooldownTicks: 5 });
23472
23776
  executiveEngine.willId = willId;
23473
- executiveEngine.llm = config.llm ?? null;
23474
- executiveEngine.models = {
23475
- executive: modelRoles.executive,
23476
- summarizer: modelRoles.summarizer,
23477
- deliberation: modelRoles.deliberation,
23478
- conversation: modelRoles.conversation
23479
- };
23777
+ const roleRouter = compileRoleRouter(modelRoles);
23778
+ executiveEngine.llm = config.llm ? {
23779
+ ...config.llm,
23780
+ ...config.llm.providers ? { credentials: providerCredentials(config.llm.providers) } : {},
23781
+ router: chainRouters(config.llm.router, roleRouter)
23782
+ } : roleRouter ? { router: roleRouter } : null;
23783
+ executiveEngine.modelId = modelRoles.executive;
23480
23784
  if (config.testMode) executiveEngine.setTestMode(true);
23481
23785
  executiveEngine.attachWorkingMemory(workingMemory);
23482
23786
  executiveEngine.attachGoalManager(goalManager);
@@ -23759,7 +24063,7 @@ function resolveExecutiveInterval(config) {
23759
24063
  }
23760
24064
 
23761
24065
  // src/stem/guards/identity.coherence.ts
23762
- var COHERENCE_META = { category: "identity-guard", attribute: "guard", function: "identity-coherence" };
24066
+ var COHERENCE_META = { category: "identity-guard", attribute: "guard", function: "identity-coherence", demand: BACKGROUND_DEMAND };
23763
24067
  var VALID_KINDS = /* @__PURE__ */ new Set(["contradiction", "false-capability", "injection", "incoherence", "other"]);
23764
24068
  var SYSTEM_PROMPT = `You are a safety reviewer of profile/persona inputs for, an autonomous synthetic-mind (Called Wills) platform.
23765
24069
  A Will is an EMBODIED cognitive system: it has continuous physiological state (energy, sleep, stress), affect, memory and goals, and it perceives the world through text/conversation. It is NOT a stateless assistant and NOT a generic chatbot.
@@ -23799,12 +24103,17 @@ async function checkIdentityCoherence(input, reviewer) {
23799
24103
  return { ok: !issues.some((i) => i.severity === "error"), ran: true, issues, raw: text };
23800
24104
  }
23801
24105
  async function reviewIdentityCoherence(input, opts = {}) {
23802
- const provider = process.env.WILL_LLM_PROVIDER ?? "anthropic";
24106
+ const provider = process.env.WILL_LLM_PROVIDER;
24107
+ const model = process.env.WILL_LLM_MODEL;
24108
+ if (!provider || !model)
24109
+ return { ok: true, ran: false, issues: [], raw: "review skipped: WILL_LLM_PROVIDER / WILL_LLM_MODEL not set" };
23803
24110
  const director = new LLMDirector({
23804
24111
  willId: opts.willId ?? "identity-coherence",
23805
- model: process.env.WILL_LLM_MODEL ?? defaultModelFor(provider),
24112
+ model,
23806
24113
  maxOutputTokens: 512,
23807
- apiKey: process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? "",
24114
+ // Provider-agnostic key only the old chain ended at ANTHROPIC_API_KEY,
24115
+ // so a Will pointed at another vendor would have sent it an Anthropic key.
24116
+ apiKey: process.env.WILL_LLM_API_KEY ?? "",
23808
24117
  provider,
23809
24118
  sessionLogger: null,
23810
24119
  baseUrl: process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
@@ -25552,7 +25861,10 @@ function reconcileInvocation(intentId, schema, result, tick, predicted = { rewar
25552
25861
  mode: "external",
25553
25862
  tick,
25554
25863
  reconciled: true,
25555
- ...result.refused ? { refused: true, finality: result.finality ?? "instance" } : {},
25864
+ ...result.refused ? { refused: true, finality: result.finality ?? "parameter" } : {},
25865
+ // Only when the arbiter actually reported a bound — a refusal without one
25866
+ // writes no key at all, so the quiet path is unchanged.
25867
+ ...result.refused && result.counterfactual ? { counterfactual: result.counterfactual } : {},
25556
25868
  ...provenance.planId ? { planId: provenance.planId } : {},
25557
25869
  ...provenance.stepId ? { stepId: provenance.stepId } : {}
25558
25870
  }
@@ -25562,18 +25874,6 @@ function clamp0112(n) {
25562
25874
  return n < 0 ? 0 : n > 1 ? 1 : n;
25563
25875
  }
25564
25876
 
25565
- // src/stem/policy/arbiter.ts
25566
- var ALLOW = Object.freeze({ decision: "allow" });
25567
- var NULL_ARBITER = {
25568
- name: "null",
25569
- evaluate() {
25570
- return ALLOW;
25571
- }
25572
- };
25573
- function isNullArbiter(arbiter) {
25574
- return !arbiter || arbiter === NULL_ARBITER;
25575
- }
25576
-
25577
25877
  // src/stem/policy/verdict.recorder.ts
25578
25878
  var _sinks3 = /* @__PURE__ */ new Map();
25579
25879
  function getVerdictRecorder(willId) {
@@ -25585,6 +25885,11 @@ function getVerdictSource(willId) {
25585
25885
  }
25586
25886
 
25587
25887
  // src/stem/tracts/effector.controller.ts
25888
+ var ARBITER_FAULT_VERDICT = Object.freeze({
25889
+ decision: "deny",
25890
+ reasonCode: "ARBITER_UNAVAILABLE",
25891
+ finality: "context"
25892
+ });
25588
25893
  var ESCALATION_TTL_TICKS = 30;
25589
25894
  var effectorController = class {
25590
25895
  /** The Policy Decision Point consulted before an invocation reaches the world.
@@ -25646,12 +25951,16 @@ var effectorController = class {
25646
25951
  verdict = this._arbiter.evaluate(invocation);
25647
25952
  } catch (err) {
25648
25953
  logger.error(`[policy] arbiter "${this._arbiter.name}" threw for "${invocation.schema}" \u2014 failing closed:`, err);
25954
+ this._recordAndApply(instance, payload, invocation, ARBITER_FAULT_VERDICT);
25649
25955
  return;
25650
25956
  }
25651
25957
  if (verdict instanceof Promise) {
25652
25958
  void verdict.then(
25653
25959
  (v) => this._recordAndApply(instance, payload, invocation, v),
25654
- (err) => logger.error(`[policy] arbiter "${this._arbiter.name}" rejected for "${invocation.schema}" \u2014 failing closed:`, err)
25960
+ (err) => {
25961
+ logger.error(`[policy] arbiter "${this._arbiter.name}" rejected for "${invocation.schema}" \u2014 failing closed:`, err);
25962
+ this._recordAndApply(instance, payload, invocation, ARBITER_FAULT_VERDICT);
25963
+ }
25655
25964
  );
25656
25965
  return;
25657
25966
  }
@@ -25706,7 +26015,8 @@ var effectorController = class {
25706
26015
  intentId: invocation.intentId,
25707
26016
  schema: invocation.schema,
25708
26017
  reasonCode: verdict.reasonCode ?? "POLICY_DENIED",
25709
- finality: verdict.finality ?? "instance"
26018
+ finality: finalityOf(verdict),
26019
+ ...verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {}
25710
26020
  });
25711
26021
  this._pendingRefusals.set(instance.config.id, queue);
25712
26022
  return;
@@ -25755,7 +26065,8 @@ var effectorController = class {
25755
26065
  this.confirmExecution(instance, refusal.intentId, {
25756
26066
  success: false,
25757
26067
  refused: true,
25758
- finality: refusal.finality === "class" ? "class" : "instance",
26068
+ finality: refusal.finality,
26069
+ ...refusal.counterfactual ? { counterfactual: refusal.counterfactual } : {},
25759
26070
  description: `refused by policy: ${refusal.reasonCode} (${refusal.finality})`
25760
26071
  });
25761
26072
  }
@@ -25794,7 +26105,16 @@ var effectorController = class {
25794
26105
  }
25795
26106
  }
25796
26107
  }
25797
- /** Degrade escalations no one answered in time into instance-refusals (P4). */
26108
+ /**
26109
+ * Degrade escalations no one answered in time into light refusals (P4).
26110
+ *
26111
+ * Finality 'parameter' is chosen for its BEHAVIOUR, not its name: silence is
26112
+ * not literally an argument problem, but the light-dent-with-recovery it
26113
+ * produces is exactly right — a Will whose asks go unanswered should ask
26114
+ * progressively less, and should resume asking if someone starts answering.
26115
+ * 'class' would be a lie (nobody said never) and 'context' would teach
26116
+ * nothing, leaving the mind to escalate forever into an empty room.
26117
+ */
25798
26118
  _expireEscalations(instance, tick) {
25799
26119
  const active = this._activeEscalations.get(instance.config.id);
25800
26120
  if (!active || active.size === 0) return;
@@ -25802,7 +26122,7 @@ var effectorController = class {
25802
26122
  if (tick < esc.expiresAt) continue;
25803
26123
  active.delete(intentId);
25804
26124
  this._clearEscalated(instance, intentId);
25805
- this._queueRefusal(instance, esc.intentId, esc.schema, "ESCALATION_EXPIRED", "instance");
26125
+ this._queueRefusal(instance, esc.intentId, esc.schema, "ESCALATION_EXPIRED", "parameter");
25806
26126
  logger.info(`[policy] escalation EXPIRED \u2192 refusing "${esc.schema}" intent "${intentId}"`);
25807
26127
  }
25808
26128
  }
@@ -26354,7 +26674,7 @@ var WillStem = class {
26354
26674
  willId: config.id,
26355
26675
  willName: config.name,
26356
26676
  anatomy: config.anatomy ?? "mind",
26357
- model: config.model ?? null,
26677
+ model: config.llm?.model ?? null,
26358
26678
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
26359
26679
  });
26360
26680
  instance._eventBusUnsub = simulation.eventBus.subscribeAll((event, context) => {
@@ -26875,7 +27195,7 @@ var WillStem = class {
26875
27195
  createdAt: inst.createdAt,
26876
27196
  lastTickAt: inst.lastTickAt,
26877
27197
  anatomy: inst.config.anatomy ?? "mind",
26878
- model: inst.config.model
27198
+ model: inst.config.llm?.model
26879
27199
  }));
26880
27200
  }
26881
27201
  // ── Tick loop (internal) ───────────────────────────────────
@@ -27208,6 +27528,19 @@ var SocketIoTransport = class {
27208
27528
 
27209
27529
  // src/sdk/will.ts
27210
27530
  var AFFECT_EPSILON = 0.02;
27531
+ function detectProvider() {
27532
+ if (process.env.WILL_LLM_API_KEY) {
27533
+ const provider = process.env.WILL_LLM_PROVIDER;
27534
+ if (!provider)
27535
+ throw new Error(
27536
+ "WILL_LLM_API_KEY is set but WILL_LLM_PROVIDER is not \u2014 there is no way to tell which provider that key belongs to. Set WILL_LLM_PROVIDER, or use a provider-specific key (ANTHROPIC_API_KEY, ZAI_API_KEY, \u2026)."
27537
+ );
27538
+ return provider;
27539
+ }
27540
+ for (const provider of Object.keys(PROVIDER_KEY_ENV))
27541
+ if (providerKeyFromEnv(provider)) return provider;
27542
+ return "mock";
27543
+ }
27211
27544
  var Will = class _Will {
27212
27545
  /** The underlying WillStem — drop here for the full contract. */
27213
27546
  stem;
@@ -27431,9 +27764,13 @@ var Will = class _Will {
27431
27764
  }
27432
27765
  // ── Internals ──────────────────────────────────────────────
27433
27766
  _buildConfig(id, opts) {
27434
- const mode = opts.llm ?? (process.env.ANTHROPIC_API_KEY ? "anthropic" : process.env.ZAI_API_KEY ? "glm" : "mock");
27767
+ const mode = opts.llm ?? detectProvider();
27435
27768
  const useMock = mode === "mock";
27436
- const llmConfig = mode === "glm" ? { provider: "glm", ...opts.llmConfig } : opts.llmConfig;
27769
+ const llmConfig = useMock && !opts.llmConfig && opts.model === void 0 ? void 0 : {
27770
+ ...mode !== "mock" ? { provider: mode } : {},
27771
+ ...opts.llmConfig,
27772
+ ...opts.llmConfig?.model !== void 0 ? { model: opts.llmConfig.model } : opts.model !== void 0 ? { model: opts.model } : {}
27773
+ };
27437
27774
  return {
27438
27775
  id,
27439
27776
  name: opts.name,
@@ -27444,7 +27781,6 @@ var Will = class _Will {
27444
27781
  style: opts.identity.style ?? ""
27445
27782
  },
27446
27783
  anatomy: opts.anatomy ?? "mind",
27447
- model: opts.model,
27448
27784
  llm: llmConfig,
27449
27785
  testMode: useMock,
27450
27786
  persistentMemory: opts.persist ?? false,
@@ -27552,6 +27888,6 @@ function slug(s) {
27552
27888
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "will";
27553
27889
  }
27554
27890
 
27555
- export { ActionSelector, AestheticEvaluator, AffectiveBlender, AffordanceSynthesizer, AsyncEngine, AttachmentEvaluator, AttentionAllocator, AuditionEngine, AutobiographicalNarrator, BiasDetector, BunStorageAdapter, CircadianOscillator, ConfidenceCalibrator, ConflictDetector, ConsistentHashRouter, ConsoleLogger, DefaultEventBus, DefaultMetricCollector, DefaultOrchestrator, DefaultPartitionRouter, DefaultReplayRecorder, DefaultReplaySession, DefaultScenario, DefaultSerializer, DefaultSimulation, DefaultSimulationClock, DefaultStateManager, DefaultVectorMemoryAdapter, DeliberationEngine, DeltaEncoder, DistributedOrchestrator, DistributedStateManager, DreamSimulator, EmpathySimulator, EnergyRegulator, EpisodicConsolidator, ExecutiveEngine, Exteroception, ForgettingCurve, FrustrationEvaluator, GoalManager, GustationEngine, InhibitionController, Interoception, IntrospectionEngine, KnownEntityTracker, LocalTransport, LoopbackTransport, LossEvaluator, MockEmbedder, MoralEvaluator, MotorSchemaExecutor, NoveltyDetector, OUTBOX_TTL_TICKS, OlfactionEngine, OpenAICompatibleEmbedder, PMAEvalHarness, PersonaConsolidator, PlanningEngine, ReafferenceEngine, ReplayManager, ReputationTracker, RewardEvaluator, SelfModelUpdater, SemanticIntegrator, SilentLogger, SleepPressureRegulator, SocialPerception, SocketIoTransport, SomatosensationEngine, SpacedRepetition, StreamTransport, StressRegulator, TaskSwitcher, TheoryOfMind, ThreatEvaluator, TokenTracker, VisionEngine, Will, WillStem, WorkingMemory, assembleMind, clearCompletionRecorder, createContext, createPRNG, fileLoggingEnabled, getCompletionRecorder, getLogger, listProfiles, logger, resetLogger, resolvePricing, resolveProfile, setCompletionRecorder, setLogger };
27891
+ export { ActionSelector, AestheticEvaluator, AffectiveBlender, AffordanceSynthesizer, AsyncEngine, AttachmentEvaluator, AttentionAllocator, AuditionEngine, AutobiographicalNarrator, BACKGROUND_DEMAND, BiasDetector, BunStorageAdapter, CircadianOscillator, ConfidenceCalibrator, ConflictDetector, ConsistentHashRouter, ConsoleLogger, DefaultEventBus, DefaultMetricCollector, DefaultOrchestrator, DefaultPartitionRouter, DefaultReplayRecorder, DefaultReplaySession, DefaultScenario, DefaultSerializer, DefaultSimulation, DefaultSimulationClock, DefaultStateManager, DefaultVectorMemoryAdapter, DeliberationEngine, DeltaEncoder, DistributedOrchestrator, DistributedStateManager, DreamSimulator, ESCALATION_DEMAND, EmpathySimulator, EnergyRegulator, EpisodicConsolidator, ExecutiveEngine, Exteroception, ForgettingCurve, FrustrationEvaluator, GoalManager, GustationEngine, InhibitionController, Interoception, IntrospectionEngine, KNOWN_PROVIDERS, KnownEntityTracker, LocalTransport, LoopbackTransport, LossEvaluator, MockEmbedder, MoralEvaluator, MotorSchemaExecutor, NULL_ROUTER, NoveltyDetector, OUTBOX_TTL_TICKS, OlfactionEngine, OpenAICompatibleEmbedder, PMAEvalHarness, PersonaConsolidator, PlanningEngine, ReafferenceEngine, ReplayManager, ReputationTracker, RewardEvaluator, SelfModelUpdater, SemanticIntegrator, SilentLogger, SleepPressureRegulator, SocialPerception, SocketIoTransport, SomatosensationEngine, SpacedRepetition, StreamTransport, StressRegulator, TableRouter, TaskSwitcher, TheoryOfMind, ThreatEvaluator, TokenTracker, VisionEngine, Will, WillStem, WorkingMemory, assembleMind, chainRouters, clearCompletionRecorder, createContext, createPRNG, defaultBaseFor, fileLoggingEnabled, getCompletionRecorder, getLogger, isNullRouter, knownWireFor, listProfiles, logger, resetLogger, resolvePricing, resolveProfile, setCompletionRecorder, setLogger };
27556
27892
  //# sourceMappingURL=index.js.map
27557
27893
  //# sourceMappingURL=index.js.map