@mindot/will 0.7.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.
- package/README.md +87 -22
- package/dist/channels/discord.d.ts +67 -6
- package/dist/channels/discord.js +112 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js +4 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +13733 -11057
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3729 -1199
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
- package/package.json +1 -1
- package/src/channels/discord.ts +189 -11
- package/src/channels/types.ts +90 -0
- package/src/channels/whatsapp.ts +13 -4
- package/src/cli.ts +9 -4
- package/src/cognition/agency/consequence.ts +122 -1
- package/src/cognition/agency/conversation.aim.ts +260 -0
- package/src/cognition/agency/engines/action.selector.ts +85 -3
- package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
- package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
- package/src/cognition/agency/engines/reafference.engine.ts +129 -2
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/agency/selection.scoring.ts +33 -0
- package/src/cognition/agency/types.ts +35 -0
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +109 -1
- package/src/cognition/event.schemas.ts +22 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +67 -13
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +686 -187
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
- package/src/cognition/faculties/executive.engine/facet.ts +89 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +21 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
- package/src/cognition/faculties/executive.engine/types.ts +69 -0
- package/src/cognition/faculties/goal.manager.ts +94 -14
- package/src/cognition/faculties/known.entity.tracker.ts +267 -28
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +10 -20
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +11 -0
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +54 -7
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/senses/audition.engine/engine.ts +404 -46
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/senses/index.ts +12 -0
- package/src/cognition/social.identity.ts +273 -0
- package/src/cognition/utilities/token.tracker.ts +246 -98
- package/src/core/orchestrator.ts +38 -0
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +415 -97
- package/src/llm/routing.ts +204 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/llm/wire.contracts.ts +19 -0
- package/src/pma/index.ts +67 -53
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +121 -22
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +82 -5
- package/src/stem/mind.ts +327 -79
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- package/src/stem/tracts/effector.controller.ts +56 -9
- package/src/stem/tracts/outbox.writer.ts +40 -2
- package/src/cognition/faculties/executive.engine/messages.ts +0 -102
|
@@ -24,7 +24,8 @@ import type {
|
|
|
24
24
|
SimulationContext,
|
|
25
25
|
ReadonlySimulationState,
|
|
26
26
|
ReasoningFootprint,
|
|
27
|
-
StateCommands
|
|
27
|
+
StateCommands,
|
|
28
|
+
EntityInput
|
|
28
29
|
} from '#core/types'
|
|
29
30
|
import { AsyncEngine } from '#core/async.engine'
|
|
30
31
|
import type { IntermediateStream } from '#core/async.engine'
|
|
@@ -42,6 +43,9 @@ import { GenerativeModel } from '#cognition/generative.model'
|
|
|
42
43
|
import { ExecutiveSummarizer } from '#llm/summarizer'
|
|
43
44
|
import { ExecutiveFacet, type ExecutiveFacetHandle } from '#faculties/executive.engine/facet'
|
|
44
45
|
import type { ExecutiveOutputFull, IdeationCandidate } from '#faculties/executive.engine/types'
|
|
46
|
+
import { DeliberationCache } from '#cognition/cache/deliberation.cache'
|
|
47
|
+
import { extractFingerprint } from '#cognition/cache/fingerprint'
|
|
48
|
+
import type { DeliberationCacheConfig, DeliberationCacheSnapshot } from '#cognition/cache/types'
|
|
45
49
|
import {
|
|
46
50
|
PromptFactory,
|
|
47
51
|
type PromptDependencies,
|
|
@@ -60,19 +64,21 @@ import {
|
|
|
60
64
|
type GatingState
|
|
61
65
|
} from '#faculties/executive.engine/gating'
|
|
62
66
|
import { LLMDirector } from '#llm/index'
|
|
63
|
-
import {
|
|
67
|
+
import { MOCK_PROVIDER, MOCK_MODEL } from '#llm/index'
|
|
68
|
+
import { getCompletionSource } from '#core/completion.recorder'
|
|
69
|
+
import { providerKeyFromEnv, type LLMProvider, type LLMCallMeta, type ProviderCredential, type LLMWire } from '#llm/index'
|
|
70
|
+
import type { ModelRouter } from '#llm/routing'
|
|
64
71
|
import { buildFallbackOutput, parseResponse } from '#faculties/executive.engine/parser'
|
|
65
72
|
import { selectProcess, ideationTemperature, DELIBERATE_THRESHOLD } from '#faculties/executive.engine/effort.gate'
|
|
66
73
|
import { proposeCandidates } from '#faculties/executive.engine/deliberate.reasoning'
|
|
67
74
|
import { readEffectiveParams } from '#cognition/persona.prior'
|
|
68
|
-
import { MessageQueue } from '#faculties/executive.engine/messages'
|
|
69
75
|
import {
|
|
70
76
|
buildStateCommands,
|
|
71
77
|
publishCognitiveEvents,
|
|
72
78
|
type CommandDependencies
|
|
73
79
|
} from '#faculties/executive.engine/commands'
|
|
74
80
|
import { DeferredEffectQueue } from '#faculties/executive.engine/deferred.effects'
|
|
75
|
-
import { EscalationBuffer } from '#faculties/executive.engine/escalation.buffer'
|
|
81
|
+
import { EscalationBuffer, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
|
|
76
82
|
import { FacetSupervisor } from '#faculties/executive.engine/facet.supervisor'
|
|
77
83
|
import type { EngineResult } from '#core/orchestrator'
|
|
78
84
|
import type { Duration } from '#core/types'
|
|
@@ -81,6 +87,17 @@ import { wallClock } from '#core/wall.clock'
|
|
|
81
87
|
// Re-export for compatibility
|
|
82
88
|
export { ExecutiveFacet, type ExecutiveFacetHandle }
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* How hard a reasoning facet pulls on attention, as an `attention.demand` urgency.
|
|
92
|
+
*
|
|
93
|
+
* Deliberately below 1: holding a live conversation is a real claim on attention,
|
|
94
|
+
* but the allocator sorts candidates into `maxFoci` slots by salience, and a facet
|
|
95
|
+
* that always outranked everything would starve perception — the mind would stop
|
|
96
|
+
* noticing the world whenever it was talking. At 0.7 an urgent percept still wins
|
|
97
|
+
* a slot; routine traffic does not.
|
|
98
|
+
*/
|
|
99
|
+
const FACET_ATTENTION_URGENCY = 0.7
|
|
100
|
+
|
|
84
101
|
// ── Engine config ───────────────────────────────────────────
|
|
85
102
|
|
|
86
103
|
export interface ExecutiveEngineConfig {
|
|
@@ -128,7 +145,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
128
145
|
private _testMode = false
|
|
129
146
|
|
|
130
147
|
// ── Message queue ──────────────────────────────────────────
|
|
131
|
-
private _messageQueue = new MessageQueue()
|
|
132
148
|
|
|
133
149
|
// ── Action diversity tracking ──────────────────────────────
|
|
134
150
|
private _recentActionTypes: string[] = []
|
|
@@ -143,16 +159,25 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
143
159
|
private _lastExecutiveOutput: ExecutiveOutputFull | null = null
|
|
144
160
|
private _lastExecutiveTick: number = -100
|
|
145
161
|
|
|
162
|
+
// ── DeliberationCache (optional fast path) ─────────────────
|
|
163
|
+
private _cache: DeliberationCache | null = null
|
|
164
|
+
private _cacheRestored = false
|
|
165
|
+
private _pendingVerify: { fingerprint: Float32Array; cachedActionTypes: string[] } | null = null
|
|
166
|
+
// Last cache outcome this cycle, surfaced as metrics/events from the committed path.
|
|
167
|
+
private _lastCacheHit = false
|
|
168
|
+
private _lastCacheConfidence = 0
|
|
169
|
+
private _lastCacheNeighborCount = 0
|
|
170
|
+
|
|
146
171
|
// ── Injected dependencies ──────────────────────────────────
|
|
147
172
|
private _willId: string | null = null
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
/**
|
|
174
|
+
* The Will's default model (config.model's `executive` role, resolved in
|
|
175
|
+
* mind.ts). Every other role reaches its model through the router — see
|
|
176
|
+
* `compileRoleRouter`.
|
|
177
|
+
*/
|
|
178
|
+
private _modelId: string | null = null
|
|
151
179
|
/** Per-Will LLM transport overrides (config.llm) — env fallbacks apply per field. */
|
|
152
|
-
private _llm: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number } | null = null
|
|
153
|
-
/** One director per distinct model — same config, different model. Shared
|
|
154
|
-
* tracker/recorder/willId, so ledger attribution and replay hold per role. */
|
|
155
|
-
private _directorCache = new Map<string, LLMDirector>()
|
|
180
|
+
private _llm: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number; credentials?: Partial<Record<string, ProviderCredential>>; router?: ModelRouter | null; wire?: LLMWire } | null = null
|
|
156
181
|
private _workingMemory: WorkingMemory | null = null
|
|
157
182
|
private _goalManager: GoalManager | null = null
|
|
158
183
|
private _episodicConsolidator: EpisodicConsolidator | null = null
|
|
@@ -172,7 +197,15 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
172
197
|
// engine keeps only the bus-subscription guard, since those subscriptions
|
|
173
198
|
// feed the gating salience buffer and are shared with the escalation path.
|
|
174
199
|
private readonly _facetSupervisor = new FacetSupervisor()
|
|
175
|
-
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Who each live facet is engaged with, learned from `executive.facet.sync`.
|
|
203
|
+
* Keyed by facetId; the last sync wins. Rendered into the master's own prompt so
|
|
204
|
+
* the singular seat can reason across its conversations "as if they were sitting
|
|
205
|
+
* at the same table" — which it cannot do while it only knows facet numbers.
|
|
206
|
+
* Stale entries age out on read (see _activeConversations).
|
|
207
|
+
*/
|
|
208
|
+
private _facetSubjects = new Map<string, { entityId: string; name?: string; tick: number }>()
|
|
176
209
|
|
|
177
210
|
// ── Cognitive models ───────────────────────────────────────
|
|
178
211
|
private readonly _model = new GenerativeModel()
|
|
@@ -184,6 +217,17 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
184
217
|
// ── Last state reference (for onReasoningComplete and facets) ─
|
|
185
218
|
private _lastStateRef: ReadonlySimulationState | null = null
|
|
186
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The tick currently being processed, refreshed every react() — distinct from
|
|
222
|
+
* `_lastStateRef` (which tracks the REASONING tick and must not move under
|
|
223
|
+
* onReasoningComplete) and from `_lastExecutiveTick` (the last cycle that ran).
|
|
224
|
+
*
|
|
225
|
+
* Off-tick arrivals — a facet handoff, in particular — need to be stamped with
|
|
226
|
+
* when they actually happened. Using `_lastExecutiveTick` for that dated them to
|
|
227
|
+
* the last master cycle, which can be hundreds of ticks behind.
|
|
228
|
+
*/
|
|
229
|
+
private _currentTick = 0
|
|
230
|
+
|
|
187
231
|
// ── Deferred manager side-effects (FN11) ───────────────────
|
|
188
232
|
// Commit-gated queue for the mirroring manager writes returned by
|
|
189
233
|
// buildStateCommands. The queue runs them only after the orchestrator
|
|
@@ -228,8 +272,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
228
272
|
goallessTickCount: 0,
|
|
229
273
|
lowValenceTickCount: 0
|
|
230
274
|
}
|
|
231
|
-
|
|
232
|
-
this._ensureFacetSyncSubscription()
|
|
233
275
|
}
|
|
234
276
|
|
|
235
277
|
// ── Dependency injection ───────────────────────────────────
|
|
@@ -253,7 +295,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
253
295
|
/** Called by CognitiveOrchestrator.addEngine() — injects the shared bus. */
|
|
254
296
|
attachBus( bus: CognitiveBus ): void {
|
|
255
297
|
this._bus = bus
|
|
256
|
-
this._ensureFacetSyncSubscription()
|
|
257
298
|
}
|
|
258
299
|
|
|
259
300
|
/**
|
|
@@ -267,13 +308,18 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
267
308
|
|
|
268
309
|
set willId( willId: string ){ this._willId = willId }
|
|
269
310
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
|
|
311
|
+
/**
|
|
312
|
+
* The Will's default model. Set before the first tick.
|
|
313
|
+
*
|
|
314
|
+
* This replaced a four-role map (W7): the other roles are routing rules now,
|
|
315
|
+
* compiled in mind.ts, so the engine holds one model and one router rather
|
|
316
|
+
* than a model per role plus a router.
|
|
317
|
+
*/
|
|
318
|
+
set modelId( id: string | null ){ this._modelId = id }
|
|
319
|
+
get modelId(): string | null { return this._modelId }
|
|
320
|
+
|
|
273
321
|
/** Per-Will LLM transport overrides (config.llm). Set before the first tick. */
|
|
274
|
-
set llm( c: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number } | null ){ this._llm = c }
|
|
275
|
-
/** The executive-role model id (back-compat read). */
|
|
276
|
-
get modelId(): string | null { return this._models.executive }
|
|
322
|
+
set llm( c: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number; credentials?: Partial<Record<string, ProviderCredential>>; router?: ModelRouter | null; wire?: LLMWire } | null ){ this._llm = c }
|
|
277
323
|
|
|
278
324
|
// ── Public surface ─────────────────────────────────────────
|
|
279
325
|
|
|
@@ -301,48 +347,108 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
301
347
|
* and subscribe() to receive facet decisions.
|
|
302
348
|
*/
|
|
303
349
|
/** Get-or-create the director for a model id (shared config, per-Will). */
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return
|
|
350
|
+
/**
|
|
351
|
+
* The provider, from config or environment — never guessed.
|
|
352
|
+
*
|
|
353
|
+
* This used to default to 'anthropic', which is how a Will configured for one
|
|
354
|
+
* vendor could quietly talk to another. An unset provider is a configuration
|
|
355
|
+
* error, and saying so at construction is far cheaper than a 401 mid-tick.
|
|
356
|
+
*/
|
|
357
|
+
private _requireProvider(): LLMProvider {
|
|
358
|
+
const provider = this._llm?.provider ?? process.env.WILL_LLM_PROVIDER
|
|
359
|
+
?? ( this._noLiveCalls() ? MOCK_PROVIDER : undefined )
|
|
360
|
+
if( !provider )
|
|
361
|
+
throw new Error(
|
|
362
|
+
'No LLM provider configured. Set one on the Will (llm.provider) or in ' +
|
|
363
|
+
'the environment (WILL_LLM_PROVIDER) — the engine carries no default.'
|
|
364
|
+
)
|
|
365
|
+
return provider as LLMProvider
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* True when this Will cannot make a live call, so provider/model are not
|
|
370
|
+
* required: mock mode, or a replay re-feeding recorded completions.
|
|
371
|
+
*/
|
|
372
|
+
private _noLiveCalls(): boolean {
|
|
373
|
+
return this._testMode || ( !!this._willId && !!getCompletionSource( this._willId ) )
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Build this Will's one and only director.
|
|
378
|
+
*
|
|
379
|
+
* There used to be a cache of them, keyed by model, because the per-role
|
|
380
|
+
* model map had no other way to make a role use a different model. Routing
|
|
381
|
+
* gave it one — the role map now compiles to rules (see `compileRoleRouter`)
|
|
382
|
+
* and a single director resolves every call's endpoint per call. That is also
|
|
383
|
+
* strictly more faithful: a facet follows the work it is doing rather than
|
|
384
|
+
* whatever role it happened to be spawned under.
|
|
385
|
+
*/
|
|
386
|
+
private _buildDirector( model: string ): LLMDirector {
|
|
387
|
+
// Resolved first: the key fallback below is keyed by it.
|
|
388
|
+
const provider = this._requireProvider()
|
|
389
|
+
|
|
390
|
+
// Per-Will transport overrides first (BYO keys), env per field otherwise.
|
|
391
|
+
return new LLMDirector( {
|
|
392
|
+
willId: this._willId!,
|
|
393
|
+
model,
|
|
394
|
+
maxOutputTokens: this._llm?.maxOutputTokens ?? parseInt( process.env.WILL_MAX_OUTPUT_TOKENS ?? '8096'),
|
|
395
|
+
// Config, then the provider-agnostic env, then THIS provider's own env.
|
|
396
|
+
// The last step is not the fallback W9 removed: that one ended at
|
|
397
|
+
// ANTHROPIC_API_KEY for every provider, so a Will pointed elsewhere sent
|
|
398
|
+
// Anthropic's key to a stranger. This one can only ever read the key
|
|
399
|
+
// belonging to the provider actually configured.
|
|
400
|
+
apiKey: this._llm?.apiKey ?? process.env.WILL_LLM_API_KEY ?? providerKeyFromEnv( provider ) ?? '',
|
|
401
|
+
provider,
|
|
402
|
+
// Optional base-URL override (e.g. Ollama / Azure / self-hosted). Unset →
|
|
403
|
+
// the director uses the provider's official endpoint.
|
|
404
|
+
baseUrl: this._llm?.baseUrl ?? process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
|
|
405
|
+
timeoutMs: this._llm?.timeoutMs ?? ( process.env.WILL_LLM_TIMEOUT_MS ? parseInt( process.env.WILL_LLM_TIMEOUT_MS ) : undefined ),
|
|
406
|
+
sessionLogger: this._sessionLogger,
|
|
407
|
+
mock: this._testMode,
|
|
408
|
+
// Inject the per-Will tracker (R4) so live calls record usage here, not
|
|
409
|
+
// through a process global. null is fine — the director skips recording.
|
|
410
|
+
tokenTracker: this._tokenTracker,
|
|
411
|
+
// Credentials for routed calls, narrowed by the stem from the host's
|
|
412
|
+
// per-provider map. Prices from that same map ride to the TokenTracker
|
|
413
|
+
// instead, so nothing carries pricing into the call path.
|
|
414
|
+
...( this._llm?.credentials ? { credentials: this._llm.credentials } : {} ),
|
|
415
|
+
// Per-call model selection — the host's router chained with the rules
|
|
416
|
+
// compiled from the per-role model map.
|
|
417
|
+
...( this._llm?.router ? { router: this._llm.router } : {} ),
|
|
418
|
+
// Dialect for the default provider — required for anything outside the
|
|
419
|
+
// known set, so the engine never guesses how to talk to an endpoint.
|
|
420
|
+
...( this._llm?.wire ? { wire: this._llm.wire } : {} ),
|
|
421
|
+
} )
|
|
328
422
|
}
|
|
329
423
|
|
|
330
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Spawn a facet.
|
|
426
|
+
*
|
|
427
|
+
* `role` declares the facet's intent at the call site. It no longer selects a
|
|
428
|
+
* model: that used to happen here, pinning a facet for life to whatever role
|
|
429
|
+
* it was spawned under, and it now happens per call from the focus function
|
|
430
|
+
* the caller sets immediately afterwards (W7). The two always agreed — every
|
|
431
|
+
* spawn site sets a focus whose `function` matches its role — so the routed
|
|
432
|
+
* answer is the same one, decided later and from the work itself.
|
|
433
|
+
*/
|
|
434
|
+
spawnFacet(
|
|
435
|
+
role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision',
|
|
436
|
+
/**
|
|
437
|
+
* What this facet is FOR — see `FacetSpawnDeps.key`. Two spawns with the same
|
|
438
|
+
* key get the same facet, so callers no longer each invent their own dedup
|
|
439
|
+
* (and `authorOutreach`, which had none, no longer opens a rival facet on a
|
|
440
|
+
* person the mind is already talking to).
|
|
441
|
+
*/
|
|
442
|
+
key?: string,
|
|
443
|
+
): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
|
|
444
|
+
void role
|
|
331
445
|
// Delegate to FacetSupervisor (R5-g-3), passing the current engine
|
|
332
446
|
// attachments. The supervisor owns the registry + attention budget and
|
|
333
447
|
// performs the throw-checks on bus / director / state ref.
|
|
334
|
-
// A role with its own configured model gets that role's director; every
|
|
335
|
-
// other facet shares the executive's (one self, role-appropriate depth).
|
|
336
|
-
// Outreach speaks with the conversation voice; supervision thinks with the
|
|
337
|
-
// executive's depth.
|
|
338
|
-
const roleModel =
|
|
339
|
-
role === 'deliberation' ? this._models.deliberation :
|
|
340
|
-
role === 'conversation' || role === 'outreach' ? this._models.conversation :
|
|
341
|
-
null
|
|
342
|
-
const director = roleModel && this._llmDirector ? this._directorFor( roleModel ) : this._llmDirector
|
|
343
448
|
return this._facetSupervisor.spawn( {
|
|
449
|
+
...( key ? { key } : {} ),
|
|
344
450
|
bus: this._bus,
|
|
345
|
-
llmDirector:
|
|
451
|
+
llmDirector: this._llmDirector,
|
|
346
452
|
stateRef: this._lastStateRef,
|
|
347
453
|
willId: this._willId,
|
|
348
454
|
inbox: this._inbox,
|
|
@@ -358,6 +464,14 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
358
464
|
} )
|
|
359
465
|
}
|
|
360
466
|
|
|
467
|
+
/**
|
|
468
|
+
* The facet already attending to `key`, if one is open — without spawning.
|
|
469
|
+
* See FacetSupervisor.handleFor.
|
|
470
|
+
*/
|
|
471
|
+
facetFor( key: string ): ExecutiveFacetHandle | undefined {
|
|
472
|
+
return this._facetSupervisor.handleFor( key )
|
|
473
|
+
}
|
|
474
|
+
|
|
361
475
|
// ── CognitiveEngine interface ──────────────────────────────
|
|
362
476
|
|
|
363
477
|
subscribes(): string[] { return [ '*' ] }
|
|
@@ -374,14 +488,28 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
374
488
|
onCognitiveEvent( event: CognitiveEvent ): StateCommands | void {
|
|
375
489
|
if( event.sourceEngine === this.name ) return
|
|
376
490
|
|
|
377
|
-
//
|
|
378
|
-
// One facet per ~0.3 free capacity units, floor at 1
|
|
491
|
+
// Spare attention scales the facet allowance within the persona's ceiling.
|
|
379
492
|
if( event.type === 'attention.state.changed'){
|
|
380
493
|
const p = event.payload as { freeFraction: number }
|
|
381
494
|
this._facetSupervisor.setAttentionState( p.freeFraction )
|
|
382
495
|
return
|
|
383
496
|
}
|
|
384
497
|
|
|
498
|
+
// The facet legs. These MUST be dispatched here rather than through their own
|
|
499
|
+
// `bus.subscribe` calls: the bus keeps one subscription per engineId, and the
|
|
500
|
+
// orchestrator's `subscribe( engine.name, engine.subscribes(), … )` runs after
|
|
501
|
+
// `attachBus`, so anything registered separately is silently replaced. This
|
|
502
|
+
// `['*']` subscription is the engine's only live one.
|
|
503
|
+
if( event.type === 'executive.facet.sync'){
|
|
504
|
+
this._onFacetSync( event )
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if( event.type === 'executive.facet.handoff'){
|
|
509
|
+
this._onFacetHandoff( event )
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
385
513
|
// GWT salience-based buffer
|
|
386
514
|
( event.salience ?? 0 ) >= WORKSPACE_THRESHOLD
|
|
387
515
|
&& this._gatingState.salienceBuffer.push( {
|
|
@@ -404,6 +532,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
404
532
|
state: ReadonlySimulationState,
|
|
405
533
|
context: SimulationContext,
|
|
406
534
|
): Promise<EngineResult> {
|
|
535
|
+
this._currentTick = tick as unknown as number
|
|
407
536
|
this._deferred.flush( state, tick as unknown as number )
|
|
408
537
|
this._deferred.markReactTick( tick as unknown as number )
|
|
409
538
|
// Per-tick facet pump: refresh every facet's state ref to THIS tick's frozen
|
|
@@ -411,7 +540,77 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
411
540
|
// Sits at a fixed point in the serial engine order — the issue-side twin of
|
|
412
541
|
// the CompletionInbox drain in Phase 2. See .TODO/FACET_REPLAY_DETERMINISM.md.
|
|
413
542
|
this._facetSupervisor.pump( state )
|
|
414
|
-
|
|
543
|
+
|
|
544
|
+
const result = await super.react( delta, tick, state, context )
|
|
545
|
+
|
|
546
|
+
// Reasoning facets occupy attention. Merged into whatever this tick already
|
|
547
|
+
// produced — AsyncEngine.react returns commands on every tick, reasoning or not,
|
|
548
|
+
// so the allocator sees the cost land the tick it is incurred.
|
|
549
|
+
const focus = this._facetAttentionDemands( state, tick as unknown as number )
|
|
550
|
+
if( focus.set.length || focus.delete.length ){
|
|
551
|
+
result.commands ??= {}
|
|
552
|
+
result.commands.set = [ ...( result.commands.set ?? [] ), ...focus.set ]
|
|
553
|
+
result.commands.delete = [ ...( result.commands.delete ?? [] ), ...focus.delete ]
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return result
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* What the mind is attending to because a facet is reasoning about it, as
|
|
561
|
+
* `attention.demand` entities the AttentionAllocator allocates real capacity
|
|
562
|
+
* against (`_extractSalienceSignals` reads this type; `costPerFocus` is then
|
|
563
|
+
* charged against the same 100-unit budget as perceptual foci).
|
|
564
|
+
*
|
|
565
|
+
* This closes a loop that was open in one direction only: the allocator's
|
|
566
|
+
* `freeFraction` scaled the facet budget, but facets never appeared in
|
|
567
|
+
* `_activeFocus`, so holding three conversations reported exactly as much spare
|
|
568
|
+
* attention as holding none. The budget was being scaled by a signal blind to the
|
|
569
|
+
* thing it was bounding.
|
|
570
|
+
*
|
|
571
|
+
* `urgency` sits below 1 on purpose: a live conversation is a genuine claim on
|
|
572
|
+
* attention but must not automatically outrank every percept — the allocator sorts
|
|
573
|
+
* candidates by salience into `maxFoci` slots, and a facet that always won would
|
|
574
|
+
* starve perception. Only BUSY facets are charged; an open-but-quiet thread is one
|
|
575
|
+
* the mind is in, not one it is attending to.
|
|
576
|
+
*/
|
|
577
|
+
private _facetAttentionDemands(
|
|
578
|
+
state: ReadonlySimulationState,
|
|
579
|
+
tick: number,
|
|
580
|
+
): { set: EntityInput[]; delete: string[] } {
|
|
581
|
+
const busy = this._facetSupervisor.busyFacetIds()
|
|
582
|
+
const set: EntityInput[] = []
|
|
583
|
+
const live = new Set<string>()
|
|
584
|
+
|
|
585
|
+
for( const facetId of busy ){
|
|
586
|
+
const id = `facet-attending-${facetId}`
|
|
587
|
+
live.add( id )
|
|
588
|
+
const subject = this._facetSubjects.get( facetId )
|
|
589
|
+
set.push({
|
|
590
|
+
id,
|
|
591
|
+
type: 'attention.demand',
|
|
592
|
+
metadata: {
|
|
593
|
+
urgency: FACET_ATTENTION_URGENCY,
|
|
594
|
+
source: 'executive-facet',
|
|
595
|
+
facetId,
|
|
596
|
+
...( subject?.entityId ? { subjectEntityId: subject.entityId } : {} ),
|
|
597
|
+
...( subject?.name ? { subjectName: subject.name } : {} ),
|
|
598
|
+
// NOT `generatesGoal` — GoalManager turns flagged demands into goals, and
|
|
599
|
+
// "I am talking to someone" is a state, not something to pursue.
|
|
600
|
+
tick,
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Release the attention a facet held once it stops reasoning (or is reaped).
|
|
606
|
+
const del: string[] = []
|
|
607
|
+
for( const [ id, e ] of state.entities )
|
|
608
|
+
if( e.type === 'attention.demand'
|
|
609
|
+
&& ( e.metadata as Record<string, unknown> | undefined )?.['source'] === 'executive-facet'
|
|
610
|
+
&& !live.has( id ) )
|
|
611
|
+
del.push( id )
|
|
612
|
+
|
|
613
|
+
return { set, delete: del }
|
|
415
614
|
}
|
|
416
615
|
|
|
417
616
|
protected override shouldAct(
|
|
@@ -425,6 +624,12 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
425
624
|
this._summarizerRestored = true
|
|
426
625
|
}
|
|
427
626
|
|
|
627
|
+
// Restore the deliberation cache from state on first tick (parallels the summary)
|
|
628
|
+
if( this._cache && !this._cacheRestored ){
|
|
629
|
+
this._restoreDeliberationCache( state )
|
|
630
|
+
this._cacheRestored = true
|
|
631
|
+
}
|
|
632
|
+
|
|
428
633
|
// Read runtime config overrides from state
|
|
429
634
|
const rtConfig = readRuntimeConfig( state, {
|
|
430
635
|
executiveInterval: this._executiveInterval,
|
|
@@ -433,21 +638,37 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
433
638
|
this._gatingState.executiveInterval = rtConfig.executiveInterval
|
|
434
639
|
this._gatingState.cooldownTicks = rtConfig.cooldownTicks
|
|
435
640
|
|
|
436
|
-
// Initialize LLM
|
|
437
|
-
//
|
|
641
|
+
// Initialize the LLM director if not yet done (requires willId). One per
|
|
642
|
+
// Will — every role reaches its model through the router.
|
|
438
643
|
if( !this._llmDirector && this._willId ){
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
644
|
+
// No default model. The engine used to fall back to a Claude id for every
|
|
645
|
+
// provider but GLM, which meant a misconfigured Will asked the wrong
|
|
646
|
+
// vendor for the wrong model and failed at the first tick with a 404
|
|
647
|
+
// instead of at construction with a sentence.
|
|
648
|
+
//
|
|
649
|
+
// Two cases legitimately have no credentials and are exempt: a test-mode
|
|
650
|
+
// Will (never reaches a network — demanding a key would break the whole
|
|
651
|
+
// point of the no-key quickstart) and a replay (completions are re-fed
|
|
652
|
+
// from the tape, which carries the model that actually served them). The
|
|
653
|
+
// sentinel is what a mock run records, so the tape still says plainly
|
|
654
|
+
// that nothing real answered.
|
|
655
|
+
const defaultModel = this._modelId ?? process.env.WILL_LLM_MODEL
|
|
656
|
+
?? ( this._noLiveCalls() ? MOCK_MODEL : undefined )
|
|
657
|
+
if( !defaultModel )
|
|
658
|
+
throw new Error(
|
|
659
|
+
'No LLM model configured. Set one on the Will (llm.model) or in the ' +
|
|
660
|
+
'environment (WILL_LLM_MODEL) — the engine carries no default.'
|
|
661
|
+
)
|
|
662
|
+
this._llmDirector = this._buildDirector( defaultModel )
|
|
663
|
+
// The summarizer shares that director. Its calls tag themselves
|
|
664
|
+
// `category: 'summarizer'`, which is what a configured summarizer role
|
|
665
|
+
// compiles to — so it still gets its own model, chosen per call.
|
|
666
|
+
this._summarizer?.attachLLMDirector( this._llmDirector )
|
|
445
667
|
}
|
|
446
668
|
|
|
447
669
|
// Evaluate gating
|
|
448
670
|
const gatingDeps: GatingDependencies = {
|
|
449
671
|
generativeModel: this._generativeModel,
|
|
450
|
-
pendingMessages: this._messageQueue.pendingMessages,
|
|
451
672
|
hasPendingWork: this.hasPendingWork
|
|
452
673
|
}
|
|
453
674
|
|
|
@@ -489,7 +710,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
489
710
|
stream: IntermediateStream,
|
|
490
711
|
): Promise<unknown> {
|
|
491
712
|
this._lastStateRef = state
|
|
492
|
-
this._messageQueue.pendingCallStartTick = state.tick
|
|
493
713
|
|
|
494
714
|
// Mark which buffer entries this cycle consumes. Anything pushed after this
|
|
495
715
|
// point arrives during the LLM call and was never part of the broadcast, so
|
|
@@ -499,6 +719,47 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
499
719
|
// Update all facets with the latest state reference
|
|
500
720
|
this._facetSupervisor.broadcastStateRef( state )
|
|
501
721
|
|
|
722
|
+
// ── DeliberationCache fast path (optional) ───────────────
|
|
723
|
+
// A learned interpolation of past executive outputs. When a highly similar,
|
|
724
|
+
// highly competent precedent exists we skip the LLM entirely. R2-safe: the
|
|
725
|
+
// fingerprint, retrieval and composition are pure functions of the frozen
|
|
726
|
+
// state and the cache's own snapshotted contents. The mandatory bookkeeping
|
|
727
|
+
// above (buffer marking, facet broadcast) has already run, so a cache hit and
|
|
728
|
+
// a slow tick leave the facet/buffer discipline in the same state.
|
|
729
|
+
this._pendingVerify = null
|
|
730
|
+
if( this._cache ){
|
|
731
|
+
const cacheTick = footprint.tickObserved as unknown as number
|
|
732
|
+
this._cache.decay()
|
|
733
|
+
const fp = extractFingerprint( state )
|
|
734
|
+
const cacheResult = this._cache.retrieve( fp, cacheTick )
|
|
735
|
+
this._lastCacheHit = cacheResult.hit
|
|
736
|
+
this._lastCacheConfidence = cacheResult.confidence
|
|
737
|
+
this._lastCacheNeighborCount = cacheResult.neighbors.length
|
|
738
|
+
|
|
739
|
+
if( cacheResult.hit && cacheResult.output ){
|
|
740
|
+
if( !this._cache.shouldVerify() ){
|
|
741
|
+
// FAST PATH — return the composed output; this tick spends no tokens.
|
|
742
|
+
stream.report('executive_complete', {
|
|
743
|
+
actionCount: cacheResult.output.actions.length,
|
|
744
|
+
planCount: 0,
|
|
745
|
+
newBeliefCount: cacheResult.output.newBeliefs?.length ?? 0,
|
|
746
|
+
hasIntrospection: false,
|
|
747
|
+
hasNarrative: false,
|
|
748
|
+
} )
|
|
749
|
+
logger.info(
|
|
750
|
+
`[executive] ⚡ cache hit tick=${cacheTick} ` +
|
|
751
|
+
`ρ=${cacheResult.confidence.toFixed( 3 )} neighbors=${cacheResult.neighbors.length}`
|
|
752
|
+
)
|
|
753
|
+
return cacheResult.output
|
|
754
|
+
}
|
|
755
|
+
// Hit selected for verification — run the LLM and score the cache against it.
|
|
756
|
+
this._pendingVerify = {
|
|
757
|
+
fingerprint: fp,
|
|
758
|
+
cachedActionTypes: cacheResult.output.actions.map( a => a.type ),
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
502
763
|
// Build executive context using PromptFactory's helper
|
|
503
764
|
const execContext = await PromptFactory.buildFreshContext( {
|
|
504
765
|
workingMemory: this._workingMemory,
|
|
@@ -532,7 +793,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
532
793
|
priorConfidence: this._lastExecutiveOutput?.confidence ?? 0.5,
|
|
533
794
|
novelty: state.metrics.get('perception.novelty') ?? 0,
|
|
534
795
|
stressLoad: state.metrics.get('stress.load') ?? 0,
|
|
535
|
-
|
|
796
|
+
// Something a conversation surfaced is waiting on the master and has not
|
|
797
|
+
// been dealt with. This used to read the pending-message queue, which was
|
|
798
|
+
// never filled — so the effort gate's `pending_reply` term was structurally
|
|
799
|
+
// zero for every master that has ever run. Escalations are the master's real
|
|
800
|
+
// version of "someone is waiting on me": the facets do the replying, the
|
|
801
|
+
// master owns what they escalate.
|
|
802
|
+
hasPendingMessage: this._escalations.size > 0,
|
|
536
803
|
}, deliberateThreshold )
|
|
537
804
|
stream.report('process_selected', {
|
|
538
805
|
process: processSelection.process,
|
|
@@ -575,11 +842,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
575
842
|
state,
|
|
576
843
|
qualityModulation,
|
|
577
844
|
epistemicUncertainty,
|
|
578
|
-
|
|
579
|
-
focus,
|
|
845
|
+
focus,
|
|
580
846
|
deps: promptDeps,
|
|
581
847
|
recentActionTypes: [ ...this._recentActionTypes ],
|
|
582
848
|
mode: 'master',
|
|
849
|
+
activeConversations: this._activeConversations(),
|
|
583
850
|
outputFormat: PromptFactory.buildIdeationFormatInstruction(),
|
|
584
851
|
} )
|
|
585
852
|
// Propose temperature scales with the creativity trait (TODO #4): a creative Will
|
|
@@ -593,7 +860,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
593
860
|
ideationUserMessage,
|
|
594
861
|
tick: state.tick,
|
|
595
862
|
proposeTemperature,
|
|
596
|
-
meta: {
|
|
863
|
+
meta: {
|
|
864
|
+
category: 'executive',
|
|
865
|
+
attribute: 'master',
|
|
866
|
+
process: 'ideation',
|
|
867
|
+
function: '-',
|
|
868
|
+
demand: processSelection.effortScore
|
|
869
|
+
},
|
|
597
870
|
} )
|
|
598
871
|
logger.info(
|
|
599
872
|
`[executive] ◆ deliberate propose tick=${state.tick} ` +
|
|
@@ -613,11 +886,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
613
886
|
state,
|
|
614
887
|
qualityModulation,
|
|
615
888
|
epistemicUncertainty,
|
|
616
|
-
pendingMessages: [ ...this._messageQueue.pendingMessages ],
|
|
617
889
|
focus,
|
|
618
890
|
deps: promptDeps,
|
|
619
891
|
recentActionTypes: [ ...this._recentActionTypes ],
|
|
620
892
|
mode: 'master',
|
|
893
|
+
activeConversations: this._activeConversations(),
|
|
621
894
|
ideationCandidates
|
|
622
895
|
} )
|
|
623
896
|
|
|
@@ -631,7 +904,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
631
904
|
// D2: context counts for per-tick cognitive state snapshot
|
|
632
905
|
workingMemoryItems: execContext.workingMemory.length,
|
|
633
906
|
goalCount: execContext.goals.length,
|
|
634
|
-
pendingMessages: this._messageQueue.pendingMessages.length,
|
|
635
907
|
beliefCount: execContext.beliefs.length,
|
|
636
908
|
beliefsOmitted: execContext.beliefsOmitted,
|
|
637
909
|
promptPath: this._llmDirector?.writeDebugPrompt( state.tick, systemPrompt, userMessage ) ?? ''
|
|
@@ -646,7 +918,10 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
646
918
|
|
|
647
919
|
try {
|
|
648
920
|
// Use streaming call when clients are connected (F3); fall back to regular call.
|
|
649
|
-
|
|
921
|
+
// MODEL_ROUTING W0 — the effort gate already weighed this tick's demand
|
|
922
|
+
// (uncertainty, prior confidence, novelty, a pending reply, stress load);
|
|
923
|
+
// forward it rather than inventing a second measure of the same thing.
|
|
924
|
+
const masterMeta: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-', demand: processSelection.effortScore }
|
|
650
925
|
const result = this._chunkBroadcaster
|
|
651
926
|
? await this._llmDirector.callStream( systemPrompt, userMessage, state.tick, this._chunkBroadcaster, undefined, masterMeta )
|
|
652
927
|
: await this._llmDirector.call( systemPrompt, userMessage, state.tick, undefined, masterMeta )
|
|
@@ -733,6 +1008,23 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
733
1008
|
hasNarrative: !!executiveOutput.narrative
|
|
734
1009
|
} )
|
|
735
1010
|
|
|
1011
|
+
// ── DeliberationCache learning (slow path) ───────────────
|
|
1012
|
+
// extractFingerprint is pure over the frozen `state`, so recomputing here
|
|
1013
|
+
// yields the identical vector used on entry — no need to thread it through.
|
|
1014
|
+
if( this._cache ){
|
|
1015
|
+
const learnTick = footprint.tickObserved as unknown as number
|
|
1016
|
+
if( this._pendingVerify ){
|
|
1017
|
+
const match = this._actionTypesMatch(
|
|
1018
|
+
this._pendingVerify.cachedActionTypes,
|
|
1019
|
+
executiveOutput.actions.map( a => a.type ),
|
|
1020
|
+
)
|
|
1021
|
+
this._cache.updateCompetence( this._pendingVerify.fingerprint, match ? 1 : 0, learnTick )
|
|
1022
|
+
this._pendingVerify = null
|
|
1023
|
+
} else {
|
|
1024
|
+
this._cache.learn( extractFingerprint( state ), executiveOutput, learnTick )
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
736
1028
|
return executiveOutput
|
|
737
1029
|
}
|
|
738
1030
|
|
|
@@ -811,7 +1103,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
811
1103
|
)
|
|
812
1104
|
|
|
813
1105
|
// ── Flush pending escalation percepts ──────────────────────
|
|
814
|
-
// Convert buffered
|
|
1106
|
+
// Convert buffered executive.facet.handoff events into high-salience
|
|
815
1107
|
// percept entities so Exteroception surfaces them as
|
|
816
1108
|
// "## Percepts (What I Notice)" on the NEXT master cycle.
|
|
817
1109
|
// The master sees them as environmental signals — not as messages to reply to.
|
|
@@ -898,12 +1190,54 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
898
1190
|
} )
|
|
899
1191
|
|
|
900
1192
|
// Clear processed messages
|
|
901
|
-
this._messageQueue.clearProcessedMessages()
|
|
902
1193
|
|
|
903
|
-
// Merge escalation percepts into final commands
|
|
904
|
-
|
|
1194
|
+
// Merge escalation percepts into final commands, and retire the ones already
|
|
1195
|
+
// honoured. See _dischargedUndertakings for why this is not optional.
|
|
1196
|
+
const { keep, discharge } = this._reconcileUndertakings( escalationPercepts, this._lastStateRef! )
|
|
1197
|
+
if( keep.length ){
|
|
1198
|
+
commands.set ??= []
|
|
1199
|
+
commands.set.push( ...keep )
|
|
1200
|
+
}
|
|
1201
|
+
if( discharge.length ){
|
|
1202
|
+
commands.delete ??= []
|
|
1203
|
+
commands.delete.push( ...discharge )
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// ── Persist deliberation cache (mirrors the rolling summary) ──
|
|
1207
|
+
// Written each executive cycle so the learned patterns survive snapshot/
|
|
1208
|
+
// restore (R2). Bounded by maxPatterns; the stored output objects are only
|
|
1209
|
+
// ever read back, so state's deep-freeze of them is harmless.
|
|
1210
|
+
if( this._cache ){
|
|
905
1211
|
commands.set ??= []
|
|
906
|
-
commands.set.push(
|
|
1212
|
+
commands.set.push( {
|
|
1213
|
+
id: 'executive-deliberation-cache',
|
|
1214
|
+
type: 'executive.cache',
|
|
1215
|
+
metadata: { snapshot: this._cache.snapshot() },
|
|
1216
|
+
} )
|
|
1217
|
+
|
|
1218
|
+
// Surface cache state the R2-safe way: metrics ride the returned
|
|
1219
|
+
// StateCommands (never a frozen-state mutation), and hit/miss events
|
|
1220
|
+
// publish from this committed path so faculties can react to how
|
|
1221
|
+
// automatic the Will is becoming.
|
|
1222
|
+
const total = this._cache.hitCount + this._cache.missCount
|
|
1223
|
+
commands.metrics ??= []
|
|
1224
|
+
commands.metrics.push(
|
|
1225
|
+
[ 'cache.hit', this._lastCacheHit ? 1 : 0 ],
|
|
1226
|
+
[ 'cache.confidence', this._lastCacheConfidence ],
|
|
1227
|
+
[ 'cache.hit_rate', total > 0 ? this._cache.hitCount / total : 0 ],
|
|
1228
|
+
[ 'cache.size', this._cache.size ],
|
|
1229
|
+
)
|
|
1230
|
+
this._bus?.publish( this._lastCacheHit
|
|
1231
|
+
? {
|
|
1232
|
+
type: 'cache.hit', version: 1, sourceEngine: this.name,
|
|
1233
|
+
salience: this._lastCacheConfidence,
|
|
1234
|
+
payload: { confidence: this._lastCacheConfidence, neighborCount: this._lastCacheNeighborCount },
|
|
1235
|
+
}
|
|
1236
|
+
: {
|
|
1237
|
+
type: 'cache.miss', version: 1, sourceEngine: this.name,
|
|
1238
|
+
salience: 0.3,
|
|
1239
|
+
payload: { confidence: this._lastCacheConfidence },
|
|
1240
|
+
} )
|
|
907
1241
|
}
|
|
908
1242
|
|
|
909
1243
|
// Track entities modified
|
|
@@ -923,122 +1257,287 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
|
|
|
923
1257
|
|
|
924
1258
|
// ── Private helpers ────────────────────────────────────────
|
|
925
1259
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
1260
|
+
/**
|
|
1261
|
+
* The people the mind is in conversation with right now, newest first.
|
|
1262
|
+
*
|
|
1263
|
+
* Pruned against the supervisor's live facets on every read: a reaped facet is a
|
|
1264
|
+
* conversation that has ended, and a master that still believes it is mid-thread
|
|
1265
|
+
* with someone reasons about a table that is no longer there.
|
|
1266
|
+
*/
|
|
1267
|
+
private _activeConversations(): { entityId: string; name?: string; sinceTick: number }[] {
|
|
1268
|
+
const live = this._facetSupervisor.liveFacetIds()
|
|
1269
|
+
for( const id of [ ...this._facetSubjects.keys() ] )
|
|
1270
|
+
if( !live.has( id ) ) this._facetSubjects.delete( id )
|
|
1271
|
+
|
|
1272
|
+
return [ ...this._facetSubjects.values() ]
|
|
1273
|
+
.sort( ( a, b ) => b.tick - a.tick )
|
|
1274
|
+
.map( s => ({ entityId: s.entityId, ...( s.name ? { name: s.name } : {} ), sinceTick: s.tick }) )
|
|
1275
|
+
}
|
|
943
1276
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1277
|
+
/**
|
|
1278
|
+
* Facet sync — remember WHO each facet is with, and wake the master.
|
|
1279
|
+
*
|
|
1280
|
+
* Reached from `onCognitiveEvent`, NOT from its own `bus.subscribe`. The bus
|
|
1281
|
+
* stores one subscription per engineId (`_subscriptions.set( engineId, … )`),
|
|
1282
|
+
* so a second `subscribe(this.name, …)` silently REPLACES the first — and the
|
|
1283
|
+
* orchestrator registers `subscribe( engine.name, engine.subscribes(), … )`
|
|
1284
|
+
* after `attachBus`, which replaced everything registered here. Two dedicated
|
|
1285
|
+
* handlers used to be installed at this point; the second overwrote the first
|
|
1286
|
+
* and the orchestrator then overwrote that, so neither ever ran. The escalation
|
|
1287
|
+
* leg had been dead in production for its whole life: a facet could escalate,
|
|
1288
|
+
* the audition engine published, and nothing was listening.
|
|
1289
|
+
*/
|
|
1290
|
+
/**
|
|
1291
|
+
* Retire undertakings the mind has already honoured, and refuse to restate one
|
|
1292
|
+
* it is already carrying.
|
|
1293
|
+
*
|
|
1294
|
+
* An undertaking percept says, in the first person, "I said I would reach X and
|
|
1295
|
+
* nothing has gone to them yet". That sentence has to stop being true at some
|
|
1296
|
+
* point, and nothing made it stop. Measured on a live Will: SEVEN of them
|
|
1297
|
+
* accumulated in state, every one still asserting nothing had been sent, while
|
|
1298
|
+
* a `conversation.sent` to that person sat right beside them. She read seven
|
|
1299
|
+
* standing unfulfilled promises every cycle and dutifully sent the same message
|
|
1300
|
+
* again, five times in five minutes and once more in the next session — the
|
|
1301
|
+
* percept meant to stop her forgetting a promise was making her unable to
|
|
1302
|
+
* believe she had kept it.
|
|
1303
|
+
*
|
|
1304
|
+
* Discharged by EVIDENCE, not by a timer: a `conversation.sent` to that target,
|
|
1305
|
+
* written no earlier than the undertaking, means the contact happened. That
|
|
1306
|
+
* record is durable and snapshots with the state, so the discharge survives a
|
|
1307
|
+
* restart exactly as the promise does — which the tick-scoped satiation in
|
|
1308
|
+
* `enactionFootprint` deliberately cannot.
|
|
1309
|
+
*
|
|
1310
|
+
* It stays a decision, not an erasure. Retiring the percept removes the standing
|
|
1311
|
+
* claim that the words are unsent; whether to say more to that person is then an
|
|
1312
|
+
* ordinary competition like any other.
|
|
1313
|
+
*/
|
|
1314
|
+
private _reconcileUndertakings(
|
|
1315
|
+
incoming: EntityInput[],
|
|
1316
|
+
state: ReadonlySimulationState,
|
|
1317
|
+
): { keep: EntityInput[]; discharge: string[] } {
|
|
1318
|
+
/** Latest tick at which anything was sent to each target. */
|
|
1319
|
+
const contacted = new Map<string, number>()
|
|
1320
|
+
for( const [ , e ] of state.entities ){
|
|
1321
|
+
if( e.type !== 'conversation.sent') continue
|
|
1322
|
+
const m = e.metadata as Record<string, unknown> | undefined
|
|
1323
|
+
const target = typeof m?.['targetEntityId'] === 'string' ? m['targetEntityId'] as string : undefined
|
|
1324
|
+
if( !target ) continue
|
|
1325
|
+
// metadata.tick where set, else the entity's own sim-clock tick. Reading
|
|
1326
|
+
// metadata alone defaulted every record to 0, so `contacted >= madeAt` was
|
|
1327
|
+
// false for any promise made after tick 0 — this discharge never once fired.
|
|
1328
|
+
const at = typeof m?.['tick'] === 'number' ? m['tick'] as number
|
|
1329
|
+
: typeof ( e as { tick?: number } ).tick === 'number' ? ( e as { tick?: number } ).tick as number
|
|
1330
|
+
: 0
|
|
1331
|
+
contacted.set( target, Math.max( contacted.get( target ) ?? 0, at ) )
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** An undertaking is spent once we have reached its target since making it. */
|
|
1335
|
+
const honoured = ( target: string | undefined, madeAt: number ): boolean =>
|
|
1336
|
+
!!target && ( contacted.get( target ) ?? -1 ) >= madeAt
|
|
1337
|
+
|
|
1338
|
+
const undertakingOf = ( m: Record<string, unknown> | undefined ) =>
|
|
1339
|
+
m?.['category'] === 'undertaking'
|
|
1340
|
+
? {
|
|
1341
|
+
target: typeof m['undertakingTarget'] === 'string' ? m['undertakingTarget'] as string : undefined,
|
|
1342
|
+
madeAt: typeof m['tick'] === 'number' ? m['tick'] as number : 0,
|
|
954
1343
|
}
|
|
1344
|
+
: undefined
|
|
1345
|
+
|
|
1346
|
+
const discharge: string[] = []
|
|
1347
|
+
/** Targets we are already carrying a live, unhonoured undertaking toward. */
|
|
1348
|
+
const carrying = new Set<string>()
|
|
1349
|
+
|
|
1350
|
+
for( const [ id, e ] of state.entities ){
|
|
1351
|
+
if( e.type !== 'percept') continue
|
|
1352
|
+
const u = undertakingOf( e.metadata as Record<string, unknown> | undefined )
|
|
1353
|
+
if( !u ) continue
|
|
1354
|
+
|
|
1355
|
+
// No target ⇒ it predates the field this reconciliation keys on, so it can
|
|
1356
|
+
// never be matched against a contact and would assert "nothing has gone to
|
|
1357
|
+
// them yet" for the rest of the mind's life. Seven of these were found in a
|
|
1358
|
+
// live snapshot, each one re-read every cycle. Undischargeable is strictly
|
|
1359
|
+
// worse than gone.
|
|
1360
|
+
if( !u.target ){ discharge.push( id ); continue }
|
|
1361
|
+
|
|
1362
|
+
if( honoured( u.target, u.madeAt ) ) discharge.push( id )
|
|
1363
|
+
else carrying.add( u.target )
|
|
1364
|
+
}
|
|
955
1365
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1366
|
+
// Drop an incoming restatement of something already honoured or already held.
|
|
1367
|
+
// Duplicates were how seven of these piled up: each master cycle drained a
|
|
1368
|
+
// fresh copy of a promise that could never be marked kept.
|
|
1369
|
+
const keep = incoming.filter( p => {
|
|
1370
|
+
const u = undertakingOf( p.metadata as Record<string, unknown> | undefined )
|
|
1371
|
+
if( !u?.target ) return true
|
|
1372
|
+
if( honoured( u.target, u.madeAt ) ) return false
|
|
1373
|
+
if( carrying.has( u.target ) ) return false
|
|
1374
|
+
carrying.add( u.target )
|
|
1375
|
+
return true
|
|
1376
|
+
} )
|
|
960
1377
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1378
|
+
if( discharge.length )
|
|
1379
|
+
logger.info(`[executive] ${ discharge.length } undertaking(s) discharged — the contact was made`)
|
|
1380
|
+
|
|
1381
|
+
return { keep, discharge }
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
private _onFacetSync( event: CognitiveEvent ): void {
|
|
1385
|
+
const payload = event.payload as {
|
|
1386
|
+
facetId?: string
|
|
1387
|
+
reasoning?: string
|
|
1388
|
+
confidence?: number
|
|
1389
|
+
tick?: number
|
|
1390
|
+
subjectEntityId?: string
|
|
1391
|
+
subjectName?: string
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Remember WHO this facet is with, not just that it reported.
|
|
1395
|
+
if( payload.facetId && payload.subjectEntityId )
|
|
1396
|
+
this._facetSubjects.set( payload.facetId, {
|
|
1397
|
+
entityId: payload.subjectEntityId,
|
|
1398
|
+
...( payload.subjectName ? { name: payload.subjectName } : {} ),
|
|
1399
|
+
tick: payload.tick ?? this._lastExecutiveTick ?? 0,
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
// Unconditional (not gated on WORKSPACE_THRESHOLD like ordinary traffic):
|
|
1403
|
+
// a facet reporting back is the master's own attention returning, and the
|
|
1404
|
+
// point of the spike is that it re-evaluates rather than waiting out its
|
|
1405
|
+
// interval.
|
|
1406
|
+
this._gatingState.salienceBuffer.push({
|
|
1407
|
+
event,
|
|
1408
|
+
tick: payload.tick ?? event.logicalTime ?? 0,
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
logger.info(
|
|
1412
|
+
`[executive] master received facet sync from ${payload.facetId}` +
|
|
1413
|
+
( payload.subjectName || payload.subjectEntityId
|
|
1414
|
+
? ` (with ${payload.subjectName ?? payload.subjectEntityId})` : '') +
|
|
1415
|
+
` (confidence=${payload.confidence?.toFixed( 2 )})`
|
|
966
1416
|
)
|
|
1417
|
+
}
|
|
967
1418
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1419
|
+
/**
|
|
1420
|
+
* A focused part of me surfaced something the singular seat owns — work to plan
|
|
1421
|
+
* (`escalation`) or an intention toward a third party (`undertaking`).
|
|
1422
|
+
*
|
|
1423
|
+
* ONE handler for every facet type. This was `_onAuditionTaskSignal`, listening
|
|
1424
|
+
* on a topic named for one sense engine and typed with one sense engine's nouns
|
|
1425
|
+
* (`entityId`, `threadId`), which meant a planning, supervision or deliberation
|
|
1426
|
+
* facet had no way to hand anything up at all. See EscalationBuffer for the full
|
|
1427
|
+
* rationale; new kinds go in `HandoffBody`, not in a new topic and a new handler
|
|
1428
|
+
* beside this one.
|
|
1429
|
+
*
|
|
1430
|
+
* Master stays out of the reply path entirely:
|
|
1431
|
+
* • The facet has already said (or will say) whatever the person in front of
|
|
1432
|
+
* it needed to hear.
|
|
1433
|
+
* • The master's job is purely cognitive: create a [PLANS] block, update
|
|
1434
|
+
* goals, reflect, or decide whether it still means to make that contact.
|
|
1435
|
+
* Any follow-up communication flows through the agency competition —
|
|
1436
|
+
* NEVER via [REPLY].
|
|
1437
|
+
*
|
|
1438
|
+
* Buffered rather than written directly: state is read-only here, so
|
|
1439
|
+
* `EscalationBuffer.drainToPercepts()` emits it as a StateCommand on the next
|
|
1440
|
+
* master cycle, where Exteroception surfaces it under "## Percepts (What I Notice)".
|
|
1441
|
+
*/
|
|
1442
|
+
private _onFacetHandoff( event: CognitiveEvent ): void {
|
|
1443
|
+
const payload = event.payload as {
|
|
1444
|
+
facetId?: string
|
|
1445
|
+
subjectEntityId?: string
|
|
1446
|
+
subjectName?: string
|
|
1447
|
+
threadId?: string
|
|
1448
|
+
confidence?: number
|
|
1449
|
+
tick?: number
|
|
1450
|
+
body: HandoffBody
|
|
1451
|
+
}
|
|
1452
|
+
if( !payload?.body?.kind ) return
|
|
1453
|
+
|
|
1454
|
+
// NOW, not the last time the master happened to run.
|
|
981
1455
|
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
1456
|
+
// This was `this._lastExecutiveTick`, so an undertaking formed at tick 900 was
|
|
1457
|
+
// stamped 780 if that was the last master cycle. `_reconcileUndertakings`
|
|
1458
|
+
// discharges on "contacted at or after `madeAt`", which means a message sent at
|
|
1459
|
+
// tick 800 — BEFORE the promise existed — retired it. Over-discharge is the safe
|
|
1460
|
+
// direction, which is why it never surfaced as a symptom, but it made the promise
|
|
1461
|
+
// unfalsifiable: it could be marked kept by something that happened first.
|
|
1462
|
+
const tick = payload.tick ?? this._currentTick
|
|
1463
|
+
|
|
1464
|
+
this._escalations.push({
|
|
1465
|
+
tick,
|
|
1466
|
+
...( payload.facetId ? { facetId: payload.facetId } : {} ),
|
|
1467
|
+
...( payload.subjectEntityId ? { subjectEntityId: payload.subjectEntityId } : {} ),
|
|
1468
|
+
...( payload.subjectName ? { subjectName: payload.subjectName } : {} ),
|
|
1469
|
+
...( payload.threadId ? { threadId: payload.threadId } : {} ),
|
|
1470
|
+
body: payload.body.kind === 'undertaking'
|
|
1471
|
+
? { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) }
|
|
1472
|
+
: { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) },
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
// Spike the salience buffer so the master fires soon rather than waiting for
|
|
1476
|
+
// the next scheduled interval. The master must NOT be handed the inbound as a
|
|
1477
|
+
// message to answer — that would make it produce a [REPLY], duplicating the
|
|
1478
|
+
// facet's and breaking the facet/master boundary. (The queue that once carried
|
|
1479
|
+
// inbound this way was removed in #114: nothing ever filled it.)
|
|
1480
|
+
this._gatingState.salienceBuffer.push({ event, tick })
|
|
1481
|
+
|
|
1482
|
+
const from = payload.subjectName ?? payload.subjectEntityId ?? payload.facetId ?? 'a focus'
|
|
1483
|
+
logger.info(
|
|
1484
|
+
payload.body.kind === 'undertaking'
|
|
1485
|
+
? `[executive] master queued undertaking from ${from} ` +
|
|
1486
|
+
`→ reach ${payload.body.target} (confidence=${payload.confidence?.toFixed( 2 )})`
|
|
1487
|
+
: `[executive] master queued escalation percept from ${from} ` +
|
|
1488
|
+
`(confidence=${payload.confidence?.toFixed( 2 )})`
|
|
1489
|
+
)
|
|
1490
|
+
}
|
|
999
1491
|
|
|
1000
|
-
|
|
1001
|
-
// "## Percepts (What I Notice)" — master sees it as an
|
|
1002
|
-
// environmental signal prompting cognitive work, not a reply.
|
|
1003
|
-
if( this._lastStateRef ){
|
|
1004
|
-
// State is read-only here; we can't write directly.
|
|
1005
|
-
// Instead, buffer the escalation for injection on next tick.
|
|
1006
|
-
// The master's shouldAct() will fire due to the salience spike below.
|
|
1007
|
-
// The actual percept write happens in onReasoningComplete() via
|
|
1008
|
-
// a synthetic percept we store here and emit as a StateCommand.
|
|
1009
|
-
this._escalations.push({
|
|
1010
|
-
entityId: payload.entityId,
|
|
1011
|
-
threadId: payload.threadId,
|
|
1012
|
-
reasoning: payload.reasoning.slice( 0, 400 ),
|
|
1013
|
-
tick: this._lastExecutiveTick ?? 0,
|
|
1014
|
-
})
|
|
1015
|
-
}
|
|
1492
|
+
// ── DeliberationCache wiring ───────────────────────────────
|
|
1016
1493
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
type: 'audition.task.signal',
|
|
1022
|
-
version: 1,
|
|
1023
|
-
sequenceNumber: 1,
|
|
1024
|
-
sourceEngine: 'audition-engine',
|
|
1025
|
-
salience: event.salience ?? 0.9,
|
|
1026
|
-
payload,
|
|
1027
|
-
wallTime: wallClock(), // telemetry field; logicalTime carries the deterministic clock
|
|
1028
|
-
logicalTime: this._lastExecutiveTick ?? 0
|
|
1029
|
-
}
|
|
1494
|
+
/** Enable the deliberation cache (off by default). Call during mind assembly. */
|
|
1495
|
+
enableCache( config?: DeliberationCacheConfig ): void {
|
|
1496
|
+
this._cache = new DeliberationCache( config )
|
|
1497
|
+
}
|
|
1030
1498
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
tick: this._lastExecutiveTick ?? 0
|
|
1034
|
-
} )
|
|
1499
|
+
/** True when the cache is active. */
|
|
1500
|
+
get cacheEnabled(): boolean { return this._cache !== null }
|
|
1035
1501
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1502
|
+
/** Telemetry snapshot for harnesses / eval. Null when disabled. */
|
|
1503
|
+
cacheStats(): { size: number; hits: number; misses: number } | null {
|
|
1504
|
+
if( !this._cache ) return null
|
|
1505
|
+
return { size: this._cache.size, hits: this._cache.hitCount, misses: this._cache.missCount }
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Reafference hook — update cache competence from a confirmed action outcome.
|
|
1510
|
+
* Optional, layered on top of the inline verify loop. Reward follows the
|
|
1511
|
+
* research sketch: mean of (action succeeded, stress relief, goal progress).
|
|
1512
|
+
*/
|
|
1513
|
+
onActionOutcome(
|
|
1514
|
+
state: ReadonlySimulationState,
|
|
1515
|
+
tick: Tick,
|
|
1516
|
+
success: boolean,
|
|
1517
|
+
stressDelta: number,
|
|
1518
|
+
goalProgressDelta: number,
|
|
1519
|
+
): void {
|
|
1520
|
+
if( !this._cache ) return
|
|
1521
|
+
const reward = (
|
|
1522
|
+
( success ? 1 : 0 ) +
|
|
1523
|
+
( 1 - Math.max( 0, Math.min( 1, stressDelta ) ) ) +
|
|
1524
|
+
Math.max( 0, Math.min( 1, goalProgressDelta ) )
|
|
1525
|
+
) / 3
|
|
1526
|
+
this._cache.updateCompetence( extractFingerprint( state ), reward, tick )
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
private _actionTypesMatch( a: string[], b: string[] ): boolean {
|
|
1530
|
+
if( a.length !== b.length ) return false
|
|
1531
|
+
for( let i = 0; i < a.length; i++ ) if( a[ i ] !== b[ i ] ) return false
|
|
1532
|
+
return true
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
private _restoreDeliberationCache( state: ReadonlySimulationState ): void {
|
|
1536
|
+
if( !this._cache ) return
|
|
1537
|
+
const entity = state.entities.get('executive-deliberation-cache')
|
|
1538
|
+
if( !entity ) return
|
|
1539
|
+
const snap = entity.metadata?.[ 'snapshot' ] as DeliberationCacheSnapshot | undefined
|
|
1540
|
+
if( snap ) this._cache.restore( snap )
|
|
1042
1541
|
}
|
|
1043
1542
|
|
|
1044
1543
|
private _restoreSummarizer( state: ReadonlySimulationState ): void {
|