pi-subagents 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { Key, matchesKey } from "@earendil-works/pi-tui";
5
+ import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
6
6
  import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
7
  import {
8
8
  DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
@@ -16,11 +16,16 @@ import {
16
16
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
17
17
  import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
18
18
  import { findModelInfo, toModelInfo } from "../shared/model-info.ts";
19
- import { formatTokens } from "../shared/formatters.ts";
19
+ import { formatTokens, shortenPath } from "../shared/formatters.ts";
20
+ import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from "../runs/background/async-status.ts";
21
+ import { scheduledRunStorePath } from "../runs/background/scheduled-runs.ts";
22
+ import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
20
23
  import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
21
24
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
22
25
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
23
26
  import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
27
+ import { openSubagentsAdmin } from "./subagents-admin.ts";
28
+ import { openSubagentFleet } from "../tui/fleet.ts";
24
29
  import {
25
30
  applySlashUpdate,
26
31
  buildSlashInitialResult,
@@ -36,6 +41,7 @@ import {
36
41
  SLASH_SUBAGENT_RESPONSE_EVENT,
37
42
  SLASH_SUBAGENT_STARTED_EVENT,
38
43
  SLASH_SUBAGENT_UPDATE_EVENT,
44
+ ASYNC_DIR,
39
45
  type Details,
40
46
  type JsonSchemaObject,
41
47
  type SingleResult,
@@ -221,6 +227,160 @@ async function withSlashStatus<T>(
221
227
  }
222
228
  }
223
229
 
230
+ type Theme = ExtensionContext["ui"]["theme"];
231
+
232
+ type StopSelectorTarget = {
233
+ kind: "async" | "scheduled";
234
+ id: string;
235
+ label: string;
236
+ detail: string;
237
+ actionLabel: string;
238
+ };
239
+
240
+ type StopSelectorResult = { confirmed: boolean; target?: StopSelectorTarget };
241
+
242
+ function commandForTarget(target: StopSelectorTarget): string {
243
+ return target.kind === "scheduled"
244
+ ? `subagent({ action: "schedule-cancel", id: ${JSON.stringify(target.id)} })`
245
+ : `subagent({ action: "stop", id: ${JSON.stringify(target.id)} })`;
246
+ }
247
+
248
+ function formatAsyncStopTarget(run: AsyncRunSummary): StopSelectorTarget {
249
+ const progress = formatAsyncRunProgressLabel(run);
250
+ const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
251
+ return {
252
+ kind: "async",
253
+ id: run.id,
254
+ label: `${run.id} · ${run.mode} · ${progress}`,
255
+ detail: `${run.state} · ${cwd}`,
256
+ actionLabel: "stop async run",
257
+ };
258
+ }
259
+
260
+ function scheduledStopTargets(ctx: ExtensionContext, state: SubagentState): StopSelectorTarget[] {
261
+ const sessionId = state.currentSessionId ?? ctx.sessionManager.getSessionId();
262
+ if (!sessionId) return [];
263
+ const storePath = scheduledRunStorePath(ctx.cwd, sessionId);
264
+ if (!fs.existsSync(storePath)) return [];
265
+ let parsed: unknown;
266
+ try {
267
+ parsed = JSON.parse(fs.readFileSync(storePath, "utf-8"));
268
+ } catch {
269
+ return [];
270
+ }
271
+ const jobs = parsed && typeof parsed === "object" && Array.isArray((parsed as { jobs?: unknown }).jobs)
272
+ ? (parsed as { jobs: Array<Record<string, unknown>> }).jobs
273
+ : [];
274
+ return jobs
275
+ .filter((job) => job.state === "scheduled" && typeof job.id === "string" && typeof job.name === "string" && typeof job.runAt === "number")
276
+ .sort((left, right) => Number(left.runAt) - Number(right.runAt))
277
+ .map((job) => ({
278
+ kind: "scheduled" as const,
279
+ id: String(job.id),
280
+ label: `${String(job.id)} · ${String(job.name)}`,
281
+ detail: `scheduled · ${new Date(Number(job.runAt)).toISOString()}`,
282
+ actionLabel: "cancel scheduled run",
283
+ }));
284
+ }
285
+
286
+ function discoverStopTargets(ctx: ExtensionContext, state: SubagentState): StopSelectorTarget[] {
287
+ const sessionId = state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined;
288
+ const asyncTargets = listAsyncRuns(ASYNC_DIR, {
289
+ states: ["queued", "running"],
290
+ ...(sessionId ? { sessionId } : {}),
291
+ }).map(formatAsyncStopTarget);
292
+ return [...asyncTargets, ...scheduledStopTargets(ctx, state)];
293
+ }
294
+
295
+ function stopFallbackText(targets: StopSelectorTarget[]): string {
296
+ if (targets.length === 0) return "No active current-session async runs or scheduled subagent runs to stop.";
297
+ const lines = ["Subagent stop targets:", ""];
298
+ for (const target of targets) {
299
+ lines.push(`- ${target.label}`);
300
+ lines.push(` ${target.detail}`);
301
+ lines.push(` ${target.actionLabel}: ${commandForTarget(target)}`);
302
+ if (target.kind === "async") lines.push(` slash: /subagents-stop ${target.id}`);
303
+ }
304
+ return lines.join("\n");
305
+ }
306
+
307
+ class SubagentsStopSelector implements Component {
308
+ readonly width = 84;
309
+ private selected = 0;
310
+ private confirming = false;
311
+ private readonly tui: TUI;
312
+ private readonly theme: Theme;
313
+ private readonly targets: StopSelectorTarget[];
314
+ private readonly done: (result: StopSelectorResult) => void;
315
+
316
+ constructor(tui: TUI, theme: Theme, targets: StopSelectorTarget[], done: (result: StopSelectorResult) => void) {
317
+ this.tui = tui;
318
+ this.theme = theme;
319
+ this.targets = targets;
320
+ this.done = done;
321
+ }
322
+
323
+ handleInput(data: string): void {
324
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
325
+ this.done({ confirmed: false });
326
+ return;
327
+ }
328
+ if (this.confirming) {
329
+ if (matchesKey(data, "return") || data.toLowerCase() === "y") {
330
+ this.done({ confirmed: true, target: this.targets[this.selected] });
331
+ return;
332
+ }
333
+ if (data.toLowerCase() === "n" || matchesKey(data, "backspace")) {
334
+ this.confirming = false;
335
+ this.tui.requestRender();
336
+ }
337
+ return;
338
+ }
339
+ if (matchesKey(data, "up")) {
340
+ this.selected = Math.max(0, this.selected - 1);
341
+ this.tui.requestRender();
342
+ return;
343
+ }
344
+ if (matchesKey(data, "down")) {
345
+ this.selected = Math.min(this.targets.length - 1, this.selected + 1);
346
+ this.tui.requestRender();
347
+ return;
348
+ }
349
+ if (matchesKey(data, "return")) {
350
+ this.confirming = true;
351
+ this.tui.requestRender();
352
+ }
353
+ }
354
+
355
+ render(width: number): string[] {
356
+ const contentWidth = Math.max(40, Math.min(this.width, width || this.width));
357
+ const lines = [this.theme.bold("Stop subagent run"), this.theme.fg("dim", "Select a current-session async run to stop, or a scheduled run to cancel."), ""];
358
+ const maxRows = 10;
359
+ const start = Math.max(0, Math.min(this.selected - maxRows + 1, Math.max(0, this.targets.length - maxRows)));
360
+ for (let index = start; index < Math.min(this.targets.length, start + maxRows); index++) {
361
+ const target = this.targets[index]!;
362
+ const selected = index === this.selected;
363
+ const marker = selected ? "›" : " ";
364
+ const actionLabel = target.actionLabel;
365
+ const action = target.kind === "scheduled" ? this.theme.fg("warning", actionLabel) : this.theme.fg("accent", actionLabel);
366
+ const labelWidth = Math.max(0, contentWidth - marker.length - actionLabel.length - 2);
367
+ lines.push(`${marker} ${action} ${target.label.slice(0, labelWidth)}`);
368
+ if (selected) lines.push(this.theme.fg("dim", ` ${target.detail}`.slice(0, contentWidth)));
369
+ }
370
+ if (this.targets.length > maxRows) lines.push(this.theme.fg("dim", `Showing ${start + 1}-${Math.min(this.targets.length, start + maxRows)} of ${this.targets.length}`));
371
+ lines.push("");
372
+ if (this.confirming) {
373
+ const target = this.targets[this.selected]!;
374
+ lines.push(this.theme.fg("warning", `Confirm: ${target.actionLabel} ${target.id}?`));
375
+ if (target.kind === "async") lines.push(this.theme.fg("dim", "Stop ends this run; use interrupt for a resumable pause."));
376
+ lines.push(this.theme.fg("dim", "Enter/Y confirms · N returns · Esc cancels"));
377
+ } else {
378
+ lines.push(this.theme.fg("dim", "↑/↓ select · Enter confirm · Esc cancel"));
379
+ }
380
+ return lines;
381
+ }
382
+ }
383
+
224
384
  function emptyUsage(): Usage {
225
385
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
226
386
  }
@@ -841,7 +1001,7 @@ function validateInlineAcceptanceInput(value: string, agent: string): void {
841
1001
  const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
842
1002
  if (errors.length > 0) throw new SlashParseError(errors[0]!);
843
1003
  if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
844
- throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or reviewed acceptance contracts.`);
1004
+ throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none or verified acceptance contracts; reviewed is inferred-only.`);
845
1005
  }
846
1006
  }
