@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
|
@@ -91,24 +91,37 @@ export class OutboxController {
|
|
|
91
91
|
break
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// 2.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
94
|
+
// 2. The world's answer about my own words — in through the sense door.
|
|
95
|
+
//
|
|
96
|
+
// SIGNAL_BOUNDARY P4. This tract used to build the percept entity itself and
|
|
97
|
+
// `setEntity` it, which made it one of three places outside a sense that
|
|
98
|
+
// wrote a percept. It is reafference by construction ("the ear hears the
|
|
99
|
+
// word you spoke"): the message went out, and the world said whether it
|
|
100
|
+
// landed. That is precisely what the somatosensation door carries since P2,
|
|
101
|
+
// and going through it means the percept is stamped, traced and swept by the
|
|
102
|
+
// same machinery as every other one instead of by hand here.
|
|
103
|
+
//
|
|
104
|
+
// The facts go as `data`, not as prose. `delivered` and the id are what the
|
|
105
|
+
// mind has to reason from; the sentence it reads is the engine's business.
|
|
106
|
+
// Two deliveries in one tick stay distinct because `messageId` is in the
|
|
107
|
+
// data and the trace id hashes the label the data composes — identical
|
|
108
|
+
// payloads in one tick are what that hash is meant to collapse.
|
|
109
|
+
//
|
|
110
|
+
// `summary` here is a host-style hint, and the engine may bound it; the data
|
|
111
|
+
// beside it is never bounded. Salience keeps the old asymmetry — a failure
|
|
112
|
+
// is worth more attention than a success, because only one of them needs
|
|
113
|
+
// anything done about it.
|
|
114
|
+
void instance.cognition.somatosensationEngine.sense({
|
|
115
|
+
kind: 'system',
|
|
116
|
+
signal: 'message_delivery',
|
|
117
|
+
provenance: 'reafferent',
|
|
118
|
+
data: {
|
|
111
119
|
messageId,
|
|
120
|
+
delivered,
|
|
121
|
+
salience: delivered ? 0.35 : 0.6,
|
|
122
|
+
summary: delivered
|
|
123
|
+
? 'My message was delivered successfully.'
|
|
124
|
+
: 'My message failed to reach the recipient.',
|
|
112
125
|
},
|
|
113
126
|
})
|
|
114
127
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// SensoryController owns the senses input + LLM chunk-streaming output
|
|
6
6
|
// boundary extracted from WillStem (R5-e):
|
|
7
|
-
// - input ingestion:
|
|
7
|
+
// - input ingestion: senseText / senseSignal / injectEvent
|
|
8
8
|
// - real-time chunk streaming: addChunkListener / addSensoryChunkListener
|
|
9
9
|
// and their sync helpers (syncChunkBroadcaster + the addChunkCallback fan-out)
|
|
10
10
|
//
|
|
@@ -34,17 +34,17 @@ export class SensoryController {
|
|
|
34
34
|
* LanguagePercept publication, conversation-facet spawn/reuse, and LLM
|
|
35
35
|
* reply generation. Reply delivery is async via the outbox / SSE channel.
|
|
36
36
|
*/
|
|
37
|
-
async
|
|
38
|
-
await instance.cognition.auditionEngine.
|
|
37
|
+
async senseText( instance: WillInstance, input: TextMessage ): Promise<void> {
|
|
38
|
+
await instance.cognition.auditionEngine.sense( input )
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Route a raw SensoryInput to the appropriate sense engine by domain.
|
|
43
43
|
* Used by the debug `POST /senses/:domain/ingest` route.
|
|
44
44
|
*/
|
|
45
|
-
async
|
|
45
|
+
async senseSignal( instance: WillInstance, domain: string, input: SensoryInput ): Promise<void> {
|
|
46
46
|
const cog = instance.cognition
|
|
47
|
-
const engineMap: Record<string, {
|
|
47
|
+
const engineMap: Record<string, { sense: (i: SensoryInput) => Promise<void> }> = {
|
|
48
48
|
audition: cog.auditionEngine,
|
|
49
49
|
vision: cog.visionEngine,
|
|
50
50
|
somatosensation: cog.somatosensationEngine,
|
|
@@ -53,7 +53,7 @@ export class SensoryController {
|
|
|
53
53
|
}
|
|
54
54
|
const engine = engineMap[ domain ]
|
|
55
55
|
if( !engine ) throw Object.assign( new Error(`Unknown sense domain: ${domain}`), { code: 400 } )
|
|
56
|
-
await engine.
|
|
56
|
+
await engine.sense( input )
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -50,7 +50,12 @@ export type LogEntryType =
|
|
|
50
50
|
| 'executive.facet.destroy'
|
|
51
51
|
| 'action.execute'
|
|
52
52
|
| 'action.error'
|
|
53
|
-
|
|
53
|
+
// A HOST ACKED AN EFFECTOR — not the `action.outcome` BUS EVENT, which is a
|
|
54
|
+
// different shape with six subscribers (SIGNAL_BOUNDARY P3). The two shared
|
|
55
|
+
// this string, and the prompt section named after it renders a third thing
|
|
56
|
+
// again. One word, three shapes; this is the one that gives it up, because
|
|
57
|
+
// "a host acked" is what it actually records and nothing else logs it.
|
|
58
|
+
| 'effector.acked'
|
|
54
59
|
| 'belief.integrate'
|
|
55
60
|
| 'conversation.in'
|
|
56
61
|
| 'conversation.out'
|
|
@@ -29,7 +29,7 @@ interface BaseEnvelope {
|
|
|
29
29
|
/**
|
|
30
30
|
* Stable id used to match an outbound envelope to its ack(s).
|
|
31
31
|
* For messages this is the OutboxMessage id; for effector invocations the
|
|
32
|
-
*
|
|
32
|
+
* intentId; for replies/chunks a generated id.
|
|
33
33
|
*/
|
|
34
34
|
correlationId: string
|
|
35
35
|
/** Monotonic per-Will sequence number — ordering + dedup at the peer. */
|
|
@@ -123,6 +123,25 @@ export interface InboundMessageEnvelope extends BaseEnvelope {
|
|
|
123
123
|
threadId: string
|
|
124
124
|
content: string
|
|
125
125
|
speakerName?: string
|
|
126
|
+
/**
|
|
127
|
+
* Whose doing this was, as the SENDING host asserts it. Optional on the wire
|
|
128
|
+
* and only there: `SensoryInput.provenance` is required inside the package,
|
|
129
|
+
* but a wire type cannot make an older peer send a field, so absence has to
|
|
130
|
+
* be survivable. It is read through `asProvenance()`, which owns the
|
|
131
|
+
* direction for untyped ingress.
|
|
132
|
+
*
|
|
133
|
+
* This field is why the transport no longer says `'unknown'`. It used to,
|
|
134
|
+
* and correctly — the absence was STRUCTURAL, there was no field to fill —
|
|
135
|
+
* but `unknown` percepts are skipped by the rupture gate in `action.selector`,
|
|
136
|
+
* so a mind reached over a transport could not be interrupted by anyone
|
|
137
|
+
* speaking to it, while the same words in-process could interrupt it. Two
|
|
138
|
+
* transports, two different minds.
|
|
139
|
+
*/
|
|
140
|
+
provenance?: import('#senses/index').SignalProvenance
|
|
141
|
+
/** True when this thread is private — just this someone and the Will. */
|
|
142
|
+
direct?: boolean
|
|
143
|
+
/** What the room is called, e.g. `#general`. A label, not an address. */
|
|
144
|
+
threadName?: string
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
/** A non-conversational external percept (webhook, system signal, etc.). */
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { logger } from '#core/logger'
|
|
24
24
|
import { wallClock } from '#core/wall.clock'
|
|
25
25
|
import { getInboundRecorder, getInboundSource } from '#core/inbound.recorder'
|
|
26
|
+
import { asProvenance } from '#senses/index'
|
|
26
27
|
import type { SensoryInput } from '#senses/index'
|
|
27
28
|
import type { effectorInvocation, OutboxMessage } from '#types'
|
|
28
29
|
import type { WillInstance } from '#stem/index'
|
|
@@ -242,7 +243,7 @@ export class TransportController {
|
|
|
242
243
|
/**
|
|
243
244
|
* Emit drained external effector invocations over the transport (2.4). Called
|
|
244
245
|
* by the tick loop after `pendingEffectorInvocations` is spliced. The peer
|
|
245
|
-
* executes each and returns a result-ack (correlationId =
|
|
246
|
+
* executes each and returns a result-ack (correlationId = intentId),
|
|
246
247
|
* which inbound dispatch routes to `confirmExecution`.
|
|
247
248
|
*/
|
|
248
249
|
emitInvocations( instance: WillInstance, invocations: effectorInvocation[] ): void {
|
|
@@ -254,7 +255,7 @@ export class TransportController {
|
|
|
254
255
|
this._emit( instance, {
|
|
255
256
|
channel: 'effector_invocation',
|
|
256
257
|
willId: instance.config.id,
|
|
257
|
-
correlationId: invocation.
|
|
258
|
+
correlationId: invocation.intentId,
|
|
258
259
|
seq: this._nextSeq( instance.config.id ),
|
|
259
260
|
wallTime: wallClock(),
|
|
260
261
|
invocation,
|
|
@@ -339,18 +340,41 @@ export class TransportController {
|
|
|
339
340
|
private _dispatch( instance: WillInstance, env: InboundEnvelope, deps: InboundApplyDeps ): void {
|
|
340
341
|
switch( env.channel ){
|
|
341
342
|
case 'inbound_message': {
|
|
342
|
-
// Fire-and-forget: AuditionEngine.
|
|
343
|
+
// Fire-and-forget: AuditionEngine.sense() serializes per-entity
|
|
343
344
|
// internally; the tick must not block on the LLM reply.
|
|
345
|
+
// THE FIX THIS COMMENT USED TO PROMISE. It said `'unknown'` was right
|
|
346
|
+
// because the absence was STRUCTURAL — the envelope had no provenance
|
|
347
|
+
// field, so a remote host could not declare one however much it knew —
|
|
348
|
+
// and that all three dropped fields were one deliberate wire change
|
|
349
|
+
// rather than three patches. This is that change: the envelope now
|
|
350
|
+
// carries `provenance`, `direct` and `threadName`.
|
|
351
|
+
//
|
|
352
|
+
// With the field there, an omission stops being structural and becomes
|
|
353
|
+
// a caller's omission, which is exactly what `asProvenance()` is for at
|
|
354
|
+
// an untyped boundary (its own comment sends this case here).
|
|
355
|
+
//
|
|
356
|
+
// It is not cosmetic. `'unknown'` percepts are skipped by the rupture
|
|
357
|
+
// gate in `action.selector`, so a mind reached over a transport COULD
|
|
358
|
+
// NOT BE INTERRUPTED by anyone speaking to it, while the same words
|
|
359
|
+
// in-process could interrupt it — and the transport path is the default
|
|
360
|
+
// whenever one is bound. Two transports, two different minds.
|
|
361
|
+
const provenance = asProvenance( env.provenance )
|
|
344
362
|
const input: SensoryInput = env.kind === 'voice'
|
|
345
|
-
? { kind: 'voice', entityId: env.entityId, threadId: env.threadId, transcription: env.content }
|
|
363
|
+
? { kind: 'voice', entityId: env.entityId, threadId: env.threadId, transcription: env.content, provenance }
|
|
346
364
|
: {
|
|
347
|
-
kind:
|
|
348
|
-
entityId:
|
|
349
|
-
threadId:
|
|
350
|
-
content:
|
|
365
|
+
kind: 'text',
|
|
366
|
+
entityId: env.entityId,
|
|
367
|
+
threadId: env.threadId,
|
|
368
|
+
content: env.content,
|
|
369
|
+
provenance,
|
|
351
370
|
...( env.speakerName ? { speakerName: env.speakerName } : {} ),
|
|
371
|
+
// Omitted rather than defaulted, the same way the SDK door does
|
|
372
|
+
// it: an unknown room is not known to be public, and a room with
|
|
373
|
+
// no name stays unnamed rather than being labelled with its id.
|
|
374
|
+
...( env.direct !== undefined ? { direct: env.direct } : {} ),
|
|
375
|
+
...( env.threadName ? { threadName: env.threadName } : {} ),
|
|
352
376
|
}
|
|
353
|
-
void instance.cognition.auditionEngine.
|
|
377
|
+
void instance.cognition.auditionEngine.sense( input )
|
|
354
378
|
break
|
|
355
379
|
}
|
|
356
380
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/channels/discord.ts — a Will present in a Discord server
|
|
2
|
+
// src/surface/channels/discord.ts — a Will present in a Discord server
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// The bridge relays both directions of the paradigm and nothing else:
|
|
6
6
|
//
|
|
7
|
-
// inbound guild/DM message → will.
|
|
7
|
+
// inbound guild/DM message → will.sense({ from, speaker, text, thread })
|
|
8
8
|
// — every author is `discord:<userId>` (stable across guilds), the
|
|
9
9
|
// display name is *learned* by the mind, and each Discord channel
|
|
10
10
|
// is its own conversation thread.
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
// `DiscordLikeClient` type keeps the dependency out of the type graph.
|
|
23
23
|
// ─────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
-
import type { Will, WillMessage } from '#sdk/will'
|
|
26
|
-
import { ChannelRoster } from '#channels/roster'
|
|
27
|
-
import { chunkText, renderAttachments, isTextual, type ChannelBridge, type ChannelAttachment } from '#channels/types'
|
|
25
|
+
import type { Will, WillMessage } from '#surface/sdk/will'
|
|
26
|
+
import { ChannelRoster } from '#surface/channels/roster'
|
|
27
|
+
import { chunkText, renderAttachments, isTextual, type ChannelBridge, type ChannelAttachment } from '#surface/channels/types'
|
|
28
28
|
|
|
29
29
|
const DISCORD_MESSAGE_LIMIT = 2000
|
|
30
30
|
|
|
@@ -41,11 +41,52 @@ const DISCORD_CDN_HOSTS = new Set( [ 'cdn.discordapp.com', 'media.discordapp.net
|
|
|
41
41
|
* bounds what is *kept*, this bounds what is fetched at all. */
|
|
42
42
|
const MAX_FETCH_BYTES = 256 * 1024
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* How much of her own message to quote back when someone reacts to it.
|
|
46
|
+
*
|
|
47
|
+
* Enough to identify WHICH thing was agreed with — a bare "someone reacted 👍"
|
|
48
|
+
* closes the answered loop but tells the mind nothing about what was affirmed,
|
|
49
|
+
* and 0.9.0 established that an answer without its content is worse than none:
|
|
50
|
+
* it invites acting on an answer never seen.
|
|
51
|
+
*/
|
|
52
|
+
const REACTION_QUOTE_CHARS = 140
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What to call the room this was said in — `#general`, `#general › release-cut`,
|
|
56
|
+
* or `#general in Mindot` when the server is known.
|
|
57
|
+
*
|
|
58
|
+
* A LABEL, not an address. `discord:1531…` is how a message gets to the room;
|
|
59
|
+
* this is what a person calls it, and 0.9.0 established those are different
|
|
60
|
+
* facts. Without it a room had a dossier and no name, so it reached the mind as
|
|
61
|
+
* "something" — which is what the prompt says for a `thing` it cannot name — and
|
|
62
|
+
* a mind choosing where to speak was picking between two opaque numbers.
|
|
63
|
+
*
|
|
64
|
+
* Returns undefined for a DM: a private thread is not a place, it is the person,
|
|
65
|
+
* and the tracker deliberately gives it no dossier to name.
|
|
66
|
+
*/
|
|
67
|
+
function roomLabel( message: DiscordLikeMessage ): string | undefined {
|
|
68
|
+
if( !message.guildId ) return undefined
|
|
69
|
+
const own = message.channel?.name
|
|
70
|
+
if( !own ) return undefined
|
|
71
|
+
const parent = message.channel?.parent?.name
|
|
72
|
+
const room = parent ? `#${ parent } › ${ own }` : `#${ own }`
|
|
73
|
+
const guild = message.guild?.name
|
|
74
|
+
return guild ? `${ room } in ${ guild }` : room
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
// ── The slice of discord.js the bridge actually uses (structural) ───────────
|
|
45
78
|
|
|
46
79
|
export interface DiscordLikeChannel {
|
|
47
80
|
send( content: string ): Promise<unknown>
|
|
48
81
|
sendTyping?(): Promise<unknown>
|
|
82
|
+
/** `general` for a text channel; absent on a DM, which has no name and is a person. */
|
|
83
|
+
name?: string | null
|
|
84
|
+
/** The thread's parent channel, so a thread reads as "#general › release-cut". */
|
|
85
|
+
parent?: { name?: string | null } | null
|
|
86
|
+
/** What the room is FOR, in the server's own words. Only read when asked. */
|
|
87
|
+
topic?: string | null
|
|
88
|
+
/** How many are in it — a count, never a roster. */
|
|
89
|
+
memberCount?: number
|
|
49
90
|
}
|
|
50
91
|
|
|
51
92
|
export interface DiscordLikeAttachment {
|
|
@@ -60,6 +101,8 @@ export interface DiscordLikeMessage {
|
|
|
60
101
|
cleanContent?: string
|
|
61
102
|
channelId: string
|
|
62
103
|
guildId?: string | null
|
|
104
|
+
/** The server this was said in. Its NAME is what a person calls the place. */
|
|
105
|
+
guild?: { name?: string | null } | null
|
|
63
106
|
author: { id: string; bot?: boolean; username?: string; displayName?: string }
|
|
64
107
|
member?: { displayName?: string } | null
|
|
65
108
|
mentions?: { has( userId: string ): boolean }
|
|
@@ -76,11 +119,38 @@ export interface DiscordLikeMessage {
|
|
|
76
119
|
attachments?: ReadonlyMap<string, DiscordLikeAttachment> | Iterable<DiscordLikeAttachment>
|
|
77
120
|
}
|
|
78
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A reaction, and the message it lands on.
|
|
124
|
+
*
|
|
125
|
+
* Both halves may be PARTIAL: Discord delivers a reaction on an uncached message
|
|
126
|
+
* with almost every field empty, which is the normal case for a message the bot
|
|
127
|
+
* sent before its current process started. `fetch()` fills it in, and the handler
|
|
128
|
+
* must call it rather than reading through the hole.
|
|
129
|
+
*/
|
|
130
|
+
export interface DiscordLikeReaction {
|
|
131
|
+
emoji: { name?: string | null; id?: string | null }
|
|
132
|
+
message: DiscordLikeMessage & {
|
|
133
|
+
partial?: boolean
|
|
134
|
+
author?: { id: string; bot?: boolean; username?: string; displayName?: string }
|
|
135
|
+
fetch?(): Promise<DiscordLikeReaction['message']>
|
|
136
|
+
}
|
|
137
|
+
partial?: boolean
|
|
138
|
+
fetch?(): Promise<DiscordLikeReaction>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DiscordLikeReactor {
|
|
142
|
+
id: string
|
|
143
|
+
bot?: boolean
|
|
144
|
+
username?: string
|
|
145
|
+
displayName?: string
|
|
146
|
+
}
|
|
147
|
+
|
|
79
148
|
export interface DiscordLikeClient {
|
|
80
149
|
user: { id: string; setPresence?( p: unknown ): void } | null
|
|
81
150
|
/** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */
|
|
82
151
|
isReady?(): boolean
|
|
83
152
|
on( event: 'messageCreate', fn: ( m: DiscordLikeMessage ) => void ): unknown
|
|
153
|
+
on( event: 'messageReactionAdd', fn: ( r: DiscordLikeReaction, u: DiscordLikeReactor ) => void ): unknown
|
|
84
154
|
once( event: string, fn: () => void ): unknown
|
|
85
155
|
login( token: string ): Promise<unknown>
|
|
86
156
|
destroy(): Promise<unknown> | void
|
|
@@ -159,6 +229,70 @@ export async function connectDiscord( will: Will, opts: DiscordBridgeOptions ):
|
|
|
159
229
|
|
|
160
230
|
// ── inbound: platform message → stimulus ──────────────────────────────────
|
|
161
231
|
client.on('messageCreate', message => { void onMessage( message ) } )
|
|
232
|
+
client.on('messageReactionAdd', ( reaction, user ) => { void onReaction( reaction, user ) } )
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Someone reacted to something the Will said.
|
|
236
|
+
*
|
|
237
|
+
* A reaction IS an answer, and until this existed the mind read it as silence.
|
|
238
|
+
* `conversation.received` is written only from a text percept, so a 👍 — the
|
|
239
|
+
* commonest acknowledgement on Discord — produced no record at all: the turn it
|
|
240
|
+
* answered stayed open, the reply window elapsed, and the mind concluded it had
|
|
241
|
+
* been ignored. That conclusion is not inert. It reaches reputation as
|
|
242
|
+
* reliability (−0.06 per unanswered turn) and goals as absence of progress, so
|
|
243
|
+
* answering her with an emoji actively taught her that person does not respond.
|
|
244
|
+
*
|
|
245
|
+
* Only reactions on the Will's OWN messages are perceived. A reaction between
|
|
246
|
+
* two other people in a busy channel is real social information, but it is not
|
|
247
|
+
* an answer to anything the mind said, and routing it here would swamp the
|
|
248
|
+
* conversation record it is meant to correct.
|
|
249
|
+
*/
|
|
250
|
+
async function onReaction( reaction: DiscordLikeReaction, user: DiscordLikeReactor ): Promise<void> {
|
|
251
|
+
const self = client.user
|
|
252
|
+
if( !self || user.id === self.id || user.bot ) return
|
|
253
|
+
|
|
254
|
+
// Both may arrive partial — a reaction on a message this process never cached
|
|
255
|
+
// is the NORMAL case for anything said before the last restart, which is
|
|
256
|
+
// exactly the long-lived conversation this is here to keep honest.
|
|
257
|
+
const full = reaction.partial && reaction.fetch ? await reaction.fetch().catch( () => null ) : reaction
|
|
258
|
+
if( !full ) return
|
|
259
|
+
const msg = full.message.partial && full.message.fetch
|
|
260
|
+
? await full.message.fetch().catch( () => null )
|
|
261
|
+
: full.message
|
|
262
|
+
if( !msg?.author ) return
|
|
263
|
+
|
|
264
|
+
if( msg.author.id !== self.id ) return // not ours — not an answer to us
|
|
265
|
+
const isDM = !msg.guildId
|
|
266
|
+
if( !isDM && allowed && !allowed.has( msg.channelId ) ) return
|
|
267
|
+
|
|
268
|
+
const emoji = full.emoji?.name ?? ( full.emoji?.id ? ':custom:' : '' )
|
|
269
|
+
if( !emoji ) return
|
|
270
|
+
|
|
271
|
+
const who = user.displayName ?? user.username
|
|
272
|
+
const said = ( msg.cleanContent || msg.content || '').trim().slice( 0, REACTION_QUOTE_CHARS )
|
|
273
|
+
|
|
274
|
+
// Described, bracketed, first person — the same shape `renderAttachments` uses
|
|
275
|
+
// for a file, and for the same reason: this reached the mind through the
|
|
276
|
+
// conversation but nobody SAID it, and a percept that reads like speech invites
|
|
277
|
+
// the mind to answer words that were never spoken.
|
|
278
|
+
const text = said
|
|
279
|
+
? `[${ who ?? 'someone' } reacted ${ emoji } to what I said: "${ said }"]`
|
|
280
|
+
: `[${ who ?? 'someone' } reacted ${ emoji } to something I said]`
|
|
281
|
+
|
|
282
|
+
await will.sense( {
|
|
283
|
+
text,
|
|
284
|
+
from: `discord:${ user.id }`,
|
|
285
|
+
thread: `discord:${ msg.channelId }`,
|
|
286
|
+
direct: isDM,
|
|
287
|
+
// Exafferent, and the near-miss is worth naming: this is ABOUT something
|
|
288
|
+
// she did, but it is not her doing it. Somebody else reacted. Reafference
|
|
289
|
+
// is the mind sensing its OWN act's consequence, not the world's response
|
|
290
|
+
// to that act — a reply would fail the same test for the same reason.
|
|
291
|
+
provenance: 'exafferent',
|
|
292
|
+
...( roomLabel( msg ) ? { threadName: roomLabel( msg ) } : {} ),
|
|
293
|
+
...( who ? { speaker: who } : {} ),
|
|
294
|
+
} )
|
|
295
|
+
}
|
|
162
296
|
|
|
163
297
|
async function onMessage( message: DiscordLikeMessage ): Promise<void> {
|
|
164
298
|
const self = client.user
|
|
@@ -201,7 +335,7 @@ export async function connectDiscord( will: Will, opts: DiscordBridgeOptions ):
|
|
|
201
335
|
)
|
|
202
336
|
const text = [ said, shared ].filter( Boolean ).join('\n')
|
|
203
337
|
|
|
204
|
-
await will.
|
|
338
|
+
await will.sense( {
|
|
205
339
|
text,
|
|
206
340
|
from: entityId,
|
|
207
341
|
thread: `discord:${ message.channelId }`,
|
|
@@ -210,6 +344,12 @@ export async function connectDiscord( will: Will, opts: DiscordBridgeOptions ):
|
|
|
210
344
|
// the right or wrong place to say something, and the mind never saw it —
|
|
211
345
|
// which is how a follow-up promised in a DM went out to #general.
|
|
212
346
|
direct: isDM,
|
|
347
|
+
// Somebody spoke. The bridge already drops her own messages (`onMessage`
|
|
348
|
+
// returns early on `author.id === self.id`), so nothing reafferent can
|
|
349
|
+
// reach this line today — but that filter is a bridge-level deletion of
|
|
350
|
+
// a signal she is entitled to sense, not a reason for the field to lie.
|
|
351
|
+
provenance: 'exafferent',
|
|
352
|
+
...( roomLabel( message ) ? { threadName: roomLabel( message ) } : {} ),
|
|
213
353
|
...( speaker ? { speaker } : {} ),
|
|
214
354
|
} )
|
|
215
355
|
}
|
|
@@ -262,6 +402,71 @@ export async function connectDiscord( will: Will, opts: DiscordBridgeOptions ):
|
|
|
262
402
|
return ( await res.text() ).slice( 0, MAX_FETCH_BYTES )
|
|
263
403
|
}
|
|
264
404
|
|
|
405
|
+
// ── the Will looks at something here, and Discord answers ─────────────────
|
|
406
|
+
//
|
|
407
|
+
// `inspect` is innate: every mind can look. What it finds depends on there
|
|
408
|
+
// being a world that can be asked, and this is Discord's answer.
|
|
409
|
+
//
|
|
410
|
+
// The answer is RETURNED AS AN OBSERVATION, and the engine turns it into a
|
|
411
|
+
// percept (SIGNAL_BOUNDARY P2). `description` says how the looking went;
|
|
412
|
+
// `observation` says what was found, and arrives at the mind as a reafferent
|
|
413
|
+
// percept tied to this act by `sourceIntentId` — so it can weigh the facts,
|
|
414
|
+
// disbelieve them, or decide they do not matter. An earlier cut wrote the
|
|
415
|
+
// answer straight into her dossier and beliefs, which is a mind being told
|
|
416
|
+
// what it knows rather than a mind finding out.
|
|
417
|
+
//
|
|
418
|
+
// This used to take TWO calls: return the ack, then separately `perceive()`
|
|
419
|
+
// the answer wrapped in a bracketed sentence — because an ack could not carry
|
|
420
|
+
// facts, and the sense door only accepted things somebody had said. The prose
|
|
421
|
+
// bracket was load-bearing: it was the only place the reafference lived where
|
|
422
|
+
// anything could read it. Both are gone.
|
|
423
|
+
will.effector('inspect', async ( _args, ctx ) => {
|
|
424
|
+
// The mind names a referent (`ke:1sqlkux`); we hold channel ids. The Will
|
|
425
|
+
// resolves the anchor to the addresses it was met at, and we take ours.
|
|
426
|
+
const address = ( ctx.targetAddresses ?? [] ).find( a => a.startsWith('discord:') )
|
|
427
|
+
if( !address ) return { success: false, description: 'Not something I can see on Discord.' }
|
|
428
|
+
|
|
429
|
+
const id = address.slice('discord:'.length )
|
|
430
|
+
const channel = await client.channels.fetch( id ).catch( () => null ) as DiscordLikeChannel | null
|
|
431
|
+
// A user id lands here too — a person is not a place, and Discord has nothing
|
|
432
|
+
// to say about them that watching them talk does not say better.
|
|
433
|
+
if( !channel?.name ) return { success: false, description: 'There is nothing here I can look up.' }
|
|
434
|
+
|
|
435
|
+
const facts: string[] = []
|
|
436
|
+
if( channel.topic ) facts.push(`it is for: ${ channel.topic }`)
|
|
437
|
+
if( channel.parent?.name ) facts.push(`it sits under #${ channel.parent.name }`)
|
|
438
|
+
if( typeof channel.memberCount === 'number')
|
|
439
|
+
// A COUNT, not a roster. Walking into a room you see that it is crowded long
|
|
440
|
+
// before you learn who anyone is; people become known by being met.
|
|
441
|
+
facts.push(`${ channel.memberCount } people are in it`)
|
|
442
|
+
|
|
443
|
+
if( facts.length === 0 )
|
|
444
|
+
return { success: false, description: `#${ channel.name } has nothing recorded about it.` }
|
|
445
|
+
|
|
446
|
+
const label = roomLabel( { channelId: id, guildId: 'g', channel } as DiscordLikeMessage ) ?? `#${ channel.name }`
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
success: true,
|
|
450
|
+
description: `Looked into ${ label }.`,
|
|
451
|
+
// The room as Discord has it, in the shape Discord has it. Not flattened
|
|
452
|
+
// into a sentence for the mind's benefit — a host that reshapes its own
|
|
453
|
+
// data is deciding what the mind may notice about it, and `observation`
|
|
454
|
+
// takes whatever shape the answer already had.
|
|
455
|
+
//
|
|
456
|
+
// `summary` is the one concession: it is what the executive prompt renders,
|
|
457
|
+
// so the host says it in words rather than leaving the mind to read JSON.
|
|
458
|
+
// Everything beside it stays available.
|
|
459
|
+
observation: {
|
|
460
|
+
summary: `I looked into ${ label }: ${ facts.join('; ') }.`,
|
|
461
|
+
room: label,
|
|
462
|
+
address,
|
|
463
|
+
...( channel.topic ? { topic: channel.topic } : {} ),
|
|
464
|
+
...( channel.parent?.name ? { parent: `#${ channel.parent.name }` } : {} ),
|
|
465
|
+
...( typeof channel.memberCount === 'number' ? { memberCount: channel.memberCount } : {} ),
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
} )
|
|
469
|
+
|
|
265
470
|
// ── outbound: projected utterance → the addressee ─────────────────────────
|
|
266
471
|
// The facade has no off(); the bridge gates its handler on `closed` instead.
|
|
267
472
|
let closed = false
|
|
@@ -352,8 +557,20 @@ async function createDiscordClient(): Promise<DiscordLikeClient> {
|
|
|
352
557
|
GatewayIntentBits.GuildMessages,
|
|
353
558
|
GatewayIntentBits.MessageContent,
|
|
354
559
|
GatewayIntentBits.DirectMessages,
|
|
560
|
+
// Neither is privileged, so this costs nothing to ask for — and without
|
|
561
|
+
// them an emoji answer never arrives and reads as being ignored.
|
|
562
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
563
|
+
GatewayIntentBits.DirectMessageReactions,
|
|
564
|
+
],
|
|
565
|
+
partials: [
|
|
566
|
+
Partials.Channel, // DMs arrive on uncached channels
|
|
567
|
+
// A reaction on a message this process did not cache — i.e. anything said
|
|
568
|
+
// before the last restart — is delivered partial, and without these the
|
|
569
|
+
// event is dropped before the handler sees it. That is precisely the
|
|
570
|
+
// long-running conversation the answered loop exists for.
|
|
571
|
+
Partials.Message,
|
|
572
|
+
Partials.Reaction,
|
|
355
573
|
],
|
|
356
|
-
partials: [ Partials.Channel ], // DMs arrive on uncached channels
|
|
357
574
|
} ) as unknown as DiscordLikeClient
|
|
358
575
|
}
|
|
359
576
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/channels/roster.ts — who the Will knows on a platform, and where
|
|
2
|
+
// src/surface/channels/roster.ts — who the Will knows on a platform, and where
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// The mind knows *entities*; a platform knows user ids and channels. The roster
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/channels/types.ts — the channel-bridge contract
|
|
2
|
+
// src/surface/channels/types.ts — the channel-bridge contract
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// A channel bridge puts a Will *in a place where people already are* (Discord,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/channels/whatsapp.ts — a Will present on WhatsApp
|
|
2
|
+
// src/surface/channels/whatsapp.ts — a Will present on WhatsApp
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// Same paradigm as the Discord bridge, different room:
|
|
6
6
|
//
|
|
7
|
-
// inbound DM / group message → will.
|
|
7
|
+
// inbound DM / group message → will.sense({ from, speaker, text, thread })
|
|
8
8
|
// — every author is `whatsapp:<number>` (stable across chats), the
|
|
9
9
|
// push name is *learned* by the mind, and each chat (DM or group)
|
|
10
10
|
// is its own conversation thread.
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
// `WaLikeSocket` type keeps the dependency out of the type graph.
|
|
26
26
|
// ─────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
import type { Will, WillMessage } from '#sdk/will'
|
|
29
|
-
import { ChannelRoster } from '#channels/roster'
|
|
30
|
-
import { chunkText, type ChannelBridge } from '#channels/types'
|
|
28
|
+
import type { Will, WillMessage } from '#surface/sdk/will'
|
|
29
|
+
import { ChannelRoster } from '#surface/channels/roster'
|
|
30
|
+
import { chunkText, type ChannelBridge } from '#surface/channels/types'
|
|
31
31
|
|
|
32
32
|
/** WhatsApp's hard per-message cap. */
|
|
33
33
|
const WHATSAPP_MESSAGE_LIMIT = 65_536
|
|
@@ -162,7 +162,10 @@ export async function connectWhatsApp( will: Will, opts: WhatsAppBridgeOptions =
|
|
|
162
162
|
const text = textOf( m )
|
|
163
163
|
if( !text.trim() ) return
|
|
164
164
|
|
|
165
|
-
await will.
|
|
165
|
+
await will.sense( {
|
|
166
|
+
// Somebody messaged her. Baileys filters `fromMe` upstream, so as with
|
|
167
|
+
// Discord nothing reafferent reaches this bridge today.
|
|
168
|
+
provenance: 'exafferent',
|
|
166
169
|
text,
|
|
167
170
|
from: entityId,
|
|
168
171
|
thread: `whatsapp:${ jid }`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ─────────────────────────────────────────────────────────────
|
|
3
|
-
// src/cli.ts — the `will` command: host a persistent mind
|
|
3
|
+
// src/surface/cli.ts — the `will` command: host a persistent mind
|
|
4
4
|
// ─────────────────────────────────────────────────────────────
|
|
5
5
|
//
|
|
6
6
|
// will mcp host over MCP stdio (Claude Desktop / Claude Code / IDEs)
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
// ─────────────────────────────────────────────────────────────
|
|
25
25
|
|
|
26
26
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
27
|
-
import { routeLogsToStderr, bootWillFromEnv } from '#
|
|
28
|
-
import { buildWillMcpServer } from '#
|
|
29
|
-
import { buildWillHttpServer } from '#
|
|
30
|
-
import { connectDiscord, parseChannels, parseMentionOnly } from '#channels/discord'
|
|
31
|
-
import { connectWhatsApp } from '#channels/whatsapp'
|
|
27
|
+
import { routeLogsToStderr, bootWillFromEnv } from '#surface/host/boot'
|
|
28
|
+
import { buildWillMcpServer } from '#surface/mcp/server'
|
|
29
|
+
import { buildWillHttpServer } from '#surface/serve/server'
|
|
30
|
+
import { connectDiscord, parseChannels, parseMentionOnly } from '#surface/channels/discord'
|
|
31
|
+
import { connectWhatsApp } from '#surface/channels/whatsapp'
|
|
32
32
|
|
|
33
33
|
// stdout is the MCP protocol channel under `will mcp` — route logs FIRST.
|
|
34
34
|
routeLogsToStderr()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/host/boot.ts — shared boot/shutdown for the `will` CLI hosts
|
|
2
|
+
// src/surface/host/boot.ts — shared boot/shutdown for the `will` CLI hosts
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// Both hosts (`will mcp`, `will serve`) raise the same mind the same way:
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
28
28
|
import { dirname, resolve } from 'node:path'
|
|
29
29
|
import { setLogger } from '#core/logger'
|
|
30
|
-
import { Will, detectProvider, type CreateWillOptions } from '#sdk/will'
|
|
30
|
+
import { Will, detectProvider, type CreateWillOptions } from '#surface/sdk/will'
|
|
31
31
|
import type { PMASnapshot } from '#pma/index'
|
|
32
|
-
import { connectMcpEffectors, type McpToolsSource } from '#
|
|
32
|
+
import { connectMcpEffectors, type McpToolsSource } from '#surface/mcp/effectors'
|
|
33
33
|
import { anthropicWireHeaders, defaultBaseFor, knownWireFor, providerKeyFromEnv, PROVIDER_KEY_ENV, type LLMProvider, type LLMWire } from '#llm/index'
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/host/utterances.ts — a host-side tap on a Will's speech
|
|
2
|
+
// src/surface/host/utterances.ts — a host-side tap on a Will's speech
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// Hosts that expose a Will over a request/response protocol (MCP tools, HTTP
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// first, else wait, else report silence (null — a choice, never an error).
|
|
11
11
|
// ─────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
import type { Will, WillMessage } from '#sdk/will'
|
|
13
|
+
import type { Will, WillMessage } from '#surface/sdk/will'
|
|
14
14
|
|
|
15
15
|
const BUFFER_CAP = 50
|
|
16
16
|
|