@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6

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 (109) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/cli.js +3799 -3729
  3. package/dist/types/async/job-manager.d.ts +8 -0
  4. package/dist/types/cli/bench-cli.d.ts +1 -7
  5. package/dist/types/cli/usage-cli.d.ts +1 -0
  6. package/dist/types/commands/usage.d.ts +7 -0
  7. package/dist/types/config/settings-schema.d.ts +19 -9
  8. package/dist/types/config/settings.d.ts +3 -2
  9. package/dist/types/discovery/helpers.d.ts +2 -2
  10. package/dist/types/extensibility/extensions/types.d.ts +36 -0
  11. package/dist/types/irc/bus.d.ts +4 -0
  12. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
  13. package/dist/types/modes/components/ask-dialog.d.ts +27 -0
  14. package/dist/types/modes/components/custom-editor.d.ts +3 -8
  15. package/dist/types/modes/components/index.d.ts +2 -1
  16. package/dist/types/modes/components/model-browser.d.ts +100 -0
  17. package/dist/types/modes/components/model-hub.d.ts +52 -0
  18. package/dist/types/modes/components/pause-screen.d.ts +43 -0
  19. package/dist/types/modes/components/session-selector.d.ts +13 -0
  20. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  21. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  22. package/dist/types/modes/controllers/input-controller.d.ts +2 -0
  23. package/dist/types/modes/interactive-mode.d.ts +3 -0
  24. package/dist/types/modes/queue-input.d.ts +8 -0
  25. package/dist/types/modes/shared.d.ts +1 -1
  26. package/dist/types/modes/types.d.ts +4 -0
  27. package/dist/types/session/agent-session.d.ts +1 -1
  28. package/dist/types/session/agent-storage.d.ts +57 -0
  29. package/dist/types/session/session-context.d.ts +9 -0
  30. package/dist/types/task/executor.d.ts +26 -13
  31. package/dist/types/task/index.d.ts +12 -11
  32. package/dist/types/task/label.d.ts +4 -0
  33. package/dist/types/task/repair-args.d.ts +8 -8
  34. package/dist/types/task/types.d.ts +31 -56
  35. package/dist/types/tools/ask.d.ts +12 -0
  36. package/dist/types/tools/conflict-detect.d.ts +17 -1
  37. package/dist/types/tools/job.d.ts +16 -0
  38. package/package.json +12 -12
  39. package/scripts/build-binary.ts +0 -1
  40. package/scripts/compile-binary.ts +4 -3
  41. package/src/async/job-manager.ts +9 -0
  42. package/src/cli/bench-cli.ts +7 -26
  43. package/src/cli/usage-cli.ts +11 -0
  44. package/src/commands/usage.ts +13 -2
  45. package/src/commit/agentic/tools/analyze-file.ts +2 -3
  46. package/src/config/settings-schema.ts +18 -7
  47. package/src/config/settings.ts +13 -4
  48. package/src/discovery/helpers.ts +3 -4
  49. package/src/extensibility/custom-tools/loader.ts +70 -37
  50. package/src/extensibility/extensions/types.ts +46 -0
  51. package/src/irc/bus.ts +61 -20
  52. package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
  53. package/src/modes/components/advisor-config.ts +32 -22
  54. package/src/modes/components/ask-dialog.ts +888 -0
  55. package/src/modes/components/custom-editor.test.ts +58 -1
  56. package/src/modes/components/custom-editor.ts +42 -11
  57. package/src/modes/components/index.ts +2 -1
  58. package/src/modes/components/model-browser.ts +769 -0
  59. package/src/modes/components/model-hub.ts +2002 -0
  60. package/src/modes/components/pause-screen.ts +208 -0
  61. package/src/modes/components/session-selector.ts +299 -42
  62. package/src/modes/components/tool-execution.ts +2 -0
  63. package/src/modes/components/usage-row.ts +5 -6
  64. package/src/modes/controllers/event-controller.ts +8 -2
  65. package/src/modes/controllers/extension-ui-controller.ts +252 -5
  66. package/src/modes/controllers/input-controller.ts +140 -6
  67. package/src/modes/controllers/selector-controller.ts +160 -97
  68. package/src/modes/controllers/tan-command-controller.ts +1 -1
  69. package/src/modes/controllers/todo-command-controller.ts +1 -2
  70. package/src/modes/interactive-mode.ts +8 -0
  71. package/src/modes/queue-input.ts +132 -0
  72. package/src/modes/shared.ts +1 -1
  73. package/src/modes/theme/theme.ts +3 -3
  74. package/src/modes/types.ts +4 -0
  75. package/src/modes/utils/ui-helpers.ts +50 -24
  76. package/src/prompts/agents/scout.md +0 -1
  77. package/src/prompts/agents/task.md +1 -1
  78. package/src/prompts/system/subagent-system-prompt.md +1 -5
  79. package/src/prompts/system/subagent-yield-reminder.md +10 -0
  80. package/src/prompts/system/task-label.md +23 -0
  81. package/src/prompts/tools/job.md +1 -1
  82. package/src/prompts/tools/task-summary.md +3 -0
  83. package/src/prompts/tools/task.md +17 -18
  84. package/src/session/agent-session.ts +186 -49
  85. package/src/session/agent-storage.ts +330 -3
  86. package/src/session/history-storage.ts +1 -34
  87. package/src/session/session-context.test.ts +73 -0
  88. package/src/session/session-context.ts +43 -26
  89. package/src/slash-commands/builtin-registry.ts +18 -0
  90. package/src/task/agents.ts +2 -0
  91. package/src/task/executor.ts +159 -46
  92. package/src/task/index.ts +377 -239
  93. package/src/task/label.ts +38 -0
  94. package/src/task/render.ts +74 -22
  95. package/src/task/repair-args.ts +20 -31
  96. package/src/task/spawn-policy.test.ts +4 -4
  97. package/src/task/types.ts +46 -66
  98. package/src/tools/ask.ts +233 -40
  99. package/src/tools/conflict-detect.ts +102 -5
  100. package/src/tools/index.ts +1 -0
  101. package/src/tools/irc.ts +20 -11
  102. package/src/tools/job.ts +158 -18
  103. package/src/tools/write.ts +70 -6
  104. package/src/vibe/runtime.ts +1 -1
  105. package/src/web/search/providers/browser-headers.ts +30 -13
  106. package/dist/types/modes/components/model-selector.d.ts +0 -37
  107. package/dist/types/tools/bash-command-fixup.d.ts +0 -3
  108. package/src/modes/components/model-selector.ts +0 -1291
  109. package/src/tools/bash-command-fixup.ts +0 -4
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tiny-model UI labels for spawned subagents.
3
+ */
4
+ import { logger, prompt } from "@oh-my-pi/pi-utils";
5
+ import type { ModelRegistry } from "../config/model-registry";
6
+ import type { Settings } from "../config/settings";
7
+ import taskLabelSystemPrompt from "../prompts/system/task-label.md" with { type: "text" };
8
+ import { generateSessionTitle } from "../utils/title-generator";
9
+
10
+ const TASK_LABEL_SYSTEM_PROMPT = prompt.render(taskLabelSystemPrompt);
11
+
12
+ /** Compresses a delegated assignment into a one-sentence UI label via the tiny title model — fired by the executor spawn path because the task wire schema no longer carries a `description`; null on empty input or failure. */
13
+ export async function generateTaskLabel(
14
+ assignment: string,
15
+ registry: ModelRegistry,
16
+ settings: Settings,
17
+ sessionId?: string,
18
+ ): Promise<string | null> {
19
+ const text = assignment.trim();
20
+ if (!text) return null;
21
+ try {
22
+ return await generateSessionTitle(
23
+ text,
24
+ registry,
25
+ settings,
26
+ sessionId,
27
+ undefined,
28
+ undefined,
29
+ TASK_LABEL_SYSTEM_PROMPT,
30
+ );
31
+ } catch (err) {
32
+ logger.debug("task-label: generation failed", {
33
+ sessionId,
34
+ error: err instanceof Error ? err.message : String(err),
35
+ });
36
+ return null;
37
+ }
38
+ }
@@ -677,6 +677,35 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
677
677
  return `Output: ${pairs.join(", ")}`;
