@tt-a1i/openpi 0.3.0 → 0.4.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 (61) hide show
  1. package/README.md +87 -24
  2. package/SETUP.md +3 -3
  3. package/extensions/ask-user/index.ts +30 -14
  4. package/extensions/background-terminals/src/prompt.ts +1 -1
  5. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  6. package/extensions/capabilities/index.ts +35 -44
  7. package/extensions/capabilities/src/ui.ts +93 -0
  8. package/extensions/file-mutation-display/index.ts +34 -76
  9. package/extensions/file-mutation-display/render.ts +387 -88
  10. package/extensions/file-search/index.ts +8 -7
  11. package/extensions/file-search/src/binaries.ts +18 -18
  12. package/extensions/git-info/src/changed-files-view.ts +47 -14
  13. package/extensions/git-read/index.ts +330 -0
  14. package/extensions/git-read/src/args.ts +171 -0
  15. package/extensions/git-read/src/process.ts +81 -0
  16. package/extensions/git-read/src/prompt.ts +56 -0
  17. package/extensions/sessions/index.ts +70 -55
  18. package/extensions/setup/index.ts +6 -6
  19. package/extensions/shared/activity-status.ts +6 -5
  20. package/extensions/shared/below-editor-navigation.ts +26 -0
  21. package/extensions/shared/capability-intent.ts +53 -0
  22. package/extensions/shared/child-session.ts +7 -1
  23. package/extensions/shared/result-budget.ts +134 -0
  24. package/extensions/shared/screen-chrome.ts +133 -0
  25. package/extensions/shared/setup-config.ts +24 -5
  26. package/extensions/shared/spinner.ts +28 -0
  27. package/extensions/shared/text-projection.ts +56 -0
  28. package/extensions/shared/tool-surface.ts +13 -6
  29. package/extensions/subagents/index.ts +216 -170
  30. package/extensions/subagents/navigation.ts +52 -23
  31. package/extensions/subagents/src/agent-types.ts +37 -15
  32. package/extensions/subagents/src/backends/stub.ts +7 -0
  33. package/extensions/subagents/src/id-sequence.ts +84 -0
  34. package/extensions/subagents/src/manager.ts +620 -537
  35. package/extensions/subagents/src/prompt.ts +153 -38
  36. package/extensions/subagents/src/result-artifact.ts +142 -0
  37. package/extensions/subagents/src/result-delivery.ts +50 -5
  38. package/extensions/subagents/src/runtime.ts +8 -5
  39. package/extensions/subagents/src/ui/takeover.ts +84 -109
  40. package/extensions/subagents/src/ui/transcript.ts +76 -42
  41. package/extensions/subagents/src/ui/wait-result.ts +1 -1
  42. package/extensions/tasks/ui.ts +79 -62
  43. package/extensions/ui-customization/footer.ts +7 -4
  44. package/extensions/user-input-fold/index.ts +185 -0
  45. package/extensions/workflows/artifacts.ts +35 -0
  46. package/extensions/workflows/controller.ts +14 -2
  47. package/extensions/workflows/coordinator.ts +64 -0
  48. package/extensions/workflows/dashboard.ts +353 -173
  49. package/extensions/workflows/handoff.ts +62 -20
  50. package/extensions/workflows/index.ts +647 -387
  51. package/extensions/workflows/model.ts +57 -15
  52. package/extensions/workflows/navigation.ts +33 -14
  53. package/extensions/workflows/prompt.ts +104 -8
  54. package/extensions/workflows/replay-safety.ts +16 -6
  55. package/extensions/workflows/result-delivery.ts +189 -0
  56. package/extensions/workflows/sandbox-child.cjs +11 -0
  57. package/package.json +1 -1
  58. package/skills/subagents/SKILL.md +2 -2
  59. package/skills/workflows/REFERENCE.md +7 -4
  60. package/skills/workflows/SKILL.md +53 -10
  61. package/extensions/subagents/src/format.ts +0 -48
