@mindot/will 0.1.1 → 0.2.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 (36) hide show
  1. package/README.md +38 -3
  2. package/dist/index.d.ts +52 -7760
  3. package/dist/index.js +374 -86
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/cli.d.ts +1 -0
  6. package/dist/mcp/cli.js +26048 -0
  7. package/dist/mcp/cli.js.map +1 -0
  8. package/dist/mcp/effectors.d.ts +55 -0
  9. package/dist/mcp/effectors.js +76 -0
  10. package/dist/mcp/effectors.js.map +1 -0
  11. package/dist/will-B5eKs3Wv.d.ts +7903 -0
  12. package/package.json +38 -30
  13. package/src/cognition/agency/engines/action.selector.ts +3 -0
  14. package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -12
  15. package/src/cognition/agency/engines/deliberation.engine.ts +7 -2
  16. package/src/cognition/agency/engines/motor.schema.executor.ts +2 -0
  17. package/src/cognition/agency/schemas/external.ts +27 -10
  18. package/src/cognition/agency/schemas/repertoire.ts +12 -0
  19. package/src/cognition/agency/types.ts +51 -2
  20. package/src/cognition/faculties/executive.engine/commands.ts +47 -11
  21. package/src/cognition/faculties/executive.engine/context.ts +28 -0
  22. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +15 -1
  23. package/src/cognition/faculties/executive.engine/facet.ts +32 -6
  24. package/src/cognition/faculties/executive.engine/prompt.factory.ts +11 -1
  25. package/src/cognition/faculties/executive.engine/types.ts +24 -2
  26. package/src/cognition/index.ts +1 -1
  27. package/src/core/abstracts.ts +39 -12
  28. package/src/index.ts +6 -0
  29. package/src/mcp/cli.ts +129 -0
  30. package/src/mcp/effectors.ts +159 -0
  31. package/src/mcp/server.ts +167 -0
  32. package/src/sdk/will.ts +269 -44
  33. package/src/stem/index.ts +16 -0
  34. package/src/stem/mind.ts +11 -4
  35. package/src/stem/tracts/effector.controller.ts +1 -0
  36. package/src/types.ts +2 -0
package/src/sdk/will.ts CHANGED
@@ -9,8 +9,14 @@
9
9
  // const will = await Will.create({ name: 'Aria', identity: {...} })
10
10
  // will.on('message', m => console.log(m.content))
11
11
  // will.effector('search_docs', async a => await myDb.search(a.query))
12
- // await will.say('What should we work on today?')
13
- // const pma = await will.hibernate() // Will.wake(pma) later
12
+ // await will.perceive({ from: 'ada', text: 'What should we work on?' })
13
+ // const reply = await will.nextUtterance({ to: 'ada' }) // WillMessage | null
14
+ // const pma = await will.save() // non-destructive; keeps ticking
15
+ //
16
+ // A Will is a *subject*, not a function: you `perceive` stimuli to it and observe
17
+ // its *projections* (message / effector / emotion / state) — you never `await`
18
+ // a computed return. `nextUtterance` is a thin, honest adapter for callers that
19
+ // want a reply-shaped await; `null` means the Will chose silence, not an error.
14
20
  //
15
21
  // Everything under here already existed; the facade hides the plumbing:
16
22
  // • one message → outbox drain → delivery-ack, surfaced as an event;
@@ -25,9 +31,27 @@ import { WillStem } from '#stem/index'
25
31
  import type { WillConfig, WillIdentity, EngineTier, ModelTier, InitialGoal } from '#stem/mind'
26
32
  import type { PMASnapshot } from '#pma/index'
27
33
  import type { effectorInvocation } from '#types'
34
+ import type { EffectorDeclaration, SchemaPrecondition } from '#agency/types'
28
35
 
29
36
  // ── Public surface ────────────────────────────────────────────
30
37
 
