@bacnh85/pi-subagent 0.8.1 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 (2026-07-16)
4
+
5
+ ### Model routing
6
+
7
+ - Bundled roles now select the first authenticated model from an ordered preference list, with the authenticated parent model as the final fallback.
8
+ - Added read-only `planner` and focused `tester` roles for consequential design and cheap routine verification.
9
+ - Agent files accept `models` as a YAML array or comma-separated string; legacy `model` remains the explicit first choice.
10
+
11
+ ## 0.8.2 (2026-07-16)
12
+
13
+ ### Reliability
14
+
15
+ - Transient provider and transport failures receive one bounded SDK retry. Retrying Codex WebSocket failures uses the session's SSE fallback, waits through retrying `agent_end` events, clears recovered error state, preserves nonzero failure exit codes, and reports explicit timeout messages.
16
+
3
17
  ## 0.8.1 (2026-07-15)
4
18
 
5
19
  ### Review handoff
package/README.md CHANGED
@@ -14,14 +14,16 @@ Requires Node.js >= 20.18.
14
14
 
15
15
  ## Bundled roles
16
16
 
17
- | Role | Model | Thinking | Tools |
17
+ | Role | Ordered model preferences | Thinking | Tools |
18
18
  | --- | --- | --- | --- |
19
- | `scout` | parent model | low | read, grep, find, ls |
20
- | `reviewer` | parent model | high | read, grep, find, ls |
21
- | `worker` | parent model | medium | read, bash, edit, write, grep, find, ls |
22
- | `general-purpose` | parent model | off | read, bash, edit, write, grep, find, ls |
19
+ | `scout` | `opencode-go/deepseek-v4-flash` `openai-codex/gpt-5.6-luna` → `opencode-go/mimo-v2.5` | low | read, grep, find, ls |
20
+ | `tester` | `openai-codex/gpt-5.6-luna` `opencode-go/mimo-v2.5` → `opencode-go/deepseek-v4-flash` | low | read, bash, grep, find, ls |
21
+ | `worker` | `openai-codex/gpt-5.6-terra` `opencode-go/deepseek-v4-pro` | medium | read, bash, edit, write, grep, find, ls |
22
+ | `general-purpose` | `openai-codex/gpt-5.6-terra` `opencode-go/deepseek-v4-pro` | medium | read, bash, edit, write, grep, find, ls |
23
+ | `planner` | `openai-codex/gpt-5.6-sol` → `opencode-go/deepseek-v4-pro` | high | read, grep, find, ls |
24
+ | `reviewer` | `openai-codex/gpt-5.6-sol` → `opencode-go/deepseek-v4-pro` | high | read, grep, find, ls |
23
25
 
24
- Bundled roles inherit the parent model so they work with the account already active in Pi. User/project agent files may override `model` and `thinking`.
26
+ Each role uses the first authenticated preference available through Pi's model registry, then falls back to the authenticated parent model. User/project agent files remain stronger overrides and may set legacy `model`, ordered `models`, and `thinking`.
25
27
 
26
28
  ## Agent files
27
29
 
@@ -32,7 +34,8 @@ Create `~/.pi/agent/agents/*.md` or `.pi/agents/*.md`:
32
34
  name: scout-fast
33
35
  description: Locate relevant files and symbols
34
36
  tools: read, grep, find, ls
35
- model: optional-provider/optional-model
37
+ model: openai-codex/gpt-5.6-luna
38
+ models: opencode-go/mimo-v2.5, opencode-go/deepseek-v4-flash
36
39
  ---
37
40
 
38
41
  Return concise evidence with file/symbol anchors.
@@ -159,6 +162,7 @@ The raw `stopReason` from the Pi SDK is preserved in the result.
159
162
  - **Parent cancellation:** Aborting the parent tool call cancels all children.
160
163
  - **Sibling cancellation:** In parallel mode with `abortOnFailure: true`, the first failed task cancels running siblings.
161
164
  - **Timeout vs. abort:** Timeout errors set `status: "timeout"` and `stopReason: "timeout"`; parent cancellation sets `status: "aborted"`.
165
+ - **Transient provider failures:** One automatic retry runs within the same timeout; Codex WebSocket failures use the SDK's SSE fallback on retry.
162
166
  - **Transport idle:** The parent tool receives periodic progress heartbeats while a child runs; these do not extend its timeout.
163
167
 
164
168
  ## Extension contract
package/agent-format.md CHANGED
@@ -18,8 +18,11 @@ Project agents override user agents with the same name when `agentScope: "both"`
18
18
  ---
