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
@@ -0,0 +1,25 @@
1
+ import * as fs from "node:fs";
2
+ import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, runFileSystemOperationWithRetry, waitForFileSystemRetry } from "./file-system-retry.ts";
3
+
4
+ type AccessibleDirFs = Pick<typeof fs, "accessSync" | "mkdirSync">;
5
+
6
+ type AccessibleDirOptions = {
7
+ fs?: AccessibleDirFs;
8
+ retryDirectoryErrors?: boolean;
9
+ retryDelaysMs?: readonly number[];
10
+ wait?: (delayMs: number) => void;
11
+ };
12
+
13
+ export function ensureAccessibleDir(dirPath: string, options: AccessibleDirOptions = {}): void {
14
+ const fsImpl = options.fs ?? fs;
15
+ const retryDirectoryErrors = options.retryDirectoryErrors ?? process.platform === "win32";
16
+ const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];
17
+ const wait = options.wait ?? waitForFileSystemRetry;
18
+
19
+ runFileSystemOperationWithRetry(() => {
20
+ fsImpl.mkdirSync(dirPath, { recursive: true });
21
+ }, { retryDelaysMs, wait });
22
+ runFileSystemOperationWithRetry(() => {
23
+ fsImpl.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
24
+ }, { retryDelaysMs, wait });
25
+ }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
3
+ import { TEMP_ARTIFACTS_DIR, type ArtifactPaths, type ArtifactDirPreference } from "./types.ts";
4
4
  import { getAgentDir } from "./utils.ts";
5
5
  const CLEANUP_MARKER_FILE = ".last-cleanup";
6
6
  const PROJECT_ARTIFACT_ROOT = ".pi-subagents";
@@ -17,13 +17,30 @@ export function getProjectChainRunsDir(cwd: string): string {
17
17
  return path.join(getProjectSubagentsDir(cwd), "chain-runs");
18
18
  }
19
19
 
