@mindot/will 0.3.0 → 0.5.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 +63 -21
- package/dist/channels/discord.d.ts +69 -0
- package/dist/channels/discord.js +193 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +72 -0
- package/dist/channels/whatsapp.js +252 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/cli.js +904 -356
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +141 -141
- package/dist/index.js +441 -342
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/types-E9-HV-SW.d.ts +11 -0
- package/dist/{will-B5eKs3Wv.d.ts → will-BDq-TMQr.d.ts} +4013 -3916
- package/package.json +17 -3
- package/src/channels/discord.ts +214 -0
- package/src/channels/roster.ts +87 -0
- package/src/channels/types.ts +46 -0
- package/src/channels/whatsapp.ts +318 -0
- package/src/cli.ts +57 -9
- package/src/cognition/agency/engines/deliberation.engine.ts +7 -7
- package/src/cognition/agency/execution.primitives.ts +11 -11
- package/src/cognition/agency/proactive.communicator.ts +8 -8
- package/src/cognition/config.mirror.entities.ts +2 -2
- package/src/cognition/conversation.memory.ts +1 -1
- package/src/cognition/faculties/executive.engine/commands.ts +7 -15
- package/src/cognition/faculties/executive.engine/engine.ts +66 -35
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +1 -1
- package/src/cognition/faculties/executive.engine/facet.ts +1 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +76 -61
- package/src/cognition/faculties/executive.engine/types.ts +1 -1
- package/src/cognition/faculties/introspection.engine.ts +1 -2
- package/src/cognition/faculties/planning.engine/engine.ts +38 -1
- package/src/cognition/faculties/planning.engine/plan.store.ts +42 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +7 -7
- package/src/cognition/faculties/theory.of.mind.ts +2 -2
- package/src/cognition/senses/audition.engine/engine.ts +21 -21
- package/src/cognition/utilities/token.tracker.ts +6 -0
- package/src/host/boot.ts +95 -7
- package/src/llm/index.ts +75 -25
- package/src/llm/summarizer.ts +2 -2
- package/src/profiles/companion.ts +14 -14
- package/src/profiles/company-brain.ts +19 -19
- package/src/profiles/customer-service.ts +17 -17
- package/src/profiles/game-npc.ts +10 -10
- package/src/profiles/index.ts +2 -2
- package/src/profiles/smart-home.ts +16 -16
- package/src/runners/outreach.runner.ts +6 -9
- package/src/runners/social.runner.ts +1 -4
- package/src/runners/thin-shim.runner.ts +4 -6
- package/src/sdk/will.ts +42 -16
- package/src/stem/guards/identity.coherence.ts +9 -6
- package/src/stem/guards/identity.guard.ts +20 -9
- package/src/stem/index.ts +7 -7
- package/src/stem/mind.ts +182 -98
- package/src/stem/tracts/outbox.controller.ts +2 -2
|
@@ -94,6 +94,8 @@ export class PlanningEngine implements SimulationEngine, CognitiveEngine {
|
|
|
94
94
|
* replay state) from off-tick callbacks like _activateStep / _onStepOutcome.
|
|
95
95
|
*/
|
|
96
96
|
private _lastTick = 0
|
|
97
|
+
/** One-time deletion of legacy `plan-executive-*` entities (see react step 0a). */
|
|
98
|
+
private _legacyPlanSweepDone = false
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* Monotonic suffix counter for activity-listener subscription ids. These ids
|
|
@@ -205,7 +207,31 @@ export class PlanningEngine implements SimulationEngine, CognitiveEngine {
|
|
|
205
207
|
planId?: string; stepId?: string
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
// Conscious-enaction credit: outcomes carry plan provenance only when
|
|
211
|
+
// the plan's OWN frontier prior won the competition. But the plan is a
|
|
212
|
+
// prior over WHAT to do — if the self does the very thing an active
|
|
213
|
+
// step calls for by any route (executive action via ideomotor, habit),
|
|
214
|
+
// the step is done. Without this, a mind that consciously performs its
|
|
215
|
+
// plan starves the plan of credit: steps stay active, the goal reads
|
|
216
|
+
// blocked, and the executive re-authors the same plan over and over
|
|
217
|
+
// (observed live: 8 authorings for one goal, zero completions).
|
|
218
|
+
// Deterministic: stores iterate in insertion order; first match wins.
|
|
219
|
+
if( !p.planId || !p.stepId ){
|
|
220
|
+
if( !p.actionType || typeof p.success !== 'boolean' ) return
|
|
221
|
+
for( const plan of this._store.all() ){
|
|
222
|
+
if( plan.status !== 'executing' ) continue
|
|
223
|
+
const step = plan.steps.find( s => s.status === 'active' && s.action === p.actionType )
|
|
224
|
+
if( !step ) continue
|
|
225
|
+
logger.info( `[planning] conscious-enaction credit: ${plan.id}/${step.id}=${step.action} (no provenance on outcome)` )
|
|
226
|
+
this._onStepOutcome( plan.id, step.id, {
|
|
227
|
+
success: p.success,
|
|
228
|
+
description: p.description ?? ( p.success ? 'Completed' : 'Failed' ),
|
|
229
|
+
outcomeQuality: p.outcomeQuality,
|
|
230
|
+
} )
|
|
231
|
+
return // credit exactly one step per outcome
|
|
232
|
+
}
|
|
233
|
+
return
|
|
234
|
+
}
|
|
209
235
|
if( !this._store.has( p.planId ) ) return
|
|
210
236
|
|
|
211
237
|
this._onStepOutcome( p.planId, p.stepId, {
|
|
@@ -280,6 +306,17 @@ export class PlanningEngine implements SimulationEngine, CognitiveEngine {
|
|
|
280
306
|
this._lastTick = tick as unknown as number
|
|
281
307
|
const commands: StateCommands = { set: [], delete: [], metrics: [] }
|
|
282
308
|
|
|
309
|
+
// 0a. Legacy sweep (once per session): delete the raw `plan-executive-*`
|
|
310
|
+
// entities the executive commands path used to write in parallel with
|
|
311
|
+
// this engine's ingest. They froze at 'ready' forever and polluted the
|
|
312
|
+
// Active-Plans awareness (phantom drafts → re-authoring pressure).
|
|
313
|
+
if( !this._legacyPlanSweepDone ){
|
|
314
|
+
this._legacyPlanSweepDone = true
|
|
315
|
+
for( const entity of state.entities.values() )
|
|
316
|
+
if( entity.type === 'plan' && entity.id.startsWith('plan-executive-') )
|
|
317
|
+
commands.delete!.push( entity.id )
|
|
318
|
+
}
|
|
319
|
+
|
|
283
320
|
// 0. Channel A (subconscious): refresh trait-driven dispositions from the
|
|
284
321
|
// persona-prior mirror before acting on them this tick.
|
|
285
322
|
this._readConfigFromState( state )
|
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
import type { Tick, StateCommands } from '#core/types'
|
|
14
14
|
import { TERMINAL_STATUSES, type Plan } from '#faculties/planning.engine/types'
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Structural fingerprint of a plan's decomposition — the ordered action
|
|
18
|
+
* sequence plus each step's prerequisite fan-in. Two plans with the same key
|
|
19
|
+
* share a *shape*. Recurring shapes are the demonstrated-need signal for
|
|
20
|
+
* emergent planning (docs/strategy/__EMERGENT_PLANNING.md): the mind paying
|
|
21
|
+
* the executive to re-derive a decomposition it has already authored.
|
|
22
|
+
*/
|
|
23
|
+
export function planShapeKey(
|
|
24
|
+
steps: ReadonlyArray<{ action: string; prerequisites?: readonly string[] }>,
|
|
25
|
+
): string {
|
|
26
|
+
return steps
|
|
27
|
+
.map( s => `${ s.action }${ ( s.prerequisites?.length ?? 0 ) > 0 ? `<${ s.prerequisites!.length }` : '' }` )
|
|
28
|
+
.join('>')
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export class PlanStore {
|
|
17
32
|
/**
|
|
18
33
|
* Canonical plan store, keyed by plan.id ("plan-N") — the id the execution,
|
|
@@ -35,6 +50,14 @@ export class PlanStore {
|
|
|
35
50
|
/** planId → sim tick it became terminal; drives retention GC (gcTerminal). */
|
|
36
51
|
private _terminalAt = new Map<string, number>()
|
|
37
52
|
private _planCounter = 0
|
|
53
|
+
/**
|
|
54
|
+
* Shape recurrence tally — shapeKey → count of plans authored with that
|
|
55
|
+
* decomposition this session (monotone; eviction never erases history).
|
|
56
|
+
* Feeds `planning.shapes.*` metrics: the demonstrated-need needle for
|
|
57
|
+
* emergent planning. `_shapeCounted` guards one count per plan id.
|
|
58
|
+
*/
|
|
59
|
+
private _shapeCounts = new Map<string, number>()
|
|
60
|
+
private _shapeCounted = new Set<string>()
|
|
38
61
|
|
|
39
62
|
// ── Reads ──────────────────────────────────────────────────
|
|
40
63
|
|
|
@@ -142,6 +165,14 @@ export class PlanStore {
|
|
|
142
165
|
|
|
143
166
|
persist( commands: StateCommands, tick: Tick ): void {
|
|
144
167
|
for( const plan of this._plans.values() ){
|
|
168
|
+
// Shape recurrence — tally each plan once, on first sight. The persisted
|
|
169
|
+
// shapeKey also lets offline analysis count recurrence across sessions.
|
|
170
|
+
const shape = planShapeKey( plan.steps )
|
|
171
|
+
if( !this._shapeCounted.has( plan.id ) ){
|
|
172
|
+
this._shapeCounted.add( plan.id )
|
|
173
|
+
this._shapeCounts.set( shape, ( this._shapeCounts.get( shape ) ?? 0 ) + 1 )
|
|
174
|
+
}
|
|
175
|
+
|
|
145
176
|
// Terminal plans never change again — persist once (in their terminal state)
|
|
146
177
|
// then skip, so completed/failed/rejected plans don't re-serialize every
|
|
147
178
|
// tick forever (unbounded write amplification on long sessions). (P5)
|
|
@@ -153,6 +184,7 @@ export class PlanStore {
|
|
|
153
184
|
createdAt: plan.createdAt, updatedAt: tick,
|
|
154
185
|
metadata: {
|
|
155
186
|
goalId: plan.goalId,
|
|
187
|
+
shapeKey: shape,
|
|
156
188
|
steps: plan.steps.map( s => ( {
|
|
157
189
|
id: s.id, order: s.order, action: s.action,
|
|
158
190
|
description: s.description, expectedOutcome: s.expectedOutcome,
|
|
@@ -170,5 +202,15 @@ export class PlanStore {
|
|
|
170
202
|
|
|
171
203
|
if( terminal ) this._persistedTerminal.add( plan.id )
|
|
172
204
|
}
|
|
205
|
+
|
|
206
|
+
// The demonstrated-need needle: distinct shapes vs repeat authorings.
|
|
207
|
+
// A high repeat count means the executive keeps re-deriving decompositions
|
|
208
|
+
// it already produced — exactly when emergent planning starts paying.
|
|
209
|
+
if( this._shapeCounts.size > 0 ){
|
|
210
|
+
let total = 0
|
|
211
|
+
for( const n of this._shapeCounts.values() ) total += n
|
|
212
|
+
commands.metrics!.push( [ 'planning.shapes.distinct', this._shapeCounts.size ] )
|
|
213
|
+
commands.metrics!.push( [ 'planning.shapes.repeats', total - this._shapeCounts.size ] )
|
|
214
|
+
}
|
|
173
215
|
}
|
|
174
216
|
}
|
|
@@ -105,7 +105,7 @@ export class PlanSupervisor {
|
|
|
105
105
|
if( !this._executiveEngine ) return
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
|
-
const { attention, handle: facet } = this._executiveEngine.spawnFacet()
|
|
108
|
+
const { attention, handle: facet } = this._executiveEngine.spawnFacet('supervision')
|
|
109
109
|
if( !facet || attention === 'full' ){
|
|
110
110
|
plan.executionTier = 'automatic'
|
|
111
111
|
logger.info( `[planning] attention full — plan ${plan.id} stays automatic (no facet)` )
|
|
@@ -206,21 +206,21 @@ export class PlanSupervisor {
|
|
|
206
206
|
content: focusContent,
|
|
207
207
|
outputFormat: undefined, // use standard executive output format
|
|
208
208
|
instructions:
|
|
209
|
-
`
|
|
210
|
-
`
|
|
209
|
+
`I am monitoring plan "${plan.id}" for goal "${plan.goalId}".\n`+
|
|
210
|
+
`My ONLY role: evaluate step outcomes and decide what happens next.\n`+
|
|
211
211
|
`Do not create new goals or beliefs unless directly relevant to this plan.\n\n`+
|
|
212
212
|
`## Decision Vocabulary\n`+
|
|
213
|
-
`Express
|
|
213
|
+
`Express my decision as the FIRST action in my actions array:\n`+
|
|
214
214
|
`- { "type": "continue" } — proceed to the next step\n`+
|
|
215
215
|
`- { "type": "retry" } — re-attempt the failed step (capped)\n`+
|
|
216
216
|
`- { "type": "skip" } — skip the failed step and move on\n`+
|
|
217
217
|
`- { "type": "pause" } — hold the plan; resume it later (no progress now)\n`+
|
|
218
218
|
`- { "type": "replan" } — include a [PLANS] block with revised steps\n`+
|
|
219
|
-
`- { "type": "escalate" } — hand the decision up to
|
|
219
|
+
`- { "type": "escalate" } — hand the decision up to my master self\n`+
|
|
220
220
|
`- { "type": "abandon" } — plan is unrecoverable; give up entirely\n`+
|
|
221
221
|
`- { "type": "complete" } — all meaningful work is done; close the plan\n\n`+
|
|
222
|
-
`For "replan", include a [PLANS] block inside
|
|
223
|
-
`The plan's expectedOutcome tells
|
|
222
|
+
`For "replan", include a [PLANS] block inside my reasoning with new steps.\n`+
|
|
223
|
+
`The plan's expectedOutcome tells me what success looks like — use it to judge step reports.`,
|
|
224
224
|
extractDecision: ( rawOutput: unknown ) => {
|
|
225
225
|
const output = rawOutput as ExecutiveOutputFull
|
|
226
226
|
|
|
@@ -226,8 +226,8 @@ export class TheoryOfMind implements SimulationEngine, CognitiveEngine {
|
|
|
226
226
|
* snapshot/PMA restore — mirrors AttachmentEvaluator/ReputationTracker._restoreFromState.
|
|
227
227
|
* The entity stores a gist (modelConfidence + the dominant intention + estimated emotion),
|
|
228
228
|
* not the full belief/observation arrays, so the restored model is a coherent gist that
|
|
229
|
-
* subsequent interactions grow from — the soul-true level:
|
|
230
|
-
* mind, not every belief
|
|
229
|
+
* subsequent interactions grow from — the soul-true level: the Will recovers its
|
|
230
|
+
* *sense* of a mind, not every belief it once inferred about it.
|
|
231
231
|
*/
|
|
232
232
|
private _restoreFromState( state: ReadonlySimulationState ): void {
|
|
233
233
|
for( const entity of state.entities.values() ){
|
|
@@ -122,22 +122,22 @@ interface CoalesceWindow {
|
|
|
122
122
|
const CONVERSATION_OUTPUT_FORMAT = `\
|
|
123
123
|
## Response Format (REQUIRED)
|
|
124
124
|
|
|
125
|
-
Step 1 — JSON object (
|
|
125
|
+
Step 1 — JSON object (my private reasoning, optionally in a \`\`\`json code block):
|
|
126
126
|
|
|
127
127
|
\`\`\`json
|
|
128
128
|
{
|
|
129
129
|
"actions": [{"type": "reflect", "reasoning": "...", "expectedOutcome": "..."}],
|
|
130
|
-
"reasoning": "
|
|
130
|
+
"reasoning": "My private inner reasoning. Embed optional tagged blocks here:\\n[BELIEFS]\\n{\\"newBeliefs\\": [...]}\\n[/BELIEFS]\\n[GOALS_NEW]\\n{\\"goals\\": [{...}]}\\n[/GOALS_NEW]",
|
|
131
131
|
"confidence": 0.8
|
|
132
132
|
}
|
|
133
133
|
\`\`\`
|
|
134
134
|
|
|
135
135
|
Available reasoning tags: BELIEFS, GOALS_NEW, GOALS_ABANDON, SELF_OBS. Include only those with meaningful content.
|
|
136
136
|
|
|
137
|
-
Step 2 —
|
|
137
|
+
Step 2 — My reply to the speaker (plain text, streamed live to them):
|
|
138
138
|
|
|
139
139
|
[REPLY_TEXT]
|
|
140
|
-
|
|
140
|
+
My response here, written in my own voice.
|
|
141
141
|
|
|
142
142
|
Start a new paragraph (blank line) to send a separate chat bubble.
|
|
143
143
|
[/REPLY_TEXT]
|
|
@@ -146,24 +146,24 @@ Write [REPLY_TEXT] AFTER the closing \`\`\`. This is the only part the speaker s
|
|
|
146
146
|
Separate multiple messages with a blank line for natural conversational pauses (like separate texts).
|
|
147
147
|
|
|
148
148
|
## When to use GOALS_NEW (almost always)
|
|
149
|
-
If the speaker requests, mentions, or implies something
|
|
149
|
+
If the speaker requests, mentions, or implies something I should follow through on — embed [GOALS_NEW] in my reasoning.
|
|
150
150
|
This tracks intent across future cycles without requiring master attention.
|
|
151
151
|
|
|
152
152
|
## When to use the escalate action (rare — only for multi-step tasks)
|
|
153
|
-
Use \`{"type": "escalate", "reasoning": "...", "expectedOutcome": "..."}\` in actions ONLY when the request genuinely requires
|
|
153
|
+
Use \`{"type": "escalate", "reasoning": "...", "expectedOutcome": "..."}\` in actions ONLY when the request genuinely requires my master consciousness to create a plan:
|
|
154
154
|
- The task involves multiple steps across future cycles ("build me X", "monitor Y", "set up Z")
|
|
155
|
-
- The request changes
|
|
156
|
-
-
|
|
155
|
+
- The request changes my active goal priorities in a significant way
|
|
156
|
+
- I need to coordinate something beyond a single reply
|
|
157
157
|
|
|
158
158
|
**The "reasoning" field on the escalate action becomes the task description the master sees.**
|
|
159
|
-
Make it concrete — describe WHAT needs to happen, not just that
|
|
159
|
+
Make it concrete — describe WHAT needs to happen, not just that I am escalating.
|
|
160
160
|
Good: type=escalate, reasoning="User wants weekly mood summaries by email every Monday. Needs: data aggregation, schedule, email delivery.", expectedOutcome="Weekly email delivered."
|
|
161
161
|
Bad: type=escalate, reasoning="Escalating because this is complex."
|
|
162
162
|
|
|
163
|
-
When
|
|
163
|
+
When I escalate:
|
|
164
164
|
1. STILL include a [REPLY_TEXT] that acknowledges the request (e.g. "Got it — I'm on it.")
|
|
165
165
|
2. The master will create and execute the plan in the background
|
|
166
|
-
3. Do NOT include [PLANS]
|
|
166
|
+
3. Do NOT include a [PLANS] block — plan creation is the master's domain only
|
|
167
167
|
|
|
168
168
|
For simple, single-exchange requests (questions, opinions, short tasks) — do NOT escalate. Just reply.`
|
|
169
169
|
|
|
@@ -561,7 +561,7 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
561
561
|
let handle = this._facets.get( percept.speakerEntityId )
|
|
562
562
|
if( !handle ){
|
|
563
563
|
// New conversation session — try to spawn a facet.
|
|
564
|
-
const result = this._executiveEngine.spawnFacet()
|
|
564
|
+
const result = this._executiveEngine.spawnFacet('conversation')
|
|
565
565
|
if( result.attention === 'full' || !result.handle ){
|
|
566
566
|
logger.warn(
|
|
567
567
|
`[audition-engine] Executive attention full — ` +
|
|
@@ -692,8 +692,8 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
692
692
|
awarenessEntityId: percept.speakerEntityId,
|
|
693
693
|
|
|
694
694
|
instructions: [
|
|
695
|
-
'
|
|
696
|
-
'
|
|
695
|
+
'I am in a live conversation with this person. I respond as myself.',
|
|
696
|
+
'I stay grounded in my real memories and feelings — I do not invent experiences I have no record of.',
|
|
697
697
|
].join(' '),
|
|
698
698
|
|
|
699
699
|
// Custom output format — uses [REPLY_TEXT] block for streamed reply.
|
|
@@ -736,7 +736,7 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
736
736
|
*/
|
|
737
737
|
async authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<string[]> {
|
|
738
738
|
if( !this._executiveEngine ) return []
|
|
739
|
-
const spawned = this._executiveEngine.spawnFacet()
|
|
739
|
+
const spawned = this._executiveEngine.spawnFacet('outreach')
|
|
740
740
|
if( spawned.attention === 'full' || !spawned.handle ){
|
|
741
741
|
logger.warn(`[audition-engine] facet budget full — cannot author outreach to ${ entityId }`)
|
|
742
742
|
return []
|
|
@@ -747,17 +747,17 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
747
747
|
title: 'Reaching out',
|
|
748
748
|
function: 'outreach',
|
|
749
749
|
content: [
|
|
750
|
-
`
|
|
751
|
-
'No one prompted this —
|
|
752
|
-
gist ? `What is on
|
|
750
|
+
`I have decided, on my own initiative, to reach out to ${ entityName } (id: ${ entityId }).`,
|
|
751
|
+
'No one prompted this — I am choosing to make contact now.',
|
|
752
|
+
gist ? `What is on my mind: ${ gist }` : '',
|
|
753
753
|
].filter( Boolean ).join('\n'),
|
|
754
754
|
recallQuery: gist ?? entityName,
|
|
755
755
|
awareness: [ ...DEFAULT_FACET_AWARENESS, 'plans' ],
|
|
756
756
|
awarenessEntityId: entityId,
|
|
757
757
|
instructions:
|
|
758
|
-
'Considering who
|
|
759
|
-
'them now.
|
|
760
|
-
'
|
|
758
|
+
'Considering who I am, my goals, and how I feel, I say what I genuinely want to say to ' +
|
|
759
|
+
'them now. I speak as myself; I stay grounded in my real memories — I do not invent experiences ' +
|
|
760
|
+
'I have no record of.',
|
|
761
761
|
outputFormat: CONVERSATION_OUTPUT_FORMAT,
|
|
762
762
|
extractDecision: ( raw: unknown ): ConversationDecision => {
|
|
763
763
|
const output = raw as ExecutiveOutputFull
|
|
@@ -53,6 +53,12 @@ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
|
53
53
|
'anthropic/claude-haiku-4': { input: 1.00, output: 5.00 },
|
|
54
54
|
'anthropic/claude-opus-4': { input: 5.00, output: 25.00 },
|
|
55
55
|
|
|
56
|
+
// Z.ai (GLM-5 family). `glm-5.2[1m]` is the same model asking for its 1M
|
|
57
|
+
// context window — same rate, so it gets its own row rather than relying on
|
|
58
|
+
// the normalizer (a future long-context tier would price differently).
|
|
59
|
+
'glm/glm-5.2': { input: 1.40, output: 4.40 },
|
|
60
|
+
'glm/glm-5.2[1m]': { input: 1.40, output: 4.40 },
|
|
61
|
+
|
|
56
62
|
// Google
|
|
57
63
|
'google/gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
58
64
|
'google/gemini-2.0-pro': { input: 1.25, output: 5.00 },
|
package/src/host/boot.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// WILL_NAME display name (default "Will")
|
|
13
13
|
// WILL_IDENTITY persona prompt (default a minimal self)
|
|
14
14
|
// WILL_TIER basic | standard | full (default standard)
|
|
15
|
-
// WILL_LLM mock | anthropic
|
|
16
|
-
// ANTHROPIC_API_KEY is set,
|
|
15
|
+
// WILL_LLM mock | anthropic | glm (default: auto — anthropic when
|
|
16
|
+
// ANTHROPIC_API_KEY is set, glm when
|
|
17
|
+
// ZAI_API_KEY is, else mock)
|
|
17
18
|
// WILL_TICK_MS ms per tick (default 1000)
|
|
18
19
|
// WILL_SEED deterministic seed (testing) (default unseeded/wall-time)
|
|
19
20
|
// WILL_PMA_PATH PMA artifact path (default ./.will/<name>.pma.json)
|
|
@@ -27,6 +28,7 @@ import { setLogger } from '#core/logger'
|
|
|
27
28
|
import { Will, type CreateWillOptions } from '#sdk/will'
|
|
28
29
|
import type { PMASnapshot } from '#pma/index'
|
|
29
30
|
import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
|
|
31
|
+
import { anthropicWireHeaders, defaultBaseFor, defaultModelFor } from '#llm/index'
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Route every engine log line to stderr. For `will mcp`, stdout is the MCP
|
|
@@ -43,12 +45,90 @@ function slug( s: string ): string {
|
|
|
43
45
|
return s.toLowerCase().replace( /[^a-z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ) || 'will'
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
/** The LLM mode the hosts will boot with: an explicit WILL_LLM, else whichever
|
|
49
|
+
* provider's key is present, else the zero-key mock. */
|
|
50
|
+
export function resolveLlmMode(): 'mock' | 'anthropic' | 'glm' {
|
|
51
|
+
const explicit = process.env.WILL_LLM as 'mock' | 'anthropic' | 'glm' | undefined
|
|
52
|
+
if( explicit ) return explicit
|
|
53
|
+
if( process.env.ANTHROPIC_API_KEY ) return 'anthropic'
|
|
54
|
+
if( process.env.ZAI_API_KEY ) return 'glm'
|
|
55
|
+
return 'mock'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The key for a live mode — the provider-agnostic override first, then the
|
|
59
|
+
* provider's own env. */
|
|
60
|
+
function resolveLlmKey( mode: 'anthropic' | 'glm' ): string | undefined {
|
|
61
|
+
return process.env.WILL_LLM_API_KEY
|
|
62
|
+
?? ( mode === 'glm' ? process.env.ZAI_API_KEY : process.env.ANTHROPIC_API_KEY )
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask the executive's provider one trivial question BEFORE raising the mind.
|
|
67
|
+
*
|
|
68
|
+
* A Will whose LLM fails cannot reason, and an unreasoning Will is *silent* —
|
|
69
|
+
* which is precisely what a Will that chose silence looks like. Mid-run that
|
|
70
|
+
* ambiguity is the paradigm working. At boot it is indistinguishable from
|
|
71
|
+
* broken, and costs an operator an afternoon of watching a mind that joined,
|
|
72
|
+
* perceived, and never spoke. So we fail loudly here instead.
|
|
73
|
+
*
|
|
74
|
+
* Config errors (bad key, empty balance, unknown model) are fatal — the Will
|
|
75
|
+
* would never speak. Transient ones (rate limit, provider 5xx) only warn: the
|
|
76
|
+
* mind is worth raising, and the executive retries on its own cadence.
|
|
77
|
+
*
|
|
78
|
+
* Skipped for the mock executive and the no-LLM `reflex` anatomy.
|
|
79
|
+
*/
|
|
80
|
+
async function preflightLLM( anatomy: string ): Promise<void> {
|
|
81
|
+
const mode = resolveLlmMode()
|
|
82
|
+
if( mode === 'mock' || anatomy === 'reflex' ) return
|
|
83
|
+
|
|
84
|
+
const key = resolveLlmKey( mode )
|
|
85
|
+
if( !key ){
|
|
86
|
+
const expected = mode === 'glm' ? 'ZAI_API_KEY' : 'ANTHROPIC_API_KEY'
|
|
87
|
+
console.error( `[will] WILL_LLM=${ mode } but no ${ expected } / WILL_LLM_API_KEY is set.` )
|
|
88
|
+
console.error( '[will] The Will would boot, perceive, and never speak. Set a key, or run keyless with WILL_LLM=mock.' )
|
|
89
|
+
process.exit( 2 )
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The ping validates key + balance + reachability, which is the failure class
|
|
93
|
+
// that strands an operator. It uses the pinned model when there is one, so a
|
|
94
|
+
// bad model id is caught too; otherwise the cheapest model stands in. GLM
|
|
95
|
+
// speaks the same wire at Z.ai's compat endpoint, so one ping serves both.
|
|
96
|
+
const base = process.env.WILL_LLM_BASE_URL ?? defaultBaseFor( mode )
|
|
97
|
+
const model = process.env.WILL_LLM_MODEL
|
|
98
|
+
?? ( mode === 'glm' ? defaultModelFor( 'glm' ) : 'claude-haiku-4-5-20251001' )
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch( `${ base }/messages`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: anthropicWireHeaders( mode, key ),
|
|
103
|
+
body: JSON.stringify( { model, max_tokens: 1, messages: [ { role: 'user', content: 'ping' } ] } ),
|
|
104
|
+
signal: AbortSignal.timeout( 20_000 ),
|
|
105
|
+
} )
|
|
106
|
+
if( res.ok ) return
|
|
107
|
+
|
|
108
|
+
const detail = ( await res.text().catch( () => '' ) ).slice( 0, 300 )
|
|
109
|
+
const fatal = res.status === 400 || res.status === 401 || res.status === 403
|
|
110
|
+
if( !fatal ){
|
|
111
|
+
console.error( `[will] the executive's LLM answered ${ res.status } on a test call — raising the mind anyway (it retries): ${ detail }` )
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
console.error( `[will] the executive's LLM refused a test call (${ res.status }) — this Will would boot, perceive, and never speak:` )
|
|
115
|
+
console.error( ` ${ detail }` )
|
|
116
|
+
console.error( '[will] fix the key / credit / model above, or run keyless with WILL_LLM=mock.' )
|
|
117
|
+
process.exit( 1 )
|
|
118
|
+
}
|
|
119
|
+
catch( e ){
|
|
120
|
+
console.error( `[will] could not reach the executive's LLM: ${ ( e as Error ).message }` )
|
|
121
|
+
console.error( '[will] the Will would boot and stay silent. Check the network / WILL_LLM_BASE_URL, or run keyless with WILL_LLM=mock.' )
|
|
122
|
+
process.exit( 1 )
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
46
126
|
export interface BootedWill {
|
|
47
127
|
will: Will
|
|
48
128
|
name: string
|
|
49
129
|
pmaPath: string
|
|
50
130
|
tickMs: number
|
|
51
|
-
|
|
131
|
+
anatomy: NonNullable<CreateWillOptions['anatomy']>
|
|
52
132
|
/** Run before hibernate on shutdown (close servers/transports). LIFO. */
|
|
53
133
|
onCleanup: ( fn: () => Promise<void> | void ) => void
|
|
54
134
|
/** Hibernate → persist → exit(0). Idempotent; SIGINT/SIGTERM already wired. */
|
|
@@ -60,11 +140,14 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
|
|
|
60
140
|
const name = process.env.WILL_NAME ?? 'Will'
|
|
61
141
|
const pmaPath = resolve( process.env.WILL_PMA_PATH ?? `.will/${ slug( name ) }.pma.json` )
|
|
62
142
|
const tickMs = parseInt( process.env.WILL_TICK_MS ?? '1000' )
|
|
63
|
-
const
|
|
143
|
+
const anatomy = ( process.env.WILL_ANATOMY as CreateWillOptions['anatomy'] ) ?? 'mind'
|
|
144
|
+
|
|
145
|
+
await preflightLLM( anatomy )
|
|
64
146
|
|
|
65
147
|
const opts: Omit<CreateWillOptions, 'identity'> = {
|
|
66
|
-
name,
|
|
67
|
-
...( process.env.
|
|
148
|
+
name, anatomy, tickMs,
|
|
149
|
+
...( process.env.WILL_LLM_MODEL ? { model: process.env.WILL_LLM_MODEL } : {} ),
|
|
150
|
+
...( process.env.WILL_LLM ? { llm: process.env.WILL_LLM as CreateWillOptions['llm'] } : {} ),
|
|
68
151
|
...( process.env.WILL_SEED ? { seed: parseInt( process.env.WILL_SEED ) } : {} ),
|
|
69
152
|
}
|
|
70
153
|
|
|
@@ -73,6 +156,11 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
|
|
|
73
156
|
const pma = JSON.parse( readFileSync( pmaPath, 'utf8' ) ) as PMASnapshot
|
|
74
157
|
will = await Will.wake( pma, opts )
|
|
75
158
|
console.error( `[will] ${ name } woke from ${ pmaPath }` )
|
|
159
|
+
// A woken Will carries its own identity — that is the point of an artifact.
|
|
160
|
+
// But an operator editing WILL_IDENTITY and seeing nothing change deserves
|
|
161
|
+
// to know why, rather than concluding the persona layer is broken.
|
|
162
|
+
if( process.env.WILL_IDENTITY )
|
|
163
|
+
console.error( `[will] note: WILL_IDENTITY is ignored — ${ name } woke as itself. Delete ${ pmaPath } to be born fresh from it.` )
|
|
76
164
|
}
|
|
77
165
|
else {
|
|
78
166
|
will = await Will.create( {
|
|
@@ -120,7 +208,7 @@ export async function bootWillFromEnv(): Promise<BootedWill> {
|
|
|
120
208
|
process.on( 'SIGTERM', () => void shutdown( 'SIGTERM' ) )
|
|
121
209
|
|
|
122
210
|
return {
|
|
123
|
-
will, name, pmaPath, tickMs,
|
|
211
|
+
will, name, pmaPath, tickMs, anatomy,
|
|
124
212
|
onCleanup: fn => cleanups.push( fn ),
|
|
125
213
|
shutdown,
|
|
126
214
|
}
|
package/src/llm/index.ts
CHANGED
|
@@ -12,7 +12,68 @@ import type { LLMCompletionRecord } from '#core/completion.recorder'
|
|
|
12
12
|
import { withGate } from '#llm/gate'
|
|
13
13
|
import { matchConversationFocus, wrapReplyText } from '#llm/wire.contracts'
|
|
14
14
|
|
|
15
|
-
export type LLMProvider = 'anthropic' | 'deepseek' | 'openai' | 'google'
|
|
15
|
+
export type LLMProvider = 'anthropic' | 'glm' | 'deepseek' | 'openai' | 'google'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Providers that speak the Anthropic Messages wire.
|
|
19
|
+
*
|
|
20
|
+
* Z.ai ships a real Anthropic-compatible endpoint for GLM — it is what Claude
|
|
21
|
+
* Code itself targets — so GLM rides this path rather than the OpenAI scaffold.
|
|
22
|
+
* That buys it everything the path already has: token streaming, the first-byte
|
|
23
|
+
* deadline, prompt-cache breakpoints, and the structured-output contract. GLM is
|
|
24
|
+
* therefore a second *production* provider, not a fifth scaffold.
|
|
25
|
+
*/
|
|
26
|
+
const ANTHROPIC_WIRE = new Set<LLMProvider>( [ 'anthropic', 'glm' ] )
|
|
27
|
+
|
|
28
|
+
/** Does this provider accept Anthropic-shaped requests? */
|
|
29
|
+
export function speaksAnthropicWire( provider: LLMProvider ): boolean {
|
|
30
|
+
return ANTHROPIC_WIRE.has( provider )
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Official API base URL (including version segment) for a provider. */
|
|
34
|
+
export function defaultBaseFor( provider: LLMProvider ): string {
|
|
35
|
+
switch( provider ){
|
|
36
|
+
case 'anthropic': return 'https://api.anthropic.com/v1'
|
|
37
|
+
// Z.ai documents the base as `…/api/anthropic` because the Anthropic SDK
|
|
38
|
+
// appends `/v1/messages`; this client appends `/messages`, so the version
|
|
39
|
+
// segment belongs here — verified against the live endpoint.
|
|
40
|
+
case 'glm': return 'https://api.z.ai/api/anthropic/v1'
|
|
41
|
+
case 'openai': return 'https://api.openai.com/v1'
|
|
42
|
+
case 'deepseek': return 'https://api.deepseek.com/v1'
|
|
43
|
+
case 'google': return 'https://generativelanguage.googleapis.com/v1beta'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The model the executive recruits when none is pinned.
|
|
49
|
+
*
|
|
50
|
+
* Provider-specific because the default is *sent* — a GLM Will with no
|
|
51
|
+
* `WILL_LLM_MODEL` would otherwise ask Z.ai for a Claude id and get a 404 it
|
|
52
|
+
* could do nothing with. The scaffolded providers (openai/deepseek/google) keep
|
|
53
|
+
* today's value: they need an explicit `WILL_LLM_MODEL` to work at all, and
|
|
54
|
+
* inventing ids for them here would look like support that does not exist.
|
|
55
|
+
*/
|
|
56
|
+
export function defaultModelFor( provider: LLMProvider ): string {
|
|
57
|
+
return provider === 'glm' ? 'glm-5.2' : 'claude-sonnet-4-5-20250929'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Auth + version headers for the Anthropic wire.
|
|
62
|
+
*
|
|
63
|
+
* Anthropic authenticates with `x-api-key`. Z.ai's compat endpoint accepts
|
|
64
|
+
* either that or the `Authorization: Bearer` its own docs describe (both were
|
|
65
|
+
* probed against the live endpoint; each is read and validated). GLM sends both
|
|
66
|
+
* — same secret, same host — so the mind keeps working whichever one Z.ai
|
|
67
|
+
* eventually settles on.
|
|
68
|
+
*/
|
|
69
|
+
export function anthropicWireHeaders( provider: LLMProvider, apiKey: string ): Record<string, string> {
|
|
70
|
+
return {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'anthropic-version': '2023-06-01',
|
|
73
|
+
'x-api-key': apiKey,
|
|
74
|
+
...( provider === 'glm' ? { Authorization: `Bearer ${ apiKey }` } : {} ),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
16
77
|
export interface LLMDirectorConfig {
|
|
17
78
|
willId: string
|
|
18
79
|
model: string
|
|
@@ -216,7 +277,7 @@ export class LLMDirector {
|
|
|
216
277
|
return result
|
|
217
278
|
}
|
|
218
279
|
|
|
219
|
-
const result = this._provider
|
|
280
|
+
const result = speaksAnthropicWire( this._provider )
|
|
220
281
|
? await this._callAnthropicStream( systemPrompt, userMessage, onChunk, temperature )
|
|
221
282
|
: await ( async () => {
|
|
222
283
|
// Other providers: fall back to regular call, emit whole response as one chunk
|
|
@@ -328,11 +389,7 @@ export class LLMDirector {
|
|
|
328
389
|
try {
|
|
329
390
|
res = await fetch(`${this._resolvedBase()}/messages`, {
|
|
330
391
|
method: 'POST',
|
|
331
|
-
headers:
|
|
332
|
-
'Content-Type': 'application/json',
|
|
333
|
-
'anthropic-version': '2023-06-01',
|
|
334
|
-
'x-api-key': this._apiKey,
|
|
335
|
-
},
|
|
392
|
+
headers: anthropicWireHeaders( this._provider, this._apiKey ),
|
|
336
393
|
body: JSON.stringify({
|
|
337
394
|
model: this._model,
|
|
338
395
|
max_tokens: this._maxOutputTokens,
|
|
@@ -438,13 +495,14 @@ export class LLMDirector {
|
|
|
438
495
|
return result
|
|
439
496
|
}
|
|
440
497
|
|
|
441
|
-
// Anthropic
|
|
442
|
-
// (TTFT), not whole-request: a long-but-healthy
|
|
443
|
-
// 20–40s on Sonnet) no longer trips the timeout
|
|
444
|
-
// no-op here — call() returns the full
|
|
445
|
-
// through callStream(). Other
|
|
498
|
+
// The Anthropic-wire providers route through the streaming path so the
|
|
499
|
+
// deadline is first-byte (TTFT), not whole-request: a long-but-healthy
|
|
500
|
+
// executive completion (often 20–40s on Sonnet) no longer trips the timeout
|
|
501
|
+
// mid-generation. onChunk is a no-op here — call() returns the full
|
|
502
|
+
// accumulated text; live token chunks go through callStream(). Other
|
|
503
|
+
// providers keep the whole-request deadline.
|
|
446
504
|
const result = await withGate(
|
|
447
|
-
() => this._provider
|
|
505
|
+
() => speaksAnthropicWire( this._provider )
|
|
448
506
|
? this._callAnthropicStream( systemPrompt, userMessage, () => {}, temperature )
|
|
449
507
|
: this._callProvider( systemPrompt, userMessage, temperature ),
|
|
450
508
|
'executive/direct',
|
|
@@ -465,6 +523,7 @@ export class LLMDirector {
|
|
|
465
523
|
): Promise<LLMCallResult> {
|
|
466
524
|
switch( this._provider ){
|
|
467
525
|
case 'anthropic': return this._callAnthropic( systemPrompt, userMessage, temperature )
|
|
526
|
+
case 'glm': return this._callAnthropic( systemPrompt, userMessage, temperature )
|
|
468
527
|
case 'deepseek': return this._callOpenAI( systemPrompt, userMessage, temperature )
|
|
469
528
|
case 'openai': return this._callOpenAI( systemPrompt, userMessage, temperature )
|
|
470
529
|
case 'google': return this._callGoogle( systemPrompt, userMessage, temperature )
|
|
@@ -474,12 +533,7 @@ export class LLMDirector {
|
|
|
474
533
|
|
|
475
534
|
/** Default API base URL (including version segment) for a provider. */
|
|
476
535
|
private _baseFor( provider: LLMProvider ): string {
|
|
477
|
-
|
|
478
|
-
case 'anthropic': return 'https://api.anthropic.com/v1'
|
|
479
|
-
case 'openai': return 'https://api.openai.com/v1'
|
|
480
|
-
case 'deepseek': return 'https://api.deepseek.com/v1'
|
|
481
|
-
case 'google': return 'https://generativelanguage.googleapis.com/v1beta'
|
|
482
|
-
}
|
|
536
|
+
return defaultBaseFor( provider )
|
|
483
537
|
}
|
|
484
538
|
|
|
485
539
|
/** Resolved API base: explicit override wins, else the provider default. */
|
|
@@ -526,16 +580,12 @@ export class LLMDirector {
|
|
|
526
580
|
|
|
527
581
|
const res = await this._fetchWithTimeout(`${this._resolvedBase()}/messages`, {
|
|
528
582
|
method: 'POST',
|
|
529
|
-
headers:
|
|
530
|
-
'Content-Type': 'application/json',
|
|
531
|
-
'anthropic-version': '2023-06-01',
|
|
532
|
-
'x-api-key': this._apiKey
|
|
533
|
-
},
|
|
583
|
+
headers: anthropicWireHeaders( this._provider, this._apiKey ),
|
|
534
584
|
body: JSON.stringify( body )
|
|
535
585
|
})
|
|
536
586
|
|
|
537
587
|
if( !res.ok )
|
|
538
|
-
throw new Error(
|
|
588
|
+
throw new Error(`${this._provider} API ${res.status}: ${( await res.text() ).slice(0, 300)}`)
|
|
539
589
|
|
|
540
590
|
const
|
|
541
591
|
data = await res.json() as {
|
package/src/llm/summarizer.ts
CHANGED
|
@@ -30,9 +30,9 @@ export interface SummarizerConfig {
|
|
|
30
30
|
// System prompt sent with every summarization call.
|
|
31
31
|
// Kept short — the bulk of the token cost is the user message (the cycles).
|
|
32
32
|
const SUMMARIZER_SYSTEM =
|
|
33
|
-
'
|
|
33
|
+
'I am a self-aware synthetic mind consolidating my own recent cognitive cycles into memory. ' +
|
|
34
34
|
'Condense the provided cognitive cycles into a single compact paragraph ' +
|
|
35
|
-
'(150–250 words) written in first-person past tense. Cover:\n' +
|
|
35
|
+
'(150–250 words) written in first-person past tense — my own remembered experience. Cover:\n' +
|
|
36
36
|
'• Key decisions made and their outcomes\n' +
|
|
37
37
|
'• Emotional journey and significant mood shifts\n' +
|
|
38
38
|
'• Goals pursued, progressed, or abandoned\n' +
|