@mindot/will 0.1.1 → 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.
Files changed (36) hide show
  1. package/README.md +38 -3
  2. package/dist/index.d.ts +52 -7760
  3. package/dist/index.js +374 -86
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/cli.d.ts +1 -0
  6. package/dist/mcp/cli.js +26048 -0
  7. package/dist/mcp/cli.js.map +1 -0
  8. package/dist/mcp/effectors.d.ts +55 -0
  9. package/dist/mcp/effectors.js +76 -0
  10. package/dist/mcp/effectors.js.map +1 -0
  11. package/dist/will-B5eKs3Wv.d.ts +7903 -0
  12. package/package.json +38 -30
  13. package/src/cognition/agency/engines/action.selector.ts +3 -0
  14. package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -12
  15. package/src/cognition/agency/engines/deliberation.engine.ts +7 -2
  16. package/src/cognition/agency/engines/motor.schema.executor.ts +2 -0
  17. package/src/cognition/agency/schemas/external.ts +27 -10
  18. package/src/cognition/agency/schemas/repertoire.ts +12 -0
  19. package/src/cognition/agency/types.ts +51 -2
  20. package/src/cognition/faculties/executive.engine/commands.ts +47 -11
  21. package/src/cognition/faculties/executive.engine/context.ts +28 -0
  22. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +15 -1
  23. package/src/cognition/faculties/executive.engine/facet.ts +32 -6
  24. package/src/cognition/faculties/executive.engine/prompt.factory.ts +11 -1
  25. package/src/cognition/faculties/executive.engine/types.ts +24 -2
  26. package/src/cognition/index.ts +1 -1
  27. package/src/core/abstracts.ts +39 -12
  28. package/src/index.ts +6 -0
  29. package/src/mcp/cli.ts +129 -0
  30. package/src/mcp/effectors.ts +159 -0
  31. package/src/mcp/server.ts +167 -0
  32. package/src/sdk/will.ts +269 -44
  33. package/src/stem/index.ts +16 -0
  34. package/src/stem/mind.ts +11 -4
  35. package/src/stem/tracts/effector.controller.ts +1 -0
  36. package/src/types.ts +2 -0
package/package.json CHANGED
@@ -1,21 +1,47 @@
1
1
  {
2
2
  "name": "@mindot/will",
3
- "version": "0.1.1",
4
- "description": "Will — an engine for persistent machine minds. A Will is a continuously-ticking synthetic mind built from 38 cognitive faculties, a learning agency pipeline, and a dual-process executive — deterministic, persistent, and portable (PMA).",
3
+ "version": "0.2.0",
5
4
  "author": "Fabrice <fabrice8@github.com>",
6
- "license": "Apache-2.0",
7
- "packageManager": "bun@1.3.9",
8
- "engines": {
9
- "bun": ">=1.0.0"
10
- },
11
- "homepage": "https://github.com/mindot-ai/will#readme",
12
5
  "repository": {
13
6
  "type": "git",
14
7
  "url": "git+https://github.com/mindot-ai/will.git"
15
8
  },
9
+ "bin": {
10
+ "will": "dist/mcp/cli.js"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "devDependencies": {
14
+ "@types/bun": "latest",
15
+ "tsup": "^8.5.1",
16
+ "typescript": "^5.9.0",
17
+ "vitest": "^4.1.5"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
21
+ "zod": "^4.0.0"
22
+ },
23
+ "optionalDependencies": {
24
+ "socket.io-client": "^4.8.1"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/index.js",
29
+ "types": "./dist/index.d.ts"
30
+ },
31
+ "./mcp": {
32
+ "import": "./dist/mcp/effectors.js",
33
+ "types": "./dist/mcp/effectors.d.ts"
34
+ }
35
+ },
16
36
  "bugs": {
17
37
  "url": "https://github.com/mindot-ai/will/issues"
18
38
  },
39
+ "description": "Will — an engine for persistent machine minds. A Will is a continuously-ticking synthetic mind built from 38 cognitive faculties, a learning agency pipeline, and a dual-process executive — deterministic, persistent, and portable (PMA).",
40
+ "engines": {
41
+ "bun": ">=1.0.0"
42
+ },
43
+ "files": ["dist", "src"],
44
+ "homepage": "https://github.com/mindot-ai/will#readme",
19
45
  "keywords": [
20
46
  "ai",
21
47
  "agent",
@@ -30,19 +56,11 @@
30
56
  "mind",
31
57
  "deterministic"
32
58
  ],
59
+ "license": "Apache-2.0",
60
+ "packageManager": "bun@1.3.9",
33
61
  "publishConfig": {
34
62
  "access": "public"
35
63
  },
36
- "type": "module",
37
- "main": "./dist/index.js",
38
- "types": "./dist/index.d.ts",
39
- "exports": {
40
- ".": {
41
- "import": "./dist/index.js",
42
- "types": "./dist/index.d.ts"
43
- }
44
- },
45
- "files": ["dist", "src"],
46
64
  "scripts": {
47
65
  "build": "tsup",
48
66
  "build:types": "tsc --project tsconfig.build.json",
@@ -58,16 +76,6 @@
58
76
  "test:watch": "vitest watch",
59
77
  "test:integration": "vitest run src/tests/integration"
60
78
  },
61
- "dependencies": {
62
- "@aws-sdk/client-s3": "^3.1048.0"
63
- },
64
- "optionalDependencies": {
65
- "socket.io-client": "^4.8.1"
66
- },
67
- "devDependencies": {
68
- "@types/bun": "latest",
69
- "tsup": "^8.5.1",
70
- "typescript": "^5.9.0",
71
- "vitest": "^4.1.5"
72
- }
79
+ "type": "module",
80
+ "types": "./dist/index.d.ts"
73
81
  }