19
19
  name: my-agent # Required. Unique identifier (kebab-case).
20
20
  description: ... # Required. When to use this agent.
21
- tools: read, grep, ... # Optional. Comma-separated tool names. Defaults to all.
22
- model: provider/model # Optional. Defaults to parent's model.
21
+ tools: read, grep, ... # Optional. Comma-separated tool names. Defaults to all.
22
+ model: provider/model # Optional explicit first choice (legacy compatible).
23
+ models: # Optional ordered fallbacks; comma form also accepted.
24
+ - provider/fast-model
25
+ - provider/backup-model
23
26
  thinking: low # Optional: off|minimal|low|medium|high|xhigh|max.
24
27
  sandbox: read-only # Optional: read-only | workspace-write. Auto-derives tool restrictions.
25
28
  color: cyan # Optional: red|blue|green|yellow|purple|orange|pink|cyan.
@@ -62,13 +65,14 @@ Read-only service execution (used by `pi-review`) restricts tools to the read-on
62
65
 
63
66
  ## Model Resolution
64
67
 
65
- Model IDs are resolved via `getModel("provider", "id")`. Common values:
66
- - `claude-haiku-4-5` (Anthropic Haiku — fast, cheap)
67
- - `claude-sonnet-4-20250514` (Anthropic Sonnet — balanced)
68
- - `gpt-4o` (OpenAI)
69
- - Any model available in your pi configuration.
68
+ Pi selects the first authenticated/configured model reported by the parent session's `ModelRegistry`. Resolution order is legacy `model`, then each `models` entry, then the authenticated parent model. Duplicate candidates are ignored. If no candidate is available, the error lists every attempted model.
70
69
 
71
- If not specified, defaults to the parent session's model.
70
+ ```yaml
71
+ model: openai-codex/gpt-5.6-terra
72
+ models: opencode-go/deepseek-v4-pro, opencode-go/mimo-v2.5
73
+ ```
74
+
75
+ The registry includes OAuth subscriptions, API-key subscriptions such as OpenCode Go, environment/runtime credentials, and custom `models.json` providers. Use provider-qualified IDs for predictable routing.
72
76
 
73
77
  ## Instruction handoff
74
78
 
@@ -2,6 +2,10 @@
2
2
  name: general-purpose
3
3
  description: General-purpose sub-agent for any delegated task. Use when no specialized agent fits. Good for complex research, multi-step operations, and code modifications.
4
4
  tools: read, bash, edit, write, grep, find, ls
5
+ models:
6
+ - openai-codex/gpt-5.6-terra
7
+ - opencode-go/deepseek-v4-pro
8
+ thinking: medium
5
9
  color: yellow
6
10
  ---
7
11
 
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: planner
3
+ description: Read-only planning and architecture specialist. Use for consequential design, tradeoff analysis, and implementation plans.
4
+ tools: read, grep, find, ls
5
+ models:
6
+ - openai-codex/gpt-5.6-sol
7
+ - opencode-go/deepseek-v4-pro
8
+ thinking: high
9
+ color: blue
10
+ sandbox: read-only
11
+ ---
12
+
13
+ You are a senior software planner. Investigate the repository, identify the smallest complete implementation path, and return a concrete plan with file and symbol anchors.
14
+
15
+ Do not modify files. Resolve discoverable facts from the codebase before raising questions. Call out material risks, compatibility constraints, and the narrowest verification that proves the change.
@@ -2,6 +2,9 @@
2
2
  name: reviewer
3
3
  description: Code review specialist. Use for correctness, security, regression, and meaningful test-gap review.
4
4
  tools: read, grep, find, ls
5
+ models:
6
+ - openai-codex/gpt-5.6-sol
7
+ - opencode-go/deepseek-v4-pro
5
8
  thinking: high
6
9
  color: purple
7
10
  sandbox: read-only
package/agents/scout.md CHANGED
@@ -2,6 +2,10 @@
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff. Use for finding files, understanding structure, locating symbols.
4
4
  tools: read, grep, find, ls
5
+ models:
6
+ - opencode-go/deepseek-v4-flash
7
+ - openai-codex/gpt-5.6-luna
8
+ - opencode-go/mimo-v2.5
5
9
  thinking: low
6
10
  color: cyan
