@leing2021/super-pi 0.20.0 → 0.22.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/README.md +40 -19
- package/extensions/ce-core/index.ts +7 -136
- package/extensions/subagent/agent-management.ts +595 -0
- package/extensions/subagent/agent-manager-chain-detail.ts +162 -0
- package/extensions/subagent/agent-manager-detail.ts +231 -0
- package/extensions/subagent/agent-manager-edit.ts +390 -0
- package/extensions/subagent/agent-manager-list.ts +278 -0
- package/extensions/subagent/agent-manager-parallel.ts +304 -0
- package/extensions/subagent/agent-manager.ts +705 -0
- package/extensions/subagent/agent-scope.ts +8 -0
- package/extensions/subagent/agent-selection.ts +25 -0
- package/extensions/subagent/agent-serializer.ts +123 -0
- package/extensions/subagent/agent-templates.ts +62 -0
- package/extensions/subagent/agents/context-builder.md +37 -0
- package/extensions/subagent/agents/delegate.md +9 -0
- package/extensions/subagent/agents/oracle.md +73 -0
- package/extensions/subagent/agents/planner.md +52 -0
- package/extensions/subagent/agents/researcher.md +50 -0
- package/extensions/subagent/agents/reviewer.md +38 -0
- package/extensions/subagent/agents/scout.md +48 -0
- package/extensions/subagent/agents/worker.md +52 -0
- package/extensions/subagent/agents.ts +761 -0
- package/extensions/subagent/artifacts.ts +100 -0
- package/extensions/subagent/async-execution.ts +520 -0
- package/extensions/subagent/async-job-tracker.ts +216 -0
- package/extensions/subagent/async-status.ts +241 -0
- package/extensions/subagent/chain-clarify.ts +1364 -0
- package/extensions/subagent/chain-execution.ts +853 -0
- package/extensions/subagent/chain-serializer.ts +126 -0
- package/extensions/subagent/completion-dedupe.ts +65 -0
- package/extensions/subagent/doctor.ts +200 -0
- package/extensions/subagent/execution.ts +738 -0
- package/extensions/subagent/file-coalescer.ts +42 -0
- package/extensions/subagent/fork-context.ts +63 -0
- package/extensions/subagent/formatters.ts +122 -0
- package/extensions/subagent/frontmatter.ts +31 -0
- package/extensions/subagent/index.ts +585 -0
- package/extensions/subagent/intercom-bridge.ts +240 -0
- package/extensions/subagent/jsonl-writer.ts +83 -0
- package/extensions/subagent/model-fallback.ts +108 -0
- package/extensions/subagent/notify.ts +110 -0
- package/extensions/subagent/parallel-utils.ts +108 -0
- package/extensions/subagent/pi-args.ts +138 -0
- package/extensions/subagent/pi-spawn.ts +100 -0
- package/extensions/subagent/post-exit-stdio-guard.ts +87 -0
- package/extensions/subagent/prompt-template-bridge.ts +399 -0
- package/extensions/subagent/prompts/gather-context-and-clarify.md +13 -0
- package/extensions/subagent/prompts/parallel-cleanup.md +42 -0
- package/extensions/subagent/prompts/parallel-research.md +50 -0
- package/extensions/subagent/prompts/parallel-review.md +40 -0
- package/extensions/subagent/render-helpers.ts +82 -0
- package/extensions/subagent/render.ts +836 -0
- package/extensions/subagent/result-intercom.ts +237 -0
- package/extensions/subagent/result-watcher.ts +171 -0
- package/extensions/subagent/run-history.ts +57 -0
- package/extensions/subagent/run-status.ts +136 -0
- package/extensions/subagent/schemas.ts +164 -0
- package/extensions/subagent/session-tokens.ts +50 -0
- package/extensions/subagent/settings.ts +367 -0
- package/extensions/subagent/single-output.ts +97 -0
- package/extensions/subagent/skills.ts +626 -0
- package/extensions/subagent/slash-bridge.ts +176 -0
- package/extensions/subagent/slash-commands.ts +303 -0
- package/extensions/subagent/slash-live-state.ts +294 -0
- package/extensions/subagent/subagent-control.ts +150 -0
- package/extensions/subagent/subagent-executor.ts +1899 -0
- package/extensions/subagent/subagent-prompt-runtime.ts +75 -0
- package/extensions/subagent/subagent-runner.ts +1470 -0
- package/extensions/subagent/subagents-status.ts +472 -0
- package/extensions/subagent/text-editor.ts +272 -0
- package/extensions/subagent/top-level-async.ts +15 -0
- package/extensions/subagent/types.ts +623 -0
- package/extensions/subagent/utils.ts +456 -0
- package/extensions/subagent/worktree.ts +579 -0
- package/extensions/super-pi-extension/agents/ce-worker.md +1 -1
- package/extensions/super-pi-extension/index.ts +2 -55
- package/package.json +12 -5
- package/skills/03-work/SKILL.md +3 -3
- package/skills/pi-subagents/SKILL.md +566 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
|
|
2
|
+
// MIT License
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
type Details,
|
|
6
|
+
type IntercomEventBus,
|
|
7
|
+
type SingleResult,
|
|
8
|
+
type SubagentResultIntercomChild,
|
|
9
|
+
type SubagentResultIntercomPayload,
|
|
10
|
+
type SubagentResultStatus,
|
|
11
|
+
SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
|
|
12
|
+
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export function resolveSubagentResultStatus(input: {
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
success?: boolean;
|
|
18
|
+
state?: string;
|
|
19
|
+
interrupted?: boolean;
|
|
20
|
+
detached?: boolean;
|
|
21
|
+
}): SubagentResultStatus {
|
|
22
|
+
if (input.detached) return "detached";
|
|
23
|
+
if (input.interrupted || input.state === "paused") return "paused";
|
|
24
|
+
if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
|
|
25
|
+
if (input.state === "complete") return "completed";
|
|
26
|
+
if (input.state === "failed") return "failed";
|
|
27
|
+
if (typeof input.exitCode === "number") return input.exitCode === 0 ? "completed" : "failed";
|
|
28
|
+
return "failed";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function countStatuses(children: SubagentResultIntercomChild[]): Record<SubagentResultStatus, number> {
|
|
32
|
+
const counts: Record<SubagentResultStatus, number> = {
|
|
33
|
+
completed: 0,
|
|
34
|
+
failed: 0,
|
|
35
|
+
paused: 0,
|
|
36
|
+
detached: 0,
|
|
37
|
+
};
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
counts[child.status] += 1;
|
|
40
|
+
}
|
|
41
|
+
return counts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatStatusCounts(counts: Record<SubagentResultStatus, number>): string {
|
|
45
|
+
const parts = [
|
|
46
|
+
counts.completed ? `${counts.completed} completed` : undefined,
|
|
47
|
+
counts.failed ? `${counts.failed} failed` : undefined,
|
|
48
|
+
counts.paused ? `${counts.paused} paused` : undefined,
|
|
49
|
+
counts.detached ? `${counts.detached} detached` : undefined,
|
|
50
|
+
].filter((part): part is string => Boolean(part));
|
|
51
|
+
return parts.length ? parts.join(", ") : "0 results";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
|
|
55
|
+
const counts = countStatuses(children);
|
|
56
|
+
if (counts.failed > 0) return "failed";
|
|
57
|
+
if (counts.paused > 0) return "paused";
|
|
58
|
+
if (counts.completed > 0) return "completed";
|
|
59
|
+
if (counts.detached > 0) return "detached";
|
|
60
|
+
return "failed";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GroupedResultIntercomMessageInput {
|
|
64
|
+
to: string;
|
|
65
|
+
runId: string;
|
|
66
|
+
mode: "single" | "parallel" | "chain";
|
|
67
|
+
source: "foreground" | "async";
|
|
68
|
+
children: SubagentResultIntercomChild[];
|
|
69
|
+
asyncId?: string;
|
|
70
|
+
asyncDir?: string;
|
|
71
|
+
chainSteps?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatSubagentResultIntercomMessage(input: {
|
|
75
|
+
runId: string;
|
|
76
|
+
mode: "single" | "parallel" | "chain";
|
|
77
|
+
status: SubagentResultStatus;
|
|
78
|
+
children: SubagentResultIntercomChild[];
|
|
79
|
+
asyncId?: string;
|
|
80
|
+
asyncDir?: string;
|
|
81
|
+
chainSteps?: number;
|
|
82
|
+
}): string {
|
|
83
|
+
const counts = countStatuses(input.children);
|
|
84
|
+
const lines: string[] = [
|
|
85
|
+
"subagent results",
|
|
86
|
+
"",
|
|
87
|
+
`Run: ${input.runId}`,
|
|
88
|
+
`Mode: ${input.mode}`,
|
|
89
|
+
`Status: ${input.status}`,
|
|
90
|
+
`Children: ${formatStatusCounts(counts)}`,
|
|
91
|
+
];
|
|
92
|
+
if (input.mode === "chain" && typeof input.chainSteps === "number") {
|
|
93
|
+
lines.push(`Chain steps: ${input.chainSteps}`);
|
|
94
|
+
}
|
|
95
|
+
if (input.asyncId) lines.push(`Async id: ${input.asyncId}`);
|
|
96
|
+
if (input.asyncDir) lines.push(`Async dir: ${input.asyncDir}`);
|
|
97
|
+
if (input.children.some((child) => child.intercomTarget)) {
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("For clarification, message a listed subagent at its Intercom target.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < input.children.length; index++) {
|
|
103
|
+
const child = input.children[index]!;
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(`${index + 1}. ${child.agent} — ${child.status}`);
|
|
106
|
+
if (child.intercomTarget) lines.push(`Intercom target: ${child.intercomTarget}`);
|
|
107
|
+
if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
|
|
108
|
+
if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
|
|
109
|
+
lines.push("Summary:");
|
|
110
|
+
lines.push(child.summary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomMessageInput): SubagentResultIntercomPayload {
|
|
117
|
+
const children = input.children.map((child) => ({
|
|
118
|
+
...child,
|
|
119
|
+
summary: child.summary.trim() || "(no output)",
|
|
120
|
+
}));
|
|
121
|
+
const status = resolveGroupedStatus(children);
|
|
122
|
+
const summary = formatStatusCounts(countStatuses(children));
|
|
123
|
+
const firstChild = children[0];
|
|
124
|
+
const payload: SubagentResultIntercomPayload = {
|
|
125
|
+
to: input.to,
|
|
126
|
+
runId: input.runId,
|
|
127
|
+
mode: input.mode,
|
|
128
|
+
status,
|
|
129
|
+
summary,
|
|
130
|
+
source: input.source,
|
|
131
|
+
children,
|
|
132
|
+
...(input.asyncId ? { asyncId: input.asyncId } : {}),
|
|
133
|
+
...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
|
|
134
|
+
...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
|
|
135
|
+
...(firstChild?.agent ? { agent: firstChild.agent } : {}),
|
|
136
|
+
...(firstChild?.index !== undefined ? { index: firstChild.index } : {}),
|
|
137
|
+
...(firstChild?.artifactPath ? { artifactPath: firstChild.artifactPath } : {}),
|
|
138
|
+
...(firstChild?.sessionPath ? { sessionPath: firstChild.sessionPath } : {}),
|
|
139
|
+
message: "",
|
|
140
|
+
};
|
|
141
|
+
payload.message = formatSubagentResultIntercomMessage(payload);
|
|
142
|
+
return payload;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function deliverSubagentResultIntercomEvent(
|
|
146
|
+
events: IntercomEventBus,
|
|
147
|
+
payload: SubagentResultIntercomPayload,
|
|
148
|
+
timeoutMs = 500,
|
|
149
|
+
): Promise<boolean> {
|
|
150
|
+
if (typeof events.on !== "function" || typeof events.emit !== "function") return false;
|
|
151
|
+
const requestId = payload.requestId ?? randomUUID();
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let settled = false;
|
|
154
|
+
let unsubscribe: (() => void) | undefined;
|
|
155
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
156
|
+
const finish = (delivered: boolean) => {
|
|
157
|
+
if (settled) return;
|
|
158
|
+
settled = true;
|
|
159
|
+
if (timer) clearTimeout(timer);
|
|
160
|
+
unsubscribe?.();
|
|
161
|
+
resolve(delivered);
|
|
162
|
+
};
|
|
163
|
+
unsubscribe = events.on(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, (data) => {
|
|
164
|
+
if (!data || typeof data !== "object") return;
|
|
165
|
+
const delivery = data as { requestId?: unknown; delivered?: unknown };
|
|
166
|
+
if (delivery.requestId !== requestId) return;
|
|
167
|
+
finish(delivery.delivered === true);
|
|
168
|
+
});
|
|
169
|
+
timer = setTimeout(() => finish(false), timeoutMs);
|
|
170
|
+
try {
|
|
171
|
+
events.emit(SUBAGENT_RESULT_INTERCOM_EVENT, { ...payload, requestId });
|
|
172
|
+
} catch {
|
|
173
|
+
finish(false);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stripSingleResultOutputs(result: SingleResult): SingleResult {
|
|
179
|
+
return {
|
|
180
|
+
...result,
|
|
181
|
+
messages: undefined,
|
|
182
|
+
finalOutput: undefined,
|
|
183
|
+
truncation: undefined,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function stripDetailsOutputsForIntercomReceipt(details: Details): Details {
|
|
188
|
+
return {
|
|
189
|
+
...details,
|
|
190
|
+
results: details.results.map(stripSingleResultOutputs),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function formatSubagentResultReceipt(input: {
|
|
195
|
+
mode: "single" | "parallel" | "chain";
|
|
196
|
+
runId: string;
|
|
197
|
+
payload: SubagentResultIntercomPayload;
|
|
198
|
+
}): string {
|
|
199
|
+
const counts = countStatuses(input.payload.children);
|
|
200
|
+
const modeLabel = input.mode === "single"
|
|
201
|
+
? "single subagent result"
|
|
202
|
+
: input.mode === "parallel"
|
|
203
|
+
? "parallel subagent results"
|
|
204
|
+
: "chain subagent results";
|
|
205
|
+
const lines = [
|
|
206
|
+
`Delivered ${modeLabel} via intercom.`,
|
|
207
|
+
`Run: ${input.runId}`,
|
|
208
|
+
`Children: ${formatStatusCounts(counts)}`,
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const artifacts = input.payload.children.filter((child) => typeof child.artifactPath === "string");
|
|
212
|
+
if (artifacts.length > 0) {
|
|
213
|
+
lines.push("Artifacts:");
|
|
214
|
+
for (const child of artifacts) {
|
|
215
|
+
lines.push(`- ${child.agent} [${child.status}]: ${child.artifactPath}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const intercomTargets = input.payload.children.filter((child) => typeof child.intercomTarget === "string");
|
|
220
|
+
if (intercomTargets.length > 0) {
|
|
221
|
+
lines.push("Intercom targets:");
|
|
222
|
+
for (const child of intercomTargets) {
|
|
223
|
+
lines.push(`- ${child.agent} [${child.status}]: ${child.intercomTarget}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sessions = input.payload.children.filter((child) => typeof child.sessionPath === "string");
|
|
228
|
+
if (sessions.length > 0) {
|
|
229
|
+
lines.push("Sessions:");
|
|
230
|
+
for (const child of sessions) {
|
|
231
|
+
lines.push(`- ${child.agent} [${child.status}]: ${child.sessionPath}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lines.push("Full grouped output was sent over intercom.");
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
|
|
2
|
+
// MIT License
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
7
|
+
import { createFileCoalescer } from "./file-coalescer.ts";
|
|
8
|
+
import {
|
|
9
|
+
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
10
|
+
type SubagentState,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
import {
|
|
13
|
+
buildSubagentResultIntercomPayload,
|
|
14
|
+
deliverSubagentResultIntercomEvent,
|
|
15
|
+
resolveSubagentResultStatus,
|
|
16
|
+
} from "./result-intercom.ts";
|
|
17
|
+
|
|
18
|
+
function isNotFoundError(error: unknown): boolean {
|
|
19
|
+
return typeof error === "object"
|
|
20
|
+
&& error !== null
|
|
21
|
+
&& "code" in error
|
|
22
|
+
&& (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createResultWatcher(
|
|
26
|
+
pi: ExtensionAPI,
|
|
27
|
+
state: SubagentState,
|
|
28
|
+
resultsDir: string,
|
|
29
|
+
completionTtlMs: number,
|
|
30
|
+
): {
|
|
31
|
+
startResultWatcher: () => void;
|
|
32
|
+
primeExistingResults: () => void;
|
|
33
|
+
stopResultWatcher: () => void;
|
|
34
|
+
} {
|
|
35
|
+
const handleResult = async (file: string) => {
|
|
36
|
+
const resultPath = path.join(resultsDir, file);
|
|
37
|
+
if (!fs.existsSync(resultPath)) return;
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as {
|
|
40
|
+
id?: string;
|
|
41
|
+
runId?: string;
|
|
42
|
+
agent?: string;
|
|
43
|
+
success?: boolean;
|
|
44
|
+
state?: string;
|
|
45
|
+
mode?: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
results?: Array<{
|
|
48
|
+
agent?: string;
|
|
49
|
+
output?: string;
|
|
50
|
+
success?: boolean;
|
|
51
|
+
artifactPaths?: { outputPath?: string };
|
|
52
|
+
intercomTarget?: string;
|
|
53
|
+
}>;
|
|
54
|
+
sessionId?: string;
|
|
55
|
+
cwd?: string;
|
|
56
|
+
sessionFile?: string;
|
|
57
|
+
asyncDir?: string;
|
|
58
|
+
intercomTarget?: string;
|
|
59
|
+
};
|
|
60
|
+
if (data.sessionId && data.sessionId !== state.currentSessionId) return;
|
|
61
|
+
if (!data.sessionId && data.cwd && data.cwd !== state.baseCwd) return;
|
|
62
|
+
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
65
|
+
if (markSeenWithTtl(state.completionSeen, completionKey, now, completionTtlMs)) {
|
|
66
|
+
fs.unlinkSync(resultPath);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const intercomTarget = data.intercomTarget?.trim();
|
|
71
|
+
if (intercomTarget) {
|
|
72
|
+
const childResults = Array.isArray(data.results) && data.results.length > 0
|
|
73
|
+
? data.results
|
|
74
|
+
: [{
|
|
75
|
+
agent: data.agent,
|
|
76
|
+
output: data.summary,
|
|
77
|
+
success: data.success,
|
|
78
|
+
}];
|
|
79
|
+
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
80
|
+
const mode = data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
|
|
81
|
+
? data.mode
|
|
82
|
+
: childResults.length > 1 ? "chain" : "single";
|
|
83
|
+
const payload = buildSubagentResultIntercomPayload({
|
|
84
|
+
to: intercomTarget,
|
|
85
|
+
runId,
|
|
86
|
+
mode,
|
|
87
|
+
source: "async",
|
|
88
|
+
children: childResults.map((result = {}, index) => ({
|
|
89
|
+
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
90
|
+
status: resolveSubagentResultStatus({
|
|
91
|
+
success: result.success,
|
|
92
|
+
state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
|
|
93
|
+
}),
|
|
94
|
+
summary: result.output ?? data.summary ?? "(no output)",
|
|
95
|
+
index,
|
|
96
|
+
artifactPath: result.artifactPaths?.outputPath,
|
|
97
|
+
sessionPath: data.sessionFile,
|
|
98
|
+
intercomTarget: result.intercomTarget,
|
|
99
|
+
})),
|
|
100
|
+
asyncId: data.id,
|
|
101
|
+
asyncDir: data.asyncDir,
|
|
102
|
+
});
|
|
103
|
+
const delivered = await deliverSubagentResultIntercomEvent(pi.events, payload);
|
|
104
|
+
if (!delivered) {
|
|
105
|
+
console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, data);
|
|
110
|
+
fs.unlinkSync(resultPath);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (isNotFoundError(error)) return;
|
|
113
|
+
console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
state.resultFileCoalescer = createFileCoalescer((file) => {
|
|
118
|
+
void handleResult(file);
|
|
119
|
+
}, 50);
|
|
120
|
+
|
|
121
|
+
const scheduleRestart = () => {
|
|
122
|
+
state.watcherRestartTimer = setTimeout(() => {
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
125
|
+
startResultWatcher();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Failed to restart subagent result watcher for '${resultsDir}':`, error);
|
|
128
|
+
}
|
|
129
|
+
}, 3000);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const startResultWatcher = () => {
|
|
133
|
+
state.watcherRestartTimer = null;
|
|
134
|
+
try {
|
|
135
|
+
state.watcher = fs.watch(resultsDir, (ev, file) => {
|
|
136
|
+
if (ev !== "rename" || !file) return;
|
|
137
|
+
const fileName = file.toString();
|
|
138
|
+
if (!fileName.endsWith(".json")) return;
|
|
139
|
+
state.resultFileCoalescer.schedule(fileName);
|
|
140
|
+
});
|
|
141
|
+
state.watcher.on("error", (error) => {
|
|
142
|
+
console.error(`Subagent result watcher failed for '${resultsDir}':`, error);
|
|
143
|
+
state.watcher = null;
|
|
144
|
+
scheduleRestart();
|
|
145
|
+
});
|
|
146
|
+
state.watcher.unref?.();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Failed to start subagent result watcher for '${resultsDir}':`, error);
|
|
149
|
+
state.watcher = null;
|
|
150
|
+
scheduleRestart();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const primeExistingResults = () => {
|
|
155
|
+
fs.readdirSync(resultsDir)
|
|
156
|
+
.filter((f) => f.endsWith(".json"))
|
|
157
|
+
.forEach((file) => state.resultFileCoalescer.schedule(file, 0));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const stopResultWatcher = () => {
|
|
161
|
+
state.watcher?.close();
|
|
162
|
+
state.watcher = null;
|
|
163
|
+
if (state.watcherRestartTimer) {
|
|
164
|
+
clearTimeout(state.watcherRestartTimer);
|
|
165
|
+
}
|
|
166
|
+
state.watcherRestartTimer = null;
|
|
167
|
+
state.resultFileCoalescer.clear();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return { startResultWatcher, primeExistingResults, stopResultWatcher };
|
|
171
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
|
|
2
|
+
// MIT License
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
export interface RunEntry {
|
|
8
|
+
agent: string;
|
|
9
|
+
task: string;
|
|
10
|
+
ts: number;
|
|
11
|
+
status: "ok" | "error";
|
|
12
|
+
duration: number;
|
|
13
|
+
exit?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");
|
|
17
|
+
const ROTATE_READ_THRESHOLD = 1200;
|
|
18
|
+
const ROTATE_KEEP = 1000;
|
|
19
|
+
|
|
20
|
+
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
21
|
+
try {
|
|
22
|
+
const entry: RunEntry = {
|
|
23
|
+
agent,
|
|
24
|
+
task: task.slice(0, 200),
|
|
25
|
+
ts: Math.floor(Date.now() / 1000),
|
|
26
|
+
status: exitCode === 0 ? "ok" : "error",
|
|
27
|
+
duration: durationMs,
|
|
28
|
+
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
29
|
+
};
|
|
30
|
+
fs.mkdirSync(path.dirname(HISTORY_PATH), { recursive: true });
|
|
31
|
+
fs.appendFileSync(HISTORY_PATH, `${JSON.stringify(entry)}\n`);
|
|
32
|
+
} catch {
|
|
33
|
+
// Best-effort — never crash the execution flow for history recording
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
38
|
+
if (!fs.existsSync(HISTORY_PATH)) return [];
|
|
39
|
+
let raw: string;
|
|
40
|
+
try {
|
|
41
|
+
raw = fs.readFileSync(HISTORY_PATH, "utf-8");
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let lines = raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
47
|
+
|
|
48
|
+
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
49
|
+
lines = lines.slice(-ROTATE_KEEP);
|
|
50
|
+
try { fs.writeFileSync(HISTORY_PATH, `${lines.join("\n")}\n`, "utf-8"); } catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines
|
|
54
|
+
.map((line) => { try { return JSON.parse(line) as RunEntry; } catch { return undefined; } })
|
|
55
|
+
.filter((entry): entry is RunEntry => Boolean(entry) && entry.agent === agent)
|
|
56
|
+
.reverse();
|
|
57
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
|
|
2
|
+
// MIT License
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
6
|
+
import { formatAsyncRunList, listAsyncRuns } from "./async-status.ts";
|
|
7
|
+
import { ASYNC_DIR, RESULTS_DIR, type Details } from "./types.ts";
|
|
8
|
+
import { findByPrefix, readStatus } from "./utils.ts";
|
|
9
|
+
|
|
10
|
+
export interface RunStatusParams {
|
|
11
|
+
action?: "status";
|
|
12
|
+
id?: string;
|
|
13
|
+
runId?: string;
|
|
14
|
+
dir?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function activityText(activityState: unknown, lastActivityAt: unknown): string | undefined {
|
|
18
|
+
if (typeof lastActivityAt !== "number") return undefined;
|
|
19
|
+
const seconds = Math.floor(Math.max(0, Date.now() - lastActivityAt) / 1000);
|
|
20
|
+
return activityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function inspectSubagentStatus(params: RunStatusParams): AgentToolResult<Details> {
|
|
24
|
+
if (!params.id && !params.runId && !params.dir) {
|
|
25
|
+
try {
|
|
26
|
+
const runs = listAsyncRuns(ASYNC_DIR, { states: ["queued", "running"] });
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: formatAsyncRunList(runs) }],
|
|
29
|
+
details: { mode: "single", results: [] },
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: message }],
|
|
35
|
+
isError: true,
|
|
36
|
+
details: { mode: "single", results: [] },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let asyncDir: string | null = null;
|
|
42
|
+
let resolvedId = params.id ?? params.runId;
|
|
43
|
+
|
|
44
|
+
if (params.dir) {
|
|
45
|
+
asyncDir = path.resolve(params.dir);
|
|
46
|
+
} else if (resolvedId) {
|
|
47
|
+
const direct = path.join(ASYNC_DIR, resolvedId);
|
|
48
|
+
if (fs.existsSync(direct)) {
|
|
49
|
+
asyncDir = direct;
|
|
50
|
+
} else {
|
|
51
|
+
const match = findByPrefix(ASYNC_DIR, resolvedId);
|
|
52
|
+
if (match) {
|
|
53
|
+
asyncDir = match;
|
|
54
|
+
resolvedId = path.basename(match);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resultPath = resolvedId && !asyncDir ? findByPrefix(RESULTS_DIR, resolvedId, ".json") : null;
|
|
60
|
+
|
|
61
|
+
if (!asyncDir && !resultPath) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Async run not found. Provide id or dir." }],
|
|
64
|
+
isError: true,
|
|
65
|
+
details: { mode: "single", results: [] },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (asyncDir) {
|
|
70
|
+
let status;
|
|
71
|
+
try {
|
|
72
|
+
status = readStatus(asyncDir);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: message }],
|
|
77
|
+
isError: true,
|
|
78
|
+
details: { mode: "single", results: [] },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const logPath = path.join(asyncDir, `subagent-log-${resolvedId ?? "unknown"}.md`);
|
|
82
|
+
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
83
|
+
if (status) {
|
|
84
|
+
const stepsTotal = status.steps?.length ?? 1;
|
|
85
|
+
const current = status.currentStep !== undefined ? status.currentStep + 1 : undefined;
|
|
86
|
+
const stepLine = current !== undefined ? `Step: ${current}/${stepsTotal}` : `Steps: ${stepsTotal}`;
|
|
87
|
+
const started = new Date(status.startedAt).toISOString();
|
|
88
|
+
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
89
|
+
const statusActivityText = status.state === "running" ? activityText(status.activityState, status.lastActivityAt) : undefined;
|
|
90
|
+
|
|
91
|
+
const lines = [
|
|
92
|
+
`Run: ${status.runId}`,
|
|
93
|
+
`State: ${status.state}`,
|
|
94
|
+
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
95
|
+
`Mode: ${status.mode}`,
|
|
96
|
+
stepLine,
|
|
97
|
+
`Started: ${started}`,
|
|
98
|
+
`Updated: ${updated}`,
|
|
99
|
+
`Dir: ${asyncDir}`,
|
|
100
|
+
].filter((line): line is string => Boolean(line));
|
|
101
|
+
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
102
|
+
const stepActivityText = step.status === "running" ? activityText(step.activityState, step.lastActivityAt) : undefined;
|
|
103
|
+
lines.push(`Step ${index + 1}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
106
|
+
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
107
|
+
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
108
|
+
|
|
109
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (resultPath) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
116
|
+
const data = JSON.parse(raw) as { id?: string; success?: boolean; summary?: string; exitCode?: number; state?: string };
|
|
117
|
+
const status = data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
118
|
+
const lines = [`Run: ${data.id ?? resolvedId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
119
|
+
if (data.summary) lines.push("", data.summary);
|
|
120
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `Failed to read async result file: ${message}` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
details: { mode: "single", results: [] },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: "Status file not found." }],
|
|
133
|
+
isError: true,
|
|
134
|
+
details: { mode: "single", results: [] },
|
|
135
|
+
};
|
|
136
|
+
}
|