@animalabs/connectome-host 0.8.0 → 0.8.1
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/CHANGELOG.md +75 -0
- package/bun.lock +4 -6
- package/package.json +3 -3
- package/src/commands.ts +125 -32
- package/src/framework-agent-config.ts +3 -0
- package/src/framework-strategy.ts +5 -0
- package/src/gate-telemetry.ts +30 -2
- package/src/index.ts +35 -8
- package/src/modules/web-ui-module.ts +32 -4
- package/src/recipe.ts +89 -0
- package/src/web/panel-data.ts +25 -1
- package/test/commands-qa-family.test.ts +239 -0
- package/test/count-tokens-model.test.ts +31 -0
- package/test/framework-strategy-defaults.test.ts +41 -0
- package/test/gate-telemetry.test.ts +34 -1
- package/test/subconscious-recipe.test.ts +86 -0
- package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
- package/test/web-ui-module.test.ts +41 -0
- package/web/src/App.tsx +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,81 @@ release time — see [CONTRIBUTING.md](CONTRIBUTING.md#changelog).
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.8.1 — 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Recipe `subconscious` block (tune-out, agent-framework#77): `enabled`,
|
|
14
|
+
`systemPrompt` (required — the subconscious's mode block), optional `name`,
|
|
15
|
+
`model`, `allowChannelSpeech`, `reAnchorFraction`. Validated at recipe load
|
|
16
|
+
(unknown fields refused by name) and passed through verbatim to
|
|
17
|
+
`FrameworkConfig.subconscious`; the framework owns the defaults. Requires
|
|
18
|
+
agent-framework with tune-out (#115).
|
|
19
|
+
|
|
20
|
+
- Add validated recipe plumbing for Agent Framework's default-off `agent.toolWrapperProseGuard` containment boundary.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Gate telemetry reads `x-gate-channel` from the turn's routing locus or,
|
|
25
|
+
for gate-batched wakes that set none, from the framework's telemetry-only
|
|
26
|
+
`wakeChannelId` (agent-framework ≥0.14). On residents with a subconscious
|
|
27
|
+
the stamp now follows the primary (the exactly-one-agent guard had
|
|
28
|
+
silenced it), and — because one provider adapter serves every agent — the
|
|
29
|
+
origin trio is stamped only while the primary is the only agent with a
|
|
30
|
+
turn in flight; overlap withholds rather than misattributes. Compression
|
|
31
|
+
debt is read from the primary as well.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Recipes' `agent.strategy.mergeMaxSourceSpanMessages` now reaches the Context
|
|
36
|
+
Manager (it was accepted but never passed through, so the CM default applied
|
|
37
|
+
regardless of the recipe). Also plumbs and validates the Context Manager's
|
|
38
|
+
`compressionSplitFallback`, `compressionSplitPlaceholder`,
|
|
39
|
+
`compressionSplitMaxCallsPerChunk` and `compressionSplitMaxCallsPer10Min`
|
|
40
|
+
keys (all default off / CM defaults).
|
|
41
|
+
|
|
42
|
+
- Gate telemetry classifies EventGate-batched wakes (`gate:debounce`, source
|
|
43
|
+
`gate`) as origin `event` instead of passing the raw reason through.
|
|
44
|
+
|
|
45
|
+
- Name-taking commands (`/checkpoint`, `/restore`, `/checkout`,
|
|
46
|
+
`/session switch`, `/session delete`) parse the rest of the line instead of
|
|
47
|
+
only the first token, so multi-word names round-trip with
|
|
48
|
+
`/session rename` instead of silently truncating (`/checkpoint my test
|
|
49
|
+
point` used to save a checkpoint named `my`).
|
|
50
|
+
- `/session delete` requires `--confirm`: the bare command echoes exactly
|
|
51
|
+
which session matched (name, id, message count) before anything
|
|
52
|
+
irreversible happens. `/help` documents that switch/delete accept ids.
|
|
53
|
+
- Head-moving commands (`/undo`, `/redo`, `/checkout`, `/restore`,
|
|
54
|
+
`/branchto`, `/newtopic`) are refused while a turn is in flight — moving
|
|
55
|
+
the head mid-stream committed the streaming reply onto the wrong branch,
|
|
56
|
+
detached from its request (orphaned Chronicle nodes), including when the
|
|
57
|
+
move came from a second client on the same session.
|
|
58
|
+
- `/mcp add` on an existing server preserves its env vars and `toolPrefix`
|
|
59
|
+
(and reports the kept env keys); previously a command update silently
|
|
60
|
+
wiped the server's env, which only surfaced when the server next started
|
|
61
|
+
without its tokens.
|
|
62
|
+
- Checkpoints are visible: `/branches` lists them alongside branches, and
|
|
63
|
+
bare `/checkpoint` lists existing checkpoints (matching bare `/restore`).
|
|
64
|
+
- `/budget` displays small values exactly instead of flooring to `0k`
|
|
65
|
+
(`/budget 50` used to report "set to 0k" while rejecting `/budget 0`).
|
|
66
|
+
- `/clear` clears the WebUI transcript view (client-side, like the TUI's
|
|
67
|
+
scrollback wipe) instead of appending a "(cleared)" line while clearing
|
|
68
|
+
nothing; `/help` and the headless reply now say what `/clear` actually
|
|
69
|
+
does — display only, history and context kept.
|
|
70
|
+
|
|
71
|
+
- The WebUI HTTP surface answers honestly: unknown `/debug/*` paths (typos,
|
|
72
|
+
casing, trailing slashes) return a JSON 404 instead of the SPA shell with
|
|
73
|
+
a 200; missing `/assets/*` files return 404 instead of HTML (which
|
|
74
|
+
produced a blank page with a MIME error on stale bundle hashes); non-GET
|
|
75
|
+
methods get 405 with an `Allow` header. SPA client-side routes still fall
|
|
76
|
+
back to the shell.
|
|
77
|
+
- The context-makeup panel's exact token count calls `count_tokens` with the
|
|
78
|
+
model the agent actually runs (provider/Bedrock prefixes normalized away)
|
|
79
|
+
instead of a hardcoded id that 404'd on every install and silently nulled
|
|
80
|
+
`exactTotalTokens`. `COUNT_TOKENS_MODEL` remains as an explicit override;
|
|
81
|
+
non-Anthropic models report `count_tokens_unsupported_model` instead of
|
|
82
|
+
counting against the wrong tokenizer.
|
|
83
|
+
|
|
9
84
|
## 0.8.0 — 2026-09-05
|
|
10
85
|
|
|
11
86
|
### Added
|
package/bun.lock
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@animalabs/connectome-host",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@animalabs/agent-framework": "^0.
|
|
8
|
+
"@animalabs/agent-framework": "^0.13.0",
|
|
9
9
|
"@animalabs/chronicle": "^0.3.0",
|
|
10
|
-
"@animalabs/context-manager": "^0.
|
|
10
|
+
"@animalabs/context-manager": "^0.8.0",
|
|
11
11
|
"@animalabs/membrane": "^0.5.82",
|
|
12
12
|
"@opentui/core": "^0.1.82",
|
|
13
13
|
},
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
"packages": {
|
|
22
|
-
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.
|
|
22
|
+
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.13.0", "", { "dependencies": { "@animalabs/chronicle": "^0.3.0", "@animalabs/context-manager": "^0.8.0", "@animalabs/membrane": "^0.5.78", "chokidar": "^4.0.3", "discord.js": "^14.25.1", "ws": "^8.18.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js", "agent-framework-recover": "dist/src/recovery/recover-cli.js" } }, "sha512-j4QY9pEjOHdmqnwyUPp4NU6fW1f5EHb3Lp+ygoAB3LKj2QjLITlglNFNuLqjRWtGiwwamSopcuqgbwHwAp0wyQ=="],
|
|
23
23
|
|
|
24
24
|
"@animalabs/chronicle": ["@animalabs/chronicle@0.3.0", "", {}, "sha512-6BBJ9pWb8AnuHTNJTU07Y6Wut3IvdgWFxsy05IdlWcEnR5W23dlaBlaeJq7gDc7TwX05HfsDC+QZFEc69G+s4Q=="],
|
|
25
25
|
|
|
26
|
-
"@animalabs/context-manager": ["@animalabs/context-manager@0.
|
|
26
|
+
"@animalabs/context-manager": ["@animalabs/context-manager@0.8.0", "", { "dependencies": { "@animalabs/chronicle": "^0.3.0", "@animalabs/membrane": "^0.5.69" } }, "sha512-lMYU+FazgSl6FFNig8N8Zjo6vqxn16j6H5GkYldS3R902nnUX4gjsqhup/yQDqrrOJuwSvpkrQBpnV8q2iOykA=="],
|
|
27
27
|
|
|
28
28
|
"@animalabs/membrane": ["@animalabs/membrane@0.5.82", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-kCI3s/HXnXBPVcYCXIZfsdJBLRj77j5QyA9Hor1mC/nyD35D7e7N/VgR9awxqdGGWgIvgpjs6owXbGMk5t1pQA=="],
|
|
29
29
|
|
|
@@ -275,8 +275,6 @@
|
|
|
275
275
|
|
|
276
276
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
277
277
|
|
|
278
|
-
"@animalabs/context-manager/@animalabs/membrane": ["@animalabs/membrane@0.5.80", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-D4xEij+J/3sJo2ZKFyjAeb0VuJl3QmygzEmi44J0nDNtmJSpUPYMJMaSid/ABj3dIs5EyXds/Ltr4qqyQ2DRTw=="],
|
|
279
|
-
|
|
280
278
|
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
|
281
279
|
|
|
282
280
|
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/connectome-host",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "General-purpose agent TUI host with recipe-based configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"postinstall": "test -d web && npm --prefix web install && npm --prefix web run build || true"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@animalabs/agent-framework": "^0.
|
|
17
|
+
"@animalabs/agent-framework": "^0.13.0",
|
|
18
18
|
"@animalabs/chronicle": "^0.3.0",
|
|
19
|
-
"@animalabs/context-manager": "^0.
|
|
19
|
+
"@animalabs/context-manager": "^0.8.0",
|
|
20
20
|
"@animalabs/membrane": "^0.5.82",
|
|
21
21
|
"@opentui/core": "^0.1.82"
|
|
22
22
|
},
|
package/src/commands.ts
CHANGED
|
@@ -121,6 +121,35 @@ function getAgentCM(framework: AgentFramework, agentName?: string): ContextManag
|
|
|
121
121
|
return all[0]?.getContextManager() ?? null;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Refuse head-moving commands while the main agent's turn is in flight.
|
|
126
|
+
*
|
|
127
|
+
* Moving the Chronicle head (undo/redo/checkout/restore/branchto/newtopic)
|
|
128
|
+
* is not atomic with respect to a streaming generation: the in-flight reply
|
|
129
|
+
* commits onto whatever branch is current WHEN IT COMPLETES, so a head move
|
|
130
|
+
* mid-stream detaches the reply from its request — it lands on the new
|
|
131
|
+
* branch attached to the wrong parent (orphaned node), including when the
|
|
132
|
+
* move is issued from a second client on the same session. The supported
|
|
133
|
+
* sequence is: stop the generation, then move the head.
|
|
134
|
+
*
|
|
135
|
+
* Returns null when the command may proceed. Agents without a state field
|
|
136
|
+
* (stubs, minimal harnesses) are treated as idle.
|
|
137
|
+
*/
|
|
138
|
+
function inFlightGuard(app: AppContext, cmd: string): CommandResult | null {
|
|
139
|
+
const framework = app.framework;
|
|
140
|
+
const agent = (app.agentName ? framework.getAgent(app.agentName) : undefined)
|
|
141
|
+
?? framework.getAllAgents()[0];
|
|
142
|
+
const status = (agent as { state?: { status?: string } } | undefined)?.state?.status;
|
|
143
|
+
if (status === undefined || status === 'idle') return null;
|
|
144
|
+
return {
|
|
145
|
+
lines: [
|
|
146
|
+
{ text: `/${cmd} refused: a turn is in flight (${(agent as { name?: string }).name ?? 'agent'}: ${status}).`, style: 'system' },
|
|
147
|
+
{ text: ' Moving the head mid-generation would attach the streaming reply to the wrong', style: 'system' },
|
|
148
|
+
{ text: ' branch/request. Stop the generation first, then retry.', style: 'system' },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
125
154
|
const parts = command.slice(1).split(/\s+/);
|
|
126
155
|
const cmd = parts[0]!;
|
|
@@ -147,16 +176,16 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
147
176
|
{ text: '--- Commands ---', style: 'system' },
|
|
148
177
|
{ text: ' /quit, /q Exit the app', style: 'system' },
|
|
149
178
|
{ text: ' /status Show agent status', style: 'system' },
|
|
150
|
-
{ text: ' /clear Clear
|
|
179
|
+
{ text: ' /clear Clear this client\'s display (history/context are kept)', style: 'system' },
|
|
151
180
|
{ text: ' /lessons Show lesson library', style: 'system' },
|
|
152
181
|
{ text: ' /export Export lessons to ./output/ (JSON + markdown)', style: 'system' },
|
|
153
182
|
{ text: ' /undo Revert last agent turn', style: 'system' },
|
|
154
183
|
{ text: ' /redo Re-apply undone action', style: 'system' },
|
|
155
184
|
{ text: ' /nudge [agent] Run inference on current context (no new events)', style: 'system' },
|
|
156
185
|
{ text: ' /puppet <tool> [json] Admin: execute a tool AS the agent, store the pair', style: 'system' },
|
|
157
|
-
{ text: ' /checkpoint
|
|
158
|
-
{ text: ' /restore
|
|
159
|
-
{ text: ' /branches List Chronicle branches', style: 'system' },
|
|
186
|
+
{ text: ' /checkpoint [name] Save current state (no name: list checkpoints)', style: 'system' },
|
|
187
|
+
{ text: ' /restore [name] Restore to checkpoint (no name: list checkpoints)', style: 'system' },
|
|
188
|
+
{ text: ' /branches List Chronicle branches and checkpoints', style: 'system' },
|
|
160
189
|
{ text: ' /checkout <name> Switch to branch', style: 'system' },
|
|
161
190
|
{ text: ' /history [n] Show state transitions (last n)', style: 'system' },
|
|
162
191
|
{ text: ' /find <text> Search messages for text', style: 'system' },
|
|
@@ -170,9 +199,9 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
170
199
|
{ text: ' /session Show current session', style: 'system' },
|
|
171
200
|
{ text: ' /session list List all sessions', style: 'system' },
|
|
172
201
|
{ text: ' /session new [name] Create new session', style: 'system' },
|
|
173
|
-
{ text: ' /session switch <name> Switch to session', style: 'system' },
|
|
202
|
+
{ text: ' /session switch <name or id> Switch to session', style: 'system' },
|
|
174
203
|
{ text: ' /session rename <name> Rename current session', style: 'system' },
|
|
175
|
-
{ text: ' /session delete <name> Delete a session', style: 'system' },
|
|
204
|
+
{ text: ' /session delete <name or id> --confirm Delete a session (irreversible)', style: 'system' },
|
|
176
205
|
{ text: ' /recipe Show current recipe info', style: 'system' },
|
|
177
206
|
{ text: ' /newtopic [context] Reset head window (auto-summarize if empty)', style: 'system' },
|
|
178
207
|
{ text: ' /usage Show session token usage and costs', style: 'system' },
|
|
@@ -182,7 +211,12 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
182
211
|
};
|
|
183
212
|
|
|
184
213
|
case 'clear':
|
|
185
|
-
|
|
214
|
+
// Display-clearing is client-side: the TUI wipes its scrollback and
|
|
215
|
+
// the SPA wipes its transcript view before this handler is ever
|
|
216
|
+
// reached. This line is only seen by surfaces with no display to
|
|
217
|
+
// clear (headless), where it honestly reports that nothing else —
|
|
218
|
+
// Chronicle, context — was touched.
|
|
219
|
+
return { lines: [{ text: '(display cleared on clients; history and context are kept)', style: 'system' }] };
|
|
186
220
|
|
|
187
221
|
case 'status':
|
|
188
222
|
return handleStatus(framework);
|
|
@@ -194,7 +228,7 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
194
228
|
return handleExport(app);
|
|
195
229
|
|
|
196
230
|
case 'undo':
|
|
197
|
-
return handleUndo(app);
|
|
231
|
+
return inFlightGuard(app, cmd) ?? handleUndo(app);
|
|
198
232
|
|
|
199
233
|
case 'nudge':
|
|
200
234
|
return handleNudge(app, args[0]);
|
|
@@ -203,25 +237,29 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
203
237
|
return handlePuppet(app, args);
|
|
204
238
|
|
|
205
239
|
case 'redo':
|
|
206
|
-
return handleRedo(app);
|
|
240
|
+
return inFlightGuard(app, cmd) ?? handleRedo(app);
|
|
207
241
|
|
|
242
|
+
// Name-taking commands join the REST of the line, not just the first
|
|
243
|
+
// token: names may contain spaces (/checkpoint my test point), and
|
|
244
|
+
// /session rename already accepts multi-word names — parsing them
|
|
245
|
+
// differently made multi-word names silently truncate here.
|
|
208
246
|
case 'checkpoint':
|
|
209
|
-
return handleCheckpoint(app, args
|
|
247
|
+
return handleCheckpoint(app, args.join(' ') || undefined);
|
|
210
248
|
|
|
211
249
|
case 'restore':
|
|
212
|
-
return handleRestore(app, args
|
|
250
|
+
return inFlightGuard(app, cmd) ?? handleRestore(app, args.join(' ') || undefined);
|
|
213
251
|
|
|
214
252
|
case 'branches':
|
|
215
|
-
return handleBranches(
|
|
253
|
+
return handleBranches(app);
|
|
216
254
|
|
|
217
255
|
case 'checkout':
|
|
218
|
-
return handleCheckout(framework, args
|
|
256
|
+
return inFlightGuard(app, cmd) ?? handleCheckout(framework, args.join(' ') || undefined);
|
|
219
257
|
|
|
220
258
|
case 'history':
|
|
221
259
|
return handleHistory(framework, args[0]);
|
|
222
260
|
|
|
223
261
|
case 'branchto':
|
|
224
|
-
return handleBranchTo(app, args[0]);
|
|
262
|
+
return inFlightGuard(app, cmd) ?? handleBranchTo(app, args[0]);
|
|
225
263
|
|
|
226
264
|
case 'find':
|
|
227
265
|
return handleFind(framework, args.join(' '));
|
|
@@ -242,7 +280,7 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
242
280
|
return handleRecipe(app);
|
|
243
281
|
|
|
244
282
|
case 'newtopic':
|
|
245
|
-
return handleNewTopic(app, args);
|
|
283
|
+
return inFlightGuard(app, cmd) ?? handleNewTopic(app, args);
|
|
246
284
|
|
|
247
285
|
case 'usage':
|
|
248
286
|
return handleUsage(app);
|
|
@@ -310,12 +348,14 @@ function handleSession(app: AppContext, args: string[]): CommandResult {
|
|
|
310
348
|
return handleSessionNew(app, args.slice(1).join(' ') || undefined);
|
|
311
349
|
case 'switch':
|
|
312
350
|
case 'sw':
|
|
313
|
-
|
|
351
|
+
// Rest-of-line, matching rename: session names may contain spaces, and
|
|
352
|
+
// a session renamed to a multi-word name must stay reachable by name.
|
|
353
|
+
return handleSessionSwitch(app, args.slice(1).join(' ') || undefined);
|
|
314
354
|
case 'rename':
|
|
315
355
|
return handleSessionRename(app, args.slice(1).join(' ') || undefined);
|
|
316
356
|
case 'delete':
|
|
317
357
|
case 'rm':
|
|
318
|
-
return handleSessionDelete(app, args
|
|
358
|
+
return handleSessionDelete(app, args.slice(1));
|
|
319
359
|
default:
|
|
320
360
|
return { lines: [{ text: `Unknown /session subcommand: ${sub}. Try /session list.`, style: 'system' }] };
|
|
321
361
|
}
|
|
@@ -411,9 +451,17 @@ function handleSessionRename(app: AppContext, name?: string): CommandResult {
|
|
|
411
451
|
return { lines: [{ text: `Session renamed to "${name}".`, style: 'system' }] };
|
|
412
452
|
}
|
|
413
453
|
|
|
414
|
-
function handleSessionDelete(app: AppContext,
|
|
454
|
+
function handleSessionDelete(app: AppContext, args: string[]): CommandResult {
|
|
455
|
+
// Deletion is irreversible, so it takes an explicit second step: the bare
|
|
456
|
+
// command shows exactly what matched (name + id + message count) and asks
|
|
457
|
+
// for --confirm. This also defuses the truncated-name amplifier: a typo'd
|
|
458
|
+
// or partial name can match a DIFFERENT session, and without the echo the
|
|
459
|
+
// wrong one died silently.
|
|
460
|
+
const confirmed = args[args.length - 1] === '--confirm';
|
|
461
|
+
const nameOrId = (confirmed ? args.slice(0, -1) : args).join(' ') || undefined;
|
|
462
|
+
|
|
415
463
|
if (!nameOrId) {
|
|
416
|
-
return { lines: [{ text: 'Usage: /session delete <name or id>', style: 'system' }] };
|
|
464
|
+
return { lines: [{ text: 'Usage: /session delete <name or id> [--confirm]', style: 'system' }] };
|
|
417
465
|
}
|
|
418
466
|
|
|
419
467
|
const session = app.sessionManager.findSession(nameOrId);
|
|
@@ -421,6 +469,16 @@ function handleSessionDelete(app: AppContext, nameOrId?: string): CommandResult
|
|
|
421
469
|
return { lines: [{ text: `Session "${nameOrId}" not found.`, style: 'system' }] };
|
|
422
470
|
}
|
|
423
471
|
|
|
472
|
+
if (!confirmed) {
|
|
473
|
+
const msgs = session.messageCount !== undefined ? `, ${session.messageCount} msgs` : '';
|
|
474
|
+
return {
|
|
475
|
+
lines: [
|
|
476
|
+
{ text: `Will delete session "${session.name}" [${session.id}]${msgs} — irreversible.`, style: 'system' },
|
|
477
|
+
{ text: `To proceed: /session delete ${session.id} --confirm`, style: 'system' },
|
|
478
|
+
],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
424
482
|
try {
|
|
425
483
|
app.sessionManager.deleteSession(session.id);
|
|
426
484
|
return { lines: [{ text: `Deleted session "${session.name}" [${session.id}].`, style: 'system' }] };
|
|
@@ -526,14 +584,16 @@ function handleBudget(framework: AgentFramework, arg?: string): CommandResult {
|
|
|
526
584
|
}
|
|
527
585
|
|
|
528
586
|
if (!arg) {
|
|
529
|
-
// Show current budgets
|
|
587
|
+
// Show current budgets. fmtTokens is exact below 1000 — flooring to "0k"
|
|
588
|
+
// hid real small values and made the display contradict the validator
|
|
589
|
+
// (which rejects 0 but accepts 50).
|
|
530
590
|
const lines: Line[] = [{ text: '--- Stream Token Budgets ---', style: 'system' }];
|
|
531
591
|
for (const agent of agents) {
|
|
532
592
|
const budget = agent.maxStreamTokens;
|
|
533
593
|
const last = agent.lastStreamInputTokens;
|
|
534
594
|
const pct = budget > 0 ? ((last / budget) * 100).toFixed(0) : '—';
|
|
535
595
|
lines.push({
|
|
536
|
-
text: ` ${agent.name}: ${(budget
|
|
596
|
+
text: ` ${agent.name}: ${fmtTokens(budget)} (last: ${fmtTokens(last)}, ${pct}%)`,
|
|
537
597
|
style: 'system',
|
|
538
598
|
});
|
|
539
599
|
}
|
|
@@ -559,12 +619,8 @@ function handleBudget(framework: AgentFramework, arg?: string): CommandResult {
|
|
|
559
619
|
agent.maxStreamTokens = tokens;
|
|
560
620
|
}
|
|
561
621
|
|
|
562
|
-
const display = tokens >= 1_000_000
|
|
563
|
-
? `${(tokens / 1_000_000).toFixed(1)}m`
|
|
564
|
-
: `${(tokens / 1_000).toFixed(0)}k`;
|
|
565
|
-
|
|
566
622
|
return {
|
|
567
|
-
lines: [{ text: `Stream budget set to ${
|
|
623
|
+
lines: [{ text: `Stream budget set to ${fmtTokens(tokens)} tokens for all agents.`, style: 'system' }],
|
|
568
624
|
};
|
|
569
625
|
}
|
|
570
626
|
|
|
@@ -838,7 +894,17 @@ function handleRedo(app: AppContext): CommandResult {
|
|
|
838
894
|
|
|
839
895
|
function handleCheckpoint(app: AppContext, name?: string): CommandResult {
|
|
840
896
|
if (!name) {
|
|
841
|
-
|
|
897
|
+
// Bare /checkpoint lists what exists — same affordance as bare /restore —
|
|
898
|
+
// so the lifecycle is discoverable from either end.
|
|
899
|
+
const names = [...app.branchState.checkpoints.keys()];
|
|
900
|
+
return {
|
|
901
|
+
lines: [
|
|
902
|
+
{ text: 'Usage: /checkpoint <name>', style: 'system' },
|
|
903
|
+
...(names.length > 0
|
|
904
|
+
? [{ text: `Saved checkpoints: ${names.join(', ')}`, style: 'system' as const }]
|
|
905
|
+
: []),
|
|
906
|
+
],
|
|
907
|
+
};
|
|
842
908
|
}
|
|
843
909
|
|
|
844
910
|
const cm = getAgentCM(app.framework);
|
|
@@ -934,8 +1000,8 @@ function handleRestore(app: AppContext, name?: string): CommandResult {
|
|
|
934
1000
|
};
|
|
935
1001
|
}
|
|
936
1002
|
|
|
937
|
-
function handleBranches(
|
|
938
|
-
const cm = getAgentCM(framework);
|
|
1003
|
+
function handleBranches(app: AppContext): CommandResult {
|
|
1004
|
+
const cm = getAgentCM(app.framework);
|
|
939
1005
|
if (!cm) return { lines: [{ text: 'No agent context manager.', style: 'system' }] };
|
|
940
1006
|
|
|
941
1007
|
const branches = cm.listBranches();
|
|
@@ -950,6 +1016,19 @@ function handleBranches(framework: AgentFramework): CommandResult {
|
|
|
950
1016
|
});
|
|
951
1017
|
}
|
|
952
1018
|
|
|
1019
|
+
// Checkpoints are positions (branch + message), not branches — but they're
|
|
1020
|
+
// part of the same mental model, and being invisible here made the whole
|
|
1021
|
+
// checkpoint lifecycle run blind: created → not listed anywhere → restored
|
|
1022
|
+
// on faith. Session-scoped, in-memory (cleared on session switch/restart).
|
|
1023
|
+
const cps = [...app.branchState.checkpoints.entries()];
|
|
1024
|
+
if (cps.length > 0) {
|
|
1025
|
+
lines.push({ text: `--- Checkpoints (${cps.length}, this session) ---`, style: 'system' });
|
|
1026
|
+
for (const [name, point] of cps) {
|
|
1027
|
+
const at = point.messageId ? ` @ [${point.messageId}]` : '';
|
|
1028
|
+
lines.push({ text: ` ${name} → ${point.branchName}${at}`, style: 'system' });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
953
1032
|
return { lines };
|
|
954
1033
|
}
|
|
955
1034
|
|
|
@@ -1137,13 +1216,27 @@ function handleMcpAdd(args: string[]): CommandResult {
|
|
|
1137
1216
|
|
|
1138
1217
|
const [id, command, ...cmdArgs] = args;
|
|
1139
1218
|
const servers = readMcplServersFile(DEFAULT_CONFIG_PATH);
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1219
|
+
const prev = servers[id!];
|
|
1220
|
+
// Overwrite replaces the command line (command + args, which travel as one
|
|
1221
|
+
// unit) but PRESERVES env and other settings: env is edited via /mcp env
|
|
1222
|
+
// and the panel editor, whose contract is "cleared only by explicit empty
|
|
1223
|
+
// save" — a command update silently dropping tokens/settings broke working
|
|
1224
|
+
// servers in a way that only surfaced at next start.
|
|
1225
|
+
servers[id!] = {
|
|
1226
|
+
...prev,
|
|
1227
|
+
command: command!,
|
|
1228
|
+
...(cmdArgs.length > 0 ? { args: cmdArgs } : {}),
|
|
1229
|
+
};
|
|
1230
|
+
if (cmdArgs.length === 0) delete servers[id!]!.args;
|
|
1142
1231
|
saveMcplServers(DEFAULT_CONFIG_PATH, servers);
|
|
1143
1232
|
|
|
1233
|
+
const keptEnv = prev?.env ? Object.keys(prev.env) : [];
|
|
1144
1234
|
return {
|
|
1145
1235
|
lines: [
|
|
1146
|
-
{ text: `${
|
|
1236
|
+
{ text: `${prev ? 'Updated' : 'Added'} server "${id}". Restart to apply.`, style: 'system' },
|
|
1237
|
+
...(keptEnv.length > 0
|
|
1238
|
+
? [{ text: ` (kept env: ${keptEnv.join(', ')})`, style: 'system' as const }]
|
|
1239
|
+
: []),
|
|
1147
1240
|
],
|
|
1148
1241
|
};
|
|
1149
1242
|
}
|
|
@@ -113,5 +113,8 @@ export function buildFrameworkAgentConfig(
|
|
|
113
113
|
...(recipe.agent.proseRouting !== undefined
|
|
114
114
|
? { proseRouting: recipe.agent.proseRouting }
|
|
115
115
|
: {}),
|
|
116
|
+
...(recipe.agent.toolWrapperProseGuard !== undefined
|
|
117
|
+
? { toolWrapperProseGuard: recipe.agent.toolWrapperProseGuard }
|
|
118
|
+
: {}),
|
|
116
119
|
};
|
|
117
120
|
}
|
|
@@ -17,11 +17,16 @@ const PASSTHROUGH_KEYS: ReadonlyArray<keyof RecipeStrategy> = [
|
|
|
17
17
|
'compressionSourceOnlyFallback',
|
|
18
18
|
'compressionMergeSourceOnly',
|
|
19
19
|
'compressionMergeSourceOnlyFallback',
|
|
20
|
+
'compressionSplitFallback',
|
|
21
|
+
'compressionSplitPlaceholder',
|
|
22
|
+
'compressionSplitMaxCallsPerChunk',
|
|
23
|
+
'compressionSplitMaxCallsPer10Min',
|
|
20
24
|
'compressionRecallBudgetTokens',
|
|
21
25
|
'positionedRecallPairs',
|
|
22
26
|
'recallHeaderTemplate',
|
|
23
27
|
'targetChunkTokens',
|
|
24
28
|
'mergeThreshold',
|
|
29
|
+
'mergeMaxSourceSpanMessages',
|
|
25
30
|
'summaryTargetTokens',
|
|
26
31
|
'productionBudgetTokens',
|
|
27
32
|
'l1BudgetTokens',
|
package/src/gate-telemetry.ts
CHANGED
|
@@ -41,7 +41,11 @@ function envFlag(value: string | undefined): boolean {
|
|
|
41
41
|
export interface TurnTrigger {
|
|
42
42
|
reason: string;
|
|
43
43
|
source: string;
|
|
44
|
+
/** Routing locus of the turn (direct channel wakes set it). */
|
|
44
45
|
channelId?: string;
|
|
46
|
+
/** Telemetry-only channel of a gate-batched wake (agent-framework ≥0.14:
|
|
47
|
+
* InferenceRequest.wakeChannelId) — never a locus. */
|
|
48
|
+
wakeChannelId?: string;
|
|
45
49
|
counterparty?: string;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -60,7 +64,8 @@ export function originClass(trigger: TurnTrigger): string {
|
|
|
60
64
|
const s = trigger.source.toLowerCase();
|
|
61
65
|
if (r.includes('heartbeat') || s.includes('heartbeat')) return 'heartbeat';
|
|
62
66
|
if (r.includes('mail') || s.includes('mail')) return 'mail';
|
|
63
|
-
|
|
67
|
+
// channel/push events, directly or batched through the framework's EventGate
|
|
68
|
+
if (r === 'mcpl:channel-incoming' || r === 'mcpl:push-event' || r.startsWith('discord') || r.startsWith('gate:') || s === 'gate') return 'event';
|
|
64
69
|
// a person typing at the host itself: headless IPC, CLI, TUI, web UI, API
|
|
65
70
|
if (r === 'external-message' || ['headless', 'cli', 'tui', 'webui', 'api'].includes(s)) return 'operator';
|
|
66
71
|
if (r.includes('admin') || r.includes('nudge') || r.includes('unstick') || r.includes('operator')) return 'operator';
|
|
@@ -86,6 +91,29 @@ function attr(v: string | undefined): string | null {
|
|
|
86
91
|
return t.slice(0, 120);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Which agent's turn may be stamped onto a request, given that ONE provider
|
|
96
|
+
* adapter — and therefore one header hook — serves every agent in the
|
|
97
|
+
* process (primary, subconscious, forks, ephemerals) and the hook cannot
|
|
98
|
+
* tell whose request it is decorating. Rule: stamp the primary's trigger
|
|
99
|
+
* only while the primary is the ONLY agent with a turn in flight; any other
|
|
100
|
+
* agent mid-turn → withhold (null), never guess. Debt is per agent, not per
|
|
101
|
+
* request, so it may always be read from the primary.
|
|
102
|
+
*/
|
|
103
|
+
export function stampedTrigger(view: {
|
|
104
|
+
agents: string[];
|
|
105
|
+
primary?: string | null;
|
|
106
|
+
triggerOf: (agent: string) => TurnTrigger | null | undefined;
|
|
107
|
+
}): TurnTrigger | null {
|
|
108
|
+
const primary = view.primary ?? (view.agents.length === 1 ? view.agents[0] : undefined);
|
|
109
|
+
if (!primary) return null;
|
|
110
|
+
for (const a of view.agents) {
|
|
111
|
+
if (a !== primary && view.triggerOf(a)) return null;
|
|
112
|
+
}
|
|
113
|
+
const t = view.triggerOf(primary);
|
|
114
|
+
return t ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
export function gateTelemetryHeaders(
|
|
90
118
|
env: Record<string, string | undefined>,
|
|
91
119
|
pendingDebtChunks: () => number | null,
|
|
@@ -99,7 +127,7 @@ export function gateTelemetryHeaders(
|
|
|
99
127
|
const t = activeTrigger();
|
|
100
128
|
if (!t) return out;
|
|
101
129
|
out['x-gate-origin'] = originClass(t);
|
|
102
|
-
out['x-gate-channel'] = attr(t.channelId);
|
|
130
|
+
out['x-gate-channel'] = attr(t.channelId ?? t.wakeChannelId);
|
|
103
131
|
out['x-gate-counterparty'] = attr(t.counterparty);
|
|
104
132
|
return out;
|
|
105
133
|
};
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from '@animalabs/membrane';
|
|
29
29
|
import { LoggingAnthropicAdapter } from './logging-adapter.js';
|
|
30
30
|
import { LoggingProviderAdapter } from './logging-provider-wrapper.js';
|
|
31
|
-
import { gateTelemetryHeaders, type TurnTrigger } from './gate-telemetry.js';
|
|
31
|
+
import { gateTelemetryHeaders, stampedTrigger, type TurnTrigger } from './gate-telemetry.js';
|
|
32
32
|
import { LoggingBedrockAdapter } from './logging-bedrock-adapter.js';
|
|
33
33
|
import { CodexSubscriptionAdapter } from './codex-subscription-adapter.js';
|
|
34
34
|
import { CallLedger } from './call-ledger.js';
|
|
@@ -510,6 +510,9 @@ agents: [agentConfig],
|
|
|
510
510
|
// Client-side programmatic tool calling (code_execution) — recipe opt-in.
|
|
511
511
|
...(recipe.codeExecution ? { codeExecution: recipe.codeExecution } : {}),
|
|
512
512
|
...(conversations ? { conversations } : {}),
|
|
513
|
+
// Tune-out's subconscious resident (agent-framework#77) — recipe opt-in,
|
|
514
|
+
// passed through verbatim; the framework owns the defaults.
|
|
515
|
+
...(recipe.subconscious ? { subconscious: recipe.subconscious } : {}),
|
|
513
516
|
});
|
|
514
517
|
|
|
515
518
|
// Wire post-creation hooks
|
|
@@ -946,11 +949,22 @@ async function main() {
|
|
|
946
949
|
// multi-agent process (whose debt would we even claim?), no strategy — the
|
|
947
950
|
// header is simply not sent: an unstamped call is honest, a guessed one lies.
|
|
948
951
|
let appRefForDebt: AppContext | null = null;
|
|
952
|
+
// The resident whose turn/debt we stamp. TODO(agent-framework ≥0.14): use a
|
|
953
|
+
// public getPrimaryAgentName() accessor instead of the private field read.
|
|
954
|
+
const primaryAgent = (): { name: string; agent: unknown } | null => {
|
|
955
|
+
const fw = appRefForDebt?.framework;
|
|
956
|
+
const agents = fw?.getAllAgents() ?? [];
|
|
957
|
+
const name = (fw as unknown as { primaryAgentName?: string } | undefined)?.primaryAgentName
|
|
958
|
+
?? (agents.length === 1 ? agents[0]!.name : undefined);
|
|
959
|
+
if (!name) return null;
|
|
960
|
+
const agent = agents.find((a) => a.name === name);
|
|
961
|
+
return agent ? { name, agent } : null;
|
|
962
|
+
};
|
|
949
963
|
const pendingDebtChunks = (): number | null => {
|
|
950
964
|
try {
|
|
951
|
-
const
|
|
952
|
-
if (
|
|
953
|
-
const strategy = (
|
|
965
|
+
const p = primaryAgent();
|
|
966
|
+
if (!p) return null;
|
|
967
|
+
const strategy = (p.agent as unknown as {
|
|
954
968
|
getContextManager?: () => { getStrategy?: () => { getCompressionDebt?: () => unknown } };
|
|
955
969
|
}).getContextManager?.()?.getStrategy?.();
|
|
956
970
|
const d = strategy?.getCompressionDebt?.() as { pendingChunks?: unknown } | undefined;
|
|
@@ -967,10 +981,23 @@ async function main() {
|
|
|
967
981
|
// -> null -> the origin trio is simply not sent.
|
|
968
982
|
const activeTurnTrigger = (): TurnTrigger | null => {
|
|
969
983
|
try {
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
const
|
|
973
|
-
|
|
984
|
+
const fw = appRefForDebt?.framework;
|
|
985
|
+
if (!fw) return null;
|
|
986
|
+
const agents = fw.getAllAgents();
|
|
987
|
+
// ONE adapter serves every agent in this process, so this hook cannot
|
|
988
|
+
// tell whose request it is decorating: stamp the primary's trigger only
|
|
989
|
+
// while no other agent (subconscious, fork, ephemeral) has a turn in
|
|
990
|
+
// flight — see stampedTrigger().
|
|
991
|
+
const t = stampedTrigger({
|
|
992
|
+
agents: agents.map((a) => a.name),
|
|
993
|
+
primary: primaryAgent()?.name,
|
|
994
|
+
triggerOf: (name) => {
|
|
995
|
+
const r = fw.getActiveTurnTrigger(name) as
|
|
996
|
+
(ReturnType<typeof fw.getActiveTurnTrigger> & { wakeChannelId?: string }) | undefined;
|
|
997
|
+
return r ? { reason: r.reason, source: r.source, channelId: r.channelId, wakeChannelId: r.wakeChannelId, counterparty: r.counterparty } : null;
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
return t;
|
|
974
1001
|
} catch {
|
|
975
1002
|
return null;
|
|
976
1003
|
}
|