@animalabs/connectome-host 0.3.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/.env.example +20 -0
- package/.github/workflows/publish.yml +65 -0
- package/ARCHITECTURE.md +355 -0
- package/CHANGELOG.md +30 -0
- package/HEADLESS-FLEET-PLAN.md +330 -0
- package/README.md +189 -0
- package/UNIFIED-TREE-PLAN.md +242 -0
- package/bun.lock +288 -0
- package/docs/AGENT-MEMORY-GUIDE.md +214 -0
- package/docs/AGENT-ONBOARDING.md +541 -0
- package/docs/ATTENTION-AND-GATING.md +120 -0
- package/docs/DEPLOYMENTS.md +130 -0
- package/docs/DEV-ENVIRONMENT.md +215 -0
- package/docs/LIBRARY-PIPELINE.md +286 -0
- package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
- package/docs/claudeai-evacuation.md +228 -0
- package/docs/debug-context-api.md +208 -0
- package/docs/webui-deployment.md +219 -0
- package/package.json +33 -0
- package/recipes/SETUP.md +308 -0
- package/recipes/TRIUMVIRATE-SETUP.md +467 -0
- package/recipes/claude-export-revive.json +19 -0
- package/recipes/clerk.json +157 -0
- package/recipes/knowledge-miner.json +174 -0
- package/recipes/knowledge-reviewer.json +76 -0
- package/recipes/mcpl-editor-test.json +38 -0
- package/recipes/prompts/transplant-addendum.md +17 -0
- package/recipes/triumvirate.json +63 -0
- package/recipes/webui-fleet-test.json +27 -0
- package/recipes/webui-test.json +16 -0
- package/recipes/zulip-miner.json +87 -0
- package/scripts/connectome-doctor +373 -0
- package/scripts/evacuator.ts +956 -0
- package/scripts/import-claudeai-export.ts +747 -0
- package/scripts/lib/line-reader.ts +53 -0
- package/scripts/test-historical-thinking.ts +148 -0
- package/scripts/warmup-session.ts +336 -0
- package/src/agent-name.ts +58 -0
- package/src/commands.ts +1074 -0
- package/src/headless.ts +528 -0
- package/src/index.ts +867 -0
- package/src/logging-adapter.ts +208 -0
- package/src/mcpl-config.ts +199 -0
- package/src/modules/activity-module.ts +270 -0
- package/src/modules/channel-mode-module.ts +260 -0
- package/src/modules/fleet-module.ts +1705 -0
- package/src/modules/fleet-types.ts +225 -0
- package/src/modules/lessons-module.ts +465 -0
- package/src/modules/mcpl-admin-module.ts +345 -0
- package/src/modules/retrieval-module.ts +327 -0
- package/src/modules/settings-module.ts +143 -0
- package/src/modules/subagent-module.ts +2074 -0
- package/src/modules/subscription-gc-module.ts +322 -0
- package/src/modules/time-module.ts +85 -0
- package/src/modules/tui-module.ts +76 -0
- package/src/modules/web-ui-curve-page.ts +249 -0
- package/src/modules/web-ui-module.ts +2595 -0
- package/src/recipe.ts +1003 -0
- package/src/session-manager.ts +278 -0
- package/src/state/agent-tree-reducer.ts +431 -0
- package/src/state/fleet-tree-aggregator.ts +195 -0
- package/src/strategies/frontdesk-strategy.ts +364 -0
- package/src/synesthete.ts +68 -0
- package/src/tui.ts +2200 -0
- package/src/types/bun-ffi.d.ts +6 -0
- package/src/web/protocol.ts +648 -0
- package/test/agent-name.test.ts +62 -0
- package/test/agent-tree-fleet-launch.test.ts +64 -0
- package/test/agent-tree-reducer-parity.test.ts +194 -0
- package/test/agent-tree-reducer.test.ts +443 -0
- package/test/agent-tree-rollup.test.ts +109 -0
- package/test/autobio-progress-snapshot.test.ts +40 -0
- package/test/claudeai-export-importer.test.ts +386 -0
- package/test/evacuator.test.ts +332 -0
- package/test/fleet-commands.test.ts +244 -0
- package/test/fleet-no-subfleets.test.ts +147 -0
- package/test/fleet-orchestration.test.ts +353 -0
- package/test/fleet-recipe.test.ts +124 -0
- package/test/fleet-route.test.ts +44 -0
- package/test/fleet-smoke.test.ts +479 -0
- package/test/fleet-subscribe-union-e2e.test.ts +123 -0
- package/test/fleet-subscribe-union.test.ts +85 -0
- package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
- package/test/fleet-tree-aggregator.test.ts +215 -0
- package/test/frontdesk-strategy.test.ts +326 -0
- package/test/headless-describe.test.ts +182 -0
- package/test/headless-smoke.test.ts +190 -0
- package/test/logging-adapter.test.ts +87 -0
- package/test/mcpl-admin-module.test.ts +213 -0
- package/test/mcpl-agent-overlay.test.ts +126 -0
- package/test/mock-headless-child.ts +133 -0
- package/test/recipe-cache-ttl.test.ts +37 -0
- package/test/recipe-env.test.ts +157 -0
- package/test/recipe-path-resolution.test.ts +170 -0
- package/test/subagent-async-timeout.test.ts +381 -0
- package/test/subagent-fork-materialise.test.ts +241 -0
- package/test/subagent-peek-scoping.test.ts +180 -0
- package/test/subagent-peek-zombie.test.ts +148 -0
- package/test/subagent-reaper-in-flight.test.ts +229 -0
- package/test/subscription-gc-module.test.ts +136 -0
- package/test/warmup-handoff.test.ts +150 -0
- package/test/web-ui-bind.test.ts +51 -0
- package/test/web-ui-module.test.ts +246 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/tsconfig.json +14 -0
- package/web/bun.lock +357 -0
- package/web/index.html +12 -0
- package/web/package.json +24 -0
- package/web/src/App.tsx +1931 -0
- package/web/src/Context.tsx +158 -0
- package/web/src/ContextDocument.tsx +150 -0
- package/web/src/Files.tsx +271 -0
- package/web/src/Lessons.tsx +164 -0
- package/web/src/Mcpl.tsx +310 -0
- package/web/src/Stream.tsx +119 -0
- package/web/src/TreeSidebar.tsx +283 -0
- package/web/src/Usage.tsx +182 -0
- package/web/src/main.tsx +7 -0
- package/web/src/styles.css +26 -0
- package/web/src/tree.ts +268 -0
- package/web/src/wire.ts +120 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +32 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebUI wire protocol — JSON-over-WebSocket envelope for the web admin client.
|
|
3
|
+
*
|
|
4
|
+
* Both ends import these types so the messages stay in lockstep. The shape
|
|
5
|
+
* deliberately mirrors the fleet IPC (`fleet-types.ts`) so a future external
|
|
6
|
+
* aggregator can reuse the same parsers.
|
|
7
|
+
*
|
|
8
|
+
* Versioning: v1. Breaking changes bump `WEB_PROTOCOL_VERSION` and clients
|
|
9
|
+
* refuse to connect on mismatch.
|
|
10
|
+
*
|
|
11
|
+
* v1 (2026-07): windowed history (welcome carries only a tail window plus
|
|
12
|
+
* `history` metadata; older pages via request-history/history-page), ordered
|
|
13
|
+
* `blocks` on message entries (thinking / tool_use / tool_result / media
|
|
14
|
+
* surface in full), and `message-appended` live pushes of stored messages.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Line } from '../commands.js';
|
|
18
|
+
|
|
19
|
+
export const WEB_PROTOCOL_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Server → Client
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cold-start payload sent immediately after a client connects. Contains
|
|
27
|
+
* everything needed to render a usable view without any further roundtrips.
|
|
28
|
+
* Subsequent live updates arrive as `trace` / `child-event` / etc.
|
|
29
|
+
*/
|
|
30
|
+
export interface WelcomeMessage {
|
|
31
|
+
type: 'welcome';
|
|
32
|
+
protocolVersion: number;
|
|
33
|
+
recipe: {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
};
|
|
38
|
+
agents: Array<{ name: string; model: string }>;
|
|
39
|
+
session: {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
autoNamed: boolean;
|
|
43
|
+
};
|
|
44
|
+
branch: {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
};
|
|
48
|
+
/** Conversation history snapshot — the TAIL WINDOW only (see `history`).
|
|
49
|
+
* Clients fold live `trace` / `message-appended` events on top and page
|
|
50
|
+
* older messages via `request-history`. */
|
|
51
|
+
messages: WelcomeMessageEntry[];
|
|
52
|
+
/** Paging metadata for `messages`. `startIndex` is the store slot index of
|
|
53
|
+
* the first entry (== the number of older messages not shipped);
|
|
54
|
+
* `totalCount` is the store size at snapshot time. */
|
|
55
|
+
history: {
|
|
56
|
+
startIndex: number;
|
|
57
|
+
totalCount: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Parent-local agent tree snapshot. Shape matches AgentTreeSnapshot from
|
|
61
|
+
* `state/agent-tree-reducer.ts` — kept structurally typed here to avoid
|
|
62
|
+
* a cross-package import cycle and to let the client embed the same
|
|
63
|
+
* reducer with no transitive dependency on framework internals.
|
|
64
|
+
*/
|
|
65
|
+
localTree: {
|
|
66
|
+
asOfTs: number;
|
|
67
|
+
nodes: Array<Record<string, unknown>>;
|
|
68
|
+
callIdIndex: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Per-child snapshots when FleetModule + FleetTreeAggregator are mounted.
|
|
72
|
+
* Each entry is a child name plus its current AgentTreeSnapshot. Empty
|
|
73
|
+
* array when no fleet is configured.
|
|
74
|
+
*/
|
|
75
|
+
childTrees: Array<{
|
|
76
|
+
name: string;
|
|
77
|
+
asOfTs: number;
|
|
78
|
+
nodes: Array<Record<string, unknown>>;
|
|
79
|
+
callIdIndex: Record<string, string>;
|
|
80
|
+
/** Recipe summary for the child, parsed from its recipe file at fleet
|
|
81
|
+
* registration. Undefined when the recipe couldn't be loaded (e.g. URL
|
|
82
|
+
* recipe not yet fetched, or read error); SPA falls back to showing
|
|
83
|
+
* the child name only. */
|
|
84
|
+
recipe?: {
|
|
85
|
+
name: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
version?: string;
|
|
88
|
+
agentModel?: string;
|
|
89
|
+
};
|
|
90
|
+
}>;
|
|
91
|
+
/** Cumulative session-wide token usage at connect time. */
|
|
92
|
+
usage: TokenUsage;
|
|
93
|
+
/** Per-agent cost breakdown for the parent process, present when the
|
|
94
|
+
* framework's usage tracker has data. Empty during cold start. */
|
|
95
|
+
perAgentCost?: PerAgentCost[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One content block of a stored message, in original order. This is the
|
|
100
|
+
* viewer-facing projection of the membrane ContentBlock union: full
|
|
101
|
+
* interiority (thinking, tool calls AND their results) survives, while
|
|
102
|
+
* payloads are size-capped server-side (`truncated` flags) and media is
|
|
103
|
+
* reduced to a type placeholder.
|
|
104
|
+
*/
|
|
105
|
+
export type MessageBlock =
|
|
106
|
+
| { kind: 'text'; text: string; truncated?: boolean }
|
|
107
|
+
| { kind: 'thinking'; text: string; truncated?: boolean }
|
|
108
|
+
/** Provider-encrypted reasoning; only the payload size is surfaced. */
|
|
109
|
+
| { kind: 'redacted_thinking'; bytes: number }
|
|
110
|
+
| { kind: 'tool_use'; id: string; name: string; inputJson: string; truncated?: boolean }
|
|
111
|
+
| { kind: 'tool_result'; toolUseId: string; text: string; isError?: boolean; truncated?: boolean }
|
|
112
|
+
/** Image/document/audio/video or un-inflated blob_ref placeholder. */
|
|
113
|
+
| { kind: 'media'; mediaType: string };
|
|
114
|
+
|
|
115
|
+
export interface WelcomeMessageEntry {
|
|
116
|
+
/** Stable id from the message store; clients can use this for keys. */
|
|
117
|
+
id?: string;
|
|
118
|
+
/** Store slot index — the paging cursor. For a coalesced shard run this is
|
|
119
|
+
* the first shard's index. */
|
|
120
|
+
index: number;
|
|
121
|
+
participant: 'user' | 'assistant' | 'system' | 'tool';
|
|
122
|
+
/** Ordered content blocks — the full internal life of the message. */
|
|
123
|
+
blocks: MessageBlock[];
|
|
124
|
+
/** Flattened text content (text blocks only), kept as a convenience and
|
|
125
|
+
* for degraded rendering by stale clients. */
|
|
126
|
+
text: string;
|
|
127
|
+
/** Epoch-millis timestamp if available. */
|
|
128
|
+
timestamp?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Verbatim framework TraceEvent. Clients typically pipe these into the
|
|
132
|
+
* same `AgentTreeReducer` the server uses. */
|
|
133
|
+
export interface TraceMessage {
|
|
134
|
+
type: 'trace';
|
|
135
|
+
/** TraceEvent shape — discriminated by inner `type` field. */
|
|
136
|
+
event: { type: string; [k: string]: unknown };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Per-child WireEvent passthrough. Scoped by child name so the client
|
|
140
|
+
* can route into the right reducer. */
|
|
141
|
+
export interface ChildEventMessage {
|
|
142
|
+
type: 'child-event';
|
|
143
|
+
childName: string;
|
|
144
|
+
event: { type: string; [k: string]: unknown };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Output from a slash command — mirrors CommandResult from commands.ts. */
|
|
148
|
+
export interface CommandResultMessage {
|
|
149
|
+
type: 'command-result';
|
|
150
|
+
/** Echo of the corrId from the originating client `command` message. */
|
|
151
|
+
corrId?: string;
|
|
152
|
+
lines: Line[];
|
|
153
|
+
quit?: boolean;
|
|
154
|
+
branchChanged?: boolean;
|
|
155
|
+
switchToSessionId?: string;
|
|
156
|
+
/** True when an asyncWork follow-up is incoming as a separate command-result. */
|
|
157
|
+
pending?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Periodic token-usage update; mirrors TUI's right-side meter. */
|
|
161
|
+
export interface UsageMessage {
|
|
162
|
+
type: 'usage';
|
|
163
|
+
usage: TokenUsage;
|
|
164
|
+
perAgentCost?: PerAgentCost[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface TokenUsage {
|
|
168
|
+
input: number;
|
|
169
|
+
output: number;
|
|
170
|
+
cacheRead: number;
|
|
171
|
+
cacheWrite: number;
|
|
172
|
+
/** Estimated cost for this scope. Currency is provider-derived (USD for
|
|
173
|
+
* Anthropic). Optional because not every adapter / cached usage frame
|
|
174
|
+
* carries pricing data. */
|
|
175
|
+
cost?: { total: number; currency: string };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Per-agent cost slice used by the WebUI usage panel to label rows. Only
|
|
179
|
+
* surfaces parent-process agents — fleet-child agents track their own
|
|
180
|
+
* usage in their own UsageTracker and aren't aggregated cross-process. */
|
|
181
|
+
export interface PerAgentCost {
|
|
182
|
+
name: string;
|
|
183
|
+
cost: { total: number; currency: string };
|
|
184
|
+
inferenceCount: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Sent when the active branch changes (undo/redo/checkout). Clients should
|
|
188
|
+
* re-fetch their conversation by reconnecting or treating subsequent traces
|
|
189
|
+
* as authoritative. */
|
|
190
|
+
export interface BranchChangedMessage {
|
|
191
|
+
type: 'branch-changed';
|
|
192
|
+
branch: { id: string; name: string };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Sent when the active session changes. Clients should soft-reconnect. */
|
|
196
|
+
export interface SessionChangedMessage {
|
|
197
|
+
type: 'session-changed';
|
|
198
|
+
session: { id: string; name: string };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Live peek stream for one subagent (Phase 5). Multiplexed by `scope`. */
|
|
202
|
+
export interface PeekMessage {
|
|
203
|
+
type: 'peek';
|
|
204
|
+
scope: string;
|
|
205
|
+
event: { type: string; [k: string]: unknown };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Lesson library snapshot — response to RequestLessonsMessage. Empty array
|
|
209
|
+
* when LessonsModule isn't loaded, with `loaded: false` so the SPA can
|
|
210
|
+
* surface a "module not loaded" hint rather than appearing broken. */
|
|
211
|
+
export interface LessonsListMessage {
|
|
212
|
+
type: 'lessons-list';
|
|
213
|
+
loaded: boolean;
|
|
214
|
+
lessons: Array<{
|
|
215
|
+
id: string;
|
|
216
|
+
content: string;
|
|
217
|
+
confidence: number;
|
|
218
|
+
tags: string[];
|
|
219
|
+
deprecated: boolean;
|
|
220
|
+
deprecationReason?: string;
|
|
221
|
+
created?: number;
|
|
222
|
+
updated?: number;
|
|
223
|
+
}>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Workspace mount summary — response to request-workspace-mounts. */
|
|
227
|
+
export interface WorkspaceMountsMessage {
|
|
228
|
+
type: 'workspace-mounts';
|
|
229
|
+
/** True iff WorkspaceModule is loaded in the recipe. */
|
|
230
|
+
loaded: boolean;
|
|
231
|
+
mounts: Array<{
|
|
232
|
+
name: string;
|
|
233
|
+
/** Filesystem path the mount maps to. */
|
|
234
|
+
path: string;
|
|
235
|
+
/** 'read-only' | 'read-write' (or any other future mode). */
|
|
236
|
+
mode: string;
|
|
237
|
+
}>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Recursive file listing for one mount — response to request-workspace-tree. */
|
|
241
|
+
export interface WorkspaceTreeMessage {
|
|
242
|
+
type: 'workspace-tree';
|
|
243
|
+
mount: string;
|
|
244
|
+
entries: Array<{ path: string; size: number }>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** File content — response to request-workspace-file. Content is
|
|
248
|
+
* line-numbered (cat -n style) as returned by the workspace `read` tool. */
|
|
249
|
+
export interface WorkspaceFileMessage {
|
|
250
|
+
type: 'workspace-file';
|
|
251
|
+
path: string;
|
|
252
|
+
totalLines: number;
|
|
253
|
+
fromLine: number;
|
|
254
|
+
toLine: number;
|
|
255
|
+
content: string;
|
|
256
|
+
/** True if the response was capped at the line limit; the SPA can warn
|
|
257
|
+
* that the file is larger than what's shown. */
|
|
258
|
+
truncated: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** MCPL server config snapshot — response to request-mcpl, mcpl-add, etc.
|
|
262
|
+
* Sent only to the requesting client; mutations don't broadcast since
|
|
263
|
+
* changes are file-only and require restart anyway. */
|
|
264
|
+
export interface McplListMessage {
|
|
265
|
+
type: 'mcpl-list';
|
|
266
|
+
/** Path to the config file (informational — operator may want to grep
|
|
267
|
+
* for it locally). */
|
|
268
|
+
configPath: string;
|
|
269
|
+
servers: Array<{
|
|
270
|
+
id: string;
|
|
271
|
+
command: string;
|
|
272
|
+
args?: string[];
|
|
273
|
+
env?: Record<string, string>;
|
|
274
|
+
toolPrefix?: string;
|
|
275
|
+
reconnect?: boolean;
|
|
276
|
+
enabledFeatureSets?: string[];
|
|
277
|
+
disabledFeatureSets?: string[];
|
|
278
|
+
}>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** A page of older history — response to `request-history`, routed only to
|
|
282
|
+
* the requesting client and correlated by `corrId`. */
|
|
283
|
+
export interface HistoryPageMessage {
|
|
284
|
+
type: 'history-page';
|
|
285
|
+
corrId: string;
|
|
286
|
+
/** Entries in store order (oldest first), ending just before the
|
|
287
|
+
* requested `beforeIndex`. */
|
|
288
|
+
entries: WelcomeMessageEntry[];
|
|
289
|
+
/** Slot index of the first entry — the next page's `beforeIndex`. */
|
|
290
|
+
startIndex: number;
|
|
291
|
+
/** Store size at read time. */
|
|
292
|
+
totalCount: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** A message was appended to the store (any participant — including
|
|
296
|
+
* assistant turns and tool results, which never surface as trace
|
|
297
|
+
* `message:added` events). Clients dedupe by `entry.id` against messages
|
|
298
|
+
* they already rendered optimistically or via streaming. */
|
|
299
|
+
export interface MessageAppendedMessage {
|
|
300
|
+
type: 'message-appended';
|
|
301
|
+
entry: WelcomeMessageEntry;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Server-side error response. Non-fatal; the client stays connected. */
|
|
305
|
+
export interface ErrorMessage {
|
|
306
|
+
type: 'error';
|
|
307
|
+
/** corrId echo if the error was triggered by a specific request. */
|
|
308
|
+
corrId?: string;
|
|
309
|
+
message: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* /quit was issued while one or more fleet children are still running.
|
|
314
|
+
* The server holds the shutdown until the operator confirms what to do
|
|
315
|
+
* with them. Mirrors the TUI's three-way prompt — kill/cancel/detach.
|
|
316
|
+
*/
|
|
317
|
+
export interface QuitConfirmRequiredMessage {
|
|
318
|
+
type: 'quit-confirm-required';
|
|
319
|
+
/** Names of fleet children currently in 'ready' or 'starting' state. */
|
|
320
|
+
children: string[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* An external message just landed in the agent's context — e.g. an MCPL push
|
|
325
|
+
* (zulip notification, etc.) or channel incoming. The TUI quietly switches to
|
|
326
|
+
* "thinking" when this happens; the WebUI surfaces it as a labeled box so the
|
|
327
|
+
* operator can see *why* the agent is suddenly active.
|
|
328
|
+
*
|
|
329
|
+
* Not emitted for inputs typed in the WebUI itself (those are already
|
|
330
|
+
* optimistically rendered as a user message).
|
|
331
|
+
*/
|
|
332
|
+
export interface InboundTriggerMessage {
|
|
333
|
+
type: 'inbound-trigger';
|
|
334
|
+
/** Trace-event source field — e.g. 'mcpl:channel-incoming', 'mcpl:push-event'. */
|
|
335
|
+
source: string;
|
|
336
|
+
/** Human-readable origin label like 'zulip#general' or 'discord/myserver'. */
|
|
337
|
+
origin: string;
|
|
338
|
+
/** Did this message wake the agent up? When false the message landed but
|
|
339
|
+
* the gate filtered it out (no inference triggered). */
|
|
340
|
+
triggered: boolean;
|
|
341
|
+
/** Author display name where applicable. */
|
|
342
|
+
author?: string;
|
|
343
|
+
/** Brief text excerpt — capped server-side to keep the wire frame small. */
|
|
344
|
+
text: string;
|
|
345
|
+
/** Server time when the message was added. */
|
|
346
|
+
timestamp: number;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export type WebUiServerMessage =
|
|
350
|
+
| WelcomeMessage
|
|
351
|
+
| TraceMessage
|
|
352
|
+
| ChildEventMessage
|
|
353
|
+
| CommandResultMessage
|
|
354
|
+
| UsageMessage
|
|
355
|
+
| BranchChangedMessage
|
|
356
|
+
| SessionChangedMessage
|
|
357
|
+
| PeekMessage
|
|
358
|
+
| InboundTriggerMessage
|
|
359
|
+
| QuitConfirmRequiredMessage
|
|
360
|
+
| LessonsListMessage
|
|
361
|
+
| McplListMessage
|
|
362
|
+
| WorkspaceMountsMessage
|
|
363
|
+
| WorkspaceTreeMessage
|
|
364
|
+
| WorkspaceFileMessage
|
|
365
|
+
| HistoryPageMessage
|
|
366
|
+
| MessageAppendedMessage
|
|
367
|
+
| ErrorMessage;
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Client → Server
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
/** Plain user message; equivalent to typing in the TUI. Triggers inference. */
|
|
374
|
+
export interface UserMessageMessage {
|
|
375
|
+
type: 'user-message';
|
|
376
|
+
content: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Slash command. Server runs `handleCommand()` and replies with a
|
|
380
|
+
* `command-result` carrying the same `corrId`. */
|
|
381
|
+
export interface CommandMessage {
|
|
382
|
+
type: 'command';
|
|
383
|
+
command: string;
|
|
384
|
+
corrId?: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** @childname routing for fleet children; bypasses the conductor agent. */
|
|
388
|
+
export interface RouteToChildMessage {
|
|
389
|
+
type: 'route-to-child';
|
|
390
|
+
childName: string;
|
|
391
|
+
content: string;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Cancel any in-flight inference (Esc parity in the TUI). */
|
|
395
|
+
export interface InterruptMessage {
|
|
396
|
+
type: 'interrupt';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Cancel one specific in-process subagent by display name. If `childName`
|
|
400
|
+
* is set, the cancel is forwarded to that fleet child's headless runtime;
|
|
401
|
+
* otherwise the conductor's local SubagentModule is used. Required for
|
|
402
|
+
* subagents that live inside a fleet child (e.g. Clerk-spawned subagents) —
|
|
403
|
+
* the conductor typically has no SubagentModule loaded, so a non-routed
|
|
404
|
+
* cancel would hit "subagent module not loaded". */
|
|
405
|
+
export interface CancelSubagentMessage {
|
|
406
|
+
type: 'cancel-subagent';
|
|
407
|
+
name: string;
|
|
408
|
+
childName?: string;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Stop a fleet child gracefully. */
|
|
412
|
+
export interface FleetStopMessage {
|
|
413
|
+
type: 'fleet-stop';
|
|
414
|
+
name: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Restart a fleet child. */
|
|
418
|
+
export interface FleetRestartMessage {
|
|
419
|
+
type: 'fleet-restart';
|
|
420
|
+
name: string;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Open or close a peek window for a specific subagent or fleet child. */
|
|
424
|
+
export interface SubscribePeekMessage {
|
|
425
|
+
type: 'subscribe-peek';
|
|
426
|
+
scope: string;
|
|
427
|
+
/** True to open, false to close. */
|
|
428
|
+
active: boolean;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Keepalive. Server replies with nothing; the round-trip is the proof. */
|
|
432
|
+
export interface PingMessage {
|
|
433
|
+
type: 'ping';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Operator's response to a quit-confirm-required prompt. The action mirrors
|
|
438
|
+
* the TUI's [Y/n/d] choices: kill the children gracefully and exit, leave
|
|
439
|
+
* them running and exit anyway, or cancel quit altogether.
|
|
440
|
+
*/
|
|
441
|
+
export interface QuitConfirmMessage {
|
|
442
|
+
type: 'quit-confirm';
|
|
443
|
+
action: 'kill-children' | 'detach' | 'cancel';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Pull a lesson library — defaults to the parent process; pass a fleet
|
|
447
|
+
* child name in `scope` to query that child instead. The response is a
|
|
448
|
+
* `lessons-list` envelope routed only to the requesting client (children
|
|
449
|
+
* don't broadcast). */
|
|
450
|
+
export interface RequestLessonsMessage {
|
|
451
|
+
type: 'request-lessons';
|
|
452
|
+
/** Fleet child name, or 'local'/undefined for the parent process. */
|
|
453
|
+
scope?: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Pull the configured MCPL servers from mcpl-servers.json. Response is an
|
|
457
|
+
* `mcpl-list` envelope. Recipe-defined servers are excluded — those live in
|
|
458
|
+
* the recipe file and aren't editable from here. */
|
|
459
|
+
export interface RequestMcplMessage {
|
|
460
|
+
type: 'request-mcpl';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Add or overwrite an MCPL server entry in mcpl-servers.json. Restart is
|
|
464
|
+
* required for the host to pick up the change; the response is a fresh
|
|
465
|
+
* `mcpl-list` so the SPA reflects the new state. */
|
|
466
|
+
export interface McplAddMessage {
|
|
467
|
+
type: 'mcpl-add';
|
|
468
|
+
id: string;
|
|
469
|
+
command: string;
|
|
470
|
+
args?: string[];
|
|
471
|
+
env?: Record<string, string>;
|
|
472
|
+
toolPrefix?: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface McplRemoveMessage {
|
|
476
|
+
type: 'mcpl-remove';
|
|
477
|
+
id: string;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface McplSetEnvMessage {
|
|
481
|
+
type: 'mcpl-set-env';
|
|
482
|
+
id: string;
|
|
483
|
+
/** Replaces the existing env block in full. Empty object clears it. */
|
|
484
|
+
env: Record<string, string>;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Pull the list of workspace mounts. Response is `workspace-mounts`.
|
|
488
|
+
* Optional `scope` selects a fleet child instead of the parent. */
|
|
489
|
+
export interface RequestWorkspaceMountsMessage {
|
|
490
|
+
type: 'request-workspace-mounts';
|
|
491
|
+
scope?: string;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Pull a recursive flat listing of files in one mount. Response is
|
|
495
|
+
* `workspace-tree`. The flat shape mirrors what WorkspaceModule's `ls`
|
|
496
|
+
* tool returns; the SPA folds it into a hierarchy locally. */
|
|
497
|
+
export interface RequestWorkspaceTreeMessage {
|
|
498
|
+
type: 'request-workspace-tree';
|
|
499
|
+
mount: string;
|
|
500
|
+
scope?: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Read a workspace file, capped to N lines so the wire frame stays small.
|
|
504
|
+
* Response is `workspace-file`. */
|
|
505
|
+
export interface RequestWorkspaceFileMessage {
|
|
506
|
+
type: 'request-workspace-file';
|
|
507
|
+
/** Mount-prefixed path (e.g. "tickets/2026-05-06-foo.md"). */
|
|
508
|
+
path: string;
|
|
509
|
+
scope?: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Page older conversation history. Response is a `history-page` with the
|
|
513
|
+
* same `corrId` (or an `error`). Entries returned end just before
|
|
514
|
+
* `beforeIndex`; pass the previous page's `startIndex` to keep walking
|
|
515
|
+
* backward. */
|
|
516
|
+
export interface RequestHistoryMessage {
|
|
517
|
+
type: 'request-history';
|
|
518
|
+
corrId: string;
|
|
519
|
+
/** Fetch messages strictly before this store slot index. */
|
|
520
|
+
beforeIndex: number;
|
|
521
|
+
/** Page size; server clamps to its maximum (500). */
|
|
522
|
+
limit?: number;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export type WebUiClientMessage =
|
|
526
|
+
| UserMessageMessage
|
|
527
|
+
| RequestHistoryMessage
|
|
528
|
+
| CommandMessage
|
|
529
|
+
| RouteToChildMessage
|
|
530
|
+
| InterruptMessage
|
|
531
|
+
| CancelSubagentMessage
|
|
532
|
+
| FleetStopMessage
|
|
533
|
+
| FleetRestartMessage
|
|
534
|
+
| SubscribePeekMessage
|
|
535
|
+
| QuitConfirmMessage
|
|
536
|
+
| RequestLessonsMessage
|
|
537
|
+
| RequestMcplMessage
|
|
538
|
+
| McplAddMessage
|
|
539
|
+
| McplRemoveMessage
|
|
540
|
+
| McplSetEnvMessage
|
|
541
|
+
| RequestWorkspaceMountsMessage
|
|
542
|
+
| RequestWorkspaceTreeMessage
|
|
543
|
+
| RequestWorkspaceFileMessage
|
|
544
|
+
| PingMessage;
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// Helpers
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Type guard for narrowing parsed JSON to a known client message. Per-variant
|
|
552
|
+
* payload validation lives here so handlers downstream can trust field
|
|
553
|
+
* shapes — without this, a malformed payload like `{type:'mcpl-add', id: 42,
|
|
554
|
+
* command: null}` would slip through and propagate into disk writes / file
|
|
555
|
+
* paths / spawn() args, where the eventual failure is far from the cause.
|
|
556
|
+
*/
|
|
557
|
+
export function isClientMessage(value: unknown): value is WebUiClientMessage {
|
|
558
|
+
if (!value || typeof value !== 'object') return false;
|
|
559
|
+
const v = value as Record<string, unknown>;
|
|
560
|
+
if (typeof v.type !== 'string') return false;
|
|
561
|
+
switch (v.type) {
|
|
562
|
+
case 'ping':
|
|
563
|
+
case 'interrupt':
|
|
564
|
+
case 'request-mcpl':
|
|
565
|
+
return true;
|
|
566
|
+
case 'user-message':
|
|
567
|
+
return typeof v.content === 'string';
|
|
568
|
+
case 'command':
|
|
569
|
+
return typeof v.command === 'string'
|
|
570
|
+
&& (v.corrId === undefined || typeof v.corrId === 'string');
|
|
571
|
+
case 'route-to-child':
|
|
572
|
+
return isNonEmptyString(v.childName) && typeof v.content === 'string';
|
|
573
|
+
case 'cancel-subagent':
|
|
574
|
+
return isNonEmptyString(v.name)
|
|
575
|
+
&& (v.childName === undefined || isNonEmptyString(v.childName));
|
|
576
|
+
case 'fleet-stop':
|
|
577
|
+
case 'fleet-restart':
|
|
578
|
+
return isNonEmptyString(v.name);
|
|
579
|
+
case 'subscribe-peek':
|
|
580
|
+
return typeof v.scope === 'string' && typeof v.active === 'boolean';
|
|
581
|
+
case 'quit-confirm':
|
|
582
|
+
return v.action === 'kill-children' || v.action === 'detach' || v.action === 'cancel';
|
|
583
|
+
case 'request-lessons':
|
|
584
|
+
return v.scope === undefined || typeof v.scope === 'string';
|
|
585
|
+
case 'mcpl-add':
|
|
586
|
+
return isValidMcplId(v.id)
|
|
587
|
+
&& isNonEmptyString(v.command)
|
|
588
|
+
&& isOptionalStringArray(v.args)
|
|
589
|
+
&& isOptionalStringMap(v.env)
|
|
590
|
+
&& (v.toolPrefix === undefined || isNonEmptyString(v.toolPrefix));
|
|
591
|
+
case 'mcpl-remove':
|
|
592
|
+
return isValidMcplId(v.id);
|
|
593
|
+
case 'mcpl-set-env':
|
|
594
|
+
return isValidMcplId(v.id) && isStringMap(v.env);
|
|
595
|
+
case 'request-workspace-mounts':
|
|
596
|
+
return v.scope === undefined || typeof v.scope === 'string';
|
|
597
|
+
case 'request-workspace-tree':
|
|
598
|
+
return isNonEmptyString(v.mount)
|
|
599
|
+
&& (v.scope === undefined || typeof v.scope === 'string');
|
|
600
|
+
case 'request-workspace-file':
|
|
601
|
+
return isNonEmptyString(v.path)
|
|
602
|
+
&& (v.scope === undefined || typeof v.scope === 'string');
|
|
603
|
+
case 'request-history':
|
|
604
|
+
return isNonEmptyString(v.corrId)
|
|
605
|
+
&& Number.isInteger(v.beforeIndex) && (v.beforeIndex as number) >= 0
|
|
606
|
+
&& (v.limit === undefined
|
|
607
|
+
|| (Number.isInteger(v.limit) && (v.limit as number) >= 1 && (v.limit as number) <= 500));
|
|
608
|
+
default:
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
614
|
+
return typeof v === 'string' && v.length > 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** MCPL config keys end up as filesystem-adjacent identifiers (the spec
|
|
618
|
+
* treats them as opaque strings, but they're surfaced in error paths and
|
|
619
|
+
* may flow into other tooling). Reject path separators, control bytes, and
|
|
620
|
+
* anything that looks like it would escape JSON-key territory. */
|
|
621
|
+
function isValidMcplId(v: unknown): v is string {
|
|
622
|
+
if (typeof v !== 'string' || v.length === 0 || v.length > 128) return false;
|
|
623
|
+
if (v.includes('/') || v.includes('\\') || v.includes('\0')) return false;
|
|
624
|
+
// Control chars are a robustness hole — reject 0x00-0x1f and 0x7f.
|
|
625
|
+
for (let i = 0; i < v.length; i++) {
|
|
626
|
+
const c = v.charCodeAt(i);
|
|
627
|
+
if (c < 0x20 || c === 0x7f) return false;
|
|
628
|
+
}
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function isStringMap(v: unknown): v is Record<string, string> {
|
|
633
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
634
|
+
for (const val of Object.values(v as Record<string, unknown>)) {
|
|
635
|
+
if (typeof val !== 'string') return false;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isOptionalStringMap(v: unknown): v is Record<string, string> | undefined {
|
|
641
|
+
return v === undefined || isStringMap(v);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function isOptionalStringArray(v: unknown): v is string[] | undefined {
|
|
645
|
+
if (v === undefined) return true;
|
|
646
|
+
if (!Array.isArray(v)) return false;
|
|
647
|
+
return v.every((x) => typeof x === 'string');
|
|
648
|
+
}
|