@mindot/will 0.8.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 (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  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 +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  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-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -0,0 +1,296 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/social.identity.ts — what something IS, vs where to find it
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // A `keid` used to be minted by the transport: `discord:${author.id}`,
6
+ // `whatsapp:${userId}`. Identity WAS the address, and whichever channel spoke
7
+ // first won the right to name the person. Everything downstream inherited that:
8
+ // twenty-two files key off a keid, so the same human met on two channels was two
9
+ // people to the reputation tracker, the theory-of-mind model, the attachment
10
+ // bond and the PMA — with no way to notice, and no way to say so.
11
+ //
12
+ // It also made "how should I reach them?" unaskable. There was exactly one id
13
+ // and it WAS a route, so the question collapsed into a roster guess about where
14
+ // the person was last seen. Live, that sent a follow-up promised in a DM into a
15
+ // public channel, because the roster's last-seen answer is not the same question
16
+ // as "where did I promise this".
17
+ //
18
+ // So identity and route are separated here:
19
+ //
20
+ // ke:<opaque> — a referent. Never a route. The anchor everything hangs on.
21
+ // handle — a way that referent has been reachable, with the
22
+ // circumstances under which it worked.
23
+ //
24
+ // Deliberately NOT social-only. `keid` has always stood for *known entity* id and
25
+ // the dossier has carried `kind: 'sentient' | 'thing'` since it shipped; the
26
+ // first cut of this minted `person:` ids, which was a narrower word than the
27
+ // system already used. A document, a repo, a dashboard, a room each have a what
28
+ // and a where, and the where can change while the what stays put.
29
+ //
30
+ // This is closer to how a person actually holds it. You know someone; you know
31
+ // places; and separately you know where you usually find whom. Three things that
32
+ // compose, not one contact record — which is why a handle carries evidence
33
+ // (`lastAnsweredTick`) rather than a priority number somebody configured.
34
+ //
35
+ // Everything here is pure and deterministic. `mintReferentId` hashes the first
36
+ // transport id ever seen for that referent, so a recorded run and its replay mint
37
+ // the same id (R2) — no clock, no counter, no RNG.
38
+ // ─────────────────────────────────────────────────────────────
39
+
40
+ import type { Tick } from '#core/types'
41
+ import { fnv1a } from '#agency/consequence'
42
+
43
+ /**
44
+ * Marks an id as a referent rather than an address.
45
+ *
46
+ * `ke` for known-entity, which is what `keid` has always stood for — the
47
+ * vocabulary was never social. The first cut of this minted `person:` ids and
48
+ * that was a narrower word than the system uses: the dossier has carried
49
+ * `kind: 'sentient' | 'thing'` since it shipped, and the split here is not a
50
+ * social idea. It is referent vs. access path, which is how anything is held —
51
+ * you remember the book, and separately that it is on the shelf, on the Kindle,
52
+ * or at the library. A document, a repo, a dashboard, a room all have several
53
+ * routes that come and go while the thing itself stays put.
54
+ */
55
+ export const REFERENT_PREFIX = 'ke:'
56
+
57
+ export const ALIAS_TYPE = 'known-entity-alias'
58
+ export const DOSSIER_TYPE = 'known-entity'
59
+
60
+ /** True for an anchor, false for a transport address (`discord:…`, `whatsapp:…`). */
61
+ export function isReferentId( id: string ): boolean {
62
+ return id.startsWith( REFERENT_PREFIX )
63
+ }
64
+
65
+ /**
66
+ * Mint the anchor for a referent first met at `seedKeid`.
67
+ *
68
+ * Kind is deliberately NOT in the id. It lives on the dossier, because kind is
69
+ * LEARNED and correctable — a handle you took for a bot turns out to be a person.
70
+ * Baked into the id, correcting it would mean re-identifying, and every faculty
71
+ * keyed off that id would lose its history of them at the moment it finally
72
+ * understood what they were.
73
+ *
74
+ * Deterministic by construction: the same first-seen transport id always yields
75
+ * the same referent id, so a replay of a recorded run mints identically and the
76
+ * state hashes match (R2). A counter would drift the moment two runs met people
77
+ * in a different order; a clock or RNG would never match at all.
78
+ *
79
+ * Opaque on purpose. The moment an id is readable as `ke:discord:123`
80
+ * something downstream starts parsing it back into a route, and the separation
81
+ * this whole module exists for quietly stops holding.
82
+ */
83
+ export function mintReferentId( seedKeid: string ): string {
84
+ return `${ REFERENT_PREFIX }${ fnv1a( seedKeid ).toString( 36 ) }`
85
+ }
86
+
87
+ /**
88
+ * A way this referent has been reachable, and what happened there.
89
+ *
90
+ * `kind` is the one fact that decides whether a room is the right place for a
91
+ * given utterance, and it was being computed at the Discord edge (`isDM`) and
92
+ * discarded before the mind could see it.
93
+ *
94
+ * `lastAnsweredTick` is evidence, not configuration — it arrives free from the
95
+ * `social.responsiveness` signal. It is what lets the mind prefer the DM because
96
+ * that is where this person actually answers, rather than because a constant in
97
+ * the code says DMs rank higher.
98
+ */
99
+ export interface Handle {
100
+ /** The transport address — what a channel bridge can actually deliver to. */
101
+ keid: string
102
+ /**
103
+ * 'dm' — a private thread. 'room' — somewhere others are listening. Left open
104
+ * for a non-social referent, where the meaningful distinction is a different one.
105
+ */
106
+ kind: 'dm' | 'room' | 'unknown'
107
+ /** The place this handle lives in, once places are dossiers of their own. */
108
+ place?: string
109
+ /** When the mind last SAID something here. */
110
+ lastUsedTick?: Tick
111
+ /** When someone last answered it here — the only evidence that this route works. */
112
+ lastAnsweredTick?: Tick
113
+ /** Free-form, so a host can mark what its own vocabulary cares about. */
114
+ tags?: string[]
115
+ }
116
+
117
+ interface EntityLike {
118
+ type: string
119
+ /** A frozen state entity may hand metadata over as either shape. */
120
+ metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>
121
+ }
122
+
123
+ /** Normalize whichever shape metadata arrived in. */
124
+ function meta( e: EntityLike ): Record<string, unknown> {
125
+ const m = e.metadata
126
+ if( !m ) return {}
127
+ return m instanceof Map ? Object.fromEntries( m ) : m as Record<string, unknown>
128
+ }
129
+
130
+ function str( v: unknown ): string | undefined {
131
+ return typeof v === 'string' && v.length > 0 ? v : undefined
132
+ }
133
+
134
+ /**
135
+ * alias keid → canonical referent id.
136
+ *
137
+ * Every transport address a referent has been met at is an alias of its anchor,
138
+ * which is what lets the twenty-two keid consumers keep working untouched: they
139
+ * still see one opaque string per referent, it is simply no longer a route.
140
+ */
141
+ export function readAliases( entities: ReadonlyMap<string, EntityLike> ): Map<string, string> {
142
+ const out = new Map<string, string>()
143
+ for( const [ , e ] of entities ){
144
+ if( e.type !== ALIAS_TYPE ) continue
145
+ const m = meta( e )
146
+ const a = str( m['aliasKeid'] )
147
+ const c = str( m['canonicalKeid'] )
148
+ if( a && c ) out.set( a, c )
149
+ }
150
+ return out
151
+ }
152
+
153
+ /** Follow an alias chain to the anchor. Cycle-safe; returns the input when unaliased. */
154
+ export function canonicalOf( aliases: ReadonlyMap<string, string>, keid: string ): string {
155
+ const seen = new Set<string>()
156
+ let id = keid
157
+ while( true ){
158
+ const next = aliases.get( id )
159
+ if( !next || next === id || seen.has( next ) ) return id
160
+ seen.add( id )
161
+ id = next
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Resolve anything the mind might name — an anchor, a transport address, or a
167
+ * learned name — to the anchor.
168
+ *
169
+ * ONE resolver, because there were two and they disagreed:
170
+ * `extractKnownEntities` folded aliases (so the prompt showed one person) while
171
+ * `resolveKnownEntity` did not (so willing a reach-out to that same person could
172
+ * resolve to a keid that had been merged away, and the intention evaporated).
173
+ * Same question, two answers, in the same tick.
174
+ *
175
+ * Name matching stays last and exact. It is the weakest evidence here — two
176
+ * people genuinely can share a name — and the KnownEntityTracker's recognition
177
+ * pass already guards fusing them.
178
+ */
179
+ export function resolveKeid(
180
+ entities: ReadonlyMap<string, EntityLike>,
181
+ ref: string,
182
+ ): string | undefined {
183
+ const needle = ref.trim().toLowerCase()
184
+ if( !needle ) return undefined
185
+
186
+ const aliases = readAliases( entities )
187
+
188
+ // An anchor or an address named directly.
189
+ const direct = canonicalOf( aliases, ref.trim() )
190
+ for( const [ , e ] of entities )
191
+ if( e.type === DOSSIER_TYPE && str( meta( e )['keid'] ) === direct ) return direct
192
+
193
+ // Otherwise a keid or a name, case-insensitively, in stable entity order.
194
+ for( const [ , e ] of entities ){
195
+ if( e.type !== DOSSIER_TYPE ) continue
196
+ const m = meta( e )
197
+ const keid = str( m['keid'] )
198
+ if( !keid ) continue
199
+ if( keid.toLowerCase() === needle ) return canonicalOf( aliases, keid )
200
+ if( str( m['name'] )?.toLowerCase() === needle ) return canonicalOf( aliases, keid )
201
+ }
202
+
203
+ // A known alias whose dossier was absorbed — still a real reference to someone.
204
+ for( const [ alias, canon ] of aliases )
205
+ if( alias.toLowerCase() === needle ) return canon
206
+
207
+ return undefined
208
+ }
209
+
210
+ /** The name the mind has learned for this referent, or undefined — never a placeholder. */
211
+ export function nameOf( entities: ReadonlyMap<string, EntityLike>, referentId: string ): string | undefined {
212
+ for( const [ , e ] of entities ){
213
+ if( e.type !== DOSSIER_TYPE || str( meta( e )['keid'] ) !== referentId ) continue
214
+ return str( meta( e )['name'] )?.trim() || undefined
215
+ }
216
+ return undefined
217
+ }
218
+
219
+ /** Every route the mind holds for this referent, most recently answered first. */
220
+ export function handlesOf( entities: ReadonlyMap<string, EntityLike>, referentId: string ): Handle[] {
221
+ for( const [ , e ] of entities ){
222
+ if( e.type !== DOSSIER_TYPE || str( meta( e )['keid'] ) !== referentId ) continue
223
+ const raw = meta( e )['handles']
224
+ if( !Array.isArray( raw ) ) return []
225
+ return ( raw as Handle[] )
226
+ .filter( h => h && typeof h.keid === 'string' )
227
+ .sort( ( a, b ) =>
228
+ ( b.lastAnsweredTick ?? -1 ) - ( a.lastAnsweredTick ?? -1 )
229
+ || ( b.lastUsedTick ?? -1 ) - ( a.lastUsedTick ?? -1 )
230
+ || ( a.keid < b.keid ? -1 : a.keid > b.keid ? 1 : 0 ) )
231
+ }
232
+ return []
233
+ }
234
+
235
+ /**
236
+ * Every address the world knows this referent by.
237
+ *
238
+ * The inverse of the alias table: an anchor in, the transport ids that resolve to
239
+ * it out. An anchor is deliberately opaque and deliberately not an address —
240
+ * `ke:1sqlkux` is who someone IS — so anything that must actually reach them in
241
+ * the world has to come back through here.
242
+ *
243
+ * Two callers now, which is why it is a function and not inline: the outbox, so a
244
+ * message addressed to a referent can be delivered, and the agency's dispatch, so
245
+ * a host asked to act on a referent knows which of its own ids that is. Both are
246
+ * the mind reaching OUT; nothing here lets a surface name someone to the mind.
247
+ *
248
+ * Sorted, so a recorded run and its replay produce the same list (R2).
249
+ */
250
+ export function addressesOf( entities: ReadonlyMap<string, EntityLike>, referentId: string ): string[] {
251
+ const aliases = readAliases( entities )
252
+ return [ ...aliases.entries() ]
253
+ .filter( ( [ , canonical ] ) => canonical === referentId )
254
+ .map( ( [ alias ] ) => alias )
255
+ .sort()
256
+ }
257
+
258
+ /**
259
+ * The route to use when the mind has expressed no preference.
260
+ *
261
+ * A DEFAULT, not a decision. Which room to speak in is the mind's call, made
262
+ * from the circumstances it can now see; this only answers "nothing was chosen
263
+ * and the words must still go somewhere" — the case where the alternative is
264
+ * dropping the message.
265
+ *
266
+ * Ordering is evidence-first: somewhere this person has actually answered beats
267
+ * somewhere they have not, and a DM beats a room only as a tiebreak. A live Will
268
+ * lost a promised follow-up into a public channel precisely because its fallback
269
+ * ranked "where I last saw them" above "where they talk to me".
270
+ */
271
+ export function defaultHandle( handles: readonly Handle[] ): Handle | undefined {
272
+ if( handles.length === 0 ) return undefined
273
+ const answered = handles.filter( h => h.lastAnsweredTick !== undefined )
274
+ const pool = answered.length > 0 ? answered : handles
275
+ return pool.find( h => h.kind === 'dm') ?? pool[0]
276
+ }
277
+
278
+ /**
279
+ * Fold a newly-seen address into a person's handle list.
280
+ *
281
+ * Merges rather than appends: meeting someone again in a room already known is
282
+ * not a new way to reach them, it is news about an existing one. Pure — returns
283
+ * a fresh array, so a caller writing it back through `setEntity` cannot
284
+ * accidentally share a mutable reference with frozen state.
285
+ */
286
+ export function withHandle( handles: readonly Handle[], next: Handle ): Handle[] {
287
+ const out = handles.filter( h => h.keid !== next.keid )
288
+ const old = handles.find( h => h.keid === next.keid )
289
+ out.push( old ? { ...old, ...next,
290
+ // Never let a fresh sighting erase evidence the old record already held.
291
+ lastUsedTick: next.lastUsedTick ?? old.lastUsedTick,
292
+ lastAnsweredTick: next.lastAnsweredTick ?? old.lastAnsweredTick,
293
+ tags: [ ...new Set([ ...( old.tags ?? [] ), ...( next.tags ?? [] ) ]) ],
294
+ } : next )
295
+ return out.sort( ( a, b ) => ( a.keid < b.keid ? -1 : a.keid > b.keid ? 1 : 0 ) )
296
+ }
@@ -54,10 +54,15 @@ export type LLMCallAttribute =
54
54
  | 'memory' // consolidation / embedding
55
55
  | 'guard' // a safety reviewer
56
56
 
57
- /** The specific cognitive function being paid for. */
58
- export type LLMCallFunction =
57
+ /** The specific cognitive process being paid for. */
58
+ export type LLMCallProcess =
59
+ | 'cog' // background cogs' call
59
60
  | 'decision' // the master's fused decision call
60
61
  | 'ideation' // the deliberate path's propose pass
62
+
63
+ /** The specific cognitive function being paid for. */
64
+ export type LLMCallFunction =
65
+ | '-' // no-specific function
61
66
  | 'deliberation' // action choice under contest
62
67
  | 'conversation' // a live reply
63
68
  | 'outreach' // an unprompted message
@@ -72,9 +77,24 @@ export type LLMCallFunction =
72
77
  export type TokenLedgerRecord = Record<string, unknown>
73
78
  export type TokenRecordListener = ( record: TokenLedgerRecord ) => void
74
79
 
75
- // ── Prompt-cache pricing (Anthropic) ──────────────────────
80
+ // ── Prompt-cache pricing ──────────────────────────────────
76
81
  // `input_tokens` in the API usage already EXCLUDES cached tokens, so the full
77
- // input cost is: fresh input ×1 + cache reads ×0.1 + cache writes ×1.25.
82
+ // input cost is: fresh input ×1 + cache reads + cache writes.
83
+ //
84
+ // These two ratios are ANTHROPIC's, and they are the last prices this engine
85
+ // ships. That is the inconsistency they encode: the module refuses to bake in
86
+ // an input or output rate on the explicit grounds that "prices belong to the
87
+ // host — they change on a vendor's schedule, differ per account, and are ~0 for
88
+ // a self-hosted model", and then hardcodes two more prices because they happen
89
+ // to be expressible as a ratio.
90
+ //
91
+ // They are wrong the moment a host is not on Anthropic. Z.ai charges $0.26/M
92
+ // for cached input against $1.40/M fresh — a ratio of 0.186, not 0.10 — so a
93
+ // COO's cache line came out 46% under. Its cache WRITES are free outright,
94
+ // which 1.25 cannot express at all.
95
+ //
96
+ // So they are now a FALLBACK, used only when the host's `ModelPrice` does not
97
+ // state the real rate. A host that says nothing gets exactly the old numbers.
78
98
  const CACHE_READ_MULT = 0.1
79
99
  const CACHE_WRITE_MULT = 1.25
80
100
 
@@ -93,7 +113,21 @@ function normalizeModelKey( model: string ): string {
93
113
  return m.replace( /[-@]\d{6,8}$/, '') // drop trailing -YYYYMMDD date stamp
94
114
  }
95
115
  /** USD per 1M tokens for one model. */
96
- export interface ModelPrice { input: number; output: number }
116
+ export interface ModelPrice {
117
+ input: number
118
+ output: number
119
+ /**
120
+ * USD per 1M cache-READ tokens. Absent ⇒ `input × 0.1` (Anthropic's ratio).
121
+ *
122
+ * State it whenever the host is not Anthropic. `0` is meaningful and honoured
123
+ * — some providers do not charge for cache reads at all — so this is read as
124
+ * "absent", not "falsy".
125
+ */
126
+ cachedInput?: number
127
+ /** USD per 1M cache-WRITE tokens. Absent ⇒ `input × 1.25`. `0` is honoured:
128
+ * Z.ai's cache storage is free, which no multiple of input can express. */
129
+ cacheWrite?: number
130
+ }
97
131
 
98
132
  /**
99
133
  * Host-supplied prices, keyed by model id. Matching is exact first, then
@@ -176,9 +210,25 @@ export interface TokenUsage {
176
210
  */
177
211
  priced: boolean
178
212
 
213
+ /**
214
+ * How much this call demanded, 0..1 — the cognitive measure the router saw.
215
+ *
216
+ * Recorded so routing can be ANSWERED rather than argued. Every call computes
217
+ * this, routes on it, and until now threw it away — which left questions like
218
+ * "is deliberation being rated by the tick's mood rather than the stakes of
219
+ * its own choice?" with no dataset at all.
220
+ *
221
+ * Absent means UNMEASURED, never zero. It must stay nullable all the way to
222
+ * storage: a call that never reported demand and a call that reported 0.0 are
223
+ * different facts, and collapsing them would put a floor of invented
224
+ * confidence under exactly the analysis this exists to enable.
225
+ */
226
+ demand?: number
227
+
179
228
  // ── 5-axis cost attribution ──────────────────────────────
180
229
  category: LLMCallCategory
181
230
  attribute: LLMCallAttribute
231
+ process: LLMCallProcess
182
232
  function: LLMCallFunction
183
233
  /** Optional specific id or namespace: facet id, entity id, model name. */
184
234
  scope?: string
@@ -197,9 +247,17 @@ export interface TokenUsage {
197
247
  /** What callers pass to {@link TokenTracker.recordUsage} — cost and label are derived. */
198
248
  export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label' | 'priced'> & { label?: string }
199
249
 
200
- /** Compose a stable, readable label from the attribution axes. */
201
- function composeLabel( m: { category: LLMCallCategory; attribute: LLMCallAttribute; function: LLMCallFunction; scope?: string } ): string {
202
- const base = `${m.category}/${m.attribute}/${m.function}`
250
+ /**
251
+ * Compose a stable, readable label from the attribution axes.
252
+ *
253
+ * `function` carries '-' when the call has no specific one (the master's own
254
+ * decision/ideation passes), so it is dropped rather than rendered — a label of
255
+ * `executive/master/-` names nothing. Process always shows: it is what
256
+ * distinguishes the propose pass from the decision it feeds.
257
+ */
258
+ function composeLabel( m: { category: LLMCallCategory; attribute: LLMCallAttribute; process: LLMCallProcess; function: LLMCallFunction; scope?: string } ): string {
259
+ const base = [ m.category, m.attribute, m.process, m.function === '-' ? '' : m.function ]
260
+ .filter( Boolean ).join('/')
203
261
  return m.scope ? `${base}#${m.scope}` : base
204
262
  }
205
263
 
@@ -245,6 +303,12 @@ export class TokenTracker implements SimulationEngine {
245
303
  private _categoryTokens = new Map<string, { prompt: number; completion: number }>()
246
304
  private _functionCosts = new Map<string, number>()
247
305
  private _functionTokens = new Map<string, { prompt: number; completion: number }>()
306
+ // Split out of the function axis: `decision` vs `ideation` is which PROCESS ran,
307
+ // not which function it served. Without its own bucket the deliberate path's
308
+ // propose pass became invisible in cost reporting — every master call landing in
309
+ // the '-' function bucket regardless of whether it deliberated.
310
+ private _processCosts = new Map<string, number>()
311
+ private _processTokens = new Map<string, { prompt: number; completion: number }>()
248
312
  // Per-provider spend. The axis a host actually reconciles against invoices —
249
313
  // "which vendor did we pay?" is not answerable from the model id once routing
250
314
  // can reach one model through several of them.
@@ -299,8 +363,10 @@ export class TokenTracker implements SimulationEngine {
299
363
  const costUsd = pricing
300
364
  ? ( usage.promptTokens / 1_000_000 ) * pricing.input +
301
365
  ( usage.completionTokens / 1_000_000 ) * pricing.output +
302
- ( cacheRead / 1_000_000 ) * pricing.input * CACHE_READ_MULT +
303
- ( cacheWrite / 1_000_000 ) * pricing.input * CACHE_WRITE_MULT
366
+ // `?? fallback` rather than `||`: a stated 0 is a real rate (free cache
367
+ // reads / free storage) and must not silently become the Anthropic ratio.
368
+ ( cacheRead / 1_000_000 ) * ( pricing.cachedInput ?? pricing.input * CACHE_READ_MULT ) +
369
+ ( cacheWrite / 1_000_000 ) * ( pricing.cacheWrite ?? pricing.input * CACHE_WRITE_MULT )
304
370
  : 0
305
371
 
306
372
  const full: TokenUsage = {
@@ -320,6 +386,7 @@ export class TokenTracker implements SimulationEngine {
320
386
  // Per-axis breakdowns — the repartition surface (category × function).
321
387
  this._accumulate( this._categoryCosts, this._categoryTokens, full.category, full )
322
388
  this._accumulate( this._functionCosts, this._functionTokens, full.function, full )
389
+ this._accumulate( this._processCosts, this._processTokens, full.process, full )
323
390
  // Unattributed rather than guessed: a caller that did not say which
324
391
  // provider served the call must not be silently folded into the default.
325
392
  this._accumulate( this._providerCosts, this._providerTokens, full.provider ?? 'unattributed', full )
@@ -350,6 +417,7 @@ export class TokenTracker implements SimulationEngine {
350
417
  provider: full.provider,
351
418
  category: full.category,
352
419
  attribute: full.attribute,
420
+ process: full.process,
353
421
  function: full.function,
354
422
  scope: full.scope,
355
423
  label: full.label,
@@ -363,6 +431,9 @@ export class TokenTracker implements SimulationEngine {
363
431
  // priced this model, NOT because the call was free — a consumer summing
364
432
  // spend must not fold unpriced calls in as zero.
365
433
  priced: full.priced,
434
+ // Undefined stays undefined — see TokenUsage.demand. A consumer that
435
+ // coerces this to 0 has silently invented a measurement.
436
+ demand: full.demand,
366
437
  latencyMs: full.latencyMs,
367
438
  }
368
439
 
@@ -461,6 +532,7 @@ export class TokenTracker implements SimulationEngine {
461
532
  totalCost: this._totalCost,
462
533
  categoryBreakdown: Object.fromEntries( this._categoryCosts ),
463
534
  functionBreakdown: Object.fromEntries( this._functionCosts ),
535
+ processBreakdown: Object.fromEntries( this._processCosts ),
464
536
  },
465
537
  })
466
538
  }
@@ -502,6 +574,16 @@ export class TokenTracker implements SimulationEngine {
502
574
  return this._functionTokens
503
575
  }
504
576
 
577
+ /** Cost broken down by process ('decision' | 'ideation' | 'cog'). */
578
+ get processBreakdown(): ReadonlyMap<string, number> {
579
+ return this._processCosts
580
+ }
581
+
582
+ /** Token counts (prompt + completion) broken down by process. */
583
+ get processTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
584
+ return this._processTokens
585
+ }
586
+
505
587
  /**
506
588
  * Cost broken down by provider ('anthropic' | 'glm' | 'moonshot' | …), plus
507
589
  * an `unattributed` bucket for usage recorded without one.
@@ -554,6 +636,8 @@ export class TokenTracker implements SimulationEngine {
554
636
  this._categoryTokens.clear()
555
637
  this._functionCosts.clear()
556
638
  this._functionTokens.clear()
639
+ this._processCosts.clear()
640
+ this._processTokens.clear()
557
641
  this._providerCosts.clear()
558
642
  this._providerTokens.clear()
559
643
  this._tickCosts = []
@@ -35,6 +35,19 @@ export interface EngineResult {
35
35
  export interface SimulationEngine {
36
36
  readonly name: string
37
37
 
38
+ /**
39
+ * Entity types this engine writes as part of the MIND'S OWN operation — its
40
+ * bookkeeping, not the world's furniture. Declared here, they fall inside the
41
+ * sense boundary and the outward senses stop re-perceiving them (see
42
+ * `#cognition/sense.boundary`).
43
+ *
44
+ * Optional and silent by default, because the default is the right one for a
45
+ * host engine: an engine that maintains rooms, documents or sensor readings
46
+ * wants those perceived. Declare only what the mind should not encounter as an
47
+ * event in its world.
48
+ */
49
+ readonly writes?: readonly string[]
50
+
38
51
  /**
39
52
  * Called once per tick with a frozen read-only snapshot.
40
53
  * Engines must NOT mutate state directly — return commands instead.
@@ -62,6 +75,13 @@ export interface SimulationEngine {
62
75
  }
63
76
 
64
77
 
78
+ /**
79
+ * Fraction of one tick interval a single non-async engine may consume before the
80
+ * orchestrator says so. Half a tick is generous for work that is meant to be
81
+ * in-memory, and low enough to fire long before tick-denominated deadlines drift.
82
+ */
83
+ const SLOW_ENGINE_FRACTION = 0.5
84
+
65
85
  // ── Middleware ───────────────────────────────────────────────
66
86
 
67
87
  export type TickMiddleware = (
@@ -384,6 +404,35 @@ export class DefaultOrchestrator implements Orchestrator {
384
404
  * calls _executeTick() which handles pause/stop checks internally.
385
405
  * The orchestrator is the sole driver of ticks.
386
406
  */
407
+ /**
408
+ * A plain SimulationEngine's `react()` is implicitly "finish inside the tick" —
409
+ * only an AsyncEngine is allowed to span ticks, and it does so by LAUNCHING work
410
+ * and landing it later (see async.engine.ts: "react() never awaits LLM calls").
411
+ * Nothing enforces that on everyone else, and the failure is silent and severe:
412
+ * every agency deadline is denominated in TICKS, so an engine that awaits network
413
+ * I/O does not merely run slowly, it rescales time for the whole mind.
414
+ *
415
+ * Measured: one rate-limited embedding call awaited inside EpisodicConsolidator
416
+ * made two consecutive ticks take 64.9s and 63.5s. `AWAIT_TIMEOUT` — 15 ticks,
417
+ * normally ~15s — silently became 15 minutes, so a communicate intent sat
418
+ * 'awaiting' forever and the serial selector never chose anything again. 45
419
+ * executive decisions produced one intent and zero delivered messages, with no
420
+ * error anywhere. This turns that into a line in the log the first time it happens.
421
+ */
422
+ private _warnIfSlow( engine: SimulationEngine, elapsedMs: number ): void {
423
+ // AsyncEngines are exempt by design — spanning ticks is their contract.
424
+ if( ( engine as { hasPendingWork?: unknown } ).hasPendingWork !== undefined ) return
425
+
426
+ const budget = ( this._config.tickIntervalMs ?? 1000 ) * SLOW_ENGINE_FRACTION
427
+ if( elapsedMs <= budget ) return
428
+
429
+ logger.warn(
430
+ `[Orchestrator] Engine "${ engine.name }" held the tick for ${ Math.round( elapsedMs ) }ms ` +
431
+ `at tick ${ this._currentTick } (budget ${ Math.round( budget ) }ms). A non-async engine must not ` +
432
+ `await network I/O — tick-denominated deadlines elsewhere are being stretched by this.`
433
+ )
434
+ }
435
+
387
436
  private _runLoop( context: SimulationContext ): void {
388
437
  if( this._tickTimer ) return
389
438
 
@@ -478,6 +527,7 @@ export class DefaultOrchestrator implements Orchestrator {
478
527
  const allResults: EngineResult[] = []
479
528
 
480
529
  const runEngine = async ( engine: SimulationEngine ): Promise<void> => {
530
+ const startedAt = wallClock()
481
531
  try {
482
532
  const result = await engine.react?.( this._clock.delta, this._currentTick, snapshot, engineContext )
483
533
  if( result ) allResults.push( result )
@@ -494,6 +544,7 @@ export class DefaultOrchestrator implements Orchestrator {
494
544
  )
495
545
  }
496
546
  }
547
+ finally { this._warnIfSlow( engine, wallClock() - startedAt ) }
497
548
  }
498
549
 
499
550
  for( const engine of this._enginesTick() )
package/src/index.ts CHANGED
@@ -87,8 +87,8 @@ export {
87
87
  } from '#cognition/memory/index'
88
88
 
89
89
  // Profile registry — built-in profiles are auto-registered when this package is imported
90
- // (mind.ts imports '#profiles/built-in' as a side effect).
91
- export { resolveProfile, listProfiles, type WorldProfile } from '#profiles/index'
90
+ // (mind.ts imports '#stem/profiles/built-in' as a side effect).
91
+ export { resolveProfile, listProfiles, type WorldProfile } from '#stem/profiles/index'
92
92
 
93
93
  // PMA (Personal Mind Archive) snapshot types
94
94
  export type {
@@ -109,14 +109,20 @@ export type {
109
109
  PMAProbe,
110
110
  } from '#pma/eval'
111
111
 
112
- // Sensory input types — used by callers of WillManager.ingestText()
112
+ // Sensory input types — used by callers of WillManager.senseText()
113
113
  export type { TextMessage, VoiceChunk, SensoryInput } from '#senses/index'
114
+ // Provenance — whose doing a signal was. Required on every signal, because
115
+ // nothing inside the mind can tell its own echo from a stranger saying the same
116
+ // words: only the host knows, so the host says. `asProvenance` is for untyped
117
+ // ingress only (a tool call, an HTTP body) — the one place a default exists.
118
+ export type { SignalProvenance, SensorySignal, Transduced } from '#senses/provenance'
119
+ export { asProvenance } from '#senses/provenance'
114
120
 
115
121
  // ── SDK facade — the ergonomic embedding API (recommended entry point) ──
116
122
  // `Will.create()` wraps WillStem in the shape a developer expects: on('message'),
117
123
  // effector(name, handler), say(), state(), hibernate()/wake(). Drop to `.stem`
118
124
  // for the full contract. See src/sdk/will.ts + examples/effectors.ts.
119
- export { Will } from '#sdk/will'
125
+ export { Will } from '#surface/sdk/will'
120
126
  export type {
121
127
  CreateWillOptions,
122
128
  Stimulus,
@@ -128,5 +134,33 @@ export type {
128
134
  EffectorResult,
129
135
  EffectorSpec,
130
136
  EffectorEntry,
131
- } from '#sdk/will'
137
+ } from '#surface/sdk/will'
132
138
  export type { SchemaPrecondition, EffectorDeclaration } from '#agency/types'
139
+
140
+ // ── Policy — the PDP/PEP boundary (POLICY_REAFFERENCE) ──────────────────
141
+ // A host installs a PolicyArbiter via `WillStem.setArbiter()` / `Will.setArbiter()`
142
+ // to gate host-owned effector invocations before they reach the world: allow,
143
+ // deny (with `finality` — class/parameter/context, see arbiter.ts), or escalate
144
+ // (hold the intent, voice the ask, resolve via `resolveEscalation()`). No arbiter
145
+ // installed ⇒ byte-identical to a Will with no policy layer. `RuleTableArbiter`
146
+ // is the local reference PDP — pure and declarative, sufficient for static
147
+ // schema/target/parameter rules; a host needing live world state (session/DB
148
+ // lookups) implements `PolicyArbiter` directly instead.
149
+ export {
150
+ NULL_ARBITER,
151
+ isNullArbiter,
152
+ finalityOf,
153
+ asFinality,
154
+ type PolicyArbiter,
155
+ type PolicyDecision,
156
+ type DenialFinality,
157
+ type PolicyCounterfactual,
158
+ type Verdict,
159
+ type PolicyInvocation,
160
+ } from '#stem/policy/arbiter'
161
+ export {
162
+ RuleTableArbiter,
163
+ type PolicyRule,
164
+ type ParamConstraint,
165
+ type RuleTableOptions,
166
+ } from '#stem/policy/rule.table'