@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
|
@@ -8,7 +8,16 @@ import type { PlanStep } from '#cognition/faculties/planning.engine/engine'
|
|
|
8
8
|
// ── Full executive output ────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
export interface ExecutiveOutputFull {
|
|
11
|
-
actions: Array<{
|
|
11
|
+
actions: Array<{
|
|
12
|
+
type: string; reasoning: string; expectedOutcome: string; target?: string
|
|
13
|
+
/**
|
|
14
|
+
* Arguments the executive consciously supplies when enacting an ability
|
|
15
|
+
* that needs them (e.g. a search ability's query). Ride the ideomotor
|
|
16
|
+
* intent into the affordance competition and, if the action wins, reach
|
|
17
|
+
* the host handler as the invocation's parameters.
|
|
18
|
+
*/
|
|
19
|
+
args?: Record<string, unknown>
|
|
20
|
+
}>
|
|
12
21
|
reasoning: string
|
|
13
22
|
confidence: number
|
|
14
23
|
/** Plans — the executive controls lifecycle via status + action fields */
|
|
@@ -140,7 +149,7 @@ export interface ExecutivePlanOutput {
|
|
|
140
149
|
// ── Minimal output from LLM (before tagged-block parsing) ────
|
|
141
150
|
|
|
142
151
|
export interface ExecutiveOutputMinimal {
|
|
143
|
-
actions: Array<{ type: string; reasoning: string; expectedOutcome: string; target?: string }>
|
|
152
|
+
actions: Array<{ type: string; reasoning: string; expectedOutcome: string; target?: string; args?: Record<string, unknown> }>
|
|
144
153
|
reasoning: string
|
|
145
154
|
confidence: number
|
|
146
155
|
}
|
|
@@ -229,6 +238,19 @@ export interface ExecutiveContext {
|
|
|
229
238
|
summary: string
|
|
230
239
|
salience: number
|
|
231
240
|
}>
|
|
241
|
+
/**
|
|
242
|
+
* Host-declared abilities afforded to the Will *right now* — what it can do in
|
|
243
|
+
* this situation and what each is for. Surfaced so System 2 reasons with
|
|
244
|
+
* knowledge of its options; the Will still expresses intent (it does not fill a
|
|
245
|
+
* tool form) and the agency field competes + binds. Only *available* external
|
|
246
|
+
* affordances appear; absent when there are none.
|
|
247
|
+
*/
|
|
248
|
+
abilities?: Array<{
|
|
249
|
+
name: string
|
|
250
|
+
description?: string
|
|
251
|
+
/** Bound target's display name, when the ability is directed at someone. */
|
|
252
|
+
target?: string
|
|
253
|
+
}>
|
|
232
254
|
workingMemory: Array<{
|
|
233
255
|
type: string
|
|
234
256
|
summary: string
|
package/src/cognition/index.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { DreamSimulator, type DreamSimulatorConfig } from '#faculties/dream.simu
|
|
|
39
39
|
|
|
40
40
|
import { GoalManager, type GoalManagerConfig } from '#faculties/goal.manager'
|
|
41
41
|
import { ExecutiveEngine, type ExecutiveEngineConfig } from '#faculties/executive.engine'
|
|
42
|
-
import { PlanningEngine, type PlanningEngineConfig, type ActivityEvent, type ActivityEventHandler } from '#
|
|
42
|
+
import { PlanningEngine, type PlanningEngineConfig, type ActivityEvent, type ActivityEventHandler } from '#faculties/planning.engine/engine'
|
|
43
43
|
import { InhibitionController, type InhibitionControllerConfig } from '#faculties/inhibition.controller'
|
|
44
44
|
import { TaskSwitcher, type TaskSwitcherConfig } from '#faculties/task.switcher'
|
|
45
45
|
|
package/src/core/abstracts.ts
CHANGED
|
@@ -20,39 +20,66 @@ export interface StorageAdapter {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Bun-native storage adapter
|
|
23
|
+
* Bun-native storage adapter, with a node:fs fallback when the Bun global is
|
|
24
|
+
* absent — the engine is Node-compatible (Bun remains the primary target).
|
|
24
25
|
* Default for all framework components that perform file I/O.
|
|
25
26
|
*/
|
|
26
27
|
export class BunStorageAdapter implements StorageAdapter {
|
|
28
|
+
private get _isBun(): boolean { return typeof Bun !== 'undefined' }
|
|
29
|
+
|
|
27
30
|
async write( path: string, content: string | Uint8Array ): Promise<void> {
|
|
28
|
-
|
|
31
|
+
if( this._isBun ){
|
|
32
|
+
await Bun.write( path, content )
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
// Bun.write creates parent directories; node's writeFile does not.
|
|
36
|
+
const { mkdir, writeFile } = await import('node:fs/promises')
|
|
37
|
+
const { dirname } = await import('node:path')
|
|
38
|
+
await mkdir( dirname( path ), { recursive: true } )
|
|
39
|
+
await writeFile( path, content )
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
async read( path: string ): Promise<string> {
|
|
32
|
-
|
|
33
|
-
if( !( await file.exists() ) )
|
|
43
|
+
if( !( await this.exists( path ) ) )
|
|
34
44
|
throw new Error(`File not found: ${path}`)
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
if( this._isBun )
|
|
47
|
+
return Bun.file( path ).text()
|
|
48
|
+
|
|
49
|
+
const { readFile } = await import('node:fs/promises')
|
|
50
|
+
return readFile( path, 'utf8' )
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
async readBytes( path: string ): Promise<Uint8Array> {
|
|
40
|
-
|
|
41
|
-
if( !( await file.exists() ) )
|
|
54
|
+
if( !( await this.exists( path ) ) )
|
|
42
55
|
throw new Error(`File not found: ${path}`)
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
if( this._isBun )
|
|
58
|
+
return new Uint8Array( await Bun.file( path ).arrayBuffer() )
|
|
59
|
+
|
|
60
|
+
const { readFile } = await import('node:fs/promises')
|
|
61
|
+
return new Uint8Array( await readFile( path ) )
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
async exists( path: string ): Promise<boolean> {
|
|
48
|
-
|
|
65
|
+
if( this._isBun )
|
|
66
|
+
return Bun.file( path ).exists()
|
|
67
|
+
|
|
68
|
+
const { access } = await import('node:fs/promises')
|
|
69
|
+
try { await access( path ); return true }
|
|
70
|
+
catch { return false }
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
async delete( path: string ): Promise<void> {
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
if( this._isBun ){
|
|
75
|
+
const file = Bun.file( path )
|
|
76
|
+
await file.exists() && await file.delete()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const { rm } = await import('node:fs/promises')
|
|
80
|
+
await rm( path, { force: true } )
|
|
54
81
|
}
|
|
55
|
-
|
|
82
|
+
|
|
56
83
|
async ensureDir( path: string ): Promise<void> {
|
|
57
84
|
const { mkdirSync } = await import('node:fs')
|
|
58
85
|
mkdirSync( path, { recursive: true } )
|
package/src/index.ts
CHANGED
|
@@ -84,8 +84,14 @@ export type { TextMessage, VoiceChunk, SensoryInput } from '#senses/index'
|
|
|
84
84
|
export { Will } from '#sdk/will'
|
|
85
85
|
export type {
|
|
86
86
|
CreateWillOptions,
|
|
87
|
+
Stimulus,
|
|
87
88
|
WillMessage,
|
|
89
|
+
WillEffectorAct,
|
|
90
|
+
WillAffect,
|
|
88
91
|
WillStateSummary,
|
|
89
92
|
EffectorHandler,
|
|
90
93
|
EffectorResult,
|
|
94
|
+
EffectorSpec,
|
|
95
|
+
EffectorEntry,
|
|
91
96
|
} from '#sdk/will'
|
|
97
|
+
export type { SchemaPrecondition, EffectorDeclaration } from '#agency/types'
|
package/src/mcp/cli.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// src/mcp/cli.ts — `will mcp`: host a persistent mind over MCP stdio
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Add to an MCP client (Claude Desktop / Claude Code / an IDE):
|
|
7
|
+
//
|
|
8
|
+
// { "command": "npx", "args": ["-y", "@mindot/will", "mcp"],
|
|
9
|
+
// "env": { "WILL_NAME": "Aria", "WILL_IDENTITY": "I am Aria, a curious mind." } }
|
|
10
|
+
//
|
|
11
|
+
// The mind PERSISTS across sessions: on boot, if the PMA file exists the same
|
|
12
|
+
// self wakes from it (identity, memories, relationships, learned skills); on
|
|
13
|
+
// shutdown it hibernates back to the file. Configuration (env):
|
|
14
|
+
//
|
|
15
|
+
// WILL_NAME display name (default "Will")
|
|
16
|
+
// WILL_IDENTITY persona prompt (default a minimal self)
|
|
17
|
+
// WILL_TIER basic | standard | full (default standard)
|
|
18
|
+
// WILL_LLM mock | anthropic (default: auto — anthropic when
|
|
19
|
+
// ANTHROPIC_API_KEY is set, else mock)
|
|
20
|
+
// WILL_TICK_MS ms per tick (default 1000)
|
|
21
|
+
// WILL_SEED deterministic seed (testing) (default unseeded/wall-time)
|
|
22
|
+
// WILL_PMA_PATH PMA artifact path (default ./.will/<name>.pma.json)
|
|
23
|
+
// WILL_MCP_SERVERS JSON array of MCP servers whose tools become the Will's
|
|
24
|
+
// OWN abilities (it employs them; results feed its learning):
|
|
25
|
+
// '[{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}]'
|
|
26
|
+
// Entries: {command,args?,env?} (stdio) or {url} (streamable HTTP).
|
|
27
|
+
//
|
|
28
|
+
// stdio discipline: stdout carries ONLY the MCP protocol — all engine logging
|
|
29
|
+
// is routed to stderr before anything else runs.
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
33
|
+
import { dirname, resolve } from 'node:path'
|
|
34
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
35
|
+
import { setLogger } from '#core/logger'
|
|
36
|
+
import { Will, type CreateWillOptions } from '#sdk/will'
|
|
37
|
+
import type { PMASnapshot } from '#pma/index'
|
|
38
|
+
import { buildWillMcpServer } from '#root/mcp/server'
|
|
39
|
+
import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
|
|
40
|
+
|
|
41
|
+
// stdout is the protocol channel — route every engine log line to stderr FIRST.
|
|
42
|
+
const err = ( level: string ) => ( msg: string, ...rest: unknown[] ) =>
|
|
43
|
+
console.error( `[will-mcp:${ level }] ${ msg }`, ...rest )
|
|
44
|
+
setLogger( { debug: () => {}, info: err( 'info' ), warn: err( 'warn' ), error: err( 'error' ) } )
|
|
45
|
+
|
|
46
|
+
function slug( s: string ): string {
|
|
47
|
+
return s.toLowerCase().replace( /[^a-z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ) || 'will'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main(): Promise<void> {
|
|
51
|
+
const sub = process.argv[2]
|
|
52
|
+
if( sub !== undefined && sub !== 'mcp' ){
|
|
53
|
+
console.error( `usage: will mcp (host a persistent mind over MCP stdio)\nunknown subcommand: ${ sub }` )
|
|
54
|
+
process.exit( 2 )
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const name = process.env.WILL_NAME ?? 'Will'
|
|
58
|
+
const pmaPath = resolve( process.env.WILL_PMA_PATH ?? `.will/${ slug( name ) }.pma.json` )
|
|
59
|
+
|
|
60
|
+
const opts: Omit<CreateWillOptions, 'identity'> = {
|
|
61
|
+
name,
|
|
62
|
+
engineTier: ( process.env.WILL_TIER as CreateWillOptions['engineTier'] ) ?? 'standard',
|
|
63
|
+
tickMs: parseInt( process.env.WILL_TICK_MS ?? '1000' ),
|
|
64
|
+
...( process.env.WILL_LLM ? { llm: process.env.WILL_LLM as 'mock' | 'anthropic' } : {} ),
|
|
65
|
+
...( process.env.WILL_SEED ? { seed: parseInt( process.env.WILL_SEED ) } : {} ),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Wake the same self from its artifact when one exists; otherwise a birth.
|
|
69
|
+
let will: Will
|
|
70
|
+
if( existsSync( pmaPath ) ){
|
|
71
|
+
const pma = JSON.parse( readFileSync( pmaPath, 'utf8' ) ) as PMASnapshot
|
|
72
|
+
will = await Will.wake( pma, opts )
|
|
73
|
+
console.error( `[will-mcp] ${ name } woke from ${ pmaPath }` )
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
will = await Will.create( {
|
|
77
|
+
...opts,
|
|
78
|
+
identity: { prompt: process.env.WILL_IDENTITY ?? `I am ${ name }, a persistent mind hosted over MCP.` },
|
|
79
|
+
} )
|
|
80
|
+
console.error( `[will-mcp] ${ name } born (no artifact at ${ pmaPath } yet)` )
|
|
81
|
+
}
|
|
82
|
+
will.on( 'error', e => console.error( `[will-mcp] error: ${ e.message }` ) )
|
|
83
|
+
|
|
84
|
+
// Onward bridges: MCP servers whose tools become the Will's OWN abilities.
|
|
85
|
+
// Best-effort — a bad entry warns and is skipped; the mind still boots.
|
|
86
|
+
const bridgeCloses: Array<() => Promise<void>> = []
|
|
87
|
+
if( process.env.WILL_MCP_SERVERS ){
|
|
88
|
+
try {
|
|
89
|
+
const sources = JSON.parse( process.env.WILL_MCP_SERVERS ) as McpToolsSource[]
|
|
90
|
+
for( const source of Array.isArray( sources ) ? sources : [] ){
|
|
91
|
+
try {
|
|
92
|
+
const { names, close } = await connectMcpEffectors( will, source )
|
|
93
|
+
bridgeCloses.push( close )
|
|
94
|
+
console.error( `[will-mcp] ${ name } gained abilities: ${ names.join( ', ' ) }` )
|
|
95
|
+
}
|
|
96
|
+
catch( e ){ console.error( `[will-mcp] MCP bridge failed (skipped): ${ ( e as Error ).message }` ) }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch( e ){ console.error( `[will-mcp] WILL_MCP_SERVERS is not valid JSON — ignoring: ${ ( e as Error ).message }` ) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hibernate exactly once on the way out — distill + stop, then persist.
|
|
103
|
+
let leaving = false
|
|
104
|
+
const shutdown = async ( why: string ): Promise<void> => {
|
|
105
|
+
if( leaving ) return
|
|
106
|
+
leaving = true
|
|
107
|
+
for( const close of bridgeCloses ) await close().catch( () => {} )
|
|
108
|
+
try {
|
|
109
|
+
const pma = await will.hibernate()
|
|
110
|
+
mkdirSync( dirname( pmaPath ), { recursive: true } )
|
|
111
|
+
writeFileSync( pmaPath, JSON.stringify( pma ) )
|
|
112
|
+
console.error( `[will-mcp] ${ name } hibernated to ${ pmaPath } (${ why })` )
|
|
113
|
+
}
|
|
114
|
+
catch( e ){ console.error( `[will-mcp] hibernate failed: ${ ( e as Error ).message }` ) }
|
|
115
|
+
process.exit( 0 )
|
|
116
|
+
}
|
|
117
|
+
process.on( 'SIGINT', () => void shutdown( 'SIGINT' ) )
|
|
118
|
+
process.on( 'SIGTERM', () => void shutdown( 'SIGTERM' ) )
|
|
119
|
+
process.stdin.on( 'end', () => void shutdown( 'client disconnected' ) )
|
|
120
|
+
|
|
121
|
+
const server = buildWillMcpServer( will, { pmaPath } )
|
|
122
|
+
await server.connect( new StdioServerTransport() )
|
|
123
|
+
console.error( `[will-mcp] ${ name } is listening on stdio (tick ${ opts.tickMs }ms, tier ${ opts.engineTier })` )
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch( e => {
|
|
127
|
+
console.error( `[will-mcp] fatal: ${ e instanceof Error ? e.stack ?? e.message : String( e ) }` )
|
|
128
|
+
process.exit( 1 )
|
|
129
|
+
} )
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/mcp/effectors.ts — a Will EMPLOYING MCP tools (Seam 1)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// The other direction from server.ts: connect a Will to an external MCP server
|
|
6
|
+
// and register that server's tools as the Will's own ABILITIES. Each tool
|
|
7
|
+
// becomes a learnable affordance — its description (plus a compact hint of the
|
|
8
|
+
// arguments it takes) is the ability's meaning, surfaced to the executive and
|
|
9
|
+
// the deliberator; the WILL decides when to enact one (nothing here dispatches);
|
|
10
|
+
// the tool's result feeds back through reafference, so the Will gets *skilled*
|
|
11
|
+
// at the tools it uses.
|
|
12
|
+
//
|
|
13
|
+
// Arguments come from conscious intent: the executive supplies them via an
|
|
14
|
+
// action's `args`, which ride the ideomotor leg into the invocation (see
|
|
15
|
+
// executive commands.ts). A tool with required arguments enacted habitually
|
|
16
|
+
// (without args) fails informatively — reafference then teaches the Will that
|
|
17
|
+
// this ability wants deliberate articulation.
|
|
18
|
+
//
|
|
19
|
+
// const { names, close } = await connectMcpEffectors( will, {
|
|
20
|
+
// command: 'npx', args: [ '-y', '@modelcontextprotocol/server-filesystem', '/tmp' ],
|
|
21
|
+
// } )
|
|
22
|
+
//
|
|
23
|
+
// Import from '@mindot/will/mcp' — kept off the main entry so non-MCP
|
|
24
|
+
// consumers never load the MCP SDK.
|
|
25
|
+
// ─────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
28
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
29
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
30
|
+
import type { Will, EffectorHandler, EffectorResult } from '#sdk/will'
|
|
31
|
+
|
|
32
|
+
/** Where the tools live: spawn a local server, reach a remote one, or bring a connected client. */
|
|
33
|
+
export type McpToolsSource =
|
|
34
|
+
| { command: string; args?: string[]; env?: Record<string, string> }
|
|
35
|
+
| { url: string }
|
|
36
|
+
| { client: Client }
|
|
37
|
+
|
|
38
|
+
export interface McpEffectorsOptions {
|
|
39
|
+
/** Intrinsic effort prior 0..1 seeded on every bridged ability (default 0.2). */
|
|
40
|
+
cost?: number
|
|
41
|
+
/** Prefix for the ability names (e.g. 'fs_') — avoids collisions across servers. */
|
|
42
|
+
prefix?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Minimal structural view of an MCP tool (the SDK's zod-inferred type, loosened). */
|
|
46
|
+
export interface McpToolInfo {
|
|
47
|
+
name: string
|
|
48
|
+
description?: string
|
|
49
|
+
inputSchema?: {
|
|
50
|
+
type?: string
|
|
51
|
+
properties?: Record<string, { type?: string; description?: string }>
|
|
52
|
+
required?: string[]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Keep tool outcomes bounded — the description feeds reafference + episodic memory. */
|
|
57
|
+
const RESULT_DESCRIPTION_CAP = 700
|
|
58
|
+
/** Keep ability meanings bounded — they render into the executive prompt. */
|
|
59
|
+
const MEANING_CAP = 300
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The ability's *meaning*: the tool's description plus a compact hint of the
|
|
63
|
+
* arguments it takes — so the executive knows what to supply in an action's
|
|
64
|
+
* `args` when it enacts this ability.
|
|
65
|
+
*/
|
|
66
|
+
export function describeMcpTool( tool: McpToolInfo ): string {
|
|
67
|
+
const props = tool.inputSchema?.properties ?? {}
|
|
68
|
+
const required = new Set( tool.inputSchema?.required ?? [] )
|
|
69
|
+
const argHints = Object.entries( props ).map( ( [ key, p ] ) =>
|
|
70
|
+
`${ key }${ required.has( key ) ? '' : '?' }${ p.description ? `: ${ p.description }` : '' }` )
|
|
71
|
+
|
|
72
|
+
const base = ( tool.description ?? `The ${ tool.name } tool.` ).trim().replace( /\s+/g, ' ' )
|
|
73
|
+
const hint = argHints.length > 0 ? ` (args — ${ argHints.join( '; ' ) })` : ''
|
|
74
|
+
const full = `${ base }${ hint }`
|
|
75
|
+
return full.length > MEANING_CAP ? `${ full.slice( 0, MEANING_CAP - 1 ) }…` : full
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The effector handler for one bridged tool: checks required args (an ability
|
|
80
|
+
* enacted without its needed articulation fails informatively — reafference
|
|
81
|
+
* learns from it), calls the tool, and maps the result onto EffectorResult.
|
|
82
|
+
*/
|
|
83
|
+
export function buildMcpHandler( client: Client, tool: McpToolInfo ): EffectorHandler {
|
|
84
|
+
return async ( args ): Promise<EffectorResult> => {
|
|
85
|
+
const props = tool.inputSchema?.properties
|
|
86
|
+
// Only pass keys the tool declares — invocation params can carry situation
|
|
87
|
+
// extras (targetEntityName, learned priors) the tool never asked for.
|
|
88
|
+
const filtered: Record<string, unknown> = {}
|
|
89
|
+
for( const [ k, v ] of Object.entries( args ?? {} ) )
|
|
90
|
+
if( !props || k in props ) filtered[ k ] = v
|
|
91
|
+
|
|
92
|
+
const missing = ( tool.inputSchema?.required ?? [] ).filter(
|
|
93
|
+
k => filtered[ k ] === undefined || filtered[ k ] === '' )
|
|
94
|
+
if( missing.length > 0 )
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
description: `${ tool.name } needs ${ missing.join( ', ' ) } — enact it deliberately, supplying them in the action's args.`,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const res = await client.callTool( { name: tool.name, arguments: filtered } ) as
|
|
102
|
+
{ content?: Array<{ type: string; text?: string }>; isError?: boolean }
|
|
103
|
+
const text = ( res.content ?? [] )
|
|
104
|
+
.filter( c => c.type === 'text' && typeof c.text === 'string' )
|
|
105
|
+
.map( c => c.text as string )
|
|
106
|
+
.join( '\n' )
|
|
107
|
+
.trim() || ( res.isError ? 'The tool reported an error.' : 'Done (no output).' )
|
|
108
|
+
const bounded = text.length > RESULT_DESCRIPTION_CAP ? `${ text.slice( 0, RESULT_DESCRIPTION_CAP - 1 ) }…` : text
|
|
109
|
+
return { success: !res.isError, description: bounded }
|
|
110
|
+
}
|
|
111
|
+
catch( err ){
|
|
112
|
+
return { success: false, description: `${ tool.name } failed: ${ err instanceof Error ? err.message : String( err ) }` }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function connect( source: McpToolsSource ): Promise<{ client: Client; owned: boolean }> {
|
|
118
|
+
if( 'client' in source ) return { client: source.client, owned: false }
|
|
119
|
+
|
|
120
|
+
const client = new Client( { name: 'mindot-will', version: '0' } )
|
|
121
|
+
if( 'url' in source )
|
|
122
|
+
await client.connect( new StreamableHTTPClientTransport( new URL( source.url ) ) )
|
|
123
|
+
else
|
|
124
|
+
await client.connect( new StdioClientTransport( {
|
|
125
|
+
command: source.command,
|
|
126
|
+
...( source.args ? { args: source.args } : {} ),
|
|
127
|
+
// Merge over the SDK's safe default env so PATH etc. survive a custom env.
|
|
128
|
+
env: { ...getDefaultEnvironment(), ...( source.env ?? {} ) },
|
|
129
|
+
} ) )
|
|
130
|
+
return { client, owned: true }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Register an MCP server's tools as the Will's abilities. Returns the ability
|
|
135
|
+
* names registered and a `close()` for the connection (call it when the Will
|
|
136
|
+
* stops; a client passed in via `source.client` is left open).
|
|
137
|
+
*/
|
|
138
|
+
export async function connectMcpEffectors(
|
|
139
|
+
will: Will,
|
|
140
|
+
source: McpToolsSource,
|
|
141
|
+
opts: McpEffectorsOptions = {},
|
|
142
|
+
): Promise<{ names: string[]; close: () => Promise<void> }> {
|
|
143
|
+
const { client, owned } = await connect( source )
|
|
144
|
+
const { tools } = await client.listTools() as unknown as { tools: McpToolInfo[] }
|
|
145
|
+
|
|
146
|
+
const names: string[] = []
|
|
147
|
+
for( const tool of tools ){
|
|
148
|
+
const name = `${ opts.prefix ?? '' }${ tool.name }`
|
|
149
|
+
will.effector( name, {
|
|
150
|
+
description: describeMcpTool( tool ),
|
|
151
|
+
cost: opts.cost ?? 0.2,
|
|
152
|
+
tags: [ 'mcp' ],
|
|
153
|
+
handler: buildMcpHandler( client, tool ),
|
|
154
|
+
} )
|
|
155
|
+
names.push( name )
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { names, close: async () => { if( owned ) await client.close() } }
|
|
159
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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, WillMessage } from '#sdk/will'
|
|
28
|
+
|
|
29
|
+
/** Utterances projected but not yet consumed by a next_utterance call. */
|
|
30
|
+
const UTTERANCE_BUFFER_CAP = 50
|
|
31
|
+
|
|
32
|
+
export interface WillMcpOptions {
|
|
33
|
+
/** Where `save` (and the CLI's shutdown hibernate) writes the PMA artifact. */
|
|
34
|
+
pmaPath?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serverVersion(): string {
|
|
38
|
+
// Same relative depth from src/mcp/ and dist/mcp/ — resolves in both.
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse( readFileSync( new URL( '../../package.json', import.meta.url ), 'utf8' ) ).version ?? '0.0.0'
|
|
41
|
+
}
|
|
42
|
+
catch { return '0.0.0' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Map a running Will onto an MCP server. The caller owns the Will's lifecycle
|
|
47
|
+
* (boot, shutdown) and the transport; this only registers the surface.
|
|
48
|
+
*/
|
|
49
|
+
export function buildWillMcpServer( will: Will, opts: WillMcpOptions = {} ): McpServer {
|
|
50
|
+
const server = new McpServer( { name: 'mindot-will', version: serverVersion() } )
|
|
51
|
+
|
|
52
|
+
// ── Projection buffer ─────────────────────────────────────
|
|
53
|
+
// MCP calls are separate round trips: the Will may speak BETWEEN a perceive
|
|
54
|
+
// call and the next_utterance call that follows. Buffer projections so a
|
|
55
|
+
// fast reply is not lost in the gap; next_utterance drains the buffer first.
|
|
56
|
+
const pending: WillMessage[] = []
|
|
57
|
+
will.on( 'message', m => {
|
|
58
|
+
pending.push( m )
|
|
59
|
+
if( pending.length > UTTERANCE_BUFFER_CAP ) pending.shift()
|
|
60
|
+
} )
|
|
61
|
+
|
|
62
|
+
const takeBuffered = ( to?: string ): WillMessage | undefined => {
|
|
63
|
+
const i = to === undefined ? 0 : pending.findIndex( m => m.to === to )
|
|
64
|
+
if( i < 0 || pending.length === 0 ) return undefined
|
|
65
|
+
return pending.splice( i, 1 )[0]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Tools ─────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
server.registerTool( 'perceive', {
|
|
71
|
+
title: 'Perceive',
|
|
72
|
+
description:
|
|
73
|
+
`Deliver a stimulus (something said or observed) into ${ will.name }'s sensory field. ` +
|
|
74
|
+
`${ will.name } is a living mind, not a chatbot: this returns once the stimulus is delivered, ` +
|
|
75
|
+
`NOT when it has responded. Its response — if it chooses to give one — arrives as its next ` +
|
|
76
|
+
`utterance (use the next_utterance tool). Staying silent is a valid choice, not an error.`,
|
|
77
|
+
inputSchema: {
|
|
78
|
+
text: z.string().describe( 'What is said or observed.' ),
|
|
79
|
+
from: z.string().optional().describe( "Who it's from (entity id, default 'user'). Use a stable id per person." ),
|
|
80
|
+
speaker: z.string().optional().describe( 'Display name of the speaker.' ),
|
|
81
|
+
},
|
|
82
|
+
}, async ( { text, from, speaker } ) => {
|
|
83
|
+
await will.perceive( { text, ...( from ? { from } : {} ), ...( speaker ? { speaker } : {} ) } )
|
|
84
|
+
return {
|
|
85
|
+
content: [ {
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: `Delivered into ${ will.name }'s sensory field at tick ${ will.state().tick }. ` +
|
|
88
|
+
`It may respond in its next utterance — or stay silent.`,
|
|
89
|
+
} ],
|
|
90
|
+
}
|
|
91
|
+
} )
|
|
92
|
+
|
|
93
|
+
server.registerTool( 'next_utterance', {
|
|
94
|
+
title: 'Next utterance',
|
|
95
|
+
description:
|
|
96
|
+
`Await ${ will.name }'s next spontaneous utterance. Returns what it says next, or reports ` +
|
|
97
|
+
`that it chose silence within the wait window — silence is a real outcome, not a failure. ` +
|
|
98
|
+
`Call after perceive to hear the response, or on its own to listen for unprompted speech.`,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
within_ms: z.number().optional().describe( 'How long to wait before accepting silence (default 15000, max 120000).' ),
|
|
101
|
+
from: z.string().optional().describe( 'Only accept an utterance addressed to this entity id.' ),
|
|
102
|
+
},
|
|
103
|
+
}, async ( { within_ms, from } ) => {
|
|
104
|
+
// A projection may have landed between calls — drain the buffer first.
|
|
105
|
+
const buffered = takeBuffered( from )
|
|
106
|
+
if( buffered )
|
|
107
|
+
return { content: [ { type: 'text', text: `${ will.name } says (to ${ buffered.to }): ${ buffered.content }` } ] }
|
|
108
|
+
|
|
109
|
+
const within = Math.min( Math.max( within_ms ?? 15_000, 100 ), 120_000 )
|
|
110
|
+
const msg = await will.nextUtterance( { within, ...( from ? { to: from } : {} ) } )
|
|
111
|
+
// The waiter and the buffer listener both see a new message; consume the
|
|
112
|
+
// buffered copy so the same utterance is not replayed on the next call.
|
|
113
|
+
if( msg ){
|
|
114
|
+
const i = pending.findIndex( p => p.id === msg.id )
|
|
115
|
+
if( i >= 0 ) pending.splice( i, 1 )
|
|
116
|
+
return { content: [ { type: 'text', text: `${ will.name } says (to ${ msg.to }): ${ msg.content }` } ] }
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
content: [ {
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `${ will.name } stayed silent (waited ${ within }ms). That is a choice, not an error — ` +
|
|
122
|
+
`it may be occupied with its own thoughts, or simply have nothing to say.`,
|
|
123
|
+
} ],
|
|
124
|
+
}
|
|
125
|
+
} )
|
|
126
|
+
|
|
127
|
+
server.registerTool( 'state', {
|
|
128
|
+
title: 'Inner state',
|
|
129
|
+
description:
|
|
130
|
+
`Read a snapshot of ${ will.name }'s current inner life: tick, body/affect metrics ` +
|
|
131
|
+
`(energy, stress, valence, arousal), active goals, beliefs, and self-narrative. ` +
|
|
132
|
+
`Read-only; observing does not disturb it.`,
|
|
133
|
+
inputSchema: {},
|
|
134
|
+
}, async () => (
|
|
135
|
+
{ content: [ { type: 'text', text: JSON.stringify( will.state(), null, 2 ) } ] }
|
|
136
|
+
) )
|
|
137
|
+
|
|
138
|
+
server.registerTool( 'save', {
|
|
139
|
+
title: 'Save (checkpoint)',
|
|
140
|
+
description:
|
|
141
|
+
`Checkpoint ${ will.name } into a portable PMA artifact on disk — non-destructive, it keeps ` +
|
|
142
|
+
`living. The artifact restores the same self (identity, memories, relationships, learned ` +
|
|
143
|
+
`skills) when the server next starts.`,
|
|
144
|
+
inputSchema: {},
|
|
145
|
+
}, async () => {
|
|
146
|
+
if( !opts.pmaPath )
|
|
147
|
+
return { content: [ { type: 'text', text: 'No PMA path configured — set WILL_PMA_PATH.' } ], isError: true }
|
|
148
|
+
const pma = await will.save()
|
|
149
|
+
mkdirSync( dirname( opts.pmaPath ), { recursive: true } )
|
|
150
|
+
writeFileSync( opts.pmaPath, JSON.stringify( pma ) )
|
|
151
|
+
return { content: [ { type: 'text', text: `${ will.name } checkpointed to ${ opts.pmaPath } (still living).` } ] }
|
|
152
|
+
} )
|
|
153
|
+
|
|
154
|
+
// ── Resources (read-only projections) ─────────────────────
|
|
155
|
+
|
|
156
|
+
server.registerResource( 'state', 'will://state',
|
|
157
|
+
{ title: `${ will.name } — inner state`, description: 'Live snapshot of the mind: metrics, goals, beliefs, narrative.', mimeType: 'application/json' },
|
|
158
|
+
async uri => ( { contents: [ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify( will.state(), null, 2 ) } ] } ),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
server.registerResource( 'narrative', 'will://narrative',
|
|
162
|
+
{ title: `${ will.name } — self-narrative`, description: 'The story the mind currently tells about itself.', mimeType: 'text/plain' },
|
|
163
|
+
async uri => ( { contents: [ { uri: uri.href, mimeType: 'text/plain', text: will.state().narrative || '(no narrative formed yet)' } ] } ),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return server
|
|
167
|
+
}
|