@wrongstack/cli 0.1.10 → 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/README.md +19 -3
- package/dist/index.js +594 -67
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color, DefaultPathResolver, 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, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, makeDirectorSessionFactory,
|
|
2
|
+
import { color, DefaultPathResolver, 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, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, InputBuilder, decryptConfigSecrets, encryptConfigSecrets, DefaultPluginAPI } from '@wrongstack/core';
|
|
3
3
|
import * as fs3 from 'fs/promises';
|
|
4
4
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
@@ -8,7 +8,7 @@ import * as path13 from 'path';
|
|
|
8
8
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
9
9
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
10
10
|
import { rememberTool, forgetTool } from '@wrongstack/tools';
|
|
11
|
-
import {
|
|
11
|
+
import { builtinToolsPack } from '@wrongstack/tools/pack';
|
|
12
12
|
import * as os3 from 'os';
|
|
13
13
|
import * as readline from 'readline';
|
|
14
14
|
import { randomUUID } from 'crypto';
|
|
@@ -1470,7 +1470,7 @@ function buildStatsCommand(opts) {
|
|
|
1470
1470
|
function buildFleetCommand(opts) {
|
|
1471
1471
|
return {
|
|
1472
1472
|
name: "fleet",
|
|
1473
|
-
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|help]",
|
|
1473
|
+
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|retry [taskId]|log <id>|stream on|off|help]",
|
|
1474
1474
|
help: [
|
|
1475
1475
|
"Usage:",
|
|
1476
1476
|
" /fleet Show fleet status (alias for /fleet status).",
|
|
@@ -1478,6 +1478,13 @@ function buildFleetCommand(opts) {
|
|
|
1478
1478
|
" /fleet usage Per-subagent runtime cost.",
|
|
1479
1479
|
" /fleet kill <id> Terminate a running subagent.",
|
|
1480
1480
|
" /fleet manifest Print the director manifest.",
|
|
1481
|
+
" /fleet retry List interrupted tasks from the last run.",
|
|
1482
|
+
" /fleet retry <taskId> Re-spawn the matching subagent and re-assign the task.",
|
|
1483
|
+
" /fleet retry all Re-assign every interrupted task at once.",
|
|
1484
|
+
" /fleet log List subagent transcripts available on disk.",
|
|
1485
|
+
" /fleet log <id> Print a compact summary of a subagent transcript.",
|
|
1486
|
+
" /fleet log <id> raw Dump the full per-subagent JSONL.",
|
|
1487
|
+
" /fleet stream on|off Show/hide subagent activity in the main history.",
|
|
1481
1488
|
" /fleet help Show this help."
|
|
1482
1489
|
].join("\n"),
|
|
1483
1490
|
async run(args) {
|
|
@@ -1496,6 +1503,38 @@ function buildFleetCommand(opts) {
|
|
|
1496
1503
|
if (!target) return { message: "Usage: /fleet kill <subagent-id>" };
|
|
1497
1504
|
return { message: await opts.onFleet("kill", target) };
|
|
1498
1505
|
}
|
|
1506
|
+
case "retry": {
|
|
1507
|
+
if (!opts.onFleetRetry) {
|
|
1508
|
+
return { message: "Retry is only available when director mode is active." };
|
|
1509
|
+
}
|
|
1510
|
+
const msg = await opts.onFleetRetry(target);
|
|
1511
|
+
return { message: msg };
|
|
1512
|
+
}
|
|
1513
|
+
case "log": {
|
|
1514
|
+
if (!opts.onFleetLog) {
|
|
1515
|
+
return { message: "Log inspection is only available when a fleet root is configured." };
|
|
1516
|
+
}
|
|
1517
|
+
const [id, ...modeRest] = rest;
|
|
1518
|
+
const mode = modeRest.join(" ").trim() === "raw" ? "raw" : "summary";
|
|
1519
|
+
return { message: await opts.onFleetLog(id, mode) };
|
|
1520
|
+
}
|
|
1521
|
+
case "stream": {
|
|
1522
|
+
const ctrl = opts.fleetStreamController;
|
|
1523
|
+
if (!ctrl) {
|
|
1524
|
+
return { message: "Stream toggle is only available in the TUI." };
|
|
1525
|
+
}
|
|
1526
|
+
const arg = (target ?? "").toLowerCase();
|
|
1527
|
+
if (arg === "" || arg === "status") {
|
|
1528
|
+
return { message: `Fleet streaming is ${ctrl.enabled ? "on" : "off"}.` };
|
|
1529
|
+
}
|
|
1530
|
+
if (arg !== "on" && arg !== "off") {
|
|
1531
|
+
return { message: "Usage: /fleet stream on|off" };
|
|
1532
|
+
}
|
|
1533
|
+
const enabled = arg === "on";
|
|
1534
|
+
ctrl.setEnabled(enabled);
|
|
1535
|
+
ctrl.enabled = enabled;
|
|
1536
|
+
return { message: `Fleet streaming ${enabled ? "enabled" : "disabled"}.` };
|
|
1537
|
+
}
|
|
1499
1538
|
case "help":
|
|
1500
1539
|
case "?":
|
|
1501
1540
|
return {
|
|
@@ -1713,6 +1752,66 @@ function buildMetricsCommand(opts) {
|
|
|
1713
1752
|
}
|
|
1714
1753
|
};
|
|
1715
1754
|
}
|
|
1755
|
+
function buildPlanCommand(opts) {
|
|
1756
|
+
return {
|
|
1757
|
+
name: "plan",
|
|
1758
|
+
description: "Strategic plan board: /plan [show|add <title>|done <id|#>|remove <id|#>|clear]",
|
|
1759
|
+
async run(args) {
|
|
1760
|
+
const planPath = opts.planPath;
|
|
1761
|
+
if (!planPath) return { message: "Plan storage is not configured for this session." };
|
|
1762
|
+
const ctx = opts.context;
|
|
1763
|
+
const sessionId = ctx?.session.id ?? "unknown";
|
|
1764
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
1765
|
+
const restJoined = rest.join(" ").trim();
|
|
1766
|
+
const plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
1767
|
+
switch (verb) {
|
|
1768
|
+
case "":
|
|
1769
|
+
case "show":
|
|
1770
|
+
case "list": {
|
|
1771
|
+
return { message: formatPlan(plan) };
|
|
1772
|
+
}
|
|
1773
|
+
case "add": {
|
|
1774
|
+
if (!restJoined) return { message: "Usage: /plan add <title>" };
|
|
1775
|
+
const { plan: updated, item } = addPlanItem(plan, restJoined);
|
|
1776
|
+
await savePlan(planPath, updated);
|
|
1777
|
+
return { message: `Added: ${item.title}
|
|
1778
|
+
${formatPlan(updated)}` };
|
|
1779
|
+
}
|
|
1780
|
+
case "start":
|
|
1781
|
+
case "progress": {
|
|
1782
|
+
if (!restJoined) return { message: "Usage: /plan start <id|index>" };
|
|
1783
|
+
const updated = setPlanItemStatus(plan, restJoined, "in_progress");
|
|
1784
|
+
await savePlan(planPath, updated);
|
|
1785
|
+
return { message: formatPlan(updated) };
|
|
1786
|
+
}
|
|
1787
|
+
case "done":
|
|
1788
|
+
case "complete": {
|
|
1789
|
+
if (!restJoined) return { message: "Usage: /plan done <id|index>" };
|
|
1790
|
+
const updated = setPlanItemStatus(plan, restJoined, "done");
|
|
1791
|
+
await savePlan(planPath, updated);
|
|
1792
|
+
return { message: formatPlan(updated) };
|
|
1793
|
+
}
|
|
1794
|
+
case "remove":
|
|
1795
|
+
case "delete":
|
|
1796
|
+
case "rm": {
|
|
1797
|
+
if (!restJoined) return { message: "Usage: /plan remove <id|index>" };
|
|
1798
|
+
const updated = removePlanItem(plan, restJoined);
|
|
1799
|
+
await savePlan(planPath, updated);
|
|
1800
|
+
return { message: formatPlan(updated) };
|
|
1801
|
+
}
|
|
1802
|
+
case "clear": {
|
|
1803
|
+
const updated = clearPlan(plan);
|
|
1804
|
+
await savePlan(planPath, updated);
|
|
1805
|
+
return { message: "Plan cleared." };
|
|
1806
|
+
}
|
|
1807
|
+
default:
|
|
1808
|
+
return {
|
|
1809
|
+
message: `Unknown subcommand "${verb}". Try: show | add <title> | start <id|#> | done <id|#> | remove <id|#> | clear`
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1716
1815
|
function buildSaveCommand(opts) {
|
|
1717
1816
|
return {
|
|
1718
1817
|
name: "save",
|
|
@@ -1922,6 +2021,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
1922
2021
|
buildHealthCommand(opts),
|
|
1923
2022
|
buildMemoryCommand(opts),
|
|
1924
2023
|
buildTodosCommand(opts),
|
|
2024
|
+
buildPlanCommand(opts),
|
|
1925
2025
|
buildSaveCommand(opts),
|
|
1926
2026
|
buildLoadCommand(opts),
|
|
1927
2027
|
buildExitCommand(opts)
|
|
@@ -3704,6 +3804,35 @@ function fmtTok(n) {
|
|
|
3704
3804
|
function patchConfig(base, patch) {
|
|
3705
3805
|
return Object.freeze({ ...base, ...patch });
|
|
3706
3806
|
}
|
|
3807
|
+
function fmtDuration(ms) {
|
|
3808
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3809
|
+
const s = ms / 1e3;
|
|
3810
|
+
if (s < 60) return `${s.toFixed(s < 10 ? 1 : 0)}s`;
|
|
3811
|
+
const m = Math.floor(s / 60);
|
|
3812
|
+
const remSec = Math.round(s - m * 60);
|
|
3813
|
+
if (m < 60) return remSec === 0 ? `${m}m` : `${m}m${remSec}s`;
|
|
3814
|
+
const h = Math.floor(m / 60);
|
|
3815
|
+
const remMin = m - h * 60;
|
|
3816
|
+
return `${h}h${remMin}m`;
|
|
3817
|
+
}
|
|
3818
|
+
function fmtTaskResultLine(r, color28) {
|
|
3819
|
+
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
3820
|
+
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
3821
|
+
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
3822
|
+
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
3823
|
+
const errKindChip = errKind ? color28.dim(` [${errKind}]`) : "";
|
|
3824
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color28.dim(errTail)}` : "";
|
|
3825
|
+
switch (r.status) {
|
|
3826
|
+
case "success":
|
|
3827
|
+
return { mark: color28.green("\u2713"), stats, tail: "" };
|
|
3828
|
+
case "timeout":
|
|
3829
|
+
return { mark: color28.yellow("\u23F1"), stats: `${color28.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
3830
|
+
case "stopped":
|
|
3831
|
+
return { mark: color28.dim("\u2298"), stats: `${color28.dim("stopped")} ${stats}`, tail: errSnip };
|
|
3832
|
+
case "failed":
|
|
3833
|
+
return { mark: color28.red("\u2717"), stats: `${color28.red("failed")} ${stats}`, tail: errSnip };
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3707
3836
|
|
|
3708
3837
|
// src/boot.ts
|
|
3709
3838
|
function resolveBundledSkillsDir() {
|
|
@@ -3747,7 +3876,10 @@ async function boot(argv) {
|
|
|
3747
3876
|
bundledDir: resolveBundledSkillsDir()
|
|
3748
3877
|
});
|
|
3749
3878
|
const toolRegistryForSubcmd = new ToolRegistry();
|
|
3750
|
-
|
|
3879
|
+
toolRegistryForSubcmd.registerAllOrThrow(
|
|
3880
|
+
[...builtinToolsPack.tools ?? []],
|
|
3881
|
+
builtinToolsPack.name
|
|
3882
|
+
);
|
|
3751
3883
|
const code = await subcommands[first](positional.slice(1), {
|
|
3752
3884
|
config,
|
|
3753
3885
|
renderer,
|
|
@@ -4013,7 +4145,10 @@ async function execute(deps) {
|
|
|
4013
4145
|
savedProviderCfg,
|
|
4014
4146
|
resolvedProvider,
|
|
4015
4147
|
getPickableProviders,
|
|
4016
|
-
switchProviderAndModel
|
|
4148
|
+
switchProviderAndModel,
|
|
4149
|
+
director,
|
|
4150
|
+
fleetRoster,
|
|
4151
|
+
fleetStreamController
|
|
4017
4152
|
} = deps;
|
|
4018
4153
|
let code = 0;
|
|
4019
4154
|
try {
|
|
@@ -4021,6 +4156,11 @@ async function execute(deps) {
|
|
|
4021
4156
|
if (promptFlag) {
|
|
4022
4157
|
positional.unshift(promptFlag);
|
|
4023
4158
|
}
|
|
4159
|
+
const goalFlag = typeof flags["goal"] === "string" ? flags["goal"] : void 0;
|
|
4160
|
+
const askFlag = typeof flags["ask"] === "string" ? flags["ask"] : void 0;
|
|
4161
|
+
if ((goalFlag || askFlag) && positional.length === 0 && !promptFlag) {
|
|
4162
|
+
flags.tui = true;
|
|
4163
|
+
}
|
|
4024
4164
|
if (positional.length > 0 || promptFlag) {
|
|
4025
4165
|
const query = positional.join(" ");
|
|
4026
4166
|
const ctrl = new AbortController();
|
|
@@ -4107,7 +4247,14 @@ async function execute(deps) {
|
|
|
4107
4247
|
getPickableProviders,
|
|
4108
4248
|
switchProviderAndModel,
|
|
4109
4249
|
effectiveMaxContext,
|
|
4110
|
-
|
|
4250
|
+
// Default OFF so the terminal's native scrollback works for chat
|
|
4251
|
+
// history out of the box (mouse wheel / Shift+PgUp). Users who hit
|
|
4252
|
+
// resize/overlay-leak artifacts can opt back into alt-screen with
|
|
4253
|
+
// `--alt-screen` or `/altscreen on`. `--no-alt-screen` still wins
|
|
4254
|
+
// when both are passed.
|
|
4255
|
+
altScreen: flags["alt-screen"] === true && flags["no-alt-screen"] !== true,
|
|
4256
|
+
director,
|
|
4257
|
+
fleetRoster,
|
|
4111
4258
|
onAfterExit: () => {
|
|
4112
4259
|
process.stdout.write(
|
|
4113
4260
|
color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
|
|
@@ -4116,7 +4263,10 @@ async function execute(deps) {
|
|
|
4116
4263
|
onClearHistory: (dispatch) => {
|
|
4117
4264
|
dispatch({ type: "clearHistory" });
|
|
4118
4265
|
dispatch({ type: "resetContextChip" });
|
|
4119
|
-
}
|
|
4266
|
+
},
|
|
4267
|
+
fleetStreamController,
|
|
4268
|
+
initialGoal: goalFlag,
|
|
4269
|
+
initialAsk: askFlag
|
|
4120
4270
|
});
|
|
4121
4271
|
} finally {
|
|
4122
4272
|
renderer.setSilent(false);
|
|
@@ -4185,6 +4335,13 @@ var MultiAgentHost = class {
|
|
|
4185
4335
|
pending = /* @__PURE__ */ new Map();
|
|
4186
4336
|
results = [];
|
|
4187
4337
|
opts;
|
|
4338
|
+
/**
|
|
4339
|
+
* Populated by `promoteToDirector` when it refuses to promote (typically
|
|
4340
|
+
* because a non-director coordinator is already running). The delegate
|
|
4341
|
+
* tool reads this through `getPromotionBlockReason` to render an
|
|
4342
|
+
* actionable error instead of a generic "could not activate director".
|
|
4343
|
+
*/
|
|
4344
|
+
promotionBlockReason = null;
|
|
4188
4345
|
/**
|
|
4189
4346
|
* Force the lazy build path to run *now* and return the live Director,
|
|
4190
4347
|
* or null when director mode is off. Used by the CLI to register the
|
|
@@ -4208,8 +4365,64 @@ var MultiAgentHost = class {
|
|
|
4208
4365
|
directorRunId: this.opts.directorRunId
|
|
4209
4366
|
});
|
|
4210
4367
|
}
|
|
4368
|
+
const coordinatorConfig = {
|
|
4369
|
+
coordinatorId: randomUUID(),
|
|
4370
|
+
doneCondition: { type: "all_tasks_done" },
|
|
4371
|
+
maxConcurrent: 8
|
|
4372
|
+
// No defaultBudget. Caps land on a subagent ONLY when the
|
|
4373
|
+
// orchestrator (delegate-tool / spawn_subagent) or the user
|
|
4374
|
+
// (CLI flag) sets them explicitly. The prior defaults
|
|
4375
|
+
// (1000 tools / 200 iter / 4h) silently killed long autonomous
|
|
4376
|
+
// runs; for a "work until done" director we want no implicit
|
|
4377
|
+
// ceilings. The orchestrator can still cap a single subagent
|
|
4378
|
+
// by passing maxToolCalls/maxIterations through the spawn tool.
|
|
4379
|
+
};
|
|
4380
|
+
if (this.opts.directorMode) {
|
|
4381
|
+
this.director = new Director({
|
|
4382
|
+
config: coordinatorConfig,
|
|
4383
|
+
manifestPath: this.opts.manifestPath,
|
|
4384
|
+
sharedScratchpadPath: this.opts.sharedScratchpadPath,
|
|
4385
|
+
stateCheckpointPath: this.opts.stateCheckpointPath,
|
|
4386
|
+
sessionWriter: this.opts.sessionWriter,
|
|
4387
|
+
// Autonomy: allow nested directors a few levels deep. Default
|
|
4388
|
+
// is 2 (root + one tier of workers), which trips the moment a
|
|
4389
|
+
// worker tries to recurse into "let me delegate the parser
|
|
4390
|
+
// analysis to a tighter specialist". 5 lets the director
|
|
4391
|
+
// structure work as deeply as the task requires without us
|
|
4392
|
+
// having to pass a flag every time.
|
|
4393
|
+
maxSpawnDepth: 5
|
|
4394
|
+
});
|
|
4395
|
+
this.director.on("task.completed", ({ task, result }) => {
|
|
4396
|
+
this.results.push(result);
|
|
4397
|
+
this.pending.delete(task.id);
|
|
4398
|
+
this.emitLifecycleCompleted(task.id, result);
|
|
4399
|
+
});
|
|
4400
|
+
this.coordinator = this.director.coordinator;
|
|
4401
|
+
} else {
|
|
4402
|
+
this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, {});
|
|
4403
|
+
this.coordinator.on(
|
|
4404
|
+
"task.completed",
|
|
4405
|
+
({ task, result }) => {
|
|
4406
|
+
this.results.push(result);
|
|
4407
|
+
this.pending.delete(task.id);
|
|
4408
|
+
this.emitLifecycleCompleted(task.id, result);
|
|
4409
|
+
}
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
this.coordinator.on(
|
|
4413
|
+
"task.assigned",
|
|
4414
|
+
({ task, subagentId }) => {
|
|
4415
|
+
this.deps.events.emit("subagent.task_started", {
|
|
4416
|
+
subagentId,
|
|
4417
|
+
taskId: task.id,
|
|
4418
|
+
description: task.description
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
);
|
|
4211
4422
|
const runner = this.buildSubagentRunner(config);
|
|
4212
|
-
|
|
4423
|
+
const innerCoord = this.opts.directorMode ? this.director.coordinator : this.coordinator;
|
|
4424
|
+
innerCoord.setRunner(runner);
|
|
4425
|
+
return this.coordinator;
|
|
4213
4426
|
}
|
|
4214
4427
|
/**
|
|
4215
4428
|
* Build the per-subagent runner: agent factory → runner. Extracted so
|
|
@@ -4224,7 +4437,12 @@ var MultiAgentHost = class {
|
|
|
4224
4437
|
projectRoot: this.deps.projectRoot,
|
|
4225
4438
|
tools: this.filterTools(subCfg.tools),
|
|
4226
4439
|
model: subCfg.model ?? config.model,
|
|
4227
|
-
provider: subCfg.provider ?? config.provider
|
|
4440
|
+
provider: subCfg.provider ?? config.provider,
|
|
4441
|
+
// Tell the builder this is a subagent build — skips the host's
|
|
4442
|
+
// plan injection so each subagent gets a clean, task-scoped
|
|
4443
|
+
// prompt instead of inheriting strategic context that's
|
|
4444
|
+
// meaningless to a single delegated subtask.
|
|
4445
|
+
subagent: true
|
|
4228
4446
|
});
|
|
4229
4447
|
let subSession;
|
|
4230
4448
|
if (this.sessionFactory) {
|
|
@@ -4259,46 +4477,34 @@ var MultiAgentHost = class {
|
|
|
4259
4477
|
providers: this.deps.providerRegistry,
|
|
4260
4478
|
events,
|
|
4261
4479
|
pipelines: createDefaultPipelines(),
|
|
4262
|
-
context: ctx
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
/**
|
|
4269
|
-
* Build the coordinator (or Director wrapper) with task-completion
|
|
4270
|
-
* drain wired into the host's result buffer.
|
|
4271
|
-
*/
|
|
4272
|
-
buildCoordinator(runner) {
|
|
4273
|
-
const coordinatorConfig = {
|
|
4274
|
-
coordinatorId: randomUUID(),
|
|
4275
|
-
doneCondition: { type: "all_tasks_done" },
|
|
4276
|
-
maxConcurrent: 2,
|
|
4277
|
-
defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
|
|
4278
|
-
};
|
|
4279
|
-
if (this.opts.directorMode) {
|
|
4280
|
-
this.director = new Director({
|
|
4281
|
-
config: coordinatorConfig,
|
|
4282
|
-
runner,
|
|
4283
|
-
manifestPath: this.opts.manifestPath,
|
|
4284
|
-
sharedScratchpadPath: this.opts.sharedScratchpadPath
|
|
4480
|
+
context: ctx,
|
|
4481
|
+
// Subagents cannot answer interactive permission prompts — they
|
|
4482
|
+
// run under a director, not the user. Auto-approve everything
|
|
4483
|
+
// (except tool-level hard denies); the user already authorized
|
|
4484
|
+
// the work when they invoked the leader.
|
|
4485
|
+
permissionPolicy: new AutoApprovePermissionPolicy()
|
|
4285
4486
|
});
|
|
4286
|
-
this.
|
|
4287
|
-
|
|
4288
|
-
|
|
4487
|
+
const hostEvents = this.deps.events;
|
|
4488
|
+
const offToolBridge = events.on("tool.executed", (e) => {
|
|
4489
|
+
hostEvents.emit("subagent.tool_executed", {
|
|
4490
|
+
subagentId: subCfg.id ?? subCfg.name ?? "subagent",
|
|
4491
|
+
name: e.name,
|
|
4492
|
+
durationMs: e.durationMs,
|
|
4493
|
+
ok: e.ok,
|
|
4494
|
+
input: e.input,
|
|
4495
|
+
outputBytes: e.outputBytes
|
|
4496
|
+
});
|
|
4289
4497
|
});
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
);
|
|
4301
|
-
return this.coordinator;
|
|
4498
|
+
const dispose = async () => {
|
|
4499
|
+
offToolBridge();
|
|
4500
|
+
try {
|
|
4501
|
+
await subSession.close?.();
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
};
|
|
4505
|
+
return { agent, events, dispose };
|
|
4506
|
+
};
|
|
4507
|
+
return makeAgentSubagentRunner({ factory, fleetBus: this.director?.fleet });
|
|
4302
4508
|
}
|
|
4303
4509
|
/**
|
|
4304
4510
|
* Build a Provider for a subagent. When `overrideId` is supplied (from
|
|
@@ -4348,21 +4554,30 @@ var MultiAgentHost = class {
|
|
|
4348
4554
|
const subagentConfig = {
|
|
4349
4555
|
name: opts?.name ?? "adhoc",
|
|
4350
4556
|
role: "general",
|
|
4351
|
-
maxToolCalls: 20,
|
|
4352
|
-
maxIterations: 20,
|
|
4353
4557
|
provider: opts?.provider,
|
|
4354
4558
|
model: opts?.model,
|
|
4355
4559
|
tools: opts?.tools
|
|
4356
4560
|
};
|
|
4561
|
+
const transcriptPath = this.sessionFactory ? path13.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
4357
4562
|
if (this.director) {
|
|
4358
4563
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
4359
4564
|
const taskId2 = randomUUID();
|
|
4360
4565
|
this.pending.set(taskId2, { description, subagentId });
|
|
4566
|
+
this.deps.events.emit("subagent.spawned", {
|
|
4567
|
+
subagentId,
|
|
4568
|
+
taskId: taskId2,
|
|
4569
|
+
name: subagentConfig.name,
|
|
4570
|
+
provider: opts?.provider,
|
|
4571
|
+
model: opts?.model,
|
|
4572
|
+
description,
|
|
4573
|
+
transcriptPath
|
|
4574
|
+
});
|
|
4361
4575
|
await this.director.assign({
|
|
4362
4576
|
id: taskId2,
|
|
4363
4577
|
description,
|
|
4364
|
-
subagentId
|
|
4365
|
-
maxToolCalls
|
|
4578
|
+
subagentId
|
|
4579
|
+
// No maxToolCalls — same reasoning as the spawn config above.
|
|
4580
|
+
// The director / orchestrator owns the budget decision.
|
|
4366
4581
|
});
|
|
4367
4582
|
return { subagentId, taskId: taskId2 };
|
|
4368
4583
|
}
|
|
@@ -4370,14 +4585,42 @@ var MultiAgentHost = class {
|
|
|
4370
4585
|
const spawned = await coord.spawn(subagentConfig);
|
|
4371
4586
|
const taskId = randomUUID();
|
|
4372
4587
|
this.pending.set(taskId, { description, subagentId: spawned.subagentId });
|
|
4588
|
+
this.deps.events.emit("subagent.spawned", {
|
|
4589
|
+
subagentId: spawned.subagentId,
|
|
4590
|
+
taskId,
|
|
4591
|
+
name: subagentConfig.name,
|
|
4592
|
+
provider: opts?.provider,
|
|
4593
|
+
model: opts?.model,
|
|
4594
|
+
description,
|
|
4595
|
+
transcriptPath
|
|
4596
|
+
});
|
|
4373
4597
|
await coord.assign({
|
|
4374
4598
|
id: taskId,
|
|
4375
4599
|
description,
|
|
4376
|
-
subagentId: spawned.subagentId
|
|
4377
|
-
maxToolCalls
|
|
4600
|
+
subagentId: spawned.subagentId
|
|
4601
|
+
// No maxToolCalls — see comment on the director branch above.
|
|
4378
4602
|
});
|
|
4379
4603
|
return { subagentId: spawned.subagentId, taskId };
|
|
4380
4604
|
}
|
|
4605
|
+
/**
|
|
4606
|
+
* Relay a `task.completed` notification (from either the Director or
|
|
4607
|
+
* the raw coordinator) to the EventBus so non-director TUIs and any
|
|
4608
|
+
* other observer can react. We forward the full result shape rather
|
|
4609
|
+
* than mutating the existing `task.completed` schema — coordination
|
|
4610
|
+
* code already binds to that event, and adding subscribers there
|
|
4611
|
+
* would change ordering semantics for those listeners.
|
|
4612
|
+
*/
|
|
4613
|
+
emitLifecycleCompleted(taskId, result) {
|
|
4614
|
+
this.deps.events.emit("subagent.task_completed", {
|
|
4615
|
+
subagentId: result.subagentId,
|
|
4616
|
+
taskId,
|
|
4617
|
+
status: result.status,
|
|
4618
|
+
iterations: result.iterations,
|
|
4619
|
+
toolCalls: result.toolCalls,
|
|
4620
|
+
durationMs: result.durationMs,
|
|
4621
|
+
error: result.error
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4381
4624
|
status() {
|
|
4382
4625
|
const pending = Array.from(this.pending.entries()).map(([taskId, v]) => ({
|
|
4383
4626
|
taskId,
|
|
@@ -4459,6 +4702,10 @@ var MultiAgentHost = class {
|
|
|
4459
4702
|
async promoteToDirector() {
|
|
4460
4703
|
if (this.director) return this.director;
|
|
4461
4704
|
if (this.coordinator) {
|
|
4705
|
+
const status = this.coordinator.getStatus();
|
|
4706
|
+
const running = status.subagents.filter((s) => s.status === "running").length;
|
|
4707
|
+
const idle = status.subagents.filter((s) => s.status === "idle").length;
|
|
4708
|
+
this.promotionBlockReason = `Cannot promote to director: a non-director coordinator is already in use (${running} running, ${idle} idle, ${status.pendingTasks} pending tasks). Stop the existing subagents with /fleet kill <id> or wait for them to finish, then retry \u2014 or restart wstack with --director to start in director mode.`;
|
|
4462
4709
|
return null;
|
|
4463
4710
|
}
|
|
4464
4711
|
this.opts.directorMode = true;
|
|
@@ -4471,6 +4718,9 @@ var MultiAgentHost = class {
|
|
|
4471
4718
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
4472
4719
|
this.opts.sessionsRoot = path13.join(this.opts.fleetRoot, "subagents");
|
|
4473
4720
|
}
|
|
4721
|
+
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
4722
|
+
this.opts.stateCheckpointPath = path13.join(this.opts.fleetRoot, "director-state.json");
|
|
4723
|
+
}
|
|
4474
4724
|
await this.ensureDirector();
|
|
4475
4725
|
return this.director ?? null;
|
|
4476
4726
|
}
|
|
@@ -4482,6 +4732,16 @@ var MultiAgentHost = class {
|
|
|
4482
4732
|
isDirectorMode() {
|
|
4483
4733
|
return !!this.director;
|
|
4484
4734
|
}
|
|
4735
|
+
/**
|
|
4736
|
+
* Why the most recent `promoteToDirector` call returned null. Cleared
|
|
4737
|
+
* implicitly on the next successful promotion. The delegate tool reads
|
|
4738
|
+
* this so the LLM sees the actual blocker (e.g. "3 running subagents,
|
|
4739
|
+
* wait or /fleet kill") instead of a generic "Director could not be
|
|
4740
|
+
* activated" message that gives no path forward.
|
|
4741
|
+
*/
|
|
4742
|
+
getPromotionBlockReason() {
|
|
4743
|
+
return this.promotionBlockReason;
|
|
4744
|
+
}
|
|
4485
4745
|
/**
|
|
4486
4746
|
* Terminate a single subagent. Returns true when the subagent existed
|
|
4487
4747
|
* (regardless of whether stop() succeeded or it was already idle),
|
|
@@ -4844,6 +5104,7 @@ async function main(argv) {
|
|
|
4844
5104
|
supportsVision: resolvedModel.capabilities.vision,
|
|
4845
5105
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
4846
5106
|
} : void 0;
|
|
5107
|
+
const sessionRef = {};
|
|
4847
5108
|
container.bind(
|
|
4848
5109
|
TOKENS.SystemPromptBuilder,
|
|
4849
5110
|
() => new DefaultSystemPromptBuilder({
|
|
@@ -4852,7 +5113,10 @@ async function main(argv) {
|
|
|
4852
5113
|
modeStore,
|
|
4853
5114
|
modeId,
|
|
4854
5115
|
modePrompt,
|
|
4855
|
-
modelCapabilities
|
|
5116
|
+
modelCapabilities,
|
|
5117
|
+
// Reads the ref each time — returns undefined until the session
|
|
5118
|
+
// is created, then resolves to the per-session plan JSON path.
|
|
5119
|
+
planPath: () => sessionRef.current ? path13.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
4856
5120
|
})
|
|
4857
5121
|
);
|
|
4858
5122
|
container.bind(TOKENS.Renderer, () => renderer);
|
|
@@ -4891,7 +5155,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
4891
5155
|
}
|
|
4892
5156
|
}
|
|
4893
5157
|
const toolRegistry = new ToolRegistry();
|
|
4894
|
-
|
|
5158
|
+
toolRegistry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
4895
5159
|
toolRegistry.registerDefault(
|
|
4896
5160
|
createContextManagerTool({ compactor: container.resolve(TOKENS.Compactor) })
|
|
4897
5161
|
);
|
|
@@ -5091,6 +5355,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5091
5355
|
provider: config.provider
|
|
5092
5356
|
});
|
|
5093
5357
|
}
|
|
5358
|
+
sessionRef.current = session;
|
|
5094
5359
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
5095
5360
|
const attachments = new DefaultAttachmentStore({
|
|
5096
5361
|
spoolDir: path13.join(wpaths.projectSessions, session.id, "attachments")
|
|
@@ -5122,6 +5387,54 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5122
5387
|
if (restoredMessages.length > 0) {
|
|
5123
5388
|
context.state.replaceMessages(restoredMessages);
|
|
5124
5389
|
}
|
|
5390
|
+
const todosCheckpointPath = path13.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
5391
|
+
if (resumeId) {
|
|
5392
|
+
try {
|
|
5393
|
+
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
5394
|
+
if (restoredTodos && restoredTodos.length > 0) {
|
|
5395
|
+
context.state.replaceTodos(restoredTodos);
|
|
5396
|
+
renderer.writeInfo(
|
|
5397
|
+
`Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? "" : "s"} from previous run.`
|
|
5398
|
+
);
|
|
5399
|
+
}
|
|
5400
|
+
} catch {
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
attachTodosCheckpoint(
|
|
5404
|
+
context.state,
|
|
5405
|
+
todosCheckpointPath,
|
|
5406
|
+
session.id
|
|
5407
|
+
);
|
|
5408
|
+
const planPath = path13.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
5409
|
+
context.state.setMeta("plan.path", planPath);
|
|
5410
|
+
if (resumeId) {
|
|
5411
|
+
try {
|
|
5412
|
+
const fleetRoot2 = path13.join(wpaths.projectSessions, session.id);
|
|
5413
|
+
const dirState = await loadDirectorState(path13.join(fleetRoot2, "director-state.json"));
|
|
5414
|
+
if (dirState) {
|
|
5415
|
+
const tCounts = {};
|
|
5416
|
+
for (const t of dirState.tasks) {
|
|
5417
|
+
tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
5418
|
+
}
|
|
5419
|
+
const summary = Object.entries(tCounts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
5420
|
+
renderer.writeInfo(
|
|
5421
|
+
`Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? "" : "s"}, tasks ${summary || "(none)"}.`
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
} catch {
|
|
5425
|
+
}
|
|
5426
|
+
try {
|
|
5427
|
+
const plan = await loadPlan(planPath);
|
|
5428
|
+
if (plan && plan.items.length > 0) {
|
|
5429
|
+
const open = plan.items.filter((p) => p.status !== "done").length;
|
|
5430
|
+
const done = plan.items.length - open;
|
|
5431
|
+
renderer.writeInfo(
|
|
5432
|
+
`Plan: ${plan.items.length} item${plan.items.length === 1 ? "" : "s"} (${open} open, ${done} done). Use /plan to review.`
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
} catch {
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5125
5438
|
const pipelines = createDefaultPipelines();
|
|
5126
5439
|
const installBoundary = (p) => {
|
|
5127
5440
|
p.setErrorHandler((ev) => {
|
|
@@ -5244,7 +5557,14 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5244
5557
|
slashCommandRegistry: slashRegistry,
|
|
5245
5558
|
mcpRegistry,
|
|
5246
5559
|
config,
|
|
5247
|
-
log: logger
|
|
5560
|
+
log: logger,
|
|
5561
|
+
extensions: agent.extensions,
|
|
5562
|
+
sessionWriter: {
|
|
5563
|
+
transcriptPath: context.session.transcriptPath,
|
|
5564
|
+
append: (e) => context.session.append(e)
|
|
5565
|
+
},
|
|
5566
|
+
metricsSink,
|
|
5567
|
+
configStore
|
|
5248
5568
|
})
|
|
5249
5569
|
});
|
|
5250
5570
|
}
|
|
@@ -5268,10 +5588,12 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5268
5588
|
}
|
|
5269
5589
|
};
|
|
5270
5590
|
const directorMode = flags["director"] === true;
|
|
5591
|
+
let director = null;
|
|
5271
5592
|
const fleetRoot = directorMode ? path13.join(wpaths.projectSessions, session.id) : void 0;
|
|
5272
5593
|
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path13.join(fleetRoot, "fleet.json") : void 0;
|
|
5273
5594
|
const sharedScratchpadPath = directorMode ? path13.join(fleetRoot, "shared") : void 0;
|
|
5274
5595
|
const subagentSessionsRoot = directorMode ? path13.join(fleetRoot, "subagents") : void 0;
|
|
5596
|
+
const stateCheckpointPath = directorMode ? path13.join(fleetRoot, "director-state.json") : void 0;
|
|
5275
5597
|
const fleetRootForPromotion = path13.join(wpaths.projectSessions, session.id);
|
|
5276
5598
|
const multiAgentHost = new MultiAgentHost(
|
|
5277
5599
|
{
|
|
@@ -5292,11 +5614,25 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5292
5614
|
sharedScratchpadPath,
|
|
5293
5615
|
sessionsRoot: subagentSessionsRoot,
|
|
5294
5616
|
directorRunId: session.id,
|
|
5295
|
-
fleetRoot: fleetRootForPromotion
|
|
5617
|
+
fleetRoot: fleetRootForPromotion,
|
|
5618
|
+
stateCheckpointPath,
|
|
5619
|
+
sessionWriter: session
|
|
5296
5620
|
}
|
|
5297
5621
|
);
|
|
5622
|
+
toolRegistry.register(
|
|
5623
|
+
createDelegateTool({
|
|
5624
|
+
host: multiAgentHost,
|
|
5625
|
+
roster: FLEET_ROSTER,
|
|
5626
|
+
// Wire the per-subagent transcript location so the tool can
|
|
5627
|
+
// extract partial output on timeout / budget exhaustion. Without
|
|
5628
|
+
// this, a subagent that hit its iteration cap returns an empty
|
|
5629
|
+
// result and the host LLM has no idea what work was done.
|
|
5630
|
+
sessionsRoot: subagentSessionsRoot,
|
|
5631
|
+
directorRunId: session.id
|
|
5632
|
+
})
|
|
5633
|
+
);
|
|
5298
5634
|
if (directorMode) {
|
|
5299
|
-
|
|
5635
|
+
director = await multiAgentHost.ensureDirector();
|
|
5300
5636
|
if (director) {
|
|
5301
5637
|
for (const tool of director.tools(FLEET_ROSTER)) {
|
|
5302
5638
|
toolRegistry.register(tool);
|
|
@@ -5310,6 +5646,12 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5310
5646
|
renderer.writeInfo(`Director mode enabled. Fleet manifest \u2192 ${manifestPath}`);
|
|
5311
5647
|
}
|
|
5312
5648
|
}
|
|
5649
|
+
const fleetStreamController = {
|
|
5650
|
+
enabled: true,
|
|
5651
|
+
setEnabled(enabled) {
|
|
5652
|
+
this.enabled = enabled;
|
|
5653
|
+
}
|
|
5654
|
+
};
|
|
5313
5655
|
const slashCmds = buildBuiltinSlashCommands({
|
|
5314
5656
|
registry: slashRegistry,
|
|
5315
5657
|
toolRegistry,
|
|
@@ -5322,6 +5664,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5322
5664
|
context,
|
|
5323
5665
|
metricsSink,
|
|
5324
5666
|
healthRegistry,
|
|
5667
|
+
planPath,
|
|
5668
|
+
fleetStreamController,
|
|
5325
5669
|
onSpawn: async (description, spawnOpts) => {
|
|
5326
5670
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
5327
5671
|
const tags = [];
|
|
@@ -5338,9 +5682,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5338
5682
|
lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
|
|
5339
5683
|
}
|
|
5340
5684
|
for (const r of s.completed) {
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
);
|
|
5685
|
+
const fmt = fmtTaskResultLine(r, color);
|
|
5686
|
+
lines.push(` ${fmt.mark} ${r.taskId.slice(0, 8)} ${fmt.stats}${fmt.tail}`);
|
|
5344
5687
|
}
|
|
5345
5688
|
return lines.join("\n");
|
|
5346
5689
|
},
|
|
@@ -5359,9 +5702,9 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5359
5702
|
if (s.completed.length > 0) {
|
|
5360
5703
|
lines.push("", color.dim(" Completed"));
|
|
5361
5704
|
for (const r of s.completed) {
|
|
5362
|
-
const
|
|
5705
|
+
const fmt = fmtTaskResultLine(r, color);
|
|
5363
5706
|
lines.push(
|
|
5364
|
-
` ${mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${
|
|
5707
|
+
` ${fmt.mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${fmt.stats}${fmt.tail}`
|
|
5365
5708
|
);
|
|
5366
5709
|
}
|
|
5367
5710
|
}
|
|
@@ -5402,10 +5745,191 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5402
5745
|
}
|
|
5403
5746
|
return `Unknown fleet action: ${action}`;
|
|
5404
5747
|
},
|
|
5748
|
+
onFleetLog: async (subagentId, mode) => {
|
|
5749
|
+
const subagentsRoot = path13.join(fleetRootForPromotion, "subagents");
|
|
5750
|
+
let runDirs;
|
|
5751
|
+
try {
|
|
5752
|
+
runDirs = await fs3.readdir(subagentsRoot);
|
|
5753
|
+
} catch {
|
|
5754
|
+
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
5755
|
+
}
|
|
5756
|
+
const found = [];
|
|
5757
|
+
for (const runId of runDirs) {
|
|
5758
|
+
const runDir = path13.join(subagentsRoot, runId);
|
|
5759
|
+
let files;
|
|
5760
|
+
try {
|
|
5761
|
+
files = await fs3.readdir(runDir);
|
|
5762
|
+
} catch {
|
|
5763
|
+
continue;
|
|
5764
|
+
}
|
|
5765
|
+
for (const f of files) {
|
|
5766
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
5767
|
+
const full = path13.join(runDir, f);
|
|
5768
|
+
try {
|
|
5769
|
+
const stat2 = await fs3.stat(full);
|
|
5770
|
+
found.push({
|
|
5771
|
+
runId,
|
|
5772
|
+
subagentId: f.replace(/\.jsonl$/, ""),
|
|
5773
|
+
file: full,
|
|
5774
|
+
size: stat2.size
|
|
5775
|
+
});
|
|
5776
|
+
} catch {
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
if (found.length === 0) {
|
|
5781
|
+
return "No subagent transcripts found on disk.";
|
|
5782
|
+
}
|
|
5783
|
+
if (!subagentId) {
|
|
5784
|
+
const lines2 = [
|
|
5785
|
+
`${found.length} subagent transcript${found.length === 1 ? "" : "s"} on disk:`
|
|
5786
|
+
];
|
|
5787
|
+
for (const t2 of found) {
|
|
5788
|
+
lines2.push(
|
|
5789
|
+
` ${color.cyan(t2.subagentId.padEnd(18))} ${color.dim(t2.runId.slice(0, 18))} ${color.dim(`${(t2.size / 1024).toFixed(1)} KB`)}`
|
|
5790
|
+
);
|
|
5791
|
+
}
|
|
5792
|
+
lines2.push("Use `/fleet log <subagentId>` for a summary, or append `raw` for the full JSONL.");
|
|
5793
|
+
return lines2.join("\n");
|
|
5794
|
+
}
|
|
5795
|
+
const matches = found.filter(
|
|
5796
|
+
(t2) => t2.subagentId === subagentId || t2.subagentId.startsWith(subagentId)
|
|
5797
|
+
);
|
|
5798
|
+
if (matches.length === 0) {
|
|
5799
|
+
return `No transcript matched "${subagentId}". Run \`/fleet log\` to list available ids.`;
|
|
5800
|
+
}
|
|
5801
|
+
if (matches.length > 1) {
|
|
5802
|
+
return [
|
|
5803
|
+
`Ambiguous id "${subagentId}" \u2014 ${matches.length} matches:`,
|
|
5804
|
+
...matches.map((m) => ` ${m.subagentId} (${m.runId})`)
|
|
5805
|
+
].join("\n");
|
|
5806
|
+
}
|
|
5807
|
+
const t = matches[0];
|
|
5808
|
+
const raw = await fs3.readFile(t.file, "utf8");
|
|
5809
|
+
if (mode === "raw") return raw;
|
|
5810
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
5811
|
+
const counts = {};
|
|
5812
|
+
let firstUser = null;
|
|
5813
|
+
let lastResponse = null;
|
|
5814
|
+
let totalIterations = 0;
|
|
5815
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
5816
|
+
for (const line of lines) {
|
|
5817
|
+
try {
|
|
5818
|
+
const ev = JSON.parse(line);
|
|
5819
|
+
counts[ev.type] = (counts[ev.type] ?? 0) + 1;
|
|
5820
|
+
if (ev.type === "user_input" && !firstUser) {
|
|
5821
|
+
const txt = typeof ev.content === "string" ? ev.content : Array.isArray(ev.content) ? ev.content.filter((b) => b.type === "text").map((b) => b.text).join(" ") : "";
|
|
5822
|
+
firstUser = txt.slice(0, 120);
|
|
5823
|
+
}
|
|
5824
|
+
if (ev.type === "llm_response") {
|
|
5825
|
+
if (Array.isArray(ev.content)) {
|
|
5826
|
+
const txt = ev.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ");
|
|
5827
|
+
if (txt) lastResponse = txt.slice(0, 240);
|
|
5828
|
+
}
|
|
5829
|
+
totalIterations += 1;
|
|
5830
|
+
}
|
|
5831
|
+
if (ev.type === "tool_use" && typeof ev.name === "string") {
|
|
5832
|
+
toolNames.set(ev.name, (toolNames.get(ev.name) ?? 0) + 1);
|
|
5833
|
+
}
|
|
5834
|
+
} catch {
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
const toolBreakdown = toolNames.size > 0 ? Array.from(toolNames.entries()).sort((a, b) => b[1] - a[1]).map(([n, c]) => `${n}\xD7${c}`).join(", ") : "(none)";
|
|
5838
|
+
const out = [
|
|
5839
|
+
color.bold(`Subagent ${t.subagentId}`) + color.dim(` (run ${t.runId})`),
|
|
5840
|
+
` ${lines.length} events \xB7 ${totalIterations} llm iterations \xB7 ${(t.size / 1024).toFixed(1)} KB`,
|
|
5841
|
+
` tools: ${toolBreakdown}`
|
|
5842
|
+
];
|
|
5843
|
+
if (firstUser) out.push("", color.dim(" task:"), ` ${firstUser}`);
|
|
5844
|
+
if (lastResponse) out.push("", color.dim(" last response:"), ` ${lastResponse}`);
|
|
5845
|
+
out.push("", color.dim(" event mix:"));
|
|
5846
|
+
for (const [type, count] of Object.entries(counts).sort((a, b) => b[1] - a[1])) {
|
|
5847
|
+
out.push(` ${type.padEnd(20)} ${count}`);
|
|
5848
|
+
}
|
|
5849
|
+
out.push("", color.dim("Use `/fleet log <id> raw` for the full JSONL."));
|
|
5850
|
+
return out.join("\n");
|
|
5851
|
+
},
|
|
5852
|
+
onFleetRetry: async (taskId) => {
|
|
5853
|
+
if (!multiAgentHost.isDirectorMode()) {
|
|
5854
|
+
const promoted = await multiAgentHost.promoteToDirector();
|
|
5855
|
+
if (!promoted) {
|
|
5856
|
+
return "Cannot retry: a coordinator already exists in non-director mode.";
|
|
5857
|
+
}
|
|
5858
|
+
for (const tool of promoted.tools(FLEET_ROSTER)) {
|
|
5859
|
+
toolRegistry.register(tool);
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
const dir = await multiAgentHost.ensureDirector();
|
|
5863
|
+
if (!dir) return "Director is not available.";
|
|
5864
|
+
const dirStatePath = path13.join(fleetRootForPromotion, "director-state.json");
|
|
5865
|
+
const prior = await loadDirectorState(dirStatePath);
|
|
5866
|
+
if (!prior) {
|
|
5867
|
+
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
5868
|
+
}
|
|
5869
|
+
const interrupted = prior.tasks.filter(
|
|
5870
|
+
(t) => t.status === "running" || t.status === "pending"
|
|
5871
|
+
);
|
|
5872
|
+
if (interrupted.length === 0) {
|
|
5873
|
+
return "No interrupted tasks: every prior task reached a terminal state.";
|
|
5874
|
+
}
|
|
5875
|
+
if (!taskId) {
|
|
5876
|
+
const lines = [
|
|
5877
|
+
`${interrupted.length} interrupted task${interrupted.length === 1 ? "" : "s"} from prior run:`
|
|
5878
|
+
];
|
|
5879
|
+
for (const t of interrupted) {
|
|
5880
|
+
const owner = t.subagentId ? prior.subagents.find((s) => s.id === t.subagentId) : void 0;
|
|
5881
|
+
const tag = owner ? `${owner.name ?? owner.id} (${owner.role ?? "no-role"})` : "no-owner";
|
|
5882
|
+
lines.push(
|
|
5883
|
+
` ${t.taskId.slice(0, 12)} ${t.status.padEnd(8)} ${tag} ${(t.description ?? "").slice(0, 60)}`
|
|
5884
|
+
);
|
|
5885
|
+
}
|
|
5886
|
+
lines.push("Run `/fleet retry <taskId>` or `/fleet retry all` to re-assign.");
|
|
5887
|
+
return lines.join("\n");
|
|
5888
|
+
}
|
|
5889
|
+
const targets = taskId === "all" ? interrupted : interrupted.filter(
|
|
5890
|
+
(t) => t.taskId === taskId || t.taskId.startsWith(taskId)
|
|
5891
|
+
);
|
|
5892
|
+
if (targets.length === 0) {
|
|
5893
|
+
return `No interrupted task matched "${taskId}".`;
|
|
5894
|
+
}
|
|
5895
|
+
const results = [];
|
|
5896
|
+
for (const t of targets) {
|
|
5897
|
+
const owner = t.subagentId ? prior.subagents.find((s) => s.id === t.subagentId) : void 0;
|
|
5898
|
+
if (!owner) {
|
|
5899
|
+
results.push(` - ${t.taskId.slice(0, 12)}: no owner record, skipped.`);
|
|
5900
|
+
continue;
|
|
5901
|
+
}
|
|
5902
|
+
const rosterCfg = owner.role ? FLEET_ROSTER[owner.role] : void 0;
|
|
5903
|
+
const cfg = rosterCfg ? { ...rosterCfg } : {
|
|
5904
|
+
name: owner.name ?? owner.id,
|
|
5905
|
+
role: owner.role,
|
|
5906
|
+
provider: owner.provider,
|
|
5907
|
+
model: owner.model
|
|
5908
|
+
};
|
|
5909
|
+
try {
|
|
5910
|
+
const newSubId = await dir.spawn(cfg);
|
|
5911
|
+
const newTaskId = await dir.assign({
|
|
5912
|
+
id: "",
|
|
5913
|
+
description: t.description ?? "(no description)",
|
|
5914
|
+
subagentId: newSubId
|
|
5915
|
+
});
|
|
5916
|
+
results.push(
|
|
5917
|
+
` ${color.green("\u2713")} ${t.taskId.slice(0, 12)} \u2192 re-spawned ${newSubId.slice(0, 12)} (task ${newTaskId.slice(0, 12)})`
|
|
5918
|
+
);
|
|
5919
|
+
} catch (err) {
|
|
5920
|
+
results.push(
|
|
5921
|
+
` ${color.red("\u2717")} ${t.taskId.slice(0, 12)} \u2192 ${err instanceof Error ? err.message : String(err)}`
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
return [`Retried ${targets.length} task${targets.length === 1 ? "" : "s"}:`, ...results].join(
|
|
5926
|
+
"\n"
|
|
5927
|
+
);
|
|
5928
|
+
},
|
|
5405
5929
|
onDirector: async () => {
|
|
5406
|
-
const
|
|
5407
|
-
if (!
|
|
5408
|
-
for (const tool of
|
|
5930
|
+
const director2 = await multiAgentHost.promoteToDirector();
|
|
5931
|
+
if (!director2) return null;
|
|
5932
|
+
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
5409
5933
|
toolRegistry.register(tool);
|
|
5410
5934
|
}
|
|
5411
5935
|
const mp = path13.join(fleetRootForPromotion, "fleet.json");
|
|
@@ -5477,7 +6001,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5477
6001
|
savedProviderCfg,
|
|
5478
6002
|
resolvedProvider: resolvedProvider ?? void 0,
|
|
5479
6003
|
getPickableProviders: () => buildPickableProviders(modelsRegistry, config),
|
|
5480
|
-
switchProviderAndModel
|
|
6004
|
+
switchProviderAndModel,
|
|
6005
|
+
director: director ?? null,
|
|
6006
|
+
fleetRoster: FLEET_ROSTER,
|
|
6007
|
+
fleetStreamController
|
|
5481
6008
|
});
|
|
5482
6009
|
}
|
|
5483
6010
|
async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|