@mindot/will 0.1.0 → 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.
- package/README.md +38 -3
- package/dist/index.d.ts +52 -7760
- package/dist/index.js +374 -86
- package/dist/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +1 -0
- package/dist/mcp/cli.js +26048 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/effectors.d.ts +55 -0
- package/dist/mcp/effectors.js +76 -0
- package/dist/mcp/effectors.js.map +1 -0
- package/dist/will-B5eKs3Wv.d.ts +7903 -0
- package/package.json +38 -30
- package/src/cognition/agency/engines/action.selector.ts +3 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -12
- package/src/cognition/agency/engines/deliberation.engine.ts +7 -2
- package/src/cognition/agency/engines/motor.schema.executor.ts +2 -0
- package/src/cognition/agency/schemas/external.ts +27 -10
- package/src/cognition/agency/schemas/repertoire.ts +12 -0
- package/src/cognition/agency/types.ts +51 -2
- package/src/cognition/faculties/executive.engine/commands.ts +47 -11
- package/src/cognition/faculties/executive.engine/context.ts +28 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +15 -1
- package/src/cognition/faculties/executive.engine/facet.ts +32 -6
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +11 -1
- package/src/cognition/faculties/executive.engine/types.ts +24 -2
- package/src/cognition/index.ts +1 -1
- package/src/core/abstracts.ts +39 -12
- package/src/index.ts +6 -0
- package/src/mcp/cli.ts +129 -0
- package/src/mcp/effectors.ts +159 -0
- package/src/mcp/server.ts +167 -0
- package/src/sdk/will.ts +269 -44
- package/src/stem/index.ts +16 -0
- package/src/stem/mind.ts +11 -4
- package/src/stem/tracts/effector.controller.ts +1 -0
- 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.
|
|
13
|
-
// const
|
|
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
|
-
/**
|
|
88
|
-
|
|
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
|
-
|
|
114
|
-
private readonly
|
|
115
|
-
private readonly
|
|
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,
|
|
131
|
-
will.
|
|
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,
|
|
152
|
-
will.
|
|
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
|
-
// ──
|
|
268
|
+
// ── Perceiving ─────────────────────────────────────────────
|
|
165
269
|
|
|
166
270
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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.
|
|
290
|
+
return this.perceive( { text, from: 'user', speaker: 'You' } )
|
|
172
291
|
}
|
|
173
292
|
|
|
174
|
-
/**
|
|
293
|
+
/** Perceive from a specific interlocutor (multi-party). Sugar over `perceive`. */
|
|
175
294
|
async tell( entityId: string, speakerName: string, text: string ): Promise<void> {
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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,
|
|
190
|
-
this.
|
|
191
|
-
//
|
|
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',
|
|
219
|
-
on( event: 'state',
|
|
220
|
-
on( event: '
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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.
|
|
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 →
|
|
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
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
481
|
+
// Projections that read the state summary — compute 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, {
|
|
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?:
|
|
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(
|
|
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
|
}
|