pi-subagents 0.45.2 → 0.47.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 (70) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +328 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +119 -0
  7. package/docs/models.md +192 -0
  8. package/docs/observability.md +174 -0
  9. package/docs/tool-reference.md +343 -0
  10. package/docs/watchdog.md +176 -0
  11. package/docs/workflows.md +163 -0
  12. package/package.json +4 -2
  13. package/skills/pi-subagents/references/execution-controls.md +6 -6
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  15. package/src/agents/agents.ts +17 -8
  16. package/src/agents/frontmatter.ts +7 -3
  17. package/src/agents/skills.ts +2 -9
  18. package/src/api/project-panes.ts +30 -0
  19. package/src/extension/config.ts +18 -1
  20. package/src/extension/fanout-child.ts +5 -4
  21. package/src/extension/index.ts +66 -19
  22. package/src/extension/rpc.ts +3 -6
  23. package/src/extension/schemas.ts +28 -7
  24. package/src/extension/subagent-guide.ts +39 -0
  25. package/src/extension/tool-description.ts +30 -12
  26. package/src/inspectors/herdr/project-panes.ts +459 -63
  27. package/src/missions/actions.ts +25 -2
  28. package/src/missions/lifecycle.ts +21 -2
  29. package/src/missions/store.ts +79 -2
  30. package/src/missions/types.ts +33 -0
  31. package/src/missions/workflow-state.ts +19 -13
  32. package/src/runs/background/async-execution.ts +17 -6
  33. package/src/runs/background/async-job-tracker.ts +15 -0
  34. package/src/runs/background/async-resume.ts +19 -3
  35. package/src/runs/background/async-status.ts +6 -1
  36. package/src/runs/background/completion-replay.ts +267 -0
  37. package/src/runs/background/control-channel.ts +36 -0
  38. package/src/runs/background/result-watcher.ts +28 -6
  39. package/src/runs/background/scheduled-runs.ts +2 -1
  40. package/src/runs/background/stale-run-reconciler.ts +2 -21
  41. package/src/runs/background/subagent-runner.ts +47 -6
  42. package/src/runs/background/wait-completions.ts +39 -5
  43. package/src/runs/background/wait-subscriptions.ts +18 -3
  44. package/src/runs/foreground/async-steering-action.ts +1 -1
  45. package/src/runs/foreground/chain-execution.ts +3 -0
  46. package/src/runs/foreground/execution.ts +7 -0
  47. package/src/runs/foreground/foreground-history.ts +137 -0
  48. package/src/runs/foreground/subagent-executor.ts +403 -54
  49. package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/model-fallback.ts +8 -4
  52. package/src/runs/shared/model-scope.ts +12 -2
  53. package/src/runs/shared/parallel-utils.ts +1 -0
  54. package/src/runs/shared/worktree.ts +3 -2
  55. package/src/shared/artifacts.ts +14 -14
  56. package/src/shared/display-text.ts +100 -0
  57. package/src/shared/fork-context.ts +13 -0
  58. package/src/shared/formatters.ts +4 -6
  59. package/src/shared/prompt-resources.ts +51 -0
  60. package/src/shared/settings.ts +15 -2
  61. package/src/shared/types.ts +41 -2
  62. package/src/shared/utf8.ts +11 -0
  63. package/src/shared/utils.ts +43 -33
  64. package/src/slash/prompt-workflows.ts +2 -15
  65. package/src/slash/slash-commands.ts +22 -2
  66. package/src/tui/fleet-status.ts +22 -12
  67. package/src/tui/fleet.ts +135 -25
  68. package/src/tui/render.ts +150 -33
  69. package/src/watchdog/change-signature.ts +4 -3
  70. package/src/workflows/scripted-workflow.ts +167 -10
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
6
6
  import { formatResumeFirstFailedRunDetail } from "./resume-guidance.ts";