@@ -25,7 +25,6 @@
25
25
 
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
- import { StringEnum } from "@earendil-works/pi-ai";
29
28
  import type {
30
29
  ExtensionAPI,
31
30
  ExtensionCommandContext,
@@ -37,7 +36,6 @@ import {
37
36
  DEFAULT_MAX_BYTES,
38
37
  DEFAULT_MAX_LINES,
39
38
  defineTool,
40
- formatSize,
41
39
  getAgentDir,
42
40
  getMarkdownTheme,
43
41
  keyHint,
@@ -46,41 +44,70 @@ import {
46
44
  import { Markdown, Text } from "@earendil-works/pi-tui";
47
45
  import { Type } from "typebox";
48
46
  import {
47
+ formatActivityStatus,
48
+ hasActivity,
49
+ unreadActivityCounts,
50
+ } from "../shared/activity-status.ts";
51
+ import {
52
+ BelowEditorNavigationEditor,
53
+ BelowEditorStripState,
54
+ } from "../shared/below-editor-navigation.ts";
55
+ import {
56
+ effectiveChildToolAllowlist,
57
+ resolveStandaloneChildProjectTrust,
58
+ } from "../shared/child-session.ts";
59
+ import { formatContextUtilization } from "../shared/context-utilization.ts";
60
+ import {
61
+ registerEditorLayer,
62
+ removeEditorLayer,
63
+ } from "../shared/editor-layers.ts";
64
+ import {
65
+ PLAN_MODE_CHANNEL,
66
+ type PlanModeState,
67
+ planModeAllowsDeclaredTools,
68
+ planModeChildTools,
69
+ } from "../shared/plan-mode-state.ts";
70
+ import { loadSetupConfig } from "../shared/setup-config.ts";
71
+ import {
72
+ OPENPI_TOOL_SURFACE,
73
+ patchOwnedTools,
74
+ } from "../shared/tool-surface.ts";
75
+ import {
76
+ createWorktree,
77
+ reclaimWorktree,
78
+ type Worktree,
79
+ } from "../shared/worktree.ts";
80
+ import {
81
+ normalizeSubagentTitle,
82
+ SubagentStripWidget,
83
+ selectSubagentStripEntry,
84
+ } from "./navigation.ts";
85
+ import {
86
+ type AgentType,
49
87
  formatAgentTypeDiagnostics,
50
88
  loadAgentTypes,
51
89
  roleModelForAgentType,
52
90
  selectSubagentModel,
53
- type AgentType,
54
91
  } from "./src/agent-types.ts";
55
92
  import { deriveBtwTitle, isModelVisible } from "./src/by-the-way.ts";
56
93
  import {
57
94
  BACKEND_NAMES,
58
95
  formatElapsed,
59
96
  latestText,
60
- REASONING_EFFORTS,
61
97
  type SubagentSnapshot,
62
98
  } from "./src/domain.ts";
63
99
  import {
64
- formatActivityStatus,
65
- hasActivity,
66
- unreadActivityCounts,
67
- } from "../shared/activity-status.ts";
68
- import {
69
- OPENPI_TOOL_SURFACE,
70
- patchOwnedTools,
71
- } from "../shared/tool-surface.ts";
72
- import {
73
- registerEditorLayer,
74
- removeEditorLayer,
75
- } from "../shared/editor-layers.ts";
76
- import { formatContextUtilization } from "./src/format.ts";
100
+ restoreSubagentIdCounters,
101
+ SUBAGENT_ID_WATERMARK_ENTRY_TYPE,
102
+ type SubagentIdCounters,
103
+ subagentIdWatermark,
104
+ } from "./src/id-sequence.ts";
77
105
  import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
78
106
  import {
79
107
  buildSubagentResultMessage,
80
- createAgentTypeParameterSchema,
81
108
  buildSubagentSendResult,
82
109
  buildSubagentSpawnResult,
83
- buildSubagentSpawnToolDescription,
110
+ createSubagentSpawnToolSurface,
84
111
  SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS,
85
112
  SUBAGENT_CANCEL_TOOL_DESCRIPTION,
86
113
  SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS,
@@ -88,45 +115,22 @@ import {
88
115
  SUBAGENT_LIST_TOOL_DESCRIPTION,
89
116
  SUBAGENT_SEND_PARAMETER_DESCRIPTIONS,
90
117
  SUBAGENT_SEND_TOOL_DESCRIPTION,
91
- SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS,
92
118
  SUBAGENT_SPAWN_PROMPT_GUIDELINES,
93
119
  SUBAGENT_SPAWN_PROMPT_SNIPPET,
94
- SUBAGENT_SPAWN_TOOL_DESCRIPTION,
95
120
  SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
96
121
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
97
122
  } from "./src/prompt.ts";
98
- import { createDeferredResultDelivery } from "./src/result-delivery.ts";
99
- import { resultDeliveryOptions } from "../background-terminals/src/result-delivery.ts";
100
- import {
101
- effectiveChildToolAllowlist,
102
- resolveStandaloneChildProjectTrust,
103
- } from "../shared/child-session.ts";
123
+ import { persistResultArtifact, projectResult } from "./src/result-artifact.ts";
104
124
  import {
105
- BelowEditorNavigationEditor,
106
- BelowEditorStripState,
107
- } from "../shared/below-editor-navigation.ts";
108
- import { loadSetupConfig } from "../shared/setup-config.ts";
109
- import {
110
- PLAN_MODE_CHANNEL,
111
- planModeAllowsDeclaredTools,
112
- planModeChildTools,
113
- type PlanModeState,
114
- } from "../shared/plan-mode-state.ts";
115
- import {
116
- createWorktree,
117
- reclaimWorktree,
118
- type Worktree,
119
- } from "../shared/worktree.ts";
125
+ allocateResultBudgets,
126
+ type ParentContextUsage,
127
+ } from "../shared/result-budget.ts";
128
+ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
120
129
  import {
121
130
  createSubagentRuntime,
122
131
  runTool,
123
132
  type SubagentRuntime,
124
133
  } from "./src/runtime.ts";
125
- import {
126
- normalizeSubagentTitle,
127
- selectSubagentStripEntry,
128
- SubagentStripWidget,
129
- } from "./navigation.ts";
130
134
  import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
131
135
  import {
132
136
  buildWaitResultPreview,
@@ -135,8 +139,13 @@ import {
135
139
  } from "./src/ui/wait-result.ts";
136
140
 
137
141
  const SUBAGENT_OUTPUT_MAX_BYTES = 24 * 1024;
142
+ const AUTOMATIC_OUTPUT_MAX_BYTES = 48 * 1024;
143
+ const AUTOMATIC_MIN_RESULT_BYTES = 2 * 1024;
138
144
  const WAIT_OUTPUT_MAX_BYTES = 48 * 1024;
139
145
  const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
146
+ const WAIT_MIN_RESULT_BYTES = 512;
147
+ const RESULT_HEADROOM_SHARE = 0.5;
148
+ const ESTIMATED_BYTES_PER_TOKEN = 4;
140
149
 
141
150
  interface SpawnResultDetails {
142
151
  readonly id?: string;
@@ -190,36 +199,71 @@ function describeSubagent(snap: SubagentSnapshot) {
190
199
  return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
191
200
  }
192
201
 
193
- function truncatedOutput(
202
+ export function truncatedOutput(
194
203
  snap: SubagentSnapshot,
195
204
  maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
205
+ writeArtifact: (content: string) => string = (content) =>
206
+ persistResultArtifact(getAgentDir(), content),
196
207
  ): string {
197
208
  const output = snap.finalText || "(no output)";
198
- const truncation = truncateHead(output, {
209
+ return projectResult(output, {
199
210
  maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
200
211
  maxLines: Math.min(600, DEFAULT_MAX_LINES),
201
- });
202
- let text = truncation.content;
203
- if (truncation.truncated) {
204
- text += `\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)} shown. Full transcript in session file: ${snap.meta.sessionFilePath ?? "?"}]`;
205
- }
206
- return text;
212
+ writeArtifact,
213
+ }).text;
207
214
  }
208
215
 
209
216
  export function createSubagentResultDispatcher(
210
217
  pi: ExtensionAPI,
211
- outputFor: (snap: SubagentSnapshot) => string = truncatedOutput,
218
+ outputFor: (
219
+ snap: SubagentSnapshot,
220
+ maxBytes: number,
221
+ ) => string = truncatedOutput,
222
+ getContextUsage: () => ParentContextUsage | undefined = () => undefined,
212
223
  ) {
213
- return (snaps: readonly SubagentSnapshot[], wake: boolean) => {
224
+ return (snaps: readonly SubagentSnapshot[]) => {
214
225
  if (snaps.length === 0) return;
226
+ const emptyMessages = snaps.map((snap) =>
227
+ buildSubagentResultMessage({
228
+ id: snap.id,
229
+ title: snap.title,
230
+ status: snap.status,
231
+ errorText: snap.errorText,
232
+ output: "",
233
+ }),
234
+ );
235
+ const wrapperBytes =
236
+ emptyMessages.reduce(
237
+ (sum, message) => sum + Buffer.byteLength(message, "utf8"),
238
+ 0,
239
+ ) +
240
+ Math.max(0, snaps.length - 1) * 2;
241
+ const projectionBatchBytes = Math.max(
242
+ AUTOMATIC_MIN_RESULT_BYTES * snaps.length,
243
+ AUTOMATIC_OUTPUT_MAX_BYTES - wrapperBytes,
244
+ );
245
+ const allocation = allocateResultBudgets(
246
+ snaps.map((snap) =>
247
+ Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
248
+ ),
249
+ getContextUsage(),
250
+ {
251
+ maxBatchBytes: projectionBatchBytes,
252
+ maxResultBytes: SUBAGENT_OUTPUT_MAX_BYTES,
253
+ minResultBytes: AUTOMATIC_MIN_RESULT_BYTES,
254
+ headroomShare: RESULT_HEADROOM_SHARE,
255
+ estimatedBytesPerToken: ESTIMATED_BYTES_PER_TOKEN,
256
+ fixedBytes: wrapperBytes,
257
+ },
258
+ );
215
259
  const content = snaps
216
- .map((snap) =>
260
+ .map((snap, index) =>
217
261
  buildSubagentResultMessage({
218
262
  id: snap.id,
219
263
  title: snap.title,
220
264
  status: snap.status,
221
265
  errorText: snap.errorText,
222
- output: outputFor(snap),
266
+ output: outputFor(snap, allocation.budgets[index]!),
223
267
  }),
224
268
  )
225
269
  .join("\n\n");
@@ -249,7 +293,7 @@ export function createSubagentResultDispatcher(
249
293
  display: false,
250
294
  details,
251
295
  },
252
- resultDeliveryOptions(wake),
296
+ { deliverAs: "followUp", triggerTurn: true },
253
297
  );
254
298
  };
255
299
  }
@@ -278,7 +322,7 @@ function renderSubagentResult(
278
322
  }
279
323
 
280
324
  const failed = details.status === "error";
281
- const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
325
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
282
326
  const header =
283
327
  `${icon} ` +
284
328
  theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
@@ -307,6 +351,10 @@ function renderSubagentResult(
307
351
  export default function (pi: ExtensionAPI) {
308
352
  let runtime: SubagentRuntime | undefined;
309
353
  let managerPromise: Promise<SubagentManagerShape> | undefined;
354
+ let restoredIdCounters: SubagentIdCounters = {
355
+ modelCounter: 0,
356
+ btwCounter: 0,
357
+ };
310
358
  let sessionContext: ExtensionContext | undefined;
311
359
  let ui: ExtensionUIContext | undefined;
312
360
  let unsubStatus: (() => void) | undefined;
@@ -322,18 +370,31 @@ export default function (pi: ExtensionAPI) {
322
370
  let requestWidgetRender: (() => void) | undefined;
323
371
  let navigationLayerRegistered = false;
324
372
  let dashboardOpen = false;
325
- const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
326
- const dispatchResults = createSubagentResultDispatcher(pi);
327
- const hideLifecycleTools = () =>
328
- patchOwnedTools(pi, "subagents", {
329
- disable: OPENPI_TOOL_SURFACE.subagents.deferred,
330
- });
331
- const showLifecycleTools = () =>
373
+ const dispatchResults = createSubagentResultDispatcher(
374
+ pi,
375
+ truncatedOutput,
376
+ () => sessionContext?.getContextUsage(),
377
+ );
378
+ const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
379
+ isIdle: () => sessionContext?.isIdle() === true,
380
+ // Every unconsumed fire-and-forget result must reach the parent. The
381
+ // delivery coordinator batches results that settled while it was busy.
382
+ deliver: dispatchResults,
383
+ });
384
+ pi.on("agent_settled", () => resultDelivery.parentSettled());
385
+ const registerStableToolFamily = () =>
332
386
  patchOwnedTools(pi, "subagents", {
333
- enable: OPENPI_TOOL_SURFACE.subagents.deferred,
387
+ enable: OPENPI_TOOL_SURFACE.subagents.entry,
334
388
  });
335
389
 
336
- const getRuntime = () => (runtime ??= createSubagentRuntime());
390
+ const getRuntime = () =>
391
+ (runtime ??= createSubagentRuntime({
392
+ initialModelCounter: restoredIdCounters.modelCounter,
393
+ initialBtwCounter: restoredIdCounters.btwCounter,
394
+ }));
395
+
396
+ const persistId = (id: string) =>
397
+ pi.appendEntry(SUBAGENT_ID_WATERMARK_ENTRY_TYPE, subagentIdWatermark(id));
337
398
 
338
399
  /** Resolve the manager service once per runtime and wire the extension hooks. */
339
400
  const getManager = () => {
@@ -387,9 +448,12 @@ export default function (pi: ExtensionAPI) {
387
448
  manager.view.list(),
388
449
  settledAcknowledgedAt,
389
450
  );
451
+ // In the TUI the below-editor strip already reports the same activity and
452
+ // carries the manage affordance, so a footer status line would repeat it.
453
+ const tui = sessionContext?.mode === "tui";
390
454
  ui.setStatus(
391
455
  "subagents",
392
- hasActivity(counts)
456
+ !tui && hasActivity(counts)
393
457
  ? formatActivityStatus(ui.theme, "subagents", counts)
394
458
  : undefined,
395
459
  );
@@ -436,25 +500,6 @@ export default function (pi: ExtensionAPI) {
436
500
  navigationLayerRegistered = true;
437
501
  };
438
502
 
439
- /**
440
- * `wake` decides whether this costs the model a turn. A subagent that
441
- * settled while the model sits idle is the result it is waiting on. A
442
- * backlog that piled up while it worked is not: waking once per stale
443
- * subagent forces a turn each, and the model can only answer "that one
444
- * already finished". `nextTurn` still enters context with the user's next
445
- * message, without demanding a reply.
446
- */
447
- const deliverResults = (
448
- snaps: readonly SubagentSnapshot[],
449
- wake: boolean,
450
- ) => {
451
- dispatchResults(snaps, wake);
452
- };
453
-
454
- const flushResults = (wake: boolean) => {
455
- deliverResults(resultDelivery.drain(), wake);
456
- };
457
-
458
503
  const deliverBtwResult = (snap: SubagentSnapshot) => {
459
504
  // appendEntry is a synchronous SessionManager operation and emits an
460
505
  // entry_appended event, so it is safe while the parent is streaming and
@@ -500,15 +545,18 @@ export default function (pi: ExtensionAPI) {
500
545
  // subagent_wait can consume it before agent_settled flushes follow-ups.
501
546
  // Defer a copy: the live snapshot keeps mutating if the subagent is
502
547
  // restarted before the deferred result flushes.
548
+ // The delivery coordinator closes both sides of the wake-up race: it
549
+ // flushes now if the parent is already idle, otherwise the parent's next
550
+ // agent_settled edge rechecks this same pending Map.
503
551
  resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
504
- // Settled while the model sits idle: it has nothing else in flight, so
505
- // this is the result it is waiting on — wake it.
506
- if (sessionContext?.isIdle()) flushResults(true);
507
552
  };
508
553
 
509
554
  pi.on("session_start", (_event, ctx) => {
555
+ restoredIdCounters = restoreSubagentIdCounters(
556
+ ctx.sessionManager.getBranch(),
557
+ );
510
558
  refreshAgentTypes(ctx.cwd, ctx.isProjectTrusted());
511
- hideLifecycleTools();
559
+ registerStableToolFamily();
512
560
  sessionContext = ctx;
513
561
  settledAcknowledgedAt = 0;
514
562
  if (ctx.hasUI) ui = ctx.ui;
@@ -530,10 +578,6 @@ export default function (pi: ExtensionAPI) {
530
578
  managerPromise?.then(updateStatus).catch(() => undefined);
531
579
  });
532
580
 
533
- // These settled while the model was working on something else, so they go
534
- // into context without forcing a turn per stale subagent.
535
- pi.on("agent_settled", () => flushResults(false));
536
-
537
581
  pi.on("session_shutdown", async () => {
538
582
  if (navigationLayerRegistered) {
539
583
  removeEditorLayer(pi, "subagents");
@@ -610,57 +654,22 @@ export default function (pi: ExtensionAPI) {
610
654
  agentTypes = loaded.agentTypes;
611
655
  agentTypeDiagnostics = loaded.diagnostics;
612
656
  agentTypeList = [...agentTypes.values()];
613
- subagentSpawnTool.description =
614
- buildSubagentSpawnToolDescription(agentTypeList);
615
- subagentSpawnTool.parameters = createSubagentSpawnParameters();
657
+ const surface = createSubagentSpawnToolSurface(agentTypeList);
658
+ subagentSpawnTool.description = surface.description;
659
+ subagentSpawnTool.parameters = surface.parameters;
616
660
  registerSubagentSpawnTool();
617
661
  };
618
662
 
619
663
  // --- Tools -------------------------------------------------------------
620
664
 
621
- const createSubagentSpawnParameters = () =>
622
- Type.Object({
623
- agent_type: createAgentTypeParameterSchema(agentTypeList),
624
- prompt: Type.String({
625
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
626
- }),
627
- name: Type.String({
628
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
629
- }),
630
- harness: Type.Optional(
631
- StringEnum(BACKEND_NAMES, {
632
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness,
633
- }),
634
- ),
635
- working_dir: Type.Optional(
636
- Type.String({
637
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
638
- }),
639
- ),
640
- isolation: Type.Optional(
641
- StringEnum(["worktree"] as const, {
642
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation,
643
- }),
644
- ),
645
- model: Type.Optional(
646
- Type.String({
647
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
648
- }),
649
- ),
650
- reasoning_effort: Type.Optional(
651
- StringEnum(REASONING_EFFORTS, {
652
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
653
- }),
654
- ),
655
- });
665
+ const initialSpawnSurface = createSubagentSpawnToolSurface(agentTypeList);
656
666
 
657
667
  const subagentSpawnTool = defineTool({
658
668
  name: "subagent_spawn",
659
669
  label: "Spawn Subagent",
660
- description: buildSubagentSpawnToolDescription(agentTypeList),
670
+ ...initialSpawnSurface,
661
671
  promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
662
672
  promptGuidelines: SUBAGENT_SPAWN_PROMPT_GUIDELINES,
663
- parameters: createSubagentSpawnParameters(),
664
673
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
665
674
  // Only one backend exists; harness is optional and defaults to it.
666
675
  const harness = params.harness ?? BACKEND_NAMES[0];
@@ -785,8 +794,7 @@ export default function (pi: ExtensionAPI) {
785
794
  if (worktree) await reclaimWorktree(cwd, worktree).catch(() => {});
786
795
  throw error;
787
796
  }
788
-
789
- showLifecycleTools();
797
+ persistId(snap.id);
790
798
 
791
799
  return {
792
800
  content: [
@@ -831,8 +839,11 @@ export default function (pi: ExtensionAPI) {
831
839
  const meta = [details.harness, details.model]
832
840
  .filter(Boolean)
833
841
  .join(" \u00b7 ");
842
+ // A spawn is an event, not a state, so it speaks in the activity rows'
843
+ // verb language ("Wrote" / "Ran" / "Spawned") rather than a glyph; the
844
+ // strip's spinner carries the running state from here on.
834
845
  return new Text(
835
- `${theme.fg("success", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
846
+ `${theme.fg("toolTitle", "Spawned")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
836
847
  0,
837
848
  0,
838
849
  );
@@ -852,7 +863,7 @@ export default function (pi: ExtensionAPI) {
852
863
  description: SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS.ids,
853
864
  }),
854
865
  }),
855
- async execute(_toolCallId, params, signal, onUpdate) {
866
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
856
867
  const manager = await getManager();
857
868
  const ids = [...new Set(params.ids)];
858
869
  if (ids.length === 0)
@@ -888,33 +899,66 @@ export default function (pi: ExtensionAPI) {
888
899
  // deferred automatic delivery now that the tool is returning the result.
889
900
  resultDelivery.consume(ids);
890
901
 
891
- const sections: string[] = [];
892
- let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
893
- for (const id of ids) {
902
+ const entries: Array<
903
+ | { readonly id: string; readonly section: string }
904
+ | {
905
+ readonly id: string;
906
+ readonly snap: SubagentSnapshot;
907
+ readonly header: string;
908
+ }
909
+ > = ids.map((id) => {
894
910
  const snap = manager.view.get(id);
895
- if (!snap) {
896
- sections.push(`## ${id}\n\n(no longer tracked)`);
897
- continue;
898
- }
911
+ if (!snap) return { id, section: `## ${id}\n\n(no longer tracked)` };
899
912
  const verb = snap.status === "error" ? "failed" : "finished";
900
- let section = `## ${snap.id} "${snap.title}" ${verb}`;
901
- if (snap.errorText) section += `\nError: ${snap.errorText}`;
902
- const headerBytes = Buffer.byteLength(section, "utf8") + 2;
903
- const outputBudget = Math.max(
904
- 512,
905
- Math.min(WAIT_PER_AGENT_MAX_BYTES, remainingBytes - headerBytes),
913
+ let header = `## ${snap.id} "${snap.title}" ${verb}`;
914
+ if (snap.errorText) header += `\nError: ${snap.errorText}`;
915
+ return { id, snap, header };
916
+ });
917
+ const separatorsBytes = Math.max(0, entries.length - 1) * 7;
918
+ const fixedBytes =
919
+ separatorsBytes +
920
+ entries.reduce(
921
+ (sum, entry) =>
922
+ sum +
923
+ Buffer.byteLength(
924
+ "section" in entry ? entry.section : `${entry.header}\n\n`,
925
+ "utf8",
926
+ ),
927
+ 0,
906
928
  );
907
- section += `\n\n${truncatedOutput(snap, outputBudget)}`;
908
- const sectionBytes = Buffer.byteLength(section, "utf8");
909
- if (sectionBytes > remainingBytes) {
910
- sections.push(
911
- `## ${snap.id} "${snap.title}"\n\n[omitted: total wait output limit reached]`,
912
- );
913
- break;
914
- }
915
- sections.push(section);
916
- remainingBytes -= sectionBytes;
917
- }
929
+ const resultEntries = entries.filter(
930
+ (
931
+ entry,
932
+ ): entry is {
933
+ readonly id: string;
934
+ readonly snap: SubagentSnapshot;
935
+ readonly header: string;
936
+ } => "snap" in entry,
937
+ );
938
+ const projectionBatchBytes = Math.max(
939
+ WAIT_MIN_RESULT_BYTES * resultEntries.length,
940
+ WAIT_OUTPUT_MAX_BYTES - fixedBytes,
941
+ );
942
+ const allocation = allocateResultBudgets(
943
+ resultEntries.map(({ snap }) =>
944
+ Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
945
+ ),
946
+ ctx.getContextUsage(),
947
+ {
948
+ maxBatchBytes: projectionBatchBytes,
949
+ maxResultBytes: WAIT_PER_AGENT_MAX_BYTES,
950
+ minResultBytes: WAIT_MIN_RESULT_BYTES,
951
+ headroomShare: RESULT_HEADROOM_SHARE,
952
+ estimatedBytesPerToken: ESTIMATED_BYTES_PER_TOKEN,
953
+ fixedBytes,
954
+ },
955
+ );
956
+ let resultIndex = 0;
957
+ const sections = entries.map((entry) => {
958
+ if ("section" in entry) return entry.section;
959
+ const outputBudget = allocation.budgets[resultIndex++]!;
960
+ return `${entry.header}\n\n${truncatedOutput(entry.snap, outputBudget)}`;
961
+ });
918
962
 
919
963
  const combined = sections.join("\n\n---\n\n");
920
964
  const bounded = truncateHead(combined, {
@@ -1170,11 +1214,12 @@ export default function (pi: ExtensionAPI) {
1170
1214
  (entry, _options, theme) => {
1171
1215
  const data = entry.data;
1172
1216
  const failed = data?.status === "error";
1217
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "✓");
1173
1218
  return new Text(
1174
- `${theme.fg(failed ? "error" : "success", "\u25cf")} ` +
1219
+ `${icon} ${theme.fg("accent", data?.title ?? "?")}` +
1175
1220
  theme.fg(
1176
- "muted",
1177
- `Agent "${data?.title ?? "?"}" ${failed ? "failed" : "finished"} \u00b7 ${data?.elapsed ?? "?"}`,
1221
+ "dim",
1222
+ ` ${failed ? "failed" : "finished"} · ${data?.elapsed ?? "?"}`,
1178
1223
  ),
1179
1224
  1,
1180
1225
  0,
@@ -1187,7 +1232,7 @@ export default function (pi: ExtensionAPI) {
1187
1232
  (entry, { expanded }, theme) => {
1188
1233
  const data = entry.data;
1189
1234
  const failed = data?.status === "error";
1190
- const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
1235
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
1191
1236
  const header =
1192
1237
  `${icon} ` +
1193
1238
  theme.fg("accent", theme.bold(`by the way · ${data?.title ?? "?"}`)) +
@@ -1271,6 +1316,7 @@ export default function (pi: ExtensionAPI) {
1271
1316
  );
1272
1317
  return;
1273
1318
  }
1319
+ persistId(snap.id);
1274
1320
 
1275
1321
  await openSubagentTakeover(ctx, manager.view, snap.id, {
1276
1322
  badge: "by the way",