acpus 0.0.1 → 0.0.2
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 +45 -32
- package/dist/cli.mjs +427 -270
- package/dist/index.d.mts +980 -1528
- package/dist/index.mjs +243 -2
- package/dist/{monitor-app-CSjUPe9j.mjs → monitor-app-CPlEcyHR.mjs} +8 -73
- package/dist/monitor-rendering-LGr9Ebd_.mjs +78 -0
- package/dist/run-picker-app-utJ2f5CU.mjs +104 -0
- package/dist/{run-workflow-CbxKhAqF.mjs → run-workflow-DdIAC8Zu.mjs} +3844 -4508
- package/package.json +2 -2
- package/schemas/workflow-spec.schema.json +1915 -1308
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,243 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
|
|
1
|
+
import { $ as ConditionSchema, B as retryCountByReason, C as RUN_MONITOR_VIEW_VERSION, Ct as TaskStageSchema, Dt as WorkflowLimitsSchema, E as buildTaskDetailView, Et as WorkflowInputSchema, F as parseWorkflowOutput, Ft as runDir, G as compileSchemaDsl, H as retryableOutputFailure, I as AGENT_TASK_RETRY_BUDGET, J as outputSchemaFooter, K as defaultAgentOutputZod, L as AGENT_TASK_RETRY_DELAY_MS, N as syncRun, Ot as WorkflowSpecSchema, P as collectWorkflowOutputCandidates, Q as AgentFaninSchema, R as agentTaskRetryDelayMs, S as resultFromIssues, St as TaskProgramStageSchema, T as buildRunMonitorView, Tt as VariableSchema, U as setAgentTaskRetryDelayForTests, V as retryExhaustedEnvelope, W as DEFAULT_AGENT_OUTPUT_SCHEMA, X as ActorModeSchema, Y as zodForCompiledSchema, Z as ActorSchema, _t as RouteStageSchema, a as renderStagePrompt, at as GateAgentStageSchema, b as OrchestratorError, bt as StageSchema, c as EXECUTION_PLAN_VERSION, ct as LimitBindingSchema, d as previewRunView, dt as LoopStageSchema, et as FaninSchema, f as runViewFromIndex, ft as PositiveIntegerLimitSchema, g as lintWorkflowSpec, gt as RouteRuleSchema, h as stringifyWorkflowSpec, ht as RouteProgramStageSchema, i as renderPromptMap, it as FanoutStageSchema, kt as RuntimeErrorCodes, l as estimateAgentCalls, lt as LimitValueSchema, m as loadWorkflowSpec, mt as RouteAgentStageSchema, n as startPreparedRun, nt as FanoutPolicySchema, o as stageActorLabel, ot as GateProgramStageSchema, p as isWorkflowYamlPath, pt as ProgramFaninSchema, q as formatSchema, r as compileExecutionPlan, rt as FanoutStageLimitsSchema, s as topologicalOrder, st as GateStageSchema, t as prepareRun, tt as FanoutLaneSchema, u as estimateFanoutWork, ut as LoopBodyStageSchema, vt as SCHEMA_VERSION, w as TASK_DETAIL_VIEW_VERSION, wt as TransformSchema, x as issue, xt as TaskAgentStageSchema, yt as StageLimitsSchema, z as formatContinuationPrompt } from "./run-workflow-DdIAC8Zu.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
//#region src/projections/run-diagnostics.ts
|
|
5
|
+
const RUN_DIAGNOSTICS_VIEW_VERSION = "acpus.diagnostics/v1";
|
|
6
|
+
const RUN_LEVEL_RUNTIME_CODES = new Set([
|
|
7
|
+
RuntimeErrorCodes.EVENT_APPEND_LOCK_TIMEOUT,
|
|
8
|
+
RuntimeErrorCodes.RUN_INDEX_LOCK_TIMEOUT,
|
|
9
|
+
RuntimeErrorCodes.AGENT_TASK_RETRY_EXHAUSTED,
|
|
10
|
+
RuntimeErrorCodes.FANOUT_ITEM_UNSTARTED_TIMEOUT,
|
|
11
|
+
RuntimeErrorCodes.FANOUT_ITEM_BLOCKED,
|
|
12
|
+
RuntimeErrorCodes.FANOUT_ITEM_CASCADE_BLOCKED,
|
|
13
|
+
RuntimeErrorCodes.FANOUT_LANE_RESULT_MISMATCH,
|
|
14
|
+
RuntimeErrorCodes.NO_SELECTED_LANES,
|
|
15
|
+
RuntimeErrorCodes.MISSING_FANOUT_ITEM_OUTPUT,
|
|
16
|
+
RuntimeErrorCodes.FANOUT_STAGE_STUCK_PENDING_BATCH,
|
|
17
|
+
RuntimeErrorCodes.RUN_INDEX_OUTPUT_MISMATCH,
|
|
18
|
+
RuntimeErrorCodes.LOOP_EXHAUSTED,
|
|
19
|
+
RuntimeErrorCodes.LOOP_BODY_STAGE_BLOCKED,
|
|
20
|
+
RuntimeErrorCodes.LOOP_BODY_STAGE_FAILED,
|
|
21
|
+
RuntimeErrorCodes.LOOP_BODY_OUTPUT_MISSING,
|
|
22
|
+
RuntimeErrorCodes.VARIABLE_RESOLUTION_FAILED,
|
|
23
|
+
RuntimeErrorCodes.AGENT_RUNTIME_ERROR,
|
|
24
|
+
RuntimeErrorCodes.AGENT_TURN_FAILED,
|
|
25
|
+
RuntimeErrorCodes.AGENT_TURN_CANCELLED,
|
|
26
|
+
RuntimeErrorCodes.AGENT_STAGE_STALE_RECOVERY,
|
|
27
|
+
RuntimeErrorCodes.GATE_CONDITION_FAILED,
|
|
28
|
+
RuntimeErrorCodes.GATE_VERDICT_BLOCKED,
|
|
29
|
+
RuntimeErrorCodes.GATE_VERDICT_FAILED,
|
|
30
|
+
RuntimeErrorCodes.GATE_VERDICT_UNKNOWN
|
|
31
|
+
]);
|
|
32
|
+
const RunDiagnosticCodes = { SCHEDULER_RECOVERY_SUCCEEDED_WITH_BLOCKED_VERDICT: "SCHEDULER_RECOVERY_SUCCEEDED_WITH_BLOCKED_VERDICT" };
|
|
33
|
+
const DEFAULT_EVENT_TAIL_LIMIT = 50;
|
|
34
|
+
const DEFAULT_EVENT_TAIL_MAX_BYTES = 256 * 1024;
|
|
35
|
+
async function buildRunDiagnosticsView(cwd, index, options = {}) {
|
|
36
|
+
const dir = runDir(index.logicalRunId, cwd);
|
|
37
|
+
const eventTail = await readEventTail(path.join(dir, "events.ndjson"), {
|
|
38
|
+
limit: options.eventTailLimit ?? DEFAULT_EVENT_TAIL_LIMIT,
|
|
39
|
+
maxBytes: options.eventTailMaxBytes ?? DEFAULT_EVENT_TAIL_MAX_BYTES
|
|
40
|
+
});
|
|
41
|
+
const diagnostics = [...await buildRuntimeDiagnostics(dir, index), ...buildEventTailDiagnostics(path.join(dir, "events.ndjson"), eventTail)];
|
|
42
|
+
return {
|
|
43
|
+
version: RUN_DIAGNOSTICS_VIEW_VERSION,
|
|
44
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
45
|
+
run: {
|
|
46
|
+
logicalRunId: index.logicalRunId,
|
|
47
|
+
workflowName: index.workflowName,
|
|
48
|
+
status: index.status,
|
|
49
|
+
blockedReason: index.blockedReason,
|
|
50
|
+
gateVerdict: index.gateVerdict,
|
|
51
|
+
runDir: dir
|
|
52
|
+
},
|
|
53
|
+
diagnostics,
|
|
54
|
+
eventTail
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function readNdjsonTail(filePath, maxLines, maxBytes = DEFAULT_EVENT_TAIL_MAX_BYTES) {
|
|
58
|
+
if (maxLines <= 0 || maxBytes <= 0) return [];
|
|
59
|
+
let handle;
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(filePath);
|
|
62
|
+
if (stat.size === 0) return [];
|
|
63
|
+
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
64
|
+
const start = stat.size - bytesToRead;
|
|
65
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
66
|
+
handle = await fs.open(filePath, "r");
|
|
67
|
+
await handle.read(buffer, 0, bytesToRead, start);
|
|
68
|
+
let lines = buffer.toString("utf8").split("\n");
|
|
69
|
+
if (start > 0) lines = lines.slice(1);
|
|
70
|
+
return lines.filter((line) => line.trim().length > 0).slice(-maxLines);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
} finally {
|
|
74
|
+
await handle?.close().catch(() => void 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function readEventTail(filePath, options) {
|
|
78
|
+
return (await readNdjsonTail(filePath, options.limit, options.maxBytes)).map(parseEventLine).filter((event) => event !== void 0);
|
|
79
|
+
}
|
|
80
|
+
async function buildRuntimeDiagnostics(dir, index) {
|
|
81
|
+
const diagnostics = [];
|
|
82
|
+
if (index.blockedReason && isRunLevelRuntimeCode(index.blockedReason)) diagnostics.push(runtimeDiagnostic({
|
|
83
|
+
code: index.blockedReason,
|
|
84
|
+
path: path.join(dir, "run.json"),
|
|
85
|
+
summary: runLevelBlockedSummary(index),
|
|
86
|
+
source: "run_index"
|
|
87
|
+
}));
|
|
88
|
+
for (const stage of Object.values(index.stages)) {
|
|
89
|
+
if (!stage.fanout && (stage.blockedReason === RuntimeErrorCodes.AGENT_RUNTIME_ERROR || stage.blockedReason === RuntimeErrorCodes.AGENT_STAGE_STALE_RECOVERY || stage.blockedReason === RuntimeErrorCodes.AGENT_TASK_RETRY_EXHAUSTED)) {
|
|
90
|
+
const outputPath = stage.outputPath ? path.join(dir, stage.outputPath) : path.join(dir, "outputs", `${stage.stageId}.json`);
|
|
91
|
+
const code = stage.blockedReason;
|
|
92
|
+
diagnostics.push(runtimeDiagnostic({
|
|
93
|
+
code,
|
|
94
|
+
stageId: stage.stageId,
|
|
95
|
+
path: outputPath,
|
|
96
|
+
summary: stageRetryDiagnosticSummary(code),
|
|
97
|
+
source: "run_index"
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
if (!stage.fanout) continue;
|
|
101
|
+
const hasRunningItems = stage.fanout.items.some((item) => item.status === "running");
|
|
102
|
+
const queuedItems = stage.fanout.items.filter((item) => item.status === "pending" || item.status === "ready");
|
|
103
|
+
if (stage.status === "running" && !hasRunningItems && queuedItems.length > 0) diagnostics.push(runtimeDiagnostic({
|
|
104
|
+
code: RuntimeErrorCodes.FANOUT_STAGE_STUCK_PENDING_BATCH,
|
|
105
|
+
stageId: stage.stageId,
|
|
106
|
+
path: path.join(dir, "run.json"),
|
|
107
|
+
summary: `Fanout stage ${stage.stageId} is running with no running items and ${queuedItems.length} queued item(s).`,
|
|
108
|
+
source: "run_index"
|
|
109
|
+
}));
|
|
110
|
+
for (const item of stage.fanout.items) {
|
|
111
|
+
const outputPath = item.outputPath ? path.join(dir, item.outputPath) : path.join(dir, "outputs", stage.stageId, `${safeFileName(item.id)}.json`);
|
|
112
|
+
if (item.status === "running" && await fileExists(outputPath)) diagnostics.push(runtimeDiagnostic({
|
|
113
|
+
code: RuntimeErrorCodes.RUN_INDEX_OUTPUT_MISMATCH,
|
|
114
|
+
stageId: stage.stageId,
|
|
115
|
+
itemId: item.id,
|
|
116
|
+
path: outputPath,
|
|
117
|
+
summary: `Fanout item ${stage.stageId}/${item.id} is running in run.json but has an output file.`,
|
|
118
|
+
source: "stage_output"
|
|
119
|
+
}));
|
|
120
|
+
if (item.errorCode) diagnostics.push(runtimeDiagnostic({
|
|
121
|
+
code: item.errorCode,
|
|
122
|
+
stageId: stage.stageId,
|
|
123
|
+
itemId: item.id,
|
|
124
|
+
path: outputPath,
|
|
125
|
+
summary: item.errorMessage ?? item.blockedReason ?? item.errorCode,
|
|
126
|
+
source: "run_index"
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const recoveryVerdictDiagnostic = buildRecoverySucceededWithBlockedVerdictDiagnostic(dir, index);
|
|
131
|
+
if (recoveryVerdictDiagnostic) diagnostics.push(recoveryVerdictDiagnostic);
|
|
132
|
+
return diagnostics;
|
|
133
|
+
}
|
|
134
|
+
function stageRetryDiagnosticSummary(code) {
|
|
135
|
+
if (code === RuntimeErrorCodes.AGENT_STAGE_STALE_RECOVERY) return "Agent task retry exhausted after scheduler stale recovery.";
|
|
136
|
+
if (code === RuntimeErrorCodes.AGENT_TASK_RETRY_EXHAUSTED) return "Agent task retry budget exhausted.";
|
|
137
|
+
return "Agent runtime failed after retry exhaustion.";
|
|
138
|
+
}
|
|
139
|
+
function buildEventTailDiagnostics(eventPath, eventTail) {
|
|
140
|
+
const diagnostics = [];
|
|
141
|
+
for (const event of eventTail) {
|
|
142
|
+
if (!(event.summary?.includes("Lock file is already being held") || event.errorCode === RuntimeErrorCodes.RUN_INDEX_LOCK_TIMEOUT || event.errorCode === RuntimeErrorCodes.EVENT_APPEND_LOCK_TIMEOUT)) continue;
|
|
143
|
+
diagnostics.push(runtimeDiagnostic({
|
|
144
|
+
code: event.errorCode ?? "LOCK_CONTENTION",
|
|
145
|
+
stageId: event.stageId,
|
|
146
|
+
itemId: event.itemId,
|
|
147
|
+
attemptId: event.attemptId,
|
|
148
|
+
path: eventPath,
|
|
149
|
+
summary: "Runtime lock contention was observed in recent events.",
|
|
150
|
+
source: "event_tail",
|
|
151
|
+
status: "blocked"
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
return diagnostics;
|
|
155
|
+
}
|
|
156
|
+
function buildRecoverySucceededWithBlockedVerdictDiagnostic(dir, index) {
|
|
157
|
+
if (!isBlockedGateVerdictCode(index.blockedReason) || !index.gateVerdict) return void 0;
|
|
158
|
+
const recoveredFanoutStages = Object.values(index.stages).filter((stage) => {
|
|
159
|
+
if (!stage.fanout) return false;
|
|
160
|
+
if (stage.fanout.items.some((item) => item.status === "running" || item.status === "pending" || item.status === "ready")) return false;
|
|
161
|
+
return stage.fanout.items.some((item) => item.errorCode === RuntimeErrorCodes.AGENT_TASK_RETRY_EXHAUSTED || item.errorCode === RuntimeErrorCodes.FANOUT_ITEM_CASCADE_BLOCKED || item.errorCode === RuntimeErrorCodes.FANOUT_ITEM_UNSTARTED_TIMEOUT || item.errorCode === RuntimeErrorCodes.RUN_INDEX_OUTPUT_MISMATCH);
|
|
162
|
+
});
|
|
163
|
+
if (recoveredFanoutStages.length === 0) return void 0;
|
|
164
|
+
const stageIds = recoveredFanoutStages.map((stage) => stage.stageId);
|
|
165
|
+
return {
|
|
166
|
+
id: `runtime-${RunDiagnosticCodes.SCHEDULER_RECOVERY_SUCCEEDED_WITH_BLOCKED_VERDICT}-run-all`,
|
|
167
|
+
code: RunDiagnosticCodes.SCHEDULER_RECOVERY_SUCCEEDED_WITH_BLOCKED_VERDICT,
|
|
168
|
+
status: "completed",
|
|
169
|
+
summary: `Scheduler recovery completed for fanout stage(s) ${stageIds.join(", ")}, but workflow gate verdict remains ${index.gateVerdict}.`,
|
|
170
|
+
path: path.join(dir, "run.json"),
|
|
171
|
+
source: "run_index"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function runtimeDiagnostic(input) {
|
|
175
|
+
return {
|
|
176
|
+
id: `runtime-${input.code}-${input.stageId ?? "run"}-${input.itemId ?? input.attemptId ?? "all"}`,
|
|
177
|
+
code: input.code,
|
|
178
|
+
stageId: input.stageId,
|
|
179
|
+
itemId: input.itemId,
|
|
180
|
+
attemptId: input.attemptId,
|
|
181
|
+
status: input.status ?? "blocked",
|
|
182
|
+
summary: input.summary,
|
|
183
|
+
path: input.path,
|
|
184
|
+
source: input.source
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function isRunLevelRuntimeCode(value) {
|
|
188
|
+
return RUN_LEVEL_RUNTIME_CODES.has(value);
|
|
189
|
+
}
|
|
190
|
+
function isBlockedGateVerdictCode(value) {
|
|
191
|
+
return value === RuntimeErrorCodes.GATE_VERDICT_BLOCKED || value === RuntimeErrorCodes.GATE_VERDICT_FAILED || value === RuntimeErrorCodes.GATE_VERDICT_UNKNOWN;
|
|
192
|
+
}
|
|
193
|
+
function runLevelBlockedSummary(index) {
|
|
194
|
+
if (index.blockedReason === RuntimeErrorCodes.AGENT_TASK_RETRY_EXHAUSTED) return "Agent Task Retry budget exhausted.";
|
|
195
|
+
if (index.blockedReason === RuntimeErrorCodes.AGENT_RUNTIME_ERROR) return "Agent runtime failed after Agent Task Retry budget exhaustion.";
|
|
196
|
+
if (index.blockedReason === RuntimeErrorCodes.AGENT_STAGE_STALE_RECOVERY) return "Agent stage stale recovery exhausted Agent Task Retry budget.";
|
|
197
|
+
if (index.blockedReason === RuntimeErrorCodes.FANOUT_ITEM_BLOCKED) return "Fanout stage blocked because one or more items did not complete.";
|
|
198
|
+
if (index.blockedReason === RuntimeErrorCodes.GATE_CONDITION_FAILED) return "Program gate condition failed.";
|
|
199
|
+
if (index.gateVerdict === "blocked") return "Gate returned verdict=blocked.";
|
|
200
|
+
if (index.gateVerdict === "failed") return "Gate returned verdict=failed.";
|
|
201
|
+
return "Gate returned verdict=unknown.";
|
|
202
|
+
}
|
|
203
|
+
function parseEventLine(line) {
|
|
204
|
+
const record = objectRecord(safeJson(line));
|
|
205
|
+
if (!record) return void 0;
|
|
206
|
+
const errorCode = stringField(record, "errorCode") ?? stringField(record, "code");
|
|
207
|
+
return {
|
|
208
|
+
at: stringField(record, "at"),
|
|
209
|
+
type: stringField(record, "type"),
|
|
210
|
+
stageId: stringField(record, "stageId"),
|
|
211
|
+
itemId: stringField(record, "itemId"),
|
|
212
|
+
attemptId: stringField(record, "attemptId"),
|
|
213
|
+
errorCode,
|
|
214
|
+
summary: stringField(record, "summary") ?? stringField(record, "error") ?? stringField(record, "errorMessage")
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function fileExists(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
await fs.access(filePath);
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function safeJson(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(value);
|
|
228
|
+
} catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function objectRecord(value) {
|
|
233
|
+
return value && typeof value === "object" ? value : void 0;
|
|
234
|
+
}
|
|
235
|
+
function stringField(value, key) {
|
|
236
|
+
const field = value?.[key];
|
|
237
|
+
return typeof field === "string" ? field : void 0;
|
|
238
|
+
}
|
|
239
|
+
function safeFileName(value) {
|
|
240
|
+
return String(value || "item").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
export { AGENT_TASK_RETRY_BUDGET, AGENT_TASK_RETRY_DELAY_MS, ActorModeSchema, ActorSchema, AgentFaninSchema, ConditionSchema, DEFAULT_AGENT_OUTPUT_SCHEMA, EXECUTION_PLAN_VERSION, FaninSchema, FanoutLaneSchema, FanoutPolicySchema, FanoutStageLimitsSchema, FanoutStageSchema, GateAgentStageSchema, GateProgramStageSchema, GateStageSchema, LimitBindingSchema, LimitValueSchema, LoopBodyStageSchema, LoopStageSchema, OrchestratorError, PositiveIntegerLimitSchema, ProgramFaninSchema, RUN_DIAGNOSTICS_VIEW_VERSION, RUN_MONITOR_VIEW_VERSION, RouteAgentStageSchema, RouteProgramStageSchema, RouteRuleSchema, RouteStageSchema, RunDiagnosticCodes, SCHEMA_VERSION, StageLimitsSchema, StageSchema, TASK_DETAIL_VIEW_VERSION, TaskAgentStageSchema, TaskProgramStageSchema, TaskStageSchema, TransformSchema, VariableSchema, WorkflowInputSchema, WorkflowLimitsSchema, WorkflowSpecSchema, agentTaskRetryDelayMs, buildRunDiagnosticsView, buildRunMonitorView, buildTaskDetailView, collectWorkflowOutputCandidates, compileExecutionPlan, compileSchemaDsl, defaultAgentOutputZod, estimateAgentCalls, estimateFanoutWork, formatContinuationPrompt, formatSchema, isWorkflowYamlPath, issue, lintWorkflowSpec, loadWorkflowSpec, outputSchemaFooter, parseWorkflowOutput, prepareRun, previewRunView, readNdjsonTail, renderPromptMap, renderStagePrompt, resultFromIssues, retryCountByReason, retryExhaustedEnvelope, retryableOutputFailure, runViewFromIndex, setAgentTaskRetryDelayForTests, stageActorLabel, startPreparedRun, stringifyWorkflowSpec, syncRun, topologicalOrder, zodForCompiledSchema };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as buildTaskDetailView, Ft as runDir, N as syncRun, Ot as WorkflowSpecSchema, T as buildRunMonitorView } from "./run-workflow-DdIAC8Zu.mjs";
|
|
2
|
+
import { n as resolveRunLocator } from "./cli.mjs";
|
|
3
|
+
import { a as nextIndex, c as shorten, d as tasksForStage, i as formatDuration, l as stageProgressLabel, n as defaultStageIndex, o as runProgressLabel, r as detailSummary, s as runStatusLabel, t as clampIndex, u as statusMark } from "./monitor-rendering-LGr9Ebd_.mjs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import fs from "node:fs/promises";
|
|
6
|
+
import YAML from "yaml";
|
|
4
7
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
8
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
6
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -8,7 +11,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
8
11
|
async function loadMonitorSnapshot(runArg) {
|
|
9
12
|
const locator = await resolveRunLocator(runArg);
|
|
10
13
|
const index = await syncRun(locator.cwd, locator.runId, { startPending: false });
|
|
11
|
-
const spec = WorkflowSpecSchema.parse(
|
|
14
|
+
const spec = WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(runDir(locator.runId, locator.cwd), "workflow.spec.yaml"), "utf8")));
|
|
12
15
|
return {
|
|
13
16
|
locator,
|
|
14
17
|
view: await buildRunMonitorView(locator.cwd, spec, index)
|
|
@@ -16,78 +19,10 @@ async function loadMonitorSnapshot(runArg) {
|
|
|
16
19
|
}
|
|
17
20
|
async function loadTaskDetail(locator, taskId) {
|
|
18
21
|
const index = await syncRun(locator.cwd, locator.runId, { startPending: false });
|
|
19
|
-
const spec = WorkflowSpecSchema.parse(
|
|
22
|
+
const spec = WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(runDir(locator.runId, locator.cwd), "workflow.spec.yaml"), "utf8")));
|
|
20
23
|
return buildTaskDetailView(locator.cwd, spec, index, taskId);
|
|
21
24
|
}
|
|
22
25
|
//#endregion
|
|
23
|
-
//#region src/tui/monitor-rendering.ts
|
|
24
|
-
function defaultStageIndex(stages) {
|
|
25
|
-
const running = stages.findIndex((stage) => stage.status === "running");
|
|
26
|
-
if (running >= 0) return running;
|
|
27
|
-
const blocked = stages.findIndex((stage) => stage.status === "blocked" || stage.status === "failed");
|
|
28
|
-
if (blocked >= 0) return blocked;
|
|
29
|
-
const open = stages.findIndex((stage) => stage.status !== "completed" && stage.status !== "skipped");
|
|
30
|
-
return open >= 0 ? open : 0;
|
|
31
|
-
}
|
|
32
|
-
function clampIndex(index, length) {
|
|
33
|
-
if (length <= 0) return 0;
|
|
34
|
-
return Math.min(Math.max(index, 0), length - 1);
|
|
35
|
-
}
|
|
36
|
-
function tasksForStage(view, stageId) {
|
|
37
|
-
if (!view || !stageId) return [];
|
|
38
|
-
return view.tasks.filter((task) => task.stageId === stageId);
|
|
39
|
-
}
|
|
40
|
-
function stageProgressLabel(stage) {
|
|
41
|
-
const counts = stage.taskCounts;
|
|
42
|
-
return `${counts.completed}/${counts.total}`;
|
|
43
|
-
}
|
|
44
|
-
function runProgressLabel(view) {
|
|
45
|
-
return `${view.progress.completedTasks}/${view.progress.knownTasks} tasks`;
|
|
46
|
-
}
|
|
47
|
-
function statusMark(status) {
|
|
48
|
-
if (status === "completed") return "✔";
|
|
49
|
-
if (status === "running" || status === "raw_received" || status === "parsing" || status === "repairing") return "●";
|
|
50
|
-
if (status === "blocked" || status === "failed" || status === "cancelled" || status === "timed_out") return "!";
|
|
51
|
-
if (status === "skipped") return "-";
|
|
52
|
-
return " ";
|
|
53
|
-
}
|
|
54
|
-
function shorten(value, width) {
|
|
55
|
-
if (!value) return "";
|
|
56
|
-
if (width <= 0) return "";
|
|
57
|
-
if (value.length <= width) return value;
|
|
58
|
-
if (width <= 3) return value.slice(0, width);
|
|
59
|
-
return `${value.slice(0, width - 3)}...`;
|
|
60
|
-
}
|
|
61
|
-
function detailSummary(detail) {
|
|
62
|
-
if (!detail) return ["No task selected"];
|
|
63
|
-
return [
|
|
64
|
-
`${statusMark(detail.task.status)} ${detail.task.status} - ${detail.task.execution}${detail.task.agent ? ` - ${detail.task.agent}` : ""}`,
|
|
65
|
-
detail.task.durationMs !== void 0 || detail.task.elapsedMs !== void 0 ? `Time: ${formatDuration(detail.task.durationMs ?? detail.task.elapsedMs ?? 0)}` : void 0,
|
|
66
|
-
detail.task.blockedReason ? `Reason: ${detail.task.blockedReason}` : void 0,
|
|
67
|
-
detail.outcome?.summary ? `Outcome: ${detail.outcome.summary}` : void 0,
|
|
68
|
-
detail.outcome?.path ? `Output: ${detail.outcome.path}` : void 0,
|
|
69
|
-
detail.prompt ? `Prompt: ${detail.prompt.lines} line(s)` : void 0,
|
|
70
|
-
detail.prompt?.preview ? detail.prompt.preview : void 0,
|
|
71
|
-
detail.activity.totalAttempts > 0 ? `Attempts: ${detail.activity.totalAttempts}` : "No agent attempts",
|
|
72
|
-
...detail.activity.attempts.map((attempt) => `${attempt.id} ${attempt.status} ${attempt.path}`),
|
|
73
|
-
...(detail.outcome?.artifacts ?? []).map((artifact) => `Artifact: ${artifact.label ?? artifact.kind ?? "artifact"} ${artifact.path ?? artifact.url ?? ""}`)
|
|
74
|
-
].filter((line) => typeof line === "string" && line.length > 0);
|
|
75
|
-
}
|
|
76
|
-
function nextIndex(current, delta, length) {
|
|
77
|
-
return clampIndex(current + delta, length);
|
|
78
|
-
}
|
|
79
|
-
function formatDuration(milliseconds) {
|
|
80
|
-
if (milliseconds === void 0 || !Number.isFinite(milliseconds)) return "";
|
|
81
|
-
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
82
|
-
const seconds = totalSeconds % 60;
|
|
83
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
84
|
-
const minutes = totalMinutes % 60;
|
|
85
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
86
|
-
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
87
|
-
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
88
|
-
return `${seconds}s`;
|
|
89
|
-
}
|
|
90
|
-
//#endregion
|
|
91
26
|
//#region src/tui/monitor-app.tsx
|
|
92
27
|
function MonitorApp({ runArg, pollMs = 1e3, initialView, initialLocator, initialFocus = "stages", loadSnapshot = loadMonitorSnapshot, loadDetail = loadTaskDetail }) {
|
|
93
28
|
const { exit } = useApp();
|
|
@@ -247,7 +182,7 @@ function Header({ view }) {
|
|
|
247
182
|
const title = `${view.run.workflowName}`;
|
|
248
183
|
const worker = view.run.worker ? ` - worker ${view.run.worker.status}` : "";
|
|
249
184
|
const runTime = formatDuration(view.run.durationMs ?? view.run.elapsedMs);
|
|
250
|
-
const meta = `${runProgressLabel(view)} - ${view
|
|
185
|
+
const meta = `${runProgressLabel(view)} - ${runStatusLabel(view)}${runTime ? ` - ${runTime}` : ""}${worker}`;
|
|
251
186
|
return /* @__PURE__ */ jsxs(Box, {
|
|
252
187
|
flexDirection: "column",
|
|
253
188
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
@@ -413,7 +348,7 @@ function StatusMark({ status }) {
|
|
|
413
348
|
color: "green",
|
|
414
349
|
children: mark
|
|
415
350
|
});
|
|
416
|
-
if (status === "running" || status === "raw_received" || status === "parsing"
|
|
351
|
+
if (status === "running" || status === "raw_received" || status === "parsing") return /* @__PURE__ */ jsx(Text, {
|
|
417
352
|
color: "yellow",
|
|
418
353
|
children: mark
|
|
419
354
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/tui/monitor-rendering.ts
|
|
2
|
+
function defaultStageIndex(stages) {
|
|
3
|
+
const running = stages.findIndex((stage) => stage.status === "running");
|
|
4
|
+
if (running >= 0) return running;
|
|
5
|
+
const blocked = stages.findIndex((stage) => stage.status === "blocked" || stage.status === "failed");
|
|
6
|
+
if (blocked >= 0) return blocked;
|
|
7
|
+
const open = stages.findIndex((stage) => stage.status !== "completed" && stage.status !== "skipped");
|
|
8
|
+
return open >= 0 ? open : 0;
|
|
9
|
+
}
|
|
10
|
+
function clampIndex(index, length) {
|
|
11
|
+
if (length <= 0) return 0;
|
|
12
|
+
return Math.min(Math.max(index, 0), length - 1);
|
|
13
|
+
}
|
|
14
|
+
function tasksForStage(view, stageId) {
|
|
15
|
+
if (!view || !stageId) return [];
|
|
16
|
+
return view.tasks.filter((task) => task.stageId === stageId);
|
|
17
|
+
}
|
|
18
|
+
function stageProgressLabel(stage) {
|
|
19
|
+
const counts = stage.taskCounts;
|
|
20
|
+
return `${counts.completed}/${counts.total}`;
|
|
21
|
+
}
|
|
22
|
+
function runProgressLabel(view) {
|
|
23
|
+
return `${view.progress.completedTasks}/${view.progress.knownTasks} tasks`;
|
|
24
|
+
}
|
|
25
|
+
function runStatusLabel(view) {
|
|
26
|
+
if ((view.run.status === "running" || view.run.status === "pending") && view.run.worker?.status === "stale") return "stale";
|
|
27
|
+
return view.run.status;
|
|
28
|
+
}
|
|
29
|
+
function statusMark(status) {
|
|
30
|
+
if (status === "completed") return "✔";
|
|
31
|
+
if (status === "running" || status === "raw_received" || status === "parsing") return "●";
|
|
32
|
+
if (status === "blocked" || status === "failed" || status === "cancelled" || status === "timed_out") return "!";
|
|
33
|
+
if (status === "skipped") return "-";
|
|
34
|
+
return " ";
|
|
35
|
+
}
|
|
36
|
+
function shorten(value, width) {
|
|
37
|
+
if (!value) return "";
|
|
38
|
+
if (width <= 0) return "";
|
|
39
|
+
if (value.length <= width) return value;
|
|
40
|
+
if (width <= 3) return value.slice(0, width);
|
|
41
|
+
return `${value.slice(0, width - 3)}...`;
|
|
42
|
+
}
|
|
43
|
+
function detailSummary(detail) {
|
|
44
|
+
if (!detail) return ["No task selected"];
|
|
45
|
+
return [
|
|
46
|
+
`${statusMark(detail.task.status)} ${detail.task.status} - ${detail.task.execution}${detail.task.agent ? ` - ${detail.task.agent}` : ""}`,
|
|
47
|
+
detail.task.durationMs !== void 0 || detail.task.elapsedMs !== void 0 ? `Time: ${formatDuration(detail.task.durationMs ?? detail.task.elapsedMs ?? 0)}` : void 0,
|
|
48
|
+
detail.task.blockedReason ? `Reason: ${detail.task.blockedReason}` : void 0,
|
|
49
|
+
detail.task.lastRetryReason ? `Retry: ${detail.task.lastRetryReason} ${detail.task.retryBudgetUsed ?? 0}/${detail.task.retryBudgetLimit ?? "?"}` : void 0,
|
|
50
|
+
detail.task.lastFailureCode ? `Last failure: ${detail.task.lastFailureCode}` : void 0,
|
|
51
|
+
detail.outcome?.summary ? `Outcome: ${detail.outcome.summary}` : void 0,
|
|
52
|
+
detail.outcome?.path ? `Output: ${detail.outcome.path}` : void 0,
|
|
53
|
+
detail.prompt ? `Prompt: ${detail.prompt.lines} line(s)` : void 0,
|
|
54
|
+
detail.prompt?.preview ? detail.prompt.preview : void 0,
|
|
55
|
+
detail.activity.totalAttempts > 0 ? `Attempts: ${detail.activity.totalAttempts}` : "No agent attempts",
|
|
56
|
+
...detail.activity.attempts.map((attempt) => {
|
|
57
|
+
const retry = attempt.isRetry ? ` retry=${attempt.retryReason ?? "unknown"}#${attempt.retryOrdinal ?? "?"}` : "";
|
|
58
|
+
const failure = attempt.lastFailureCode ? ` last=${attempt.lastFailureCode}` : "";
|
|
59
|
+
return `${attempt.id} ${attempt.status}${retry}${failure} ${attempt.path}`;
|
|
60
|
+
})
|
|
61
|
+
].filter((line) => typeof line === "string" && line.length > 0);
|
|
62
|
+
}
|
|
63
|
+
function nextIndex(current, delta, length) {
|
|
64
|
+
return clampIndex(current + delta, length);
|
|
65
|
+
}
|
|
66
|
+
function formatDuration(milliseconds) {
|
|
67
|
+
if (milliseconds === void 0 || !Number.isFinite(milliseconds)) return "";
|
|
68
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
69
|
+
const seconds = totalSeconds % 60;
|
|
70
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
71
|
+
const minutes = totalMinutes % 60;
|
|
72
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
73
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
74
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
75
|
+
return `${seconds}s`;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { nextIndex as a, shorten as c, tasksForStage as d, formatDuration as i, stageProgressLabel as l, defaultStageIndex as n, runProgressLabel as o, detailSummary as r, runStatusLabel as s, clampIndex as t, statusMark as u };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { t as listRunSummaries } from "./cli.mjs";
|
|
2
|
+
import { a as nextIndex, c as shorten, i as formatDuration, u as statusMark } from "./monitor-rendering-LGr9Ebd_.mjs";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/tui/run-picker-app.tsx
|
|
7
|
+
function RunPickerApp({ title, pollMs = 1e3, initialList, loadRuns = listRunSummaries, onSelect }) {
|
|
8
|
+
const { exit } = useApp();
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const [list, setList] = useState(initialList);
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const loadRunsRef = useRef(loadRuns);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
loadRunsRef.current = loadRuns;
|
|
16
|
+
}, [loadRuns]);
|
|
17
|
+
const refresh = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const next = await loadRunsRef.current();
|
|
20
|
+
setList(next);
|
|
21
|
+
setSelectedIndex((current) => Math.max(0, Math.min(current, Math.max(0, next.entries.length - 1))));
|
|
22
|
+
setError(void 0);
|
|
23
|
+
} catch (loadError) {
|
|
24
|
+
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
refresh();
|
|
29
|
+
const timer = setInterval(() => void refresh(), pollMs);
|
|
30
|
+
return () => clearInterval(timer);
|
|
31
|
+
}, [pollMs, refresh]);
|
|
32
|
+
useInput((input, key) => {
|
|
33
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
34
|
+
onSelect(void 0);
|
|
35
|
+
exit();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (input === "r") refresh();
|
|
39
|
+
if (key.upArrow) setSelectedIndex((current) => nextIndex(current, -1, list?.entries.length ?? 0));
|
|
40
|
+
if (key.downArrow) setSelectedIndex((current) => nextIndex(current, 1, list?.entries.length ?? 0));
|
|
41
|
+
if (key.return) {
|
|
42
|
+
const selected = list?.entries[selectedIndex];
|
|
43
|
+
if (!selected || selected.invalid) return;
|
|
44
|
+
onSelect(selected.runId);
|
|
45
|
+
exit();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const width = Math.max(80, stdout.columns ?? 120);
|
|
49
|
+
const entries = useMemo(() => list?.entries ?? [], [list?.entries]);
|
|
50
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
51
|
+
flexDirection: "column",
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx(Text, {
|
|
54
|
+
color: "blue",
|
|
55
|
+
bold: true,
|
|
56
|
+
children: title
|
|
57
|
+
}),
|
|
58
|
+
/* @__PURE__ */ jsx(Text, {
|
|
59
|
+
dimColor: true,
|
|
60
|
+
children: list ? `runs in ${list.dir}` : "Loading runs..."
|
|
61
|
+
}),
|
|
62
|
+
error ? /* @__PURE__ */ jsxs(Text, {
|
|
63
|
+
color: "red",
|
|
64
|
+
children: ["Error: ", error]
|
|
65
|
+
}) : null,
|
|
66
|
+
entries.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
67
|
+
dimColor: true,
|
|
68
|
+
children: "No runs found."
|
|
69
|
+
}) : null,
|
|
70
|
+
/* @__PURE__ */ jsx(Box, {
|
|
71
|
+
flexDirection: "column",
|
|
72
|
+
marginTop: 1,
|
|
73
|
+
children: entries.map((entry, index) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
74
|
+
/* @__PURE__ */ jsxs(Text, { children: [index === selectedIndex ? ">" : " ", " "] }),
|
|
75
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
76
|
+
dimColor: entry.invalid,
|
|
77
|
+
children: [statusMark(entry.status ?? "invalid"), " "]
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsxs(Text, { children: [shorten(entry.runId, 40), " "] }),
|
|
80
|
+
/* @__PURE__ */ jsxs(Text, { children: [shorten(entry.status ?? "invalid", 10), " "] }),
|
|
81
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
82
|
+
dimColor: true,
|
|
83
|
+
children: [shorten(entry.progress?.label ?? "-", 12), " "]
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
86
|
+
dimColor: true,
|
|
87
|
+
children: [shorten(entry.worker ? `worker ${entry.worker.status}` : "", 16), " "]
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ jsxs(Text, { children: [shorten(entry.workflowName ?? "", Math.max(12, width - 100)), " "] }),
|
|
90
|
+
/* @__PURE__ */ jsx(Text, {
|
|
91
|
+
dimColor: true,
|
|
92
|
+
children: formatDuration(entry.durationMs ?? entry.elapsedMs)
|
|
93
|
+
})
|
|
94
|
+
] }, entry.runId))
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsx(Text, {
|
|
97
|
+
dimColor: true,
|
|
98
|
+
children: "up/down move - enter select - r refresh - q quit"
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { RunPickerApp };
|