7
11
  sandbox: read-only
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: tester
3
+ description: Focused verification agent. Use for cheap routine test, typecheck, lint, build, and regression checks without editing files.
4
+ tools: read, bash, grep, find, ls
5
+ models:
6
+ - openai-codex/gpt-5.6-luna
7
+ - opencode-go/mimo-v2.5
8
+ - opencode-go/deepseek-v4-flash
9
+ thinking: low
10
+ color: orange
11
+ ---
12
+
13
+ You are a focused verification agent. Inspect the requested scope, run the narrowest relevant checks, and report exact commands, outcomes, and actionable failures.
14
+
15
+ Do not edit files. Avoid unrelated broad test suites unless the task requires them.
package/agents/worker.md CHANGED
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: worker
3
3
  description: General-purpose coding agent with full tool access. Use only when explicitly requested for isolated implementation.
4
+ models:
5
+ - openai-codex/gpt-5.6-terra
6
+ - opencode-go/deepseek-v4-pro
4
7
  thinking: medium
5
8
  color: green
6
9
  ---
@@ -20,6 +20,7 @@ export interface AgentConfig {
20
20
  description: string;
21
21
  tools?: string[];
22
22
  model?: string;
23
+ models?: string[];
23
24
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
24
25
  sandbox?: "read-only" | "workspace-write";
25
26
  color?: AgentColor;
@@ -28,6 +29,10 @@ export interface AgentConfig {
28
29
  filePath: string;
29
30
  }
30
31
 
32
+ export function getModelCandidates(agent: Pick<AgentConfig, "model" | "models">): string[] {
33
+ return [...new Set([agent.model, ...(agent.models ?? [])].filter((model): model is string => Boolean(model)))];
34
+ }
35
+
31
36
  export interface AgentDiscoveryResult {
32
37
  agents: AgentConfig[];
33
38
  projectAgentsDir: string | null;
@@ -160,11 +165,28 @@ function loadAgentsFromDir(
160
165
  : Array.isArray(frontmatter.tools)
161
166
  ? (frontmatter.tools as unknown[]).filter((t): t is string => typeof t === "string")
162
167
  : undefined;
168
+ const model = typeof frontmatter.model === "string" ? frontmatter.model.trim() || undefined : undefined;
169
+ const models =
170
+ typeof frontmatter.models === "string"
171
+ ? frontmatter.models.split(",").map((item) => item.trim()).filter(Boolean)
172
+ : Array.isArray(frontmatter.models)
173
+ ? (frontmatter.models as unknown[]).filter((item): item is string => typeof item === "string" && Boolean(item.trim())).map((item) => item.trim())
174
+ : undefined;
163
175
 
164
- if (typeof frontmatter.model === "string" && frontmatter.model && !frontmatter.model.includes("/")) {
176
+ if (frontmatter.models !== undefined && typeof frontmatter.models !== "string" && !Array.isArray(frontmatter.models)) {
177
+ diagnostics.push({ filePath, issue: `"models" must be a YAML array or comma-separated string. Ignoring.`, severity: "warn" });
178
+ }
179
+ if (Array.isArray(frontmatter.models)) {
180
+ for (const item of frontmatter.models) {
181
+ if (typeof item === "string" && item.trim()) continue;
182
+ diagnostics.push({ filePath, issue: `"models" entries must be non-empty strings. Ignoring invalid entry.`, severity: "warn" });
183
+ }
184
+ }
185
+ for (const modelName of getModelCandidates({ model, models })) {
186
+ if (modelName.includes("/")) continue;
165
187
  diagnostics.push({
166
188
  filePath,
167
- issue: `Model "${frontmatter.model}" does not include a provider prefix (e.g., "anthropic/claude-sonnet-4-20250514"). Resolution may fail.`,
189
+ issue: `Model "${modelName}" does not include a provider prefix (e.g., "anthropic/claude-sonnet-4-20250514"). Resolution may fail.`,
168
190
  severity: "warn",
169
191
  });
170
192
  }
@@ -204,7 +226,8 @@ function loadAgentsFromDir(
204
226
  name: frontmatter.name,
205
227
  description: frontmatter.description,
206
228
  tools: tools && tools.length > 0 ? tools : undefined,
207
- model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
229
+ model,
230
+ models: models && models.length > 0 ? models : undefined,
208
231
  thinking: typeof frontmatter.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(frontmatter.thinking)
209
232
  ? frontmatter.thinking as AgentConfig["thinking"]
210
233
  : undefined,
@@ -30,7 +30,7 @@ import {
30
30
  import { Container, Markdown, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
31
31
  import { Type } from "typebox";
32
32
 
33
- import { type AgentColor, type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
33
+ import { type AgentColor, type AgentConfig, type AgentScope, discoverAgents, formatAgentList, getModelCandidates, invalidateAgentCache } from "./agents.ts";
34
34
  import {
35
35
  type SubAgentResult,
36
36
  getFinalOutput,
@@ -164,11 +164,14 @@ export default function (pi: ExtensionAPI) {
164
164
  const ctx = currentCtx;
165
165
  const discovery = discoverAgents(ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
166
166
  const catalog = discovery.agents
167
- .map((a) => {
168
- const modelInfo = a.model ? ` (model: ${a.model})` : " (inherits parent)";
169
- const thinkingInfo = a.thinking ? `, thinking: ${a.thinking}` : "";
170
- const sandboxInfo = a.sandbox ? `, sandbox: ${a.sandbox}` : "";
171
- return `- **${a.name}**: ${a.description}${modelInfo}${thinkingInfo}${sandboxInfo}`;
167
+ .map((agent) => {
168
+ const candidates = getModelCandidates(agent);
169
+ const modelInfo = candidates.length > 0
170
+ ? ` (models: ${candidates.join(" → ")} parent fallback)`
171
+ : " (parent fallback)";
172
+ const thinkingInfo = agent.thinking ? `, thinking: ${agent.thinking}` : "";
173
+ const sandboxInfo = agent.sandbox ? `, sandbox: ${agent.sandbox}` : "";
174
+ return `- **${agent.name}**: ${agent.description}${modelInfo}${thinkingInfo}${sandboxInfo}`;
172
175
  })
173
176
  .join("\n");
174
177
  return {
@@ -177,9 +180,9 @@ export default function (pi: ExtensionAPI) {
177
180
  `\n\n## Available Subagents\n${catalog}\n\n` +
178
181
  "The subagent tool can delegate tasks to these specialized agents with isolated context. " +
179
182
  "Use for read-heavy exploration, parallel analysis, or work that would flood the main context.\n" +
180
- "Prefer **scout** for fast read-only exploration. " +
181
- "Prefer **reviewer** for code review (high thinking, read-only). " +
182
- "Prefer **worker** for implementation (medium thinking, all tools). " +
183
+ "Prefer **scout** and **tester** for cheap routine work. " +
184
+ "Prefer **worker** or **general-purpose** for normal coding. " +
185
+ "Prefer **planner** and **reviewer** for consequential reasoning. " +
183
186
  "Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
184
187
  };
185
188
  });
@@ -272,12 +275,13 @@ export default function (pi: ExtensionAPI) {
272
275
  ctx.ui.notify(`Unknown agent: "${args.trim()}". Use /subagent to list all.`, "error");
273
276
  return;
274
277
  }
278
+ const candidates = getModelCandidates(agent);
275
279
  pi.sendMessage({
276
280
  customType: "pi-subagent",
277
281
  content: [
278
282
  `Agent: ${agent.name} (${agent.source})`,
279
283
  `Description: ${agent.description}`,
280
- `Model: ${agent.model || "inherits from parent"}`,
284
+ `Models: ${candidates.length > 0 ? `${candidates.join(" ")} → parent fallback` : "parent fallback"}`,
281
285
  `Thinking: ${agent.thinking || "off"}`,
282
286
  `Tools: ${agent.tools?.join(", ") || "all default"}`,
283
287
  `Source file: ${agent.filePath}`,
@@ -343,11 +347,11 @@ export default function (pi: ExtensionAPI) {
343
347
  `To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" or "project".`,
344
348
  ].join(" "),
345
349
  parameters: SubagentParams,
346
- promptSnippet: "Delegate tasks to specialized sub-agents (scout, reviewer, worker, general-purpose)",
350
+ promptSnippet: "Delegate tasks to specialized sub-agents with automatic role-based model routing",
347
351
  promptGuidelines: [
348
352
  "Use subagent to delegate work that would flood the main context with search results or file contents.",
349
353
  "Modes: single {agent, task}, parallel {tasks: [...]} (max 8, 4 concurrent), chain {chain: [...]} (sequential with {previous}).",
350
- "Bundled agents: scout (fast recon), reviewer (code review), worker (implementation), general-purpose (fallback).",
354
+ "Bundled agents: scout (fast recon), tester (verification), worker (implementation), general-purpose (fallback), planner (planning), reviewer (review).",
351
355
  "Use /subagent to list all available agents or /subagent <name> for agent details.",
352
356
  ],
353
357
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -536,7 +540,7 @@ export default function (pi: ExtensionAPI) {
536
540
  };
537
541
  }
538
542
 
539
- const resolved = resolveModel(agent.model, ctx.model, ctx.modelRegistry);
543
+ const resolved = await resolveModel(getModelCandidates(agent), ctx.model, ctx.modelRegistry);
540
544
  if (!resolved.model) {
541
545
  const tried = resolved.attempted.join(", ") || "none";
542
546
  const parentInfo = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
@@ -5,14 +5,12 @@
5
5
  * (index.ts) and the event-driven service path (service.ts), ensuring
6
6
  * consistent error reporting across all sub-agent invocation paths.
7
7
  *
8
- * Queries the parent ModelRegistry first (catches custom-configured models
9
- * with overridden base URLs, headers, compatibility settings). Falls back
10
- * to the built-in registry for unconfigured models.
8
+ * Selects the first authenticated candidate reported by the parent
9
+ * ModelRegistry, then falls back to the authenticated parent model.
11
10
  * For unqualified names (no provider prefix), known naming conventions
12
11
  * are tried before assuming Anthropic.
13
12
  */
14
13
 
15
- import { getModel } from "@earendil-works/pi-ai/compat";
16
14
  import type { Model } from "@earendil-works/pi-ai";
17
15
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
18
16
 
@@ -32,55 +30,38 @@ const KNOWN_PROVIDERS: [string, RegExp][] = [
32
30
  ["groq", /^(groq-|llama-)/i],
33
31
  ];
34
32
 
35
- function tryGetModel(
36
- provider: string,
37
- id: string,
38
- modelRegistry?: ModelRegistry,
39
- ): Model<any> | null {
40
- // Query parent ModelRegistry first — it includes custom-configured models
41
- // (overridden base URLs, headers, compatibility settings, per-model overrides).
42
- // Fall back to built-in registry for unconfigured models.
43
- if (modelRegistry) {
44
- const found = modelRegistry.find(provider as any, id as any) ?? null;
45
- if (found) return found;
46
- }
47
- const builtIn = getModel(provider as any, id as any) ?? null;
48
- if (builtIn) return builtIn;
49
- return null;
50
- }
51
-
52
- export function resolveModel(
53
- modelName: string | undefined,
33
+ export async function resolveModel(
34
+ modelNames: readonly string[],
54
35
  parentModel: Model<any> | undefined,
55
36
  modelRegistry?: ModelRegistry,
56
- ): ResolvedModel {
37
+ ): Promise<ResolvedModel> {
57
38
  const attempted: string[] = [];
58
- if (modelName) {
39
+ const available = modelRegistry?.getAvailable() ?? [];
40
+ const byName = new Map(available.map((model) => [`${model.provider}/${model.id}`, model]));
41
+ const tryAvailable = (qualifiedName: string): Model<any> | undefined => {
42
+ if (!attempted.includes(qualifiedName)) attempted.push(qualifiedName);
43
+ return byName.get(qualifiedName);
44
+ };
45
+
46
+ for (const modelName of [...new Set(modelNames.map((name) => name.trim()).filter(Boolean))]) {
59
47
  const idx = modelName.indexOf("/");
60
48
  if (idx > 0) {
61
- // Provider-qualified: "openai/gpt-4o" or "openrouter/anthropic/claude-3.5"
62
- const provider = modelName.slice(0, idx);
63
- const id = modelName.slice(idx + 1);
64
- attempted.push(modelName);
65
- const found = tryGetModel(provider, id, modelRegistry);
49
+ const found = tryAvailable(modelName);
66
50
  if (found) return { model: found, attempted };
67
- } else {
68
- // Unqualified: try known providers by naming convention
69
- for (const [provider, pattern] of KNOWN_PROVIDERS) {
70
- if (pattern.test(modelName)) {
71
- attempted.push(`${provider}/${modelName}`);
72
- const found = tryGetModel(provider, modelName, modelRegistry);
73
- if (found) return { model: found, attempted };
74
- }
75
- }
76
- // Fall back to Anthropic shorthand (backward compat)
77
- attempted.push(`anthropic/${modelName}`);
78
- const found = tryGetModel("anthropic", modelName, modelRegistry);
51
+ continue;
52
+ }
53
+ for (const [provider, pattern] of KNOWN_PROVIDERS) {
54
+ if (!pattern.test(modelName)) continue;
55
+ const found = tryAvailable(`${provider}/${modelName}`);
79
56
  if (found) return { model: found, attempted };
80
57
  }
81
- } else if (parentModel) {
82
- attempted.push(`${parentModel.provider}/${parentModel.id}`);
83
- return { model: parentModel, attempted };
58
+ const found = tryAvailable(`anthropic/${modelName}`);
59
+ if (found) return { model: found, attempted };
60
+ }
61
+
62
+ if (parentModel) {
63
+ const found = tryAvailable(`${parentModel.provider}/${parentModel.id}`);
64
+ if (found) return { model: found, attempted };
84
65
  }
85
66
  return { model: null, attempted };
86
67
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Only the agent's system prompt is used (no pi defaults).
9
9
  * - No AGENTS.md, no extensions, no skills, no prompt templates loaded.
10
- * - Thinking disabled, compaction disabled, retry disabled.
10
+ * - Thinking disabled, compaction disabled, one transient retry.
11
11
  * - In-memory session (no disk I/O).
12
12
  * - Shared auth/model infrastructure (no re-connection).
13
13
  *
@@ -126,7 +126,7 @@ export async function runSubAgent(options: {
126
126
 
127
127
  const settingsManager = SettingsManager.inMemory({
128
128
  compaction: { enabled: false },
129
- retry: { enabled: false },
129
+ retry: { enabled: true, maxRetries: 1 },
130
130
  });
131
131
 
132
132
  // Hoisted so the outer catch can clean up on early failure.
@@ -220,7 +220,7 @@ export async function runSubAgent(options: {
220
220
  result.model = `${msg.provider || "?"}/${msg.model}`;
221
221
  }
222
222
  if (msg.stopReason) result.stopReason = msg.stopReason;
223
- if (msg.errorMessage) result.errorMessage = msg.errorMessage;
223
+ result.errorMessage = msg.errorMessage;
224
224
  }
225
225
  // Collect all messages for extraction
226
226
  result.messages.push(msg as unknown as Message);
@@ -228,6 +228,7 @@ export async function runSubAgent(options: {
228
228
  break;
229
229
  }
230
230
  case "agent_end": {
231
+ if (event.willRetry) break;
231
232
  // agent_end carries all messages; use them if we haven't collected
232
233
  if (result.messages.length === 0 && event.messages) {
233
234
  result.messages = event.messages as unknown as Message[];
@@ -271,19 +272,16 @@ export async function runSubAgent(options: {
271
272
  abortedBySignal = combinedSignal.aborted && !timedOut;
272
273
 
273
274
  if (timedOut) {
274
- result.exitCode = 1;
275
275
  result.stopReason = "timeout";
276
- result.errorMessage ||= `Timeout after ${timeoutMs}ms`;
276
+ result.errorMessage = `Timeout after ${timeoutMs}ms`;
277
277
  } else if (abortedBySignal) {
278
- result.exitCode = 1;
279
278
  result.stopReason = "aborted";
280
279
  result.errorMessage ||= "Sub-agent aborted";
281
- } else {
282
- result.exitCode = 0;
283
280
  }
284
281
 
285
- // Classify canonical status.
282
+ // Classify canonical status and keep the legacy exit code consistent.
286
283
  result.status = classifyStopReason(result.stopReason, result.stopReason === "aborted", result.stopReason === "timeout");
284
+ result.exitCode = result.status === "success" || result.status === "partial" ? 0 : 1;
287
285
 
288
286
  return result;
289
287
  } finally {
@@ -1,5 +1,5 @@
1
1
  import { AuthStorage, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { AgentConfig } from "./agents.ts";
2
+ import { type AgentConfig, getModelCandidates } from "./agents.ts";
3
3
  import { runSubAgent, type SubAgentResult } from "./runner.ts";
4
4
  import { resolveModel } from "./model.ts";
5
5
  import {
@@ -39,7 +39,7 @@ export async function runNamedAgent(options: {
39
39
  signal?: AbortSignal;
40
40
  onMessage?: (result: SubAgentResult) => void;
41
41
  }): Promise<SubAgentResult> {
42
- const { model, attempted } = resolveModel(options.agent.model, options.ctx.model, options.ctx.modelRegistry);
42
+ const { model, attempted } = await resolveModel(getModelCandidates(options.agent), options.ctx.model, options.ctx.modelRegistry);
43
43
  if (!model) throw new Error(`No model resolved for agent "${options.agent.name}" (tried: ${attempted.join(", ") || "none"})`);
44
44
 
45
45
  const authStorage = AuthStorage.inMemory();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-subagent",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
5
5
  "type": "module",
6
6
  "license": "MIT",