@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.
Files changed (67) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +48 -38
  3. package/README.md +3 -3
  4. package/dist/application/dag/args.js +9 -1
  5. package/dist/application/dag/run-dag.js +16 -2
  6. package/dist/application/dag/validate-dag.js +14 -1
  7. package/dist/cli/command-definitions.js +22 -4
  8. package/dist/cli/help.js +3 -2
  9. package/dist/cli/program.js +7 -5
  10. package/dist/commands/import-prd.js +76 -0
  11. package/dist/commands/init.js +230 -32
  12. package/dist/commands/instructions.js +90 -58
  13. package/dist/executors/config-core.js +3 -2
  14. package/dist/executors/dag-pi-executor.js +1 -0
  15. package/dist/executors/model-routing.js +43 -0
  16. package/dist/executors/pi-sdk-executor.js +63 -1
  17. package/dist/governance/manifest-types.js +9 -1
  18. package/dist/shared/preview.js +39 -0
  19. package/dist/task/source-references.js +221 -0
  20. package/dist/worker/cli.js +62 -1
  21. package/dist/worker/loop-agent/loop-agent-client.js +97 -5
  22. package/dist/worker/materialize/harness-task-materializer.js +162 -5
  23. package/dist/worker/observability/event-store.js +82 -0
  24. package/dist/worker/observability/events.js +79 -0
  25. package/dist/worker/observability/progress-composite.js +33 -0
  26. package/dist/worker/observability/read-model.js +1013 -0
  27. package/dist/worker/observability/snapshot-store.js +43 -0
  28. package/dist/worker/observability/types.js +1 -0
  29. package/dist/worker/observe/paths.js +64 -0
  30. package/dist/worker/observe/routes.js +423 -0
  31. package/dist/worker/observe/server.js +61 -0
  32. package/dist/worker/observe/static/app.js +1419 -0
  33. package/dist/worker/observe/static/index.html +63 -0
  34. package/dist/worker/observe/static/styles.css +613 -0
  35. package/dist/worker/pool/failure-routing.js +41 -6
  36. package/dist/worker/pool/run-store.js +59 -1
  37. package/dist/worker/progress-reporter.js +0 -18
  38. package/dist/worker/run-task/run-task.js +327 -92
  39. package/dist/worker/runner/run-ready.js +112 -4
  40. package/dist/workflows/dag/event-observer.js +132 -0
  41. package/dist/workflows/dag/init-hybrid.js +150 -26
  42. package/dist/workflows/dag/observer-compose.js +52 -0
  43. package/dist/workflows/dag/skill-instructions.js +4 -0
  44. package/dist/workflows/dag/types.js +1 -1
  45. package/dist/workflows/dag/validate.js +3 -2
  46. package/docs/README.md +2 -0
  47. package/docs/architecture/runtime-boundaries.md +18 -3
  48. package/docs/design/README.md +22 -9
  49. package/docs/exec-plans/active/README.md +6 -1
  50. package/docs/exec-plans/completed/README.md +12 -0
  51. package/docs/init-surface.manifest.json +32 -2
  52. package/docs/loop-agent-harness.md +13 -0
  53. package/docs/reports/README.md +4 -0
  54. package/docs/templates/agent-dag.base.json +1 -1
  55. package/docs/templates/agent-dag.final-verification.json +1 -1
  56. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  57. package/docs/templates/hybrid-dag.json +1 -1
  58. package/docs/templates/worker-dogfood-evidence.md +52 -0
  59. package/docs/templates/worker-dogfood-setup.md +48 -0
  60. package/examples/example-dag.json +1 -1
  61. package/examples/hybrid-loop-agent-dag.json +1 -1
  62. package/harness.json +5 -29
  63. package/package.json +6 -6
  64. package/skills/loop-agent/SKILL.md +5 -3
  65. package/skills/loop-agent/references/command-reference.md +12 -3
  66. package/skills/loop-agent/references/harness-policy.md +7 -3
  67. package/skills/loop-agent/references/task-workflow.md +8 -3
