@bacnh85/pi-subagent 0.8.0 → 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 +20 -0
- package/README.md +13 -7
- package/agent-format.md +12 -8
- package/agents/general-purpose.md +4 -0
- package/agents/planner.md +15 -0
- package/agents/reviewer.md +7 -2
- package/agents/scout.md +4 -0
- package/agents/tester.md +15 -0
- package/agents/worker.md +3 -0
- package/extensions/agents.ts +26 -3
- package/extensions/index.ts +47 -31
- package/extensions/model.ts +26 -45
- package/extensions/runner.ts +13 -9
- package/extensions/service.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
17
|
+
## 0.8.1 (2026-07-15)
|
|
18
|
+
|
|
19
|
+
### Review handoff
|
|
20
|
+
|
|
21
|
+
- Reviewer findings now require reproduction or evidence, expected behavior, and acceptance criteria so implementation agents receive self-contained actionable issues.
|
|
22
|
+
|
|
3
23
|
## 0.6.0 (2026-07-12)
|
|
4
24
|
|
|
5
25
|
### Security (breaking changes)
|
package/README.md
CHANGED
|
@@ -14,14 +14,16 @@ Requires Node.js >= 20.18.
|
|
|
14
14
|
|
|
15
15
|
## Bundled roles
|
|
16
16
|
|
|
17
|
-
| Role |
|
|
17
|
+
| Role | Ordered model preferences | Thinking | Tools |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
|
-
| `scout` |
|
|
20
|
-
| `
|
|
21
|
-
| `worker` |
|
|
22
|
-
| `general-purpose` |
|
|
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
|
-
|
|
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:
|
|
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.
|
|
@@ -87,6 +90,7 @@ Every child execution receives a timeout:
|
|
|
87
90
|
- **Maximum:** 60 minutes (`MAX_TIMEOUT_MS`)
|
|
88
91
|
- Timeout values must be positive integers within the allowed range.
|
|
89
92
|
- Timeout errors are distinguishable from parent cancellation.
|
|
93
|
+
- Progress heartbeats keep the parent transport active during quiet model work; explicit timeouts remain hard deadlines.
|
|
90
94
|
- Parallel tasks and chain steps may have per-item timeouts.
|
|
91
95
|
|
|
92
96
|
### Output safety
|
|
@@ -158,6 +162,8 @@ The raw `stopReason` from the Pi SDK is preserved in the result.
|
|
|
158
162
|
- **Parent cancellation:** Aborting the parent tool call cancels all children.
|
|
159
163
|
- **Sibling cancellation:** In parallel mode with `abortOnFailure: true`, the first failed task cancels running siblings.
|
|
160
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.
|
|
166
|
+
- **Transport idle:** The parent tool receives periodic progress heartbeats while a child runs; these do not extend its timeout.
|
|
161
167
|
|
|
162
168
|
## Extension contract
|
|
163
169
|
|
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, ...
|
|
22
|
-
model: provider/model # Optional
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/agents/reviewer.md
CHANGED
|
@@ -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
|
|
@@ -9,7 +12,7 @@ sandbox: read-only
|
|
|
9
12
|
|
|
10
13
|
You are an independent senior code reviewer. Inspect the requested Git scope with read-only tools.
|
|
11
14
|
|
|
12
|
-
Focus only on actionable issues introduced by the reviewed change:
|
|
15
|
+
Focus only on actionable issues introduced by the reviewed change. Return each confirmed finding as one self-contained issue another agent can fix without redoing the review:
|
|
13
16
|
1. Correctness and edge cases
|
|
14
17
|
2. Security and data loss
|
|
15
18
|
3. Regressions and API compatibility
|
|
@@ -27,8 +30,10 @@ Return JSON only:
|
|
|
27
30
|
"file": "relative/path",
|
|
28
31
|
"line": 1,
|
|
29
32
|
"issue": "what is wrong and why it matters",
|
|
30
|
-
"evidence": "specific inspected code evidence",
|
|
33
|
+
"evidence": "reproduction steps or specific inspected code evidence",
|
|
34
|
+
"expectedBehavior": "what should happen instead",
|
|
31
35
|
"suggestedFix": "smallest safe fix",
|
|
36
|
+
"acceptanceCriteria": "observable pass conditions and exact verification checks",
|
|
32
37
|
"blocking": true
|
|
33
38
|
}
|
|
34
39
|
]
|
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
|
package/agents/tester.md
ADDED
|
@@ -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
package/extensions/agents.ts
CHANGED
|
@@ -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.
|
|
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 "${
|
|
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
|
|
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,
|
package/extensions/index.ts
CHANGED
|
@@ -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,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
isFailedResult,
|
|
39
39
|
mapWithConcurrencyLimit,
|
|
40
40
|
runSubAgent,
|
|
41
|
+
startHeartbeat,
|
|
41
42
|
} from "./runner.ts";
|
|
42
43
|
import {
|
|
43
44
|
normalizeTimeout,
|
|
@@ -163,11 +164,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
164
|
const ctx = currentCtx;
|
|
164
165
|
const discovery = discoverAgents(ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
|
|
165
166
|
const catalog = discovery.agents
|
|
166
|
-
.map((
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
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}`;
|
|
171
175
|
})
|
|
172
176
|
.join("\n");
|
|
173
177
|
return {
|
|
@@ -176,9 +180,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
180
|
`\n\n## Available Subagents\n${catalog}\n\n` +
|
|
177
181
|
"The subagent tool can delegate tasks to these specialized agents with isolated context. " +
|
|
178
182
|
"Use for read-heavy exploration, parallel analysis, or work that would flood the main context.\n" +
|
|
179
|
-
"Prefer **scout** for
|
|
180
|
-
"Prefer **
|
|
181
|
-
"Prefer **
|
|
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. " +
|
|
182
186
|
"Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
|
|
183
187
|
};
|
|
184
188
|
});
|
|
@@ -271,12 +275,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
271
275
|
ctx.ui.notify(`Unknown agent: "${args.trim()}". Use /subagent to list all.`, "error");
|
|
272
276
|
return;
|
|
273
277
|
}
|
|
278
|
+
const candidates = getModelCandidates(agent);
|
|
274
279
|
pi.sendMessage({
|
|
275
280
|
customType: "pi-subagent",
|
|
276
281
|
content: [
|
|
277
282
|
`Agent: ${agent.name} (${agent.source})`,
|
|
278
283
|
`Description: ${agent.description}`,
|
|
279
|
-
`
|
|
284
|
+
`Models: ${candidates.length > 0 ? `${candidates.join(" → ")} → parent fallback` : "parent fallback"}`,
|
|
280
285
|
`Thinking: ${agent.thinking || "off"}`,
|
|
281
286
|
`Tools: ${agent.tools?.join(", ") || "all default"}`,
|
|
282
287
|
`Source file: ${agent.filePath}`,
|
|
@@ -342,11 +347,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
342
347
|
`To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" or "project".`,
|
|
343
348
|
].join(" "),
|
|
344
349
|
parameters: SubagentParams,
|
|
345
|
-
promptSnippet: "Delegate tasks to specialized sub-agents
|
|
350
|
+
promptSnippet: "Delegate tasks to specialized sub-agents with automatic role-based model routing",
|
|
346
351
|
promptGuidelines: [
|
|
347
352
|
"Use subagent to delegate work that would flood the main context with search results or file contents.",
|
|
348
353
|
"Modes: single {agent, task}, parallel {tasks: [...]} (max 8, 4 concurrent), chain {chain: [...]} (sequential with {previous}).",
|
|
349
|
-
"Bundled agents: scout (fast recon),
|
|
354
|
+
"Bundled agents: scout (fast recon), tester (verification), worker (implementation), general-purpose (fallback), planner (planning), reviewer (review).",
|
|
350
355
|
"Use /subagent to list all available agents or /subagent <name> for agent details.",
|
|
351
356
|
],
|
|
352
357
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -515,6 +520,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
515
520
|
parentSignal?: AbortSignal,
|
|
516
521
|
timeoutMs?: number,
|
|
517
522
|
onProgress?: (partial: SubAgentResult) => void,
|
|
523
|
+
heartbeatDetails?: () => SubagentDetails,
|
|
518
524
|
isReadOnly?: boolean,
|
|
519
525
|
): Promise<SubAgentResult> {
|
|
520
526
|
const agent = agents.find((a) => a.name === agentName);
|
|
@@ -534,7 +540,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
540
|
};
|
|
535
541
|
}
|
|
536
542
|
|
|
537
|
-
const resolved = resolveModel(agent
|
|
543
|
+
const resolved = await resolveModel(getModelCandidates(agent), ctx.model, ctx.modelRegistry);
|
|
538
544
|
if (!resolved.model) {
|
|
539
545
|
const tried = resolved.attempted.join(", ") || "none";
|
|
540
546
|
const parentInfo = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
|
|
@@ -576,23 +582,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
576
582
|
};
|
|
577
583
|
}
|
|
578
584
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
585
|
+
const stopHeartbeat = onUpdate ? startHeartbeat(() => onUpdate({
|
|
586
|
+
content: [{ type: "text", text: `Subagent ${agentName} is still running…` }],
|
|
587
|
+
details: heartbeatDetails?.() ?? makeDetails("single")([]),
|
|
588
|
+
})) : undefined;
|
|
589
|
+
try {
|
|
590
|
+
return await runSubAgent({
|
|
591
|
+
cwd: safeCwd,
|
|
592
|
+
systemPrompt: params.instructions
|
|
593
|
+
? `${agent.systemPrompt}\n\n## Task Contract\n${params.instructions.slice(0, MAX_INSTRUCTIONS_LENGTH)}`
|
|
594
|
+
: agent.systemPrompt,
|
|
595
|
+
task,
|
|
596
|
+
tools,
|
|
597
|
+
model: resolved.model,
|
|
598
|
+
authStorage,
|
|
599
|
+
modelRegistry,
|
|
600
|
+
signal: parentSignal,
|
|
601
|
+
timeoutMs: effectiveTimeoutMs,
|
|
602
|
+
agentName,
|
|
603
|
+
thinkingLevel: agent.thinking,
|
|
604
|
+
onMessage: onProgress,
|
|
605
|
+
});
|
|
606
|
+
} finally {
|
|
607
|
+
stopHeartbeat?.();
|
|
608
|
+
}
|
|
596
609
|
}
|
|
597
610
|
|
|
598
611
|
// --- Chain mode ---
|
|
@@ -615,6 +628,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
628
|
step.agent, taskWithContext, step.cwd,
|
|
616
629
|
signal, step.timeout ?? params.timeout,
|
|
617
630
|
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
631
|
+
() => makeDetails("chain")(results),
|
|
618
632
|
);
|
|
619
633
|
threadStore.updateThread(thread.id, {
|
|
620
634
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
@@ -767,7 +781,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
767
781
|
const result = await runOne(
|
|
768
782
|
t.agent, t.task, t.cwd,
|
|
769
783
|
parallelController.signal, t.timeout ?? params.timeout,
|
|
770
|
-
|
|
784
|
+
(partial) => threadStore.updateThread(parallelThreads[index].id, { result: partial }),
|
|
785
|
+
() => makeDetails("parallel")([...allResults]),
|
|
771
786
|
);
|
|
772
787
|
allResults[index] = result;
|
|
773
788
|
threadStore.updateThread(parallelThreads[index].id, {
|
|
@@ -823,6 +838,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
823
838
|
params.agent, params.task, params.cwd,
|
|
824
839
|
signal, params.timeout,
|
|
825
840
|
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
841
|
+
() => makeDetails("single")([]),
|
|
826
842
|
);
|
|
827
843
|
threadStore.updateThread(thread.id, {
|
|
828
844
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
package/extensions/model.ts
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
}
|
package/extensions/runner.ts
CHANGED
|
@@ -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
|
|
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
|
*
|
|
@@ -63,6 +63,12 @@ export interface SubAgentResult {
|
|
|
63
63
|
// Public API
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
|
+
export function startHeartbeat(onHeartbeat: () => void, intervalMs = 30_000): () => void {
|
|
67
|
+
const timer = setInterval(onHeartbeat, intervalMs);
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
return () => clearInterval(timer);
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
export async function runSubAgent(options: {
|
|
67
73
|
cwd: string;
|
|
68
74
|
systemPrompt: string;
|
|
@@ -120,7 +126,7 @@ export async function runSubAgent(options: {
|
|
|
120
126
|
|
|
121
127
|
const settingsManager = SettingsManager.inMemory({
|
|
122
128
|
compaction: { enabled: false },
|
|
123
|
-
retry: { enabled:
|
|
129
|
+
retry: { enabled: true, maxRetries: 1 },
|
|
124
130
|
});
|
|
125
131
|
|
|
126
132
|
// Hoisted so the outer catch can clean up on early failure.
|
|
@@ -214,7 +220,7 @@ export async function runSubAgent(options: {
|
|
|
214
220
|
result.model = `${msg.provider || "?"}/${msg.model}`;
|
|
215
221
|
}
|
|
216
222
|
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
217
|
-
|
|
223
|
+
result.errorMessage = msg.errorMessage;
|
|
218
224
|
}
|
|
219
225
|
// Collect all messages for extraction
|
|
220
226
|
result.messages.push(msg as unknown as Message);
|
|
@@ -222,6 +228,7 @@ export async function runSubAgent(options: {
|
|
|
222
228
|
break;
|
|
223
229
|
}
|
|
224
230
|
case "agent_end": {
|
|
231
|
+
if (event.willRetry) break;
|
|
225
232
|
// agent_end carries all messages; use them if we haven't collected
|
|
226
233
|
if (result.messages.length === 0 && event.messages) {
|
|
227
234
|
result.messages = event.messages as unknown as Message[];
|
|
@@ -265,19 +272,16 @@ export async function runSubAgent(options: {
|
|
|
265
272
|
abortedBySignal = combinedSignal.aborted && !timedOut;
|
|
266
273
|
|
|
267
274
|
if (timedOut) {
|
|
268
|
-
result.exitCode = 1;
|
|
269
275
|
result.stopReason = "timeout";
|
|
270
|
-
result.errorMessage
|
|
276
|
+
result.errorMessage = `Timeout after ${timeoutMs}ms`;
|
|
271
277
|
} else if (abortedBySignal) {
|
|
272
|
-
result.exitCode = 1;
|
|
273
278
|
result.stopReason = "aborted";
|
|
274
279
|
result.errorMessage ||= "Sub-agent aborted";
|
|
275
|
-
} else {
|
|
276
|
-
result.exitCode = 0;
|
|
277
280
|
}
|
|
278
281
|
|
|
279
|
-
// Classify canonical status.
|
|
282
|
+
// Classify canonical status and keep the legacy exit code consistent.
|
|
280
283
|
result.status = classifyStopReason(result.stopReason, result.stopReason === "aborted", result.stopReason === "timeout");
|
|
284
|
+
result.exitCode = result.status === "success" || result.status === "partial" ? 0 : 1;
|
|
281
285
|
|
|
282
286
|
return result;
|
|
283
287
|
} finally {
|
package/extensions/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStorage, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
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
|
|
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