pi-subagents 0.31.0 → 0.32.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 +48 -0
- package/README.md +170 -8
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -1
- package/src/agents/agent-management.ts +6 -1
- package/src/agents/agents.ts +55 -11
- package/src/extension/companion-suggestions.ts +359 -0
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +2 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +69 -4
- package/src/extension/schemas.ts +2 -2
- package/src/intercom/intercom-bridge.ts +25 -1
- package/src/profiles/profiles.ts +637 -0
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-resume.ts +11 -13
- package/src/runs/background/async-status.ts +41 -9
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/control-channel.ts +227 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +459 -113
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +240 -44
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +4 -0
- package/src/runs/shared/nested-events.ts +58 -0
- package/src/runs/shared/parallel-utils.ts +49 -1
- package/src/runs/shared/pi-args.ts +5 -3
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +15 -1
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/types.ts +82 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +726 -40
- package/src/tui/render.ts +16 -4
|
@@ -1,27 +1,115 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
|
|
4
6
|
type SubagentExecutionContext = "fresh" | "fork";
|
|
5
7
|
|
|
8
|
+
interface BranchSessionEntry {
|
|
9
|
+
type: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
parentId?: string | null;
|
|
12
|
+
timestamp?: string;
|
|
13
|
+
message?: {
|
|
14
|
+
role?: string;
|
|
15
|
+
content?: unknown;
|
|
16
|
+
provider?: string;
|
|
17
|
+
api?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
};
|
|
20
|
+
thinkingLevel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BranchSessionManager {
|
|
24
|
+
createBranchedSession(leafId: string): string | undefined;
|
|
25
|
+
getHeader?: () => BranchSessionEntry | null;
|
|
26
|
+
getEntries?: () => BranchSessionEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
interface ForkableSessionManager {
|
|
7
30
|
getSessionFile(): string | undefined;
|
|
8
31
|
getLeafId(): string | null;
|
|
9
32
|
getSessionDir?(): string;
|
|
10
|
-
openSession?: (path: string, sessionDir?: string) =>
|
|
33
|
+
openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
|
|
11
34
|
}
|
|
12
35
|
|
|
13
36
|
interface ForkContextResolverOptions {
|
|
14
|
-
openSession?: (path: string, sessionDir?: string) =>
|
|
37
|
+
openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ForkContextResolution {
|
|
41
|
+
sessionFile: string;
|
|
42
|
+
thinkingOverride?: "off";
|
|
15
43
|
}
|
|
16
44
|
|
|
17
45
|
interface ForkContextResolver {
|
|
18
46
|
sessionFileForIndex(index?: number): string | undefined;
|
|
47
|
+
thinkingOverrideForIndex(index?: number): "off" | undefined;
|
|
19
48
|
}
|
|
20
49
|
|
|
21
50
|
export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
|
|
22
51
|
return value === "fork" ? "fork" : "fresh";
|
|
23
52
|
}
|
|
24
53
|
|
|
54
|
+
function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"], block: unknown): boolean {
|
|
55
|
+
if (!message || !block || typeof block !== "object" || !("type" in block)) return false;
|
|
56
|
+
const provider = typeof message.provider === "string" ? message.provider.toLowerCase() : "";
|
|
57
|
+
const api = typeof message.api === "string" ? message.api.toLowerCase() : "";
|
|
58
|
+
const model = typeof message.model === "string" ? message.model.toLowerCase() : "";
|
|
59
|
+
const isAnthropic = provider === "anthropic" || api === "anthropic-messages" || model.startsWith("anthropic/");
|
|
60
|
+
if (block.type === "redacted_thinking") return true;
|
|
61
|
+
if (block.type !== "thinking" || !isAnthropic) return false;
|
|
62
|
+
const signature = "thinkingSignature" in block ? block.thinkingSignature : "signature" in block ? block.signature : undefined;
|
|
63
|
+
return block.redacted === true || (typeof signature === "string" && signature.length > 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createEntryId(entries: BranchSessionEntry[]): string {
|
|
67
|
+
const ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === "string"));
|
|
68
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
69
|
+
const id = randomUUID().slice(0, 8);
|
|
70
|
+
if (!ids.has(id)) return id;
|
|
71
|
+
}
|
|
72
|
+
return randomUUID();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendThinkingOffEntry(entries: BranchSessionEntry[]): void {
|
|
76
|
+
const last = entries[entries.length - 1];
|
|
77
|
+
if (last?.type === "thinking_level_change" && last.thinkingLevel === "off") return;
|
|
78
|
+
const parent = [...entries].reverse().find((entry) => typeof entry.id === "string");
|
|
79
|
+
entries.push({
|
|
80
|
+
type: "thinking_level_change",
|
|
81
|
+
id: createEntryId(entries),
|
|
82
|
+
parentId: parent?.id ?? null,
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
thinkingLevel: "off",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {
|
|
89
|
+
let sanitized = false;
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
92
|
+
const filtered = entry.message.content.filter((block) => !isUnsafeAnthropicThinkingBlock(entry.message, block));
|
|
93
|
+
if (filtered.length === entry.message.content.length) continue;
|
|
94
|
+
entry.message.content = filtered;
|
|
95
|
+
sanitized = true;
|
|
96
|
+
}
|
|
97
|
+
if (sanitized) appendThinkingOffEntry(entries);
|
|
98
|
+
return sanitized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
|
|
102
|
+
const lines = fs.readFileSync(sessionFile, "utf-8").split("\n").filter((line) => line.trim().length > 0);
|
|
103
|
+
return lines.map((line, index) => {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(line) as BranchSessionEntry;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
108
|
+
throw new Error(`Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`, { cause });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
25
113
|
export function createForkContextResolver(
|
|
26
114
|
sessionManager: ForkableSessionManager,
|
|
27
115
|
requestedContext: unknown,
|
|
@@ -30,6 +118,7 @@ export function createForkContextResolver(
|
|
|
30
118
|
if (resolveSubagentContext(requestedContext) !== "fork") {
|
|
31
119
|
return {
|
|
32
120
|
sessionFileForIndex: () => undefined,
|
|
121
|
+
thinkingOverrideForIndex: () => undefined,
|
|
33
122
|
};
|
|
34
123
|
}
|
|
35
124
|
|
|
@@ -47,30 +136,52 @@ export function createForkContextResolver(
|
|
|
47
136
|
?? sessionManager.openSession
|
|
48
137
|
?? ((file: string, dir?: string) => SessionManager.open(file, dir));
|
|
49
138
|
const sessionDir = sessionManager.getSessionDir?.();
|
|
50
|
-
const
|
|
139
|
+
const cachedResolutions = new Map<number, ForkContextResolution>();
|
|
51
140
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
141
|
+
const resolveFork = (index = 0): ForkContextResolution => {
|
|
142
|
+
const cached = cachedResolutions.get(index);
|
|
143
|
+
if (cached) return cached;
|
|
144
|
+
try {
|
|
145
|
+
if (!fs.existsSync(parentSessionFile)) {
|
|
146
|
+
throw new Error(`Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`);
|
|
147
|
+
}
|
|
148
|
+
const sourceManager = openSession(parentSessionFile, sessionDir);
|
|
149
|
+
const sessionFile = sourceManager.createBranchedSession(leafId);
|
|
150
|
+
if (!sessionFile) {
|
|
151
|
+
throw new Error("Session manager did not return a forked session file.");
|
|
152
|
+
}
|
|
153
|
+
let thinkingOverride: "off" | undefined;
|
|
154
|
+
if (!fs.existsSync(sessionFile)) {
|
|
155
|
+
const header = sourceManager.getHeader?.();
|
|
156
|
+
const entries = sourceManager.getEntries?.();
|
|
157
|
+
if (!header || !entries) {
|
|
158
|
+
throw new Error(`Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`);
|
|
64
159
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
160
|
+
if (sanitizeUnsafeThinkingBlocks(entries)) thinkingOverride = "off";
|
|
161
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
162
|
+
fs.writeFileSync(sessionFile, `${[header, ...entries].map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
163
|
+
} else {
|
|
164
|
+
const entries = readSessionEntries(sessionFile);
|
|
165
|
+
if (sanitizeUnsafeThinkingBlocks(entries)) {
|
|
166
|
+
thinkingOverride = "off";
|
|
167
|
+
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
67
168
|
}
|
|
68
|
-
cachedSessionFiles.set(index, sessionFile);
|
|
69
|
-
return sessionFile;
|
|
70
|
-
} catch (error) {
|
|
71
|
-
const cause = error instanceof Error ? error : new Error(String(error));
|
|
72
|
-
throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
|
|
73
169
|
}
|
|
170
|
+
const resolution = { sessionFile, ...(thinkingOverride ? { thinkingOverride } : {}) };
|
|
171
|
+
cachedResolutions.set(index, resolution);
|
|
172
|
+
return resolution;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
175
|
+
throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
sessionFileForIndex(index = 0): string | undefined {
|
|
181
|
+
return resolveFork(index).sessionFile;
|
|
182
|
+
},
|
|
183
|
+
thinkingOverrideForIndex(index = 0): "off" | undefined {
|
|
184
|
+
return resolveFork(index).thinkingOverride;
|
|
74
185
|
},
|
|
75
186
|
};
|
|
76
187
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -144,17 +144,25 @@ export interface ControlEvent {
|
|
|
144
144
|
|
|
145
145
|
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
146
146
|
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
147
|
+
export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 1;
|
|
148
|
+
export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
|
|
147
149
|
|
|
148
150
|
export type PublicNestedStepSummary = Pick<
|
|
149
151
|
NestedStepSummary,
|
|
150
|
-
"agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error"
|
|
152
|
+
"agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error" | "timedOut"
|
|
151
153
|
> & {
|
|
152
154
|
children?: PublicNestedRunSummary[];
|
|
153
155
|
};
|
|
154
156
|
|
|
157
|
+
export type CostSummary = {
|
|
158
|
+
inputTokens: number;
|
|
159
|
+
outputTokens: number;
|
|
160
|
+
costUsd: number;
|
|
161
|
+
};
|
|
162
|
+
|
|
155
163
|
export type PublicNestedRunSummary = Pick<
|
|
156
164
|
NestedRunSummary,
|
|
157
|
-
"id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "startedAt" | "endedAt" | "lastUpdate" | "error"
|
|
165
|
+
"id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "totalCost" | "startedAt" | "endedAt" | "lastUpdate" | "error" | "timeoutMs" | "deadlineAt" | "timedOut"
|
|
158
166
|
> & {
|
|
159
167
|
steps?: PublicNestedStepSummary[];
|
|
160
168
|
children?: PublicNestedRunSummary[];
|
|
@@ -416,6 +424,7 @@ export interface SingleResult {
|
|
|
416
424
|
structuredOutputPath?: string;
|
|
417
425
|
structuredOutputSchemaPath?: string;
|
|
418
426
|
acceptance?: AcceptanceLedger;
|
|
427
|
+
children?: NestedRunSummary[];
|
|
419
428
|
}
|
|
420
429
|
|
|
421
430
|
export interface Details {
|
|
@@ -426,6 +435,9 @@ export interface Details {
|
|
|
426
435
|
controlEvents?: ControlEvent[];
|
|
427
436
|
asyncId?: string;
|
|
428
437
|
asyncDir?: string;
|
|
438
|
+
timeoutMs?: number;
|
|
439
|
+
deadlineAt?: number;
|
|
440
|
+
timedOut?: boolean;
|
|
429
441
|
progress?: AgentProgress[];
|
|
430
442
|
progressSummary?: ProgressSummary;
|
|
431
443
|
artifacts?: {
|
|
@@ -444,6 +456,10 @@ export interface Details {
|
|
|
444
456
|
currentStepIndex?: number; // 0-indexed current step (for running chains)
|
|
445
457
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
446
458
|
outputs?: ChainOutputMap;
|
|
459
|
+
// Aggregated child usage across all agents in the run
|
|
460
|
+
totalChildUsage?: Usage;
|
|
461
|
+
// Aggregated cost across all agents in the run
|
|
462
|
+
totalCost?: CostSummary;
|
|
447
463
|
}
|
|
448
464
|
|
|
449
465
|
// ============================================================================
|
|
@@ -502,6 +518,7 @@ export interface NestedStepSummary {
|
|
|
502
518
|
startedAt?: number;
|
|
503
519
|
endedAt?: number;
|
|
504
520
|
error?: string;
|
|
521
|
+
timedOut?: boolean;
|
|
505
522
|
children?: NestedRunSummary[];
|
|
506
523
|
}
|
|
507
524
|
|
|
@@ -533,9 +550,13 @@ export interface NestedRunSummary extends NestedRunAddress {
|
|
|
533
550
|
turnCount?: number;
|
|
534
551
|
toolCount?: number;
|
|
535
552
|
totalTokens?: TokenUsage;
|
|
553
|
+
totalCost?: CostSummary;
|
|
536
554
|
startedAt?: number;
|
|
537
555
|
endedAt?: number;
|
|
538
556
|
lastUpdate?: number;
|
|
557
|
+
timeoutMs?: number;
|
|
558
|
+
deadlineAt?: number;
|
|
559
|
+
timedOut?: boolean;
|
|
539
560
|
error?: string;
|
|
540
561
|
}
|
|
541
562
|
|
|
@@ -547,6 +568,7 @@ export interface NestedRouteInfo {
|
|
|
547
568
|
}
|
|
548
569
|
|
|
549
570
|
export interface AsyncStartedEvent {
|
|
571
|
+
lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
|
|
550
572
|
id?: string;
|
|
551
573
|
asyncDir?: string;
|
|
552
574
|
pid?: number;
|
|
@@ -558,14 +580,18 @@ export interface AsyncStartedEvent {
|
|
|
558
580
|
chainStepCount?: number;
|
|
559
581
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
560
582
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
583
|
+
timeoutMs?: number;
|
|
584
|
+
deadlineAt?: number;
|
|
561
585
|
nestedRoute?: NestedRouteInfo;
|
|
562
586
|
}
|
|
563
587
|
|
|
564
588
|
export interface AsyncStatus {
|
|
589
|
+
lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
|
|
565
590
|
runId: string;
|
|
566
591
|
sessionId?: string;
|
|
567
592
|
mode: SubagentRunMode;
|
|
568
593
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
594
|
+
error?: string;
|
|
569
595
|
activityState?: ActivityState;
|
|
570
596
|
lastActivityAt?: number;
|
|
571
597
|
currentTool?: string;
|
|
@@ -576,6 +602,9 @@ export interface AsyncStatus {
|
|
|
576
602
|
startedAt: number;
|
|
577
603
|
endedAt?: number;
|
|
578
604
|
lastUpdate?: number;
|
|
605
|
+
timeoutMs?: number;
|
|
606
|
+
deadlineAt?: number;
|
|
607
|
+
timedOut?: boolean;
|
|
579
608
|
pid?: number;
|
|
580
609
|
cwd?: string;
|
|
581
610
|
currentStep?: number;
|
|
@@ -606,12 +635,14 @@ export interface AsyncStatus {
|
|
|
606
635
|
endedAt?: number;
|
|
607
636
|
durationMs?: number;
|
|
608
637
|
exitCode?: number | null;
|
|
638
|
+
timedOut?: boolean;
|
|
609
639
|
tokens?: TokenUsage;
|
|
610
640
|
skills?: string[];
|
|
611
641
|
model?: string;
|
|
612
642
|
thinking?: string;
|
|
613
643
|
attemptedModels?: string[];
|
|
614
644
|
modelAttempts?: ModelAttempt[];
|
|
645
|
+
totalCost?: CostSummary;
|
|
615
646
|
error?: string;
|
|
616
647
|
structuredOutput?: unknown;
|
|
617
648
|
structuredOutputPath?: string;
|
|
@@ -621,6 +652,7 @@ export interface AsyncStatus {
|
|
|
621
652
|
sessionDir?: string;
|
|
622
653
|
outputFile?: string;
|
|
623
654
|
totalTokens?: TokenUsage;
|
|
655
|
+
totalCost?: CostSummary;
|
|
624
656
|
sessionFile?: string;
|
|
625
657
|
outputs?: ChainOutputMap;
|
|
626
658
|
}
|
|
@@ -655,6 +687,9 @@ export interface AsyncJobState {
|
|
|
655
687
|
activeParallelGroup?: boolean;
|
|
656
688
|
startedAt?: number;
|
|
657
689
|
updatedAt?: number;
|
|
690
|
+
timeoutMs?: number;
|
|
691
|
+
deadlineAt?: number;
|
|
692
|
+
timedOut?: boolean;
|
|
658
693
|
sessionDir?: string;
|
|
659
694
|
outputFile?: string;
|
|
660
695
|
totalTokens?: TokenUsage;
|
|
@@ -683,6 +718,7 @@ export interface SubagentState {
|
|
|
683
718
|
baseCwd: string;
|
|
684
719
|
currentSessionId: string | null;
|
|
685
720
|
subagentInProgress?: boolean;
|
|
721
|
+
subagentSpawns?: { sessionId: string | null; count: number };
|
|
686
722
|
asyncJobs: Map<string, AsyncJobState>;
|
|
687
723
|
foregroundRuns?: Map<string, ForegroundResumeRun>;
|
|
688
724
|
foregroundControls: Map<string, {
|
|
@@ -712,6 +748,8 @@ export interface SubagentState {
|
|
|
712
748
|
completionSeen: Map<string, number>;
|
|
713
749
|
watcher: FSWatcher | null;
|
|
714
750
|
watcherRestartTimer: ReturnType<typeof setTimeout> | null;
|
|
751
|
+
companionSuggestionStartupShown?: boolean;
|
|
752
|
+
companionSuggestionListShown?: boolean;
|
|
715
753
|
resultFileCoalescer: {
|
|
716
754
|
schedule(file: string, delayMs?: number): boolean;
|
|
717
755
|
clear(): void;
|
|
@@ -784,6 +822,8 @@ export interface RunSyncOptions {
|
|
|
784
822
|
nestedRoute?: NestedRouteInfo;
|
|
785
823
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
786
824
|
modelOverride?: string;
|
|
825
|
+
/** Override the agent's default thinking level for this run */
|
|
826
|
+
thinkingOverride?: AgentConfig["thinking"];
|
|
787
827
|
/** Registry models available for heuristic bare-model resolution */
|
|
788
828
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
789
829
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
@@ -829,18 +869,41 @@ export interface ProactiveSkillSubagentsConfig {
|
|
|
829
869
|
preferredAgent?: string;
|
|
830
870
|
}
|
|
831
871
|
|
|
872
|
+
export type CompanionSuggestionPackage = "pi-prompt-template-model" | "pi-intercom";
|
|
873
|
+
export type CompanionSuggestionSurface = "session_start" | "list" | "doctor";
|
|
874
|
+
|
|
875
|
+
export interface CompanionSuggestionPackageConfig {
|
|
876
|
+
enabled?: boolean;
|
|
877
|
+
surfaces?: CompanionSuggestionSurface[];
|
|
878
|
+
dismissed?: {
|
|
879
|
+
user?: boolean;
|
|
880
|
+
workspaces?: string[];
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export interface CompanionSuggestionsConfig {
|
|
885
|
+
enabled?: boolean;
|
|
886
|
+
packages?: Partial<Record<CompanionSuggestionPackage, CompanionSuggestionPackageConfig>>;
|
|
887
|
+
}
|
|
888
|
+
|
|
832
889
|
export interface ExtensionConfig {
|
|
833
890
|
asyncByDefault?: boolean;
|
|
834
891
|
forceTopLevelAsync?: boolean;
|
|
835
892
|
defaultSessionDir?: string;
|
|
893
|
+
singleRunOutputBaseDir?: string;
|
|
836
894
|
maxSubagentDepth?: number;
|
|
895
|
+
maxSubagentSpawnsPerSession?: number;
|
|
896
|
+
/** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
|
|
897
|
+
globalConcurrencyLimit?: number;
|
|
837
898
|
control?: ControlConfig;
|
|
838
899
|
parallel?: TopLevelParallelConfig;
|
|
839
900
|
chain?: ExtensionChainConfig;
|
|
840
901
|
worktreeSetupHook?: string;
|
|
841
902
|
worktreeSetupHookTimeoutMs?: number;
|
|
903
|
+
worktreeBaseDir?: string;
|
|
842
904
|
intercomBridge?: IntercomBridgeConfig;
|
|
843
905
|
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
906
|
+
companionSuggestions?: CompanionSuggestionsConfig | false;
|
|
844
907
|
}
|
|
845
908
|
|
|
846
909
|
// ============================================================================
|
|
@@ -923,6 +986,7 @@ export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
|
|
|
923
986
|
export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
|
|
924
987
|
export const WIDGET_KEY = "subagent-async";
|
|
925
988
|
export const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
989
|
+
export const SLASH_TEXT_RESULT_TYPE = "subagent-slash-text-result";
|
|
926
990
|
export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
927
991
|
export const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
|
|
928
992
|
export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
|
|
@@ -931,6 +995,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
931
995
|
export const POLL_INTERVAL_MS = 250;
|
|
932
996
|
export const MAX_WIDGET_JOBS = 4;
|
|
933
997
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
998
|
+
export const DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION = 40;
|
|
934
999
|
export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
935
1000
|
|
|
936
1001
|
export const DEFAULT_FORK_PREAMBLE =
|
|
@@ -974,12 +1039,16 @@ export function wrapForkTask(task: string, preamble?: string | false): string {
|
|
|
974
1039
|
// Recursion Depth Guard
|
|
975
1040
|
// ============================================================================
|
|
976
1041
|
|
|
977
|
-
|
|
1042
|
+
function normalizeNonNegativeInteger(value: unknown): number | undefined {
|
|
978
1043
|
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
979
1044
|
if (!Number.isInteger(parsed) || parsed < 0) return undefined;
|
|
980
1045
|
return parsed;
|
|
981
1046
|
}
|
|
982
1047
|
|
|
1048
|
+
export function normalizeMaxSubagentDepth(value: unknown): number | undefined {
|
|
1049
|
+
return normalizeNonNegativeInteger(value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
983
1052
|
export function resolveCurrentMaxSubagentDepth(configMaxDepth?: number): number {
|
|
984
1053
|
return normalizeMaxSubagentDepth(process.env.PI_SUBAGENT_MAX_DEPTH)
|
|
985
1054
|
?? normalizeMaxSubagentDepth(configMaxDepth)
|
|
@@ -1008,6 +1077,16 @@ export function getSubagentDepthEnv(maxDepth?: number): Record<string, string> {
|
|
|
1008
1077
|
};
|
|
1009
1078
|
}
|
|
1010
1079
|
|
|
1080
|
+
export function normalizeMaxSubagentSpawnsPerSession(value: unknown): number | undefined {
|
|
1081
|
+
return normalizeNonNegativeInteger(value);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export function resolveMaxSubagentSpawnsPerSession(configMaxSpawns?: number): number {
|
|
1085
|
+
return normalizeMaxSubagentSpawnsPerSession(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_SESSION)
|
|
1086
|
+
?? normalizeMaxSubagentSpawnsPerSession(configMaxSpawns)
|
|
1087
|
+
?? DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1011
1090
|
// ============================================================================
|
|
1012
1091
|
// Utility Functions
|
|
1013
1092
|
// ============================================================================
|
package/src/shared/utils.ts
CHANGED
|
@@ -5,22 +5,60 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
10
9
|
import { formatToolCall } from "./formatters.ts";
|
|
11
|
-
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult, ToolCallSummary } from "./types.ts";
|
|
10
|
+
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
|
|
12
11
|
|
|
13
12
|
// ============================================================================
|
|
14
13
|
// File System Utilities
|
|
15
14
|
// ============================================================================
|
|
16
15
|
|
|
17
16
|
const DEFAULT_CONFIG_DIR_NAME = ".pi";
|
|
17
|
+
const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
18
|
+
export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function validConfigDirName(value: unknown): string | undefined {
|
|
21
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readConfigDirNameFromPackageRoot(packageRoot: string | undefined): string | undefined {
|
|
25
|
+
if (!packageRoot) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8")) as {
|
|
28
|
+
name?: unknown;
|
|
29
|
+
piConfig?: { configDir?: unknown };
|
|
30
|
+
};
|
|
31
|
+
if (pkg.name !== PI_CODING_AGENT_PACKAGE_NAME) return undefined;
|
|
32
|
+
return validConfigDirName(pkg.piConfig?.configDir);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveConfigDirNameFromPackageJson(entryPoint = process.argv[1], packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]): string | undefined {
|
|
39
|
+
const packageRootValue = readConfigDirNameFromPackageRoot(packageRoot);
|
|
40
|
+
if (packageRootValue) return packageRootValue;
|
|
41
|
+
if (!entryPoint) return undefined;
|
|
42
|
+
try {
|
|
43
|
+
let dir = path.dirname(fs.realpathSync(entryPoint));
|
|
44
|
+
while (dir !== path.dirname(dir)) {
|
|
45
|
+
const value = readConfigDirNameFromPackageRoot(dir);
|
|
46
|
+
if (value) return value;
|
|
47
|
+
dir = path.dirname(dir);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Package metadata lookup is best-effort; detached runners must not fail here.
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveConfigDirName(codingAgentModule?: unknown, entryPoint?: string, packageRoot?: string): string {
|
|
56
|
+
const moduleValue = codingAgentModule && typeof codingAgentModule === "object"
|
|
57
|
+
? validConfigDirName((codingAgentModule as { CONFIG_DIR_NAME?: unknown }).CONFIG_DIR_NAME)
|
|
22
58
|
: undefined;
|
|
23
|
-
return
|
|
59
|
+
return moduleValue
|
|
60
|
+
?? resolveConfigDirNameFromPackageJson(entryPoint, packageRoot)
|
|
61
|
+
?? DEFAULT_CONFIG_DIR_NAME;
|
|
24
62
|
}
|
|
25
63
|
|
|
26
64
|
export function getConfigDirName(): string {
|
|
@@ -204,19 +242,28 @@ function writePrompt(agent: string, prompt: string): { dir: string; path: string
|
|
|
204
242
|
* Get the final text output from a list of messages
|
|
205
243
|
*/
|
|
206
244
|
export function getFinalOutput(messages: Message[]): string {
|
|
245
|
+
const validTextParts: string[] = [];
|
|
207
246
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
208
247
|
const msg = messages[i];
|
|
209
|
-
if (msg.role
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
if (msg.role !== "assistant") continue;
|
|
249
|
+
const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
|
|
250
|
+
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
251
|
+
if (hasAssistantError) continue;
|
|
252
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
253
|
+
const part = msg.content[j];
|
|
254
|
+
if (part.type !== "text" || part.text.trim().length === 0) continue;
|
|
255
|
+
validTextParts.push(part.text);
|
|
256
|
+
if (/```acceptance-report\s*\n[\s\S]*?```/i.test(part.text)) return part.text;
|
|
257
|
+
for (const match of part.text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
|
|
258
|
+
const body = match[1] ?? "";
|
|
259
|
+
if (/"criteriaSatisfied"/.test(body) && /"(?:changedFiles|testsAddedOrUpdated|commandsRun|validationOutput|residualRisks|noStagedFiles|diffSummary|reviewFindings|manualNotes)"/.test(body)) {
|
|
260
|
+
return part.text;
|
|
261
|
+
}
|
|
216
262
|
}
|
|
263
|
+
if (/ACCEPTANCE_REPORT\s*:/i.test(part.text)) return part.text;
|
|
217
264
|
}
|
|
218
265
|
}
|
|
219
|
-
return "";
|
|
266
|
+
return validTextParts[0] ?? "";
|
|
220
267
|
}
|
|
221
268
|
|
|
222
269
|
export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" | "messages">): string {
|
|
@@ -278,6 +325,44 @@ function extractToolCallSummaries(messages: Message[] | undefined): ToolCallSumm
|
|
|
278
325
|
return summaries;
|
|
279
326
|
}
|
|
280
327
|
|
|
328
|
+
export function sumResultsUsage(results: SingleResult[]): Usage {
|
|
329
|
+
const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
330
|
+
for (const result of results) {
|
|
331
|
+
usage.input += result.usage.input;
|
|
332
|
+
usage.output += result.usage.output;
|
|
333
|
+
usage.cacheRead += result.usage.cacheRead;
|
|
334
|
+
usage.cacheWrite += result.usage.cacheWrite;
|
|
335
|
+
usage.cost += result.usage.cost;
|
|
336
|
+
usage.turns += result.usage.turns;
|
|
337
|
+
}
|
|
338
|
+
return usage;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function addNestedCost(total: NonNullable<Details["totalCost"]>, children: NestedRunSummary[] | undefined): void {
|
|
342
|
+
for (const child of children ?? []) {
|
|
343
|
+
if (child.totalCost) {
|
|
344
|
+
total.inputTokens += child.totalCost.inputTokens;
|
|
345
|
+
total.outputTokens += child.totalCost.outputTokens;
|
|
346
|
+
total.costUsd += child.totalCost.costUsd;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
addNestedCost(total, child.children);
|
|
350
|
+
for (const step of child.steps ?? []) addNestedCost(total, step.children);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Sum input tokens, output tokens, and cost across a set of SingleResults. */
|
|
355
|
+
export function sumResultsCost(results: SingleResult[]): NonNullable<Details["totalCost"]> {
|
|
356
|
+
const total = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
357
|
+
for (const result of results) {
|
|
358
|
+
total.inputTokens += result.usage.input;
|
|
359
|
+
total.outputTokens += result.usage.output;
|
|
360
|
+
total.costUsd += result.usage.cost;
|
|
361
|
+
addNestedCost(total, result.children);
|
|
362
|
+
}
|
|
363
|
+
return total;
|
|
364
|
+
}
|
|
365
|
+
|
|
281
366
|
export function compactForegroundResult(result: SingleResult): SingleResult {
|
|
282
367
|
if (result.progress?.status === "running") return result;
|
|
283
368
|
const toolCalls = result.toolCalls?.length ? result.toolCalls : extractToolCallSummaries(result.messages);
|