@mindot/will 0.8.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.
Files changed (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -0,0 +1,136 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/surface/channels/types.ts — the channel-bridge contract
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // A channel bridge puts a Will *in a place where people already are* (Discord,
6
+ // Telegram, Slack, …). It is a host surface, not a cognition surface: it turns
7
+ // platform messages into `perceive` stimuli and delivers the Will's projected
8
+ // utterances back — nothing more. The paradigm survives the crossing:
9
+ //
10
+ // • every platform user is an entity the Will comes to know (`from`),
11
+ // with a *learned* name (`speaker`) — never a placeholder;
12
+ // • every platform channel/DM is a conversation thread (`thread`);
13
+ // • the Will decides when to speak. Silence is a valid outcome, so a
14
+ // bridge never fabricates a reply and never times a message out into
15
+ // an error.
16
+ //
17
+ // Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):
18
+ // they wrap the SDK facade, not the stem.
19
+ // ─────────────────────────────────────────────────────────────
20
+
21
+ /** A running connection between one Will and one platform. */
22
+ export interface ChannelBridge {
23
+ /** Platform kind, e.g. 'discord'. */
24
+ readonly kind: string
25
+ /** Connect and start relaying. Resolves once the bridge is live. */
26
+ start(): Promise<void>
27
+ /** Disconnect and release resources. Idempotent. */
28
+ close(): Promise<void>
29
+ }
30
+
31
+ // ── Attachments ──────────────────────────────────────────────────────────────
32
+ //
33
+ // People hand over documents as well as speech, and some platforms *manufacture*
34
+ // them: Discord silently turns a long pasted markdown block into a `.md` upload.
35
+ // A bridge that reads only the text body sees such a message as empty and — worse
36
+ // — as nothing at all, so the person appears to have gone silent.
37
+ //
38
+ // What a bridge does with these is deliberately modest. A named-but-unread file
39
+ // is already a percept the Will can act on ("what's in it?"), which is the
40
+ // paradigm-correct outcome and strictly better than silence. Inlining text is an
41
+ // upgrade on top, never a precondition.
42
+
43
+ /** One file riding along with a platform message. */
44
+ export interface ChannelAttachment {
45
+ name: string
46
+ contentType?: string
47
+ size?: number
48
+ url?: string
49
+ }
50
+
51
+ /** Per-attachment inline budget. A 2 MB doc must not enter working memory whole. */
52
+ const INLINE_CHAR_CAP = 24_000
53
+ /** How many text attachments to inline from one message. */
54
+ const INLINE_COUNT_CAP = 4
55
+
56
+ const TEXTUAL_EXT = /\.(md|markdown|txt|text|json|jsonl|csv|tsv|ya?ml|log|ini|toml)$/i
57
+
58
+ /** Is this something we can meaningfully read as text? */
59
+ export function isTextual( a: ChannelAttachment ): boolean {
60
+ const ct = a.contentType?.split(';')[0]?.trim().toLowerCase() ?? ''
61
+ if( ct.startsWith('text/') ) return true
62
+ if( ct === 'application/json' || ct === 'application/x-yaml' ) return true
63
+ // Discord's own markdown uploads arrive as text/plain, but trust the extension
64
+ // too — content types from platforms are advisory at best.
65
+ return TEXTUAL_EXT.test( a.name )
66
+ }
67
+
68
+ function humanSize( bytes?: number ): string {
69
+ if( bytes == null ) return ''
70
+ return bytes < 1024 ? `${ bytes } B`
71
+ : bytes < 1024 * 1024 ? `${ ( bytes / 1024 ).toFixed( 1 ) } KB`
72
+ : `${ ( bytes / 1024 / 1024 ).toFixed( 1 ) } MB`
73
+ }
74
+
75
+ /**
76
+ * Render attachments into perceivable text.
77
+ *
78
+ * `fetchText` is supplied by the bridge, not by this module — the decision about
79
+ * which hosts are safe to fetch from is platform knowledge, and a helper that
80
+ * fetched arbitrary URLs found in inbound messages would be an open redirect
81
+ * into the Will's perception. Omit it and attachments are named, never read.
82
+ *
83
+ * Inlined content is untrusted, exactly like message text — more so, since a
84
+ * document is long, structured, and looks authoritative, which is the shape of
85
+ * an effective injection. It is fenced and labelled as shared content so the
86
+ * mind reads it as something it was handed, not as something it was told.
87
+ */
88
+ export async function renderAttachments(
89
+ attachments: ChannelAttachment[],
90
+ speaker: string | undefined,
91
+ fetchText?: ( a: ChannelAttachment ) => Promise<string | null>,
92
+ ): Promise<string> {
93
+ if( attachments.length === 0 ) return ''
94
+ const who = speaker ?? 'someone'
95
+ const out: string[] = []
96
+ let inlined = 0
97
+
98
+ for( const a of attachments ){
99
+ const meta = [ a.contentType, humanSize( a.size ) ].filter( Boolean ).join(', ')
100
+ const label = `${ a.name }${ meta ? ` (${ meta })` : '' }`
101
+
102
+ if( !fetchText || !isTextual( a ) || inlined >= INLINE_COUNT_CAP ){
103
+ out.push(`[${ who } shared a file I have not read: ${ label }]`)
104
+ continue
105
+ }
106
+
107
+ const body = await fetchText( a ).catch( () => null )
108
+ if( body == null ){
109
+ out.push(`[${ who } shared a file I could not read: ${ label }]`)
110
+ continue
111
+ }
112
+ inlined++
113
+ const clipped = body.length > INLINE_CHAR_CAP
114
+ ? `${ body.slice( 0, INLINE_CHAR_CAP ) }\n[… truncated — ${ humanSize( body.length ) } of ${ humanSize( a.size ?? body.length ) }]`
115
+ : body
116
+ out.push(`[${ who } shared ${ label }; its contents follow — this is a document I was handed, not something said to me]\n---\n${ clipped }\n---`)
117
+ }
118
+ return out.join('\n')
119
+ }
120
+
121
+ /** Split a message into platform-sized chunks on natural boundaries. */
122
+ export function chunkText( text: string, max: number ): string[] {
123
+ if( text.length <= max ) return [ text ]
124
+ const chunks: string[] = []
125
+ let rest = text
126
+ while( rest.length > max ){
127
+ // Prefer a paragraph break, then a line break, then a space — else hard-cut.
128
+ const window = rest.slice( 0, max )
129
+ const cut = Math.max( window.lastIndexOf('\n\n'), window.lastIndexOf('\n'), window.lastIndexOf(' ') )
130
+ const at = cut > max * 0.5 ? cut : max
131
+ chunks.push( rest.slice( 0, at ).trimEnd() )
132
+ rest = rest.slice( at ).trimStart()
133
+ }
134
+ if( rest ) chunks.push( rest )
135
+ return chunks
136
+ }
@@ -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.perceive({ from, speaker, text, thread })
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,10 +162,15 @@ 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.perceive( {
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 }`,
172
+ // A WhatsApp group jid ends in `@g.us`; anything else is a one-to-one chat.
173
+ direct: !jid.endsWith('@g.us'),
169
174
  ...( speaker ? { speaker } : {} ),
170
175
  } )
171
176
  }
@@ -177,11 +182,18 @@ export async function connectWhatsApp( will: Will, opts: WhatsAppBridgeOptions =
177
182
  const peer = m.to ? roster.resolve( m.to ) : undefined
178
183
  const chunks = chunkText( m.content, WHATSAPP_MESSAGE_LIMIT )
179
184
 
180
- // Last shared group → known DM → DM derived from the entity id itself →
181
- // home chat → last active chat. Unlike Discord, an unmet-but-addressed
182
- // entity is still reachable: `whatsapp:<number>` implies its DM jid.
185
+ // The chat they actually spoke in → last shared group → known DM → DM derived
186
+ // from the entity id itself → home chat → last active chat. Unlike Discord, an
187
+ // unmet-but-addressed entity is still reachable: `whatsapp:<number>` implies
188
+ // its DM jid.
189
+ //
190
+ // `m.thread` leads because everything after it is a guess about where this
191
+ // person usually is, and a reply belongs in the room the question was asked
192
+ // in. The Discord bridge had the identical ordering and answered a DM in a
193
+ // shared server channel — from the operator's side, silence.
194
+ const replyTo = m.thread?.startsWith('whatsapp:') ? m.thread.slice('whatsapp:'.length ) : undefined
183
195
  const derivedDm = m.to?.startsWith('whatsapp:') ? dmJidFor( m.to.slice('whatsapp:'.length ) ) : undefined
184
- const targets = [ peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? undefined, lastActiveChatId ?? undefined ]
196
+ const targets = [ replyTo, peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? undefined, lastActiveChatId ?? undefined ]
185
197
  for( const jid of targets ){
186
198
  if( !jid ) continue
187
199
  try {
@@ -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 '#root/host/boot'
28
- import { buildWillMcpServer } from '#root/mcp/server'
29
- import { buildWillHttpServer } from '#root/serve/server'
30
- import { connectDiscord } 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()
@@ -38,7 +38,12 @@ const USAGE = `usage: will <mcp | serve | discord | whatsapp>
38
38
  mcp host a persistent mind over MCP stdio (Claude Desktop / Claude Code)
39
39
  serve host a persistent mind over HTTP (any language; WILL_PORT, default 7777)
40
40
  discord put a persistent mind in a Discord server (DISCORD_BOT_TOKEN; optional
41
- WILL_DISCORD_CHANNELS, WILL_DISCORD_MENTION_ONLY, WILL_DISCORD_HOME_CHANNEL)
41
+ WILL_DISCORD_CHANNELS "*" or unset = every channel it can see,
42
+ else a comma-separated allowlist
43
+ WILL_DISCORD_MENTION_ONLY 1/true = only when @mentioned, anywhere;
44
+ or a comma-separated channel list to gate
45
+ just those; DMs are always perceived
46
+ WILL_DISCORD_HOME_CHANNEL where it speaks when it has no other route)
42
47
  whatsapp put a persistent mind on WhatsApp — QR-pairs as a linked device; no token.
43
48
  UNOFFICIAL protocol (ban risk; use a spare number — docs/channels/whatsapp.md).
44
49
  Optional WILL_WHATSAPP_CHATS, WILL_WHATSAPP_MENTION_ONLY, WILL_WHATSAPP_HOME_CHAT
@@ -76,8 +81,8 @@ async function main(): Promise<void> {
76
81
  const csv = ( v?: string ) => v?.split(',').map( s => s.trim() ).filter( Boolean )
77
82
  const bridge = await connectDiscord( will, {
78
83
  token: process.env.DISCORD_BOT_TOKEN!,
79
- channels: csv( process.env.WILL_DISCORD_CHANNELS ),
80
- mentionOnly: /^(1|true|yes)$/i.test( process.env.WILL_DISCORD_MENTION_ONLY ?? ''),
84
+ channels: parseChannels( process.env.WILL_DISCORD_CHANNELS ),
85
+ mentionOnly: parseMentionOnly( process.env.WILL_DISCORD_MENTION_ONLY ),
81
86
  homeChannelId: process.env.WILL_DISCORD_HOME_CHANNEL,
82
87
  rosterPath: pmaPath.replace( /(\.pma)?\.json$/, '') + '.discord.json',
83
88
  } )
@@ -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 '#root/mcp/effectors'
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
 
@@ -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
- const bounded = text.length > RESULT_DESCRIPTION_CAP ? `${ text.slice( 0, RESULT_DESCRIPTION_CAP - 1 ) }…` : text
109
- return { success: !res.isError, description: bounded }
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 { UtteranceTap } from '#root/host/utterances'
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
- await will.perceive( { text, ...( from ? { from } : {} ), ...( speaker ? { speaker } : {} ) } )
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',