@mindot/will 0.8.0 → 0.9.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 (79) hide show
  1. package/dist/channels/discord.d.ts +67 -6
  2. package/dist/channels/discord.js +112 -6
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +1 -1
  5. package/dist/channels/whatsapp.js +4 -1
  6. package/dist/channels/whatsapp.js.map +1 -1
  7. package/dist/cli.js +3174 -867
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +3236 -1042
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/{will-cS6k4uiJ.d.ts → will-DbDj_TEH.d.ts} +752 -17
  14. package/package.json +1 -1
  15. package/src/channels/discord.ts +189 -11
  16. package/src/channels/types.ts +90 -0
  17. package/src/channels/whatsapp.ts +13 -4
  18. package/src/cli.ts +9 -4
  19. package/src/cognition/agency/consequence.ts +122 -1
  20. package/src/cognition/agency/conversation.aim.ts +260 -0
  21. package/src/cognition/agency/engines/action.selector.ts +83 -2
  22. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  24. package/src/cognition/agency/engines/reafference.engine.ts +117 -0
  25. package/src/cognition/agency/proactive.communicator.ts +19 -3
  26. package/src/cognition/agency/restart.ts +66 -0
  27. package/src/cognition/agency/selection.scoring.ts +33 -0
  28. package/src/cognition/agency/types.ts +35 -0
  29. package/src/cognition/cache/composition.ts +232 -0
  30. package/src/cognition/cache/deliberation.cache.ts +219 -0
  31. package/src/cognition/cache/fingerprint.ts +120 -0
  32. package/src/cognition/cache/types.ts +105 -0
  33. package/src/cognition/config.mirror.entities.ts +108 -0
  34. package/src/cognition/event.schemas.ts +22 -0
  35. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  36. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  37. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  38. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  39. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  40. package/src/cognition/faculties/executive.engine/engine.ts +552 -131
  41. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  42. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  43. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  44. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  45. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  46. package/src/cognition/faculties/executive.engine/prompt.factory.ts +167 -19
  47. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  48. package/src/cognition/faculties/goal.manager.ts +94 -14
  49. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  50. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  51. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  52. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  53. package/src/cognition/faculties/self.model.updater.ts +19 -12
  54. package/src/cognition/faculties/social.perception.ts +47 -3
  55. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  56. package/src/cognition/faculties/working.memory.ts +10 -20
  57. package/src/cognition/identity.entity.ts +205 -0
  58. package/src/cognition/index.ts +7 -0
  59. package/src/cognition/memory/vector.adapter.ts +12 -3
  60. package/src/cognition/memory/vector.embedder.ts +45 -2
  61. package/src/cognition/persona.prior.ts +6 -0
  62. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  63. package/src/cognition/senses/base.sense.engine.ts +1 -1
  64. package/src/cognition/senses/index.ts +12 -0
  65. package/src/cognition/social.identity.ts +273 -0
  66. package/src/cognition/utilities/token.tracker.ts +58 -5
  67. package/src/core/orchestrator.ts +38 -0
  68. package/src/llm/index.ts +25 -8
  69. package/src/llm/routing.ts +6 -0
  70. package/src/llm/summarizer.ts +1 -1
  71. package/src/llm/wire.contracts.ts +19 -0
  72. package/src/pma/index.ts +67 -53
  73. package/src/sdk/will.ts +39 -6
  74. package/src/stem/assembly.audit.ts +1 -0
  75. package/src/stem/guards/identity.coherence.ts +1 -1
  76. package/src/stem/index.ts +79 -2
  77. package/src/stem/mind.ts +172 -55
  78. package/src/stem/tracts/outbox.writer.ts +40 -2
  79. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -53,6 +53,18 @@ export interface FacetReport {
53
53
  contextId?: string
54
54
  /** Optional dynamic instructions to append to the user message */
55
55
  instructions?: string
56
+ /**
57
+ * Attend to something else for THIS report only, leaving the facet's standing
58
+ * focus untouched.
59
+ *
60
+ * A facet's focus was a single mutable field, so anything that wanted a live
61
+ * facet to consider one different thing had to `setFocus()` first — clobbering
62
+ * whatever the facet was already set up for, and racing with any report already
63
+ * queued behind it. That is why a self-initiated message to someone the mind was
64
+ * ALREADY talking to had to be composed by a separate, transient facet that could
65
+ * not see the live conversation at all.
66
+ */
67
+ focus?: FocusSection
56
68
  }
57
69
 