@@ -52,12 +52,14 @@ export function deriveFailureRoute(result) {
52
52
  if (result.status === "succeeded")
53
53
  return undefined;
54
54
  const primaryFailure = readObject(result.reportDecision.primaryFailure);
55
- const category = readProductCategory(primaryFailure, "productLineFailureCategory") ??
56
- readProductCategory(primaryFailure, "productLineCategory") ??
57
- readProductCategory(primaryFailure, "product_line_failure_category") ??
58
- mapDagCategory(readString(primaryFailure, "failureCategory")) ??
59
- mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
60
- categoryFromDecisionReason(result.reportDecision.reason);
55
+ const category = looksLikeExecutorConfigurationFailure(primaryFailure)
56
+ ? "EnvFailure"
57
+ : readProductCategory(primaryFailure, "productLineFailureCategory") ??
58
+ readProductCategory(primaryFailure, "productLineCategory") ??
59
+ readProductCategory(primaryFailure, "product_line_failure_category") ??
60
+ mapDagCategory(readString(primaryFailure, "failureCategory")) ??
61
+ mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
62
+ categoryFromDecisionReason(result.reportDecision.reason);
61
63
  const recommendedFollowUpKind = readString(primaryFailure, "recommendedFollowUpKind") ??
62
64
  readString(primaryFailure, "recommendedFollowUp") ??
63
65
  FOLLOW_UP_BY_CATEGORY[category];
@@ -68,6 +70,18 @@ export function deriveFailureRoute(result) {
68
70
  source: primaryFailure ? "report-primary-failure" : "fallback",
69
71
  };
70
72
  }
73
+ /** Route a failure raised before a DAG report can be produced. */
74
+ export function deriveFailureRouteFromError(message, businessId) {
75
+ const category = looksLikeExecutorConfigurationText(message)
76
+ ? "EnvFailure"
77
+ : "Unknown";
78
+ return {
79
+ category,
80
+ recommendedFollowUpKind: FOLLOW_UP_BY_CATEGORY[category],
81
+ derivedFollowUpTaskId: `${FOLLOW_UP_PREFIX_BY_CATEGORY[category]}-${businessId}`,
82
+ source: "runner-error",
83
+ };
84
+ }
71
85
  function readProductCategory(value, key) {
72
86
  const candidate = readString(value, key);
73
87
  return candidate && PRODUCT_CATEGORIES.has(candidate)
@@ -85,6 +99,27 @@ function categoryFromDecisionReason(reason) {
85
99
  }
86
100
  return "Unknown";
87
101
  }
