pi-workflow-engine 0.10.3 → 0.12.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/.pi/extensions/pi-workflow-engine/index.ts +418 -213
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +82 -207
- package/USAGE.md +295 -21
- package/package.json +9 -9
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
3
4
|
|
|
4
5
|
export interface BoundedProcessOptions {
|
|
5
6
|
readonly file: string;
|
|
6
7
|
readonly args: readonly string[];
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly env?: NodeJS.ProcessEnv;
|
|
10
|
+
readonly stdin?: string | Uint8Array;
|
|
9
11
|
readonly signal?: AbortSignal;
|
|
10
12
|
readonly timeoutMs: number;
|
|
11
13
|
readonly maxBufferBytes?: number;
|
|
@@ -16,81 +18,243 @@ export interface BoundedProcessOptions {
|
|
|
16
18
|
readonly exitError: (stderr: string, code: number | null, signal: NodeJS.Signals | null) => string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
readonly ok: boolean;
|
|
21
|
+
interface BoundedProcessResultBase {
|
|
21
22
|
readonly stdout: string;
|
|
22
23
|
readonly stderr: string;
|
|
23
24
|
readonly durationMs: number;
|
|
24
25
|
readonly bytes: number;
|
|
25
|
-
readonly error?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export
|
|
28
|
+
export interface UnconfirmedProcessTermination {
|
|
29
|
+
readonly status: "unconfirmed";
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type BoundedProcessFailure =
|
|
34
|
+
| { readonly kind: "spawn" | "abort" | "timeout" | "max-buffer"; readonly message: string }
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "exit";
|
|
37
|
+
readonly message: string;
|
|
38
|
+
readonly code: number | null;
|
|
39
|
+
readonly signal: NodeJS.Signals | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BoundedProcessResult =
|
|
43
|
+
| (BoundedProcessResultBase & { readonly ok: true; readonly error?: undefined; readonly failure?: undefined })
|
|
44
|
+
| (BoundedProcessResultBase & {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
/** Backward-compatible display message. Branch on `failure.kind` for control flow. */
|
|
47
|
+
readonly error: string;
|
|
48
|
+
readonly failure: BoundedProcessFailure;
|
|
49
|
+
/** Present only when the runner's final bound elapsed without observing child closure. */
|
|
50
|
+
readonly termination?: UnconfirmedProcessTermination;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type WindowsTaskkillResult =
|
|
54
|
+
| { readonly ok: true }
|
|
55
|
+
| { readonly ok: false; readonly reason: string };
|
|
56
|
+
|
|
57
|
+
export interface ProcessRunnerDependencies {
|
|
58
|
+
readonly platform: NodeJS.Platform;
|
|
59
|
+
readonly runWindowsTaskkill: (pid: number, complete: (result: WindowsTaskkillResult) => void) => void;
|
|
60
|
+
readonly killChild: (child: ChildProcess, signal: NodeJS.Signals) => boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NODE_PROCESS_RUNNER_DEPENDENCIES: ProcessRunnerDependencies = {
|
|
64
|
+
platform: process.platform,
|
|
65
|
+
runWindowsTaskkill(pid, complete) {
|
|
66
|
+
let completed = false;
|
|
67
|
+
const finish = (result: WindowsTaskkillResult) => {
|
|
68
|
+
if (completed) return;
|
|
69
|
+
completed = true;
|
|
70
|
+
complete(result);
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const killer = spawn("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
|
|
74
|
+
detached: false,
|
|
75
|
+
stdio: "ignore",
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
});
|
|
78
|
+
killer.once("error", (error) => finish({ ok: false, reason: `taskkill failed to start: ${unknownErrorMessage(error)}` }));
|
|
79
|
+
killer.once("close", (code, signal) => {
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
finish({ ok: true });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
finish({
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `taskkill exited with ${code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`}`,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
killer.unref();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
finish({ ok: false, reason: `taskkill could not be started: ${unknownErrorMessage(error)}` });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
killChild: (child, signal) => child.kill(signal),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export async function runBoundedProcess(
|
|
98
|
+
options: BoundedProcessOptions,
|
|
99
|
+
dependencies: ProcessRunnerDependencies = NODE_PROCESS_RUNNER_DEPENDENCIES,
|
|
100
|
+
): Promise<BoundedProcessResult> {
|
|
29
101
|
const start = performance.now();
|
|
30
102
|
let stdout = "";
|
|
31
103
|
let stderr = "";
|
|
32
104
|
let bytes = 0;
|
|
33
|
-
let
|
|
105
|
+
let pendingFailure: BoundedProcessFailure | undefined;
|
|
34
106
|
const child = spawn(options.file, [...options.args], {
|
|
35
107
|
cwd: options.cwd,
|
|
36
108
|
env: options.env ?? process.env,
|
|
37
|
-
|
|
109
|
+
detached: dependencies.platform !== "win32",
|
|
110
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
38
111
|
});
|
|
39
112
|
|
|
40
113
|
return await new Promise<BoundedProcessResult>((resolve) => {
|
|
41
114
|
let settled = false;
|
|
42
115
|
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
let hardSettleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
117
|
+
let directChildKillAttempted = false;
|
|
118
|
+
let windowsTerminationReason = "process close was not observed after taskkill and direct-child fallback";
|
|
43
119
|
const killGraceMs = Math.max(1, options.killGraceMs ?? 100);
|
|
44
|
-
const
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
123
|
+
if (hardSettleTimer) clearTimeout(hardSettleTimer);
|
|
124
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
125
|
+
};
|
|
126
|
+
const finish = (failure?: BoundedProcessFailure, termination?: UnconfirmedProcessTermination) => {
|
|
45
127
|
if (settled) return;
|
|
46
128
|
settled = true;
|
|
47
129
|
cleanup();
|
|
48
|
-
|
|
130
|
+
const base = { stdout, stderr, durationMs: performance.now() - start, bytes };
|
|
131
|
+
if (!failure) {
|
|
132
|
+
resolve({ ...base, ok: true });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
resolve({ ...base, ok: false, error: failure.message, failure, ...(termination ? { termination } : {}) });
|
|
49
136
|
};
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
137
|
+
const killDirectChild = (signal: NodeJS.Signals, reason: string) => {
|
|
138
|
+
if (directChildKillAttempted) return;
|
|
139
|
+
directChildKillAttempted = true;
|
|
140
|
+
windowsTerminationReason = reason;
|
|
141
|
+
try {
|
|
142
|
+
if (!dependencies.killChild(child, signal)) {
|
|
143
|
+
windowsTerminationReason = `${reason}; direct-child termination was not accepted`;
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
windowsTerminationReason = `${reason}; direct-child termination failed: ${unknownErrorMessage(error)}`;
|
|
147
|
+
}
|
|
57
148
|
};
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
149
|
+
const signalPosixProcess = (signal: NodeJS.Signals) => {
|
|
150
|
+
let groupSignalled = false;
|
|
151
|
+
if (child.pid !== undefined) {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(-child.pid, signal);
|
|
154
|
+
groupSignalled = true;
|
|
155
|
+
} catch {
|
|
156
|
+
// The process may have exited before its descendants released stdio.
|
|
157
|
+
// Fall back to the direct child below.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!groupSignalled) {
|
|
161
|
+
try {
|
|
162
|
+
dependencies.killChild(child, signal);
|
|
163
|
+
} catch {
|
|
164
|
+
// The hard-settlement timer remains authoritative when signalling fails.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
64
167
|
};
|
|
168
|
+
const destroyStdio = () => {
|
|
169
|
+
child.stdin?.destroy();
|
|
170
|
+
child.stdout?.destroy();
|
|
171
|
+
child.stderr?.destroy();
|
|
172
|
+
};
|
|
173
|
+
const terminate = (failure: BoundedProcessFailure) => {
|
|
174
|
+
pendingFailure ??= failure;
|
|
175
|
+
if (forceKillTimer) return;
|
|
176
|
+
if (dependencies.platform === "win32") {
|
|
177
|
+
if (child.pid === undefined) {
|
|
178
|
+
killDirectChild("SIGKILL", "taskkill was unavailable because the child process had no pid");
|
|
179
|
+
} else {
|
|
180
|
+
try {
|
|
181
|
+
dependencies.runWindowsTaskkill(child.pid, (result) => {
|
|
182
|
+
if (settled || result.ok) return;
|
|
183
|
+
killDirectChild("SIGKILL", result.reason);
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
killDirectChild("SIGKILL", `taskkill invocation failed: ${unknownErrorMessage(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
signalPosixProcess("SIGTERM");
|
|
191
|
+
}
|
|
192
|
+
forceKillTimer = setTimeout(() => {
|
|
193
|
+
if (dependencies.platform === "win32") {
|
|
194
|
+
if (directChildKillAttempted) {
|
|
195
|
+
windowsTerminationReason = `${windowsTerminationReason}; child did not close within the termination grace period`;
|
|
196
|
+
} else {
|
|
197
|
+
killDirectChild("SIGKILL", "taskkill did not close the child within the termination grace period");
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
signalPosixProcess("SIGKILL");
|
|
201
|
+
}
|
|
202
|
+
destroyStdio();
|
|
203
|
+
if (!settled) {
|
|
204
|
+
hardSettleTimer = setTimeout(() => {
|
|
205
|
+
if (!pendingFailure) return;
|
|
206
|
+
finish(
|
|
207
|
+
pendingFailure,
|
|
208
|
+
{ status: "unconfirmed", reason: dependencies.platform === "win32"
|
|
209
|
+
? windowsTerminationReason
|
|
210
|
+
: "process close was not observed after forced process-group termination" },
|
|
211
|
+
);
|
|
212
|
+
}, killGraceMs);
|
|
213
|
+
}
|
|
214
|
+
}, killGraceMs);
|
|
215
|
+
};
|
|
216
|
+
const onAbort = () => terminate({ kind: "abort", message: options.abortError });
|
|
217
|
+
const timeout = setTimeout(() => terminate({ kind: "timeout", message: options.timeoutError }), options.timeoutMs);
|
|
65
218
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
69
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
child.stdout?.on("data", (chunk: Buffer) => {
|
|
219
|
+
const captureChunk = (chunk: Buffer, append: (text: string) => void) => {
|
|
220
|
+
if (pendingFailure) return;
|
|
73
221
|
bytes += chunk.length;
|
|
74
222
|
if (options.maxBufferBytes !== undefined && bytes > options.maxBufferBytes) {
|
|
75
|
-
|
|
223
|
+
terminate({
|
|
224
|
+
kind: "max-buffer",
|
|
225
|
+
message: options.maxBufferError ?? `process output exceeded ${options.maxBufferBytes} bytes`,
|
|
226
|
+
});
|
|
76
227
|
return;
|
|
77
228
|
}
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
child.
|
|
81
|
-
|
|
229
|
+
append(chunk.toString("utf8"));
|
|
230
|
+
};
|
|
231
|
+
child.stdout?.on("data", (chunk: Buffer) => captureChunk(chunk, (text) => (stdout += text)));
|
|
232
|
+
child.stderr?.on("data", (chunk: Buffer) => captureChunk(chunk, (text) => (stderr += text)));
|
|
233
|
+
child.on("error", (spawnError) => {
|
|
234
|
+
if (!pendingFailure) finish({ kind: "spawn", message: spawnError.message });
|
|
82
235
|
});
|
|
83
|
-
child.on("error", (spawnError) => finish(false, spawnError.message));
|
|
84
236
|
child.on("close", (code, signal) => {
|
|
85
|
-
if (
|
|
86
|
-
finish(
|
|
237
|
+
if (pendingFailure) {
|
|
238
|
+
finish(pendingFailure);
|
|
87
239
|
return;
|
|
88
240
|
}
|
|
89
241
|
if (code === 0) {
|
|
90
|
-
finish(
|
|
242
|
+
finish();
|
|
91
243
|
return;
|
|
92
244
|
}
|
|
93
|
-
|
|
245
|
+
const message = options.exitError(stderr, code, signal);
|
|
246
|
+
finish({ kind: "exit", message, code, signal });
|
|
94
247
|
});
|
|
248
|
+
if (options.signal?.aborted) {
|
|
249
|
+
onAbort();
|
|
250
|
+
} else {
|
|
251
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
252
|
+
}
|
|
253
|
+
if (options.stdin !== undefined) {
|
|
254
|
+
// The child may reject input and exit before consuming it. Its process exit
|
|
255
|
+
// remains the authoritative result; avoid surfacing a secondary EPIPE.
|
|
256
|
+
child.stdin?.on("error", () => undefined);
|
|
257
|
+
child.stdin?.end(options.stdin);
|
|
258
|
+
}
|
|
95
259
|
});
|
|
96
260
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
2
|
+
|
|
3
|
+
export type AgentRowStatus = "queued" | "running" | "done" | "failed";
|
|
4
|
+
export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
|
|
5
|
+
|
|
6
|
+
export interface AgentRowSnapshot {
|
|
7
|
+
readonly id: number;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly status: AgentRowStatus;
|
|
10
|
+
readonly startedAt?: number;
|
|
11
|
+
readonly doneAt?: number;
|
|
12
|
+
readonly toolUses: number;
|
|
13
|
+
readonly lastTool?: string;
|
|
14
|
+
readonly error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PhaseSnapshot {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly agents: readonly AgentRowSnapshot[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkflowCounterSnapshot {
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly value: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkflowLaneItemSnapshot {
|
|
29
|
+
readonly lane: string;
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly subtitle?: string;
|
|
32
|
+
readonly status: WorkflowLaneItemStatus;
|
|
33
|
+
readonly details?: string;
|
|
34
|
+
readonly createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkflowProgressSnapshot {
|
|
38
|
+
readonly runId: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly startedAt: number;
|
|
41
|
+
readonly doneAt?: number;
|
|
42
|
+
readonly currentPhase: string;
|
|
43
|
+
readonly phases: readonly PhaseSnapshot[];
|
|
44
|
+
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
45
|
+
readonly summary: readonly [string, string | number][];
|
|
46
|
+
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
47
|
+
readonly laneOverflow: readonly [string, number][];
|
|
48
|
+
readonly logs: readonly string[];
|
|
49
|
+
readonly usage?: WorkflowUsageSnapshot;
|
|
50
|
+
}
|
|
@@ -1,54 +1,20 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
3
|
-
import type { WorkflowLaneItemStatus,
|
|
2
|
+
import type { WorkflowProgressEvent } from "./types.ts";
|
|
3
|
+
import type { AgentRowStatus, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
4
|
+
import { formatWorkflowUsageLine, type WorkflowUsageSnapshot } from "./usage.ts";
|
|
5
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
4
6
|
import { statusTextFromCounts, type WorkflowStatusCounts } from "./ui/workflow-format.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
readonly lastTool?: string;
|
|
17
|
-
readonly error?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface PhaseSnapshot {
|
|
21
|
-
readonly title: string;
|
|
22
|
-
readonly agents: readonly AgentRowSnapshot[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface WorkflowCounterSnapshot {
|
|
26
|
-
readonly key: string;
|
|
27
|
-
readonly label: string;
|
|
28
|
-
readonly value: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface WorkflowLaneItemSnapshot {
|
|
32
|
-
readonly lane: string;
|
|
33
|
-
readonly title: string;
|
|
34
|
-
readonly subtitle?: string;
|
|
35
|
-
readonly status: WorkflowLaneItemStatus;
|
|
36
|
-
readonly details?: string;
|
|
37
|
-
readonly createdAt: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface WorkflowProgressSnapshot {
|
|
41
|
-
readonly title: string;
|
|
42
|
-
readonly startedAt: number;
|
|
43
|
-
readonly doneAt?: number;
|
|
44
|
-
readonly currentPhase: string;
|
|
45
|
-
readonly phases: readonly PhaseSnapshot[];
|
|
46
|
-
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
47
|
-
readonly summary: readonly [string, string | number][];
|
|
48
|
-
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
49
|
-
readonly laneOverflow: readonly [string, number][];
|
|
50
|
-
readonly logs: readonly string[];
|
|
51
|
-
}
|
|
7
|
+
import { renderWorkflowWidgetLines } from "./ui/workflow-widget.ts";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
AgentRowSnapshot,
|
|
11
|
+
AgentRowStatus,
|
|
12
|
+
PhaseSnapshot,
|
|
13
|
+
WorkflowCounterSnapshot,
|
|
14
|
+
WorkflowLaneItemSnapshot,
|
|
15
|
+
WorkflowLaneItemStatus,
|
|
16
|
+
WorkflowProgressSnapshot,
|
|
17
|
+
} from "./progress-types.ts";
|
|
52
18
|
|
|
53
19
|
interface AgentRow {
|
|
54
20
|
id: number;
|
|
@@ -82,6 +48,7 @@ interface WorkflowLaneItem {
|
|
|
82
48
|
}
|
|
83
49
|
|
|
84
50
|
const LOG_LIMIT = 24;
|
|
51
|
+
const WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
85
52
|
export const DEFAULT_LANE_ITEM_LIMIT = 200;
|
|
86
53
|
|
|
87
54
|
/**
|
|
@@ -102,17 +69,18 @@ export class ProgressTracker {
|
|
|
102
69
|
private doneAt: number | undefined;
|
|
103
70
|
private currentPhase = "Workflow";
|
|
104
71
|
private nextAgentId = 1;
|
|
105
|
-
private widget: WorkflowWidget | undefined;
|
|
106
|
-
private widgetRegistered = false;
|
|
107
|
-
private tui: Pick<TUI, "requestRender"> | undefined;
|
|
108
|
-
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
109
72
|
private lastStatusText: string | undefined;
|
|
110
|
-
private
|
|
73
|
+
private usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
74
|
+
private widgetRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
75
|
+
private readonly surfaceKey: string;
|
|
111
76
|
|
|
112
77
|
constructor(
|
|
113
78
|
private readonly ctx: ExtensionContext,
|
|
114
79
|
private readonly title: string,
|
|
80
|
+
private readonly runId: string,
|
|
81
|
+
private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
|
|
115
82
|
) {
|
|
83
|
+
this.surfaceKey = `workflow:${runId}`;
|
|
116
84
|
this.ensurePhase(this.currentPhase);
|
|
117
85
|
}
|
|
118
86
|
|
|
@@ -129,14 +97,14 @@ export class ProgressTracker {
|
|
|
129
97
|
this.currentPhase = title;
|
|
130
98
|
this.ensurePhase(title);
|
|
131
99
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${title}\n`);
|
|
132
|
-
this.
|
|
100
|
+
this.publish();
|
|
133
101
|
}
|
|
134
102
|
|
|
135
103
|
log(message: string): void {
|
|
136
104
|
this.logs.push(message);
|
|
137
105
|
while (this.logs.length > LOG_LIMIT) this.logs.shift();
|
|
138
106
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${message}\n`);
|
|
139
|
-
this.
|
|
107
|
+
this.publish();
|
|
140
108
|
}
|
|
141
109
|
|
|
142
110
|
event(event: WorkflowProgressEvent): void {
|
|
@@ -171,7 +139,7 @@ export class ProgressTracker {
|
|
|
171
139
|
this.summary.set(event.key, event.value);
|
|
172
140
|
break;
|
|
173
141
|
}
|
|
174
|
-
this.
|
|
142
|
+
this.publish();
|
|
175
143
|
}
|
|
176
144
|
|
|
177
145
|
agentQueued(phase: string | undefined, label: string): number {
|
|
@@ -180,7 +148,7 @@ export class ProgressTracker {
|
|
|
180
148
|
this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
|
|
181
149
|
this.rowsById.set(id, row);
|
|
182
150
|
this.agentCounts.queued++;
|
|
183
|
-
this.
|
|
151
|
+
this.publish();
|
|
184
152
|
return id;
|
|
185
153
|
}
|
|
186
154
|
|
|
@@ -202,7 +170,7 @@ export class ProgressTracker {
|
|
|
202
170
|
this.rowsById.set(nextRow.id, nextRow);
|
|
203
171
|
this.agentCounts.running++;
|
|
204
172
|
}
|
|
205
|
-
this.
|
|
173
|
+
this.publish();
|
|
206
174
|
}
|
|
207
175
|
|
|
208
176
|
agentTool(label: string, tool: string, id?: number): void {
|
|
@@ -211,7 +179,7 @@ export class ProgressTracker {
|
|
|
211
179
|
row.lastTool = tool;
|
|
212
180
|
row.toolUses += 1;
|
|
213
181
|
}
|
|
214
|
-
this.
|
|
182
|
+
this.publish();
|
|
215
183
|
}
|
|
216
184
|
|
|
217
185
|
agentDone(label: string, id?: number): void {
|
|
@@ -220,7 +188,7 @@ export class ProgressTracker {
|
|
|
220
188
|
this.transitionAgentStatus(row, "done");
|
|
221
189
|
row.doneAt = Date.now();
|
|
222
190
|
}
|
|
223
|
-
this.
|
|
191
|
+
this.publish();
|
|
224
192
|
}
|
|
225
193
|
|
|
226
194
|
agentFailed(label: string, error: unknown, id?: number): void {
|
|
@@ -228,13 +196,19 @@ export class ProgressTracker {
|
|
|
228
196
|
if (row) {
|
|
229
197
|
this.transitionAgentStatus(row, "failed");
|
|
230
198
|
row.doneAt = Date.now();
|
|
231
|
-
row.error =
|
|
199
|
+
row.error = unknownErrorMessage(error);
|
|
232
200
|
}
|
|
233
|
-
this.
|
|
201
|
+
this.publish();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateUsage(snapshot: WorkflowUsageSnapshot): void {
|
|
205
|
+
this.usageSnapshot = snapshot;
|
|
206
|
+
this.publish();
|
|
234
207
|
}
|
|
235
208
|
|
|
236
209
|
snapshot(): WorkflowProgressSnapshot {
|
|
237
210
|
return {
|
|
211
|
+
runId: this.runId,
|
|
238
212
|
title: this.title,
|
|
239
213
|
startedAt: this.startedAt,
|
|
240
214
|
doneAt: this.doneAt,
|
|
@@ -248,6 +222,7 @@ export class ProgressTracker {
|
|
|
248
222
|
lanes: [...this.lanes.entries()].map(([lane, items]) => [lane, items.map((item) => ({ ...item }))]),
|
|
249
223
|
laneOverflow: [...this.laneOverflow.entries()],
|
|
250
224
|
logs: [...this.logs],
|
|
225
|
+
usage: this.usageSnapshot,
|
|
251
226
|
};
|
|
252
227
|
}
|
|
253
228
|
|
|
@@ -299,25 +274,35 @@ export class ProgressTracker {
|
|
|
299
274
|
return this.rowsById.get(id);
|
|
300
275
|
}
|
|
301
276
|
|
|
302
|
-
private
|
|
277
|
+
private publish(): void {
|
|
278
|
+
this.publishSnapshot();
|
|
303
279
|
if (!this.ctx.hasUI) return;
|
|
304
|
-
this.
|
|
305
|
-
this.
|
|
306
|
-
this.requestRenderSoon();
|
|
280
|
+
this.publishWidget();
|
|
281
|
+
this.startWidgetRefresh();
|
|
307
282
|
this.publishStatus();
|
|
308
283
|
}
|
|
309
284
|
|
|
310
|
-
private
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
285
|
+
private publishWidget(): void {
|
|
286
|
+
this.ctx.ui.setWidget(
|
|
287
|
+
this.surfaceKey,
|
|
288
|
+
renderWorkflowWidgetLines(this.snapshot(), this.ctx.ui.theme),
|
|
289
|
+
{ placement: "aboveEditor" },
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private startWidgetRefresh(): void {
|
|
294
|
+
if (this.widgetRefreshInterval !== undefined) return;
|
|
295
|
+
this.widgetRefreshInterval = setInterval(() => this.publishWidget(), WIDGET_REFRESH_INTERVAL_MS);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private stopWidgetRefresh(): void {
|
|
299
|
+
if (this.widgetRefreshInterval === undefined) return;
|
|
300
|
+
clearInterval(this.widgetRefreshInterval);
|
|
301
|
+
this.widgetRefreshInterval = undefined;
|
|
317
302
|
}
|
|
318
303
|
|
|
319
304
|
private publishStatus(): void {
|
|
320
|
-
const
|
|
305
|
+
const status = statusTextFromCounts(
|
|
321
306
|
{
|
|
322
307
|
title: this.title,
|
|
323
308
|
doneAt: this.doneAt,
|
|
@@ -327,55 +312,26 @@ export class ProgressTracker {
|
|
|
327
312
|
this.statusCountsSnapshot(),
|
|
328
313
|
this.ctx.ui.theme,
|
|
329
314
|
);
|
|
315
|
+
const usage = formatWorkflowUsageLine(this.usageSnapshot);
|
|
316
|
+
const next = usage ? `${status} · ${usage}` : status;
|
|
330
317
|
if (next === this.lastStatusText) return;
|
|
331
|
-
this.ctx.ui.setStatus(
|
|
318
|
+
this.ctx.ui.setStatus(this.surfaceKey, next);
|
|
332
319
|
this.lastStatusText = next;
|
|
333
320
|
}
|
|
334
321
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this.widget = createWorkflowWidget(() => this.snapshot());
|
|
338
|
-
this.ctx.ui.setWidget(
|
|
339
|
-
"workflow",
|
|
340
|
-
(tui, theme) => {
|
|
341
|
-
this.tui = tui;
|
|
342
|
-
return {
|
|
343
|
-
render: (width?: number) => this.widget?.render(width ?? tui.terminal.columns, theme) ?? [],
|
|
344
|
-
invalidate: () => this.widget?.invalidate(),
|
|
345
|
-
};
|
|
346
|
-
},
|
|
347
|
-
{ placement: "aboveEditor" },
|
|
348
|
-
);
|
|
349
|
-
this.widgetRegistered = true;
|
|
350
|
-
this.startWidgetTimer();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private startWidgetTimer(): void {
|
|
354
|
-
if (this.widgetInterval !== undefined) return;
|
|
355
|
-
this.widgetInterval = setInterval(() => {
|
|
356
|
-
this.widget?.nextFrame();
|
|
357
|
-
this.tui?.requestRender();
|
|
358
|
-
}, 100);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private stopWidgetTimer(): void {
|
|
362
|
-
if (this.widgetInterval === undefined) return;
|
|
363
|
-
clearInterval(this.widgetInterval);
|
|
364
|
-
this.widgetInterval = undefined;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Clear live workflow surfaces; optionally leave a one-line final status. */
|
|
368
|
-
done(status?: string): void {
|
|
322
|
+
/** Clear this run's live workflow surfaces. Final feedback is delivered by the result surface. */
|
|
323
|
+
done(): void {
|
|
369
324
|
this.doneAt = Date.now();
|
|
370
|
-
this.
|
|
325
|
+
this.stopWidgetRefresh();
|
|
326
|
+
this.publishSnapshot();
|
|
371
327
|
if (!this.ctx.hasUI) return;
|
|
372
|
-
this.ctx.ui.setWidget(
|
|
373
|
-
this.ctx.ui.setStatus(
|
|
374
|
-
this.lastStatusText =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.
|
|
328
|
+
this.ctx.ui.setWidget(this.surfaceKey, undefined);
|
|
329
|
+
this.ctx.ui.setStatus(this.surfaceKey, undefined);
|
|
330
|
+
this.lastStatusText = undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private publishSnapshot(): void {
|
|
334
|
+
this.onSnapshot?.(this.snapshot());
|
|
379
335
|
}
|
|
380
336
|
}
|
|
381
337
|
|
|
@@ -384,7 +340,3 @@ function laneItemLimitFromEnv(): number {
|
|
|
384
340
|
if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_LANE_ITEM_LIMIT;
|
|
385
341
|
return Math.trunc(parsed);
|
|
386
342
|
}
|
|
387
|
-
|
|
388
|
-
function errorMessage(error: unknown): string {
|
|
389
|
-
return error instanceof Error ? error.message : String(error);
|
|
390
|
-
}
|