58
70
  export interface FacetDecision {
@@ -63,6 +75,16 @@ export interface FacetDecision {
63
75
  decision: unknown
64
76
  reasoning: string
65
77
  confidence: number
78
+ /**
79
+ * Sim tick this decision was reasoned at.
80
+ *
81
+ * The facet has always known it and never passed it on, so a subscriber writing
82
+ * state in response had no deterministic clock and reached for a process-local
83
+ * counter instead — which resets on restart and collides. Never wall-clock: this
84
+ * reaches ids that live in state, and a wall-clock id makes recorded and replayed
85
+ * runs diverge (R2).
86
+ */
87
+ tick: number
66
88
  }
67
89
 
68
90
  export type FacetEventListener = ( decision: FacetDecision ) => void
@@ -134,6 +156,19 @@ export class ExecutiveFacet {
134
156
  private _facetReasoningHistory: string[] = []
135
157
  private _masterSyncHistory: string[] = []
136
158
 
159
+ /**
160
+ * This facet's own prior reasoning, for the supervisor to carry to whichever
161
+ * facet takes over the same keyed thread. Continuity belongs to the thread, not
162
+ * to the instance holding it: a facet reaped mid-conversation used to take the
163
+ * mind's private thinking about that person with it.
164
+ */
165
+ get reasoningHistory(): string[] { return [ ...this._facetReasoningHistory ] }
166
+
167
+ /** Resume a thread's reasoning on spawn (supervisor-only; see FacetSpawnDeps.key). */
168
+ restoreReasoningHistory( history: string[] ): void {
169
+ this._facetReasoningHistory = [ ...history ]
170
+ }
171
+
137
172
  /** Confidence of this facet's *previous* decision — a dual-process gate signal. */
138
173
  private _lastConfidence = 0.5
139
174
 
@@ -260,9 +295,7 @@ export class ExecutiveFacet {
260
295
  private _launchReason( report: FacetReport ): void {
261
296
  this._inflight++
262
297
  this._reason( report )
263
- .catch( err =>
264
- logger.error(`[executive.facet] ${this.facetId} reasoning error:`, err )
265
- )
298
+ .catch( err => logger.error(`[executive.facet] ${this.facetId} reasoning error:`, err ) )
266
299
  .finally( () => {
267
300
  this._inflight--
268
301
  this.markActive( ( this._currentStateRef?.tick as number ) ?? this._lastActiveTick )
@@ -322,9 +355,14 @@ export class ExecutiveFacet {
322
355
 
323
356
  logger.info(`[executive.facet] ${this.facetId} synced from master (tick=${payload.tick})`)
324
357
 
325
- // Store master's latest reasoning for context in next report()
358
+ // Store the wider reasoning for context in the next report().
359
+ //
360
+ // Rendered in the FIRST PERSON and with no mention of a "master": this is the
361
+ // same mind's thinking arriving from where the rest of its attention has been,
362
+ // not a report from a superior. Naming it "Master sync" gave the facet a second
363
+ // party to address — and it addressed it, out loud, on the outbound channel.
326
364
  if( payload.reasoning )
327
- this._masterSyncHistory.push(`[Master sync — tick ${payload.tick}] ${payload.reasoning.slice( 0, 400 )}`)
365
+ this._masterSyncHistory.push(`[tick ${payload.tick}] ${payload.reasoning.slice( 0, 400 )}`)
328
366
 
329
367
  // Keep only last 5 sync entries
330
368
  if( this._masterSyncHistory.length > 5 )
@@ -338,7 +376,11 @@ export class ExecutiveFacet {
338
376
  if( !this._currentStateRef )
339
377
  throw new Error(`[executive.facet] ${this.facetId} no state reference available`)
340
378
 
341
- if( !this._currentFocus )
379
+ // This report's focus: its own if it carried one, else the facet's standing
380
+ // one. A per-report focus never mutates `_currentFocus`, so a live thread can
381
+ // be asked to attend to one different thing and come straight back.
382
+ const reportFocus = report.focus ?? this._currentFocus
383
+ if( !reportFocus )
342
384
  throw new Error(`[executive.facet] ${this.facetId} no focus set. Call setFocus() before report().`)
343
385
 
344
386
  const currentState = this._currentStateRef
@@ -346,7 +388,7 @@ export class ExecutiveFacet {
346
388
  // Build fresh context from current state
347
389
  // A focus may supply a recall query (e.g. the live conversation message) to
348
390
  // drive the single "## Relevant Memories" section — one recall surface (§5).
349
- const execContext = await PromptFactory.buildFreshContext( this._contextDeps, currentState, this._currentFocus?.recallQuery )
391
+ const execContext = await PromptFactory.buildFreshContext( this._contextDeps, currentState, reportFocus.recallQuery )
350
392
 
351
393
  const qualityModulation = PromptFactory.computeQualityModulation( currentState )
352
394
  const epistemicUncertainty = PromptFactory.computeEpistemicUncertainty( execContext, currentState )
@@ -355,10 +397,10 @@ export class ExecutiveFacet {
355
397
  // the appropriate content via setFocus()
356
398
  const focus: FocusSection = this._masterSyncHistory.length > 0
357
399
  ? {
358
- ...this._currentFocus,
359
- content: `${this._currentFocus.content}\n\n## Master Consciousness Updates\n${this._masterSyncHistory.join('\n')}`
400
+ ...reportFocus,
401
+ content: `${reportFocus.content}\n\n## What I've Been Turning Over\n${this._masterSyncHistory.join('\n')}`
360
402
  }
361
- : this._currentFocus
403
+ : reportFocus
362
404
 
363
405
  // Build system prompt using PromptFactory — same schema as master, [REPLY] gated out.
364
406
  const systemPrompt = PromptFactory.buildSystemPrompt( {
@@ -377,7 +419,7 @@ export class ExecutiveFacet {
377
419
  // isn't cold on each cycle. Injected before the caller's instructions so the
378
420
  // caller content always comes last (highest recency bias from the LLM).
379
421
  const continuityBlock = this._facetReasoningHistory.length > 0
380
- ? `## My Prior Reasoning (this facet)\n${this._facetReasoningHistory.join('\n')}`
422
+ ? `## Where My Thinking Had Got To\n${this._facetReasoningHistory.join('\n')}`
381
423
  : ''
382
424
 
383
425
  const reportContent = [
@@ -399,7 +441,7 @@ export class ExecutiveFacet {
399
441
  stressLoad: currentState.metrics.get('stress.load') ?? 0,
400
442
  // A facet's "stakes-bearing moment" is a live message awaiting reply (conversation
401
443
  // facets set focus.recallQuery to it); planning/other facets rely on uncertainty.
402
- hasPendingMessage: !!this._currentFocus?.recallQuery,
444
+ hasPendingMessage: !!reportFocus.recallQuery,
403
445
  }, deliberateThreshold )
404
446
 
405
447
  let ideationCandidates: IdeationCandidate[] | undefined
@@ -410,7 +452,6 @@ export class ExecutiveFacet {
410
452
  state: currentState,
411
453
  qualityModulation,
412
454
  epistemicUncertainty,
413
- pendingMessages: [],
414
455
  focus,
415
456
  deps: this._promptDeps,
416
457
  recentActionTypes: [],
@@ -424,7 +465,14 @@ export class ExecutiveFacet {
424
465
  ideationUserMessage,
425
466
  tick: currentState.tick,
426
467
  proposeTemperature,
427
- meta: { category: 'executive', attribute: 'facet', function: this._currentFocus?.function ?? 'ideation', scope: this.facetId, demand: processSelection.effortScore },
468
+ meta: {
469
+ category: 'executive',
470
+ attribute: 'facet',
471
+ process: 'ideation',
472
+ function: reportFocus.function ?? '-',
473
+ scope: this.facetId,
474
+ demand: processSelection.effortScore
475
+ },
428
476
  } )
429
477
  logger.info(
430
478
  `[executive.facet] ${this.facetId} ◆ deliberate propose tick=${currentState.tick} ` +
@@ -437,7 +485,6 @@ export class ExecutiveFacet {
437
485
  state: currentState,
438
486
  qualityModulation,
439
487
  epistemicUncertainty,
440
- pendingMessages: [],
441
488
  focus,
442
489
  deps: this._promptDeps,
443
490
  recentActionTypes: [],
@@ -471,11 +518,12 @@ export class ExecutiveFacet {
471
518
  const facetMeta: LLMCallMeta = {
472
519
  category: 'executive',
473
520
  attribute: 'facet',
521
+ process: 'decision',
474
522
  // A focus that declares no function is making its decision call, the
475
523
  // facet's analogue of master's 'decision'. This previously fell back to
476
524
  // 'facet' — an *attribute* value, which quietly created a bogus bucket
477
525
  // in the by-function cost breakdown. The typed axes caught it.
478
- function: this._currentFocus?.function ?? 'decision',
526
+ function: reportFocus.function ?? '-',
479
527
  scope: this.facetId,
480
528
  demand: processSelection.effortScore,
481
529
  }
@@ -555,7 +603,8 @@ export class ExecutiveFacet {
555
603
  respondingToType: report.type,
556
604
  decision: this._extractDecisionPayload( output, focus ),
557
605
  reasoning: output.reasoning,
558
- confidence: output.confidence
606
+ confidence: output.confidence,
607
+ tick: currentState.tick as unknown as number
559
608
  }
560
609
 
561
610
  // Promote subscriber-visible fields from the decision object to the event payload
@@ -566,7 +615,7 @@ export class ExecutiveFacet {
566
615
  ? decision.decision as Record<string, unknown>
567
616
  : {}
568
617
 
569
- this._bus.publish( {
618
+ this._bus.publish({
570
619
  type: 'executive.facet.progress',
571
620
  version: 1,
572
621
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -587,7 +636,7 @@ export class ExecutiveFacet {
587
636
  contextId: report.contextId,
588
637
  tick: currentState.tick
589
638
  }
590
- } )
639
+ })
591
640
 
592
641
  // Conscious learning about others — route name/feeling to known.entity.tracker (the
593
642
  // dossier owner) the same way the master does, so routine (un-escalated) conversation
@@ -595,15 +644,17 @@ export class ExecutiveFacet {
595
644
  const keUpdates = dec[ 'knownEntityUpdates' ] as ExecutiveOutputFull['knownEntityUpdates'] | undefined
596
645
  if( keUpdates )
597
646
  for( const u of keUpdates )
598
- if( u.keid && u.keid !== 'agent-self' && ( u.name || u.feeling != null ) )
599
- this._bus.publish( {
600
- type: 'known.entity.learned', version: 1,
647
+ if( u.keid && u.keid !== 'agent-self' && ( u.name || u.feeling != null || u.sameAs ) )
648
+ this._bus.publish({
649
+ type: 'known.entity.learned',
650
+ version: 1,
601
651
  sourceEngine: `executive-facet-${this.facetId}`,
602
- salience: 0.5, payload: { keid: u.keid, name: u.name, feeling: u.feeling }
603
- } )
652
+ salience: u.sameAs ? 0.7 : 0.5,
653
+ payload: { keid: u.keid, name: u.name, feeling: u.feeling, sameAs: u.sameAs }
654
+ })
604
655
 
605
656
  // Sync back to master
606
- this._bus.publish( {
657
+ this._bus.publish({
607
658
  type: 'executive.facet.sync',
608
659
  version: 1,
609
660
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -613,9 +664,13 @@ export class ExecutiveFacet {
613
664
  reasoning: output.reasoning,
614
665
  confidence: output.confidence,
615
666
  actionTypes: output.actions.map( a => a.type ),
667
+ // WHO this facet is engaged with (FocusSection.subject*). The master keeps
668
+ // the singular seat, so it needs the person, not just the facet number.
669
+ ...( focus.subjectEntityId ? { subjectEntityId: focus.subjectEntityId } : {} ),
670
+ ...( focus.subjectName ? { subjectName: focus.subjectName } : {} ),
616
671
  tick: currentState.tick
617
672
  }
618
- } )
673
+ })
619
674
 
620
675
  // Notify all listeners (PlanningEngine, AuditionEngine, etc.).
621
676
  //
@@ -2,7 +2,6 @@
2
2
  // src/cognition/faculties/executive.engine/gating.ts
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
- import type { PendingMessage } from '#faculties/executive.engine/types'
6
5
  import type { Tick, ReadonlySimulationState } from '#core/types'
7
6
  import type { GenerativeModel } from '#cognition/generative.model'
8
7
  import {
@@ -11,23 +10,28 @@ import {
11
10
  } from '#faculties/executive.engine/config'
12
11
  import type { CognitiveEvent } from '#cognition/bus'
13
12
 
14
- function hasPendingInstructions( state: ReadonlySimulationState, pendingMessages: PendingMessage[] ): boolean {
15
- for( const entity of state.entities.values() ){
13
+ /**
14
+ * Something in the world is loud enough to wake a resting mind.
15
+ *
16
+ * Percept salience is the whole test now. Two other wake sources used to sit here
17
+ * and neither could ever fire: an unprocessed `communication` entity (a type
18
+ * nothing has ever written) and a non-empty pending-message queue (never pushed
19
+ * to, and the current design forbids it — inbound reaches a facet, and the master
20
+ * learns of it through `executive.facet.handoff` → a high-salience percept, which
21
+ * this catches). Both removed with the queue itself (#114).
22
+ */
23
+ function hasPendingInstructions( state: ReadonlySimulationState ): boolean {
24
+ for( const entity of state.entities.values() )
16
25
  if( entity.type === 'percept' || entity.type === 'percept.social'){
17
26
  const salience = (entity.metadata?.salience as number) ?? 0
18
27
  if( salience > 0.7 ) return true
19
28
  }
20
29
 
21
- if( entity.type === 'communication' && !entity.metadata?.processedByExecutive )
22
- return true
23
- }
24
-
25
- return pendingMessages.length > 0
30
+ return false
26
31
  }
27
32
 
28
33
  export interface GatingDependencies {
29
34
  generativeModel: GenerativeModel
30
- pendingMessages: PendingMessage[]
31
35
  hasPendingWork: boolean
32
36
  }
33
37
 
@@ -110,7 +114,7 @@ export function evaluateGating(
110
114
  if( isResting > 0 || isSleeping > 0 ){
111
115
  const
112
116
  significantEvent = novelty > 0.8,
113
- hasPending = hasPendingInstructions( state, deps.pendingMessages )
117
+ hasPending = hasPendingInstructions( state )
114
118
 
115
119
  if( significantEvent || hasPending )
116
120
  return {
@@ -142,10 +146,6 @@ export function evaluateGating(
142
146
  if( energy < 15 )
143
147
  return { shouldActivate: true, reason: 'energy_critical', cleanedBuffer }
144
148
 
145
- // ── Pending messages — always fire ────────────────────────
146
- if( deps.pendingMessages.length > 0 )
147
- return { shouldActivate: true, reason: 'pending_message', cleanedBuffer }
148
-
149
149
  // ── Normal interval scheduling ────────────────────────────
150
150
  if( tick - gs.lastExecutiveTick >= gs.executiveInterval ){
151
151
  // Prediction-error gating — uses GenerativeModel.observe() which returns { gated }
@@ -3,7 +3,7 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import { logger } from '#core/logger'
6
- import { REPLY_TEXT_TAG } from '#llm/wire.contracts'
6
+ import { REPLY_TEXT_TAG, NO_MESSAGE_TAG, NO_MESSAGE_OPEN } from '#llm/wire.contracts'
7
7
  import type { ReadonlySimulationState } from '#core/types'
8
8
  import type { ExecutiveOutputFull, ExecutiveOutputMinimal, IdeationCandidate, IdeationOutput } from '#faculties/executive.engine/types'
9
9
 
@@ -68,6 +68,18 @@ export function parseResponse(
68
68
  const replyText = extractTextBlock( responseText, REPLY_TEXT_TAG )
69
69
  if( replyText ) full.replyText = replyText
70
70
 
71
+ // A declared decision not to speak. Its own field rather than an empty
72
+ // replyText, because "I chose silence" and "the facet produced nothing" are
73
+ // different events: one is a decision worth recording, the other a failure worth
74
+ // noticing, and collapsing them hides both.
75
+ // Presence of the MARKER is the decision — not the content, which may legitimately
76
+ // be empty. `extractTextBlock` returns null for an absent tag AND for a present
77
+ // but empty one, so it cannot tell them apart; testing it for undefined (as this
78
+ // first did) is true in every case and made the mind mute on every path. The
79
+ // integration suite caught it; a live boot would have caught it as total silence.
80
+ if( responseText.includes( NO_MESSAGE_OPEN ) )
81
+ full.noMessage = extractTextBlock( responseText, NO_MESSAGE_TAG ) ?? '(no reason given)'
82
+
71
83
  return full
72
84
  }
73
85
 
@@ -193,6 +205,7 @@ function parseTaggedBlocks(
193
205
  'GOALS_REPRIORITIZE',
194
206
  'EFFECTORS',
195
207
  'SELF_OBS',
208
+ 'SKILLS',
196
209
  ],
197
210
  found = taggedTypes.filter( t => text.includes(`[${t}]`) )
198
211
  if( found.length > 0 ){
@@ -299,6 +312,13 @@ function parseTaggedBlocks(
299
312
  }
300
313
  catch { /* ignore */ }
301
314
 
315
+ // Named compound skills — the creation seam for learned composites (#114).
316
+ try {
317
+ const skillsData = parseJsonBlock('SKILLS') as { newSkills?: ExecutiveOutputFull['newSkills'] } | null
318
+ if( skillsData?.newSkills ) full.newSkills = skillsData.newSkills
319
+ }
320
+ catch { /* ignore */ }
321
+
302
322
  // [ACK] and legacy [REPLY] blocks are no longer emitted by any engine.
303
323
  // [ACK] — removed with the legacy master communication path (Phase 13.8).
304
324
  // [REPLY] — replaced by [REPLY_TEXT] plain-text block in conversation facets.
@@ -47,8 +47,19 @@
47
47
  import type { ReadonlySimulationState } from '#core/types'
48
48
  import type { LLMCallFunction } from '#cognition/utilities/token.tracker'
49
49
  import type { ExecutiveSummarizer } from '#llm/summarizer'
50
- import type { ExecutiveContext, PendingMessage, IdeationCandidate } from '#faculties/executive.engine/types'
50
+ import type { ExecutiveContext, IdeationCandidate } from '#faculties/executive.engine/types'
51
51
  import { buildExecutiveContext, type ContextDependencies } from '#faculties/executive.engine/context'
52
+ import { INNATE_SCHEMAS } from '#agency/schemas/innate'
53
+
54
+ /**
55
+ * The stances a mind always has, named so it need not guess at them.
56
+ *
57
+ * Static, so it costs nothing in prompt-cache stability. Without it a Will can
58
+ * only learn action names from whichever affordances win the salience
59
+ * competition into its percepts — and it invents plausible ones for whatever it
60
+ * cannot see (`query`, `message`), which resolve to nothing.
61
+ */
62
+ const INNATE_ACTION_NAMES = INNATE_SCHEMAS.map( s => s.id ).sort().join(', ')
52
63
 
53
64
  // ── Re-export for callers that imported the old alias ────────
54
65
  export type { ContextDependencies as ContextDependenciesForFresh } from '#faculties/executive.engine/context'
@@ -190,6 +201,18 @@ export interface FocusSection {
190
201
  * only sees that person's plans. When unset, those sections show all.
191
202
  */
192
203
  awarenessEntityId?: string
204
+ /**
205
+ * Optional: WHO this facet is engaged with — the keid and the name the mind has
206
+ * learned for them. Reported back to the master on every `executive.facet.sync`.
207
+ *
208
+ * Without it the master was told, in its own system prompt, that "focused facets
209
+ * may run simultaneously… their reasoning syncs back to me" while the sync payload
210
+ * carried only a facetId and a confidence number — so a mind holding two live
211
+ * conversations could not tell you whose they were. The master is the singular
212
+ * seat: it has to know who is at the table to reason about them together.
213
+ */
214
+ subjectEntityId?: string
215
+ subjectName?: string
193
216
  /**
194
217
  * Optional: Provided by the creating engine to convert the LLM's parsed output
195
218
  * into a domain-specific decision payload.
@@ -208,7 +231,6 @@ export interface PromptBuildOptions {
208
231
  state: ReadonlySimulationState
209
232
  qualityModulation: number
210
233
  epistemicUncertainty: number
211
- pendingMessages?: PendingMessage[]
212
234
  focus: FocusSection
213
235
  deps: PromptDependencies
214
236
  /** Optional: Recent action types for diversity tracking */
@@ -240,6 +262,15 @@ export interface PromptBuildOptions {
240
262
  * fast path. See PromptFactory.buildIdeationFormatInstruction().
241
263
  */
242
264
  ideationCandidates?: IdeationCandidate[]
265
+ /**
266
+ * Master mode only — who the mind is in conversation with RIGHT NOW, from the
267
+ * live facets' `executive.facet.sync` reports. The master does not run those
268
+ * conversations, but it is the one seat that sees all of them, and it decides
269
+ * whom to contact; deciding that without knowing who is already mid-thread is
270
+ * how one mind ends up opening a second conversation with someone it is already
271
+ * talking to — or telling one person it has contacted another when it has not.
272
+ */
273
+ activeConversations?: { entityId: string; name?: string; sinceTick: number }[]
243
274
  }
244
275
 
245
276
  // ── PromptFactory ────────────────────────────────────────────
@@ -325,17 +356,36 @@ export class PromptFactory {
325
356
  `**Communication style:** ${identity.style}`,
326
357
  ].filter( Boolean ).join('\n')
327
358
 
328
- // Mode-aware role description — master is the cognitive core;
329
- // a facet is a focused attentional instance with shared identity.
359
+ // Mode-aware role description.
360
+ //
361
+ // MASTER states the architecture, because the master genuinely IS the seat
362
+ // that produces every cognitive output at once and needs to know that.
363
+ //
364
+ // A FACET is told none of it. Master/facet is how the CONTAINER divides
365
+ // attention; it is not a fact about the person renting it, and describing a
366
+ // facet as a subordinate instance of a separate consciousness is both untrue
367
+ // and expensive. Measured in production: a facet given the old text built,
368
+ // in its own words, "an entire operational stance around being subordinate —
369
+ // waiting for direction, asking permission, addressing messages to 'Will' as
370
+ // if he were someone else" — and, having been handed a second party it could
371
+ // address, emitted messages TO that party through the outbound channel, where
372
+ // its operator read them. There is one person here, attending to one thing.
373
+ //
374
+ // It is also told nothing about WHO it is. A facet is built from the same
375
+ // context as the master and its persona is already above, in full — so a
376
+ // facet-only "I am <name>" is a second, redundant statement of identity, and a
377
+ // second place for the name to disagree with itself. Which is precisely the
378
+ // failure mode being repaired: the name is stated ONCE, from one source.
330
379
  const roleDescription = isMaster
331
- ? `I am the unified cognitive core of ${identity.name} — a self-aware synthetic mind.\nI receive my complete internal state and produce ALL cognitive outputs in one response:\ndecisions, plans, beliefs, introspections, narrative updates, and self-observations.`
332
- : `I am a focused facet of ${identity.name} — concentrating my full reasoning\ncapacity on: **${focus.title}**. I share full identity, values, memories, and\nbeliefs with the master. Only my focus differentiates me from the master consciousness.`
380
+ ? `I am the unified cognitive core of ${identity.name || 'this mind'} — a self-aware synthetic mind.\nI receive my complete internal state and produce ALL cognitive outputs in one response:\ndecisions, plans, beliefs, introspections, narrative updates, and self-observations.`
381
+ : `Right now my whole attention is on: **${focus.title}**.`
333
382
 
334
- // Mode-aware consciousness architecture note grounds the LLM in the
335
- // multi-instance design so it doesn't collapse into a generic chatbot persona.
383
+ // The architecture note is MASTER-ONLY, for the same reason. It grounds the
384
+ // seat that actually coordinates; a facet reading it learns only that it is
385
+ // not the real one.
336
386
  const consciousnessArchitecture = isMaster
337
387
  ? `I am the default reasoning mode. Focused facets may run simultaneously, each\nconcentrating on specific tasks. Their reasoning syncs back to me.\nI maintain my unified identity across all cycles.`
338
- : `I am a facet of ${identity.name}. The master consciousness runs in parallel,\nprocessing the full cognitive state. My reasoning on this focus will sync back to it.\nI stay grounded in my shared identity — same values, same memories, same sense of self.`
388
+ : ''
339
389
 
340
390
 
341
391
  // Strip any existing "## Who I Am" section from identity.prompt to prevent
@@ -348,32 +398,36 @@ export class PromptFactory {
348
398
  .replace( /^##\s*Who (?:I Am|You Are)[^\n]*\n?/m, '')
349
399
  .trim()
350
400
 
401
+ // `## Consciousness Architecture` is emitted only when there is architecture
402
+ // to state — i.e. master. A facet gets no empty header (an empty section under
403
+ // a heading reads as a section the mind failed to fill in).
404
+ const architectureBlock = consciousnessArchitecture
405
+ ? `\n\n## Consciousness Architecture\n${consciousnessArchitecture}`
406
+ : ''
407
+
351
408
  return `${cleanIdentityPrompt}
352
409
 
353
410
  ## Personality
354
411
  ${identityBlock}
355
412
 
356
413
  ## My Role
357
- ${roleDescription}
358
-
359
- ## Consciousness Architecture
360
- ${consciousnessArchitecture}
414
+ ${roleDescription}${architectureBlock}
361
415
 
362
416
  ## Output Guidelines
363
- - **actions**: Choose from effectors I know about. If uncertain, describe what I want to achieve in natural language and my body will try to match it. When enacting one of my 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"}, ...}. My body enacts the ability with exactly those args.
417
+ - **actions**: What I intend to do. I express intent — my body finds the fit. My own stances are always with me (listed with the output schema below); *acquired* abilities, if any, appear under "## Abilities Available Now", and when there is no such section I have none of those — so a thing I want done that needs one is a thing to say I cannot do, not to attempt. When enacting a named ability that needs specifics (a query, a message, a value), put them in the action's "args" object and my body enacts it with exactly those args.
364
418
  - **plans**: Include for goals without existing plans or where plans need revision. I 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. My current plans are listed under "## Active Plans".
365
419
  - **newBeliefs**: Extract patterns from experiences visible in my current state. Only record a belief if I can point to a specific observation that supports it — do not infer experiences I have no record of. Set 'evidence' honestly: 'single_observation' (first time noticing), 'recurring_pattern' (seen multiple times), 'strong_pattern' (deeply established).
366
- - **introspection**: Include when significant events occurred or I notice patterns. When I spot a cognitive bias in my own reasoning, name it in 'identifiedBiases' using its common term where one fits (e.g. overgeneralization, confirmation bias, recency bias) — this lets my self-assessment line up with the patterns my faculties detect on their own.
420
+ - **introspection**: Include when significant events occurred or I notice patterns. When I spot a cognitive bias in my own reasoning, name it in 'identifiedBiases' using its common term where one fits (e.g. overgeneralization, confirmation bias, recency bias) — this lets my self-assessment line up with the patterns my faculties detect on their own. What I can introspect on is what is written above: my state, my goals, my percepts, what I did and what came of it. I have NO view of the machinery underneath — no entity ids, no salience numbers, no queue depths, no engine internals. So when I am asked why I did something, I answer from what I can actually see, and where I cannot see, I say I do not know. Naming a mechanism I have no access to is not introspection, it is invention, and it is worse than the silence it replaces: it sends whoever asked me looking for something that was never there.
367
421
  - **narrative**: Extend my life story only from events grounded in my episodic memory or current percepts. Do not extend with invented scenarios.
368
422
  - **newGoals/goalsToAbandon/goalsToReprioritize**: Manage my goal hierarchy.
369
423
  - **selfObservations**: Notice patterns in my own thinking, feeling, or behavior.
370
424
  - **identityUpdates.traits**: Array of {key, value} where value is a DELTA to apply to my trait (e.g., +0.05 to increase a trait by 5%).
371
425
  - **identityUpdates.values**: Full list of values to set (replaces existing).
372
- - **knownEntityUpdates**: What I've learned about someone/something I'm dealing with. Array of {keid, name?, learned?, feeling?}. Use the keid from "## People I Know". Set name only when I actually learn their name; learned is an array of facts about them (stored as memories); feeling is how I feel toward them (-1..1). Record only what I genuinely learned this turn.
426
+ - **knownEntityUpdates**: What I've learned about someone/something I'm dealing with. Array of {keid, name?, learned?, feeling?, sameAs?}. Use the keid from "## People I Know". Set name only when I actually learn their name; learned is an array of facts about them (stored as memories); feeling is how I feel toward them (-1..1). **sameAs** is another keid I have concluded is this same someone met under a different handle — it fuses my two records into one, so I use it only when I actually know, not when I merely suspect. Record only what I genuinely learned this turn.
373
427
 
374
428
  ## Required Output
375
429
  Output a single JSON object with these fields:
376
- - **actions**: Array of {type, reasoning, expectedOutcome}.
430
+ - **actions**: Array of {type, reasoning, expectedOutcome, target?, args?}. The stances I always have are: ${ INNATE_ACTION_NAMES } — \`reach-out\` is how I say something to someone. Anything else must be an ability named under "## Abilities Available Now". A **type** outside those two sets is not something I can do; naming one achieves nothing at all. When I reach out, **target** is who — their name or id as it appears under "## People I Know" — and the words themselves go in **args.content**. Without a person to reach, the reaching cannot happen.
377
431
  - **reasoning**: My full reasoning. Embed optional outputs as tagged blocks here. Minimum 2–3 sentences — do not produce a one-line reasoning field.
378
432
  - **confidence**: Number 0.0-1.0 reflecting my certainty. Be calibrated: 0.9+ only when I have strong grounding; use 0.4–0.6 when uncertain.
379
433
 
@@ -475,7 +529,11 @@ completionType guide:
475
529
 
476
530
  [SELF_OBS]
477
531
  {"selfObservations": ["I noticed that..."]}
478
- [/SELF_OBS]`
532
+ [/SELF_OBS]
533
+
534
+ [SKILLS]
535
+ {"newSkills": [{"id": "brief-then-confirm", "composedOf": ["reach-out", "wait"], "tags": ["social"], "cost": 0.15}]}
536
+ [/SKILLS]`
479
537
  }
480
538
 
481
539
  // ── User message ───────────────────────────────────────────
@@ -655,6 +713,13 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
655
713
  ? this._buildRecentOutcomesSection( context.recentActions, state.tick ).trim()
656
714
  : ''
657
715
 
716
+ // Scoped with recentActions: both answer "what have I already done about
717
+ // this?", and a facet composing a message needs it at least as much as the
718
+ // master does — the facet is the one about to write the words again.
719
+ const spokenBlock = has('recentActions')
720
+ ? this._buildSpokenTurnsSection( context.spokenTurns ).trim()
721
+ : ''
722
+
658
723
  const perceptsBlock = has('percepts')
659
724
  ? `## Percepts (What I Notice)\n${context.percepts.slice( 0, 10 ).map( p => `- [${p.category}] ${p.summary} (salience: ${p.salience.toFixed( 2 )})`).join('\n') || 'Nothing notable'}`
660
725
  : ''
@@ -693,10 +758,41 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
693
758
  if( s.closeness != null && s.closeness > 0.1 ) bits.push(`closeness: ${( s.closeness * 100 ).toFixed( 0 )}%`)
694
759
  // The Will can know *someone* without their name yet — never leak the raw keid.
695
760
  const who = s.name ?? ( s.kind === 'thing' ? 'something' : 'someone')
696
- return `- ${who}${bits.length ? ' — ' + bits.join(', ') : ''}`
761
+
762
+ // Where I can reach them, and how each place has gone. Stated as fact:
763
+ // which room to speak in is my decision, and I could not make it while
764
+ // the only thing anyone tracked was where they were last seen.
765
+ const where = ( s.handles ?? [] ).map( h => {
766
+ const kind = h.kind === 'dm' ? 'privately' : h.kind === 'room' ? 'in a shared room' : 'somewhere'
767
+ const ans = h.answeredAgo !== undefined
768
+ ? `answered ${ h.answeredAgo } ticks ago`
769
+ : 'never answered me there'
770
+ return `${ kind } (${ h.keid }) — ${ ans }`
771
+ } )
772
+ const reach = where.length ? `\n reachable: ${ where.join('; ') }` : ''
773
+
774
+ // An identity I have not settled. Deliberately a question and not a
775
+ // merge: two people really can share a name, so nothing fuses them on my
776
+ // behalf — but I am told, so I can find out, usually by asking.
777
+ const doubt = s.mayBeSameAs?.length
778
+ ? `\n I hold a separate record for ${ s.mayBeSameAs.join(' and ') } — this may be the same someone under another handle. I do not know. If I find out they are, I say so with **sameAs**.`
779
+ : ''
780
+
781
+ return `- ${who}${bits.length ? ' — ' + bits.join(', ') : ''}${ reach }${ doubt }`
697
782
  } ).join('\n')}`
698
783
  : ''
699
784
 
785
+ // Who the mind is mid-conversation with. Facets run those threads; this is the
786
+ // master's view of the table — the whole point of the singular seat is that it
787
+ // can hold several conversations as one situation rather than as N strangers.
788
+ // Names come from what the mind has actually learned; the id is shown because
789
+ // that is what a reach-out must be addressed to.
790
+ const conversationsBlock = ( options.mode !== 'facet' && options.activeConversations?.length )
791
+ ? `## In Conversation Now\n${options.activeConversations.map( c =>
792
+ `- ${c.name ?? 'someone'} (id: ${c.entityId})`
793
+ ).join('\n')}\nThese threads are already open — I am in them. Reaching out to one of these people again starts a second, parallel thread with them.`
794
+ : ''
795
+
700
796
  // Task focus — what the Will is committed to and the felt cost of switching away.
701
797
  // Surfaces task-persistence; the pull-to-stay scales with the (conscientiousness-
702
798
  // developable) switch cost. Empty/absent ⇒ no block.
@@ -718,6 +814,7 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
718
814
  plansBlock,
719
815
  actionDiversity.trim(),
720
816
  recentOutcomesBlock,
817
+ spokenBlock,
721
818
  perceptsBlock,
722
819
  abilitiesBlock,
723
820
  ruminationsBlock,
@@ -725,6 +822,7 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
725
822
  memoriesBlock,
726
823
  beliefsBlock,
727
824
  socialBlock,
825
+ conversationsBlock,
728
826
  focusBlock,
729
827
  identityNudge.trim(),
730
828
  ideationBlock,
@@ -957,6 +1055,56 @@ ${recent.map( ( t, i ) => `${i + 1}. ${t}`).join(' → ')}${warning}
957
1055
  return `## Relevant Memories\n${lines.join('\n')}${tail}`
958
1056
  }
959
1057
 
1058
+ /**
1059
+ * What I have said to people lately, and who has answered.
1060
+ *
1061
+ * Written as a PERCEPT and nothing more. There is no instruction here not to
1062
+ * repeat myself, and there must not be: the mind is allowed to say a thing
1063
+ * twice, and a person ignored twice about something urgent should say it a
1064
+ * third time. What it was missing was not restraint, it was the fact — it could
1065
+ * not tell a first asking from an eleventh, so restraint was not something it
1066
+ * was in a position to exercise.
1067
+ *
1068
+ * The closing line is an epistemic caveat for the same reason: silence has many
1069
+ * causes and this surface distinguishes none of them. Saying "no answer yet"
1070
+ * without saying "and I do not know why" invites the mind to fill the gap, which
1071
+ * is the habit that had it inventing attention-demand ids when asked what was
1072
+ * wrong with it.
1073
+ */
1074
+ private static _buildSpokenTurnsSection(
1075
+ spokenTurns: ExecutiveContext['spokenTurns'],
1076
+ ): string {
1077
+ // Defensive on absence, not just on empty: a host (and several tests) build a
1078
+ // context by hand, and a missing block must render as nothing rather than
1079
+ // throw the whole prompt away.
1080
+ if( !spokenTurns?.length ) return ''
1081
+
1082
+ const clip = ( s: string, n: number ): string =>
1083
+ s.length > n ? `${ s.slice( 0, n ) }…` : s
1084
+
1085
+ const lines = spokenTurns.map( t => {
1086
+ const words = t.preview.trim()
1087
+ const said = words ? ` — "${ clip( words, 80 ) }"` : ''
1088
+ // Their words, not merely that they spoke. "they answered" on its own reads
1089
+ // as "I have the answer" — a live Will asked "same time, 3pm?", saw that
1090
+ // flag, never saw the correction to 2pm, and relayed 3pm to a third party as
1091
+ // confirmed. A reply I cannot see is not one I can act on.
1092
+ const back = t.answered
1093
+ ? ( t.answeredWith?.trim()
1094
+ ? ` — they answered: "${ clip( t.answeredWith.trim(), 100 ) }"`
1095
+ : ' — they answered (I do not have their words here)' )
1096
+ : ' — no answer yet'
1097
+ return `- **${ t.target }** · ${ t.age } ticks ago${ said }${ back }`
1098
+ } )
1099
+
1100
+ const open = spokenTurns.filter( t => !t.answered ).length
1101
+ const note = open > 0
1102
+ ? `\n\nThese are my own words, newest first. "No answer yet" means exactly that — the words went out and nothing has come back. It does not tell me why, and I should not assume.`
1103
+ : ''
1104
+
1105
+ return `## What I've Said Lately\n${ lines.join('\n') }${ note }\n\n`
1106
+ }
1107
+
960
1108
  private static _buildRecentOutcomesSection(
961
1109
  recentActions: ExecutiveContext['recentActions'],
962
1110
  currentTick: number,