@mindot/will 0.9.0 → 0.10.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 (110) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +16660 -15206
  4. package/dist/index.js.map +1 -1
  5. package/dist/{channels → surface/channels}/discord.d.ts +50 -3
  6. package/dist/{channels → surface/channels}/discord.js +102 -7
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +7 -4
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +4406 -2904
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-DbDj_TEH.d.ts → will-evj9_vrd.d.ts} +5699 -4854
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +237 -3
  19. package/src/cognition/agency/conversation.aim.ts +32 -0
  20. package/src/cognition/agency/engines/action.selector.ts +59 -3
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +114 -10
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +362 -27
  24. package/src/cognition/agency/engines/reafference.engine.ts +43 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/reconcile.learning.ts +5 -4
  27. package/src/cognition/agency/schemas/innate.ts +96 -1
  28. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  29. package/src/cognition/agency/selection.scoring.ts +16 -0
  30. package/src/cognition/agency/settlement.ts +203 -0
  31. package/src/cognition/agency/types.ts +38 -1
  32. package/src/cognition/config.mirror.entities.ts +0 -1
  33. package/src/cognition/event.schemas.ts +0 -8
  34. package/src/cognition/faculties/affective.blender.ts +21 -4
  35. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  36. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  37. package/src/cognition/faculties/executive.engine/context.ts +41 -35
  38. package/src/cognition/faculties/executive.engine/engine.ts +270 -113
  39. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +126 -59
  40. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +10 -0
  41. package/src/cognition/faculties/executive.engine/parser.ts +100 -38
  42. package/src/cognition/faculties/executive.engine/prompt.factory.ts +168 -23
  43. package/src/cognition/faculties/executive.engine/types.ts +18 -15
  44. package/src/cognition/faculties/exteroception.ts +99 -98
  45. package/src/cognition/faculties/goal.manager.ts +50 -2
  46. package/src/cognition/faculties/known.entity.tracker.ts +22 -3
  47. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  48. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  49. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  50. package/src/cognition/faculties/working.memory.ts +6 -1
  51. package/src/cognition/instruction.handler.ts +1 -1
  52. package/src/cognition/memory/vector.embedder.ts +1 -3
  53. package/src/cognition/percept.entity.ts +122 -0
  54. package/src/cognition/sense.boundary.ts +176 -0
  55. package/src/cognition/senses/audition.engine/engine.ts +97 -16
  56. package/src/cognition/senses/base.sense.engine.ts +104 -6
  57. package/src/cognition/senses/index.ts +71 -13
  58. package/src/cognition/senses/provenance.ts +128 -0
  59. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  60. package/src/cognition/social.identity.ts +23 -0
  61. package/src/cognition/utilities/token.tracker.ts +36 -5
  62. package/src/core/orchestrator.ts +13 -0
  63. package/src/index.ts +39 -5
  64. package/src/llm/gate.ts +48 -0
  65. package/src/llm/index.ts +102 -25
  66. package/src/llm/wire.contracts.ts +38 -0
  67. package/src/pma/index.ts +1 -1
  68. package/src/stem/index.ts +92 -21
  69. package/src/stem/mind.ts +26 -2
  70. package/src/stem/policy/arbiter.ts +10 -0
  71. package/src/stem/profiles/built-in.ts +7 -0
  72. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  73. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  74. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  75. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  76. package/src/{profiles → stem/profiles}/index.ts +1 -1
  77. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  78. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  79. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  80. package/src/stem/tracts/effector/types.ts +110 -0
  81. package/src/stem/tracts/effector.controller.ts +112 -401
  82. package/src/stem/tracts/outbox.controller.ts +30 -17
  83. package/src/stem/tracts/sensory.controller.ts +6 -6
  84. package/src/stem/tracts/session.logger.ts +6 -1
  85. package/src/stem/tracts/transport/types.ts +20 -1
  86. package/src/stem/tracts/transport.controller.ts +33 -9
  87. package/src/{channels → surface/channels}/discord.ts +224 -7
  88. package/src/{channels → surface/channels}/roster.ts +1 -1
  89. package/src/{channels → surface/channels}/types.ts +1 -1
  90. package/src/{channels → surface/channels}/whatsapp.ts +9 -6
  91. package/src/{cli.ts → surface/cli.ts} +6 -6
  92. package/src/{host → surface/host}/boot.ts +3 -3
  93. package/src/{host → surface/host}/utterances.ts +2 -2
  94. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  95. package/src/{mcp → surface/mcp}/server.ts +19 -5
  96. package/src/{sdk → surface/sdk}/will.ts +156 -28
  97. package/src/{serve → surface/serve}/server.ts +11 -4
  98. package/src/types.ts +24 -3
  99. package/dist/channels/discord.js.map +0 -1
  100. package/dist/channels/whatsapp.js.map +0 -1
  101. package/dist/cli.js.map +0 -1
  102. package/dist/mcp/effectors.js.map +0 -1
  103. package/src/extensions/livestream.ext.ts +0 -570
  104. package/src/extensions/time.ext.ts +0 -339
  105. package/src/profiles/built-in.ts +0 -7
  106. package/src/runners/coherence.runner.ts +0 -49
  107. package/src/runners/outreach.runner.ts +0 -155
  108. package/src/runners/social.runner.ts +0 -179
  109. package/src/runners/thin-shim.runner.ts +0 -205
  110. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -31,6 +31,7 @@