@@ -293,6 +293,9 @@ export class ActionSelector implements CognitiveEngine {
293
293
  targetEntityId: s.affordance.targetEntityId,
294
294
  parameters: s.affordance.parameters,
295
295
  activation: s.activation,
296
+ // Carry the ability's meaning so the Deliberator weighs what each
297
+ // option is FOR, not bare labels.
298
+ ...( s.affordance.description ? { description: s.affordance.description } : {} ),
296
299
  // Channel B: flag a candidate that is an active plan's frontier step, so
297
300
  // the deliberation facet can own "this is my plan's next step" in-character.
298
301
  ...( s.affordance.source === 'plan' ? { fromPlan: true } : {} ),
@@ -149,24 +149,36 @@ export class AffordanceSynthesizer implements CognitiveEngine {
149
149
  })
150
150
  }
151
151
 
152
- const entitySchema = schemas.find( s => s.binds === 'entity' )
153
- if( entitySchema )
152
+ // Target-bound schemas are bound against each perceived known-entity of the
153
+ // matching kind: `binds: 'entity'` schemas (innate `reach-out` + host
154
+ // person-effectors) to sentient entities, `binds: 'object'` schemas to
155
+ // things — so the Will can direct `give`/`greet` at someone or `use`/`pick-up`
156
+ // at something in particular. Each (schema × target) enters as a candidate at
157
+ // the target's salience and competes through the attention cap.
158
+ const personSchemas = schemas.filter( s => s.binds === 'entity' )
159
+ const objectSchemas = schemas.filter( s => s.binds === 'object' )
160
+ if( personSchemas.length > 0 || objectSchemas.length > 0 )
154
161
  for( const [ id, e ] of state.entities ){
155
162
  if( e.type !== 'known-entity' ) continue
156
- const m = e.metadata
157
- if( str( m?.['kind'] ) !== 'sentient' ) continue
163
+ const m = e.metadata
164
+ const kind = str( m?.['kind'] )
165
+ const applicable = kind === 'sentient' ? personSchemas : kind === 'thing' ? objectSchemas : null
166
+ if( !applicable || applicable.length === 0 ) continue
158
167
  const keid = str( m?.['keid'] ) ?? id
159
168
  const fam = num( m?.['familiarity'], 0 )
160
169
  const val = num( m?.['valence'], 0 )
161
170
  const res = num( m?.['resolutionConfidence'], 0 )
162
- candidates.push({
163
- salience: fam * 0.6 + Math.max( 0, val ) * 0.3 + res * 0.1 + ( goalTargets.get( keid ) ?? 0 ),
164
- affordance: this._build( entitySchema, tick, state, valence, energyLow, skills, {
165
- evokedBy: id,
166
- targetEntityId: keid,
167
- parameters: { targetEntityName: str( m?.['name'] ) ?? keid },
168
- } ),
169
- })
171
+ const salience = fam * 0.6 + Math.max( 0, val ) * 0.3 + res * 0.1 + ( goalTargets.get( keid ) ?? 0 )
172
+ const name = str( m?.['name'] ) ?? keid
173
+ for( const schema of applicable )
174
+ candidates.push({
175
+ salience,
176
+ affordance: this._build( schema, tick, state, valence, energyLow, skills, {
177
+ evokedBy: id,
178
+ targetEntityId: keid,
179
+ parameters: { targetEntityName: name },
180
+ } ),
181
+ })
170
182
  }
