pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -29,10 +29,23 @@ export interface RoutedSubagentTasks {
29
29
  warnings: string[];
30
30
  }
31
31
 
32
+ /** A recoverable selection error: the parent must correct the batch before spawn. */
33
+ export class SubagentRoutingError extends Error {
34
+ constructor(reason: string, readonly taskIds: string[], readonly allowedTypes: string[]) {
35
+ super([
36
+ reason,
37
+ `Tasks requiring a valid role: ${taskIds.join(", ")}.`,
38
+ "Set an explicit valid subagentType for these tasks and retry; defaultType is not an error fallback.",
39
+ `Available types: ${allowedTypes.join(", ") || "(none configured)"}.`,
40
+ ].join("\n"));
41
+ this.name = "SubagentRoutingError";
42
+ }
43
+ }
44
+
32
45
  const ROUTER_SYSTEM_PROMPT = [
33
46
  "You route Pi async sub-agent tasks to the best configured subagentType.",
34
47
  "Choose exactly one allowed type for each task. Use the allowed type descriptions as the source of truth.",
35
- "Prefer the most specific matching type over generic quick/deep. Use frontend for UI/UX implementation or visual frontend polish. For pure image inspection, use the lookup tool rather than subagents.",
48
+ "Prefer a matching project specialist. Otherwise use research for reading/review and evidence, implement for code/docs/tests/UI changes, verify for running checks, browser-qa for real-browser testing. Oracle is a deliberate strong second opinion, not the default for difficult work.",
36
49
  "Return only strict JSON with this shape: {\"routes\":[{\"id\":\"task-id\",\"subagentType\":\"type\"}]}",
37
50
  "Do not include markdown, comments, explanations, or unknown types.",
38
51
  ].join("\n");