7
+ import { readCompletionReplay } from "./completion-replay.ts";
7
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
9
  import {
9
10
  DIRS,
@@ -14,6 +15,7 @@ import {
14
15
  SUBAGENT_FOREGROUND_COMPLETE_EVENT,
15
16
  SUBAGENT_RESULT_INTERCOM_EVENT,
16
17
  type SubagentState,
18
+ type WaitCompletion,
17
19
  type WaitSubscriptionRecord,
18
20
  } from "../../shared/types.ts";
19
21
 
@@ -104,7 +106,7 @@ export function createWaitSubscriptionManager(
104
106
  unresolvedRestoredForegroundTokens.delete(record.token);
105
107
  };
106
108
 
107
- const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string) => {
109
+ const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string, completion?: WaitCompletion) => {
108
110
  if (disposed || state.currentSessionId !== record.sessionId) return;
109
111
  try {
110
112
  remove(record);
@@ -117,7 +119,12 @@ export function createWaitSubscriptionManager(
117
119
  customType: "subagent-wait-subscription",
118
120
  content: `Wait subscription ${record.token} fired for run ${record.runId}: ${outcome}. ${detail}`,
119
121
  display: true,
120
- details: { token: record.token, runId: record.runId, outcome },
122
+ details: {
123
+ token: record.token,
124
+ runId: record.runId,
125
+ outcome,
126
+ ...(completion ? { completions: [completion] } : {}),
127
+ },
121
128
  }, { triggerTurn: true });
122
129
  } catch (error) {
123
130
  console.error(`Failed to deliver wait subscription '${record.token}' after clearing it:`, error);
@@ -167,7 +174,15 @@ export function createWaitSubscriptionManager(
167
174
  return;
168
175
  }
169
176
  if (run.state !== "queued" && run.state !== "running") {
170
- settle(record, run.state === "complete" ? "completed" : run.state, formatResumeFirstFailedRunDetail(run) ?? "Inspect the run status for its final output.");
177
+ let completion: WaitCompletion | undefined;
178
+ try {
179
+ completion = readCompletionReplay(resultsDir, record.runId, { sessionId: record.sessionId, now: now() })?.completion;
180
+ } catch (error) {
181
+ console.error(`Failed to read completion replay for wait subscription '${record.token}':`, error);
182
+ }
183
+ const detail = formatResumeFirstFailedRunDetail(run) ?? "Inspect the run status for its final output.";
184
+ const archiveDetail = completion?.archivePath ? ` Completion archive: ${completion.archivePath}.` : "";
185
+ settle(record, run.state === "complete" ? "completed" : run.state, `${detail}${archiveDetail}`, completion);
171
186
  }
172
187
  };
173
188
 