171
183
 
172
184
  // ── ideomotor candidates: the executive's imagined actions, pre-activated ──
@@ -310,6 +322,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
310
322
  habitStrength,
311
323
  available: this._available( schema.preconditions, ( k ) => metric( state, k, 0 ) ),
312
324
  tags: schema.tags ?? [],
325
+ ...( schema.description ? { description: schema.description } : {} ),
313
326
  planBias: ctx.planBias,
314
327
  planId: ctx.planId,
315
328
  stepId: ctx.stepId,
@@ -333,6 +346,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
333
346
  habitStrength: a.habitStrength,
334
347
  available: a.available,
335
348
  tags: a.tags,
349
+ description: a.description,
336
350
  planBias: a.planBias,
337
351
  planId: a.planId,
338
352
  stepId: a.stepId,
@@ -50,6 +50,8 @@ interface Candidate {
50
50
  targetEntityId?: string
51
51
  parameters?: Record<string, unknown>
52
52
  activation?: number
53
+ /** The ability's declared meaning (external effectors) — what it is for. */
54
+ description?: string
53
55
  /** Channel B: this candidate is an active plan's current frontier step. */
54
56
  fromPlan?: boolean
55
57
  }
@@ -212,10 +214,13 @@ export class DeliberationEngine implements CognitiveEngine {
212
214
  lines.push( 'Your automatic action-selection was uncertain. Candidate actions:' )
213
215
  candidates.forEach( ( c, i ) => {
214
216
  const to = c.targetEntityId ? ` toward ${ c.targetEntityId }` : ''
217
+ // The ability's meaning, so the facet weighs what each option IS FOR rather
218
+ // than choosing among bare labels ("give" vs "attack" is a real difference).
219
+ const what = c.description ? ` — ${ c.description }` : ''
215
220
  // Channel B: name the plan link so the facet chooses as the self pursuing it,
216
221
  // not blindly among labels ("this one is the next step of the plan I'm on").
217
- const plan = c.fromPlan ? " your current plan's next step" : ''
218
- lines.push( `${ i + 1 }. ${ c.schema }${ to }${ plan }` )
222
+ const plan = c.fromPlan ? " (your current plan's next step)" : ''
223
+ lines.push( `${ i + 1 }. ${ c.schema }${ to }${ what }${ plan }` )
219
224
  })
220
225
  return lines.join( '\n' )
221
226
  }
@@ -464,6 +464,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
464
464
  payload: {
465
465
  schema: intent.schema, intentId: intent.id,
466
466
  targetEntityId: intent.targetEntityId, parameters: intent.parameters, tick,
467
+ // The ability's declared meaning, carried to the host handler.
468
+ description: this._resolve( intent.schema )?.description,
467
469
  },
468
470
  })
469
471
  }
@@ -21,37 +21,54 @@
21
21
  // entity schema. See CUSTOM_EFFECTOR_WIRING_TODO.md.
22
22
  // ─────────────────────────────────────────────────────────────
23
23
 
24
- import type { MotorSchema } from '#agency/types'
24
+ import type { MotorSchema, EffectorDeclaration } from '#agency/types'
25
25
  import { EXPLICIT_EFFECTORS } from '#agency/access.grants'
26
26
  import { INNATE_SCHEMA_BY_ID } from '#agency/schemas/innate'
27
27
 
28
28
  /** Default effort/energy demand for a host action when none is specified. */
29
29
  const DEFAULT_EXTERNAL_COST = 0.15
30
30
 
