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.
- package/CHANGELOG.md +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- 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(", ")}
|
|
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);
|