pi-subagents 0.45.1 → 0.46.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 +33 -0
- package/README.md +2 -0
- package/docs/agents.md +342 -0
- package/docs/configuration.md +320 -0
- package/docs/extension-api.md +308 -0
- package/docs/missions.md +117 -0
- package/docs/models.md +190 -0
- package/docs/observability.md +174 -0
- package/docs/tool-reference.md +343 -0
- package/docs/watchdog.md +176 -0
- package/docs/workflows.md +163 -0
- package/package.json +4 -2
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/src/agents/agents.ts +17 -8
- package/src/agents/frontmatter.ts +7 -3
- package/src/agents/skills.ts +2 -9
- package/src/api/project-panes.ts +30 -0
- package/src/extension/config.ts +15 -1
- package/src/extension/index.ts +36 -16
- package/src/extension/schemas.ts +3 -2
- package/src/extension/subagent-guide.ts +39 -0
- package/src/extension/tool-description.ts +4 -4
- package/src/inspectors/herdr/project-panes.ts +457 -62
- package/src/missions/actions.ts +25 -2
- package/src/missions/lifecycle.ts +21 -2
- package/src/missions/store.ts +77 -1
- package/src/missions/types.ts +33 -0
- package/src/runs/background/async-execution.ts +7 -1
- package/src/runs/background/completion-replay.ts +267 -0
- package/src/runs/background/result-watcher.ts +12 -4
- package/src/runs/background/resume-guidance.ts +33 -0
- package/src/runs/background/subagent-wait.ts +5 -2
- package/src/runs/background/wait-completions.ts +39 -5
- package/src/runs/background/wait-subscriptions.ts +21 -5
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +137 -0
- package/src/runs/foreground/subagent-executor.ts +326 -38
- package/src/runs/shared/subagent-prompt-runtime.ts +71 -5
- package/src/shared/fork-context.ts +13 -0
- package/src/shared/prompt-resources.ts +51 -0
- package/src/shared/types.ts +30 -1
- package/src/shared/utf8.ts +11 -0
- package/src/slash/prompt-workflows.ts +2 -15
- package/src/slash/slash-commands.ts +19 -1
- package/src/tui/fleet-status.ts +8 -2
- package/src/tui/fleet.ts +135 -25
- package/src/tui/render.ts +120 -7
- package/src/workflows/scripted-workflow.ts +187 -11
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
4
4
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
5
|
+
import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
|
|
5
6
|
|
|
6
7
|
function asNonEmptyString(value: unknown): string | undefined {
|
|
7
8
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -68,12 +69,34 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
68
69
|
* file is deleted after delivery, so this record is the only in-process source once
|
|
69
70
|
* the watcher has consumed it.
|
|
70
71
|
*/
|
|
71
|
-
export function recordWaitCompletion(
|
|
72
|
+
export function recordWaitCompletion(
|
|
73
|
+
state: SubagentState,
|
|
74
|
+
runId: string,
|
|
75
|
+
data: Record<string, unknown>,
|
|
76
|
+
now: number,
|
|
77
|
+
ttlMs: number,
|
|
78
|
+
persistence?: { resultsDir: string; sessionId: string },
|
|
79
|
+
): void {
|
|
72
80
|
const store = state.completedResults ??= new Map();
|
|
73
81
|
for (const [key, entry] of store) {
|
|
74
82
|
if (now - entry.seenAt > ttlMs) store.delete(key);
|
|
75
83
|
}
|
|
76
|
-
|
|
84
|
+
let completion = toWaitCompletion(data, runId);
|
|
85
|
+
if (persistence) {
|
|
86
|
+
try {
|
|
87
|
+
completion = writeCompletionReplay({
|
|
88
|
+
...persistence,
|
|
89
|
+
runId,
|
|
90
|
+
completion,
|
|
91
|
+
data,
|
|
92
|
+
now,
|
|
93
|
+
ttlMs,
|
|
94
|
+
}).completion;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Failed to persist completion replay for '${runId}':`, error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
store.set(runId, { seenAt: now, completion });
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
/**
|
|
@@ -102,10 +125,21 @@ export function collectWaitCompletions(terminal: AsyncRunSummary[], state: Subag
|
|
|
102
125
|
});
|
|
103
126
|
}
|
|
104
127
|
// The watcher may have consumed the file between the store check and the
|
|
105
|
-
// read
|
|
106
|
-
//
|
|
128
|
+
// read. Prefer its in-memory record, then the durable replay written before
|
|
129
|
+
// result cleanup so watcher reloads do not lose completion details.
|
|
107
130
|
const late = state.completedResults?.get(run.id);
|
|
108
|
-
if (late)
|
|
131
|
+
if (late) {
|
|
132
|
+
completions.push(late.completion);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const replay = readCompletionReplay(resultsDir, run.id, { sessionId: run.sessionId });
|
|
137
|
+
if (replay) completions.push(replay.completion);
|
|
138
|
+
} catch (replayError) {
|
|
139
|
+
throw new Error(`Failed to read completion replay for '${run.id}': ${errorMessage(replayError)}`, {
|
|
140
|
+
cause: replayError instanceof Error ? replayError : undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
109
143
|
}
|
|
110
144
|
}
|
|
111
145
|
return completions.length > 0 ? completions : undefined;
|
|
@@ -2,7 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
6
|
+
import { formatResumeFirstFailedRunDetail } from "./resume-guidance.ts";
|
|
7
|
+
import { readCompletionReplay } from "./completion-replay.ts";
|
|
6
8
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
9
|
import {
|
|
8
10
|
DIRS,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
SUBAGENT_FOREGROUND_COMPLETE_EVENT,
|
|
14
16
|
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
15
17
|
type SubagentState,
|
|
18
|
+
type WaitCompletion,
|
|
16
19
|
type WaitSubscriptionRecord,
|
|
17
20
|
} from "../../shared/types.ts";
|
|
18
21
|
|
|
@@ -61,7 +64,7 @@ function parseRecord(value: unknown): WaitSubscriptionRecord | undefined {
|
|
|
61
64
|
return record as WaitSubscriptionRecord;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
function needsAttention(run:
|
|
67
|
+
function needsAttention(run: AsyncRunSummary): boolean {
|
|
65
68
|
return run.activityState === "needs_attention" || run.steps.some((step) => step.activityState === "needs_attention");
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -103,7 +106,7 @@ export function createWaitSubscriptionManager(
|
|
|
103
106
|
unresolvedRestoredForegroundTokens.delete(record.token);
|
|
104
107
|
};
|
|
105
108
|
|
|
106
|
-
const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string) => {
|
|
109
|
+
const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string, completion?: WaitCompletion) => {
|
|
107
110
|
if (disposed || state.currentSessionId !== record.sessionId) return;
|
|
108
111
|
try {
|
|
109
112
|
remove(record);
|
|
@@ -116,7 +119,12 @@ export function createWaitSubscriptionManager(
|
|
|
116
119
|
customType: "subagent-wait-subscription",
|
|
117
120
|
content: `Wait subscription ${record.token} fired for run ${record.runId}: ${outcome}. ${detail}`,
|
|
118
121
|
display: true,
|
|
119
|
-
details: {
|
|
122
|
+
details: {
|
|
123
|
+
token: record.token,
|
|
124
|
+
runId: record.runId,
|
|
125
|
+
outcome,
|
|
126
|
+
...(completion ? { completions: [completion] } : {}),
|
|
127
|
+
},
|
|
120
128
|
}, { triggerTurn: true });
|
|
121
129
|
} catch (error) {
|
|
122
130
|
console.error(`Failed to deliver wait subscription '${record.token}' after clearing it:`, error);
|
|
@@ -166,7 +174,15 @@ export function createWaitSubscriptionManager(
|
|
|
166
174
|
return;
|
|
167
175
|
}
|
|
168
176
|
if (run.state !== "queued" && run.state !== "running") {
|
|
169
|
-
|
|
177
|
+
let completion: WaitCompletion | undefined;
|
|
178
|
+
try {
|
|
179
|
+
completion = readCompletionReplay(resultsDir, record.runId, { sessionId: record.sessionId, now: now() })?.completion;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Failed to read completion replay for wait subscription '${record.token}':`, error);
|
|
182
|
+
}
|
|
183
|
+
const detail = formatResumeFirstFailedRunDetail(run) ?? "Inspect the run status for its final output.";
|
|
184
|
+
const archiveDetail = completion?.archivePath ? ` Completion archive: ${completion.archivePath}.` : "";
|
|
185
|
+
settle(record, run.state === "complete" ? "completed" : run.state, `${detail}${archiveDetail}`, completion);
|
|
170
186
|
}
|
|
171
187
|
};
|
|
172
188
|
|
|
@@ -8,6 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
10
10
|
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
11
|
+
import { alignForkedSessionCwd } from "../../shared/fork-context.ts";
|
|
11
12
|
import {
|
|
12
13
|
ensureArtifactsDir,
|
|
13
14
|
formatOutputArtifactContent,
|
|
@@ -1416,6 +1417,9 @@ async function runSyncCompletion(
|
|
|
1416
1417
|
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
|
|
1417
1418
|
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
1418
1419
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
1420
|
+
if (options.context === "fork" && options.sessionFile && existsSync(options.sessionFile)) {
|
|
1421
|
+
alignForkedSessionCwd(options.sessionFile, options.cwd ?? runtimeCwd);
|
|
1422
|
+
}
|
|
1419
1423
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
1420
1424
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
1421
1425
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
|
|
4
|
+
import { DIRS } from "../../shared/types.ts";
|
|
5
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
|
+
import { utf8Tail } from "../../shared/utf8.ts";
|
|
7
|
+
|
|
8
|
+
export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
|
|
9
|
+
const HISTORY_VERSION = 1;
|
|
10
|
+
const MAX_INLINE_OUTPUT_BYTES = 64 * 1024;
|
|
11
|
+
|
|
12
|
+
interface ForegroundHistoryIndex {
|
|
13
|
+
version: 1;
|
|
14
|
+
runs: ForegroundResumeRun[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function historyPath(resultsDir: string): string {
|
|
18
|
+
return path.join(resultsDir, "foreground-history.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boundedTail(value: string): string {
|
|
22
|
+
return utf8Tail(value, MAX_INLINE_OUTPUT_BYTES).text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compactChild(child: ForegroundResumeChild): ForegroundResumeChild {
|
|
26
|
+
const outputPath = child.artifactPaths?.outputPath ?? child.savedOutputPath;
|
|
27
|
+
return {
|
|
28
|
+
agent: child.agent,
|
|
29
|
+
index: child.index,
|
|
30
|
+
...(child.context ? { context: child.context } : {}),
|
|
31
|
+
...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
|
|
32
|
+
...(child.model ? { model: child.model } : {}),
|
|
33
|
+
...(child.thinking ? { thinking: child.thinking } : {}),
|
|
34
|
+
status: child.status,
|
|
35
|
+
...(child.activityState ? { activityState: child.activityState } : {}),
|
|
36
|
+
...(child.lastActivityAt !== undefined ? { lastActivityAt: child.lastActivityAt } : {}),
|
|
37
|
+
...(child.currentTool ? { currentTool: child.currentTool } : {}),
|
|
38
|
+
...(child.currentToolStartedAt !== undefined ? { currentToolStartedAt: child.currentToolStartedAt } : {}),
|
|
39
|
+
...(child.currentPath ? { currentPath: child.currentPath } : {}),
|
|
40
|
+
...(child.turnCount !== undefined ? { turnCount: child.turnCount } : {}),
|
|
41
|
+
...(child.tokens !== undefined ? { tokens: child.tokens } : {}),
|
|
42
|
+
...(child.toolCount !== undefined ? { toolCount: child.toolCount } : {}),
|
|
43
|
+
...(child.exitCode !== undefined ? { exitCode: child.exitCode } : {}),
|
|
44
|
+
...(child.error ? { error: child.error } : {}),
|
|
45
|
+
...(!outputPath && child.finalOutput ? { finalOutput: boundedTail(child.finalOutput) } : {}),
|
|
46
|
+
...(child.outputState ? { outputState: child.outputState } : {}),
|
|
47
|
+
...(child.outputMode ? { outputMode: child.outputMode } : {}),
|
|
48
|
+
...(child.savedOutputPath ? { savedOutputPath: child.savedOutputPath } : {}),
|
|
49
|
+
...(child.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
|
|
50
|
+
...(child.artifactPaths ? { artifactPaths: child.artifactPaths } : {}),
|
|
51
|
+
...(child.transcriptPath ? { transcriptPath: child.transcriptPath } : {}),
|
|
52
|
+
...(child.transcriptError ? { transcriptError: child.transcriptError } : {}),
|
|
53
|
+
...(child.acceptance ? { acceptance: child.acceptance } : {}),
|
|
54
|
+
...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
|
|
55
|
+
...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
|
|
56
|
+
...(child.updatedAt !== undefined ? { updatedAt: child.updatedAt } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isRestorableForegroundStatus(status: unknown): status is ForegroundResumeChild["status"] {
|
|
61
|
+
return status === "completed" || status === "failed" || status === "paused" || status === "stopped";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compactRun(run: ForegroundResumeRun): ForegroundResumeRun | undefined {
|
|
65
|
+
if (!run.sessionId) return undefined;
|
|
66
|
+
if (run.children.length === 0 || !run.children.every((child) => isRestorableForegroundStatus(child.status))) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
runId: run.runId,
|
|
69
|
+
mode: run.mode,
|
|
70
|
+
cwd: run.cwd,
|
|
71
|
+
sessionId: run.sessionId,
|
|
72
|
+
updatedAt: run.updatedAt,
|
|
73
|
+
children: run.children.map(compactChild),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readIndex(resultsDir: string): ForegroundHistoryIndex {
|
|
78
|
+
const filePath = historyPath(resultsDir);
|
|
79
|
+
if (!fs.existsSync(filePath)) return { version: HISTORY_VERSION, runs: [] };
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { version: HISTORY_VERSION, runs: [] };
|
|
83
|
+
const record = parsed as Partial<ForegroundHistoryIndex>;
|
|
84
|
+
if (record.version !== HISTORY_VERSION || !Array.isArray(record.runs)) return { version: HISTORY_VERSION, runs: [] };
|
|
85
|
+
return { version: HISTORY_VERSION, runs: record.runs.filter(isRestorableRun) };
|
|
86
|
+
} catch {
|
|
87
|
+
return { version: HISTORY_VERSION, runs: [] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isRestorableRun(value: unknown): value is ForegroundResumeRun {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
93
|
+
const run = value as Partial<ForegroundResumeRun>;
|
|
94
|
+
return typeof run.runId === "string" && Boolean(run.runId)
|
|
95
|
+
&& (run.mode === "single" || run.mode === "parallel" || run.mode === "chain")
|
|
96
|
+
&& typeof run.cwd === "string" && Boolean(run.cwd)
|
|
97
|
+
&& typeof run.sessionId === "string" && Boolean(run.sessionId)
|
|
98
|
+
&& typeof run.updatedAt === "number" && Number.isFinite(run.updatedAt)
|
|
99
|
+
&& Array.isArray(run.children)
|
|
100
|
+
&& run.children.length > 0
|
|
101
|
+
&& run.children.every((child) => Boolean(child && typeof child === "object" && !Array.isArray(child)
|
|
102
|
+
&& typeof (child as Partial<ForegroundResumeChild>).agent === "string"
|
|
103
|
+
&& typeof (child as Partial<ForegroundResumeChild>).index === "number"
|
|
104
|
+
&& isRestorableForegroundStatus((child as Partial<ForegroundResumeChild>).status)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sortAndBound(runs: ForegroundResumeRun[], limit: number): ForegroundResumeRun[] {
|
|
108
|
+
return [...runs].sort((left, right) => right.updatedAt - left.updatedAt).slice(0, limit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function persistForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; limit?: number } = {}): void {
|
|
112
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
113
|
+
const limit = options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS;
|
|
114
|
+
const existing = readIndex(resultsDir);
|
|
115
|
+
const merged = new Map(existing.runs.map((run) => [run.runId, run]));
|
|
116
|
+
for (const run of state.foregroundRuns?.values() ?? []) {
|
|
117
|
+
const compact = compactRun(run);
|
|
118
|
+
if (compact) merged.set(compact.runId, compact);
|
|
119
|
+
}
|
|
120
|
+
const runs = sortAndBound([...merged.values()], limit);
|
|
121
|
+
writeAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function restoreForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; sessionId?: string | null; limit?: number } = {}): number {
|
|
125
|
+
const sessionId = options.sessionId ?? state.currentSessionId;
|
|
126
|
+
if (!sessionId) return 0;
|
|
127
|
+
const index = readIndex(options.resultsDir ?? DIRS.results);
|
|
128
|
+
const runs = sortAndBound(index.runs.filter((run) => run.sessionId === sessionId), options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
|
|
129
|
+
state.foregroundRuns ??= new Map();
|
|
130
|
+
let restored = 0;
|
|
131
|
+
for (const run of runs) {
|
|
132
|
+
if (state.foregroundRuns.has(run.runId)) continue;
|
|
133
|
+
state.foregroundRuns.set(run.runId, run);
|
|
134
|
+
restored += 1;
|
|
135
|
+
}
|
|
136
|
+
return restored;
|
|
137
|
+
}
|