pi-subagents 0.12.5 → 0.13.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 +9 -0
- package/README.md +22 -4
- package/agent-management.ts +27 -0
- package/agent-manager-edit.ts +4 -2
- package/agent-serializer.ts +3 -0
- package/agents.ts +6 -0
- package/async-execution.ts +15 -2
- package/async-status.ts +7 -0
- package/chain-execution.ts +7 -21
- package/execution.ts +203 -119
- package/model-fallback.ts +100 -0
- package/package.json +1 -1
- package/parallel-utils.ts +3 -0
- package/pi-args.ts +1 -4
- package/render.ts +6 -0
- package/subagent-executor.ts +30 -12
- package/subagent-runner.ts +163 -32
- package/subagents-status.ts +8 -1
- package/types.ts +23 -1
package/execution.ts
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
type AgentProgress,
|
|
16
16
|
type ArtifactPaths,
|
|
17
|
+
type ModelAttempt,
|
|
17
18
|
type RunSyncOptions,
|
|
18
19
|
type SingleResult,
|
|
20
|
+
type Usage,
|
|
19
21
|
DEFAULT_MAX_OUTPUT,
|
|
20
22
|
truncateOutput,
|
|
21
23
|
getSubagentDepthEnv,
|
|
@@ -31,81 +33,87 @@ import { buildSkillInjection, resolveSkills } from "./skills.ts";
|
|
|
31
33
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
32
34
|
import { createJsonlWriter } from "./jsonl-writer.ts";
|
|
33
35
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
34
|
-
import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
runtimeCwd: string,
|
|
41
|
-
agents: AgentConfig[],
|
|
42
|
-
agentName: string,
|
|
43
|
-
task: string,
|
|
44
|
-
options: RunSyncOptions,
|
|
45
|
-
): Promise<SingleResult> {
|
|
46
|
-
const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index, modelOverride } = options;
|
|
47
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
48
|
-
if (!agent) {
|
|
49
|
-
return {
|
|
50
|
-
agent: agentName,
|
|
51
|
-
task,
|
|
52
|
-
exitCode: 1,
|
|
53
|
-
messages: [],
|
|
54
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
55
|
-
error: `Unknown agent: ${agentName}`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
36
|
+
import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "./single-output.ts";
|
|
37
|
+
import {
|
|
38
|
+
buildModelCandidates,
|
|
39
|
+
formatModelAttemptNote,
|
|
40
|
+
isRetryableModelFailure,
|
|
41
|
+
} from "./model-fallback.ts";
|
|
58
42
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const modelArg = applyThinkingSuffix(effectiveModel, agent.thinking);
|
|
63
|
-
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
43
|
+
function emptyUsage(): Usage {
|
|
44
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
45
|
+
}
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
function sumUsage(target: Usage, source: Usage): void {
|
|
48
|
+
target.input += source.input;
|
|
49
|
+
target.output += source.output;
|
|
50
|
+
target.cacheRead += source.cacheRead;
|
|
51
|
+
target.cacheWrite += source.cacheWrite;
|
|
52
|
+
target.cost += source.cost;
|
|
53
|
+
target.turns += source.turns;
|
|
54
|
+
}
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
|
|
57
|
+
if (lines.length === 0) return;
|
|
58
|
+
progress.recentOutput.push(...lines.filter((line) => line.trim()));
|
|
59
|
+
if (progress.recentOutput.length > 50) {
|
|
60
|
+
progress.recentOutput.splice(0, progress.recentOutput.length - 50);
|
|
72
61
|
}
|
|
62
|
+
}
|
|
73
63
|
|
|
64
|
+
async function runSingleAttempt(
|
|
65
|
+
runtimeCwd: string,
|
|
66
|
+
agent: AgentConfig,
|
|
67
|
+
task: string,
|
|
68
|
+
model: string | undefined,
|
|
69
|
+
options: RunSyncOptions,
|
|
70
|
+
shared: {
|
|
71
|
+
sessionEnabled: boolean;
|
|
72
|
+
systemPrompt: string;
|
|
73
|
+
skillNames: string[];
|
|
74
|
+
resolvedSkillNames?: string[];
|
|
75
|
+
skillsWarning?: string;
|
|
76
|
+
jsonlPath?: string;
|
|
77
|
+
attemptNotes: string[];
|
|
78
|
+
outputSnapshot?: SingleOutputSnapshot;
|
|
79
|
+
},
|
|
80
|
+
): Promise<SingleResult> {
|
|
81
|
+
const modelArg = applyThinkingSuffix(model, agent.thinking);
|
|
74
82
|
const { args, env: sharedEnv, tempDir } = buildPiArgs({
|
|
75
83
|
baseArgs: ["--mode", "json", "-p"],
|
|
76
84
|
task,
|
|
77
|
-
sessionEnabled,
|
|
85
|
+
sessionEnabled: shared.sessionEnabled,
|
|
78
86
|
sessionDir: options.sessionDir,
|
|
79
87
|
sessionFile: options.sessionFile,
|
|
80
|
-
model
|
|
88
|
+
model,
|
|
81
89
|
thinking: agent.thinking,
|
|
82
90
|
tools: agent.tools,
|
|
83
91
|
extensions: agent.extensions,
|
|
84
|
-
skills: skillNames,
|
|
85
|
-
systemPrompt,
|
|
92
|
+
skills: shared.skillNames,
|
|
93
|
+
systemPrompt: shared.systemPrompt,
|
|
86
94
|
mcpDirectTools: agent.mcpDirectTools,
|
|
87
95
|
promptFileStem: agent.name,
|
|
88
96
|
});
|
|
89
97
|
|
|
90
98
|
const result: SingleResult = {
|
|
91
|
-
agent:
|
|
99
|
+
agent: agent.name,
|
|
92
100
|
task,
|
|
93
101
|
exitCode: 0,
|
|
94
102
|
messages: [],
|
|
95
|
-
usage:
|
|
103
|
+
usage: emptyUsage(),
|
|
96
104
|
model: modelArg,
|
|
97
|
-
skills:
|
|
98
|
-
skillsWarning:
|
|
105
|
+
skills: shared.resolvedSkillNames,
|
|
106
|
+
skillsWarning: shared.skillsWarning,
|
|
99
107
|
};
|
|
100
108
|
|
|
101
109
|
const progress: AgentProgress = {
|
|
102
|
-
index: index ?? 0,
|
|
103
|
-
agent:
|
|
110
|
+
index: options.index ?? 0,
|
|
111
|
+
agent: agent.name,
|
|
104
112
|
status: "running",
|
|
105
113
|
task,
|
|
106
|
-
skills:
|
|
114
|
+
skills: shared.resolvedSkillNames,
|
|
107
115
|
recentTools: [],
|
|
108
|
-
recentOutput: [],
|
|
116
|
+
recentOutput: [...shared.attemptNotes],
|
|
109
117
|
toolCount: 0,
|
|
110
118
|
tokens: 0,
|
|
111
119
|
durationMs: 0,
|
|
@@ -113,40 +121,25 @@ export async function runSync(
|
|
|
113
121
|
result.progress = progress;
|
|
114
122
|
|
|
115
123
|
const startTime = Date.now();
|
|
116
|
-
|
|
117
|
-
let artifactPathsResult: ArtifactPaths | undefined;
|
|
118
|
-
let jsonlPath: string | undefined;
|
|
119
|
-
if (artifactsDir && artifactConfig?.enabled !== false) {
|
|
120
|
-
artifactPathsResult = getArtifactPaths(artifactsDir, runId, agentName, index);
|
|
121
|
-
ensureArtifactsDir(artifactsDir);
|
|
122
|
-
if (artifactConfig?.includeInput !== false) {
|
|
123
|
-
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
|
|
124
|
-
}
|
|
125
|
-
if (artifactConfig?.includeJsonl !== false) {
|
|
126
|
-
jsonlPath = artifactPathsResult.jsonlPath;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
124
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
131
125
|
|
|
132
126
|
let closeJsonlWriter: (() => Promise<void>) | undefined;
|
|
133
127
|
const exitCode = await new Promise<number>((resolve) => {
|
|
134
128
|
const spawnSpec = getPiSpawnCommand(args);
|
|
135
129
|
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
|
136
|
-
cwd: cwd ?? runtimeCwd,
|
|
130
|
+
cwd: options.cwd ?? runtimeCwd,
|
|
137
131
|
env: spawnEnv,
|
|
138
132
|
stdio: ["ignore", "pipe", "pipe"],
|
|
139
133
|
});
|
|
140
|
-
const jsonlWriter = createJsonlWriter(jsonlPath, proc.stdout);
|
|
134
|
+
const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
|
|
141
135
|
closeJsonlWriter = () => jsonlWriter.close();
|
|
142
136
|
let buf = "";
|
|
143
|
-
|
|
144
137
|
let processClosed = false;
|
|
145
138
|
|
|
146
139
|
const fireUpdate = () => {
|
|
147
|
-
if (!onUpdate || processClosed) return;
|
|
140
|
+
if (!options.onUpdate || processClosed) return;
|
|
148
141
|
progress.durationMs = Date.now() - startTime;
|
|
149
|
-
onUpdate({
|
|
142
|
+
options.onUpdate({
|
|
150
143
|
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
151
144
|
details: { mode: "single", results: [result], progress: [progress] },
|
|
152
145
|
});
|
|
@@ -195,37 +188,14 @@ export async function runSync(
|
|
|
195
188
|
}
|
|
196
189
|
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
197
190
|
if (evt.message.errorMessage) result.error = evt.message.errorMessage;
|
|
198
|
-
|
|
199
|
-
const text = extractTextFromContent(evt.message.content);
|
|
200
|
-
if (text) {
|
|
201
|
-
const lines = text
|
|
202
|
-
.split("\n")
|
|
203
|
-
.filter((l) => l.trim())
|
|
204
|
-
.slice(-10);
|
|
205
|
-
// Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
|
|
206
|
-
progress.recentOutput.push(...lines);
|
|
207
|
-
if (progress.recentOutput.length > 50) {
|
|
208
|
-
progress.recentOutput.splice(0, progress.recentOutput.length - 50);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
191
|
+
appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
|
|
211
192
|
}
|
|
212
193
|
fireUpdate();
|
|
213
194
|
}
|
|
195
|
+
|
|
214
196
|
if (evt.type === "tool_result_end" && evt.message) {
|
|
215
197
|
result.messages.push(evt.message);
|
|
216
|
-
|
|
217
|
-
const toolText = extractTextFromContent(evt.message.content);
|
|
218
|
-
if (toolText) {
|
|
219
|
-
const toolLines = toolText
|
|
220
|
-
.split("\n")
|
|
221
|
-
.filter((l) => l.trim())
|
|
222
|
-
.slice(-10);
|
|
223
|
-
// Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
|
|
224
|
-
progress.recentOutput.push(...toolLines);
|
|
225
|
-
if (progress.recentOutput.length > 50) {
|
|
226
|
-
progress.recentOutput.splice(0, progress.recentOutput.length - 50);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
198
|
+
appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
|
|
229
199
|
fireUpdate();
|
|
230
200
|
}
|
|
231
201
|
} catch {
|
|
@@ -254,13 +224,13 @@ export async function runSync(
|
|
|
254
224
|
});
|
|
255
225
|
proc.on("error", () => resolve(1));
|
|
256
226
|
|
|
257
|
-
if (signal) {
|
|
227
|
+
if (options.signal) {
|
|
258
228
|
const kill = () => {
|
|
259
229
|
proc.kill("SIGTERM");
|
|
260
230
|
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
261
231
|
};
|
|
262
|
-
if (signal.aborted) kill();
|
|
263
|
-
else signal.addEventListener("abort", kill, { once: true });
|
|
232
|
+
if (options.signal.aborted) kill();
|
|
233
|
+
else options.signal.addEventListener("abort", kill, { once: true });
|
|
264
234
|
}
|
|
265
235
|
});
|
|
266
236
|
|
|
@@ -294,7 +264,6 @@ export async function runSync(
|
|
|
294
264
|
}
|
|
295
265
|
}
|
|
296
266
|
|
|
297
|
-
result.progress = progress;
|
|
298
267
|
result.progressSummary = {
|
|
299
268
|
toolCount: progress.toolCount,
|
|
300
269
|
tokens: progress.tokens,
|
|
@@ -303,29 +272,150 @@ export async function runSync(
|
|
|
303
272
|
|
|
304
273
|
let fullOutput = getFinalOutput(result.messages);
|
|
305
274
|
if (options.outputPath && result.exitCode === 0) {
|
|
306
|
-
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, outputSnapshot);
|
|
275
|
+
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
|
|
307
276
|
fullOutput = resolvedOutput.fullOutput;
|
|
308
277
|
result.savedOutputPath = resolvedOutput.savedPath;
|
|
309
278
|
result.outputSaveError = resolvedOutput.saveError;
|
|
310
279
|
}
|
|
311
280
|
result.finalOutput = fullOutput;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
312
283
|
|
|
313
|
-
|
|
314
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Run a subagent synchronously (blocking until complete)
|
|
286
|
+
*/
|
|
287
|
+
export async function runSync(
|
|
288
|
+
runtimeCwd: string,
|
|
289
|
+
agents: AgentConfig[],
|
|
290
|
+
agentName: string,
|
|
291
|
+
task: string,
|
|
292
|
+
options: RunSyncOptions,
|
|
293
|
+
): Promise<SingleResult> {
|
|
294
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
295
|
+
if (!agent) {
|
|
296
|
+
return {
|
|
297
|
+
agent: agentName,
|
|
298
|
+
task,
|
|
299
|
+
exitCode: 1,
|
|
300
|
+
messages: [],
|
|
301
|
+
usage: emptyUsage(),
|
|
302
|
+
error: `Unknown agent: ${agentName}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const shareEnabled = options.share === true;
|
|
307
|
+
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
308
|
+
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
309
|
+
const skillNames = options.skills ?? agent.skills ?? [];
|
|
310
|
+
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
|
|
311
|
+
let systemPrompt = agent.systemPrompt?.trim() || "";
|
|
312
|
+
if (resolvedSkills.length > 0) {
|
|
313
|
+
const skillInjection = buildSkillInjection(resolvedSkills);
|
|
314
|
+
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
|
|
315
|
+
}
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
const candidates = buildModelCandidates(
|
|
318
|
+
options.modelOverride ?? agent.model,
|
|
319
|
+
agent.fallbackModels,
|
|
320
|
+
options.availableModels,
|
|
321
|
+
);
|
|
322
|
+
const attemptedModels: string[] = [];
|
|
323
|
+
const modelAttempts: ModelAttempt[] = [];
|
|
324
|
+
const aggregateUsage = emptyUsage();
|
|
325
|
+
const attemptNotes: string[] = [];
|
|
326
|
+
let totalToolCount = 0;
|
|
327
|
+
let totalDurationMs = 0;
|
|
328
|
+
|
|
329
|
+
let artifactPathsResult: ArtifactPaths | undefined;
|
|
330
|
+
let jsonlPath: string | undefined;
|
|
331
|
+
if (options.artifactsDir && options.artifactConfig?.enabled !== false) {
|
|
332
|
+
artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
|
|
333
|
+
ensureArtifactsDir(options.artifactsDir);
|
|
334
|
+
if (options.artifactConfig?.includeInput !== false) {
|
|
335
|
+
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
|
|
336
|
+
}
|
|
337
|
+
if (options.artifactConfig?.includeJsonl !== false) {
|
|
338
|
+
jsonlPath = artifactPathsResult.jsonlPath;
|
|
318
339
|
}
|
|
319
|
-
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let lastResult: SingleResult | undefined;
|
|
343
|
+
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
344
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
345
|
+
const candidate = modelsToTry[i];
|
|
346
|
+
if (candidate) attemptedModels.push(candidate);
|
|
347
|
+
const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
|
|
348
|
+
sessionEnabled,
|
|
349
|
+
systemPrompt,
|
|
350
|
+
skillNames,
|
|
351
|
+
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
352
|
+
skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
|
|
353
|
+
jsonlPath,
|
|
354
|
+
attemptNotes,
|
|
355
|
+
outputSnapshot,
|
|
356
|
+
});
|
|
357
|
+
lastResult = result;
|
|
358
|
+
sumUsage(aggregateUsage, result.usage);
|
|
359
|
+
totalToolCount += result.progressSummary?.toolCount ?? 0;
|
|
360
|
+
totalDurationMs += result.progressSummary?.durationMs ?? 0;
|
|
361
|
+
const attempt: ModelAttempt = {
|
|
362
|
+
model: candidate ?? result.model ?? agent.model ?? "default",
|
|
363
|
+
success: result.exitCode === 0,
|
|
364
|
+
exitCode: result.exitCode,
|
|
365
|
+
error: result.error,
|
|
366
|
+
usage: { ...result.usage },
|
|
367
|
+
};
|
|
368
|
+
modelAttempts.push(attempt);
|
|
369
|
+
if (result.exitCode === 0) {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = lastResult ?? {
|
|
379
|
+
agent: agentName,
|
|
380
|
+
task,
|
|
381
|
+
exitCode: 1,
|
|
382
|
+
messages: [],
|
|
383
|
+
usage: emptyUsage(),
|
|
384
|
+
error: "Subagent did not produce a result.",
|
|
385
|
+
} satisfies SingleResult;
|
|
386
|
+
|
|
387
|
+
result.usage = aggregateUsage;
|
|
388
|
+
result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
|
|
389
|
+
result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
|
|
390
|
+
result.progressSummary = {
|
|
391
|
+
toolCount: totalToolCount,
|
|
392
|
+
tokens: aggregateUsage.input + aggregateUsage.output,
|
|
393
|
+
durationMs: totalDurationMs,
|
|
394
|
+
};
|
|
395
|
+
if (attemptNotes.length > 0 && result.progress) {
|
|
396
|
+
result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
|
|
397
|
+
if (result.progress.recentOutput.length > 50) {
|
|
398
|
+
result.progress.recentOutput.splice(50);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
|
|
403
|
+
result.artifactPaths = artifactPathsResult;
|
|
404
|
+
if (options.artifactConfig?.includeOutput !== false) {
|
|
405
|
+
writeArtifact(artifactPathsResult.outputPath, result.finalOutput ?? "");
|
|
406
|
+
}
|
|
407
|
+
if (options.artifactConfig?.includeMetadata !== false) {
|
|
320
408
|
writeMetadata(artifactPathsResult.metadataPath, {
|
|
321
|
-
runId,
|
|
409
|
+
runId: options.runId,
|
|
322
410
|
agent: agentName,
|
|
323
411
|
task,
|
|
324
412
|
exitCode: result.exitCode,
|
|
325
413
|
usage: result.usage,
|
|
326
414
|
model: result.model,
|
|
327
|
-
|
|
328
|
-
|
|
415
|
+
attemptedModels: result.attemptedModels,
|
|
416
|
+
modelAttempts: result.modelAttempts,
|
|
417
|
+
durationMs: result.progressSummary?.durationMs,
|
|
418
|
+
toolCount: result.progressSummary?.toolCount,
|
|
329
419
|
error: result.error,
|
|
330
420
|
skills: result.skills,
|
|
331
421
|
skillsWarning: result.skillsWarning,
|
|
@@ -333,19 +423,15 @@ export async function runSync(
|
|
|
333
423
|
});
|
|
334
424
|
}
|
|
335
425
|
|
|
336
|
-
if (maxOutput) {
|
|
337
|
-
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
338
|
-
const truncationResult = truncateOutput(
|
|
339
|
-
if (truncationResult.truncated)
|
|
340
|
-
result.truncation = truncationResult;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} else if (maxOutput) {
|
|
344
|
-
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
345
|
-
const truncationResult = truncateOutput(fullOutput, config);
|
|
346
|
-
if (truncationResult.truncated) {
|
|
347
|
-
result.truncation = truncationResult;
|
|
426
|
+
if (options.maxOutput) {
|
|
427
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
|
|
428
|
+
const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
|
|
429
|
+
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
348
430
|
}
|
|
431
|
+
} else if (options.maxOutput) {
|
|
432
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
|
|
433
|
+
const truncationResult = truncateOutput(result.finalOutput ?? "", config);
|
|
434
|
+
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
349
435
|
}
|
|
350
436
|
|
|
351
437
|
if (shareEnabled) {
|
|
@@ -353,8 +439,6 @@ export async function runSync(
|
|
|
353
439
|
?? (options.sessionDir ? findLatestSessionFile(options.sessionDir) : null);
|
|
354
440
|
if (sessionFile) {
|
|
355
441
|
result.sessionFile = sessionFile;
|
|
356
|
-
// HTML export disabled - module resolution issues with global pi installation
|
|
357
|
-
// Users can still access the session file directly
|
|
358
442
|
}
|
|
359
443
|
}
|
|
360
444
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Usage } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface AvailableModelInfo {
|
|
4
|
+
provider: string;
|
|
5
|
+
id: string;
|
|
6
|
+
fullId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ModelAttemptSummary {
|
|
10
|
+
model: string;
|
|
11
|
+
success: boolean;
|
|
12
|
+
exitCode?: number | null;
|
|
13
|
+
error?: string;
|
|
14
|
+
usage?: Usage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
18
|
+
const colonIdx = model.lastIndexOf(":");
|
|
19
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
20
|
+
return {
|
|
21
|
+
baseModel: model.substring(0, colonIdx),
|
|
22
|
+
thinkingSuffix: model.substring(colonIdx),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveModelCandidate(
|
|
27
|
+
model: string | undefined,
|
|
28
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
29
|
+
): string | undefined {
|
|
30
|
+
if (!model) return undefined;
|
|
31
|
+
if (model.includes("/")) return model;
|
|
32
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
33
|
+
|
|
34
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
35
|
+
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
36
|
+
if (matches.length !== 1) return model;
|
|
37
|
+
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildModelCandidates(
|
|
41
|
+
primaryModel: string | undefined,
|
|
42
|
+
fallbackModels: string[] | undefined,
|
|
43
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
44
|
+
): string[] {
|
|
45
|
+
const seen = new Set<string>();
|
|
46
|
+
const candidates: string[] = [];
|
|
47
|
+
for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
|
|
48
|
+
if (!raw) continue;
|
|
49
|
+
const normalized = resolveModelCandidate(raw.trim(), availableModels);
|
|
50
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
51
|
+
seen.add(normalized);
|
|
52
|
+
candidates.push(normalized);
|
|
53
|
+
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
58
|
+
/rate\s*limit/i,
|
|
59
|
+
/too many requests/i,
|
|
60
|
+
/\b429\b/,
|
|
61
|
+
/quota/i,
|
|
62
|
+
/billing/i,
|
|
63
|
+
/credit/i,
|
|
64
|
+
/auth(?:entication)?/i,
|
|
65
|
+
/unauthori[sz]ed/i,
|
|
66
|
+
/forbidden/i,
|
|
67
|
+
/api key/i,
|
|
68
|
+
/token expired/i,
|
|
69
|
+
/invalid key/i,
|
|
70
|
+
/provider.*unavailable/i,
|
|
71
|
+
/model.*unavailable/i,
|
|
72
|
+
/model.*disabled/i,
|
|
73
|
+
/model.*not found/i,
|
|
74
|
+
/unknown model/i,
|
|
75
|
+
/overloaded/i,
|
|
76
|
+
/service unavailable/i,
|
|
77
|
+
/temporar(?:ily)? unavailable/i,
|
|
78
|
+
/connection refused/i,
|
|
79
|
+
/fetch failed/i,
|
|
80
|
+
/network error/i,
|
|
81
|
+
/socket hang up/i,
|
|
82
|
+
/upstream/i,
|
|
83
|
+
/timed? out/i,
|
|
84
|
+
/timeout/i,
|
|
85
|
+
/\b502\b/,
|
|
86
|
+
/\b503\b/,
|
|
87
|
+
/\b504\b/,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
91
|
+
if (!error) return false;
|
|
92
|
+
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
96
|
+
const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
|
|
97
|
+
return nextModel
|
|
98
|
+
? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
|
|
99
|
+
: `[fallback] ${attempt.model} failed: ${failure}.`;
|
|
100
|
+
}
|
package/package.json
CHANGED
package/parallel-utils.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface RunnerSubagentStep {
|
|
|
10
10
|
task: string;
|
|
11
11
|
cwd?: string;
|
|
12
12
|
model?: string;
|
|
13
|
+
modelCandidates?: string[];
|
|
13
14
|
tools?: string[];
|
|
14
15
|
extensions?: string[];
|
|
15
16
|
mcpDirectTools?: string[];
|
|
@@ -83,6 +84,8 @@ export interface ParallelTaskResult {
|
|
|
83
84
|
output: string;
|
|
84
85
|
exitCode: number | null;
|
|
85
86
|
error?: string;
|
|
87
|
+
model?: string;
|
|
88
|
+
attemptedModels?: string[];
|
|
86
89
|
outputTargetPath?: string;
|
|
87
90
|
outputTargetExists?: boolean;
|
|
88
91
|
}
|
package/pi-args.ts
CHANGED
|
@@ -51,10 +51,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
51
51
|
|
|
52
52
|
const modelArg = applyThinkingSuffix(input.model, input.thinking);
|
|
53
53
|
if (modelArg) {
|
|
54
|
-
|
|
55
|
-
// without a companion --provider flag. --models resolves the provider
|
|
56
|
-
// automatically via resolveModelScope. See: #8
|
|
57
|
-
args.push("--models", modelArg);
|
|
54
|
+
args.push("--model", modelArg);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
const toolExtensionPaths: string[] = [];
|
package/render.ts
CHANGED
|
@@ -265,6 +265,9 @@ export function renderSubagentResult(
|
|
|
265
265
|
if (r.skillsWarning) {
|
|
266
266
|
c.addChild(new Text(truncLine(theme.fg("warning", `⚠️ ${r.skillsWarning}`), w), 0, 0));
|
|
267
267
|
}
|
|
268
|
+
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
269
|
+
c.addChild(new Text(truncLine(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
|
|
270
|
+
}
|
|
268
271
|
c.addChild(new Text(truncLine(theme.fg("dim", formatUsage(r.usage, r.model)), w), 0, 0));
|
|
269
272
|
if (r.sessionFile) {
|
|
270
273
|
c.addChild(new Text(truncLine(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), w), 0, 0));
|
|
@@ -427,6 +430,9 @@ export function renderSubagentResult(
|
|
|
427
430
|
if (r.skillsWarning) {
|
|
428
431
|
c.addChild(new Text(truncLine(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), w), 0, 0));
|
|
429
432
|
}
|
|
433
|
+
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
434
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
|
|
435
|
+
}
|
|
430
436
|
|
|
431
437
|
if (rRunning && rProg) {
|
|
432
438
|
if (rProg.skills?.length) {
|