@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,154 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/serve/server.ts — a Will, exposed over plain HTTP (the sidecar)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// `will serve` for hosts that aren't Node and aren't MCP clients — a Python
|
|
6
|
+
// app, a game server, a cron job, anything that can speak HTTP. Same paradigm
|
|
7
|
+
// as the SDK facade and the MCP surface: a Will is a SUBJECT you speak to and
|
|
8
|
+
// observe, never a request/response function —
|
|
9
|
+
//
|
|
10
|
+
// POST /perceive deliver a stimulus → 202 (delivered, not answered)
|
|
11
|
+
// GET /next-utterance long-poll its next words; 200 {silence:true} is a
|
|
12
|
+
// real outcome, never an error
|
|
13
|
+
// GET /utterances SSE stream of projections (utterance/emotion/action)
|
|
14
|
+
// GET /state snapshot of its inner life
|
|
15
|
+
// POST /save checkpoint the living mind (non-destructive)
|
|
16
|
+
// GET /health liveness: name, tick, uptime
|
|
17
|
+
//
|
|
18
|
+
// There is deliberately no ask()-shaped route. Zero dependencies (node:http).
|
|
19
|
+
// Boot/persistence/shutdown wiring lives in the CLI; this module only maps a
|
|
20
|
+
// facade instance onto a server (testable on an ephemeral port).
|
|
21
|
+
// ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
24
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { dirname } from 'node:path'
|
|
26
|
+
import type { Will } from '#sdk/will'
|
|
27
|
+
import { UtteranceTap } from '#root/host/utterances'
|
|
28
|
+
|
|
29
|
+
export interface WillHttpOptions {
|
|
30
|
+
/** Where POST /save writes the PMA artifact. */
|
|
31
|
+
pmaPath?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SSE_HEARTBEAT_MS = 15_000
|
|
35
|
+
|
|
36
|
+
function json( res: ServerResponse, status: number, body: unknown ): void {
|
|
37
|
+
const text = JSON.stringify( body )
|
|
38
|
+
res.writeHead( status, { 'content-type': 'application/json', 'access-control-allow-origin': '*' } )
|
|
39
|
+
res.end( text )
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readJsonBody( req: IncomingMessage ): Promise<Record<string, unknown>> {
|
|
43
|
+
const chunks: Buffer[] = []
|
|
44
|
+
for await ( const c of req ) chunks.push( c as Buffer )
|
|
45
|
+
const raw = Buffer.concat( chunks ).toString( 'utf8' ).trim()
|
|
46
|
+
if( !raw ) return {}
|
|
47
|
+
return JSON.parse( raw ) as Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map a running Will onto an HTTP server. The caller owns the Will's lifecycle
|
|
52
|
+
* and calls `listen()`; everything here is the protocol surface.
|
|
53
|
+
*/
|
|
54
|
+
export function buildWillHttpServer( will: Will, opts: WillHttpOptions = {} ): Server {
|
|
55
|
+
const tap = new UtteranceTap( will )
|
|
56
|
+
const born = Date.now()
|
|
57
|
+
|
|
58
|
+
// SSE subscribers — every projection fans out to all open streams.
|
|
59
|
+
const streams = new Set<ServerResponse>()
|
|
60
|
+
const fanout = ( event: string, data: unknown ): void => {
|
|
61
|
+
const frame = `event: ${ event }\ndata: ${ JSON.stringify( data ) }\n\n`
|
|
62
|
+
for( const res of streams ) res.write( frame )
|
|
63
|
+
}
|
|
64
|
+
will.on( 'message', m => fanout( 'utterance', m ) )
|
|
65
|
+
will.on( 'emotion', a => fanout( 'emotion', a ) )
|
|
66
|
+
will.on( 'effector', a => fanout( 'action', a ) )
|
|
67
|
+
|
|
68
|
+
const server = createServer( ( req, res ) => {
|
|
69
|
+
void handle( req, res ).catch( err => {
|
|
70
|
+
if( !res.headersSent )
|
|
71
|
+
json( res, 500, { error: err instanceof Error ? err.message : String( err ) } )
|
|
72
|
+
else res.end()
|
|
73
|
+
} )
|
|
74
|
+
} )
|
|
75
|
+
|
|
76
|
+
async function handle( req: IncomingMessage, res: ServerResponse ): Promise<void> {
|
|
77
|
+
const url = new URL( req.url ?? '/', 'http://sidecar' )
|
|
78
|
+
const route = `${ req.method } ${ url.pathname }`
|
|
79
|
+
|
|
80
|
+
if( req.method === 'OPTIONS' ){
|
|
81
|
+
res.writeHead( 204, {
|
|
82
|
+
'access-control-allow-origin': '*',
|
|
83
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
84
|
+
'access-control-allow-headers': 'content-type',
|
|
85
|
+
} )
|
|
86
|
+
res.end()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
switch( route ){
|
|
91
|
+
case 'GET /health':
|
|
92
|
+
return json( res, 200, { ok: true, name: will.name, tick: will.state().tick, uptimeMs: Date.now() - born } )
|
|
93
|
+
|
|
94
|
+
case 'GET /state':
|
|
95
|
+
return json( res, 200, will.state() )
|
|
96
|
+
|
|
97
|
+
case 'POST /perceive': {
|
|
98
|
+
const body = await readJsonBody( req )
|
|
99
|
+
const text = typeof body.text === 'string' ? body.text : ''
|
|
100
|
+
if( !text ) return json( res, 400, { error: 'text is required' } )
|
|
101
|
+
await will.perceive( {
|
|
102
|
+
text,
|
|
103
|
+
...( typeof body.from === 'string' ? { from: body.from } : {} ),
|
|
104
|
+
...( typeof body.speaker === 'string' ? { speaker: body.speaker } : {} ),
|
|
105
|
+
} )
|
|
106
|
+
// 202: delivered into the sensory field — NOT answered. A response, if
|
|
107
|
+
// any, arrives on /utterances or /next-utterance; silence is valid.
|
|
108
|
+
return json( res, 202, { delivered: true, tick: will.state().tick } )
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'GET /next-utterance': {
|
|
112
|
+
const within = Math.min( Math.max( parseInt( url.searchParams.get( 'within_ms' ) ?? '15000' ) || 15_000, 100 ), 120_000 )
|
|
113
|
+
const from = url.searchParams.get( 'from' ) ?? undefined
|
|
114
|
+
const msg = await tap.next( within, from )
|
|
115
|
+
return msg
|
|
116
|
+
? json( res, 200, { utterance: msg } )
|
|
117
|
+
: json( res, 200, { silence: true, waitedMs: within } ) // a choice, not an error
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'GET /utterances': {
|
|
121
|
+
res.writeHead( 200, {
|
|
122
|
+
'content-type': 'text/event-stream',
|
|
123
|
+
'cache-control': 'no-cache',
|
|
124
|
+
'connection': 'keep-alive',
|
|
125
|
+
'access-control-allow-origin': '*',
|
|
126
|
+
} )
|
|
127
|
+
res.write( `event: hello\ndata: ${ JSON.stringify( { name: will.name, tick: will.state().tick } ) }\n\n` )
|
|
128
|
+
streams.add( res )
|
|
129
|
+
const heartbeat = setInterval( () => res.write( `: tick ${ will.state().tick }\n\n` ), SSE_HEARTBEAT_MS )
|
|
130
|
+
req.on( 'close', () => { clearInterval( heartbeat ); streams.delete( res ) } )
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'POST /save': {
|
|
135
|
+
if( !opts.pmaPath ) return json( res, 409, { error: 'no PMA path configured — set WILL_PMA_PATH' } )
|
|
136
|
+
const pma = await will.save()
|
|
137
|
+
mkdirSync( dirname( opts.pmaPath ), { recursive: true } )
|
|
138
|
+
writeFileSync( opts.pmaPath, JSON.stringify( pma ) )
|
|
139
|
+
return json( res, 200, { saved: true, path: opts.pmaPath } )
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
return json( res, 404, {
|
|
144
|
+
error: `no such route: ${ route }`,
|
|
145
|
+
routes: [ 'GET /health', 'GET /state', 'POST /perceive', 'GET /next-utterance', 'GET /utterances (SSE)', 'POST /save' ],
|
|
146
|
+
} )
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Close open SSE streams when the server closes (so close() can complete).
|
|
151
|
+
server.on( 'close', () => { for( const res of streams ) res.end(); streams.clear() } )
|
|
152
|
+
|
|
153
|
+
return server
|
|
154
|
+
}
|
package/src/stem/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ import { TransportController } from '#stem/tracts/transport.controller'
|
|
|
38
38
|
import { InboundQueue } from '#stem/tracts/inbound.queue'
|
|
39
39
|
import type { ExternalTransport } from '#stem/tracts/transport'
|
|
40
40
|
import { effectorController } from '#stem/tracts/effector.controller'
|
|
41
|
+
import { externalSchemas } from '#agency/schemas/external'
|
|
42
|
+
import type { EffectorDeclaration } from '#agency/types'
|
|
41
43
|
import { SensoryController } from '#stem/tracts/sensory.controller'
|
|
42
44
|
import { BiographyWriter } from '#stem/tracts/biography.writer'
|
|
43
45
|
import { HealthReporter } from '#stem/tracts/health.reporter'
|
|
@@ -772,6 +774,20 @@ export class WillStem {
|
|
|
772
774
|
this._effector.setAllowed( this._get( id ), effectors )
|
|
773
775
|
}
|
|
774
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Register a host effector on a *running* Will (post-create `.effector()`).
|
|
779
|
+
* Builds its external schema and adds it to the live repertoire so the Will
|
|
780
|
+
* can actually perceive + enact it — a grant alone only gates; without the
|
|
781
|
+
* schema the ability could never be afforded. Comms names are no-ops here
|
|
782
|
+
* (governed by AccessGrants). This is a runtime mutation, like a grant change;
|
|
783
|
+
* the deterministic/replayable path is declaring effectors at create time.
|
|
784
|
+
*/
|
|
785
|
+
registerEffector( id: string, declaration: EffectorDeclaration ): void {
|
|
786
|
+
const repertoire = this._get( id ).cognition.schemaRepertoire
|
|
787
|
+
for( const schema of externalSchemas( [ declaration ] ) )
|
|
788
|
+
repertoire.registerExternal( schema )
|
|
789
|
+
}
|
|
790
|
+
|
|
775
791
|
/**
|
|
776
792
|
* Called by the host/WorldInterface after executing a host-owned effector.
|
|
777
793
|
* `invocationId` is the correlation handle the host echoed (the awaiting
|
package/src/stem/mind.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { InstructionIntake } from '#agency/engines/instruction.intake'
|
|
|
40
40
|
import { SchemaRepertoire } from '#agency/schemas/repertoire'
|
|
41
41
|
import { INNATE_SCHEMAS } from '#agency/schemas/innate'
|
|
42
42
|
import { externalSchemas } from '#agency/schemas/external'
|
|
43
|
+
import { effectorName, type EffectorDeclaration } from '#agency/types'
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
import {
|
|
@@ -260,8 +261,12 @@ export interface WillConfig {
|
|
|
260
261
|
*
|
|
261
262
|
* null or omitted = no communication effectors (minimal default).
|
|
262
263
|
* Example: ['listen', 'talk', 'text'] enables inbound + text outbound.
|
|
264
|
+
*
|
|
265
|
+
* A domain effector may be a bare name or an object carrying its meaning +
|
|
266
|
+
* intrinsic priors: `{ name, description?, cost?, valence?, preconditions? }`
|
|
267
|
+
* (see EffectorDeclaration). Comms names are always bare.
|
|
263
268
|
*/
|
|
264
|
-
allowedGenericEffectors?:
|
|
269
|
+
allowedGenericEffectors?: EffectorDeclaration[] | null
|
|
265
270
|
|
|
266
271
|
/**
|
|
267
272
|
* When true the executive engine uses a canned mock LLM response instead of
|
|
@@ -506,7 +511,7 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
|
|
|
506
511
|
// creation; warnings surface; safe issues are sanitized in place.
|
|
507
512
|
const idGuard = validateWillIdentity({
|
|
508
513
|
identity: config.identity,
|
|
509
|
-
effectors: Array.isArray( config.allowedGenericEffectors ) ? config.allowedGenericEffectors : ( profile?.effectors ?? null ),
|
|
514
|
+
effectors: ( Array.isArray( config.allowedGenericEffectors ) ? config.allowedGenericEffectors : ( profile?.effectors ?? null ) )?.map( effectorName ) ?? null,
|
|
510
515
|
profileContext: profile?.context,
|
|
511
516
|
})
|
|
512
517
|
if( !idGuard.ok )
|
|
@@ -655,13 +660,15 @@ function _constructCognition(
|
|
|
655
660
|
// This matters when a profile Will is created without specifying effectors:
|
|
656
661
|
// the DB stores null, the service passes null, and profile effectors must win.
|
|
657
662
|
// An empty array [] means "explicitly no effectors" (survives restart correctly).
|
|
658
|
-
const resolvedEffectors = Array.isArray( config.allowedGenericEffectors )
|
|
663
|
+
const resolvedEffectors: EffectorDeclaration[] | null = Array.isArray( config.allowedGenericEffectors )
|
|
659
664
|
? config.allowedGenericEffectors
|
|
660
665
|
: ( profile?.effectors ?? null )
|
|
666
|
+
// Name-only view for the grant / permission surfaces (comms gating is by name).
|
|
667
|
+
const resolvedEffectorNames = resolvedEffectors?.map( effectorName ) ?? null
|
|
661
668
|
|
|
662
669
|
// Agency-native permission / sense-gate authority, seeded from the resolved
|
|
663
670
|
// grant list. The senses + reply path read this. (Replaced effectorRegistry.)
|
|
664
|
-
const accessGrants = new AccessGrants(
|
|
671
|
+
const accessGrants = new AccessGrants( resolvedEffectorNames )
|
|
665
672
|
|
|
666
673
|
// ── Executive Engine ────────────────────────────────────────
|
|
667
674
|
// Created for all tiers so the Cognition type is always satisfied.
|
|
@@ -51,6 +51,7 @@ export class effectorController {
|
|
|
51
51
|
parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
|
|
52
52
|
targetEntityId: payload.targetEntityId as string | undefined,
|
|
53
53
|
reasoning: ( payload.reasoning as string ) ?? '',
|
|
54
|
+
...( typeof payload.description === 'string' ? { description: payload.description } : {} ),
|
|
54
55
|
tick: ( payload.tick as number ) ?? 0,
|
|
55
56
|
timestamp: Date.now()
|
|
56
57
|
})
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface effectorInvocation {
|
|
|
29
29
|
parameters: Record<string, unknown>
|
|
30
30
|
targetEntityId: string | undefined
|
|
31
31
|
reasoning: string
|
|
32
|
+
/** The ability's declared meaning (from its EffectorDeclaration), when present. */
|
|
33
|
+
description?: string
|
|
32
34
|
tick: number
|
|
33
35
|
timestamp: number
|
|
34
36
|
}
|