@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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/agency/conversation.aim.ts — did the words achieve what they were for?
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// A communicative act has an AIM that is not its EXECUTION. Sending succeeded;
|
|
6
|
+
// that is a fact about the channel. Whether anyone answered is a fact about the
|
|
7
|
+
// world, it arrives later, and until this module existed nothing in the mind
|
|
8
|
+
// represented it.
|
|
9
|
+
//
|
|
10
|
+
// The cost of not representing it, measured on a live Will: `agency.skill` for
|
|
11
|
+
// `reach-out` read `enactments 28, successes 28` — a 100% success rate, because
|
|
12
|
+
// the outbox accepted every one. Habit (0.78) and value (0.5) therefore rose with
|
|
13
|
+
// each repetition while the only opposing term, satiation, is bounded and decays.
|
|
14
|
+
// The mind asked one person the same question eleven times in two and a half
|
|
15
|
+
// minutes and, from the inside, every one of them was the first: it could see
|
|
16
|
+
// `✓ reach-out` in its recent outcomes and nothing at all about being ignored.
|
|
17
|
+
//
|
|
18
|
+
// So this is not a rule against repeating. It is the missing PERCEPT. The mind
|
|
19
|
+
// gets to know that it spoke and was not answered; what to do about that is its
|
|
20
|
+
// own business — a person who has been ignored twice may well try a third time,
|
|
21
|
+
// and should be able to, having noticed.
|
|
22
|
+
//
|
|
23
|
+
// Everything here is pure and tick-denominated: no wall clock, no RNG, no state
|
|
24
|
+
// writes. Ids and decisions stay identical between a recorded run and its replay
|
|
25
|
+
// (R2). The resolution is folded into state by ReafferenceEngine, which already
|
|
26
|
+
// owns the question "did the world confirm my act?".
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
import type { Tick } from '#core/types'
|
|
30
|
+
import { readAliases, canonicalOf } from '#cognition/social.identity'
|
|
31
|
+
|
|
32
|
+
export const SENT_TYPE = 'conversation.sent'
|
|
33
|
+
export const RECEIVED_TYPE = 'conversation.received'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How long the mind waits before it counts a silence as an answer in itself.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately much longer than the echo window (`CONSEQUENCE_TTL_TICKS`, 30), a
|
|
39
|
+
* motor await (`AWAIT_TIMEOUT`, 15), or satiation (`repeatWindowTicks`, 60).
|
|
40
|
+
* Those ask "did my act leave the building?" and "does saying it again feel right
|
|
41
|
+
* yet?"; this asks "did a person get back to me?", and people take their time. At
|
|
42
|
+
* the 1s tick a hosted Will actually runs on, 240 is four minutes — long enough
|
|
43
|
+
* that a silence is genuinely a silence and not somebody typing.
|
|
44
|
+
*
|
|
45
|
+
* Tuned through the persona prior rather than fixed, because how long a quiet
|
|
46
|
+
* takes to mean something is a trait — the same reason `facetIdleTtlTicks` and
|
|
47
|
+
* `repeatWindowTicks` are not constants.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_REPLY_WINDOW_TICKS = 240
|
|
50
|
+
|
|
51
|
+
/** Minimal shape this module needs off a state entity — keeps it testable without a StateManager. */
|
|
52
|
+
export interface EntityLike {
|
|
53
|
+
type: string
|
|
54
|
+
/**
|
|
55
|
+
* The real field. `StateManager.setEntity` stamps it from the sim clock on
|
|
56
|
+
* every write, and it is the ONLY tick a record written off-tick can have —
|
|
57
|
+
* an inbound turn arrives between ticks and its writer has no clock to quote.
|
|
58
|
+
*/
|
|
59
|
+
updatedAtTick?: number
|
|
60
|
+
/** Not on `SimulationEntity`; accepted so a test can state a tick directly. */
|
|
61
|
+
tick?: number
|
|
62
|
+
metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* When a record happened: what its writer declared, else when the state manager
|
|
67
|
+
* stamped it.
|
|
68
|
+
*
|
|
69
|
+
* `SimulationEntity` has no `tick` — it is `updatedAtTick`, and reading the
|
|
70
|
+
* former silently yields `undefined` on every real entity. That is not a
|
|
71
|
+
* hypothetical: this module shipped reading `e.tick`, so `lastHeard` was 0 for
|
|
72
|
+
* everybody, `0 > sentTick` was never true, and NO turn could ever be marked
|
|
73
|
+
* answered. Caught on a live run where she was mid-conversation and the engine
|
|
74
|
+
* announced "no answer from Fabrice" about a message he had already replied to.
|
|
75
|
+
*
|
|
76
|
+
* The same dead read sits in `spokenAtByEntity` (agency/consequence.ts) behind a
|
|
77
|
+
* comment explaining why the fallback matters. It never fired there either.
|
|
78
|
+
*/
|
|
79
|
+
export function tickOf( e: EntityLike, meta: Record<string, unknown> ): Tick {
|
|
80
|
+
return ( num( meta['tick'] ) ?? num( e.updatedAtTick ) ?? num( e.tick ) ?? 0 ) as Tick
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One thing the mind said to one person, and what became of it. */
|
|
84
|
+
export interface SpokenTurn {
|
|
85
|
+
entityId: string
|
|
86
|
+
targetEntityId: string
|
|
87
|
+
targetEntityName?: string
|
|
88
|
+
preview: string
|
|
89
|
+
tick: Tick
|
|
90
|
+
/** Set once the target has spoken after this turn. */
|
|
91
|
+
answeredAt?: Tick
|
|
92
|
+
/**
|
|
93
|
+
* WHAT they said back.
|
|
94
|
+
*
|
|
95
|
+
* Recorded on the turn rather than looked up later, because a
|
|
96
|
+
* `conversation.received` lives exactly one tick — SocialPerception sweeps it as
|
|
97
|
+
* a one-shot event — so the text is unavailable by the next render. Without it
|
|
98
|
+
* the mind is told an answer ARRIVED and never shown it, which is worse than
|
|
99
|
+
* silence: it invites the mind to act as though it has the answer. Live, that
|
|
100
|
+
* put a wrong meeting time in front of a third party — she asked "same time,
|
|
101
|
+
* 3pm?", was told `they answered`, never saw the correction to 2pm, and relayed
|
|
102
|
+
* 3pm as confirmed.
|
|
103
|
+
*/
|
|
104
|
+
answeredWith?: string
|
|
105
|
+
/** Set once the reply window closed on a silence — recorded so it is announced once, not every tick. */
|
|
106
|
+
unansweredAt?: Tick
|
|
107
|
+
/** An acknowledgement closes a turn rather than opening one; it awaits nothing. */
|
|
108
|
+
isAck: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function meta( e: EntityLike ): Record<string, unknown> {
|
|
112
|
+
const m = e.metadata
|
|
113
|
+
if( !m ) return {}
|
|
114
|
+
return m instanceof Map ? Object.fromEntries( m ) : m as Record<string, unknown>
|
|
115
|
+
}
|
|
116
|
+
function str( v: unknown ): string | undefined { return typeof v === 'string' ? v : undefined }
|
|
117
|
+
function num( v: unknown ): number | undefined {
|
|
118
|
+
return typeof v === 'number' && Number.isFinite( v ) ? v : undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Every `conversation.sent` in state, as turns.
|
|
123
|
+
*
|
|
124
|
+
* The tick comes from `metadata.tick` where the writer set one, else the entity's
|
|
125
|
+
* own tick — `StateManager.setEntity` stamps that from the sim clock on every
|
|
126
|
+
* write. The fallback is not decoration: the two writers (ProactiveCommunicator
|
|
127
|
+
* and AuditionEngine) disagree about which they fill, and defaulting to 0 made
|
|
128
|
+
* every record of the second kind look infinitely old. Same rule as
|
|
129
|
+
* `spokenAtByEntity`, deliberately.
|
|
130
|
+
*/
|
|
131
|
+
export function readSpokenTurns( entities: ReadonlyMap<string, EntityLike> ): SpokenTurn[] {
|
|
132
|
+
// Through the alias table, because the two writers name their target in
|
|
133
|
+
// different id spaces: a REPLY is addressed to the transport id the percept
|
|
134
|
+
// arrived with (`discord:1019…`), while a PROACTIVE message is addressed to the
|
|
135
|
+
// anchor the executive resolved (`ke:…`). Both are the same someone. Matched
|
|
136
|
+
// raw, an answer to one could never close the other — the same two-halves-in-
|
|
137
|
+
// two-id-spaces failure that made `answered` impossible the first time.
|
|
138
|
+
const aliases = readAliases( entities )
|
|
139
|
+
const out: SpokenTurn[] = []
|
|
140
|
+
for( const [ id, e ] of entities ){
|
|
141
|
+
if( e.type !== SENT_TYPE ) continue
|
|
142
|
+
const m = meta( e )
|
|
143
|
+
const target = str( m['targetEntityId'] )
|
|
144
|
+
if( !target ) continue
|
|
145
|
+
out.push({
|
|
146
|
+
entityId: id,
|
|
147
|
+
targetEntityId: canonicalOf( aliases, target ),
|
|
148
|
+
targetEntityName: str( m['targetEntityName'] ),
|
|
149
|
+
preview: str( m['preview'] ) ?? '',
|
|
150
|
+
tick: tickOf( e, m ),
|
|
151
|
+
answeredAt: num( m['answeredAt'] ) as Tick | undefined,
|
|
152
|
+
answeredWith: str( m['answeredWith'] ),
|
|
153
|
+
unansweredAt: num( m['unansweredAt'] ) as Tick | undefined,
|
|
154
|
+
isAck: m['isAck'] === true,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
// Stable, deterministic order: oldest first, ties broken by id so two turns on
|
|
158
|
+
// one tick never swap between a run and its replay.
|
|
159
|
+
return out.sort( ( a, b ) => a.tick - b.tick || ( a.entityId < b.entityId ? -1 : a.entityId > b.entityId ? 1 : 0 ) )
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* When each person last spoke TO the mind, from `conversation.received`.
|
|
164
|
+
*
|
|
165
|
+
* This is the evidence that an utterance was answered. It is deliberately the
|
|
166
|
+
* whole of the evidence: any turn from them after we spoke counts, without trying
|
|
167
|
+
* to decide whether it was *about* what we said. Matching on topic would be a
|
|
168
|
+
* guess dressed as a fact, and getting it wrong in the strict direction would
|
|
169
|
+
* teach the mind it is being ignored by someone who is talking to it.
|
|
170
|
+
*/
|
|
171
|
+
export function lastHeardByEntity( entities: ReadonlyMap<string, EntityLike> ): Map<string, Heard> {
|
|
172
|
+
const aliases = readAliases( entities )
|
|
173
|
+
const out = new Map<string, Heard>()
|
|
174
|
+
for( const [ , e ] of entities ){
|
|
175
|
+
if( e.type !== RECEIVED_TYPE ) continue
|
|
176
|
+
const m = meta( e )
|
|
177
|
+
const raw = str( m['sourceKeid'] )
|
|
178
|
+
if( !raw ) continue
|
|
179
|
+
const source = canonicalOf( aliases, raw )
|
|
180
|
+
const at = tickOf( e, m )
|
|
181
|
+
if( at > ( out.get( source )?.tick ?? -Infinity ) )
|
|
182
|
+
out.set( source, { tick: at, preview: str( m['preview'] ) ?? '' } )
|
|
183
|
+
}
|
|
184
|
+
return out
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** When someone last spoke to us, AND what they said. */
|
|
188
|
+
export interface Heard { tick: Tick; preview: string }
|
|
189
|
+
|
|
190
|
+
/** A turn still in the air: said, not acknowledged-only, and not yet answered. */
|
|
191
|
+
export function isOpen( t: SpokenTurn ): boolean {
|
|
192
|
+
return !t.isAck && t.answeredAt === undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface Resolution {
|
|
196
|
+
/** Turns the target has now answered — fold `answeredAt` + what they said into them. */
|
|
197
|
+
answered: Array<{ turn: SpokenTurn; at: Tick; with: string }>
|
|
198
|
+
/** Turns whose reply window closed in silence, newly, this tick. */
|
|
199
|
+
unanswered: SpokenTurn[]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Fold the world's answer (or its absence) onto the turns still in the air.
|
|
204
|
+
*
|
|
205
|
+
* Pure: reads frozen state, writes nothing, decides nothing about what the mind
|
|
206
|
+
* should do next. A turn resolves exactly once — `answeredAt`/`unansweredAt` are
|
|
207
|
+
* the latches, so the announcement fires on one tick rather than every tick for
|
|
208
|
+
* the rest of the session.
|
|
209
|
+
*
|
|
210
|
+
* A silence is only reported once the window has fully elapsed. Before that the
|
|
211
|
+
* turn is simply open, which is a third state and the honest one: not yet
|
|
212
|
+
* answered is not the same as ignored, and collapsing them would have the mind
|
|
213
|
+
* conclude it was being snubbed one tick after speaking.
|
|
214
|
+
*/
|
|
215
|
+
export function resolveReplyExpectations(
|
|
216
|
+
entities: ReadonlyMap<string, EntityLike>,
|
|
217
|
+
tick: Tick,
|
|
218
|
+
windowTicks: number = DEFAULT_REPLY_WINDOW_TICKS,
|
|
219
|
+
): Resolution {
|
|
220
|
+
const turns = readSpokenTurns( entities )
|
|
221
|
+
const lastHeard = lastHeardByEntity( entities )
|
|
222
|
+
|
|
223
|
+
const answered: Resolution['answered'] = []
|
|
224
|
+
const unanswered: SpokenTurn[] = []
|
|
225
|
+
|
|
226
|
+
for( const t of turns ){
|
|
227
|
+
if( t.isAck || t.answeredAt !== undefined ) continue
|
|
228
|
+
|
|
229
|
+
const heard = lastHeard.get( t.targetEntityId )
|
|
230
|
+
if( heard !== undefined && heard.tick > t.tick ){
|
|
231
|
+
answered.push({ turn: t, at: heard.tick, with: heard.preview })
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Already announced as a silence — stays open, but says nothing more.
|
|
236
|
+
if( t.unansweredAt !== undefined ) continue
|
|
237
|
+
// A turn stamped LATER than now came back with a restored snapshot whose tick
|
|
238
|
+
// counter has not caught up. Reading it as `tick - t.tick` negative would make
|
|
239
|
+
// it trivially "not yet due" forever, which is the benign direction, but a
|
|
240
|
+
// window <= 0 would then fire on everything at once. Guard both.
|
|
241
|
+
if( windowTicks > 0 && tick - t.tick >= windowTicks ) unanswered.push( t )
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { answered, unanswered }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The turns worth showing the mind: still in the air, newest first, capped.
|
|
249
|
+
*
|
|
250
|
+
* This is what makes repetition visible from the inside. `conversation.sent` has
|
|
251
|
+
* always been in state — 57 records on the Will above — and reached no prompt at
|
|
252
|
+
* all, so the mind's only view of having spoken was `✓ reach-out` under "Recent
|
|
253
|
+
* Action Outcomes": a tick mark, no words, no person, no silence.
|
|
254
|
+
*/
|
|
255
|
+
export function openTurns(
|
|
256
|
+
entities: ReadonlyMap<string, EntityLike>,
|
|
257
|
+
limit: number = 6,
|
|
258
|
+
): SpokenTurn[] {
|
|
259
|
+
return readSpokenTurns( entities ).filter( isOpen ).reverse().slice( 0, limit )
|
|
260
|
+
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
|
|
39
39
|
import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
|
|
40
40
|
import { liveConsequences, matchConsequenceText } from '#agency/consequence'
|
|
41
|
+
import { asFinality } from '#stem/policy/arbiter'
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Activation margin (winner − runner-up) below which the choice is "contested" —
|
|
@@ -370,6 +371,42 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
370
371
|
const winner = scored[0]
|
|
371
372
|
if( !winner ) return busy( eligible.length )
|
|
372
373
|
|
|
374
|
+
// Whenever more than one person is a candidate for contact, record how the
|
|
375
|
+
// competition between them actually resolved. A Will that names someone and
|
|
376
|
+
// never messages them looks identical from outside whether the intention was
|
|
377
|
+
// never formed or was formed and beaten every cycle; this is the only place
|
|
378
|
+
// that difference is visible, and it is one line per contested tick.
|
|
379
|
+
// Why a WILLED contact did not happen. Gated on the executive having actually
|
|
380
|
+
// willed one (source 'ideomotor') that is not the winner — so a mind whose
|
|
381
|
+
// decision is being enacted logs nothing, and a mind that decided to contact
|
|
382
|
+
// someone and then did not says exactly what beat it.
|
|
383
|
+
//
|
|
384
|
+
// The earlier version of this gated on `reachers.length > 1`, assuming the
|
|
385
|
+
// failure was one addressee out-competing another. It is not: each master cycle
|
|
386
|
+
// sweeps every executive intent it did not name that cycle, so there is usually
|
|
387
|
+
// exactly ONE reach-out candidate in the field and the contest log never fired.
|
|
388
|
+
// The competition that matters is the willed contact against the rest of the
|
|
389
|
+
// field, which is what this prints.
|
|
390
|
+
// EVERY willed contact that lost, not just the strongest. An earlier version
|
|
391
|
+
// used `.find()`, which reports only the highest-scoring one — so with two
|
|
392
|
+
// live intents a second loser went unlogged, and the log would have quietly
|
|
393
|
+
// understated how many people the mind decided to contact and then did not.
|
|
394
|
+
const lostReaches = scored.filter( s =>
|
|
395
|
+
s !== winner
|
|
396
|
+
&& s.affordance.schema === 'reach-out'
|
|
397
|
+
&& s.affordance.source === 'ideomotor'
|
|
398
|
+
&& s.affordance.targetEntityId )
|
|
399
|
+
|
|
400
|
+
for( const lost of lostReaches )
|
|
401
|
+
logger.info(
|
|
402
|
+
`[selector] willed reach-out → ${ lost.affordance.targetEntityId } NOT selected: ` +
|
|
403
|
+
`${ lost.activation.toFixed( 3 ) }` +
|
|
404
|
+
`${ lost.affordance.justEnacted ? ` (justEnacted ${ lost.affordance.justEnacted.toFixed( 2 ) })` : '' }` +
|
|
405
|
+
` < ${ winner.affordance.schema }` +
|
|
406
|
+
`${ winner.affordance.targetEntityId ? `→${ winner.affordance.targetEntityId }` : '' }` +
|
|
407
|
+
` ${ winner.activation.toFixed( 3 ) }`
|
|
408
|
+
)
|
|
409
|
+
|
|
373
410
|
// ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
|
|
374
411
|
// A strong/high-stakes challenger cuts the routine off AND takes the body
|
|
375
412
|
// the same tick. We cannot delete the parent here: the executor runs later
|
|
@@ -425,12 +462,37 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
425
462
|
const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
|
|
426
463
|
if( sameAction ) return busy( eligible.length ) // field still favours what we await
|
|
427
464
|
|
|
428
|
-
|
|
465
|
+
// Clamped at BOTH ends. The upper bound was always there; the lower one matters
|
|
466
|
+
// because a restored intent's age is negative (state snapshots, the tick counter
|
|
467
|
+
// restarts at 1), and an unclamped negative staleness flips the decay into
|
|
468
|
+
// amplification: `1 - (-39 × 0.5)` = 20.6×, turning a 0.47 incumbent into 9.74 and
|
|
469
|
+
// making it permanently unpreemptable. The executor now clears such intents, so
|
|
470
|
+
// this is the second line of defence rather than the fix — but an incumbent's
|
|
471
|
+
// hysteresis must never be able to exceed its own recorded activation, whatever
|
|
472
|
+
// arithmetic feeds it.
|
|
473
|
+
const staleness = Math.min( 1, Math.max( 0, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS ) )
|
|
429
474
|
const incumbentStrength = awaiting.activation * ( 1 - staleness * STALE_DECAY )
|
|
430
475
|
const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
|
|
431
476
|
|
|
432
|
-
if( winner.activation <= incumbentStrength + switchCost )
|
|
477
|
+
if( winner.activation <= incumbentStrength + switchCost ){
|
|
478
|
+
// A challenger aimed at a DIFFERENT person losing to an awaiting incumbent
|
|
479
|
+
// is the starvation shape: every proactive reach-out sits 'awaiting' while a
|
|
480
|
+
// facet authors its words (8–18s = many ticks), and the incumbent's recorded
|
|
481
|
+
// activation does not decay with the challenger's damping. If one addressee
|
|
482
|
+
// can hold the channel this way, a Will can decide to contact someone else
|
|
483
|
+
// every cycle and never once do it — which is exactly the report.
|
|
484
|
+
if( winner.affordance.schema === 'reach-out'
|
|
485
|
+
&& winner.affordance.targetEntityId
|
|
486
|
+
&& winner.affordance.targetEntityId !== awaiting.target )
|
|
487
|
+
logger.info(
|
|
488
|
+
`[selector] reach-out → ${ winner.affordance.targetEntityId } BLOCKED by awaiting ` +
|
|
489
|
+
`${ awaiting.schema } → ${ awaiting.target || '—' }: ` +
|
|
490
|
+
`challenger ${ winner.activation.toFixed( 3 ) } ≤ incumbent ${ incumbentStrength.toFixed( 3 ) } ` +
|
|
491
|
+
`+ switch ${ switchCost.toFixed( 3 ) } (awaiting ${ tick - awaiting.dispatchedAt } ticks)`
|
|
492
|
+
)
|
|
493
|
+
|
|
433
494
|
return busy( eligible.length ) // not worth interrupting — keep waiting
|
|
495
|
+
}
|
|
434
496
|
|
|
435
497
|
preemptDelete = awaiting.id // PREEMPT — fall through and commit the challenger
|
|
436
498
|
preemptedFrom = awaiting.schema
|
|
@@ -477,6 +539,11 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
477
539
|
targetEntityId: winner.affordance.targetEntityId,
|
|
478
540
|
parameters: winner.affordance.parameters,
|
|
479
541
|
source: winner.affordance.source,
|
|
542
|
+
// What evoked this — for an ideomotor winner, the `ideomotor.intent` entity
|
|
543
|
+
// the executive wrote. Carried so the executor can DISCHARGE it once the act
|
|
544
|
+
// happens: an intention that has been acted on is no longer an intention, and
|
|
545
|
+
// nothing was deleting these. See MotorSchemaExecutor._dischargeWill.
|
|
546
|
+
...( winner.affordance.evokedBy ? { evokedBy: winner.affordance.evokedBy } : {} ),
|
|
480
547
|
// Plan provenance (when a plan's frontier-step prior won the competition) —
|
|
481
548
|
// flows through the executor's action.outcome so the PlanningEngine advances.
|
|
482
549
|
...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
|
|
@@ -657,7 +724,7 @@ function refusedClassSchemas( state: ReadonlySimulationState ): Set<string> {
|
|
|
657
724
|
for( const e of state.entities.values() ){
|
|
658
725
|
if( e.type !== 'agency.outcome') continue
|
|
659
726
|
const m = e.metadata
|
|
660
|
-
if( m?.['refused'] !== true ||
|
|
727
|
+
if( m?.['refused'] !== true || asFinality( m?.['finality'] ) !== 'class') continue
|
|
661
728
|
const schema = str( m?.['schema'] )
|
|
662
729
|
if( schema ) out.add( schema )
|
|
663
730
|
}
|
|
@@ -676,6 +743,19 @@ function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
|
|
|
676
743
|
...DEFAULT_WEIGHTS,
|
|
677
744
|
risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
|
|
678
745
|
novelty: Math.max( 0, num( p['noveltyWeight'], DEFAULT_WEIGHTS.novelty ) ),
|
|
746
|
+
// How long the mind sits with something it has already said before saying it
|
|
747
|
+
// again. Agreeableness raises it (does not badger); demonstrated persistence
|
|
748
|
+
// lowers it (follows up sooner). Clamped at 0 so a prior can flatten the
|
|
749
|
+
// damping into indifference but never turn repeating into a *reward*.
|
|
750
|
+
repeat: Math.max( 0, num( p['repeatDamping'], DEFAULT_WEIGHTS.repeat ) ),
|
|
751
|
+
// How much the mind's learned read on a person biases acting toward them.
|
|
752
|
+
// Deliberately NOT clamped at 0 — this weight is genuinely signed territory,
|
|
753
|
+
// and it is the tenant's to occupy. A warm, reciprocal mind leans toward
|
|
754
|
+
// whoever answers; a dogged, duty-bound one chases a silence precisely
|
|
755
|
+
// because it is silent. Both are coherent people. The container supplies the
|
|
756
|
+
// term and the persona supplies the sign; hardcoding it here would be the
|
|
757
|
+
// container deciding what kind of colleague every tenant has to be.
|
|
758
|
+
social: num( p['socialWeight'], DEFAULT_WEIGHTS.social ),
|
|
679
759
|
}
|
|
680
760
|
}
|
|
681
761
|
|
|
@@ -728,6 +808,8 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
|
|
|
728
808
|
available: meta['available'] === true,
|
|
729
809
|
tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
|
|
730
810
|
planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
|
|
811
|
+
willBias: typeof meta['willBias'] === 'number' ? ( meta['willBias'] as number ) : undefined,
|
|
812
|
+
socialPrior: typeof meta['socialPrior'] === 'number' ? ( meta['socialPrior'] as number ) : undefined,
|
|
731
813
|
planId: str( meta['planId'] ),
|
|
732
814
|
stepId: str( meta['stepId'] ),
|
|
733
815
|
tick: num( meta['tick'], 0 ),
|
|
@@ -36,6 +36,8 @@ import type { Affordance, AffordanceSource, MotorSchema, LearnedSkill, SchemaPre
|
|
|
36
36
|
import type { SchemaRepertoire } from '#agency/schemas/repertoire'
|
|
37
37
|
import { INNATE_SCHEMAS } from '#agency/schemas/innate'
|
|
38
38
|
import { collectGoalTargets } from '#agency/selection.scoring'
|
|
39
|
+
import { readEffectiveParams } from '#cognition/persona.prior'
|
|
40
|
+
import { liveConsequences, enactionFootprint, spokenAtByEntity, CONSEQUENCE_TTL_TICKS, type ConsequenceDescriptor } from '#agency/consequence'
|
|
39
41
|
|
|
40
42
|
/** Default field width for non-innate affordances when no attention metric exists. */
|
|
41
43
|
const DEFAULT_ATTENTION_CAP = 5
|
|
@@ -59,6 +61,19 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
59
61
|
private _schemas: MotorSchema[]
|
|
60
62
|
private _skills: SkillAccessor | null = null
|
|
61
63
|
private _repertoire: SchemaRepertoire | null = null
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This tick's live consequence descriptors — the acts the mind has performed
|
|
67
|
+
* whose outcome has not yet come back. Refreshed once at the top of react()
|
|
68
|
+
* because `_build` runs per candidate and reading them is a full-entity scan.
|
|
69
|
+
*/
|
|
70
|
+
private _inFlight: readonly ConsequenceDescriptor[] = []
|
|
71
|
+
|
|
72
|
+
/** Ticks an act stays satiating (engine-config-action-selector.repeatWindowTicks). */
|
|
73
|
+
private _satiationWindow: number = CONSEQUENCE_TTL_TICKS
|
|
74
|
+
|
|
75
|
+
/** Tick of the last thing said to each entity — outlives the descriptor sweep. */
|
|
76
|
+
private _spokenAt: ReadonlyMap<string, number> = new Map()
|
|
62
77
|
private _bus: CognitiveBus | null = null
|
|
63
78
|
private _defaultCap: number
|
|
64
79
|
private _lastFieldSize = 0
|
|
@@ -112,6 +127,21 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
112
127
|
const valence = metric( state, 'affect.valence', 0 )
|
|
113
128
|
const energyLow = metric( state, 'energy.level', 0 ) < 30
|
|
114
129
|
|
|
130
|
+
// The mind's own acts still in flight (EXAFFERENCE P5). Read once per tick —
|
|
131
|
+
// `_build` runs for every candidate and this is a full-entity scan. Each
|
|
132
|
+
// candidate whose (schema, target) matches one of these carries a decaying
|
|
133
|
+
// `justEnacted`, so having just done a thing damps doing it again.
|
|
134
|
+
this._inFlight = liveConsequences( state.entities, tick )
|
|
135
|
+
// How long this mind sits with something it has already done before doing it
|
|
136
|
+
// again — the tenant's, not the container's. Falls back to the echo TTL only
|
|
137
|
+
// when unseeded, so a bare harness behaves as before.
|
|
138
|
+
this._satiationWindow = readEffectiveParams( state, 'engine-config-action-selector').repeatWindowTicks
|
|
139
|
+
?? CONSEQUENCE_TTL_TICKS
|
|
140
|
+
// Durable "when did I last speak to them". Descriptors alone cannot carry
|
|
141
|
+
// satiation — the executor deletes them at their echo TTL, so any window
|
|
142
|
+
// longer than that was a no-op.
|
|
143
|
+
this._spokenAt = spokenAtByEntity( state.entities )
|
|
144
|
+
|
|
115
145
|
const set: EntityInput[] = []
|
|
116
146
|
const del: string[] = []
|
|
117
147
|
|
|
@@ -204,13 +234,18 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
204
234
|
|
|
205
235
|
if( !schema ) continue
|
|
206
236
|
|
|
237
|
+
// `priority` is the confidence the executive decided with. It sets BOTH the
|
|
238
|
+
// evoke-salience (field admission) and — via willBias — the activation the
|
|
239
|
+
// selector scores, so a decision made at 0.85 pushes harder than one at 0.5.
|
|
240
|
+
const willBias = clamp01( num( m?.['priority'], 0.8 ) )
|
|
207
241
|
candidates.push({
|
|
208
|
-
salience: IDEOMOTOR_BASE_SALIENCE +
|
|
242
|
+
salience: IDEOMOTOR_BASE_SALIENCE + willBias,
|
|
209
243
|
affordance: this._build( schema, tick, state, valence, energyLow, skills, {
|
|
210
244
|
evokedBy: id,
|
|
211
245
|
targetEntityId: str( m?.['targetEntityId'] ),
|
|
212
246
|
parameters: ( m?.['parameters'] as Record<string, unknown> ) ?? {},
|
|
213
247
|
source: 'ideomotor',
|
|
248
|
+
willBias,
|
|
214
249
|
} ),
|
|
215
250
|
})
|
|
216
251
|
}
|
|
@@ -303,6 +338,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
303
338
|
parameters?: Record<string, unknown>
|
|
304
339
|
source?: AffordanceSource
|
|
305
340
|
planBias?: number
|
|
341
|
+
willBias?: number
|
|
306
342
|
planId?: string
|
|
307
343
|
stepId?: string
|
|
308
344
|
},
|
|
@@ -313,6 +349,13 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
313
349
|
// Will's affordance field is byte-identical. Present only once a refusal dented it.
|
|
314
350
|
const availability = this._repertoire?.availabilityOf( schema.id ) ?? 1
|
|
315
351
|
|
|
352
|
+
// What the mind has LEARNED about the person this act is aimed at. Only for a
|
|
353
|
+
// targeted act; 0 for everything else, so the field is unchanged for a mind that
|
|
354
|
+
// knows no one. See Affordance.socialPrior.
|
|
355
|
+
const socialPrior = ctx.targetEntityId
|
|
356
|
+
? socialStanding( state, ctx.targetEntityId )
|
|
357
|
+
: 0
|
|
358
|
+
|
|
316
359
|
// Learned value if known, else the schema's intrinsic prior mapped to 0..1.
|
|
317
360
|
const expectedReward = skill?.valueEstimate ?? clamp01( ( ( schema.baseValence ?? 0 ) + 1 ) / 2 )
|
|
318
361
|
const expectedValence = schema.baseValence ?? valence
|
|
@@ -320,6 +363,16 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
320
363
|
// Low energy makes everything feel costlier.
|
|
321
364
|
const cost = clamp01( schema.cost * ( energyLow ? 1.5 : 1 ) )
|
|
322
365
|
|
|
366
|
+
// This act's own footprint, if it is still in flight toward this same person.
|
|
367
|
+
// Satiation only applies to acts aimed at someone the mind SPEAKS to — the
|
|
368
|
+
// `conversation.sent` half is keyed by person, so it must not damp, say,
|
|
369
|
+
// inspecting them.
|
|
370
|
+
const speaks = schema.tags?.includes('communication') ?? false
|
|
371
|
+
const justEnacted = enactionFootprint(
|
|
372
|
+
this._inFlight, schema.id, ctx.targetEntityId, tick, this._satiationWindow,
|
|
373
|
+
speaks ? this._spokenAt : undefined,
|
|
374
|
+
)
|
|
375
|
+
|
|
323
376
|
const key = ctx.targetEntityId ?? ctx.evokedBy ?? schema.id
|
|
324
377
|
const source = ctx.source ?? schema.source
|
|
325
378
|
// A non-default-source candidate (ideomotor) gets a distinct id so it can coexist
|
|
@@ -341,7 +394,10 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
341
394
|
tags: schema.tags ?? [],
|
|
342
395
|
...( schema.description ? { description: schema.description } : {} ),
|
|
343
396
|
...( availability < 1 ? { availability } : {} ),
|
|
397
|
+
...( socialPrior !== 0 ? { socialPrior } : {} ),
|
|
398
|
+
...( justEnacted > 0 ? { justEnacted } : {} ),
|
|
344
399
|
planBias: ctx.planBias,
|
|
400
|
+
willBias: ctx.willBias,
|
|
345
401
|
planId: ctx.planId,
|
|
346
402
|
stepId: ctx.stepId,
|
|
347
403
|
tick,
|
|
@@ -366,7 +422,9 @@ export class AffordanceSynthesizer implements CognitiveEngine {
|
|
|
366
422
|
tags: a.tags,
|
|
367
423
|
description: a.description,
|
|
368
424
|
...( a.availability !== undefined ? { availability: a.availability } : {} ),
|
|
425
|
+
...( a.socialPrior !== undefined ? { socialPrior: a.socialPrior } : {} ),
|
|
369
426
|
planBias: a.planBias,
|
|
427
|
+
willBias: a.willBias,
|
|
370
428
|
planId: a.planId,
|
|
371
429
|
stepId: a.stepId,
|
|
372
430
|
tick: a.tick,
|
|
@@ -415,3 +473,34 @@ function str( v: unknown ): string | undefined {
|
|
|
415
473
|
function clamp01( n: number ): number {
|
|
416
474
|
return n < 0 ? 0 : n > 1 ? 1 : n
|
|
417
475
|
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The mind's learned read on one person, −1..1 (0 = unknown or neutral).
|
|
479
|
+
*
|
|
480
|
+
* Every input is something the mind formed from experience, none is a constant:
|
|
481
|
+
* • ReputationTracker's `trustworthiness`, centred on 0.5 and scaled by that
|
|
482
|
+
* model's own `confidence`, so an opinion held on two interactions pushes
|
|
483
|
+
* far less than one held on fifty.
|
|
484
|
+
* • the mind's current affective tone, as a gentle tilt — feeling low makes
|
|
485
|
+
* reaching for anyone a little less attractive, which is a mood, not a verdict
|
|
486
|
+
* on them, so it is weighted small and applies the same way to everybody.
|
|
487
|
+
*
|
|
488
|
+
* This is the path by which "they never answer me" reaches the competition: the
|
|
489
|
+
* ReputationTracker learns it from `interaction.occurred`, which only started firing
|
|
490
|
+
* once inbound conversation reached social cognition (#113).
|
|
491
|
+
*/
|
|
492
|
+
function socialStanding( state: ReadonlySimulationState, keid: string ): number {
|
|
493
|
+
let trust = 0
|
|
494
|
+
for( const e of state.entities.values() ){
|
|
495
|
+
if( e.type !== 'reputation') continue
|
|
496
|
+
const m = e.metadata as Record<string, unknown> | undefined
|
|
497
|
+
if( m?.['keid'] !== keid ) continue
|
|
498
|
+
const t = num( m['trustworthiness'], 0.5 )
|
|
499
|
+
const c = clamp01( num( m['confidence'], 0 ) )
|
|
500
|
+
trust = ( t - 0.5 ) * 2 * c
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const mood = num( state.metrics.get('affect.valence'), 0 ) * 0.25
|
|
505
|
+
return Math.max( -1, Math.min( 1, trust + mood ) )
|
|
506
|
+
}
|