@tea-agent/loop-agent 0.8.0 → 0.10.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 -0
- package/CHANGELOG.md +51 -1
- package/README.md +20 -0
- package/dist/application/dag/args.js +9 -2
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/dag-reconcile-run.js +118 -0
- package/dist/commands/init.js +12 -3
- package/dist/executors/shell-executor.js +74 -8
- package/dist/governance/manifest-types.js +4 -0
- package/dist/shared/reference-context.js +48 -22
- package/dist/task/config-types.js +1 -1
- package/dist/task/runtime.js +1 -1
- package/dist/worker/cli.js +216 -0
- package/dist/worker/closeout/apply.js +73 -0
- package/dist/worker/closeout/preview.js +30 -0
- package/dist/worker/delivery/final-verification.js +158 -0
- package/dist/worker/delivery/git-transaction.js +354 -0
- package/dist/worker/delivery/package.js +449 -0
- package/dist/worker/feature/decision-loader.js +68 -0
- package/dist/worker/feature/discover.js +14 -0
- package/dist/worker/feature/next-action.js +74 -0
- package/dist/worker/feature/reducer.js +133 -0
- package/dist/worker/feature/review.js +502 -0
- package/dist/worker/feature/run.js +313 -0
- package/dist/worker/feature/types.js +1 -0
- package/dist/worker/follow-up/approve.js +270 -0
- package/dist/worker/follow-up/factory.js +234 -0
- package/dist/worker/follow-up/paths.js +25 -0
- package/dist/worker/follow-up/policy.js +26 -0
- package/dist/worker/follow-up/schema.js +93 -0
- package/dist/worker/follow-up/store.js +96 -0
- package/dist/worker/loop-agent/loop-agent-client.js +51 -10
- package/dist/worker/metrics/projector.js +139 -0
- package/dist/worker/observability/read-model.js +256 -15
- package/dist/worker/observe/paths.js +17 -5
- package/dist/worker/observe/routes.js +78 -20
- package/dist/worker/observe/server.js +8 -6
- package/dist/worker/observe/static/app.js +1045 -177
- package/dist/worker/observe/static/index.html +70 -43
- package/dist/worker/observe/static/styles.css +553 -610
- package/dist/worker/pool/run-store.js +14 -2
- package/dist/worker/pool/validation.js +59 -0
- package/dist/worker/report/morning-report.js +41 -6
- package/dist/worker/run-task/run-task.js +1 -1
- package/dist/worker/runner/run-ready.js +19 -5
- package/dist/workflows/dag/init-hybrid.js +3 -1
- package/dist/workflows/dag/lifecycle.js +146 -0
- package/dist/workflows/dag/node-execution.js +3 -0
- package/dist/workflows/dag/prompt.js +16 -0
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/runner.js +133 -104
- package/dist/workflows/dag/types.js +3 -0
- package/docs/README.md +21 -0
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/design/README.md +13 -7
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +15 -0
- package/docs/loop-agent-harness.md +45 -2
- package/docs/progress/README.md +2 -0
- package/docs/reports/README.md +13 -0
- package/docs/templates/agent-dag-report.schema.json +5 -3
- package/docs/templates/harness.schema.json +7 -2
- package/docs/templates/init-evolution-review.md +4 -2
- package/docs/verification-matrix.md +7 -0
- package/harness.json +4 -3
- package/package.json +4 -2
- package/scripts/check-product-line-docs.sh +7 -3
- package/scripts/check-task-pool-root.sh +1 -1
- package/skills/init-capability-evolution/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +21 -0
- package/skills/loop-agent/references/hybrid-dag.md +4 -3
- package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseWorkerEventLine } from "../observability/events.js";
|
|
5
5
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
6
6
|
import { buildGlobalSnapshot } from "../observability/read-model.js";
|
|
7
7
|
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
8
8
|
import { resolveArtifactPath, toRepoRelativeArtifactPath, } from "./paths.js";
|
|
9
|
+
export function createObserveSnapshotCache() {
|
|
10
|
+
return { expiresAt: 0 };
|
|
11
|
+
}
|
|
9
12
|
const ROUTES = [
|
|
10
13
|
{ method: "GET", pattern: /^\/api\/health$/, handler: handleHealth },
|
|
11
14
|
{ method: "GET", pattern: /^\/api\/snapshot$/, handler: handleSnapshot },
|
|
@@ -99,16 +102,38 @@ async function handleHealth(_req, res, _match, ctx) {
|
|
|
99
102
|
generatedAt: new Date().toISOString(),
|
|
100
103
|
});
|
|
101
104
|
}
|
|
105
|
+
async function getSnapshot(ctx) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (ctx.snapshotCache.snapshot && now < ctx.snapshotCache.expiresAt) {
|
|
108
|
+
return ctx.snapshotCache.snapshot;
|
|
109
|
+
}
|
|
110
|
+
if (ctx.snapshotCache.inFlight) {
|
|
111
|
+
return ctx.snapshotCache.inFlight;
|
|
112
|
+
}
|
|
113
|
+
const inFlight = buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
|
|
114
|
+
ctx.snapshotCache.inFlight = inFlight;
|
|
115
|
+
try {
|
|
116
|
+
const snapshot = await inFlight;
|
|
117
|
+
ctx.snapshotCache.snapshot = snapshot;
|
|
118
|
+
ctx.snapshotCache.expiresAt = Date.now() + ctx.pollIntervalMs;
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (ctx.snapshotCache.inFlight === inFlight) {
|
|
123
|
+
ctx.snapshotCache.inFlight = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
async function handleSnapshot(_req, res, _match, ctx) {
|
|
103
|
-
const snapshot = await
|
|
128
|
+
const snapshot = await getSnapshot(ctx);
|
|
104
129
|
sendJson(res, 200, snapshot);
|
|
105
130
|
}
|
|
106
131
|
async function handleBatches(_req, res, _match, ctx) {
|
|
107
|
-
const snapshot = await
|
|
132
|
+
const snapshot = await getSnapshot(ctx);
|
|
108
133
|
sendJson(res, 200, snapshot.batches);
|
|
109
134
|
}
|
|
110
135
|
async function handleBatchById(_req, res, match, ctx) {
|
|
111
|
-
const snapshot = await
|
|
136
|
+
const snapshot = await getSnapshot(ctx);
|
|
112
137
|
const batch = snapshot.batches.find((b) => b.batchRunId === match.params.id);
|
|
113
138
|
if (!batch) {
|
|
114
139
|
sendJson(res, 404, { error: "Batch not found" });
|
|
@@ -117,11 +142,11 @@ async function handleBatchById(_req, res, match, ctx) {
|
|
|
117
142
|
sendJson(res, 200, batch);
|
|
118
143
|
}
|
|
119
144
|
async function handleTasks(_req, res, _match, ctx) {
|
|
120
|
-
const snapshot = await
|
|
145
|
+
const snapshot = await getSnapshot(ctx);
|
|
121
146
|
sendJson(res, 200, snapshot.tasks);
|
|
122
147
|
}
|
|
123
148
|
async function handleTaskById(_req, res, match, ctx) {
|
|
124
|
-
const snapshot = await
|
|
149
|
+
const snapshot = await getSnapshot(ctx);
|
|
125
150
|
const task = snapshot.tasks.find((t) => t.taskId === match.params.id);
|
|
126
151
|
if (!task) {
|
|
127
152
|
sendJson(res, 404, { error: "Task not found" });
|
|
@@ -134,7 +159,7 @@ async function handleRunById(_req, res, match, ctx) {
|
|
|
134
159
|
sendJson(res, 400, { error: "Invalid run identifier" });
|
|
135
160
|
return;
|
|
136
161
|
}
|
|
137
|
-
const snapshot = await
|
|
162
|
+
const snapshot = await getSnapshot(ctx);
|
|
138
163
|
const task = findRunByWorkerRunId(snapshot.tasks, snapshot.batches, match.params.id);
|
|
139
164
|
if (!task) {
|
|
140
165
|
sendJson(res, 404, { error: "Run not found" });
|
|
@@ -159,7 +184,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
|
|
|
159
184
|
sendJson(res, 400, { error: "Invalid run identifier" });
|
|
160
185
|
return;
|
|
161
186
|
}
|
|
162
|
-
const snapshot = await
|
|
187
|
+
const snapshot = await getSnapshot(ctx);
|
|
163
188
|
const task = findRunByWorkerRunId(snapshot.tasks, snapshot.batches, workerRunId);
|
|
164
189
|
const artifacts = [];
|
|
165
190
|
const seen = new Set();
|
|
@@ -190,7 +215,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
|
|
|
190
215
|
sendJson(res, 200, { artifacts });
|
|
191
216
|
}
|
|
192
217
|
async function handleDagRunById(_req, res, match, ctx) {
|
|
193
|
-
const snapshot = await
|
|
218
|
+
const snapshot = await getSnapshot(ctx);
|
|
194
219
|
const dagRun = snapshot.dagRuns.find((d) => d.dagRunId === match.params.id);
|
|
195
220
|
if (!dagRun) {
|
|
196
221
|
sendJson(res, 404, { error: "DAG run not found" });
|
|
@@ -206,7 +231,7 @@ async function handleDagNodeSessionEvents(_req, res, match, ctx) {
|
|
|
206
231
|
return;
|
|
207
232
|
}
|
|
208
233
|
const after = parseNonNegativeInt(match.query.get("after"), 0);
|
|
209
|
-
const snapshot = await
|
|
234
|
+
const snapshot = await getSnapshot(ctx);
|
|
210
235
|
const dagRun = snapshot.dagRuns.find((d) => d.dagRunId === dagRunId);
|
|
211
236
|
const eventsPath = resolveSessionEventsPath(ctx.repoRoot, dagRunId, nodeId, dagRun?.dagPath);
|
|
212
237
|
if (!eventsPath) {
|
|
@@ -258,9 +283,29 @@ async function handleEventStream(req, res, match, ctx) {
|
|
|
258
283
|
const observabilityRoot = path.join(getTaskPoolRoot(ctx.repoRoot), "observability");
|
|
259
284
|
const eventsPath = path.join(observabilityRoot, "events.jsonl");
|
|
260
285
|
let offset = 0;
|
|
286
|
+
let knownFileSize;
|
|
287
|
+
let timer;
|
|
288
|
+
let closed = false;
|
|
261
289
|
const sendMatchingEvents = async (initial) => {
|
|
290
|
+
let currentFileSize;
|
|
291
|
+
try {
|
|
292
|
+
currentFileSize = (await stat(eventsPath)).size;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
knownFileSize = undefined;
|
|
296
|
+
offset = 0;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!shouldReadSseEventFile(knownFileSize, currentFileSize))
|
|
300
|
+
return;
|
|
301
|
+
if (knownFileSize !== undefined && currentFileSize < knownFileSize) {
|
|
302
|
+
offset = 0;
|
|
303
|
+
}
|
|
262
304
|
const page = await readFilteredEvents(eventsPath, offset, batchRunId, ctx.repoRoot);
|
|
305
|
+
if (!page.ok)
|
|
306
|
+
return;
|
|
263
307
|
offset = page.nextOffset;
|
|
308
|
+
knownFileSize = currentFileSize;
|
|
264
309
|
for (const event of page.events) {
|
|
265
310
|
if (initial || !res.writableEnded) {
|
|
266
311
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
@@ -268,15 +313,25 @@ async function handleEventStream(req, res, match, ctx) {
|
|
|
268
313
|
}
|
|
269
314
|
};
|
|
270
315
|
await sendMatchingEvents(true);
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
316
|
+
const scheduleNextPoll = () => {
|
|
317
|
+
if (closed)
|
|
318
|
+
return;
|
|
319
|
+
timer = setTimeout(() => {
|
|
320
|
+
timer = undefined;
|
|
321
|
+
void sendMatchingEvents(false)
|
|
322
|
+
.then(scheduleNextPoll)
|
|
323
|
+
.catch(() => {
|
|
324
|
+
closed = true;
|
|
325
|
+
if (!res.writableEnded)
|
|
326
|
+
res.end();
|
|
327
|
+
});
|
|
328
|
+
}, ctx.pollIntervalMs);
|
|
329
|
+
};
|
|
330
|
+
scheduleNextPoll();
|
|
278
331
|
req.on("close", () => {
|
|
279
|
-
|
|
332
|
+
closed = true;
|
|
333
|
+
if (timer)
|
|
334
|
+
clearTimeout(timer);
|
|
280
335
|
if (!res.writableEnded)
|
|
281
336
|
res.end();
|
|
282
337
|
});
|
|
@@ -368,12 +423,15 @@ async function readFilteredEvents(filePath, offset, batchRunId, repoRoot) {
|
|
|
368
423
|
events.push(normalizeEventArtifactRefs(repoRoot, event));
|
|
369
424
|
}
|
|
370
425
|
}
|
|
371
|
-
return { events, nextOffset: lines.length };
|
|
426
|
+
return { events, nextOffset: lines.length, ok: true };
|
|
372
427
|
}
|
|
373
428
|
catch {
|
|
374
|
-
return { events: [], nextOffset: offset };
|
|
429
|
+
return { events: [], nextOffset: offset, ok: false };
|
|
375
430
|
}
|
|
376
431
|
}
|
|
432
|
+
export function shouldReadSseEventFile(previousSize, currentSize) {
|
|
433
|
+
return previousSize !== currentSize;
|
|
434
|
+
}
|
|
377
435
|
function normalizeEventArtifactRefs(repoRoot, value) {
|
|
378
436
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
379
437
|
return value;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { handleRequest, serveStatic } from "./routes.js";
|
|
4
|
+
import { createObserveSnapshotCache, handleRequest, serveStatic, } from "./routes.js";
|
|
5
5
|
const STATIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
6
6
|
export function createObserveServer(options) {
|
|
7
7
|
const repoRoot = path.resolve(options.repoRoot);
|
|
@@ -11,6 +11,12 @@ export function createObserveServer(options) {
|
|
|
11
11
|
}
|
|
12
12
|
const port = options.port ?? 0;
|
|
13
13
|
const pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
14
|
+
const routeContext = {
|
|
15
|
+
repoRoot,
|
|
16
|
+
pollIntervalMs,
|
|
17
|
+
staticDir: STATIC_DIR,
|
|
18
|
+
snapshotCache: createObserveSnapshotCache(),
|
|
19
|
+
};
|
|
14
20
|
return new Promise((resolve, reject) => {
|
|
15
21
|
const server = createServer((req, res) => {
|
|
16
22
|
void dispatch(req, res).catch((error) => {
|
|
@@ -27,11 +33,7 @@ export function createObserveServer(options) {
|
|
|
27
33
|
});
|
|
28
34
|
});
|
|
29
35
|
async function dispatch(req, res) {
|
|
30
|
-
const handled = await handleRequest(req, res,
|
|
31
|
-
repoRoot,
|
|
32
|
-
pollIntervalMs,
|
|
33
|
-
staticDir: STATIC_DIR,
|
|
34
|
-
});
|
|
36
|
+
const handled = await handleRequest(req, res, routeContext);
|
|
35
37
|
if (!handled) {
|
|
36
38
|
await serveStatic(req, res, STATIC_DIR);
|
|
37
39
|
}
|