31
+ const clamp = ( n: number, lo: number, hi: number ): number => n < lo ? lo : n > hi ? hi : n
32
+
31
33
  /**
32
34
  * Build enactable MotorSchemas for a host's declared domain effectors. Comms
33
35
  * names and innate-shadowing names are filtered out; the rest become objectless,
34
36
  * `external`-tagged primitives that route to the host on enaction.
37
+ *
38
+ * A declaration may be a bare name (uniform prior) or an object carrying the
39
+ * ability's meaning + intrinsic priors (`description`, `cost`, `valence`,
40
+ * `preconditions`) — these seed the affordance so the ability competes with a
41
+ * real effort/reward/gating profile instead of a flat default. Reafference
42
+ * refines them from there.
35
43
  */
36
- export function externalSchemas( effectors?: string[] | null ): MotorSchema[] {
44
+ export function externalSchemas( effectors?: EffectorDeclaration[] | null ): MotorSchema[] {
37
45
  const seen = new Set<string>()
38
46
  const out: MotorSchema[] = []
39
47
 
40
- for( const name of effectors ?? [] ){
48
+ for( const decl of effectors ?? [] ){
49
+ const name = typeof decl === 'string' ? decl : decl?.name
41
50
  if( typeof name !== 'string' || name.length === 0 ) continue
42
51
  if( EXPLICIT_EFFECTORS.has( name ) ) continue // communication — handled by AccessGrants
43
52
  if( INNATE_SCHEMA_BY_ID.has( name ) ) continue // shadows an innate stance
44
53
  if( seen.has( name ) ) continue
45
54
  seen.add( name )
46
55
 
56
+ const meta = typeof decl === 'string' ? null : decl
57
+ const binds = meta?.binds === 'entity' ? 'entity' : meta?.binds === 'object' ? 'object' : 'none'
58
+ // Declared routing tags merged with the mandatory 'external'/'host' markers
59
+ // (the executor routes on 'external'; dedup keeps the set clean).
60
+ const tags = [ ...new Set( [ ...( meta?.tags ?? [] ), 'external', 'host' ] ) ]
61
+
47
62
  out.push({
48
- id: name,
49
- kind: 'primitive',
50
- source: 'external',
51
- cost: DEFAULT_EXTERNAL_COST,
52
- binds: 'none',
53
- baseValence: 0,
54
- tags: [ 'external', 'host' ],
63
+ id: name,
64
+ kind: 'primitive',
65
+ source: 'external',
66
+ cost: typeof meta?.cost === 'number' ? clamp( meta.cost, 0, 1 ) : DEFAULT_EXTERNAL_COST,
67
+ binds,
68
+ baseValence: typeof meta?.valence === 'number' ? clamp( meta.valence, -1, 1 ) : 0,
69
+ ...( meta?.preconditions ? { preconditions: meta.preconditions } : {} ),
70
+ ...( meta?.description ? { description: meta.description } : {} ),
71
+ tags,
55
72
  })
56
73
  }
57
74
 
@@ -71,6 +71,16 @@ export class SchemaRepertoire {
71
71
  this._skills.set( schema.id, freshSkill( schema.id, 0.4, 0 ) )
72
72
  }
73
73
 
74
+ /**
75
+ * Register a host effector's primitive schema at runtime (post-create
76
+ * `.effector()`). Unlike a composite it is NOT marked learned — it is a
77
+ * capacity the host granted, which the synthesizer surfaces immediately and
78
+ * reafference then builds skill on. Idempotent; re-registering updates it.
79
+ */
80
+ registerExternal( schema: MotorSchema ): void {
81
+ this._templates.set( schema.id, schema )
82
+ }
83
+
74
84
  // ── skills ────────────────────────────────────────────────────
75
85
  skills(): ReadonlyMap<string, LearnedSkill> { return this._skills }
76
86
  getSkill( id: string ): LearnedSkill | undefined { return this._skills.get( id ) }
@@ -221,6 +231,7 @@ function schemaEntity( s: MotorSchema ): EntityInput {
221
231
  preconditions: s.preconditions,
222
232
  composedOf: s.composedOf,
223
233
  baseValence: s.baseValence,
234
+ description: s.description,
224
235
  tags: s.tags,
225
236
  },
226
237
  }
@@ -241,6 +252,7 @@ function readSchema( m: Record<string, unknown> | undefined ): MotorSchema | und
241
252
  preconditions: meta['preconditions'] as MotorSchema['preconditions'],
242
253
  composedOf: Array.isArray( meta['composedOf'] ) ? meta['composedOf'] as string[] : undefined,
243
254
  baseValence: typeof meta['baseValence'] === 'number' ? meta['baseValence'] as number : undefined,
255
+ description: typeof meta['description'] === 'string' ? meta['description'] as string : undefined,
244
256
  tags: Array.isArray( meta['tags'] ) ? meta['tags'] as string[] : undefined,
245
257
  }
246
258
  }