31
31
  // ─────────────────────────────────────────────────────────────
32
32
 
33
33
  import type { EntityInput } from '#core/types'
34
+ import { perceptEntity } from '#cognition/percept.entity'
34
35
 
35
36
  /**
36
37
  * Work the master should plan or re-goal. The facet has already said whatever
@@ -42,14 +43,33 @@ export interface EscalationHandoff {
42
43
  }
43
44
 
44
45
  /**
45
- * An intention toward a THIRD party "I'll reach out to FKEM now" formed
46
- * while attending to something else. The facet never opens that channel itself,
47
- * so this is the only thing standing between having decided to make contact and
48
- * having made it.
46
+ * SOMETHING I SAID I WOULD DO, declared by the part of me that said it.
47
+ *
48
+ * It was `{ target, gist }` a contact and nothing else because the defect it
49
+ * was built for was capability-shaped rather than promise-shaped: a conversation
50
+ * facet can speak in its own thread but cannot open a channel to a third party,
51
+ * so the handoff carried the one act it structurally could not perform. Named
52
+ * "undertaking" and dressed in promise language, it read as a general commitment
53
+ * system with a strange restriction, and it was not one.
54
+ *
55
+ * `what` is the commitment in the mind's own words — "reach FKEM about the demo",
56
+ * "have the scoping doc by Friday", "look into the pricing". `target` is set only
57
+ * when it is toward someone the mind would have to REACH, which is still the case
58
+ * the tract exists for; a promise about work has no target and needs none.
59
+ *
60
+ * Note what this deliberately does NOT become: a task list. It is filed beside
61
+ * what the facet concluded and read once by the master, which may make a goal of
62
+ * it — the conversation format already says to raise [GOALS_NEW] for anything to
63
+ * follow through on, and goals are the thing that persists. This only ensures the
64
+ * seat that decides never learns of it too late to decide.
49
65
  */
50
66
  export interface UndertakingHandoff {
51
67
  kind: 'undertaking'
52
- target: string
68
+ /** What I said I would do, in my own words. */
69
+ what: string
70
+ /** Who it is toward — only when keeping it means reaching someone. */
71
+ target?: string
72
+ /** The words I had in mind for them, when I had any. */
53
73
  gist?: string
54
74
  reasoning: string
55
75
  }
@@ -68,11 +88,18 @@ export function validateFacetHandoff( payload: unknown ): string | null {
68
88
  if( !body || typeof body !== 'object') return 'payload.body is required'
69
89
  const kind = ( body as { kind?: unknown } ).kind
70
90
  if( kind === 'escalation') return null
71
- if( kind === 'undertaking')
72
- return typeof ( body as { target?: unknown } ).target === 'string'
73
- && ( body as { target: string } ).target.trim().length > 0
91
+ if( kind === 'undertaking'){
92
+ // `what` is the requirement now, not `target`. A promise about work has no
93
+ // one to reach and is still a promise; a promise with a target but nothing
94
+ // said is not one.
95
+ const what = ( body as { what?: unknown } ).what
96
+ if( typeof what !== 'string' || what.trim().length === 0 )
97
+ return 'undertaking handoff requires a non-empty what'
98
+ const target = ( body as { target?: unknown } ).target
99
+ return target === undefined || ( typeof target === 'string' && target.trim().length > 0 )
74
100
  ? null
75
- : 'undertaking handoff requires a non-empty target'
101
+ : 'undertaking handoff target, when present, must be a non-empty string'
102
+ }
76
103
  return `unknown handoff kind: ${String( kind )}`
77
104
  }
