@mindot/will 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/dist/index.d.ts +188 -141
- package/dist/index.js +16660 -15206
- package/dist/index.js.map +1 -1
- package/dist/{channels → surface/channels}/discord.d.ts +50 -3
- package/dist/{channels → surface/channels}/discord.js +102 -7
- package/dist/surface/channels/discord.js.map +1 -0
- package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
- package/dist/{channels → surface/channels}/whatsapp.js +7 -4
- package/dist/surface/channels/whatsapp.js.map +1 -0
- package/dist/{cli.js → surface/cli.js} +4406 -2904
- package/dist/surface/cli.js.map +1 -0
- package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
- package/dist/{mcp → surface/mcp}/effectors.js +10 -4
- package/dist/surface/mcp/effectors.js.map +1 -0
- package/dist/{will-DbDj_TEH.d.ts → will-evj9_vrd.d.ts} +5699 -4854
- package/package.json +11 -12
- package/src/cognition/agency/consequence.ts +237 -3
- package/src/cognition/agency/conversation.aim.ts +32 -0
- package/src/cognition/agency/engines/action.selector.ts +59 -3
- package/src/cognition/agency/engines/affordance.synthesizer.ts +114 -10
- package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
- package/src/cognition/agency/engines/motor.schema.executor.ts +362 -27
- package/src/cognition/agency/engines/reafference.engine.ts +43 -4
- package/src/cognition/agency/execution.primitives.ts +17 -4
- package/src/cognition/agency/reconcile.learning.ts +5 -4
- package/src/cognition/agency/schemas/innate.ts +96 -1
- package/src/cognition/agency/schemas/repertoire.ts +19 -1
- package/src/cognition/agency/selection.scoring.ts +16 -0
- package/src/cognition/agency/settlement.ts +203 -0
- package/src/cognition/agency/types.ts +38 -1
- package/src/cognition/config.mirror.entities.ts +0 -1
- package/src/cognition/event.schemas.ts +0 -8
- package/src/cognition/faculties/affective.blender.ts +21 -4
- package/src/cognition/faculties/circadian.oscillator.ts +36 -7
- package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
- package/src/cognition/faculties/executive.engine/context.ts +41 -35
- package/src/cognition/faculties/executive.engine/engine.ts +270 -113
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +126 -59
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +10 -0
- package/src/cognition/faculties/executive.engine/parser.ts +100 -38
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +168 -23
- package/src/cognition/faculties/executive.engine/types.ts +18 -15
- package/src/cognition/faculties/exteroception.ts +99 -98
- package/src/cognition/faculties/goal.manager.ts +50 -2
- package/src/cognition/faculties/known.entity.tracker.ts +22 -3
- package/src/cognition/faculties/planning.engine/engine.ts +4 -4
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
- package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
- package/src/cognition/faculties/working.memory.ts +6 -1
- package/src/cognition/instruction.handler.ts +1 -1
- package/src/cognition/memory/vector.embedder.ts +1 -3
- package/src/cognition/percept.entity.ts +122 -0
- package/src/cognition/sense.boundary.ts +176 -0
- package/src/cognition/senses/audition.engine/engine.ts +97 -16
- package/src/cognition/senses/base.sense.engine.ts +104 -6
- package/src/cognition/senses/index.ts +71 -13
- package/src/cognition/senses/provenance.ts +128 -0
- package/src/cognition/senses/somatosensation.engine.ts +117 -9
- package/src/cognition/social.identity.ts +23 -0
- package/src/cognition/utilities/token.tracker.ts +36 -5
- package/src/core/orchestrator.ts +13 -0
- package/src/index.ts +39 -5
- package/src/llm/gate.ts +48 -0
- package/src/llm/index.ts +102 -25
- package/src/llm/wire.contracts.ts +38 -0
- package/src/pma/index.ts +1 -1
- package/src/stem/index.ts +92 -21
- package/src/stem/mind.ts +26 -2
- package/src/stem/policy/arbiter.ts +10 -0
- package/src/stem/profiles/built-in.ts +7 -0
- package/src/{profiles → stem/profiles}/companion.ts +1 -1
- package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
- package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
- package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
- package/src/{profiles → stem/profiles}/index.ts +1 -1
- package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
- package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
- package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
- package/src/stem/tracts/effector/types.ts +110 -0
- package/src/stem/tracts/effector.controller.ts +112 -401
- package/src/stem/tracts/outbox.controller.ts +30 -17
- package/src/stem/tracts/sensory.controller.ts +6 -6
- package/src/stem/tracts/session.logger.ts +6 -1
- package/src/stem/tracts/transport/types.ts +20 -1
- package/src/stem/tracts/transport.controller.ts +33 -9
- package/src/{channels → surface/channels}/discord.ts +224 -7
- package/src/{channels → surface/channels}/roster.ts +1 -1
- package/src/{channels → surface/channels}/types.ts +1 -1
- package/src/{channels → surface/channels}/whatsapp.ts +9 -6
- package/src/{cli.ts → surface/cli.ts} +6 -6
- package/src/{host → surface/host}/boot.ts +3 -3
- package/src/{host → surface/host}/utterances.ts +2 -2
- package/src/{mcp → surface/mcp}/effectors.ts +19 -6
- package/src/{mcp → surface/mcp}/server.ts +19 -5
- package/src/{sdk → surface/sdk}/will.ts +156 -28
- package/src/{serve → surface/serve}/server.ts +11 -4
- package/src/types.ts +24 -3
- package/dist/channels/discord.js.map +0 -1
- package/dist/channels/whatsapp.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/mcp/effectors.js.map +0 -1
- package/src/extensions/livestream.ext.ts +0 -570
- package/src/extensions/time.ext.ts +0 -339
- package/src/profiles/built-in.ts +0 -7
- package/src/runners/coherence.runner.ts +0 -49
- package/src/runners/outreach.runner.ts +0 -155
- package/src/runners/social.runner.ts +0 -179
- package/src/runners/thin-shim.runner.ts +0 -205
- /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
|
@@ -19,105 +19,57 @@
|
|
|
19
19
|
// The ops touch only WillInstance fields (no Will id needed), so these
|
|
20
20
|
// methods take the resolved instance directly. WillStem still validates
|
|
21
21
|
// existence via _get(id) before delegating.
|
|
22
|
+
//
|
|
23
|
+
// ── SIGNAL_BOUNDARY P1a ───────────────────────────────────────
|
|
24
|
+
// This file used to hold seven jobs. Two are now their own:
|
|
25
|
+
//
|
|
26
|
+
// ./effector/policy.enforcement.ts arbiter · verdicts · refusals
|
|
27
|
+
// ./effector/escalation.lifecycle.ts hold · resolve · expire
|
|
28
|
+
//
|
|
29
|
+
// What is left is the one job the name describes — the AGENCY↔HOST crossing:
|
|
30
|
+
// take an act the mind committed to, put it on the wire, and turn what comes
|
|
31
|
+
// back into something the mind can learn from. Plus the tick-boundary ordering
|
|
32
|
+
// the two collaborators share, which is a real responsibility and lives here
|
|
33
|
+
// because neither of them owns the tick.
|
|
34
|
+
//
|
|
35
|
+
// A pure move: no behaviour change, gated by replay.equivalence, same discipline
|
|
36
|
+
// as the planning.engine split. The import surface is unchanged — `effectorController`
|
|
37
|
+
// is still the export, so no caller moved.
|
|
22
38
|
// ─────────────────────────────────────────────────────────────
|
|
23
39
|
|
|
24
40
|
import { logger } from '#core/logger'
|
|
25
41
|
import { reconcileInvocation } from '#agency/reconcile.learning'
|
|
26
|
-
import {
|
|
27
|
-
import type { PolicyArbiter, PolicyInvocation, Verdict, DenialFinality, PolicyCounterfactual } from '#stem/policy/arbiter'
|
|
28
|
-
import { finalityOf, asFinality } from '#stem/policy/arbiter'
|
|
29
|
-
import {
|
|
30
|
-
getVerdictRecorder, getVerdictSource, type PolicyVerdictRecord,
|
|
31
|
-
} from '#stem/policy/verdict.recorder'
|
|
42
|
+
import type { PolicyArbiter, PolicyInvocation } from '#stem/policy/arbiter'
|
|
32
43
|
import type { effectorInvocation } from '#types'
|
|
33
44
|
import type { WillInstance } from '#stem/index'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
schema: string
|
|
39
|
-
reasonCode: string
|
|
40
|
-
finality: DenialFinality
|
|
41
|
-
/** ENVELOPE_NARROWING P0 — what WOULD have been allowed, carried through to
|
|
42
|
-
* the outcome the mind learns from. Absent on refusals that have no bound to
|
|
43
|
-
* report (a flat ban, a fault, an unanswered escalation). */
|
|
44
|
-
counterfactual?: PolicyCounterfactual
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The verdict a fault produces (POLICY_REAFFERENCE P5, conformance S9).
|
|
49
|
-
*
|
|
50
|
-
* An arbiter that throws or rejects has always failed CLOSED — the effect is
|
|
51
|
-
* withheld — but it used to withhold *silently*, queueing no refusal. The held
|
|
52
|
-
* intent then expired at the executor's AWAIT_TIMEOUT and reconciled as a plain
|
|
53
|
-
* failure, landing on COMPETENCE: a PDP outage taught the mind it was unskilled
|
|
54
|
-
* at something it is perfectly capable of. So a fault now yields a real verdict:
|
|
55
|
-
*
|
|
56
|
-
* • 'deny' — still fail-closed, unchanged. The effect never reaches the world.
|
|
57
|
-
* • 'context' — but it teaches NOTHING. The arbiter being unreachable is not a
|
|
58
|
-
* fact about the ability, so nothing about the ability may move.
|
|
59
|
-
*
|
|
60
|
-
* It goes through `_recordAndApply` rather than straight to the refusal queue so
|
|
61
|
-
* the fault lands on the VERDICT TAPE too. That closes a replay hole: an
|
|
62
|
-
* unrecorded fault left the source with nothing to re-feed, and a source miss
|
|
63
|
-
* reproduces a buffered ALLOW — so a live run that withheld the effect would
|
|
64
|
-
* have replayed as one that dispatched it.
|
|
65
|
-
*/
|
|
66
|
-
const ARBITER_FAULT_VERDICT: Readonly<Verdict> = Object.freeze({
|
|
67
|
-
decision: 'deny' as const,
|
|
68
|
-
reasonCode: 'ARBITER_UNAVAILABLE',
|
|
69
|
-
finality: 'context' as const,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
/** How long an escalated intent is held awaiting a resolution before it degrades
|
|
73
|
-
* to a refusal — 2× the host-ack timeout, so a human has real time to answer. */
|
|
74
|
-
const ESCALATION_TTL_TICKS = 30
|
|
75
|
-
|
|
76
|
-
/** An escalation the Will has raised: the intent is held, the ask is voiced once,
|
|
77
|
-
* and the original payload is kept so an approval can dispatch it to the world. */
|
|
78
|
-
interface Escalation {
|
|
79
|
-
intentId: string
|
|
80
|
-
schema: string
|
|
81
|
-
reasonCode: string
|
|
82
|
-
/** The withheld invocation payload — replayed to the host on approval. */
|
|
83
|
-
payload: Record<string, unknown>
|
|
84
|
-
expiresAt: number
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** A host's answer to an escalation, applied at the next tick boundary. */
|
|
88
|
-
interface PendingResolution {
|
|
89
|
-
intentId: string
|
|
90
|
-
approved: boolean
|
|
91
|
-
}
|
|
45
|
+
import { PolicyEnforcement } from './effector/policy.enforcement'
|
|
46
|
+
import { EscalationLifecycle } from './effector/escalation.lifecycle'
|
|
47
|
+
import type { EffectorAck } from './effector/types'
|
|
48
|
+
import type { PlanLink } from '#agency/types'
|
|
92
49
|
|
|
93
50
|
export class effectorController {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
private _activeEscalations = new Map<string, Map<string, Escalation>>()
|
|
110
|
-
/** Host answers awaiting application at the next tick boundary. */
|
|
111
|
-
private _pendingResolutions = new Map<string, PendingResolution[]>()
|
|
51
|
+
// The two collaborators are wired with arrow closures rather than a shared
|
|
52
|
+
// `this` reference, which is what lets the cycle exist without either class
|
|
53
|
+
// importing the other: policy raises an escalation, an escalation queues a
|
|
54
|
+
// refusal. Resolution is deferred to call time, so field order does not matter.
|
|
55
|
+
private readonly _escalations = new EscalationLifecycle({
|
|
56
|
+
buffer: ( i, p ) => this._buffer( i, p ),
|
|
57
|
+
confirmExecution: ( i, id, r ) => this.confirmExecution( i, id, r ),
|
|
58
|
+
queueRefusal: ( i, id, s, rc, f ) => this._policy.queueRefusal( i, id, s, rc, f ),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
private readonly _policy = new PolicyEnforcement({
|
|
62
|
+
buffer: ( i, p ) => this._buffer( i, p ),
|
|
63
|
+
confirmExecution: ( i, id, r ) => this.confirmExecution( i, id, r ),
|
|
64
|
+
raiseEscalation: ( i, esc ) => this._escalations.raise( i, esc ),
|
|
65
|
+
})
|
|
112
66
|
|
|
113
67
|
/**
|
|
114
68
|
* Install a Policy Decision Point (POLICY_REAFFERENCE P0). Passing null
|
|
115
|
-
* restores the no-op default.
|
|
116
|
-
* simulation state — and its verdict decides whether the invocation is
|
|
117
|
-
* handed to the host at all.
|
|
69
|
+
* restores the no-op default.
|
|
118
70
|
*/
|
|
119
71
|
setArbiter( arbiter: PolicyArbiter | null ): void {
|
|
120
|
-
this.
|
|
72
|
+
this._policy.setArbiter( arbiter )
|
|
121
73
|
}
|
|
122
74
|
|
|
123
75
|
/**
|
|
@@ -132,288 +84,37 @@ export class effectorController {
|
|
|
132
84
|
* Buffer a host-owned effector invocation for delivery. Called from the WillStem
|
|
133
85
|
* `agency.invocation` bus subscription with the event payload
|
|
134
86
|
* (`{ schema, intentId, targetEntityId, parameters, tick }`). The awaiting
|
|
135
|
-
* `agency.intent` id is the **correlation handle** (`
|
|
87
|
+
* `agency.intent` id is the **correlation handle** (`intentId`): the host
|
|
136
88
|
* echoes it on its result-ack, and `confirmExecution` uses it to find the intent.
|
|
137
|
-
*/
|
|
138
|
-
bufferInvocation( instance: WillInstance, payload: Record<string, unknown> ): void {
|
|
139
|
-
const willId = instance.config.id
|
|
140
|
-
|
|
141
|
-
// Replay: a registered source re-feeds the recorded verdict instead of
|
|
142
|
-
// re-consulting a live (or absent) PDP — the arbiter is an external oracle,
|
|
143
|
-
// exactly like the LLM. Checked FIRST so replay never re-enters the arbiter.
|
|
144
|
-
const source = getVerdictSource( willId )
|
|
145
|
-
if( source ){
|
|
146
|
-
const invocation = toPolicyInvocation( instance, payload )
|
|
147
|
-
const record = source.verdictFor( invocation.tick, invocation.intentId )
|
|
148
|
-
// A miss means the live run had no verdict here (null arbiter at record
|
|
149
|
-
// time) — the invocation was simply buffered, so reproduce that.
|
|
150
|
-
if( record ) this._applyVerdict( instance, payload, invocation, recordToVerdict( record ) )
|
|
151
|
-
else this._buffer( instance, payload )
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Fast path: no policy configured ⇒ the seam does not exist. No allocation,
|
|
156
|
-
// no branch beyond this one — the byte-identical guarantee.
|
|
157
|
-
if( isNullArbiter( this._arbiter ) ){
|
|
158
|
-
this._buffer( instance, payload )
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const invocation = toPolicyInvocation( instance, payload )
|
|
163
|
-
let verdict: Verdict | Promise<Verdict>
|
|
164
|
-
|
|
165
|
-
// An arbiter that throws must never become an implicit allow.
|
|
166
|
-
try { verdict = this._arbiter.evaluate( invocation ) }
|
|
167
|
-
catch( err ){
|
|
168
|
-
logger.error(`[policy] arbiter "${this._arbiter.name}" threw for "${invocation.schema}" — failing closed:`, err )
|
|
169
|
-
this._recordAndApply( instance, payload, invocation, ARBITER_FAULT_VERDICT )
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if( verdict instanceof Promise ){
|
|
174
|
-
// An external PDP resolves out of tick. Safe by construction: the executor
|
|
175
|
-
// holds the intent 'awaiting' for AWAIT_TIMEOUT (15 ticks), and the refusal
|
|
176
|
-
// queue drains each tick, so a verdict landing a few ticks late still lands.
|
|
177
|
-
void verdict.then(
|
|
178
|
-
v => this._recordAndApply( instance, payload, invocation, v ),
|
|
179
|
-
err => {
|
|
180
|
-
logger.error(`[policy] arbiter "${this._arbiter.name}" rejected for "${invocation.schema}" — failing closed:`, err )
|
|
181
|
-
this._recordAndApply( instance, payload, invocation, ARBITER_FAULT_VERDICT )
|
|
182
|
-
},
|
|
183
|
-
)
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
this._recordAndApply( instance, payload, invocation, verdict )
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Capture the verdict on the tape (if a recorder is attached), then enforce it. */
|
|
191
|
-
private _recordAndApply(
|
|
192
|
-
instance: WillInstance,
|
|
193
|
-
payload: Record<string, unknown>,
|
|
194
|
-
invocation: PolicyInvocation,
|
|
195
|
-
verdict: Verdict,
|
|
196
|
-
): void {
|
|
197
|
-
const sink = getVerdictRecorder( instance.config.id )
|
|
198
|
-
sink?.recordVerdict({
|
|
199
|
-
tick: invocation.tick,
|
|
200
|
-
willId: instance.config.id,
|
|
201
|
-
intentId: invocation.intentId,
|
|
202
|
-
schema: invocation.schema,
|
|
203
|
-
arbiter: this._arbiter.name,
|
|
204
|
-
decision: verdict.decision,
|
|
205
|
-
...( verdict.reasonCode ? { reasonCode: verdict.reasonCode } : {} ),
|
|
206
|
-
...( verdict.finality ? { finality: verdict.finality } : {} ),
|
|
207
|
-
...( verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {} ),
|
|
208
|
-
timestamp: Date.now(),
|
|
209
|
-
})
|
|
210
|
-
this._applyVerdict( instance, payload, invocation, verdict )
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Enforce a verdict (POLICY_REAFFERENCE P1).
|
|
215
|
-
*
|
|
216
|
-
* • allow → hand the invocation to the world.
|
|
217
|
-
* • deny → queue a refusal ack, applied at the next tick boundary via
|
|
218
|
-
* `confirmExecution` — the same lifecycle as a host rejection,
|
|
219
|
-
* so the mind meets *world resistance*, not a permission dialog.
|
|
220
|
-
* • escalate → raise a held escalation (POLICY_REAFFERENCE P4): the intent is
|
|
221
|
-
* held (the executor stops timing it out), the Will voices a
|
|
222
|
-
* first-person ask once, and a host resolution later approves
|
|
223
|
-
* (dispatch) or denies (refuse). Unresolved, it degrades to a
|
|
224
|
-
* refusal at ESCALATION_TTL_TICKS.
|
|
225
89
|
*
|
|
226
|
-
*
|
|
227
|
-
* signal (forbidden ≠ unskilled). P2 routes it to affordance AVAILABILITY
|
|
228
|
-
* instead of competence.
|
|
90
|
+
* Policy sits between here and the wire — see `PolicyEnforcement.evaluate`.
|
|
229
91
|
*/
|
|
230
|
-
|
|
231
|
-
instance
|
|
232
|
-
payload: Record<string, unknown>,
|
|
233
|
-
invocation: PolicyInvocation,
|
|
234
|
-
verdict: Verdict,
|
|
235
|
-
): void {
|
|
236
|
-
if( verdict.decision === 'allow'){
|
|
237
|
-
this._buffer( instance, payload )
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const cf = verdict.counterfactual
|
|
242
|
-
logger.info(
|
|
243
|
-
`[policy] ${verdict.decision.toUpperCase()} "${invocation.schema}" intent "${invocation.intentId}"` +
|
|
244
|
-
` — ${verdict.reasonCode ?? 'no reason code'}` +
|
|
245
|
-
( verdict.finality ? ` (${verdict.finality})` : '') +
|
|
246
|
-
( cf ? ` [${cf.field}: requested ${JSON.stringify( cf.requested )}, allowed ${JSON.stringify( cf.allowed )}]` : ''),
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
if( verdict.decision === 'deny'){
|
|
250
|
-
const queue = this._pendingRefusals.get( instance.config.id ) ?? []
|
|
251
|
-
queue.push({
|
|
252
|
-
intentId: invocation.intentId,
|
|
253
|
-
schema: invocation.schema,
|
|
254
|
-
reasonCode: verdict.reasonCode ?? 'POLICY_DENIED',
|
|
255
|
-
finality: finalityOf( verdict ),
|
|
256
|
-
...( verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {} ),
|
|
257
|
-
})
|
|
258
|
-
this._pendingRefusals.set( instance.config.id, queue )
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 'escalate' — raise a held escalation, applied (marked + voiced) at the boundary.
|
|
263
|
-
const escalations = this._newEscalations.get( instance.config.id ) ?? []
|
|
264
|
-
escalations.push({
|
|
265
|
-
intentId: invocation.intentId,
|
|
266
|
-
schema: invocation.schema,
|
|
267
|
-
reasonCode: verdict.reasonCode ?? 'APPROVAL_REQUIRED',
|
|
268
|
-
payload,
|
|
269
|
-
expiresAt: 0, // stamped when applied (we don't have the current tick here)
|
|
270
|
-
})
|
|
271
|
-
this._newEscalations.set( instance.config.id, escalations )
|
|
92
|
+
bufferInvocation( instance: WillInstance, payload: Record<string, unknown> ): void {
|
|
93
|
+
this._policy.evaluate( instance, payload, toPolicyInvocation )
|
|
272
94
|
}
|
|
273
95
|
|
|
274
96
|
/**
|
|
275
97
|
* Record a host's answer to an escalation (POLICY_REAFFERENCE P4). Applied at
|
|
276
|
-
* the next tick boundary
|
|
277
|
-
*
|
|
278
|
-
* no-op if the intent id is not (or no longer) an active escalation.
|
|
98
|
+
* the next tick boundary. A no-op if the intent id is not (or no longer) an
|
|
99
|
+
* active escalation.
|
|
279
100
|
*/
|
|
280
101
|
resolveEscalation( instance: WillInstance, intentId: string, approved: boolean ): void {
|
|
281
|
-
|
|
282
|
-
queue.push({ intentId, approved })
|
|
283
|
-
this._pendingResolutions.set( instance.config.id, queue )
|
|
102
|
+
this._escalations.resolve( instance, intentId, approved )
|
|
284
103
|
}
|
|
285
104
|
|
|
286
105
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
106
|
+
* The tick-boundary ordering both collaborators depend on, and the reason this
|
|
107
|
+
* stays in the controller: neither of them owns the tick, and the sequence is
|
|
108
|
+
* load-bearing. Called by the tick loop at the same boundary as inbound acks —
|
|
109
|
+
* BEFORE the step, stamped to this tick — so a denial reconciled here has the
|
|
110
|
+
* exact lifecycle of a host rejection that arrived between ticks.
|
|
291
111
|
*/
|
|
292
112
|
applyPolicyOutcomes( instance: WillInstance ): void {
|
|
293
113
|
const tick = instance.tickCount
|
|
294
|
-
this.
|
|
295
|
-
this.
|
|
296
|
-
this.
|
|
297
|
-
this.
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Drain queued refusals into failure acks (POLICY_REAFFERENCE P1). */
|
|
301
|
-
private _applyRefusals( instance: WillInstance ): void {
|
|
302
|
-
const queue = this._pendingRefusals.get( instance.config.id )
|
|
303
|
-
if( !queue || queue.length === 0 ) return
|
|
304
|
-
this._pendingRefusals.set( instance.config.id, [] )
|
|
305
|
-
|
|
306
|
-
for( const refusal of queue )
|
|
307
|
-
this.confirmExecution( instance, refusal.intentId, {
|
|
308
|
-
success: false,
|
|
309
|
-
refused: true,
|
|
310
|
-
finality: refusal.finality,
|
|
311
|
-
...( refusal.counterfactual ? { counterfactual: refusal.counterfactual } : {} ),
|
|
312
|
-
description: `refused by policy: ${refusal.reasonCode} (${refusal.finality})`,
|
|
313
|
-
} )
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** Raise each newly-escalated intent (POLICY_REAFFERENCE P4): mark it held in
|
|
317
|
-
* simulation state, voice the ask ONCE, and move it to the resolvable set. */
|
|
318
|
-
private _applyNewEscalations( instance: WillInstance, tick: number ): void {
|
|
319
|
-
const pending = this._newEscalations.get( instance.config.id )
|
|
320
|
-
if( !pending || pending.length === 0 ) return
|
|
321
|
-
this._newEscalations.set( instance.config.id, [] )
|
|
322
|
-
|
|
323
|
-
const active = this._activeEscalations.get( instance.config.id ) ?? new Map<string, Escalation>()
|
|
324
|
-
for( const esc of pending ){
|
|
325
|
-
esc.expiresAt = tick + ESCALATION_TTL_TICKS
|
|
326
|
-
this._markEscalated( instance, esc.intentId, esc.expiresAt )
|
|
327
|
-
this._voiceEscalation( instance, esc )
|
|
328
|
-
active.set( esc.intentId, esc )
|
|
329
|
-
}
|
|
330
|
-
this._activeEscalations.set( instance.config.id, active )
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** Apply host answers to active escalations (POLICY_REAFFERENCE P4). */
|
|
334
|
-
private _applyResolutions( instance: WillInstance ): void {
|
|
335
|
-
const queue = this._pendingResolutions.get( instance.config.id )
|
|
336
|
-
if( !queue || queue.length === 0 ) return
|
|
337
|
-
this._pendingResolutions.set( instance.config.id, [] )
|
|
338
|
-
|
|
339
|
-
const active = this._activeEscalations.get( instance.config.id )
|
|
340
|
-
for( const { intentId, approved } of queue ){
|
|
341
|
-
const esc = active?.get( intentId )
|
|
342
|
-
if( !esc ) continue // unknown / already resolved — ignore
|
|
343
|
-
active!.delete( intentId )
|
|
344
|
-
this._clearEscalated( instance, intentId ) // release the executor's hold
|
|
345
|
-
if( approved ){
|
|
346
|
-
this._buffer( instance, esc.payload ) // dispatch the held invocation now
|
|
347
|
-
logger.info(`[policy] escalation APPROVED → dispatching "${esc.schema}" intent "${intentId}"`)
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
this._queueRefusal( instance, esc.intentId, esc.schema, esc.reasonCode, 'class')
|
|
351
|
-
logger.info(`[policy] escalation DENIED → refusing "${esc.schema}" intent "${intentId}"`)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Degrade escalations no one answered in time into light refusals (P4).
|
|
358
|
-
*
|
|
359
|
-
* Finality 'parameter' is chosen for its BEHAVIOUR, not its name: silence is
|
|
360
|
-
* not literally an argument problem, but the light-dent-with-recovery it
|
|
361
|
-
* produces is exactly right — a Will whose asks go unanswered should ask
|
|
362
|
-
* progressively less, and should resume asking if someone starts answering.
|
|
363
|
-
* 'class' would be a lie (nobody said never) and 'context' would teach
|
|
364
|
-
* nothing, leaving the mind to escalate forever into an empty room.
|
|
365
|
-
*/
|
|
366
|
-
private _expireEscalations( instance: WillInstance, tick: number ): void {
|
|
367
|
-
const active = this._activeEscalations.get( instance.config.id )
|
|
368
|
-
if( !active || active.size === 0 ) return
|
|
369
|
-
for( const [ intentId, esc ] of active ){
|
|
370
|
-
if( tick < esc.expiresAt ) continue
|
|
371
|
-
active.delete( intentId )
|
|
372
|
-
this._clearEscalated( instance, intentId )
|
|
373
|
-
this._queueRefusal( instance, esc.intentId, esc.schema, 'ESCALATION_EXPIRED', 'parameter')
|
|
374
|
-
logger.info(`[policy] escalation EXPIRED → refusing "${esc.schema}" intent "${intentId}"`)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** Push a refusal onto the queue drained by _applyRefusals this same tick. */
|
|
379
|
-
private _queueRefusal(
|
|
380
|
-
instance: WillInstance, intentId: string, schema: string, reasonCode: string, finality: DenialFinality,
|
|
381
|
-
): void {
|
|
382
|
-
const queue = this._pendingRefusals.get( instance.config.id ) ?? []
|
|
383
|
-
queue.push({ intentId, schema, reasonCode, finality })
|
|
384
|
-
this._pendingRefusals.set( instance.config.id, queue )
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Mark the awaiting intent held: the executor stops timing it out (P4). */
|
|
388
|
-
private _markEscalated( instance: WillInstance, intentId: string, expiresAt: number ): void {
|
|
389
|
-
const intent = instance.simulation.stateManager.snapshot().entities.get( intentId )
|
|
390
|
-
if( !intent || intent.type !== 'agency.intent') return
|
|
391
|
-
instance.simulation.stateManager.setEntity({
|
|
392
|
-
id: intent.id,
|
|
393
|
-
type: intent.type,
|
|
394
|
-
metadata: { ...( intent.metadata ?? {} ), escalated: true, escalationExpiresAt: expiresAt },
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** Release the hold so the executor resumes normal timeout for this intent. */
|
|
399
|
-
private _clearEscalated( instance: WillInstance, intentId: string ): void {
|
|
400
|
-
const intent = instance.simulation.stateManager.snapshot().entities.get( intentId )
|
|
401
|
-
if( !intent || intent.type !== 'agency.intent') return
|
|
402
|
-
const meta = { ...( intent.metadata ?? {} ) } as Record<string, unknown>
|
|
403
|
-
delete meta['escalated']; delete meta['escalationExpiresAt']
|
|
404
|
-
instance.simulation.stateManager.setEntity({ id: intent.id, type: intent.type, metadata: meta })
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** Voice the escalation as a first-person broadcast ask — once, at raise time. */
|
|
408
|
-
private _voiceEscalation( instance: WillInstance, esc: Escalation ): void {
|
|
409
|
-
try {
|
|
410
|
-
instance.cognition.outboxWriter.enqueue({
|
|
411
|
-
targetEntityId: '*',
|
|
412
|
-
content: escalationAsk( esc.schema, esc.reasonCode ),
|
|
413
|
-
effectorName: 'broadcast',
|
|
414
|
-
})
|
|
415
|
-
}
|
|
416
|
-
catch( err ){ logger.warn(`[policy] escalation voice failed for "${esc.schema}": ${errMsg( err )}`) }
|
|
114
|
+
this._escalations.applyResolutions( instance ) // host answers land first
|
|
115
|
+
this._escalations.expire( instance, tick ) // then time out the unanswered
|
|
116
|
+
this._escalations.applyNew( instance, tick ) // then raise + voice the newest
|
|
117
|
+
this._policy.applyRefusals( instance ) // then the plain denials
|
|
417
118
|
}
|
|
418
119
|
|
|
419
120
|
/** Queue an approved invocation for the delivery layer. */
|
|
@@ -421,10 +122,14 @@ export class effectorController {
|
|
|
421
122
|
const intentId = ( payload.intentId as string ) ?? ''
|
|
422
123
|
instance.pendingEffectorInvocations.push({
|
|
423
124
|
id: intentId,
|
|
424
|
-
|
|
125
|
+
intentId: intentId, // correlation handle — the awaiting agency.intent id
|
|
425
126
|
effectorName: ( payload.schema as string ) ?? '',
|
|
426
127
|
parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
|
|
427
128
|
targetEntityId: payload.targetEntityId as string | undefined,
|
|
129
|
+
// Without this a host receives an anchor it cannot resolve to anything in
|
|
130
|
+
// its own world — see effectorInvocation.targetAddresses.
|
|
131
|
+
...( Array.isArray( payload.targetAddresses )
|
|
132
|
+
? { targetAddresses: payload.targetAddresses as string[] } : {} ),
|
|
428
133
|
reasoning: ( payload.reasoning as string ) ?? '',
|
|
429
134
|
...( typeof payload.description === 'string' ? { description: payload.description } : {} ),
|
|
430
135
|
tick: ( payload.tick as number ) ?? 0,
|
|
@@ -444,11 +149,11 @@ export class effectorController {
|
|
|
444
149
|
/**
|
|
445
150
|
* Called by the host/WorldInterface after executing a host-owned effector.
|
|
446
151
|
* `invocationId` is the correlation handle the host echoed — the awaiting
|
|
447
|
-
* `agency.intent`'s id (= the `
|
|
152
|
+
* `agency.intent`'s id (= the `intentId` field of the effectorInvocation).
|
|
448
153
|
*
|
|
449
154
|
* It reconciles the ack into an `agency.outcome` (via `reconcileInvocation`),
|
|
450
155
|
* carrying the intent's efference copy (predicted reward/valence) so surprise is
|
|
451
|
-
* honest, and its plan
|
|
156
|
+
* honest, and its plan link (planId/stepId) when it was a plan's frontier
|
|
452
157
|
* step. The ReafferenceEngine consumes that next tick: it learns the real result,
|
|
453
158
|
* frees the awaiting intent, and — for a plan-tagged outcome — emits the
|
|
454
159
|
* `action.outcome` the PlanningEngine advances on. No decision.record, no legacy
|
|
@@ -457,17 +162,7 @@ export class effectorController {
|
|
|
457
162
|
confirmExecution(
|
|
458
163
|
instance: WillInstance,
|
|
459
164
|
invocationId: string,
|
|
460
|
-
result:
|
|
461
|
-
success: boolean
|
|
462
|
-
description: string
|
|
463
|
-
metrics?: Record<string, number>
|
|
464
|
-
/** POLICY_REAFFERENCE P2 — set when the ack is a policy refusal, so the
|
|
465
|
-
* ReafferenceEngine routes it to availability rather than competence. */
|
|
466
|
-
refused?: boolean
|
|
467
|
-
finality?: DenialFinality
|
|
468
|
-
/** ENVELOPE_NARROWING P0 — the bound that was exceeded, if the arbiter said. */
|
|
469
|
-
counterfactual?: PolicyCounterfactual
|
|
470
|
-
},
|
|
165
|
+
result: EffectorAck,
|
|
471
166
|
): void {
|
|
472
167
|
const tick = instance.tickCount
|
|
473
168
|
|
|
@@ -486,14 +181,49 @@ export class effectorController {
|
|
|
486
181
|
reward: num( m['predictedReward'], num( m['expectedReward'], 0.5 ) ),
|
|
487
182
|
valence: num( m['predictedValence'], num( m['expectedValence'], 0 ) ),
|
|
488
183
|
}
|
|
489
|
-
const
|
|
184
|
+
const planLink: PlanLink = { planId: m['planId'] as string | undefined, stepId: m['stepId'] as string | undefined }
|
|
490
185
|
|
|
491
186
|
// Reconcile → agency.outcome. ReafferenceEngine consumes it next tick (learn +
|
|
492
187
|
// free the intent + emit the plan's action.outcome when plan-tagged).
|
|
188
|
+
//
|
|
189
|
+
// This is the FATE half, and it is unchanged: what the mind learns about its
|
|
190
|
+
// own competence at this act.
|
|
493
191
|
instance.simulation.stateManager.setEntity(
|
|
494
|
-
reconcileInvocation( invocationId, schema, result, tick, predicted,
|
|
192
|
+
reconcileInvocation( invocationId, schema, result, tick, predicted, planLink )
|
|
495
193
|
)
|
|
496
194
|
|
|
195
|
+
// ── The FACTS half (SIGNAL_BOUNDARY P2) ────────────────────
|
|
196
|
+
//
|
|
197
|
+
// An ack that carries an `observation` is not only feedback about an act —
|
|
198
|
+
// it is new information about the world, and information about the world
|
|
199
|
+
// reaches this mind exactly one way: as afference it can weigh, remember and
|
|
200
|
+
// recall. So it goes in through a sense, tagged REAFFERENT and tied to the
|
|
201
|
+
// act by `sourceIntentId`, rather than being flattened into a description
|
|
202
|
+
// the executive reads at 120 characters and nothing ever stores.
|
|
203
|
+
//
|
|
204
|
+
// Somatosensation, because that is what it is for: "webhooks, system
|
|
205
|
+
// signals, and external API callbacks — awareness of interaction with
|
|
206
|
+
// external systems". An effector ack is precisely an external callback.
|
|
207
|
+
//
|
|
208
|
+
// This is what replaces the two-call dance a host used to need — return the
|
|
209
|
+
// ack, then separately call `perceive()` and dress the answer up as
|
|
210
|
+
// something somebody said. `discord_inspect_channel` was the last host doing
|
|
211
|
+
// that and moved onto this in #151; nothing in the package launders an act's
|
|
212
|
+
// own result into news from the world any more.
|
|
213
|
+
//
|
|
214
|
+
// Fire-and-forget for the same reason the wake is: `ingest` is async and the
|
|
215
|
+
// tick boundary is not. Audition has ingested off-tick since it existed.
|
|
216
|
+
if( result.observation !== undefined && result.observation !== null )
|
|
217
|
+
void instance.cognition.somatosensationEngine.sense({
|
|
218
|
+
kind: 'system',
|
|
219
|
+
signal: schema,
|
|
220
|
+
provenance: 'reafferent',
|
|
221
|
+
sourceIntentId: invocationId,
|
|
222
|
+
// The observation itself, in whatever shape the host had it. The sense
|
|
223
|
+
// renders it for reading; nothing reshapes or shortens it here.
|
|
224
|
+
data: result.observation,
|
|
225
|
+
})
|
|
226
|
+
|
|
497
227
|
// Optional host-supplied metric deltas (e.g. the world moved a body metric).
|
|
498
228
|
// Validate each value is a finite number before it touches simulation state —
|
|
499
229
|
// a NaN/Infinity/garbage value from a buggy host would persist and corrupt
|
|
@@ -506,15 +236,19 @@ export class effectorController {
|
|
|
506
236
|
logger.warn(`[effector] confirmExecution: dropped non-finite metric "${k}"=${String( v )} from host ack (${schema})`)
|
|
507
237
|
}
|
|
508
238
|
|
|
239
|
+
// No `as never` any more. The cast was load-bearing in the wrong direction:
|
|
240
|
+
// it opted this write out of `LogEntryType` entirely, so the string was never
|
|
241
|
+
// checked against the union it belongs to. Without it a typo — or a type that
|
|
242
|
+
// no longer exists — fails the build.
|
|
509
243
|
instance.sessionLogger?.write({
|
|
510
|
-
type: '
|
|
244
|
+
type: 'effector.acked',
|
|
511
245
|
tick,
|
|
512
246
|
actionType: schema,
|
|
513
247
|
success: result.success,
|
|
514
|
-
outcome: result.description
|
|
248
|
+
outcome: result.description,
|
|
515
249
|
outcomeQuality: result.success ? 0.8 : 0.2,
|
|
516
250
|
confirmedExternally: true,
|
|
517
|
-
}
|
|
251
|
+
})
|
|
518
252
|
|
|
519
253
|
logger.info(`[effector] ✓ reconciled ${result.success ? 'success' : 'failure'}: intent "${invocationId}" (${schema})`)
|
|
520
254
|
}
|
|
@@ -524,38 +258,13 @@ function num( v: unknown, fallback: number ): number {
|
|
|
524
258
|
return typeof v === 'number' && Number.isFinite( v ) ? v : fallback
|
|
525
259
|
}
|
|
526
260
|
|
|
527
|
-
/** First-person ask for an escalated action, carrying the reason's MEANING (P4).
|
|
528
|
-
* Kept template-simple here; the facet-authored version is a later refinement. */
|
|
529
|
-
function escalationAsk( schema: string, reasonCode: string ): string {
|
|
530
|
-
const meaning = ESCALATION_MEANINGS[ reasonCode ] ?? 'I need your approval before I can do this'
|
|
531
|
-
return `I want to ${ schema }, but ${ meaning }. May I go ahead?`
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/** reasonCode → human meaning. Unknown codes fall back to a generic phrase. */
|
|
535
|
-
const ESCALATION_MEANINGS: Record<string, string> = {
|
|
536
|
-
APPROVAL_REQUIRED: 'I need your approval before I can on my own',
|
|
537
|
-
WRITE_REQUIRES_APPROVAL: "it writes to the world and I shouldn't on my own",
|
|
538
|
-
PAYMENT_REQUIRES_APPROVAL: 'it moves money and I must not do that unattended',
|
|
539
|
-
DEPLOY_REQUIRES_APPROVAL: 'it ships something and needs a human to sign off',
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function errMsg( err: unknown ): string {
|
|
543
|
-
return err instanceof Error ? err.message : String( err )
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/** Reconstruct an enforceable Verdict from a recorded verdict (replay path). */
|
|
547
|
-
function recordToVerdict( record: PolicyVerdictRecord ): Verdict {
|
|
548
|
-
return {
|
|
549
|
-
decision: record.decision,
|
|
550
|
-
...( record.reasonCode ? { reasonCode: record.reasonCode } : {} ),
|
|
551
|
-
...( record.finality ? { finality: record.finality } : {} ),
|
|
552
|
-
...( record.counterfactual ? { counterfactual: record.counterfactual } : {} ),
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
261
|
/**
|
|
557
262
|
* Project the `agency.invocation` payload onto the policy boundary's view of a
|
|
558
263
|
* proposed act. Only the act crosses — no cognitive internals, no state handle.
|
|
264
|
+
*
|
|
265
|
+
* It lives here rather than with enforcement because the payload shape is the
|
|
266
|
+
* translation seam's business: this file is the one that knows what an
|
|
267
|
+
* `agency.invocation` carries.
|
|
559
268
|
*/
|
|
560
269
|
function toPolicyInvocation( instance: WillInstance, payload: Record<string, unknown> ): PolicyInvocation {
|
|
561
270
|
return {
|
|
@@ -564,6 +273,8 @@ function toPolicyInvocation( instance: WillInstance, payload: Record<string, unk
|
|
|
564
273
|
schema: ( payload.schema as string ) ?? '',
|
|
565
274
|
parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
|
|
566
275
|
...( typeof payload.targetEntityId === 'string' ? { targetEntityId: payload.targetEntityId } : {} ),
|
|
276
|
+
...( Array.isArray( payload.targetAddresses )
|
|
277
|
+
? { targetAddresses: payload.targetAddresses as string[] } : {} ),
|
|
567
278
|
...( typeof payload.description === 'string' ? { description: payload.description } : {} ),
|
|
568
279
|
tick: ( payload.tick as number ) ?? 0,
|
|
569
280
|
}
|