@tea-agent/loop-agent 0.10.0 → 0.12.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 +10 -2
- package/CHANGELOG.md +91 -24
- package/README.md +84 -12
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +38 -2
- package/dist/application/dag/run-dag.js +11 -27
- package/dist/application/dag/validate-dag.js +13 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +547 -95
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +488 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +29 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +32 -3
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +338 -83
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +165 -21
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/static/api.js +27 -0
- package/dist/worker/observe/static/app.js +120 -2317
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +128 -0
- package/dist/worker/observe/static/router.js +85 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1720 -495
- package/dist/worker/observe/static/views/batch.js +226 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +477 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +442 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +453 -0
- package/dist/worker/observe/static/views/pool.js +347 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +260 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +797 -27
- package/dist/workflows/dag/node-execution.js +183 -35
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +77 -17
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +529 -0
- package/dist/workflows/dag/types.js +86 -10
- package/dist/workflows/dag/validate.js +73 -12
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +28 -3
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +140 -0
- package/docs/architecture/evolution.md +53 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +43 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +1 -3
- package/docs/exec-plans/completed/README.md +23 -0
- package/docs/feature-workflow.md +78 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +50 -1
- package/docs/reports/README.md +90 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +23 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +70 -3
- package/docs/templates/agent-dag.supervised-implementation.json +9 -8
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +276 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +14 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +36 -20
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -3,9 +3,12 @@ import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHuman
|
|
|
3
3
|
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
4
4
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
5
5
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
6
|
+
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
7
|
+
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
|
+
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
6
9
|
import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
|
|
7
10
|
import { resolveDagNodeSkills } from "./skills.js";
|
|
8
|
-
import { parseRepairArtifactFromText, validateRepairArtifactScope, } from "./repair-artifact.js";
|
|
11
|
+
import { parseRepairArtifactFromText, resolveRepairTaskForGate, validateRepairArtifactScope, } from "./repair-artifact.js";
|
|
9
12
|
import { resolveModelForTask, } from "./types.js";
|
|
10
13
|
export function buildNodePrompt(spec, task, upstream) {
|
|
11
14
|
return buildDagNodePromptEnvelope({
|
|
@@ -17,7 +20,7 @@ export function buildNodePrompt(spec, task, upstream) {
|
|
|
17
20
|
}
|
|
18
21
|
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
19
22
|
const skillNames = resolveDagNodeSkills(spec, task);
|
|
20
|
-
const resolvedSkillInstructions = task.executor === "
|
|
23
|
+
const resolvedSkillInstructions = task.executor === "pi"
|
|
21
24
|
? await resolveDagSkillInstructions(skillNames, {
|
|
22
25
|
cwd,
|
|
23
26
|
includeLearnedPatterns: task.role === "implementer",
|
|
@@ -53,10 +56,6 @@ function assertRepairArtifactVerdictMatchesSupervisor(input) {
|
|
|
53
56
|
throw new Error(`repair artifact gate failed: verdict mismatch between supervisor ${supervisorVerdict} and REPAIR_ARTIFACT_JSON ${input.artifactVerdict}`);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
|
-
function findRepairTaskForGate(input) {
|
|
57
|
-
return Array.from(input.tasksById.values()).find((candidate) => candidate.depends_on.includes(input.gateTask.id) &&
|
|
58
|
-
candidate.id === "repair-cursor");
|
|
59
|
-
}
|
|
60
59
|
function parseSupervisorRepairArtifact(node) {
|
|
61
60
|
const text = `${node?.assistantText ?? ""}\n${node?.stdout ?? ""}`;
|
|
62
61
|
return parseRepairArtifactFromText(text);
|
|
@@ -75,12 +74,21 @@ function validateRepairArtifactGateBeforeShell(input) {
|
|
|
75
74
|
artifactVerdict: parsed.artifact.verdict,
|
|
76
75
|
});
|
|
77
76
|
upstream.repairArtifact = parsed.artifact;
|
|
77
|
+
const resolution = resolveRepairTaskForGate({
|
|
78
|
+
tasks: Array.from(input.tasksById.values()),
|
|
79
|
+
gateTask: input.task,
|
|
80
|
+
});
|
|
81
|
+
if (!resolution.ok) {
|
|
82
|
+
// A pass verdict does not need a repair writer; only fail closed when the
|
|
83
|
+
// supervisor requested a revision that must be routed to a repair node.
|
|
84
|
+
if (parsed.artifact.verdict === "request-revision") {
|
|
85
|
+
throw new Error(`repair artifact gate failed: ${resolution.reason}`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
78
89
|
const scoped = validateRepairArtifactScope({
|
|
79
90
|
artifact: parsed.artifact,
|
|
80
|
-
repairTask:
|
|
81
|
-
tasksById: input.tasksById,
|
|
82
|
-
gateTask: input.task,
|
|
83
|
-
}),
|
|
91
|
+
repairTask: resolution.repairTask,
|
|
84
92
|
});
|
|
85
93
|
if (!scoped.ok) {
|
|
86
94
|
throw new Error(`repair artifact gate failed: ${scoped.reason}`);
|
|
@@ -95,6 +103,15 @@ function recordRepairArtifactForSupervisorNode(input) {
|
|
|
95
103
|
input.node.repairArtifact = parsed.artifact;
|
|
96
104
|
}
|
|
97
105
|
}
|
|
106
|
+
function sleep(ms) {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
|
109
|
+
function sumAttemptMetric(attempts, select) {
|
|
110
|
+
const values = attempts.map(select).filter((value) => value !== undefined);
|
|
111
|
+
return values.length > 0
|
|
112
|
+
? values.reduce((sum, value) => sum + value, 0)
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
98
115
|
async function notifyNodeObserver(observer, event, nodeId, state, chunk) {
|
|
99
116
|
try {
|
|
100
117
|
if (event === "onNodeOutput") {
|
|
@@ -111,6 +128,45 @@ export async function executeDagNode(input) {
|
|
|
111
128
|
const { nodeId, tasksById, state, spec, cwd, runDir, executeNode } = input;
|
|
112
129
|
const task = tasksById.get(nodeId);
|
|
113
130
|
const node = state.nodes[nodeId];
|
|
131
|
+
const failSkillSnapshot = async (error) => {
|
|
132
|
+
const failedAt = new Date().toISOString();
|
|
133
|
+
node.startedAt ??= failedAt;
|
|
134
|
+
node.status = "ERROR";
|
|
135
|
+
node.stderr = error instanceof Error ? error.message : String(error);
|
|
136
|
+
node.failureCategory = "skill-snapshot-integrity";
|
|
137
|
+
node.finishedAt = failedAt;
|
|
138
|
+
node.lastActivityAt = node.finishedAt;
|
|
139
|
+
node.durationMs = Math.max(0, Date.now() - new Date(node.startedAt).getTime());
|
|
140
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
141
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
142
|
+
await input.persistState();
|
|
143
|
+
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
144
|
+
};
|
|
145
|
+
const isDynamicTask = Boolean(task.dynamicExpansion
|
|
146
|
+
|| task.dynamicReduction
|
|
147
|
+
|| task.dynamicCondition
|
|
148
|
+
|| task.dynamicLoopUntil);
|
|
149
|
+
let snapshotPrompt;
|
|
150
|
+
if (state.skillSnapshotRef) {
|
|
151
|
+
try {
|
|
152
|
+
const skillSnapshot = await readSkillSnapshot(runDir, state.skillSnapshotRef, {
|
|
153
|
+
expectedRunId: state.runId,
|
|
154
|
+
});
|
|
155
|
+
assertSkillSnapshotCoversSpec(skillSnapshot, spec, state);
|
|
156
|
+
if (!isDynamicTask) {
|
|
157
|
+
snapshotPrompt = buildNodePromptFromSnapshot({
|
|
158
|
+
spec,
|
|
159
|
+
task,
|
|
160
|
+
upstream: state.nodes,
|
|
161
|
+
snapshot: skillSnapshot,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
await failSkillSnapshot(error);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
114
170
|
node.status = "RUNNING";
|
|
115
171
|
node.startedAt = new Date().toISOString();
|
|
116
172
|
node.lastActivityAt = node.startedAt;
|
|
@@ -119,10 +175,7 @@ export async function executeDagNode(input) {
|
|
|
119
175
|
}
|
|
120
176
|
await input.persistState();
|
|
121
177
|
await notifyNodeObserver(input.observer, "onNodeStart", nodeId, state);
|
|
122
|
-
if (
|
|
123
|
-
task.dynamicReduction ||
|
|
124
|
-
task.dynamicCondition ||
|
|
125
|
-
task.dynamicLoopUntil) {
|
|
178
|
+
if (isDynamicTask) {
|
|
126
179
|
const started = Date.now();
|
|
127
180
|
try {
|
|
128
181
|
const result = await input.executeDynamicNode({
|
|
@@ -165,35 +218,130 @@ export async function executeDagNode(input) {
|
|
|
165
218
|
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
166
219
|
return;
|
|
167
220
|
}
|
|
168
|
-
|
|
221
|
+
let prompt;
|
|
222
|
+
let resolvedSkills;
|
|
223
|
+
try {
|
|
224
|
+
if (snapshotPrompt) {
|
|
225
|
+
prompt = snapshotPrompt.prompt;
|
|
226
|
+
resolvedSkills = snapshotPrompt.resolvedSkills;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
({ prompt, resolvedSkills } =
|
|
230
|
+
await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (!isDagSkillSnapshotIntegrityError(error))
|
|
235
|
+
throw error;
|
|
236
|
+
await failSkillSnapshot(error);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
169
239
|
node.resolvedSkills = resolvedSkills;
|
|
170
240
|
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
171
241
|
const model = resolveModelForTask(task, spec.executorModels);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
const retryPolicy = task.retryPolicy && isSafeReadOnlyPiRetryCandidate(task)
|
|
243
|
+
? task.retryPolicy
|
|
244
|
+
: undefined;
|
|
245
|
+
const maxAttempts = retryPolicy?.maxAttempts ?? 1;
|
|
246
|
+
const attempts = [];
|
|
247
|
+
let totalBackoffMs = 0;
|
|
248
|
+
let terminalResult;
|
|
249
|
+
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
250
|
+
const attemptStartedAt = new Date().toISOString();
|
|
251
|
+
const attemptStarted = Date.now();
|
|
252
|
+
let result;
|
|
253
|
+
try {
|
|
254
|
+
validateRepairArtifactGateBeforeShell({
|
|
255
|
+
task,
|
|
256
|
+
tasksById,
|
|
257
|
+
state,
|
|
258
|
+
});
|
|
259
|
+
result = await executeNode({ task, cwd, model, prompt });
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
result = {
|
|
263
|
+
ok: false,
|
|
264
|
+
stdout: "",
|
|
265
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
266
|
+
durationMs: Date.now() - attemptStarted,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const attemptFinishedAt = new Date().toISOString();
|
|
270
|
+
const attemptRecord = {
|
|
271
|
+
attempt: attemptNumber,
|
|
272
|
+
startedAt: attemptStartedAt,
|
|
273
|
+
finishedAt: attemptFinishedAt,
|
|
274
|
+
durationMs: result.durationMs ?? Date.now() - attemptStarted,
|
|
275
|
+
ok: result.ok,
|
|
276
|
+
stdout: result.stdout,
|
|
277
|
+
stderr: result.stderr,
|
|
278
|
+
assistantText: result.assistantText,
|
|
279
|
+
failureCategory: result.failureCategory,
|
|
280
|
+
backend: result.backend,
|
|
281
|
+
sdkAttempted: result.sdkAttempted,
|
|
282
|
+
tokensUsed: result.tokensUsed,
|
|
283
|
+
parsedEvents: result.parsedEvents,
|
|
284
|
+
artifactPath: `${nodeId}/attempt-${attemptNumber}.json`,
|
|
285
|
+
};
|
|
286
|
+
if (retryPolicy !== undefined) {
|
|
287
|
+
attempts.push(attemptRecord);
|
|
288
|
+
// Persist immutable attempt evidence BEFORE waiting for the next attempt
|
|
289
|
+
// and before applying the result to the node record, so a later success
|
|
290
|
+
// never overwrites prior failure evidence.
|
|
291
|
+
await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
|
|
292
|
+
node.attempts = attempts;
|
|
293
|
+
}
|
|
294
|
+
// Reflect the latest attempt on the node so progress is observable,
|
|
295
|
+
// but keep node.status RUNNING while retry is still possible.
|
|
296
|
+
node.durationMs =
|
|
297
|
+
retryPolicy === undefined
|
|
298
|
+
? attemptRecord.durationMs
|
|
299
|
+
: attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0) +
|
|
300
|
+
totalBackoffMs;
|
|
181
301
|
node.stdout = result.stdout;
|
|
182
302
|
node.stderr = result.stderr;
|
|
183
303
|
node.failureCategory = result.failureCategory;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
304
|
+
node.assistantText = result.assistantText;
|
|
305
|
+
node.backend = result.backend;
|
|
306
|
+
node.sdkAttempted = result.sdkAttempted;
|
|
307
|
+
node.tokensUsed =
|
|
308
|
+
retryPolicy === undefined
|
|
309
|
+
? result.tokensUsed
|
|
310
|
+
: sumAttemptMetric(attempts, (attempt) => attempt.tokensUsed);
|
|
311
|
+
node.parsedEvents =
|
|
312
|
+
retryPolicy === undefined
|
|
313
|
+
? result.parsedEvents
|
|
314
|
+
: sumAttemptMetric(attempts, (attempt) => attempt.parsedEvents);
|
|
315
|
+
node.lastActivityAt = attemptFinishedAt;
|
|
316
|
+
if (retryPolicy !== undefined) {
|
|
317
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
318
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
319
|
+
await input.persistState();
|
|
191
320
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
321
|
+
terminalResult = result;
|
|
322
|
+
if (result.ok)
|
|
323
|
+
break;
|
|
324
|
+
const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
|
|
325
|
+
const retryable = retryPolicy !== undefined &&
|
|
326
|
+
isRetryablePiFailureCategory(result.failureCategory, {
|
|
327
|
+
retryCategories: retryPolicy.retryCategories,
|
|
328
|
+
});
|
|
329
|
+
if (!canRetry || !retryable)
|
|
330
|
+
break;
|
|
331
|
+
const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
|
|
332
|
+
if (delayMs > 0) {
|
|
333
|
+
totalBackoffMs += delayMs;
|
|
334
|
+
await sleep(delayMs);
|
|
335
|
+
// Keep lastActivityAt fresh so backoff wait is not misread as node-quiet.
|
|
336
|
+
node.lastActivityAt = new Date().toISOString();
|
|
337
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
338
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
339
|
+
await input.persistState();
|
|
196
340
|
}
|
|
341
|
+
}
|
|
342
|
+
const result = terminalResult;
|
|
343
|
+
const started = Date.now();
|
|
344
|
+
try {
|
|
197
345
|
recordRepairArtifactForSupervisorNode({
|
|
198
346
|
task,
|
|
199
347
|
node,
|
|
@@ -123,6 +123,97 @@ export function validateRepairArtifactScope(input) {
|
|
|
123
123
|
}
|
|
124
124
|
return { ok: true, artifact };
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Return the list of governed-writer contract violations for a candidate repair
|
|
128
|
+
* node. An empty array means the node is a governed Pi writer eligible to
|
|
129
|
+
* receive a structured repair artifact.
|
|
130
|
+
*/
|
|
131
|
+
export function repairWriterContractIssues(task) {
|
|
132
|
+
const issues = [];
|
|
133
|
+
if (task.executor !== "pi") {
|
|
134
|
+
issues.push(`executor must be pi (got ${task.executor ?? "unset"})`);
|
|
135
|
+
}
|
|
136
|
+
if (task.toolProfile !== "write") {
|
|
137
|
+
issues.push(`toolProfile must be write (got ${task.toolProfile ?? "unset"})`);
|
|
138
|
+
}
|
|
139
|
+
if (task.writePolicy !== "exclusive") {
|
|
140
|
+
issues.push(`writePolicy must be exclusive (got ${task.writePolicy ?? "unset"})`);
|
|
141
|
+
}
|
|
142
|
+
const allowed = task.allowedPaths ?? [];
|
|
143
|
+
const writeSet = task.writeSet ?? [];
|
|
144
|
+
const forbidden = task.forbiddenPaths ?? [];
|
|
145
|
+
if (allowed.length === 0) {
|
|
146
|
+
issues.push("allowedPaths must be non-empty");
|
|
147
|
+
}
|
|
148
|
+
if (writeSet.length === 0) {
|
|
149
|
+
issues.push("writeSet must be non-empty");
|
|
150
|
+
}
|
|
151
|
+
for (const entry of writeSet) {
|
|
152
|
+
const normalized = normalizePath(entry);
|
|
153
|
+
if (forbidden.some((pattern) => pathMatchesPattern(normalized, pattern))) {
|
|
154
|
+
issues.push(`writeSet entry "${entry}" conflicts with forbiddenPaths`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return issues;
|
|
158
|
+
}
|
|
159
|
+
export function isGovernedRepairWriter(task) {
|
|
160
|
+
return repairWriterContractIssues(task).length === 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the repair writer a repair-artifact gate feeds, using the explicit
|
|
164
|
+
* `repairNodeId` reference when present and otherwise deriving a unique safe
|
|
165
|
+
* downstream Pi writer. Never guesses by node name.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveRepairTaskForGate(input) {
|
|
168
|
+
const gate = input.gateTask.shell?.repairArtifactGate;
|
|
169
|
+
if (!gate) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: `task ${input.gateTask.id} has no repairArtifactGate`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const directDownstream = input.tasks.filter((task) => task.depends_on.includes(input.gateTask.id));
|
|
176
|
+
if (gate.repairNodeId) {
|
|
177
|
+
const declared = input.tasks.find((task) => task.id === gate.repairNodeId);
|
|
178
|
+
if (!declared) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
reason: `repair artifact gate "${input.gateTask.id}" declares repairNodeId "${gate.repairNodeId}" but no task with that id exists`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (!declared.depends_on.includes(input.gateTask.id)) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
reason: `repair node "${declared.id}" declared by gate "${input.gateTask.id}" must depend_on the gate`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const issues = repairWriterContractIssues(declared);
|
|
191
|
+
if (issues.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
reason: `repair node "${declared.id}" declared by gate "${input.gateTask.id}" is not a governed Pi writer: ${issues.join("; ")}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, repairTask: declared };
|
|
198
|
+
}
|
|
199
|
+
const candidates = directDownstream.filter(isGovernedRepairWriter);
|
|
200
|
+
if (candidates.length === 1) {
|
|
201
|
+
return { ok: true, repairTask: candidates[0] };
|
|
202
|
+
}
|
|
203
|
+
const candidateIds = directDownstream.map((task) => task.id).join(", ") || "none";
|
|
204
|
+
if (candidates.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
reason: `repair artifact gate "${input.gateTask.id}" has no repairNodeId and no unique governed Pi writer among direct downstream nodes (${candidateIds}); regenerate the DAG with an explicit repairNodeId`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: `repair artifact gate "${input.gateTask.id}" has no repairNodeId and multiple governed Pi writer candidates downstream (${candidates
|
|
213
|
+
.map((task) => task.id)
|
|
214
|
+
.join(", ")}); declare repairNodeId to disambiguate`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
126
217
|
export function formatRepairArtifactForPrompt(artifact) {
|
|
127
218
|
return [
|
|
128
219
|
"Repair artifact:",
|
|
@@ -10,6 +10,7 @@ import { dagNormalizedFailureCategorySchema, normalizeDagFailureCategory, } from
|
|
|
10
10
|
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
11
11
|
import { DAG_RECOVERY_ACTIONS, planDagRecovery, } from "./recovery-recommendation.js";
|
|
12
12
|
import { dagNodeExecutorSchema, dagNodeStatusSchema, LEGACY_TOP_LEVEL_MODELS_ERROR, parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
13
|
+
import { checkRuntimeContractCompatible, DAG_CONTROLLER_CAPABILITIES, } from "./runtime-contract.js";
|
|
13
14
|
export const DAG_REPORT_SCHEMA_VERSION = 1;
|
|
14
15
|
export const DAG_REPORT_SCHEMA_RELATIVE_PATH = "docs/templates/agent-dag-report.schema.json";
|
|
15
16
|
const dagRunLifecycleSchema = z.enum(["active", "paused", "completed"]);
|
|
@@ -169,6 +170,22 @@ const dagRunReportEntrySchema = z
|
|
|
169
170
|
convergence: dagConvergenceReportSchema.optional(),
|
|
170
171
|
pausedByNodeId: z.string().optional(),
|
|
171
172
|
pauseReason: z.string().optional(),
|
|
173
|
+
controllerIdentity: z
|
|
174
|
+
.object({
|
|
175
|
+
status: z.enum(["pinned", "legacy-unpinned"]),
|
|
176
|
+
capturedAt: z.string().min(1),
|
|
177
|
+
packageName: z.string().optional(),
|
|
178
|
+
packageVersion: z.string().optional(),
|
|
179
|
+
packageFingerprint: z.string().optional(),
|
|
180
|
+
binarySha256: z.string().optional(),
|
|
181
|
+
runtimeContractCompatibility: z.enum([
|
|
182
|
+
"compatible",
|
|
183
|
+
"incompatible",
|
|
184
|
+
"legacy-unspecified",
|
|
185
|
+
]),
|
|
186
|
+
runtimeContractReason: z.string().optional(),
|
|
187
|
+
})
|
|
188
|
+
.optional(),
|
|
172
189
|
executorJsonl: dagArtifactRefSchema,
|
|
173
190
|
nodes: z.array(dagNodeReportRowSchema),
|
|
174
191
|
})
|
|
@@ -291,6 +308,38 @@ function resolvePrimaryRecovery(run, primaryNode) {
|
|
|
291
308
|
autoRetryEligible: false,
|
|
292
309
|
};
|
|
293
310
|
}
|
|
311
|
+
function summarizeControllerIdentityForReport(ref, spec) {
|
|
312
|
+
if (!ref)
|
|
313
|
+
return {};
|
|
314
|
+
const compatibility = spec.runtimeContract
|
|
315
|
+
? checkRuntimeContractCompatible(spec.runtimeContract, {
|
|
316
|
+
...DAG_CONTROLLER_CAPABILITIES,
|
|
317
|
+
...(ref.packageVersion
|
|
318
|
+
? { controllerVersion: ref.packageVersion }
|
|
319
|
+
: {}),
|
|
320
|
+
})
|
|
321
|
+
: { ok: true };
|
|
322
|
+
return {
|
|
323
|
+
controllerIdentity: {
|
|
324
|
+
status: ref.status,
|
|
325
|
+
capturedAt: ref.capturedAt,
|
|
326
|
+
...(ref.packageName ? { packageName: ref.packageName } : {}),
|
|
327
|
+
...(ref.packageVersion ? { packageVersion: ref.packageVersion } : {}),
|
|
328
|
+
...(ref.packageFingerprint
|
|
329
|
+
? { packageFingerprint: ref.packageFingerprint }
|
|
330
|
+
: {}),
|
|
331
|
+
...(ref.binarySha256 ? { binarySha256: ref.binarySha256 } : {}),
|
|
332
|
+
runtimeContractCompatibility: spec.runtimeContract
|
|
333
|
+
? compatibility.ok
|
|
334
|
+
? "compatible"
|
|
335
|
+
: "incompatible"
|
|
336
|
+
: "legacy-unspecified",
|
|
337
|
+
...(!compatibility.ok
|
|
338
|
+
? { runtimeContractReason: compatibility.reason }
|
|
339
|
+
: {}),
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
294
343
|
function collectTransitiveDescendantNodeIds(spec, rootNodeId) {
|
|
295
344
|
const childrenByParent = new Map();
|
|
296
345
|
for (const task of spec.tasks) {
|
|
@@ -452,6 +501,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
452
501
|
recoveryRecommendation,
|
|
453
502
|
pausedByNodeId: input.state.pausedByNodeId,
|
|
454
503
|
pauseReason: input.state.pauseReason,
|
|
504
|
+
...summarizeControllerIdentityForReport(input.state.controllerIdentityRef, input.spec),
|
|
455
505
|
nodes,
|
|
456
506
|
};
|
|
457
507
|
const primaryNode = resolvePrimaryFailureNode(partialEntry);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Failure categories that are safe to auto-retry for read-only Pi nodes.
|
|
4
|
+
*
|
|
5
|
+
* These are environmental/transient failures with no repository write
|
|
6
|
+
* side-effects: model connection interruption, provider rate-limiting,
|
|
7
|
+
* temporary provider unavailability, or request timeout.
|
|
8
|
+
*
|
|
9
|
+
* `quota` is intentionally NOT included: a quota exhaustion is an account
|
|
10
|
+
* billing/plan state, not a transient rate limit, and retrying immediately
|
|
11
|
+
* will not help and may waste budget.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DAG_RETRY_CATEGORIES = [
|
|
14
|
+
"timeout",
|
|
15
|
+
"network",
|
|
16
|
+
"rate-limit",
|
|
17
|
+
"unavailable",
|
|
18
|
+
];
|
|
19
|
+
const RETRY_SAFE_PI_ROLES = new Set([
|
|
20
|
+
"planner",
|
|
21
|
+
"scout",
|
|
22
|
+
"reviewer",
|
|
23
|
+
"verifier",
|
|
24
|
+
"closeout",
|
|
25
|
+
]);
|
|
26
|
+
export const dagRetryCategorySchema = z.enum(DEFAULT_DAG_RETRY_CATEGORIES);
|
|
27
|
+
export const dagRetryBackoffSchema = z.enum(["exponential"]);
|
|
28
|
+
/**
|
|
29
|
+
* Opt-in retry policy for a DagTask. Generated only for safe read-only Pi
|
|
30
|
+
* nodes. Validation (validate.ts) rejects retryPolicy on writers, dynamic,
|
|
31
|
+
* shell, static, and decision-gate nodes.
|
|
32
|
+
*/
|
|
33
|
+
export const dagRetryPolicySchema = z
|
|
34
|
+
.object({
|
|
35
|
+
/**
|
|
36
|
+
* Total number of attempts (including the first try). Must be 1..5.
|
|
37
|
+
* A value of 1 disables retry (one attempt, no retry).
|
|
38
|
+
*/
|
|
39
|
+
maxAttempts: z.number().int().positive().max(5),
|
|
40
|
+
backoff: dagRetryBackoffSchema.default("exponential"),
|
|
41
|
+
/** Initial delay before the second attempt, in milliseconds. */
|
|
42
|
+
initialDelayMs: z.number().int().nonnegative().default(2000),
|
|
43
|
+
/** Upper bound on a single backoff delay, in milliseconds. */
|
|
44
|
+
maxDelayMs: z.number().int().nonnegative().default(30000),
|
|
45
|
+
/**
|
|
46
|
+
* Explicit set of failure categories eligible for retry. Defaults to
|
|
47
|
+
* {@link DEFAULT_DAG_RETRY_CATEGORIES}. `quota` and other categories
|
|
48
|
+
* are never retried.
|
|
49
|
+
*/
|
|
50
|
+
retryCategories: z
|
|
51
|
+
.array(dagRetryCategorySchema)
|
|
52
|
+
.default([...DEFAULT_DAG_RETRY_CATEGORIES]),
|
|
53
|
+
})
|
|
54
|
+
.superRefine((policy, ctx) => {
|
|
55
|
+
if (policy.maxDelayMs < policy.initialDelayMs) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
message: "retryPolicy.maxDelayMs must be >= retryPolicy.initialDelayMs",
|
|
59
|
+
path: ["maxDelayMs"],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* The default retry policy applied to safe generated read-only Pi nodes.
|
|
65
|
+
* Total attempts: 3, exponential backoff with cap.
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
|
|
68
|
+
maxAttempts: 3,
|
|
69
|
+
backoff: "exponential",
|
|
70
|
+
initialDelayMs: 2000,
|
|
71
|
+
maxDelayMs: 30000,
|
|
72
|
+
retryCategories: [...DEFAULT_DAG_RETRY_CATEGORIES],
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic helper: is this raw failure category eligible for retry under
|
|
76
|
+
* the given policy? Pure function; executor never decides retry eligibility.
|
|
77
|
+
*/
|
|
78
|
+
export function isRetryablePiFailureCategory(rawFailureCategory, options = {}) {
|
|
79
|
+
if (!rawFailureCategory)
|
|
80
|
+
return false;
|
|
81
|
+
const categories = options.retryCategories ?? DEFAULT_DAG_RETRY_CATEGORIES;
|
|
82
|
+
return categories.includes(rawFailureCategory);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Deterministic helper: is this task a safe candidate for read-only Pi retry?
|
|
86
|
+
*
|
|
87
|
+
* A safe candidate is:
|
|
88
|
+
* - executor === "pi"
|
|
89
|
+
* - role is planner/scout/reviewer/verifier/closeout (never supervisor/implementer)
|
|
90
|
+
* - writePolicy is read-only/none/default and toolProfile is not write
|
|
91
|
+
* - NOT dynamic (no dynamicExpansion/Reduction/Condition/LoopUntil)
|
|
92
|
+
* - NOT a decision gate
|
|
93
|
+
*
|
|
94
|
+
* Writers and source supervisors can have non-idempotent side effects or
|
|
95
|
+
* control-flow meaning and must not auto-retry. Dynamic nodes expand into
|
|
96
|
+
* children and must not retry at the controller level.
|
|
97
|
+
*/
|
|
98
|
+
export function isSafeReadOnlyPiRetryCandidate(task) {
|
|
99
|
+
if (task.executor !== "pi")
|
|
100
|
+
return false;
|
|
101
|
+
if (!task.role || !RETRY_SAFE_PI_ROLES.has(task.role))
|
|
102
|
+
return false;
|
|
103
|
+
if (task.toolProfile === "write")
|
|
104
|
+
return false;
|
|
105
|
+
if (task.writePolicy !== undefined &&
|
|
106
|
+
task.writePolicy !== "read-only" &&
|
|
107
|
+
task.writePolicy !== "none") {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (task.decisionGate?.enabled)
|
|
111
|
+
return false;
|
|
112
|
+
if (task.dynamicExpansion ||
|
|
113
|
+
task.dynamicReduction ||
|
|
114
|
+
task.dynamicCondition ||
|
|
115
|
+
task.dynamicLoopUntil) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compute the backoff delay (in ms) to wait before the given attempt number.
|
|
122
|
+
*
|
|
123
|
+
* attemptNumber is 1-based: the delay returned is the wait BEFORE that attempt
|
|
124
|
+
* runs (i.e. the wait between attempt N-1 and attempt N). attemptNumber <= 1
|
|
125
|
+
* returns 0 (no wait before the first try).
|
|
126
|
+
*
|
|
127
|
+
* Exponential: initialDelayMs * 2^(attemptNumber - 2), capped at maxDelayMs.
|
|
128
|
+
*/
|
|
129
|
+
export function computeBackoffDelayMs(attemptNumber, policy) {
|
|
130
|
+
if (attemptNumber <= 1)
|
|
131
|
+
return 0;
|
|
132
|
+
if (policy.backoff !== "exponential") {
|
|
133
|
+
return Math.min(policy.initialDelayMs, policy.maxDelayMs);
|
|
134
|
+
}
|
|
135
|
+
const exponent = attemptNumber - 2;
|
|
136
|
+
const raw = policy.initialDelayMs * Math.pow(2, exponent);
|
|
137
|
+
return Math.min(raw, policy.maxDelayMs);
|
|
138
|
+
}
|