@@ -29,8 +29,12 @@ export interface SchemaPrecondition {
29
29
  value: number
30
30
  }
31
31
 
32
- /** What kind of target a schema binds when it becomes an affordance. */
33
- export type SchemaBinding = 'none' | 'entity' | 'percept'
32
+ /**
33
+ * What kind of target a schema binds when it becomes an affordance.
34
+ * 'entity' = a sentient known-entity (a person); 'object' = a non-sentient
35
+ * known-entity (a thing); 'percept' = a salient percept; 'none' = objectless.
36
+ */
37
+ export type SchemaBinding = 'none' | 'entity' | 'object' | 'percept'
34
38
 
35
39
  /**
36
40
  * A MotorSchema — a parameterized control program, not a flat effector row.
@@ -52,9 +56,52 @@ export interface MotorSchema {
52
56
  composedOf?: string[]
53
57
  /** Intrinsic affective prior (−1..1) before any learning has occurred. */
54
58
  baseValence?: number
59
+ /** What the schema is *for* — its meaning, carried to the host on enaction. */
60
+ description?: string
55
61
  tags?: string[]
56
62
  }
57
63
 
64
+ /**
65
+ * How a host declares a domain effector to a Will. A bare string is the
66
+ * name-only form (`CUSTOM_ABILITY_WIRING.md` Phase 1). The object form seeds the
67
+ * ability as a *learnable affordance*: `description` is its meaning; `cost`,
68
+ * `valence`, and `preconditions` are the intrinsic priors the mind starts from
69
+ * before reafference refines them through use. Args still bind from the
70
+ * situation — this is not a tool-call parameter form.
71
+ */
72
+ export type EffectorDeclaration =
73
+ | string
74
+ | {
75
+ name: string
76
+ /** What the ability is for — its meaning, carried to perception + the host. */
77
+ description?: string
78
+ /** Intrinsic effort/energy demand 0..1 (default 0.15). */
79
+ cost?: number
80
+ /** Intrinsic affective prior −1..1 the mind expects before learning (default 0). */
81
+ valence?: number
82
+ /** Body-state gates; the affordance is unavailable unless all pass. */
83
+ preconditions?: SchemaPrecondition[]
84
+ /**
85
+ * Whether the ability targets a specific *perceived* target (default
86
+ * 'none'). 'entity' binds it to each sentient known-entity (a person),
87
+ * 'object' to each non-sentient one (a thing) — so the Will can
88
+ * `give`/`greet` someone or `use`/`pick-up` something in particular; the
89
+ * bound target reaches the host as `ctx.targetEntityId`.
90
+ */
91
+ binds?: 'none' | 'entity' | 'object'
92
+ /**
93
+ * Routing tags folded into the schema (merged with 'external'/'host').
94
+ * A tag the drive system recognises (e.g. 'social', 'nourishment') lets a
95
+ * homeostatic drive lift this ability in the competition when pressing.
96
+ */
97
+ tags?: string[]
98
+ }
99
+
100
+ /** The effector name of a declaration, whichever form it takes. */
101
+ export function effectorName( d: EffectorDeclaration ): string {
102
+ return typeof d === 'string' ? d : d.name
103
+ }
104
+
58
105
  /**
59
106
  * An Affordance — a possibility the current state affords *for this body now*,
60
107
  * with its parameters already bound from the thing that evoked it. Parameters
@@ -85,6 +132,8 @@ export interface Affordance {
85
132
  available: boolean
86
133
  /** Schema tags, carried so selection can route drives/risk without the registry. */
87
134
  tags: string[]
