pi-subagents 0.28.0 → 0.29.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 +14 -0
- package/README.md +18 -61
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +4 -35
- package/src/agents/agent-management.ts +10 -20
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +0 -10
- package/src/agents/agents.ts +304 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +0 -2
- package/src/extension/index.ts +3 -8
- package/src/extension/schemas.ts +32 -22
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +20 -11
- package/src/runs/background/run-status.ts +1 -7
- package/src/runs/background/subagent-runner.ts +81 -211
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +38 -343
- package/src/runs/foreground/subagent-executor.ts +28 -99
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +6 -10
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +10 -48
- package/src/shared/utils.ts +2 -8
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
|
@@ -20,6 +20,44 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Sentinel model value requesting that a subagent inherit the parent session's model. */
|
|
24
|
+
export const INHERIT_MODEL = "inherit";
|
|
25
|
+
|
|
26
|
+
/** Minimal shape of the parent session's in-memory model (`ctx.model`). */
|
|
27
|
+
export interface ParentModel {
|
|
28
|
+
provider: string;
|
|
29
|
+
id: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the `--model` override passed to a spawned subagent.
|
|
34
|
+
*
|
|
35
|
+
* When no model is requested (`undefined`, `false`, empty, or the `"inherit"`
|
|
36
|
+
* sentinel), the child must inherit the parent session's *in-memory* model
|
|
37
|
+
* (`provider/id`) instead of being left to resolve its own model. Without an
|
|
38
|
+
* explicit `provider/id`, the child falls back to the global
|
|
39
|
+
* `~/.pi/agent/settings.json` default, which is shared across every open PI
|
|
40
|
+
* session — so a different session that last changed its model in the TUI would
|
|
41
|
+
* silently contaminate this session's subagents (see issue #266). Passing an
|
|
42
|
+
* explicit `provider/id` keeps each session's children isolated to that
|
|
43
|
+
* session's model.
|
|
44
|
+
*
|
|
45
|
+
* An explicitly requested model string is resolved via {@link resolveModelCandidate}.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSubagentModelOverride(
|
|
48
|
+
requestedModel: string | boolean | undefined,
|
|
49
|
+
parentModel: ParentModel | undefined,
|
|
50
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
51
|
+
preferredProvider?: string,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
54
|
+
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
55
|
+
if (explicit === undefined) {
|
|
56
|
+
return parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
57
|
+
}
|
|
58
|
+
return resolveModelCandidate(explicit, availableModels, preferredProvider);
|
|
59
|
+
}
|
|
60
|
+
|
|
23
61
|
export function resolveModelCandidate(
|
|
24
62
|
model: string | undefined,
|
|
25
63
|
availableModels: AvailableModelInfo[] | undefined,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { DynamicCollectSpec, DynamicExpandSpec } from "../../shared/settings.ts";
|
|
2
|
-
import type { JsonSchemaObject, ResolvedAcceptanceConfig } from "../../shared/types.ts";
|
|
3
|
-
|
|
4
1
|
export interface RunnerSubagentStep {
|
|
5
2
|
agent: string;
|
|
6
3
|
task: string;
|
|
@@ -25,15 +22,13 @@ export interface RunnerSubagentStep {
|
|
|
25
22
|
outputMode?: "inline" | "file-only";
|
|
26
23
|
sessionFile?: string;
|
|
27
24
|
maxSubagentDepth?: number;
|
|
28
|
-
maxExecutionTimeMs?: number;
|
|
29
|
-
maxTokens?: number;
|
|
30
25
|
structuredOutput?: {
|
|
31
|
-
schema: JsonSchemaObject;
|
|
26
|
+
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
32
27
|
schemaPath: string;
|
|
33
28
|
outputPath: string;
|
|
34
29
|
};
|
|
35
|
-
structuredOutputSchema?: JsonSchemaObject;
|
|
36
|
-
effectiveAcceptance?: ResolvedAcceptanceConfig;
|
|
30
|
+
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
31
|
+
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
export interface ParallelStepGroup {
|
|
@@ -44,13 +39,14 @@ export interface ParallelStepGroup {
|
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
export interface DynamicRunnerGroup {
|
|
47
|
-
expand: DynamicExpandSpec;
|
|
42
|
+
expand: import("../../shared/settings.ts").DynamicExpandSpec;
|
|
48
43
|
parallel: RunnerSubagentStep;
|
|
49
|
-
collect: DynamicCollectSpec;
|
|
44
|
+
collect: import("../../shared/settings.ts").DynamicCollectSpec;
|
|
50
45
|
concurrency?: number;
|
|
51
46
|
failFast?: boolean;
|
|
52
47
|
phase?: string;
|
|
53
48
|
label?: string;
|
|
49
|
+
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
@@ -135,14 +135,15 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] {
|
|
138
|
+
const preserveCurrentFanoutToolHistory = process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1";
|
|
138
139
|
let changed = false;
|
|
139
140
|
const filtered: unknown[] = [];
|
|
140
141
|
for (const message of messages) {
|
|
141
|
-
if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) {
|
|
142
|
+
if (isParentOnlySubagentMessage(message) || (!preserveCurrentFanoutToolHistory && isSubagentToolResultMessage(message))) {
|
|
142
143
|
changed = true;
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
145
|
-
const stripped = stripAssistantSubagentToolCallBlocks(message);
|
|
146
|
+
const stripped = preserveCurrentFanoutToolHistory ? message : stripAssistantSubagentToolCallBlocks(message);
|
|
146
147
|
if (stripped === undefined) {
|
|
147
148
|
changed = true;
|
|
148
149
|
continue;
|
|
@@ -5,7 +5,7 @@ export interface WorkflowGraphBuildInput {
|
|
|
5
5
|
runId: string;
|
|
6
6
|
mode?: SubagentRunMode;
|
|
7
7
|
steps: ChainStep[];
|
|
8
|
-
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "
|
|
8
|
+
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "error" | "acceptance">>;
|
|
9
9
|
currentFlatIndex?: number;
|
|
10
10
|
currentStepIndex?: number;
|
|
11
11
|
stepStatuses?: Array<{ status?: string; error?: string }>;
|
|
@@ -26,8 +26,6 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
26
26
|
return "paused";
|
|
27
27
|
case "detached":
|
|
28
28
|
return "detached";
|
|
29
|
-
case "timed-out":
|
|
30
|
-
return "timed-out";
|
|
31
29
|
case "pending":
|
|
32
30
|
return "pending";
|
|
33
31
|
default:
|
|
@@ -35,10 +33,9 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"
|
|
36
|
+
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"> | undefined): WorkflowNodeStatus | undefined {
|
|
39
37
|
if (!result) return undefined;
|
|
40
38
|
if (result.detached) return "detached";
|
|
41
|
-
if (result.timedOut) return "timed-out";
|
|
42
39
|
if (result.interrupted) return "paused";
|
|
43
40
|
return result.exitCode === 0 ? "completed" : "failed";
|
|
44
41
|
}
|
|
@@ -66,7 +63,6 @@ function seqLabel(step: SequentialStep, stepIndex: number): string {
|
|
|
66
63
|
function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
|
|
67
64
|
if (statuses.some((status) => status === "running")) return "running";
|
|
68
65
|
if (statuses.some((status) => status === "failed")) return "failed";
|
|
69
|
-
if (statuses.some((status) => status === "timed-out")) return "timed-out";
|
|
70
66
|
if (statuses.some((status) => status === "paused")) return "paused";
|
|
71
67
|
if (statuses.some((status) => status === "detached")) return "detached";
|
|
72
68
|
if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
|
|
@@ -1,16 +1,73 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
type AtomicJsonFs = Pick<typeof fs, "mkdirSync" | "writeFileSync" | "renameSync" | "rmSync">;
|
|
5
|
+
|
|
6
|
+
type AtomicJsonWriterOptions = {
|
|
7
|
+
fs?: AtomicJsonFs;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
pid?: number;
|
|
10
|
+
random?: () => number;
|
|
11
|
+
retryRenameErrors?: boolean;
|
|
12
|
+
retryDelaysMs?: readonly number[];
|
|
13
|
+
wait?: (delayMs: number) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200] as const;
|
|
17
|
+
const RETRYABLE_RENAME_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
18
|
+
|
|
19
|
+
function waitSync(delayMs: number): void {
|
|
20
|
+
const end = Date.now() + delayMs;
|
|
21
|
+
while (Date.now() < end) {
|
|
22
|
+
// writeAtomicJson is synchronous because callers often update status from sync callbacks.
|
|
15
23
|
}
|
|
16
24
|
}
|
|
25
|
+
|
|
26
|
+
function isRetryableRenameError(error: unknown): boolean {
|
|
27
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
28
|
+
return typeof code === "string" && RETRYABLE_RENAME_ERROR_CODES.has(code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renameWithRetry(
|
|
32
|
+
fsImpl: AtomicJsonFs,
|
|
33
|
+
sourcePath: string,
|
|
34
|
+
targetPath: string,
|
|
35
|
+
retryDelaysMs: readonly number[],
|
|
36
|
+
wait: (delayMs: number) => void,
|
|
37
|
+
): void {
|
|
38
|
+
for (let attempt = 0; ; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
fsImpl.renameSync(sourcePath, targetPath);
|
|
41
|
+
return;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const delayMs = retryDelaysMs[attempt];
|
|
44
|
+
if (delayMs === undefined || !isRetryableRenameError(error)) throw error;
|
|
45
|
+
wait(delayMs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (filePath: string, payload: object) => void {
|
|
51
|
+
const fsImpl = options.fs ?? fs;
|
|
52
|
+
const now = options.now ?? Date.now;
|
|
53
|
+
const pid = options.pid ?? process.pid;
|
|
54
|
+
const random = options.random ?? Math.random;
|
|
55
|
+
const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
|
|
56
|
+
const retryDelaysMs = retryRenameErrors ? options.retryDelaysMs ?? DEFAULT_RENAME_RETRY_DELAYS_MS : [];
|
|
57
|
+
const wait = options.wait ?? waitSync;
|
|
58
|
+
return (filePath: string, payload: object): void => {
|
|
59
|
+
fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
|
+
const tempPath = path.join(
|
|
61
|
+
path.dirname(filePath),
|
|
62
|
+
`.${path.basename(filePath)}.${pid}.${now()}.${random().toString(36).slice(2)}.tmp`,
|
|
63
|
+
);
|
|
64
|
+
try {
|
|
65
|
+
fsImpl.writeFileSync(tempPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
66
|
+
renameWithRetry(fsImpl, tempPath, filePath, retryDelaysMs, wait);
|
|
67
|
+
} finally {
|
|
68
|
+
fsImpl.rmSync(tempPath, { force: true });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const writeAtomicJson = createAtomicJsonWriter();
|
package/src/shared/settings.ts
CHANGED
package/src/shared/types.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ChainOutputMapEntry {
|
|
|
30
30
|
|
|
31
31
|
export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
|
|
32
32
|
|
|
33
|
-
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached"
|
|
33
|
+
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached";
|
|
34
34
|
|
|
35
35
|
export interface WorkflowGraphNode {
|
|
36
36
|
id: string;
|
|
@@ -142,7 +142,7 @@ export interface ControlEvent {
|
|
|
142
142
|
recentFailureSummary?: string;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached"
|
|
145
|
+
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
146
146
|
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
147
147
|
|
|
148
148
|
export type PublicNestedStepSummary = Pick<
|
|
@@ -239,7 +239,7 @@ export interface ModelAttempt {
|
|
|
239
239
|
usage?: Usage;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
export type
|
|
242
|
+
export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified" | "reviewed";
|
|
243
243
|
|
|
244
244
|
export type AcceptanceEvidenceKind =
|
|
245
245
|
| "changed-files"
|
|
@@ -275,15 +275,16 @@ export interface AcceptanceReviewGate {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
export interface AcceptanceConfig {
|
|
278
|
+
level?: AcceptanceLevel;
|
|
278
279
|
criteria?: Array<string | AcceptanceGate>;
|
|
279
280
|
evidence?: AcceptanceEvidenceKind[];
|
|
280
281
|
verify?: AcceptanceVerifyCommand[];
|
|
281
|
-
review?: AcceptanceReviewGate;
|
|
282
|
+
review?: AcceptanceReviewGate | false;
|
|
282
283
|
stopRules?: string[];
|
|
283
|
-
|
|
284
|
+
reason?: string;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
export type AcceptanceInput = AcceptanceConfig;
|
|
287
|
+
export type AcceptanceInput = AcceptanceLevel | false | AcceptanceConfig;
|
|
287
288
|
|
|
288
289
|
export interface ResolvedAcceptanceGate extends AcceptanceGate {
|
|
289
290
|
id: string;
|
|
@@ -293,18 +294,15 @@ export interface ResolvedAcceptanceGate extends AcceptanceGate {
|
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
export interface ResolvedAcceptanceConfig {
|
|
296
|
-
level:
|
|
297
|
+
level: Exclude<AcceptanceLevel, "auto">;
|
|
297
298
|
explicit: boolean;
|
|
298
299
|
inferredReason: string[];
|
|
299
300
|
criteria: ResolvedAcceptanceGate[];
|
|
300
301
|
evidence: AcceptanceEvidenceKind[];
|
|
301
302
|
verify: AcceptanceVerifyCommand[];
|
|
302
|
-
review?: AcceptanceReviewGate;
|
|
303
|
+
review?: AcceptanceReviewGate | false;
|
|
303
304
|
stopRules: string[];
|
|
304
|
-
|
|
305
|
-
mode: "none" | "self-review-loop";
|
|
306
|
-
maxTurns: number;
|
|
307
|
-
};
|
|
305
|
+
reason?: string;
|
|
308
306
|
}
|
|
309
307
|
|
|
310
308
|
export interface AcceptanceReport {
|
|
@@ -368,25 +366,6 @@ export type AcceptanceLedgerStatus =
|
|
|
368
366
|
| "accepted"
|
|
369
367
|
| "rejected";
|
|
370
368
|
|
|
371
|
-
export interface AcceptanceFinalizationTurn {
|
|
372
|
-
turn: number;
|
|
373
|
-
prompt: string;
|
|
374
|
-
status: AcceptanceLedgerStatus;
|
|
375
|
-
rawOutput?: string;
|
|
376
|
-
report?: AcceptanceReport;
|
|
377
|
-
parseError?: string;
|
|
378
|
-
runtimeChecks: AcceptanceRuntimeCheck[];
|
|
379
|
-
verifyRuns: AcceptanceVerifyResult[];
|
|
380
|
-
failureMessage?: string;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export interface AcceptanceFinalizationLedger {
|
|
384
|
-
mode: "self-review-loop";
|
|
385
|
-
status: "not-run" | "completed" | "failed";
|
|
386
|
-
maxTurns: number;
|
|
387
|
-
turns: AcceptanceFinalizationTurn[];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
369
|
export interface AcceptanceLedger {
|
|
391
370
|
status: AcceptanceLedgerStatus;
|
|
392
371
|
explicit: boolean;
|
|
@@ -395,12 +374,9 @@ export interface AcceptanceLedger {
|
|
|
395
374
|
criteria: ResolvedAcceptanceGate[];
|
|
396
375
|
childReport?: AcceptanceReport;
|
|
397
376
|
childReportParseError?: string;
|
|
398
|
-
initialChildReport?: AcceptanceReport;
|
|
399
|
-
initialChildReportParseError?: string;
|
|
400
377
|
runtimeChecks: AcceptanceRuntimeCheck[];
|
|
401
378
|
verifyRuns: AcceptanceVerifyResult[];
|
|
402
379
|
reviewResult?: AcceptanceReviewResult;
|
|
403
|
-
finalization?: AcceptanceFinalizationLedger;
|
|
404
380
|
parentDecision?: {
|
|
405
381
|
status: "accepted" | "rejected";
|
|
406
382
|
at: string;
|
|
@@ -408,13 +384,6 @@ export interface AcceptanceLedger {
|
|
|
408
384
|
};
|
|
409
385
|
}
|
|
410
386
|
|
|
411
|
-
export interface ResourceLimitExceeded {
|
|
412
|
-
kind: "maxExecutionTimeMs" | "maxTokens";
|
|
413
|
-
limit: number;
|
|
414
|
-
observed?: number;
|
|
415
|
-
message: string;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
387
|
export interface SingleResult {
|
|
419
388
|
agent: string;
|
|
420
389
|
task: string;
|
|
@@ -422,8 +391,6 @@ export interface SingleResult {
|
|
|
422
391
|
detached?: boolean;
|
|
423
392
|
detachedReason?: string;
|
|
424
393
|
interrupted?: boolean;
|
|
425
|
-
timedOut?: boolean;
|
|
426
|
-
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
427
394
|
messages?: Message[];
|
|
428
395
|
usage: Usage;
|
|
429
396
|
model?: string;
|
|
@@ -648,7 +615,6 @@ export interface AsyncStatus {
|
|
|
648
615
|
structuredOutputPath?: string;
|
|
649
616
|
structuredOutputSchemaPath?: string;
|
|
650
617
|
acceptance?: AcceptanceLedger;
|
|
651
|
-
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
652
618
|
}>;
|
|
653
619
|
sessionDir?: string;
|
|
654
620
|
outputFile?: string;
|
|
@@ -790,8 +756,6 @@ export interface RunSyncOptions {
|
|
|
790
756
|
cwd?: string;
|
|
791
757
|
signal?: AbortSignal;
|
|
792
758
|
interruptSignal?: AbortSignal;
|
|
793
|
-
timeoutMs?: number;
|
|
794
|
-
timeoutAt?: number;
|
|
795
759
|
allowIntercomDetach?: boolean;
|
|
796
760
|
intercomEvents?: IntercomEventBus;
|
|
797
761
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
@@ -810,8 +774,6 @@ export interface RunSyncOptions {
|
|
|
810
774
|
outputPath?: string;
|
|
811
775
|
outputMode?: OutputMode;
|
|
812
776
|
maxSubagentDepth?: number;
|
|
813
|
-
maxExecutionTimeMs?: number;
|
|
814
|
-
maxTokens?: number;
|
|
815
777
|
nestedRoute?: NestedRouteInfo;
|
|
816
778
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
817
779
|
modelOverride?: string;
|
package/src/shared/utils.ts
CHANGED
|
@@ -192,7 +192,8 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
192
192
|
const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
|
|
193
193
|
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
194
194
|
if (hasAssistantError) continue;
|
|
195
|
-
for (
|
|
195
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
196
|
+
const part = msg.content[j];
|
|
196
197
|
if (part.type === "text" && part.text.trim().length > 0) return part.text;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -204,13 +205,6 @@ export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" |
|
|
|
204
205
|
return result.finalOutput ?? getFinalOutput(result.messages ?? []);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
export function formatResourceLimitExceeded(input: { agent: string; kind: "maxExecutionTimeMs" | "maxTokens"; limit: number; observed?: number }): string {
|
|
208
|
-
if (input.kind === "maxExecutionTimeMs") {
|
|
209
|
-
return `Resource limit exceeded for ${input.agent}: maxExecutionTimeMs ${input.limit}ms.`;
|
|
210
|
-
}
|
|
211
|
-
return `Resource limit exceeded for ${input.agent}: maxTokens ${input.limit}${input.observed !== undefined ? ` (observed ${input.observed})` : ""}.`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
208
|
/**
|
|
215
209
|
* Extract display items (text and tool calls) from messages
|
|
216
210
|
*/
|
package/src/tui/render.ts
CHANGED
|
@@ -222,22 +222,11 @@ function firstOutputLine(text: string): string {
|
|
|
222
222
|
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
|
|
226
|
-
const acceptance = result.acceptance;
|
|
227
|
-
if (!acceptance?.status || acceptance.status === "not-required") return undefined;
|
|
228
|
-
const finalization = acceptance.finalization
|
|
229
|
-
? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
|
|
230
|
-
: "";
|
|
231
|
-
return `acceptance: ${acceptance.status}${finalization}`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
225
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
235
226
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
236
|
-
if (result.timedOut) return `Timed out${result.error ? `: ${result.error}` : ""}`;
|
|
237
227
|
if (result.interrupted) return "Paused";
|
|
238
228
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
239
|
-
|
|
240
|
-
if (acceptance) return `Done · ${acceptance}`;
|
|
229
|
+
if (result.acceptance?.status && result.acceptance.status !== "not-required") return `Done · acceptance: ${result.acceptance.status}`;
|
|
241
230
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
242
231
|
return "Done";
|
|
243
232
|
}
|
|
@@ -245,7 +234,6 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
245
234
|
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
246
235
|
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
247
236
|
if (result.detached) return theme.fg("warning", "■");
|
|
248
|
-
if (result.timedOut) return theme.fg("error", "✗");
|
|
249
237
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
250
238
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
251
239
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
@@ -365,19 +353,18 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
365
353
|
return theme.fg("error", "✗");
|
|
366
354
|
}
|
|
367
355
|
|
|
368
|
-
function widgetStepGlyph(status: AsyncJobStep["status"]
|
|
356
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
369
357
|
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
370
358
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
371
|
-
if (status === "failed"
|
|
359
|
+
if (status === "failed") return theme.fg("error", "✗");
|
|
372
360
|
if (status === "paused") return theme.fg("warning", "■");
|
|
373
361
|
return theme.fg("muted", "◦");
|
|
374
362
|
}
|
|
375
363
|
|
|
376
|
-
function widgetStepStatus(status: AsyncJobStep["status"]
|
|
364
|
+
function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
|
|
377
365
|
if (status === "running") return theme.fg("accent", "running");
|
|
378
366
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
379
367
|
if (status === "failed") return theme.fg("error", "failed");
|
|
380
|
-
if (status === "timed-out") return theme.fg("error", "timed out");
|
|
381
368
|
if (status === "paused") return theme.fg("warning", "paused");
|
|
382
369
|
return theme.fg("dim", status);
|
|
383
370
|
}
|
|
@@ -514,7 +501,7 @@ function isDoneResult(result: Details["results"][number]): boolean {
|
|
|
514
501
|
const status = result.progress?.status;
|
|
515
502
|
if (status === "completed") return true;
|
|
516
503
|
if (status === "running" || status === "pending") return false;
|
|
517
|
-
if (result.interrupted || result.detached
|
|
504
|
+
if (result.interrupted || result.detached) return false;
|
|
518
505
|
return result.exitCode === 0;
|
|
519
506
|
}
|
|
520
507
|
|
|
@@ -586,7 +573,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
586
573
|
|
|
587
574
|
if (details.mode === "parallel") {
|
|
588
575
|
const totalCount = details.totalSteps ?? details.results.length;
|
|
589
|
-
const statuses = new Array(totalCount).fill("pending") as
|
|
576
|
+
const statuses = new Array(totalCount).fill("pending") as Array<"pending" | "running" | "completed" | "failed" | "detached">;
|
|
590
577
|
for (const progress of details.progress ?? []) {
|
|
591
578
|
if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
|
|
592
579
|
}
|
|
@@ -597,13 +584,11 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
597
584
|
const index = result.progress?.index ?? progressFromArray?.index ?? i;
|
|
598
585
|
if (index < 0 || index >= totalCount) continue;
|
|
599
586
|
const status = result.progress?.status
|
|
600
|
-
?? (result.
|
|
601
|
-
? "
|
|
602
|
-
: result.
|
|
603
|
-
? "
|
|
604
|
-
:
|
|
605
|
-
? "completed"
|
|
606
|
-
: "failed");
|
|
587
|
+
?? (result.interrupted || result.detached
|
|
588
|
+
? "detached"
|
|
589
|
+
: result.exitCode === 0
|
|
590
|
+
? "completed"
|
|
591
|
+
: "failed");
|
|
607
592
|
statuses[index] = status;
|
|
608
593
|
}
|
|
609
594
|
const running = statuses.filter((status) => status === "running").length;
|
|
@@ -1065,7 +1050,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1065
1050
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
1066
1051
|
|| workflowGraphHasStatus(d, ["running"]);
|
|
1067
1052
|
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1068
|
-
|| workflowGraphHasStatus(d, ["failed"
|
|
1053
|
+
|| workflowGraphHasStatus(d, ["failed"]);
|
|
1069
1054
|
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1070
1055
|
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1071
1056
|
let totalSummary = d.progressSummary;
|
|
@@ -1141,7 +1126,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1141
1126
|
const activity = compactCurrentActivity(rProg);
|
|
1142
1127
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1143
1128
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
1144
|
-
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached ||
|
|
1129
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1145
1130
|
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1146
1131
|
}
|
|
1147
1132
|
const outputTarget = extractOutputTarget(r.task);
|
|
@@ -1278,7 +1263,7 @@ export function renderSubagentResult(
|
|
|
1278
1263
|
&& r.progress?.status !== "running"
|
|
1279
1264
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1280
1265
|
);
|
|
1281
|
-
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"
|
|
1266
|
+
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1282
1267
|
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1283
1268
|
const icon = hasRunning
|
|
1284
1269
|
? theme.fg("warning", "running")
|