38
+ /**
39
+ * A stimulus entering the Will's sensory field. A Will is a subject, not a
40
+ * function: you don't *call* it with input and await a return — you `perceive`
41
+ * something to it, and it *may* project a response later (see `nextUtterance`),
42
+ * coloured by its current state. Silence is a valid, meaningful outcome.
43
+ */
44
+ export interface Stimulus {
45
+ /** What was said / observed. */
46
+ text: string
47
+ /** Who it's from (entity id). Default 'user'. */
48
+ from?: string
49
+ /** Display name of the speaker. Default 'You' for `user`, else the `from` id. */
50
+ speaker?: string
51
+ /** Conversation/thread id (default = `from`). */
52
+ thread?: string
53
+ }
54
+
31
55
  /** A message the Will emitted to someone. */
32
56
  export interface WillMessage {
33
57
  /** Message id (stable — dedupe on it). */
@@ -38,6 +62,28 @@ export interface WillMessage {
38
62
  to: string
39
63
  }
40
64
 
65
+ /**
66
+ * A motor act the Will *chose* to enact — a projection of its agency, surfaced
67
+ * whether or not you registered a handler for it. (When you did, the handler
68
+ * still runs and its outcome feeds reafference.)
69
+ */
70
+ export interface WillEffectorAct {
71
+ /** The effector the Will selected. */
72
+ name: string
73
+ /** The arguments it bound. */
74
+ args: Record<string, unknown>
75
+ /** Its stated reason for the act. */
76
+ reasoning: string
77
+ /** Bound target entity, when the act binds one. */
78
+ to?: string
79
+ }
80
+
81
+ /** The Will's affect, projected when it shifts. Valence/arousal ∈ −1..1. */
82
+ export interface WillAffect {
83
+ valence: number
84
+ arousal: number
85
+ }
86
+
41
87
  /**
42
88
  * The result of an effector your handler ran. Return a bare string as shorthand
43
89
  * for `{ success: true, description }`. `metrics` optionally writes world state
@@ -52,9 +98,45 @@ export type EffectorResult = string | {
52
98
  /** Your implementation of an ability the Will can choose to use. */
53
99
  export type EffectorHandler = (
54
100
  args: Record<string, unknown>,
55
- ctx: { reasoning: string; targetEntityId?: string },
101
+ ctx: { reasoning: string; targetEntityId?: string; description?: string },
56
102
  ) => EffectorResult | Promise<EffectorResult>
57
103
 
104
+ /**
105
+ * A richer effector declaration — the ability seeded as a *learnable affordance*.
106
+ * `description` is its meaning (carried to perception + your handler); `cost`,
107
+ * `valence`, and `preconditions` are the intrinsic priors the mind starts from,
108
+ * refined by reafference through use. Args still bind from the situation — this
109
+ * is not a tool-call parameter form. Declare rich effectors in `create()`'s
110
+ * `effectors` map (that is where they enter the affordance repertoire).
111
+ */
112
+ export interface EffectorSpec {
113
+ /** What the ability is for. */
114
+ description?: string
115
+ /** Intrinsic effort / energy demand 0..1 (default 0.15). */
116
+ cost?: number
117
+ /** Intrinsic reward prior −1..1 the mind expects before learning (default 0). */
118
+ valence?: number
119
+ /** Body-state gates; the ability is unavailable unless all pass. */
120
+ preconditions?: SchemaPrecondition[]
121
+ /**
122
+ * Whether the ability targets a specific perceived target (default 'none').
123
+ * 'entity' directs it at a known person, 'object' at a known thing; the bound
124
+ * target arrives as `ctx.targetEntityId`.
125
+ */
126
+ binds?: 'none' | 'entity' | 'object'
127
+ /**
128
+ * Routing tags (merged with 'external'/'host'). A drive-recognised tag (e.g.
129
+ * 'social', 'nourishment') lets a homeostatic drive lift this ability when it
130
+ * presses.
131
+ */
132
+ tags?: string[]
133
+ /** Your implementation. */
134
+ handler: EffectorHandler
135
+ }
136
+
137
+ /** An effectors-map value: a bare handler, or a spec carrying meaning + priors. */
138
+ export type EffectorEntry = EffectorHandler | EffectorSpec
139
+
58
140
  /** A compact read of the mind's current inner state. */
59
141
  export interface WillStateSummary {
60
142
  tick: number
@@ -84,8 +166,13 @@ export interface CreateWillOptions {
84
166
  * (needs ANTHROPIC_API_KEY / WILL_LLM_* env). Omit to auto-detect.
85
167
  */
86
168
  llm?: 'mock' | 'anthropic'
87
- /** Abilities the Will can choose to enact. name → your handler. */
88
- effectors?: Record<string, EffectorHandler>
169
+ /**
170
+ * Abilities the Will can choose to enact. `name → handler`, or
171
+ * `name → { handler, description?, cost?, valence?, preconditions? }` to seed
172
+ * the ability with meaning + intrinsic priors (see EffectorSpec). Declared
173
+ * here (create time) so they enter the affordance repertoire.
174
+ */
175
+ effectors?: Record<string, EffectorEntry>
89
176
  /** Goals seeded before the first tick. Usually leave empty — the Will forms its own. */
90
177
  initialGoals?: InitialGoal[]
91
178
  /** Persist snapshots to disk across restarts (default false). */
@@ -98,7 +185,18 @@ export interface CreateWillOptions {
98
185
  id?: string
99
186
  }
100
187
 
101
- type WillEvent = 'message' | 'state' | 'error'
188
+ type WillEvent = 'message' | 'state' | 'effector' | 'emotion' | 'error'
189
+
190
+ /** A caller awaiting the Will's next spontaneous utterance (see nextUtterance). */
191
+ interface UtteranceWaiter {
192
+ /** Only resolve on an utterance addressed to this entity, when set. */
193
+ to?: string
194
+ resolve: ( m: WillMessage | null ) => void
195
+ timer: ReturnType<typeof setTimeout>
196
+ }
197
+
198
+ /** Affect must move at least this far (−1..1) before another `emotion` projection. */
199
+ const AFFECT_EPSILON = 0.02
102
200
 
103
201
  // ── The facade ────────────────────────────────────────────────
104
202
 
@@ -110,9 +208,15 @@ export class Will {
110
208
  readonly name: string
111
209
 
112
210
  private readonly _effectors = new Map<string, EffectorHandler>()
113
- private readonly _messageHandlers = new Set<( m: WillMessage ) => void>()
114
- private readonly _stateHandlers = new Set<( s: WillStateSummary ) => void>()
115
- private readonly _errorHandlers = new Set<( e: Error ) => void>()
211
+ /** Rich declarations for create-time effectors seed the affordance repertoire. */
212
+ private readonly _effectorDecls = new Map<string, EffectorDeclaration>()
213
+ private readonly _messageHandlers = new Set<( m: WillMessage ) => void>()
214
+ private readonly _stateHandlers = new Set<( s: WillStateSummary ) => void>()
215
+ private readonly _effectorHandlers = new Set<( a: WillEffectorAct ) => void>()
216
+ private readonly _emotionHandlers = new Set<( a: WillAffect ) => void>()
217
+ private readonly _errorHandlers = new Set<( e: Error ) => void>()
218
+ private readonly _utteranceWaiters = new Set<UtteranceWaiter>()
219
+ private _lastAffect: WillAffect | null = null
116
220
  private _unsub: ( () => void ) | null = null
117
221
 
118
222
  private constructor( stem: WillStem, id: string, name: string ){
@@ -127,8 +231,8 @@ export class Will {
127
231
  const stem = new WillStem()
128
232
  const will = new Will( stem, id, opts.name )
129
233
 
130
- for( const [ name, handler ] of Object.entries( opts.effectors ?? {} ) )
131
- will._effectors.set( name, handler )
234
+ for( const [ name, entry ] of Object.entries( opts.effectors ?? {} ) )
235
+ will._register( name, entry )
132
236
 
133
237
  await stem.createWill( will._buildConfig( id, opts ) )
134
238
  will._attach()
@@ -148,8 +252,8 @@ export class Will {
148
252
  const stem = new WillStem()
149
253
  const will = new Will( stem, id, opts.name )
150
254
 
151
- for( const [ name, handler ] of Object.entries( opts.effectors ?? {} ) )
152
- will._effectors.set( name, handler )
255
+ for( const [ name, entry ] of Object.entries( opts.effectors ?? {} ) )
256
+ will._register( name, entry )
153
257
 
154
258
  await stem.createWill(
155
259
  will._buildConfig( id, { ...opts, identity: { prompt: '', ...opts.identity } } ),
@@ -161,38 +265,107 @@ export class Will {
161
265
  return will
162
266
  }
163
267
 
164
- // ── Talking ────────────────────────────────────────────────
268
+ // ── Perceiving ─────────────────────────────────────────────
165
269
 
166
270
  /**
167
- * Speak to the Will as the default user. The reply is asynchronous it
168
- * arrives on the `message` event once the Will has reasoned about it.
271
+ * Deliver a stimulus into the Will's sensory field. This is the one true
272
+ * intake `say`/`tell` are sugar over it. It returns once the stimulus is
273
+ * *delivered*, NOT once the Will has responded: a response (if any) is a
274
+ * projection that arrives later on the `message` event, or via
275
+ * `nextUtterance()`. The Will may also stay silent — that is not an error.
169
276
  */
277
+ async perceive( stimulus: Stimulus ): Promise<void> {
278
+ const from = stimulus.from ?? 'user'
279
+ await this.stem.ingestText( this.id, {
280
+ kind: 'text',
281
+ entityId: from,
282
+ threadId: stimulus.thread ?? from,
283
+ content: stimulus.text,
284
+ speakerName: stimulus.speaker ?? ( from === 'user' ? 'You' : from ),
285
+ } )
286
+ }
287
+
288
+ /** Perceive from the default user. Sugar over `perceive`. */
170
289
  async say( text: string ): Promise<void> {
171
- return this.tell( 'user', 'You', text )
290
+ return this.perceive( { text, from: 'user', speaker: 'You' } )
172
291
  }
173
292
 
174
- /** Speak as a specific interlocutor (multi-party conversations). */
293
+ /** Perceive from a specific interlocutor (multi-party). Sugar over `perceive`. */
175
294
  async tell( entityId: string, speakerName: string, text: string ): Promise<void> {
176
- await this.stem.ingestText( this.id, {
177
- kind: 'text', entityId, threadId: entityId, content: text, speakerName,
295
+ return this.perceive( { text, from: entityId, speaker: speakerName } )
296
+ }
297
+
298
+ /**
299
+ * Await the Will's *next spontaneous utterance* — a thin, honest adapter over
300
+ * the `message` projection stream for request/response callers. Resolves with
301
+ * the message, or `null` if the Will stays silent within `within` ms (default
302
+ * 5000). `null` is a real outcome — the Will chose not to speak — not a
303
+ * failure. Pass `to` to only accept an utterance addressed to that entity.
304
+ *
305
+ * await will.perceive( { from: 'ada', text: 'Hi!' } )
306
+ * const reply = await will.nextUtterance( { to: 'ada', within: 3000 } )
307
+ * // reply is a WillMessage, or null if Ada got the silent treatment.
308
+ */
309
+ nextUtterance( opts: { within?: number; to?: string } = {} ): Promise<WillMessage | null> {
310
+ return new Promise<WillMessage | null>( resolve => {
311
+ const timer = setTimeout( () => {
312
+ this._utteranceWaiters.delete( waiter )
313
+ resolve( null )
314
+ }, opts.within ?? 5000 )
315
+ // Don't let a pending wait keep the host process alive.
316
+ ;( timer as { unref?: () => void } ).unref?.()
317
+ const waiter: UtteranceWaiter = { to: opts.to, resolve, timer }
318
+ this._utteranceWaiters.add( waiter )
178
319
  } )
179
320
  }
180
321
 
181
322
  // ── Abilities ──────────────────────────────────────────────
182
323
 
183
324
  /**
184
- * Register an ability the Will can choose to enact. When the Will decides to
185
- * use `name`, your handler runs with the arguments it chose; the return value
186
- * is fed back as the outcome (closing the reafference loop that lets the Will
187
- * learn the ability). Registering makes the effector available immediately.
325
+ * Register an ability the Will can choose to enact, at runtime. `entry` is a
326
+ * bare handler or a full spec (`{ handler, description?, cost?, valence?,
327
+ * preconditions?, binds?, tags? }`). When the Will decides to use `name`, your
328
+ * handler runs with the arguments it chose; the return value feeds back as the
329
+ * outcome (the reafference loop that lets the Will learn the ability).
330
+ *
331
+ * The ability's schema is added to the live repertoire so it can actually be
332
+ * *afforded* immediately (a grant alone only gates), then granted. Note: this
333
+ * is a runtime mutation — the deterministic/replayable path is declaring
334
+ * effectors in `create()`'s `effectors` map.
188
335
  */
189
- effector( name: string, handler: EffectorHandler ): this {
190
- this._effectors.set( name, handler )
191
- // Communication effectors + every registered custom name.
336
+ effector( name: string, entry: EffectorEntry ): this {
337
+ this._register( name, entry )
338
+ // Add its schema to the live repertoire (so it can be perceived + enacted),
339
+ // then grant it. Comms names are no-ops in registerEffector (grant-governed).
340
+ this.stem.registerEffector( this.id, this._effectorDecls.get( name )! )
192
341
  this.stem.setAllowedEffectors( this.id, [ ...COMMUNICATION, ...this._effectors.keys() ] )
193
342
  return this
194
343
  }
195
344
 
345
+ /** Split an effectors-map entry into a handler + an EffectorDeclaration. */
346
+ private _register( name: string, entry: EffectorEntry ): void {
347
+ if( typeof entry === 'function' ){
348
+ this._effectors.set( name, entry )
349
+ this._effectorDecls.set( name, name ) // bare name — uniform prior
350
+ return
351
+ }
352
+ this._effectors.set( name, entry.handler )
353
+ const hasMeta = entry.description !== undefined || entry.cost !== undefined
354
+ || entry.valence !== undefined || entry.preconditions !== undefined
355
+ || entry.binds !== undefined || entry.tags !== undefined
356
+ this._effectorDecls.set( name, hasMeta
357
+ ? {
358
+ name,
359
+ ...( entry.description !== undefined ? { description: entry.description } : {} ),
360
+ ...( entry.cost !== undefined ? { cost: entry.cost } : {} ),
361
+ ...( entry.valence !== undefined ? { valence: entry.valence } : {} ),
362
+ ...( entry.preconditions !== undefined ? { preconditions: entry.preconditions } : {} ),
363
+ ...( entry.binds !== undefined ? { binds: entry.binds } : {} ),
364
+ ...( entry.tags !== undefined ? { tags: entry.tags } : {} ),
365
+ }
366
+ : name )
367
+ }
368
+
196
369
  // ── Introspection ──────────────────────────────────────────
197
370
 
198
371
  /** A compact snapshot of the mind's current inner state. */
@@ -215,13 +388,19 @@ export class Will {
215
388
 
216
389
  // ── Events ─────────────────────────────────────────────────
217
390
 
218
- on( event: 'message', handler: ( m: WillMessage ) => void ): this
219
- on( event: 'state', handler: ( s: WillStateSummary ) => void ): this
220
- on( event: 'error', handler: ( e: Error ) => void ): this
391
+ on( event: 'message', handler: ( m: WillMessage ) => void ): this
392
+ on( event: 'state', handler: ( s: WillStateSummary ) => void ): this
393
+ on( event: 'effector', handler: ( a: WillEffectorAct ) => void ): this
394
+ on( event: 'emotion', handler: ( a: WillAffect ) => void ): this
395
+ on( event: 'error', handler: ( e: Error ) => void ): this
221
396
  on( event: WillEvent, handler: ( arg: never ) => void ): this {
222
- if( event === 'message' ) this._messageHandlers.add( handler as ( m: WillMessage ) => void )
223
- else if( event === 'state' ) this._stateHandlers.add( handler as ( s: WillStateSummary ) => void )
224
- else this._errorHandlers.add( handler as ( e: Error ) => void )
397
+ switch( event ){
398
+ case 'message': this._messageHandlers.add( handler as ( m: WillMessage ) => void ); break
399
+ case 'state': this._stateHandlers.add( handler as ( s: WillStateSummary ) => void ); break
400
+ case 'effector': this._effectorHandlers.add( handler as ( a: WillEffectorAct ) => void ); break
401
+ case 'emotion': this._emotionHandlers.add( handler as ( a: WillAffect ) => void ); break
402
+ default: this._errorHandlers.add( handler as ( e: Error ) => void )
403
+ }
225
404
  return this
226
405
  }
227
406
 
@@ -231,9 +410,19 @@ export class Will {
231
410
  resume(): void { this.stem.resumeWill( this.id ) }
232
411
 
233
412
  /**
234
- * Distil the mind into a portable PMA artifact and archive it. The returned
235
- * snapshot restores the same self via `Will.wake()` across a restart, a
236
- * fork, or a machine boundary.
413
+ * Checkpoint the living mind into a portable PMA artifact NON-destructive.
414
+ * The Will keeps ticking; the snapshot is a point-in-time copy you can archive
415
+ * or wake elsewhere. Use this for periodic saves; use `hibernate()` to sleep.
416
+ */
417
+ async save(): Promise<PMASnapshot> {
418
+ return this.stem.distillPMA( this.id )
419
+ }
420
+
421
+ /**
422
+ * Distil the mind into a portable PMA artifact and archive it — DESTRUCTIVE:
423
+ * the tick loop stops (the Will sleeps). The returned snapshot restores the
424
+ * same self via `Will.wake()` — across a restart, a fork, or a machine
425
+ * boundary. For a copy that leaves the Will running, use `save()`.
237
426
  */
238
427
  async hibernate(): Promise<PMASnapshot> {
239
428
  const pma = this.stem.distillPMA( this.id )
@@ -243,6 +432,9 @@ export class Will {
243
432
 
244
433
  /** Tear the Will down (its tick loop stops; state is discarded unless persisted). */
245
434
  async stop(): Promise<void> {
435
+ // Resolve anyone awaiting an utterance — the Will won't speak again.
436
+ for( const w of this._utteranceWaiters ){ clearTimeout( w.timer ); w.resolve( null ) }
437
+ this._utteranceWaiters.clear()
246
438
  this._unsub?.()
247
439
  this._unsub = null
248
440
  await this.stem.archiveWill( this.id )
@@ -266,7 +458,7 @@ export class Will {
266
458
  persistentMemory: opts.persist ?? false,
267
459
  snapshotInterval: 100,
268
460
  tickIntervalMs: opts.tickMs ?? 1000,
269
- allowedGenericEffectors: [ ...COMMUNICATION, ...this._effectors.keys() ],
461
+ allowedGenericEffectors: [ ...COMMUNICATION, ...this._effectorDecls.values() ],
270
462
  initialGoals: opts.initialGoals ?? [],
271
463
  ...( opts.seed !== undefined ? { randomSeed: opts.seed, clock: { fixedDeltaMs: 1000, startTime: 0 } } : {} ),
272
464
  }
@@ -280,14 +472,22 @@ export class Will {
280
472
  this._emitMessage( { id: msg.id, content: msg.content, to: msg.targetEntityId } )
281
473
  try { this.stem.confirmMessageDelivery( this.id, msg.id, true ) } catch { /* best-effort */ }
282
474
  }
283
- // Effector invocations → run the handler → execution-ack.
284
- for( const inv of invocations )
475
+ // Effector invocations → project the motor act, then run the handler → ack.
476
+ for( const inv of invocations ){
477
+ this._emitEffectorAct( { name: inv.effectorName, args: inv.parameters, reasoning: inv.reasoning, to: inv.targetEntityId } )
285
478
  void this._runEffector( inv )
479
+ }
286
480
 
287
- // State subscribers (only when someone is listening state() is cheap but not free).
288
- if( this._stateHandlers.size > 0 ){
289
- const s = this.state()
290
- for( const h of this._stateHandlers ) try { h( s ) } catch { /* isolate */ }
481
+ // Projections that read the state summarycompute it once, and only when
482
+ // something is actually observing (state() is cheap but not free). Isolated
483
+ // so a transient read error can never stall the tick loop.
484
+ if( this._stateHandlers.size > 0 || this._emotionHandlers.size > 0 ){
485
+ try {
486
+ const s = this.state()
487
+ for( const h of this._stateHandlers ) try { h( s ) } catch { /* isolate */ }
488
+ if( this._emotionHandlers.size > 0 ) this._maybeEmitAffect( s.metrics.valence, s.metrics.arousal )
489
+ }
490
+ catch( e ){ this._emitError( e as Error ) }
291
491
  }
292
492
  } ) ?? null
293
493
  }
@@ -304,7 +504,11 @@ export class Will {
304
504
  }
305
505
 
306
506
  try {
307
- const raw = await handler( inv.parameters, { reasoning: inv.reasoning, targetEntityId: inv.targetEntityId } )
507
+ const raw = await handler( inv.parameters, {
508
+ reasoning: inv.reasoning,
509
+ targetEntityId: inv.targetEntityId,
510
+ ...( inv.description ? { description: inv.description } : {} ),
511
+ } )
308
512
  const result = typeof raw === 'string' ? { success: true, description: raw } : raw
309
513
  this.stem.confirmEffectorExecution( this.id, inv.decisionRecordId, result )
310
514
  }
@@ -318,6 +522,27 @@ export class Will {
318
522
 
319
523
  private _emitMessage( m: WillMessage ): void {
320
524
  for( const h of this._messageHandlers ) try { h( m ) } catch( e ){ this._emitError( e as Error ) }
525
+ // Wake any nextUtterance() awaiter this message satisfies.
526
+ if( this._utteranceWaiters.size > 0 ){
527
+ for( const w of [ ...this._utteranceWaiters ] ){
528
+ if( w.to !== undefined && w.to !== m.to ) continue
529
+ clearTimeout( w.timer )
530
+ this._utteranceWaiters.delete( w )
531
+ w.resolve( m )
532
+ }
533
+ }
534
+ }
535
+ private _emitEffectorAct( a: WillEffectorAct ): void {
536
+ for( const h of this._effectorHandlers ) try { h( a ) } catch( e ){ this._emitError( e as Error ) }
537
+ }
538
+ private _maybeEmitAffect( valence: number, arousal: number ): void {
539
+ const last = this._lastAffect
540
+ // Emit on the first read, then only when affect moves past the epsilon.
541
+ if( last
542
+ && Math.abs( last.valence - valence ) < AFFECT_EPSILON
543
+ && Math.abs( last.arousal - arousal ) < AFFECT_EPSILON ) return
544
+ this._lastAffect = { valence, arousal }
545
+ for( const h of this._emotionHandlers ) try { h( { valence, arousal } ) } catch( e ){ this._emitError( e as Error ) }
321
546
  }
322
547
  private _emitError( e: Error ): void {
323
548
  for( const h of this._errorHandlers ) try { h( e ) } catch { /* nowhere left to go */ }
package/src/stem/index.ts CHANGED
@@ -38,6 +38,8 @@ import { TransportController } from '#stem/tracts/transport.controller'
38
38
  import { InboundQueue } from '#stem/tracts/inbound.queue'
39
39
  import type { ExternalTransport } from '#stem/tracts/transport'
40
40
  import { effectorController } from '#stem/tracts/effector.controller'
41
+ import { externalSchemas } from '#agency/schemas/external'
42
+ import type { EffectorDeclaration } from '#agency/types'
41
43
  import { SensoryController } from '#stem/tracts/sensory.controller'
42
44
  import { BiographyWriter } from '#stem/tracts/biography.writer'
43
45
  import { HealthReporter } from '#stem/tracts/health.reporter'
@@ -772,6 +774,20 @@ export class WillStem {
772
774
  this._effector.setAllowed( this._get( id ), effectors )
773
775
  }
774
776
 
777
+ /**
778
+ * Register a host effector on a *running* Will (post-create `.effector()`).
779
+ * Builds its external schema and adds it to the live repertoire so the Will
780
+ * can actually perceive + enact it — a grant alone only gates; without the
781
+ * schema the ability could never be afforded. Comms names are no-ops here
782
+ * (governed by AccessGrants). This is a runtime mutation, like a grant change;
783
+ * the deterministic/replayable path is declaring effectors at create time.
784
+ */
785
+ registerEffector( id: string, declaration: EffectorDeclaration ): void {
786
+ const repertoire = this._get( id ).cognition.schemaRepertoire
787
+ for( const schema of externalSchemas( [ declaration ] ) )
788
+ repertoire.registerExternal( schema )
789
+ }
790
+
775
791
  /**
776
792
  * Called by the host/WorldInterface after executing a host-owned effector.
777
793
  * `invocationId` is the correlation handle the host echoed (the awaiting
package/src/stem/mind.ts CHANGED
@@ -40,6 +40,7 @@ import { InstructionIntake } from '#agency/engines/instruction.intake'
40
40
  import { SchemaRepertoire } from '#agency/schemas/repertoire'
41
41
  import { INNATE_SCHEMAS } from '#agency/schemas/innate'
42
42
  import { externalSchemas } from '#agency/schemas/external'
43
+ import { effectorName, type EffectorDeclaration } from '#agency/types'
43
44
 
44
45
 
45
46
  import {
@@ -260,8 +261,12 @@ export interface WillConfig {
260
261
  *
261
262
  * null or omitted = no communication effectors (minimal default).
262
263
  * Example: ['listen', 'talk', 'text'] enables inbound + text outbound.
264
+ *
265
+ * A domain effector may be a bare name or an object carrying its meaning +
266
+ * intrinsic priors: `{ name, description?, cost?, valence?, preconditions? }`
267
+ * (see EffectorDeclaration). Comms names are always bare.
263
268
  */
264
- allowedGenericEffectors?: string[] | null
269
+ allowedGenericEffectors?: EffectorDeclaration[] | null
265
270
 
266
271
  /**
267
272
  * When true the executive engine uses a canned mock LLM response instead of
@@ -506,7 +511,7 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
506
511
  // creation; warnings surface; safe issues are sanitized in place.
507
512
  const idGuard = validateWillIdentity({
508
513
  identity: config.identity,
509
- effectors: Array.isArray( config.allowedGenericEffectors ) ? config.allowedGenericEffectors : ( profile?.effectors ?? null ),
514
+ effectors: ( Array.isArray( config.allowedGenericEffectors ) ? config.allowedGenericEffectors : ( profile?.effectors ?? null ) )?.map( effectorName ) ?? null,
510
515
  profileContext: profile?.context,
511
516
  })
512
517
  if( !idGuard.ok )
@@ -655,13 +660,15 @@ function _constructCognition(
655
660
  // This matters when a profile Will is created without specifying effectors:
656
661
  // the DB stores null, the service passes null, and profile effectors must win.
657
662
  // An empty array [] means "explicitly no effectors" (survives restart correctly).
658
- const resolvedEffectors = Array.isArray( config.allowedGenericEffectors )
663
+ const resolvedEffectors: EffectorDeclaration[] | null = Array.isArray( config.allowedGenericEffectors )
659
664
  ? config.allowedGenericEffectors
660
665
  : ( profile?.effectors ?? null )
666
+ // Name-only view for the grant / permission surfaces (comms gating is by name).
667
+ const resolvedEffectorNames = resolvedEffectors?.map( effectorName ) ?? null
661
668
 
662
669
  // Agency-native permission / sense-gate authority, seeded from the resolved
663
670
  // grant list. The senses + reply path read this. (Replaced effectorRegistry.)
664
- const accessGrants = new AccessGrants( resolvedEffectors )
671
+ const accessGrants = new AccessGrants( resolvedEffectorNames )
665
672
 
666
673
  // ── Executive Engine ────────────────────────────────────────
667
674
  // Created for all tiers so the Cognition type is always satisfied.
@@ -51,6 +51,7 @@ export class effectorController {
51
51
  parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
52
52
  targetEntityId: payload.targetEntityId as string | undefined,
53
53
  reasoning: ( payload.reasoning as string ) ?? '',
54
+ ...( typeof payload.description === 'string' ? { description: payload.description } : {} ),
54
55
  tick: ( payload.tick as number ) ?? 0,
55
56
  timestamp: Date.now()
56
57
  })
package/src/types.ts CHANGED
@@ -29,6 +29,8 @@ export interface effectorInvocation {
29
29
  parameters: Record<string, unknown>
30
30
  targetEntityId: string | undefined
31
31
  reasoning: string
32
+ /** The ability's declared meaning (from its EffectorDeclaration), when present. */
33
+ description?: string
32
34
  tick: number
33
35
  timestamp: number
34
36
  }