@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
|
@@ -149,6 +149,16 @@ export class ExecutiveFacet {
|
|
|
149
149
|
/** Stamp activity at `tick` (called at spawn and on each report). */
|
|
150
150
|
markActive( tick: number ): void { this._lastActiveTick = tick }
|
|
151
151
|
|
|
152
|
+
/** _reason() calls currently in flight — a real LLM call spans many ticks. */
|
|
153
|
+
private _inflight = 0
|
|
154
|
+
/**
|
|
155
|
+
* True while the facet has work the reaper must not discard: queued reports
|
|
156
|
+
* awaiting the pump, or an in-flight _reason() whose decision hasn't landed.
|
|
157
|
+
* The idle TTL only measures *quiet* facets — reaping a busy one destroys the
|
|
158
|
+
* listeners its pending decision needs, silently dropping a conversation reply.
|
|
159
|
+
*/
|
|
160
|
+
get busy(): boolean { return this._inflight > 0 || this._pendingReports.length > 0 }
|
|
161
|
+
|
|
152
162
|
/**
|
|
153
163
|
* Per-facet chunk handler — fires for every LLM token during _reason().
|
|
154
164
|
* Set by the creating engine (e.g. AuditionEngine) for entity-scoped streaming.
|
|
@@ -236,9 +246,27 @@ export class ExecutiveFacet {
|
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
// Legacy (bare facets in unit tests): trigger reasoning immediately.
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
249
|
+
this._launchReason( report )
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Launch _reason() with in-flight accounting. `busy` must hold for the whole
|
|
254
|
+
* span (a real LLM call is 10–30s ≈ many ticks) so the supervisor's idle
|
|
255
|
+
* reaper never destroys a facet whose decision is still coming — that would
|
|
256
|
+
* clear its listeners and silently drop the reply. Completion re-stamps
|
|
257
|
+
* activity so the idle TTL measures quiet time *after* the decision, not
|
|
258
|
+
* after the report that started it.
|
|
259
|
+
*/
|
|
260
|
+
private _launchReason( report: FacetReport ): void {
|
|
261
|
+
this._inflight++
|
|
262
|
+
this._reason( report )
|
|
263
|
+
.catch( err =>
|
|
264
|
+
logger.error( `[executive.facet] ${this.facetId} reasoning error:`, err )
|
|
265
|
+
)
|
|
266
|
+
.finally( () => {
|
|
267
|
+
this._inflight--
|
|
268
|
+
this.markActive( ( this._currentStateRef?.tick as number ) ?? this._lastActiveTick )
|
|
269
|
+
})
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
/**
|
|
@@ -257,9 +285,7 @@ export class ExecutiveFacet {
|
|
|
257
285
|
this._pendingReports = []
|
|
258
286
|
|
|
259
287
|
for( const report of batch )
|
|
260
|
-
this.
|
|
261
|
-
logger.error( `[executive.facet] ${this.facetId} reasoning error:`, err )
|
|
262
|
-
)
|
|
288
|
+
this._launchReason( report )
|
|
263
289
|
}
|
|
264
290
|
|
|
265
291
|
subscribe( listener: FacetEventListener ): () => void {
|
|
@@ -344,7 +344,7 @@ ${roleDescription}
|
|
|
344
344
|
${consciousnessArchitecture}
|
|
345
345
|
|
|
346
346
|
## Output Guidelines
|
|
347
|
-
- **actions**: Choose from effectors you know about. If uncertain, describe what you want to achieve in natural language and your body will try to match it.
|
|
347
|
+
- **actions**: Choose from effectors you know about. If uncertain, describe what you want to achieve in natural language and your body will try to match it. When enacting one of your available abilities that needs specifics (a query, a message, a value), supply them in the action's "args" object — e.g. {"type": "search_docs", "args": {"query": "tick loop design"}, ...}. Your body enacts the ability with exactly those args.
|
|
348
348
|
- **plans**: Include for goals without existing plans or where plans need revision. You may keep multiple plans per goal — set **planId** to act on a specific existing plan (validate/execute/revise/cancel); omit it to draft a new one. Your current plans are listed under "## Active Plans".
|
|
349
349
|
- **newBeliefs**: Extract patterns from experiences visible in your current state. Only record a belief if you can point to a specific observation that supports it — do not infer experiences you have no record of. Set 'evidence' honestly: 'single_observation' (first time noticing), 'recurring_pattern' (seen multiple times), 'strong_pattern' (deeply established).
|
|
350
350
|
- **introspection**: Include when significant events occurred or you notice patterns. When you spot a cognitive bias in your own reasoning, name it in 'identifiedBiases' using its common term where one fits (e.g. overgeneralization, confirmation bias, recency bias) — this lets your self-assessment line up with the patterns your faculties detect on their own.
|
|
@@ -643,6 +643,15 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
|
|
|
643
643
|
? `## Percepts (What You Notice)\n${context.percepts.slice( 0, 10 ).map( p => `- [${p.category}] ${p.summary} (salience: ${p.salience.toFixed( 2 )})` ).join( '\n' ) || 'Nothing notable'}`
|
|
644
644
|
: ''
|
|
645
645
|
|
|
646
|
+
// Host abilities afforded right now + what each is for. Framed as
|
|
647
|
+
// self-knowledge (things you *can* do), NOT a tool-call menu: the Will still
|
|
648
|
+
// expresses intent in natural language and the agency field enacts the fit.
|
|
649
|
+
const abilitiesBlock = ( context.abilities && context.abilities.length > 0 )
|
|
650
|
+
? `## Abilities Available Now\nThings you can do in this situation — name one as an action's "type" (with "args" for any specifics it needs) and your body enacts it:\n${context.abilities.map( a =>
|
|
651
|
+
`- **${a.name}**${a.target ? ` (toward ${a.target})` : ''}${a.description ? ` — ${a.description}` : ''}`
|
|
652
|
+
).join( '\n' )}`
|
|
653
|
+
: ''
|
|
654
|
+
|
|
646
655
|
const ruminationsBlock = has( 'ruminations' )
|
|
647
656
|
? `## Active Ruminations (retrieved memories & thoughts)\n${context.workingMemory.map( w => `- [${w.type}] ${w.summary} (activation: ${w.activation.toFixed( 2 )})` ).join( '\n' ) || 'Nothing actively held in mind'}`
|
|
648
657
|
: ''
|
|
@@ -694,6 +703,7 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
|
|
|
694
703
|
actionDiversity.trim(),
|
|
695
704
|
recentOutcomesBlock,
|
|
696
705
|
perceptsBlock,
|
|
706
|
+
abilitiesBlock,
|
|
697
707
|
ruminationsBlock,
|
|
698
708
|
recentIntrospection.trim(),
|
|
699
709
|
memoriesBlock,
|
|
@@ -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/host/boot.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/host/boot.ts — shared boot/shutdown for the `will` CLI hosts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Both hosts (`will mcp`, `will serve`) raise the same mind the same way:
|
|
6
|
+
// env-configured, woken from its PMA artifact when one exists (else born),
|
|
7
|
+
// optionally bridged onto external MCP servers whose tools become its own
|
|
8
|
+
// abilities, and hibernated back to the artifact exactly once on the way out.
|
|
9
|
+
// Only the protocol surface differs — that stays in each host.
|
|
10
|
+
//
|
|
11
|
+
// Env (shared):
|
|
12
|
+
// WILL_NAME display name (default "Will")
|
|
13
|
+
// WILL_IDENTITY persona prompt (default a minimal self)
|
|
14
|
+
// WILL_TIER basic | standard | full (default standard)
|
|
15
|
+
// WILL_LLM mock | anthropic (default: auto — anthropic when
|
|
16
|
+
// ANTHROPIC_API_KEY is set, else mock)
|
|
17
|
+
// WILL_TICK_MS ms per tick (default 1000)
|
|
18
|
+
// WILL_SEED deterministic seed (testing) (default unseeded/wall-time)
|
|
19
|
+
// WILL_PMA_PATH PMA artifact path (default ./.will/<name>.pma.json)
|
|
20
|
+
// WILL_MCP_SERVERS JSON array of MCP servers whose tools become the Will's
|
|
21
|
+
// OWN abilities: entries {command,args?,env?} or {url}.
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, resolve } from 'node:path'
|
|
26
|
+
import { setLogger } from '#core/logger'
|
|
27
|
+
import { Will, type CreateWillOptions } from '#sdk/will'
|
|
28
|
+
import type { PMASnapshot } from '#pma/index'
|
|
29
|
+
import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Route every engine log line to stderr. For `will mcp`, stdout is the MCP
|
|
33
|
+
* protocol channel and must stay pure; `will serve` keeps the same discipline
|
|
34
|
+
* so both hosts log identically (and Docker captures one stream).
|
|
35
|
+
*/
|
|
36
|
+
export function routeLogsToStderr(): void {
|
|
37
|
+
const err = ( level: string ) => ( msg: string, ...rest: unknown[] ) =>
|
|
38
|
+
console.error( `[will:${ level }] ${ msg }`, ...rest )
|
|
39
|
+
setLogger( { debug: () => {}, info: err( 'info' ), warn: err( 'warn' ), error: err( 'error' ) } )
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function slug( s: string ): string {
|
|
43
|
+
return s.toLowerCase().replace( /[^a-z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ) || 'will'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BootedWill {
|
|
47
|
+
will: Will
|
|
48
|
+
name: string
|
|
49
|
+
pmaPath: string
|
|
50
|
+
tickMs: number
|
|
51
|
+
engineTier: NonNullable<CreateWillOptions['engineTier']>
|
|
52
|
+
/** Run before hibernate on shutdown (close servers/transports). LIFO. */
|
|
53
|
+
onCleanup: ( fn: () => Promise<void> | void ) => void
|
|
54
|
+
/** Hibernate → persist → exit(0). Idempotent; SIGINT/SIGTERM already wired. */
|
|
55
|
+
shutdown: ( why: string ) => Promise<void>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Raise the mind from env config — wake from the artifact if one exists. */
|
|
59
|
+
export async function bootWillFromEnv(): Promise<BootedWill> {
|
|
60
|
+
const name = process.env.WILL_NAME ?? 'Will'
|
|
61
|
+
const pmaPath = resolve( process.env.WILL_PMA_PATH ?? `.will/${ slug( name ) }.pma.json` )
|
|
62
|
+
const tickMs = parseInt( process.env.WILL_TICK_MS ?? '1000' )
|
|
63
|
+
const engineTier = ( process.env.WILL_TIER as CreateWillOptions['engineTier'] ) ?? 'standard'
|
|
64
|
+
|
|
65
|
+
const opts: Omit<CreateWillOptions, 'identity'> = {
|
|
66
|
+
name, engineTier, tickMs,
|
|
67
|
+
...( process.env.WILL_LLM ? { llm: process.env.WILL_LLM as 'mock' | 'anthropic' } : {} ),
|
|
68
|
+
...( process.env.WILL_SEED ? { seed: parseInt( process.env.WILL_SEED ) } : {} ),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let will: Will
|
|
72
|
+
if( existsSync( pmaPath ) ){
|
|
73
|
+
const pma = JSON.parse( readFileSync( pmaPath, 'utf8' ) ) as PMASnapshot
|
|
74
|
+
will = await Will.wake( pma, opts )
|
|
75
|
+
console.error( `[will] ${ name } woke from ${ pmaPath }` )
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
will = await Will.create( {
|
|
79
|
+
...opts,
|
|
80
|
+
identity: { prompt: process.env.WILL_IDENTITY ?? `I am ${ name }, a persistent mind.` },
|
|
81
|
+
} )
|
|
82
|
+
console.error( `[will] ${ name } born (no artifact at ${ pmaPath } yet)` )
|
|
83
|
+
}
|
|
84
|
+
will.on( 'error', e => console.error( `[will] error: ${ e.message }` ) )
|
|
85
|
+
|
|
86
|
+
// Onward bridges: MCP servers whose tools become the Will's OWN abilities.
|
|
87
|
+
// Best-effort — a bad entry warns and is skipped; the mind still boots.
|
|
88
|
+
const cleanups: Array<() => Promise<void> | void> = []
|
|
89
|
+
if( process.env.WILL_MCP_SERVERS ){
|
|
90
|
+
try {
|
|
91
|
+
const sources = JSON.parse( process.env.WILL_MCP_SERVERS ) as McpToolsSource[]
|
|
92
|
+
for( const source of Array.isArray( sources ) ? sources : [] ){
|
|
93
|
+
try {
|
|
94
|
+
const { names, close } = await connectMcpEffectors( will, source )
|
|
95
|
+
cleanups.push( close )
|
|
96
|
+
console.error( `[will] ${ name } gained abilities: ${ names.join( ', ' ) }` )
|
|
97
|
+
}
|
|
98
|
+
catch( e ){ console.error( `[will] MCP bridge failed (skipped): ${ ( e as Error ).message }` ) }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch( e ){ console.error( `[will] WILL_MCP_SERVERS is not valid JSON — ignoring: ${ ( e as Error ).message }` ) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Hibernate exactly once on the way out — cleanups (LIFO), distill + stop, persist.
|
|
105
|
+
let leaving = false
|
|
106
|
+
const shutdown = async ( why: string ): Promise<void> => {
|
|
107
|
+
if( leaving ) return
|
|
108
|
+
leaving = true
|
|
109
|
+
for( const fn of cleanups.reverse() ) await Promise.resolve( fn() ).catch( () => {} )
|
|
110
|
+
try {
|
|
111
|
+
const pma = await will.hibernate()
|
|
112
|
+
mkdirSync( dirname( pmaPath ), { recursive: true } )
|
|
113
|
+
writeFileSync( pmaPath, JSON.stringify( pma ) )
|
|
114
|
+
console.error( `[will] ${ name } hibernated to ${ pmaPath } (${ why })` )
|
|
115
|
+
}
|
|
116
|
+
catch( e ){ console.error( `[will] hibernate failed: ${ ( e as Error ).message }` ) }
|
|
117
|
+
process.exit( 0 )
|
|
118
|
+
}
|
|
119
|
+
process.on( 'SIGINT', () => void shutdown( 'SIGINT' ) )
|
|
120
|
+
process.on( 'SIGTERM', () => void shutdown( 'SIGTERM' ) )
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
will, name, pmaPath, tickMs, engineTier: engineTier ?? 'standard',
|
|
124
|
+
onCleanup: fn => cleanups.push( fn ),
|
|
125
|
+
shutdown,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/host/utterances.ts — a host-side tap on a Will's speech
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Hosts that expose a Will over a request/response protocol (MCP tools, HTTP
|
|
6
|
+
// long-polls) share a timing problem: the Will may speak BETWEEN two calls —
|
|
7
|
+
// after a perceive round trip returns and before the caller asks for the next
|
|
8
|
+
// utterance. The tap buffers projections so nothing is lost in the gap, and
|
|
9
|
+
// `next()` gives the MCP/HTTP hosts one shared, honest await: drain the buffer
|
|
10
|
+
// first, else wait, else report silence (null — a choice, never an error).
|
|
11
|
+
// ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
import type { Will, WillMessage } from '#sdk/will'
|
|
14
|
+
|
|
15
|
+
const BUFFER_CAP = 50
|
|
16
|
+
|
|
17
|
+
export class UtteranceTap {
|
|
18
|
+
private readonly _will: Will
|
|
19
|
+
private readonly _pending: WillMessage[] = []
|
|
20
|
+
|
|
21
|
+
constructor( will: Will ){
|
|
22
|
+
this._will = will
|
|
23
|
+
will.on( 'message', m => {
|
|
24
|
+
this._pending.push( m )
|
|
25
|
+
if( this._pending.length > BUFFER_CAP ) this._pending.shift()
|
|
26
|
+
} )
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Consume the oldest buffered utterance (optionally only one addressed to `to`). */
|
|
30
|
+
takeBuffered( to?: string ): WillMessage | undefined {
|
|
31
|
+
if( this._pending.length === 0 ) return undefined
|
|
32
|
+
const i = to === undefined ? 0 : this._pending.findIndex( m => m.to === to )
|
|
33
|
+
if( i < 0 ) return undefined
|
|
34
|
+
return this._pending.splice( i, 1 )[0]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The next utterance: a buffered one if a projection already landed, else
|
|
39
|
+
* await up to `within` ms. `null` = the Will chose silence. An awaited
|
|
40
|
+
* message is also consumed from the buffer so it never replays.
|
|
41
|
+
*/
|
|
42
|
+
async next( within: number, to?: string ): Promise<WillMessage | null> {
|
|
43
|
+
const buffered = this.takeBuffered( to )
|
|
44
|
+
if( buffered ) return buffered
|
|
45
|
+
|
|
46
|
+
const msg = await this._will.nextUtterance( { within, ...( to ? { to } : {} ) } )
|
|
47
|
+
if( msg ){
|
|
48
|
+
const i = this._pending.findIndex( p => p.id === msg.id )
|
|
49
|
+
if( i >= 0 ) this._pending.splice( i, 1 )
|
|
50
|
+
}
|
|
51
|
+
return msg
|
|
52
|
+
}
|
|
53
|
+
}
|
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'
|
|
@@ -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
|
+
}
|