@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -85,15 +85,4 @@ export class AgentOutputManager {
85
85
  await this.#ensureInitialized();
86
86
  return this.#allocateUnique(id);
87
87
  }
88
-
89
- /**
90
- * Allocate unique IDs for a batch of tasks.
91
- *
92
- * @param ids Array of requested IDs
93
- * @returns Array of unique IDs in same order
94
- */
95
- async allocateBatch(ids: string[]): Promise<string[]> {
96
- await this.#ensureInitialized();
97
- return ids.map(id => this.#allocateUnique(id));
98
- }
99
88
  }
@@ -62,6 +62,7 @@ function appendAgentStats(
62
62
  line: string,
63
63
  opts: {
64
64
  toolCount?: number;
65
+ requests?: number;
65
66
  tokens: number;
66
67
  contextTokens?: number;
67
68
  contextWindow?: number;
@@ -74,6 +75,9 @@ function appendAgentStats(
74
75
  if (opts.toolCount) {
75
76
  line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.toolCount)} ${theme.icon.extensionTool}`)}`;
76
77
  }
78
+ if (opts.requests) {
79
+ line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.requests)} req`)}`;
80
+ }
77
81
  // Current per-turn context — match the status line's `<pct>%/<window>` gauge (e.g. `5.1%/1M`).
78
82
  if (opts.contextTokens && opts.contextTokens > 0) {
79
83
  const ctx =
@@ -505,65 +509,106 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
505
509
  }
506
510
 
507
511
  /**
508
- * Render the per-task list (`id` + ui `description`) for the streaming call
509
- * preview. The args stream in token by token, so the array grows over time and
510
- * trailing entries may be partially parsed — every field access is defensive.
512
+ * Render the call preview lines for the single spawned agent. The
513
+ * args stream in token by token, so every field access is defensive.
511
514
  */
512
- function renderTaskItemLines(tasks: TaskItem[] | undefined, expanded: boolean, theme: Theme): string[] {
513
- const items = tasks ?? [];
514
- if (items.length === 0) return [];
515
-
515
+ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme): string[] {
516
+ if (!args) return [];
516
517
  const bullet = theme.fg("dim", "•");
517
- const cap = expanded ? items.length : Math.min(items.length, 12);
518
- const truncated = cap < items.length;
518
+ const lines: string[] = [];
519
519
 
520
+ const rawId = typeof args.id === "string" ? args.id.trim() : "";
521
+ const idLabel = rawId ? formatTaskId(rawId) : "";
522
+ const desc = typeof args.description === "string" ? args.description.trim() : "";
523
+ if (idLabel || desc) {
524
+ let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
525
+ if (desc) {
526
+ line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
527
+ }
528
+ lines.push(line);
529
+ }
530
+ lines.push(...renderTaskItemLines(args.tasks, theme));
531
+ return lines;
532
+ }
533
+
534
+ /**
535
+ * Render the per-item list (`id` + ui `description`) for a batch call's
536
+ * streaming preview. The args stream in token by token, so the array grows
537
+ * over time and trailing entries may be partially parsed — every field access
538
+ * is defensive.
539
+ */
540
+ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): string[] {
541
+ if (!Array.isArray(tasks) || tasks.length === 0) return [];
542
+
543
+ const bullet = theme.fg("dim", "•");
544
+ const cap = Math.min(tasks.length, 12);
520
545
  const lines: string[] = [];
521
546
  for (let i = 0; i < cap; i++) {
522
- const task = items[i] as Partial<TaskItem> | undefined;
523
- const rawId = task?.id?.trim();
547
+ const task = tasks[i] as Partial<TaskItem> | undefined;
548
+ const rawId = typeof task?.id === "string" ? task.id.trim() : "";
524
549
  const idLabel = rawId ? formatTaskId(rawId) : `#${i + 1}`;
525
550
  let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
526
- const desc = task?.description?.trim();
551
+ const desc = typeof task?.description === "string" ? task.description.trim() : "";
527
552
  if (desc) {
528
553
  line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
529
554
  }
555
+ if (task?.isolated === true) {
556
+ line += theme.fg("dim", " [isolated]");
557
+ }
530
558
  lines.push(line);
531
559
  }
532
- if (truncated) {
533
- lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(items.length - cap, "agent"))}`);
560
+ if (cap < tasks.length) {
561
+ lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(tasks.length - cap, "agent"))}`);
534
562
  }
