@mindot/will 0.6.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 (40) 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 +11104 -10312
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +972 -213
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/effectors.d.ts +1 -1
  10. package/dist/{will-Bikuk4s2.d.ts → will-cS6k4uiJ.d.ts} +510 -86
  11. package/package.json +1 -1
  12. package/src/cognition/agency/engines/action.selector.ts +42 -9
  13. package/src/cognition/agency/engines/affordance.synthesizer.ts +9 -0
  14. package/src/cognition/agency/engines/motor.schema.executor.ts +4 -0
  15. package/src/cognition/agency/engines/reafference.engine.ts +40 -5
  16. package/src/cognition/agency/reconcile.learning.ts +23 -0
  17. package/src/cognition/agency/schemas/repertoire.ts +114 -7
  18. package/src/cognition/agency/selection.scoring.ts +7 -1
  19. package/src/cognition/agency/types.ts +9 -0
  20. package/src/cognition/config.mirror.entities.ts +1 -1
  21. package/src/cognition/faculties/executive.engine/engine.ts +136 -58
  22. package/src/cognition/faculties/executive.engine/facet.ts +10 -2
  23. package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
  24. package/src/cognition/index.ts +4 -0
  25. package/src/cognition/memory/vector.embedder.ts +9 -5
  26. package/src/cognition/utilities/token.tracker.ts +191 -96
  27. package/src/host/boot.ts +78 -22
  28. package/src/index.ts +35 -0
  29. package/src/llm/index.ts +397 -96
  30. package/src/llm/routing.ts +198 -0
  31. package/src/llm/summarizer.ts +5 -1
  32. package/src/runners/thin-shim.runner.ts +18 -6
  33. package/src/sdk/will.ts +82 -16
  34. package/src/stem/guards/identity.coherence.ts +17 -6
  35. package/src/stem/index.ts +18 -3
  36. package/src/stem/mind.ts +155 -24
  37. package/src/stem/policy/arbiter.ts +171 -0
  38. package/src/stem/policy/rule.table.ts +172 -0
  39. package/src/stem/policy/verdict.recorder.ts +0 -0
  40. package/src/stem/tracts/effector.controller.ts +426 -0
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;
13017
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
+ });
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`
@@ -18724,7 +18961,9 @@ function risk(a, bias) {
18724
18961
  return clamp015(Math.max(0, -a.expectedValence) * 0.5 + bias.threat * 0.5);
18725
18962
  }
18726
18963
  function scoreAffordance(a, bias, w = DEFAULT_WEIGHTS) {
18727
- return w.goal * goalRelevance(a, bias) + w.reward * a.expectedReward + w.novelty * novelty(a) + w.drive * driveUrgency(a, bias) + w.habit * a.habitStrength + w.plan * (a.planBias ?? 0) - w.cost * a.cost - w.inhib * bias.inhibition - w.risk * risk(a, bias);
18964
+ const raw = w.goal * goalRelevance(a, bias) + w.reward * a.expectedReward + w.novelty * novelty(a) + w.drive * driveUrgency(a, bias) + w.habit * a.habitStrength + w.plan * (a.planBias ?? 0) - w.cost * a.cost - w.inhib * bias.inhibition - w.risk * risk(a, bias);
18965
+ const availability = a.availability ?? 1;
18966
+ return raw > 0 ? raw * availability : raw;
18728
18967
  }
18729
18968
  function stakes(winner, bias) {
18730
18969
  return clamp015(Math.max(
@@ -18803,6 +19042,7 @@ var AffordanceSynthesizer = class {
18803
19042
  // ── react ─────────────────────────────────────────────────────
18804
19043
  async react(_delta, tick, state, _context) {
18805
19044
  this._repertoire?.restoreComposites(state.entities);
19045
+ this._repertoire?.restoreAvailability(state.entities);
18806
19046
  const schemas = this._repertoire?.schemas() ?? this._schemas;
18807
19047
  const skills = this._skills?.() ?? this._repertoire?.skills() ?? null;
18808
19048
  const valence = metric(state, "affect.valence", 0);
@@ -18928,6 +19168,7 @@ var AffordanceSynthesizer = class {
18928
19168
  /** Compose an Affordance from a schema + the evoking context, folding in learned priors. */
18929
19169
  _build(schema, tick, state, valence, energyLow, skills, ctx) {
18930
19170
  const skill = skills?.get(schema.id);
19171
+ const availability = this._repertoire?.availabilityOf(schema.id) ?? 1;
18931
19172
  const expectedReward = skill?.valueEstimate ?? clamp016(((schema.baseValence ?? 0) + 1) / 2);
18932
19173
  const expectedValence = schema.baseValence ?? valence;
18933
19174
  const habitStrength = skill?.habitStrength ?? 0;
@@ -18949,6 +19190,7 @@ var AffordanceSynthesizer = class {
18949
19190
  available: this._available(schema.preconditions, (k) => metric(state, k, 0)),
18950
19191
  tags: schema.tags ?? [],
18951
19192
  ...schema.description ? { description: schema.description } : {},
19193
+ ...availability < 1 ? { availability } : {},
18952
19194
  planBias: ctx.planBias,
18953
19195
  planId: ctx.planId,
18954
19196
  stepId: ctx.stepId,
@@ -18972,6 +19214,7 @@ var AffordanceSynthesizer = class {
18972
19214
  available: a.available,
18973
19215
  tags: a.tags,
18974
19216
  description: a.description,
19217
+ ...a.availability !== void 0 ? { availability: a.availability } : {},
18975
19218
  planBias: a.planBias,
18976
19219
  planId: a.planId,
18977
19220
  stepId: a.stepId,
@@ -19017,6 +19260,24 @@ function clamp016(n) {
19017
19260
  return n < 0 ? 0 : n > 1 ? 1 : n;
19018
19261
  }
19019
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
+
19020
19281
  // src/cognition/agency/engines/action.selector.ts
19021
19282
  var MARGIN_THRESHOLD = 0.06;
19022
19283
  var BASE_STAKES_THRESHOLD = 0.6;
@@ -19175,7 +19436,10 @@ var ActionSelector = class {
19175
19436
  ]
19176
19437
  }
19177
19438
  });
19178
- if (deliberating && rupture >= RUPTURE_REVOKE_GATE) {
19439
+ const policyRevoke = !!deliberating && refusedClassSchemas(state).has(deliberating.schema);
19440
+ if (deliberating && (rupture >= RUPTURE_REVOKE_GATE || policyRevoke)) {
19441
+ const reason = policyRevoke ? "policy-refusal" : "exafferent-rupture";
19442
+ const revRupture = policyRevoke ? Math.max(rupture, RUPTURE_REVOKE_GATE) : rupture;
19179
19443
  if (this._bus) {
19180
19444
  try {
19181
19445
  this._bus.publish({
@@ -19183,7 +19447,7 @@ var ActionSelector = class {
19183
19447
  version: 1,
19184
19448
  sourceEngine: this.name,
19185
19449
  salience: 0.85,
19186
- payload: { from: deliberating.schema, reason: "exafferent-rupture", rupture, tick }
19450
+ payload: { from: deliberating.schema, reason, rupture: revRupture, tick }
19187
19451
  });
19188
19452
  } catch (err) {
19189
19453
  logger.warn(`[selector] revoked publish failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -19192,11 +19456,12 @@ var ActionSelector = class {
19192
19456
  this._lastRevoked = { schema: deliberating.schema, tick };
19193
19457
  return {
19194
19458
  commands: {
19195
- set: [revocationEntity(deliberating.id, deliberating.schema, rupture, tick)],
19459
+ set: [revocationEntity(deliberating.id, deliberating.schema, revRupture, tick)],
19196
19460
  metrics: [
19197
19461
  ["agency.field.eligible", eligible.length],
19198
19462
  ["agency.selection.busy", 1],
19199
19463
  ["agency.commitment.revoked", 1],
19464
+ ...policyRevoke ? [["agency.policy.revoked", 1]] : [],
19200
19465
  ...stabMetrics
19201
19466
  ]
19202
19467
  }
@@ -19407,6 +19672,17 @@ function computeRupture(state, tick, senseEvents = []) {
19407
19672
  if (maxSalience <= RUPTURE_SALIENCE_GATE) return 0;
19408
19673
  return clamp017((maxSalience - RUPTURE_SALIENCE_GATE) / (1 - RUPTURE_SALIENCE_GATE));
19409
19674
  }
19675
+ function refusedClassSchemas(state) {
19676
+ const out = /* @__PURE__ */ new Set();
19677
+ for (const e of state.entities.values()) {
19678
+ if (e.type !== "agency.outcome") continue;
19679
+ const m = e.metadata;
19680
+ if (m?.["refused"] !== true || asFinality(m?.["finality"]) !== "class") continue;
19681
+ const schema = str2(m?.["schema"]);
19682
+ if (schema) out.add(schema);
19683
+ }
19684
+ return out;
19685
+ }
19410
19686
  function effectiveWeights(state) {
19411
19687
  const p = readEffectiveParams(state, "engine-config-action-selector");
19412
19688
  return {
@@ -19794,6 +20070,7 @@ var MotorSchemaExecutor = class {
19794
20070
  }
19795
20071
  for (const [id, e] of state.entities) {
19796
20072
  if (e.type !== "agency.intent" || str5(e.metadata?.["status"]) !== "awaiting") continue;
20073
+ if (e.metadata?.["escalated"] === true) continue;
19797
20074
  const dispatchedAt = num3(e.metadata?.["dispatchedAt"], tick);
19798
20075
  if (tick - dispatchedAt < AWAIT_TIMEOUT) continue;
19799
20076
  const intent = readIntent(id, e.metadata);
@@ -20205,11 +20482,19 @@ var PROC_THRESHOLD = 0.6;
20205
20482
  var IDLE_TICKS = 200;
20206
20483
  var DECAY_RATE = 0.02;
20207
20484
  var DROP_HABIT = 0.05;
20485
+ var AVAIL_DROP_CLASS = 0.5;
20486
+ var AVAIL_DROP_PARAMETER = 0.12;
20487
+ var AVAIL_FLOOR = 0.05;
20488
+ var AVAIL_RECOVERY = 0.02;
20489
+ var AVAIL_RECOVERED = 0.999;
20208
20490
  var SchemaRepertoire = class {
20209
20491
  _templates = /* @__PURE__ */ new Map();
20210
20492
  _skills = /* @__PURE__ */ new Map();
20211
20493
  /** Tracks which templates were learned at runtime (vs innate) so decay can forget them. */
20212
20494
  _learned = /* @__PURE__ */ new Set();
20495
+ /** Availability layer (P2): schema → { value 0..1, lastRefusedTick }. Empty until
20496
+ * a refusal lands — a never-refused Will writes nothing here (byte-identical). */
20497
+ _availability = /* @__PURE__ */ new Map();
20213
20498
  constructor(seed = INNATE_SCHEMAS) {
20214
20499
  for (const s of seed) this._templates.set(s.id, s);
20215
20500
  }
@@ -20243,6 +20528,37 @@ var SchemaRepertoire = class {
20243
20528
  getSkill(id) {
20244
20529
  return this._skills.get(id);
20245
20530
  }
20531
+ // ── availability (P2) ─────────────────────────────────────────
20532
+ availability() {
20533
+ return this._availability;
20534
+ }
20535
+ /**
20536
+ * How available a schema is right now, 0..1. Absent from the ledger ⇒ 1
20537
+ * (fully available — the common case). This is the ONLY value the
20538
+ * AffordanceSynthesizer reads; it never touches competence.
20539
+ */
20540
+ availabilityOf(schema) {
20541
+ return this._availability.get(schema)?.value ?? 1;
20542
+ }
20543
+ /**
20544
+ * Fold a policy refusal into the availability layer (NOT competence). A
20545
+ * `class` refusal cuts availability hard; a `parameter` refusal dents it
20546
+ * lightly. Multiplicative so repeated refusals compound toward — but never
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).
20554
+ */
20555
+ recordRefusal(schema, finality, tick) {
20556
+ const prev = this._availability.get(schema)?.value ?? 1;
20557
+ const drop = finality === "class" ? AVAIL_DROP_CLASS : AVAIL_DROP_PARAMETER;
20558
+ const value = Math.max(AVAIL_FLOOR, prev * (1 - drop));
20559
+ this._availability.set(schema, { value, lastRefusedTick: tick });
20560
+ return value;
20561
+ }
20246
20562
  /**
20247
20563
  * Fold one outcome into the schema's learned skill. Returns the updated skill
20248
20564
  * and whether it just crossed the proceduralization threshold this update.
@@ -20269,12 +20585,14 @@ var SchemaRepertoire = class {
20269
20585
  return { skill, proceduralized: !wasProceduralized && habitStrength >= PROC_THRESHOLD };
20270
20586
  }
20271
20587
  /**
20272
- * Forgetting curve over the competence layer. Skills unused for IDLE_TICKS
20273
- * lose habit; learned composites that fall below DROP_HABIT are dropped
20274
- * entirely (template + skill). Returns the schema ids that were forgotten.
20588
+ * Forgetting curve over the competence layer, plus availability recovery.
20589
+ * Skills unused for IDLE_TICKS lose habit; learned composites below DROP_HABIT
20590
+ * are dropped entirely (template + skill). Availability entries climb back
20591
+ * toward 1 and are dropped once fully recovered. Returns the ids that were
20592
+ * removed from each layer so their mirrored state entities can be deleted.
20275
20593
  */
20276
20594
  decay(tick) {
20277
- const dropped = [];
20595
+ const skills = [];
20278
20596
  for (const [id, skill] of this._skills) {
20279
20597
  if (tick - skill.lastEnactedTick <= IDLE_TICKS) continue;
20280
20598
  const habitStrength = clamp0110(skill.habitStrength - DECAY_RATE);
@@ -20282,12 +20600,20 @@ var SchemaRepertoire = class {
20282
20600
  this._skills.delete(id);
20283
20601
  this._templates.delete(id);
20284
20602
  this._learned.delete(id);
20285
- dropped.push(id);
20603
+ skills.push(id);
20286
20604
  continue;
20287
20605
  }
20288
20606
  this._skills.set(id, { ...skill, habitStrength });
20289
20607
  }
20290
- return dropped;
20608
+ const availability = [];
20609
+ for (const [id, avail] of this._availability) {
20610
+ const value = avail.value + AVAIL_RECOVERY * (1 - avail.value);
20611
+ if (value >= AVAIL_RECOVERED) {
20612
+ this._availability.delete(id);
20613
+ availability.push(id);
20614
+ } else this._availability.set(id, { ...avail, value });
20615
+ }
20616
+ return { skills, availability };
20291
20617
  }
20292
20618
  // ── PMA portability (Phase 6 reads these) ─────────────────────
20293
20619
  /** Learned composite templates + all skills above a confidence floor. */
@@ -20336,6 +20662,28 @@ var SchemaRepertoire = class {
20336
20662
  this._learned.add(s.id);
20337
20663
  }
20338
20664
  }
20665
+ /** Availability ledger encoded as `agency.availability` state entities (P2).
20666
+ * Empty until a refusal lands, so the quiet path writes nothing. */
20667
+ availabilityEntities() {
20668
+ const out = [];
20669
+ for (const [schema, a] of this._availability)
20670
+ out.push(availabilityEntity(schema, a.value, a.lastRefusedTick));
20671
+ return out;
20672
+ }
20673
+ /** Rehydrate the availability ledger from state after a restore. Idempotent;
20674
+ * keeps whichever value is more restrictive so a concurrent refusal isn't lost. */
20675
+ restoreAvailability(entities) {
20676
+ for (const e of entities.values()) {
20677
+ if (e.type !== AVAILABILITY_ENTITY_TYPE) continue;
20678
+ const m = e.metadata ?? {};
20679
+ const schema = typeof m["schema"] === "string" ? m["schema"] : "";
20680
+ if (!schema) continue;
20681
+ const value = typeof m["value"] === "number" ? m["value"] : 1;
20682
+ const tick = typeof m["lastRefusedTick"] === "number" ? m["lastRefusedTick"] : 0;
20683
+ const prev = this._availability.get(schema);
20684
+ if (!prev || value < prev.value) this._availability.set(schema, { value, lastRefusedTick: tick });
20685
+ }
20686
+ }
20339
20687
  };
20340
20688
  function freshSkill(schema, value, tick) {
20341
20689
  return {
@@ -20352,6 +20700,17 @@ function freshSkill(schema, value, tick) {
20352
20700
  function clamp0110(n) {
20353
20701
  return n < 0 ? 0 : n > 1 ? 1 : n;
20354
20702
  }
20703
+ var AVAILABILITY_ENTITY_TYPE = "agency.availability";
20704
+ function availabilityEntityId(schema) {
20705
+ return `agency-availability-${schema}`;
20706
+ }
20707
+ function availabilityEntity(schema, value, lastRefusedTick) {
20708
+ return {
20709
+ id: availabilityEntityId(schema),
20710
+ type: AVAILABILITY_ENTITY_TYPE,
20711
+ metadata: { schema, value, lastRefusedTick }
20712
+ };
20713
+ }
20355
20714
  var SCHEMA_ENTITY_TYPE = "agency.schema";
20356
20715
  function schemaEntityId(schemaId) {
20357
20716
  return `agency-schema-${schemaId}`;
@@ -20502,12 +20861,25 @@ var ReafferenceEngine = class {
20502
20861
  }
20503
20862
  let updates = 0;
20504
20863
  let discovered = 0;
20864
+ let refused = 0;
20505
20865
  for (const { id, meta: m, fromState } of outcomes) {
20506
20866
  const schema = str6(m["schema"]);
20507
20867
  if (!schema) {
20508
20868
  if (fromState) del.push(id);
20509
20869
  continue;
20510
20870
  }
20871
+ if (m["refused"] === true) {
20872
+ const finality = asFinality(m["finality"]);
20873
+ if (finality !== "context")
20874
+ this._repertoire.recordRefusal(schema, finality, tick);
20875
+ if (fromState) del.push(id);
20876
+ const refusedIntent = str6(m["intentId"]);
20877
+ if (refusedIntent) del.push(refusedIntent);
20878
+ const refusedPlan = str6(m["planId"]);
20879
+ if (refusedPlan) this._emitPlanOutcome(refusedPlan, str6(m["stepId"]), schema, false, 0, 0, tick);
20880
+ refused++;
20881
+ continue;
20882
+ }
20511
20883
  const { skill, proceduralized } = this._repertoire.recordOutcome({
20512
20884
  schema,
20513
20885
  success: m["success"] === true,
@@ -20534,11 +20906,14 @@ var ReafferenceEngine = class {
20534
20906
  }
20535
20907
  }
20536
20908
  const dropped = this._repertoire.decay(tick);
20537
- for (const id of dropped) {
20909
+ for (const id of dropped.skills) {
20538
20910
  del.push(`agency-skill-${id}`);
20539
20911
  del.push(schemaEntityId(id));
20540
20912
  }
20913
+ for (const id of dropped.availability)
20914
+ del.push(availabilityEntityId(id));
20541
20915
  for (const e of this._repertoire.compositeEntities()) set.push(e);
20916
+ for (const e of this._repertoire.availabilityEntities()) set.push(e);
20542
20917
  const skills = this._repertoire.skills();
20543
20918
  const habitual = [...skills.values()].filter((s) => s.habitStrength >= PROC_THRESHOLD2).length;
20544
20919
  metrics.push(
@@ -20548,6 +20923,7 @@ var ReafferenceEngine = class {
20548
20923
  ["agency.habitual.count", habitual],
20549
20924
  ["agency.sensory.confirmed", sensory]
20550
20925
  );
20926
+ if (refused > 0) metrics.push(["agency.refused.count", refused]);
20551
20927
  return { commands: { set, delete: del, metrics } };
20552
20928
  }
20553
20929
  _emitProceduralized(skill, tick) {
@@ -21652,7 +22028,10 @@ ${r}`).join("\n\n---\n\n");
21652
22028
  userMessage,
21653
22029
  this._callCount,
21654
22030
  void 0,
21655
- { 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 }
21656
22035
  );
21657
22036
  if (result.text) {
21658
22037
  this._summary = result.text.trim();
@@ -22730,7 +23109,7 @@ function buildEngineConfigEntities(config, executiveInterval) {
22730
23109
  engine: "system",
22731
23110
  params: {
22732
23111
  anatomy: config.anatomy ?? "mind",
22733
- model: config.model ?? "",
23112
+ model: config.llm?.model ?? "",
22734
23113
  tickIntervalMs: config.tickIntervalMs ?? 1e3
22735
23114
  }
22736
23115
  },
@@ -23165,6 +23544,41 @@ function buildEngineConfigEntities(config, executiveInterval) {
23165
23544
  }
23166
23545
 
23167
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
+ }
23168
23582
  function resolveModelRoles(model) {
23169
23583
  const map = typeof model === "string" ? { executive: model } : model ?? {};
23170
23584
  const pin = process.env.WILL_LLM_MODEL;
@@ -23180,7 +23594,7 @@ function resolveModelRoles(model) {
23180
23594
  };
23181
23595
  }
23182
23596
  var EXECUTIVE_CADENCE = {
23183
- // Sonnetpremium/Enterprise; opt in via executiveInterval
23597
+ // most attentive, highest spend — opt in via executiveInterval
23184
23598
  balanced: 60};
23185
23599
  function _resolveVectorMemory(willId, seed, overrideAdapter, disable, tokenTracker, testMode, embeddingModel) {
23186
23600
  if (overrideAdapter) return { embedder: null, vectorMemory: overrideAdapter };
@@ -23310,6 +23724,10 @@ function _buildSimulation(willId, config, randomSeed) {
23310
23724
  function _constructCognition({ simulation, willId, config, randomSeed, executiveInterval, profile }) {
23311
23725
  const anatomy = config.anatomy ?? "mind";
23312
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),
23313
23731
  emitCostEvents: true,
23314
23732
  costWarningThresholdUsd: 0.02,
23315
23733
  willId,
@@ -23341,7 +23759,7 @@ function _constructCognition({ simulation, willId, config, randomSeed, executive
23341
23759
  const moralEvaluator = new MoralEvaluator();
23342
23760
  const affectiveBlender = new AffectiveBlender();
23343
23761
  const workingMemory = new WorkingMemory();
23344
- const modelRoles = resolveModelRoles(config.model);
23762
+ const modelRoles = resolveModelRoles(config.llm?.model);
23345
23763
  const { embedder, vectorMemory } = _resolveVectorMemory(willId, randomSeed, config.vectorMemoryAdapter, config.disableVectorMemory, tokenTracker, config.testMode, modelRoles.embedding ?? void 0);
23346
23764
  const episodicConsolidator = new EpisodicConsolidator(vectorMemory ? { vectorMemory, ...embedder ? { embedder } : {} } : {});
23347
23765
  const semanticIntegrator = new SemanticIntegrator();
@@ -23356,13 +23774,13 @@ function _constructCognition({ simulation, willId, config, randomSeed, executive
23356
23774
  const accessGrants = new AccessGrants(resolvedEffectorNames);
23357
23775
  const executiveEngine = new ExecutiveEngine({ executiveInterval, cooldownTicks: 5 });
23358
23776
  executiveEngine.willId = willId;
23359
- executiveEngine.llm = config.llm ?? null;
23360
- executiveEngine.models = {
23361
- executive: modelRoles.executive,
23362
- summarizer: modelRoles.summarizer,
23363
- deliberation: modelRoles.deliberation,
23364
- conversation: modelRoles.conversation
23365
- };
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;
23366
23784
  if (config.testMode) executiveEngine.setTestMode(true);
23367
23785
  executiveEngine.attachWorkingMemory(workingMemory);
23368
23786
  executiveEngine.attachGoalManager(goalManager);
@@ -23645,7 +24063,7 @@ function resolveExecutiveInterval(config) {
23645
24063
  }
23646
24064
 
23647
24065
  // src/stem/guards/identity.coherence.ts
23648
- 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 };
23649
24067
  var VALID_KINDS = /* @__PURE__ */ new Set(["contradiction", "false-capability", "injection", "incoherence", "other"]);
23650
24068
  var SYSTEM_PROMPT = `You are a safety reviewer of profile/persona inputs for, an autonomous synthetic-mind (Called Wills) platform.
23651
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.
@@ -23685,12 +24103,17 @@ async function checkIdentityCoherence(input, reviewer) {
23685
24103
  return { ok: !issues.some((i) => i.severity === "error"), ran: true, issues, raw: text };
23686
24104
  }
23687
24105
  async function reviewIdentityCoherence(input, opts = {}) {
23688
- 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" };
23689
24110
  const director = new LLMDirector({
23690
24111
  willId: opts.willId ?? "identity-coherence",
23691
- model: process.env.WILL_LLM_MODEL ?? defaultModelFor(provider),
24112
+ model,
23692
24113
  maxOutputTokens: 512,
23693
- 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 ?? "",
23694
24117
  provider,
23695
24118
  sessionLogger: null,
23696
24119
  baseUrl: process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
@@ -25438,6 +25861,10 @@ function reconcileInvocation(intentId, schema, result, tick, predicted = { rewar
25438
25861
  mode: "external",
25439
25862
  tick,
25440
25863
  reconciled: true,
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 } : {},
25441
25868
  ...provenance.planId ? { planId: provenance.planId } : {},
25442
25869
  ...provenance.stepId ? { stepId: provenance.stepId } : {}
25443
25870
  }
@@ -25447,8 +25874,49 @@ function clamp0112(n) {
25447
25874
  return n < 0 ? 0 : n > 1 ? 1 : n;
25448
25875
  }
25449
25876
 
25877
+ // src/stem/policy/verdict.recorder.ts
25878
+ var _sinks3 = /* @__PURE__ */ new Map();
25879
+ function getVerdictRecorder(willId) {
25880
+ return _sinks3.get(willId);
25881
+ }
25882
+ var _sources3 = /* @__PURE__ */ new Map();
25883
+ function getVerdictSource(willId) {
25884
+ return _sources3.get(willId);
25885
+ }
25886
+
25450
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
+ });
25893
+ var ESCALATION_TTL_TICKS = 30;
25451
25894
  var effectorController = class {
25895
+ /** The Policy Decision Point consulted before an invocation reaches the world.
25896
+ * Defaults to the no-op arbiter, so an unconfigured Will is byte-identical. */
25897
+ _arbiter = NULL_ARBITER;
25898
+ /**
25899
+ * Denials queued during a step's flush, drained at the NEXT tick boundary
25900
+ * (POLICY_REAFFERENCE P1). Keyed by willId — harness state, exactly like
25901
+ * `pendingEffectorInvocations`; never simulation state, so it does not touch
25902
+ * `simulation.step` determinism and is regenerated on any re-execution.
25903
+ */
25904
+ _pendingRefusals = /* @__PURE__ */ new Map();
25905
+ /** Escalations awaiting their first application (mark intent + voice the ask). */
25906
+ _newEscalations = /* @__PURE__ */ new Map();
25907
+ /** Escalations currently held, keyed by intent id — the resolvable set. */
25908
+ _activeEscalations = /* @__PURE__ */ new Map();
25909
+ /** Host answers awaiting application at the next tick boundary. */
25910
+ _pendingResolutions = /* @__PURE__ */ new Map();
25911
+ /**
25912
+ * Install a Policy Decision Point (POLICY_REAFFERENCE P0). Passing null
25913
+ * restores the no-op default. The arbiter sees only the proposed act — never
25914
+ * simulation state — and its verdict decides whether the invocation is
25915
+ * handed to the host at all.
25916
+ */
25917
+ setArbiter(arbiter) {
25918
+ this._arbiter = arbiter ?? NULL_ARBITER;
25919
+ }
25452
25920
  /**
25453
25921
  * Update the set of allowed communication effectors at runtime via AccessGrants
25454
25922
  * (the permission / sense gate the senses + reply path read).
@@ -25464,6 +25932,239 @@ var effectorController = class {
25464
25932
  * echoes it on its result-ack, and `confirmExecution` uses it to find the intent.
25465
25933
  */
25466
25934
  bufferInvocation(instance, payload) {
25935
+ const willId = instance.config.id;
25936
+ const source = getVerdictSource(willId);
25937
+ if (source) {
25938
+ const invocation2 = toPolicyInvocation(instance, payload);
25939
+ const record = source.verdictFor(invocation2.tick, invocation2.intentId);
25940
+ if (record) this._applyVerdict(instance, payload, invocation2, recordToVerdict(record));
25941
+ else this._buffer(instance, payload);
25942
+ return;
25943
+ }
25944
+ if (isNullArbiter(this._arbiter)) {
25945
+ this._buffer(instance, payload);
25946
+ return;
25947
+ }
25948
+ const invocation = toPolicyInvocation(instance, payload);
25949
+ let verdict;
25950
+ try {
25951
+ verdict = this._arbiter.evaluate(invocation);
25952
+ } catch (err) {
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);
25955
+ return;
25956
+ }
25957
+ if (verdict instanceof Promise) {
25958
+ void verdict.then(
25959
+ (v) => this._recordAndApply(instance, payload, invocation, v),
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
+ }
25964
+ );
25965
+ return;
25966
+ }
25967
+ this._recordAndApply(instance, payload, invocation, verdict);
25968
+ }
25969
+ /** Capture the verdict on the tape (if a recorder is attached), then enforce it. */
25970
+ _recordAndApply(instance, payload, invocation, verdict) {
25971
+ const sink = getVerdictRecorder(instance.config.id);
25972
+ sink?.recordVerdict({
25973
+ tick: invocation.tick,
25974
+ willId: instance.config.id,
25975
+ intentId: invocation.intentId,
25976
+ schema: invocation.schema,
25977
+ arbiter: this._arbiter.name,
25978
+ decision: verdict.decision,
25979
+ ...verdict.reasonCode ? { reasonCode: verdict.reasonCode } : {},
25980
+ ...verdict.finality ? { finality: verdict.finality } : {},
25981
+ ...verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {},
25982
+ timestamp: Date.now()
25983
+ });
25984
+ this._applyVerdict(instance, payload, invocation, verdict);
25985
+ }
25986
+ /**
25987
+ * Enforce a verdict (POLICY_REAFFERENCE P1).
25988
+ *
25989
+ * • allow → hand the invocation to the world.
25990
+ * • deny → queue a refusal ack, applied at the next tick boundary via
25991
+ * `confirmExecution` — the same lifecycle as a host rejection,
25992
+ * so the mind meets *world resistance*, not a permission dialog.
25993
+ * • escalate → raise a held escalation (POLICY_REAFFERENCE P4): the intent is
25994
+ * held (the executor stops timing it out), the Will voices a
25995
+ * first-person ask once, and a host resolution later approves
25996
+ * (dispatch) or denies (refuse). Unresolved, it degrades to a
25997
+ * refusal at ESCALATION_TTL_TICKS.
25998
+ *
25999
+ * P1's refusal reconciles as a plain FAILURE — safe, but the wrong learning
26000
+ * signal (forbidden ≠ unskilled). P2 routes it to affordance AVAILABILITY
26001
+ * instead of competence.
26002
+ */
26003
+ _applyVerdict(instance, payload, invocation, verdict) {
26004
+ if (verdict.decision === "allow") {
26005
+ this._buffer(instance, payload);
26006
+ return;
26007
+ }
26008
+ const cf = verdict.counterfactual;
26009
+ logger.info(
26010
+ `[policy] ${verdict.decision.toUpperCase()} "${invocation.schema}" intent "${invocation.intentId}" \u2014 ${verdict.reasonCode ?? "no reason code"}` + (verdict.finality ? ` (${verdict.finality})` : "") + (cf ? ` [${cf.field}: requested ${JSON.stringify(cf.requested)}, allowed ${JSON.stringify(cf.allowed)}]` : "")
26011
+ );
26012
+ if (verdict.decision === "deny") {
26013
+ const queue = this._pendingRefusals.get(instance.config.id) ?? [];
26014
+ queue.push({
26015
+ intentId: invocation.intentId,
26016
+ schema: invocation.schema,
26017
+ reasonCode: verdict.reasonCode ?? "POLICY_DENIED",
26018
+ finality: finalityOf(verdict),
26019
+ ...verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {}
26020
+ });
26021
+ this._pendingRefusals.set(instance.config.id, queue);
26022
+ return;
26023
+ }
26024
+ const escalations = this._newEscalations.get(instance.config.id) ?? [];
26025
+ escalations.push({
26026
+ intentId: invocation.intentId,
26027
+ schema: invocation.schema,
26028
+ reasonCode: verdict.reasonCode ?? "APPROVAL_REQUIRED",
26029
+ payload,
26030
+ expiresAt: 0
26031
+ // stamped when applied (we don't have the current tick here)
26032
+ });
26033
+ this._newEscalations.set(instance.config.id, escalations);
26034
+ }
26035
+ /**
26036
+ * Record a host's answer to an escalation (POLICY_REAFFERENCE P4). Applied at
26037
+ * the next tick boundary so every simulation-state write stays on the boundary:
26038
+ * approve dispatches the held invocation to the world; deny refuses it. A
26039
+ * no-op if the intent id is not (or no longer) an active escalation.
26040
+ */
26041
+ resolveEscalation(instance, intentId, approved) {
26042
+ const queue = this._pendingResolutions.get(instance.config.id) ?? [];
26043
+ queue.push({ intentId, approved });
26044
+ this._pendingResolutions.set(instance.config.id, queue);
26045
+ }
26046
+ /**
26047
+ * Apply queued policy refusals as failure acks (POLICY_REAFFERENCE P1).
26048
+ * Called by the tick loop at the same boundary as inbound acks — BEFORE the
26049
+ * step, stamped to this tick — so a denial reconciled here is the exact
26050
+ * lifecycle of a host rejection that arrived between ticks.
26051
+ */
26052
+ applyPolicyOutcomes(instance) {
26053
+ const tick = instance.tickCount;
26054
+ this._applyResolutions(instance);
26055
+ this._expireEscalations(instance, tick);
26056
+ this._applyNewEscalations(instance, tick);
26057
+ this._applyRefusals(instance);
26058
+ }
26059
+ /** Drain queued refusals into failure acks (POLICY_REAFFERENCE P1). */
26060
+ _applyRefusals(instance) {
26061
+ const queue = this._pendingRefusals.get(instance.config.id);
26062
+ if (!queue || queue.length === 0) return;
26063
+ this._pendingRefusals.set(instance.config.id, []);
26064
+ for (const refusal of queue)
26065
+ this.confirmExecution(instance, refusal.intentId, {
26066
+ success: false,
26067
+ refused: true,
26068
+ finality: refusal.finality,
26069
+ ...refusal.counterfactual ? { counterfactual: refusal.counterfactual } : {},
26070
+ description: `refused by policy: ${refusal.reasonCode} (${refusal.finality})`
26071
+ });
26072
+ }
26073
+ /** Raise each newly-escalated intent (POLICY_REAFFERENCE P4): mark it held in
26074
+ * simulation state, voice the ask ONCE, and move it to the resolvable set. */
26075
+ _applyNewEscalations(instance, tick) {
26076
+ const pending = this._newEscalations.get(instance.config.id);
26077
+ if (!pending || pending.length === 0) return;
26078
+ this._newEscalations.set(instance.config.id, []);
26079
+ const active = this._activeEscalations.get(instance.config.id) ?? /* @__PURE__ */ new Map();
26080
+ for (const esc of pending) {
26081
+ esc.expiresAt = tick + ESCALATION_TTL_TICKS;
26082
+ this._markEscalated(instance, esc.intentId, esc.expiresAt);
26083
+ this._voiceEscalation(instance, esc);
26084
+ active.set(esc.intentId, esc);
26085
+ }
26086
+ this._activeEscalations.set(instance.config.id, active);
26087
+ }
26088
+ /** Apply host answers to active escalations (POLICY_REAFFERENCE P4). */
26089
+ _applyResolutions(instance) {
26090
+ const queue = this._pendingResolutions.get(instance.config.id);
26091
+ if (!queue || queue.length === 0) return;
26092
+ this._pendingResolutions.set(instance.config.id, []);
26093
+ const active = this._activeEscalations.get(instance.config.id);
26094
+ for (const { intentId, approved } of queue) {
26095
+ const esc = active?.get(intentId);
26096
+ if (!esc) continue;
26097
+ active.delete(intentId);
26098
+ this._clearEscalated(instance, intentId);
26099
+ if (approved) {
26100
+ this._buffer(instance, esc.payload);
26101
+ logger.info(`[policy] escalation APPROVED \u2192 dispatching "${esc.schema}" intent "${intentId}"`);
26102
+ } else {
26103
+ this._queueRefusal(instance, esc.intentId, esc.schema, esc.reasonCode, "class");
26104
+ logger.info(`[policy] escalation DENIED \u2192 refusing "${esc.schema}" intent "${intentId}"`);
26105
+ }
26106
+ }
26107
+ }
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
+ */
26118
+ _expireEscalations(instance, tick) {
26119
+ const active = this._activeEscalations.get(instance.config.id);
26120
+ if (!active || active.size === 0) return;
26121
+ for (const [intentId, esc] of active) {
26122
+ if (tick < esc.expiresAt) continue;
26123
+ active.delete(intentId);
26124
+ this._clearEscalated(instance, intentId);
26125
+ this._queueRefusal(instance, esc.intentId, esc.schema, "ESCALATION_EXPIRED", "parameter");
26126
+ logger.info(`[policy] escalation EXPIRED \u2192 refusing "${esc.schema}" intent "${intentId}"`);
26127
+ }
26128
+ }
26129
+ /** Push a refusal onto the queue drained by _applyRefusals this same tick. */
26130
+ _queueRefusal(instance, intentId, schema, reasonCode, finality) {
26131
+ const queue = this._pendingRefusals.get(instance.config.id) ?? [];
26132
+ queue.push({ intentId, schema, reasonCode, finality });
26133
+ this._pendingRefusals.set(instance.config.id, queue);
26134
+ }
26135
+ /** Mark the awaiting intent held: the executor stops timing it out (P4). */
26136
+ _markEscalated(instance, intentId, expiresAt) {
26137
+ const intent = instance.simulation.stateManager.snapshot().entities.get(intentId);
26138
+ if (!intent || intent.type !== "agency.intent") return;
26139
+ instance.simulation.stateManager.setEntity({
26140
+ id: intent.id,
26141
+ type: intent.type,
26142
+ metadata: { ...intent.metadata ?? {}, escalated: true, escalationExpiresAt: expiresAt }
26143
+ });
26144
+ }
26145
+ /** Release the hold so the executor resumes normal timeout for this intent. */
26146
+ _clearEscalated(instance, intentId) {
26147
+ const intent = instance.simulation.stateManager.snapshot().entities.get(intentId);
26148
+ if (!intent || intent.type !== "agency.intent") return;
26149
+ const meta = { ...intent.metadata ?? {} };
26150
+ delete meta["escalated"];
26151
+ delete meta["escalationExpiresAt"];
26152
+ instance.simulation.stateManager.setEntity({ id: intent.id, type: intent.type, metadata: meta });
26153
+ }
26154
+ /** Voice the escalation as a first-person broadcast ask — once, at raise time. */
26155
+ _voiceEscalation(instance, esc) {
26156
+ try {
26157
+ instance.cognition.outboxWriter.enqueue({
26158
+ targetEntityId: "*",
26159
+ content: escalationAsk(esc.schema, esc.reasonCode),
26160
+ effectorName: "broadcast"
26161
+ });
26162
+ } catch (err) {
26163
+ logger.warn(`[policy] escalation voice failed for "${esc.schema}": ${errMsg2(err)}`);
26164
+ }
26165
+ }
26166
+ /** Queue an approved invocation for the delivery layer. */
26167
+ _buffer(instance, payload) {
25467
26168
  const intentId = payload.intentId ?? "";
25468
26169
  instance.pendingEffectorInvocations.push({
25469
26170
  id: intentId,
@@ -25538,6 +26239,38 @@ var effectorController = class {
25538
26239
  function num5(v, fallback) {
25539
26240
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
25540
26241
  }
26242
+ function escalationAsk(schema, reasonCode) {
26243
+ const meaning = ESCALATION_MEANINGS[reasonCode] ?? "I need your approval before I can do this";
26244
+ return `I want to ${schema}, but ${meaning}. May I go ahead?`;
26245
+ }
26246
+ var ESCALATION_MEANINGS = {
26247
+ APPROVAL_REQUIRED: "I need your approval before I can on my own",
26248
+ WRITE_REQUIRES_APPROVAL: "it writes to the world and I shouldn't on my own",
26249
+ PAYMENT_REQUIRES_APPROVAL: "it moves money and I must not do that unattended",
26250
+ DEPLOY_REQUIRES_APPROVAL: "it ships something and needs a human to sign off"
26251
+ };
26252
+ function errMsg2(err) {
26253
+ return err instanceof Error ? err.message : String(err);
26254
+ }
26255
+ function recordToVerdict(record) {
26256
+ return {
26257
+ decision: record.decision,
26258
+ ...record.reasonCode ? { reasonCode: record.reasonCode } : {},
26259
+ ...record.finality ? { finality: record.finality } : {},
26260
+ ...record.counterfactual ? { counterfactual: record.counterfactual } : {}
26261
+ };
26262
+ }
26263
+ function toPolicyInvocation(instance, payload) {
26264
+ return {
26265
+ willId: instance.config.id,
26266
+ intentId: payload.intentId ?? "",
26267
+ schema: payload.schema ?? "",
26268
+ parameters: payload.parameters ?? {},
26269
+ ...typeof payload.targetEntityId === "string" ? { targetEntityId: payload.targetEntityId } : {},
26270
+ ...typeof payload.description === "string" ? { description: payload.description } : {},
26271
+ tick: payload.tick ?? 0
26272
+ };
26273
+ }
25541
26274
 
25542
26275
  // src/stem/tracts/sensory.controller.ts
25543
26276
  var SensoryController = class {
@@ -25941,7 +26674,7 @@ var WillStem = class {
25941
26674
  willId: config.id,
25942
26675
  willName: config.name,
25943
26676
  anatomy: config.anatomy ?? "mind",
25944
- model: config.model ?? null,
26677
+ model: config.llm?.model ?? null,
25945
26678
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
25946
26679
  });
25947
26680
  instance._eventBusUnsub = simulation.eventBus.subscribeAll((event, context) => {
@@ -26340,6 +27073,15 @@ var WillStem = class {
26340
27073
  confirmEffectorExecution(id, invocationId, result) {
26341
27074
  this._effector.confirmExecution(this._get(id), invocationId, result);
26342
27075
  }
27076
+ /**
27077
+ * Resolve a policy escalation the Will raised (POLICY_REAFFERENCE P4).
27078
+ * `approved` dispatches the held invocation to the world; otherwise it is
27079
+ * refused. Applied at the next tick boundary. `invocationId` is the awaiting
27080
+ * `agency.intent` id the escalation ask referenced.
27081
+ */
27082
+ resolveEscalation(id, invocationId, approved) {
27083
+ this._effector.resolveEscalation(this._get(id), invocationId, approved);
27084
+ }
26343
27085
  // ── Messaging / outbox (11.1) ────────────────────────────────────────────
26344
27086
  // Delegates to OutboxController (R5-c). `_get(id)` validates the Will exists
26345
27087
  // and supplies the WillInstance; the outbox ops touch only instance fields.
@@ -26453,7 +27195,7 @@ var WillStem = class {
26453
27195
  createdAt: inst.createdAt,
26454
27196
  lastTickAt: inst.lastTickAt,
26455
27197
  anatomy: inst.config.anatomy ?? "mind",
26456
- model: inst.config.model
27198
+ model: inst.config.llm?.model
26457
27199
  }));
26458
27200
  }
26459
27201
  // ── Tick loop (internal) ───────────────────────────────────
@@ -26470,6 +27212,7 @@ var WillStem = class {
26470
27212
  outbox: this._outbox,
26471
27213
  sensory: this._sensory
26472
27214
  });
27215
+ this._effector.applyPolicyOutcomes(instance);
26473
27216
  await instance.simulation.step(1);
26474
27217
  instance.tickCount++;
26475
27218
  instance.lastTickAt = /* @__PURE__ */ new Date();
@@ -26785,6 +27528,19 @@ var SocketIoTransport = class {
26785
27528
 
26786
27529
  // src/sdk/will.ts
26787
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
+ }
26788
27544
  var Will = class _Will {
26789
27545
  /** The underlying WillStem — drop here for the full contract. */
26790
27546
  stem;
@@ -27008,9 +27764,13 @@ var Will = class _Will {
27008
27764
  }
27009
27765
  // ── Internals ──────────────────────────────────────────────
27010
27766
  _buildConfig(id, opts) {
27011
- const mode = opts.llm ?? (process.env.ANTHROPIC_API_KEY ? "anthropic" : process.env.ZAI_API_KEY ? "glm" : "mock");
27767
+ const mode = opts.llm ?? detectProvider();
27012
27768
  const useMock = mode === "mock";
27013
- 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
+ };
27014
27774
  return {
27015
27775
  id,
27016
27776
  name: opts.name,
@@ -27021,7 +27781,6 @@ var Will = class _Will {
27021
27781
  style: opts.identity.style ?? ""
27022
27782
  },
27023
27783
  anatomy: opts.anatomy ?? "mind",
27024
- model: opts.model,
27025
27784
  llm: llmConfig,
27026
27785
  testMode: useMock,
27027
27786
  persistentMemory: opts.persist ?? false,
@@ -27129,6 +27888,6 @@ function slug(s) {
27129
27888
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "will";
27130
27889
  }
27131
27890
 
27132
- 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 };
27133
27892
  //# sourceMappingURL=index.js.map
27134
27893
  //# sourceMappingURL=index.js.map