78
105
 
@@ -126,6 +153,26 @@ export class EscalationBuffer {
126
153
  * Convert every buffered handoff into a high-salience percept entity and clear
127
154
  * the buffer. Returns the percepts plus the first handoff's requester context
128
155
  * (used to tag goals the master creates in response).
156
+ *
157
+ * WHY THESE DO NOT GO THROUGH A SENSE DOOR (SIGNAL_BOUNDARY P4).
158
+ *
159
+ * P4's rule is that only a sense or exteroception writes a percept, and these
160
+ * two are the standing exception. A sense door carries AFFERENCE: a signal
161
+ * crossing into the mind, either from the world (exafferent) or from the
162
+ * mind's own act returning THROUGH the world (reafferent). A facet handing off
163
+ * to the master crosses neither boundary — it never left the mind. Routing it
164
+ * through a sense would dress one part of a mind up as news from outside,
165
+ * which is precisely the laundering P1 removed from `inspect`, and §6 of the
166
+ * epoch names that class of over-unification as the thing not to do.
167
+ *
168
+ * They still had to stop being hand-rolled literals, and they have: everything
169
+ * goes through `perceptEntity()`, so the shape is the one shape.
170
+ *
171
+ * What is left open, and worth deciding rather than inheriting: a handoff is
172
+ * using the percept entity as a DELIVERY MECHANISM, because the percept block
173
+ * is what the executive prompt renders. A `self.handoff` type with its own
174
+ * section would say what this actually is. That is a prompt change and a
175
+ * `_reconcileUndertakings` change, so it is a decision, not a cleanup.
129
176
  */
130
177
  drainToPercepts(): DrainedEscalations {
131
178
  const percepts: EntityInput[] = []
@@ -136,63 +183,83 @@ export class EscalationBuffer {
136
183
  // silently collapse them into whichever came last.
137
184
  const id = `escalation-percept-${h.subjectEntityId ?? h.facetId ?? 'self'}-${h.tick}-${seq++}`
138
185
 
139
- const common = {
186
+ // FACTS the builder owns, and the writer's OWN fields beside them. Split
187
+ // this way because `perceptEntity()` spreads `extra` first: a writer can
188
+ // add whatever it likes and still cannot clobber `tick` or `provenance`.
189
+ const core = {
190
+ id,
191
+ tick: h.tick,
140
192
  salience: 0.85,
141
- source: 'executive-facet',
193
+ // REAFFERENT. Nothing outside the mind produced this: a facet of it
194
+ // raised the handoff, and the master is now noticing its own part's
195
+ // doing. Tagging it costs no behaviour — the rupture gate excludes
196
+ // reafferent and untagged alike — but it stops the exclusion being an
197
+ // accident, and it is the honest answer rather than the convenient one.
198
+ provenance: 'reafferent' as const,
142
199
  ...( h.subjectEntityId ? { entityId: h.subjectEntityId } : {} ),
143
- ...( h.threadId ? { threadId: h.threadId } : {} ),
144
- ...( h.facetId ? { facetId: h.facetId } : {} ),
200
+ }
201
+ const mine = {
202
+ source: 'executive-facet',
203
+ ...( h.threadId ? { threadId: h.threadId } : {} ),
204
+ ...( h.facetId ? { facetId: h.facetId } : {} ),
145
205
  }
146
206
 
207
+ // Built through `perceptEntity()` like every other percept in the package
208
+ // (SIGNAL_BOUNDARY P4). These were the last hand-rolled `type: 'percept'`
209
+ // literals, and a hand-rolled one is exactly how `tick` went missing on
210
+ // the wake and delivery percepts and made them immortal.
211
+ //
212
+ // They do NOT go through a sense door, and should not — see the note on
213
+ // `drainToPercepts` above for why.
147
214
  percepts.push( h.body.kind === 'undertaking'
148
- ? {
149
- id,
150
- type: 'percept',
151
- metadata: {
152
- ...common,
153
- category: 'undertaking',
154
- // First person: this is the mind noticing what IT said it would do, not
155
- // a report handed to it. What makes it act is that the words are still
156
- // unsent an undertaking it has already honoured reads the same until
157
- // it checks, which is exactly the check we want it making.
158
- // Everything actionable goes in `summary`: that is the only field the
159
- // executive context actually renders (context.ts extractPercepts reads
160
- // summary/content and nothing else).
161
- //
162
- // The closing clause is the whole point. A mind that has SAID it will make
163
- // contact remembers deciding, and cannot tell from the inside whether the
164
- // words went out so it follows up on a message it never sent. Naming the
165
- // gap is what lets it check. It stays a decision either way: it may have
166
- // changed its mind, or judge this the wrong moment, and a reach-out
167
- // competes with everything else like any other action.
168
- summary: `${whereItHappened( h )} I said I would reach ${h.body.target}`
169
- + ( h.body.gist ? ` what I meant to say: "${h.body.gist.slice( 0, 220 )}"` : '')
170
- + `. Nothing has gone to them yet; saying it in that conversation did not send it.`
171
- + ` If I still mean it, I reach out with target '${h.body.target}'.`
172
- + ` If I no longer do, I let it go but I do not leave it half-done while telling them it is handled.`,
173
- // Who was promised, and when the promise was made. The executive
174
- // reconciles against these (see _reconcileUndertakings): once the mind
175
- // has actually reached this target since `tick`, the percept is retired
176
- // rather than left standing as a claim the words are still unsent.
177
- undertakingTarget: h.body.target,
178
- tick: h.tick,
179
- },
180
- }
181
- : {
182
- id,
183
- type: 'percept',
184
- metadata: {
185
- ...common,
186
- category: 'task-escalation',
187
- // The steer lives IN the summary because that is the only field the
188
- // executive context renders (context.ts extractPercepts reads
189
- // summary/content and nothing else). It sat in a sibling `directive`
190
- // field for its whole life and never once reached the master.
191
- summary: `[Task from ${h.subjectName ?? h.subjectEntityId ?? 'my own focused work'}] ${h.body.reasoning}`
192
- + ' — this is mine to plan or re-goal; the part of me that raised it handles the talking.',
193
- tick: h.tick,
215
+ ? perceptEntity( {
216
+ ...core,
217
+ category: 'undertaking',
218
+ // THE GAP, AND ONLY THE GAP. First person because the mind is noticing
219
+ // what IT said it would do, not because it is being told what to do
220
+ // about it. A mind that has said it will make contact remembers
221
+ // deciding and cannot tell from the inside whether the words went out
222
+ // so it follows up on a message it never sent. Naming the gap is
223
+ // what lets it check; naming the remedy was never this line's job.
224
+ //
225
+ // What used to follow this sentence: "If I still mean it, I reach out
226
+ // with target 'X'. If I no longer do, I let it go — but I do not leave
227
+ // it half-done while telling them it is handled." An instruction
228
+ // naming an effector and its argument, and a value judgement, both
229
+ // written in the mind's own voice so it could not disagree with
230
+ // either. The pull now lives in the affordance field where it can be
231
+ // out-competed, and the judgement belongs to a persona, not a buffer.
232
+ summary: `${whereItHappened( h )} I said I would reach ${h.body.target}`
233
+ + `. Nothing has gone to them since; saying it in that conversation did not send it.`,
234
+ }, {
235
+ ...mine,
236
+ // Who was promised, and when the promise was made. The executive
237
+ // reconciles against these (see _reconcileUndertakings): once the mind
238
+ // has actually reached this target since `tick`, the percept is retired
239
+ // rather than left standing as a claim the words are still unsent.
240
+ undertakingTarget: h.body.target,
241
+ // The EVIDENCE, rendered beneath the label since P2 — which is what
242
+ // makes the prose above unnecessary. `gist` is what the mind meant to
243
+ // say; it is a fact about the promise, not a script for keeping it,
244
+ // and it is carried whole rather than clipped into a sentence.
245
+ data: {
246
+ target: h.body.target,
247
+ promisedAt: h.tick,
248
+ ...( h.subjectName ?? h.subjectEntityId ? { promisedTo: h.subjectName ?? h.subjectEntityId } : {} ),
249
+ ...( h.body.gist ? { gist: h.body.gist } : {} ),
250
+ contactedSince: false,
194
251
  },
195
252
  } )
253
+ : perceptEntity( {
254
+ ...core,
255
+ category: 'task-escalation',
256
+ // What was raised, and by which part of me. It used to close with
257
+ // "this is mine to plan or re-goal; the part of me that raised it
258
+ // handles the talking" — a role instruction. Whether to plan it,
259
+ // re-goal it or drop it is the master's call, and it has a whole
260
+ // faculty for making it.
261
+ summary: `[Raised by ${h.subjectName ?? h.subjectEntityId ?? 'my own focused work'}] ${h.body.reasoning}`,
262
+ }, mine ) )
196
263
  }
197
264
 
198
265
  // Capture requester context before clearing — used to tag new goals. Only a
@@ -333,6 +333,11 @@ export class FacetSupervisor {
333
333
  type: 'executive.facet.destroy',
334
334
  tick: this._lastStateRef?.tick as unknown as number ?? 0,
335
335
  facetId,
336
+ // The key a reader needs to tell the two populations apart. A KEYED facet
337
+ // dying is a conversation ending; a keyless one dying is a deliberation
338
+ // finishing, which is what the executive does every time it thinks. Without
339
+ // it a churn metric counts both and reads engagement as damage.
340
+ ...( key ? { key } : {} ),
336
341
  reason,
337
342
  totalFacets: this._facets.size,
338
343
  } as any)
@@ -519,6 +524,11 @@ export class FacetSupervisor {
519
524
  type: 'executive.facet.destroy',
520
525
  tick: this._lastStateRef?.tick as unknown as number ?? 0,
521
526
  facetId,
527
+ ...( key ? { key } : {} ),
528
+ // Named so the three ways a facet can end are distinguishable in a
529
+ // trace: its owner closed it, the TTL took a quiet one, or pressure
530
+ // evicted it. Only the last two are ever a symptom.
531
+ reason: 'explicit',
522
532
  totalFacets: this._facets.size,
523
533
  } as any)
524
534
  }
@@ -3,7 +3,7 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import { logger } from '#core/logger'
6
- import { REPLY_TEXT_TAG, NO_MESSAGE_TAG, NO_MESSAGE_OPEN } from '#llm/wire.contracts'
6
+ import { REPLY_TEXT_TAG, NO_MESSAGE_TAG, NO_MESSAGE_OPEN, stripProtocolMarkers } 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
 
@@ -27,7 +27,9 @@ export function parseResponse(
27
27
 
28
28
  let
29
29
  actions: Array<{ type: string; reasoning: string; expectedOutcome: string }>,
30
- confidence = 0.5
30
+ confidence = 0.5,
31
+ /** The mind's own sentence, when the response parsed as JSON and carried one. */
32
+ stated: string | undefined
31
33
 
32
34
  // Strategy 1: direct JSON.parse
33
35
  try {
@@ -41,6 +43,7 @@ export function parseResponse(
41
43
 
42
44
  actions = parsed.actions
43
45
  confidence = parsed.confidence ?? 0.5
46
+ stated = typeof parsed.reasoning === 'string' ? parsed.reasoning : undefined
44
47
  }
45
48
  catch {
46
49
  // Strategy 2: balanced-bracket extractor
@@ -60,9 +63,28 @@ export function parseResponse(
60
63
  confidence = confidenceMatch ? parseFloat( confidenceMatch[1]! ) : 0.5
61
64
  }
62
65
 
63
- // Parse tagged blocks from the extracted JSON text
66
+ // Tagged blocks are scanned over the FULL text — they are documented as embedded
67
+ // in the reasoning field, but a model puts them wherever it likes and the scan
68
+ // has always been forgiving about that. Keep that surface.
64
69
  const full = parseTaggedBlocks({ actions, reasoning: fullText, confidence }, state )
65
70
 
71
+ // …but `reasoning` itself is the mind's SENTENCE, not its serialization.
72
+ //
73
+ // It was `fullText` — the whole JSON blob — for every consumer downstream, and
74
+ // that was invisible for as long as nothing read it as prose. Three things do:
75
+ // the session log records it, a facet is handed the master's as "What I've Been
76
+ // Turning Over" (clipped to 400 chars, so facets have been reading the master's
77
+ // JSON truncated mid-object), and the master now reads a facet's back as what it
78
+ // worked out. Seen live in a prompt: `What I worked out there: { "actions": [...
79
+ //
80
+ // The scan surface and the sentence were the same string only because nothing
81
+ // had ever needed them to be different.
82
+ // Stripped of the tagged blocks, because the format tells the mind to embed them
83
+ // IN this field — so the sentence and the structured payload arrive in one
84
+ // string, and handing the whole thing back as "what I worked out" would just
85
+ // move the serialization problem one level in.
86
+ if( stated ) full.reasoning = stripTaggedBlocks( stated )
87
+
66
88
  // [REPLY_TEXT] is a plain-text block that lives OUTSIDE the JSON code block.
67
89
  // Search the full response text so we find it even when the LLM used a code fence.
68
90
  const replyText = extractTextBlock( responseText, REPLY_TEXT_TAG )
@@ -176,6 +198,29 @@ function extractBalancedArray( text: string, key: string ): string | null {
176
198
  return null
177
199
  }
178
200
 
201
+ /** Every block name the reasoning field may carry, for stripping it back to prose. */
202
+ const TAGGED_BLOCK_NAMES = [
203
+ 'PLANS', 'BELIEFS', 'INTROSPECTION', 'NARRATIVE', 'IDENTITY', 'KNOWN_ENTITIES',
204
+ 'GOALS_NEW', 'GOALS_ABANDON', 'GOALS_REPRIORITIZE', 'EFFECTORS', 'SELF_OBS',
205
+ 'SKILLS', 'REPLY_TEXT', 'ACK',
206
+ ] as const
207
+
208
+ /**
209
+ * The sentence, with the structured payload taken back out of it.
210
+ *
211
+ * Unclosed blocks are cut to the end of the string on purpose: a truncated
212
+ * `[PLANS]{…` is not prose either, and leaving it in puts half a JSON object in
213
+ * front of whoever reads this as the mind's thinking.
214
+ */
215
+ export function stripTaggedBlocks( reasoning: string ): string {
216
+ let out = reasoning
217
+ for( const tag of TAGGED_BLOCK_NAMES )
218
+ out = out
219
+ .replace( new RegExp( `\\[${ tag }\\][\\s\\S]*?\\[/${ tag }\\]`, 'g'), '')
220
+ .replace( new RegExp( `\\[${ tag }\\][\\s\\S]*$`, 'g'), '')
221
+ return out.replace( /\n{3,}/g, '\n\n').trim()
222
+ }
223
+
179
224
  /**
180
225
  * Parse tagged JSON blocks from the reasoning text.
181
226
  * The LLM embeds optional outputs as [TAG]...[/TAG] blocks.
@@ -339,7 +384,15 @@ function extractBlock( text: string, tag: string ): string | null {
339
384
  * Used for [REPLY_TEXT] — content is streamed directly to the client,
340
385
  * so it must be clean prose, not a JSON payload.
341
386
  */
342
- function extractTextBlock( text: string, tag: string ): string | null {
387
+ /**
388
+ * Pull one plain-text block out of a response.
389
+ *
390
+ * Exported for the guard: this slice is where a stray marker survives into
391
+ * content, and the seam is the thing worth testing. `parseResponse` needs a full
392
+ * well-formed response around it, which is exactly the setup that hides a defect
393
+ * living in one line of string handling.
394
+ */
395
+ export function extractTextBlock( text: string, tag: string ): string | null {
343
396
  const open = `[${tag}]`
344
397
  const close = `[/${tag}]`
345
398
  const openIdx = text.indexOf( open )
@@ -350,51 +403,60 @@ function extractTextBlock( text: string, tag: string ): string | null {
350
403
  ? text.slice( contentStart, closeIdx )
351
404
  : text.slice( contentStart ) // unclosed block — take everything after the open marker
352
405
 
353
- return content.trim() || null
406
+ // A stray marker INSIDE the body survives this slice — the block runs from the
407
+ // first open to the first close after it, and anything between is content by
408
+ // construction. Live, a reply went out as four substantive bubbles and a fifth
409
+ // reading exactly `[REPLY_TEXT]`: the person received a message whose whole
410
+ // content was the name of the slot it should have filled.
411
+ return stripProtocolMarkers( content ) || null
354
412
  }
355
413
 
356
414
  /**
357
- * Build a heuristic fallback output when LLM response parsing fails.
358
- * Chooses actions based on current physiological state.
415
+ * What the executive intends when its own reasoning did not come back.
416
+ *
417
+ * Two things had to change here. Every action this produced was PROSE, not a
418
+ * schema: `observe`, `replenish energy`, `enter deep rest to reduce fatigue`,
419
+ * `calm my mind and reduce tension`, `explore`, `learn`, `express_emotion`.
420
+ * Only `rest` names anything the body can enact. The rest reach the agency, fail
421
+ * to resolve, and become an ideomotor push toward an act that does not exist —
422
+ * observed live as `master decided [observe] conf=0.4` four times across two
423
+ * boots, each one a decision that could go nowhere.
424
+ *
425
+ * And the "no urgent need" branch was INVENTION. The executive could not read
426
+ * its own thought, so it made one up and pushed it into the field as intention.
427
+ * That is the mind being told what it wants. The substrate does not need the
428
+ * help: the affordance field is always there, System 1 selects from it every
429
+ * tick without an executive, and it now carries real satiation — the monotony
430
+ * this used to break by cycling a hardcoded list is handled where monotony
431
+ * actually lives.
432
+ *
433
+ * So: reflexes stay, invention goes. Critically low energy, high sleep pressure
434
+ * or high stress still push a real regulatory stance, because a body in trouble
435
+ * should not depend on an LLM parsing correctly. Anything else yields NO action
436
+ * and lets the substrate decide, which is what it is for.
359
437
  */
360
438
  export function buildFallbackOutput(
361
439
  state: ReadonlySimulationState,
362
- recentActionTypes: string[]
440
+ _recentActionTypes: string[]
363
441
  ): ExecutiveOutputFull {
364
- const energy = state.metrics.get('energy.level') ?? 100
442
+ const energy = state.metrics.get('energy.level') ?? 100
365
443
  const sleepPressure = state.metrics.get('sleep.pressure') ?? 0
366
- const stressLoad = state.metrics.get('stress.load') ?? 0
367
-
368
- let actionType = 'observe'
369
- let actionReason = 'No urgent needs — observing surroundings.'
444
+ const stressLoad = state.metrics.get('stress.load') ?? 0
370
445
 
371
- if( energy < 20 ){
372
- actionType = 'replenish energy'
373
- actionReason = 'Energy critically low — attempting to rest.'
374
- }
375
- else if( sleepPressure > 60 ){
376
- actionType = 'enter deep rest to reduce fatigue'
377
- actionReason = 'Sleep pressure high — attempting to sleep.'
378
- }
379
- else if( stressLoad > 70 ){
380
- actionType = 'calm my mind and reduce tension'
381
- actionReason = 'Stress elevated — attempting to meditate.'
382
- }
383
- else {
384
- // Vary the fallback when low-effort actions have dominated recently
385
- const monotonous = recentActionTypes.filter( t => t === 'reflect' || t === 'observe').length >= 3
386
- if( monotonous) {
387
- const alternatives = ['explore', 'learn', 'express_emotion', 'rest']
388
- const tick = state.tick as number
389
-
390
- actionType = alternatives[ tick % alternatives.length ]!
391
- actionReason = 'Breaking monotony — choosing a varied action during LLM fallback.'
392
- }
393
- }
446
+ // Innate stances only — what the body can actually enact.
447
+ const reflex: { type: string; reasoning: string } | null =
448
+ energy < 20 ? { type: 'rest', reasoning: 'Energy critically low — I let myself recover.' }
449
+ : sleepPressure > 60 ? { type: 'rest', reasoning: 'Sleep pressure high — I let myself recover.' }
450
+ : stressLoad > 70 ? { type: 'withdraw', reasoning: 'Stress elevated — I pull back from the press of things.' }
451
+ : null
394
452
 
395
453
  return {
396
- actions: [{ type: actionType, reasoning: actionReason, expectedOutcome: 'State improves' }],
397
- reasoning: `Heuristic fallback LLM unavailable. ${actionReason}`,
454
+ actions: reflex
455
+ ? [ { type: reflex.type, reasoning: reflex.reasoning, expectedOutcome: 'The body settles' } ]
456
+ : [],
457
+ reasoning: reflex
458
+ ? `Heuristic fallback — my reasoning did not come back. ${ reflex.reasoning }`
459
+ : 'Heuristic fallback — my reasoning did not come back, and nothing about my state is pressing. I intend nothing in particular; my body goes on choosing.',
398
460
  confidence: 0.4
399
461
  }
400
462
  }