@@ -43,31 +56,37 @@ export async function routeSubagentTasks(
43
56
  ctx: SubagentRoutingContext,
44
57
  signal?: AbortSignal,
45
58
  ): Promise<RoutedSubagentTasks> {
46
- const fallbackTasks = () => tasks.map((task) => withFallbackType(task, config));
59
+ if (signal?.aborted) throw new Error("Aborted");
60
+ // Validate even the fast path: an explicit typo must not bypass profiles.
61
+ tasks = tasks.map((task) => hasText(task.subagentType) && task.subagentType !== task.subagentType.trim()
62
+ ? { ...task, subagentType: task.subagentType.trim() }
63
+ : task);
64
+ const invalidTasks = tasks.filter((task) => hasText(task.subagentType)
65
+ && !Object.prototype.hasOwnProperty.call(config.types, task.subagentType));
66
+ if (invalidTasks.length > 0) {
67
+ throw routingError(`Unknown subagentType: ${invalidTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`, invalidTasks, config);
68
+ }
47
69
  const autoTasks = tasks.filter((task) => !hasText(task.subagentType));
48
70
  if (autoTasks.length === 0) return { tasks, usedLlm: false, routes: {}, warnings: [] };
49
71
 
50
72
  const routing = resolveSubagentRoutingConfig(config);
51
73
  if (!routing.enabled) {
52
- return { tasks: fallbackTasks(), usedLlm: false, routes: {}, warnings: ["LLM sub-agent routing is disabled; used defaultType fallback."] };
74
+ throw routingError("LLM sub-agent routing is disabled.", autoTasks, config);
53
75
  }
54
- if (signal?.aborted) throw new Error("Aborted");
76
+ if (Object.keys(config.types).length === 0) throw routingError("No sub-agent types are configured.", autoTasks, config);
55
77
 
56
78
  try {
57
79
  const candidates = await resolveRoutingModels(ctx, routing);
58
80
  if (candidates.length === 0) {
59
- const warning = `LLM sub-agent routing model unavailable (${routing.model}); used defaultType fallback.`;
60
- notifyRoutingWarning(ctx, routing, warning);
61
- return { tasks: fallbackTasks(), usedLlm: false, routes: {}, warnings: [warning] };
81
+ throw routingError(`LLM sub-agent routing model unavailable (${routing.model}).`, autoTasks, config);
62
82
  }
63
83
 
64
84
  const prompt = buildRoutingPrompt(autoTasks, config, routing);
65
85
  const failures: string[] = [];
66
- let response: RoutingResponse | undefined;
67
86
  for (const candidate of candidates) {
68
87
  if (signal?.aborted) throw new Error("Aborted");
69
88
  try {
70
- response = await completeWithModelRegistry(
89
+ const response = await completeWithModelRegistry(
71
90
  ctx.modelRegistry,
72
91
  candidate.model,
73
92
  {
@@ -91,48 +110,38 @@ export async function routeSubagentTasks(
91
110
  timeoutMs: routing.timeoutMs,
92
111
  },
93
112
  );
94
- break;
113
+ if (signal?.aborted || response.stopReason === "aborted") throw new Error("Aborted");
114
+ if (response.stopReason === "error") throw new Error(response.errorMessage || "Router provider returned an error.");
115
+ const routes = parseRoutingResponse(responseText(response), config, autoTasks);
116
+ const missingTasks = autoTasks.filter((task) => !Object.prototype.hasOwnProperty.call(routes, task.id));
117
+ if (missingTasks.length > 0) {
118
+ throw new Error(`Returned ${Object.keys(routes).length}/${autoTasks.length} valid route(s); missing: ${missingTasks.map((task) => task.id).join(", ")}.`);
119
+ }
120
+ return {
121
+ usedLlm: true,
122
+ routes,
123
+ warnings: [],
124
+ tasks: tasks.map((task) => hasText(task.subagentType) ? task : { ...task, subagentType: routes[task.id] }),
125
+ };
95
126
  } catch (error) {
96
127
  if (signal?.aborted || isAbortError(error)) throw error;
97
128
  failures.push(`${currentModelRef(candidate.model) ?? "(unknown)"}: ${errorMessage(error)}`);
98
129
  }
99
130
  }
100
131
 
101
- if (!response) {
102
- const warning = `LLM sub-agent routing failed (${failures.join("; ")}); used defaultType fallback.`;
103
- notifyRoutingWarning(ctx, routing, warning);
104
- return { tasks: fallbackTasks(), usedLlm: false, routes: {}, warnings: [warning] };
105
- }
106
-
107
- const routes = parseRoutingResponse(responseText(response), config, autoTasks);
108
- const warnings = Object.keys(routes).length === autoTasks.length
109
- ? []
110
- : [`LLM sub-agent routing returned ${Object.keys(routes).length}/${autoTasks.length} valid route(s); missing tasks used defaultType fallback.`];
111
- for (const warning of warnings) notifyRoutingWarning(ctx, routing, warning);
112
- return {
113
- usedLlm: true,
114
- routes,
115
- warnings,
116
- tasks: tasks.map((task) => applyRoute(task, config, routes)),
117
- };
132
+ throw routingError(`LLM sub-agent routing failed (${failures.join("; ")}).`, autoTasks, config);
118
133
  } catch (error) {
119
134
  if (signal?.aborted || isAbortError(error)) throw error;
120
- const warning = `LLM sub-agent routing failed (${errorMessage(error)}); used defaultType fallback.`;
121
- notifyRoutingWarning(ctx, routing, warning);
122
- return { tasks: fallbackTasks(), usedLlm: false, routes: {}, warnings: [warning] };
135
+ const failure = error instanceof SubagentRoutingError
136
+ ? error
137
+ : routingError(`LLM sub-agent routing failed (${errorMessage(error)}).`, autoTasks, config);
138
+ notifyRoutingWarning(ctx, routing, failure.message);
139
+ throw failure;
123
140
  }
124
141
  }
125
142
 
126
- function applyRoute(task: AgentTask, config: SubagentConfig, routes: Record<string, string>): AgentTask {
127
- if (hasText(task.subagentType)) return task;
128
- const fallback = withFallbackType(task, config);
129
- return routes[task.id] ? { ...fallback, subagentType: routes[task.id] } : fallback;
130
- }
131
-
132
- function withFallbackType(task: AgentTask, config: SubagentConfig): AgentTask {
133
- if (hasText(task.subagentType)) return task;
134
- const fallback = defaultSubagentType(config);
135
- return fallback ? { ...task, subagentType: fallback } : task;
143
+ function routingError(reason: string, tasks: AgentTask[], config: SubagentConfig): SubagentRoutingError {
144
+ return new SubagentRoutingError(reason, tasks.map((task) => task.id), Object.keys(config.types).sort());
136
145
  }
137
146
 
138
147
  function buildRoutingPrompt(tasks: AgentTask[], config: SubagentConfig, routing: ResolvedSubagentRoutingConfig): string {
@@ -144,7 +153,7 @@ function buildRoutingPrompt(tasks: AgentTask[], config: SubagentConfig, routing:
144
153
  return `- ${name}: ${profile.description ?? "No description; use only when the task explicitly names this type."}`;
145
154
  }),
146
155
  "",
147
- `Default fallback if genuinely ambiguous: ${defaultSubagentType(config) ?? "none"}`,
156
+ `Preferred type only for genuinely ambiguous tasks: ${defaultSubagentType(config) ?? "none"}`,
148
157
  "",
149
158
  "Tasks:",
150
159
  JSON.stringify(tasks.map((task) => ({
@@ -184,8 +193,6 @@ interface RoutingCandidate {
184
193
  env?: Record<string, string>;
185
194
  }
186
195
 
187
- type RoutingResponse = Awaited<ReturnType<typeof completeWithModelRegistry>>;
188
-
189
196
  async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): Promise<{
190
197
  model: Model<Api>;
191
198
  apiKey?: string;
@@ -212,7 +219,7 @@ function parseRoutingResponse(raw: string, config: SubagentConfig, tasks: AgentT
212
219
  const parsed = parseJsonObject(raw);
213
220
  const allowedTypes = new Map(Object.keys(config.types).map((name) => [name.toLowerCase(), name]));
214
221
  const taskIds = new Set(tasks.map((task) => task.id));
215
- const routes: Record<string, string> = {};
222
+ const routes: Record<string, string> = Object.create(null);
216
223
  if (Array.isArray(parsed)) collectRouteArray(parsed, taskIds, allowedTypes, routes);
217
224
  else if (typeof parsed === "string" && tasks.length === 1) addRoute(routes, taskIds, allowedTypes, tasks[0]!.id, parsed);
218
225
  else if (isRecord(parsed)) {
@@ -289,7 +296,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
289
296
  }
290
297
 
291
298
  function isAbortError(error: unknown): boolean {
292
- return error instanceof Error && /abort/i.test(error.name || error.message);
299
+ return error instanceof Error && (/abort/i.test(error.name) || /^aborted$/i.test(error.message));
293
300
  }
294
301
 
295
302
  function errorMessage(error: unknown): string {
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { selectSuitableToolsForModel } from "../../lib/tool-args.js";
6
+ import { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./browser-qa.js";
6
7
  import { validateBasename } from "./paths.js";
7
8
  import { getPiInvocation } from "./pi-invocation.js";
8
9
  import { writePromptFile } from "./prompt.js";
@@ -88,7 +89,10 @@ export function spawnAgent(
88
89
  // detached subprocesses do not depend on persisted local pi settings.
89
90
  const persistSessions = shouldPersistSubagentSessions();
90
91
  const sessionDir = persistSessions ? getAgentSessionDir(agentDir) : undefined;
91
- const forwardedExtraArgs = options.isolatedSkills?.length ? withoutSkillArgs(extraArgs) : extraArgs;
92
+ // QA instructions now live in the agent prompt, but normal skill discovery
93
+ // must stay disabled even when no additional skill is configured.
94
+ const isolateSkills = task.subagentType === "browser-qa" || Boolean(options.isolatedSkills?.length);
95
+ const forwardedExtraArgs = isolateSkills ? withoutSkillArgs(extraArgs) : extraArgs;
92
96
  if (sessionDir) fs.mkdirSync(sessionDir, { recursive: true });
93
97
  const piArgs: string[] = ["--mode", "rpc"];
94
98
  if (sessionDir) piArgs.push("--session-dir", sessionDir);
@@ -100,9 +104,9 @@ export function spawnAgent(
100
104
  if (usesAntigravityModel(task.model, forwardedExtraArgs)) {
101
105
  piArgs.push("--extension", getAntigravityAuthExtensionPath());
102
106
  }
103
- if (options.isolatedSkills && options.isolatedSkills.length > 0) {
107
+ if (isolateSkills) {
104
108
  piArgs.push("--no-skills");
105
- for (const skillPath of options.isolatedSkills) piArgs.push("--skill", skillPath);
109
+ for (const skillPath of options.isolatedSkills ?? []) piArgs.push("--skill", skillPath);
106
110
  }
107
111
  const envModel = task.model || getEnvModel();
108
112
  if (envModel) piArgs.push("--model", envModel);
@@ -812,7 +816,11 @@ function subagentEnvironment(env: NodeJS.ProcessEnv, agentDir?: string): NodeJS.
812
816
  ]),
813
817
  };
814
818
  delete result[SUBAGENT_AGENT_DIR_ENV];
815
- if (agentDir) result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
819
+ delete result[BROWSER_QA_RUNNER_ENV];
820
+ if (agentDir) {
821
+ result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
822
+ result[BROWSER_QA_RUNNER_ENV] = getBrowserQaRunnerPath();
823
+ }
816
824
  return result;
817
825
  }
818
826
 
@@ -19,6 +19,7 @@ import {
19
19
  } from "./lib.js";
20
20
  import { buildUltraworkPrompt, isUltraworkEnvEnabled, registerCommands } from "./commands.js";
21
21
  import { agentStrategyPrompt, appendAgentStrategyPrompt } from "./core/agent-strategy.js";
22
+ import { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
22
23
  import {
23
24
  bridgeImageAttachments,
24
25
  removeImageAttachmentBridgeState,
@@ -190,9 +191,13 @@ export default function (pi: ExtensionAPI) {
190
191
  customPrompt: Boolean(event?.systemPromptOptions?.customPrompt),
191
192
  });
192
193
  const visionPrompt = visionCapabilityPrompt(event, ctx);
193
- if (!strategyPrompt && !visionPrompt) return undefined;
194
+ const catalogPrompt = selectedToolsInclude(event, "subagents")
195
+ ? subagentCatalogPrompt((ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd())
196
+ : undefined;
197
+ if (!strategyPrompt && !visionPrompt && !catalogPrompt) return undefined;
194
198
  let systemPrompt = event.systemPrompt ?? "";
195
199
  if (strategyPrompt) systemPrompt = appendAgentStrategyPrompt(systemPrompt, strategyPrompt);
200
+ if (catalogPrompt) systemPrompt = appendAgentStrategyPrompt(systemPrompt, catalogPrompt);
196
201
  if (visionPrompt) systemPrompt = appendAgentStrategyPrompt(systemPrompt, visionPrompt);
197
202
  return { systemPrompt };
198
203
  });
@@ -281,6 +286,11 @@ function safeLoadSubagentConfig(cwd: string) {
281
286
  }
282
287
  }
283
288
 
289
+ function subagentCatalogPrompt(cwd: string): string | undefined {
290
+ const config = safeLoadSubagentConfig(cwd);
291
+ return config ? buildSubagentCatalogPrompt(config) : undefined;
292
+ }
293
+
284
294
  function startupSubagentPresetList(cwd = process.cwd()): string {
285
295
  try {
286
296
  const config = loadSubagentConfig(cwd);
@@ -23,7 +23,7 @@ export {
23
23
  DEFAULT_ROUTING_CONFIG,
24
24
  existingSubagentConfigFiles,
25
25
  getDefaultSubagentConfigPath,
26
- getBrowserQaSkillPath,
26
+ getBuiltinSubagentDefinitionsDir,
27
27
  getSubagentConfigInitTargetPath,
28
28
  getSubagentConfigSamplePath,
29
29
  isBlindModelRef,
@@ -34,8 +34,12 @@ export {
34
34
  selectSubagentType,
35
35
  shouldForceCurrentSubagentModel,
36
36
  } from "./core/config.js";
37
- export { routeSubagentTasks } from "./core/routing.js";
37
+ export { routeSubagentTasks, SubagentRoutingError } from "./core/routing.js";
38
+ export { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./core/browser-qa.js";
38
39
  export type { RoutedSubagentTasks, SubagentRoutingContext } from "./core/routing.js";
40
+ export { agentDefinitionFiles, agentNameFromFilename, parseAgentMarkdown, projectAgentDefinitionFiles, readAgentDefinitionsFromDir, readProjectAgentDefinitions } from "./core/agents-dir.js";
41
+ export type { AgentDefinition, ProjectAgentDefinition } from "./core/agents-dir.js";
42
+ export { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
39
43
  export type { SubagentPresetSelectionState } from "./core/presets.js";
40
44
  export { getActiveSubagentPresetName, getSessionSubagentPresetOverride, getSubagentPresetSelectionPath, loadSubagentPresetSelection, saveSubagentPresetSelection, setActiveSubagentPreset, setSessionSubagentPresetOverride } from "./core/presets.js";
41
45
  export { isQuotaLimitCompletion, nextFallbackModel, rememberSessionModelFallback, resetSessionModelFallbacks, selectSessionModelWithFallback } from "./core/model-fallback.js";
@@ -4,6 +4,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "@earendil-works/pi-ai";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
7
+ import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
8
+ import { selectAvailableAgentModels } from "../core/model-selection.js";
9
+ import { SubagentModelSelectionError } from "../core/config.js";
7
10
  import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, Semaphore, SpawnedAgent } from "../lib.js";
8
11
  import {
9
12
  createRunDir,
@@ -19,6 +22,7 @@ import {
19
22
  resolveAgentTaskConfig,
20
23
  resolveRunDir,
21
24
  routeSubagentTasks,
25
+ SubagentRoutingError,
22
26
  selectSessionModelWithFallback,
23
27
  shouldForceCurrentSubagentModel,
24
28
  spawnAgent,
@@ -172,7 +176,7 @@ const AgentTaskSchema = Type.Object({
172
176
  id: Type.Optional(Type.String({ description: "Short identifier for this agent (used as directory name). If omitted, the spawn action assigns agent-1, agent-2, etc." })),
173
177
  task: Type.String({ description: "Focused task description for the sub-agent" }),
174
178
  scope: Type.Optional(Type.String({ description: "Relevant files/areas for this task" })),
175
- subagentType: Type.Optional(Type.String({ description: "Logical sub-agent type/profile from config. Usually omit this so the router selects from the current config; set only for an explicit user-requested role, deterministic tests, or another concrete override." })),
179
+ subagentType: Type.Optional(Type.String({ description: SUBAGENT_TYPE_SELECTION_GUIDANCE })),
176
180
  model: Type.Optional(Type.String({ description: "Explicit model override for this sub-agent. Prefer subagentType for reusable routing." })),
177
181
  thinking: Type.Optional(Type.String({ description: "Per-agent thinking level override (off, minimal, low, medium, high, xhigh, max)." })),
178
182
  promptAppend: Type.Optional(Type.String({ description: "Extra prompt instructions appended after the generated/type prompt." })),
@@ -209,10 +213,6 @@ export function registerSpawnTool(
209
213
  const parentSession = typeof ctx.sessionManager?.getSessionFile === "function"
210
214
  ? ctx.sessionManager.getSessionFile()
211
215
  : undefined;
212
- const runDir = params.runDir
213
- ? resolveRunDir(ctx.cwd, params.runDir)
214
- : createRunDir(ctx.cwd, params.slug);
215
-
216
216
  const normalized = normalizeAgentTasks(params.tasks);
217
217
  if (normalized.error) {
218
218
  return {
@@ -240,21 +240,49 @@ export function registerSpawnTool(
240
240
  isError: true,
241
241
  };
242
242
  }
243
- const routed = await routeSubagentTasks(normalized.tasks ?? [], config, ctx as any, signal ?? undefined);
243
+ let routed: Awaited<ReturnType<typeof routeSubagentTasks>>;
244
+ try {
245
+ routed = await routeSubagentTasks(normalized.tasks ?? [], config, ctx as any, signal ?? undefined);
246
+ } catch (error) {
247
+ if (!(error instanceof SubagentRoutingError)) throw error;
248
+ return {
249
+ content: [{ type: "text", text: `No agents were launched. Correct the roles and resubmit the whole batch.\n${error.message}` }],
250
+ details: { error: "subagent_routing", taskIds: error.taskIds, allowedTypes: error.allowedTypes },
251
+ isError: true,
252
+ };
253
+ }
244
254
  const timeoutMs = timeoutMsFromSeconds(params.timeoutSeconds);
245
255
  const parentModel = currentModelRef((ctx as { model?: unknown }).model);
246
- const resolvedTasks = routed.tasks.map((task) => applySessionModelFallback(
247
- resolveAgentTaskConfig(task, config, {
248
- preset: activePreset,
249
- thinking: params.thinking,
250
- extraArgs: Array.isArray(params.extraArgs) ? params.extraArgs : [],
251
- forcedModel,
252
- parentModel,
253
- timeoutMs,
254
- }),
255
- ));
256
+ let resolvedTasks: ResolvedAgentTaskConfig[];
257
+ try {
258
+ resolvedTasks = await Promise.all(routed.tasks.map(async (task) => applySessionModelFallback(await selectAvailableAgentModels(
259
+ resolveAgentTaskConfig(task, config, {
260
+ preset: activePreset,
261
+ thinking: params.thinking,
262
+ extraArgs: Array.isArray(params.extraArgs) ? params.extraArgs : [],
263
+ forcedModel,
264
+ parentModel,
265
+ timeoutMs,
266
+ }), config, ctx.modelRegistry, signal,
267
+ ))));
268
+ } catch (error) {
269
+ if (!(error instanceof SubagentModelSelectionError)) throw error;
270
+ return { content: [{ type: "text", text: error.message }], details: { error: "subagent_model_selection" }, isError: true };
271
+ }
256
272
  const tasks: AgentTask[] = resolvedTasks.map((resolved) => resolved.task);
257
- const taskPreviews = toTaskPreviews(tasks);
273
+ if (signal?.aborted) throw new Error("Aborted");
274
+ if (signal?.aborted) throw new Error("Aborted");
275
+ // Resolve the entire batch before creating run state or launching children.
276
+ const runDir = params.runDir
277
+ ? resolveRunDir(ctx.cwd, params.runDir)
278
+ : createRunDir(ctx.cwd, params.slug);
279
+ const taskPreviews = toTaskPreviews(tasks).map((preview, index) => {
280
+ // tasks[i] === resolvedTasks[i].task (positional), so the resolved
281
+ // profile's icon rides along into every preview consumer (tool render
282
+ // details, live-state events, and the desktop subagents panel).
283
+ const icon = resolvedTasks[index]?.profile?.icon;
284
+ return icon ? { ...preview, icon } : preview;
285
+ });
258
286
  const results: { id: string; pid: number; agentDir: string }[] = [];
259
287
  const maxConcurrent = config.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
260
288
  const semaphore = getProjectSemaphore(ctx.cwd, maxConcurrent);
@@ -365,7 +393,7 @@ export function registerSpawnTool(
365
393
 
366
394
  renderResult(result, opts, theme) {
367
395
  const details = result.details as SubagentRunRenderDetails | undefined;
368
- if (!details) {
396
+ if (!details || ("isError" in result && result.isError === true)) {
369
397
  const fallback = result.content[0] && result.content[0].type === "text" ? result.content[0].text : "(no output)";
370
398
  return new Text(fallback, 0, 0);
371
399
  }
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@earendil-works/pi-ai";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { asyncSubagentToolDescriptions } from "../../tool-descriptions.js";
5
+ import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
5
6
  import { hasIndexedProjectRoot } from "../../lib/project.js";
6
7
  import type { AgentCompletionHandler } from "../lib.js";
7
8
  import { DEFAULT_SPAWN_WATCH_SECONDS, INLINE_RENDERING } from "../constants.js";
@@ -19,7 +20,7 @@ const AgentTaskSchema = Type.Object({
19
20
  id: Type.Optional(Type.String({ description: "Short identifier for this agent (used as directory name). If omitted, assigns agent-1, agent-2, etc." })),
20
21
  task: Type.String({ description: "Focused task description for the sub-agent" }),
21
22
  scope: Type.Optional(Type.String({ description: "Relevant files/areas for this task" })),
22
- subagentType: Type.Optional(Type.String({ description: "Logical sub-agent type/profile from config. Usually omit this so the router selects from the current config; set only for an explicit user-requested role, deterministic tests, or another concrete override." })),
23
+ subagentType: Type.Optional(Type.String({ description: SUBAGENT_TYPE_SELECTION_GUIDANCE })),
23
24
  model: Type.Optional(Type.String({ description: "Explicit model override for this sub-agent. Prefer subagentType for reusable routing." })),
24
25
  thinking: Type.Optional(Type.String({ description: "Per-agent thinking level override (off, minimal, low, medium, high, xhigh, max)." })),
25
26
  promptAppend: Type.Optional(Type.String({ description: "Extra prompt instructions appended after the generated/type prompt." })),
@@ -141,7 +142,7 @@ export function registerSubagentsTool(
141
142
 
142
143
  renderResult(result, _opts, theme) {
143
144
  const details = result.details as SubagentRunRenderDetails | undefined;
144
- if (!details) return new Text(firstText(result), 0, 0);
145
+ if (!details || ("isError" in result && result.isError === true)) return new Text(firstText(result), 0, 0);
145
146
  if (details.mode === "spawn" && Array.isArray(details.agents) && theme) {
146
147
  return renderSubagentSpawnPrompts(details, _opts, theme);
147
148
  }
@@ -14,6 +14,8 @@ export interface AgentTaskPreview {
14
14
  task?: string;
15
15
  scope?: string;
16
16
  model?: string;
17
+ /** Agent icon name from the resolved subagentType profile; rendered by UIs. */
18
+ icon?: string;
17
19
  }
18
20
 
19
21
  export interface SubagentRunRenderDetails {