@mindot/will 0.4.0 → 0.6.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 (135) hide show
  1. package/README.md +32 -12
  2. package/dist/channels/discord.d.ts +2 -11
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +72 -0
  5. package/dist/channels/whatsapp.js +252 -0
  6. package/dist/channels/whatsapp.js.map +1 -0
  7. package/dist/cli.js +837 -91
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +620 -79
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/mcp/effectors.js.map +1 -1
  14. package/dist/types-E9-HV-SW.d.ts +11 -0
  15. package/dist/{will-D-slky1N.d.ts → will-Bikuk4s2.d.ts} +56 -12
  16. package/package.json +12 -3
  17. package/src/channels/discord.ts +11 -11
  18. package/src/channels/roster.ts +1 -1
  19. package/src/channels/types.ts +1 -1
  20. package/src/channels/whatsapp.ts +318 -0
  21. package/src/cli.ts +46 -23
  22. package/src/cognition/acp.ts +28 -0
  23. package/src/cognition/agency/consequence.ts +174 -0
  24. package/src/cognition/agency/engines/action.selector.ts +301 -48
  25. package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -16
  26. package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
  27. package/src/cognition/agency/engines/motor.schema.executor.ts +86 -14
  28. package/src/cognition/agency/engines/reafference.engine.ts +122 -18
  29. package/src/cognition/agency/execution.primitives.ts +13 -13
  30. package/src/cognition/agency/proactive.communicator.ts +1 -1
  31. package/src/cognition/agency/reconcile.learning.ts +1 -1
  32. package/src/cognition/agency/revocation.ts +79 -0
  33. package/src/cognition/agency/schemas/repertoire.ts +2 -2
  34. package/src/cognition/agency/selection.scoring.ts +25 -9
  35. package/src/cognition/bus.ts +2 -2
  36. package/src/cognition/completion.inbox.ts +1 -1
  37. package/src/cognition/config.mirror.entities.ts +5 -5
  38. package/src/cognition/event.log.ts +2 -2
  39. package/src/cognition/event.schemas.ts +67 -67
  40. package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
  41. package/src/cognition/faculties/affective.blender.ts +36 -4
  42. package/src/cognition/faculties/attachment.evaluator.ts +6 -6
  43. package/src/cognition/faculties/attention.allocator.ts +63 -8
  44. package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
  45. package/src/cognition/faculties/bias.detector.ts +7 -7
  46. package/src/cognition/faculties/circadian.oscillator.ts +3 -3
  47. package/src/cognition/faculties/confidence.calibrator.ts +7 -7
  48. package/src/cognition/faculties/dream.simulator.ts +5 -5
  49. package/src/cognition/faculties/empathy.simulator.ts +7 -7
  50. package/src/cognition/faculties/energy.regulator.ts +4 -4
  51. package/src/cognition/faculties/episodic.consolidator.ts +6 -6
  52. package/src/cognition/faculties/executive.engine/commands.ts +6 -6
  53. package/src/cognition/faculties/executive.engine/context.ts +42 -42
  54. package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
  55. package/src/cognition/faculties/executive.engine/engine.ts +27 -26
  56. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
  57. package/src/cognition/faculties/executive.engine/facet.ts +18 -18
  58. package/src/cognition/faculties/executive.engine/gating.ts +2 -2
  59. package/src/cognition/faculties/executive.engine/messages.ts +3 -3
  60. package/src/cognition/faculties/executive.engine/parser.ts +11 -11
  61. package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
  62. package/src/cognition/faculties/exteroception.ts +99 -5
  63. package/src/cognition/faculties/forgetting.curve.ts +3 -3
  64. package/src/cognition/faculties/frustration.evaluator.ts +6 -6
  65. package/src/cognition/faculties/goal.manager.ts +15 -15
  66. package/src/cognition/faculties/inhibition.controller.ts +3 -3
  67. package/src/cognition/faculties/interoception.ts +3 -3
  68. package/src/cognition/faculties/introspection.engine.ts +3 -3
  69. package/src/cognition/faculties/known.entity.tracker.ts +15 -15
  70. package/src/cognition/faculties/loss.evaluator.ts +2 -2
  71. package/src/cognition/faculties/moral.evaluator.ts +4 -4
  72. package/src/cognition/faculties/novelty.detector.ts +4 -4
  73. package/src/cognition/faculties/persona.consolidator.ts +12 -12
  74. package/src/cognition/faculties/planning.engine/engine.ts +47 -47
  75. package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
  76. package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
  77. package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
  78. package/src/cognition/faculties/reputation.tracker.ts +6 -6
  79. package/src/cognition/faculties/reward.evaluator.ts +4 -4
  80. package/src/cognition/faculties/self.model.updater.ts +8 -8
  81. package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
  83. package/src/cognition/faculties/social.perception.ts +3 -3
  84. package/src/cognition/faculties/spaced.repetition.ts +2 -2
  85. package/src/cognition/faculties/stress.regulator.ts +27 -4
  86. package/src/cognition/faculties/task.switcher.ts +11 -6
  87. package/src/cognition/faculties/theory.of.mind.ts +6 -6
  88. package/src/cognition/faculties/threat.evaluator.ts +3 -3
  89. package/src/cognition/faculties/working.memory.ts +8 -8
  90. package/src/cognition/generative.model.ts +2 -2
  91. package/src/cognition/memory/vector.adapter.ts +5 -5
  92. package/src/cognition/memory/vector.content.ts +4 -4
  93. package/src/cognition/memory/vector.embedder.ts +9 -9
  94. package/src/cognition/persona.prior.ts +1 -1
  95. package/src/cognition/schema.registry.ts +2 -2
  96. package/src/cognition/senses/audition.engine/engine.ts +2 -2
  97. package/src/cognition/senses/base.sense.engine.ts +1 -1
  98. package/src/cognition/utilities/token.tracker.ts +11 -5
  99. package/src/core/abstracts.ts +1 -1
  100. package/src/core/completion.recorder.ts +2 -2
  101. package/src/core/serialization.ts +3 -3
  102. package/src/core/snapshot.manager.ts +1 -1
  103. package/src/core/state.manager.ts +1 -1
  104. package/src/core/utils.ts +1 -1
  105. package/src/host/boot.ts +59 -37
  106. package/src/host/utterances.ts +1 -1
  107. package/src/llm/index.ts +84 -34
  108. package/src/llm/summarizer.ts +4 -4
  109. package/src/llm/wire.contracts.ts +1 -1
  110. package/src/mcp/effectors.ts +10 -10
  111. package/src/mcp/server.ts +11 -11
  112. package/src/pma/eval.ts +1 -1
  113. package/src/pma/index.ts +21 -21
  114. package/src/runners/outreach.runner.ts +3 -3
  115. package/src/runners/social.runner.ts +1 -1
  116. package/src/runners/thin-shim.runner.ts +1 -1
  117. package/src/sdk/will.ts +20 -11
  118. package/src/serve/server.ts +12 -12
  119. package/src/stem/assembly.audit.ts +3 -3
  120. package/src/stem/guards/identity.coherence.ts +13 -12
  121. package/src/stem/guards/identity.guard.ts +22 -22
  122. package/src/stem/index.ts +11 -11
  123. package/src/stem/mind.ts +12 -12
  124. package/src/stem/tracts/biography.writer.ts +3 -3
  125. package/src/stem/tracts/effector.controller.ts +4 -4
  126. package/src/stem/tracts/health.reporter.ts +1 -1
  127. package/src/stem/tracts/outbox.controller.ts +4 -0
  128. package/src/stem/tracts/outbox.writer.ts +4 -4
  129. package/src/stem/tracts/pma.controller.ts +1 -1
  130. package/src/stem/tracts/replay.controller.ts +10 -10
  131. package/src/stem/tracts/sensory.controller.ts +2 -2
  132. package/src/stem/tracts/session.logger.ts +4 -4
  133. package/src/stem/tracts/transport/socketio.transport.ts +7 -7
  134. package/src/stem/tracts/transport/stream.transport.ts +2 -2
  135. package/src/stem/tracts/transport.controller.ts +15 -15