847
1007
 
@@ -977,6 +1137,32 @@ export function registerSlashCommands(
977
1137
  pi: ExtensionAPI,
978
1138
  state: SubagentState,
979
1139
  ): void {
1140
+ let fleetOpen = false;
1141
+ const showFleet = async (ctx: ExtensionContext) => {
1142
+ state.lastUiContext = ctx;
1143
+ if (!ctx.hasUI) {
1144
+ await runSlashSubagent(pi, ctx, { action: "status", view: "fleet" });
1145
+ return;
1146
+ }
1147
+ if (fleetOpen) {
1148
+ ctx.ui.notify("Subagent fleet inspector is already open.", "info");
1149
+ return;
1150
+ }
1151
+ fleetOpen = true;
1152
+ try {
1153
+ await openSubagentFleet(ctx, state);
1154
+ } finally {
1155
+ fleetOpen = false;
1156
+ }
1157
+ };
1158
+
1159
+ pi.registerCommand("subagents", {
1160
+ description: "Administer subagents: inspect metadata and update models, thinking, or prompts",
1161
+ handler: async (args, ctx) => {
1162
+ await openSubagentsAdmin(pi, ctx, args);
1163
+ },
1164
+ });
1165
+
980
1166
  pi.registerCommand("run", {
981
1167
  description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
982
1168
  getArgumentCompletions: makeAgentCompletions(state, false),
@@ -1090,9 +1276,56 @@ export function registerSlashCommands(
1090
1276
  });
1091
1277
 
1092
1278
  pi.registerCommand("subagents-fleet", {
1093
- description: "Show active subagent fleet status and transcript commands",
1094
- handler: async (_args, ctx) => {
1095
- await runSlashSubagent(pi, ctx, { action: "status", view: "fleet" });
1279
+ description: "Open the live, inspection-only subagent fleet",
1280
+ handler: async (_args, ctx) => showFleet(ctx),
1281
+ });
1282
+
1283
+ pi.registerShortcut(Key.ctrlAlt("f"), {
1284
+ description: "Open subagent fleet inspector",
1285
+ handler: async (ctx) => showFleet(ctx),
1286
+ });
1287
+
1288
+ pi.registerCommand("subagents-stop", {
1289
+ description: "Stop a current-session async subagent run",
1290
+ handler: async (args, ctx) => {
1291
+ const id = args.trim();
1292
+ if (id) {
1293
+ await runSlashSubagent(pi, ctx, { action: "stop", id });
1294
+ return;
1295
+ }
1296
+
1297
+ if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") {
1298
+ sendSlashText(pi, "Selector unavailable in child-safe fanout mode. Pass an explicit current-session top-level async run id, for example `/subagents-stop <run-id>` or `subagent({ action: \"stop\", id: \"<run-id>\" })`.");
1299
+ return;
1300
+ }
1301
+
1302
+ let targets: StopSelectorTarget[];
1303
+ try {
1304
+ targets = discoverStopTargets(ctx, state);
1305
+ } catch (error) {
1306
+ const message = error instanceof Error ? error.message : String(error);
1307
+ ctx.ui.notify(message, "error");
1308
+ return;
1309
+ }
1310
+ if (!ctx.hasUI) {
1311
+ sendSlashText(pi, stopFallbackText(targets));
1312
+ return;
1313
+ }
1314
+ if (targets.length === 0) {
1315
+ ctx.ui.notify("No active current-session async runs or scheduled subagent runs to stop.", "info");
1316
+ return;
1317
+ }
1318
+
1319
+ const result = await ctx.ui.custom<StopSelectorResult>(
1320
+ (tui, theme, _kb, done) => new SubagentsStopSelector(tui, theme, targets, done),
1321
+ { overlay: true, overlayOptions: { anchor: "center", width: 88, maxHeight: "80%" } },
1322
+ );
1323
+ if (!result?.confirmed || !result.target) return;
1324
+ if (result.target.kind === "scheduled") {
1325
+ await runSlashSubagent(pi, ctx, { action: "schedule-cancel", id: result.target.id });
1326
+ return;
1327
+ }
1328
+ await runSlashSubagent(pi, ctx, { action: "stop", id: result.target.id });
1096
1329
  },
1097
1330
  });
1098
1331