@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.
- package/README.md +87 -22
- package/dist/channels/discord.d.ts +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/cli.js +10823 -10454
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +562 -226
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-cS6k4uiJ.d.ts} +470 -84
- package/package.json +1 -1
- package/src/cognition/agency/engines/action.selector.ts +2 -1
- package/src/cognition/agency/engines/reafference.engine.ts +12 -2
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/config.mirror.entities.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +136 -58
- package/src/cognition/faculties/executive.engine/facet.ts +10 -2
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
- package/src/cognition/index.ts +4 -0
- package/src/cognition/memory/vector.embedder.ts +9 -5
- package/src/cognition/utilities/token.tracker.ts +191 -96
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +397 -96
- package/src/llm/routing.ts +198 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +82 -16
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +3 -3
- package/src/stem/mind.ts +155 -24
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- 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
|
+
}
|
package/src/llm/summarizer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
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]
|
|
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
|
|
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
|
|
175
|
+
* (Named llmConfig because `llm` is the provider MODE switch.) */
|
|
173
176
|
llmConfig?: WillLLMConfig
|
|
174
177
|
/**
|
|
175
|
-
* LLM mode
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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' |
|
|
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
|
-
|
|
466
|
-
|
|
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
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
+
model,
|
|
119
128
|
maxOutputTokens: 512,
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|