@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/mcp/effectors.ts — a Will EMPLOYING MCP tools (Seam 1)
|
|
2
|
+
// src/surface/mcp/effectors.ts — a Will EMPLOYING MCP tools (Seam 1)
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// The other direction from server.ts: connect a Will to an external MCP server
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
28
28
|
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
29
29
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
30
|
-
import type { Will, EffectorHandler, EffectorResult } from '#sdk/will'
|
|
30
|
+
import type { Will, EffectorHandler, EffectorResult } from '#surface/sdk/will'
|
|
31
31
|
|
|
32
32
|
/** Where the tools live: spawn a local server, reach a remote one, or bring a connected client. */
|
|
33
33
|
export type McpToolsSource =
|
|
@@ -53,8 +53,6 @@ export interface McpToolInfo {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** Keep tool outcomes bounded — the description feeds reafference + episodic memory. */
|
|
57
|
-
const RESULT_DESCRIPTION_CAP = 700
|
|
58
56
|
/** Keep ability meanings bounded — they render into the executive prompt. */
|
|
59
57
|
const MEANING_CAP = 300
|
|
60
58
|
|
|
@@ -105,8 +103,23 @@ export function buildMcpHandler( client: Client, tool: McpToolInfo ): EffectorHa
|
|
|
105
103
|
.map( c => c.text as string )
|
|
106
104
|
.join('\n')
|
|
107
105
|
.trim() || ( res.isError ? 'The tool reported an error.' : 'Done (no output).')
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// The tool's output is what the act REVEALED, so it goes to `observation`
|
|
107
|
+
// and it goes WHOLE (SIGNAL_BOUNDARY P2). It used to be crammed into
|
|
108
|
+
// `description` and cut at 700 — a number nobody chose, deciding on the
|
|
109
|
+
// mind's behalf how much of an answer it was allowed to have, and doing
|
|
110
|
+
// it at the boundary where that was the only copy.
|
|
111
|
+
//
|
|
112
|
+
// `description` now carries what it is for: how the call went. Short by
|
|
113
|
+
// nature, because a fate is short.
|
|
114
|
+
return {
|
|
115
|
+
success: !res.isError,
|
|
116
|
+
description: res.isError ? `${ tool.name } reported an error.` : `${ tool.name } ran.`,
|
|
117
|
+
// Whatever it said — including what it said when it failed. An error
|
|
118
|
+
// message is information about the world too, and cutting it or folding
|
|
119
|
+
// it into the fate is how a mind ends up knowing that something went
|
|
120
|
+
// wrong without ever learning what.
|
|
121
|
+
observation: text,
|
|
122
|
+
}
|
|
110
123
|
}
|
|
111
124
|
catch( err ){
|
|
112
125
|
return { success: false, description: `${ tool.name } failed: ${ err instanceof Error ? err.message : String( err ) }` }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/mcp/server.ts — a Will, exposed over the Model Context Protocol
|
|
2
|
+
// src/surface/mcp/server.ts — a Will, exposed over the Model Context Protocol
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// Lets any MCP client (Claude Desktop, Claude Code, an IDE) host a persistent
|
|
@@ -24,8 +24,9 @@ import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
|
24
24
|
import { dirname } from 'node:path'
|
|
25
25
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
26
26
|
import { z } from 'zod'
|
|
27
|
-
import type { Will } from '#sdk/will'
|
|
28
|
-
import {
|
|
27
|
+
import type { Will } from '#surface/sdk/will'
|
|
28
|
+
import { asProvenance } from '#senses/provenance'
|
|
29
|
+
import { UtteranceTap } from '#surface/host/utterances'
|
|
29
30
|
|
|
30
31
|
export interface WillMcpOptions {
|
|
31
32
|
/** Where `save` (and the CLI's shutdown hibernate) writes the PMA artifact. */
|
|
@@ -65,9 +66,22 @@ export function buildWillMcpServer( will: Will, opts: WillMcpOptions = {} ): Mcp
|
|
|
65
66
|
text: z.string().describe('What is said or observed.'),
|
|
66
67
|
from: z.string().optional().describe( "Who it's from (entity id, default 'user'). Use a stable id per person." ),
|
|
67
68
|
speaker: z.string().optional().describe('Display name of the speaker.'),
|
|
69
|
+
provenance: z.enum([ 'exafferent', 'reafferent', 'unknown' ]).optional().describe(
|
|
70
|
+
`Whose doing this was. 'exafferent' (default) — the world did it, somebody spoke or something happened. ` +
|
|
71
|
+
`'reafferent' — this is ${ will.name }'s OWN act coming back to it: the result of an ability it used, ` +
|
|
72
|
+
`an echo of a message it sent. 'unknown' — you looked and cannot tell. It cannot work this out for ` +
|
|
73
|
+
`itself; its own echo and a stranger saying the same words are identical from the inside.`),
|
|
68
74
|
},
|
|
69
|
-
}, async ( { text, from, speaker } ) => {
|
|
70
|
-
|
|
75
|
+
}, async ( { text, from, speaker, provenance } ) => {
|
|
76
|
+
// asProvenance, not a bare cast: this is an untyped protocol boundary, and
|
|
77
|
+
// an older client that never sends the field has not made a claim — it just
|
|
78
|
+
// predates the field. Defaulting toward 'exafferent' errs toward noticing.
|
|
79
|
+
await will.sense( {
|
|
80
|
+
text,
|
|
81
|
+
provenance: asProvenance( provenance ),
|
|
82
|
+
...( from ? { from } : {} ),
|
|
83
|
+
...( speaker ? { speaker } : {} ),
|
|
84
|
+
} )
|
|
71
85
|
return {
|
|
72
86
|
content: [ {
|
|
73
87
|
type: 'text',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/sdk/will.ts — the ergonomic facade
|
|
2
|
+
// src/surface/sdk/will.ts — the ergonomic facade
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// `WillStem` is the full, powerful contract (tick listeners, the outbox drain,
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// const will = await Will.create({ name: 'Aria', identity: {...} })
|
|
10
10
|
// will.on('message', m => console.log(m.content))
|
|
11
11
|
// will.effector('search_docs', async a => await myDb.search(a.query))
|
|
12
|
-
// await will.
|
|
12
|
+
// await will.sense({ from: 'ada', text: 'What should we work on?', provenance: 'exafferent' })
|
|
13
13
|
// const reply = await will.nextUtterance({ to: 'ada' }) // WillMessage | null
|
|
14
14
|
// const pma = await will.save() // non-destructive; keeps ticking
|
|
15
15
|
//
|
|
16
|
-
// A Will is a *subject*, not a function: you `
|
|
16
|
+
// A Will is a *subject*, not a function: you `sense` stimuli to it and observe
|
|
17
17
|
// its *projections* (message / effector / emotion / state) — you never `await`
|
|
18
18
|
// a computed return. `nextUtterance` is a thin, honest adapter for callers that
|
|
19
19
|
// want a reply-shaped await; `null` means the Will chose silence, not an error.
|
|
@@ -33,12 +33,14 @@ import { PROVIDER_KEY_ENV, providerKeyFromEnv, type LLMProvider } from '#llm/ind
|
|
|
33
33
|
import type { PMASnapshot } from '#pma/index'
|
|
34
34
|
import type { effectorInvocation } from '#types'
|
|
35
35
|
import type { EffectorDeclaration, SchemaPrecondition } from '#agency/types'
|
|
36
|
+
import type { PolicyArbiter } from '#stem/policy/arbiter'
|
|
37
|
+
import type { SignalProvenance } from '#senses/provenance'
|
|
36
38
|
|
|
37
39
|
// ── Public surface ────────────────────────────────────────────
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* A stimulus entering the Will's sensory field. A Will is a subject, not a
|
|
41
|
-
* function: you don't *call* it with input and await a return — you `
|
|
43
|
+
* function: you don't *call* it with input and await a return — you `sense`
|
|
42
44
|
* something to it, and it *may* project a response later (see `nextUtterance`),
|
|
43
45
|
* coloured by its current state. Silence is a valid, meaningful outcome.
|
|
44
46
|
*/
|
|
@@ -54,10 +56,39 @@ export interface Stimulus {
|
|
|
54
56
|
* name, the Will knows the person as "someone" until it learns one.
|
|
55
57
|
*/
|
|
56
58
|
speaker?: string
|
|
59
|
+
/**
|
|
60
|
+
* Whose doing this was: `'exafferent'` (the world), `'reafferent'` (the Will's
|
|
61
|
+
* own act, coming back), or `'unknown'` (you cannot tell).
|
|
62
|
+
*
|
|
63
|
+
* REQUIRED, as of P3. Only you can answer it — nothing inside the mind can
|
|
64
|
+
* tell the echo of its own utterance from a stranger saying the same words,
|
|
65
|
+
* which is why this is asserted at the boundary and never inferred behind it.
|
|
66
|
+
* Most inbound traffic is `'exafferent'`; use `say()`/`tell()`, whose verbs
|
|
67
|
+
* already make that claim, when that is all you mean. Reach for `'reafferent'`
|
|
68
|
+
* when you are feeding back the result of something the Will did — an
|
|
69
|
+
* ability's output, a webhook fired by its own write, a platform echo of a
|
|
70
|
+
* message it sent — and pass `sourceIntentId` if you have it.
|
|
71
|
+
*
|
|
72
|
+
* It was optional for exactly one epoch, defaulting to `'exafferent'`, so that
|
|
73
|
+
* a host migrated ONCE — at P3, alongside the `perceive()` → `sense()` rename
|
|
74
|
+
* — rather than twice. That default was the last surviving instance of the
|
|
75
|
+
* four-state hole the internal types exist to forbid: an omission silently
|
|
76
|
+
* became a claim nobody made. It is gone.
|
|
77
|
+
*
|
|
78
|
+
* If you genuinely cannot tell, say `'unknown'`. That is a different statement
|
|
79
|
+
* from `'exafferent'` and the mind treats it as one: the rupture gate in
|
|
80
|
+
* `action.selector` counts only `'exafferent'` percepts, so a mislabelled echo
|
|
81
|
+
* can interrupt a mind's train of thought with its own words.
|
|
82
|
+
*/
|
|
83
|
+
provenance: SignalProvenance
|
|
84
|
+
/** The intent whose enaction caused this, when `provenance` is `'reafferent'`. */
|
|
85
|
+
sourceIntentId?: string
|
|
57
86
|
/** Conversation/thread id (default = `from`). */
|
|
58
87
|
thread?: string
|
|
59
88
|
/** True when `thread` is private — just this someone and the Will. See TextMessage.direct. */
|
|
60
89
|
direct?: boolean
|
|
90
|
+
/** What the room is called, e.g. `#general`. A label, not an address. See TextMessage.threadName. */
|
|
91
|
+
threadName?: string
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
/** A message the Will emitted to someone. */
|
|
@@ -69,7 +100,7 @@ export interface WillMessage {
|
|
|
69
100
|
/** Entity id the Will addressed (the speaker you used in say()/tell(), or a bond). */
|
|
70
101
|
to: string
|
|
71
102
|
/**
|
|
72
|
-
* The conversation this belongs to — the `thread` from the `
|
|
103
|
+
* The conversation this belongs to — the `thread` from the `sense()` that
|
|
73
104
|
* prompted it. Absent when the Will spoke unprompted, which genuinely has no
|
|
74
105
|
* thread.
|
|
75
106
|
*
|
|
@@ -113,14 +144,75 @@ export interface WillAffect {
|
|
|
113
144
|
*/
|
|
114
145
|
export type EffectorResult = string | {
|
|
115
146
|
success: boolean
|
|
147
|
+
/** How the act WENT — its fate. What the Will learns competence from. */
|
|
116
148
|
description: string
|
|
149
|
+
/**
|
|
150
|
+
* What the act REVEALED — new information about the world (SIGNAL_BOUNDARY P2).
|
|
151
|
+
*
|
|
152
|
+
* Return it and the Will *perceives* it: it arrives as a reafferent percept
|
|
153
|
+
* tied to the act by `sourceIntentId`, so it is remembered and recallable,
|
|
154
|
+
* not merely learned from. A lookup, a listing, a snapshot has one; a kick or
|
|
155
|
+
* a warning does not — those only have a fate.
|
|
156
|
+
*
|
|
157
|
+
* return { success: true,
|
|
158
|
+
* description: 'Looked up Ada.', // how it went
|
|
159
|
+
* observation: 'Ada joined 3 months ago, …' } // what I found
|
|
160
|
+
*
|
|
161
|
+
* Any shape — a string, a record, a list — and carried WHOLE.
|
|
162
|
+
*
|
|
163
|
+
* SEND WHAT YOU HAVE, NOT WHAT IT MEANS. You are not asked to summarise, and
|
|
164
|
+
* you should not: making meaning by connecting pieces of information is the
|
|
165
|
+
* mind's entire job, and a host that hands over a conclusion has done that
|
|
166
|
+
* work on the wrong side of the boundary. A robot's vision layer reports
|
|
167
|
+
* `{ object: 'ball', confidence: 0.9, bbox: […] }`; it has no business
|
|
168
|
+
* deciding whether that is worth reacting to.
|
|
169
|
+
*
|
|
170
|
+
* The mind labels it with the ability's own name, and sees the data itself in
|
|
171
|
+
* its percepts — state, working memory and the prompt all carry it. If you
|
|
172
|
+
* happen to have a one-line `summary` on your object it is used as the label,
|
|
173
|
+
* but that is a convenience and never a requirement.
|
|
174
|
+
*
|
|
175
|
+
* SIZE IS YOURS TO JUDGE, AND NOTHING TRUNCATES IT. What you send lands in
|
|
176
|
+
* the mind's percepts and, briefly, its prompt — so a very large payload
|
|
177
|
+
* costs tokens on the ticks it is alive. The engine will not second-guess you
|
|
178
|
+
* by cutting it: a cap here decides for a mind how much of an answer it may
|
|
179
|
+
* have, and that decision is not the engine's to make. Send the record; send
|
|
180
|
+
* the field you would want it to notice.
|
|
181
|
+
*
|
|
182
|
+
* Before this, a host with facts to hand back had to return the ack AND call
|
|
183
|
+
* `sense()` separately — two calls for one act, and the second one had to
|
|
184
|
+
* pretend somebody had spoken.
|
|
185
|
+
*/
|
|
186
|
+
observation?: unknown
|
|
117
187
|
metrics?: Record<string, number>
|
|
118
188
|
}
|
|
119
189
|
|
|
120
190
|
/** Your implementation of an ability the Will can choose to use. */
|
|
121
191
|
export type EffectorHandler = (
|
|
122
192
|
args: Record<string, unknown>,
|
|
123
|
-
ctx: {
|
|
193
|
+
ctx: {
|
|
194
|
+
reasoning: string
|
|
195
|
+
/**
|
|
196
|
+
* The `agency.intent` this handler is running under — the correlation handle
|
|
197
|
+
* the Will will match an ack to (SIGNAL_BOUNDARY P1).
|
|
198
|
+
*
|
|
199
|
+
* Pass it as `sourceIntentId` on any `sense()` you make from inside a
|
|
200
|
+
* handler, and the resulting percept is a *reafference* the mind can tie back
|
|
201
|
+
* to the act that caused it, rather than an unexplained arrival. Before this
|
|
202
|
+
* existed, `discord_inspect_channel` had to say so in English — a bracketed
|
|
203
|
+
* `[I looked into #general: …]` that only the LLM could read — because the
|
|
204
|
+
* fact had nowhere structural to live.
|
|
205
|
+
*/
|
|
206
|
+
intentId: string
|
|
207
|
+
targetEntityId?: string
|
|
208
|
+
/**
|
|
209
|
+
* The addresses this host knows `targetEntityId` by — a channel id, a user id.
|
|
210
|
+
* `targetEntityId` itself is an opaque anchor (who something IS); these are
|
|
211
|
+
* where to find it. Resolved inside the Will, where the alias table lives.
|
|
212
|
+
*/
|
|
213
|
+
targetAddresses?: readonly string[]
|
|
214
|
+
description?: string
|
|
215
|
+
},
|
|
124
216
|
) => EffectorResult | Promise<EffectorResult>
|
|
125
217
|
|
|
126
218
|
/**
|
|
@@ -180,12 +272,6 @@ export interface CreateWillOptions {
|
|
|
180
272
|
identity: Partial<WillIdentity> & { prompt: string }
|
|
181
273
|
/** 'mind' (default: the whole architecture) | 'reflex' (no-LLM shell). */
|
|
182
274
|
anatomy?: Anatomy
|
|
183
|
-
/** Concrete LLM model id, or a per-role map ({ executive, summarizer?,
|
|
184
|
-
* deliberation?, embedding? } — unset thinking roles fall back to executive).
|
|
185
|
-
* Unset → env / provider default.
|
|
186
|
-
* @deprecated Pass `llmConfig: { model }` instead — model and transport are
|
|
187
|
-
* one concern. Still honoured; an explicit `llmConfig.model` wins. */
|
|
188
|
-
model?: string | WillModelConfig
|
|
189
275
|
/** Per-Will LLM config: provider, model(s), BYO apiKey, baseUrl, caps.
|
|
190
276
|
* Unset fields fall back to WILL_LLM_* envs. apiKey stays in memory only.
|
|
191
277
|
* (Named llmConfig because `llm` is the provider MODE switch.) */
|
|
@@ -372,10 +458,18 @@ export class Will {
|
|
|
372
458
|
* *delivered*, NOT once the Will has responded: a response (if any) is a
|
|
373
459
|
* projection that arrives later on the `message` event, or via
|
|
374
460
|
* `nextUtterance()`. The Will may also stay silent — that is not an error.
|
|
461
|
+
*
|
|
462
|
+
* NAMED `sense`, NOT `perceive` (SIGNAL_BOUNDARY P3). What happens here is
|
|
463
|
+
* *transduction*: a signal crosses into the mind. PERCEPTION is what a sense
|
|
464
|
+
* engine does afterwards — audition runs, judges salience, and produces a
|
|
465
|
+
* `Percept`, which may not resemble what arrived and may not happen at all
|
|
466
|
+
* (a gated sense drops it). Calling the door `perceive` said the caller had
|
|
467
|
+
* already done the mind's work, and it misled every reader of this flow,
|
|
468
|
+
* including the first draft of the epoch that renamed it.
|
|
375
469
|
*/
|
|
376
|
-
async
|
|
470
|
+
async sense( stimulus: Stimulus ): Promise<void> {
|
|
377
471
|
const from = stimulus.from ?? 'user'
|
|
378
|
-
await this.stem.
|
|
472
|
+
await this.stem.senseText( this.id, {
|
|
379
473
|
kind: 'text',
|
|
380
474
|
entityId: from,
|
|
381
475
|
threadId: stimulus.thread ?? from,
|
|
@@ -388,17 +482,32 @@ export class Will {
|
|
|
388
482
|
...( stimulus.speaker ? { speakerName: stimulus.speaker } : {} ),
|
|
389
483
|
// Omitted rather than defaulted: an unknown room is not known to be public.
|
|
390
484
|
...( stimulus.direct !== undefined ? { direct: stimulus.direct } : {} ),
|
|
485
|
+
// Omitted when the channel does not know — a room with no name stays
|
|
486
|
+
// unnamed, the same way a person does, rather than being labelled with its id.
|
|
487
|
+
...( stimulus.threadName ? { threadName: stimulus.threadName } : {} ),
|
|
488
|
+
// No default. `Stimulus.provenance` is required now, so there is nothing
|
|
489
|
+
// left to fall back to — the four-state hole is closed at every door into
|
|
490
|
+
// this mind, which was the point of the epoch.
|
|
491
|
+
provenance: stimulus.provenance,
|
|
492
|
+
...( stimulus.sourceIntentId ? { sourceIntentId: stimulus.sourceIntentId } : {} ),
|
|
391
493
|
} )
|
|
392
494
|
}
|
|
393
495
|
|
|
394
|
-
/**
|
|
496
|
+
/**
|
|
497
|
+
* Sense from the default user. Sugar over `sense`.
|
|
498
|
+
*
|
|
499
|
+
* Supplies `provenance: 'exafferent'` — that is not a default sneaking back
|
|
500
|
+
* in, it is what the verb MEANS. "Say" is somebody speaking to the Will; a
|
|
501
|
+
* caller who wants to feed back the Will's own act reaches for `sense` and
|
|
502
|
+
* says so. The assertion lives in the function name.
|
|
503
|
+
*/
|
|
395
504
|
async say( text: string ): Promise<void> {
|
|
396
|
-
return this.
|
|
505
|
+
return this.sense( { text, from: 'user', provenance: 'exafferent' } )
|
|
397
506
|
}
|
|
398
507
|
|
|
399
|
-
/**
|
|
508
|
+
/** Sense from a specific interlocutor (multi-party). Sugar over `sense` — see `say` on provenance. */
|
|
400
509
|
async tell( entityId: string, speakerName: string, text: string ): Promise<void> {
|
|
401
|
-
return this.
|
|
510
|
+
return this.sense( { text, from: entityId, speaker: speakerName, provenance: 'exafferent' } )
|
|
402
511
|
}
|
|
403
512
|
|
|
404
513
|
/**
|
|
@@ -408,7 +517,7 @@ export class Will {
|
|
|
408
517
|
* 5000). `null` is a real outcome — the Will chose not to speak — not a
|
|
409
518
|
* failure. Pass `to` to only accept an utterance addressed to that entity.
|
|
410
519
|
*
|
|
411
|
-
* await will.
|
|
520
|
+
* await will.sense( { from: 'ada', text: 'Hi!', provenance: 'exafferent' } )
|
|
412
521
|
* const reply = await will.nextUtterance( { to: 'ada', within: 3000 } )
|
|
413
522
|
* // reply is a WillMessage, or null if Ada got the silent treatment.
|
|
414
523
|
*/
|
|
@@ -515,6 +624,19 @@ export class Will {
|
|
|
515
624
|
pause(): void { this.stem.pauseWill( this.id ) }
|
|
516
625
|
resume(): void { this.stem.resumeWill( this.id ) }
|
|
517
626
|
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Install the Policy Decision Point consulted before every effector
|
|
630
|
+
* invocation this Will hands to the host (POLICY_REAFFERENCE P0). `null`
|
|
631
|
+
* restores the no-op default. See `WillStem.setArbiter` for the one scoping
|
|
632
|
+
* caveat (per-stem, not per-Will id) — irrelevant here since `Will.create()`
|
|
633
|
+
* gives this instance its own dedicated stem.
|
|
634
|
+
*/
|
|
635
|
+
setArbiter( arbiter: PolicyArbiter | null ): this {
|
|
636
|
+
this.stem.setArbiter( arbiter )
|
|
637
|
+
return this
|
|
638
|
+
}
|
|
639
|
+
|
|
518
640
|
/**
|
|
519
641
|
* Checkpoint the living mind into a portable PMA artifact — NON-destructive.
|
|
520
642
|
* The Will keeps ticking; the snapshot is a point-in-time copy you can archive
|
|
@@ -555,16 +677,14 @@ export class Will {
|
|
|
555
677
|
const mode = opts.llm ?? detectProvider()
|
|
556
678
|
const useMock = mode === 'mock'
|
|
557
679
|
// `llm` selects the provider; an explicit llmConfig.provider still wins.
|
|
558
|
-
// Model rides with the transport
|
|
559
|
-
// top-level `
|
|
560
|
-
|
|
680
|
+
// Model rides with the transport — `llmConfig.model`, and only that. The
|
|
681
|
+
// top-level `model` spelling is gone: two ways to say one thing is how a
|
|
682
|
+
// config grows a precedence rule nobody can remember.
|
|
683
|
+
const llmConfig: WillLLMConfig | undefined = useMock && !opts.llmConfig
|
|
561
684
|
? undefined
|
|
562
685
|
: {
|
|
563
686
|
...( mode !== 'mock' ? { provider: mode } : {} ),
|
|
564
687
|
...opts.llmConfig,
|
|
565
|
-
...( opts.llmConfig?.model !== undefined ? { model: opts.llmConfig.model }
|
|
566
|
-
: opts.model !== undefined ? { model: opts.model }
|
|
567
|
-
: {} ),
|
|
568
688
|
}
|
|
569
689
|
return {
|
|
570
690
|
id, name: opts.name,
|
|
@@ -622,7 +742,7 @@ export class Will {
|
|
|
622
742
|
if( !handler ){
|
|
623
743
|
// The Will chose an effector we have no handler for — report it as failed so
|
|
624
744
|
// the reafference loop learns it doesn't work, rather than hanging on the await.
|
|
625
|
-
this.stem.confirmEffectorExecution( this.id, inv.
|
|
745
|
+
this.stem.confirmEffectorExecution( this.id, inv.intentId, {
|
|
626
746
|
success: false, description: `No handler registered for effector "${inv.effectorName}"`,
|
|
627
747
|
} )
|
|
628
748
|
return
|
|
@@ -631,15 +751,23 @@ export class Will {
|
|
|
631
751
|
try {
|
|
632
752
|
const raw = await handler( inv.parameters, {
|
|
633
753
|
reasoning: inv.reasoning,
|
|
754
|
+
// The correlation handle, so a handler that feeds its own result back in
|
|
755
|
+
// can say WHICH act caused it. This line used to read
|
|
756
|
+
// `intentId: inv.decisionRecordId` — the translation that proved the
|
|
757
|
+
// wire name was already wrong.
|
|
758
|
+
intentId: inv.intentId,
|
|
634
759
|
targetEntityId: inv.targetEntityId,
|
|
760
|
+
// Which of the host's own ids that referent is — without it a handler
|
|
761
|
+
// gets an opaque anchor and nothing it can look up.
|
|
762
|
+
...( inv.targetAddresses?.length ? { targetAddresses: inv.targetAddresses } : {} ),
|
|
635
763
|
...( inv.description ? { description: inv.description } : {} ),
|
|
636
764
|
} )
|
|
637
765
|
const result = typeof raw === 'string' ? { success: true, description: raw } : raw
|
|
638
|
-
this.stem.confirmEffectorExecution( this.id, inv.
|
|
766
|
+
this.stem.confirmEffectorExecution( this.id, inv.intentId, result )
|
|
639
767
|
}
|
|
640
768
|
catch( err ){
|
|
641
769
|
this._emitError( err instanceof Error ? err : new Error( String( err ) ) )
|
|
642
|
-
this.stem.confirmEffectorExecution( this.id, inv.
|
|
770
|
+
this.stem.confirmEffectorExecution( this.id, inv.intentId, {
|
|
643
771
|
success: false, description: `Effector "${inv.effectorName}" threw: ${( err as Error ).message}`,
|
|
644
772
|
} )
|
|
645
773
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// src/serve/server.ts — a Will, exposed over plain HTTP (the sidecar)
|
|
2
|
+
// src/surface/serve/server.ts — a Will, exposed over plain HTTP (the sidecar)
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// `will serve` for hosts that aren't Node and aren't MCP clients — a Python
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
24
24
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
25
25
|
import { dirname } from 'node:path'
|
|
26
|
-
import type { Will } from '#sdk/will'
|
|
27
|
-
import {
|
|
26
|
+
import type { Will } from '#surface/sdk/will'
|
|
27
|
+
import { asProvenance } from '#senses/provenance'
|
|
28
|
+
import { UtteranceTap } from '#surface/host/utterances'
|
|
28
29
|
|
|
29
30
|
export interface WillHttpOptions {
|
|
30
31
|
/** Where POST /save writes the PMA artifact. */
|
|
@@ -98,10 +99,16 @@ export function buildWillHttpServer( will: Will, opts: WillHttpOptions = {} ): S
|
|
|
98
99
|
const body = await readJsonBody( req )
|
|
99
100
|
const text = typeof body.text === 'string' ? body.text : ''
|
|
100
101
|
if( !text ) return json( res, 400, { error: 'text is required' } )
|
|
101
|
-
await will.
|
|
102
|
+
await will.sense( {
|
|
102
103
|
text,
|
|
104
|
+
// Untyped ingress — a JSON body cannot be type-checked, and a client
|
|
105
|
+
// that predates the field has not claimed anything. asProvenance()
|
|
106
|
+
// owns the direction; see its comment for why 'exafferent' and not
|
|
107
|
+
// the tidier-looking 'unknown'.
|
|
108
|
+
provenance: asProvenance( body.provenance ),
|
|
103
109
|
...( typeof body.from === 'string' ? { from: body.from } : {} ),
|
|
104
110
|
...( typeof body.speaker === 'string' ? { speaker: body.speaker } : {} ),
|
|
111
|
+
...( typeof body.sourceIntentId === 'string' ? { sourceIntentId: body.sourceIntentId } : {} ),
|
|
105
112
|
} )
|
|
106
113
|
// 202: delivered into the sensory field — NOT answered. A response, if
|
|
107
114
|
// any, arrives on /utterances or /next-utterance; silence is valid.
|
package/src/types.ts
CHANGED
|
@@ -22,12 +22,33 @@ export interface effectorInvocation {
|
|
|
22
22
|
id: string
|
|
23
23
|
/** Correlation handle — the awaiting `agency.intent` id. Echo it when POSTing to
|
|
24
24
|
* `POST /v1/wills/:id/effectors/invoked/ack`; the Will reconciles the result onto
|
|
25
|
-
* that intent.
|
|
26
|
-
*
|
|
27
|
-
decisionRecordId
|
|
25
|
+
* that intent.
|
|
26
|
+
*
|
|
27
|
+
* Called `decisionRecordId` until the aliases came out. It stopped being a
|
|
28
|
+
* `decision.record` id at the agency cutover and was kept anyway "for
|
|
29
|
+
* wire-contract stability" — so the wire was stable and wrong, naming a
|
|
30
|
+
* record type that no longer exists, while the SDK translated it to
|
|
31
|
+
* `intentId` one hop later for the handler. Stability that preserves a false
|
|
32
|
+
* name is preserving the wrong thing. */
|
|
33
|
+
intentId: string
|
|
28
34
|
effectorName: string
|
|
29
35
|
parameters: Record<string, unknown>
|
|
30
36
|
targetEntityId: string | undefined
|
|
37
|
+
/**
|
|
38
|
+
* The addresses the world knows `targetEntityId` by.
|
|
39
|
+
*
|
|
40
|
+
* `targetEntityId` is an ANCHOR (`ke:1sqlkux`) — since 0.9.0 identity is opaque
|
|
41
|
+
* and deliberately not an address, because who someone is and where to find them
|
|
42
|
+
* are different facts. A host holds channel ids and knows nothing of anchors, so
|
|
43
|
+
* an invocation naming only the referent is one no surface can act on. Resolved
|
|
44
|
+
* inside, where the alias table lives, exactly as the outbox resolves a message
|
|
45
|
+
* addressed to a referent.
|
|
46
|
+
*
|
|
47
|
+
* Outbound only: this is the mind saying which of its own handles it means.
|
|
48
|
+
* Nothing here lets a surface name someone TO the mind — that is what perception
|
|
49
|
+
* is for.
|
|
50
|
+
*/
|
|
51
|
+
targetAddresses?: readonly string[]
|
|
31
52
|
reasoning: string
|
|
32
53
|
/** The ability's declared meaning (from its EffectorDeclaration), when present. */
|
|
33
54
|
description?: string
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/channels/roster.ts","../../src/channels/types.ts","../../src/channels/discord.ts"],"names":[],"mappings":"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","file":"discord.js","sourcesContent":["// ─────────────────────────────────────────────────────────────\n// src/channels/roster.ts — who the Will knows on a platform, and where\n// ─────────────────────────────────────────────────────────────\n//\n// The mind knows *entities*; a platform knows user ids and channels. The roster\n// is the durable seam between them: for each entity the Will has met on a\n// channel it records how to reach them again — so a *proactive* utterance\n// (`message.to` from the mind's own initiative) can find its person after a\n// restart, not just within one session.\n//\n// It persists as a small JSON file next to the PMA artifact. Writes are\n// throttled (the file is advisory routing state, not cognition — losing the\n// last few seconds costs a fallback delivery, never memory).\n// ─────────────────────────────────────────────────────────────\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/** How to reach one entity on the platform. */\nexport interface RosterEntry {\n /** The mind-side entity id, e.g. 'discord:80351110224678912'. */\n entityId: string\n /** The platform-side user id. */\n userId: string\n /** Last display name seen (advisory — the *learned* name lives in the mind). */\n displayName?: string\n /** DM channel id, once one is known. */\n dmChannelId?: string\n /** Last shared (guild) channel this entity spoke in. */\n lastChannelId?: string\n /** Epoch ms of the last message seen from them. */\n lastSeenAt: number\n}\n\nconst FLUSH_MS = 2_000\n\nexport class ChannelRoster {\n private entries = new Map<string, RosterEntry>()\n private dirty = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor( private readonly path: string ) {\n if( existsSync( path ) ){\n try {\n const raw = JSON.parse( readFileSync( path, 'utf8') ) as RosterEntry[]\n for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )\n }\n catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }\n }\n }\n\n /** Upsert what we just learned about an entity; schedules a throttled flush. */\n record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {\n const prev = this.entries.get( update.entityId )\n const next: RosterEntry = {\n lastSeenAt: Date.now(),\n ...prev,\n ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,\n }\n this.entries.set( next.entityId, next )\n this.dirty = true\n if( !this.timer ){\n this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )\n this.timer.unref?.()\n }\n return next\n }\n\n resolve( entityId: string ): RosterEntry | undefined {\n return this.entries.get( entityId )\n }\n\n all(): RosterEntry[] {\n return [ ...this.entries.values() ]\n }\n\n /** Write to disk now (no-op when clean). Called by bridges on close. */\n flush(): void {\n if( !this.dirty ) return\n try {\n mkdirSync( dirname( this.path ), { recursive: true } )\n writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )\n this.dirty = false\n }\n catch { /* advisory state — never take the mind down over it */ }\n }\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/types.ts — the channel-bridge contract\n// ─────────────────────────────────────────────────────────────\n//\n// A channel bridge puts a Will *in a place where people already are* (Discord,\n// Telegram, Slack, …). It is a host surface, not a cognition surface: it turns\n// platform messages into `perceive` stimuli and delivers the Will's projected\n// utterances back — nothing more. The paradigm survives the crossing:\n//\n// • every platform user is an entity the Will comes to know (`from`),\n// with a *learned* name (`speaker`) — never a placeholder;\n// • every platform channel/DM is a conversation thread (`thread`);\n// • the Will decides when to speak. Silence is a valid outcome, so a\n// bridge never fabricates a reply and never times a message out into\n// an error.\n//\n// Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):\n// they wrap the SDK facade, not the stem.\n// ─────────────────────────────────────────────────────────────\n\n/** A running connection between one Will and one platform. */\nexport interface ChannelBridge {\n /** Platform kind, e.g. 'discord'. */\n readonly kind: string\n /** Connect and start relaying. Resolves once the bridge is live. */\n start(): Promise<void>\n /** Disconnect and release resources. Idempotent. */\n close(): Promise<void>\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────────\n//\n// People hand over documents as well as speech, and some platforms *manufacture*\n// them: Discord silently turns a long pasted markdown block into a `.md` upload.\n// A bridge that reads only the text body sees such a message as empty and — worse\n// — as nothing at all, so the person appears to have gone silent.\n//\n// What a bridge does with these is deliberately modest. A named-but-unread file\n// is already a percept the Will can act on (\"what's in it?\"), which is the\n// paradigm-correct outcome and strictly better than silence. Inlining text is an\n// upgrade on top, never a precondition.\n\n/** One file riding along with a platform message. */\nexport interface ChannelAttachment {\n name: string\n contentType?: string\n size?: number\n url?: string\n}\n\n/** Per-attachment inline budget. A 2 MB doc must not enter working memory whole. */\nconst INLINE_CHAR_CAP = 24_000\n/** How many text attachments to inline from one message. */\nconst INLINE_COUNT_CAP = 4\n\nconst TEXTUAL_EXT = /\\.(md|markdown|txt|text|json|jsonl|csv|tsv|ya?ml|log|ini|toml)$/i\n\n/** Is this something we can meaningfully read as text? */\nexport function isTextual( a: ChannelAttachment ): boolean {\n const ct = a.contentType?.split(';')[0]?.trim().toLowerCase() ?? ''\n if( ct.startsWith('text/') ) return true\n if( ct === 'application/json' || ct === 'application/x-yaml' ) return true\n // Discord's own markdown uploads arrive as text/plain, but trust the extension\n // too — content types from platforms are advisory at best.\n return TEXTUAL_EXT.test( a.name )\n}\n\nfunction humanSize( bytes?: number ): string {\n if( bytes == null ) return ''\n return bytes < 1024 ? `${ bytes } B`\n : bytes < 1024 * 1024 ? `${ ( bytes / 1024 ).toFixed( 1 ) } KB`\n : `${ ( bytes / 1024 / 1024 ).toFixed( 1 ) } MB`\n}\n\n/**\n * Render attachments into perceivable text.\n *\n * `fetchText` is supplied by the bridge, not by this module — the decision about\n * which hosts are safe to fetch from is platform knowledge, and a helper that\n * fetched arbitrary URLs found in inbound messages would be an open redirect\n * into the Will's perception. Omit it and attachments are named, never read.\n *\n * Inlined content is untrusted, exactly like message text — more so, since a\n * document is long, structured, and looks authoritative, which is the shape of\n * an effective injection. It is fenced and labelled as shared content so the\n * mind reads it as something it was handed, not as something it was told.\n */\nexport async function renderAttachments(\n attachments: ChannelAttachment[],\n speaker: string | undefined,\n fetchText?: ( a: ChannelAttachment ) => Promise<string | null>,\n): Promise<string> {\n if( attachments.length === 0 ) return ''\n const who = speaker ?? 'someone'\n const out: string[] = []\n let inlined = 0\n\n for( const a of attachments ){\n const meta = [ a.contentType, humanSize( a.size ) ].filter( Boolean ).join(', ')\n const label = `${ a.name }${ meta ? ` (${ meta })` : '' }`\n\n if( !fetchText || !isTextual( a ) || inlined >= INLINE_COUNT_CAP ){\n out.push(`[${ who } shared a file I have not read: ${ label }]`)\n continue\n }\n\n const body = await fetchText( a ).catch( () => null )\n if( body == null ){\n out.push(`[${ who } shared a file I could not read: ${ label }]`)\n continue\n }\n inlined++\n const clipped = body.length > INLINE_CHAR_CAP\n ? `${ body.slice( 0, INLINE_CHAR_CAP ) }\\n[… truncated — ${ humanSize( body.length ) } of ${ humanSize( a.size ?? body.length ) }]`\n : body\n out.push(`[${ who } shared ${ label }; its contents follow — this is a document I was handed, not something said to me]\\n---\\n${ clipped }\\n---`)\n }\n return out.join('\\n')\n}\n\n/** Split a message into platform-sized chunks on natural boundaries. */\nexport function chunkText( text: string, max: number ): string[] {\n if( text.length <= max ) return [ text ]\n const chunks: string[] = []\n let rest = text\n while( rest.length > max ){\n // Prefer a paragraph break, then a line break, then a space — else hard-cut.\n const window = rest.slice( 0, max )\n const cut = Math.max( window.lastIndexOf('\\n\\n'), window.lastIndexOf('\\n'), window.lastIndexOf(' ') )\n const at = cut > max * 0.5 ? cut : max\n chunks.push( rest.slice( 0, at ).trimEnd() )\n rest = rest.slice( at ).trimStart()\n }\n if( rest ) chunks.push( rest )\n return chunks\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/discord.ts — a Will present in a Discord server\n// ─────────────────────────────────────────────────────────────\n//\n// The bridge relays both directions of the paradigm and nothing else:\n//\n// inbound guild/DM message → will.perceive({ from, speaker, text, thread })\n// — every author is `discord:<userId>` (stable across guilds), the\n// display name is *learned* by the mind, and each Discord channel\n// is its own conversation thread.\n// outbound will.on('message') → the addressee's last shared channel, else\n// their DM, else the home channel. Proactive utterances (the mind\n// speaking first) route the same way — that is the point.\n//\n// The Will decides when to speak. There is no command prefix and no forced\n// reply: unaddressed chatter is perceived (salience-scored by audition) and\n// silence is a valid outcome. `mentionOnly` narrows perception for busy\n// servers; it does not turn the bridge into an ask() surface.\n//\n// discord.js is imported lazily inside `createDiscordClient` — tests (and any\n// host that brings its own client) inject `client`, and the structural\n// `DiscordLikeClient` type keeps the dependency out of the type graph.\n// ─────────────────────────────────────────────────────────────\n\nimport type { Will, WillMessage } from '#sdk/will'\nimport { ChannelRoster } from '#channels/roster'\nimport { chunkText, renderAttachments, isTextual, type ChannelBridge, type ChannelAttachment } from '#channels/types'\n\nconst DISCORD_MESSAGE_LIMIT = 2000\n\n/**\n * The only hosts the bridge will fetch attachment bodies from.\n *\n * Deliberately an allowlist of Discord's own CDN. An inbound message is\n * untrusted input; following URLs it names would turn perception into an open\n * redirect, and a `url` field is no more trustworthy than the message text.\n */\nconst DISCORD_CDN_HOSTS = new Set( [ 'cdn.discordapp.com', 'media.discordapp.net' ] )\n\n/** Refuse to pull a large file into a percept — the cap in renderAttachments\n * bounds what is *kept*, this bounds what is fetched at all. */\nconst MAX_FETCH_BYTES = 256 * 1024\n\n// ── The slice of discord.js the bridge actually uses (structural) ───────────\n\nexport interface DiscordLikeChannel {\n send( content: string ): Promise<unknown>\n sendTyping?(): Promise<unknown>\n}\n\nexport interface DiscordLikeAttachment {\n name?: string | null\n contentType?: string | null\n size?: number\n url?: string\n}\n\nexport interface DiscordLikeMessage {\n content: string\n cleanContent?: string\n channelId: string\n guildId?: string | null\n author: { id: string; bot?: boolean; username?: string; displayName?: string }\n member?: { displayName?: string } | null\n mentions?: { has( userId: string ): boolean }\n channel: DiscordLikeChannel\n /**\n * Files riding with the message.\n *\n * discord.js hands us a `Collection`, which extends `Map` — so iterating it\n * directly yields `[id, attachment]` PAIRS, not attachments. Typing this as a\n * bare `Iterable` was wrong and silently produced `name: undefined` against\n * the real client while passing every test, because the test fake injects an\n * array. Both shapes are accepted now and normalised in `collectAttachments`.\n */\n attachments?: ReadonlyMap<string, DiscordLikeAttachment> | Iterable<DiscordLikeAttachment>\n}\n\nexport interface DiscordLikeClient {\n user: { id: string; setPresence?( p: unknown ): void } | null\n /** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */\n isReady?(): boolean\n on( event: 'messageCreate', fn: ( m: DiscordLikeMessage ) => void ): unknown\n once( event: string, fn: () => void ): unknown\n login( token: string ): Promise<unknown>\n destroy(): Promise<unknown> | void\n channels: { fetch( id: string ): Promise<unknown> }\n users: { fetch( id: string ): Promise<{ send( content: string ): Promise<unknown> }> }\n}\n\n// ── Options ──────────────────────────────────────────────────────────────────\n\nexport interface DiscordBridgeOptions {\n /** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */\n token?: string\n /**\n * Channel ids the Will inhabits. Unset — or the single wildcard `'*'` — means\n * every channel it can see, so adding it to a new channel in Discord is enough.\n * A list restrains it to exactly those, and a message anywhere else is dropped\n * at the bridge: the Will never perceives it and its silence there is not a choice.\n */\n channels?: readonly string[]\n /**\n * Where the Will only perceives guild messages that @mention it. DMs are always\n * perceived either way.\n *\n * `true` — everywhere\n * `[ 'id', … ]` — only in those channels; it listens openly elsewhere\n * omitted / `false` — nowhere\n *\n * The list form is what makes a wide-open roster usable: present in every channel,\n * but a quiet participant in the busy ones.\n */\n mentionOnly?: boolean | readonly string[]\n /** Fallback channel for utterances with no reachable addressee. */\n homeChannelId?: string\n /** Roster path (default: ./.will/<willId>.discord.json). */\n rosterPath?: string\n /**\n * Read the contents of text-like attachments (.md, .txt, .json, …) into the\n * percept, rather than only naming them. Default true.\n *\n * Only Discord's own CDN is ever fetched, and only up to a size cap. Set false\n * for a bridge that should never pull remote bytes — the Will still perceives\n * that a file arrived and can ask about it.\n */\n readAttachments?: boolean\n /** Test / power-user seam: bring your own client; discord.js is never imported. */\n client?: DiscordLikeClient\n log?: ( msg: string ) => void\n}\n\n// ── The bridge ───────────────────────────────────────────────────────────────\n\n/**\n * Connect a Will to Discord. Resolves once the bridge is live (logged in and\n * relaying). Close it via the returned `ChannelBridge.close()` — the Will\n * itself is not stopped; it simply loses this surface.\n */\nexport async function connectDiscord( will: Will, opts: DiscordBridgeOptions ): Promise<ChannelBridge> {\n const log = opts.log ?? ( ( m: string ) => console.error(`[will:discord] ${ m }`) )\n const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.discord.json`)\n // `null` = everywhere. An explicit '*' reads the same as omitting the list, so a\n // host can say \"all channels\" out loud instead of by leaving a variable blank.\n const allowed = opts.channels?.length && !opts.channels.includes('*')\n ? new Set( opts.channels )\n : null\n\n // Mention-gating is either global (true) or scoped to named channels.\n const mentionEverywhere = opts.mentionOnly === true\n const mentionIn = Array.isArray( opts.mentionOnly ) && opts.mentionOnly.length\n ? new Set( opts.mentionOnly )\n : null\n\n const client = opts.client ?? await createDiscordClient()\n\n /** The most recently active allowed channel — last-resort proactive target. */\n let lastActiveChannelId: string | null = opts.homeChannelId ?? null\n\n // ── inbound: platform message → stimulus ──────────────────────────────────\n client.on('messageCreate', message => { void onMessage( message ) } )\n\n async function onMessage( message: DiscordLikeMessage ): Promise<void> {\n const self = client.user\n if( !self || message.author.id === self.id || message.author.bot ) return\n\n const isDM = !message.guildId\n if( !isDM && allowed && !allowed.has( message.channelId ) ) return\n\n const addressed = isDM || ( message.mentions?.has( self.id ) ?? false )\n // DMs are addressed by definition, so this never gates them.\n if( !addressed && ( mentionEverywhere || mentionIn?.has( message.channelId ) ) ) return\n\n const entityId = `discord:${ message.author.id }`\n const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username\n\n roster.record( {\n entityId,\n userId: message.author.id,\n ...( speaker ? { displayName: speaker } : {} ),\n ...( isDM ? { dmChannelId: message.channelId } : { lastChannelId: message.channelId } ),\n } )\n if( !isDM ) lastActiveChannelId = message.channelId\n\n // Being addressed is the one moment a presence cue is honest — the mind\n // may still choose silence, and typing expires on its own.\n if( addressed ) await message.channel.sendTyping?.().catch( () => {} )\n\n const said = ( message.cleanContent || message.content ).trim()\n const files = collectAttachments( message )\n\n // An attachment-only message used to die here on the empty body: no percept,\n // no log line, nothing. From the mind's side the person had simply gone\n // quiet — and Discord *makes* these, turning a long pasted markdown block\n // into a .md upload. Only a message with neither words nor files is nothing.\n if( !said && files.length === 0 ) return\n\n const shared = await renderAttachments(\n files, speaker,\n opts.readAttachments === false ? undefined : fetchAttachmentText,\n )\n const text = [ said, shared ].filter( Boolean ).join('\\n')\n\n await will.perceive( {\n text,\n from: entityId,\n thread: `discord:${ message.channelId }`,\n // `isDM` has been computed on every inbound since this bridge shipped and\n // used only to pick a roster field. It is the one fact that makes a room\n // the right or wrong place to say something, and the mind never saw it —\n // which is how a follow-up promised in a DM went out to #general.\n direct: isDM,\n ...( speaker ? { speaker } : {} ),\n } )\n }\n\n /**\n * Normalise whatever the client gave us into attachments.\n *\n * `.values()` first: a discord.js Collection is a Map, so `for..of` over it\n * yields `[id, attachment]` pairs and every field reads `undefined`. Arrays\n * expose `.values()` too and yield their elements, so one branch covers the\n * real client, a plain array, and a Map alike.\n */\n function collectAttachments( message: DiscordLikeMessage ): ChannelAttachment[] {\n if( !message.attachments ) return []\n const source = message.attachments as { values?: () => Iterable<DiscordLikeAttachment> }\n const items: Iterable<DiscordLikeAttachment> = typeof source.values === 'function'\n ? source.values()\n : message.attachments as Iterable<DiscordLikeAttachment>\n\n const out: ChannelAttachment[] = []\n for( const a of items )\n out.push( {\n name: a.name ?? 'unnamed',\n ...( a.contentType ? { contentType: a.contentType } : {} ),\n ...( a.size != null ? { size: a.size } : {} ),\n ...( a.url ? { url: a.url } : {} ),\n } )\n return out\n }\n\n /** Fetch one text attachment — Discord CDN only, size-capped. */\n async function fetchAttachmentText( a: ChannelAttachment ): Promise<string | null> {\n if( !a.url || !isTextual( a ) ) return null\n let host: string\n try { host = new URL( a.url ).hostname }\n catch { return null }\n if( !DISCORD_CDN_HOSTS.has( host ) ){\n log(`refusing to fetch attachment '${ a.name }' from non-CDN host ${ host }`)\n return null\n }\n if( a.size != null && a.size > MAX_FETCH_BYTES ){\n log(`attachment '${ a.name }' is ${ a.size } bytes — naming it without reading`)\n return null\n }\n const res = await fetch( a.url, { signal: AbortSignal.timeout( 10_000 ) } )\n if( !res.ok ){\n log(`attachment '${ a.name }' fetch failed: ${ res.status }`)\n return null\n }\n return ( await res.text() ).slice( 0, MAX_FETCH_BYTES )\n }\n\n // ── outbound: projected utterance → the addressee ─────────────────────────\n // The facade has no off(); the bridge gates its handler on `closed` instead.\n let closed = false\n will.on('message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )\n\n async function deliver( m: WillMessage ): Promise<void> {\n const peer = m.to ? roster.resolve( m.to ) : undefined\n const chunks = chunkText( m.content, DISCORD_MESSAGE_LIMIT )\n\n // A reply goes back to the room it was said in. `m.thread` is the thread from\n // the `perceive()` that prompted this — `discord:<channelId>` — so it is not a\n // guess about where this person usually is, it is where they just spoke.\n //\n // Everything below it IS a guess, and the guesses were wrong in the way that\n // matters most: a DM arrived, she answered it in seconds, and the answer went\n // to the shared server channel because `lastChannelId` still held the last\n // room they had been in together. She looked like she was ignoring him.\n //\n // Unprompted utterances carry no thread — nothing was said to them — so those\n // still fall through to the roster, which is the right behaviour there.\n const replyTo = m.thread?.startsWith('discord:') ? m.thread.slice('discord:'.length ) : undefined\n\n // Preference order: the room they spoke in → where we last shared a room →\n // their DM → home channel.\n const channelIds = [ replyTo, peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? undefined, lastActiveChannelId ?? undefined ]\n for( const id of channelIds ){\n if( !id ) continue\n try {\n const channel = await client.channels.fetch( id ) as DiscordLikeChannel | null\n if( !channel?.send ) continue\n for( const chunk of chunks ) await channel.send( chunk )\n return\n }\n catch { /* try the next route */ }\n }\n if( peer ){\n try {\n const user = await client.users.fetch( peer.userId )\n for( const chunk of chunks ) await user.send( chunk )\n return\n }\n catch { /* fall through */ }\n }\n log(`no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)`)\n }\n\n // ── lifecycle ──────────────────────────────────────────────────────────────\n const bridge: ChannelBridge = {\n kind: 'discord',\n async start(): Promise<void> {\n if( !client.user ){\n // discord.js ≥14.22 renamed `ready` → `clientReady`. Subscribing to the\n // old name is what triggers its DeprecationWarning, so we take the new\n // name and poll `isReady()` for older builds rather than listening.\n const ready = new Promise<void>( resolve => {\n let poll: ReturnType<typeof setInterval> | null = null\n const done = (): void => { if( poll ) clearInterval( poll ); resolve() }\n client.once('clientReady', done )\n poll = setInterval( () => { if( client.isReady?.() ) done() }, 100 )\n poll.unref?.()\n } )\n await client.login( opts.token ?? '')\n await ready\n }\n log(`${ will.name } is present on Discord as user ${ client.user?.id }`)\n },\n async close(): Promise<void> {\n if( closed ) return\n closed = true\n roster.flush()\n await Promise.resolve( client.destroy() ).catch( () => {} )\n },\n }\n return bridge\n}\n\n/** Build a real discord.js client (lazy import keeps it out of non-Discord hosts). */\nasync function createDiscordClient(): Promise<DiscordLikeClient> {\n let mod: typeof import('discord.js')\n try { mod = await import('discord.js') }\n catch {\n throw new Error('discord.js is not installed (it is an optionalDependency) — run `bun add discord.js` / `npm i discord.js` and retry.')\n }\n const { Client, GatewayIntentBits, Partials } = mod\n return new Client( {\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [ Partials.Channel ], // DMs arrive on uncached channels\n } ) as unknown as DiscordLikeClient\n}\n\n/**\n * Parse `WILL_DISCORD_MENTION_ONLY` into the `mentionOnly` option.\n *\n * Accepts a boolean OR a channel list, because \"only speak when spoken to\" is\n * rarely a whole-server property — it is how you stay present in a busy channel\n * without narrating in it.\n *\n * `1` / `true` / `yes` → true (everywhere)\n * `0` / `false` / unset → false (nowhere)\n * `123,456` → only those channels\n *\n * Exported so every host parses it identically; the CLI and any SDK host share\n * this rather than each re-deriving the syntax.\n */\nexport function parseMentionOnly( raw?: string ): boolean | string[] {\n const v = raw?.trim()\n if( !v ) return false\n if( /^(1|true|yes)$/i.test( v ) ) return true\n if( /^(0|false|no)$/i.test( v ) ) return false\n\n const ids = v.split(',').map( s => s.trim() ).filter( Boolean )\n return ids.length ? ids : false\n}\n\n/**\n * Parse `WILL_DISCORD_CHANNELS`. `*` (or unset/empty) means every channel the Will\n * can see — being added to a channel in Discord is then all it takes. Anything else\n * restrains it to exactly the ids listed.\n */\nexport function parseChannels( raw?: string ): string[] | undefined {\n const ids = raw?.split(',').map( s => s.trim() ).filter( Boolean )\n return ids?.length ? ids : undefined\n}\n"]}
|