@wrongstack/cli 0.1.4 → 0.1.8
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 +98 -0
- package/dist/index.js +1020 -119
- package/dist/index.js.map +1 -1
- package/package.json +15 -8
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI,
|
|
2
|
+
import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultModeStore, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, atomicWrite, DefaultSessionReader, makeAgentSubagentRunner, Director, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
4
|
import * as fs6 from 'fs/promises';
|
|
4
5
|
import { writeFileSync } from 'fs';
|
|
5
6
|
import { createRequire } from 'module';
|
|
@@ -34,6 +35,506 @@ var init_plugin_api_factory = __esm({
|
|
|
34
35
|
"src/plugin-api-factory.ts"() {
|
|
35
36
|
}
|
|
36
37
|
});
|
|
38
|
+
|
|
39
|
+
// src/webui-server.ts
|
|
40
|
+
var webui_server_exports = {};
|
|
41
|
+
__export(webui_server_exports, {
|
|
42
|
+
runWebUI: () => runWebUI
|
|
43
|
+
});
|
|
44
|
+
async function runWebUI(opts) {
|
|
45
|
+
const port = opts.port ?? 3457;
|
|
46
|
+
const clients = /* @__PURE__ */ new Map();
|
|
47
|
+
let abortController = null;
|
|
48
|
+
const wss = new WebSocketServer({ port });
|
|
49
|
+
console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
|
|
50
|
+
const eventUnsubscribers = [];
|
|
51
|
+
function setupEvents() {
|
|
52
|
+
for (const unsub of eventUnsubscribers) unsub();
|
|
53
|
+
eventUnsubscribers.length = 0;
|
|
54
|
+
eventUnsubscribers.push(
|
|
55
|
+
opts.events.on("iteration.started", (e) => {
|
|
56
|
+
broadcast({
|
|
57
|
+
type: "iteration.started",
|
|
58
|
+
payload: { index: e.index }
|
|
59
|
+
});
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
eventUnsubscribers.push(
|
|
63
|
+
opts.events.on("provider.text_delta", (e) => {
|
|
64
|
+
broadcast({
|
|
65
|
+
type: "provider.text_delta",
|
|
66
|
+
payload: { text: e.text, messageId: "current" }
|
|
67
|
+
});
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
eventUnsubscribers.push(
|
|
71
|
+
opts.events.on("tool.started", (e) => {
|
|
72
|
+
broadcast({
|
|
73
|
+
type: "tool.started",
|
|
74
|
+
payload: {
|
|
75
|
+
id: e.id,
|
|
76
|
+
name: e.name,
|
|
77
|
+
input: e.input,
|
|
78
|
+
messageId: `tool_${e.id}`
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
eventUnsubscribers.push(
|
|
84
|
+
opts.events.on("tool.progress", (e) => {
|
|
85
|
+
broadcast({
|
|
86
|
+
type: "tool.progress",
|
|
87
|
+
payload: {
|
|
88
|
+
name: e.name,
|
|
89
|
+
id: e.id,
|
|
90
|
+
event: e.event
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
eventUnsubscribers.push(
|
|
96
|
+
opts.events.on("tool.executed", (e) => {
|
|
97
|
+
broadcast({
|
|
98
|
+
type: "tool.executed",
|
|
99
|
+
payload: {
|
|
100
|
+
// Forward the tool_use id so the WebUI can correlate this with
|
|
101
|
+
// the matching tool.started bubble for parallel tool calls.
|
|
102
|
+
id: e.id,
|
|
103
|
+
name: e.name,
|
|
104
|
+
durationMs: e.durationMs,
|
|
105
|
+
ok: e.ok,
|
|
106
|
+
input: e.input,
|
|
107
|
+
output: e.output
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
eventUnsubscribers.push(
|
|
113
|
+
opts.events.on("provider.response", (e) => {
|
|
114
|
+
broadcast({
|
|
115
|
+
type: "provider.response",
|
|
116
|
+
payload: {
|
|
117
|
+
usage: e.usage,
|
|
118
|
+
stopReason: e.stopReason,
|
|
119
|
+
messageId: "current"
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
eventUnsubscribers.push(
|
|
125
|
+
opts.events.on("error", (e) => {
|
|
126
|
+
broadcast({
|
|
127
|
+
type: "error",
|
|
128
|
+
payload: {
|
|
129
|
+
phase: e.phase,
|
|
130
|
+
message: e.err instanceof Error ? e.err.message : String(e.err)
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return new Promise((resolve3) => {
|
|
137
|
+
wss.on("listening", () => {
|
|
138
|
+
console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
|
|
139
|
+
setupEvents();
|
|
140
|
+
});
|
|
141
|
+
wss.on("connection", (ws) => {
|
|
142
|
+
const client = { ws, sessionId: opts.session.id };
|
|
143
|
+
clients.set(ws, client);
|
|
144
|
+
console.log("[WebUI] Client connected");
|
|
145
|
+
ws.on("message", async (data) => {
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(data.toString());
|
|
148
|
+
await handleMessage(ws, client, msg);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[WebUI] Failed to parse message", err);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
ws.on("close", () => {
|
|
154
|
+
console.log("[WebUI] Client disconnected");
|
|
155
|
+
clients.delete(ws);
|
|
156
|
+
});
|
|
157
|
+
send(ws, {
|
|
158
|
+
type: "session.start",
|
|
159
|
+
payload: {
|
|
160
|
+
sessionId: opts.session.id,
|
|
161
|
+
model: opts.agent.ctx.model,
|
|
162
|
+
provider: opts.agent.ctx.provider.id
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
wss.on("error", (err) => {
|
|
167
|
+
console.error("[WebUI] Server error:", err);
|
|
168
|
+
});
|
|
169
|
+
function shutdown() {
|
|
170
|
+
console.log("[WebUI] Shutting down...");
|
|
171
|
+
for (const unsub of eventUnsubscribers) unsub();
|
|
172
|
+
for (const [ws] of clients) {
|
|
173
|
+
ws.close();
|
|
174
|
+
}
|
|
175
|
+
clients.clear();
|
|
176
|
+
wss.close(() => {
|
|
177
|
+
console.log("[WebUI] Server stopped");
|
|
178
|
+
resolve3();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
process.on("SIGINT", shutdown);
|
|
182
|
+
process.on("SIGTERM", shutdown);
|
|
183
|
+
});
|
|
184
|
+
async function handleMessage(ws, client, msg) {
|
|
185
|
+
switch (msg.type) {
|
|
186
|
+
case "user_message":
|
|
187
|
+
await handleUserMessage(ws, client, msg.payload.content);
|
|
188
|
+
break;
|
|
189
|
+
case "abort":
|
|
190
|
+
abortController?.abort();
|
|
191
|
+
broadcast({
|
|
192
|
+
type: "error",
|
|
193
|
+
payload: { phase: "abort", message: "User aborted" }
|
|
194
|
+
});
|
|
195
|
+
break;
|
|
196
|
+
case "ping":
|
|
197
|
+
send(ws, { type: "pong", payload: {} });
|
|
198
|
+
break;
|
|
199
|
+
case "providers.list":
|
|
200
|
+
await handleProvidersList(ws);
|
|
201
|
+
break;
|
|
202
|
+
case "provider.models":
|
|
203
|
+
await handleProviderModels(ws, msg.payload.providerId);
|
|
204
|
+
break;
|
|
205
|
+
case "providers.saved":
|
|
206
|
+
await handleProvidersSaved(ws);
|
|
207
|
+
break;
|
|
208
|
+
case "key.add":
|
|
209
|
+
case "key.update": {
|
|
210
|
+
const m = msg;
|
|
211
|
+
await handleKeyUpsert(ws, m.payload.providerId, m.payload.label, m.payload.apiKey);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "key.delete": {
|
|
215
|
+
const m = msg;
|
|
216
|
+
await handleKeyDelete(ws, m.payload.providerId, m.payload.label);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "key.set_active": {
|
|
220
|
+
const m = msg;
|
|
221
|
+
await handleKeySetActive(ws, m.payload.providerId, m.payload.label);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "provider.add": {
|
|
225
|
+
const m = msg;
|
|
226
|
+
await handleProviderAdd(ws, m.payload);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "provider.remove": {
|
|
230
|
+
const m = msg;
|
|
231
|
+
await handleProviderRemove(ws, m.payload.providerId);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function handleUserMessage(ws, client, content) {
|
|
237
|
+
abortController?.abort();
|
|
238
|
+
abortController = new AbortController();
|
|
239
|
+
try {
|
|
240
|
+
const result = await opts.agent.run(content, {
|
|
241
|
+
signal: abortController.signal
|
|
242
|
+
});
|
|
243
|
+
send(ws, {
|
|
244
|
+
type: "run.result",
|
|
245
|
+
payload: {
|
|
246
|
+
status: result.status,
|
|
247
|
+
iterations: result.iterations,
|
|
248
|
+
finalText: result.finalText,
|
|
249
|
+
error: result.error ? {
|
|
250
|
+
code: result.error.code,
|
|
251
|
+
message: result.error.message,
|
|
252
|
+
recoverable: result.error.recoverable
|
|
253
|
+
} : void 0
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
send(ws, {
|
|
258
|
+
type: "error",
|
|
259
|
+
payload: {
|
|
260
|
+
phase: "agent.run",
|
|
261
|
+
message: err instanceof Error ? err.message : String(err)
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
} finally {
|
|
265
|
+
abortController = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function send(ws, msg) {
|
|
269
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
270
|
+
ws.send(JSON.stringify(msg));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function broadcast(msg) {
|
|
274
|
+
const data = JSON.stringify(msg);
|
|
275
|
+
for (const [ws] of clients) {
|
|
276
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
277
|
+
ws.send(data);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function handleProvidersList(ws) {
|
|
282
|
+
if (!opts.modelsRegistry) {
|
|
283
|
+
sendResult(ws, false, "Models registry not available");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const providers = await opts.modelsRegistry.listProviders();
|
|
288
|
+
const savedProviders = await loadSavedProviders();
|
|
289
|
+
const savedIds = new Set(Object.keys(savedProviders));
|
|
290
|
+
send(ws, {
|
|
291
|
+
type: "provider.catalog",
|
|
292
|
+
payload: {
|
|
293
|
+
providers: providers.map((p) => ({
|
|
294
|
+
id: p.id,
|
|
295
|
+
name: p.name,
|
|
296
|
+
family: p.family,
|
|
297
|
+
apiBase: p.apiBase,
|
|
298
|
+
envVars: p.envVars,
|
|
299
|
+
modelCount: p.models.length,
|
|
300
|
+
hasApiKey: savedIds.has(p.id) || p.envVars.some((v) => !!process.env[v])
|
|
301
|
+
}))
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function handleProviderModels(ws, providerId) {
|
|
309
|
+
if (!opts.modelsRegistry) {
|
|
310
|
+
sendResult(ws, false, "Models registry not available");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const provider = await opts.modelsRegistry.getProvider(providerId);
|
|
315
|
+
if (!provider) {
|
|
316
|
+
sendResult(ws, false, `Provider "${providerId}" not found in catalog`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
send(ws, {
|
|
320
|
+
type: "provider.models",
|
|
321
|
+
payload: {
|
|
322
|
+
provider: providerId,
|
|
323
|
+
models: provider.models.map((m) => ({
|
|
324
|
+
id: m.id,
|
|
325
|
+
name: m.name,
|
|
326
|
+
releaseDate: m.release_date,
|
|
327
|
+
contextWindow: m.limit?.context,
|
|
328
|
+
inputCost: m.cost?.input,
|
|
329
|
+
outputCost: m.cost?.output,
|
|
330
|
+
capabilities: [
|
|
331
|
+
...m.tool_call ? ["tools"] : [],
|
|
332
|
+
...m.reasoning ? ["reasoning"] : [],
|
|
333
|
+
...m.modalities?.input?.includes("image") ? ["vision"] : [],
|
|
334
|
+
...m.open_weights ? ["open_weights"] : []
|
|
335
|
+
]
|
|
336
|
+
}))
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
} catch (err) {
|
|
340
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function handleProvidersSaved(ws) {
|
|
344
|
+
try {
|
|
345
|
+
const providers = await loadSavedProviders();
|
|
346
|
+
send(ws, {
|
|
347
|
+
type: "providers.saved",
|
|
348
|
+
payload: {
|
|
349
|
+
providers: Object.entries(providers).map(([id, cfg]) => ({
|
|
350
|
+
id,
|
|
351
|
+
family: cfg.family,
|
|
352
|
+
baseUrl: cfg.baseUrl,
|
|
353
|
+
apiKeys: normalizeKeys2(cfg).map((k) => ({
|
|
354
|
+
label: k.label,
|
|
355
|
+
maskedKey: maskedKey2(k.apiKey),
|
|
356
|
+
isActive: k.label === cfg.activeKey,
|
|
357
|
+
createdAt: k.createdAt
|
|
358
|
+
}))
|
|
359
|
+
}))
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
} catch (err) {
|
|
363
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
367
|
+
try {
|
|
368
|
+
const providers = await loadSavedProviders();
|
|
369
|
+
const existing = providers[providerId] ?? { type: providerId };
|
|
370
|
+
const keys = normalizeKeys2(existing);
|
|
371
|
+
const existingIdx = keys.findIndex((k) => k.label === label);
|
|
372
|
+
if (existingIdx >= 0) {
|
|
373
|
+
keys[existingIdx] = { ...keys[existingIdx], apiKey, createdAt: nowIso2() };
|
|
374
|
+
} else {
|
|
375
|
+
keys.push({ label, apiKey, createdAt: nowIso2() });
|
|
376
|
+
}
|
|
377
|
+
writeKeysBack2(existing, keys);
|
|
378
|
+
if (!existing.activeKey) existing.activeKey = label;
|
|
379
|
+
providers[providerId] = existing;
|
|
380
|
+
await saveProviders(providers);
|
|
381
|
+
sendResult(ws, true, `Key "${label}" saved for ${providerId}`);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function handleKeyDelete(ws, providerId, label) {
|
|
387
|
+
try {
|
|
388
|
+
const providers = await loadSavedProviders();
|
|
389
|
+
const existing = providers[providerId];
|
|
390
|
+
if (!existing) {
|
|
391
|
+
sendResult(ws, false, `Provider "${providerId}" not found`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const keys = normalizeKeys2(existing).filter((k) => k.label !== label);
|
|
395
|
+
if (keys.length === 0) {
|
|
396
|
+
delete providers[providerId];
|
|
397
|
+
} else {
|
|
398
|
+
writeKeysBack2(existing, keys);
|
|
399
|
+
if (existing.activeKey === label) {
|
|
400
|
+
existing.activeKey = keys[0].label;
|
|
401
|
+
}
|
|
402
|
+
providers[providerId] = existing;
|
|
403
|
+
}
|
|
404
|
+
await saveProviders(providers);
|
|
405
|
+
sendResult(ws, true, `Key "${label}" deleted from ${providerId}`);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function handleKeySetActive(ws, providerId, label) {
|
|
411
|
+
try {
|
|
412
|
+
const providers = await loadSavedProviders();
|
|
413
|
+
const existing = providers[providerId];
|
|
414
|
+
if (!existing) {
|
|
415
|
+
sendResult(ws, false, `Provider "${providerId}" not found`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
existing.activeKey = label;
|
|
419
|
+
writeKeysBack2(existing, normalizeKeys2(existing));
|
|
420
|
+
providers[providerId] = existing;
|
|
421
|
+
await saveProviders(providers);
|
|
422
|
+
sendResult(ws, true, `Active key for ${providerId} set to "${label}"`);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function handleProviderAdd(ws, payload) {
|
|
428
|
+
try {
|
|
429
|
+
const providers = await loadSavedProviders();
|
|
430
|
+
if (providers[payload.id]) {
|
|
431
|
+
sendResult(ws, false, `Provider "${payload.id}" already exists. Use key.add to add a key.`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const newProv = {
|
|
435
|
+
type: payload.id,
|
|
436
|
+
family: payload.family,
|
|
437
|
+
baseUrl: payload.baseUrl
|
|
438
|
+
};
|
|
439
|
+
if (payload.apiKey) {
|
|
440
|
+
newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: nowIso2() }];
|
|
441
|
+
newProv.activeKey = "default";
|
|
442
|
+
}
|
|
443
|
+
providers[payload.id] = newProv;
|
|
444
|
+
await saveProviders(providers);
|
|
445
|
+
sendResult(ws, true, `Provider "${payload.id}" added`);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function handleProviderRemove(ws, providerId) {
|
|
451
|
+
try {
|
|
452
|
+
const providers = await loadSavedProviders();
|
|
453
|
+
if (!providers[providerId]) {
|
|
454
|
+
sendResult(ws, false, `Provider "${providerId}" not found`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
delete providers[providerId];
|
|
458
|
+
await saveProviders(providers);
|
|
459
|
+
sendResult(ws, true, `Provider "${providerId}" removed`);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function loadSavedProviders() {
|
|
465
|
+
if (!opts.globalConfigPath) return {};
|
|
466
|
+
let raw;
|
|
467
|
+
try {
|
|
468
|
+
raw = await fs6.readFile(opts.globalConfigPath, "utf8");
|
|
469
|
+
} catch {
|
|
470
|
+
return {};
|
|
471
|
+
}
|
|
472
|
+
let parsed = {};
|
|
473
|
+
try {
|
|
474
|
+
parsed = JSON.parse(raw);
|
|
475
|
+
} catch {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
return parsed.providers ?? {};
|
|
479
|
+
}
|
|
480
|
+
async function saveProviders(providers) {
|
|
481
|
+
if (!opts.globalConfigPath) return;
|
|
482
|
+
let raw;
|
|
483
|
+
try {
|
|
484
|
+
raw = await fs6.readFile(opts.globalConfigPath, "utf8");
|
|
485
|
+
} catch {
|
|
486
|
+
raw = "{}";
|
|
487
|
+
}
|
|
488
|
+
let parsed;
|
|
489
|
+
try {
|
|
490
|
+
parsed = JSON.parse(raw);
|
|
491
|
+
} catch {
|
|
492
|
+
parsed = {};
|
|
493
|
+
}
|
|
494
|
+
parsed.providers = providers;
|
|
495
|
+
await atomicWrite(opts.globalConfigPath, JSON.stringify(parsed, null, 2), { mode: 384 });
|
|
496
|
+
}
|
|
497
|
+
function normalizeKeys2(cfg) {
|
|
498
|
+
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
499
|
+
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
500
|
+
}
|
|
501
|
+
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
502
|
+
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
503
|
+
}
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
function writeKeysBack2(cfg, keys) {
|
|
507
|
+
if (keys.length === 0) {
|
|
508
|
+
delete cfg.apiKeys;
|
|
509
|
+
delete cfg.apiKey;
|
|
510
|
+
delete cfg.activeKey;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
cfg.apiKeys = keys;
|
|
514
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
515
|
+
cfg.apiKey = active.apiKey;
|
|
516
|
+
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
517
|
+
cfg.activeKey = active.label;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function maskedKey2(key) {
|
|
521
|
+
if (!key) return "\u2014";
|
|
522
|
+
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
523
|
+
const head = key.slice(0, 4);
|
|
524
|
+
const tail = key.slice(-4);
|
|
525
|
+
return `${head}\u2026${tail}`;
|
|
526
|
+
}
|
|
527
|
+
function nowIso2() {
|
|
528
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
529
|
+
}
|
|
530
|
+
function sendResult(ws, success, message) {
|
|
531
|
+
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
var init_webui_server = __esm({
|
|
535
|
+
"src/webui-server.ts"() {
|
|
536
|
+
}
|
|
537
|
+
});
|
|
37
538
|
var ReadlineInputReader = class {
|
|
38
539
|
rl;
|
|
39
540
|
historyFile;
|
|
@@ -516,17 +1017,17 @@ async function resolveModelSelection(answer, models, provider, _registry, render
|
|
|
516
1017
|
var theme2 = { primary: color.amber };
|
|
517
1018
|
async function saveToGlobalConfig(configPath, provider, model) {
|
|
518
1019
|
try {
|
|
519
|
-
const { atomicWrite:
|
|
520
|
-
const
|
|
1020
|
+
const { atomicWrite: atomicWrite4 } = await import('@wrongstack/core');
|
|
1021
|
+
const fs9 = await import('fs/promises');
|
|
521
1022
|
let existing = {};
|
|
522
1023
|
try {
|
|
523
|
-
const raw = await
|
|
1024
|
+
const raw = await fs9.readFile(configPath, "utf8");
|
|
524
1025
|
existing = JSON.parse(raw);
|
|
525
1026
|
} catch {
|
|
526
1027
|
}
|
|
527
1028
|
existing.provider = provider;
|
|
528
1029
|
existing.model = model;
|
|
529
|
-
await
|
|
1030
|
+
await atomicWrite4(configPath, JSON.stringify(existing, null, 2));
|
|
530
1031
|
return true;
|
|
531
1032
|
} catch {
|
|
532
1033
|
return false;
|
|
@@ -545,9 +1046,11 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
545
1046
|
statsCommand(opts),
|
|
546
1047
|
spawnCommand(opts),
|
|
547
1048
|
agentsCommand(opts),
|
|
1049
|
+
fleetCommand(opts),
|
|
548
1050
|
metricsCommand(opts),
|
|
549
1051
|
healthCommand(opts),
|
|
550
1052
|
memoryCommand(opts),
|
|
1053
|
+
todosCommand(opts),
|
|
551
1054
|
saveCommand(opts),
|
|
552
1055
|
loadCommand(opts),
|
|
553
1056
|
exitCommand(opts)
|
|
@@ -597,6 +1100,68 @@ function memoryCommand(opts) {
|
|
|
597
1100
|
}
|
|
598
1101
|
};
|
|
599
1102
|
}
|
|
1103
|
+
function todosCommand(opts) {
|
|
1104
|
+
return {
|
|
1105
|
+
name: "todos",
|
|
1106
|
+
description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
|
|
1107
|
+
async run(args) {
|
|
1108
|
+
const ctx = opts.context;
|
|
1109
|
+
if (!ctx) return { message: "No active context." };
|
|
1110
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
1111
|
+
const restJoined = rest.join(" ").trim();
|
|
1112
|
+
switch (verb) {
|
|
1113
|
+
case "":
|
|
1114
|
+
case "show":
|
|
1115
|
+
case "list": {
|
|
1116
|
+
const todos = ctx.todos;
|
|
1117
|
+
if (todos.length === 0) {
|
|
1118
|
+
return { message: "No todos. The agent will add some when it plans work." };
|
|
1119
|
+
}
|
|
1120
|
+
const lines = [];
|
|
1121
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
1122
|
+
lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
|
|
1123
|
+
todos.forEach((t, i) => {
|
|
1124
|
+
const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
|
|
1125
|
+
const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
|
|
1126
|
+
const label = t.status === "completed" ? color.dim(text) : text;
|
|
1127
|
+
lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
|
|
1128
|
+
});
|
|
1129
|
+
return { message: lines.join("\n") };
|
|
1130
|
+
}
|
|
1131
|
+
case "clear": {
|
|
1132
|
+
const n = ctx.todos.length;
|
|
1133
|
+
ctx.todos.length = 0;
|
|
1134
|
+
return { message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.` };
|
|
1135
|
+
}
|
|
1136
|
+
case "add": {
|
|
1137
|
+
if (!restJoined) return { message: "Usage: /todos add <text>" };
|
|
1138
|
+
ctx.todos.push({
|
|
1139
|
+
id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
1140
|
+
content: restJoined,
|
|
1141
|
+
status: "pending"
|
|
1142
|
+
});
|
|
1143
|
+
return { message: `Added: ${restJoined}` };
|
|
1144
|
+
}
|
|
1145
|
+
case "done":
|
|
1146
|
+
case "complete": {
|
|
1147
|
+
if (!restJoined) return { message: "Usage: /todos done <id|index>" };
|
|
1148
|
+
const asIndex = Number.parseInt(restJoined, 10);
|
|
1149
|
+
let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
|
|
1150
|
+
if (!target) {
|
|
1151
|
+
target = ctx.todos.find((t) => t.content.toLowerCase().includes(restJoined.toLowerCase()));
|
|
1152
|
+
}
|
|
1153
|
+
if (!target) return { message: `No todo matched "${restJoined}".` };
|
|
1154
|
+
target.status = "completed";
|
|
1155
|
+
return { message: `Marked done: ${target.content}` };
|
|
1156
|
+
}
|
|
1157
|
+
default:
|
|
1158
|
+
return {
|
|
1159
|
+
message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
600
1165
|
function initCommand(opts) {
|
|
601
1166
|
return {
|
|
602
1167
|
name: "init",
|
|
@@ -810,11 +1375,11 @@ function clearCommand(opts) {
|
|
|
810
1375
|
].join("\n"),
|
|
811
1376
|
async run(_args, ctx) {
|
|
812
1377
|
if (ctx) {
|
|
813
|
-
ctx.
|
|
814
|
-
ctx.
|
|
1378
|
+
ctx.state.replaceMessages([]);
|
|
1379
|
+
ctx.state.replaceTodos([]);
|
|
815
1380
|
ctx.readFiles.clear();
|
|
816
1381
|
ctx.fileMtimes.clear();
|
|
817
|
-
ctx.meta
|
|
1382
|
+
for (const key of Object.keys(ctx.meta)) ctx.state.deleteMeta(key);
|
|
818
1383
|
}
|
|
819
1384
|
await opts.memoryStore?.clear();
|
|
820
1385
|
opts.onClear?.();
|
|
@@ -960,21 +1525,25 @@ ${lines.join("\n")}
|
|
|
960
1525
|
function skillCommand(opts) {
|
|
961
1526
|
return {
|
|
962
1527
|
name: "skill",
|
|
963
|
-
description: "Show
|
|
1528
|
+
description: "Show skill details or list available skills.",
|
|
964
1529
|
async run(args) {
|
|
965
1530
|
if (!opts.skillLoader) {
|
|
966
1531
|
const msg = "No skill loader configured.";
|
|
967
1532
|
return { message: msg };
|
|
968
1533
|
}
|
|
969
1534
|
if (!args.trim()) {
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
1535
|
+
const entries = await opts.skillLoader.listEntries();
|
|
1536
|
+
if (entries.length === 0) {
|
|
972
1537
|
const msg2 = "No skills found.";
|
|
973
1538
|
return { message: msg2 };
|
|
974
1539
|
}
|
|
975
|
-
const lines =
|
|
976
|
-
|
|
977
|
-
${
|
|
1540
|
+
const lines = entries.map((e) => {
|
|
1541
|
+
const scopeTag = e.scope.length > 0 ? ` ${color.dim(`(${e.scope.slice(0, 3).join(", ")})`)}` : "";
|
|
1542
|
+
return ` ${color.bold(e.name)}${scopeTag}
|
|
1543
|
+
Use when: ${e.trigger}`;
|
|
1544
|
+
});
|
|
1545
|
+
const msg = `Available skills:
|
|
1546
|
+
${lines.join("\n\n")}
|
|
978
1547
|
`;
|
|
979
1548
|
return { message: msg };
|
|
980
1549
|
}
|
|
@@ -1105,18 +1674,46 @@ function statusIcon(status) {
|
|
|
1105
1674
|
if (status === "degraded") return color.yellow("\u25CF");
|
|
1106
1675
|
return color.red("\u25CF");
|
|
1107
1676
|
}
|
|
1677
|
+
function parseSpawnFlags(input) {
|
|
1678
|
+
const opts = {};
|
|
1679
|
+
let rest = input;
|
|
1680
|
+
const consume = (re) => {
|
|
1681
|
+
const m = rest.match(re);
|
|
1682
|
+
if (m) {
|
|
1683
|
+
rest = rest.slice(m[0].length).replace(/^\s+/, "");
|
|
1684
|
+
return m;
|
|
1685
|
+
}
|
|
1686
|
+
return null;
|
|
1687
|
+
};
|
|
1688
|
+
while (rest.length > 0) {
|
|
1689
|
+
let m;
|
|
1690
|
+
if (m = consume(/^--provider=(\S+)\s*/)) opts.provider = m[1];
|
|
1691
|
+
else if (m = consume(/^--model=(\S+)\s*/)) opts.model = m[1];
|
|
1692
|
+
else if (m = consume(/^--name=("([^"]+)"|(\S+))\s*/)) opts.name = m[2] ?? m[3];
|
|
1693
|
+
else if (m = consume(/^--tools=(\S+)\s*/)) opts.tools = m[1].split(",").map((t) => t.trim()).filter(Boolean);
|
|
1694
|
+
else if (m = consume(/^-p\s+(\S+)\s*/)) opts.provider = m[1];
|
|
1695
|
+
else if (m = consume(/^-m\s+(\S+)\s*/)) opts.model = m[1];
|
|
1696
|
+
else if (m = consume(/^-n\s+("([^"]+)"|(\S+))\s*/)) opts.name = m[2] ?? m[3];
|
|
1697
|
+
else break;
|
|
1698
|
+
}
|
|
1699
|
+
return { description: rest.trim(), opts };
|
|
1700
|
+
}
|
|
1108
1701
|
function spawnCommand(opts) {
|
|
1109
1702
|
return {
|
|
1110
1703
|
name: "spawn",
|
|
1111
|
-
description: "Spawn an isolated subagent to handle a task. Usage: /spawn <task description>",
|
|
1704
|
+
description: "Spawn an isolated subagent to handle a task. Usage: /spawn [--provider=<id>] [--model=<id>] [--name=<label>] [--tools=a,b,c] <task description>",
|
|
1112
1705
|
async run(args) {
|
|
1113
|
-
const description = args.trim();
|
|
1114
|
-
if (!description)
|
|
1706
|
+
const { description, opts: parsed } = parseSpawnFlags(args.trim());
|
|
1707
|
+
if (!description) {
|
|
1708
|
+
return {
|
|
1709
|
+
message: "Usage: /spawn [--provider=<id>] [--model=<id>] [--name=<label>] [--tools=a,b,c] <task description>"
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1115
1712
|
if (!opts.onSpawn) {
|
|
1116
1713
|
return { message: "Multi-agent is not enabled in this session." };
|
|
1117
1714
|
}
|
|
1118
1715
|
try {
|
|
1119
|
-
const summary = await opts.onSpawn(description);
|
|
1716
|
+
const summary = Object.keys(parsed).length > 0 ? await opts.onSpawn(description, parsed) : await opts.onSpawn(description);
|
|
1120
1717
|
return { message: summary };
|
|
1121
1718
|
} catch (err) {
|
|
1122
1719
|
return {
|
|
@@ -1138,6 +1735,63 @@ function agentsCommand(opts) {
|
|
|
1138
1735
|
}
|
|
1139
1736
|
};
|
|
1140
1737
|
}
|
|
1738
|
+
function fleetCommand(opts) {
|
|
1739
|
+
return {
|
|
1740
|
+
name: "fleet",
|
|
1741
|
+
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|help]",
|
|
1742
|
+
help: [
|
|
1743
|
+
"Usage:",
|
|
1744
|
+
" /fleet Show fleet status (alias for /fleet status).",
|
|
1745
|
+
" /fleet status Pending + completed subagent task table.",
|
|
1746
|
+
" /fleet usage Per-subagent runtime cost \u2014 iterations, tool calls, duration.",
|
|
1747
|
+
" /fleet kill <id> Terminate a running subagent by id (or prefix).",
|
|
1748
|
+
" /fleet manifest Print the director manifest (only with --director).",
|
|
1749
|
+
" /fleet help Show this help.",
|
|
1750
|
+
"",
|
|
1751
|
+
"Subagent ids are returned by /spawn and listed in /fleet status."
|
|
1752
|
+
].join("\n"),
|
|
1753
|
+
async run(args) {
|
|
1754
|
+
if (!opts.onFleet) {
|
|
1755
|
+
return { message: "Multi-agent is not enabled in this session." };
|
|
1756
|
+
}
|
|
1757
|
+
const trimmed = args.trim();
|
|
1758
|
+
const [verb, ...rest] = trimmed.length === 0 ? ["status"] : trimmed.split(/\s+/);
|
|
1759
|
+
const target = rest.join(" ").trim() || void 0;
|
|
1760
|
+
switch (verb) {
|
|
1761
|
+
case "status":
|
|
1762
|
+
case "usage":
|
|
1763
|
+
case "manifest": {
|
|
1764
|
+
const out = await opts.onFleet(verb, void 0);
|
|
1765
|
+
return { message: out };
|
|
1766
|
+
}
|
|
1767
|
+
case "kill": {
|
|
1768
|
+
if (!target) {
|
|
1769
|
+
return { message: "Usage: /fleet kill <subagent-id>" };
|
|
1770
|
+
}
|
|
1771
|
+
const out = await opts.onFleet("kill", target);
|
|
1772
|
+
return { message: out };
|
|
1773
|
+
}
|
|
1774
|
+
case "help":
|
|
1775
|
+
case "?":
|
|
1776
|
+
return {
|
|
1777
|
+
message: [
|
|
1778
|
+
"/fleet \u2014 inspect or control the subagent fleet",
|
|
1779
|
+
"",
|
|
1780
|
+
" /fleet \u2192 status (default)",
|
|
1781
|
+
" /fleet status pending + completed tasks per subagent",
|
|
1782
|
+
" /fleet usage iterations, tool calls, duration roll-up",
|
|
1783
|
+
" /fleet kill <id> terminate a subagent",
|
|
1784
|
+
" /fleet manifest director manifest (requires --director)"
|
|
1785
|
+
].join("\n")
|
|
1786
|
+
};
|
|
1787
|
+
default:
|
|
1788
|
+
return {
|
|
1789
|
+
message: `Unknown subcommand "${verb}". Try: status | usage | kill <id> | manifest | help`
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1141
1795
|
|
|
1142
1796
|
// src/pre-launch.ts
|
|
1143
1797
|
var MANIFESTS = [
|
|
@@ -1498,7 +2152,7 @@ function patchConfig(base, patch) {
|
|
|
1498
2152
|
|
|
1499
2153
|
// src/repl.ts
|
|
1500
2154
|
async function runRepl(opts) {
|
|
1501
|
-
if (opts.banner !== false) printBanner(opts.renderer);
|
|
2155
|
+
if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
|
|
1502
2156
|
let activeCtrl;
|
|
1503
2157
|
let interrupts = 0;
|
|
1504
2158
|
const onSigint = () => {
|
|
@@ -1516,78 +2170,82 @@ async function runRepl(opts) {
|
|
|
1516
2170
|
};
|
|
1517
2171
|
process.on("SIGINT", onSigint);
|
|
1518
2172
|
const builder = new InputBuilder({ store: opts.attachments });
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
raw = await readPossiblyMultiline(opts);
|
|
1523
|
-
} catch {
|
|
1524
|
-
break;
|
|
1525
|
-
}
|
|
1526
|
-
const trimmed = raw.trim();
|
|
1527
|
-
if (!trimmed) {
|
|
1528
|
-
interrupts = 0;
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
interrupts = 0;
|
|
1532
|
-
if (trimmed.startsWith("/")) {
|
|
2173
|
+
try {
|
|
2174
|
+
for (; ; ) {
|
|
2175
|
+
let raw;
|
|
1533
2176
|
try {
|
|
1534
|
-
|
|
1535
|
-
|
|
2177
|
+
raw = await readPossiblyMultiline(opts);
|
|
2178
|
+
} catch {
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
const trimmed = raw.trim();
|
|
2182
|
+
if (!trimmed) {
|
|
2183
|
+
interrupts = 0;
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
interrupts = 0;
|
|
2187
|
+
if (trimmed.startsWith("/")) {
|
|
2188
|
+
try {
|
|
2189
|
+
const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
|
|
2190
|
+
if (res?.message) opts.renderer.write(`${res.message}
|
|
1536
2191
|
`);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
2192
|
+
if (res?.exit) break;
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
2195
|
+
}
|
|
2196
|
+
continue;
|
|
1540
2197
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
const lineCount = raw.split("\n").length;
|
|
1546
|
-
opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
|
|
2198
|
+
const ph = await builder.appendPaste(raw);
|
|
2199
|
+
if (ph) {
|
|
2200
|
+
const lineCount = raw.split("\n").length;
|
|
2201
|
+
opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
|
|
1547
2202
|
`));
|
|
1548
|
-
}
|
|
1549
|
-
const blocks = await builder.submit();
|
|
1550
|
-
const runCtrl = new AbortController();
|
|
1551
|
-
activeCtrl = runCtrl;
|
|
1552
|
-
try {
|
|
1553
|
-
const startedAt = Date.now();
|
|
1554
|
-
const before = opts.tokenCounter?.total();
|
|
1555
|
-
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
1556
|
-
const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
|
|
1557
|
-
if (result.status === "aborted") {
|
|
1558
|
-
opts.renderer.writeWarning("Aborted.");
|
|
1559
|
-
} else if (result.status === "failed") {
|
|
1560
|
-
const err = result.error;
|
|
1561
|
-
if (err) {
|
|
1562
|
-
const tag = err.recoverable ? " (recoverable)" : "";
|
|
1563
|
-
opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
|
|
1564
|
-
} else {
|
|
1565
|
-
opts.renderer.writeError("Failed.");
|
|
1566
|
-
}
|
|
1567
|
-
} else if (result.status === "max_iterations") {
|
|
1568
|
-
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
1569
2203
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2204
|
+
const blocks = await builder.submit();
|
|
2205
|
+
const runCtrl = new AbortController();
|
|
2206
|
+
activeCtrl = runCtrl;
|
|
2207
|
+
try {
|
|
2208
|
+
const startedAt = Date.now();
|
|
2209
|
+
const before = opts.tokenCounter?.total();
|
|
2210
|
+
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
2211
|
+
const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
|
|
2212
|
+
if (result.status === "aborted") {
|
|
2213
|
+
opts.renderer.writeWarning("Aborted.");
|
|
2214
|
+
} else if (result.status === "failed") {
|
|
2215
|
+
const err = result.error;
|
|
2216
|
+
if (err) {
|
|
2217
|
+
const tag = err.recoverable ? " (recoverable)" : "";
|
|
2218
|
+
opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
|
|
2219
|
+
} else {
|
|
2220
|
+
opts.renderer.writeError("Failed.");
|
|
2221
|
+
}
|
|
2222
|
+
} else if (result.status === "max_iterations") {
|
|
2223
|
+
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
2224
|
+
}
|
|
2225
|
+
if (opts.tokenCounter && before) {
|
|
2226
|
+
const after = opts.tokenCounter.total();
|
|
2227
|
+
const costAfter = opts.tokenCounter.estimateCost().total;
|
|
2228
|
+
const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
|
|
2229
|
+
opts.renderer.write(
|
|
2230
|
+
`
|
|
1576
2231
|
${color.dim(
|
|
1577
|
-
|
|
1578
|
-
|
|
2232
|
+
`[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
|
|
2233
|
+
)}
|
|
1579
2234
|
`
|
|
1580
|
-
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
2239
|
+
} finally {
|
|
2240
|
+
activeCtrl = void 0;
|
|
1581
2241
|
}
|
|
1582
|
-
} catch (err) {
|
|
1583
|
-
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
1584
|
-
} finally {
|
|
1585
|
-
activeCtrl = void 0;
|
|
1586
2242
|
}
|
|
2243
|
+
return 0;
|
|
2244
|
+
} finally {
|
|
2245
|
+
process.off("SIGINT", onSigint);
|
|
2246
|
+
await opts.reader.close().catch(() => {
|
|
2247
|
+
});
|
|
1587
2248
|
}
|
|
1588
|
-
process.off("SIGINT", onSigint);
|
|
1589
|
-
await opts.reader.close();
|
|
1590
|
-
return 0;
|
|
1591
2249
|
}
|
|
1592
2250
|
async function readPossiblyMultiline(opts) {
|
|
1593
2251
|
const firstPrompt = theme.primary("\u203A ");
|
|
@@ -1624,13 +2282,15 @@ function renderProgress(ratio, width) {
|
|
|
1624
2282
|
const capped = Math.min(width, filled);
|
|
1625
2283
|
return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
|
|
1626
2284
|
}
|
|
1627
|
-
function printBanner(renderer) {
|
|
2285
|
+
function printBanner(renderer, projectName) {
|
|
1628
2286
|
const lines = [
|
|
1629
2287
|
theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
|
|
1630
|
-
color.dim("Built on the wrong stack. Shipped anyway.")
|
|
1631
|
-
color.dim("Type /help for commands, /exit to quit."),
|
|
1632
|
-
""
|
|
2288
|
+
color.dim("Built on the wrong stack. Shipped anyway.")
|
|
1633
2289
|
];
|
|
2290
|
+
if (projectName && projectName.length > 0) {
|
|
2291
|
+
lines.push(color.dim("Project: ") + theme.bold(projectName));
|
|
2292
|
+
}
|
|
2293
|
+
lines.push(color.dim("Type /help for commands, /exit to quit."), "");
|
|
1634
2294
|
renderer.write(`${lines.join("\n")}
|
|
1635
2295
|
`);
|
|
1636
2296
|
}
|
|
@@ -1900,25 +2560,31 @@ async function ensureProjectMeta(paths, projectRoot) {
|
|
|
1900
2560
|
}
|
|
1901
2561
|
}
|
|
1902
2562
|
var MultiAgentHost = class {
|
|
1903
|
-
constructor(deps) {
|
|
2563
|
+
constructor(deps, opts = {}) {
|
|
1904
2564
|
this.deps = deps;
|
|
2565
|
+
this.opts = opts;
|
|
1905
2566
|
}
|
|
1906
2567
|
deps;
|
|
1907
2568
|
coordinator;
|
|
2569
|
+
/** Lazily built when `opts.directorMode` is set. Owns its own internal
|
|
2570
|
+
* coordinator; the host's `coordinator` field still points at it so
|
|
2571
|
+
* the rest of the methods don't need to branch. */
|
|
2572
|
+
director;
|
|
1908
2573
|
pending = /* @__PURE__ */ new Map();
|
|
1909
2574
|
results = [];
|
|
2575
|
+
opts;
|
|
1910
2576
|
async ensureCoordinator() {
|
|
1911
2577
|
if (this.coordinator) return this.coordinator;
|
|
1912
2578
|
const config = this.deps.configStore.get();
|
|
1913
2579
|
const factory = async (subCfg) => {
|
|
1914
2580
|
const events = new EventBus();
|
|
1915
|
-
const provider = await this.buildSubagentProvider(config);
|
|
2581
|
+
const provider = await this.buildSubagentProvider(config, subCfg.provider);
|
|
1916
2582
|
const baseSystem = await this.deps.systemPromptBuilder.build({
|
|
1917
2583
|
cwd: this.deps.cwd,
|
|
1918
2584
|
projectRoot: this.deps.projectRoot,
|
|
1919
2585
|
tools: this.filterTools(subCfg.tools),
|
|
1920
2586
|
model: subCfg.model ?? config.model,
|
|
1921
|
-
provider: config.provider
|
|
2587
|
+
provider: subCfg.provider ?? config.provider
|
|
1922
2588
|
});
|
|
1923
2589
|
const parentSession = this.deps.session;
|
|
1924
2590
|
const subSession = {
|
|
@@ -1929,6 +2595,11 @@ var MultiAgentHost = class {
|
|
|
1929
2595
|
systemPrompt: baseSystem,
|
|
1930
2596
|
provider,
|
|
1931
2597
|
session: subSession,
|
|
2598
|
+
// Placeholder — Agent.run() overwrites ctx.signal with the live
|
|
2599
|
+
// per-run signal (see core/agent.ts run()). Tools/middleware that
|
|
2600
|
+
// read ctx.signal after construction will see the runtime signal,
|
|
2601
|
+
// not this one. Kept as `new AbortController().signal` so the
|
|
2602
|
+
// initial value is non-null/non-aborted.
|
|
1932
2603
|
signal: new AbortController().signal,
|
|
1933
2604
|
tokenCounter: this.deps.tokenCounter,
|
|
1934
2605
|
cwd: this.deps.cwd,
|
|
@@ -1947,15 +2618,26 @@ var MultiAgentHost = class {
|
|
|
1947
2618
|
return { agent, events };
|
|
1948
2619
|
};
|
|
1949
2620
|
const runner = makeAgentSubagentRunner({ factory });
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2621
|
+
const coordinatorConfig = {
|
|
2622
|
+
coordinatorId: randomUUID(),
|
|
2623
|
+
doneCondition: { type: "all_tasks_done" },
|
|
2624
|
+
maxConcurrent: 2,
|
|
2625
|
+
defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
|
|
2626
|
+
};
|
|
2627
|
+
if (this.opts.directorMode) {
|
|
2628
|
+
this.director = new Director({
|
|
2629
|
+
config: coordinatorConfig,
|
|
2630
|
+
runner,
|
|
2631
|
+
manifestPath: this.opts.manifestPath
|
|
2632
|
+
});
|
|
2633
|
+
this.director.on("task.completed", ({ task, result }) => {
|
|
2634
|
+
this.results.push(result);
|
|
2635
|
+
this.pending.delete(task.id);
|
|
2636
|
+
});
|
|
2637
|
+
this.coordinator = this.director.coordinator;
|
|
2638
|
+
return this.coordinator;
|
|
2639
|
+
}
|
|
2640
|
+
this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, { runner });
|
|
1959
2641
|
this.coordinator.on(
|
|
1960
2642
|
"task.completed",
|
|
1961
2643
|
({ task, result }) => {
|
|
@@ -1965,15 +2647,24 @@ var MultiAgentHost = class {
|
|
|
1965
2647
|
);
|
|
1966
2648
|
return this.coordinator;
|
|
1967
2649
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2650
|
+
/**
|
|
2651
|
+
* Build a Provider for a subagent. When `overrideId` is supplied (from
|
|
2652
|
+
* `SubagentConfig.provider`), looks that provider up in
|
|
2653
|
+
* `config.providers` and constructs it with its own apiKey/baseUrl.
|
|
2654
|
+
* Falls back to the leader's provider when `overrideId` is absent or
|
|
2655
|
+
* not configured (so a typo doesn't crash the whole run — we just
|
|
2656
|
+
* use the leader and the calling code can decide to error later).
|
|
2657
|
+
*/
|
|
2658
|
+
async buildSubagentProvider(config, overrideId) {
|
|
2659
|
+
const providerId = overrideId && config.providers?.[overrideId] ? overrideId : config.provider;
|
|
2660
|
+
const newCfg = config.providers?.[providerId] ?? {
|
|
2661
|
+
type: providerId,
|
|
1971
2662
|
apiKey: config.apiKey,
|
|
1972
2663
|
baseUrl: config.baseUrl
|
|
1973
2664
|
};
|
|
1974
|
-
return makeProviderFromConfig(
|
|
2665
|
+
return makeProviderFromConfig(providerId, {
|
|
1975
2666
|
...newCfg,
|
|
1976
|
-
type:
|
|
2667
|
+
type: providerId
|
|
1977
2668
|
});
|
|
1978
2669
|
}
|
|
1979
2670
|
/** Returns a tool slice for the subagent — full set unless restricted. */
|
|
@@ -1990,15 +2681,40 @@ var MultiAgentHost = class {
|
|
|
1990
2681
|
for (const t of this.filterTools(allow)) sub.register(t);
|
|
1991
2682
|
return sub;
|
|
1992
2683
|
}
|
|
1993
|
-
/**
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2684
|
+
/**
|
|
2685
|
+
* Spawn a fresh subagent and assign a single task. Returns task id.
|
|
2686
|
+
*
|
|
2687
|
+
* Optional `opts` lets the caller (a `/spawn` slash command or the
|
|
2688
|
+
* future director surface) override the subagent's provider, model,
|
|
2689
|
+
* and tool slice on a per-spawn basis. Without options, the legacy
|
|
2690
|
+
* behavior holds: the subagent uses the leader's provider/model and
|
|
2691
|
+
* the full tool registry.
|
|
2692
|
+
*/
|
|
2693
|
+
async spawn(description, opts) {
|
|
2694
|
+
await this.ensureCoordinator();
|
|
2695
|
+
const subagentConfig = {
|
|
2696
|
+
name: opts?.name ?? "adhoc",
|
|
1998
2697
|
role: "general",
|
|
1999
2698
|
maxToolCalls: 20,
|
|
2000
|
-
maxIterations: 20
|
|
2001
|
-
|
|
2699
|
+
maxIterations: 20,
|
|
2700
|
+
provider: opts?.provider,
|
|
2701
|
+
model: opts?.model,
|
|
2702
|
+
tools: opts?.tools
|
|
2703
|
+
};
|
|
2704
|
+
if (this.director) {
|
|
2705
|
+
const subagentId = await this.director.spawn(subagentConfig);
|
|
2706
|
+
const taskId2 = randomUUID();
|
|
2707
|
+
this.pending.set(taskId2, { description, subagentId });
|
|
2708
|
+
await this.director.assign({
|
|
2709
|
+
id: taskId2,
|
|
2710
|
+
description,
|
|
2711
|
+
subagentId,
|
|
2712
|
+
maxToolCalls: 20
|
|
2713
|
+
});
|
|
2714
|
+
return { subagentId, taskId: taskId2 };
|
|
2715
|
+
}
|
|
2716
|
+
const coord = this.coordinator;
|
|
2717
|
+
const spawned = await coord.spawn(subagentConfig);
|
|
2002
2718
|
const taskId = randomUUID();
|
|
2003
2719
|
this.pending.set(taskId, { description, subagentId: spawned.subagentId });
|
|
2004
2720
|
await coord.assign({
|
|
@@ -2018,6 +2734,79 @@ var MultiAgentHost = class {
|
|
|
2018
2734
|
const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
|
|
2019
2735
|
return { pending, completed: this.results, summary };
|
|
2020
2736
|
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Roll up per-subagent runtime cost from completed TaskResults. We don't
|
|
2739
|
+
* yet have FleetUsageAggregator wired into the simple MultiAgentHost
|
|
2740
|
+
* path (that lives on `Director`), so this aggregates iterations / tool
|
|
2741
|
+
* calls / duration which we *do* have — enough to spot a thrashing
|
|
2742
|
+
* worker without paying for a heavier orchestrator on every /spawn.
|
|
2743
|
+
*
|
|
2744
|
+
* Returns rows sorted by total duration descending (slowest first) so
|
|
2745
|
+
* the table renders the most interesting subagent at the top.
|
|
2746
|
+
*/
|
|
2747
|
+
usage() {
|
|
2748
|
+
const bySubagent = /* @__PURE__ */ new Map();
|
|
2749
|
+
for (const r of this.results) {
|
|
2750
|
+
const cur = bySubagent.get(r.subagentId) ?? { tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0, lastStatus: "unknown" };
|
|
2751
|
+
cur.tasks += 1;
|
|
2752
|
+
cur.iterations += r.iterations;
|
|
2753
|
+
cur.toolCalls += r.toolCalls;
|
|
2754
|
+
cur.durationMs += r.durationMs;
|
|
2755
|
+
cur.lastStatus = r.status;
|
|
2756
|
+
bySubagent.set(r.subagentId, cur);
|
|
2757
|
+
}
|
|
2758
|
+
const rows = Array.from(bySubagent.entries()).map(([subagentId, v]) => ({
|
|
2759
|
+
subagentId,
|
|
2760
|
+
tasks: v.tasks,
|
|
2761
|
+
iterations: v.iterations,
|
|
2762
|
+
toolCalls: v.toolCalls,
|
|
2763
|
+
durationMs: v.durationMs,
|
|
2764
|
+
status: v.lastStatus
|
|
2765
|
+
})).sort((a, b) => b.durationMs - a.durationMs);
|
|
2766
|
+
const totals = rows.reduce(
|
|
2767
|
+
(acc, r) => ({
|
|
2768
|
+
tasks: acc.tasks + r.tasks,
|
|
2769
|
+
iterations: acc.iterations + r.iterations,
|
|
2770
|
+
toolCalls: acc.toolCalls + r.toolCalls,
|
|
2771
|
+
durationMs: acc.durationMs + r.durationMs
|
|
2772
|
+
}),
|
|
2773
|
+
{ tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0 }
|
|
2774
|
+
);
|
|
2775
|
+
return { rows, totals };
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Force the director to write its manifest to disk and return the path,
|
|
2779
|
+
* or `null` when director mode is off (the simple coordinator path has
|
|
2780
|
+
* no manifest). Callers should fall back to a friendly user message
|
|
2781
|
+
* when `null` is returned — e.g. `/fleet manifest` does this already.
|
|
2782
|
+
*
|
|
2783
|
+
* The returned string is the absolute path of the manifest file. The
|
|
2784
|
+
* file contents are JSON; readers can `JSON.parse(fs.readFileSync(...))`
|
|
2785
|
+
* to consume.
|
|
2786
|
+
*/
|
|
2787
|
+
async manifest() {
|
|
2788
|
+
if (!this.director) return null;
|
|
2789
|
+
return this.director.writeManifest();
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* True when this host is running in director mode. Surfaces the mode
|
|
2793
|
+
* to slash commands and tests without exposing the underlying Director
|
|
2794
|
+
* (which would let callers bypass the host's coordination layer).
|
|
2795
|
+
*/
|
|
2796
|
+
isDirectorMode() {
|
|
2797
|
+
return !!this.director;
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Terminate a single subagent. Returns true when the subagent existed
|
|
2801
|
+
* (regardless of whether stop() succeeded or it was already idle),
|
|
2802
|
+
* false when no coordinator has been created yet — meaning the user
|
|
2803
|
+
* called /fleet kill before any /spawn, and there's nothing to do.
|
|
2804
|
+
*/
|
|
2805
|
+
async kill(subagentId) {
|
|
2806
|
+
if (!this.coordinator) return false;
|
|
2807
|
+
await this.coordinator.stop(subagentId);
|
|
2808
|
+
return true;
|
|
2809
|
+
}
|
|
2021
2810
|
async stopAll() {
|
|
2022
2811
|
if (this.coordinator) {
|
|
2023
2812
|
await this.coordinator.stopAll();
|
|
@@ -3354,7 +4143,8 @@ async function helpCmd(_args, deps) {
|
|
|
3354
4143
|
" wstack version Print version",
|
|
3355
4144
|
"",
|
|
3356
4145
|
"Global flags:",
|
|
3357
|
-
" --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config"
|
|
4146
|
+
" --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config",
|
|
4147
|
+
" --director Run with Director-backed orchestration (writes fleet manifest)"
|
|
3358
4148
|
];
|
|
3359
4149
|
deps.renderer.write(lines.join("\n") + "\n");
|
|
3360
4150
|
return 0;
|
|
@@ -3418,7 +4208,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
3418
4208
|
"alt-screen",
|
|
3419
4209
|
"output-json",
|
|
3420
4210
|
"prompt",
|
|
3421
|
-
"metrics"
|
|
4211
|
+
"metrics",
|
|
4212
|
+
"webui"
|
|
3422
4213
|
]);
|
|
3423
4214
|
function parseArgs(argv) {
|
|
3424
4215
|
const flags = {};
|
|
@@ -3618,6 +4409,8 @@ async function main(argv) {
|
|
|
3618
4409
|
TOKENS.TokenCounter,
|
|
3619
4410
|
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
3620
4411
|
);
|
|
4412
|
+
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
4413
|
+
container.bind(TOKENS.ModeStore, () => modeStore);
|
|
3621
4414
|
container.bind(
|
|
3622
4415
|
TOKENS.SessionStore,
|
|
3623
4416
|
() => new DefaultSessionStore({ dir: wpaths.projectSessions })
|
|
@@ -3629,11 +4422,25 @@ async function main(argv) {
|
|
|
3629
4422
|
bundledDir: config.features.skills ? resolveBundledSkillsDir() : void 0
|
|
3630
4423
|
});
|
|
3631
4424
|
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
4425
|
+
const activeMode = await modeStore.getActiveMode();
|
|
4426
|
+
const modeId = activeMode?.id ?? "default";
|
|
4427
|
+
const modePrompt = activeMode?.prompt ?? "";
|
|
4428
|
+
const resolvedModel = await modelsRegistry.getModel(config.provider, config.model);
|
|
4429
|
+
const modelCapabilities = resolvedModel?.capabilities ? {
|
|
4430
|
+
maxContextTokens: resolvedModel.capabilities.maxContext,
|
|
4431
|
+
supportsTools: resolvedModel.capabilities.tools,
|
|
4432
|
+
supportsVision: resolvedModel.capabilities.vision,
|
|
4433
|
+
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
4434
|
+
} : void 0;
|
|
3632
4435
|
container.bind(
|
|
3633
4436
|
TOKENS.SystemPromptBuilder,
|
|
3634
4437
|
() => new DefaultSystemPromptBuilder({
|
|
3635
4438
|
memoryStore,
|
|
3636
|
-
skillLoader: config.features.skills ? skillLoader : void 0
|
|
4439
|
+
skillLoader: config.features.skills ? skillLoader : void 0,
|
|
4440
|
+
modeStore,
|
|
4441
|
+
modeId,
|
|
4442
|
+
modePrompt,
|
|
4443
|
+
modelCapabilities
|
|
3637
4444
|
})
|
|
3638
4445
|
);
|
|
3639
4446
|
container.bind(TOKENS.Renderer, () => renderer);
|
|
@@ -3897,7 +4704,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
3897
4704
|
model: config.model
|
|
3898
4705
|
});
|
|
3899
4706
|
if (restoredMessages.length > 0) {
|
|
3900
|
-
context.
|
|
4707
|
+
context.state.replaceMessages(restoredMessages);
|
|
3901
4708
|
}
|
|
3902
4709
|
const pipelines = createDefaultPipelines();
|
|
3903
4710
|
const installBoundary = (p) => {
|
|
@@ -3957,7 +4764,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
3957
4764
|
soft: config.context.softThreshold,
|
|
3958
4765
|
hard: config.context.hardThreshold
|
|
3959
4766
|
},
|
|
3960
|
-
|
|
4767
|
+
{
|
|
4768
|
+
aggressiveOn: "soft",
|
|
4769
|
+
failureMode: "throw_on_hard",
|
|
4770
|
+
events
|
|
4771
|
+
}
|
|
3961
4772
|
);
|
|
3962
4773
|
pipelines.contextWindow.use({
|
|
3963
4774
|
name: "AutoCompaction",
|
|
@@ -4077,6 +4888,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4077
4888
|
return err instanceof Error ? err.message : String(err);
|
|
4078
4889
|
}
|
|
4079
4890
|
};
|
|
4891
|
+
const directorMode = flags["director"] === true;
|
|
4892
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path5.join(wpaths.projectSessions, session.id, "fleet.json") : void 0;
|
|
4080
4893
|
const multiAgentHost = new MultiAgentHost({
|
|
4081
4894
|
container,
|
|
4082
4895
|
toolRegistry,
|
|
@@ -4088,7 +4901,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4088
4901
|
tokenCounter,
|
|
4089
4902
|
projectRoot,
|
|
4090
4903
|
cwd
|
|
4091
|
-
});
|
|
4904
|
+
}, { directorMode, manifestPath });
|
|
4905
|
+
if (directorMode) {
|
|
4906
|
+
renderer.writeInfo(`Director mode enabled. Fleet manifest \u2192 ${manifestPath}`);
|
|
4907
|
+
}
|
|
4092
4908
|
const slashCmds = buildBuiltinSlashCommands({
|
|
4093
4909
|
registry: slashRegistry,
|
|
4094
4910
|
toolRegistry,
|
|
@@ -4101,9 +4917,14 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4101
4917
|
context,
|
|
4102
4918
|
metricsSink,
|
|
4103
4919
|
healthRegistry,
|
|
4104
|
-
onSpawn: async (description) => {
|
|
4105
|
-
const { subagentId, taskId } = await multiAgentHost.spawn(description);
|
|
4106
|
-
|
|
4920
|
+
onSpawn: async (description, spawnOpts) => {
|
|
4921
|
+
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
4922
|
+
const tags = [];
|
|
4923
|
+
if (spawnOpts?.provider) tags.push(spawnOpts.provider);
|
|
4924
|
+
if (spawnOpts?.model) tags.push(spawnOpts.model);
|
|
4925
|
+
if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
|
|
4926
|
+
const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
|
|
4927
|
+
return `Spawned subagent ${subagentId}${tag} for task ${taskId}. Use /agents to track progress.`;
|
|
4107
4928
|
},
|
|
4108
4929
|
onAgents: () => {
|
|
4109
4930
|
const s = multiAgentHost.status();
|
|
@@ -4118,6 +4939,60 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4118
4939
|
}
|
|
4119
4940
|
return lines.join("\n");
|
|
4120
4941
|
},
|
|
4942
|
+
onFleet: async (action, target) => {
|
|
4943
|
+
if (action === "status") {
|
|
4944
|
+
const s = multiAgentHost.status();
|
|
4945
|
+
const lines = [color.bold("Fleet status"), ` ${s.summary}`];
|
|
4946
|
+
if (s.pending.length > 0) {
|
|
4947
|
+
lines.push("", color.dim(" Pending"));
|
|
4948
|
+
for (const p of s.pending) {
|
|
4949
|
+
lines.push(` ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
if (s.completed.length > 0) {
|
|
4953
|
+
lines.push("", color.dim(" Completed"));
|
|
4954
|
+
for (const r of s.completed) {
|
|
4955
|
+
const mark = r.status === "success" ? color.green("\u2713") : color.red("\u2717");
|
|
4956
|
+
lines.push(` ${mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${r.iterations}it ${r.toolCalls}tc ${r.durationMs}ms`);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
return lines.join("\n");
|
|
4960
|
+
}
|
|
4961
|
+
if (action === "usage") {
|
|
4962
|
+
const u = multiAgentHost.usage();
|
|
4963
|
+
if (u.rows.length === 0) return "No completed subagent tasks yet.";
|
|
4964
|
+
const lines = [
|
|
4965
|
+
color.bold("Fleet usage"),
|
|
4966
|
+
color.dim(" subagent tasks iter tools ms status")
|
|
4967
|
+
];
|
|
4968
|
+
for (const r of u.rows) {
|
|
4969
|
+
lines.push(
|
|
4970
|
+
` ${r.subagentId.slice(0, 14).padEnd(14)} ${String(r.tasks).padStart(5)} ${String(r.iterations).padStart(4)} ${String(r.toolCalls).padStart(5)} ${String(r.durationMs).padStart(5)} ${r.status}`
|
|
4971
|
+
);
|
|
4972
|
+
}
|
|
4973
|
+
lines.push(
|
|
4974
|
+
color.dim(" \u2500".repeat(28)),
|
|
4975
|
+
` ${"TOTAL".padEnd(14)} ${String(u.totals.tasks).padStart(5)} ${String(u.totals.iterations).padStart(4)} ${String(u.totals.toolCalls).padStart(5)} ${String(u.totals.durationMs).padStart(5)}`
|
|
4976
|
+
);
|
|
4977
|
+
return lines.join("\n");
|
|
4978
|
+
}
|
|
4979
|
+
if (action === "kill") {
|
|
4980
|
+
if (!target) return "Usage: /fleet kill <subagent-id>";
|
|
4981
|
+
const ok = await multiAgentHost.kill(target);
|
|
4982
|
+
return ok ? `Sent stop signal to ${target}.` : "No coordinator is running yet \u2014 nothing to kill.";
|
|
4983
|
+
}
|
|
4984
|
+
if (action === "manifest") {
|
|
4985
|
+
if (!multiAgentHost.isDirectorMode()) {
|
|
4986
|
+
return "Manifest is only available when the run was started with --director.";
|
|
4987
|
+
}
|
|
4988
|
+
const p = await multiAgentHost.manifest();
|
|
4989
|
+
if (!p) {
|
|
4990
|
+
return "Director is active but no subagents have been spawned \u2014 nothing to record yet.";
|
|
4991
|
+
}
|
|
4992
|
+
return `Manifest written \u2192 ${p}`;
|
|
4993
|
+
}
|
|
4994
|
+
return `Unknown fleet action: ${action}`;
|
|
4995
|
+
},
|
|
4121
4996
|
onExit: () => {
|
|
4122
4997
|
void mcpRegistry.stopAll();
|
|
4123
4998
|
},
|
|
@@ -4255,11 +5130,36 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4255
5130
|
process.stdout.write(
|
|
4256
5131
|
color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
|
|
4257
5132
|
);
|
|
5133
|
+
},
|
|
5134
|
+
onClearHistory: (dispatch) => {
|
|
5135
|
+
dispatch({ type: "clearHistory" });
|
|
5136
|
+
dispatch({ type: "resetContextChip" });
|
|
4258
5137
|
}
|
|
4259
5138
|
});
|
|
4260
5139
|
} finally {
|
|
4261
5140
|
renderer.setSilent(false);
|
|
4262
5141
|
}
|
|
5142
|
+
} else if (flags.webui) {
|
|
5143
|
+
const { runWebUI: runWebUI2 } = await Promise.resolve().then(() => (init_webui_server(), webui_server_exports));
|
|
5144
|
+
const webuiPromise = runWebUI2({
|
|
5145
|
+
agent,
|
|
5146
|
+
events,
|
|
5147
|
+
session,
|
|
5148
|
+
port: Number.parseInt(String(flags.port ?? "3457"), 10),
|
|
5149
|
+
modelsRegistry,
|
|
5150
|
+
globalConfigPath: wpaths.globalConfig
|
|
5151
|
+
});
|
|
5152
|
+
code = await runRepl({
|
|
5153
|
+
agent,
|
|
5154
|
+
renderer,
|
|
5155
|
+
reader,
|
|
5156
|
+
slashRegistry,
|
|
5157
|
+
tokenCounter,
|
|
5158
|
+
attachments,
|
|
5159
|
+
effectiveMaxContext,
|
|
5160
|
+
projectName: path5.basename(projectRoot) || void 0
|
|
5161
|
+
});
|
|
5162
|
+
await webuiPromise;
|
|
4263
5163
|
} else {
|
|
4264
5164
|
code = await runRepl({
|
|
4265
5165
|
agent,
|
|
@@ -4268,7 +5168,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4268
5168
|
slashRegistry,
|
|
4269
5169
|
tokenCounter,
|
|
4270
5170
|
attachments,
|
|
4271
|
-
effectiveMaxContext
|
|
5171
|
+
effectiveMaxContext,
|
|
5172
|
+
projectName: path5.basename(projectRoot) || void 0
|
|
4272
5173
|
});
|
|
4273
5174
|
}
|
|
4274
5175
|
} finally {
|