@tea-agent/loop-agent 0.7.5 → 0.8.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/AGENTS.md +143 -142
- package/CHANGELOG.md +148 -164
- package/README.md +206 -204
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/commands/init.js +518 -488
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/governance/manifest-types.js +1 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/cli.js +3 -3
- package/dist/worker/observability/event-store.js +2 -1
- package/dist/worker/observability/read-model.js +13 -11
- package/dist/worker/observe/paths.js +2 -2
- package/dist/worker/observe/routes.js +4 -3
- package/dist/worker/observe/static/app.js +1479 -1480
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/index.html +63 -63
- package/dist/worker/observe/static/styles.css +722 -722
- package/dist/worker/pool/run-store.js +7 -8
- package/dist/worker/run-task/run-task.js +11 -2
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/README.md +80 -79
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +162 -162
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +49 -49
- package/docs/development-principles.md +73 -73
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +11 -11
- package/docs/exec-plans/completed/README.md +35 -34
- package/docs/feature-workflow.md +187 -187
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +245 -241
- package/docs/loop-agent-harness.md +63 -55
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +9 -9
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +26 -26
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/harness.schema.json +218 -0
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/init-evolution-review.md +33 -33
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/product-line/AGENTS.md +8 -8
- package/docs/templates/product-line/README.md +9 -9
- package/docs/templates/product-line/acceptance.yaml +14 -14
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/task-graph.yaml +15 -15
- package/docs/templates/product-line/task.yaml +65 -65
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +52 -52
- package/docs/templates/worker-dogfood-setup.md +48 -48
- package/docs/verification-matrix.md +49 -49
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +73 -71
- package/package.json +68 -67
- package/scripts/check-product-line-docs.sh +22 -22
- package/scripts/check-task-pool-root.sh +32 -0
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +149 -149
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +432 -412
- package/skills/loop-agent/references/harness-policy.md +263 -263
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -89
- package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
export const TASK_POOL_RELATIVE_ROOT = ".harness/task-pool";
|
|
3
4
|
export function getTaskPoolRoot(repoRoot) {
|
|
4
|
-
return path.join(repoRoot,
|
|
5
|
+
return path.join(repoRoot, ...TASK_POOL_RELATIVE_ROOT.split("/"));
|
|
5
6
|
}
|
|
6
7
|
export function getRunsJsonlPath(repoRoot) {
|
|
7
|
-
return path.join(getTaskPoolRoot(repoRoot), "runs
|
|
8
|
+
return path.join(getTaskPoolRoot(repoRoot), "runs.jsonl");
|
|
8
9
|
}
|
|
9
10
|
export function getEventsJsonlPath(repoRoot) {
|
|
10
|
-
return path.join(getTaskPoolRoot(repoRoot), "events
|
|
11
|
+
return path.join(getTaskPoolRoot(repoRoot), "events.jsonl");
|
|
11
12
|
}
|
|
12
13
|
export function getTaskStatePath(repoRoot, taskId) {
|
|
13
|
-
return path.join(getTaskPoolRoot(repoRoot), "
|
|
14
|
+
return path.join(getTaskPoolRoot(repoRoot), "states", `${taskId}.json`);
|
|
14
15
|
}
|
|
15
16
|
export async function recordTaskPoolRun(input) {
|
|
16
17
|
await ensurePoolDirs(input.repoRoot);
|
|
@@ -76,7 +77,7 @@ export async function readTaskPoolState(repoRoot, taskId) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
export async function readAllTaskPoolStates(repoRoot) {
|
|
79
|
-
const stateDir = path.join(getTaskPoolRoot(repoRoot), "
|
|
80
|
+
const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
|
|
80
81
|
try {
|
|
81
82
|
const entries = await readdir(stateDir);
|
|
82
83
|
const states = {};
|
|
@@ -127,9 +128,7 @@ async function appendJsonlFile(filePath, value) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
async function ensurePoolDirs(repoRoot) {
|
|
130
|
-
await mkdir(path.join(getTaskPoolRoot(repoRoot), "
|
|
131
|
-
await mkdir(path.join(getTaskPoolRoot(repoRoot), "events"), { recursive: true });
|
|
132
|
-
await mkdir(path.join(getTaskPoolRoot(repoRoot), "state"), { recursive: true });
|
|
131
|
+
await mkdir(path.join(getTaskPoolRoot(repoRoot), "states"), { recursive: true });
|
|
133
132
|
}
|
|
134
133
|
function stateFromRun(run) {
|
|
135
134
|
return {
|
|
@@ -5,6 +5,7 @@ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
|
|
|
5
5
|
import { redactForPreview, truncatePreview } from "../observability/events.js";
|
|
6
6
|
import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
|
|
7
7
|
import { preflightTargetRepo } from "../preflight.js";
|
|
8
|
+
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
8
9
|
export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
|
|
9
10
|
export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
|
|
10
11
|
export async function runTaskSpec(options) {
|
|
@@ -95,7 +96,7 @@ export async function runTaskSpec(options) {
|
|
|
95
96
|
progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
|
|
96
97
|
const runDagStep = "run-dag";
|
|
97
98
|
const runDagStartedAt = Date.now();
|
|
98
|
-
const dagEventsPath = path.join(options.repoRoot, "
|
|
99
|
+
const dagEventsPath = path.join(getTaskPoolRoot(options.repoRoot), "observability", "runs", workerRunId, "dag-events.jsonl");
|
|
99
100
|
await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
|
|
100
101
|
await runObservedStep(eventCtx, runDagStep, async () => {
|
|
101
102
|
progress.step("run-dag: executing DAG nodes (this is the long step)");
|
|
@@ -321,7 +322,11 @@ async function collectFailureArtifacts(input) {
|
|
|
321
322
|
cwd: input.repoRoot,
|
|
322
323
|
artifactName: "dag-doctor-markdown",
|
|
323
324
|
}), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
|
|
324
|
-
const
|
|
325
|
+
const failureHandoffRoot = path.resolve(getTaskPoolRoot(input.repoRoot), "failure-handoffs");
|
|
326
|
+
const closeoutDraftPath = path.resolve(failureHandoffRoot, `${input.workerRunId}-failure-closeout-draft.md`);
|
|
327
|
+
if (!isPathInside(failureHandoffRoot, closeoutDraftPath)) {
|
|
328
|
+
throw new Error("closeout draft output must be under .harness/task-pool/failure-handoffs/");
|
|
329
|
+
}
|
|
325
330
|
await runObservedStep(input.eventCtx, "closeout-draft", () => input.client.run([
|
|
326
331
|
"dag",
|
|
327
332
|
"closeout-draft",
|
|
@@ -343,6 +348,10 @@ async function collectFailureArtifacts(input) {
|
|
|
343
348
|
closeoutDraftPath,
|
|
344
349
|
};
|
|
345
350
|
}
|
|
351
|
+
function isPathInside(root, target) {
|
|
352
|
+
const relative = path.relative(root, target);
|
|
353
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
354
|
+
}
|
|
346
355
|
function decideFromReport(workerRunId, result) {
|
|
347
356
|
if (!result.ok || !result.json) {
|
|
348
357
|
return {
|
|
@@ -227,7 +227,7 @@ export async function runReadyTasks(options) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
const summary = summarize(tasks);
|
|
230
|
-
const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "
|
|
230
|
+
const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "batch-run.json");
|
|
231
231
|
const output = {
|
|
232
232
|
schemaVersion: 1,
|
|
233
233
|
status: summary.recordErrors > 0 || summary.runErrors > 0 ? "failed" : "completed",
|
|
@@ -195,280 +195,280 @@ export function createDagCanvasObserver(opts) {
|
|
|
195
195
|
flush: () => writer.flush(),
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
-
const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
|
|
199
|
-
import {
|
|
200
|
-
Card,
|
|
201
|
-
CardBody,
|
|
202
|
-
CardHeader,
|
|
203
|
-
Divider,
|
|
204
|
-
Grid,
|
|
205
|
-
H1,
|
|
206
|
-
H2,
|
|
207
|
-
Pill,
|
|
208
|
-
Row,
|
|
209
|
-
Stack,
|
|
210
|
-
Stat,
|
|
211
|
-
Text,
|
|
212
|
-
computeDAGLayout,
|
|
213
|
-
useHostTheme,
|
|
214
|
-
} from 'cursor/canvas';
|
|
215
|
-
import { useMemo } from 'react';
|
|
216
|
-
|
|
217
|
-
type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
|
|
218
|
-
type Complexity = 'HIGH' | 'MED' | 'LOW';
|
|
219
|
-
type Executor = 'cursor' | 'pi' | 'shell' | 'static';
|
|
220
|
-
|
|
221
|
-
interface TaskState {
|
|
222
|
-
id: string;
|
|
223
|
-
depends_on: string[];
|
|
224
|
-
complexity: Complexity;
|
|
225
|
-
executor: Executor;
|
|
226
|
-
subtask_prompt: string;
|
|
227
|
-
status: TaskStatus;
|
|
228
|
-
model: string;
|
|
229
|
-
startedAt?: number;
|
|
230
|
-
finishedAt?: number;
|
|
231
|
-
resultText?: string;
|
|
232
|
-
errorMessage?: string;
|
|
233
|
-
inputTokens?: number;
|
|
234
|
-
outputTokens?: number;
|
|
235
|
-
durationMs?: number;
|
|
236
|
-
skippedReason?: string;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
interface RunState {
|
|
240
|
-
title: string;
|
|
241
|
-
runId: string;
|
|
242
|
-
startedAt: number;
|
|
243
|
-
finishedAt?: number;
|
|
244
|
-
runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
|
|
245
|
-
runMessage?: string;
|
|
246
|
-
tasks: TaskState[];
|
|
198
|
+
const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
|
|
199
|
+
import {
|
|
200
|
+
Card,
|
|
201
|
+
CardBody,
|
|
202
|
+
CardHeader,
|
|
203
|
+
Divider,
|
|
204
|
+
Grid,
|
|
205
|
+
H1,
|
|
206
|
+
H2,
|
|
207
|
+
Pill,
|
|
208
|
+
Row,
|
|
209
|
+
Stack,
|
|
210
|
+
Stat,
|
|
211
|
+
Text,
|
|
212
|
+
computeDAGLayout,
|
|
213
|
+
useHostTheme,
|
|
214
|
+
} from 'cursor/canvas';
|
|
215
|
+
import { useMemo } from 'react';
|
|
216
|
+
|
|
217
|
+
type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
|
|
218
|
+
type Complexity = 'HIGH' | 'MED' | 'LOW';
|
|
219
|
+
type Executor = 'cursor' | 'pi' | 'shell' | 'static';
|
|
220
|
+
|
|
221
|
+
interface TaskState {
|
|
222
|
+
id: string;
|
|
223
|
+
depends_on: string[];
|
|
224
|
+
complexity: Complexity;
|
|
225
|
+
executor: Executor;
|
|
226
|
+
subtask_prompt: string;
|
|
227
|
+
status: TaskStatus;
|
|
228
|
+
model: string;
|
|
229
|
+
startedAt?: number;
|
|
230
|
+
finishedAt?: number;
|
|
231
|
+
resultText?: string;
|
|
232
|
+
errorMessage?: string;
|
|
233
|
+
inputTokens?: number;
|
|
234
|
+
outputTokens?: number;
|
|
235
|
+
durationMs?: number;
|
|
236
|
+
skippedReason?: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface RunState {
|
|
240
|
+
title: string;
|
|
241
|
+
runId: string;
|
|
242
|
+
startedAt: number;
|
|
243
|
+
finishedAt?: number;
|
|
244
|
+
runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
|
|
245
|
+
runMessage?: string;
|
|
246
|
+
tasks: TaskState[];
|
|
247
247
|
}`;
|
|
248
|
-
const CANVAS_BODY = String.raw `const NODE_W = 200;
|
|
249
|
-
const NODE_H = 72;
|
|
250
|
-
|
|
251
|
-
function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
|
|
252
|
-
switch (status) {
|
|
253
|
-
case 'PENDING':
|
|
254
|
-
return 'neutral';
|
|
255
|
-
case 'RUNNING':
|
|
256
|
-
return 'info';
|
|
257
|
-
case 'FINISHED':
|
|
258
|
-
return 'success';
|
|
259
|
-
case 'ERROR':
|
|
260
|
-
return 'warning';
|
|
261
|
-
case 'SKIPPED':
|
|
262
|
-
return 'neutral';
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function formatDuration(ms?: number): string {
|
|
267
|
-
if (ms === undefined) return '—';
|
|
268
|
-
if (ms < 1000) return ms + 'ms';
|
|
269
|
-
const s = ms / 1000;
|
|
270
|
-
if (s < 60) return s.toFixed(1) + 's';
|
|
271
|
-
const m = Math.floor(s / 60);
|
|
272
|
-
const rem = Math.round(s - m * 60);
|
|
273
|
-
return m + 'm ' + rem + 's';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function elapsed(state: RunState): number {
|
|
277
|
-
const end = state.finishedAt ?? Date.now();
|
|
278
|
-
return end - state.startedAt;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function DAGGraph({ state }: { state: RunState }): JSX.Element {
|
|
282
|
-
const theme = useHostTheme();
|
|
283
|
-
const layout = computeDAGLayout({
|
|
284
|
-
nodes: state.tasks.map((t) => ({ id: t.id })),
|
|
285
|
-
edges: state.tasks.flatMap((t) =>
|
|
286
|
-
t.depends_on.map((d) => ({ from: d, to: t.id })),
|
|
287
|
-
),
|
|
288
|
-
direction: 'vertical',
|
|
289
|
-
nodeWidth: NODE_W,
|
|
290
|
-
nodeHeight: NODE_H,
|
|
291
|
-
rankGap: 72,
|
|
292
|
-
nodeGap: 40,
|
|
293
|
-
padding: 24,
|
|
294
|
-
});
|
|
295
|
-
const byId = new Map(state.tasks.map((t) => [t.id, t]));
|
|
296
|
-
|
|
297
|
-
return (
|
|
298
|
-
<svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
|
|
299
|
-
{layout.edges.map((e, i) => (
|
|
300
|
-
<line
|
|
301
|
-
key={'e' + i}
|
|
302
|
-
x1={e.sourceX}
|
|
303
|
-
y1={e.sourceY}
|
|
304
|
-
x2={e.targetX}
|
|
305
|
-
y2={e.targetY}
|
|
306
|
-
stroke={theme.stroke.secondary}
|
|
307
|
-
strokeWidth={1.25}
|
|
308
|
-
markerEnd="url(#dag-arrow)"
|
|
309
|
-
/>
|
|
310
|
-
))}
|
|
311
|
-
<defs>
|
|
312
|
-
<marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
313
|
-
<path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
|
|
314
|
-
</marker>
|
|
315
|
-
</defs>
|
|
316
|
-
{layout.nodes.map((n) => {
|
|
317
|
-
const t = byId.get(n.id);
|
|
318
|
-
if (!t) return null;
|
|
319
|
-
return (
|
|
320
|
-
<g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
|
|
321
|
-
<rect
|
|
322
|
-
width={NODE_W}
|
|
323
|
-
height={NODE_H}
|
|
324
|
-
rx={8}
|
|
325
|
-
ry={8}
|
|
326
|
-
fill={theme.fill.secondary}
|
|
327
|
-
stroke={theme.stroke.secondary}
|
|
328
|
-
strokeWidth={t.status === 'RUNNING' ? 2 : 1}
|
|
329
|
-
/>
|
|
330
|
-
<text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
|
|
331
|
-
{t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
|
|
332
|
-
</text>
|
|
333
|
-
<text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
|
|
334
|
-
{t.executor} · {t.complexity} · {t.status}
|
|
335
|
-
</text>
|
|
336
|
-
<text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
|
|
337
|
-
{formatDuration(t.durationMs)}
|
|
338
|
-
</text>
|
|
339
|
-
</g>
|
|
340
|
-
);
|
|
341
|
-
})}
|
|
342
|
-
</svg>
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export default function DagRun(): JSX.Element {
|
|
347
|
-
const counts = useMemo(
|
|
348
|
-
() =>
|
|
349
|
-
STATE.tasks.reduce(
|
|
350
|
-
(acc, t) => {
|
|
351
|
-
acc.total += 1;
|
|
352
|
-
const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
|
|
353
|
-
if (key in acc) acc[key] += 1;
|
|
354
|
-
return acc;
|
|
355
|
-
},
|
|
356
|
-
{ total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
|
|
357
|
-
),
|
|
358
|
-
[],
|
|
359
|
-
);
|
|
360
|
-
const isFinal = STATE.finishedAt !== undefined;
|
|
361
|
-
const statusLabel =
|
|
362
|
-
STATE.runOutcome === 'PAUSED'
|
|
363
|
-
? 'PAUSED'
|
|
364
|
-
: STATE.runOutcome === 'FAILED'
|
|
365
|
-
? 'FAILED'
|
|
366
|
-
: isFinal
|
|
367
|
-
? 'COMPLETE'
|
|
368
|
-
: 'RUNNING';
|
|
369
|
-
const statusTone =
|
|
370
|
-
STATE.runOutcome === 'FAILED'
|
|
371
|
-
? 'danger'
|
|
372
|
-
: STATE.runOutcome === 'PAUSED'
|
|
373
|
-
? 'warning'
|
|
374
|
-
: isFinal
|
|
375
|
-
? 'success'
|
|
376
|
-
: 'info';
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<Stack gap={20}>
|
|
380
|
-
<Stack gap={6}>
|
|
381
|
-
<H1>{STATE.title}</H1>
|
|
382
|
-
<Text tone="secondary" size="small">
|
|
383
|
-
run {STATE.runId}
|
|
384
|
-
</Text>
|
|
385
|
-
<Row gap={10} align="center">
|
|
386
|
-
<Pill tone={statusTone} active size="sm">
|
|
387
|
-
{statusLabel}
|
|
388
|
-
</Pill>
|
|
389
|
-
<Text tone="secondary" size="small">
|
|
390
|
-
{counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
|
|
391
|
-
</Text>
|
|
392
|
-
</Row>
|
|
393
|
-
{STATE.runMessage ? (
|
|
394
|
-
<Text tone="secondary" size="small">
|
|
395
|
-
{STATE.runMessage}
|
|
396
|
-
</Text>
|
|
397
|
-
) : null}
|
|
398
|
-
</Stack>
|
|
399
|
-
|
|
400
|
-
<Grid columns={6} gap={12}>
|
|
401
|
-
<Stat value={String(counts.total)} label="Total" />
|
|
402
|
-
<Stat value={String(counts.pending)} label="Pending" />
|
|
403
|
-
<Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
|
|
404
|
-
<Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
|
|
405
|
-
<Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
|
|
406
|
-
<Stat value={String(counts.skipped)} label="Skipped" />
|
|
407
|
-
</Grid>
|
|
408
|
-
|
|
409
|
-
<Divider />
|
|
410
|
-
|
|
411
|
-
<Stack gap={12}>
|
|
412
|
-
<H2>Graph</H2>
|
|
413
|
-
<DAGGraph state={STATE} />
|
|
414
|
-
</Stack>
|
|
415
|
-
|
|
416
|
-
<Divider />
|
|
417
|
-
|
|
418
|
-
<Stack gap={12}>
|
|
419
|
-
<H2>Tasks</H2>
|
|
420
|
-
<Stack gap={10}>
|
|
421
|
-
{STATE.tasks.map((t) => (
|
|
422
|
-
<Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
|
|
423
|
-
<CardHeader
|
|
424
|
-
trailing={
|
|
425
|
-
<Row gap={6} align="center">
|
|
426
|
-
<Pill tone="neutral" size="sm">
|
|
427
|
-
{t.executor}
|
|
428
|
-
</Pill>
|
|
429
|
-
<Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
|
|
430
|
-
{t.status}
|
|
431
|
-
</Pill>
|
|
432
|
-
</Row>
|
|
433
|
-
}
|
|
434
|
-
>
|
|
435
|
-
{t.id}
|
|
436
|
-
</CardHeader>
|
|
437
|
-
<CardBody>
|
|
438
|
-
<Stack gap={8}>
|
|
439
|
-
<Text tone="secondary" size="small">
|
|
440
|
-
Model {t.model}
|
|
441
|
-
{t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
|
|
442
|
-
{t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
|
|
443
|
-
</Text>
|
|
444
|
-
<Text size="small">
|
|
445
|
-
<Text as="span" weight="semibold">
|
|
446
|
-
Prompt:{' '}
|
|
447
|
-
</Text>
|
|
448
|
-
{t.subtask_prompt}
|
|
449
|
-
</Text>
|
|
450
|
-
{t.resultText ? (
|
|
451
|
-
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
452
|
-
{t.resultText}
|
|
453
|
-
{t.status === 'RUNNING' ? '\u2588' : ''}
|
|
454
|
-
</pre>
|
|
455
|
-
) : null}
|
|
456
|
-
{t.skippedReason ? (
|
|
457
|
-
<Text size="small" tone="secondary">
|
|
458
|
-
Skipped: {t.skippedReason}
|
|
459
|
-
</Text>
|
|
460
|
-
) : null}
|
|
461
|
-
{t.errorMessage ? (
|
|
462
|
-
<Text size="small" tone="secondary">
|
|
463
|
-
{t.errorMessage}
|
|
464
|
-
</Text>
|
|
465
|
-
) : null}
|
|
466
|
-
</Stack>
|
|
467
|
-
</CardBody>
|
|
468
|
-
</Card>
|
|
469
|
-
))}
|
|
470
|
-
</Stack>
|
|
471
|
-
</Stack>
|
|
472
|
-
</Stack>
|
|
473
|
-
);
|
|
248
|
+
const CANVAS_BODY = String.raw `const NODE_W = 200;
|
|
249
|
+
const NODE_H = 72;
|
|
250
|
+
|
|
251
|
+
function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
|
|
252
|
+
switch (status) {
|
|
253
|
+
case 'PENDING':
|
|
254
|
+
return 'neutral';
|
|
255
|
+
case 'RUNNING':
|
|
256
|
+
return 'info';
|
|
257
|
+
case 'FINISHED':
|
|
258
|
+
return 'success';
|
|
259
|
+
case 'ERROR':
|
|
260
|
+
return 'warning';
|
|
261
|
+
case 'SKIPPED':
|
|
262
|
+
return 'neutral';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function formatDuration(ms?: number): string {
|
|
267
|
+
if (ms === undefined) return '—';
|
|
268
|
+
if (ms < 1000) return ms + 'ms';
|
|
269
|
+
const s = ms / 1000;
|
|
270
|
+
if (s < 60) return s.toFixed(1) + 's';
|
|
271
|
+
const m = Math.floor(s / 60);
|
|
272
|
+
const rem = Math.round(s - m * 60);
|
|
273
|
+
return m + 'm ' + rem + 's';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function elapsed(state: RunState): number {
|
|
277
|
+
const end = state.finishedAt ?? Date.now();
|
|
278
|
+
return end - state.startedAt;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function DAGGraph({ state }: { state: RunState }): JSX.Element {
|
|
282
|
+
const theme = useHostTheme();
|
|
283
|
+
const layout = computeDAGLayout({
|
|
284
|
+
nodes: state.tasks.map((t) => ({ id: t.id })),
|
|
285
|
+
edges: state.tasks.flatMap((t) =>
|
|
286
|
+
t.depends_on.map((d) => ({ from: d, to: t.id })),
|
|
287
|
+
),
|
|
288
|
+
direction: 'vertical',
|
|
289
|
+
nodeWidth: NODE_W,
|
|
290
|
+
nodeHeight: NODE_H,
|
|
291
|
+
rankGap: 72,
|
|
292
|
+
nodeGap: 40,
|
|
293
|
+
padding: 24,
|
|
294
|
+
});
|
|
295
|
+
const byId = new Map(state.tasks.map((t) => [t.id, t]));
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
|
|
299
|
+
{layout.edges.map((e, i) => (
|
|
300
|
+
<line
|
|
301
|
+
key={'e' + i}
|
|
302
|
+
x1={e.sourceX}
|
|
303
|
+
y1={e.sourceY}
|
|
304
|
+
x2={e.targetX}
|
|
305
|
+
y2={e.targetY}
|
|
306
|
+
stroke={theme.stroke.secondary}
|
|
307
|
+
strokeWidth={1.25}
|
|
308
|
+
markerEnd="url(#dag-arrow)"
|
|
309
|
+
/>
|
|
310
|
+
))}
|
|
311
|
+
<defs>
|
|
312
|
+
<marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
313
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
|
|
314
|
+
</marker>
|
|
315
|
+
</defs>
|
|
316
|
+
{layout.nodes.map((n) => {
|
|
317
|
+
const t = byId.get(n.id);
|
|
318
|
+
if (!t) return null;
|
|
319
|
+
return (
|
|
320
|
+
<g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
|
|
321
|
+
<rect
|
|
322
|
+
width={NODE_W}
|
|
323
|
+
height={NODE_H}
|
|
324
|
+
rx={8}
|
|
325
|
+
ry={8}
|
|
326
|
+
fill={theme.fill.secondary}
|
|
327
|
+
stroke={theme.stroke.secondary}
|
|
328
|
+
strokeWidth={t.status === 'RUNNING' ? 2 : 1}
|
|
329
|
+
/>
|
|
330
|
+
<text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
|
|
331
|
+
{t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
|
|
332
|
+
</text>
|
|
333
|
+
<text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
|
|
334
|
+
{t.executor} · {t.complexity} · {t.status}
|
|
335
|
+
</text>
|
|
336
|
+
<text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
|
|
337
|
+
{formatDuration(t.durationMs)}
|
|
338
|
+
</text>
|
|
339
|
+
</g>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</svg>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export default function DagRun(): JSX.Element {
|
|
347
|
+
const counts = useMemo(
|
|
348
|
+
() =>
|
|
349
|
+
STATE.tasks.reduce(
|
|
350
|
+
(acc, t) => {
|
|
351
|
+
acc.total += 1;
|
|
352
|
+
const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
|
|
353
|
+
if (key in acc) acc[key] += 1;
|
|
354
|
+
return acc;
|
|
355
|
+
},
|
|
356
|
+
{ total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
|
|
357
|
+
),
|
|
358
|
+
[],
|
|
359
|
+
);
|
|
360
|
+
const isFinal = STATE.finishedAt !== undefined;
|
|
361
|
+
const statusLabel =
|
|
362
|
+
STATE.runOutcome === 'PAUSED'
|
|
363
|
+
? 'PAUSED'
|
|
364
|
+
: STATE.runOutcome === 'FAILED'
|
|
365
|
+
? 'FAILED'
|
|
366
|
+
: isFinal
|
|
367
|
+
? 'COMPLETE'
|
|
368
|
+
: 'RUNNING';
|
|
369
|
+
const statusTone =
|
|
370
|
+
STATE.runOutcome === 'FAILED'
|
|
371
|
+
? 'danger'
|
|
372
|
+
: STATE.runOutcome === 'PAUSED'
|
|
373
|
+
? 'warning'
|
|
374
|
+
: isFinal
|
|
375
|
+
? 'success'
|
|
376
|
+
: 'info';
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<Stack gap={20}>
|
|
380
|
+
<Stack gap={6}>
|
|
381
|
+
<H1>{STATE.title}</H1>
|
|
382
|
+
<Text tone="secondary" size="small">
|
|
383
|
+
run {STATE.runId}
|
|
384
|
+
</Text>
|
|
385
|
+
<Row gap={10} align="center">
|
|
386
|
+
<Pill tone={statusTone} active size="sm">
|
|
387
|
+
{statusLabel}
|
|
388
|
+
</Pill>
|
|
389
|
+
<Text tone="secondary" size="small">
|
|
390
|
+
{counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
|
|
391
|
+
</Text>
|
|
392
|
+
</Row>
|
|
393
|
+
{STATE.runMessage ? (
|
|
394
|
+
<Text tone="secondary" size="small">
|
|
395
|
+
{STATE.runMessage}
|
|
396
|
+
</Text>
|
|
397
|
+
) : null}
|
|
398
|
+
</Stack>
|
|
399
|
+
|
|
400
|
+
<Grid columns={6} gap={12}>
|
|
401
|
+
<Stat value={String(counts.total)} label="Total" />
|
|
402
|
+
<Stat value={String(counts.pending)} label="Pending" />
|
|
403
|
+
<Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
|
|
404
|
+
<Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
|
|
405
|
+
<Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
|
|
406
|
+
<Stat value={String(counts.skipped)} label="Skipped" />
|
|
407
|
+
</Grid>
|
|
408
|
+
|
|
409
|
+
<Divider />
|
|
410
|
+
|
|
411
|
+
<Stack gap={12}>
|
|
412
|
+
<H2>Graph</H2>
|
|
413
|
+
<DAGGraph state={STATE} />
|
|
414
|
+
</Stack>
|
|
415
|
+
|
|
416
|
+
<Divider />
|
|
417
|
+
|
|
418
|
+
<Stack gap={12}>
|
|
419
|
+
<H2>Tasks</H2>
|
|
420
|
+
<Stack gap={10}>
|
|
421
|
+
{STATE.tasks.map((t) => (
|
|
422
|
+
<Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
|
|
423
|
+
<CardHeader
|
|
424
|
+
trailing={
|
|
425
|
+
<Row gap={6} align="center">
|
|
426
|
+
<Pill tone="neutral" size="sm">
|
|
427
|
+
{t.executor}
|
|
428
|
+
</Pill>
|
|
429
|
+
<Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
|
|
430
|
+
{t.status}
|
|
431
|
+
</Pill>
|
|
432
|
+
</Row>
|
|
433
|
+
}
|
|
434
|
+
>
|
|
435
|
+
{t.id}
|
|
436
|
+
</CardHeader>
|
|
437
|
+
<CardBody>
|
|
438
|
+
<Stack gap={8}>
|
|
439
|
+
<Text tone="secondary" size="small">
|
|
440
|
+
Model {t.model}
|
|
441
|
+
{t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
|
|
442
|
+
{t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
|
|
443
|
+
</Text>
|
|
444
|
+
<Text size="small">
|
|
445
|
+
<Text as="span" weight="semibold">
|
|
446
|
+
Prompt:{' '}
|
|
447
|
+
</Text>
|
|
448
|
+
{t.subtask_prompt}
|
|
449
|
+
</Text>
|
|
450
|
+
{t.resultText ? (
|
|
451
|
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
452
|
+
{t.resultText}
|
|
453
|
+
{t.status === 'RUNNING' ? '\u2588' : ''}
|
|
454
|
+
</pre>
|
|
455
|
+
) : null}
|
|
456
|
+
{t.skippedReason ? (
|
|
457
|
+
<Text size="small" tone="secondary">
|
|
458
|
+
Skipped: {t.skippedReason}
|
|
459
|
+
</Text>
|
|
460
|
+
) : null}
|
|
461
|
+
{t.errorMessage ? (
|
|
462
|
+
<Text size="small" tone="secondary">
|
|
463
|
+
{t.errorMessage}
|
|
464
|
+
</Text>
|
|
465
|
+
) : null}
|
|
466
|
+
</Stack>
|
|
467
|
+
</CardBody>
|
|
468
|
+
</Card>
|
|
469
|
+
))}
|
|
470
|
+
</Stack>
|
|
471
|
+
</Stack>
|
|
472
|
+
</Stack>
|
|
473
|
+
);
|
|
474
474
|
}`;
|