@arhen/pi-core-subagent 1.3.42 → 1.3.43
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/package.json +1 -1
- package/src/format.ts +4 -2
- package/src/manager.ts +67 -5
- package/src/types.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/format.ts
CHANGED
|
@@ -242,8 +242,9 @@ export function makeSummary(run: RunSnapshot): string {
|
|
|
242
242
|
// Edges are named so the leader can compare what it delegated against what came back.
|
|
243
243
|
const edge = task.needs?.length ? ` (${task.id}, needs ${task.needs.join(", ")})` : ` (${task.id})`;
|
|
244
244
|
const fileNote = task.agentFile ? ` [${task.agentFile}]` : "";
|
|
245
|
+
const swap = task.modelNote ? `\nModel: ${task.modelNote}` : "";
|
|
245
246
|
lines.push(
|
|
246
|
-
`\n## ${task.agent}${edge}${fileNote} ${statusIcon(task.status)}${task.error ? `\nError: ${task.error}` : `\n${truncateText(task.finalText || "(no output)")}`}${worktreeLine(task, run.tasks)}`,
|
|
247
|
+
`\n## ${task.agent}${edge}${fileNote} ${statusIcon(task.status)}${swap}${task.error ? `\nError: ${task.error}` : `\n${truncateText(task.finalText || "(no output)")}`}${worktreeLine(task, run.tasks)}`,
|
|
247
248
|
);
|
|
248
249
|
}
|
|
249
250
|
// Ceiling on the WHOLE summary — 16 tasks × 24KB would otherwise flood the parent context.
|
|
@@ -265,9 +266,10 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
265
266
|
// 403, wrong persona) that override is the likeliest cause, so it has to be
|
|
266
267
|
// in the notice, not only in the run summary the leader may never read.
|
|
267
268
|
const src = task.agentFile ? `\nAgent file: ${task.agentFile}${task.model ? ` (model ${task.model})` : ""}` : "";
|
|
269
|
+
const swap = task.modelNote ? `\nModel: ${task.modelNote}` : "";
|
|
268
270
|
return [
|
|
269
271
|
`Task ${task.agent} (${task.id}) ${kind} in run ${run.id}: ${detail}${wt}`,
|
|
270
|
-
`Goal: ${goal}${src}`,
|
|
272
|
+
`Goal: ${goal}${src}${swap}`,
|
|
271
273
|
isStartupFailure(task, kind)
|
|
272
274
|
? "Never started — stop and diagnose before spawning anything else: a config-level error (model, plan, auth, agent file) fails identically on every respawn."
|
|
273
275
|
: `Use subagent_result(runId: "${run.id}", taskId: "${task.id}") for full output.`,
|
package/src/manager.ts
CHANGED
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
activitySnippet,
|
|
23
23
|
describeCall,
|
|
24
24
|
getFirstText,
|
|
25
|
+
isStartupFailure,
|
|
25
26
|
isTalking,
|
|
26
27
|
makeNotice,
|
|
27
28
|
makeTaskNotice,
|
|
28
|
-
isStartupFailure,
|
|
29
29
|
SubagentsWidget,
|
|
30
30
|
truncateText,
|
|
31
31
|
} from "./format.ts";
|
|
@@ -160,14 +160,26 @@ export function cloneRun(run: RunSnapshot): RunSnapshot {
|
|
|
160
160
|
return JSON.parse(JSON.stringify(run)) as RunSnapshot;
|
|
161
161
|
}
|
|
162
162
|
/** Resolve a child model from the pi model registry.
|
|
163
|
-
* Order: explicit "provider/model-id" or bare id
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* Order: explicit "provider/model-id" or bare id → agent file model → parent's
|
|
164
|
+
* current model (ctx.model) → undefined (createAgentSession falls back to settings).
|
|
165
|
+
*
|
|
166
|
+
* A BARE id is ambiguous: `claude-sonnet-5` exists on anthropic, commandcode,
|
|
167
|
+
* github-copilot and openrouter at once, and an agent file's `model:` is written
|
|
168
|
+
* without a provider. Taking the registry's first match routed children to a
|
|
169
|
+
* provider the session never chose (403 MODEL_NOT_IN_PLAN on every spawn), so the
|
|
170
|
+
* ACTIVE SESSION's provider is searched first — exact id, then its own prefixed
|
|
171
|
+
* form (9router carries `cc/claude-opus-5`, not `claude-opus-5`). */
|
|
166
172
|
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
167
173
|
if (!explicit?.trim()) return ctx.model; // inherit the parent's active model
|
|
168
174
|
const ref = explicit.trim();
|
|
169
175
|
if (!ctx.modelRegistry) return ctx.model; // no registry to check against (tests, headless)
|
|
170
176
|
const available = ctx.modelRegistry.getAvailable();
|
|
177
|
+
const sessionProvider = ctx.model?.provider;
|
|
178
|
+
if (sessionProvider && !ref.includes("/")) {
|
|
179
|
+
const own = available.filter((m) => m.provider === sessionProvider);
|
|
180
|
+
const hit = own.find((m) => m.id === ref) ?? own.find((m) => m.id.endsWith(`/${ref}`));
|
|
181
|
+
if (hit) return hit;
|
|
182
|
+
}
|
|
171
183
|
// Model ids can contain slashes (e.g. 9router/cc/claude-opus-5), so a bare id
|
|
172
184
|
// match and every provider/id split point must be tried, not just the first.
|
|
173
185
|
const byId = available.find((m) => m.id === ref);
|
|
@@ -179,6 +191,48 @@ export function resolveChildModel(ctx: ExtensionContext, explicit: string | unde
|
|
|
179
191
|
throw new Error(`Model not found: ${ref}`);
|
|
180
192
|
}
|
|
181
193
|
|
|
194
|
+
/** One throwaway request against the resolved model. A child that cannot reach its
|
|
195
|
+
* model dies on its FIRST turn with no output, after a worktree and a session have
|
|
196
|
+
* already been built — and an agent file's `model:` is chosen by a file the leader
|
|
197
|
+
* never wrote, so "it resolved" is not evidence it is usable (plan gates, ZDR,
|
|
198
|
+
* dead keys all pass resolution). Returns the error text, or undefined when OK. */
|
|
199
|
+
async function probeModel(
|
|
200
|
+
ctx: ExtensionContext,
|
|
201
|
+
model: Model<Api>,
|
|
202
|
+
signal: AbortSignal | undefined,
|
|
203
|
+
): Promise<string | undefined> {
|
|
204
|
+
try {
|
|
205
|
+
const reply = await ctx.modelRegistry.complete(
|
|
206
|
+
model,
|
|
207
|
+
{ messages: [{ role: "user", content: "ping", timestamp: Date.now() }] },
|
|
208
|
+
{ maxTokens: 16, signal },
|
|
209
|
+
);
|
|
210
|
+
return reply.stopReason === "error" ? (reply.errorMessage ?? "provider returned an error") : undefined;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return err instanceof Error ? err.message : String(err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Preflight for the model a child is about to run on. Probes only what is not
|
|
217
|
+
* already proven: the session's own model answered this very turn. On failure the
|
|
218
|
+
* session model is the fallback — the one model known to work right now. */
|
|
219
|
+
export async function ensureUsableModel(
|
|
220
|
+
ctx: ExtensionContext,
|
|
221
|
+
model: Model<Api> | undefined,
|
|
222
|
+
signal: AbortSignal | undefined,
|
|
223
|
+
): Promise<{ model: Model<Api> | undefined; note?: string }> {
|
|
224
|
+
const session = ctx.model;
|
|
225
|
+
if (!model || !ctx.modelRegistry) return { model };
|
|
226
|
+
if (session && model.provider === session.provider && model.id === session.id) return { model };
|
|
227
|
+
const error = await probeModel(ctx, model, signal);
|
|
228
|
+
if (!error) return { model };
|
|
229
|
+
if (!session) throw new Error(`Model ${model.provider}/${model.id} is unusable: ${error}`);
|
|
230
|
+
return {
|
|
231
|
+
model: session,
|
|
232
|
+
note: `${model.provider}/${model.id} failed preflight (${error}); using session model ${session.provider}/${session.id}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
/** Extension-registered providers (e.g. 9router) live only in the parent's
|
|
183
237
|
* in-memory runtime. A child builds its runtime from disk and would lose them,
|
|
184
238
|
* so replay the parent's registrations before the child resolves auth. */
|
|
@@ -810,10 +864,18 @@ export class SubagentManager {
|
|
|
810
864
|
try {
|
|
811
865
|
model = resolveChildModel(ctx, file?.model ?? input.model);
|
|
812
866
|
validateThinking(model, thinking);
|
|
867
|
+
// Resolution only proves the id exists. Probe it before anything is built,
|
|
868
|
+
// and fall back to the session's own model when the probe fails.
|
|
869
|
+
const checked = await ensureUsableModel(ctx, model, signal);
|
|
870
|
+
model = checked.model;
|
|
871
|
+
if (checked.note) {
|
|
872
|
+
task.modelNote = checked.note;
|
|
873
|
+
validateThinking(model, thinking);
|
|
874
|
+
}
|
|
813
875
|
// Record the model ACTUALLY used — an agent file's `model:` overrides the
|
|
814
876
|
// requested one, and showing the request in the widget hides that entirely
|
|
815
877
|
// (a 403 then names a model the leader never asked for).
|
|
816
|
-
if (model) this.updateTask(run, task, { model: model.id }, ctx, onUpdate);
|
|
878
|
+
if (model) this.updateTask(run, task, { model: model.id, modelNote: checked.note }, ctx, onUpdate);
|
|
817
879
|
} catch (err) {
|
|
818
880
|
this.updateTask(
|
|
819
881
|
run,
|
package/src/types.ts
CHANGED
|
@@ -36,6 +36,9 @@ export interface TaskSnapshot {
|
|
|
36
36
|
finalText?: string;
|
|
37
37
|
error?: string;
|
|
38
38
|
model?: string;
|
|
39
|
+
/** Why `model` is not what was requested: preflight failed and the session's
|
|
40
|
+
* model took over. Silent substitution is worse than a slow spawn. */
|
|
41
|
+
modelNote?: string;
|
|
39
42
|
thinking?: string;
|
|
40
43
|
tools?: string[];
|
|
41
44
|
usage: UsageStats;
|