pi-subagents 0.57.0 → 0.59.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 +104 -0
- package/docs/agents.md +18 -9
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +40 -1
- package/docs/models.md +27 -4
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +92 -6
- package/docs/workflows.md +115 -0
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +1 -1
- package/skills/pi-subagents/SKILL.md +3 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/src/agents/agent-management.ts +28 -17
- package/src/agents/agent-serializer.ts +9 -2
- package/src/agents/agents.ts +103 -27
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +19 -18
- package/src/api/agents.ts +10 -5
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +32 -10
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +66 -27
- package/src/extension/public-execution.ts +15 -2
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +37 -12
- package/src/extension/tool-description.ts +25 -6
- package/src/integrations/herdr-status.ts +5 -0
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +78 -46
- package/src/runs/background/async-job-tracker.ts +14 -12
- package/src/runs/background/async-resume.ts +29 -27
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +68 -7
- package/src/runs/background/chain-append.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +184 -9
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +29 -10
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +18 -8
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +389 -350
- package/src/runs/background/subagent-wait.ts +58 -10
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +22 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +134 -121
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +527 -257
- package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +463 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +5 -4
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +422 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +25 -6
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +15 -1
- package/src/runs/shared/pi-args.ts +81 -8
- package/src/runs/shared/single-output.ts +44 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +11 -2
- package/src/shared/launch-contract.ts +5 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +240 -30
- package/src/shared/utils.ts +41 -84
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +125 -74
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +330 -33
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +230 -0
- package/src/workflows/scripted-workflow.ts +496 -35
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +79 -5
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -22,6 +22,10 @@ import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from
|
|
|
22
22
|
import { encodeInspectReply, handleInspectRpcArgs, INSPECT_WIDGET_KEY } from "../runs/background/inspect-rpc.ts";
|
|
23
23
|
import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
|
|
24
24
|
import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
25
|
+
import { resolveAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
26
|
+
import { readStatus } from "../shared/utils.ts";
|
|
27
|
+
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
28
|
+
import { readWorkflowReceipt } from "../workflows/workflow-receipt.ts";
|
|
25
29
|
import { FLEET_OPEN_SHORTCUT } from "../shared/shortcuts.ts";
|
|
26
30
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
27
31
|
import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
|
|
@@ -46,7 +50,6 @@ import {
|
|
|
46
50
|
DIRS,
|
|
47
51
|
type Details,
|
|
48
52
|
type FleetKeybindingsConfig,
|
|
49
|
-
type JsonSchemaObject,
|
|
50
53
|
type SingleResult,
|
|
51
54
|
type SubagentState,
|
|
52
55
|
type Usage,
|
|
@@ -346,25 +349,60 @@ function usageHasValue(usage: Usage): boolean {
|
|
|
346
349
|
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0;
|
|
347
350
|
}
|
|
348
351
|
|
|
352
|
+
function nonNegativeNumber(value: unknown): number | undefined {
|
|
353
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function usageFromValue(value: unknown, turnsOverride?: number): Usage | undefined {
|
|
357
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
358
|
+
const record = value as Record<string, unknown>;
|
|
359
|
+
const costValue = typeof record.cost === "object" && record.cost !== null && !Array.isArray(record.cost)
|
|
360
|
+
? (record.cost as Record<string, unknown>).total
|
|
361
|
+
: record.cost;
|
|
362
|
+
const turnsValue = turnsOverride ?? record.turns;
|
|
363
|
+
const usage = {
|
|
364
|
+
input: nonNegativeNumber(record.input) ?? 0,
|
|
365
|
+
output: nonNegativeNumber(record.output) ?? 0,
|
|
366
|
+
cacheRead: nonNegativeNumber(record.cacheRead) ?? 0,
|
|
367
|
+
cacheWrite: nonNegativeNumber(record.cacheWrite) ?? 0,
|
|
368
|
+
cost: nonNegativeNumber(costValue) ?? 0,
|
|
369
|
+
turns: nonNegativeNumber(turnsValue) ?? 0,
|
|
370
|
+
};
|
|
371
|
+
return Number.isSafeInteger(usage.turns) ? usage : undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
349
374
|
function assistantUsageFromMessage(message: unknown): Usage | undefined {
|
|
350
375
|
if (!message || typeof message !== "object") return undefined;
|
|
351
376
|
const msg = message as { role?: unknown; usage?: unknown };
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
377
|
+
return msg.role === "assistant" ? usageFromValue(msg.usage, 1) : undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function compactionUsageFromEntry(entry: unknown): Usage | undefined {
|
|
381
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
382
|
+
const record = entry as { type?: unknown; usage?: unknown };
|
|
383
|
+
return record.type === "compaction" ? usageFromValue(record.usage, 0) : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const MAX_USAGE_METADATA_BYTES = 2 * 1024 * 1024;
|
|
387
|
+
|
|
388
|
+
function readUsageMetadata(metadataPath: string): Record<string, unknown> | undefined {
|
|
389
|
+
let fd: number | undefined;
|
|
390
|
+
try {
|
|
391
|
+
fd = fs.openSync(metadataPath, "r");
|
|
392
|
+
const stat = fs.fstatSync(fd);
|
|
393
|
+
if (!stat.isFile() || stat.size > MAX_USAGE_METADATA_BYTES) return undefined;
|
|
394
|
+
const buffer = Buffer.allocUnsafe(stat.size);
|
|
395
|
+
let offset = 0;
|
|
396
|
+
while (offset < buffer.length) {
|
|
397
|
+
const bytesRead = fs.readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
398
|
+
if (bytesRead <= 0) return undefined;
|
|
399
|
+
offset += bytesRead;
|
|
400
|
+
}
|
|
401
|
+
const value: unknown = JSON.parse(buffer.toString("utf-8"));
|
|
402
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
403
|
+
} finally {
|
|
404
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
405
|
+
}
|
|
368
406
|
}
|
|
369
407
|
|
|
370
408
|
function isSubagentDetails(value: unknown): value is Details {
|
|
@@ -382,10 +420,32 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
|
382
420
|
}
|
|
383
421
|
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
384
422
|
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
385
|
-
if (message.role !== "toolResult" || message.toolName !== "subagent") return undefined;
|
|
423
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "subagent_wait")) return undefined;
|
|
386
424
|
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
387
425
|
}
|
|
388
426
|
|
|
427
|
+
function metadataUsage(
|
|
428
|
+
artifactsDirs: string[],
|
|
429
|
+
input: { runId: string; agent: string },
|
|
430
|
+
): Usage | undefined {
|
|
431
|
+
if (!/^[A-Za-z0-9._-]+$/.test(input.runId)) return undefined;
|
|
432
|
+
for (const artifactsDir of artifactsDirs) {
|
|
433
|
+
for (const index of [0, undefined] as const) {
|
|
434
|
+
const metadataPath = getArtifactPaths(artifactsDir, input.runId, input.agent, index).metadataPath;
|
|
435
|
+
try {
|
|
436
|
+
const metadata = readUsageMetadata(metadataPath);
|
|
437
|
+
if (!metadata) continue;
|
|
438
|
+
if (metadata.runId !== input.runId || metadata.agent !== input.agent) continue;
|
|
439
|
+
const usage = usageFromValue(metadata.usage);
|
|
440
|
+
if (usage && usageHasValue(usage)) return usage;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to read subagent usage metadata '${metadataPath}':`, error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
389
449
|
function formatCostUsage(label: string, usage: Usage): string {
|
|
390
450
|
const extras = [
|
|
391
451
|
usage.cacheRead ? `cache read ${formatTokens(usage.cacheRead)}` : "",
|
|
@@ -395,28 +455,94 @@ function formatCostUsage(label: string, usage: Usage): string {
|
|
|
395
455
|
return `${label}: ↑${formatTokens(usage.input)} ↓${formatTokens(usage.output)} $${usage.cost.toFixed(4)}${extras.length ? ` (${extras.join(", ")})` : ""}`;
|
|
396
456
|
}
|
|
397
457
|
|
|
398
|
-
function buildSubagentCostReport(ctx: ExtensionContext): string {
|
|
458
|
+
function buildSubagentCostReport(ctx: ExtensionContext, state: SubagentState): string {
|
|
399
459
|
const parent = emptyUsage();
|
|
400
460
|
const childTotal = emptyUsage();
|
|
401
461
|
const total = emptyUsage();
|
|
402
462
|
const children: Array<{ label: string; usage: Usage; sessionFile?: string }> = [];
|
|
463
|
+
const seenChildren = new Set<string>();
|
|
464
|
+
const workflowRunIds = new Set<string>();
|
|
465
|
+
let unresolvedAsyncChildren = 0;
|
|
466
|
+
|
|
467
|
+
const addChild = (input: { agent?: string; runId?: string; usage?: Usage; sessionFile?: string }): boolean => {
|
|
468
|
+
if (!input.usage || !usageHasValue(input.usage)) return false;
|
|
469
|
+
const identity = input.runId ? `run:${input.runId}` : input.sessionFile ? `session:${input.sessionFile}` : undefined;
|
|
470
|
+
if (identity && seenChildren.has(identity)) return true;
|
|
471
|
+
if (identity) seenChildren.add(identity);
|
|
472
|
+
const usage = { ...input.usage };
|
|
473
|
+
children.push({
|
|
474
|
+
label: `Child ${children.length + 1} (${input.agent ?? "unknown"})`,
|
|
475
|
+
usage,
|
|
476
|
+
...(input.sessionFile ? { sessionFile: input.sessionFile } : {}),
|
|
477
|
+
});
|
|
478
|
+
addUsage(childTotal, usage);
|
|
479
|
+
return true;
|
|
480
|
+
};
|
|
481
|
+
|
|
403
482
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
404
483
|
const message = entry.type === "message" ? (entry as { message?: unknown }).message : undefined;
|
|
405
|
-
const parentUsage = assistantUsageFromMessage(message);
|
|
484
|
+
const parentUsage = assistantUsageFromMessage(message) ?? compactionUsageFromEntry(entry);
|
|
406
485
|
if (parentUsage) addUsage(parent, parentUsage);
|
|
407
486
|
const details = detailsFromSessionEntry(entry);
|
|
408
487
|
if (!details) continue;
|
|
488
|
+
if (details.mode === "workflow" && details.runId) workflowRunIds.add(details.runId);
|
|
409
489
|
for (const result of details.results) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
490
|
+
const resultRunId = (result as SingleResult & { runId?: unknown }).runId;
|
|
491
|
+
addChild({ agent: result.agent, runId: typeof resultRunId === "string" ? resultRunId : undefined, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
492
|
+
}
|
|
493
|
+
for (const completion of details.completions ?? []) {
|
|
494
|
+
if (completion.mode === "workflow") workflowRunIds.add(completion.runId);
|
|
495
|
+
for (const result of completion.results ?? []) {
|
|
496
|
+
addChild({ agent: result.agent, runId: result.runId, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
497
|
+
}
|
|
418
498
|
}
|
|
419
499
|
}
|
|
500
|
+
|
|
501
|
+
let sessionFile: string | null = null;
|
|
502
|
+
try {
|
|
503
|
+
sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
504
|
+
} catch { /* an unpersisted session can still report direct child usage */ }
|
|
505
|
+
const artifactsDirs = new Set<string>();
|
|
506
|
+
const addArtifactsDir = (cwd: string | undefined): void => {
|
|
507
|
+
try {
|
|
508
|
+
artifactsDirs.add(getArtifactsDir(sessionFile, cwd, state.artifactDirPreference));
|
|
509
|
+
} catch { /* invalid or unavailable project roots are ignored */ }
|
|
510
|
+
};
|
|
511
|
+
addArtifactsDir(ctx.cwd);
|
|
512
|
+
addArtifactsDir(state.baseCwd);
|
|
513
|
+
|
|
514
|
+
for (const workflowRunId of workflowRunIds) {
|
|
515
|
+
try {
|
|
516
|
+
const status = readStatus(path.join(DIRS.async, workflowRunId));
|
|
517
|
+
addArtifactsDir(status?.cwd);
|
|
518
|
+
const receipt = readWorkflowReceipt(DIRS.async, workflowRunId);
|
|
519
|
+
const summaryChildren = new Map((receipt.workflowChildren?.children ?? []).map((child) => [child.childId, child]));
|
|
520
|
+
const refsByRunId = new Map<string, { runId: string; agent: string }>();
|
|
521
|
+
const addRef = (runId: string | undefined, agent: string | undefined): void => {
|
|
522
|
+
if (!runId || !agent || refsByRunId.has(runId)) return;
|
|
523
|
+
refsByRunId.set(runId, { runId, agent });
|
|
524
|
+
};
|
|
525
|
+
for (const [key, entry] of Object.entries(receipt.entries)) {
|
|
526
|
+
const summaryChild = summaryChildren.get(key);
|
|
527
|
+
const runIds = entry.continuation?.runIds?.length
|
|
528
|
+
? entry.continuation.runIds
|
|
529
|
+
: entry.latestRunId ? [entry.latestRunId] : summaryChild?.runId ? [summaryChild.runId] : [];
|
|
530
|
+
for (const runId of runIds) addRef(runId, entry.agent ?? summaryChild?.agent);
|
|
531
|
+
}
|
|
532
|
+
for (const child of receipt.workflowChildren?.children ?? []) {
|
|
533
|
+
if (!Object.hasOwn(receipt.entries, child.childId)) addRef(child.runId, child.agent);
|
|
534
|
+
}
|
|
535
|
+
const refs = [...refsByRunId.values()];
|
|
536
|
+
for (const ref of refs) {
|
|
537
|
+
if (seenChildren.has(`run:${ref.runId}`)) continue;
|
|
538
|
+
const usage = metadataUsage([...artifactsDirs], ref);
|
|
539
|
+
if (!usage || !addChild({ ...ref, usage })) unresolvedAsyncChildren += 1;
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to resolve async subagent usage for '${workflowRunId}':`, error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
420
546
|
addUsage(total, parent);
|
|
421
547
|
addUsage(total, childTotal);
|
|
422
548
|
const lines = [
|
|
@@ -432,6 +558,7 @@ function buildSubagentCostReport(ctx: ExtensionContext): string {
|
|
|
432
558
|
if (child.sessionFile) lines.push(` Session: ${child.sessionFile}`);
|
|
433
559
|
}
|
|
434
560
|
}
|
|
561
|
+
if (unresolvedAsyncChildren > 0) lines.push(`Async child usage unavailable: ${unresolvedAsyncChildren}.`);
|
|
435
562
|
lines.push("────────────────────────────", formatCostUsage("Children", childTotal), formatCostUsage("Total", total));
|
|
436
563
|
return lines.join("\n");
|
|
437
564
|
}
|
|
@@ -447,50 +574,76 @@ function getProfileWorkerModel(profile: { subagents?: { agentOverrides?: Record<
|
|
|
447
574
|
return typeof model === "string" && model.trim() ? model.trim() : undefined;
|
|
448
575
|
}
|
|
449
576
|
|
|
577
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
578
|
+
return error instanceof Error
|
|
579
|
+
&& (error.message.includes("This extension ctx is stale")
|
|
580
|
+
|| error.message.includes("Extension context no longer active"));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function slashHasUI(ctx: ExtensionContext): boolean | undefined {
|
|
584
|
+
try {
|
|
585
|
+
return ctx.hasUI;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (isStaleExtensionContextError(error)) return undefined;
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
450
592
|
|
|
451
593
|
async function requestSlashRun(
|
|
452
594
|
pi: ExtensionAPI,
|
|
453
595
|
ctx: ExtensionContext,
|
|
454
596
|
requestId: string,
|
|
455
597
|
params: SubagentParamsLike,
|
|
598
|
+
signal?: AbortSignal,
|
|
456
599
|
): Promise<SlashSubagentResponse> {
|
|
457
600
|
return new Promise((resolve, reject) => {
|
|
458
601
|
let done = false;
|
|
459
602
|
let started = false;
|
|
460
|
-
|
|
461
|
-
const startTimeoutMs = 15_000;
|
|
462
|
-
const startTimeout = setTimeout(() => {
|
|
463
|
-
finish(() => reject(new Error(
|
|
464
|
-
"Slash subagent bridge did not start within 15s. Ensure the extension is loaded correctly.",
|
|
465
|
-
)));
|
|
466
|
-
}, startTimeoutMs);
|
|
603
|
+
let startTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
467
604
|
|
|
468
605
|
const onStarted = (data: unknown) => {
|
|
469
|
-
if (done || !data || typeof data !== "object") return;
|
|
606
|
+
if (done || signal?.aborted || !data || typeof data !== "object") return;
|
|
470
607
|
if ((data as { requestId?: unknown }).requestId !== requestId) return;
|
|
471
608
|
started = true;
|
|
472
|
-
clearTimeout(startTimeout);
|
|
473
|
-
|
|
609
|
+
if (startTimeout) clearTimeout(startTimeout);
|
|
610
|
+
try {
|
|
611
|
+
if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", "running...");
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (isStaleExtensionContextError(error)) {
|
|
614
|
+
finish(() => reject(error));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
474
619
|
};
|
|
475
620
|
|
|
476
621
|
const onResponse = (data: unknown) => {
|
|
477
|
-
if (done || !data || typeof data !== "object") return;
|
|
622
|
+
if (done || signal?.aborted || !data || typeof data !== "object") return;
|
|
478
623
|
const response = data as Partial<SlashSubagentResponse>;
|
|
479
624
|
if (response.requestId !== requestId) return;
|
|
480
|
-
clearTimeout(startTimeout);
|
|
625
|
+
if (startTimeout) clearTimeout(startTimeout);
|
|
481
626
|
finish(() => resolve(response as SlashSubagentResponse));
|
|
482
627
|
};
|
|
483
628
|
|
|
484
629
|
const onUpdate = (data: unknown) => {
|
|
485
|
-
if (done || !data || typeof data !== "object") return;
|
|
630
|
+
if (done || signal?.aborted || !data || typeof data !== "object") return;
|
|
486
631
|
const update = data as SlashSubagentUpdate;
|
|
487
632
|
if (update.requestId !== requestId) return;
|
|
488
633
|
applySlashUpdate(requestId, update);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
634
|
+
try {
|
|
635
|
+
if (!ctx.hasUI) return;
|
|
636
|
+
const tool = update.currentTool ? ` ${update.currentTool}` : "";
|
|
637
|
+
const count = update.toolCount ?? 0;
|
|
638
|
+
const liveDetailKey = keyText("app.tools.expand");
|
|
639
|
+
ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | ${liveDetailKey} live detail`);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (isStaleExtensionContextError(error)) {
|
|
642
|
+
finish(() => reject(error));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
494
647
|
};
|
|
495
648
|
|
|
496
649
|
const onTerminalInput = ctx.hasUI
|
|
@@ -506,18 +659,43 @@ async function requestSlashRun(
|
|
|
506
659
|
const unsubResponse = pi.events.on(SLASH_SUBAGENT_RESPONSE_EVENT, onResponse);
|
|
507
660
|
const unsubUpdate = pi.events.on(SLASH_SUBAGENT_UPDATE_EVENT, onUpdate);
|
|
508
661
|
|
|
662
|
+
let onAbort: () => void;
|
|
509
663
|
const finish = (next: () => void) => {
|
|
510
664
|
if (done) return;
|
|
511
665
|
done = true;
|
|
512
|
-
clearTimeout(startTimeout);
|
|
666
|
+
if (startTimeout) clearTimeout(startTimeout);
|
|
513
667
|
unsubStarted();
|
|
514
668
|
unsubResponse();
|
|
515
669
|
unsubUpdate();
|
|
516
|
-
|
|
670
|
+
try {
|
|
671
|
+
onTerminalInput?.();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
674
|
+
}
|
|
675
|
+
signal?.removeEventListener("abort", onAbort);
|
|
517
676
|
next();
|
|
518
677
|
};
|
|
678
|
+
onAbort = () => finish(() => reject(new Error("Slash subagent request canceled during session replacement or reload.")));
|
|
679
|
+
startTimeout = setTimeout(() => {
|
|
680
|
+
finish(() => reject(new Error(
|
|
681
|
+
"Slash subagent bridge did not start within 15s. Ensure the extension is loaded correctly.",
|
|
682
|
+
)));
|
|
683
|
+
}, 15_000);
|
|
519
684
|
|
|
520
|
-
|
|
685
|
+
if (signal?.aborted) {
|
|
686
|
+
onAbort();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
690
|
+
try {
|
|
691
|
+
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params, ctx });
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (isStaleExtensionContextError(error)) {
|
|
694
|
+
finish(() => reject(error));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
521
699
|
|
|
522
700
|
// Bridge emits STARTED synchronously during REQUEST emit.
|
|
523
701
|
// If not started, no bridge received the request.
|
|
@@ -583,64 +761,76 @@ async function runSlashSubagent(
|
|
|
583
761
|
pi: ExtensionAPI,
|
|
584
762
|
ctx: ExtensionContext,
|
|
585
763
|
params: SubagentParamsLike,
|
|
764
|
+
signal?: AbortSignal,
|
|
586
765
|
): Promise<void> {
|
|
587
|
-
if (
|
|
766
|
+
if (signal?.aborted) return;
|
|
767
|
+
const initialHasUI = slashHasUI(ctx);
|
|
768
|
+
if (initialHasUI === undefined) return;
|
|
769
|
+
try {
|
|
770
|
+
if (initialHasUI) ctx.ui.setToolsExpanded(false);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (isStaleExtensionContextError(error)) return;
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
588
775
|
const requestId = randomUUID();
|
|
589
776
|
const initialDetails = buildSlashInitialResult(requestId, params);
|
|
590
777
|
const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
778
|
+
try {
|
|
779
|
+
pi.sendMessage({
|
|
780
|
+
customType: SLASH_RESULT_TYPE,
|
|
781
|
+
content: initialText,
|
|
782
|
+
display: true,
|
|
783
|
+
details: initialDetails,
|
|
784
|
+
});
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (isStaleExtensionContextError(error)) return;
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
597
789
|
persistSlashSessionSnapshot(ctx);
|
|
598
790
|
|
|
599
791
|
try {
|
|
600
|
-
const response = await requestSlashRun(pi, ctx, requestId, params);
|
|
792
|
+
const response = await requestSlashRun(pi, ctx, requestId, params, signal);
|
|
793
|
+
if (signal?.aborted) return;
|
|
794
|
+
const hasUI = slashHasUI(ctx);
|
|
795
|
+
if (hasUI === undefined) return;
|
|
601
796
|
const finalDetails = finalizeSlashResult(response);
|
|
602
797
|
pi.sendMessage({
|
|
603
798
|
customType: SLASH_RESULT_TYPE,
|
|
604
799
|
content: buildSlashExportText(response),
|
|
605
|
-
display: !
|
|
800
|
+
display: !hasUI,
|
|
606
801
|
details: finalDetails,
|
|
607
802
|
});
|
|
608
803
|
persistSlashSessionSnapshot(ctx);
|
|
609
|
-
if (
|
|
804
|
+
if (hasUI) {
|
|
610
805
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
611
806
|
}
|
|
612
|
-
if (response.isError &&
|
|
807
|
+
if (response.isError && hasUI) {
|
|
613
808
|
ctx.ui.notify(response.errorText || "Subagent failed", "error");
|
|
614
809
|
}
|
|
615
810
|
} catch (error) {
|
|
811
|
+
if (signal?.aborted || isStaleExtensionContextError(error)) return;
|
|
616
812
|
const message = error instanceof Error ? error.message : String(error);
|
|
813
|
+
const hasUI = slashHasUI(ctx);
|
|
814
|
+
if (hasUI === undefined) return;
|
|
617
815
|
const failedDetails = failSlashResult(requestId, params, message);
|
|
618
816
|
pi.sendMessage({
|
|
619
817
|
customType: SLASH_RESULT_TYPE,
|
|
620
818
|
content: `## Subagent result\n\n${message}`,
|
|
621
|
-
display: !
|
|
819
|
+
display: !hasUI,
|
|
622
820
|
details: failedDetails,
|
|
623
821
|
});
|
|
624
822
|
persistSlashSessionSnapshot(ctx);
|
|
625
|
-
if (
|
|
823
|
+
if (hasUI) {
|
|
626
824
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
627
825
|
}
|
|
628
826
|
if (message === "Cancelled") {
|
|
629
|
-
if (
|
|
827
|
+
if (hasUI) ctx.ui.notify("Cancelled", "warning");
|
|
630
828
|
return;
|
|
631
829
|
}
|
|
632
|
-
if (
|
|
830
|
+
if (hasUI) ctx.ui.notify(message, "error");
|
|
633
831
|
}
|
|
634
832
|
}
|
|
635
833
|
|
|
636
|
-
function launchSlashSubagent(
|
|
637
|
-
pi: ExtensionAPI,
|
|
638
|
-
ctx: ExtensionContext,
|
|
639
|
-
params: SubagentParamsLike,
|
|
640
|
-
): void {
|
|
641
|
-
void runSlashSubagent(pi, ctx, params);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
834
|
function slashRunWorkflowScript(key: string, child: Record<string, unknown>): string {
|
|
645
835
|
return `return runs.run(${JSON.stringify(key)}, ${JSON.stringify(child)})`;
|
|
646
836
|
}
|
|
@@ -649,12 +839,23 @@ export function registerSlashCommands(
|
|
|
649
839
|
pi: ExtensionAPI,
|
|
650
840
|
state: SubagentState,
|
|
651
841
|
options: { fleetKeybindings?: FleetKeybindingsConfig; foregroundDetachShortcut?: string } = {},
|
|
652
|
-
): void {
|
|
842
|
+
): { dispose: () => void } {
|
|
653
843
|
let fleetOpen = false;
|
|
844
|
+
let disposed = false;
|
|
845
|
+
const pendingRequests = new Set<AbortController>();
|
|
846
|
+
const runCommand = (ctx: ExtensionContext, params: SubagentParamsLike): Promise<void> => {
|
|
847
|
+
if (disposed) return Promise.resolve();
|
|
848
|
+
const controller = new AbortController();
|
|
849
|
+
pendingRequests.add(controller);
|
|
850
|
+
return runSlashSubagent(pi, ctx, params, controller.signal).finally(() => pendingRequests.delete(controller));
|
|
851
|
+
};
|
|
852
|
+
const launchCommand = (ctx: ExtensionContext, params: SubagentParamsLike): void => {
|
|
853
|
+
void runCommand(ctx, params);
|
|
854
|
+
};
|
|
654
855
|
const showFleet = async (ctx: ExtensionContext) => {
|
|
655
856
|
state.lastUiContext = ctx;
|
|
656
857
|
if (!ctx.hasUI) {
|
|
657
|
-
await
|
|
858
|
+
await runCommand(ctx, { action: "status", view: "fleet" });
|
|
658
859
|
return;
|
|
659
860
|
}
|
|
660
861
|
if (fleetOpen) {
|
|
@@ -710,21 +911,21 @@ export function registerSlashCommands(
|
|
|
710
911
|
if (inline.skill !== undefined) child.skill = inline.skill;
|
|
711
912
|
if (inline.model) child.model = inline.model;
|
|
712
913
|
if (fork) child.context = "fork";
|
|
713
|
-
|
|
914
|
+
launchCommand(ctx, { workflowScript: slashRunWorkflowScript("run", child), async: bg ? true : false });
|
|
714
915
|
},
|
|
715
916
|
});
|
|
716
917
|
|
|
717
918
|
pi.registerCommand("subagent-cost", {
|
|
718
919
|
description: "Show parent and subagent child usage cost for this session",
|
|
719
920
|
handler: async (_args, ctx) => {
|
|
720
|
-
sendSlashText(pi, buildSubagentCostReport(ctx));
|
|
921
|
+
sendSlashText(pi, buildSubagentCostReport(ctx, state));
|
|
721
922
|
},
|
|
722
923
|
});
|
|
723
924
|
|
|
724
925
|
pi.registerCommand("subagents-doctor", {
|
|
725
926
|
description: "Show subagent diagnostics",
|
|
726
927
|
handler: async (_args, ctx) => {
|
|
727
|
-
await
|
|
928
|
+
await runCommand(ctx, { action: "doctor" });
|
|
728
929
|
},
|
|
729
930
|
});
|
|
730
931
|
|
|
@@ -756,7 +957,7 @@ export function registerSlashCommands(
|
|
|
756
957
|
ctx.ui.notify("Usage: /subagents-guide [topic]", "error");
|
|
757
958
|
return;
|
|
758
959
|
}
|
|
759
|
-
await
|
|
960
|
+
await runCommand(ctx, { action: "guide", ...(topic ? { topic } : {}) });
|
|
760
961
|
},
|
|
761
962
|
});
|
|
762
963
|
|
|
@@ -769,7 +970,7 @@ export function registerSlashCommands(
|
|
|
769
970
|
ctx.ui.notify("Usage: /subagents-refine <agent>", "error");
|
|
770
971
|
return;
|
|
771
972
|
}
|
|
772
|
-
await
|
|
973
|
+
await runCommand(ctx, { action: "refine", agent: parts[0] });
|
|
773
974
|
},
|
|
774
975
|
});
|
|
775
976
|
|
|
@@ -820,11 +1021,15 @@ export function registerSlashCommands(
|
|
|
820
1021
|
}
|
|
821
1022
|
|
|
822
1023
|
pi.registerCommand("subagents-stop", {
|
|
823
|
-
description: "Stop a current-session async subagent run",
|
|
1024
|
+
description: "Stop a current-session async subagent run, or one child of it with /subagents-stop <run-id> <child-id>",
|
|
824
1025
|
handler: async (args, ctx) => {
|
|
825
|
-
const id = args.trim();
|
|
1026
|
+
const [id, childId, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
1027
|
+
if (extra.length > 0) {
|
|
1028
|
+
sendSlashText(pi, "Usage: /subagents-stop [run-id] [child-id]");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
826
1031
|
if (id) {
|
|
827
|
-
await
|
|
1032
|
+
await runCommand(ctx, childId ? { action: "stop", id, childId } : { action: "stop", id });
|
|
828
1033
|
return;
|
|
829
1034
|
}
|
|
830
1035
|
|
|
@@ -856,27 +1061,91 @@ export function registerSlashCommands(
|
|
|
856
1061
|
);
|
|
857
1062
|
if (!result?.confirmed || !result.target) return;
|
|
858
1063
|
if (result.target.kind === "scheduled") {
|
|
859
|
-
await
|
|
1064
|
+
await runCommand(ctx, { action: "schedule.pause", id: result.target.id });
|
|
860
1065
|
return;
|
|
861
1066
|
}
|
|
862
|
-
await
|
|
1067
|
+
await runCommand(ctx, { action: "stop", id: result.target.id });
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
pi.registerCommand("subagents-steer", {
|
|
1072
|
+
description: "Steer a live async subagent run with a message, or one child of it with --child <child-id>",
|
|
1073
|
+
handler: async (args, ctx) => {
|
|
1074
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
1075
|
+
const usage = "Usage: /subagents-steer <run-id> [--child <child-id>] <message>";
|
|
1076
|
+
const [id, ...rest] = tokens;
|
|
1077
|
+
if (!id || id.startsWith("--")) {
|
|
1078
|
+
sendSlashText(pi, usage);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
// Only the optional child selector is parsed as a flag. Everything
|
|
1082
|
+
// after the run id (or selector) is free-form message text, including
|
|
1083
|
+
// a leading token such as `--verbose`.
|
|
1084
|
+
let childId: string | undefined;
|
|
1085
|
+
let messageStart = 0;
|
|
1086
|
+
if (rest[0] === "--child" && rest[1] && !rest[1].startsWith("--")) {
|
|
1087
|
+
childId = rest[1];
|
|
1088
|
+
messageStart = 2;
|
|
1089
|
+
}
|
|
1090
|
+
const message = rest.slice(messageStart).join(" ").trim();
|
|
1091
|
+
if (!message) {
|
|
1092
|
+
sendSlashText(pi, usage);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
let targetId = id;
|
|
1096
|
+
let childIndex: number | undefined;
|
|
1097
|
+
if (childId !== undefined) {
|
|
1098
|
+
const sessionId = state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined;
|
|
1099
|
+
const run = listAsyncRuns(DIRS.async, {
|
|
1100
|
+
runId: id,
|
|
1101
|
+
states: ["queued", "running", "paused"],
|
|
1102
|
+
...(sessionId ? { sessionId } : {}),
|
|
1103
|
+
})[0];
|
|
1104
|
+
const status = run ? readStatus(run.asyncDir) : null;
|
|
1105
|
+
if (!run || !status) {
|
|
1106
|
+
sendSlashText(pi, `No current-session async run found for '${id}'.`);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const resolutionStatus = {
|
|
1110
|
+
...status,
|
|
1111
|
+
steps: status.steps?.map((step, index) => ({ ...step, children: run.steps[index]?.children ?? step.children })),
|
|
1112
|
+
};
|
|
1113
|
+
const resolution = resolveAsyncStatusChild(resolutionStatus, childId, { includeNested: true });
|
|
1114
|
+
if (!resolution.ok) {
|
|
1115
|
+
sendSlashText(pi, resolution.message);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (resolution.child.nested) targetId = resolution.child.nested.id;
|
|
1119
|
+
else if (resolution.child.step.runId) targetId = resolution.child.step.runId;
|
|
1120
|
+
else childIndex = resolution.child.index;
|
|
1121
|
+
}
|
|
1122
|
+
await runCommand(ctx, {
|
|
1123
|
+
action: "steer",
|
|
1124
|
+
id: targetId,
|
|
1125
|
+
message,
|
|
1126
|
+
...(childIndex !== undefined ? { index: childIndex } : {}),
|
|
1127
|
+
// Host-style exact-child authority, matching the extension RPC
|
|
1128
|
+
// nonRecoveringSteer guarantee: never swap the addressed child for
|
|
1129
|
+
// a pause-and-revive replacement after a missed acknowledgment.
|
|
1130
|
+
steeringRecovery: false,
|
|
1131
|
+
});
|
|
863
1132
|
},
|
|
864
1133
|
});
|
|
865
1134
|
|
|
866
1135
|
registerPromptWorkflowCommands({
|
|
867
1136
|
pi,
|
|
868
1137
|
run: async (params, ctx) => {
|
|
869
|
-
|
|
1138
|
+
launchCommand(ctx, params);
|
|
870
1139
|
},
|
|
871
1140
|
});
|
|
872
1141
|
|
|
873
1142
|
pi.registerCommand("subagents-models", {
|
|
874
1143
|
description: "Show runtime-loaded builtin subagent models",
|
|
875
1144
|
getArgumentCompletions: makeBuiltinAgentNameCompletions(),
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1145
|
+
handler: async (args, ctx) => {
|
|
1146
|
+
const trimmed = args.trim();
|
|
1147
|
+
if (!trimmed) {
|
|
1148
|
+
await runCommand(ctx, { action: "models" });
|
|
880
1149
|
return;
|
|
881
1150
|
}
|
|
882
1151
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
@@ -889,7 +1158,7 @@ export function registerSlashCommands(
|
|
|
889
1158
|
ctx.ui.notify(`Unknown builtin agent: ${agent}`, "error");
|
|
890
1159
|
return;
|
|
891
1160
|
}
|
|
892
|
-
await
|
|
1161
|
+
await runCommand(ctx, { action: "models", agent });
|
|
893
1162
|
},
|
|
894
1163
|
});
|
|
895
1164
|
|
|
@@ -1062,4 +1331,14 @@ export function registerSlashCommands(
|
|
|
1062
1331
|
},
|
|
1063
1332
|
});
|
|
1064
1333
|
|
|
1334
|
+
return {
|
|
1335
|
+
dispose: () => {
|
|
1336
|
+
if (disposed) return;
|
|
1337
|
+
disposed = true;
|
|
1338
|
+
// A reload revokes the command context; do not let delayed responses
|
|
1339
|
+
// resume a command against the old Pi context.
|
|
1340
|
+
for (const controller of pendingRequests) controller.abort();
|
|
1341
|
+
pendingRequests.clear();
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1065
1344
|
}
|