@mindot/will 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +1 -1
  3. package/dist/channels/whatsapp.d.ts +1 -1
  4. package/dist/cli.js +10823 -10454
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +562 -226
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/effectors.d.ts +1 -1
  10. package/dist/{will-DAW0l-lY.d.ts → will-cS6k4uiJ.d.ts} +470 -84
  11. package/package.json +1 -1
  12. package/src/cognition/agency/engines/action.selector.ts +2 -1
  13. package/src/cognition/agency/engines/reafference.engine.ts +12 -2
  14. package/src/cognition/agency/reconcile.learning.ts +16 -2
  15. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  16. package/src/cognition/config.mirror.entities.ts +1 -1
  17. package/src/cognition/faculties/executive.engine/engine.ts +136 -58
  18. package/src/cognition/faculties/executive.engine/facet.ts +10 -2
  19. package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
  20. package/src/cognition/index.ts +4 -0
  21. package/src/cognition/memory/vector.embedder.ts +9 -5
  22. package/src/cognition/utilities/token.tracker.ts +191 -96
  23. package/src/host/boot.ts +78 -22
  24. package/src/index.ts +35 -0
  25. package/src/llm/index.ts +397 -96
  26. package/src/llm/routing.ts +198 -0
  27. package/src/llm/summarizer.ts +5 -1
  28. package/src/runners/thin-shim.runner.ts +18 -6
  29. package/src/sdk/will.ts +82 -16
  30. package/src/stem/guards/identity.coherence.ts +17 -6
  31. package/src/stem/index.ts +3 -3
  32. package/src/stem/mind.ts +155 -24
  33. package/src/stem/policy/arbiter.ts +49 -14
  34. package/src/stem/policy/rule.table.ts +2 -2
  35. package/src/stem/tracts/effector.controller.ts +56 -9