20
- export function getArtifactsDir(sessionFile: string | null, projectCwd?: string): string {
21
- if (projectCwd) return getProjectArtifactsDir(projectCwd);
22
- if (sessionFile) {
23
- const sessionDir = path.dirname(sessionFile);
24
- return path.join(sessionDir, "subagent-artifacts");
20
+ export function getArtifactsDir(
21
+ sessionFile: string | null,
22
+ projectCwd?: string,
23
+ dirPreference: ArtifactDirPreference = "project",
24
+ ): string {
25
+ switch (dirPreference) {
26
+ case "session":
27
+ if (sessionFile) {
28
+ const sessionDir = path.dirname(sessionFile);
29
+ return path.join(sessionDir, "subagent-artifacts");
30
+ }
31
+ return TEMP_ARTIFACTS_DIR;
32
+ case "temp":
33
+ return TEMP_ARTIFACTS_DIR;
34
+ case "project":
35
+ if (projectCwd) return getProjectArtifactsDir(projectCwd);
36
+ if (sessionFile) {
37
+ const sessionDir = path.dirname(sessionFile);
38
+ return path.join(sessionDir, "subagent-artifacts");
39
+ }
40
+ return TEMP_ARTIFACTS_DIR;
41
+ default:
42
+ throw new Error(`Unsupported artifactDir ${JSON.stringify(dirPreference)}; expected "project", "session", or "temp".`);
25
43
  }
26
- return TEMP_ARTIFACTS_DIR;
27
44
  }
28
45
 
29
46
  export function getArtifactPaths(artifactsDir: string, runId: string, agent: string, index?: number): ArtifactPaths {
@@ -47,6 +64,19 @@ export function writeArtifact(filePath: string, content: string): void {
47
64
  fs.writeFileSync(filePath, content, "utf-8");
48
65
  }
49
66
 
67
+ export function formatOutputArtifactContent(input: {
68
+ output: string;
69
+ error?: string;
70
+ transcriptPath?: string;
71
+ metadataPath?: string;
72
+ }): string {
73
+ if (input.output.trim() || !input.error) return input.output;
74
+ const lines = ["Subagent run failed before producing output.", "", "Error:", input.error];
75
+ if (input.transcriptPath) lines.push("", `Transcript: ${input.transcriptPath}`);
76
+ if (input.metadataPath) lines.push(`Metadata: ${input.metadataPath}`);
77
+ return lines.join("\n");
78
+ }
79
+
50
80
  export function writeMetadata(filePath: string, metadata: object): void {
51
81
  fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), "utf-8");
52
82
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, runFileSystemOperationWithRetry, waitForFileSystemRetry } from "./file-system-retry.ts";
3
4
 
4
5
  type AtomicJsonFs = Pick<typeof fs, "mkdirSync" | "writeFileSync" | "renameSync" | "rmSync">;
5
6
 
@@ -10,38 +11,11 @@ type AtomicJsonWriterOptions = {
10
11
  random?: () => number;
11
12
  mode?: number;
12
13
  retryRenameErrors?: boolean;
14
+ retryDirectoryErrors?: boolean;
13
15
  retryDelaysMs?: readonly number[];
14
16
  wait?: (delayMs: number) => void;
15
17
  };
16
18
 
17
- const DEFAULT_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 500, 1000, 2000, 4000] as const;
18
- const RETRYABLE_RENAME_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
19
- const WAIT_BUFFER = typeof SharedArrayBuffer !== "undefined" ? new SharedArrayBuffer(4) : undefined;
20
- const WAIT_VIEW = WAIT_BUFFER ? new Int32Array(WAIT_BUFFER) : undefined;
21
-
22
- function waitSync(delayMs: number): void {
23
- if (delayMs <= 0) return;
24
- if (WAIT_VIEW) {
25
- try {
26
- // writeAtomicJson is synchronous because callers often update status from sync callbacks.
27
- // Atomics.wait gives Windows rename locks time to clear without burning CPU.
28
- Atomics.wait(WAIT_VIEW, 0, 0, delayMs);
29
- return;
30
- } catch {
31
- // Fall through to the portable busy wait below.
32
- }
33
- }
34
- const end = Date.now() + delayMs;
35
- while (Date.now() < end) {
36
- // Portable fallback for runtimes where Atomics.wait is unavailable.
37
- }
38
- }
39
-
40
- function isRetryableRenameError(error: unknown): boolean {
41
- const code = (error as NodeJS.ErrnoException | undefined)?.code;
42
- return typeof code === "string" && RETRYABLE_RENAME_ERROR_CODES.has(code);
43
- }
44
-
45
19
  function renameWithRetry(
46
20
  fsImpl: AtomicJsonFs,
47
21
  sourcePath: string,
@@ -49,16 +23,9 @@ function renameWithRetry(
49
23
  retryDelaysMs: readonly number[],
50
24
  wait: (delayMs: number) => void,
51
25
  ): void {
52
- for (let attempt = 0; ; attempt++) {
53
- try {
54
- fsImpl.renameSync(sourcePath, targetPath);
55
- return;
56
- } catch (error) {
57
- const delayMs = retryDelaysMs[attempt];
58
- if (delayMs === undefined || !isRetryableRenameError(error)) throw error;
59
- wait(delayMs);
60
- }
61
- }
26
+ runFileSystemOperationWithRetry(() => {
27
+ fsImpl.renameSync(sourcePath, targetPath);
28
+ }, { retryDelaysMs, wait });
62
29
  }
63
30
 
64
31
  export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (filePath: string, payload: object) => void {
@@ -68,17 +35,22 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
68
35
  const random = options.random ?? Math.random;
69
36
  const mode = options.mode;
70
37
  const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
71
- const retryDelaysMs = retryRenameErrors ? options.retryDelaysMs ?? DEFAULT_RENAME_RETRY_DELAYS_MS : [];
72
- const wait = options.wait ?? waitSync;
38
+ const retryDirectoryErrors = options.retryDirectoryErrors ?? retryRenameErrors;
39
+ const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
40
+ const renameRetryDelaysMs = retryRenameErrors ? retryDelaysMs : [];
41
+ const directoryRetryDelaysMs = retryDirectoryErrors ? retryDelaysMs : [];
42
+ const wait = options.wait ?? waitForFileSystemRetry;
73
43
  return (filePath: string, payload: object): void => {
74
- fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
44
+ runFileSystemOperationWithRetry(() => {
45
+ fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
46
+ }, { retryDelaysMs: directoryRetryDelaysMs, wait });
75
47
  const tempPath = path.join(
76
48
  path.dirname(filePath),
77
49
  `.${path.basename(filePath)}.${pid}.${now()}.${random().toString(36).slice(2)}.tmp`,
78
50
  );
79
51
  try {
80
52
  fsImpl.writeFileSync(tempPath, JSON.stringify(payload, null, 2), mode === undefined ? "utf-8" : { encoding: "utf-8", mode });
81
- renameWithRetry(fsImpl, tempPath, filePath, retryDelaysMs, wait);
53
+ renameWithRetry(fsImpl, tempPath, filePath, renameRetryDelaysMs, wait);
82
54
  } finally {
83
55
  fsImpl.rmSync(tempPath, { force: true });
84
56
  }
@@ -3,6 +3,30 @@ import * as path from "node:path";
3
3
  import type { Message } from "@earendil-works/pi-ai";
4
4
  import { extractTextFromContent, extractToolArgsPreview } from "./utils.ts";
5
5
 
6
+ const MAX_TOOL_PAYLOAD_BYTES = 32 * 1024;
7
+ const TOOL_PAYLOAD_TRUNCATION_MARKER = "\n\n… payload truncated";
8
+
9
+ function boundedPayload(value: unknown, maxBytes = MAX_TOOL_PAYLOAD_BYTES): string | undefined {
10
+ let text: string;
11
+ if (typeof value === "string") text = value;
12
+ else {
13
+ try {
14
+ const serialized = JSON.stringify(value, null, 2);
15
+ if (serialized === undefined) return undefined;
16
+ text = serialized;
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ if (!text.trim()) return undefined;
22
+ const payload = Buffer.from(text, "utf-8");
23
+ if (payload.length <= maxBytes) return text;
24
+ const markerBytes = Buffer.byteLength(TOOL_PAYLOAD_TRUNCATION_MARKER, "utf-8");
25
+ let end = Math.max(0, maxBytes - markerBytes);
26
+ while (end > 0 && (payload[end]! & 0xc0) === 0x80) end--;
27
+ return `${payload.subarray(0, end).toString("utf-8")}${TOOL_PAYLOAD_TRUNCATION_MARKER}`;
28
+ }
29
+
6
30
  export const CHILD_TRANSCRIPT_ARTIFACT_VERSION = 1;
7
31
  const DEFAULT_MAX_CHILD_TRANSCRIPT_BYTES = 50 * 1024 * 1024;
8
32
 
@@ -19,8 +43,10 @@ type ChildTranscriptMessage = Message & {
19
43
  interface ChildTranscriptEvent {
20
44
  type?: string;
21
45
  message?: ChildTranscriptMessage;
46
+ toolCallId?: string;
22
47
  toolName?: string;
23
48
  args?: unknown;
49
+ isError?: boolean;
24
50
  }
25
51
 
26
52
  interface ChildTranscriptWriterInput {
@@ -147,6 +173,27 @@ export function createChildTranscriptWriter(input: ChildTranscriptWriterInput):
147
173
 
148
174
  const writeMessage = (sourceEventType: string, message: ChildTranscriptMessage) => {
149
175
  const text = extractTextFromContent(message.content);
176
+ if (message.role === "toolResult") {
177
+ const output = boundedPayload(text);
178
+ writeRecord({
179
+ ...baseRecord("message"),
180
+ sourceEventType,
181
+ role: message.role,
182
+ toolCallId: message.toolCallId,
183
+ toolName: message.toolName,
184
+ isError: message.isError,
185
+ ...(output ? { text: output, outputTruncated: output.includes("… payload truncated") } : {}),
186
+ message: {
187
+ role: message.role,
188
+ toolCallId: message.toolCallId,
189
+ toolName: message.toolName,
190
+ isError: message.isError,
191
+ content: output ? [{ type: "text", text: output }] : [],
192
+ ...(message.timestamp !== undefined ? { timestamp: message.timestamp } : {}),
193
+ },
194
+ });
195
+ return;
196
+ }
150
197
  writeRecord({
151
198
  ...baseRecord("message"),
152
199
  sourceEventType,
@@ -178,11 +225,14 @@ export function createChildTranscriptWriter(input: ChildTranscriptWriterInput):
178
225
  }
179
226
  if (event.type === "tool_execution_start" && event.toolName) {
180
227
  const args = eventArgs(event);
228
+ const argsPayload = boundedPayload(args);
181
229
  writeRecord({
182
230
  ...baseRecord("tool_start"),
183
231
  sourceEventType: event.type,
232
+ ...(event.toolCallId ? { toolCallId: event.toolCallId } : {}),
184
233
  toolName: event.toolName,
185
234
  ...(Object.keys(args).length > 0 ? { argsPreview: extractToolArgsPreview(args) } : {}),
235
+ ...(argsPayload ? { argsPayload } : {}),
186
236
  });
187
237
  return;
188
238
  }
@@ -190,7 +240,9 @@ export function createChildTranscriptWriter(input: ChildTranscriptWriterInput):
190
240
  writeRecord({
191
241
  ...baseRecord("tool_end"),
192
242
  sourceEventType: event.type,
243
+ ...(event.toolCallId ? { toolCallId: event.toolCallId } : {}),
193
244
  ...(event.toolName ? { toolName: event.toolName } : {}),
245
+ ...(typeof event.isError === "boolean" ? { isError: event.isError } : {}),
194
246
  });
195
247
  }
196
248
  },
@@ -0,0 +1,47 @@
1
+ const WAIT_BUFFER = typeof SharedArrayBuffer !== "undefined" ? new SharedArrayBuffer(4) : undefined;
2
+ const WAIT_VIEW = WAIT_BUFFER ? new Int32Array(WAIT_BUFFER) : undefined;
3
+ const RETRYABLE_FILE_SYSTEM_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
4
+
5
+ export const DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 500, 1000, 2000, 4000] as const;
6
+
7
+ export type FileSystemRetryOptions = {
8
+ retryDelaysMs?: readonly number[];
9
+ wait?: (delayMs: number) => void;
10
+ };
11
+
12
+ export function waitForFileSystemRetry(delayMs: number): void {
13
+ if (delayMs <= 0) return;
14
+ if (WAIT_VIEW) {
15
+ try {
16
+ // Callers are synchronous status/result writers; Atomics.wait gives
17
+ // Windows directory and rename locks time to clear without burning CPU.
18
+ Atomics.wait(WAIT_VIEW, 0, 0, delayMs);
19
+ return;
20
+ } catch {
21
+ // Fall through to the portable busy wait below.
22
+ }
23
+ }
24
+ const end = Date.now() + delayMs;
25
+ while (Date.now() < end) {
26
+ // Portable fallback for runtimes where Atomics.wait is unavailable.
27
+ }
28
+ }
29
+
30
+ export function isRetryableFileSystemError(error: unknown): boolean {
31
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
32
+ return typeof code === "string" && RETRYABLE_FILE_SYSTEM_ERROR_CODES.has(code);
33
+ }
34
+
35
+ export function runFileSystemOperationWithRetry<T>(operation: () => T, options: FileSystemRetryOptions = {}): T {
36
+ const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
37
+ const wait = options.wait ?? waitForFileSystemRetry;
38
+ for (let attempt = 0; ; attempt++) {
39
+ try {
40
+ return operation();
41
+ } catch (error) {
42
+ const delayMs = retryDelaysMs[attempt];
43
+ if (delayMs === undefined || !isRetryableFileSystemError(error)) throw error;
44
+ wait(delayMs);
45
+ }
46
+ }
47
+ }
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentConfig } from "../agents/agents.ts";
8
8
  import { normalizeSkillInput } from "../agents/skills.ts";
9
- import { CHAIN_RUNS_DIR, type AcceptanceInput, type JsonSchemaObject, type OutputMode, type ToolBudgetConfig } from "./types.ts";
9
+ import { CHAIN_RUNS_DIR, type AcceptanceInput, type AgentContract, type ChainGateLayer, type JsonSchemaObject, type OutputMode, type ToolBudgetConfig } from "./types.ts";
10
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
11
11
  const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
12
12
 
@@ -57,6 +57,8 @@ export interface SequentialStep {
57
57
  model?: string;
58
58
  toolBudget?: ToolBudgetConfig;
59
59
  acceptance?: AcceptanceInput;
60
+ agentContract?: AgentContract;
61
+ gateOn?: ChainGateLayer;
60
62
  }
61
63
 
62
64
  /** Parallel task item within a parallel step */
@@ -77,6 +79,8 @@ export interface ParallelTaskItem {
77
79
  model?: string;
78
80
  toolBudget?: ToolBudgetConfig;
79
81
  acceptance?: AcceptanceInput;
82
+ agentContract?: AgentContract;
83
+ gateOn?: ChainGateLayer;
80
84
  }
81
85
 
82
86
  export interface DynamicExpandSpec {
@@ -106,6 +110,8 @@ export interface DynamicParallelStep {
106
110
  phase?: string;
107
111
  label?: string;
108
112
  acceptance?: AcceptanceInput;
113
+ agentContract?: AgentContract;
114
+ gateOn?: ChainGateLayer;
109
115
  }
110
116
 
111
117
  /** Parallel step: multiple agents running concurrently */
@@ -115,6 +121,8 @@ export interface ParallelStep {
115
121
  failFast?: boolean;
116
122
  worktree?: boolean;
117
123
  cwd?: string;
124
+ agentContract?: AgentContract;
125
+ gateOn?: ChainGateLayer;
118
126
  }
119
127
 
120
128
  /** Union type for chain steps */