@@ -68,10 +68,10 @@ export class OpenAICompatibleEmbedder implements EmbeddingProvider {
68
68
  this._tokenTracker = config.tokenTracker ?? null
69
69
  }
70
70
 
71
- async embed( content: unknown, fn: string = 'recall' ): Promise<number[]> {
71
+ async embed( content: unknown, fn: string = 'recall'): Promise<number[]> {
72
72
  let response: Response
73
73
  try {
74
- response = await fetch( `${this._apiUrl}/embeddings`, {
74
+ response = await fetch(`${this._apiUrl}/embeddings`, {
75
75
  method: 'POST',
76
76
  headers: {
77
77
  'Content-Type': 'application/json',
@@ -87,13 +87,13 @@ export class OpenAICompatibleEmbedder implements EmbeddingProvider {
87
87
  }
88
88
  catch( err ){
89
89
  // AbortSignal.timeout rejects with a DOMException named 'TimeoutError'.
90
- if( err instanceof Error && err.name === 'TimeoutError' )
91
- throw new Error( `Embedding request timed out after ${this._timeoutMs}ms` )
90
+ if( err instanceof Error && err.name === 'TimeoutError')
91
+ throw new Error(`Embedding request timed out after ${this._timeoutMs}ms`)
92
92
  throw err
93
93
  }
94
94
 
95
95
  if( !response.ok )
96
- throw new Error( `Embedding failed: ${response.status} ${response.statusText}` )
96
+ throw new Error(`Embedding failed: ${response.status} ${response.statusText}`)
97
97
 
98
98
  const data = await response.json() as {
99
99
  data?: Array<{ embedding?: number[] }>
@@ -104,7 +104,7 @@ export class OpenAICompatibleEmbedder implements EmbeddingProvider {
104
104
  // opaque TypeError on an empty/malformed body (FN16).
105
105
  const embedding = data?.data?.[ 0 ]?.embedding
106
106
  if( !Array.isArray( embedding ) || embedding.length === 0 )
107
- throw new Error( `Embedding response was empty or malformed for model ${this.modelName}` )
107
+ throw new Error(`Embedding response was empty or malformed for model ${this.modelName}`)
108
108
 
109
109
  // Meter embedding token usage (input-only — embeddings have no completion).
110
110
  // Recorded under the 'embedding' category so per-Will dashboards can split
@@ -128,7 +128,7 @@ export class OpenAICompatibleEmbedder implements EmbeddingProvider {
128
128
  return embedding
129
129
  }
130
130
 
131
- async embedBatch( contents: unknown[], fn: string = 'index' ): Promise<number[][]> {
131
+ async embedBatch( contents: unknown[], fn: string = 'index'): Promise<number[][]> {
132
132
  // Bounded fan-out: cap concurrent requests at _maxConcurrency instead of
133
133
  // firing all of them at once (FN16), while preserving input order.
134
134
  const results: number[][] = new Array( contents.length )
@@ -170,7 +170,7 @@ export class MockEmbedder implements EmbeddingProvider {
170
170
  this._seed = seed
171
171
  }
172
172
 
173
- async embed( content: unknown, _fn: string = 'recall' ): Promise<number[]> {
173
+ async embed( content: unknown, _fn: string = 'recall'): Promise<number[]> {
174
174
  const str = typeof content === 'string' ? content : JSON.stringify( content )
175
175
  const hash = this._hashString( str )
176
176
  const embedding: number[] = []
@@ -184,7 +184,7 @@ export class MockEmbedder implements EmbeddingProvider {
184
184
  return embedding
185
185
  }
186
186
 
187
- async embedBatch( contents: unknown[], fn: string = 'index' ): Promise<number[][]> {
187
+ async embedBatch( contents: unknown[], fn: string = 'index'): Promise<number[][]> {
188
188
  return Promise.all( contents.map( c => this.embed( c, fn ) ) )
189
189
  }
190
190
 
@@ -77,7 +77,7 @@ export function readEffectiveParams(
77
77
 
78
78
  const out: Record<string, number> = { ...base }
79
79
  for( const [ key, delta ] of Object.entries( prior ) )
80
- if( typeof out[ key ] === 'number' && typeof delta === 'number' )
80
+ if( typeof out[ key ] === 'number' && typeof delta === 'number')
81
81
  out[ key ] = out[ key ] + delta
82
82
 
83
83
  return out
@@ -137,8 +137,8 @@ globalSchemaRegistry.register({
137
137
  version: 1,
138
138
  validate( payload ){
139
139
  if( !isRecord( payload ) ) return 'clock.tick payload must be an object'
140
- if( typeof payload['tick'] !== 'number' ) return 'clock.tick requires numeric tick'
141
- if( typeof payload['delta'] !== 'number' ) return 'clock.tick requires numeric delta'
140
+ if( typeof payload['tick'] !== 'number') return 'clock.tick requires numeric tick'
141
+ if( typeof payload['delta'] !== 'number') return 'clock.tick requires numeric delta'
142
142
 
143
143
  return null
144
144
  }
@@ -474,7 +474,7 @@ export class AuditionEngine extends BaseSenseEngine {
474
474
  attachmentScore: this._getAttachmentScore?.( entityId ) ?? 0,
475
475
  activeGoalText: this._getActiveGoalText?.() ?? [],
476
476
  })
477
- const salience = this._model.observe( `audition.${entityId}`, langEnergy ).salience
477
+ const salience = this._model.observe(`audition.${entityId}`, langEnergy ).salience
478
478
 
479
479
  // ── Percept ────────────────────────────────────────────────
480
480
  const percept: LanguagePercept = {
@@ -717,7 +717,7 @@ export class AuditionEngine extends BaseSenseEngine {
717
717
  goalsToAbandon: output.goalsToAbandon,
718
718
  newBeliefs: output.newBeliefs,
719
719
  knownEntityUpdates: output.knownEntityUpdates,
720
- requiresMasterAttention: ( output.actions ?? [] ).some( a => a.type === 'escalate' ),
720
+ requiresMasterAttention: ( output.actions ?? [] ).some( a => a.type === 'escalate'),
721
721
  }
722
722
  }
723
723
  }
@@ -112,6 +112,6 @@ export abstract class ShellSenseEngine extends BaseSenseEngine {
112
112
  }
113
113
 
114
114
  protected async _perceive( _input: SensoryInput ): Promise<void> {
115
- logger.warn( `[${this.name}] ingest() called — engine is a shell, not yet implemented.` )
115
+ logger.warn(`[${this.name}] ingest() called — engine is a shell, not yet implemented.`)
116
116
  }
117
117
  }
@@ -53,6 +53,12 @@ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
53
53
  'anthropic/claude-haiku-4': { input: 1.00, output: 5.00 },
54
54
  'anthropic/claude-opus-4': { input: 5.00, output: 25.00 },
55
55
 
56
+ // Z.ai (GLM-5 family). `glm-5.2[1m]` is the same model asking for its 1M
57
+ // context window — same rate, so it gets its own row rather than relying on
58
+ // the normalizer (a future long-context tier would price differently).
59
+ 'glm/glm-5.2': { input: 1.40, output: 4.40 },
60
+ 'glm/glm-5.2[1m]': { input: 1.40, output: 4.40 },
61
+
56
62
  // Google
57
63
  'google/gemini-2.0-flash': { input: 0.10, output: 0.40 },
58
64
  'google/gemini-2.0-pro': { input: 1.25, output: 5.00 },
@@ -90,9 +96,9 @@ const CACHE_WRITE_MULT = 1.25
90
96
  */
91
97
  function normalizeModelKey( model: string ): string {
92
98
  let m = model.toLowerCase().trim()
93
- const slash = m.lastIndexOf( '/' )
99
+ const slash = m.lastIndexOf('/')
94
100
  if( slash >= 0 ) m = m.slice( slash + 1 ) // drop "provider/" prefix
95
- return m.replace( /[-@]\d{6,8}$/, '' ) // drop trailing -YYYYMMDD date stamp
101
+ return m.replace( /[-@]\d{6,8}$/, '') // drop trailing -YYYYMMDD date stamp
96
102
  }
97
103
 
98
104
  // Pre-index the pricing table by normalized model name for O(1), date-insensitive
@@ -100,7 +106,7 @@ function normalizeModelKey( model: string ): string {
100
106
  const PRICING_BY_NORM: Record<string, { input: number; output: number }> = ( () => {
101
107
  const out: Record<string, { input: number; output: number }> = {}
102
108
  for( const [ key, price ] of Object.entries( MODEL_PRICING ) ){
103
- if( key === '__default__' ) continue
109
+ if( key === '__default__') continue
104
110
  out[ normalizeModelKey( key ) ] = price
105
111
  }
106
112
  return out
@@ -299,10 +305,10 @@ export class TokenTracker implements SimulationEngine {
299
305
  if( this._ledgerPath ){
300
306
  try {
301
307
  if( !this._ledgerDirReady ){
302
- mkdirSync( this._ledgerPath.slice( 0, this._ledgerPath.lastIndexOf( '/' ) ), { recursive: true } )
308
+ mkdirSync( this._ledgerPath.slice( 0, this._ledgerPath.lastIndexOf('/') ), { recursive: true } )
303
309
  this._ledgerDirReady = true
304
310
  }
305
- appendFileSync( this._ledgerPath, JSON.stringify( record ) + '\n' )
311
+ appendFileSync( this._ledgerPath, JSON.stringify( record ) + '\n')
306
312
  }
307
313
  catch { /* ledger file is best-effort */ }
308
314
  }
@@ -47,7 +47,7 @@ export class BunStorageAdapter implements StorageAdapter {
47
47
  return Bun.file( path ).text()
48
48
 
49
49
  const { readFile } = await import('node:fs/promises')
50
- return readFile( path, 'utf8' )
50
+ return readFile( path, 'utf8')
51
51
  }
52
52
 
53
53
  async readBytes( path: string ): Promise<Uint8Array> {
@@ -97,11 +97,11 @@ export class RecordedCompletionSource implements LLMCompletionSource {
97
97
  nextCompletion( tick: number, systemPrompt: string, userMessage: string ): LLMCompletionRecord {
98
98
  const queue = this._byTick.get( tick )
99
99
  if( !queue || queue.length === 0 )
100
- throw new Error( `replay: no recorded LLM completion for tick ${tick} (recording is incomplete or call timing diverged)` )
100
+ throw new Error(`replay: no recorded LLM completion for tick ${tick} (recording is incomplete or call timing diverged)`)
101
101
 
102
102
  const idx = queue.findIndex( r => r.systemPrompt === systemPrompt && r.userMessage === userMessage )
103
103
  if( idx === -1 )
104
- throw new Error( `replay: LLM prompt diverged at tick ${tick} — no recorded completion matches this call (upstream non-determinism)` )
104
+ throw new Error(`replay: LLM prompt diverged at tick ${tick} — no recorded completion matches this call (upstream non-determinism)`)
105
105
 
106
106
  return queue.splice( idx, 1 )[ 0 ]!
107
107
  }
@@ -222,7 +222,7 @@ export class DefaultSerializer implements Serializer {
222
222
  if( Array.isArray( value ) )
223
223
  return value.map( v => this._encodeSpecial( v ) )
224
224
 
225
- if( typeof value === 'object' ){
225
+ if( typeof value === 'object'){
226
226
  const out: Record<string, unknown> = {}
227
227
  for( const [ k, v ] of Object.entries( value as Record<string, unknown> ) )
228
228
  out[k] = this._encodeSpecial( v )
@@ -233,11 +233,11 @@ export class DefaultSerializer implements Serializer {
233
233
  }
234
234
 
235
235
  private _decodeSpecial( value: unknown ): unknown {
236
- if( value === null || typeof value !== 'object' ) return value
236
+ if( value === null || typeof value !== 'object') return value
237
237
  if( Array.isArray( value ) ) return value.map( v => this._decodeSpecial( v ) )
238
238
 
239
239
  const tag = ( value as Record<string, unknown> ).__ser
240
- if( typeof tag === 'string' ){
240
+ if( typeof tag === 'string'){
241
241
  const payload = ( value as Record<string, unknown> ).__val
242
242
  switch( tag ){
243
243
  case 'undefined': return undefined
@@ -243,7 +243,7 @@ export class SnapshotManager {
243
243
  return this._serializer.deserialize( JSON.stringify( parsed ) )
244
244
  }
245
245
  catch( err ){
246
- logger.warn( `[SnapshotManager] Could not load latest snapshot:`, err )
246
+ logger.warn(`[SnapshotManager] Could not load latest snapshot:`, err )
247
247
  return undefined
248
248
  }
249
249
  }
@@ -14,7 +14,7 @@ import { deepFreeze } from '#core/utils'
14
14
  */
15
15
  function resolveFreezeState(): boolean {
16
16
  const flag = process.env.WILL_FREEZE_STATE
17
- if( flag !== undefined && flag !== '' )
17
+ if( flag !== undefined && flag !== '')
18
18
  return flag !== '0' && flag.toLowerCase() !== 'false'
19
19
  return process.env.NODE_ENV !== 'production'
20
20
  }
package/src/core/utils.ts CHANGED
@@ -52,7 +52,7 @@ export function createPRNG( seed: number ): SeededPRNG {
52
52
  */
53
53
  export function deepFreeze<T>( value: T ): T {
54
54
  // Only objects/arrays/functions are freezable; primitives are immutable already.
55
- if( value === null || typeof value !== 'object' ) return value
55
+ if( value === null || typeof value !== 'object') return value
56
56
  if( Object.isFrozen( value ) ) return value
57
57
 
58
58
  // Freeze the container first so a cyclic reference back to it hits the
package/src/host/boot.ts CHANGED
@@ -12,8 +12,9 @@
12
12
  // WILL_NAME display name (default "Will")
13
13
  // WILL_IDENTITY persona prompt (default a minimal self)
14
14
  // WILL_TIER basic | standard | full (default standard)
15
- // WILL_LLM mock | anthropic (default: auto — anthropic when
16
- // ANTHROPIC_API_KEY is set, else mock)
15
+ // WILL_LLM mock | anthropic | glm (default: auto — anthropic when
16
+ // ANTHROPIC_API_KEY is set, glm when
17
+ // ZAI_API_KEY is, else mock)
17
18
  // WILL_TICK_MS ms per tick (default 1000)
18
19
  // WILL_SEED deterministic seed (testing) (default unseeded/wall-time)
19
20
  // WILL_PMA_PATH PMA artifact path (default ./.will/<name>.pma.json)
@@ -27,6 +28,7 @@ import { setLogger } from '#core/logger'
27
28
  import { Will, type CreateWillOptions } from '#sdk/will'
28
29
  import type { PMASnapshot } from '#pma/index'
29
30
  import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
31
+ import { anthropicWireHeaders, defaultBaseFor, defaultModelFor } from '#llm/index'
30
32
 
31
33
  /**
32
34
  * Route every engine log line to stderr. For `will mcp`, stdout is the MCP
@@ -35,12 +37,29 @@ import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
35
37
  */
36
38
  export function routeLogsToStderr(): void {
37
39
  const err = ( level: string ) => ( msg: string, ...rest: unknown[] ) =>
38
- console.error( `[will:${ level }] ${ msg }`, ...rest )
39
- setLogger( { debug: () => {}, info: err( 'info' ), warn: err( 'warn' ), error: err( 'error' ) } )
40
+ console.error(`[will:${ level }] ${ msg }`, ...rest )
41
+ setLogger( { debug: () => {}, info: err('info'), warn: err('warn'), error: err('error') } )
40
42
  }
41
43
 
42
44
  function slug( s: string ): string {
43
- return s.toLowerCase().replace( /[^a-z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ) || 'will'
45
+ return s.toLowerCase().replace( /[^a-z0-9]+/g, '-').replace( /^-+|-+$/g, '') || 'will'
46
+ }
47
+
48
+ /** The LLM mode the hosts will boot with: an explicit WILL_LLM, else whichever
49
+ * provider's key is present, else the zero-key mock. */
50
+ export function resolveLlmMode(): 'mock' | 'anthropic' | 'glm' {
51
+ const explicit = process.env.WILL_LLM as 'mock' | 'anthropic' | 'glm' | undefined
52
+ if( explicit ) return explicit
53
+ if( process.env.ANTHROPIC_API_KEY ) return 'anthropic'
54
+ if( process.env.ZAI_API_KEY ) return 'glm'
55
+ return 'mock'
56
+ }
57
+
58
+ /** The key for a live mode — the provider-agnostic override first, then the
59
+ * provider's own env. */
60
+ function resolveLlmKey( mode: 'anthropic' | 'glm'): string | undefined {
61
+ return process.env.WILL_LLM_API_KEY
62
+ ?? ( mode === 'glm' ? process.env.ZAI_API_KEY : process.env.ANTHROPIC_API_KEY )
44
63
  }
45
64
 
46
65
  /**
@@ -59,44 +78,47 @@ function slug( s: string ): string {
59
78
  * Skipped for the mock executive and the no-LLM `reflex` anatomy.
60
79
  */
61
80
  async function preflightLLM( anatomy: string ): Promise<void> {
62
- const mode = process.env.WILL_LLM ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : 'mock' )
63
- if( mode === 'mock' || anatomy === 'reflex' ) return
81
+ const mode = resolveLlmMode()
82
+ if( mode === 'mock' || anatomy === 'reflex') return
64
83
 
65
- const key = process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY
84
+ const key = resolveLlmKey( mode )
66
85
  if( !key ){
67
- console.error( '[will] WILL_LLM=anthropic but no ANTHROPIC_API_KEY / WILL_LLM_API_KEY is set.' )
68
- console.error( '[will] The Will would boot, perceive, and never speak. Set a key, or run keyless with WILL_LLM=mock.' )
86
+ const expected = mode === 'glm' ? 'ZAI_API_KEY' : 'ANTHROPIC_API_KEY'
87
+ console.error(`[will] WILL_LLM=${ mode } but no ${ expected } / WILL_LLM_API_KEY is set.`)
88
+ console.error('[will] The Will would boot, perceive, and never speak. Set a key, or run keyless with WILL_LLM=mock.')
69
89
  process.exit( 2 )
70
90
  }
71
91
 
72
92
  // The ping validates key + balance + reachability, which is the failure class
73
93
  // that strands an operator. It uses the pinned model when there is one, so a
74
- // bad model id is caught too; otherwise the cheapest model stands in.
75
- const base = process.env.WILL_LLM_BASE_URL ?? 'https://api.anthropic.com/v1'
76
- const model = process.env.WILL_LLM_MODEL ?? 'claude-haiku-4-5-20251001'
94
+ // bad model id is caught too; otherwise the cheapest model stands in. GLM
95
+ // speaks the same wire at Z.ai's compat endpoint, so one ping serves both.
96
+ const base = process.env.WILL_LLM_BASE_URL ?? defaultBaseFor( mode )
97
+ const model = process.env.WILL_LLM_MODEL
98
+ ?? ( mode === 'glm' ? defaultModelFor('glm') : 'claude-haiku-4-5-20251001')
77
99
  try {
78
- const res = await fetch( `${ base }/messages`, {
100
+ const res = await fetch(`${ base }/messages`, {
79
101
  method: 'POST',
80
- headers: { 'content-type': 'application/json', 'anthropic-version': '2023-06-01', 'x-api-key': key },
102
+ headers: anthropicWireHeaders( mode, key ),
81
103
  body: JSON.stringify( { model, max_tokens: 1, messages: [ { role: 'user', content: 'ping' } ] } ),
82
104
  signal: AbortSignal.timeout( 20_000 ),
83
105
  } )
84
106
  if( res.ok ) return
85
107
 
86
- const detail = ( await res.text().catch( () => '' ) ).slice( 0, 300 )
108
+ const detail = ( await res.text().catch( () => '') ).slice( 0, 300 )
87
109
  const fatal = res.status === 400 || res.status === 401 || res.status === 403
88
110
  if( !fatal ){
89
- console.error( `[will] the executive's LLM answered ${ res.status } on a test call — raising the mind anyway (it retries): ${ detail }` )
111
+ console.error(`[will] the executive's LLM answered ${ res.status } on a test call — raising the mind anyway (it retries): ${ detail }`)
90
112
  return
91
113
  }
92
- console.error( `[will] the executive's LLM refused a test call (${ res.status }) — this Will would boot, perceive, and never speak:` )
93
- console.error( ` ${ detail }` )
94
- console.error( '[will] fix the key / credit / model above, or run keyless with WILL_LLM=mock.' )
114
+ console.error(`[will] the executive's LLM refused a test call (${ res.status }) — this Will would boot, perceive, and never speak:`)
115
+ console.error(` ${ detail }`)
116
+ console.error('[will] fix the key / credit / model above, or run keyless with WILL_LLM=mock.')
95
117
  process.exit( 1 )
96
118
  }
97
119
  catch( e ){
98
- console.error( `[will] could not reach the executive's LLM: ${ ( e as Error ).message }` )
99
- console.error( '[will] the Will would boot and stay silent. Check the network / WILL_LLM_BASE_URL, or run keyless with WILL_LLM=mock.' )
120
+ console.error(`[will] could not reach the executive's LLM: ${ ( e as Error ).message }`)
121
+ console.error('[will] the Will would boot and stay silent. Check the network / WILL_LLM_BASE_URL, or run keyless with WILL_LLM=mock.')
100
122
  process.exit( 1 )
101
123
  }
102
124
  }
@@ -116,8 +138,8 @@ export interface BootedWill {
116
138
  /** Raise the mind from env config — wake from the artifact if one exists. */
117
139
  export async function bootWillFromEnv(): Promise<BootedWill> {
118
140
  const name = process.env.WILL_NAME ?? 'Will'
119
- const pmaPath = resolve( process.env.WILL_PMA_PATH ?? `.will/${ slug( name ) }.pma.json` )
120
- const tickMs = parseInt( process.env.WILL_TICK_MS ?? '1000' )
141
+ const pmaPath = resolve( process.env.WILL_PMA_PATH ?? `.will/${ slug( name ) }.pma.json`)
142
+ const tickMs = parseInt( process.env.WILL_TICK_MS ?? '1000')
121
143
  const anatomy = ( process.env.WILL_ANATOMY as CreateWillOptions['anatomy'] ) ?? 'mind'
122
144
 
123
145
  await preflightLLM( anatomy )
@@ -125,29 +147,29 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
125
147
  const opts: Omit<CreateWillOptions, 'identity'> = {
126
148
  name, anatomy, tickMs,
127
149
  ...( process.env.WILL_LLM_MODEL ? { model: process.env.WILL_LLM_MODEL } : {} ),
128
- ...( process.env.WILL_LLM ? { llm: process.env.WILL_LLM as 'mock' | 'anthropic' } : {} ),
150
+ ...( process.env.WILL_LLM ? { llm: process.env.WILL_LLM as CreateWillOptions['llm'] } : {} ),
129
151
  ...( process.env.WILL_SEED ? { seed: parseInt( process.env.WILL_SEED ) } : {} ),
130
152
  }
131
153
 
132
154
  let will: Will
133
155
  if( existsSync( pmaPath ) ){
134
- const pma = JSON.parse( readFileSync( pmaPath, 'utf8' ) ) as PMASnapshot
156
+ const pma = JSON.parse( readFileSync( pmaPath, 'utf8') ) as PMASnapshot
135
157
  will = await Will.wake( pma, opts )
136
- console.error( `[will] ${ name } woke from ${ pmaPath }` )
158
+ console.error(`[will] ${ name } woke from ${ pmaPath }`)
137
159
  // A woken Will carries its own identity — that is the point of an artifact.
138
160
  // But an operator editing WILL_IDENTITY and seeing nothing change deserves
139
161
  // to know why, rather than concluding the persona layer is broken.
140
162
  if( process.env.WILL_IDENTITY )
141
- console.error( `[will] note: WILL_IDENTITY is ignored — ${ name } woke as itself. Delete ${ pmaPath } to be born fresh from it.` )
163
+ console.error(`[will] note: WILL_IDENTITY is ignored — ${ name } woke as itself. Delete ${ pmaPath } to be born fresh from it.`)
142
164
  }
143
165
  else {
144
166
  will = await Will.create( {
145
167
  ...opts,
146
168
  identity: { prompt: process.env.WILL_IDENTITY ?? `I am ${ name }, a persistent mind.` },
147
169
  } )
148
- console.error( `[will] ${ name } born (no artifact at ${ pmaPath } yet)` )
170
+ console.error(`[will] ${ name } born (no artifact at ${ pmaPath } yet)`)
149
171
  }
150
- will.on( 'error', e => console.error( `[will] error: ${ e.message }` ) )
172
+ will.on('error', e => console.error(`[will] error: ${ e.message }`) )
151
173
 
152
174
  // Onward bridges: MCP servers whose tools become the Will's OWN abilities.
153
175
  // Best-effort — a bad entry warns and is skipped; the mind still boots.
@@ -159,12 +181,12 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
159
181
  try {
160
182
  const { names, close } = await connectMcpEffectors( will, source )
161
183
  cleanups.push( close )
162
- console.error( `[will] ${ name } gained abilities: ${ names.join( ', ' ) }` )
184
+ console.error(`[will] ${ name } gained abilities: ${ names.join(', ') }`)
163
185
  }
164
- catch( e ){ console.error( `[will] MCP bridge failed (skipped): ${ ( e as Error ).message }` ) }
186
+ catch( e ){ console.error(`[will] MCP bridge failed (skipped): ${ ( e as Error ).message }`) }
165
187
  }
166
188
  }
167
- catch( e ){ console.error( `[will] WILL_MCP_SERVERS is not valid JSON — ignoring: ${ ( e as Error ).message }` ) }
189
+ catch( e ){ console.error(`[will] WILL_MCP_SERVERS is not valid JSON — ignoring: ${ ( e as Error ).message }`) }
168
190
  }
169
191
 
170
192
  // Hibernate exactly once on the way out — cleanups (LIFO), distill + stop, persist.
@@ -177,13 +199,13 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
177
199
  const pma = await will.hibernate()
178
200
  mkdirSync( dirname( pmaPath ), { recursive: true } )
179
201
  writeFileSync( pmaPath, JSON.stringify( pma ) )
180
- console.error( `[will] ${ name } hibernated to ${ pmaPath } (${ why })` )
202
+ console.error(`[will] ${ name } hibernated to ${ pmaPath } (${ why })`)
181
203
  }
182
- catch( e ){ console.error( `[will] hibernate failed: ${ ( e as Error ).message }` ) }
204
+ catch( e ){ console.error(`[will] hibernate failed: ${ ( e as Error ).message }`) }
183
205
  process.exit( 0 )
184
206
  }
185
- process.on( 'SIGINT', () => void shutdown( 'SIGINT' ) )
186
- process.on( 'SIGTERM', () => void shutdown( 'SIGTERM' ) )
207
+ process.on('SIGINT', () => void shutdown('SIGINT') )
208
+ process.on('SIGTERM', () => void shutdown('SIGTERM') )
187
209
 
188
210
  return {
189
211
  will, name, pmaPath, tickMs, anatomy,
@@ -20,7 +20,7 @@ export class UtteranceTap {
20
20
 
21
21
  constructor( will: Will ){
22
22
  this._will = will
23
- will.on( 'message', m => {
23
+ will.on('message', m => {
24
24
  this._pending.push( m )
25
25
  if( this._pending.length > BUFFER_CAP ) this._pending.shift()
26
26
  } )