@mindot/will 0.1.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/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/tracts/outbox.controller.ts — per-Will messaging / outbox subsystem
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// OutboxController owns the outbound-message machinery extracted from
|
|
6
|
+
// WillStem (R5-c): draining / peeking / re-queuing the per-Will outbox,
|
|
7
|
+
// the message-delivery receipt (reafference percept), and TTL expiry of
|
|
8
|
+
// stale messages. WillStem delegates drainOutbox/peekOutbox/
|
|
9
|
+
// requeueToOutbox/confirmMessageDelivery here and calls expireStale()
|
|
10
|
+
// from its tick loop, so the god-object no longer owns this concern.
|
|
11
|
+
//
|
|
12
|
+
// The outbox ops touch only WillInstance fields (no Will id needed), so
|
|
13
|
+
// these methods take the resolved instance directly. WillStem still
|
|
14
|
+
// validates existence via _get(id) before delegating.
|
|
15
|
+
//
|
|
16
|
+
// OUTBOX_TTL_TICKS lives here (source of truth) and is re-exported from
|
|
17
|
+
// #stem/index for backward-compatible public-API consumers.
|
|
18
|
+
//
|
|
19
|
+
// Behaviour is preserved verbatim from the original WillStem methods;
|
|
20
|
+
// this is a pure extract-collaborator refactor.
|
|
21
|
+
// ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
import type { OutboxMessage } from '#types'
|
|
24
|
+
import type { WillInstance } from '#stem/index'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Number of ticks an undelivered outbox message survives before it is
|
|
28
|
+
* expired. Override via WILL_OUTBOX_TTL_TICKS. Default: 100 ticks.
|
|
29
|
+
*/
|
|
30
|
+
export const OUTBOX_TTL_TICKS = parseInt( process.env.WILL_OUTBOX_TTL_TICKS ?? '100')
|
|
31
|
+
|
|
32
|
+
export class OutboxController {
|
|
33
|
+
/**
|
|
34
|
+
* Drain all queued outbound messages from the Will's outbox.
|
|
35
|
+
* Called by the SSE/webhook delivery layer to retrieve messages.
|
|
36
|
+
* Stamps each message with the current tick if createdAtTick is 0.
|
|
37
|
+
*/
|
|
38
|
+
drain( instance: WillInstance ): OutboxMessage[] {
|
|
39
|
+
const messages = instance.outbox.splice( 0 )
|
|
40
|
+
|
|
41
|
+
for( const msg of messages )
|
|
42
|
+
if( msg.createdAtTick === 0 ) msg.createdAtTick = instance.tickCount
|
|
43
|
+
|
|
44
|
+
return messages
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Peek at outbox without draining it. */
|
|
48
|
+
peek( instance: WillInstance ): readonly OutboxMessage[] {
|
|
49
|
+
return instance.outbox
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Re-queue messages that were drained but not successfully delivered.
|
|
54
|
+
* Prepends them so they are the first to be drained on the next tick.
|
|
55
|
+
*/
|
|
56
|
+
requeue( instance: WillInstance, messages: OutboxMessage[] ): void {
|
|
57
|
+
if( messages.length === 0 ) return
|
|
58
|
+
instance.outbox.unshift( ...messages )
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called by the delivery layer (SSE consumer or webhook) after a message
|
|
63
|
+
* has been confirmed received by the target entity.
|
|
64
|
+
*
|
|
65
|
+
* Writes a message.delivery percept so the mind perceives its own words
|
|
66
|
+
* landing — the "ear hears the word you spoke" reafference signal.
|
|
67
|
+
*/
|
|
68
|
+
confirmDelivery( instance: WillInstance, messageId: string, delivered: boolean ): void {
|
|
69
|
+
const tick = instance.tickCount
|
|
70
|
+
|
|
71
|
+
// 1. Update the conversation.sent entity that tracks this outbox message.
|
|
72
|
+
// ProactiveCommunicator stores outboxMessageIds in the entity metadata (11.3)
|
|
73
|
+
// so we can correlate messageId → sent entity without extra bookkeeping.
|
|
74
|
+
// Use the O(1) type index rather than scanning every entity.
|
|
75
|
+
for( const entity of instance.simulation.stateManager.getEntitiesByType('conversation.sent') ){
|
|
76
|
+
const ids = entity.metadata?.outboxMessageIds as string[] | undefined
|
|
77
|
+
if( !ids?.includes( messageId ) ) continue
|
|
78
|
+
|
|
79
|
+
instance.simulation.stateManager.setEntity({
|
|
80
|
+
id: entity.id,
|
|
81
|
+
type: 'conversation.sent',
|
|
82
|
+
createdAt: entity.createdAt,
|
|
83
|
+
updatedAt: Date.now(),
|
|
84
|
+
metadata: {
|
|
85
|
+
...entity.metadata,
|
|
86
|
+
delivered,
|
|
87
|
+
deliveredAt: tick,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Write a percept so Exteroception perceives delivery ("ear hears the word")
|
|
95
|
+
instance.simulation.stateManager.setEntity({
|
|
96
|
+
id: `msg-delivered-${messageId}`,
|
|
97
|
+
type: 'percept',
|
|
98
|
+
createdAt: Date.now(),
|
|
99
|
+
updatedAt: Date.now(),
|
|
100
|
+
metadata: {
|
|
101
|
+
category: 'message-delivery',
|
|
102
|
+
summary: delivered
|
|
103
|
+
? `Your message was delivered successfully.`
|
|
104
|
+
: `Your message failed to reach the recipient.`,
|
|
105
|
+
salience: delivered ? 0.35 : 0.6,
|
|
106
|
+
changeType: delivered ? 'delivered' : 'failed',
|
|
107
|
+
messageId,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
instance.sessionLogger?.write({
|
|
112
|
+
type: 'conversation.delivery',
|
|
113
|
+
tick,
|
|
114
|
+
messageId,
|
|
115
|
+
delivered,
|
|
116
|
+
} as any)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Expire stale outbox messages. Called from the Will tick loop each tick.
|
|
121
|
+
*
|
|
122
|
+
* Messages created with createdAtTick=0 (OutboxWriter default) have not yet been
|
|
123
|
+
* stamped with a real tick — stamp them now so the TTL clock starts here rather
|
|
124
|
+
* than instantly expiring them (age = tickCount - 0 would be >> TTL).
|
|
125
|
+
* Mutate in-place (splice, not filter) so the shared outbox array (held by
|
|
126
|
+
* OutboxWriter) continues to reference the same object after TTL cleanup.
|
|
127
|
+
* Array.filter() would create a new array and silently detach the shared ref.
|
|
128
|
+
*/
|
|
129
|
+
expireStale( instance: WillInstance ): void {
|
|
130
|
+
const expiredMessages: OutboxMessage[] = []
|
|
131
|
+
|
|
132
|
+
for( let i = instance.outbox.length - 1; i >= 0; i-- ){
|
|
133
|
+
const msg = instance.outbox[ i ]!
|
|
134
|
+
if( msg.createdAtTick === 0 ) msg.createdAtTick = instance.tickCount
|
|
135
|
+
|
|
136
|
+
const age = instance.tickCount - msg.createdAtTick
|
|
137
|
+
if( age > OUTBOX_TTL_TICKS ){
|
|
138
|
+
expiredMessages.push( msg )
|
|
139
|
+
instance.outbox.splice( i, 1 )
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for( const msg of expiredMessages ){
|
|
144
|
+
instance.sessionLogger?.write({
|
|
145
|
+
type: 'outbox.expire',
|
|
146
|
+
tick: instance.tickCount,
|
|
147
|
+
messageId: msg.id,
|
|
148
|
+
targetEntityId: msg.targetEntityId,
|
|
149
|
+
effectorName: msg.effectorName,
|
|
150
|
+
ageAtExpiry: instance.tickCount - msg.createdAtTick,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
instance.simulation.eventBus.publish(
|
|
155
|
+
{
|
|
156
|
+
type: 'communication.outbound.undelivered',
|
|
157
|
+
source: 'will-manager',
|
|
158
|
+
payload: { messageId: msg.id, targetEntityId: msg.targetEntityId, effectorName: msg.effectorName },
|
|
159
|
+
},
|
|
160
|
+
instance.simulation.context,
|
|
161
|
+
instance.tickCount as any,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
catch { /* event bus may not be flushing — non-critical */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/tracts/outbox.writer.ts — the outbox PRODUCER primitive
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// OutboxWriter is the producer half of the outbox, paired with OutboxController
|
|
6
|
+
// (the consumer half: drain / expire / confirm). It owns the ONE canonical
|
|
7
|
+
// outbox-row shape (id format, default fields) and the `conversation.out`
|
|
8
|
+
// session-audit log for facet replies. Everything that wants to put words on the
|
|
9
|
+
// wire goes through here, so the row shape lives in exactly one place.
|
|
10
|
+
//
|
|
11
|
+
// It is a leaf primitive — constructed with the shared `outbox` array (no
|
|
12
|
+
// WillInstance) and injected into both producers:
|
|
13
|
+
// - ProactiveCommunicator → enqueue() (effector-dispatch path)
|
|
14
|
+
// - AuditionEngine → enqueueReply() (facet-reply fast-path)
|
|
15
|
+
//
|
|
16
|
+
// There is NO gating here. Emission is authorized upstream by AccessGrants
|
|
17
|
+
// (the `talk`/`text`/… grants), checked by the calling engine.
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
import { logger } from '#core/logger'
|
|
21
|
+
import type { OutboxMessage } from '#types'
|
|
22
|
+
import type { SessionLogger } from './session.logger'
|
|
23
|
+
|
|
24
|
+
/** The caller-supplied fields of an outbox row; the writer stamps id + defaults. */
|
|
25
|
+
export interface OutboxRow {
|
|
26
|
+
targetEntityId: string
|
|
27
|
+
content: string
|
|
28
|
+
effectorName: OutboxMessage[ 'effectorName' ]
|
|
29
|
+
targetEntityName?: string
|
|
30
|
+
gestureType?: string
|
|
31
|
+
replyToMessageId?: string
|
|
32
|
+
threadId?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class OutboxWriter {
|
|
36
|
+
private _outbox: OutboxMessage[]
|
|
37
|
+
private _willId: string
|
|
38
|
+
private _sessionLogger: SessionLogger | null = null
|
|
39
|
+
/**
|
|
40
|
+
* Per-Will monotonic id counter. Deterministic for replay: the writer is
|
|
41
|
+
* created once per Will and `enqueue` is called in the same order on a
|
|
42
|
+
* re-execution, so the ids reproduce exactly (vs the old Date.now()+random,
|
|
43
|
+
* which made the embedded ids in `conversation.sent` diverge every run).
|
|
44
|
+
*/
|
|
45
|
+
private _seq = 0
|
|
46
|
+
|
|
47
|
+
constructor( opts: { outbox?: OutboxMessage[]; willId?: string } = {} ){
|
|
48
|
+
this._outbox = opts.outbox ?? []
|
|
49
|
+
this._willId = opts.willId ?? 'will'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
attachSessionLogger( logger: SessionLogger | null ): void {
|
|
53
|
+
this._sessionLogger = logger
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _genId( suffix = '' ): string {
|
|
57
|
+
return `outbox-${ this._willId }-${ ++this._seq }${ suffix }`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Push one canonical outbox row and return its generated id. The single point
|
|
62
|
+
* where the row shape is materialized — all producers funnel through here.
|
|
63
|
+
*/
|
|
64
|
+
enqueue( row: OutboxRow, idSuffix = '' ): string {
|
|
65
|
+
const id = this._genId( idSuffix )
|
|
66
|
+
this._outbox.push({
|
|
67
|
+
id,
|
|
68
|
+
targetEntityId: row.targetEntityId,
|
|
69
|
+
...( row.targetEntityName !== undefined ? { targetEntityName: row.targetEntityName } : {} ),
|
|
70
|
+
content: row.content,
|
|
71
|
+
effectorName: row.effectorName,
|
|
72
|
+
...( row.gestureType ? { gestureType: row.gestureType } : {} ),
|
|
73
|
+
...( row.replyToMessageId ? { replyToMessageId: row.replyToMessageId } : {} ),
|
|
74
|
+
...( row.threadId ? { threadId: row.threadId } : {} ),
|
|
75
|
+
deliveryStatus: 'pending',
|
|
76
|
+
createdAtTick: 0,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
})
|
|
79
|
+
return id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reply convenience for AuditionEngine (formerly ProactiveCommunicator.deliverReply).
|
|
84
|
+
*
|
|
85
|
+
* Pushes the facet's reply bubbles as `text` outbox rows and writes the
|
|
86
|
+
* `conversation.out` audit entry. Returns the generated message ids (always
|
|
87
|
+
* generated, for delivery correlation), or an empty array when there are no
|
|
88
|
+
* bubbles.
|
|
89
|
+
*
|
|
90
|
+
* `pushToOutbox` (default true) controls only whether a copy is queued: set
|
|
91
|
+
* false when an ExternalTransport already delivered the reply via the fast-path
|
|
92
|
+
* (avoids double delivery) — the ids are still generated and returned.
|
|
93
|
+
*/
|
|
94
|
+
enqueueReply( opts: {
|
|
95
|
+
entityId: string
|
|
96
|
+
entityName: string
|
|
97
|
+
bubbles: string[]
|
|
98
|
+
threadId?: string
|
|
99
|
+
tick?: number
|
|
100
|
+
pushToOutbox?: boolean
|
|
101
|
+
} ): string[] {
|
|
102
|
+
const { entityId, entityName, bubbles, threadId, tick = 0, pushToOutbox = true } = opts
|
|
103
|
+
|
|
104
|
+
if( bubbles.length === 0 ) return []
|
|
105
|
+
|
|
106
|
+
const ids: string[] = []
|
|
107
|
+
bubbles.forEach( ( bubble, i ) => {
|
|
108
|
+
if( !pushToOutbox ){ ids.push( this._genId( `-${ i }` ) ); return }
|
|
109
|
+
logger.info(`[outbox-writer] reply → ${ entityId } bubble[${ i }] "${ bubble.slice( 0, 80 ) }"`)
|
|
110
|
+
ids.push( this.enqueue({
|
|
111
|
+
targetEntityId: entityId,
|
|
112
|
+
targetEntityName: entityName,
|
|
113
|
+
content: bubble,
|
|
114
|
+
effectorName: 'text',
|
|
115
|
+
threadId,
|
|
116
|
+
}, `-${ i }` ) )
|
|
117
|
+
} )
|
|
118
|
+
|
|
119
|
+
this._sessionLogger?.write({
|
|
120
|
+
type: 'conversation.out',
|
|
121
|
+
tick,
|
|
122
|
+
targetEntityId: entityId,
|
|
123
|
+
targetEntityName: entityName,
|
|
124
|
+
messageCount: bubbles.length,
|
|
125
|
+
messages: bubbles.map( b => b.slice( 0, 300 ) ),
|
|
126
|
+
preview: bubbles[0]?.slice( 0, 100 ) ?? '',
|
|
127
|
+
threadId,
|
|
128
|
+
source: 'audition-facet',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return ids
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/tracts/pma.controller.ts — per-Will PMA (portable mind archive)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// PMAController owns the PMA subsystem extracted from WillStem (R5-b):
|
|
6
|
+
// the distiller (state → portable snapshot), the loader (snapshot →
|
|
7
|
+
// seeded Will), and the reconstruction-fidelity eval harness. WillStem
|
|
8
|
+
// delegates its loadPMA/distillPMA/runPMAEval methods here, resolving the
|
|
9
|
+
// WillInstance (and validating existence) before each call.
|
|
10
|
+
//
|
|
11
|
+
// Behaviour is preserved verbatim from the original WillStem methods;
|
|
12
|
+
// this is a pure extract-collaborator refactor.
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
import { PMADistiller, PMALoader, type PMASnapshot } from '../../pma'
|
|
16
|
+
import { PMAEvalHarness, type ReconstructionFidelityReport } from '../../pma/eval'
|
|
17
|
+
import { validateWillIdentity } from '../guards/identity.guard'
|
|
18
|
+
import { logger } from '#core/logger'
|
|
19
|
+
import type { WillInstance } from '#stem/index'
|
|
20
|
+
|
|
21
|
+
export class PMAController {
|
|
22
|
+
private readonly _distiller = new PMADistiller()
|
|
23
|
+
private readonly _loader = new PMALoader()
|
|
24
|
+
private readonly _evalHarness = new PMAEvalHarness()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load a PMASnapshot into a paused or freshly-created Will. The Will must
|
|
28
|
+
* not be active (status 'paused' or 'initializing'); seeding an active Will
|
|
29
|
+
* with existing beliefs may cause unintended merges.
|
|
30
|
+
*/
|
|
31
|
+
load( id: string, instance: WillInstance, pma: PMASnapshot ): void {
|
|
32
|
+
if( instance.status === 'active' )
|
|
33
|
+
throw new Error(`Cannot load PMA into an active Will (${id}). Pause first.`)
|
|
34
|
+
|
|
35
|
+
// Re-validate the artifact's persona at the load boundary (Phase 2): a stored
|
|
36
|
+
// or tampered PMA must not inject a collapsed / colliding / injected self on
|
|
37
|
+
// reload, the same way creation guards the operator-supplied identity.
|
|
38
|
+
const guard = validateWillIdentity({ identity: {
|
|
39
|
+
prompt: pma.identity.prompt,
|
|
40
|
+
values: pma.identity.values,
|
|
41
|
+
traits: pma.identity.traits,
|
|
42
|
+
style: pma.identity.style,
|
|
43
|
+
} })
|
|
44
|
+
if( !guard.ok )
|
|
45
|
+
throw new Error(`Invalid PMA identity for "${id}": ${ guard.errors.join('; ') }`)
|
|
46
|
+
for( const w of guard.warnings )
|
|
47
|
+
logger.warn(`[identity-guard] PMA ${id}: ${w}`)
|
|
48
|
+
|
|
49
|
+
const safePma: PMASnapshot = { ...pma, identity: { ...pma.identity, ...guard.sanitized.identity } }
|
|
50
|
+
|
|
51
|
+
this._loader.load(
|
|
52
|
+
safePma,
|
|
53
|
+
instance.simulation,
|
|
54
|
+
instance.cognition,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Distill the current state of a Will into a portable PMASnapshot. Reads
|
|
60
|
+
* simulation state and JSONL profile logs. Does NOT pause or modify the
|
|
61
|
+
* Will — safe to call while active.
|
|
62
|
+
*/
|
|
63
|
+
distill( id: string, instance: WillInstance ): PMASnapshot {
|
|
64
|
+
const
|
|
65
|
+
state = instance.simulation.stateManager.snapshot(),
|
|
66
|
+
sessionId = instance.sessionLogger?.sessionId ?? 'unknown',
|
|
67
|
+
dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
68
|
+
|
|
69
|
+
return this._distiller.distill(
|
|
70
|
+
id,
|
|
71
|
+
instance.config.name ?? id,
|
|
72
|
+
state,
|
|
73
|
+
sessionId,
|
|
74
|
+
dataDir,
|
|
75
|
+
instance.cognition.schemaRepertoire,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Distill the Will and score how faithfully the PMA reconstructs it.
|
|
81
|
+
* `vsOriginal` (with `behavioral`) captures the original's probe baseline
|
|
82
|
+
* before evaluating, for reconstruction-vs-original behavioral fidelity.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Not read-only when `behavioral` is set: the probe phase runs probes against
|
|
85
|
+
* the LIVE Will, perturbing its state. Don't call it on a production Will you
|
|
86
|
+
* need pristine — distil + eval a paused clone instead.
|
|
87
|
+
*/
|
|
88
|
+
async runEval(
|
|
89
|
+
id: string,
|
|
90
|
+
instance: WillInstance,
|
|
91
|
+
opts: { behavioral?: boolean; vsOriginal?: boolean } = {}
|
|
92
|
+
): Promise<ReconstructionFidelityReport> {
|
|
93
|
+
// Distil the CLEAN state first — capturing the baseline below probes (and
|
|
94
|
+
// therefore mutates) the live original, so order matters.
|
|
95
|
+
const pma = this.distill( id, instance )
|
|
96
|
+
|
|
97
|
+
let baselineDist: Record<string, Record<string, number>> | undefined
|
|
98
|
+
if( opts.behavioral && opts.vsOriginal )
|
|
99
|
+
baselineDist = await this._evalHarness.captureProbeBaseline(
|
|
100
|
+
instance.simulation,
|
|
101
|
+
instance.cognition,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return this._evalHarness.evaluate( pma, instance.config, {
|
|
105
|
+
runBehavioralProbes: opts.behavioral ?? false,
|
|
106
|
+
baselineDist,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/stem/tracts/replay.controller.ts — per-Will record/replay subsystem
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// ReplayController owns the record/replay machinery extracted from
|
|
6
|
+
// WillStem (R5-a): the ReplayManager, the set of active recorders, the
|
|
7
|
+
// completed-replay metadata cache, and the on-disk replay paths — all
|
|
8
|
+
// keyed by Will id. WillStem delegates its start/stop/getMeta/list/compare
|
|
9
|
+
// replay methods here, so the god-object no longer owns this concern.
|
|
10
|
+
//
|
|
11
|
+
// Behaviour is preserved verbatim from the original WillStem methods;
|
|
12
|
+
// this is a pure extract-collaborator refactor.
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
import { mkdirSync, existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import {
|
|
18
|
+
ReplayManager,
|
|
19
|
+
type DefaultReplayRecorder,
|
|
20
|
+
type ReplayMetadata,
|
|
21
|
+
type ReplayComparison,
|
|
22
|
+
} from '#core/replay'
|
|
23
|
+
import { setCompletionRecorder, clearCompletionRecorder } from '#core/completion.recorder'
|
|
24
|
+
import { setInboundRecorder, clearInboundRecorder } from '#core/inbound.recorder'
|
|
25
|
+
import type { WillInstance } from '#stem/index'
|
|
26
|
+
|
|
27
|
+
export class ReplayController {
|
|
28
|
+
private readonly _replayManager = new ReplayManager()
|
|
29
|
+
private readonly _activeRecorders = new Map<string, { runId: string; unsub: () => void; recorder: DefaultReplayRecorder }>()
|
|
30
|
+
private readonly _completedReplays = new Map<string, ReplayMetadata>()
|
|
31
|
+
private readonly _replayPaths = new Map<string, string>()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Begin recording the given Will's ticks (and LLM completions) into a new
|
|
35
|
+
* replay run. Returns the run id. The caller is responsible for resolving
|
|
36
|
+
* `instance` (and thereby validating the Will exists) before calling.
|
|
37
|
+
*/
|
|
38
|
+
start( id: string, instance: WillInstance ): string {
|
|
39
|
+
if( this._activeRecorders.has( id ) )
|
|
40
|
+
throw new Error(`Replay already active for Will: ${id}`)
|
|
41
|
+
|
|
42
|
+
const runId = `replay-${Date.now()}-${Math.random().toString(36).slice( 2, 8 )}`
|
|
43
|
+
|
|
44
|
+
// Pre-create the replay dir so flush() can persist segments incrementally
|
|
45
|
+
// (stop() saves the consolidated file into the same dir).
|
|
46
|
+
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
47
|
+
const dir = join( dataDir, 'wills', id, 'replays' )
|
|
48
|
+
mkdirSync( dir, { recursive: true } )
|
|
49
|
+
|
|
50
|
+
const recorder = this._replayManager.createRecorder( id, runId, undefined, {
|
|
51
|
+
flushBasePath: join( dir, runId ),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const unsub = ( instance.simulation.orchestrator as any ).onAfterTick(
|
|
55
|
+
( tick: number ) => recorder.recordTick( tick, [] )
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Route this Will's LLM completions + external inbound into the recorder
|
|
59
|
+
// while recording is active (both are non-deterministic inputs for replay).
|
|
60
|
+
setCompletionRecorder( id, recorder )
|
|
61
|
+
setInboundRecorder( id, recorder )
|
|
62
|
+
|
|
63
|
+
this._activeRecorders.set( id, { runId, unsub, recorder } )
|
|
64
|
+
return runId
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async stop( id: string ): Promise<ReplayMetadata> {
|
|
68
|
+
const active = this._activeRecorders.get( id )
|
|
69
|
+
if( !active ) throw new Error(`No active replay for Will: ${id}`)
|
|
70
|
+
|
|
71
|
+
active.unsub()
|
|
72
|
+
clearCompletionRecorder( id )
|
|
73
|
+
clearInboundRecorder( id )
|
|
74
|
+
this._activeRecorders.delete( id )
|
|
75
|
+
|
|
76
|
+
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
77
|
+
const dir = join( dataDir, 'wills', id, 'replays' )
|
|
78
|
+
mkdirSync( dir, { recursive: true } )
|
|
79
|
+
|
|
80
|
+
const path = join( dir, `${active.runId}.json` )
|
|
81
|
+
await active.recorder.save( path )
|
|
82
|
+
|
|
83
|
+
const meta = active.recorder.getMetadata()
|
|
84
|
+
const key = `${id}:${active.runId}`
|
|
85
|
+
this._completedReplays.set( key, meta )
|
|
86
|
+
this._replayPaths.set( key, path )
|
|
87
|
+
|
|
88
|
+
return meta
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getMeta( id: string, runId: string ): ReplayMetadata | null {
|
|
92
|
+
const active = this._activeRecorders.get( id )
|
|
93
|
+
if( active?.runId === runId ) return active.recorder.getMetadata()
|
|
94
|
+
|
|
95
|
+
const key = `${id}:${runId}`
|
|
96
|
+
const cached = this._completedReplays.get( key )
|
|
97
|
+
if( cached ) return cached
|
|
98
|
+
|
|
99
|
+
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
100
|
+
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json` )
|
|
101
|
+
if( !existsSync( path ) ) return null
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const meta = ( JSON.parse( readFileSync( path, 'utf8' ) ) as { metadata: ReplayMetadata } ).metadata
|
|
105
|
+
this._completedReplays.set( key, meta )
|
|
106
|
+
this._replayPaths.set( key, path )
|
|
107
|
+
return meta
|
|
108
|
+
} catch {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
list( id: string ): ReplayMetadata[] {
|
|
114
|
+
const results: ReplayMetadata[] = []
|
|
115
|
+
const prefix = `${id}:`
|
|
116
|
+
|
|
117
|
+
const active = this._activeRecorders.get( id )
|
|
118
|
+
if( active ) results.push( active.recorder.getMetadata() )
|
|
119
|
+
|
|
120
|
+
for( const [ key, meta ] of this._completedReplays )
|
|
121
|
+
if( key.startsWith( prefix ) ) results.push( meta )
|
|
122
|
+
|
|
123
|
+
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
124
|
+
const dir = join( dataDir, 'wills', id, 'replays' )
|
|
125
|
+
|
|
126
|
+
if( existsSync( dir ) ){
|
|
127
|
+
const inMem = new Set(
|
|
128
|
+
[ ...this._completedReplays.keys() ]
|
|
129
|
+
.filter( k => k.startsWith( prefix ) )
|
|
130
|
+
.map( k => k.slice( prefix.length ) )
|
|
131
|
+
)
|
|
132
|
+
if( active ) inMem.add( active.runId )
|
|
133
|
+
|
|
134
|
+
for( const file of readdirSync( dir ) ){
|
|
135
|
+
if( !file.endsWith('.json') ) continue
|
|
136
|
+
const runId = file.slice( 0, -5 )
|
|
137
|
+
if( inMem.has( runId ) ) continue
|
|
138
|
+
try {
|
|
139
|
+
const path = join( dir, file )
|
|
140
|
+
const meta = ( JSON.parse( readFileSync( path, 'utf8' ) ) as { metadata: ReplayMetadata } ).metadata
|
|
141
|
+
const key = `${id}:${runId}`
|
|
142
|
+
this._completedReplays.set( key, meta )
|
|
143
|
+
this._replayPaths.set( key, path )
|
|
144
|
+
results.push( meta )
|
|
145
|
+
} catch { /* skip malformed */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return results.sort( ( a, b ) => b.startTime - a.startTime )
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async compare( id: string, runId1: string, runId2: string ): Promise<ReplayComparison> {
|
|
153
|
+
const path1 = this._resolvePath( id, runId1 )
|
|
154
|
+
const path2 = this._resolvePath( id, runId2 )
|
|
155
|
+
if( !path1 ) throw Object.assign( new Error(`Replay not found: ${runId1}`), { code: 404 } )
|
|
156
|
+
if( !path2 ) throw Object.assign( new Error(`Replay not found: ${runId2}`), { code: 404 } )
|
|
157
|
+
return this._replayManager.compare( path1, path2 )
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a replay file path: prefer the in-memory cache, else fall back to the
|
|
162
|
+
* conventional on-disk location. Without the fallback, compare() would 404 for
|
|
163
|
+
* replays that exist on disk but weren't loaded via list()/getMeta() this
|
|
164
|
+
* process (e.g. after a server restart).
|
|
165
|
+
*/
|
|
166
|
+
private _resolvePath( id: string, runId: string ): string | undefined {
|
|
167
|
+
const cached = this._replayPaths.get( `${id}:${runId}` )
|
|
168
|
+
if( cached ) return cached
|
|
169
|
+
|
|
170
|
+
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
171
|
+
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json` )
|
|
172
|
+
if( !existsSync( path ) ) return undefined
|
|
173
|
+
|
|
174
|
+
this._replayPaths.set( `${id}:${runId}`, path )
|
|
175
|
+
return path
|
|
176
|
+
}
|
|
177
|
+
}
|