535
563
  return lines;
536
564
  }
537
565
 
538
- /**
539
- * Build the shared-context section (the `# Goal / # Constraints` background
540
- * passed to every subagent). Rendered in both the streaming call preview and
541
- * the merged result frame so the brief stays visible for the whole task
542
- * lifecycle — not just until the first progress snapshot replaces the call view.
543
- */
544
- type TaskRenderSection = { lines: readonly string[] };
545
- type ContextSectionRenderer = (width: number) => TaskRenderSection;
566
+ /** One renderable frame section: optional label, body rows, leading divider. */
567
+ type TaskRenderSection = { label?: string; lines: readonly string[]; separator?: boolean };
568
+ type AssignmentSectionRenderer = (width: number) => TaskRenderSection;
546
569
 
547
570
  // Default output-block layout is: left border + one-cell content inset + right
548
571
  // border. Render markdown at that inner width so the output block does not need
549
- // to rewrap already-rendered context lines.
550
- const CONTEXT_FRAME_INSET = 3;
572
+ // to rewrap already-rendered assignment lines.
573
+ const ASSIGNMENT_FRAME_INSET = 3;
551
574
 
552
- function contextMarkdownWidth(frameWidth: number): number {
553
- return Math.max(1, frameWidth - CONTEXT_FRAME_INSET);
575
+ /**
576
+ * Build the assignment section (the markdown brief handed to the subagent).
577
+ * Rendered in both the streaming call preview and the result frame so the
578
+ * brief stays visible for the whole task lifecycle — not just until the first
579
+ * progress snapshot replaces the call view.
580
+ */
581
+ function createAssignmentSectionRenderer(
582
+ args: Partial<TaskParams> | undefined,
583
+ theme: Theme,
584
+ ): AssignmentSectionRenderer | undefined {
585
+ // `renderResult` receives the raw tool args (unlike `renderCall`, which is
586
+ // fed through `repairTaskParams`), so undo any per-field double-encoding
587
+ // here too. The repair is idempotent on already-clean text.
588
+ const assignment = repairDoubleEncodedJsonString(typeof args?.assignment === "string" ? args.assignment : "").trim();
589
+ if (!assignment) return undefined;
590
+ return createMarkdownSectionRenderer(assignment, theme);
554
591
  }
555
592
 
556
- function createContextSectionRenderer(args: TaskParams | undefined, theme: Theme): ContextSectionRenderer | undefined {
557
- // `renderResult` receives the raw tool args (unlike `renderCall`, which is
558
- // fed through `repairTaskParams`), so undo any per-field double-encoding here
559
- // too. The repair is idempotent on already-clean text.
560
- const context = repairDoubleEncodedJsonString(args?.context ?? "").trim();
593
+ /**
594
+ * Build the shared-context section (the `# Goal / # Constraints` background a
595
+ * batch call hands every subagent). Rendered like the assignment brief so the
596
+ * shared background stays visible for the whole task lifecycle.
597
+ */
598
+ function createContextSectionRenderer(
599
+ args: Partial<TaskParams> | undefined,
600
+ theme: Theme,
601
+ ): AssignmentSectionRenderer | undefined {
602
+ const context = repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : "").trim();
561
603
  if (!context) return undefined;
604
+ return createMarkdownSectionRenderer(context, theme);
605
+ }
562
606
 
563
- const markdown = new Markdown(context, 0, 0, getMarkdownTheme(), {
564
- color: text => theme.fg("muted", text),
607
+ function createMarkdownSectionRenderer(text: string, theme: Theme): AssignmentSectionRenderer {
608
+ const markdown = new Markdown(text, 0, 0, getMarkdownTheme(), {
609
+ color: line => theme.fg("muted", line),
565
610
  });
566
- return width => ({ lines: markdown.render(contextMarkdownWidth(width)) });
611
+ return width => ({ lines: markdown.render(Math.max(1, width - ASSIGNMENT_FRAME_INSET)) });
567
612
  }
568
613
 
569
614
  /**
@@ -576,21 +621,22 @@ export function renderCall(
576
621
  ): Component {
577
622
  const showIsolated = "isolated" in args && args.isolated === true;
578
623
  const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
579
- const contextSectionRenderer = createContextSectionRenderer(args, theme);
624
+ const assignmentSection = createAssignmentSectionRenderer(args, theme);
625
+ const contextSection = createContextSectionRenderer(args, theme);
580
626
  return framedBlock(theme, width => {
581
627
  const sections: Array<{ label?: string; lines: readonly string[]; separator?: boolean }> = [];
582
628
 
583
- if (contextSectionRenderer) sections.push(contextSectionRenderer(width));
584
-
585
- // The per-task preview list only exists to surface dispatched agents while
586
- // the call args stream in. Once a result snapshot exists, `renderResult`
587
- // draws the same agents as progress/result lines, so showing the Tasks
588
- // section here would just repeat the count the result frame already shows.
629
+ // The call preview only exists to surface the dispatched agent while the
630
+ // args stream in. Once a result snapshot exists, `renderResult` draws the
631
+ // same agent (and the assignment brief) itself, so showing it here would
632
+ // repeat what the result frame already shows.
589
633
  if (!options.renderContext?.hasResult) {
590
634
  sections.push({
591
635
  separator: true,
592
- lines: renderTaskItemLines(args.tasks, options.expanded, theme),
636
+ lines: renderTaskCallLines(args, theme),
593
637
  });
638
+ if (contextSection) sections.push(contextSection(width));
639
+ if (assignmentSection) sections.push(assignmentSection(width));
594
640
  }
595
641
 
596
642
  return {
@@ -631,8 +677,12 @@ function renderAgentProgress(
631
677
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
632
678
  const indent = prefix ? `${prefix} ` : "";
633
679
  let statusLine: string;
634
- if (progress.status === "running") {
635
- const bullet = theme.styledSymbol("status.done", "text");
680
+ if (progress.status === "running" || progress.status === "pending") {
681
+ // Live (or queued) agents shimmer their description so the row reads as
682
+ // in-flight even after the block freezes — the async spawn result keeps
683
+ // the agent on "pending" while the detached job runs.
684
+ const bullet =
685
+ progress.status === "running" ? theme.styledSymbol("status.done", "text") : theme.fg(iconColor, icon);
636
686
  const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
637
687
  statusLine = `${indent}${bullet} ${name}`;
638
688
  if (description) {
@@ -945,6 +995,7 @@ function renderAgentResult(
945
995
  statusLine,
946
996
  {
947
997
  tokens: result.tokens,
998
+ requests: result.requests,
948
999
  contextTokens: result.contextTokens,
949
1000
  contextWindow: result.contextWindow,
950
1001
  cost: result.usage?.cost.total ?? 0,
@@ -1072,6 +1123,32 @@ function renderAgentResult(
1072
1123
  return lines;
1073
1124
  }
1074
1125
 
1126
+ /**
1127
+ * Order live progress entries so finished agents render first — sorted by
1128
+ * runtime ascending, matching {@link orderResultsForDisplay} — while
1129
+ * unfinished (pending/running) ones stay pinned at the bottom in dispatch
1130
+ * order. Because a finished agent's runtime is fixed, finalization renders
1131
+ * the same order and rows never reshuffle.
1132
+ */
1133
+ function orderProgressForDisplay(progress: readonly AgentProgress[]): AgentProgress[] {
1134
+ const finished: AgentProgress[] = [];
1135
+ const unfinished: AgentProgress[] = [];
1136
+ for (const p of progress) {
1137
+ (p.status === "pending" || p.status === "running" ? unfinished : finished).push(p);
1138
+ }
1139
+ finished.sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
1140
+ return finished.concat(unfinished);
1141
+ }
1142
+
1143
+ /**
1144
+ * Order finalized results by runtime ascending (tie-break: dispatch index) so
1145
+ * the finalized list matches the live-progress order produced by
1146
+ * {@link orderProgressForDisplay}.
1147
+ */
1148
+ function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[] {
1149
+ return [...results].sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
1150
+ }
1151
+
1075
1152
  /**
1076
1153
  * Render the tool result.
1077
1154
  */
@@ -1083,25 +1160,28 @@ export function renderResult(
1083
1160
  ): Component {
1084
1161
  const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
1085
1162
  const details = result.details;
1086
- const contextSectionRenderer = createContextSectionRenderer(args, theme);
1163
+ const agentLabel = args?.agent?.trim() || undefined;
1164
+ const assignmentSection = createAssignmentSectionRenderer(args, theme);
1165
+ const contextSection = createContextSectionRenderer(args, theme);
1087
1166
 
1088
1167
  if (!details) {
1089
1168
  const text = result.content.find(c => c.type === "text")?.text || "";
1090
1169
  const errored = result.isError === true;
1091
1170
  const header = errored
1092
- ? renderStatusLine({ icon: "error", title: "Task", description: args?.agent }, theme)
1171
+ ? renderStatusLine({ icon: "error", title: "Task", description: agentLabel }, theme)
1093
1172
  : renderStatusLine(
1094
1173
  {
1095
1174
  iconOverride: theme.styledSymbol("status.done", "accent"),
1096
1175
  title: "Task",
1097
- description: args?.agent,
1176
+ description: agentLabel,
1098
1177
  },
1099
1178
  theme,
1100
1179
  );
1101
1180
  return framedBlock(theme, width => ({
1102
1181
  header,
1103
1182
  sections: [
1104
- ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1183
+ ...(contextSection ? [contextSection(width)] : []),
1184
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1105
1185
  ...(text ? [{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
1106
1186
  ],
1107
1187
  state: errored ? "error" : "success",
@@ -1117,12 +1197,10 @@ export function renderResult(
1117
1197
  const isError = aborted || failed;
1118
1198
  const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
1119
1199
  const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1120
- // Surface the dispatched agent type (e.g. `Reviewer`) alongside the count so
1121
- // the header reads `Task 16 agents: Reviewer`. All tasks in one call share a
1122
- // single `agent` type (top-level param), so one label covers the whole batch.
1123
- const agentName = args?.agent?.trim();
1200
+ // Surface the dispatched agent type (e.g. `Reviewer`) alongside the count
1201
+ // so the header reads `Task 1 agent: Reviewer`.
1124
1202
  const countLabel = agentCount > 0 ? `${agentCount} ${agentCount === 1 ? "agent" : "agents"}` : undefined;
1125
- const metaLabel = countLabel ? (agentName ? `${countLabel}: ${agentName}` : countLabel) : agentName;
1203
+ const metaLabel = countLabel ? (agentLabel ? `${countLabel}: ${agentLabel}` : countLabel) : agentLabel;
1126
1204
  const header = renderStatusLine(
1127
1205
  {
1128
1206
  icon: icon === "success" ? undefined : icon,
@@ -1140,11 +1218,11 @@ export function renderResult(
1140
1218
  const shouldRenderProgress =
1141
1219
  Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
1142
1220
  if (shouldRenderProgress && details.progress) {
1143
- details.progress.forEach(progress => {
1221
+ orderProgressForDisplay(details.progress).forEach(progress => {
1144
1222
  lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame));
1145
1223
  });
1146
1224
  } else if (details.results && details.results.length > 0) {
1147
- details.results.forEach(res => {
1225
+ orderResultsForDisplay(details.results).forEach(res => {
1148
1226
  lines.push(...renderAgentResult(res, "", " ", expanded, theme));
1149
1227
  });
1150
1228
 
@@ -1157,6 +1235,8 @@ export function renderResult(
1157
1235
  if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
1158
1236
  if (mergeFailedCount > 0) summaryParts.push(theme.fg("warning", `${mergeFailedCount} merge failed`));
1159
1237
  if (failCount > 0) summaryParts.push(theme.fg("error", `${failCount} failed`));
1238
+ const totalRequests = details.results.reduce((sum, r) => sum + (r.requests ?? 0), 0);
1239
+ if (totalRequests > 0) summaryParts.push(theme.fg("dim", `${formatNumber(totalRequests)} req`));
1160
1240
  summaryParts.push(theme.fg("dim", formatDuration(details.totalDurationMs)));
1161
1241
  // Wrap the run summary in the theme's bracket glyphs (dim chrome, colored
1162
1242
  // counts) to match the bash tool's `[Wall: … | Exit: …]` footer.
@@ -1175,7 +1255,8 @@ export function renderResult(
1175
1255
  return {
1176
1256
  header,
1177
1257
  sections: [
1178
- ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1258
+ ...(contextSection ? [contextSection(width)] : []),
1259
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1179
1260
  { separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] },
1180
1261
  ],
1181
1262
  state,
@@ -1205,7 +1286,8 @@ export function renderResult(
1205
1286
  return {
1206
1287
  header,
1207
1288
  sections: [
1208
- ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1289
+ ...(contextSection ? [contextSection(width)] : []),
1290
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1209
1291
  ...(lines.length > 0 ? [{ separator: true, lines }] : []),
1210
1292
  ],
1211
1293
  state,
@@ -1238,8 +1320,9 @@ function renderNestedTaskResults(detailsList: TaskToolDetails[], expanded: boole
1238
1320
  const lines: string[] = [];
1239
1321
  for (const details of detailsList) {
1240
1322
  if (!details.results || details.results.length === 0) continue;
1241
- details.results.forEach((result, index) => {
1242
- const { prefix, continuePrefix } = nestedMarkers(index === details.results.length - 1, theme);
1323
+ const ordered = orderResultsForDisplay(details.results);
1324
+ ordered.forEach((result, index) => {
1325
+ const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1243
1326
  lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1244
1327
  });
1245
1328
  }
@@ -1261,16 +1344,18 @@ function renderNestedTaskTree(
1261
1344
  for (const details of detailsList) {
1262
1345
  const hasResults = Boolean(details.results && details.results.length > 0);
1263
1346
  if (hasResults) {
1264
- details.results.forEach((result, index) => {
1265
- const { prefix, continuePrefix } = nestedMarkers(index === details.results.length - 1, theme);
1347
+ const ordered = orderResultsForDisplay(details.results);
1348
+ ordered.forEach((result, index) => {
1349
+ const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1266
1350
  lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1267
1351
  });
1268
1352
  continue;
1269
1353
  }
1270
1354
  const inflight = details.progress;
1271
1355
  if (inflight && inflight.length > 0) {
1272
- inflight.forEach((prog, index) => {
1273
- const { prefix, continuePrefix } = nestedMarkers(index === inflight.length - 1, theme);
1356
+ const ordered = orderProgressForDisplay(inflight);
1357
+ ordered.forEach((prog, index) => {
1358
+ const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1274
1359
  lines.push(...renderAgentProgress(prog, prefix, continuePrefix, expanded, theme, spinnerFrame));
1275
1360
  });
1276
1361
  }
@@ -2,7 +2,7 @@
2
2
  * Repair double-encoded JSON string arguments for the task tool.
3
3
  *
4
4
  * Models occasionally JSON-escape a string value twice when emitting a
5
- * `task` tool call, so a `context`/`assignment` that should read
5
+ * `task` tool call, so an `assignment` that should read
6
6
  *
7
7
  * # Role
8
8
  * You are a judge … "describe this" … return —
@@ -24,7 +24,7 @@
24
24
  * string.
25
25
  *
26
26
  * This is deliberately scoped to the task tool's natural-language fields
27
- * (`context`, `assignment`, `description`). It is NOT applied to code-bearing
27
+ * (`assignment`, `description`). It is NOT applied to code-bearing
28
28
  * tools (write/edit/bash/search), where a backslash or quote is load-bearing
29
29
  * and a false-positive unescape would silently corrupt a file or command.
30
30
  */
@@ -90,15 +90,20 @@ function repairTaskItem(task: TaskItem): TaskItem {
90
90
  }
91
91
 
92
92
  /**
93
- * Repair double-encoded prose in task-tool params (`context` and each task's
94
- * `assignment`/`description`). Returns the same reference when nothing changed
95
- * so callers can cheaply skip work. Defensive against partially-streamed args
96
- * (missing/undefined fields, partial task arrays) so it is safe on the render
97
- * path as well as on execution.
93
+ * Repair double-encoded prose in task-tool params (`assignment`,
94
+ * `description`, shared `context`, and each batch task item's prose fields).
95
+ * Returns the same reference when nothing changed so callers can cheaply skip
96
+ * work. Defensive against partially-streamed args (missing/undefined fields,
97
+ * partial task arrays) so it is safe on the render path as well as on
98
+ * execution.
98
99
  */
99
100
  export function repairTaskParams(params: TaskParams): TaskParams {
100
101
  if (params === null || typeof params !== "object") return params;
101
102
 
103
+ const assignment =
104
+ typeof params.assignment === "string" ? repairDoubleEncodedJsonString(params.assignment) : params.assignment;
105
+ const description =
106
+ typeof params.description === "string" ? repairDoubleEncodedJsonString(params.description) : params.description;
102
107
  const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
103
108
 
104
109
  let tasks = params.tasks;
@@ -112,6 +117,13 @@ export function repairTaskParams(params: TaskParams): TaskParams {
112
117
  if (changed) tasks = repaired;
113
118
  }
114
119
 
115
- if (context === params.context && tasks === params.tasks) return params;
116
- return { ...params, context, tasks };
120
+ if (
121
+ assignment === params.assignment &&
122
+ description === params.description &&
123
+ context === params.context &&
124
+ tasks === params.tasks
125
+ ) {
126
+ return params;
127
+ }
128
+ return { ...params, assignment, description, context, tasks };
117
129
  }
package/src/task/types.ts CHANGED
@@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Usage } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
4
  import * as z from "zod/v4";
5
- import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
5
+ import type { AgentSessionEvent } from "../session/agent-session";
6
6
  import type { NestedRepoPatch } from "./worktree";
7
7
 
8
8
  /** Source of an agent definition */
@@ -41,11 +41,18 @@ export interface SubagentProgressPayload {
41
41
  agent: string;
42
42
  agentSource: AgentSource;
43
43
  task: string;
44
+ parentToolCallId?: string;
44
45
  assignment?: string;
45
46
  progress: AgentProgress;
46
47
  sessionFile?: string;
47
48
  }
48
49
 
50
+ /** Payload emitted on TASK_SUBAGENT_EVENT_CHANNEL */
51
+ export interface SubagentEventPayload {
52
+ id: string;
53
+ event: AgentSessionEvent;
54
+ }
55
+
49
56
  /** Payload emitted on TASK_SUBAGENT_LIFECYCLE_CHANNEL */
50
57
  export interface SubagentLifecyclePayload {
51
58
  id: string;
@@ -54,87 +61,92 @@ export interface SubagentLifecyclePayload {
54
61
  description?: string;
55
62
  status: "started" | "completed" | "failed" | "aborted";
56
63
  sessionFile?: string;
64
+ parentToolCallId?: string;
57
65
  index: number;
58
66
  }
59
67
 
60
- const assignmentDescription = "per-task instructions; self-contained";
61
-
62
- const createTaskItemSchema = (_contextEnabled: boolean) =>
63
- z.object({
64
- id: z.string().max(48).describe("camelcase identifier"),
65
- description: z.string().describe("ui label, not seen by subagent"),
66
- assignment: z.string().describe(assignmentDescription),
67
- });
68
-
69
- /** Single task item for parallel execution (default shape with context enabled). */
70
- export const taskItemSchema = createTaskItemSchema(true);
71
- export type TaskItem = z.infer<typeof taskItemSchema>;
72
-
73
- const createTaskSchema = (options: { isolationEnabled: boolean; simpleMode: TaskSimpleMode }) => {
74
- const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(options.simpleMode);
75
- const itemSchema = createTaskItemSchema(contextEnabled);
76
-
77
- let schema = z.object({
78
- agent: z.string().describe("agent type"),
79
- tasks: z.array(itemSchema).describe("tasks to execute in parallel"),
80
- });
81
- if (contextEnabled) {
82
- schema = schema.extend({
83
- context: z.string().optional().describe("shared background prepended to each assignment"),
84
- });
85
- }
86
-
87
- if (customSchemaEnabled) {
88
- schema = schema.extend({
89
- schema: z.string().optional().describe("jtd schema for expected response shape"),
90
- });
91
- }
68
+ /**
69
+ * One unit of work. The single-spawn schema is `{ agent, ...taskItemSchema }`;
70
+ * the batch schema (`task.batch`) is `{ agent, context, tasks: taskItemSchema[] }`.
71
+ * When task isolation is enabled, `isolated` joins the item shape (per-item in
72
+ * batch form, top-level in the flat form via the spread).
73
+ */
74
+ const taskItemShape = {
75
+ id: z.string().max(48).optional().describe("stable agent id; default generated"),
76
+ description: z.string().optional().describe("ui label, not seen by subagent"),
77
+ assignment: z.string().describe("the work; self-contained instructions"),
78
+ };
79
+ const isolatedShape = {
80
+ isolated: z.boolean().optional().describe("run in isolated env; returns patches"),
81
+ };
82
+ const agentShape = {
83
+ agent: z.string().describe("agent type to spawn"),
84
+ };
85
+ const contextShape = {
86
+ context: z.string().describe("shared background prepended to each assignment"),
87
+ };
92
88
 
93
- if (options.isolationEnabled) {
94
- schema = schema.extend({
95
- isolated: z.boolean().optional().describe("run in isolated env; returns patches"),
96
- });
97
- }
89
+ export const taskItemSchema = z.object(taskItemShape);
90
+ const taskItemSchemaIsolated = z.object({ ...taskItemShape, ...isolatedShape });
98
91
 
99
- return schema;
100
- };
92
+ /** Single task item. Fields are optional defensively: args stream in token by token. */
93
+ export interface TaskItem {
94
+ /** Stable agent id; default = generated AdjectiveNoun. */
95
+ id?: string;
96
+ /** UI label, not seen by the subagent. */
97
+ description?: string;
98
+ /** The work; required by the schema. */
99
+ assignment?: string;
100
+ /** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
101
+ isolated?: boolean;
102
+ }
101
103
 
102
- export const taskSchema = createTaskSchema({ isolationEnabled: true, simpleMode: "default" });
103
- export const taskSchemaNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "default" });
104
- const taskSchemaSchemaFree = createTaskSchema({ isolationEnabled: true, simpleMode: "schema-free" });
105
- const taskSchemaSchemaFreeNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "schema-free" });
106
- const taskSchemaIndependent = createTaskSchema({ isolationEnabled: true, simpleMode: "independent" });
107
- const taskSchemaIndependentNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "independent" });
108
- const ALL_TASK_SCHEMAS = [
109
- taskSchema,
110
- taskSchemaNoIsolation,
111
- taskSchemaSchemaFree,
112
- taskSchemaSchemaFreeNoIsolation,
113
- taskSchemaIndependent,
114
- taskSchemaIndependentNoIsolation,
115
- ] as const;
104
+ export const taskSchema = z.object({ ...agentShape, ...taskItemShape, ...isolatedShape });
105
+ const taskSchemaNoIsolation = z.object({ ...agentShape, ...taskItemShape });
106
+ const taskSchemaBatch = z.object({
107
+ ...agentShape,
108
+ ...contextShape,
109
+ tasks: z.array(taskItemSchemaIsolated).describe("tasks to spawn; one subagent per item"),
110
+ });
111
+ const taskSchemaBatchNoIsolation = z.object({
112
+ ...agentShape,
113
+ ...contextShape,
114
+ tasks: z.array(taskItemSchema).describe("tasks to spawn; one subagent per item"),
115
+ });
116
+ const ALL_TASK_SCHEMAS = [taskSchema, taskSchemaNoIsolation, taskSchemaBatch, taskSchemaBatchNoIsolation] as const;
116
117
 
117
118
  type DynamicTaskSchema = (typeof ALL_TASK_SCHEMAS)[number];
118
119
  export type TaskSchema = typeof taskSchema;
119
- /** Active task tool parameter schema for the current simple-mode / isolation flags */
120
+ /** Active task tool parameter schema for the current isolation / batch flags */
120
121
  export type TaskToolSchemaInstance = DynamicTaskSchema;
121
122
 
122
- export function getTaskSchema(options: { isolationEnabled: boolean; simpleMode: TaskSimpleMode }): DynamicTaskSchema {
123
- switch (options.simpleMode) {
124
- case "schema-free":
125
- return options.isolationEnabled ? taskSchemaSchemaFree : taskSchemaSchemaFreeNoIsolation;
126
- case "independent":
127
- return options.isolationEnabled ? taskSchemaIndependent : taskSchemaIndependentNoIsolation;
128
- default:
129
- return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
123
+ export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema {
124
+ if (options.batchEnabled) {
125
+ return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
130
126
  }
127
+ return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
131
128
  }
132
129
 
130
+ /**
131
+ * Runtime params union over both wire shapes. The model sees exactly one shape
132
+ * (`{ agent, context, tasks[] }` when `task.batch` is on, `{ agent, ...item }`
133
+ * otherwise); runtime stays permissive so internal callers and stale
134
+ * transcripts using the flat form keep working under either setting.
135
+ */
133
136
  export interface TaskParams {
134
- agent: string;
137
+ /** Agent type; required. */
138
+ agent?: string;
139
+ /** Stable agent id (flat form); default = generated AdjectiveNoun. */
140
+ id?: string;
141
+ /** UI label (flat form), not seen by the subagent. */
142
+ description?: string;
143
+ /** The work (flat form). */
144
+ assignment?: string;
145
+ /** Batch form (`task.batch`): one subagent per item. */
146
+ tasks?: TaskItem[];
147
+ /** Batch form: shared background prepended to every assignment; required by the batch schema. */
135
148
  context?: string;
136
- schema?: string;
137
- tasks: TaskItem[];
149
+ /** Run in an isolated worktree (flat form; per-item in batch form). */
138
150
  isolated?: boolean;
139
151
  }
140
152
 
@@ -197,6 +209,8 @@ export interface AgentProgress {
197
209
  recentTools: Array<{ tool: string; args: string; endMs: number }>;
198
210
  recentOutput: string[];
199
211
  toolCount: number;
212
+ /** Count of assistant requests (assistant message_end events) across the run. Drives the soft request budget guard. */
213
+ requests: number;
200
214
  /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
201
215
  tokens: number;
202
216
  /**
@@ -267,6 +281,8 @@ export interface SingleResult {
267
281
  durationMs: number;
268
282
  /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
269
283
  tokens: number;
284
+ /** Count of assistant requests (assistant message_end events) across the run. */
285
+ requests: number;
270
286
  /** Latest per-turn context size at task completion. See `AgentProgress.contextTokens`. */
271
287
  contextTokens?: number;
272
288
  /** Model's context window in tokens, when known. */