@@ -220,7 +220,7 @@ export async function steerAsyncRun(input: {
220
220
  const revived = await input.recover(limits);
221
221
  if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
222
222
  const sourceStatus = readStatus(asyncDir);
223
- const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
223
+ const targetIndex = input.index ?? sourceStatus?.steering?.recent.find((request) => request.id === requestId)?.targets[0]?.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
224
224
  if (sourceStatus?.state === "paused" && sourceStatus.steering && targetIndex >= 0) {
225
225
  updateSteeringTarget(sourceStatus.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
226
226
  const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
@@ -34,6 +34,7 @@ import {
34
34
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
35
35
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
36
36
  import { runSync } from "./execution.ts";
37
+ import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
37
38
  import {
38
39
  beginForegroundChild,
39
40
  finishForegroundChild,
@@ -387,6 +388,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
387
388
  result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
388
389
  permissions: input.permissions,
389
390
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
391
+ ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
390
392
  capabilityCeiling: input.capabilityCeiling,
391
393
  context: input.contextForAgent?.(task.agent),
392
394
  cwd: taskCwd,
@@ -1367,6 +1369,7 @@ ${step.message}` : ""}` }],
1367
1369
  r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1368
1370
  permissions: params.permissions,
1369
1371
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1372
+ ...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
1370
1373
  capabilityCeiling: params.capabilityCeiling,
1371
1374
  context: params.contextForAgent?.(seqStep.agent),
1372
1375
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
@@ -8,6 +8,7 @@ import * as path from "node:path";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { AgentConfig } from "../../agents/agents.ts";
10
10
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
11
+ import { alignForkedSessionCwd } from "../../shared/fork-context.ts";
11
12
  import {
12
13
  ensureArtifactsDir,
13
14
  formatOutputArtifactContent,
@@ -337,6 +338,9 @@ async function runSingleAttempt(
337
338
  parentRootRunId: options.nestedRoute?.rootRunId,
338
339
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
339
340
  parentSessionId: options.parentSessionId,
341
+ steerInboxDir: options.steerInboxDir,
342
+ steerCapabilityPath: options.steerCapabilityPath,
343
+ steerAckDir: options.steerAckDir,
340
344
  structuredOutput: options.structuredOutput,
341
345
  toolBudget: options.toolBudget,
342
346
  allowZeroToolBudget: options.allowZeroToolBudget,
@@ -1416,6 +1420,9 @@ async function runSyncCompletion(
1416
1420
  const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1417
1421
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1418
1422
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
1423
+ if (options.context === "fork" && options.sessionFile && existsSync(options.sessionFile)) {
1424
+ alignForkedSessionCwd(options.sessionFile, options.cwd ?? runtimeCwd);
1425
+ }
1419
1426
  const skillNames = options.skills ?? agent.skills ?? [];
1420
1427
  const skillCwd = options.cwd ?? runtimeCwd;
1421
1428
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
@@ -0,0 +1,137 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
4
+ import { DIRS } from "../../shared/types.ts";
5
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
6
+ import { utf8Tail } from "../../shared/utf8.ts";
7
+
8
+ export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
9
+ const HISTORY_VERSION = 1;
10
+ const MAX_INLINE_OUTPUT_BYTES = 64 * 1024;
11
+
12
+ interface ForegroundHistoryIndex {
13
+ version: 1;
14
+ runs: ForegroundResumeRun[];
15
+ }
16
+
17
+ function historyPath(resultsDir: string): string {
18
+ return path.join(resultsDir, "foreground-history.json");
19
+ }
20
+
21
+ function boundedTail(value: string): string {
22
+ return utf8Tail(value, MAX_INLINE_OUTPUT_BYTES).text;
23
+ }
24
+
25
+ function compactChild(child: ForegroundResumeChild): ForegroundResumeChild {
26
+ const outputPath = child.artifactPaths?.outputPath ?? child.savedOutputPath;
27
+ return {
28
+ agent: child.agent,
29
+ index: child.index,
30
+ ...(child.context ? { context: child.context } : {}),
31
+ ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
32
+ ...(child.model ? { model: child.model } : {}),
33
+ ...(child.thinking ? { thinking: child.thinking } : {}),
34
+ status: child.status,
35
+ ...(child.activityState ? { activityState: child.activityState } : {}),
36
+ ...(child.lastActivityAt !== undefined ? { lastActivityAt: child.lastActivityAt } : {}),
37
+ ...(child.currentTool ? { currentTool: child.currentTool } : {}),
38
+ ...(child.currentToolStartedAt !== undefined ? { currentToolStartedAt: child.currentToolStartedAt } : {}),
39
+ ...(child.currentPath ? { currentPath: child.currentPath } : {}),
40
+ ...(child.turnCount !== undefined ? { turnCount: child.turnCount } : {}),
41
+ ...(child.tokens !== undefined ? { tokens: child.tokens } : {}),
42
+ ...(child.toolCount !== undefined ? { toolCount: child.toolCount } : {}),
43
+ ...(child.exitCode !== undefined ? { exitCode: child.exitCode } : {}),
44
+ ...(child.error ? { error: child.error } : {}),
45
+ ...(!outputPath && child.finalOutput ? { finalOutput: boundedTail(child.finalOutput) } : {}),
46
+ ...(child.outputState ? { outputState: child.outputState } : {}),
47
+ ...(child.outputMode ? { outputMode: child.outputMode } : {}),
48
+ ...(child.savedOutputPath ? { savedOutputPath: child.savedOutputPath } : {}),
49
+ ...(child.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
50
+ ...(child.artifactPaths ? { artifactPaths: child.artifactPaths } : {}),
51
+ ...(child.transcriptPath ? { transcriptPath: child.transcriptPath } : {}),
52
+ ...(child.transcriptError ? { transcriptError: child.transcriptError } : {}),
53
+ ...(child.acceptance ? { acceptance: child.acceptance } : {}),
54
+ ...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
55
+ ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
56
+ ...(child.updatedAt !== undefined ? { updatedAt: child.updatedAt } : {}),
57
+ };
58
+ }
59
+
60
+ function isRestorableForegroundStatus(status: unknown): status is ForegroundResumeChild["status"] {
61
+ return status === "completed" || status === "failed" || status === "paused" || status === "stopped";
62
+ }
63
+
64
+ function compactRun(run: ForegroundResumeRun): ForegroundResumeRun | undefined {
65
+ if (!run.sessionId) return undefined;
66
+ if (run.children.length === 0 || !run.children.every((child) => isRestorableForegroundStatus(child.status))) return undefined;
67
+ return {
68
+ runId: run.runId,
69
+ mode: run.mode,
70
+ cwd: run.cwd,
71
+ sessionId: run.sessionId,
72
+ updatedAt: run.updatedAt,
73
+ children: run.children.map(compactChild),
74
+ };
75
+ }
76
+
77
+ function readIndex(resultsDir: string): ForegroundHistoryIndex {
78
+ const filePath = historyPath(resultsDir);
79
+ if (!fs.existsSync(filePath)) return { version: HISTORY_VERSION, runs: [] };
80
+ try {
81
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
82
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { version: HISTORY_VERSION, runs: [] };
83
+ const record = parsed as Partial<ForegroundHistoryIndex>;
84
+ if (record.version !== HISTORY_VERSION || !Array.isArray(record.runs)) return { version: HISTORY_VERSION, runs: [] };
85
+ return { version: HISTORY_VERSION, runs: record.runs.filter(isRestorableRun) };
86
+ } catch {
87
+ return { version: HISTORY_VERSION, runs: [] };
88
+ }
89
+ }
90
+
91
+ function isRestorableRun(value: unknown): value is ForegroundResumeRun {
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
93
+ const run = value as Partial<ForegroundResumeRun>;
94
+ return typeof run.runId === "string" && Boolean(run.runId)
95
+ && (run.mode === "single" || run.mode === "parallel" || run.mode === "chain")
96
+ && typeof run.cwd === "string" && Boolean(run.cwd)
97
+ && typeof run.sessionId === "string" && Boolean(run.sessionId)
98
+ && typeof run.updatedAt === "number" && Number.isFinite(run.updatedAt)
99
+ && Array.isArray(run.children)
100
+ && run.children.length > 0
101
+ && run.children.every((child) => Boolean(child && typeof child === "object" && !Array.isArray(child)
102
+ && typeof (child as Partial<ForegroundResumeChild>).agent === "string"
103
+ && typeof (child as Partial<ForegroundResumeChild>).index === "number"
104
+ && isRestorableForegroundStatus((child as Partial<ForegroundResumeChild>).status)));
105
+ }
106
+
107
+ function sortAndBound(runs: ForegroundResumeRun[], limit: number): ForegroundResumeRun[] {
108
+ return [...runs].sort((left, right) => right.updatedAt - left.updatedAt).slice(0, limit);
109
+ }
110
+
111
+ export function persistForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; limit?: number } = {}): void {
112
+ const resultsDir = options.resultsDir ?? DIRS.results;
113
+ const limit = options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS;
114
+ const existing = readIndex(resultsDir);
115
+ const merged = new Map(existing.runs.map((run) => [run.runId, run]));
116
+ for (const run of state.foregroundRuns?.values() ?? []) {
117
+ const compact = compactRun(run);
118
+ if (compact) merged.set(compact.runId, compact);
119
+ }
120
+ const runs = sortAndBound([...merged.values()], limit);
121
+ writeAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
122
+ }
123
+
124
+ export function restoreForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; sessionId?: string | null; limit?: number } = {}): number {
125
+ const sessionId = options.sessionId ?? state.currentSessionId;
126
+ if (!sessionId) return 0;
127
+ const index = readIndex(options.resultsDir ?? DIRS.results);
128
+ const runs = sortAndBound(index.runs.filter((run) => run.sessionId === sessionId), options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
129
+ state.foregroundRuns ??= new Map();
130
+ let restored = 0;
131
+ for (const run of runs) {
132
+ if (state.foregroundRuns.has(run.runId)) continue;
133
+ state.foregroundRuns.set(run.runId, run);
134
+ restored += 1;
135
+ }
136
+ return restored;
137
+ }