102
+ function looksLikeExecutorConfigurationFailure(value) {
103
+ if (!value)
104
+ return false;
105
+ return looksLikeExecutorConfigurationText([
106
+ "failureCategory",
107
+ "normalizedFailureCategory",
108
+ "message",
109
+ "error",
110
+ "stderr",
111
+ "detail",
112
+ ]
113
+ .map((key) => readString(value, key))
114
+ .filter((candidate) => Boolean(candidate))
115
+ .join("\n"));
116
+ }
117
+ function looksLikeExecutorConfigurationText(message) {
118
+ const value = message.toLowerCase();
119
+ const executorTerms = /provider|model|executor|api[ _-]?key|credential|thinking level/;
120
+ const configurationTerms = /unknown|invalid|unsupported|missing|not configured|configuration|unavailable/;
121
+ return executorTerms.test(value) && configurationTerms.test(value);
122
+ }
88
123
  function readObject(value) {
89
124
  if (!value || typeof value !== "object" || Array.isArray(value))
90
125
  return undefined;
@@ -22,6 +22,48 @@ export async function findRunByWorkerRunId(repoRoot, workerRunId) {
22
22
  const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
23
23
  return runs.find((run) => run.workerRunId === workerRunId);
24
24
  }
25
+ /**
26
+ * Explicitly requeue a failed task without deleting or rewriting prior evidence.
27
+ * The runner consumes `retryOfWorkerRunId` and derives a guaranteed fresh run id.
28
+ */
29
+ export async function prepareTaskPoolRetry(input) {
30
+ const current = await readTaskPoolState(input.repoRoot, input.taskId);
31
+ if (!current) {
32
+ throw new Error(`cannot retry ${input.taskId}: Task Pool state does not exist`);
33
+ }
34
+ if (current.status !== "Failed") {
35
+ throw new Error(`cannot retry ${input.taskId}: expected Failed state, found ${current.status}`);
36
+ }
37
+ if (!current.workerRunId) {
38
+ throw new Error(`cannot retry ${input.taskId}: failed state is missing workerRunId`);
39
+ }
40
+ const retryRequestedAt = (input.now ?? new Date()).toISOString();
41
+ const result = {
42
+ taskId: input.taskId,
43
+ status: "Ready",
44
+ previousWorkerRunId: current.workerRunId,
45
+ retryRequestedAt,
46
+ ...(input.reason ? { reason: input.reason } : {}),
47
+ };
48
+ await writeTaskPoolState(input.repoRoot, {
49
+ taskId: current.taskId,
50
+ status: "Ready",
51
+ updatedAt: retryRequestedAt,
52
+ retryOfWorkerRunId: current.workerRunId,
53
+ retryRequestedAt,
54
+ ...(input.reason ? { retryReason: input.reason } : {}),
55
+ });
56
+ await ensurePoolDirs(input.repoRoot);
57
+ await appendJsonlFile(getEventsJsonlPath(input.repoRoot), {
58
+ schemaVersion: 1,
59
+ at: retryRequestedAt,
60
+ type: "task-retry-requested",
61
+ taskId: input.taskId,
62
+ previousWorkerRunId: current.workerRunId,
63
+ ...(input.reason ? { reason: input.reason } : {}),
64
+ });
65
+ return result;
66
+ }
25
67
  export async function readTaskPoolState(repoRoot, taskId) {
26
68
  const statePath = getTaskStatePath(repoRoot, taskId);
27
69
  try {
@@ -74,7 +116,15 @@ export async function readJsonlFile(filePath) {
74
116
  }
75
117
  async function appendJsonlFile(filePath, value) {
76
118
  await mkdir(path.dirname(filePath), { recursive: true });
77
- await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf-8");
119
+ try {
120
+ await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf-8");
121
+ }
122
+ catch (error) {
123
+ const detail = error instanceof Error ? error.message : String(error);
124
+ // Include path so callers/tests can identify which JSONL file failed
125
+ // (Node EISDIR messages on Windows often omit the path).
126
+ throw new Error(`failed to append ${filePath}: ${detail}`, { cause: error });
127
+ }
78
128
  }
79
129
  async function ensurePoolDirs(repoRoot) {
80
130
  await mkdir(path.join(getTaskPoolRoot(repoRoot), "runs"), { recursive: true });
@@ -89,11 +139,19 @@ function stateFromRun(run) {
89
139
  workerRunId: run.workerRunId,
90
140
  ...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
91
141
  ...(run.failure ? { failure: run.failure } : {}),
142
+ ...(run.retryOfWorkerRunId
143
+ ? { retryOfWorkerRunId: run.retryOfWorkerRunId }
144
+ : {}),
92
145
  };
93
146
  }
94
147
  function stateStatusFromRun(run) {
95
148
  if (run.status === "succeeded")
96
149
  return "Done";
150
+ // A pre-DAG provider/executor configuration failure is retryable after the
151
+ // operator corrects the environment; do not bury it in a non-retryable Blocked state.
152
+ if (run.status === "run-error" && run.failure?.category === "EnvFailure") {
153
+ return "Failed";
154
+ }
97
155
  if (run.status === "run-error")
98
156
  return "Blocked";
99
157
  return "Failed";
@@ -1,21 +1,3 @@
1
- /**
2
- * Human-readable progress reporting for long-running worker commands.
3
- *
4
- * Why this exists: agent-worker batch runs can take many minutes per task with
5
- * zero stdout/stderr feedback, which looks indistinguishable from a hang. The
6
- * system writes rich state to disk (state.json, run.json, .task-pool artifacts)
7
- * but never echoes it to the triggering terminal, so users assume it is dead.
8
- *
9
- * Design:
10
- * - Progress goes to STDERR only. STDOUT stays reserved for the final
11
- * machine-readable JSON payload, so `... | jq` and pipes keep working.
12
- * - Default ON. `--quiet` disables it. Non-interactive use that only wants JSON
13
- * can pass `--quiet` (or redirect stderr).
14
- * - TTY-aware for ANSI only: if stderr is not a TTY, we still print the lines
15
- * (they are line-buffered text and flush fine) but skip ANSI decoration.
16
- * - A `noop` reporter keeps library callers (and tests that don't care) quiet
17
- * with zero changes to their call sites beyond accepting the option.
18
- */
19
1
  const ANSI = {
20
2
  bold: "\x1b[1m",
21
3
  green: "\x1b[32m",