@mindot/will 0.1.1 → 0.3.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 +71 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26221 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +52 -7760
- package/dist/index.js +374 -86
- package/dist/index.js.map +1 -1
- 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/cli.ts +75 -0
- 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/host/boot.ts +127 -0
- package/src/host/utterances.ts +53 -0
- package/src/index.ts +6 -0
- package/src/mcp/effectors.ts +159 -0
- package/src/mcp/server.ts +144 -0
- package/src/sdk/will.ts +269 -44
- package/src/serve/server.ts +154 -0
- 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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/mcp/server.ts — a Will, exposed over the Model Context Protocol
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Lets any MCP client (Claude Desktop, Claude Code, an IDE) host a persistent
|
|
6
|
+
// mind. The surface follows the same paradigm as the SDK facade: a Will is a
|
|
7
|
+
// SUBJECT that emits projections, not a chatbot function —
|
|
8
|
+
//
|
|
9
|
+
// • `perceive` — deliver a stimulus into its sensory field
|
|
10
|
+
// • `next_utterance` — await its next words (silence is a real outcome)
|
|
11
|
+
// • `state` — read a snapshot of its inner life
|
|
12
|
+
// • `save` — checkpoint the living mind to a PMA file
|
|
13
|
+
//
|
|
14
|
+
// plus read-only resources (`will://state`, `will://narrative`) for clients
|
|
15
|
+
// that surface them. There is deliberately no `ask()`-shaped tool: you speak
|
|
16
|
+
// TO the Will and observe what it projects; it may choose not to answer.
|
|
17
|
+
//
|
|
18
|
+
// One Will per server process. Boot/persistence/stdio wiring lives in cli.ts;
|
|
19
|
+
// this module only maps an existing facade instance onto MCP (testable with
|
|
20
|
+
// InMemoryTransport).
|
|
21
|
+
// ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { dirname } from 'node:path'
|
|
25
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
26
|
+
import { z } from 'zod'
|
|
27
|
+
import type { Will } from '#sdk/will'
|
|
28
|
+
import { UtteranceTap } from '#root/host/utterances'
|
|
29
|
+
|
|
30
|
+
export interface WillMcpOptions {
|
|
31
|
+
/** Where `save` (and the CLI's shutdown hibernate) writes the PMA artifact. */
|
|
32
|
+
pmaPath?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function serverVersion(): string {
|
|
36
|
+
// Same relative depth from src/mcp/ and dist/mcp/ — resolves in both.
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse( readFileSync( new URL( '../../package.json', import.meta.url ), 'utf8' ) ).version ?? '0.0.0'
|
|
39
|
+
}
|
|
40
|
+
catch { return '0.0.0' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map a running Will onto an MCP server. The caller owns the Will's lifecycle
|
|
45
|
+
* (boot, shutdown) and the transport; this only registers the surface.
|
|
46
|
+
*/
|
|
47
|
+
export function buildWillMcpServer( will: Will, opts: WillMcpOptions = {} ): McpServer {
|
|
48
|
+
const server = new McpServer( { name: 'mindot-will', version: serverVersion() } )
|
|
49
|
+
|
|
50
|
+
// MCP calls are separate round trips: the Will may speak BETWEEN a perceive
|
|
51
|
+
// call and the next_utterance call that follows. The tap buffers projections
|
|
52
|
+
// so a fast reply is not lost in the gap (see host/utterances.ts).
|
|
53
|
+
const tap = new UtteranceTap( will )
|
|
54
|
+
|
|
55
|
+
// ── Tools ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
server.registerTool( 'perceive', {
|
|
58
|
+
title: 'Perceive',
|
|
59
|
+
description:
|
|
60
|
+
`Deliver a stimulus (something said or observed) into ${ will.name }'s sensory field. ` +
|
|
61
|
+
`${ will.name } is a living mind, not a chatbot: this returns once the stimulus is delivered, ` +
|
|
62
|
+
`NOT when it has responded. Its response — if it chooses to give one — arrives as its next ` +
|
|
63
|
+
`utterance (use the next_utterance tool). Staying silent is a valid choice, not an error.`,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
text: z.string().describe( 'What is said or observed.' ),
|
|
66
|
+
from: z.string().optional().describe( "Who it's from (entity id, default 'user'). Use a stable id per person." ),
|
|
67
|
+
speaker: z.string().optional().describe( 'Display name of the speaker.' ),
|
|
68
|
+
},
|
|
69
|
+
}, async ( { text, from, speaker } ) => {
|
|
70
|
+
await will.perceive( { text, ...( from ? { from } : {} ), ...( speaker ? { speaker } : {} ) } )
|
|
71
|
+
return {
|
|
72
|
+
content: [ {
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: `Delivered into ${ will.name }'s sensory field at tick ${ will.state().tick }. ` +
|
|
75
|
+
`It may respond in its next utterance — or stay silent.`,
|
|
76
|
+
} ],
|
|
77
|
+
}
|
|
78
|
+
} )
|
|
79
|
+
|
|
80
|
+
server.registerTool( 'next_utterance', {
|
|
81
|
+
title: 'Next utterance',
|
|
82
|
+
description:
|
|
83
|
+
`Await ${ will.name }'s next spontaneous utterance. Returns what it says next, or reports ` +
|
|
84
|
+
`that it chose silence within the wait window — silence is a real outcome, not a failure. ` +
|
|
85
|
+
`Call after perceive to hear the response, or on its own to listen for unprompted speech.`,
|
|
86
|
+
inputSchema: {
|
|
87
|
+
within_ms: z.number().optional().describe( 'How long to wait before accepting silence (default 15000, max 120000).' ),
|
|
88
|
+
from: z.string().optional().describe( 'Only accept an utterance addressed to this entity id.' ),
|
|
89
|
+
},
|
|
90
|
+
}, async ( { within_ms, from } ) => {
|
|
91
|
+
const within = Math.min( Math.max( within_ms ?? 15_000, 100 ), 120_000 )
|
|
92
|
+
const msg = await tap.next( within, from )
|
|
93
|
+
if( msg )
|
|
94
|
+
return { content: [ { type: 'text', text: `${ will.name } says (to ${ msg.to }): ${ msg.content }` } ] }
|
|
95
|
+
return {
|
|
96
|
+
content: [ {
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `${ will.name } stayed silent (waited ${ within }ms). That is a choice, not an error — ` +
|
|
99
|
+
`it may be occupied with its own thoughts, or simply have nothing to say.`,
|
|
100
|
+
} ],
|
|
101
|
+
}
|
|
102
|
+
} )
|
|
103
|
+
|
|
104
|
+
server.registerTool( 'state', {
|
|
105
|
+
title: 'Inner state',
|
|
106
|
+
description:
|
|
107
|
+
`Read a snapshot of ${ will.name }'s current inner life: tick, body/affect metrics ` +
|
|
108
|
+
`(energy, stress, valence, arousal), active goals, beliefs, and self-narrative. ` +
|
|
109
|
+
`Read-only; observing does not disturb it.`,
|
|
110
|
+
inputSchema: {},
|
|
111
|
+
}, async () => (
|
|
112
|
+
{ content: [ { type: 'text', text: JSON.stringify( will.state(), null, 2 ) } ] }
|
|
113
|
+
) )
|
|
114
|
+
|
|
115
|
+
server.registerTool( 'save', {
|
|
116
|
+
title: 'Save (checkpoint)',
|
|
117
|
+
description:
|
|
118
|
+
`Checkpoint ${ will.name } into a portable PMA artifact on disk — non-destructive, it keeps ` +
|
|
119
|
+
`living. The artifact restores the same self (identity, memories, relationships, learned ` +
|
|
120
|
+
`skills) when the server next starts.`,
|
|
121
|
+
inputSchema: {},
|
|
122
|
+
}, async () => {
|
|
123
|
+
if( !opts.pmaPath )
|
|
124
|
+
return { content: [ { type: 'text', text: 'No PMA path configured — set WILL_PMA_PATH.' } ], isError: true }
|
|
125
|
+
const pma = await will.save()
|
|
126
|
+
mkdirSync( dirname( opts.pmaPath ), { recursive: true } )
|
|
127
|
+
writeFileSync( opts.pmaPath, JSON.stringify( pma ) )
|
|
128
|
+
return { content: [ { type: 'text', text: `${ will.name } checkpointed to ${ opts.pmaPath } (still living).` } ] }
|
|
129
|
+
} )
|
|
130
|
+
|
|
131
|
+
// ── Resources (read-only projections) ─────────────────────
|
|
132
|
+
|
|
133
|
+
server.registerResource( 'state', 'will://state',
|
|
134
|
+
{ title: `${ will.name } — inner state`, description: 'Live snapshot of the mind: metrics, goals, beliefs, narrative.', mimeType: 'application/json' },
|
|
135
|
+
async uri => ( { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify( will.state(), null, 2 ) } ] } ),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
server.registerResource( 'narrative', 'will://narrative',
|
|
139
|
+
{ title: `${ will.name } — self-narrative`, description: 'The story the mind currently tells about itself.', mimeType: 'text/plain' },
|
|
140
|
+
async uri => ( { contents: [ { uri: uri.href, mimeType: 'text/plain', text: will.state().narrative || '(no narrative formed yet)' } ] } ),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return server
|
|
144
|
+
}
|
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 */ }
|