@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.
Files changed (74) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +51 -1
  3. package/README.md +20 -0
  4. package/dist/application/dag/args.js +9 -2
  5. package/dist/cli/command-definitions.js +7 -0
  6. package/dist/cli/program.js +6 -1
  7. package/dist/commands/dag-reconcile-run.js +118 -0
  8. package/dist/commands/init.js +12 -3
  9. package/dist/executors/shell-executor.js +74 -8
  10. package/dist/governance/manifest-types.js +4 -0
  11. package/dist/shared/reference-context.js +48 -22
  12. package/dist/task/config-types.js +1 -1
  13. package/dist/task/runtime.js +1 -1
  14. package/dist/worker/cli.js +216 -0
  15. package/dist/worker/closeout/apply.js +73 -0
  16. package/dist/worker/closeout/preview.js +30 -0
  17. package/dist/worker/delivery/final-verification.js +158 -0
  18. package/dist/worker/delivery/git-transaction.js +354 -0
  19. package/dist/worker/delivery/package.js +449 -0
  20. package/dist/worker/feature/decision-loader.js +68 -0
  21. package/dist/worker/feature/discover.js +14 -0
  22. package/dist/worker/feature/next-action.js +74 -0
  23. package/dist/worker/feature/reducer.js +133 -0
  24. package/dist/worker/feature/review.js +502 -0
  25. package/dist/worker/feature/run.js +313 -0
  26. package/dist/worker/feature/types.js +1 -0
  27. package/dist/worker/follow-up/approve.js +270 -0
  28. package/dist/worker/follow-up/factory.js +234 -0
  29. package/dist/worker/follow-up/paths.js +25 -0
  30. package/dist/worker/follow-up/policy.js +26 -0
  31. package/dist/worker/follow-up/schema.js +93 -0
  32. package/dist/worker/follow-up/store.js +96 -0
  33. package/dist/worker/loop-agent/loop-agent-client.js +51 -10
  34. package/dist/worker/metrics/projector.js +139 -0
  35. package/dist/worker/observability/read-model.js +256 -15
  36. package/dist/worker/observe/paths.js +17 -5
  37. package/dist/worker/observe/routes.js +78 -20
  38. package/dist/worker/observe/server.js +8 -6
  39. package/dist/worker/observe/static/app.js +1045 -177
  40. package/dist/worker/observe/static/index.html +70 -43
  41. package/dist/worker/observe/static/styles.css +553 -610
  42. package/dist/worker/pool/run-store.js +14 -2
  43. package/dist/worker/pool/validation.js +59 -0
  44. package/dist/worker/report/morning-report.js +41 -6
  45. package/dist/worker/run-task/run-task.js +1 -1
  46. package/dist/worker/runner/run-ready.js +19 -5
  47. package/dist/workflows/dag/init-hybrid.js +3 -1
  48. package/dist/workflows/dag/lifecycle.js +146 -0
  49. package/dist/workflows/dag/node-execution.js +3 -0
  50. package/dist/workflows/dag/prompt.js +16 -0
  51. package/dist/workflows/dag/report.js +2 -0
  52. package/dist/workflows/dag/runner.js +133 -104
  53. package/dist/workflows/dag/types.js +3 -0
  54. package/docs/README.md +21 -0
  55. package/docs/agent-dag-recovery-playbook.md +1 -1
  56. package/docs/architecture/runtime-boundaries.md +3 -2
  57. package/docs/design/README.md +13 -7
  58. package/docs/exec-plans/active/README.md +2 -2
  59. package/docs/exec-plans/completed/README.md +15 -0
  60. package/docs/loop-agent-harness.md +45 -2
  61. package/docs/progress/README.md +2 -0
  62. package/docs/reports/README.md +13 -0
  63. package/docs/templates/agent-dag-report.schema.json +5 -3
  64. package/docs/templates/harness.schema.json +7 -2
  65. package/docs/templates/init-evolution-review.md +4 -2
  66. package/docs/verification-matrix.md +7 -0
  67. package/harness.json +4 -3
  68. package/package.json +4 -2
  69. package/scripts/check-product-line-docs.sh +7 -3
  70. package/scripts/check-task-pool-root.sh +1 -1
  71. package/skills/init-capability-evolution/SKILL.md +1 -0
  72. package/skills/loop-agent/references/command-reference.md +21 -0
  73. package/skills/loop-agent/references/hybrid-dag.md +4 -3
  74. 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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 timer = setInterval(() => {
272
- void sendMatchingEvents(false).catch(() => {
273
- clearInterval(timer);
274
- if (!res.writableEnded)
275
- res.end();
276
- });
277
- }, ctx.pollIntervalMs);
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
- clearInterval(timer);
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
  }