@ferris1225/pi-subagents 1.0.1 → 2.0.1
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/LICENSE +2 -0
- package/README.md +183 -92
- package/agents/cleaner.md +51 -0
- package/agents/explore.md +6 -4
- package/package.json +9 -7
- package/src/announcements.ts +8 -0
- package/src/config.ts +10 -27
- package/src/dispatch.ts +98 -77
- package/src/format.ts +26 -23
- package/src/index.ts +1 -1
- package/src/models.ts +91 -98
- package/src/monitor.ts +11 -3
- package/src/prompt.ts +13 -8
- package/src/rpc-run.ts +89 -6
- package/src/runtime.ts +0 -2
- package/src/setup.ts +463 -639
- package/src/spawn.ts +152 -107
- package/src/tools.ts +22 -6
- package/src/widget.ts +51 -14
package/src/spawn.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The process transport itself lives in rpc-run.ts. Each attempt starts pi in
|
|
5
5
|
* persistent `--mode rpc`, sends commands over strict LF-delimited JSONL, and
|
|
6
|
-
* settles only on `agent_settled`. This module
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* settles only on `agent_settled`. This module owns startup-race recovery,
|
|
7
|
+
* selected-to-main model handoff, capability-clamped thinking, accounting, and
|
|
8
|
+
* result formatting around those attempts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import { type Dirent, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
|
-
import { basename, join } from "node:path";
|
|
15
|
+
import { basename, join, resolve } from "node:path";
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { AgentConfig } from "./agents.ts";
|
|
18
18
|
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
RpcRunControl,
|
|
26
26
|
runRpcAgentAttempt,
|
|
27
27
|
sessionExists,
|
|
28
|
+
writeChildRetryPolicyExtension,
|
|
28
29
|
SUBAGENT_KILL_GRACE_MS,
|
|
29
30
|
type RpcSingleResult,
|
|
30
31
|
type SubagentLiveEvent,
|
|
@@ -39,6 +40,7 @@ export {
|
|
|
39
40
|
RpcRunControl,
|
|
40
41
|
sessionExists,
|
|
41
42
|
SUBAGENT_KILL_GRACE_MS,
|
|
43
|
+
writeChildRetryPolicyExtension,
|
|
42
44
|
};
|
|
43
45
|
export type { SubagentLiveEvent, UsageStats };
|
|
44
46
|
|
|
@@ -47,7 +49,6 @@ export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
|
47
49
|
export const SUBAGENT_DEFAULT_IDLE_TIMEOUT_MS = 0;
|
|
48
50
|
export const SUBAGENT_STARTUP_RETRY_DELAYS_MS = [250, 750, 1500] as const;
|
|
49
51
|
export const MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS = 2000;
|
|
50
|
-
export const SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS = [2_000, 4_000, 8_000, 16_000, 30_000] as const;
|
|
51
52
|
|
|
52
53
|
export interface SingleResult extends RpcSingleResult {}
|
|
53
54
|
|
|
@@ -97,14 +98,89 @@ export function truncateResultOutput(output: string, maxLines: number): Truncate
|
|
|
97
98
|
return { text: kept.join("\n"), truncated: true };
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
export const RESULT_ARTIFACT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
102
|
+
export const RESULT_ARTIFACT_MAX_FILES_PER_PROJECT = 50;
|
|
103
|
+
// Explicit current prefix plus the strict timestamp/token convention used by 1.1.0.
|
|
104
|
+
const RESULT_ARTIFACT_NAME = /^(?:pi-subagent-\d{13,}-[0-9a-f]{12}|\d{13,}-[a-z0-9]{6})-[\w.-]+\.md$/;
|
|
105
|
+
|
|
106
|
+
interface ResultArtifactRetentionOptions {
|
|
107
|
+
now?: number;
|
|
108
|
+
maxAgeMs?: number;
|
|
109
|
+
maxFilesPerProject?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Remove only stale/overflow Markdown result artifacts. Unknown files and
|
|
113
|
+
* symlinks are never touched. Called on each artifact write, so storage stays
|
|
114
|
+
* bounded without deleting a result that the current completion just linked. */
|
|
115
|
+
export function pruneResultArtifacts(
|
|
116
|
+
rootDir: string = join(tmpdir(), "pi-subagents-results"),
|
|
117
|
+
options: ResultArtifactRetentionOptions = {},
|
|
118
|
+
): void {
|
|
119
|
+
const now = options.now ?? Date.now();
|
|
120
|
+
const maxAgeMs = Math.max(0, options.maxAgeMs ?? RESULT_ARTIFACT_MAX_AGE_MS);
|
|
121
|
+
const maxFiles = Math.max(0, Math.floor(options.maxFilesPerProject ?? RESULT_ARTIFACT_MAX_FILES_PER_PROJECT));
|
|
122
|
+
let projects: Dirent[];
|
|
123
|
+
try {
|
|
124
|
+
projects = readdirSync(rootDir, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const project of projects) {
|
|
130
|
+
if (!project.isDirectory() || project.isSymbolicLink()) continue;
|
|
131
|
+
const projectDir = join(rootDir, project.name);
|
|
132
|
+
let entries: Dirent[];
|
|
133
|
+
try {
|
|
134
|
+
entries = readdirSync(projectDir, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const artifacts = entries
|
|
139
|
+
.filter((entry) => entry.isFile() && !entry.isSymbolicLink() && RESULT_ARTIFACT_NAME.test(entry.name))
|
|
140
|
+
.flatMap((entry) => {
|
|
141
|
+
const path = join(projectDir, entry.name);
|
|
142
|
+
try {
|
|
143
|
+
return [{ path, mtimeMs: statSync(path).mtimeMs }];
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
149
|
+
|
|
150
|
+
for (const [index, artifact] of artifacts.entries()) {
|
|
151
|
+
if (index < maxFiles && now - artifact.mtimeMs <= maxAgeMs) continue;
|
|
152
|
+
try {
|
|
153
|
+
rmSync(artifact.path, { force: true });
|
|
154
|
+
} catch {
|
|
155
|
+
// Temp cleanup is best-effort; result delivery must still succeed.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resultArtifactProjectKey(cwd?: string): string {
|
|
162
|
+
if (!cwd) return "default";
|
|
163
|
+
let canonical: string;
|
|
164
|
+
try {
|
|
165
|
+
canonical = realpathSync.native(cwd);
|
|
166
|
+
} catch {
|
|
167
|
+
canonical = resolve(cwd);
|
|
168
|
+
}
|
|
169
|
+
if (process.platform === "win32") canonical = canonical.toLowerCase();
|
|
170
|
+
const slug = basename(canonical).replace(/[^\w.-]+/g, "_") || "project";
|
|
171
|
+
const digest = createHash("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
172
|
+
return `${slug}-${digest}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
100
175
|
export function writeResultArtifact(output: string, agentName: string, cwd?: string): string {
|
|
101
|
-
const
|
|
102
|
-
const dir = join(
|
|
176
|
+
const rootDir = join(tmpdir(), "pi-subagents-results");
|
|
177
|
+
const dir = join(rootDir, resultArtifactProjectKey(cwd));
|
|
103
178
|
mkdirSync(dir, { recursive: true });
|
|
104
|
-
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
105
|
-
const unique =
|
|
179
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_") || "agent";
|
|
180
|
+
const unique = `pi-subagent-${Date.now()}-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
106
181
|
const filePath = join(dir, `${unique}-${safeName}.md`);
|
|
107
182
|
writeFileSync(filePath, output, "utf8");
|
|
183
|
+
pruneResultArtifacts(rootDir);
|
|
108
184
|
return filePath;
|
|
109
185
|
}
|
|
110
186
|
|
|
@@ -121,13 +197,6 @@ function lastAssistantMessage(messages: Message[]): Extract<Message, { role: "as
|
|
|
121
197
|
return undefined;
|
|
122
198
|
}
|
|
123
199
|
|
|
124
|
-
function assistantText(message: Extract<Message, { role: "assistant" }>): string {
|
|
125
|
-
return message.content
|
|
126
|
-
.filter((part): part is Extract<(typeof message.content)[number], { type: "text" }> => part.type === "text")
|
|
127
|
-
.map((part) => part.text)
|
|
128
|
-
.join("");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
200
|
export function isModelLevelFailure(result: SingleResult): boolean {
|
|
132
201
|
if (!isFailedResult(result)) return false;
|
|
133
202
|
if (result.stopReason === "aborted") return false;
|
|
@@ -141,46 +210,26 @@ export function isModelLevelFailure(result: SingleResult): boolean {
|
|
|
141
210
|
// must not hide a later provider error (for example a second-turn 503).
|
|
142
211
|
const finalAssistant = lastAssistantMessage(result.messages);
|
|
143
212
|
if (finalAssistant) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
result.errorMessage?.trim() ||
|
|
149
|
-
result.stderr.trim() ||
|
|
150
|
-
finalAssistant.content.length === 0
|
|
151
|
-
);
|
|
213
|
+
// Provider streams may preserve partial text on a terminal error. The stop
|
|
214
|
+
// reason, not content emptiness, is the transport boundary; ordinary tool or
|
|
215
|
+
// task failures settle with a non-error assistant stop reason.
|
|
216
|
+
return finalAssistant.stopReason === "error";
|
|
152
217
|
}
|
|
153
218
|
|
|
154
219
|
if ((result.failedTools?.length ?? 0) > 0) return false;
|
|
155
|
-
return Boolean(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/model[_ -]?not[_ -]?found|no\s+models?\s+(?:found|matched)|(?:model|provider)[^.\n]{0,80}(?:not\s+found|unknown|does\s+not\s+exist|unsupported|invalid)|(?:not\s+found|unknown|unsupported|invalid)[^.\n]{0,40}(?:model|provider)|\b404\b/i;
|
|
162
|
-
|
|
163
|
-
export function isTerminalModelError(result: SingleResult): boolean {
|
|
164
|
-
const message = result.errorMessage?.trim();
|
|
165
|
-
if (message) return TERMINAL_MODEL_ERROR_PATTERN.test(message);
|
|
166
|
-
const stderr = result.stderr.trim();
|
|
167
|
-
return stderr.length > 0 && TERMINAL_MODEL_ERROR_PATTERN.test(stderr);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** A permanent failure of this model/provider reference (stale id, unknown
|
|
171
|
-
* provider, 404 config route). Skip same-candidate backoff, but keep advancing
|
|
172
|
-
* through backup and current-main candidates. */
|
|
173
|
-
export function isPermanentModelCandidateError(result: SingleResult): boolean {
|
|
174
|
-
const message = result.errorMessage?.trim();
|
|
175
|
-
if (message) return PERMANENT_MODEL_CANDIDATE_ERROR_PATTERN.test(message);
|
|
176
|
-
const stderr = result.stderr.trim();
|
|
177
|
-
return stderr.length > 0 && PERMANENT_MODEL_CANDIDATE_ERROR_PATTERN.test(stderr);
|
|
220
|
+
return Boolean(
|
|
221
|
+
result.rpcPromptAccepted ||
|
|
222
|
+
result.rpcActivity ||
|
|
223
|
+
result.errorMessage?.trim() ||
|
|
224
|
+
result.stderr.trim(),
|
|
225
|
+
);
|
|
178
226
|
}
|
|
179
227
|
|
|
180
228
|
export function isRetryableStartupFailure(result: SingleResult, durationMs: number): boolean {
|
|
181
229
|
if (result.exitCode === 0) return false;
|
|
182
230
|
if (result.stopReason === "aborted") return false;
|
|
183
231
|
if (result.dispatchFailed) return false;
|
|
232
|
+
if (result.rpcPromptAccepted || result.rpcActivity) return false;
|
|
184
233
|
if (result.errorMessage?.includes("idle timeout")) return false;
|
|
185
234
|
if (getFinalOutput(result.messages)) return false;
|
|
186
235
|
if (result.messages.length > 0) return false;
|
|
@@ -252,8 +301,8 @@ export function buildResumePrompt(task: string, reason: string): string {
|
|
|
252
301
|
|
|
253
302
|
export function buildFallbackResumeReason(fromModel?: string): string {
|
|
254
303
|
return fromModel
|
|
255
|
-
? `the
|
|
256
|
-
: "the
|
|
304
|
+
? `the selected model (${fromModel}) failed at the model/provider level, so the current main model is continuing`
|
|
305
|
+
: "the selected model failed at the model/provider level, so the current main model is continuing";
|
|
257
306
|
}
|
|
258
307
|
|
|
259
308
|
export interface RunSingleOptions {
|
|
@@ -263,9 +312,10 @@ export interface RunSingleOptions {
|
|
|
263
312
|
task: string;
|
|
264
313
|
cwd?: string;
|
|
265
314
|
thinkingLevel?: ThinkingLevel;
|
|
315
|
+
/** Resolve the effective level for each runtime model candidate. */
|
|
316
|
+
thinkingLevelForModel?: (modelRef?: string) => ThinkingLevel;
|
|
266
317
|
idleTimeoutMs?: number;
|
|
267
318
|
startupRetryDelaysMs?: readonly number[];
|
|
268
|
-
runLevelRetryDelaysMs?: readonly number[];
|
|
269
319
|
sessionDir?: string;
|
|
270
320
|
sessionId?: string;
|
|
271
321
|
/** Initial RPC prompt. Kept under the old name to limit caller churn. */
|
|
@@ -347,19 +397,18 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
347
397
|
}
|
|
348
398
|
|
|
349
399
|
/**
|
|
350
|
-
* Run one logical generation
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
400
|
+
* Run one logical generation on the selected model, then hand directly to the
|
|
401
|
+
* current main model after any model/provider-level failure. Startup-race retries
|
|
402
|
+
* remain process-level recovery; provider/model retries and extra candidates do not.
|
|
403
|
+
* Both attempts resume the same retained Pi session.
|
|
354
404
|
*/
|
|
355
|
-
export async function
|
|
405
|
+
export async function runSingleAgentWithMainFallback(
|
|
356
406
|
options: RunSingleOptions,
|
|
357
|
-
|
|
407
|
+
mainFallbackRef?: string,
|
|
358
408
|
): Promise<SingleResult> {
|
|
359
409
|
const agent = options.agent;
|
|
360
410
|
const launchedRef = agent?.model;
|
|
361
411
|
const startupDelays = options.startupRetryDelaysMs ?? SUBAGENT_STARTUP_RETRY_DELAYS_MS;
|
|
362
|
-
const runDelays = options.runLevelRetryDelaysMs ?? SUBAGENT_RUN_LEVEL_RETRY_DELAYS_MS;
|
|
363
412
|
|
|
364
413
|
const sessionId = options.sessionId ?? randomUUID();
|
|
365
414
|
const sessionDir = options.sessionDir ?? (await mkdtemp(join(tmpdir(), "pi-subagent-session-")));
|
|
@@ -438,26 +487,49 @@ export async function runSingleAgentWithModelFallback(
|
|
|
438
487
|
}
|
|
439
488
|
};
|
|
440
489
|
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
if (launchedRef?.trim()) seenRefs.add(launchedRef.trim());
|
|
444
|
-
for (const candidate of fallbackModelRefs) {
|
|
445
|
-
const ref = candidate.trim();
|
|
446
|
-
if (!ref || seenRefs.has(ref)) continue;
|
|
447
|
-
seenRefs.add(ref);
|
|
448
|
-
fallbackRefs.push(ref);
|
|
449
|
-
}
|
|
450
|
-
|
|
490
|
+
const selectedRef = launchedRef?.trim() || undefined;
|
|
491
|
+
const normalizedMainRef = mainFallbackRef?.trim() || undefined;
|
|
451
492
|
const candidates: Array<{ agent: AgentConfig; ref?: string }> = [
|
|
452
|
-
{ agent, ref:
|
|
493
|
+
{ agent, ref: selectedRef },
|
|
453
494
|
];
|
|
454
|
-
|
|
495
|
+
if (normalizedMainRef && normalizedMainRef !== selectedRef) {
|
|
496
|
+
candidates.push({ agent: { ...agent, model: normalizedMainRef }, ref: normalizedMainRef });
|
|
497
|
+
}
|
|
455
498
|
|
|
456
|
-
let modelRetries = 0;
|
|
457
499
|
let fallbackUsed = false;
|
|
458
500
|
let result: SingleResult | undefined;
|
|
501
|
+
const priorFailedTools: NonNullable<SingleResult["failedTools"]> = [];
|
|
502
|
+
const priorUsage = emptyUsage();
|
|
503
|
+
|
|
504
|
+
const retainAttemptDiagnostics = (attempt: SingleResult): void => {
|
|
505
|
+
priorFailedTools.push(...(attempt.failedTools ?? []));
|
|
506
|
+
priorUsage.input += attempt.usage.input;
|
|
507
|
+
priorUsage.output += attempt.usage.output;
|
|
508
|
+
priorUsage.cacheRead += attempt.usage.cacheRead;
|
|
509
|
+
priorUsage.cacheWrite += attempt.usage.cacheWrite;
|
|
510
|
+
priorUsage.cost += attempt.usage.cost;
|
|
511
|
+
priorUsage.turns += attempt.usage.turns;
|
|
512
|
+
priorUsage.contextTokens = attempt.usage.contextTokens || priorUsage.contextTokens;
|
|
513
|
+
};
|
|
459
514
|
|
|
460
515
|
const finish = async (settled: SingleResult): Promise<SingleResult> => {
|
|
516
|
+
if (priorFailedTools.length > 0) {
|
|
517
|
+
settled.failedTools = [...priorFailedTools, ...(settled.failedTools ?? [])];
|
|
518
|
+
}
|
|
519
|
+
if (
|
|
520
|
+
priorUsage.turns || priorUsage.input || priorUsage.output || priorUsage.cacheRead ||
|
|
521
|
+
priorUsage.cacheWrite || priorUsage.cost || priorUsage.contextTokens
|
|
522
|
+
) {
|
|
523
|
+
settled.usage = {
|
|
524
|
+
input: priorUsage.input + settled.usage.input,
|
|
525
|
+
output: priorUsage.output + settled.usage.output,
|
|
526
|
+
cacheRead: priorUsage.cacheRead + settled.usage.cacheRead,
|
|
527
|
+
cacheWrite: priorUsage.cacheWrite + settled.usage.cacheWrite,
|
|
528
|
+
cost: priorUsage.cost + settled.usage.cost,
|
|
529
|
+
turns: priorUsage.turns + settled.usage.turns,
|
|
530
|
+
contextTokens: settled.usage.contextTokens || priorUsage.contextTokens,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
461
533
|
const persistedSession = sessionExists(sessionDir, sessionId);
|
|
462
534
|
if (!settled.dispatchFailed || persistedSession || options.sessionDir) {
|
|
463
535
|
settled.sessionId ??= sessionId;
|
|
@@ -469,7 +541,6 @@ export async function runSingleAgentWithModelFallback(
|
|
|
469
541
|
}
|
|
470
542
|
settled.task = options.control?.getObjective() ?? settled.task;
|
|
471
543
|
if (fallbackUsed && launchedRef) settled.modelFallbackFrom = launchedRef;
|
|
472
|
-
settled.modelRetries = modelRetries;
|
|
473
544
|
options.control?.markSettled();
|
|
474
545
|
return settled;
|
|
475
546
|
};
|
|
@@ -478,9 +549,11 @@ export async function runSingleAgentWithModelFallback(
|
|
|
478
549
|
const candidate = candidates[candidateIndex];
|
|
479
550
|
fallbackUsed ||= candidateIndex > 0;
|
|
480
551
|
const previousModel = result?.model ?? candidates[candidateIndex - 1]?.ref;
|
|
552
|
+
const candidateThinking = options.thinkingLevelForModel?.(candidate.ref) ?? options.thinkingLevel;
|
|
481
553
|
const candidateOptions: RunSingleOptions = {
|
|
482
554
|
...baseOptions,
|
|
483
555
|
agent: candidate.agent,
|
|
556
|
+
thinkingLevel: candidateThinking,
|
|
484
557
|
...(candidateIndex > 0
|
|
485
558
|
? {
|
|
486
559
|
stdinText: buildResumePrompt(
|
|
@@ -494,6 +567,7 @@ export async function runSingleAgentWithModelFallback(
|
|
|
494
567
|
options.onLive?.({
|
|
495
568
|
kind: "model",
|
|
496
569
|
model: candidate.ref,
|
|
570
|
+
thinking: candidateThinking,
|
|
497
571
|
...(candidateIndex > 0 && launchedRef ? { fallbackFrom: launchedRef } : {}),
|
|
498
572
|
});
|
|
499
573
|
} catch {
|
|
@@ -501,41 +575,12 @@ export async function runSingleAgentWithModelFallback(
|
|
|
501
575
|
}
|
|
502
576
|
|
|
503
577
|
result = await runWithStartupRetry(candidateOptions);
|
|
504
|
-
if (result.parked || result.stopReason === "aborted") return result;
|
|
505
|
-
if (!isModelLevelFailure(result)) return finish(result);
|
|
506
|
-
|
|
507
|
-
if (!isTerminalModelError(result) && !isPermanentModelCandidateError(result)) {
|
|
508
|
-
const retryOptions: RunSingleOptions = {
|
|
509
|
-
...candidateOptions,
|
|
510
|
-
stdinText: buildResumePrompt(
|
|
511
|
-
options.control?.getObjective() ?? options.task,
|
|
512
|
-
"a transient provider error on the same model",
|
|
513
|
-
),
|
|
514
|
-
};
|
|
515
|
-
for (const delay of runDelays) {
|
|
516
|
-
baseOptions.control?.markRetrying();
|
|
517
|
-
try {
|
|
518
|
-
options.onLive?.({ kind: "status", status: "running" });
|
|
519
|
-
} catch {
|
|
520
|
-
/* never throw from event handling */
|
|
521
|
-
}
|
|
522
|
-
if (!(await waitForControlledRetry(delay, options.signal, options.control))) {
|
|
523
|
-
return controlledDisposition(baseOptions, result) ?? result;
|
|
524
|
-
}
|
|
525
|
-
result = await runWithStartupRetry(retryOptions);
|
|
526
|
-
modelRetries++;
|
|
527
|
-
if (result.parked || result.stopReason === "aborted") return result;
|
|
528
|
-
if (
|
|
529
|
-
!isModelLevelFailure(result) ||
|
|
530
|
-
isTerminalModelError(result) ||
|
|
531
|
-
isPermanentModelCandidateError(result)
|
|
532
|
-
) break;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
578
|
+
if (result.parked || result.stopReason === "aborted") return finish(result);
|
|
536
579
|
if (!isModelLevelFailure(result)) return finish(result);
|
|
537
|
-
//
|
|
538
|
-
//
|
|
580
|
+
// Any model-level failure advances immediately to the sole fallback (the
|
|
581
|
+
// current main model). Retain selected-attempt tool diagnostics and usage;
|
|
582
|
+
// ordinary task/tool failures returned above without a handoff.
|
|
583
|
+
if (candidateIndex < candidates.length - 1) retainAttemptDiagnostics(result);
|
|
539
584
|
}
|
|
540
585
|
|
|
541
586
|
return finish(result!);
|
package/src/tools.ts
CHANGED
|
@@ -235,7 +235,10 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
235
235
|
if (settledIds.length > 0) {
|
|
236
236
|
return {
|
|
237
237
|
content: [
|
|
238
|
-
{ type: "text", text: settledIds.map((id) =>
|
|
238
|
+
{ type: "text", text: settledIds.map((id) => {
|
|
239
|
+
const result = runtime.settledRuns.get(id)!;
|
|
240
|
+
return formatCompletionBlock(result, config.maxResultLines, result.projectCwd ?? ctx.cwd);
|
|
241
|
+
}).join("\n\n") },
|
|
239
242
|
],
|
|
240
243
|
details: {},
|
|
241
244
|
};
|
|
@@ -330,7 +333,9 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
330
333
|
|
|
331
334
|
const outcomes = await Promise.all(targets.map((run) => waitForRun(run.id)));
|
|
332
335
|
const blocks = outcomes.map((outcome) =>
|
|
333
|
-
outcome.result
|
|
336
|
+
outcome.result
|
|
337
|
+
? formatCompletionBlock(outcome.result, config.maxResultLines, outcome.result.projectCwd ?? ctx.cwd)
|
|
338
|
+
: (outcome.note ?? "(no outcome)"),
|
|
334
339
|
);
|
|
335
340
|
return { content: [{ type: "text", text: blocks.join("\n\n") }], details: {} };
|
|
336
341
|
},
|
|
@@ -383,7 +388,17 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
383
388
|
if (settledIds.length > 0) {
|
|
384
389
|
return {
|
|
385
390
|
content: [
|
|
386
|
-
{
|
|
391
|
+
{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: settledIds
|
|
394
|
+
.map((id) => formatCompletionBlock(
|
|
395
|
+
runtime.settledRuns.get(id)!,
|
|
396
|
+
config.maxResultLines,
|
|
397
|
+
runtime.settledRuns.get(id)!.projectCwd ?? ctx.cwd,
|
|
398
|
+
{ failedToolDetails: true },
|
|
399
|
+
))
|
|
400
|
+
.join("\n\n"),
|
|
401
|
+
},
|
|
387
402
|
],
|
|
388
403
|
details: {},
|
|
389
404
|
};
|
|
@@ -421,7 +436,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
421
436
|
const activeLines = activeRuns.map((run) => {
|
|
422
437
|
const thread = runtime.threads.get(run.id);
|
|
423
438
|
const model = run.modelFallbackFrom
|
|
424
|
-
? `${run.model ?? "?"} (
|
|
439
|
+
? `${run.model ?? "?"} (main after ${run.modelFallbackFrom} failed)`
|
|
425
440
|
: (run.model ?? "?");
|
|
426
441
|
const parts = [
|
|
427
442
|
`#${run.id} ${run.agent}`,
|
|
@@ -450,7 +465,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
450
465
|
const usage = formatUsage(result.usage);
|
|
451
466
|
const label = runLabel(result.task);
|
|
452
467
|
const model = result.modelFallbackFrom
|
|
453
|
-
? `${result.model ?? "?"} (
|
|
468
|
+
? `${result.model ?? "?"} (main after ${result.modelFallbackFrom} failed)`
|
|
454
469
|
: (result.model ?? "?");
|
|
455
470
|
const isolation = result.isolation === "worktree" ? ` · worktree ${result.integrationStatus ?? "unknown"}` : "";
|
|
456
471
|
const relations = [
|
|
@@ -654,6 +669,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
654
669
|
usage: emptyUsage(),
|
|
655
670
|
model: run?.model,
|
|
656
671
|
thinking: run?.thinking,
|
|
672
|
+
projectCwd: thread.cwd,
|
|
657
673
|
stopReason: "aborted",
|
|
658
674
|
errorMessage: stopMessage,
|
|
659
675
|
runId,
|
|
@@ -677,7 +693,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
677
693
|
const maxResultLines = (await configPromise)?.maxResultLines ?? DEFAULT_MAX_RESULT_LINES;
|
|
678
694
|
runtime.sendCompletionGroup(completionResults.map((result) => ({
|
|
679
695
|
agent: result.agent,
|
|
680
|
-
block: formatCompletionBlock(result, maxResultLines, ctx.cwd),
|
|
696
|
+
block: formatCompletionBlock(result, maxResultLines, result.projectCwd ?? ctx.cwd),
|
|
681
697
|
triggerTurn: true,
|
|
682
698
|
})));
|
|
683
699
|
runtime.completionBatcher.flush();
|
package/src/widget.ts
CHANGED
|
@@ -4,10 +4,10 @@ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import {
|
|
6
6
|
formatElapsed,
|
|
7
|
+
formatTaskSummary,
|
|
7
8
|
isRunActiveStatus,
|
|
8
9
|
monitor,
|
|
9
10
|
statusIcon,
|
|
10
|
-
statusLabel,
|
|
11
11
|
type RunView,
|
|
12
12
|
} from "./monitor.ts";
|
|
13
13
|
|
|
@@ -25,8 +25,10 @@ function compactLine(left: string, right: string, width: number): string {
|
|
|
25
25
|
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** One compact line per genuinely active run
|
|
29
|
-
*
|
|
28
|
+
/** One compact primary line per genuinely active run, plus an optional indented
|
|
29
|
+
* activity line. The primary line reserves effective model/thinking and elapsed
|
|
30
|
+
* width before truncating the task. Settled and parked threads never appear, so
|
|
31
|
+
* elapsed time cannot keep ticking beside a terminal status. */
|
|
30
32
|
export function formatActiveRunLines(
|
|
31
33
|
runs: readonly RunView[],
|
|
32
34
|
theme: Theme,
|
|
@@ -36,20 +38,55 @@ export function formatActiveRunLines(
|
|
|
36
38
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
37
39
|
return runs
|
|
38
40
|
.filter((run) => isRunActiveStatus(run.status))
|
|
39
|
-
.
|
|
41
|
+
.flatMap((run) => {
|
|
40
42
|
const icon = statusIcon(run.status, theme);
|
|
41
43
|
const name = theme.fg("accent", theme.bold(run.agent));
|
|
42
|
-
const
|
|
43
|
-
const activity = run.status === "running"
|
|
44
|
-
? (run.activity ?? statusLabel(run.status))
|
|
45
|
-
: statusLabel(run.status);
|
|
46
|
-
const parts = [
|
|
47
|
-
`${icon} ${dim(`#${run.id}`)} ${name}`,
|
|
48
|
-
context ? dim(`· ${context}`) : undefined,
|
|
49
|
-
activity ? dim(`· ${activity}`) : undefined,
|
|
50
|
-
].filter((part): part is string => Boolean(part));
|
|
44
|
+
const identity = `${icon} ${dim(`#${run.id}`)} ${name}`;
|
|
51
45
|
const elapsed = formatElapsed(run, now);
|
|
52
|
-
|
|
46
|
+
// Render only the resolved model id plus thinking level. Provider auth and
|
|
47
|
+
// other configuration never enter monitor state or this line.
|
|
48
|
+
const modelId = run.model?.split("/").at(-1);
|
|
49
|
+
const modelSource = formatTaskSummary(
|
|
50
|
+
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
51
|
+
64,
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
|
+
const taskSource = formatTaskSummary(run.task, 64);
|
|
55
|
+
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
56
|
+
const contentWidth = Math.max(
|
|
57
|
+
0,
|
|
58
|
+
width -
|
|
59
|
+
visibleWidth(identity) -
|
|
60
|
+
visibleWidth(elapsed) -
|
|
61
|
+
(primaryPartCount - 1) * visibleWidth(" · "),
|
|
62
|
+
);
|
|
63
|
+
const modelDesired = visibleWidth(modelSource);
|
|
64
|
+
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
65
|
+
let modelWidth = modelSource
|
|
66
|
+
? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
|
|
67
|
+
: 0;
|
|
68
|
+
let taskWidth = contentWidth - modelWidth;
|
|
69
|
+
if (visibleWidth(taskSource) < taskWidth) {
|
|
70
|
+
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
|
|
71
|
+
taskWidth = contentWidth - modelWidth;
|
|
72
|
+
}
|
|
73
|
+
const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth) : "";
|
|
74
|
+
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
75
|
+
const primaryLeft = [
|
|
76
|
+
identity,
|
|
77
|
+
task ? dim(task) : undefined,
|
|
78
|
+
modelThinking ? dim(modelThinking) : undefined,
|
|
79
|
+
].filter((part): part is string => Boolean(part)).join(" · ");
|
|
80
|
+
const primary = compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
|
|
81
|
+
|
|
82
|
+
const activity = run.activity?.trim();
|
|
83
|
+
if (!activity) return [primary];
|
|
84
|
+
const activityIndent = " ";
|
|
85
|
+
const activityWidth = width - visibleWidth(activityIndent);
|
|
86
|
+
if (activityWidth <= 0) return [primary];
|
|
87
|
+
const activitySummary = formatTaskSummary(activity, activityWidth);
|
|
88
|
+
if (!activitySummary) return [primary];
|
|
89
|
+
return [primary, truncateToWidth(`${activityIndent}${dim(activitySummary)}`, width, "")];
|
|
53
90
|
});
|
|
54
91
|
}
|
|
55
92
|
|