pi-subagents 0.58.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -1,10 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { Message } from "@earendil-works/pi-ai";
4
+ import type { Message, Usage as PiUsage } from "@earendil-works/pi-ai";
5
5
  import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "./display-text.ts";
6
6
  import { formatToolCall } from "./formatters.ts";
7
7
  import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
8
+ import { validateAsyncStatusLaneMetadata } from "../runs/shared/lane-metadata.ts";
8
9
 
9
10
  const DEFAULT_CONFIG_DIR_NAME = ".pi";
10
11
  const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
@@ -183,6 +184,13 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
183
184
  cause: error instanceof Error ? error : undefined,
184
185
  });
185
186
  }
187
+ try {
188
+ validateAsyncStatusLaneMetadata(status, `Invalid async status '${statusPath}'`);
189
+ } catch (error) {
190
+ throw new Error(`Failed to validate async status file '${statusPath}': ${getErrorMessage(error)}`, {
191
+ cause: error instanceof Error ? error : undefined,
192
+ });
193
+ }
186
194
 
187
195
  statusCache.set(statusPath, {
188
196
  mtime: stat.mtimeMs,
@@ -199,53 +207,6 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
199
207
  return status;
200
208
  }
201
209
 
202
- const outputTailCache = new Map<string, { mtime: number; size: number; lines: string[] }>();
203
-
204
- /**
205
- * Get the last N lines from an output file (with mtime/size-based caching)
206
- */
207
- function getOutputTail(outputFile: string | undefined, maxLines: number = 3): string[] {
208
- if (!outputFile) return [];
209
- let fd: number | null = null;
210
- try {
211
- const stat = fs.statSync(outputFile);
212
- if (stat.size === 0) return [];
213
-
214
- const cached = outputTailCache.get(outputFile);
215
- if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
216
- return cached.lines;
217
- }
218
-
219
- const tailBytes = 4096;
220
- const start = Math.max(0, stat.size - tailBytes);
221
- fd = fs.openSync(outputFile, "r");
222
- const buffer = Buffer.alloc(Math.min(tailBytes, stat.size));
223
- fs.readSync(fd, buffer, 0, buffer.length, start);
224
- const content = buffer.toString("utf-8");
225
- const allLines = content.split("\n").filter((l) => l.trim());
226
- const lines = allLines.slice(-maxLines).map((l) => l.slice(0, 120) + (l.length > 120 ? "..." : ""));
227
-
228
- outputTailCache.set(outputFile, { mtime: stat.mtimeMs, size: stat.size, lines });
229
- if (outputTailCache.size > 20) {
230
- const firstKey = outputTailCache.keys().next().value;
231
- if (firstKey) outputTailCache.delete(firstKey);
232
- }
233
-
234
- return lines;
235
- } catch {
236
- // Output tails are UI-only hints; unreadable or missing files should render as no tail.
237
- return [];
238
- } finally {
239
- if (fd !== null) {
240
- try {
241
- fs.closeSync(fd);
242
- } catch {
243
- // Closing the best-effort tail file handle should not surface over the main status view.
244
- }
245
- }
246
- }
247
- }
248
-
249
210
  export function getLastActivity(outputFile: string | undefined): string {
250
211
  if (!outputFile) return "";
251
212
  try {
@@ -276,13 +237,6 @@ export function findLatestSessionFile(sessionDir: string): string | null {
276
237
  return latest ? latest.path : null;
277
238
  }
278
239
 
279
- function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
280
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
281
- const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
282
- fs.writeFileSync(p, prompt, { mode: 0o600 });
283
- return { dir, path: p };
284
- }
285
-
286
240
  export function getFinalOutput(messages: Message[]): string {
287
241
  const validTextParts: string[] = [];
288
242
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -338,6 +292,7 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
338
292
  return {
339
293
  index: progress.index,
340
294
  agent: progress.agent,
295
+ ...(progress.sessionName ? { sessionName: progress.sessionName } : {}),
341
296
  status: progress.status,
342
297
  activityState: progress.activityState,
343
298
  task: "[prompt redacted]",
@@ -384,6 +339,17 @@ export function sumResultsUsage(results: SingleResult[]): Usage {
384
339
  return usage;
385
340
  }
386
341
 
342
+ export function toAgentToolUsage(usage: Usage): PiUsage {
343
+ return {
344
+ input: usage.input,
345
+ output: usage.output,
346
+ cacheRead: usage.cacheRead,
347
+ cacheWrite: usage.cacheWrite,
348
+ totalTokens: usage.input + usage.output + usage.cacheRead + usage.cacheWrite,
349
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: usage.cost },
350
+ };
351
+ }
352
+
387
353
  function addNestedCost(total: NonNullable<Details["totalCost"]>, children: NestedRunSummary[] | undefined): void {
388
354
  for (const child of children ?? []) {
389
355
  if (child.totalCost) {
@@ -482,6 +448,20 @@ export function hasEmptyTerminalAssistantResponse(messages: Message[]): boolean
482
448
  && lastAssistant.usage.output === 0;
483
449
  }
484
450
 
451
+ export function formatEmptyTerminalAssistantResponseError(messages: Message[]): string {
452
+ const lastAssistant = messages.findLast((message) => message.role === "assistant");
453
+ const errorMessage = lastAssistant && "errorMessage" in lastAssistant && typeof lastAssistant.errorMessage === "string" && lastAssistant.errorMessage.trim()
454
+ ? lastAssistant.errorMessage.trim()
455
+ : undefined;
456
+ if (errorMessage) return errorMessage;
457
+ const stopReason = lastAssistant && "stopReason" in lastAssistant && typeof lastAssistant.stopReason === "string" && lastAssistant.stopReason.trim()
458
+ ? lastAssistant.stopReason.trim()
459
+ : undefined;
460
+ return stopReason && stopReason !== "stop"
461
+ ? `Subagent produced no output after terminal assistant stopReason "${stopReason}".`
462
+ : "Subagent produced no output (possible model cold-start or empty response).";
463
+ }
464
+
485
465
  /**
486
466
  * Detect errors in subagent execution from messages (only errors with no subsequent success)
487
467
  */
@@ -6,7 +6,7 @@ import {
6
6
  type SubagentDelegationUpdate,
7
7
  type SubagentDelegationValue,
8
8
  } from "../api/delegation.ts";
9
- import type { AcceptanceInput, AgentContract, EffectsProjection, ExecutionProjection, JsonSchemaObject, ReviewProjection, ToolBudgetConfig, TurnBudgetConfig, Usage } from "../shared/types.ts";
9
+ import type { AcceptanceInput, AgentContract, EffectsProjection, ExecutionProjection, JsonSchemaObject, ReviewProjection, ToolBudgetConfig, Usage } from "../shared/types.ts";
10
10
  import { cloneJsonWithinByteLimit } from "./delegation-json.ts";
11
11
 
12
12
  export interface PromptTemplateDelegationRequest {
@@ -82,7 +82,6 @@ export interface PromptTemplateBridgeResult {
82
82
  interrupted?: boolean;
83
83
  timedOut?: boolean;
84
84
  stopped?: boolean;
85
- turnBudgetExceeded?: boolean;
86
85
  toolBudgetBlocked?: boolean;
87
86
  structuredOutputFailed?: boolean;
88
87
  savedOutputPath?: string;
@@ -120,9 +119,6 @@ export interface DelegatedSubagentExecutionParams {
120
119
  model?: string;
121
120
  cwd: string;
122
121
  timeoutMs?: number;
123
- turnBudget?: TurnBudgetConfig;
124
- /** Internal-only strict turn-boundary enforcement for structured foreground delegation. */
125
- enforceHardTurnLimit?: boolean;
126
122
  toolBudget?: ToolBudgetConfig;
127
123
  skill?: string | string[] | boolean;
128
124
  output?: string | boolean;
@@ -294,8 +290,6 @@ export function toSubagentDelegationExecutionParams(request: SubagentDelegationR
294
290
  cwd: request.cwd,
295
291
  model: request.model,
296
292
  timeoutMs: request.timeoutMs,
297
- turnBudget: request.turnBudget,
298
- enforceHardTurnLimit: true,
299
293
  toolBudget: request.toolBudget,
300
294
  skill: request.skill,
301
295
  ...(request.result.kind === "structured" ? { outputSchema: request.result.schema } : {}),
@@ -341,7 +335,6 @@ function resolveSubagentDelegationStatus(
341
335
  if (!child) return "failed";
342
336
  if (result.details?.timedOut || child.timedOut) return "timed_out";
343
337
  if (child?.structuredOutputFailed) return "structured_output_failed";
344
- if (child?.turnBudgetExceeded) return "turn_budget_exhausted";
345
338
  if (child?.toolBudgetBlocked) return "tool_budget_exhausted";
346
339
  if (child?.acceptance?.status === "rejected" && child.acceptance.explicit) return "acceptance_failed";
347
340
  if (result.details?.stopped || child?.stopped || child?.interrupted) return "interrupted";
@@ -2,7 +2,6 @@ import {
2
2
  type SubagentDelegationRequest,
3
3
  } from "../api/delegation.ts";
4
4
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
5
- import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
6
5
  import { cloneJsonWithinByteLimit } from "./delegation-json.ts";
7
6
 
8
7
  export type SubagentDelegationParseResult =
@@ -20,7 +19,6 @@ const supportedFields = new Set([
20
19
  "model",
21
20
  "thinking",
22
21
  "timeoutMs",
23
- "turnBudget",
24
22
  "toolBudget",
25
23
  "skill",
26
24
  "artifacts",
@@ -80,8 +78,6 @@ export function parseSubagentDelegationRequest(data: unknown): SubagentDelegatio
80
78
  if (timeoutMs !== undefined && timeoutMs > 2_147_483_647) {
81
79
  return { ok: false, ...identity, error: "timeoutMs must be <= 2147483647." };
82
80
  }
83
- const turnBudget = resolveTurnBudgetConfig(value.turnBudget);
84
- if (turnBudget.error) return { ok: false, ...identity, error: turnBudget.error };
85
81
  if (value.toolBudget && typeof value.toolBudget === "object" && !Array.isArray(value.toolBudget)) {
86
82
  const unsupportedToolBudgetField = Object.keys(value.toolBudget).find((key) => key !== "soft" && key !== "hard" && key !== "block");
87
83
  if (unsupportedToolBudgetField) {
@@ -39,7 +39,6 @@ interface EventBus {
39
39
 
40
40
  interface SlashBridgeOptions {
41
41
  events: EventBus;
42
- asyncByDefault: boolean;
43
42
  getContext: () => ExtensionContext | null;
44
43
  execute: (
45
44
  id: string,
@@ -80,7 +79,7 @@ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
80
79
  const request = data as Partial<SlashSubagentRequest>;
81
80
  if (typeof request.requestId !== "string" || !request.params) return;
82
81
  const { requestId } = request as SlashSubagentRequest;
83
- const normalized = normalizePublicSubagentExecution((request as SlashSubagentRequest).params, { asyncByDefault: options.asyncByDefault });
82
+ const normalized = normalizePublicSubagentExecution((request as SlashSubagentRequest).params);
84
83
  if (!normalized.ok) {
85
84
  options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
86
85
  requestId,