@agwab/pi-workflow 0.2.1 → 0.3.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/dist/compiler.js +6 -8
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +112 -27
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +27 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.js +55 -11
- package/dist/subagent-backend.js +155 -29
- package/dist/types.d.ts +6 -0
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/src/compiler.ts +14 -9
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +145 -24
- package/src/extension.ts +33 -4
- package/src/index.ts +3 -1
- package/src/store.ts +74 -11
- package/src/subagent-backend.ts +201 -28
- package/src/types.ts +6 -0
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
|
@@ -1,364 +1,680 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
readRunEvents,
|
|
5
|
+
readRunRecord,
|
|
6
|
+
relativeRunEventsPath,
|
|
7
|
+
relativeRunRecordPath,
|
|
8
|
+
type ArtifactRef,
|
|
9
|
+
type CompletionMetadata,
|
|
10
|
+
type ResultEnvelope,
|
|
11
|
+
type ResultMetadata,
|
|
12
|
+
type RunAttemptRecord,
|
|
13
|
+
type RunEvent,
|
|
14
|
+
type RunRecord,
|
|
15
15
|
} from "../artifacts/index.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
STATUSES,
|
|
18
|
+
type AsyncDependency,
|
|
19
|
+
type ExecutionMode,
|
|
20
|
+
type FailureKind,
|
|
21
|
+
type ResolvedBackend,
|
|
22
|
+
type Status,
|
|
23
|
+
} from "../core/constants.ts";
|
|
24
|
+
import { resolveRunRef } from "./run-ref.ts";
|
|
17
25
|
|
|
18
26
|
const DEFAULT_RUNS_DIR = ".pi/agent/runs";
|
|
19
27
|
const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
20
28
|
const EVENT_TAIL_LIMIT = 20;
|
|
29
|
+
const CHILD_EVENT_SCAN_LIMIT = Number.POSITIVE_INFINITY;
|
|
21
30
|
|
|
22
31
|
export interface RunStatusRef {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
runId: string;
|
|
33
|
+
attemptId?: string;
|
|
34
|
+
/** @deprecated v1 compatibility only. */
|
|
35
|
+
taskId?: string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
runsDir?: string;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export interface RunLogRef extends ArtifactRef {
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
type: "stdout" | "stderr" | "output" | "result";
|
|
42
|
+
artifactCwd?: string;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export interface RunAttemptStatusSnapshot {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type RunTaskStatusSnapshot = RunAttemptStatusSnapshot & {
|
|
46
|
+
attemptId: string;
|
|
47
|
+
status: Status;
|
|
48
|
+
backend: ResolvedBackend | null;
|
|
49
|
+
failureKind: FailureKind | null;
|
|
50
|
+
startedAt: string;
|
|
51
|
+
completedAt: string | null;
|
|
52
|
+
heartbeatAt?: string;
|
|
53
|
+
resultPath: string | null;
|
|
54
|
+
outputPath: string | null;
|
|
55
|
+
stdoutPath: string | null;
|
|
56
|
+
stderrPath: string | null;
|
|
57
|
+
artifactCwd?: string;
|
|
58
|
+
pid?: number;
|
|
59
|
+
processGroupId?: number;
|
|
60
|
+
workerPid?: number;
|
|
61
|
+
workerProcessGroupId?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type RunTaskStatusSnapshot = RunAttemptStatusSnapshot & {
|
|
65
|
+
taskId?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface RunChildFailureSummary {
|
|
69
|
+
childRunId: string;
|
|
70
|
+
workflowRunId?: string;
|
|
71
|
+
taskId?: string;
|
|
72
|
+
status: Status;
|
|
73
|
+
failureKind: FailureKind | string | null;
|
|
74
|
+
message?: string;
|
|
75
|
+
timestamp: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RunChildSummary {
|
|
79
|
+
total: number;
|
|
80
|
+
pending: number;
|
|
81
|
+
running: number;
|
|
82
|
+
completed: number;
|
|
83
|
+
failed: number;
|
|
84
|
+
cancelled: number;
|
|
85
|
+
activeChildRunIds: string[];
|
|
86
|
+
latestFailure: RunChildFailureSummary | null;
|
|
87
|
+
worstDescendantStatus: Status | null;
|
|
88
|
+
}
|
|
56
89
|
|
|
57
90
|
export interface RunStatusSnapshot {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
runId: string;
|
|
92
|
+
attemptId: string;
|
|
93
|
+
/** @deprecated v1 compatibility only. */
|
|
94
|
+
taskId?: string;
|
|
95
|
+
correlationId?: string;
|
|
96
|
+
backend: ResolvedBackend;
|
|
97
|
+
status: Status;
|
|
98
|
+
failureKind: FailureKind | null;
|
|
99
|
+
startedAt: string;
|
|
100
|
+
completedAt: string | null;
|
|
101
|
+
durationMs: number | null;
|
|
102
|
+
logs: RunLogRef[];
|
|
103
|
+
resultPath: string | null;
|
|
104
|
+
completion?: CompletionMetadata;
|
|
105
|
+
metadata: ResultMetadata;
|
|
106
|
+
mode?: ExecutionMode;
|
|
107
|
+
dependency?: AsyncDependency | null;
|
|
108
|
+
registryPath?: string;
|
|
109
|
+
eventsPath?: string;
|
|
110
|
+
eventTail?: RunEvent[];
|
|
111
|
+
childSummary?: RunChildSummary;
|
|
112
|
+
attempts?: RunAttemptStatusSnapshot[];
|
|
113
|
+
/** @deprecated v1 compatibility only. */
|
|
114
|
+
tasks?: RunTaskStatusSnapshot[];
|
|
81
115
|
}
|
|
82
116
|
|
|
83
117
|
export interface RunLogsSnapshot extends RunStatusSnapshot {
|
|
84
|
-
|
|
118
|
+
logText: Partial<Record<RunLogRef["type"] | "events", string>>;
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
export interface WaitForRunOptions extends RunStatusRef {
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
pollIntervalMs?: number;
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
export interface WaitForRunResult {
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
status: "completed" | "timeout";
|
|
128
|
+
snapshot: RunStatusSnapshot | null;
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
function assertSafeId(name: string, value: string): void {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
if (!SAFE_ID_PATTERN.test(value)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${name} must contain only letters, numbers, dots, underscores, or dashes.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
function isInsideOrEqual(parent: string, child: string): boolean {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
const childRelative = relative(parent, child);
|
|
141
|
+
return (
|
|
142
|
+
childRelative === "" ||
|
|
143
|
+
(!childRelative.startsWith("..") && !isAbsolute(childRelative))
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pathsFor(ref: RunStatusRef): {
|
|
148
|
+
cwd: string;
|
|
149
|
+
runsDir: string;
|
|
150
|
+
runDir: string;
|
|
151
|
+
} {
|
|
152
|
+
assertSafeId("runId", ref.runId);
|
|
153
|
+
if (ref.attemptId !== undefined) assertSafeId("attemptId", ref.attemptId);
|
|
154
|
+
if (ref.taskId !== undefined) assertSafeId("taskId", ref.taskId);
|
|
155
|
+
const cwd = resolve(ref.cwd ?? process.cwd());
|
|
156
|
+
const runsDir = resolve(cwd, ref.runsDir ?? DEFAULT_RUNS_DIR);
|
|
157
|
+
if (!isInsideOrEqual(cwd, runsDir)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"runsDir must be inside cwd so lifecycle refs remain relative and safe.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { cwd, runsDir, runDir: join(runsDir, ref.runId) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function safeArtifactPath(
|
|
166
|
+
cwd: string,
|
|
167
|
+
artifact: Pick<RunLogRef, "path" | "artifactCwd">,
|
|
168
|
+
): string {
|
|
169
|
+
if (isAbsolute(artifact.path) || artifact.path.split("/").includes(".."))
|
|
170
|
+
throw new Error("artifact path must be a safe relative path.");
|
|
171
|
+
const artifactCwd = resolve(artifact.artifactCwd ?? cwd);
|
|
172
|
+
const path = resolve(artifactCwd, artifact.path.split("/").join(sep));
|
|
173
|
+
if (!isInsideOrEqual(artifactCwd, path))
|
|
174
|
+
throw new Error("artifact path must stay inside its artifact cwd.");
|
|
175
|
+
return path;
|
|
126
176
|
}
|
|
127
177
|
|
|
128
178
|
function sleep(ms: number): Promise<void> {
|
|
129
|
-
|
|
179
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
130
180
|
}
|
|
131
181
|
|
|
132
|
-
function artifactFromAttempt(
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
function artifactFromAttempt(
|
|
183
|
+
attempt: RunAttemptRecord,
|
|
184
|
+
type: RunLogRef["type"],
|
|
185
|
+
path: string | undefined,
|
|
186
|
+
): RunLogRef | null {
|
|
187
|
+
if (path === undefined) return null;
|
|
188
|
+
return { type, path, artifactCwd: attempt.artifactCwd };
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
function attemptSnapshot(attempt: RunAttemptRecord): RunAttemptStatusSnapshot {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
return {
|
|
193
|
+
attemptId: attempt.attemptId,
|
|
194
|
+
status: attempt.status,
|
|
195
|
+
backend: attempt.backend ?? null,
|
|
196
|
+
failureKind: attempt.failureKind,
|
|
197
|
+
startedAt: attempt.startedAt,
|
|
198
|
+
completedAt: attempt.completedAt,
|
|
199
|
+
...(attempt.heartbeatAt === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { heartbeatAt: attempt.heartbeatAt }),
|
|
202
|
+
resultPath: attempt.resultPath ?? null,
|
|
203
|
+
outputPath: attempt.outputPath ?? null,
|
|
204
|
+
stdoutPath: attempt.stdoutPath ?? null,
|
|
205
|
+
stderrPath: attempt.stderrPath ?? null,
|
|
206
|
+
...(attempt.artifactCwd === undefined
|
|
207
|
+
? {}
|
|
208
|
+
: { artifactCwd: attempt.artifactCwd }),
|
|
209
|
+
...(attempt.process?.pid === undefined ? {} : { pid: attempt.process.pid }),
|
|
210
|
+
...(attempt.process?.processGroupId === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { processGroupId: attempt.process.processGroupId }),
|
|
213
|
+
...(attempt.process?.workerPid === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { workerPid: attempt.process.workerPid }),
|
|
216
|
+
...(attempt.process?.workerProcessGroupId === undefined
|
|
217
|
+
? {}
|
|
218
|
+
: { workerProcessGroupId: attempt.process.workerProcessGroupId }),
|
|
219
|
+
};
|
|
156
220
|
}
|
|
157
221
|
|
|
158
222
|
function resultLogs(result: ResultEnvelope): RunLogRef[] {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
return result.artifacts
|
|
224
|
+
.filter(
|
|
225
|
+
(artifact): artifact is RunLogRef =>
|
|
226
|
+
artifact.type === "stdout" ||
|
|
227
|
+
artifact.type === "stderr" ||
|
|
228
|
+
artifact.type === "output" ||
|
|
229
|
+
artifact.type === "result",
|
|
230
|
+
)
|
|
231
|
+
.map((artifact) => ({ ...artifact, artifactCwd: result.cwd }));
|
|
162
232
|
}
|
|
163
233
|
|
|
164
234
|
export function isStatus(value: unknown): value is Status {
|
|
165
|
-
|
|
235
|
+
return (
|
|
236
|
+
typeof value === "string" && (STATUSES as readonly string[]).includes(value)
|
|
237
|
+
);
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
export function isTerminalStatus(status: Status): boolean {
|
|
169
|
-
|
|
241
|
+
return (
|
|
242
|
+
status === "completed" || status === "failed" || status === "cancelled"
|
|
243
|
+
);
|
|
170
244
|
}
|
|
171
245
|
|
|
172
246
|
export function statusSucceeded(status: Status): boolean {
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function statusFailedClosed(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
247
|
+
return status === "completed";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function statusFailedClosed(
|
|
251
|
+
status: Status,
|
|
252
|
+
failureKind: FailureKind | null,
|
|
253
|
+
): boolean {
|
|
254
|
+
return (
|
|
255
|
+
(status === "failed" || status === "cancelled") && failureKind !== null
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function dataString(
|
|
260
|
+
data: Record<string, unknown> | undefined,
|
|
261
|
+
key: string,
|
|
262
|
+
): string | undefined {
|
|
263
|
+
const value = data?.[key];
|
|
264
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function statusFromChildEvent(event: RunEvent): Status | undefined {
|
|
268
|
+
if (isStatus(event.status)) return event.status;
|
|
269
|
+
if (event.type === "child.started" || event.type === "child.updated")
|
|
270
|
+
return "running";
|
|
271
|
+
if (event.type === "child.completed") return "completed";
|
|
272
|
+
if (event.type === "child.failed") return "failed";
|
|
273
|
+
if (event.type === "child.cancelled") return "cancelled";
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function summarizeChildEvents(
|
|
278
|
+
events: readonly RunEvent[],
|
|
279
|
+
): RunChildSummary | undefined {
|
|
280
|
+
const children = new Map<
|
|
281
|
+
string,
|
|
282
|
+
{
|
|
283
|
+
status: Status;
|
|
284
|
+
failureKind: FailureKind | string | null;
|
|
285
|
+
workflowRunId?: string;
|
|
286
|
+
taskId?: string;
|
|
287
|
+
message?: string;
|
|
288
|
+
timestamp: string;
|
|
289
|
+
}
|
|
290
|
+
>();
|
|
291
|
+
let latestFailure: RunChildFailureSummary | null = null;
|
|
292
|
+
let anonymous = 0;
|
|
293
|
+
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
if (!event.type.startsWith("child.")) continue;
|
|
296
|
+
const status = statusFromChildEvent(event);
|
|
297
|
+
if (status === undefined) continue;
|
|
298
|
+
const data = event.data;
|
|
299
|
+
const childRunId =
|
|
300
|
+
dataString(data, "childRunId") ??
|
|
301
|
+
dataString(data, "childId") ??
|
|
302
|
+
dataString(data, "descendantRunId") ??
|
|
303
|
+
`anonymous-child-${anonymous++}`;
|
|
304
|
+
const failureKind = dataString(data, "failureKind") ?? null;
|
|
305
|
+
const child = {
|
|
306
|
+
status,
|
|
307
|
+
failureKind,
|
|
308
|
+
...(dataString(data, "workflowRunId") === undefined
|
|
309
|
+
? {}
|
|
310
|
+
: { workflowRunId: dataString(data, "workflowRunId") }),
|
|
311
|
+
...(dataString(data, "taskId") === undefined
|
|
312
|
+
? {}
|
|
313
|
+
: { taskId: dataString(data, "taskId") }),
|
|
314
|
+
...(event.message === undefined ? {} : { message: event.message }),
|
|
315
|
+
timestamp: event.timestamp,
|
|
316
|
+
};
|
|
317
|
+
children.set(childRunId, child);
|
|
318
|
+
if (status === "failed" || status === "cancelled") {
|
|
319
|
+
latestFailure = {
|
|
320
|
+
childRunId,
|
|
321
|
+
...(child.workflowRunId === undefined
|
|
322
|
+
? {}
|
|
323
|
+
: { workflowRunId: child.workflowRunId }),
|
|
324
|
+
...(child.taskId === undefined ? {} : { taskId: child.taskId }),
|
|
325
|
+
status,
|
|
326
|
+
failureKind,
|
|
327
|
+
...(event.message === undefined ? {} : { message: event.message }),
|
|
328
|
+
timestamp: event.timestamp,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (children.size === 0) return undefined;
|
|
334
|
+
let pending = 0;
|
|
335
|
+
let running = 0;
|
|
336
|
+
let completed = 0;
|
|
337
|
+
let failed = 0;
|
|
338
|
+
let cancelled = 0;
|
|
339
|
+
const activeChildRunIds: string[] = [];
|
|
340
|
+
for (const [childRunId, child] of children) {
|
|
341
|
+
if (child.status === "pending") pending += 1;
|
|
342
|
+
else if (child.status === "running") running += 1;
|
|
343
|
+
else if (child.status === "completed") completed += 1;
|
|
344
|
+
else if (child.status === "failed") failed += 1;
|
|
345
|
+
else if (child.status === "cancelled") cancelled += 1;
|
|
346
|
+
if (child.status === "pending" || child.status === "running")
|
|
347
|
+
activeChildRunIds.push(childRunId);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
total: children.size,
|
|
351
|
+
pending,
|
|
352
|
+
running,
|
|
353
|
+
completed,
|
|
354
|
+
failed,
|
|
355
|
+
cancelled,
|
|
356
|
+
activeChildRunIds,
|
|
357
|
+
latestFailure,
|
|
358
|
+
worstDescendantStatus:
|
|
359
|
+
failed > 0
|
|
360
|
+
? "failed"
|
|
361
|
+
: cancelled > 0
|
|
362
|
+
? "cancelled"
|
|
363
|
+
: running > 0
|
|
364
|
+
? "running"
|
|
365
|
+
: pending > 0
|
|
366
|
+
? "pending"
|
|
367
|
+
: children.size > 0
|
|
368
|
+
? "completed"
|
|
369
|
+
: null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function withChildSummary<T extends RunStatusSnapshot>(
|
|
374
|
+
snapshot: T,
|
|
375
|
+
childSummary: RunChildSummary | undefined,
|
|
376
|
+
): T {
|
|
377
|
+
return childSummary === undefined ? snapshot : { ...snapshot, childSummary };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function createRunStatusSnapshot(
|
|
381
|
+
result: ResultEnvelope,
|
|
382
|
+
): RunStatusSnapshot {
|
|
383
|
+
const logs = resultLogs(result);
|
|
384
|
+
const resultArtifact =
|
|
385
|
+
logs.find((artifact) => artifact.type === "result") ?? null;
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
runId: result.runId,
|
|
389
|
+
attemptId: result.attemptId,
|
|
390
|
+
...(result.taskId === undefined ? {} : { taskId: result.taskId }),
|
|
391
|
+
...(result.correlationId === undefined
|
|
392
|
+
? {}
|
|
393
|
+
: { correlationId: result.correlationId }),
|
|
394
|
+
backend: result.backend,
|
|
395
|
+
status: result.status,
|
|
396
|
+
failureKind: result.failureKind,
|
|
397
|
+
startedAt: result.startedAt,
|
|
398
|
+
completedAt: result.completedAt,
|
|
399
|
+
durationMs: result.durationMs,
|
|
400
|
+
logs,
|
|
401
|
+
resultPath: resultArtifact?.path ?? null,
|
|
402
|
+
metadata: result.metadata,
|
|
403
|
+
...(result.completion === undefined
|
|
404
|
+
? {}
|
|
405
|
+
: { completion: result.completion }),
|
|
406
|
+
};
|
|
200
407
|
}
|
|
201
408
|
|
|
202
409
|
async function readJsonFile(path: string): Promise<unknown | null> {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
410
|
+
try {
|
|
411
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (
|
|
414
|
+
error &&
|
|
415
|
+
typeof error === "object" &&
|
|
416
|
+
"code" in error &&
|
|
417
|
+
error.code === "ENOENT"
|
|
418
|
+
)
|
|
419
|
+
return null;
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
209
422
|
}
|
|
210
423
|
|
|
211
424
|
function coerceResultEnvelope(value: unknown): ResultEnvelope | null {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
425
|
+
if (typeof value !== "object" || value === null) return null;
|
|
426
|
+
const raw = value as Partial<ResultEnvelope> & { taskId?: string };
|
|
427
|
+
if (
|
|
428
|
+
typeof raw.runId !== "string" ||
|
|
429
|
+
typeof raw.backend !== "string" ||
|
|
430
|
+
typeof raw.status !== "string" ||
|
|
431
|
+
typeof raw.cwd !== "string"
|
|
432
|
+
)
|
|
433
|
+
return null;
|
|
434
|
+
const attemptId =
|
|
435
|
+
typeof raw.attemptId === "string"
|
|
436
|
+
? raw.attemptId
|
|
437
|
+
: typeof raw.taskId === "string"
|
|
438
|
+
? raw.taskId
|
|
439
|
+
: "task-1";
|
|
440
|
+
return {
|
|
441
|
+
schemaVersion: 2,
|
|
442
|
+
runId: raw.runId,
|
|
443
|
+
attemptId,
|
|
444
|
+
...(raw.taskId === undefined ? {} : { taskId: raw.taskId }),
|
|
445
|
+
...(raw.correlationId === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { correlationId: raw.correlationId }),
|
|
448
|
+
backend: raw.backend,
|
|
449
|
+
status: raw.status,
|
|
450
|
+
failureKind: raw.failureKind ?? null,
|
|
451
|
+
cwd: raw.cwd,
|
|
452
|
+
startedAt: raw.startedAt ?? new Date(0).toISOString(),
|
|
453
|
+
completedAt: raw.completedAt ?? null,
|
|
454
|
+
durationMs: raw.durationMs ?? null,
|
|
455
|
+
workspace: raw.workspace ?? {
|
|
456
|
+
mode: "shared",
|
|
457
|
+
cwd: raw.cwd,
|
|
458
|
+
worktreePath: null,
|
|
459
|
+
},
|
|
460
|
+
sandbox: raw.sandbox ?? { enabled: false },
|
|
461
|
+
exitCode: raw.exitCode ?? null,
|
|
462
|
+
signal: raw.signal ?? null,
|
|
463
|
+
artifacts: Array.isArray(raw.artifacts) ? raw.artifacts : [],
|
|
464
|
+
metadata: raw.metadata ?? { contextLengthExceeded: false },
|
|
465
|
+
...(raw.tmux === undefined ? {} : { tmux: raw.tmux }),
|
|
466
|
+
...(raw.completion === undefined ? {} : { completion: raw.completion }),
|
|
467
|
+
} as ResultEnvelope;
|
|
238
468
|
}
|
|
239
469
|
|
|
240
470
|
async function readResultFile(path: string): Promise<ResultEnvelope | null> {
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function readRunResultFromAttempt(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
471
|
+
return coerceResultEnvelope(await readJsonFile(path));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function readRunResultFromAttempt(
|
|
475
|
+
attempt: RunAttemptRecord | undefined,
|
|
476
|
+
): Promise<ResultEnvelope | null> {
|
|
477
|
+
if (attempt?.resultPath === undefined || attempt.artifactCwd === undefined)
|
|
478
|
+
return null;
|
|
479
|
+
return await readResultFile(
|
|
480
|
+
safeArtifactPath(attempt.artifactCwd, { path: attempt.resultPath }),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function readRunResultFallback(
|
|
485
|
+
ref: RunStatusRef,
|
|
486
|
+
): Promise<ResultEnvelope | null> {
|
|
487
|
+
const { runDir } = pathsFor(ref);
|
|
488
|
+
const attemptId = ref.attemptId ?? ref.taskId;
|
|
489
|
+
if (attemptId !== undefined) {
|
|
490
|
+
return (
|
|
491
|
+
(await readResultFile(
|
|
492
|
+
join(runDir, "attempts", attemptId, "result.json"),
|
|
493
|
+
)) ?? (await readResultFile(join(runDir, attemptId, "result.json")))
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
const attemptsDir = join(runDir, "attempts");
|
|
497
|
+
const entries = await readdir(attemptsDir, { withFileTypes: true }).catch(
|
|
498
|
+
() => [],
|
|
499
|
+
);
|
|
500
|
+
const attemptDirs = entries
|
|
501
|
+
.filter((entry) => entry.isDirectory())
|
|
502
|
+
.map((entry) => entry.name)
|
|
503
|
+
.sort();
|
|
504
|
+
const latest = attemptDirs.at(-1);
|
|
505
|
+
if (latest !== undefined)
|
|
506
|
+
return await readResultFile(join(attemptsDir, latest, "result.json"));
|
|
507
|
+
// v1 fallback
|
|
508
|
+
return await readResultFile(
|
|
509
|
+
join(runDir, ref.taskId ?? "task-1", "result.json"),
|
|
510
|
+
);
|
|
263
511
|
}
|
|
264
512
|
|
|
265
513
|
function recordLogs(attempt: RunAttemptRecord): RunLogRef[] {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function selectedAttempt(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export async function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
514
|
+
return [
|
|
515
|
+
artifactFromAttempt(attempt, "stdout", attempt.stdoutPath),
|
|
516
|
+
artifactFromAttempt(attempt, "stderr", attempt.stderrPath),
|
|
517
|
+
artifactFromAttempt(attempt, "output", attempt.outputPath),
|
|
518
|
+
artifactFromAttempt(attempt, "result", attempt.resultPath),
|
|
519
|
+
].filter((artifact): artifact is RunLogRef => artifact !== null);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function selectedAttempt(
|
|
523
|
+
record: RunRecord,
|
|
524
|
+
ref: RunStatusRef,
|
|
525
|
+
): RunAttemptRecord | undefined {
|
|
526
|
+
const requested =
|
|
527
|
+
ref.attemptId ??
|
|
528
|
+
ref.taskId ??
|
|
529
|
+
record.latestAttemptId ??
|
|
530
|
+
record.activeAttemptId ??
|
|
531
|
+
undefined;
|
|
532
|
+
return requested === undefined
|
|
533
|
+
? record.attempts.at(-1)
|
|
534
|
+
: (record.attempts.find((attempt) => attempt.attemptId === requested) ??
|
|
535
|
+
record.attempts.at(-1));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function snapshotFromRecord(
|
|
539
|
+
record: RunRecord,
|
|
540
|
+
ref: RunStatusRef,
|
|
541
|
+
events: RunEvent[],
|
|
542
|
+
): RunStatusSnapshot {
|
|
543
|
+
const attempt = selectedAttempt(record, ref);
|
|
544
|
+
return {
|
|
545
|
+
runId: record.runId,
|
|
546
|
+
attemptId:
|
|
547
|
+
attempt?.attemptId ??
|
|
548
|
+
ref.attemptId ??
|
|
549
|
+
ref.taskId ??
|
|
550
|
+
record.latestAttemptId ??
|
|
551
|
+
"unknown",
|
|
552
|
+
correlationId: record.correlationId,
|
|
553
|
+
backend: attempt?.backend ?? record.backend ?? "headless",
|
|
554
|
+
status: record.status,
|
|
555
|
+
failureKind: record.failureKind,
|
|
556
|
+
startedAt: attempt?.startedAt ?? record.startedAt,
|
|
557
|
+
completedAt: record.completedAt,
|
|
558
|
+
durationMs: null,
|
|
559
|
+
logs: attempt === undefined ? [] : recordLogs(attempt),
|
|
560
|
+
resultPath: attempt?.resultPath ?? null,
|
|
561
|
+
metadata: { contextLengthExceeded: false },
|
|
562
|
+
mode: record.mode,
|
|
563
|
+
dependency: record.dependency,
|
|
564
|
+
registryPath: relativeRunRecordPath(ref),
|
|
565
|
+
eventsPath: relativeRunEventsPath(ref),
|
|
566
|
+
eventTail: events,
|
|
567
|
+
attempts: record.attempts.map(attemptSnapshot),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function mergeRecordSnapshot(
|
|
572
|
+
snapshot: RunStatusSnapshot,
|
|
573
|
+
record: RunRecord,
|
|
574
|
+
ref: RunStatusRef,
|
|
575
|
+
events: RunEvent[],
|
|
576
|
+
): RunStatusSnapshot {
|
|
577
|
+
const attempt = record.attempts.find(
|
|
578
|
+
(candidate) => candidate.attemptId === snapshot.attemptId,
|
|
579
|
+
);
|
|
580
|
+
return {
|
|
581
|
+
...snapshot,
|
|
582
|
+
correlationId: snapshot.correlationId ?? record.correlationId,
|
|
583
|
+
status: record.status,
|
|
584
|
+
failureKind: record.failureKind,
|
|
585
|
+
completedAt: record.completedAt,
|
|
586
|
+
mode: record.mode,
|
|
587
|
+
dependency: record.dependency,
|
|
588
|
+
registryPath: relativeRunRecordPath(ref),
|
|
589
|
+
eventsPath: relativeRunEventsPath(ref),
|
|
590
|
+
eventTail: events,
|
|
591
|
+
attempts: record.attempts.map(attemptSnapshot),
|
|
592
|
+
logs:
|
|
593
|
+
snapshot.logs.length > 0
|
|
594
|
+
? snapshot.logs
|
|
595
|
+
: attempt === undefined
|
|
596
|
+
? snapshot.logs
|
|
597
|
+
: recordLogs(attempt),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function readRunResult(
|
|
602
|
+
ref: RunStatusRef,
|
|
603
|
+
): Promise<ResultEnvelope | null> {
|
|
604
|
+
const resolvedRef = await resolveRunRef(ref);
|
|
605
|
+
const record = await readRunRecord(resolvedRef);
|
|
606
|
+
const resultFromRecord =
|
|
607
|
+
record === null
|
|
608
|
+
? null
|
|
609
|
+
: await readRunResultFromAttempt(selectedAttempt(record, resolvedRef));
|
|
610
|
+
return resultFromRecord ?? (await readRunResultFallback(resolvedRef));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export async function getRunStatus(
|
|
614
|
+
ref: RunStatusRef,
|
|
615
|
+
): Promise<RunStatusSnapshot | null> {
|
|
616
|
+
const resolvedRef = await resolveRunRef(ref);
|
|
617
|
+
const record = await readRunRecord(resolvedRef);
|
|
618
|
+
const scannedEvents = await readRunEvents(
|
|
619
|
+
resolvedRef,
|
|
620
|
+
CHILD_EVENT_SCAN_LIMIT,
|
|
621
|
+
).catch(() => []);
|
|
622
|
+
const events = scannedEvents.slice(-EVENT_TAIL_LIMIT);
|
|
623
|
+
const childSummary = summarizeChildEvents(scannedEvents);
|
|
624
|
+
const result = await readRunResult(resolvedRef);
|
|
625
|
+
if (result !== null) {
|
|
626
|
+
const snapshot = createRunStatusSnapshot(result);
|
|
627
|
+
return withChildSummary(
|
|
628
|
+
record === null
|
|
629
|
+
? snapshot
|
|
630
|
+
: mergeRecordSnapshot(snapshot, record, resolvedRef, events),
|
|
631
|
+
childSummary,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return record === null
|
|
635
|
+
? null
|
|
636
|
+
: withChildSummary(
|
|
637
|
+
snapshotFromRecord(record, resolvedRef, events),
|
|
638
|
+
childSummary,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function getRunLogs(
|
|
643
|
+
ref: RunStatusRef,
|
|
644
|
+
): Promise<RunLogsSnapshot | null> {
|
|
645
|
+
const resolvedRef = await resolveRunRef(ref);
|
|
646
|
+
const { cwd } = pathsFor(resolvedRef);
|
|
647
|
+
const snapshot = await getRunStatus(resolvedRef);
|
|
648
|
+
if (snapshot === null) return null;
|
|
649
|
+
|
|
650
|
+
const logText: RunLogsSnapshot["logText"] = {};
|
|
651
|
+
for (const log of snapshot.logs) {
|
|
652
|
+
logText[log.type] = await readFile(
|
|
653
|
+
safeArtifactPath(cwd, log),
|
|
654
|
+
"utf8",
|
|
655
|
+
).catch(() => "");
|
|
656
|
+
}
|
|
657
|
+
if (snapshot.eventsPath !== undefined) {
|
|
658
|
+
logText.events = await readFile(
|
|
659
|
+
safeArtifactPath(cwd, { path: snapshot.eventsPath }),
|
|
660
|
+
"utf8",
|
|
661
|
+
).catch(() => "");
|
|
662
|
+
}
|
|
663
|
+
return { ...snapshot, logText };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export async function waitForRun(
|
|
667
|
+
options: WaitForRunOptions,
|
|
668
|
+
): Promise<WaitForRunResult> {
|
|
669
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
670
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
671
|
+
const deadline = Date.now() + timeoutMs;
|
|
672
|
+
let snapshot = await getRunStatus(options);
|
|
673
|
+
while (Date.now() <= deadline) {
|
|
674
|
+
snapshot = await getRunStatus(options);
|
|
675
|
+
if (snapshot !== null && isTerminalStatus(snapshot.status))
|
|
676
|
+
return { status: "completed", snapshot };
|
|
677
|
+
await sleep(pollIntervalMs);
|
|
678
|
+
}
|
|
679
|
+
return { status: "timeout", snapshot };
|
|
364
680
|
}
|