@tea-agent/loop-agent 0.26.2 → 0.26.4
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 +43 -0
- package/dist/executors/dag-pi-executor.js +182 -10
- package/dist/executors/pi-executor.js +233 -147
- package/dist/executors/pi-sdk-executor.js +140 -81
- package/dist/executors/pi-writer-tool-policy.js +266 -0
- package/dist/executors/shell-executor.js +355 -52
- package/dist/executors/shell-write-guard.js +145 -12
- package/dist/governance/document-index-closure.js +164 -0
- package/dist/worker/observe/node-input.js +8 -11
- package/dist/workflows/dag/convergence/controller.js +100 -3
- package/dist/workflows/dag/frontend-repair.js +29 -29
- package/dist/workflows/dag/frontend-verification-trace.js +12 -2
- package/dist/workflows/dag/governance-profile.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +70 -16
- package/dist/workflows/dag/repair-artifact.js +100 -4
- package/dist/workflows/dag/rerun-plan.js +14 -7
- package/dist/workflows/dag/types.js +7 -1
- package/dist/workflows/dag/workspace-checkpoint.js +42 -0
- package/docs/templates/init-managed-agents.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +13 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { spawn, spawnSync } from
|
|
2
|
-
import { existsSync } from
|
|
3
|
-
import { homedir } from
|
|
4
|
-
import path from
|
|
5
|
-
import { PI_REUSE_RUNTIME_ENV, isPiReuseRuntimeEffective, resolvePiReuseRuntimeMode as resolveConfiguredPiReuseRuntimeMode, } from
|
|
6
|
-
import { cleanupPiPromptTransport, createPiPromptTransport, } from
|
|
7
|
-
import { processTreeSpawnOptions, terminateProcessTree } from
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { PI_REUSE_RUNTIME_ENV, isPiReuseRuntimeEffective, resolvePiReuseRuntimeMode as resolveConfiguredPiReuseRuntimeMode, } from "./pi-runtime-reuse.js";
|
|
6
|
+
import { cleanupPiPromptTransport, createPiPromptTransport, } from "./pi-prompt-transport.js";
|
|
7
|
+
import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
|
|
8
8
|
export function extractSubagentStats(stdout) {
|
|
9
9
|
let totalCalls = 0;
|
|
10
10
|
let failedCalls = 0;
|
|
@@ -16,20 +16,22 @@ export function extractSubagentStats(stdout) {
|
|
|
16
16
|
continue;
|
|
17
17
|
try {
|
|
18
18
|
const event = JSON.parse(trimmed);
|
|
19
|
-
if (event.type ===
|
|
19
|
+
if (event.type === "tool_execution_start" &&
|
|
20
|
+
event.toolName === "subagent") {
|
|
20
21
|
totalCalls += 1;
|
|
21
22
|
if (isRecord(event.input)) {
|
|
22
23
|
if (event.input.agent)
|
|
23
24
|
agents.add(String(event.input.agent));
|
|
24
25
|
if (event.input.tasks)
|
|
25
|
-
modes.add(
|
|
26
|
+
modes.add("parallel");
|
|
26
27
|
if (event.input.chain)
|
|
27
|
-
modes.add(
|
|
28
|
+
modes.add("chain");
|
|
28
29
|
if (event.input.agent && !event.input.tasks && !event.input.chain)
|
|
29
|
-
modes.add(
|
|
30
|
+
modes.add("single");
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
if (event.type ===
|
|
33
|
+
if (event.type === "tool_execution_end" &&
|
|
34
|
+
event.toolName === "subagent") {
|
|
33
35
|
if (event.isError === true)
|
|
34
36
|
failedCalls += 1;
|
|
35
37
|
}
|
|
@@ -39,15 +41,15 @@ export function extractSubagentStats(stdout) {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
if (totalCalls === 0)
|
|
42
|
-
return
|
|
44
|
+
return "calls=0";
|
|
43
45
|
const parts = [`calls=${totalCalls}`];
|
|
44
46
|
if (failedCalls > 0)
|
|
45
47
|
parts.push(`${failedCalls} failed`);
|
|
46
48
|
if (modes.size > 0)
|
|
47
|
-
parts.push(Array.from(modes).sort().join(
|
|
49
|
+
parts.push(Array.from(modes).sort().join("+"));
|
|
48
50
|
if (agents.size > 0)
|
|
49
|
-
parts.push(`agents=[${Array.from(agents).sort().join(
|
|
50
|
-
return parts.join(
|
|
51
|
+
parts.push(`agents=[${Array.from(agents).sort().join(",")}]`);
|
|
52
|
+
return parts.join(" | ");
|
|
51
53
|
}
|
|
52
54
|
/** Absolute max wall clock for a single Pi attempt (4h). Not renewed by empty heartbeats. */
|
|
53
55
|
export const DEFAULT_TIMEOUT_MS = 14_400_000;
|
|
@@ -56,14 +58,14 @@ export const MAX_ALLOWED_TIMEOUT_MS = 14_400_000;
|
|
|
56
58
|
export const DEFAULT_STALL_TIMEOUT_MS = 900_000;
|
|
57
59
|
/** Grace after SIGTERM before SIGKILL on absolute max. */
|
|
58
60
|
export const DEFAULT_ABORT_GRACE_MS = 30_000;
|
|
59
|
-
const PI_BACKEND_ENV =
|
|
61
|
+
const PI_BACKEND_ENV = "CODE_AGENT_PI_BACKEND";
|
|
60
62
|
const OUTPUT_PREVIEW_HEAD_CHARS = 64_000;
|
|
61
63
|
const OUTPUT_PREVIEW_TAIL_CHARS = 64_000;
|
|
62
64
|
const MAX_STREAM_JSONL_LINE_CHARS = 16_000_000;
|
|
63
65
|
const MAX_ASSISTANT_TEXT_CHARS = 16_000_000;
|
|
64
66
|
const PI_CLI_PACKAGE_NAMES = [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
"@earendil-works/pi-coding-agent",
|
|
68
|
+
"@mariozechner/pi-coding-agent",
|
|
67
69
|
];
|
|
68
70
|
export { PI_REUSE_RUNTIME_ENV };
|
|
69
71
|
/** Resolve configured Pi SDK runtime reuse mode. Default off; unknown values fall back to off. */
|
|
@@ -82,7 +84,7 @@ export async function withPiReuseRuntimeScope(fn) {
|
|
|
82
84
|
if (!isPiReuseRuntimeEnabled()) {
|
|
83
85
|
return fn();
|
|
84
86
|
}
|
|
85
|
-
const { beginPiSdkReuseScope, endPiSdkReuseScope } = await import(
|
|
87
|
+
const { beginPiSdkReuseScope, endPiSdkReuseScope } = await import("./pi-sdk-executor.js");
|
|
86
88
|
beginPiSdkReuseScope();
|
|
87
89
|
try {
|
|
88
90
|
return await fn();
|
|
@@ -94,14 +96,14 @@ export async function withPiReuseRuntimeScope(fn) {
|
|
|
94
96
|
/** Resolve configured Pi backend mode. Default sdk-first; cli-only preserves pure CLI path. */
|
|
95
97
|
export function resolvePiBackend() {
|
|
96
98
|
const raw = process.env[PI_BACKEND_ENV]?.trim().toLowerCase();
|
|
97
|
-
if (raw ===
|
|
98
|
-
return
|
|
99
|
-
if (raw ===
|
|
100
|
-
return
|
|
101
|
-
return
|
|
99
|
+
if (raw === "cli-only")
|
|
100
|
+
return "cli-only";
|
|
101
|
+
if (raw === "sdk-first" || !raw)
|
|
102
|
+
return "sdk-first";
|
|
103
|
+
return "sdk-first";
|
|
102
104
|
}
|
|
103
105
|
function piCliPathFromPackageRoot(nodeModulesRoot, packageName) {
|
|
104
|
-
return path.join(nodeModulesRoot, ...packageName.split(
|
|
106
|
+
return path.join(nodeModulesRoot, ...packageName.split("/"), "dist", "cli.js");
|
|
105
107
|
}
|
|
106
108
|
function findExistingPiCliUnderNodeModules(nodeModulesRoot) {
|
|
107
109
|
for (const packageName of PI_CLI_PACKAGE_NAMES) {
|
|
@@ -113,7 +115,7 @@ function findExistingPiCliUnderNodeModules(nodeModulesRoot) {
|
|
|
113
115
|
}
|
|
114
116
|
function resolveGlobalNpmPiCliPath() {
|
|
115
117
|
try {
|
|
116
|
-
const npmRoot = spawnSync(
|
|
118
|
+
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
117
119
|
if (npmRoot.status === 0 && npmRoot.stdout.trim()) {
|
|
118
120
|
return findExistingPiCliUnderNodeModules(npmRoot.stdout.trim());
|
|
119
121
|
}
|
|
@@ -127,23 +129,25 @@ function resolvePiCliNearShim(shimPath) {
|
|
|
127
129
|
if (!existsSync(shimPath))
|
|
128
130
|
return undefined;
|
|
129
131
|
const shimDir = path.dirname(shimPath);
|
|
130
|
-
const nearGlobalShim = findExistingPiCliUnderNodeModules(path.join(shimDir,
|
|
132
|
+
const nearGlobalShim = findExistingPiCliUnderNodeModules(path.join(shimDir, "node_modules"));
|
|
131
133
|
if (nearGlobalShim)
|
|
132
134
|
return nearGlobalShim;
|
|
133
135
|
if (shimDir.endsWith(`${path.sep}.bin`)) {
|
|
134
|
-
const nearLocalBin = findExistingPiCliUnderNodeModules(path.join(shimDir,
|
|
136
|
+
const nearLocalBin = findExistingPiCliUnderNodeModules(path.join(shimDir, ".."));
|
|
135
137
|
if (nearLocalBin)
|
|
136
138
|
return nearLocalBin;
|
|
137
139
|
}
|
|
138
140
|
return undefined;
|
|
139
141
|
}
|
|
140
142
|
function resolvePiCliFromPathLookup() {
|
|
141
|
-
const lookupCommands = process.platform ===
|
|
142
|
-
? [{ command:
|
|
143
|
-
: [{ command:
|
|
143
|
+
const lookupCommands = process.platform === "win32"
|
|
144
|
+
? [{ command: "where.exe", args: ["pi"] }]
|
|
145
|
+
: [{ command: "which", args: ["pi"] }];
|
|
144
146
|
for (const lookup of lookupCommands) {
|
|
145
147
|
try {
|
|
146
|
-
const result = spawnSync(lookup.command, lookup.args, {
|
|
148
|
+
const result = spawnSync(lookup.command, lookup.args, {
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
});
|
|
147
151
|
if (result.status !== 0 || !result.stdout.trim())
|
|
148
152
|
continue;
|
|
149
153
|
for (const rawPath of result.stdout.split(/\r?\n/)) {
|
|
@@ -153,10 +157,10 @@ function resolvePiCliFromPathLookup() {
|
|
|
153
157
|
const nearShim = resolvePiCliNearShim(lookupPath);
|
|
154
158
|
if (nearShim)
|
|
155
159
|
return nearShim;
|
|
156
|
-
if (process.platform !==
|
|
160
|
+
if (process.platform !== "win32") {
|
|
157
161
|
try {
|
|
158
|
-
const realPathResult = spawnSync(
|
|
159
|
-
encoding:
|
|
162
|
+
const realPathResult = spawnSync("readlink", ["-f", lookupPath], {
|
|
163
|
+
encoding: "utf-8",
|
|
160
164
|
});
|
|
161
165
|
if (realPathResult.status === 0 && realPathResult.stdout.trim()) {
|
|
162
166
|
const resolved = realPathResult.stdout.trim();
|
|
@@ -191,20 +195,20 @@ function resolvePiCliPath() {
|
|
|
191
195
|
const home = homedir();
|
|
192
196
|
// Try nvm paths with both "v"-prefixed and non-prefixed version directories
|
|
193
197
|
const nodeVersion = process.versions.node; // e.g. "22.19.0"
|
|
194
|
-
const nvmBase = path.join(home,
|
|
198
|
+
const nvmBase = path.join(home, ".nvm", "versions", "node");
|
|
195
199
|
const nvmCandidates = [
|
|
196
|
-
path.join(nvmBase, `v${nodeVersion}`,
|
|
197
|
-
path.join(nvmBase, nodeVersion,
|
|
200
|
+
path.join(nvmBase, `v${nodeVersion}`, "lib", "node_modules"),
|
|
201
|
+
path.join(nvmBase, nodeVersion, "lib", "node_modules"),
|
|
198
202
|
].flatMap((nodeModulesRoot) => PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(nodeModulesRoot, packageName)));
|
|
199
203
|
const otherCandidates = [
|
|
200
204
|
// Prefer the project-local Pi dependency. `npm run` adds node_modules/.bin
|
|
201
205
|
// to PATH, but direct invocations from the DAG runner may not, so resolve
|
|
202
206
|
// the package from the current workspace explicitly.
|
|
203
|
-
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.resolve(process.cwd(),
|
|
207
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.resolve(process.cwd(), "node_modules"), packageName)),
|
|
204
208
|
// Global npm installation (non-nvm)
|
|
205
|
-
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(
|
|
209
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot("/usr/local/lib/node_modules", packageName)),
|
|
206
210
|
// Bun installation
|
|
207
|
-
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.join(home,
|
|
211
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.join(home, ".bun", "install", "global", "node_modules"), packageName)),
|
|
208
212
|
];
|
|
209
213
|
const allCandidates = [...nvmCandidates, ...otherCandidates];
|
|
210
214
|
for (const candidate of allCandidates) {
|
|
@@ -218,7 +222,7 @@ function resolvePiCliPath() {
|
|
|
218
222
|
const pathLookupPi = resolvePiCliFromPathLookup();
|
|
219
223
|
if (pathLookupPi)
|
|
220
224
|
return pathLookupPi;
|
|
221
|
-
throw new Error(
|
|
225
|
+
throw new Error("Could not resolve pi CLI path. Ensure pi is installed.");
|
|
222
226
|
}
|
|
223
227
|
let cachedPiCliPath;
|
|
224
228
|
/** Lazily resolve and cache the pi CLI script path (avoids import-time sync I/O). */
|
|
@@ -235,17 +239,19 @@ export function resetPiCliPathCacheForTests() {
|
|
|
235
239
|
export function checkPiAvailability() {
|
|
236
240
|
// Use process.execPath to run pi CLI directly, avoiding shebang-based
|
|
237
241
|
// resolution that can drop built-in providers when spawned from tsx.
|
|
238
|
-
const result = spawnSync(process.execPath, [getPiCliPath(),
|
|
242
|
+
const result = spawnSync(process.execPath, [getPiCliPath(), "--help"], {
|
|
243
|
+
encoding: "utf-8",
|
|
244
|
+
});
|
|
239
245
|
if (result.error) {
|
|
240
246
|
return { ok: false, detail: result.error.message };
|
|
241
247
|
}
|
|
242
248
|
if (result.status !== 0) {
|
|
243
249
|
return { ok: false, detail: result.stderr || `exit ${result.status}` };
|
|
244
250
|
}
|
|
245
|
-
return { ok: true, detail:
|
|
251
|
+
return { ok: true, detail: "pi available" };
|
|
246
252
|
}
|
|
247
253
|
export function extractAssistantTextFromPiJson(stdout) {
|
|
248
|
-
let assistantText =
|
|
254
|
+
let assistantText = "";
|
|
249
255
|
let parsedEvents = 0;
|
|
250
256
|
for (const line of stdout.split(/\r?\n/)) {
|
|
251
257
|
const trimmed = line.trim();
|
|
@@ -292,9 +298,22 @@ function extractUsageFromEvent(event) {
|
|
|
292
298
|
for (const candidate of candidates) {
|
|
293
299
|
if (!isRecord(candidate))
|
|
294
300
|
continue;
|
|
295
|
-
const input = readNumericUsageField(candidate, [
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
const input = readNumericUsageField(candidate, [
|
|
302
|
+
"input_tokens",
|
|
303
|
+
"inputTokens",
|
|
304
|
+
"prompt_tokens",
|
|
305
|
+
"promptTokens",
|
|
306
|
+
]);
|
|
307
|
+
const output = readNumericUsageField(candidate, [
|
|
308
|
+
"output_tokens",
|
|
309
|
+
"outputTokens",
|
|
310
|
+
"completion_tokens",
|
|
311
|
+
"completionTokens",
|
|
312
|
+
]);
|
|
313
|
+
const total = readNumericUsageField(candidate, [
|
|
314
|
+
"total_tokens",
|
|
315
|
+
"totalTokens",
|
|
316
|
+
]);
|
|
298
317
|
if (input !== null && output !== null)
|
|
299
318
|
return input + output;
|
|
300
319
|
if (total !== null)
|
|
@@ -305,20 +324,20 @@ function extractUsageFromEvent(event) {
|
|
|
305
324
|
function readNumericUsageField(record, keys) {
|
|
306
325
|
for (const key of keys) {
|
|
307
326
|
const value = record[key];
|
|
308
|
-
if (typeof value ===
|
|
327
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
309
328
|
return Math.trunc(value);
|
|
310
329
|
}
|
|
311
330
|
}
|
|
312
331
|
return null;
|
|
313
332
|
}
|
|
314
333
|
function extractAssistantTextFromEvent(event) {
|
|
315
|
-
if (event.type ===
|
|
334
|
+
if (event.type === "turn_end" && isRecord(event.message)) {
|
|
316
335
|
return extractAssistantTextFromMessage(event.message);
|
|
317
336
|
}
|
|
318
|
-
if (event.type ===
|
|
337
|
+
if (event.type === "message_end" && isRecord(event.message)) {
|
|
319
338
|
return extractAssistantTextFromMessage(event.message);
|
|
320
339
|
}
|
|
321
|
-
if (event.type ===
|
|
340
|
+
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
322
341
|
for (let i = event.messages.length - 1; i >= 0; i -= 1) {
|
|
323
342
|
const message = event.messages[i];
|
|
324
343
|
if (isRecord(message)) {
|
|
@@ -328,27 +347,27 @@ function extractAssistantTextFromEvent(event) {
|
|
|
328
347
|
}
|
|
329
348
|
}
|
|
330
349
|
}
|
|
331
|
-
return
|
|
350
|
+
return "";
|
|
332
351
|
}
|
|
333
352
|
function extractAssistantTextFromMessage(message) {
|
|
334
|
-
if (message.role !==
|
|
335
|
-
return
|
|
353
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
354
|
+
return "";
|
|
336
355
|
const textParts = message.content
|
|
337
356
|
.filter(isRecord)
|
|
338
|
-
.filter((item) => item.type ===
|
|
357
|
+
.filter((item) => item.type === "text" && typeof item.text === "string")
|
|
339
358
|
.map((item) => item.text);
|
|
340
|
-
return textParts.join(
|
|
359
|
+
return textParts.join("\n").trim();
|
|
341
360
|
}
|
|
342
361
|
function isRecord(value) {
|
|
343
|
-
return typeof value ===
|
|
362
|
+
return typeof value === "object" && value !== null;
|
|
344
363
|
}
|
|
345
364
|
export class BoundedTextPreview {
|
|
346
365
|
label;
|
|
347
366
|
headMax;
|
|
348
367
|
tailMax;
|
|
349
368
|
parts = [];
|
|
350
|
-
head =
|
|
351
|
-
tail =
|
|
369
|
+
head = "";
|
|
370
|
+
tail = "";
|
|
352
371
|
bufferedChars = 0;
|
|
353
372
|
totalChars = 0;
|
|
354
373
|
truncated = false;
|
|
@@ -367,7 +386,7 @@ export class BoundedTextPreview {
|
|
|
367
386
|
this.bufferedChars += chunk.length;
|
|
368
387
|
return;
|
|
369
388
|
}
|
|
370
|
-
const combined = this.parts.join(
|
|
389
|
+
const combined = this.parts.join("") + chunk;
|
|
371
390
|
this.head = combined.slice(0, this.headMax);
|
|
372
391
|
this.tail = combined.slice(-this.tailMax);
|
|
373
392
|
this.parts = [];
|
|
@@ -379,19 +398,19 @@ export class BoundedTextPreview {
|
|
|
379
398
|
}
|
|
380
399
|
text() {
|
|
381
400
|
if (!this.truncated)
|
|
382
|
-
return this.parts.join(
|
|
401
|
+
return this.parts.join("");
|
|
383
402
|
const omitted = Math.max(0, this.totalChars - this.head.length - this.tail.length);
|
|
384
403
|
return [
|
|
385
404
|
this.head,
|
|
386
405
|
`[${this.label} truncated: omitted ${omitted} chars; kept first ${this.head.length} and last ${this.tail.length}]`,
|
|
387
406
|
this.tail,
|
|
388
|
-
].join(
|
|
407
|
+
].join("\n");
|
|
389
408
|
}
|
|
390
409
|
}
|
|
391
410
|
export class PiJsonlStreamCollector {
|
|
392
|
-
lineBuffer =
|
|
411
|
+
lineBuffer = "";
|
|
393
412
|
discardUntilNewline = false;
|
|
394
|
-
assistantText =
|
|
413
|
+
assistantText = "";
|
|
395
414
|
parsedEvents = 0;
|
|
396
415
|
tokensUsed = 0;
|
|
397
416
|
outputTooLarge = false;
|
|
@@ -404,7 +423,7 @@ export class PiJsonlStreamCollector {
|
|
|
404
423
|
return;
|
|
405
424
|
const lines = chunk.split(/\r?\n/);
|
|
406
425
|
for (let index = 0; index < lines.length; index += 1) {
|
|
407
|
-
const part = lines[index] ??
|
|
426
|
+
const part = lines[index] ?? "";
|
|
408
427
|
const isLast = index === lines.length - 1;
|
|
409
428
|
if (this.discardUntilNewline) {
|
|
410
429
|
if (!isLast)
|
|
@@ -413,7 +432,7 @@ export class PiJsonlStreamCollector {
|
|
|
413
432
|
}
|
|
414
433
|
if (this.lineBuffer.length + part.length > MAX_STREAM_JSONL_LINE_CHARS) {
|
|
415
434
|
this.outputTooLarge = true;
|
|
416
|
-
this.lineBuffer =
|
|
435
|
+
this.lineBuffer = "";
|
|
417
436
|
if (isLast)
|
|
418
437
|
this.discardUntilNewline = true;
|
|
419
438
|
continue;
|
|
@@ -421,7 +440,7 @@ export class PiJsonlStreamCollector {
|
|
|
421
440
|
this.lineBuffer += part;
|
|
422
441
|
if (!isLast) {
|
|
423
442
|
this.consumeLine(this.lineBuffer);
|
|
424
|
-
this.lineBuffer =
|
|
443
|
+
this.lineBuffer = "";
|
|
425
444
|
}
|
|
426
445
|
}
|
|
427
446
|
}
|
|
@@ -429,7 +448,7 @@ export class PiJsonlStreamCollector {
|
|
|
429
448
|
if (!this.discardUntilNewline && this.lineBuffer.trim()) {
|
|
430
449
|
this.consumeLine(this.lineBuffer);
|
|
431
450
|
}
|
|
432
|
-
this.lineBuffer =
|
|
451
|
+
this.lineBuffer = "";
|
|
433
452
|
return {
|
|
434
453
|
assistantText: this.assistantText.trim(),
|
|
435
454
|
outputTooLarge: this.outputTooLarge,
|
|
@@ -465,47 +484,51 @@ export class PiJsonlStreamCollector {
|
|
|
465
484
|
const usage = extractUsageFromEvent(event);
|
|
466
485
|
if (usage !== null)
|
|
467
486
|
this.tokensUsed += usage;
|
|
468
|
-
if (event.type ===
|
|
487
|
+
if (event.type === "tool_execution_start" &&
|
|
488
|
+
event.toolName === "subagent") {
|
|
469
489
|
this.subagentTotalCalls += 1;
|
|
470
490
|
if (isRecord(event.input)) {
|
|
471
491
|
if (event.input.agent)
|
|
472
492
|
this.subagentAgents.add(String(event.input.agent));
|
|
473
493
|
if (event.input.tasks)
|
|
474
|
-
this.subagentModes.add(
|
|
494
|
+
this.subagentModes.add("parallel");
|
|
475
495
|
if (event.input.chain)
|
|
476
|
-
this.subagentModes.add(
|
|
496
|
+
this.subagentModes.add("chain");
|
|
477
497
|
if (event.input.agent && !event.input.tasks && !event.input.chain) {
|
|
478
|
-
this.subagentModes.add(
|
|
498
|
+
this.subagentModes.add("single");
|
|
479
499
|
}
|
|
480
500
|
}
|
|
481
501
|
}
|
|
482
|
-
if (event.type ===
|
|
502
|
+
if (event.type === "tool_execution_end" && event.toolName === "subagent") {
|
|
483
503
|
if (event.isError === true)
|
|
484
504
|
this.subagentFailedCalls += 1;
|
|
485
505
|
}
|
|
486
506
|
}
|
|
487
507
|
formatSubagentStats() {
|
|
488
508
|
if (this.subagentTotalCalls === 0)
|
|
489
|
-
return
|
|
509
|
+
return "calls=0";
|
|
490
510
|
const parts = [`calls=${this.subagentTotalCalls}`];
|
|
491
511
|
if (this.subagentFailedCalls > 0)
|
|
492
512
|
parts.push(`${this.subagentFailedCalls} failed`);
|
|
493
513
|
if (this.subagentModes.size > 0)
|
|
494
|
-
parts.push(Array.from(this.subagentModes).sort().join(
|
|
514
|
+
parts.push(Array.from(this.subagentModes).sort().join("+"));
|
|
495
515
|
if (this.subagentAgents.size > 0) {
|
|
496
|
-
parts.push(`agents=[${Array.from(this.subagentAgents).sort().join(
|
|
516
|
+
parts.push(`agents=[${Array.from(this.subagentAgents).sort().join(",")}]`);
|
|
497
517
|
}
|
|
498
|
-
return parts.join(
|
|
518
|
+
return parts.join(" | ");
|
|
499
519
|
}
|
|
500
520
|
}
|
|
501
521
|
export function createPiJsonlStreamCollector() {
|
|
502
522
|
return new PiJsonlStreamCollector();
|
|
503
523
|
}
|
|
504
524
|
function withSubagentStats(result) {
|
|
505
|
-
return {
|
|
525
|
+
return {
|
|
526
|
+
...result,
|
|
527
|
+
subagentStats: result.subagentStats ?? extractSubagentStats(result.stdout),
|
|
528
|
+
};
|
|
506
529
|
}
|
|
507
530
|
function withCliBackend(result) {
|
|
508
|
-
return { ...result, backend:
|
|
531
|
+
return { ...result, backend: "cli" };
|
|
509
532
|
}
|
|
510
533
|
/** CLI-only orchestration: primary attempt + optional fallback model retry. */
|
|
511
534
|
async function runCliOrchestration(options, meta) {
|
|
@@ -534,15 +557,20 @@ async function runCliOrchestrationWithPromptFile(options, promptFilePath, meta)
|
|
|
534
557
|
}
|
|
535
558
|
const primaryHardFail = !primaryResult.ok;
|
|
536
559
|
const primaryInvalidOutput = primaryResult.ok && primaryIssues.length > 0;
|
|
537
|
-
const shouldFallback = primary.fallback &&
|
|
538
|
-
&&
|
|
539
|
-
|
|
540
|
-
|
|
560
|
+
const shouldFallback = primary.fallback &&
|
|
561
|
+
((primaryHardFail &&
|
|
562
|
+
primaryResult.failureCategory !== "termination-unconfirmed" &&
|
|
563
|
+
shouldRetryWithFallback(primaryResult.stderr, primaryResult.stdout)) ||
|
|
564
|
+
primaryInvalidOutput);
|
|
541
565
|
if (shouldFallback) {
|
|
542
566
|
const fallbackResult = await executeSingleCliAttempt(options, primary.fallback, promptFilePath);
|
|
543
567
|
attemptedModels.push(fallbackResult.modelDisplay);
|
|
544
|
-
const combinedStderr = [primaryResult.stderr, fallbackResult.stderr]
|
|
545
|
-
|
|
568
|
+
const combinedStderr = [primaryResult.stderr, fallbackResult.stderr]
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.join("\n--- fallback ---\n");
|
|
571
|
+
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout]
|
|
572
|
+
.filter(Boolean)
|
|
573
|
+
.join("\n--- fallback ---\n");
|
|
546
574
|
const combinedStdoutTruncated = primaryResult.stdoutTruncated || fallbackResult.stdoutTruncated;
|
|
547
575
|
const combinedStderrTruncated = primaryResult.stderrTruncated || fallbackResult.stderrTruncated;
|
|
548
576
|
const combinedOutputTooLarge = primaryResult.outputTooLarge || fallbackResult.outputTooLarge;
|
|
@@ -567,11 +595,11 @@ async function runCliOrchestrationWithPromptFile(options, promptFilePath, meta)
|
|
|
567
595
|
return withSubagentStats(withCliBackend({
|
|
568
596
|
...fallbackResult,
|
|
569
597
|
ok: false,
|
|
570
|
-
failureCategory:
|
|
598
|
+
failureCategory: "invalid-output",
|
|
571
599
|
attemptedModels,
|
|
572
600
|
fallbackUsed: true,
|
|
573
601
|
sdkAttempted: meta?.sdkAttempted,
|
|
574
|
-
stderr: combinedStderr + `\n[invalid-output] ${fallbackIssues.join(
|
|
602
|
+
stderr: combinedStderr + `\n[invalid-output] ${fallbackIssues.join("; ")}`,
|
|
575
603
|
stdout: combinedStdout,
|
|
576
604
|
stdoutTruncated: combinedStdoutTruncated,
|
|
577
605
|
stderrTruncated: combinedStderrTruncated,
|
|
@@ -594,11 +622,12 @@ async function runCliOrchestrationWithPromptFile(options, promptFilePath, meta)
|
|
|
594
622
|
return withSubagentStats(withCliBackend({
|
|
595
623
|
...primaryResult,
|
|
596
624
|
ok: false,
|
|
597
|
-
failureCategory:
|
|
625
|
+
failureCategory: "invalid-output",
|
|
598
626
|
attemptedModels,
|
|
599
627
|
fallbackUsed: false,
|
|
600
628
|
sdkAttempted: meta?.sdkAttempted,
|
|
601
|
-
stderr: primaryResult.stderr +
|
|
629
|
+
stderr: primaryResult.stderr +
|
|
630
|
+
`\n[invalid-output] ${primaryIssues.join("; ")}`,
|
|
602
631
|
}));
|
|
603
632
|
}
|
|
604
633
|
return withSubagentStats(withCliBackend({
|
|
@@ -608,29 +637,71 @@ async function runCliOrchestrationWithPromptFile(options, promptFilePath, meta)
|
|
|
608
637
|
sdkAttempted: meta?.sdkAttempted,
|
|
609
638
|
}));
|
|
610
639
|
}
|
|
611
|
-
function shouldFallbackSdkToCli(sdkResult, validationIssues) {
|
|
640
|
+
function shouldFallbackSdkToCli(sdkResult, validationIssues, options) {
|
|
641
|
+
// Writers must never fall back to unrestricted CLI mutation tools.
|
|
642
|
+
if (options?.writerToolPolicy?.requireSdk)
|
|
643
|
+
return false;
|
|
612
644
|
if (sdkResult.ok && validationIssues.length > 0)
|
|
613
645
|
return true;
|
|
614
|
-
if (!sdkResult.ok &&
|
|
615
|
-
|
|
616
|
-
|
|
646
|
+
if (!sdkResult.ok &&
|
|
647
|
+
(sdkResult.failureCategory === "auth" ||
|
|
648
|
+
sdkResult.failureCategory === "timeout" ||
|
|
649
|
+
sdkResult.failureCategory === "termination-unconfirmed")) {
|
|
617
650
|
return false;
|
|
618
651
|
}
|
|
619
|
-
if (!sdkResult.ok &&
|
|
652
|
+
if (!sdkResult.ok &&
|
|
653
|
+
shouldRetryWithFallback(sdkResult.stderr, sdkResult.stdout))
|
|
620
654
|
return true;
|
|
621
655
|
return false;
|
|
622
656
|
}
|
|
657
|
+
function writerPolicyFailureResult(options, message, extras) {
|
|
658
|
+
return {
|
|
659
|
+
assistantText: "",
|
|
660
|
+
command: [],
|
|
661
|
+
durationMs: 0,
|
|
662
|
+
exitCode: 1,
|
|
663
|
+
failureCategory: "tool-policy",
|
|
664
|
+
modelDisplay: options.modelConfig?.model
|
|
665
|
+
? `${options.modelConfig.provider ?? "default"}/${options.modelConfig.model}`
|
|
666
|
+
: "default",
|
|
667
|
+
ok: false,
|
|
668
|
+
parsedEvents: 0,
|
|
669
|
+
stderr: message,
|
|
670
|
+
stdout: "",
|
|
671
|
+
timedOut: false,
|
|
672
|
+
attemptedModels: [],
|
|
673
|
+
fallbackUsed: false,
|
|
674
|
+
tokensUsed: 0,
|
|
675
|
+
backend: extras?.backend,
|
|
676
|
+
sdkAttempted: extras?.sdkAttempted ?? false,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
623
679
|
export async function executePiStep(options) {
|
|
624
680
|
const backendMode = resolvePiBackend();
|
|
625
|
-
|
|
681
|
+
const writerRequiresSdk = options.writerToolPolicy?.requireSdk === true;
|
|
682
|
+
if (backendMode === "cli-only") {
|
|
683
|
+
if (writerRequiresSdk) {
|
|
684
|
+
return writerPolicyFailureResult(options, "pi writer tool policy requires SDK backend; CODE_AGENT_PI_BACKEND=cli-only is fail-closed for writers", { sdkAttempted: false, backend: "cli" });
|
|
685
|
+
}
|
|
626
686
|
return runCliOrchestration(options, { sdkAttempted: false });
|
|
627
687
|
}
|
|
628
|
-
|
|
688
|
+
if (writerRequiresSdk) {
|
|
689
|
+
const customTools = options.writerToolPolicy?.customTools;
|
|
690
|
+
if (!Array.isArray(customTools) || customTools.length === 0) {
|
|
691
|
+
return writerPolicyFailureResult(options, "pi writer tool policy missing customTools; refusing uncontrolled writer session", { sdkAttempted: false });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const { checkPiSdkAvailability, executeSingleSdkAttempt, getActivePiSdkReuseScope, } = await import("./pi-sdk-executor.js");
|
|
629
695
|
const sdkCheck = await checkPiSdkAvailability(options.repoRoot);
|
|
630
696
|
if (!sdkCheck.ok) {
|
|
697
|
+
if (writerRequiresSdk) {
|
|
698
|
+
return writerPolicyFailureResult(options, `pi writer tool policy requires SDK; ${sdkCheck.detail}`, { sdkAttempted: false });
|
|
699
|
+
}
|
|
631
700
|
return runCliOrchestration(options, { sdkAttempted: false });
|
|
632
701
|
}
|
|
633
|
-
const reuseScope = isPiReuseRuntimeEnabled()
|
|
702
|
+
const reuseScope = isPiReuseRuntimeEnabled()
|
|
703
|
+
? getActivePiSdkReuseScope()
|
|
704
|
+
: undefined;
|
|
634
705
|
const reuseRuntimeActive = Boolean(reuseScope);
|
|
635
706
|
const primary = options.modelConfig ?? {};
|
|
636
707
|
const sdkResult = await executeSingleSdkAttempt({
|
|
@@ -651,10 +722,12 @@ export async function executePiStep(options) {
|
|
|
651
722
|
reuseRuntimeActive,
|
|
652
723
|
});
|
|
653
724
|
}
|
|
654
|
-
if (!shouldFallbackSdkToCli(sdkResult, validationIssues)) {
|
|
655
|
-
const failureCategory = validationIssues.length > 0
|
|
725
|
+
if (!shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
|
|
726
|
+
const failureCategory = validationIssues.length > 0
|
|
727
|
+
? "invalid-output"
|
|
728
|
+
: sdkResult.failureCategory;
|
|
656
729
|
const stderr = validationIssues.length > 0
|
|
657
|
-
? sdkResult.stderr + `\n[invalid-output] ${validationIssues.join(
|
|
730
|
+
? sdkResult.stderr + `\n[invalid-output] ${validationIssues.join("; ")}`
|
|
658
731
|
: sdkResult.stderr;
|
|
659
732
|
return withSubagentStats({
|
|
660
733
|
...sdkResult,
|
|
@@ -676,13 +749,13 @@ export async function executePiStep(options) {
|
|
|
676
749
|
async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
677
750
|
const modelDisplay = modelConfig.provider && modelConfig.model
|
|
678
751
|
? `${modelConfig.provider}/${modelConfig.model}`
|
|
679
|
-
: modelConfig.model ??
|
|
752
|
+
: (modelConfig.model ?? "default");
|
|
680
753
|
const timeoutMs = options.timeoutMs ?? modelConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
681
754
|
const stallTimeoutMs = options.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
682
755
|
const abortGraceMs = options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS;
|
|
683
756
|
let timedOut = false;
|
|
684
757
|
let terminationUnconfirmed = false;
|
|
685
|
-
let supervisionStderr =
|
|
758
|
+
let supervisionStderr = "";
|
|
686
759
|
let timeoutHandle;
|
|
687
760
|
let stallHandle;
|
|
688
761
|
let sigkillHandle;
|
|
@@ -690,22 +763,30 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
690
763
|
// Build pi CLI arguments (same as before).
|
|
691
764
|
// We invoke pi via `node <pi-cli-path>` instead of relying on `#!/usr/bin/env node`
|
|
692
765
|
// to avoid provider resolution issues when spawned from tsx-managed processes.
|
|
693
|
-
const piArgs = [
|
|
766
|
+
const piArgs = [
|
|
767
|
+
"-p",
|
|
768
|
+
"--mode",
|
|
769
|
+
"json",
|
|
770
|
+
"--no-session",
|
|
771
|
+
"--no-context-files",
|
|
772
|
+
"--no-skills",
|
|
773
|
+
"--no-extensions",
|
|
774
|
+
];
|
|
694
775
|
if (modelConfig.provider) {
|
|
695
|
-
piArgs.push(
|
|
776
|
+
piArgs.push("--provider", modelConfig.provider);
|
|
696
777
|
}
|
|
697
778
|
if (modelConfig.model) {
|
|
698
|
-
piArgs.push(
|
|
779
|
+
piArgs.push("--model", modelConfig.model);
|
|
699
780
|
}
|
|
700
781
|
if (modelConfig.thinking) {
|
|
701
|
-
piArgs.push(
|
|
782
|
+
piArgs.push("--thinking", modelConfig.thinking);
|
|
702
783
|
}
|
|
703
|
-
piArgs.push(
|
|
704
|
-
piArgs.push(
|
|
784
|
+
piArgs.push("--tools", options.toolNames.join(","));
|
|
785
|
+
piArgs.push("--append-system-prompt", promptFilePath);
|
|
705
786
|
piArgs.push(...options.attachedFiles.map((file) => `@${file}`));
|
|
706
787
|
piArgs.push(options.userMessage);
|
|
707
|
-
const recordedPiArgs = piArgs.map((arg, index) => index > 0 && piArgs[index - 1] ===
|
|
708
|
-
?
|
|
788
|
+
const recordedPiArgs = piArgs.map((arg, index) => index > 0 && piArgs[index - 1] === "--append-system-prompt"
|
|
789
|
+
? "<system-prompt-file>"
|
|
709
790
|
: arg);
|
|
710
791
|
const startedAt = Date.now();
|
|
711
792
|
return await new Promise((resolve, reject) => {
|
|
@@ -718,22 +799,22 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
718
799
|
// When tsx runs inside a non-login shell, some vars set in ~/.zshrc
|
|
719
800
|
// may not be in process.env; this ensures pi can resolve providers.
|
|
720
801
|
HOME: process.env.HOME,
|
|
721
|
-
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ??
|
|
722
|
-
AIMAX_API_KEY: process.env.AIMAX_API_KEY ??
|
|
723
|
-
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY ??
|
|
724
|
-
NODE_PATH: process.env.NODE_PATH ??
|
|
802
|
+
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? "",
|
|
803
|
+
AIMAX_API_KEY: process.env.AIMAX_API_KEY ?? "",
|
|
804
|
+
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY ?? "",
|
|
805
|
+
NODE_PATH: process.env.NODE_PATH ?? "",
|
|
725
806
|
// Prevent upstream Node loader hooks (tsx/preload/import) from leaking into pi.
|
|
726
|
-
NODE_OPTIONS:
|
|
727
|
-
PATH: process.env.PATH ??
|
|
807
|
+
NODE_OPTIONS: "",
|
|
808
|
+
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
728
809
|
},
|
|
729
|
-
stdio: [
|
|
810
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
730
811
|
});
|
|
731
|
-
const stdoutPreview = new BoundedTextPreview(
|
|
732
|
-
const stderrPreview = new BoundedTextPreview(
|
|
812
|
+
const stdoutPreview = new BoundedTextPreview("stdout");
|
|
813
|
+
const stderrPreview = new BoundedTextPreview("stderr", 16_000, 16_000);
|
|
733
814
|
const stdoutCollector = new PiJsonlStreamCollector();
|
|
734
815
|
let settled = false;
|
|
735
|
-
child.stdout.setEncoding(
|
|
736
|
-
child.stderr.setEncoding(
|
|
816
|
+
child.stdout.setEncoding("utf-8");
|
|
817
|
+
child.stderr.setEncoding("utf-8");
|
|
737
818
|
const clearSupervisionTimers = () => {
|
|
738
819
|
if (timeoutHandle)
|
|
739
820
|
clearTimeout(timeoutHandle);
|
|
@@ -752,10 +833,12 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
752
833
|
const durationMs = Date.now() - startedAt;
|
|
753
834
|
const stdout = stdoutPreview.text();
|
|
754
835
|
const capturedStderr = stderrPreview.text();
|
|
755
|
-
const stderr = [supervisionStderr, capturedStderr]
|
|
836
|
+
const stderr = [supervisionStderr, capturedStderr]
|
|
837
|
+
.filter(Boolean)
|
|
838
|
+
.join("\n");
|
|
756
839
|
const collected = stdoutCollector.finish();
|
|
757
840
|
const failureCategory = terminationUnconfirmed
|
|
758
|
-
?
|
|
841
|
+
? "termination-unconfirmed"
|
|
759
842
|
: classifyPiFailure({
|
|
760
843
|
assistantText: collected.assistantText,
|
|
761
844
|
exitCode: code,
|
|
@@ -766,12 +849,15 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
766
849
|
});
|
|
767
850
|
resolve({
|
|
768
851
|
assistantText: collected.assistantText,
|
|
769
|
-
command: [
|
|
852
|
+
command: ["pi", ...recordedPiArgs],
|
|
770
853
|
durationMs,
|
|
771
854
|
exitCode: code,
|
|
772
855
|
failureCategory,
|
|
773
856
|
modelDisplay,
|
|
774
|
-
ok: !timedOut &&
|
|
857
|
+
ok: !timedOut &&
|
|
858
|
+
code === 0 &&
|
|
859
|
+
collected.assistantText.length > 0 &&
|
|
860
|
+
!collected.outputTooLarge,
|
|
775
861
|
parsedEvents: collected.parsedEvents,
|
|
776
862
|
stderr,
|
|
777
863
|
stdout,
|
|
@@ -789,7 +875,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
789
875
|
return;
|
|
790
876
|
timedOut = true;
|
|
791
877
|
supervisionStderr = reason;
|
|
792
|
-
terminateProcessTree(child,
|
|
878
|
+
terminateProcessTree(child, "SIGTERM");
|
|
793
879
|
sigkillHandle = setTimeout(() => {
|
|
794
880
|
if (settled)
|
|
795
881
|
return;
|
|
@@ -797,7 +883,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
797
883
|
finish(child.exitCode);
|
|
798
884
|
return;
|
|
799
885
|
}
|
|
800
|
-
terminateProcessTree(child,
|
|
886
|
+
terminateProcessTree(child, "SIGKILL");
|
|
801
887
|
exitConfirmationHandle = setTimeout(() => {
|
|
802
888
|
if (settled)
|
|
803
889
|
return;
|
|
@@ -823,29 +909,29 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
823
909
|
const reportOutputActivity = () => {
|
|
824
910
|
armStallWatchdog();
|
|
825
911
|
try {
|
|
826
|
-
options.onActivity?.({ kind:
|
|
912
|
+
options.onActivity?.({ kind: "output", at: new Date().toISOString() });
|
|
827
913
|
}
|
|
828
914
|
catch {
|
|
829
915
|
// best-effort: never fail the pi step
|
|
830
916
|
}
|
|
831
917
|
};
|
|
832
|
-
child.stdout.on(
|
|
918
|
+
child.stdout.on("data", (chunk) => {
|
|
833
919
|
stdoutPreview.append(chunk);
|
|
834
920
|
stdoutCollector.append(chunk);
|
|
835
921
|
if (chunk.length > 0)
|
|
836
922
|
reportOutputActivity();
|
|
837
923
|
});
|
|
838
|
-
child.stderr.on(
|
|
924
|
+
child.stderr.on("data", (chunk) => {
|
|
839
925
|
stderrPreview.append(chunk);
|
|
840
926
|
if (chunk.length > 0)
|
|
841
927
|
reportOutputActivity();
|
|
842
928
|
});
|
|
843
|
-
child.on(
|
|
929
|
+
child.on("error", (err) => {
|
|
844
930
|
clearSupervisionTimers();
|
|
845
931
|
settled = true;
|
|
846
932
|
reject(err);
|
|
847
933
|
});
|
|
848
|
-
child.on(
|
|
934
|
+
child.on("close", (code) => {
|
|
849
935
|
finish(code);
|
|
850
936
|
});
|
|
851
937
|
armStallWatchdog();
|
|
@@ -858,35 +944,35 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
858
944
|
}
|
|
859
945
|
export function shouldRetryWithFallback(stderr, stdout) {
|
|
860
946
|
const category = classifyPiFailure({
|
|
861
|
-
assistantText:
|
|
947
|
+
assistantText: "",
|
|
862
948
|
exitCode: null,
|
|
863
949
|
stderr,
|
|
864
950
|
stdout,
|
|
865
951
|
timedOut: false,
|
|
866
952
|
});
|
|
867
|
-
return [
|
|
953
|
+
return ["quota", "rate-limit", "unavailable", "network"].includes(category);
|
|
868
954
|
}
|
|
869
955
|
export function classifyPiFailure(input) {
|
|
870
956
|
if (input.timedOut)
|
|
871
|
-
return
|
|
957
|
+
return "timeout";
|
|
872
958
|
if (input.outputTooLarge)
|
|
873
|
-
return
|
|
959
|
+
return "output-too-large";
|
|
874
960
|
if (input.exitCode === 0 && input.assistantText.trim())
|
|
875
|
-
return
|
|
961
|
+
return "success";
|
|
876
962
|
const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
|
|
877
963
|
if (/quota|usage limit|reached.*limit|5\s*小时|5小时/.test(combined))
|
|
878
|
-
return
|
|
964
|
+
return "quota";
|
|
879
965
|
if (/rate.?limit/.test(combined))
|
|
880
|
-
return
|
|
966
|
+
return "rate-limit";
|
|
881
967
|
if (/unauthorized|forbidden|invalid api key|authentication|auth failed/.test(combined))
|
|
882
|
-
return
|
|
968
|
+
return "auth";
|
|
883
969
|
if (/unknown provider|unknown model|model.*unavailable|provider.*unavailable|\bunavailable\b|overloaded|capacity|temporarily unavailable/.test(combined))
|
|
884
|
-
return
|
|
970
|
+
return "unavailable";
|
|
885
971
|
if (/network|econnreset|etimedout|socket hang up|connection reset|dns|fetch failed/.test(combined))
|
|
886
|
-
return
|
|
972
|
+
return "network";
|
|
887
973
|
if (input.exitCode === 0 && !input.assistantText.trim())
|
|
888
|
-
return
|
|
974
|
+
return "empty-output";
|
|
889
975
|
if (input.exitCode !== 0)
|
|
890
|
-
return
|
|
891
|
-
return
|
|
976
|
+
return "nonzero-exit";
|
|
977
|
+
return "unknown";
|
|
892
978
|
}
|