678
678
  }
679
679
 
680
+ /**
681
+ * First line of a streamed `task` brief, trimmed — a row's secondary text.
682
+ * The args stream in token by token, so non-string values fall through to "".
683
+ */
684
+ function taskFirstLine(task: unknown): string {
685
+ if (typeof task !== "string") return "";
686
+ const trimmed = task.trim();
687
+ const newline = trimmed.indexOf("\n");
688
+ return newline === -1 ? trimmed : trimmed.slice(0, newline);
689
+ }
690
+
691
+ /**
692
+ * Header label for a task call while nothing has spawned yet: the flat form's
693
+ * `agent` type. Batch calls return undefined — each item row carries its own
694
+ * `⟨agent⟩` badge, so a joined list in the header would just repeat them.
695
+ */
696
+ function formatAgentHeaderLabel(args: Partial<TaskParams> | undefined): string | undefined {
697
+ if (!args) return undefined;
698
+ const flat = typeof args.agent === "string" ? args.agent.trim() : "";
699
+ return flat || undefined;
700
+ }
701
+
702
+ /** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */
703
+ function agentTypeBadge(agent: string | undefined, theme: Theme): string {
704
+ const trimmed = agent?.trim();
705
+ if (!trimmed || trimmed === "task") return "";
706
+ return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
707
+ }
708
+
680
709
  /**
681
710
  * Render the call preview lines for the single spawned agent. The
682
711
  * args stream in token by token, so every field access is defensive.
@@ -686,14 +715,15 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
686
715
  const bullet = theme.fg("dim", "•");
687
716
  const lines: string[] = [];
688
717
 
689
- const rawId = typeof args.id === "string" ? args.id.trim() : "";
690
- const idLabel = rawId ? formatTaskId(rawId) : "";
691
- const desc = typeof args.description === "string" ? args.description.trim() : "";
692
- if (idLabel || desc) {
718
+ const rawName = typeof args.name === "string" ? args.name.trim() : "";
719
+ const idLabel = rawName ? formatTaskId(rawName) : "";
720
+ const brief = taskFirstLine(args.task);
721
+ if (idLabel || brief) {
693
722
  let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
694
- if (desc) {
695
- line += `: ${theme.fg("muted", previewLine(desc, 64))}`;
723
+ if (brief) {
724
+ line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
696
725
  }
726
+ line += agentTypeBadge(args.agent, theme);
697
727
  lines.push(line);
698
728
  }
699
729
  lines.push(...renderTaskItemLines(args.tasks, theme));
@@ -707,7 +737,7 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
707
737
  const COLLAPSED_AGENT_LIMIT = 4;
708
738
 
709
739
  /**
710
- * Render the per-item list (`id` + ui `description`) for a batch call's
740
+ * Render the per-item list (`name` + `task` brief) for a batch call's
711
741
  * streaming preview. The args stream in token by token, so the array grows
712
742
  * over time and trailing entries may be partially parsed — every field access
713
743
  * is defensive.
@@ -719,15 +749,16 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
719
749
  const cap = Math.min(tasks.length, COLLAPSED_AGENT_LIMIT);
720
750
  const lines: string[] = [];
721
751
  for (let i = 0; i < cap; i++) {
722
- const task = tasks[i] as Partial<TaskItem> | undefined;
723
- const rawId = typeof task?.id === "string" ? task.id.trim() : "";
724
- const idLabel = rawId ? formatTaskId(rawId) : `#${i + 1}`;
752
+ const item = tasks[i] as Partial<TaskItem> | undefined;
753
+ const rawName = typeof item?.name === "string" ? item.name.trim() : "";
754
+ const idLabel = rawName ? formatTaskId(rawName) : `#${i + 1}`;
725
755
  let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
726
- const desc = typeof task?.description === "string" ? task.description.trim() : "";
727
- if (desc) {
728
- line += `: ${theme.fg("muted", previewLine(desc, 64))}`;
756
+ const brief = taskFirstLine(item?.task);
757
+ if (brief) {
758
+ line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
729
759
  }
730
- if (task?.isolated === true) {
760
+ line += agentTypeBadge(item?.agent, theme);
761
+ if (item?.isolated === true) {
731
762
  line += theme.fg("dim", " [isolated]");
732
763
  }
733
764
  lines.push(line);
@@ -760,7 +791,7 @@ function createAssignmentSectionRenderer(
760
791
  // `renderResult` receives the raw tool args (unlike `renderCall`, which is
761
792
  // fed through `repairTaskParams`), so undo any per-field double-encoding
762
793
  // here too. The repair is idempotent on already-clean text.
763
- const assignment = repairDoubleEncodedJsonString(typeof args?.assignment === "string" ? args.assignment : "").trim();
794
+ const assignment = repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : "").trim();
764
795
  if (!assignment) return undefined;
765
796
  return createMarkdownSectionRenderer(assignment, theme);
766
797
  }
@@ -795,7 +826,11 @@ export function renderCall(args: TaskParams, options: TaskRenderOptions, theme:
795
826
  // pending/hourglass icon would misread the call as something the turn
796
827
  // waits on.
797
828
  const header = renderStatusLine(
798
- { iconOverride: theme.styledSymbol("tool.task", "accent"), title: "Task", description: args.agent },
829
+ {
830
+ iconOverride: theme.styledSymbol("tool.task", "accent"),
831
+ title: "Task",
832
+ description: formatAgentHeaderLabel(args),
833
+ },
799
834
  theme,
800
835
  );
801
836
  const assignmentSection = createAssignmentSectionRenderer(args, theme);
@@ -883,6 +918,7 @@ function renderAgentProgress(
883
918
  } else {
884
919
  statusLine = `${indent}${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
885
920
  }
921
+ statusLine += agentTypeBadge(progress.agent, theme);
886
922
 
887
923
  // Show retry-blocked badge so the parent immediately sees that a child
888
924
  // is sleeping on a provider 429, not silently progressing. Wins over the
@@ -1215,7 +1251,7 @@ function renderAgentResult(
1215
1251
  let statusLine = `${prefix ? `${prefix} ` : ""}${theme.fg(iconColor, icon)} ${theme.fg(
1216
1252
  success && !needsWarning ? "text" : "accent",
1217
1253
  titlePart,
1218
- )} ${formatBadge(statusText, iconColor, theme)}`;
1254
+ )}${agentTypeBadge(result.agent, theme)} ${formatBadge(statusText, iconColor, theme)}`;
1219
1255
  const showBadge = settings.get("task.showResolvedModelBadge");
1220
1256
  statusLine = appendAgentStats(
1221
1257
  statusLine,
@@ -1460,7 +1496,7 @@ export function renderResult(
1460
1496
  ): Component {
1461
1497
  const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
1462
1498
  const details = result.details;
1463
- const agentLabel = args?.agent?.trim() || undefined;
1499
+ const agentLabel = formatAgentHeaderLabel(args);
1464
1500
  const assignmentSection = createAssignmentSectionRenderer(args, theme);
1465
1501
  const contextSection = createContextSectionRenderer(args, theme);
1466
1502
 
@@ -1515,10 +1551,11 @@ export function renderResult(
1515
1551
  const isError = aborted || failed;
1516
1552
  const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
1517
1553
  const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1518
- // Surface the dispatched agent type (e.g. `Reviewer`) alongside the count
1519
- // so the header reads `Task 1 agent: Reviewer`.
1554
+ // Header meta is the spawn count only; each row carries its own ⟨agent⟩
1555
+ // badge, so a joined type list here would repeat them. Before anything
1556
+ // spawns, fall back to the flat form's agent type from the call args.
1520
1557
  const countLabel = agentCount > 0 ? `${agentCount} ${agentCount === 1 ? "agent" : "agents"}` : undefined;
1521
- const metaLabel = countLabel ? (agentLabel ? `${countLabel}: ${agentLabel}` : countLabel) : agentLabel;
1558
+ const metaLabel = countLabel ?? agentLabel;
1522
1559
  const header = renderStatusLine(
1523
1560
  {
1524
1561
  icon: icon === "success" || icon === "running" ? undefined : icon,
@@ -1542,8 +1579,10 @@ export function renderResult(
1542
1579
  const frozen = options.renderContext?.frozen === true;
1543
1580
  const lines: string[] = [];
1544
1581
 
1582
+ // Result rows win once any exist; progress rows for spawns without a
1583
+ // result (a mixed call's async subset) render as a supplement below.
1545
1584
  const shouldRenderProgress =
1546
- Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
1585
+ Boolean(details.progress && details.progress.length > 0) && details.results.length === 0;
1547
1586
  if (shouldRenderProgress && details.progress) {
1548
1587
  const ordered = orderProgressForDisplay(details.progress);
1549
1588
  // Collapsed view keeps the live edge: finished rows sort to the top of
@@ -1570,6 +1609,19 @@ export function renderResult(
1570
1609
  );
1571
1610
  }
1572
1611
 
1612
+ // Mixed blocking+async call: async spawns never land in `results`
1613
+ // (their payloads deliver through jobs) — keep their rows visible
1614
+ // beside the finalized inline results, live while running and
1615
+ // settled once their jobs finish.
1616
+ const supplementalProgress = details.progress
1617
+ ? orderProgressForDisplay(
1618
+ details.progress.filter(progress => !details.results.some(res => res.id === progress.id)),
1619
+ )
1620
+ : [];
1621
+ for (const progress of supplementalProgress) {
1622
+ lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
1623
+ }
1624
+
1573
1625
  const summaryParts: string[] = [];
1574
1626
  if (abortedCount > 0) summaryParts.push(theme.fg("error", `${abortedCount} aborted`));
1575
1627
  if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
@@ -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 an `assignment` that should read
5
+ * `task` tool call, so a `task` field that should read
6
6
  *
7
7
  * # Role
8
8
  * You are a judge … "describe this" … return —
@@ -24,7 +24,8 @@
24
24
  * string.
25
25
  *
26
26
  * This is deliberately scoped to the task tool's natural-language fields
27
- * (`assignment`, `description`). It is NOT applied to code-bearing
27
+ * (`task`, shared `context`); identifier fields (`name`, `agent`)
28
+ * are never repaired. It is NOT applied to code-bearing
28
29
  * tools (write/edit/bash/search), where a backslash or quote is load-bearing
29
30
  * and a false-positive unescape would silently corrupt a file or command.
30
31
  */
