pi-subagents 0.80.0 → 0.81.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 (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-26C3WLYQ.js +8388 -0
  7. package/bundle/chunk-2XYWN7QJ.js +128 -0
  8. package/bundle/chunk-2ZSNH26H.js +4164 -0
  9. package/bundle/chunk-3DC5KBVP.js +235 -0
  10. package/bundle/chunk-3IF2PGW7.js +380 -0
  11. package/bundle/chunk-4QFZZV3O.js +57 -0
  12. package/bundle/chunk-563BHAZ5.js +1365 -0
  13. package/bundle/chunk-5LFLVSTM.js +260 -0
  14. package/bundle/chunk-62OCY2S4.js +14 -0
  15. package/bundle/chunk-74RT5YQE.js +326 -0
  16. package/bundle/chunk-7F6RKT6W.js +132 -0
  17. package/bundle/chunk-7J4ZXMY7.js +18 -0
  18. package/bundle/chunk-ARRKBB2B.js +179 -0
  19. package/bundle/chunk-BJN5LUQY.js +458 -0
  20. package/bundle/chunk-BPS2AGBZ.js +5421 -0
  21. package/bundle/chunk-CEKS3TFO.js +1337 -0
  22. package/bundle/chunk-CTPAE36Y.js +94 -0
  23. package/bundle/chunk-D4EHT5SJ.js +14 -0
  24. package/bundle/chunk-EYNJ6VL6.js +526 -0
  25. package/bundle/chunk-G5DKFLLI.js +1185 -0
  26. package/bundle/chunk-KSWQVGDW.js +1027 -0
  27. package/bundle/chunk-L5ZHE4L4.js +946 -0
  28. package/bundle/chunk-LSRUTHT7.js +57 -0
  29. package/bundle/chunk-N2BN7BF5.js +159 -0
  30. package/bundle/chunk-NF7IEKBH.js +490 -0
  31. package/bundle/chunk-NIXWDZZV.js +9 -0
  32. package/bundle/chunk-NNLASTQE.js +4082 -0
  33. package/bundle/chunk-TUWLXTAM.js +935 -0
  34. package/bundle/chunk-UKG4V45O.js +228 -0
  35. package/bundle/chunk-V2JWDB5U.js +753 -0
  36. package/bundle/chunk-V3BIUPLU.js +9163 -0
  37. package/bundle/chunk-V6MR5TOQ.js +270 -0
  38. package/bundle/chunk-VJHOELQP.js +8245 -0
  39. package/bundle/chunk-WSBNENFS.js +732 -0
  40. package/bundle/chunk-XMFLCKMQ.js +16664 -0
  41. package/bundle/chunk-YFCK7H2S.js +129 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZBC44JJ3.js +8606 -0
  45. package/bundle/chunk-ZP3HODFH.js +175 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/extension-api.md +27 -0
  59. package/docs/observability.md +6 -2
  60. package/package.json +18 -18
  61. package/src/agents/frontmatter.d.ts +2 -0
  62. package/src/agents/frontmatter.js +9 -2
  63. package/src/api/delegation.d.ts +20 -0
  64. package/src/extension/index.js +27 -7
  65. package/src/runs/background/run-child-session.js +18 -0
  66. package/src/runs/background/stale-run-reconciler.js +2 -0
  67. package/src/runs/foreground/execution.js +39 -2
  68. package/src/runs/shared/structured-output.d.ts +1 -0
  69. package/src/runs/shared/structured-output.js +1 -0
  70. package/src/shared/accessible-dir.js +4 -1
  71. package/src/shared/provider-diagnostics.d.ts +9 -0
  72. package/src/shared/provider-diagnostics.js +75 -0
  73. package/src/shared/types.d.ts +3 -0
  74. package/src/slash/delegation-adapters.d.ts +3 -1
  75. package/src/slash/delegation-adapters.js +7 -0
  76. package/src/slash/prompt-template-bridge.js +3 -1
  77. package/src/slash/subagents-admin.d.ts +1 -0
  78. package/src/slash/subagents-admin.js +1 -1
  79. package/src/tui/fleet-layout.d.ts +20 -0
  80. package/src/tui/fleet-layout.js +17 -0
  81. package/src/tui/fleet-viewport.d.ts +12 -0
  82. package/src/tui/fleet-viewport.js +35 -0
  83. package/src/tui/fleet.d.ts +5 -6
  84. package/src/tui/fleet.js +69 -51
  85. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  86. package/src/workflows/structured-workflow-scripts.js +5 -4
  87. package/src/workflows/workflow-resources.js +2 -2
@@ -39,6 +39,8 @@ function foldBlock(block) {
39
39
  /**
40
40
  * Normalize a simple-scalar frontmatter list from comma-separated or block-list syntax.
41
41
  * Only the standard `- item` marker is removed; ordinary hyphenated values stay intact.
42
+ * A trailing ` #...` comment is dropped from unquoted `- item` lines. Single-line values are left
43
+ * alone because parseFrontmatter has already removed their quotes, so `"notes #1.md"` must survive.
42
44
  */
43
45
  export function parseFrontmatterList(raw) {
44
46
  if (raw === undefined)
@@ -47,8 +49,9 @@ export function parseFrontmatterList(raw) {
47
49
  .split("\n")
48
50
  .flatMap((line) => {
49
51
  const value = line.trim();
50
- const listItem = value.match(/^-\s+(.+)$/);
51
- return (listItem?.[1] ?? value).split(",");
52
+ const listItem = value.match(/^-\s+(.+)$/)?.[1];
53
+ const item = listItem === undefined || /^["']/.test(listItem) ? listItem ?? value : listItem.replace(/(?:^|\s)#.*$/, "");
54
+ return item.split(",");
52
55
  })
53
56
  .map((value) => value.trim())
54
57
  .filter(Boolean);
@@ -80,6 +83,10 @@ export function parseFrontmatter(content) {
80
83
  for (const line of lines) {
81
84
  const indent = line.search(/\S|$/); // position of first non-whitespace char
82
85
  const trimmed = line.trim();
86
+ // Comment lines are content only inside a | or > block scalar. Elsewhere they must
87
+ // neither end a pending list block nor become part of its value.
88
+ if (trimmed.startsWith("#") && !currentFolded && !currentLiteral)
89
+ continue;
83
90
  if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || ((currentFolded || currentLiteral) && trimmed === ""))) {
84
91
  // This line is part of the current block value
85
92
  currentBlockLines.push(line);
@@ -53,7 +53,26 @@ export interface SubagentDelegationUpdateUsage {
53
53
  cacheWrite: number;
54
54
  turns: number;
55
55
  }
56
+ /** Observed assistant-request errors and Pi retries, not provider-internal HTTP attempts. */
57
+ export interface SubagentDelegationProviderDiagnostics {
58
+ errorCount: number;
59
+ lastError?: {
60
+ kind: "rate_limited" | "transient_other" | "unknown";
61
+ /** Fixed safe description; never the raw provider error/body. */
62
+ message: string;
63
+ at: number;
64
+ };
65
+ retry?: {
66
+ phase: "backoff" | "in_flight";
67
+ attempt: number;
68
+ maxAttempts: number;
69
+ delayMs: number;
70
+ /** Observation time of auto_retry_start; delayMs is Pi's planned delay. */
71
+ scheduledAt: number;
72
+ };
73
+ }
56
74
  export interface SubagentDelegationUpdate extends SubagentDelegationStarted {
75
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
57
76
  runId?: string;
58
77
  currentTool?: string;
59
78
  currentToolArgs?: string;
@@ -88,6 +107,7 @@ export interface SubagentDelegationUsage {
88
107
  durationMs: number;
89
108
  }
90
109
  export interface SubagentDelegationTerminalResponse extends SubagentDelegationStarted {
110
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
91
111
  status: Exclude<SubagentDelegationStatus, "invalid_request">;
92
112
  error?: string;
93
113
  runId?: string;
@@ -43,6 +43,7 @@ import { createResultWatcher } from "../runs/background/result-watcher.js";
43
43
  import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.js";
44
44
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.js";
45
45
  import { registerSlashCommands } from "../slash/slash-commands.js";
46
+ import { ADMIN_MESSAGE_TYPE } from "../slash/subagents-admin.js";
46
47
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.js";
47
48
  import { registerMainWatchdog } from "../watchdog/register-main.js";
48
49
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.js";
@@ -557,13 +558,32 @@ export default function registerSubagentExtension(pi) {
557
558
  return undefined;
558
559
  return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, foregroundDetachShortcut, () => state.lastUiContext?.ui.theme ?? theme);
559
560
  });
560
- // Keep view-only reports in the transcript/RPC stream, but out of model context.
561
- // SLASH_RESULT_TYPE carries delegated work and must remain model-visible.
562
- pi.on("context", (event) => ({
563
- messages: event.messages.filter((message) => message.role !== "custom"
564
- || message.customType !== SLASH_TEXT_RESULT_TYPE
565
- || message.details?.viewOnly !== true),
566
- }));
561
+ // Pi sends custom messages to the model as plain user messages, so every model-visible
562
+ // pi-subagents message is wrapped in a marker naming its source. The marker depends only on
563
+ // the message, so history renders the same bytes every turn and cached prefixes stay valid.
564
+ const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
565
+ pi.on("context", (event) => {
566
+ const messages = [];
567
+ for (const message of event.messages) {
568
+ if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
569
+ messages.push(message);
570
+ continue;
571
+ }
572
+ // Keep view-only reports in the transcript/RPC stream, but out of model context.
573
+ // SLASH_RESULT_TYPE carries delegated work and must remain model-visible.
574
+ if (message.customType === SLASH_TEXT_RESULT_TYPE && message.details?.viewOnly === true)
575
+ continue;
576
+ const open = `<pi_subagents_notice type="${message.customType}" origin="automated message from the pi-subagents extension, not typed by the user">`;
577
+ const close = "</pi_subagents_notice>";
578
+ messages.push({
579
+ ...message,
580
+ content: typeof message.content === "string"
581
+ ? `${open}\n${message.content}\n${close}`
582
+ : [{ type: "text", text: `${open}\n` }, ...message.content, { type: "text", text: `\n${close}` }],
583
+ });
584
+ }
585
+ return { messages };
586
+ });
567
587
  pi.registerMessageRenderer(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
568
588
  const content = typeof message.content === "string"
569
589
  ? message.content
@@ -10,6 +10,8 @@ import { createReportedChildSessionInput } from "../shared/child-launch.js";
10
10
  import { createPartialOutputTracker, formatPartialOutput } from "../shared/partial-output.js";
11
11
  import { childSessionHasQueuedMessages, projectChildSessionEventForJson } from "../shared/child-session.js";
12
12
  import { reconcileAttemptUsage } from "../shared/usage-reconciliation.js";
13
+ import { isOrdinaryToolForMidToolExit } from "../shared/process-signal.js";
14
+ import { STRUCTURED_OUTPUT_FOLLOW_UP } from "../shared/structured-output.js";
13
15
  import { formatSteerMessage } from "../shared/subagent-prompt-runtime.js";
14
16
  import { takeMatchingAcceptedSteer, unconsumedSteerReason } from "./steering.js";
15
17
  /** Events the child emits while the model streams; not persisted into the diagnostic log. */
@@ -616,6 +618,22 @@ export function runChildSession(input) {
616
618
  abortChild();
617
619
  messageBaseline = created.messages.length;
618
620
  await created.prompt(input.prompt);
621
+ // A structured child that finished normally without calling structured_output gets one more turn (#2997).
622
+ if (input.launch.session.runtime.structuredOutput
623
+ && !structuredOutputToolInvoked
624
+ && !settled
625
+ && !created.shutDown
626
+ && !interrupted
627
+ && !timedOut
628
+ && !stopped
629
+ && !forcedTermination
630
+ && !error
631
+ && !assistantError
632
+ && !input.launch.capture.toolDiagnostic()
633
+ && !(currentTool && isOrdinaryToolForMidToolExit(currentTool))) {
634
+ applyChildLifecycle("cancel-drain");
635
+ await created.prompt(STRUCTURED_OUTPUT_FOLLOW_UP);
636
+ }
619
637
  promptSettled = true;
620
638
  settle(undefined);
621
639
  }
@@ -273,6 +273,8 @@ function buildFailedRepair(status, asyncDir, now, reason) {
273
273
  agent: resultAgent,
274
274
  mode: status.mode,
275
275
  ...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
276
+ ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
277
+ ...(status.workflowKey ? { workflowKey: status.workflowKey } : {}),
276
278
  success: false,
277
279
  state: "failed",
278
280
  summary: message,
@@ -24,7 +24,7 @@ import { deriveChildSessionName } from "../../shared/child-session-name.js";
24
24
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.js";
25
25
  import { qualifyModelWithProvider, resolveEffectiveThinking } from "../../shared/model-info.js";
26
26
  import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../shared/thinking-ceiling.js";
27
- import { formatStructuredOutputRejectionError, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.js";
27
+ import { formatStructuredOutputRejectionError, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, STRUCTURED_OUTPUT_FOLLOW_UP } from "../shared/structured-output.js";
28
28
  import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.js";
29
29
  import { formatChildToolDiagnostic } from "../shared/tool-availability.js";
30
30
  import { formatChildModelResolutionDiagnostic, formatInheritedProviderDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.js";
@@ -45,6 +45,7 @@ import { acceptChildWatchdogEvent, applyChildWatchdogMessage, childWatchdogIsAct
45
45
  import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../shared/child-launch.js";
46
46
  import { childSessionFactory, childSessionHasQueuedMessages, projectChildSessionEventForJson } from "../shared/child-session.js";
47
47
  import { reconcileAttemptUsage } from "../shared/usage-reconciliation.js";
48
+ import { clearProviderRetry, observeProviderDiagnostics, snapshotProviderDiagnostics } from "../../shared/provider-diagnostics.js";
48
49
  const artifactOutputByResult = new WeakMap();
49
50
  const acceptanceOutputByResult = new WeakMap();
50
51
  function emptyUsage() {
@@ -165,6 +166,7 @@ function snapshotProgress(progress) {
165
166
  ...progress,
166
167
  task: PROMPT_REDACTED,
167
168
  skills: progress.skills ? [...progress.skills] : undefined,
169
+ providerDiagnostics: snapshotProviderDiagnostics(progress.providerDiagnostics),
168
170
  recentTools: boundStreamedRecentTools(progress.recentTools),
169
171
  recentOutput: boundStreamedRecentOutput(progress.recentOutput),
170
172
  };
@@ -175,6 +177,7 @@ function snapshotResult(result, progress) {
175
177
  task: PROMPT_REDACTED,
176
178
  messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
177
179
  usage: { ...result.usage },
180
+ providerDiagnostics: snapshotProviderDiagnostics(result.providerDiagnostics),
178
181
  skills: result.skills ? [...result.skills] : undefined,
179
182
  controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
180
183
  progress,
@@ -198,6 +201,7 @@ function snapshotStreamResult(result, progress) {
198
201
  }
199
202
  function captureStructuredDelegationProgressState(progress, result) {
200
203
  return {
204
+ providerDiagnostics: progress.providerDiagnostics,
201
205
  currentTool: progress.currentTool,
202
206
  currentToolArgs: progress.currentToolArgs,
203
207
  recentOutput: [...progress.recentOutput],
@@ -210,7 +214,9 @@ function captureStructuredDelegationProgressState(progress, result) {
210
214
  };
211
215
  }
212
216
  function structuredDelegationProgressChanged(previous, progress, result) {
213
- if (previous.currentTool !== progress.currentTool
217
+ // Observations replace internal diagnostics; no transcript walk or serialization on heartbeats.
218
+ if (previous.providerDiagnostics !== progress.providerDiagnostics
219
+ || previous.currentTool !== progress.currentTool
214
220
  || previous.currentToolArgs !== progress.currentToolArgs
215
221
  || previous.activityState !== progress.activityState
216
222
  || previous.model !== (progress.model ?? result.model)
@@ -352,6 +358,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
352
358
  exitCode: 0,
353
359
  outputState: "absent",
354
360
  messages: [],
361
+ providerDiagnostics: shared.providerDiagnostics,
355
362
  usage: emptyUsage(),
356
363
  model: modelArg,
357
364
  ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
@@ -383,6 +390,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
383
390
  let cacheReadUsageComplete = true;
384
391
  let cacheWriteUsageComplete = true;
385
392
  const progress = {
393
+ providerDiagnostics: result.providerDiagnostics,
386
394
  index: options.index ?? 0,
387
395
  agent: agent.name,
388
396
  ...(childSessionName ? { sessionName: childSessionName } : {}),
@@ -972,6 +980,14 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
972
980
  return;
973
981
  }
974
982
  const now = Date.now();
983
+ const providerDiagnostics = observeProviderDiagnostics(result.providerDiagnostics, evt, now);
984
+ if (providerDiagnostics !== result.providerDiagnostics) {
985
+ result.providerDiagnostics = providerDiagnostics;
986
+ progress.providerDiagnostics = providerDiagnostics;
987
+ // message_end already emits synchronously below; retry-only transitions need their own update.
988
+ if (evt.type !== "message_end")
989
+ fireUpdate();
990
+ }
975
991
  progress.durationMs = now - startTime;
976
992
  progress.lastActivityAt = now;
977
993
  updateActivityState(now);
@@ -1396,6 +1412,24 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1396
1412
  // A runtime replacement can stop a child that detached while it was still being created.
1397
1413
  if (!hostStopReason)
1398
1414
  await created.prompt(`Task: ${task}`);
1415
+ // A structured child that finished normally without calling structured_output gets one more turn (#2997).
1416
+ if (options.structuredOutput
1417
+ && !structuredOutputToolInvoked
1418
+ && !sessionSettled
1419
+ && !lifecycleFinished
1420
+ && !hostStopReason
1421
+ && !created.shutDown
1422
+ && !abortedBySignal
1423
+ && !interruptedByControl
1424
+ && !result.timedOut
1425
+ && !forcedTermination
1426
+ && !result.error
1427
+ && !assistantError
1428
+ && !capture.toolDiagnostic()
1429
+ && !(progress.currentTool && isOrdinaryToolForMidToolExit(progress.currentTool))) {
1430
+ applyChildLifecycle("cancel-drain");
1431
+ await created.prompt(STRUCTURED_OUTPUT_FOLLOW_UP);
1432
+ }
1399
1433
  settle(undefined);
1400
1434
  }
1401
1435
  catch (error) {
@@ -1404,6 +1438,8 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1404
1438
  })();
1405
1439
  });
1406
1440
  result.exitCode = exitCode;
1441
+ result.providerDiagnostics = clearProviderRetry(result.providerDiagnostics);
1442
+ progress.providerDiagnostics = result.providerDiagnostics;
1407
1443
  if (afterCompactionSettlement) {
1408
1444
  result[AFTER_COMPACTION_SETTLEMENT] = true;
1409
1445
  }
@@ -1855,6 +1891,7 @@ async function runSyncCompletionInner(runtimeCwd, agents, agentName, task, optio
1855
1891
  orcaProgressTab,
1856
1892
  launchWarnings,
1857
1893
  verifyModel,
1894
+ providerDiagnostics: lastResult?.providerDiagnostics,
1858
1895
  });
1859
1896
  lastResult = attemptResult;
1860
1897
  sumUsage(aggregateUsage, attemptResult.usage);
@@ -2,6 +2,7 @@ import type { Message } from "@earendil-works/pi-ai";
2
2
  import type { JsonSchemaObject } from "../../shared/types.ts";
3
3
  import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
4
4
  export declare const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
5
+ export declare const STRUCTURED_OUTPUT_FOLLOW_UP = "You ended without calling `structured_output`. Call it now with your result. Do not repeat the work.";
5
6
  export declare const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
6
7
  export declare const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
7
8
  export declare const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
@@ -14,6 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.js";
15
15
  import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "./pi-spawn.js";
16
16
  export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
17
+ export const STRUCTURED_OUTPUT_FOLLOW_UP = "You ended without calling `structured_output`. Call it now with your result. Do not repeat the work.";
17
18
  export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
18
19
  export const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
19
20
  export const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
@@ -4,7 +4,10 @@ export function ensureAccessibleDir(dirPath, options = {}) {
4
4
  const fsImpl = options.fs ?? fs;
5
5
  const pid = options.pid ?? process.pid;
6
6
  const retryDirectoryErrors = options.retryDirectoryErrors ?? process.platform === "win32";
7
- const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];
7
+ // Runs synchronously at extension registration. The full ladder would block
8
+ // every Pi start for ~7.9 s on a directory with broken ACLs before reaching
9
+ // the pid fallback, so only its first steps (85 ms per operation) are used here.
10
+ const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS.slice(0, 3) : [];
8
11
  const wait = options.wait ?? waitForFileSystemRetry;
9
12
  const mkdirWithRetry = (target) => {
10
13
  runFileSystemOperationWithRetry(() => {
@@ -0,0 +1,9 @@
1
+ import type { SubagentDelegationProviderDiagnostics } from "../api/delegation.ts";
2
+ import type { ChildSessionEvent } from "../runs/shared/child-session.ts";
3
+ type Diagnostics = SubagentDelegationProviderDiagnostics;
4
+ /** Immutable, bounded observations only. This never decides whether to retry. */
5
+ export declare function observeProviderDiagnostics(current: Diagnostics | undefined, event: ChildSessionEvent, at: number): Diagnostics | undefined;
6
+ export declare function clearProviderRetry(current: Diagnostics | undefined): Diagnostics | undefined;
7
+ export declare function snapshotProviderDiagnostics(current: Diagnostics | undefined): Diagnostics | undefined;
8
+ export declare function sameProviderDiagnostics(left: Diagnostics | undefined, right: Diagnostics | undefined): boolean;
9
+ export {};
@@ -0,0 +1,75 @@
1
+ import { isRetryableAssistantError } from "@earendil-works/pi-ai";
2
+ // Account exhaustion can also carry 429. Do not present it as transient throttling.
3
+ // Pi's classifier supplies its own provider-specific quota/billing exclusions too.
4
+ const ACCOUNT_LIMIT = /quota|billing|out of budget|available balance|(?:account|subscription|credits?|balance|usage)[\s_-]*(?:(?:is|are|has been|have been)[\s_-]*)?(?:limit|exhaust|deplet|exceed)|(?:insufficient|exhausted|depleted|low|no(?:[\s_-]+remaining)?)[\s_-]*(?:credits?|balance|fund)/i;
5
+ const RATE_LIMIT = /\brate[\s_-]*limit(?:ed|ing|s|_(?:exceeded|error))?\b|\btoo[\s_-]+many[\s_-]+requests\b|\b(?:HTTP(?:\/\d(?:\.\d)?)?\s*[:=]?\s*429|(?:status|statusCode|status_code)["']?\s*[:=]?\s*429)\b|^\s*429\b/i;
6
+ function classifyError(message) {
7
+ const text = message.errorMessage;
8
+ if (!text || ACCOUNT_LIMIT.test(text) || !isRetryableAssistantError(message))
9
+ return "unknown";
10
+ return RATE_LIMIT.test(text) ? "rate_limited" : "transient_other";
11
+ }
12
+ const SAFE_ERROR_MESSAGES = {
13
+ rate_limited: "Assistant request was rate limited.",
14
+ transient_other: "Assistant request encountered a transient provider or transport error.",
15
+ unknown: "Assistant request failed.",
16
+ };
17
+ /** Immutable, bounded observations only. This never decides whether to retry. */
18
+ export function observeProviderDiagnostics(current, event, at) {
19
+ if (event.type === "message_end") {
20
+ const message = event.message;
21
+ if (message?.role === "assistant" && message.stopReason === "error") {
22
+ const kind = classifyError(message);
23
+ return {
24
+ ...current,
25
+ errorCount: (current?.errorCount ?? 0) + 1,
26
+ lastError: { kind, message: SAFE_ERROR_MESSAGES[kind], at },
27
+ };
28
+ }
29
+ }
30
+ else if (event.type === "auto_retry_start") {
31
+ const { attempt, maxAttempts, delayMs } = event;
32
+ if (typeof attempt === "number" && Number.isSafeInteger(attempt) && attempt > 0
33
+ && typeof maxAttempts === "number" && Number.isSafeInteger(maxAttempts) && maxAttempts >= attempt
34
+ && typeof delayMs === "number" && Number.isFinite(delayMs) && delayMs >= 0) {
35
+ return { ...current, errorCount: current?.errorCount ?? 0, retry: { phase: "backoff", attempt, maxAttempts, delayMs, scheduledAt: at } };
36
+ }
37
+ }
38
+ else if ((event.type === "agent_start" || event.type === "turn_start") && current?.retry?.phase === "backoff") {
39
+ return { ...current, retry: { ...current.retry, phase: "in_flight" } };
40
+ }
41
+ else if (event.type === "auto_retry_end" || event.type === "agent_settled") {
42
+ return clearProviderRetry(current);
43
+ }
44
+ return current;
45
+ }
46
+ export function clearProviderRetry(current) {
47
+ if (!current?.retry)
48
+ return current;
49
+ const { retry: _retry, ...history } = current;
50
+ return history;
51
+ }
52
+ export function snapshotProviderDiagnostics(current) {
53
+ if (!current)
54
+ return undefined;
55
+ return {
56
+ errorCount: current.errorCount,
57
+ ...(current.lastError ? { lastError: { ...current.lastError } } : {}),
58
+ ...(current.retry ? { retry: { ...current.retry } } : {}),
59
+ };
60
+ }
61
+ export function sameProviderDiagnostics(left, right) {
62
+ if (left === right)
63
+ return true;
64
+ if (!left || !right)
65
+ return false;
66
+ return left.errorCount === right.errorCount
67
+ && left.lastError?.kind === right.lastError?.kind
68
+ && left.lastError?.message === right.lastError?.message
69
+ && left.lastError?.at === right.lastError?.at
70
+ && left.retry?.phase === right.retry?.phase
71
+ && left.retry?.attempt === right.retry?.attempt
72
+ && left.retry?.maxAttempts === right.retry?.maxAttempts
73
+ && left.retry?.delayMs === right.retry?.delayMs
74
+ && left.retry?.scheduledAt === right.retry?.scheduledAt;
75
+ }
@@ -2,6 +2,7 @@
2
2
  * Type definitions for the subagent extension
3
3
  */
4
4
  import type { Message } from "@earendil-works/pi-ai";
5
+ import type { SubagentDelegationProviderDiagnostics } from "../api/delegation.ts";
5
6
  import type { AgentConfig } from "../agents/agents.ts";
6
7
  import type { FSWatcher } from "node:fs";
7
8
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -864,6 +865,7 @@ export interface ChildWatchdogProgress {
864
865
  warnings?: ChildWatchdogWarningSummary[];
865
866
  }
866
867
  export interface AgentProgress {
868
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
867
869
  index: number;
868
870
  agent: string;
869
871
  /** Human-readable display name for the child's session, when derived at launch. */
@@ -1159,6 +1161,7 @@ export interface SingleResult {
1159
1161
  toolBudget?: ToolBudgetState;
1160
1162
  toolBudgetBlocked?: boolean;
1161
1163
  messages?: Message[];
1164
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
1162
1165
  usage: Usage;
1163
1166
  model?: string;
1164
1167
  /** Authoritative before/after Git evidence captured by a pane-native remote machine. */
@@ -1,4 +1,4 @@
1
- import { type SubagentDelegationRequest, type SubagentDelegationResponse, type SubagentDelegationThinking, type SubagentDelegationUpdate, type SubagentDelegationUpdateUsage } from "../api/delegation.ts";
1
+ import { type SubagentDelegationProviderDiagnostics, type SubagentDelegationRequest, type SubagentDelegationResponse, type SubagentDelegationThinking, type SubagentDelegationUpdate, type SubagentDelegationUpdateUsage } from "../api/delegation.ts";
2
2
  import type { AcceptanceInput, AgentContract, EffectsProjection, ExecutionProjection, IntercomBridgeConfig, JsonSchemaObject, ReviewProjection, ToolBudgetConfig, Usage } from "../shared/types.ts";
3
3
  export interface PromptTemplateDelegationRequest {
4
4
  requestId: string;
@@ -88,6 +88,7 @@ export interface PromptTemplateBridgeResult {
88
88
  review?: ReviewProjection;
89
89
  effects?: EffectsProjection;
90
90
  usage?: Usage;
91
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
91
92
  progressSummary?: {
92
93
  toolCount?: number;
93
94
  durationMs?: number;
@@ -116,6 +117,7 @@ export interface PromptTemplateBridgeResult {
116
117
  cacheRead?: number;
117
118
  cacheWrite?: number;
118
119
  turnCount?: number;
120
+ providerDiagnostics?: SubagentDelegationProviderDiagnostics;
119
121
  }>;
120
122
  };
121
123
  }
@@ -1,4 +1,5 @@
1
1
  import { cloneJsonWithinByteLimit } from "./delegation-json.js";
2
+ import { clearProviderRetry, snapshotProviderDiagnostics } from "../shared/provider-diagnostics.js";
2
3
  export function parsePromptTemplateRequest(data) {
3
4
  if (!data || typeof data !== "object")
4
5
  return undefined;
@@ -207,6 +208,9 @@ export function toSubagentDelegationUpdate(request, result) {
207
208
  ...(typeof legacy.durationMs === "number" ? { durationMs: legacy.durationMs } : {}),
208
209
  ...(typeof legacy.tokens === "number" ? { tokens: legacy.tokens } : {}),
209
210
  ...(legacy.usage ? { usage: legacy.usage } : {}),
211
+ ...(result.details?.progress?.[0]?.providerDiagnostics ? {
212
+ providerDiagnostics: snapshotProviderDiagnostics(result.details.progress[0].providerDiagnostics),
213
+ } : {}),
210
214
  };
211
215
  }
212
216
  function resolveSubagentDelegationStatus(result, aborted) {
@@ -282,6 +286,9 @@ export function toSubagentDelegationResponse(request, result, aborted) {
282
286
  ...(typeof child?.exitCode === "number" ? { exitCode: child.exitCode } : {}),
283
287
  ...(childLaunchContractDigest ? { launchContractDigest: childLaunchContractDigest } : {}),
284
288
  ...(projectedResult ? { result: projectedResult } : {}),
289
+ ...(child?.providerDiagnostics ? {
290
+ providerDiagnostics: snapshotProviderDiagnostics(clearProviderRetry(child.providerDiagnostics)),
291
+ } : {}),
285
292
  ...(usage ? {
286
293
  usage: {
287
294
  input: usage.input,
@@ -1,5 +1,6 @@
1
1
  import { SUBAGENT_DELEGATION_CANCEL_EVENT, SUBAGENT_DELEGATION_REQUEST_EVENT, SUBAGENT_DELEGATION_RESPONSE_EVENT, SUBAGENT_DELEGATION_STARTED_EVENT, SUBAGENT_DELEGATION_UPDATE_EVENT, } from "../api/delegation.js";
2
2
  import { parseSubagentDelegationRequest } from "./delegation-request.js";
3
+ import { sameProviderDiagnostics } from "../shared/provider-diagnostics.js";
3
4
  import { parsePromptTemplateRequest, toDelegationUpdate, toPromptTemplateResponse, toSubagentDelegationExecutionParams, toSubagentDelegationResponse, toSubagentDelegationUpdate, } from "./delegation-adapters.js";
4
5
  export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = SUBAGENT_DELEGATION_REQUEST_EVENT;
5
6
  export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = SUBAGENT_DELEGATION_STARTED_EVENT;
@@ -58,7 +59,8 @@ function sameStructuredDelegationUpdateProgress(left, right) {
58
59
  && left.model === right.model
59
60
  && left.toolCount === right.toolCount
60
61
  && left.tokens === right.tokens
61
- && sameUpdateUsage(left.usage, right.usage);
62
+ && sameUpdateUsage(left.usage, right.usage)
63
+ && sameProviderDiagnostics(left.providerDiagnostics, right.providerDiagnostics);
62
64
  }
63
65
  export function registerPromptTemplateDelegationBridge(options) {
64
66
  const legacyControllers = new Map();
@@ -1,2 +1,3 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export declare const ADMIN_MESSAGE_TYPE = "subagents-admin";
2
3
  export declare function openSubagentsAdmin(pi: ExtensionAPI, ctx: ExtensionContext, args?: string): Promise<void>;
@@ -7,7 +7,7 @@ import { editableAgentConfig, preservedAgentFrontmatterFields } from "../agents/
7
7
  import { findModelInfo, getSupportedThinkingLevels, toModelInfo } from "../shared/model-info.js";
8
8
  import { SelectorComponent } from "./selector.js";
9
9
  import { canRenderComponents } from "../shared/extension-context.js";
10
- const ADMIN_MESSAGE_TYPE = "subagents-admin";
10
+ export const ADMIN_MESSAGE_TYPE = "subagents-admin";
11
11
  const INHERIT_MODEL_CHOICE = "Default / inherit session model";
12
12
  const INHERIT_THINKING_CHOICE = "Default / inherit session thinking";
13
13
  function sourceRank(source) {
@@ -0,0 +1,20 @@
1
+ export interface FleetRect {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export interface FleetLayout {
8
+ width: number;
9
+ rows: number;
10
+ bodyHeight: number;
11
+ roster: FleetRect;
12
+ output: FleetRect;
13
+ rosterStart: number;
14
+ rosterKeys: string[];
15
+ }
16
+ /** Coordinates are overlay-local, matching Pi's normalized input contract. */
17
+ export declare function fleetLayout(width: number, rows: number, selected: number, items: readonly {
18
+ key: string;
19
+ }[]): FleetLayout | undefined;
20
+ export declare function fleetContains(rect: FleetRect, x: number, y: number): boolean;
@@ -0,0 +1,17 @@
1
+ /** Coordinates are overlay-local, matching Pi's normalized input contract. */
2
+ export function fleetLayout(width, rows, selected, items) {
3
+ if (width < 36)
4
+ return undefined;
5
+ const bodyHeight = Math.max(2, Math.floor(rows * 0.85) - 6);
6
+ const rosterWidth = Math.max(22, Math.min(46, Math.floor((width - 3) * 0.38)));
7
+ const rosterStart = Math.max(0, Math.min(selected - bodyHeight + 1, Math.max(0, items.length - bodyHeight)));
8
+ return {
9
+ width, rows, bodyHeight, rosterStart,
10
+ roster: { x: 1, y: 3, width: rosterWidth, height: bodyHeight },
11
+ output: { x: rosterWidth + 2, y: 3, width: Math.max(1, width - rosterWidth - 3), height: bodyHeight },
12
+ rosterKeys: items.slice(rosterStart, rosterStart + bodyHeight).map((item) => item.key),
13
+ };
14
+ }
15
+ export function fleetContains(rect, x, y) {
16
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
17
+ }
@@ -0,0 +1,12 @@
1
+ import { ScrollView } from "@earendil-works/pi-tui";
2
+ /** Overlay-local clipping: ordinary overlays do not enter Pi's scroll layout traversal. */
3
+ export declare class FleetViewport {
4
+ private readonly body;
5
+ readonly scroll: ScrollView;
6
+ private readonly requestRender;
7
+ private resetTopOnLayout;
8
+ constructor(requestRender: () => void);
9
+ render(lines: string[], width: number, height: number): string[];
10
+ resetTop(): void;
11
+ follow(): void;
12
+ }
@@ -0,0 +1,35 @@
1
+ import { ScrollView } from "@earendil-works/pi-tui";
2
+ /** Overlay-local clipping: ordinary overlays do not enter Pi's scroll layout traversal. */
3
+ export class FleetViewport {
4
+ body = {
5
+ lines: [],
6
+ render() { return this.lines; },
7
+ invalidate() { },
8
+ };
9
+ scroll = new ScrollView(this.body, { follow: "end", overscroll: "contain" });
10
+ requestRender;
11
+ resetTopOnLayout = false;
12
+ constructor(requestRender) {
13
+ this.requestRender = requestRender;
14
+ }
15
+ render(lines, width, height) {
16
+ this.body.lines = lines;
17
+ const rendered = this.scroll.render(width);
18
+ this.scroll.updateLayout(rendered.length, height, this.requestRender);
19
+ if (this.resetTopOnLayout) {
20
+ this.resetTopOnLayout = false;
21
+ this.scroll.scrollTo(0, { disableFollow: true });
22
+ }
23
+ return rendered.slice(this.scroll.scrollTop, this.scroll.scrollTop + this.scroll.viewportHeight);
24
+ }
25
+ resetTop() {
26
+ // Apply again after the new view's dimensions arrive: disableFollow suppresses following
27
+ // at the actual content end, which may differ from the previous view's end.
28
+ this.resetTopOnLayout = true;
29
+ this.scroll.scrollTo(0, { disableFollow: true });
30
+ }
31
+ follow() {
32
+ this.resetTopOnLayout = false;
33
+ this.scroll.scrollToEnd();
34
+ }
35
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
2
+ import { type Component, type MarkdownTheme, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
3
3
  import { type ExternalRun } from "../api/external-runs.ts";
4
4
  import { type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
5
5
  import { type AsyncRunSummary } from "../runs/background/async-status.ts";
@@ -111,11 +111,8 @@ export declare class SubagentFleetComponent implements Component {
111
111
  private snapshot;
112
112
  private selected;
113
113
  private selectedKey;
114
- private detailScroll;
115
- private detailAutoFollow;
116
- private detailLineCount;
117
- private detailViewportHeight;
118
- private bodyHeight;
114
+ private readonly viewport;
115
+ private renderedLayout;
119
116
  private expandedTools;
120
117
  private promptAuditOpen;
121
118
  private promptAuditView;
@@ -141,6 +138,7 @@ export declare class SubagentFleetComponent implements Component {
141
138
  private stopRefresh;
142
139
  private refresh;
143
140
  private moveSelection;
141
+ private selectItem;
144
142
  private selectedPromptAudit;
145
143
  private promptAuditItems;
146
144
  private selectedPromptText;
@@ -155,6 +153,7 @@ export declare class SubagentFleetComponent implements Component {
155
153
  private setActionNotice;
156
154
  private runAction;
157
155
  private scrollDetail;
156
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
158
157
  handleInput(data: string): void;
159
158
  private rosterLines;
160
159
  private renderedTranscript;