pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -38,6 +38,7 @@
38
38
  * polling.
39
39
  */
40
40
 
41
+ import * as fs from "node:fs";
41
42
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
42
43
  import {
43
44
  listBackgroundWorkWakeChannels,
@@ -48,6 +49,7 @@ import {
48
49
  import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
49
50
  import {
50
51
  ASYNC_DIR,
52
+ INTERCOM_DETACH_REQUEST_EVENT,
51
53
  RESULTS_DIR,
52
54
  SUBAGENT_ASYNC_COMPLETE_EVENT,
53
55
  SUBAGENT_FOREGROUND_COMPLETE_EVENT,
@@ -89,6 +91,8 @@ export interface WaitEventBus {
89
91
 
90
92
  export interface SubagentWaitDeps {
91
93
  state: SubagentState;
94
+ /** Stream live wait status into Pi's pending tool row. */
95
+ onUpdate?: (result: AgentToolResult<Details>) => void;
92
96
  asyncDirRoot?: string;
93
97
  resultsDir?: string;
94
98
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
@@ -118,6 +122,7 @@ export interface SubagentWaitDeps {
118
122
 
119
123
  /** Bus channels that indicate a run changed state or needs attention. */
120
124
  const WAKE_CHANNELS = [
125
+ INTERCOM_DETACH_REQUEST_EVENT,
121
126
  SUBAGENT_ASYNC_COMPLETE_EVENT,
122
127
  SUBAGENT_FOREGROUND_COMPLETE_EVENT,
123
128
  SUBAGENT_CONTROL_EVENT,
@@ -214,9 +219,26 @@ function summarizeForegroundChildren(run: ForegroundResumeRun, indices: Set<numb
214
219
  return [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ");
215
220
  }
216
221
 
222
+ function foregroundChildrenNeedingAttention(run: ForegroundResumeRun, indices: Set<number>) {
223
+ return run.children.filter((child) => indices.has(child.index) && child.status === "detached" && child.activityState === "needs_attention");
224
+ }
225
+
226
+ function formatForegroundAttention(run: ForegroundResumeRun, children: ReturnType<typeof foregroundChildrenNeedingAttention>, elapsedMs: number): AgentToolResult<Details> {
227
+ const childList = children.map((child) => `${child.agent}${child.index !== undefined ? `#${child.index}` : ""}`).join(", ");
228
+ return result(
229
+ `Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call subagent_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
230
+ );
231
+ }
232
+
217
233
  /** A running run that has flagged it needs the parent's attention. */
218
234
  function needsAttention(run: AsyncRunSummary): boolean {
219
- return run.activityState === "needs_attention";
235
+ return run.activityState === "needs_attention" || run.steps.some((step) => step.activityState === "needs_attention");
236
+ }
237
+
238
+ function hasSupervisorTool(run: AsyncRunSummary): boolean {
239
+ return run.currentTool === "contact_supervisor"
240
+ || run.currentTool === "intercom"
241
+ || run.steps.some((step) => step.currentTool === "contact_supervisor" || step.currentTool === "intercom");
220
242
  }
221
243
 
222
244
  function backgroundWorkIdentity(item: RegisteredBackgroundWorkItem): string {
@@ -239,6 +261,7 @@ function activeRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps
239
261
  resultsDir,
240
262
  kill: deps.kill,
241
263
  now: deps.now,
264
+ ...(params.id ? { runId: params.id } : {}),
242
265
  });
243
266
  return params.id ? runs.filter((run) => matchesId(run, params.id!)) : runs;
244
267
  }
@@ -257,6 +280,7 @@ function allRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps):
257
280
  resultsDir,
258
281
  kill: deps.kill,
259
282
  now: deps.now,
283
+ ...(params.id ? { runId: params.id } : {}),
260
284
  });
261
285
  return params.id ? runs.filter((run) => matchesId(run, params.id!)) : runs;
262
286
  }
@@ -283,6 +307,101 @@ function result(text: string, isError = false): AgentToolResult<Details> {
283
307
  };
284
308
  }
285
309
 
310
+ const TRANSCRIPT_TAIL_BYTES = 128 * 1024;
311
+ const TRANSCRIPT_PREVIEW_LINES = 3;
312
+ const TRANSCRIPT_PREVIEW_WIDTH = 220;
313
+
314
+ interface TranscriptActivity {
315
+ currentTool?: string;
316
+ currentToolArgs?: string;
317
+ latestAt?: number;
318
+ recent: string[];
319
+ }
320
+
321
+ function compactTranscriptText(value: unknown, maxWidth = TRANSCRIPT_PREVIEW_WIDTH): string | undefined {
322
+ if (typeof value !== "string") return undefined;
323
+ const compact = value.replace(/\s+/g, " ").trim();
324
+ if (!compact) return undefined;
325
+ return compact.length <= maxWidth ? compact : `${compact.slice(0, Math.max(1, maxWidth - 1))}…`;
326
+ }
327
+
328
+ function readTranscriptActivity(transcriptPath: string | undefined): TranscriptActivity | undefined {
329
+ if (!transcriptPath) return undefined;
330
+ let fd: number | undefined;
331
+ try {
332
+ const stat = fs.statSync(transcriptPath);
333
+ if (!stat.isFile() || stat.size <= 0) return undefined;
334
+ const start = Math.max(0, stat.size - TRANSCRIPT_TAIL_BYTES);
335
+ const length = stat.size - start;
336
+ const buffer = Buffer.allocUnsafe(length);
337
+ fd = fs.openSync(transcriptPath, "r");
338
+ const bytesRead = fs.readSync(fd, buffer, 0, length, start);
339
+ let lines = buffer.subarray(0, bytesRead).toString("utf-8").split(/\r?\n/);
340
+ if (start > 0) lines = lines.slice(1); // The first tail line may be partial JSON.
341
+
342
+ let currentTool: string | undefined;
343
+ let currentToolArgs: string | undefined;
344
+ let latestAt: number | undefined;
345
+ const recent: string[] = [];
346
+ for (const line of lines) {
347
+ if (!line.trim()) continue;
348
+ let record: Record<string, unknown>;
349
+ try {
350
+ record = JSON.parse(line) as Record<string, unknown>;
351
+ } catch {
352
+ continue; // Concurrent append can leave the final line temporarily incomplete.
353
+ }
354
+ if (typeof record.ts === "number" && Number.isFinite(record.ts)) latestAt = record.ts;
355
+ if (record.recordType === "tool_start" && typeof record.toolName === "string") {
356
+ currentTool = record.toolName;
357
+ currentToolArgs = compactTranscriptText(record.argsPreview, 140);
358
+ const preview = currentToolArgs ? `${currentTool}: ${currentToolArgs}` : currentTool;
359
+ recent.push(`tool: ${preview}`);
360
+ continue;
361
+ }
362
+ if (record.recordType === "tool_end") {
363
+ currentTool = undefined;
364
+ currentToolArgs = undefined;
365
+ continue;
366
+ }
367
+ if (record.recordType === "message") {
368
+ if (record.sourceEventType === "initial_prompt" || record.role === "user") continue;
369
+ const text = compactTranscriptText(record.text);
370
+ if (text) recent.push(`${typeof record.role === "string" ? record.role : "message"}: ${text}`);
371
+ continue;
372
+ }
373
+ if (record.recordType === "stdout" || record.recordType === "stderr") {
374
+ const text = compactTranscriptText(record.text);
375
+ if (text) recent.push(`${record.recordType}: ${text}`);
376
+ }
377
+ }
378
+ return { currentTool, currentToolArgs, latestAt, recent: recent.slice(-TRANSCRIPT_PREVIEW_LINES) };
379
+ } catch {
380
+ return undefined;
381
+ } finally {
382
+ if (fd !== undefined) {
383
+ try { fs.closeSync(fd); } catch { /* best-effort close */ }
384
+ }
385
+ }
386
+ }
387
+
388
+ function detachedForegroundWaitUpdate(run: ForegroundResumeRun, pendingIndices: Set<number>, nowMs: number, elapsedMs: number): AgentToolResult<Details> {
389
+ const lines = [`Waiting for detached foreground run "${run.runId}" · ${formatDuration(elapsedMs)}`];
390
+ for (const child of run.children) {
391
+ if (!pendingIndices.has(child.index) || child.status !== "detached") continue;
392
+ const activity = readTranscriptActivity(child.transcriptPath);
393
+ const age = activity?.latestAt !== undefined ? ` · activity ${formatDuration(Math.max(0, nowMs - activity.latestAt))} ago` : "";
394
+ lines.push(`${child.agent} · working after supervisor handoff${age}`);
395
+ if (activity?.currentTool) {
396
+ lines.push(` current: ${activity.currentTool}${activity.currentToolArgs ? `: ${activity.currentToolArgs}` : ""}`);
397
+ }
398
+ for (const preview of activity?.recent ?? []) lines.push(` ${preview}`);
399
+ if (!activity && child.transcriptPath) lines.push(" live transcript has no new activity yet");
400
+ if (!child.transcriptPath) lines.push(" live transcript unavailable; waiting for completion event");
401
+ }
402
+ return result(lines.join("\n"));
403
+ }
404
+
286
405
  async function waitForDetachedForegroundRun(
287
406
  run: ForegroundResumeRun,
288
407
  signal: AbortSignal | undefined,
@@ -295,21 +414,25 @@ async function waitForDetachedForegroundRun(
295
414
  const initialDetachedIndices = new Set(run.children.filter((child) => child.status === "detached").map((child) => child.index));
296
415
  while (true) {
297
416
  if (deps.state.currentSessionId !== run.sessionId) {
298
- return result(`Wait stopped because the active session changed while remembered foreground run "${run.runId}" was still detached. Return to the originating session to inspect or wait for it.`, true);
417
+ return result(`Wait stopped because the active session changed while remembered foreground run "${run.runId}" was still detached. Return to the originating session to inspect or wait for it. Reply to any pending supervisor request before resuming or launching a replacement.`, true);
299
418
  }
300
419
  const current = deps.state.foregroundRuns?.get(run.runId);
301
420
  if (!current || current.sessionId !== run.sessionId) {
302
421
  return result(`Remembered foreground run "${run.runId}" disappeared before a terminal child result was recorded. Completion cannot be confirmed; do not launch a replacement without checking the originating child session.`, true);
303
422
  }
304
423
  const pending = current.children.filter((child) => initialDetachedIndices.has(child.index) && child.status === "detached");
424
+ const attention = deps.stopOnAttention === false ? [] : foregroundChildrenNeedingAttention(current, initialDetachedIndices);
425
+ if (attention.length > 0) return formatForegroundAttention(current, attention, now() - startedAt);
305
426
  if (pending.length === 0) {
306
427
  const outcome = summarizeForegroundChildren(current, initialDetachedIndices);
307
428
  return result(
308
429
  `Waited ${formatDuration(now() - startedAt)} for remembered detached foreground run "${run.runId}"; done. Outcome: ${outcome || "no recovered child status"}. Completion event observed; inspect with subagent({ action: "status", id: "${run.runId}" }) for recovered output.`,
309
430
  );
310
431
  }
432
+ const updateNow = now();
433
+ deps.onUpdate?.(detachedForegroundWaitUpdate(current, initialDetachedIndices, updateNow, updateNow - startedAt));
311
434
  if (signal?.aborted) {
312
- return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Remembered foreground run "${run.runId}" remains detached.`, true);
435
+ return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Remembered foreground run "${run.runId}" remains detached. Reply to any pending supervisor request before resuming or launching a replacement.`, true);
313
436
  }
314
437
  if (now() - startedAt >= timeoutMs) {
315
438
  return result(
@@ -446,8 +569,11 @@ export async function waitForSubagents(
446
569
  }
447
570
 
448
571
  const relevantAttention = attention.filter((run) => initialAsyncIds.has(run.id));
572
+ const supervisorAttentionHint = relevantAttention.some(hasSupervisorTool)
573
+ ? " Reply to any pending supervisor request. If subagent_supervisor({ action: \"pending\" }) is empty, check intercom({ action: \"pending\" }) because an external intercom tool may own the request."
574
+ : "";
449
575
  const attentionNote = relevantAttention.length > 0
450
- ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} — inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.`
576
+ ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.`
451
577
  : "";
452
578
  const stillRunning = active.filter((run) => initialAsyncIds.has(run.id)).length
453
579
  + providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
@@ -18,8 +18,8 @@ In an interactive chat, do not call this merely to wait: return control to the u
18
18
 
19
19
  Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
20
20
  parameters: SubagentWaitParams,
21
- execute(_id, params, signal) {
22
- return waitForSubagents(params, signal, { state, events: pi.events, enabled });
21
+ execute(_id, params, signal, onUpdate) {
22
+ return waitForSubagents(params, signal, { state, events: pi.events, enabled, onUpdate });
23
23
  },
24
24
  };
25
25
  pi.registerTool(tool);