135
+ /** The ability's declared meaning (external effectors) — what it is for. */
136
+ description?: string
88
137
  /**
89
138
  * Top-down planning bias 0..1 — set when an executing plan's frontier step
90
139
  * projected this affordance (source 'plan'). It lifts activation in the
@@ -399,10 +399,15 @@ function resolveKnownEntity( target: string, state: ReadonlySimulationState ): s
399
399
  }
400
400
 
401
401
  /**
402
- * Turn the executive's imagined communicate actions into `ideomotor.intent` entities
403
- * (the ideomotor leg). The AffordanceSynthesizer surfaces them as competing reach-out
404
- * affordances next tick. Refreshed each executive cyclestale intents the executive
405
- * no longer imagines are deleted, so a momentary "I'd like to reach out" doesn't linger.
402
+ * Turn the executive's imagined actions into `ideomotor.intent` entities (the
403
+ * ideomotor leg). Two kinds are pre-activated: communicate actions (as reach-out
404
+ * toward the resolved entity) and *currently-afforded host abilities* an action
405
+ * whose type names an external affordance in the field, optionally carrying the
406
+ * `args` the executive consciously supplied (a mind formulating "search the docs
407
+ * for X" IS the situation providing the argument). The AffordanceSynthesizer
408
+ * surfaces them as competing candidates next tick — executive intention ENTERS
409
+ * the competition, it never bypasses it. Refreshed each executive cycle — stale
410
+ * intents the executive no longer imagines are deleted.
406
411
  */