@@ -78,52 +79,40 @@ export function repairDoubleEncodedJsonString(value: string): string {
78
79
  return typeof decoded === "string" && decoded !== value ? decoded : value;
79
80
  }
80
81
 
81
- /** Repair a single (possibly partial) task item's prose fields. */
82
- function repairTaskItem(task: TaskItem): TaskItem {
83
- if (task === null || typeof task !== "object") return task;
84
- const assignment =
85
- typeof task.assignment === "string" ? repairDoubleEncodedJsonString(task.assignment) : task.assignment;
86
- const description =
87
- typeof task.description === "string" ? repairDoubleEncodedJsonString(task.description) : task.description;
88
- if (assignment === task.assignment && description === task.description) return task;
89
- return { ...task, assignment, description };
82
+ /** Repair a single (possibly partial) task item's prose field (`task`). */
83
+ function repairTaskItem(item: TaskItem): TaskItem {
84
+ if (item === null || typeof item !== "object") return item;
85
+ const task = typeof item.task === "string" ? repairDoubleEncodedJsonString(item.task) : item.task;
86
+ if (task === item.task) return item;
87
+ return { ...item, task };
90
88
  }
91
89
 
92
90
  /**
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.
91
+ * Repair double-encoded prose in task-tool params (flat `task`, shared
92
+ * `context`, and each batch task item's `task`). Returns the same reference
93
+ * when nothing changed so callers can cheaply skip work. Defensive against
94
+ * partially-streamed args (missing/undefined fields, partial task arrays) so
95
+ * it is safe on the render path as well as on execution.
99
96
  */
100
97
  export function repairTaskParams(params: TaskParams): TaskParams {
101
98
  if (params === null || typeof params !== "object") return params;
102
99
 
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;
100
+ const task = typeof params.task === "string" ? repairDoubleEncodedJsonString(params.task) : params.task;
107
101
  const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
108
102
 
109
103
  let tasks = params.tasks;
110
104
  if (Array.isArray(params.tasks)) {
111
105
  let changed = false;
112
- const repaired = params.tasks.map(task => {
113
- const next = repairTaskItem(task);
114
- if (next !== task) changed = true;
106
+ const repaired = params.tasks.map(item => {
107
+ const next = repairTaskItem(item);
108
+ if (next !== item) changed = true;
115
109
  return next;
116
110
  });
117
111
  if (changed) tasks = repaired;
118
112
  }
119
113
 
120
- if (
121
- assignment === params.assignment &&
122
- description === params.description &&
123
- context === params.context &&
124
- tasks === params.tasks
125
- ) {
114
+ if (task === params.task && context === params.context && tasks === params.tasks) {
126
115
  return params;
127
116
  }
128
- return { ...params, assignment, description, context, tasks };
117
+ return { ...params, task, context, tasks };
129
118
  }
@@ -42,9 +42,9 @@ describe("task spawn policy surfaces", () => {
42
42
 
43
43
  it("uses the first allowed spawn as the schema default", () => {
44
44
  const schema = getTaskSchema({ isolationEnabled: false, batchEnabled: false, defaultAgent: "fact-finder" });
45
- const parsed = schema({ assignment: "check" });
45
+ const parsed = schema({ task: "check" });
46
46
 
47
- expect(parsed).toEqual({ agent: "fact-finder", assignment: "check" });
47
+ expect(parsed).toEqual({ agent: "fact-finder", task: "check" });
48
48
  });
49
49
 
50
50
  it("renders the restricted spawn default in the task description", async () => {
@@ -56,8 +56,8 @@ describe("task spawn policy surfaces", () => {
56
56
  const tool = await TaskTool.create(makeSession("fact-finder,oracle"));
57
57
  const description = tool.description;
58
58
 
59
- expect(description).toContain("Defaults to `fact-finder`");
59
+ expect(description).toContain("the general-purpose worker (`fact-finder`)");
60
60
  expect(description).toContain("Current spawn policy allows: `fact-finder`, `oracle`.");
61
- expect(description).not.toContain("Defaults to `task`");
61
+ expect(description).not.toContain("(`task`)");
62
62
  });
63
63
  });
package/src/task/types.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
1
  import type { Usage } from "@oh-my-pi/pi-ai";
3
2
  import { $env } from "@oh-my-pi/pi-utils";
4
3
  import { type BaseType, type } from "arktype";
5
4
  import type { AgentSessionEvent } from "../session/agent-session";
5
+ import type { ConfiguredThinkingLevel } from "../thinking";
6
6
  import type { NestedRepoPatch } from "./worktree";
7
7
 
8
8
  /** Source of an agent definition */
@@ -75,66 +75,53 @@ export interface SubagentLifecyclePayload {
75
75
  }
76
76
 
77
77
  /** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
78
- export const ROLE_LABEL_MAX = 80;
79
- /** Schema bound on the raw `role` input, before it is label-normalized at every use site. */
80
- export const ROLE_INPUT_MAX = 256;
81
- const ROLE_INPUT_SCHEMA = `string <= ${ROLE_INPUT_MAX}` as const;
78
+ export const LABEL_MAX = 80;
82
79
 
83
80
  export const taskItemSchema = type({
84
- "id?": "string",
85
- "description?": "string",
86
- "role?": ROLE_INPUT_SCHEMA,
87
- assignment: "string",
81
+ "name?": "string",
82
+ agent: "string = 'task'",
83
+ task: "string",
88
84
  "+": "delete",
89
85
  });
90
86
  const taskItemSchemaIsolated = type({
91
- "id?": "string",
92
- "description?": "string",
93
- "role?": ROLE_INPUT_SCHEMA,
94
- assignment: "string",
87
+ "name?": "string",
88
+ agent: "string = 'task'",
89
+ task: "string",
95
90
  "isolated?": "boolean",
96
91
  "+": "delete",
97
92
  });
98
93
 
99
94
  /** Single task item. Fields are optional defensively: args stream in token by token. */
100
95
  export interface TaskItem {
101
- /** Stable agent id; default = generated AdjectiveNoun. */
102
- id?: string;
103
- /** UI label, not seen by the subagent. */
104
- description?: string;
105
- /** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
106
- role?: string;
96
+ /** Stable agent name; becomes the registry/IRC id. Default = generated AdjectiveNoun. */
97
+ name?: string;
98
+ /** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
99
+ agent?: string;
107
100
  /** The work; required by the schema. */
108
- assignment?: string;
101
+ task?: string;
109
102
  /** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
110
103
  isolated?: boolean;
111
104
  }
112
105
 
113
106
  export const taskSchema = type({
107
+ "name?": "string",
114
108
  agent: "string = 'task'",
115
- "id?": "string",
116
- "description?": "string",
117
- "role?": ROLE_INPUT_SCHEMA,
118
- assignment: "string",
109
+ task: "string",
119
110
  "isolated?": "boolean",
120
111
  "+": "delete",
121
112
  });
122
113
  const taskSchemaNoIsolation = type({
114
+ "name?": "string",
123
115
  agent: "string = 'task'",
124
- "id?": "string",
125
- "description?": "string",
126
- "role?": ROLE_INPUT_SCHEMA,
127
- assignment: "string",
116
+ task: "string",
128
117
  "+": "delete",
129
118
  });
130
119
  const taskSchemaBatch = type({
131
- agent: "string = 'task'",
132
120
  context: "string",
133
121
  tasks: taskItemSchemaIsolated.array(),
134
122
  "+": "delete",
135
123
  });
136
124
  const taskSchemaBatchNoIsolation = type({
137
- agent: "string = 'task'",
138
125
  context: "string",
139
126
  tasks: taskItemSchema.array(),
140
127
  "+": "delete",
@@ -165,37 +152,44 @@ function createTaskSchema(options: {
165
152
  const agent = taskAgentSchemaRule(options.defaultAgent);
166
153
  if (options.batchEnabled) {
167
154
  if (options.isolationEnabled) {
168
- return type.raw({
155
+ const item = type.raw({
156
+ "name?": "string",
169
157
  agent,
158
+ task: "string",
159
+ "isolated?": "boolean",
160
+ "+": "delete",
161
+ });
162
+ return type.raw({
170
163
  context: "string",
171
- tasks: taskItemSchemaIsolated.array(),
164
+ tasks: item.array(),
172
165
  "+": "delete",
173
166
  });
174
167
  }
175
- return type.raw({
168
+ const item = type.raw({
169
+ "name?": "string",
176
170
  agent,
171
+ task: "string",
172
+ "+": "delete",
173
+ });
174
+ return type.raw({
177
175
  context: "string",
178
- tasks: taskItemSchema.array(),
176
+ tasks: item.array(),
179
177
  "+": "delete",
180
178
  });
181
179
  }
182
180
  if (options.isolationEnabled) {
183
181
  return type.raw({
182
+ "name?": "string",
184
183
  agent,
185
- "id?": "string",
186
- "description?": "string",
187
- "role?": ROLE_INPUT_SCHEMA,
188
- assignment: "string",
184
+ task: "string",
189
185
  "isolated?": "boolean",
190
186
  "+": "delete",
191
187
  });
192
188
  }
193
189
  return type.raw({
190
+ "name?": "string",
194
191
  agent,
195
- "id?": "string",
196
- "description?": "string",
197
- "role?": ROLE_INPUT_SCHEMA,
198
- assignment: "string",
192
+ task: "string",
199
193
  "+": "delete",
200
194
  });
201
195
  }
@@ -226,21 +220,17 @@ export function getTaskSchema(options: {
226
220
 
227
221
  /**
228
222
  * Runtime params union over both wire shapes. The model sees exactly one shape
229
- * (`{ agent, context, tasks[] }` when `task.batch` is on, `{ agent, ...item }`
223
+ * (`{ context, tasks[] }` when `task.batch` is on, `{ name?, agent?, task }`
230
224
  * otherwise); runtime stays permissive so internal callers and stale
231
225
  * transcripts using the flat form keep working under either setting.
232
226
  */
233
227
  export interface TaskParams {
234
- /** Agent type to spawn; omitted values resolve from the session spawn policy. */
228
+ /** Stable agent name (flat form). */
229
+ name?: string;
230
+ /** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
235
231
  agent?: string;
236
- /** Stable agent id (flat form); default = generated AdjectiveNoun. */
237
- id?: string;
238
- /** UI label (flat form), not seen by the subagent. */
239
- description?: string;
240
- /** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
241
- role?: string;
242
232
  /** The work (flat form). */
243
- assignment?: string;
233
+ task?: string;
244
234
  /** Batch form (`task.batch`): one subagent per item. */
245
235
  tasks?: TaskItem[];
246
236
  /** Batch form: shared background prepended to every assignment; required by the batch schema. */
@@ -254,11 +244,11 @@ export interface TaskParams {
254
244
  * `displayName`, or a system-prompt field. Collapses every run of whitespace
255
245
  * AND control/format characters — including U+0085 NEL, ESC/ANSI, and the
256
246
  * zero-width separators that `\s` misses — to a single space, then caps length.
257
- * So untrusted text (a spawn `role`, a peer activity gist) can neither break the
258
- * line, inject prompt structure, nor smuggle terminal escapes. Caps at `max`
259
- * characters (clamped to >= 1; default `ROLE_LABEL_MAX`), appending an ellipsis when truncated.
247
+ * So untrusted text (a generated task label, a peer activity gist) can neither
248
+ * break the line, inject prompt structure, nor smuggle terminal escapes. Caps at
249
+ * `max` characters (clamped to >= 1; default `LABEL_MAX`), appending an ellipsis when truncated.
260
250
  */
261
- export function oneLineLabel(text: string, max = ROLE_LABEL_MAX): string {
251
+ export function oneLineLabel(text: string, max = LABEL_MAX): string {
262
252
  const oneLine = text.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
263
253
  const cap = Math.max(1, max);
264
254
  // Count/cut by code point, not UTF-16 code unit, so truncation can never
@@ -267,16 +257,6 @@ export function oneLineLabel(text: string, max = ROLE_LABEL_MAX): string {
267
257
  return chars.length > cap ? `${chars.slice(0, cap - 1).join("")}…` : oneLine;
268
258
  }
269
259
 
270
- /**
271
- * Display name for a spawned subagent: its tailored `role` (label-normalized)
272
- * when one is given, else the agent type's name. Empty/whitespace roles fall
273
- * back to the agent name.
274
- */
275
- export function resolveSubagentDisplayName(role: string | undefined, agentName: string): string {
276
- const trimmed = role?.trim();
277
- return trimmed ? oneLineLabel(trimmed) : agentName;
278
- }
279
-
280
260
  /**
281
261
  * Whether an agent at `taskDepth` may still spawn children — i.e. it currently
282
262
  * holds the `task` tool. Mirrors the task-tool availability gate;
@@ -318,7 +298,7 @@ export interface AgentDefinition {
318
298
  tools?: string[];
319
299
  spawns?: string[] | "*";
320
300
  model?: string[];
321
- thinkingLevel?: ThinkingLevel;
301
+ thinkingLevel?: ConfiguredThinkingLevel;
322
302
  output?: unknown;
323
303
  blocking?: boolean;
324
304
  autoloadSkills?: string[];