@@ -0,0 +1,198 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/llm/routing.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // MODEL_ROUTING W2 — per-call model selection.
6
+ //
7
+ // A Will runs one LLMDirector, and every call site shares it: the master's
8
+ // decision, each facet, the propose pass, the rolling summariser, the identity
9
+ // guard. They are not the same cognitive act, and they need not be the same
10
+ // inference. This module lets a host decide which model serves which call,
11
+ // without the engine learning anything it should not know.
12
+ //
13
+ // WHAT THIS IS NOT. This is a mechanism, never a policy. The engine may know
14
+ // that a call is routine or consequential — that is a cognitive fact it already
15
+ // computes (`LLMCallMeta.demand`). It must never know who is paying, what plan
16
+ // they are on, or what anything costs us. A router that needs commercial
17
+ // information is the host's to write; the seam below carries none of it.
18
+ //
19
+ // DETERMINISM CONTRACT. A router is an external oracle, exactly like the LLM
20
+ // itself and the policy arbiter. Its choice is already captured: the completion
21
+ // tape records `provider` and `model` on every call, and replay re-feeds the
22
+ // recorded completion rather than re-deciding. So replay never consults a
23
+ // router, and a run replays byte-for-byte whether the router is absent,
24
+ // present, or since reconfigured.
25
+ //
26
+ // SCOPE. A router may read the call meta and its own configuration, and nothing
27
+ // else. It must not reach into simulation state — that is what keeps this
28
+ // module below cognition (src/llm imports no cognition behaviour) and keeps
29
+ // routing from becoming a hidden input to the mind.
30
+ // ─────────────────────────────────────────────────────────────
31
+
32
+ import { logger } from '#core/logger'
33
+ import type { LLMProvider, LLMCallMeta } from '#llm/index'
34
+
35
+ /**
36
+ * Where a single call should go. Every field except `model` falls back to the
37
+ * Will's default when omitted.
38
+ */
39
+ export interface ModelRoute {
40
+ /**
41
+ * Omit to keep the Will's default provider and change only the model — the
42
+ * common "same vendor, different model for this kind of work" route, and what
43
+ * the per-role model map compiles to (a role has never had a provider of its
44
+ * own). Name a provider to cross vendors; it must appear in `llm.providers`
45
+ * or the route falls back to the default.
46
+ */
47
+ provider?: LLMProvider
48
+ model: string
49
+ /** Override the provider's API base (self-hosted / OpenAI-compatible servers). */
50
+ baseUrl?: string
51
+ /** Override the output-token ceiling for this call. */
52
+ maxOutputTokens?: number
53
+ }
54
+
55
+ /**
56
+ * Chooses a model for a call.
57
+ *
58
+ * Returning `null` means "no opinion" — the Will's default model is used. A
59
+ * router should return `null` rather than guess when it does not recognise a
60
+ * call: falling back is always safe, and a wrong route is not.
61
+ */
62
+ export interface ModelRouter {
63
+ /** Stable identifier, recorded alongside routing telemetry. */
64
+ readonly name: string
65
+ route( meta: LLMCallMeta ): ModelRoute | null
66
+ }
67
+
68
+ /**
69
+ * The default. Has no opinion about anything, allocates nothing.
70
+ *
71
+ * A Will running this must be byte-identical to one built before the routing
72
+ * seam existed — that property is asserted by test, and it is what lets this
73
+ * ship dark.
74
+ */
75
+ export const NULL_ROUTER: ModelRouter = {
76
+ name: 'null',
77
+ route(): ModelRoute | null { return null },
78
+ }
79
+
80
+ /** True when the router is the no-op default (used to skip the seam entirely). */
81
+ export function isNullRouter( router: ModelRouter | null | undefined ): boolean {
82
+ return !router || router === NULL_ROUTER
83
+ }
84
+
85
+ // ── Reference implementation ──────────────────────────────────
86
+
87
+ /**
88
+ * One entry in a {@link TableRouter}'s table. All present conditions must match
89
+ * (logical AND); an absent condition matches anything.
90
+ */
91
+ export interface RoutingRule {
92
+ /**
93
+ * Match `LLMCallMeta.category` exactly (e.g. 'executive', 'summarizer').
94
+ *
95
+ * The axes are typed rather than free strings so a rule that names a bucket
96
+ * the engine never emits fails to compile instead of silently never matching
97
+ * — a routing table's worst failure is the rule that looks right and is dead.
98
+ */
99
+ category?: LLMCallMeta['category']
100
+ /** Match `LLMCallMeta.attribute` exactly (e.g. 'master', 'facet', 'guard'). */
101
+ attribute?: LLMCallMeta['attribute']
102
+ /** Match `LLMCallMeta.function` exactly (e.g. 'decision', 'consolidation'). */
103
+ function?: LLMCallMeta['function']
104
+ /**
105
+ * Inclusive lower bound on `LLMCallMeta.demand`. A call with no demand
106
+ * reported never matches a rule that sets this — absent means unknown, and
107
+ * unknown must not be treated as zero.
108
+ */
109
+ minDemand?: number
110
+ /** Exclusive upper bound on `LLMCallMeta.demand`. Same absence rule. */
111
+ maxDemand?: number
112
+ /** Where a matching call goes. */
113
+ route: ModelRoute
114
+ }
115
+
116
+ /**
117
+ * A worked example of the seam: first matching rule wins, otherwise no opinion.
118
+ *
119
+ * This ships so that the interface has a reference implementation and so that
120
+ * hosts have something to copy — it is deliberately dumb. It is not a routing
121
+ * strategy, and the engine ships no table of its own: what belongs where is the
122
+ * host's decision, expressed as configuration.
123
+ *
124
+ * Rules are evaluated in order, so put specific rules before general ones.
125
+ */
126
+ export class TableRouter implements ModelRouter {
127
+ readonly name: string
128
+ private readonly _rules: readonly RoutingRule[]
129
+
130
+ constructor( rules: readonly RoutingRule[], name = 'table' ){
131
+ this._rules = [ ...rules ]
132
+ this.name = name
133
+ }
134
+
135
+ route( meta: LLMCallMeta ): ModelRoute | null {
136
+ for( const rule of this._rules ){
137
+ if( matches( rule, meta ) ) return rule.route
138
+ }
139
+ return null
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Ask each router in turn; the first with an opinion wins.
145
+ *
146
+ * This exists because a Will can have two sources of routing at once: the
147
+ * host's own router, and the one compiled from its per-role model map. Order
148
+ * expresses precedence — an explicit router is consulted before the role map,
149
+ * which is the precedence those two mechanisms already had when roles were
150
+ * served by separate directors.
151
+ *
152
+ * A throwing link is skipped, not propagated. The links are independent
153
+ * decisions, and one broken router must not take a working one down with it —
154
+ * that would silently demote every role-mapped call to the default model.
155
+ */
156
+ export function chainRouters( ...routers: ( ModelRouter | null | undefined )[] ): ModelRouter {
157
+ const chain = routers.filter( ( r ): r is ModelRouter => !isNullRouter( r ) )
158
+ if( chain.length === 0 ) return NULL_ROUTER
159
+ if( chain.length === 1 ) return chain[ 0 ]!
160
+
161
+ const warned = new Set<string>()
162
+ return {
163
+ name: chain.map( r => r.name ).join('>'),
164
+ route( meta: LLMCallMeta ): ModelRoute | null {
165
+ for( const router of chain ){
166
+ try {
167
+ const hit = router.route( meta )
168
+ if( hit ) return hit
169
+ }
170
+ catch( err ){
171
+ if( !warned.has( router.name ) ){
172
+ warned.add( router.name )
173
+ logger.warn(`[llm.routing] router "${router.name}" threw — skipping it: ${( err as Error ).message}`)
174
+ }
175
+ }
176
+ }
177
+ return null
178
+ },
179
+ }
180
+ }
181
+
182
+ function matches( rule: RoutingRule, meta: LLMCallMeta ): boolean {
183
+ if( rule.category !== undefined && rule.category !== meta.category ) return false
184
+ if( rule.attribute !== undefined && rule.attribute !== meta.attribute ) return false
185
+ if( rule.function !== undefined && rule.function !== meta.function ) return false
186
+
187
+ // Absent demand is UNKNOWN, not zero: a demand-bounded rule cannot claim a
188
+ // call whose demand was never measured.
189
+ const bounded = rule.minDemand !== undefined || rule.maxDemand !== undefined
190
+ if( bounded ){
191
+ const d = meta.demand
192
+ if( typeof d !== 'number' || Number.isNaN( d ) ) return false
193
+ if( rule.minDemand !== undefined && d < rule.minDemand ) return false
194
+ if( rule.maxDemand !== undefined && d >= rule.maxDemand ) return false
195
+ }
196
+
197
+ return true
198
+ }
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { logger } from '#core/logger'
19
19
  import type { LLMDirector } from '#llm/index'
20
+ import { BACKGROUND_DEMAND } from '#llm/index'
20
21
 
21
22
  export interface SummarizerConfig {
22
23
  /** How many executive calls between summarization runs. Default: 10 */
@@ -148,7 +149,10 @@ export class ExecutiveSummarizer {
148
149
  userMessage,
149
150
  this._callCount as any,
150
151
  undefined,
151
- { category: 'summarizer', attribute: 'memory', function: 'consolidation' }
152
+ // MODEL_ROUTING W0 compression is background work at a constant low
153
+ // demand: distilling excerpts is the same job whether the mind is calm
154
+ // or in crisis, so there is no honest per-tick measure to forward here.
155
+ { category: 'summarizer', attribute: 'memory', function: 'consolidation', demand: BACKGROUND_DEMAND }
152
156
  )
153
157
 
154
158
  if( result.text ){
@@ -16,6 +16,14 @@
16
16
 
17
17
  import type { WillConfig } from '#stem/mind'
18
18
  import type { StateSnapshot } from '#stem/index'
19
+ import type { TokenTracker } from '#cognition/utilities/token.tracker'
20
+
21
+ /**
22
+ * The Will's token tracker, for cost display. Held here rather than passed in
23
+ * because printStatus is registered as a TickListener, whose third parameter is
24
+ * the outbox. Cost is no longer a state metric (W8c) — it lives on the tracker.
25
+ */
26
+ let _tracker: TokenTracker | undefined
19
27
  import { WillStem } from '#stem/index'
20
28
 
21
29
  // ── Env config ────────────────────────────────────────────────
@@ -88,14 +96,15 @@ function printStatus( snapshot: StateSnapshot, tick: number ): void {
88
96
  ` entities=${entities}`
89
97
  )
90
98
 
91
- // LLM stats every LOG_INTERVAL
92
- const costTotal = ( snapshot.metrics.get('llm.cost_total_usd') ?? 0 ).toFixed( 4 )
93
- const costThisTick = ( snapshot.metrics.get('llm.cost_this_tick_usd') ?? 0 ).toFixed( 5 )
94
- const totalCalls = snapshot.metrics.get('llm.total_calls') ?? 0
99
+ // LLM stats every LOG_INTERVAL. Tokens come from state; cost comes from the
100
+ // tracker dollars are host accounting and no longer live in state (W8c).
101
+ const totalCalls = snapshot.metrics.get('llm.total_calls') ?? 0
102
+ const cost = _tracker
103
+ ? ` total_cost=$${_tracker.totalCostUsd.toFixed( 4 )}`
104
+ : ''
95
105
 
96
106
  console.log(
97
- ` [llm] total_cost=$${costTotal}` +
98
- ` tick_cost=$${costThisTick}` +
107
+ ` [llm]${cost}` +
99
108
  ` calls=${totalCalls}` +
100
109
  ` prompt=${snapshot.metrics.get('llm.prompt_tokens_total') ?? 0}` +
101
110
  ` completion=${snapshot.metrics.get('llm.completion_tokens_total') ?? 0}`
@@ -141,6 +150,9 @@ async function main(): Promise<void> {
141
150
  console.log('[init] Assembling mind...')
142
151
  await manager.createWill( willConfig )
143
152
 
153
+ // Cost is no longer a state metric (W8c) — read it off the Will's tracker.
154
+ _tracker = manager.getWillCognition( WILL_ID ).tokenTracker
155
+
144
156
  manager.addTickListener( WILL_ID, printStatus )
145
157
 
146
158
  // Graceful shutdown
package/src/sdk/will.ts CHANGED
@@ -29,6 +29,7 @@
29
29
 
30
30
  import { WillStem } from '#stem/index'
31
31
  import type { WillConfig, WillIdentity, Anatomy, InitialGoal, WillModelConfig, WillLLMConfig } from '#stem/mind'
32
+ import { PROVIDER_KEY_ENV, providerKeyFromEnv, type LLMProvider } from '#llm/index'
32
33
  import type { PMASnapshot } from '#pma/index'
33
34
  import type { effectorInvocation } from '#types'
34
35
  import type { EffectorDeclaration, SchemaPrecondition } from '#agency/types'
@@ -165,20 +166,42 @@ export interface CreateWillOptions {
165
166
  anatomy?: Anatomy
166
167
  /** Concrete LLM model id, or a per-role map ({ executive, summarizer?,
167
168
  * deliberation?, embedding? } — unset thinking roles fall back to executive).
168
- * Unset → env / provider default. */
169
+ * Unset → env / provider default.
170
+ * @deprecated Pass `llmConfig: { model }` instead — model and transport are
171
+ * one concern. Still honoured; an explicit `llmConfig.model` wins. */
169
172
  model?: string | WillModelConfig
170
- /** Per-Will LLM transport overrides (provider, BYO apiKey, baseUrl, caps).
173
+ /** Per-Will LLM config: provider, model(s), BYO apiKey, baseUrl, caps.
171
174
  * Unset fields fall back to WILL_LLM_* envs. apiKey stays in memory only.
172
- * (Named llmConfig because `llm` is the mock/anthropic MODE switch.) */
175
+ * (Named llmConfig because `llm` is the provider MODE switch.) */
173
176
  llmConfig?: WillLLMConfig
174
177
  /**
175
- * LLM mode. 'mock' (default when no key is present) runs a deterministic
176
- * canned executive — zero keys, zero cost. 'anthropic' calls Claude (needs
177
- * ANTHROPIC_API_KEY / WILL_LLM_* env); 'glm' calls Z.ai's GLM over its
178
- * Anthropic-compatible endpoint (needs ZAI_API_KEY / WILL_LLM_*). Omit to
179
- * auto-detect from whichever key is set.
178
+ * LLM mode which provider the executive speaks to.
179
+ *
180
+ * 'mock' (the default when no key is present) runs a deterministic canned
181
+ * executive: zero keys, zero cost. Every other value names a provider and
182
+ * needs its key, either the provider's own env below or the
183
+ * provider-agnostic WILL_LLM_API_KEY:
184
+ *
185
+ * anthropic ANTHROPIC_API_KEY Claude, native Messages wire
186
+ * glm ZAI_API_KEY Z.ai GLM, Anthropic-compatible wire
187
+ * openai OPENAI_API_KEY OpenAI wire
188
+ * google GOOGLE_API_KEY | GEMINI_API_KEY native Gemini wire
189
+ * deepseek DEEPSEEK_API_KEY OpenAI wire
190
+ * moonshot MOONSHOT_API_KEY Kimi — OpenAI wire
191
+ * qwen DASHSCOPE_API_KEY Alibaba Model Studio — OpenAI wire
192
+ * xai XAI_API_KEY Grok — OpenAI wire
193
+ * minimax MINIMAX_API_KEY OpenAI wire
194
+ * mistral MISTRAL_API_KEY OpenAI wire
195
+ * ollama · vllm local; no key, set `llm` explicitly
196
+ *
197
+ * Any other string works too — it just has to declare its `wire` and
198
+ * `baseUrl` on `llmConfig.providers`. Naming the vendor rather than
199
+ * borrowing `openai` because it speaks that wire is what keeps the
200
+ * completion tape and the cost breakdown honest.
201
+ *
202
+ * Omit to auto-detect from whichever key is set.
180
203
  */
181
- llm?: 'mock' | 'anthropic' | 'glm'
204
+ llm?: 'mock' | LLMProvider
182
205
  /**
183
206
  * Abilities the Will can choose to enact. `name → handler`, or
184
207
  * `name → { handler, description?, cost?, valence?, preconditions? }` to seed
@@ -213,6 +236,41 @@ const AFFECT_EPSILON = 0.02
213
236
 
214
237
  // ── The facade ────────────────────────────────────────────────
215
238
 
239
+
240
+ /**
241
+ * Which provider to talk to when the caller did not say.
242
+ *
243
+ * Checks the provider-agnostic key first (it means "I configured this
244
+ * deliberately"), then each provider's conventional env. The order among
245
+ * providers is detection precedence when several keys happen to be present —
246
+ * it is not a ranking, and no provider here is more supported than another.
247
+ */
248
+ export function detectProvider(): 'mock' | LLMProvider {
249
+ // A provider-agnostic key says nothing about who to send it to. Guessing
250
+ // here is how a key meant for one vendor ends up at another; the guess used
251
+ // to be 'anthropic' unconditionally.
252
+ if( process.env.WILL_LLM_API_KEY ){
253
+ const provider = process.env.WILL_LLM_PROVIDER
254
+ if( !provider )
255
+ throw new Error(
256
+ 'WILL_LLM_API_KEY is set but WILL_LLM_PROVIDER is not — there is no way ' +
257
+ 'to tell which provider that key belongs to. Set WILL_LLM_PROVIDER, or ' +
258
+ 'use a provider-specific key (ANTHROPIC_API_KEY, ZAI_API_KEY, …).'
259
+ )
260
+ return provider as LLMProvider
261
+ }
262
+ // A provider-specific key IS the explicit statement — no guess involved.
263
+ // Order is precedence when several are present, and is append-only: moving an
264
+ // entry silently changes which vendor an existing environment talks to.
265
+ //
266
+ // Read through providerKeyFromEnv rather than raw truthiness, so "is a key
267
+ // set?" and "what is the key?" cannot disagree — a blank `XAI_API_KEY=` would
268
+ // otherwise select xai here and then supply nothing to call it with.
269
+ for( const provider of Object.keys( PROVIDER_KEY_ENV ) )
270
+ if( providerKeyFromEnv( provider ) ) return provider
271
+ return 'mock'
272
+ }
273
+
216
274
  export class Will {
217
275
  /** The underlying WillStem — drop here for the full contract. */
218
276
  readonly stem: WillStem
@@ -462,13 +520,22 @@ export class Will {
462
520
 
463
521
  private _buildConfig( id: string, opts: CreateWillOptions ): WillConfig {
464
522
  // Auto-detect from whichever provider key is present; explicit `llm` wins.
465
- const mode = opts.llm
466
- ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : process.env.ZAI_API_KEY ? 'glm' : 'mock')
523
+ // Order is detection precedence when several keys are set, not a
524
+ // recommendation every provider here is a first-class target.
525
+ const mode = opts.llm ?? detectProvider()
467
526
  const useMock = mode === 'mock'
468
- // `llm: 'glm'` selects the provider; an explicit llmConfig still overrides it.
469
- const llmConfig = mode === 'glm'
470
- ? { provider: 'glm' as const, ...opts.llmConfig }
471
- : opts.llmConfig
527
+ // `llm` selects the provider; an explicit llmConfig.provider still wins.
528
+ // Model rides with the transport: `llmConfig.model` is canonical, the
529
+ // top-level `opts.model` is the deprecated spelling of the same thing.
530
+ const llmConfig: WillLLMConfig | undefined = useMock && !opts.llmConfig && opts.model === undefined
531
+ ? undefined
532
+ : {
533
+ ...( mode !== 'mock' ? { provider: mode } : {} ),
534
+ ...opts.llmConfig,
535
+ ...( opts.llmConfig?.model !== undefined ? { model: opts.llmConfig.model }
536
+ : opts.model !== undefined ? { model: opts.model }
537
+ : {} ),
538
+ }
472
539
  return {
473
540
  id, name: opts.name,
474
541
  identity: {
@@ -478,7 +545,6 @@ export class Will {
478
545
  style: opts.identity.style ?? '',
479
546
  },
480
547
  anatomy: opts.anatomy ?? 'mind',
481
- model: opts.model,
482
548
  llm: llmConfig,
483
549
  testMode: useMock,
484
550
  persistentMemory: opts.persist ?? false,
@@ -18,7 +18,7 @@
18
18
  // ─────────────────────────────────────────────────────────────
19
19
 
20
20
  import type { WillIdentity } from '#stem/mind'
21
- import { LLMDirector, defaultModelFor, type LLMProvider, type LLMCallMeta } from '#llm/index'
21
+ import { LLMDirector, BACKGROUND_DEMAND, type LLMProvider, type LLMCallMeta } from '#llm/index'
22
22
  import type { TokenTracker } from '#cognition/utilities/token.tracker'
23
23
 
24
24
  export interface CoherenceIssue {
@@ -48,7 +48,10 @@ export interface IdentityReviewer {
48
48
  }
49
49
 
50
50
  /** Attribution tag for the one creation-time coherence-review call. */
51
- const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', function: 'identity-coherence' }
51
+ // MODEL_ROUTING W0 a one-shot classification at creation time; constant low
52
+ // demand (structurally background, and it runs before there is a mind whose
53
+ // state could modulate it).
54
+ const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
52
55
 
53
56
  const VALID_KINDS = new Set<CoherenceIssue['kind']>( [ 'contradiction', 'false-capability', 'injection', 'incoherence', 'other' ] )
54
57
 
@@ -112,13 +115,21 @@ export async function reviewIdentityCoherence(
112
115
  input: CoherenceInput,
113
116
  opts: { willId?: string; tokenTracker?: TokenTracker | null } = {},
114
117
  ): Promise<CoherenceResult> {
115
- const provider = ( process.env.WILL_LLM_PROVIDER ?? 'anthropic') as LLMProvider
118
+ // No defaults: an unconfigured environment cannot review a persona, and
119
+ // silently reviewing it with somebody else's model is worse than not running.
120
+ const provider = process.env.WILL_LLM_PROVIDER
121
+ const model = process.env.WILL_LLM_MODEL
122
+ if( !provider || !model )
123
+ return { ok: true, ran: false, issues: [], raw: 'review skipped: WILL_LLM_PROVIDER / WILL_LLM_MODEL not set' }
124
+
116
125
  const director = new LLMDirector({
117
126
  willId: opts.willId ?? 'identity-coherence',
118
- model: process.env.WILL_LLM_MODEL ?? defaultModelFor( provider ),
127
+ model,
119
128
  maxOutputTokens: 512,
120
- apiKey: process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? '',
121
- provider,
129
+ // Provider-agnostic key only — the old chain ended at ANTHROPIC_API_KEY,
130
+ // so a Will pointed at another vendor would have sent it an Anthropic key.
131
+ apiKey: process.env.WILL_LLM_API_KEY ?? '',
132
+ provider: provider as LLMProvider,
122
133
  sessionLogger: null,
123
134
  baseUrl: process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
124
135
  // When a per-Will tracker is supplied, the creation-time review records under
package/src/stem/index.ts CHANGED
@@ -95,7 +95,7 @@ export interface WillSummary {
95
95
  createdAt: Date
96
96
  lastTickAt: Date | null
97
97
  anatomy: WillConfig['anatomy']
98
- model: WillConfig['model']
98
+ model: NonNullable<WillConfig['llm']>['model']
99
99
  }
100
100
 
101
101
  // Re-export WillConfig so the API layer only imports from manager
@@ -289,7 +289,7 @@ export class WillStem {
289
289
  willId: config.id,
290
290
  willName: config.name,
291
291
  anatomy: config.anatomy ?? 'mind',
292
- model: config.model ?? null,
292
+ model: config.llm?.model ?? null,
293
293
  startedAt: new Date().toISOString(),
294
294
  })
295
295
 
@@ -942,7 +942,7 @@ export class WillStem {
942
942
  createdAt: inst.createdAt,
943
943
  lastTickAt: inst.lastTickAt,
944
944
  anatomy: inst.config.anatomy ?? 'mind',
945
- model: inst.config.model,
945
+ model: inst.config.llm?.model,
946
946
  }))
947
947
  }
948
948