@tea-agent/loop-agent 0.4.0 → 0.6.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 +2 -2
- package/CHANGELOG.md +48 -38
- package/README.md +3 -3
- package/dist/application/dag/args.js +9 -1
- package/dist/application/dag/run-dag.js +16 -2
- package/dist/application/dag/validate-dag.js +14 -1
- package/dist/cli/command-definitions.js +22 -4
- package/dist/cli/help.js +3 -2
- package/dist/cli/program.js +7 -5
- package/dist/commands/import-prd.js +76 -0
- package/dist/commands/init.js +230 -32
- package/dist/commands/instructions.js +90 -58
- package/dist/executors/config-core.js +3 -2
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/executors/model-routing.js +43 -0
- package/dist/executors/pi-sdk-executor.js +63 -1
- package/dist/governance/manifest-types.js +9 -1
- package/dist/shared/preview.js +39 -0
- package/dist/task/source-references.js +221 -0
- package/dist/worker/cli.js +62 -1
- package/dist/worker/loop-agent/loop-agent-client.js +97 -5
- package/dist/worker/materialize/harness-task-materializer.js +162 -5
- package/dist/worker/observability/event-store.js +82 -0
- package/dist/worker/observability/events.js +79 -0
- package/dist/worker/observability/progress-composite.js +33 -0
- package/dist/worker/observability/read-model.js +1013 -0
- package/dist/worker/observability/snapshot-store.js +43 -0
- package/dist/worker/observability/types.js +1 -0
- package/dist/worker/observe/paths.js +64 -0
- package/dist/worker/observe/routes.js +423 -0
- package/dist/worker/observe/server.js +61 -0
- package/dist/worker/observe/static/app.js +1419 -0
- package/dist/worker/observe/static/index.html +63 -0
- package/dist/worker/observe/static/styles.css +613 -0
- package/dist/worker/pool/failure-routing.js +41 -6
- package/dist/worker/pool/run-store.js +59 -1
- package/dist/worker/progress-reporter.js +0 -18
- package/dist/worker/run-task/run-task.js +327 -92
- package/dist/worker/runner/run-ready.js +112 -4
- package/dist/workflows/dag/event-observer.js +132 -0
- package/dist/workflows/dag/init-hybrid.js +150 -26
- package/dist/workflows/dag/observer-compose.js +52 -0
- package/dist/workflows/dag/skill-instructions.js +4 -0
- package/dist/workflows/dag/types.js +1 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/README.md +2 -0
- package/docs/architecture/runtime-boundaries.md +18 -3
- package/docs/design/README.md +22 -9
- package/docs/exec-plans/active/README.md +6 -1
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/init-surface.manifest.json +32 -2
- package/docs/loop-agent-harness.md +13 -0
- package/docs/reports/README.md +4 -0
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/worker-dogfood-evidence.md +52 -0
- package/docs/templates/worker-dogfood-setup.md +48 -0
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +5 -29
- package/package.json +6 -6
- package/skills/loop-agent/SKILL.md +5 -3
- package/skills/loop-agent/references/command-reference.md +12 -3
- package/skills/loop-agent/references/harness-policy.md +7 -3
- package/skills/loop-agent/references/task-workflow.md +8 -3
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
|
|
5
|
+
import { redactForPreview, truncatePreview } from "../observability/events.js";
|
|
5
6
|
import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
|
|
6
7
|
import { preflightTargetRepo } from "../preflight.js";
|
|
7
8
|
export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
|
|
@@ -10,110 +11,139 @@ export async function runTaskSpec(options) {
|
|
|
10
11
|
const now = options.now ?? new Date();
|
|
11
12
|
const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
|
|
12
13
|
const commands = [];
|
|
13
|
-
const client = new RecordingRunTaskClient(options.client, commands);
|
|
14
14
|
const progress = options.progress ?? noopProgressReporter;
|
|
15
|
+
const taskId = options.taskSpec.id;
|
|
16
|
+
const eventCtx = { progress, workerRunId, taskId };
|
|
17
|
+
const recordingClient = new RecordingRunTaskClient(options.client, commands);
|
|
18
|
+
const client = new ObservedRunTaskClient(recordingClient, eventCtx);
|
|
15
19
|
if (options.preflight !== false) {
|
|
16
|
-
|
|
20
|
+
const preflightStep = "preflight";
|
|
17
21
|
const preflightStartedAt = Date.now();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
await runObservedStep(eventCtx, preflightStep, async () => {
|
|
23
|
+
progress.step("preflight: version + inspect + docs-audit + git-status" + (options.preflight && typeof options.preflight === "object" && options.preflight.runCheckRepo ? " + check-repo" : ""));
|
|
24
|
+
const preflight = await preflightTargetRepo({
|
|
25
|
+
repoRoot: options.repoRoot,
|
|
26
|
+
client,
|
|
27
|
+
...(typeof options.preflight === "object" ? options.preflight : {}),
|
|
28
|
+
});
|
|
29
|
+
if (!preflight.ok) {
|
|
30
|
+
progress.note(`preflight FAILED: ${preflight.code}`);
|
|
31
|
+
throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
|
|
32
|
+
}
|
|
22
33
|
});
|
|
23
|
-
if (!preflight.ok) {
|
|
24
|
-
progress.note(`preflight FAILED: ${preflight.code}`);
|
|
25
|
-
throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
|
|
26
|
-
}
|
|
27
34
|
progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
const materializeStep = "materialize";
|
|
30
37
|
const materializeStartedAt = Date.now();
|
|
31
|
-
const materializeManifest = await
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const materializeManifest = await runObservedStep(eventCtx, materializeStep, async () => {
|
|
39
|
+
progress.step("materialize harness task");
|
|
40
|
+
return materializeTaskSpec({
|
|
41
|
+
repoRoot: options.repoRoot,
|
|
42
|
+
taskSpec: options.taskSpec,
|
|
43
|
+
taskSpecPath: options.taskSpecPath,
|
|
44
|
+
client,
|
|
45
|
+
now,
|
|
46
|
+
});
|
|
37
47
|
});
|
|
38
48
|
progress.step(`materialized ${materializeManifest.harnessTaskId} in ${formatDuration(Date.now() - materializeStartedAt)}`);
|
|
39
49
|
const taskArtifactsDir = path.join(options.repoRoot, ".harness", "tasks", materializeManifest.harnessTaskId, "artifacts");
|
|
40
50
|
await mkdir(taskArtifactsDir, { recursive: true });
|
|
41
51
|
const dagPath = path.join(taskArtifactsDir, `${workerRunId}-dag.json`);
|
|
42
52
|
const runRecordPath = path.join(taskArtifactsDir, "worker-run-record.json");
|
|
43
|
-
|
|
53
|
+
const dagGenStep = "dag-run-task";
|
|
44
54
|
const dagGenStartedAt = Date.now();
|
|
45
|
-
await
|
|
46
|
-
"dag"
|
|
47
|
-
"run-task",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
await runObservedStep(eventCtx, dagGenStep, async () => {
|
|
56
|
+
progress.step("dag run-task: generate DAG");
|
|
57
|
+
await runRequiredCommand(options.repoRoot, client, "dag-run-task", [
|
|
58
|
+
"dag",
|
|
59
|
+
"run-task",
|
|
60
|
+
materializeManifest.harnessTaskId,
|
|
61
|
+
"--profile",
|
|
62
|
+
materializeManifest.loopAgentProfile,
|
|
63
|
+
"--strict-models",
|
|
64
|
+
"--output",
|
|
65
|
+
dagPath,
|
|
66
|
+
"--cwd",
|
|
67
|
+
options.repoRoot,
|
|
68
|
+
...noCursorArgs(options.taskSpec),
|
|
69
|
+
], true, undefined, eventCtx);
|
|
70
|
+
});
|
|
58
71
|
progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
|
|
59
72
|
if (options.piModel) {
|
|
60
|
-
|
|
61
|
-
await
|
|
73
|
+
const piModelStep = "pi-model-override";
|
|
74
|
+
await runObservedStep(eventCtx, piModelStep, async () => {
|
|
75
|
+
progress.step(`pi-model override: rewriting executorModels.pi → ${options.piModel}`);
|
|
76
|
+
await applyPiModelOverride(dagPath, options.piModel);
|
|
77
|
+
});
|
|
62
78
|
}
|
|
63
|
-
|
|
79
|
+
const validateStep = "dag-validate";
|
|
64
80
|
const validateStartedAt = Date.now();
|
|
65
|
-
await
|
|
66
|
-
"dag"
|
|
67
|
-
"validate",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
await runObservedStep(eventCtx, validateStep, async () => {
|
|
82
|
+
progress.step("dag validate");
|
|
83
|
+
await runRequiredCommand(options.repoRoot, client, "dag-validate", [
|
|
84
|
+
"dag",
|
|
85
|
+
"validate",
|
|
86
|
+
"--dag",
|
|
87
|
+
dagPath,
|
|
88
|
+
...strictModelsArgs(options.piModel),
|
|
89
|
+
"--strict-governance",
|
|
90
|
+
"--spine-task",
|
|
91
|
+
materializeManifest.harnessTaskId,
|
|
92
|
+
...forbidCursorArgs(options.taskSpec),
|
|
93
|
+
], true, undefined, eventCtx);
|
|
94
|
+
});
|
|
76
95
|
progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
|
|
77
|
-
|
|
96
|
+
const runDagStep = "run-dag";
|
|
78
97
|
const runDagStartedAt = Date.now();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"--lifecycle",
|
|
97
|
-
"all",
|
|
98
|
-
"--json",
|
|
99
|
-
], {
|
|
100
|
-
cwd: options.repoRoot,
|
|
101
|
-
artifactName: "dag-report-json",
|
|
102
|
-
expectJson: true,
|
|
103
|
-
});
|
|
104
|
-
const reportDecision = decideFromReport(workerRunId, reportJson);
|
|
105
|
-
await client.run([
|
|
106
|
-
"dag",
|
|
107
|
-
"report",
|
|
108
|
-
"--run-id",
|
|
109
|
-
workerRunId,
|
|
110
|
-
"--lifecycle",
|
|
111
|
-
"all",
|
|
112
|
-
"--markdown",
|
|
113
|
-
], {
|
|
114
|
-
cwd: options.repoRoot,
|
|
115
|
-
artifactName: "dag-report-markdown",
|
|
98
|
+
const dagEventsPath = path.join(options.repoRoot, ".task-pool", "observability", "runs", workerRunId, "dag-events.jsonl");
|
|
99
|
+
await mkdir(path.dirname(dagEventsPath), { recursive: true }).catch(() => { });
|
|
100
|
+
await runObservedStep(eventCtx, runDagStep, async () => {
|
|
101
|
+
progress.step("run-dag: executing DAG nodes (this is the long step)");
|
|
102
|
+
await runDagWithEventsFallback(options.repoRoot, client, [
|
|
103
|
+
"run-dag",
|
|
104
|
+
"--dag",
|
|
105
|
+
dagPath,
|
|
106
|
+
"--cwd",
|
|
107
|
+
options.repoRoot,
|
|
108
|
+
"--run-id",
|
|
109
|
+
workerRunId,
|
|
110
|
+
"--events-jsonl",
|
|
111
|
+
dagEventsPath,
|
|
112
|
+
...maxConcurrentArgs(options.taskSpec),
|
|
113
|
+
...noCursorArgs(options.taskSpec),
|
|
114
|
+
], resolveRunDagTimeoutMs(options.taskSpec), eventCtx);
|
|
116
115
|
});
|
|
116
|
+
progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
|
|
117
|
+
const reportStep = "report-decision";
|
|
118
|
+
const reportDecision = await runObservedStep(eventCtx, reportStep, async () => {
|
|
119
|
+
const reportJson = await client.run([
|
|
120
|
+
"dag",
|
|
121
|
+
"report",
|
|
122
|
+
"--run-id",
|
|
123
|
+
workerRunId,
|
|
124
|
+
"--lifecycle",
|
|
125
|
+
"all",
|
|
126
|
+
"--json",
|
|
127
|
+
], {
|
|
128
|
+
cwd: options.repoRoot,
|
|
129
|
+
artifactName: "dag-report-json",
|
|
130
|
+
expectJson: true,
|
|
131
|
+
});
|
|
132
|
+
const decision = decideFromReport(workerRunId, reportJson);
|
|
133
|
+
await client.run([
|
|
134
|
+
"dag",
|
|
135
|
+
"report",
|
|
136
|
+
"--run-id",
|
|
137
|
+
workerRunId,
|
|
138
|
+
"--lifecycle",
|
|
139
|
+
"all",
|
|
140
|
+
"--markdown",
|
|
141
|
+
], {
|
|
142
|
+
cwd: options.repoRoot,
|
|
143
|
+
artifactName: "dag-report-markdown",
|
|
144
|
+
});
|
|
145
|
+
return decision;
|
|
146
|
+
}, { statusForResult: (decision) => decision.succeeded ? "succeeded" : "failed" });
|
|
117
147
|
const status = reportDecision.succeeded ? "succeeded" : "failed";
|
|
118
148
|
progress.step(`report decision: ${status} (${reportDecision.reason}${reportDecision.runStatus ? `, status=${reportDecision.runStatus}` : ""})`);
|
|
119
149
|
const failureArtifacts = reportDecision.succeeded
|
|
@@ -123,11 +153,17 @@ export async function runTaskSpec(options) {
|
|
|
123
153
|
repoRoot: options.repoRoot,
|
|
124
154
|
workerRunId,
|
|
125
155
|
taskArtifactsDir,
|
|
156
|
+
eventCtx,
|
|
126
157
|
});
|
|
127
158
|
if (reportDecision.succeeded) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
await runObservedStep(eventCtx, "promote-run", async () => {
|
|
160
|
+
progress.step("promote run");
|
|
161
|
+
await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
|
|
162
|
+
});
|
|
163
|
+
await runObservedStep(eventCtx, "closeout-task", async () => {
|
|
164
|
+
progress.step("closeout task");
|
|
165
|
+
await runRequiredCommand(options.repoRoot, client, "closeout-task", ["closeout", "task", materializeManifest.harnessTaskId], true, undefined, eventCtx);
|
|
166
|
+
});
|
|
131
167
|
}
|
|
132
168
|
const record = {
|
|
133
169
|
schemaVersion: 1,
|
|
@@ -145,6 +181,24 @@ export async function runTaskSpec(options) {
|
|
|
145
181
|
...(failureArtifacts ? { failureArtifacts } : {}),
|
|
146
182
|
};
|
|
147
183
|
await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
184
|
+
emit(progress, {
|
|
185
|
+
type: "artifact.written",
|
|
186
|
+
source: "artifact",
|
|
187
|
+
label: "worker-run-record",
|
|
188
|
+
workerRunId,
|
|
189
|
+
taskId,
|
|
190
|
+
artifactRefs: {
|
|
191
|
+
runRecordPath,
|
|
192
|
+
dagPath,
|
|
193
|
+
...(failureArtifacts
|
|
194
|
+
? {
|
|
195
|
+
reportMarkdown: failureArtifacts.reportMarkdownArtifactPath,
|
|
196
|
+
doctorMarkdown: failureArtifacts.doctorMarkdownArtifactPath,
|
|
197
|
+
closeoutDraft: failureArtifacts.closeoutDraftPath,
|
|
198
|
+
}
|
|
199
|
+
: {}),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
148
202
|
return {
|
|
149
203
|
status,
|
|
150
204
|
workerRunId,
|
|
@@ -164,7 +218,7 @@ export function buildWorkerRunId(businessId, now) {
|
|
|
164
218
|
.slice(0, 6);
|
|
165
219
|
return `wr-${date}-${businessId}-${hash}`;
|
|
166
220
|
}
|
|
167
|
-
async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs) {
|
|
221
|
+
async function runRequiredCommand(repoRoot, client, artifactName, args, expectJson = true, timeoutMs, eventCtx) {
|
|
168
222
|
const result = await client.run(args, {
|
|
169
223
|
cwd: repoRoot,
|
|
170
224
|
artifactName,
|
|
@@ -176,16 +230,99 @@ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJs
|
|
|
176
230
|
}
|
|
177
231
|
return result;
|
|
178
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* `--events-jsonl` is additive observability. Older published controllers do
|
|
235
|
+
* not know the flag; their parser rejects before any DAG node can execute, so
|
|
236
|
+
* retrying the exact command without it is safe and preserves the same run id.
|
|
237
|
+
*/
|
|
238
|
+
async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, eventCtx) {
|
|
239
|
+
const result = await client.run(args, {
|
|
240
|
+
cwd: repoRoot,
|
|
241
|
+
artifactName: "run-dag",
|
|
242
|
+
expectJson: true,
|
|
243
|
+
timeoutMs,
|
|
244
|
+
});
|
|
245
|
+
if (result.ok)
|
|
246
|
+
return result;
|
|
247
|
+
if (!rejectsEventsJsonl(result) || !args.includes("--events-jsonl")) {
|
|
248
|
+
throw new Error(`loop-agent command failed: ${args.join(" ")}`);
|
|
249
|
+
}
|
|
250
|
+
const fallbackArgs = withoutFlagAndValue(args, "--events-jsonl");
|
|
251
|
+
eventCtx.progress.note("run-dag controller does not support --events-jsonl; retrying without DAG event stream");
|
|
252
|
+
const fallback = await client.run(fallbackArgs, {
|
|
253
|
+
cwd: repoRoot,
|
|
254
|
+
artifactName: "run-dag-events-fallback",
|
|
255
|
+
expectJson: true,
|
|
256
|
+
timeoutMs,
|
|
257
|
+
});
|
|
258
|
+
if (!fallback.ok) {
|
|
259
|
+
throw new Error(`loop-agent command failed: ${fallbackArgs.join(" ")}`);
|
|
260
|
+
}
|
|
261
|
+
return fallback;
|
|
262
|
+
}
|
|
263
|
+
function rejectsEventsJsonl(result) {
|
|
264
|
+
return /unknown run-dag flag:\s*--events-jsonl/i.test(`${result.stderr}\n${result.stdout}`);
|
|
265
|
+
}
|
|
266
|
+
function withoutFlagAndValue(args, flag) {
|
|
267
|
+
const index = args.indexOf(flag);
|
|
268
|
+
if (index < 0)
|
|
269
|
+
return args;
|
|
270
|
+
return [...args.slice(0, index), ...args.slice(index + 2)];
|
|
271
|
+
}
|
|
272
|
+
function emit(progress, input) {
|
|
273
|
+
const structured = progress;
|
|
274
|
+
if (typeof structured.event !== "function")
|
|
275
|
+
return;
|
|
276
|
+
void structured.event(input).catch(() => { });
|
|
277
|
+
}
|
|
278
|
+
function emitStepStarted(ctx, stepName) {
|
|
279
|
+
emit(ctx.progress, {
|
|
280
|
+
type: "step.started",
|
|
281
|
+
source: "worker",
|
|
282
|
+
label: stepName,
|
|
283
|
+
spanId: `step:${ctx.workerRunId}:${stepName}`,
|
|
284
|
+
parentSpanId: ctx.workerRunId,
|
|
285
|
+
workerRunId: ctx.workerRunId,
|
|
286
|
+
taskId: ctx.taskId,
|
|
287
|
+
status: "running",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function emitStepFinished(ctx, stepName, status, durationMs) {
|
|
291
|
+
emit(ctx.progress, {
|
|
292
|
+
type: "step.finished",
|
|
293
|
+
source: "worker",
|
|
294
|
+
label: stepName,
|
|
295
|
+
spanId: `step:${ctx.workerRunId}:${stepName}`,
|
|
296
|
+
parentSpanId: ctx.workerRunId,
|
|
297
|
+
workerRunId: ctx.workerRunId,
|
|
298
|
+
taskId: ctx.taskId,
|
|
299
|
+
status,
|
|
300
|
+
durationMs,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async function runObservedStep(ctx, stepName, run, options) {
|
|
304
|
+
const startedAt = Date.now();
|
|
305
|
+
emitStepStarted(ctx, stepName);
|
|
306
|
+
try {
|
|
307
|
+
const result = await run();
|
|
308
|
+
emitStepFinished(ctx, stepName, options?.statusForResult?.(result) ?? "succeeded", Date.now() - startedAt);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
emitStepFinished(ctx, stepName, "failed", Date.now() - startedAt);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
179
316
|
function resolveRunDagTimeoutMs(taskSpec) {
|
|
180
317
|
return Math.min(taskSpec.worker.timeout_ms ?? DEFAULT_RUN_DAG_TIMEOUT_MS, MAX_WORKER_TIMEOUT_MS);
|
|
181
318
|
}
|
|
182
319
|
async function collectFailureArtifacts(input) {
|
|
183
|
-
const doctor = await input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
|
|
320
|
+
const doctor = await runObservedStep(input.eventCtx, "dag-doctor", () => input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
|
|
184
321
|
cwd: input.repoRoot,
|
|
185
322
|
artifactName: "dag-doctor-markdown",
|
|
186
|
-
});
|
|
323
|
+
}), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
|
|
187
324
|
const closeoutDraftPath = path.join(input.repoRoot, ".task-pool", "failure-handoffs", `${input.workerRunId}-failure-closeout-draft.md`);
|
|
188
|
-
await input.client.run([
|
|
325
|
+
await runObservedStep(input.eventCtx, "closeout-draft", () => input.client.run([
|
|
189
326
|
"dag",
|
|
190
327
|
"closeout-draft",
|
|
191
328
|
"--run-id",
|
|
@@ -195,8 +332,9 @@ async function collectFailureArtifacts(input) {
|
|
|
195
332
|
], {
|
|
196
333
|
cwd: input.repoRoot,
|
|
197
334
|
artifactName: "dag-closeout-draft",
|
|
198
|
-
});
|
|
199
|
-
const reportMarkdown = input.client instanceof
|
|
335
|
+
}), { statusForResult: (result) => (result.ok ? "succeeded" : "failed") });
|
|
336
|
+
const reportMarkdown = input.client instanceof ObservedRunTaskClient ||
|
|
337
|
+
input.client instanceof RecordingRunTaskClient
|
|
200
338
|
? input.client.findArtifact("dag-report-markdown")
|
|
201
339
|
: undefined;
|
|
202
340
|
return {
|
|
@@ -298,6 +436,100 @@ class RecordingRunTaskClient {
|
|
|
298
436
|
return this.records.find((record) => record.artifactName === artifactName);
|
|
299
437
|
}
|
|
300
438
|
}
|
|
439
|
+
class ObservedRunTaskClient {
|
|
440
|
+
delegate;
|
|
441
|
+
eventCtx;
|
|
442
|
+
constructor(delegate, eventCtx) {
|
|
443
|
+
this.delegate = delegate;
|
|
444
|
+
this.eventCtx = eventCtx;
|
|
445
|
+
}
|
|
446
|
+
run(args, options) {
|
|
447
|
+
return this.runObserved(args, options, (observedOptions) => this.delegate.run(args, observedOptions));
|
|
448
|
+
}
|
|
449
|
+
runExternal(command, args, options) {
|
|
450
|
+
return this.runObserved([command, ...args], options, (observedOptions) => this.delegate.runExternal(command, args, observedOptions));
|
|
451
|
+
}
|
|
452
|
+
findArtifact(artifactName) {
|
|
453
|
+
return this.delegate.findArtifact(artifactName);
|
|
454
|
+
}
|
|
455
|
+
async runObserved(command, options, run) {
|
|
456
|
+
const observedOptions = {
|
|
457
|
+
...options,
|
|
458
|
+
onSpawn: (info) => {
|
|
459
|
+
options.onSpawn?.(info);
|
|
460
|
+
emit(this.eventCtx.progress, {
|
|
461
|
+
type: "command.started",
|
|
462
|
+
source: "loop-agent-command",
|
|
463
|
+
label: options.artifactName,
|
|
464
|
+
workerRunId: this.eventCtx.workerRunId,
|
|
465
|
+
taskId: this.eventCtx.taskId,
|
|
466
|
+
pid: info.pid,
|
|
467
|
+
timeoutMs: options.timeoutMs,
|
|
468
|
+
command,
|
|
469
|
+
artifactRefs: info.artifactRefs,
|
|
470
|
+
status: "running",
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
onStdout: (chunk) => {
|
|
474
|
+
options.onStdout?.(chunk);
|
|
475
|
+
this.emitOutput(options.artifactName, chunk);
|
|
476
|
+
},
|
|
477
|
+
onStderr: (chunk) => {
|
|
478
|
+
options.onStderr?.(chunk);
|
|
479
|
+
this.emitOutput(options.artifactName, chunk);
|
|
480
|
+
},
|
|
481
|
+
onHeartbeat: (info) => {
|
|
482
|
+
options.onHeartbeat?.(info);
|
|
483
|
+
emit(this.eventCtx.progress, {
|
|
484
|
+
type: "command.heartbeat",
|
|
485
|
+
source: "loop-agent-command",
|
|
486
|
+
label: options.artifactName,
|
|
487
|
+
workerRunId: this.eventCtx.workerRunId,
|
|
488
|
+
taskId: this.eventCtx.taskId,
|
|
489
|
+
message: `elapsed ${info.elapsedMs}ms`,
|
|
490
|
+
});
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
try {
|
|
494
|
+
const result = await run(observedOptions);
|
|
495
|
+
emit(this.eventCtx.progress, {
|
|
496
|
+
type: "command.finished",
|
|
497
|
+
source: "loop-agent-command",
|
|
498
|
+
label: options.artifactName,
|
|
499
|
+
workerRunId: this.eventCtx.workerRunId,
|
|
500
|
+
taskId: this.eventCtx.taskId,
|
|
501
|
+
exitCode: result.exitCode,
|
|
502
|
+
timedOut: result.timedOut,
|
|
503
|
+
durationMs: result.durationMs,
|
|
504
|
+
artifactRefs: result.artifacts,
|
|
505
|
+
status: result.ok ? "succeeded" : "failed",
|
|
506
|
+
});
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
emit(this.eventCtx.progress, {
|
|
511
|
+
type: "command.finished",
|
|
512
|
+
source: "loop-agent-command",
|
|
513
|
+
label: options.artifactName,
|
|
514
|
+
workerRunId: this.eventCtx.workerRunId,
|
|
515
|
+
taskId: this.eventCtx.taskId,
|
|
516
|
+
status: "failed",
|
|
517
|
+
message: errorMessage(error),
|
|
518
|
+
});
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
emitOutput(label, chunk) {
|
|
523
|
+
emit(this.eventCtx.progress, {
|
|
524
|
+
type: "command.output",
|
|
525
|
+
source: "loop-agent-command",
|
|
526
|
+
label,
|
|
527
|
+
workerRunId: this.eventCtx.workerRunId,
|
|
528
|
+
taskId: this.eventCtx.taskId,
|
|
529
|
+
outputPreview: redactForPreview(truncatePreview(chunk)),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
301
533
|
function commandRecordName(args, artifactName) {
|
|
302
534
|
if (args[0] === "new-task")
|
|
303
535
|
return "new-task";
|
|
@@ -357,3 +589,6 @@ function readProperty(value, key) {
|
|
|
357
589
|
return undefined;
|
|
358
590
|
return value[key];
|
|
359
591
|
}
|
|
592
|
+
function errorMessage(error) {
|
|
593
|
+
return error instanceof Error ? error.message : String(error);
|
|
594
|
+
}
|