407
412
  function buildIdeomotorIntents(
408
413
  output: ExecutiveOutputFull,
@@ -413,16 +418,47 @@ function buildIdeomotorIntents(
413
418
  const seen = new Set<string>()
414
419
  const priority = clamp01( output.confidence ?? 0.8 )
415
420
 
421
+ // The host abilities currently afforded (source 'external' in the live field) —
422
+ // the executive can only pre-activate what the situation actually offers.
423
+ const externalBySchema = new Map<string, string>()
424
+ for( const e of state.entities.values() ){
425
+ if( e.type !== 'affordance' ) continue
426
+ const m = e.metadata as Record<string, unknown> | undefined
427
+ if( m?.['source'] !== 'external' ) continue
428
+ const schema = typeof m['schema'] === 'string' ? m['schema'] as string : undefined
429
+ if( schema ) externalBySchema.set( schema.toLowerCase(), schema )
430
+ }
431
+
416
432
  for( const action of output.actions ){
417
- if( !COMMUNICATE_ACTION_TYPES.has( action.type.toLowerCase() ) ) continue
418
- if( !action.target ) continue
419
- const keid = resolveKnownEntity( action.target, state )
420
- if( !keid || seen.has( keid ) ) continue
421
- seen.add( keid )
433
+ const t = action.type.toLowerCase()
434
+
435
+ if( COMMUNICATE_ACTION_TYPES.has( t ) ){
436
+ if( !action.target ) continue
437
+ const keid = resolveKnownEntity( action.target, state )
438
+ if( !keid || seen.has( keid ) ) continue
439
+ seen.add( keid )
440
+ set.push({
441
+ id: `ideomotor-reach-out-${ keid }`,
442
+ type: 'ideomotor.intent',
443
+ metadata: { schema: 'reach-out', targetEntityId: keid, priority, origin: 'executive', tick: footprint.tickObserved },
444
+ })
445
+ continue
446
+ }
447
+
448
+ // A host ability the executive imagines enacting, with its conscious args.
449
+ const schema = externalBySchema.get( t )
450
+ if( !schema || seen.has( `ability:${ schema }` ) ) continue
451
+ seen.add( `ability:${ schema }` )
452
+ const keid = action.target ? resolveKnownEntity( action.target, state ) : undefined
422
453
  set.push({
423
- id: `ideomotor-reach-out-${ keid }`,
454
+ id: `ideomotor-${ schema }${ keid ? `-${ keid }` : '' }`,
424
455
  type: 'ideomotor.intent',
425
- metadata: { schema: 'reach-out', targetEntityId: keid, priority, origin: 'executive', tick: footprint.tickObserved },
456
+ metadata: {
457
+ schema,
458
+ ...( keid ? { targetEntityId: keid } : {} ),
459
+ ...( action.args && typeof action.args === 'object' ? { parameters: action.args } : {} ),
460
+ priority, origin: 'executive', tick: footprint.tickObserved,
461
+ },
426
462
  })
427
463
  }
428
464
 
@@ -322,6 +322,7 @@ export async function buildExecutiveContext(
322
322
  plans,
323
323
  relevantPlanIds,
324
324
  percepts,
325
+ abilities: extractAbilities( state ),
325
326
  workingMemory,
326
327
  memories,
327
328
  beliefs,
@@ -334,6 +335,33 @@ export async function buildExecutiveContext(
334
335
  }
335
336
  }
336
337
 
338
+ /**
339
+ * Host-declared abilities afforded to the Will right now — read from the current
340
+ * `affordance` field (source 'external', available). Gives System 2 knowledge of
341
+ * what it can do + what each is for; the innate stances are already in the
342
+ * preamble, so only host effectors surface here. Capped so a wide catalog can't
343
+ * bloat the prompt.
344
+ */
345
+ const MAX_SURFACED_ABILITIES = 8
346
+ export function extractAbilities( state: ReadonlySimulationState ): ExecutiveContext['abilities'] {
347
+ const out: NonNullable<ExecutiveContext['abilities']> = []
348
+ for( const e of state.entities.values() ){
349
+ if( e.type !== 'affordance' ) continue
350
+ const m = e.metadata as Record<string, unknown> | undefined
351
+ if( m?.['source'] !== 'external' || m?.['available'] === false ) continue
352
+ const name = typeof m?.['schema'] === 'string' ? m['schema'] as string : undefined
353
+ if( !name ) continue
354
+ const description = typeof m['description'] === 'string' ? m['description'] as string : undefined
355
+ const params = m['parameters'] as Record<string, unknown> | undefined
356
+ const target = m['targetEntityId']
357
+ ? ( typeof params?.['targetEntityName'] === 'string' ? params['targetEntityName'] as string : String( m['targetEntityId'] ) )
358
+ : undefined
359
+ out.push( { name, ...( description ? { description } : {} ), ...( target ? { target } : {} ) } )
360
+ if( out.length >= MAX_SURFACED_ABILITIES ) break
361
+ }
362
+ return out.length > 0 ? out : undefined
363
+ }
364
+
337
365
  /**
338
366
  * What the Will is currently focused on (the `task-switch-focus` entity from task.switcher)
339
367
  * plus the effort cost of switching away (the `task_switch.switch_cost` metric = effective
@@ -133,9 +133,15 @@ export class FacetSupervisor {
133
133
  }
134
134
 
135
135
  private _reapIdle( tick: number ): void {
136
- for( const [ id, facet ] of [ ...this._facets ] )
136
+ for( const [ id, facet ] of [ ...this._facets ] ){
137
+ // Never reap a busy facet: queued reports / an in-flight LLM call span
138
+ // many ticks (a real call is 10–30s), and destroying the facet clears the
139
+ // listeners its pending decision lands on — a conversation reply would
140
+ // vanish silently. The TTL measures *quiet* facets only.
141
+ if( facet.busy ) continue
137
142
  if( tick - facet.lastActiveTick > this._idleTtlTicks )
138
143
  this._reap( id, 'idle' )
144
+ }
139
145
  }
140
146
 
141
147
  /** Destroy + deregister a facet and notify its owner. Shared by the reaper + LRU eviction. */
@@ -163,8 +169,16 @@ export class FacetSupervisor {
163
169
  }
164
170
 
165
171
  private _leastRecentlyActive(): string | null {
172
+ // Prefer a quiet victim: evicting a busy facet drops its in-flight decision
173
+ // (same silent-loss mode the idle reaper guards against). Only when every
174
+ // facet is busy does pressure eviction fall back to the absolute LRU — a
175
+ // new conversation still preempts rather than being refused.
166
176
  let id: string | null = null
167
177
  let min = Infinity
178
+ for( const [ fid, facet ] of this._facets )
179
+ if( !facet.busy && facet.lastActiveTick < min ){ min = facet.lastActiveTick; id = fid }
180
+ if( id ) return id
181
+
168
182
  for( const [ fid, facet ] of this._facets )
169
183
  if( facet.lastActiveTick < min ){ min = facet.lastActiveTick; id = fid }
170
184
  return id
@@ -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._reason( report ).catch( err =>
240
- logger.error( `[executive.facet] ${this.facetId} reasoning error:`, err )
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._reason( report ).catch( err =>
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,