@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.2
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 +4 -8
- package/CHANGELOG.md +76 -18
- package/README.md +76 -299
- package/dist/application/evaluation/alias.js +184 -0
- package/dist/application/evaluation/budget.js +192 -0
- package/dist/application/evaluation/campaign-hash.js +47 -0
- package/dist/application/evaluation/campaign-matrix.js +372 -0
- package/dist/application/evaluation/campaign-scorecard.js +135 -0
- package/dist/application/evaluation/campaign.js +370 -0
- package/dist/application/evaluation/candidate.js +23 -6
- package/dist/application/evaluation/corpus-hash.js +38 -0
- package/dist/application/evaluation/corpus.js +56 -0
- package/dist/application/evaluation/experiment.js +294 -0
- package/dist/application/evaluation/ignition.js +198 -0
- package/dist/application/evaluation/integrity-audit.js +162 -0
- package/dist/application/evaluation/outer-loop.js +132 -0
- package/dist/application/evaluation/pi-cell-executor.js +39 -0
- package/dist/application/evaluation/private-verifier.js +46 -0
- package/dist/application/evaluation/promotion-policy.js +151 -0
- package/dist/application/evaluation/proposer.js +98 -0
- package/dist/application/evaluation/types.js +522 -0
- package/dist/cli/command-definitions.js +19 -3
- package/dist/commands/dag-reconcile-run.js +3 -116
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +7 -1
- package/dist/executors/dag-pi-executor.js +8 -46
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +213 -30
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -1
- package/dist/infrastructure/evaluation/alias-store.js +199 -0
- package/dist/infrastructure/evaluation/campaign-store.js +154 -0
- package/dist/infrastructure/evaluation/corpus-store.js +181 -0
- package/dist/infrastructure/evaluation/experiment-store.js +124 -0
- package/dist/infrastructure/evaluation/ignition-store.js +82 -0
- package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
- package/dist/infrastructure/evaluation/proposer-store.js +78 -0
- package/dist/records/promotion.js +3 -1
- package/dist/shared/git-progress.js +9 -2
- package/dist/worker/cli.js +83 -0
- package/dist/worker/delivery/git-transaction.js +75 -0
- package/dist/worker/delivery/verification-bundle.js +13 -2
- package/dist/worker/feature/review.js +3 -2
- package/dist/worker/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- package/dist/worker/observe/static/dag-helpers.js +0 -62
- package/dist/worker/observe/static/styles.css +18 -55
- package/dist/worker/observe/static/views/dag.js +13 -5
- package/dist/worker/outcomes/adapters.js +4 -1
- package/dist/worker/outcomes/declared-artifacts.js +103 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +10 -11
- package/dist/worker/outcomes/projector.js +30 -4
- package/dist/worker/outcomes/types.js +3 -0
- package/dist/worker/pool/reconcile.js +285 -0
- package/dist/worker/run-task/run-task.js +81 -4
- package/dist/worker/runner/run-ready.js +25 -2
- package/dist/worker/task-graph/ready-planner.js +14 -8
- package/dist/worker/task-graph/task-graph-schema.js +5 -3
- package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +8 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
- package/dist/workflows/dag/init-hybrid.js +624 -172
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/reconcile-run.js +121 -0
- package/dist/workflows/dag/report.js +12 -0
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/scheduler.js +87 -17
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +49 -1
- package/dist/workflows/dag/validate.js +35 -15
- package/docs/README.md +3 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/init-surface.manifest.json +4 -0
- package/docs/local-development-environment.md +52 -0
- package/docs/templates/agent-dag.schema.json +25 -7
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- package/docs/templates/branch-merge-report.md +14 -0
- package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
- package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
- package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
- package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
- package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
- package/docs/templates/product-line/AGENTS.md +1 -0
- package/docs/templates/product-line/README.md +17 -0
- package/docs/templates/product-line/acceptance.yaml +9 -0
- package/docs/templates/product-line/feature.yaml +11 -0
- package/docs/templates/product-line/task-graph.yaml +8 -0
- package/docs/templates/product-line/task.yaml +4 -0
- package/package.json +2 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/command-reference.md +5 -0
- package/skills/loop-agent/references/hybrid-dag.md +7 -4
|
@@ -103,6 +103,7 @@ export async function curateKnowledgePatterns(input) {
|
|
|
103
103
|
ok: true,
|
|
104
104
|
patternsPath,
|
|
105
105
|
patternCount: 0,
|
|
106
|
+
patterns: [],
|
|
106
107
|
safetyFindings: [],
|
|
107
108
|
message: "no patterns.jsonl found; no proposal generated",
|
|
108
109
|
};
|
|
@@ -139,6 +140,7 @@ export async function curateKnowledgePatterns(input) {
|
|
|
139
140
|
patternsPath,
|
|
140
141
|
outputPath,
|
|
141
142
|
patternCount: patterns.length,
|
|
143
|
+
patterns,
|
|
142
144
|
proposalMarkdown,
|
|
143
145
|
safetyFindings,
|
|
144
146
|
message: "proposal failed skill safety audit",
|
|
@@ -153,6 +155,7 @@ export async function curateKnowledgePatterns(input) {
|
|
|
153
155
|
patternsPath,
|
|
154
156
|
outputPath,
|
|
155
157
|
patternCount: patterns.length,
|
|
158
|
+
patterns,
|
|
156
159
|
proposalMarkdown,
|
|
157
160
|
safetyFindings,
|
|
158
161
|
message: patterns.length === 0
|
|
@@ -4,7 +4,7 @@ import { hostname as localHostname } from "node:os";
|
|
|
4
4
|
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { parseDagSpec } from "./types.js";
|
|
6
6
|
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
7
|
-
import { routeDagFailure } from "./failure-routing.js";
|
|
7
|
+
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
8
8
|
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
9
9
|
"paused",
|
|
10
10
|
"active",
|
|
@@ -663,6 +663,36 @@ function findDoctorFailureNode(state) {
|
|
|
663
663
|
rawFailureCategory: selected[1].failureCategory,
|
|
664
664
|
};
|
|
665
665
|
}
|
|
666
|
+
async function readRunOwnedBackendTestClassification(runDir) {
|
|
667
|
+
try {
|
|
668
|
+
const raw = JSON.parse(await readFile(path.join(runDir, "classify-backend-test-result-pi.json"), "utf-8"));
|
|
669
|
+
const text = raw.assistantText ?? raw.stdout;
|
|
670
|
+
if (typeof text !== "string")
|
|
671
|
+
return undefined;
|
|
672
|
+
const trimmed = text.trim();
|
|
673
|
+
const fenced = trimmed.match(/^```json\s*([\s\S]*?)\s*```$/i);
|
|
674
|
+
const classification = JSON.parse(fenced?.[1] ?? trimmed);
|
|
675
|
+
return typeof classification.category === "string" &&
|
|
676
|
+
dagProductLineFailureCategoryValues.includes(classification.category)
|
|
677
|
+
? classification.category
|
|
678
|
+
: undefined;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function resolveDoctorFailureRouting(input) {
|
|
685
|
+
const classifiedCategory = await readRunOwnedBackendTestClassification(input.runDir);
|
|
686
|
+
if (classifiedCategory) {
|
|
687
|
+
return routeDagFailure({
|
|
688
|
+
rawFailureCategory: classifiedCategory,
|
|
689
|
+
normalizedFailureCategory: "unknown",
|
|
690
|
+
nodeId: input.nodeId,
|
|
691
|
+
productLineFailureCategory: classifiedCategory,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return routeDagFailure(input);
|
|
695
|
+
}
|
|
666
696
|
async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
667
697
|
const located = await locateDagRun(repoRoot, runId);
|
|
668
698
|
if (!located) {
|
|
@@ -675,7 +705,8 @@ async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
|
675
705
|
(state.status === "paused" ? "human-required" : state.failureCategory);
|
|
676
706
|
const failureStatus = state.status === "paused" ? "paused" : failure.status;
|
|
677
707
|
const normalizedCategory = normalizeDagFailureCategory(rawFailureCategory, failureStatus ?? state.status);
|
|
678
|
-
const routing =
|
|
708
|
+
const routing = await resolveDoctorFailureRouting({
|
|
709
|
+
runDir: located.runDir,
|
|
679
710
|
rawFailureCategory,
|
|
680
711
|
normalizedFailureCategory: normalizedCategory,
|
|
681
712
|
nodeId: failure.nodeId,
|
|
@@ -3,29 +3,35 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
|
|
5
5
|
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
6
|
+
import { resolveContextPolicy } from "./context-policy.js";
|
|
6
7
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
7
8
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
8
9
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
9
10
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
10
11
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
11
12
|
import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
|
|
12
|
-
import { resolveDagNodeSkills } from "./skills.js";
|
|
13
13
|
import { parseRepairArtifactFromText, resolveRepairTaskForGate, validateRepairArtifactScope, } from "./repair-artifact.js";
|
|
14
14
|
import { resolveModelForTask, } from "./types.js";
|
|
15
15
|
export function buildNodePrompt(spec, task, upstream) {
|
|
16
|
+
const policy = resolveContextPolicy(spec);
|
|
16
17
|
return buildDagNodePromptEnvelope({
|
|
17
18
|
spec,
|
|
18
19
|
task,
|
|
19
20
|
upstream,
|
|
20
|
-
resolvedSkills:
|
|
21
|
+
resolvedSkills: policy.resolveSkills(spec, task),
|
|
22
|
+
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
21
23
|
});
|
|
22
24
|
}
|
|
23
25
|
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
24
|
-
const
|
|
26
|
+
const policy = resolveContextPolicy(spec);
|
|
27
|
+
const skillNames = policy.resolveSkills(spec, task);
|
|
28
|
+
const budget = policy.resolveSkillInstructionBudget(task);
|
|
25
29
|
const resolvedSkillInstructions = task.executor === "pi"
|
|
26
30
|
? await resolveDagSkillInstructions(skillNames, {
|
|
27
31
|
cwd,
|
|
28
|
-
includeLearnedPatterns:
|
|
32
|
+
includeLearnedPatterns: budget.includeLearnedPatterns,
|
|
33
|
+
perSkillMaxChars: budget.perSkillMaxChars,
|
|
34
|
+
totalMaxChars: budget.totalMaxChars,
|
|
29
35
|
})
|
|
30
36
|
: [];
|
|
31
37
|
return {
|
|
@@ -35,6 +41,7 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
|
|
|
35
41
|
upstream,
|
|
36
42
|
resolvedSkills: skillNames,
|
|
37
43
|
resolvedSkillInstructions,
|
|
44
|
+
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
38
45
|
}),
|
|
39
46
|
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
40
47
|
};
|
|
@@ -6,7 +6,7 @@ export const DAG_AUTHORING_GUIDANCE = [
|
|
|
6
6
|
"Prefer same-rank parallel read-only scouts over unnecessary serial depends_on chains.",
|
|
7
7
|
"Add depends_on only when a child truly needs upstream output; default to independent ranks.",
|
|
8
8
|
"Every task must explicitly declare executor; defaults.executor is schema-only, not a runtime fallback.",
|
|
9
|
-
"
|
|
9
|
+
"Pi is the only governed Agent DAG writer; cursor-prompt is an explicit manual one-shot sidecar and must not enter Loop auto-execute or Delegate writers.",
|
|
10
10
|
"exclusive nodes require narrow, concrete, disjoint writeSet paths; never use ** or repo root.",
|
|
11
11
|
"Read-only nodes must not write repository files, including root artifacts/**; return findings in node output only.",
|
|
12
12
|
"If the DAG is a single linear chain, challenge whether read-only work can run in parallel ranks.",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
3
|
+
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, assertDagRunTransferTargetAvailable, getDagRunDir, locateDagRun, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
4
|
+
export function parseDagReconcileRunArgs(args) {
|
|
5
|
+
let runId;
|
|
6
|
+
let action;
|
|
7
|
+
let reason;
|
|
8
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9
|
+
const arg = args[index];
|
|
10
|
+
if (arg === "--run-id")
|
|
11
|
+
runId = args[++index];
|
|
12
|
+
else if (arg.startsWith("--run-id="))
|
|
13
|
+
runId = arg.slice("--run-id=".length);
|
|
14
|
+
else if (arg === "--action")
|
|
15
|
+
action = parseAction(args[++index]);
|
|
16
|
+
else if (arg.startsWith("--action="))
|
|
17
|
+
action = parseAction(arg.slice("--action=".length));
|
|
18
|
+
else if (arg === "--reason")
|
|
19
|
+
reason = args[++index];
|
|
20
|
+
else if (arg.startsWith("--reason="))
|
|
21
|
+
reason = arg.slice("--reason=".length);
|
|
22
|
+
else if (arg.startsWith("-"))
|
|
23
|
+
throw new Error(`unknown dag reconcile-run flag: ${arg}`);
|
|
24
|
+
else
|
|
25
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
if (!runId)
|
|
28
|
+
throw new Error("dag reconcile-run requires --run-id <id>");
|
|
29
|
+
if (action && !reason?.trim()) {
|
|
30
|
+
throw new Error("dag reconcile-run mutation requires --reason <text>");
|
|
31
|
+
}
|
|
32
|
+
return { runId, action, ...(reason?.trim() ? { reason: reason.trim() } : {}) };
|
|
33
|
+
}
|
|
34
|
+
function parseAction(value) {
|
|
35
|
+
if (value === "supersede" || value === "abandon")
|
|
36
|
+
return value;
|
|
37
|
+
throw new Error("dag reconcile-run --action must be supersede or abandon");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* DAG-kernel reconcile for a single run. Lives in workflows so Worker outer-loop
|
|
41
|
+
* recovery can call it without importing the CLI/commands layer.
|
|
42
|
+
*/
|
|
43
|
+
export async function executeDagReconcileRun(repoRoot, rawArgs) {
|
|
44
|
+
const parsed = parseDagReconcileRunArgs(rawArgs);
|
|
45
|
+
const located = await locateDagRun(repoRoot, parsed.runId);
|
|
46
|
+
if (!located)
|
|
47
|
+
throw new Error(`dag run not found: ${parsed.runId}`);
|
|
48
|
+
const state = await readDagRunState(located.runDir);
|
|
49
|
+
const liveness = assessDagRunLiveness({ state });
|
|
50
|
+
const eligibility = assessDagRunRecoveryEligibility({
|
|
51
|
+
lifecycle: located.lifecycle,
|
|
52
|
+
state,
|
|
53
|
+
liveness: liveness.status,
|
|
54
|
+
});
|
|
55
|
+
if (!parsed.action) {
|
|
56
|
+
return {
|
|
57
|
+
action: "inspect",
|
|
58
|
+
runId: state.runId,
|
|
59
|
+
lifecycle: located.lifecycle,
|
|
60
|
+
status: state.status,
|
|
61
|
+
liveness: liveness.status,
|
|
62
|
+
eligibility,
|
|
63
|
+
nextRecommendedAction: eligibility.canReconcile
|
|
64
|
+
? "Re-run with --action supersede|abandon and --reason <text>."
|
|
65
|
+
: "Do not reconcile this run; inspect runner and lifecycle evidence first.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!eligibility.canReconcile || !eligibility.allowedActions.includes(parsed.action)) {
|
|
69
|
+
throw new Error(`dag run ${state.runId} is not eligible for reconciliation: ${eligibility.reasons.join(", ") || "unknown reason"}`);
|
|
70
|
+
}
|
|
71
|
+
const reconciledAt = new Date().toISOString();
|
|
72
|
+
const completedRunDir = getDagRunDir(repoRoot, "completed", state.runId);
|
|
73
|
+
if (located.lifecycle !== "completed") {
|
|
74
|
+
await assertDagRunTransferTargetAvailable(completedRunDir);
|
|
75
|
+
}
|
|
76
|
+
const artifactRelativePath = path.join(".harness", "dag-runs", "completed", state.runId, "reconciliation.json").replace(/\\/g, "/");
|
|
77
|
+
await writeJsonAtomic(path.join(located.runDir, "reconciliation.json"), {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
runId: state.runId,
|
|
80
|
+
action: parsed.action,
|
|
81
|
+
reason: parsed.reason,
|
|
82
|
+
reconciledAt,
|
|
83
|
+
previousLifecycle: located.lifecycle,
|
|
84
|
+
previousStatus: state.status,
|
|
85
|
+
liveness: liveness.status,
|
|
86
|
+
originalState: state,
|
|
87
|
+
});
|
|
88
|
+
const previousStatus = state.status;
|
|
89
|
+
for (const node of Object.values(state.nodes)) {
|
|
90
|
+
if (node.status !== "RUNNING")
|
|
91
|
+
continue;
|
|
92
|
+
node.status = "ERROR";
|
|
93
|
+
node.finishedAt = reconciledAt;
|
|
94
|
+
node.failureCategory = `operator-${parsed.action}`;
|
|
95
|
+
}
|
|
96
|
+
state.status = parsed.action === "supersede" ? "superseded" : "abandoned";
|
|
97
|
+
state.finishedAt = reconciledAt;
|
|
98
|
+
state.failureCategory = `operator-${parsed.action}`;
|
|
99
|
+
state.reconciliation = {
|
|
100
|
+
action: parsed.action,
|
|
101
|
+
reason: parsed.reason,
|
|
102
|
+
reconciledAt,
|
|
103
|
+
previousStatus,
|
|
104
|
+
previousLifecycle: located.lifecycle,
|
|
105
|
+
liveness: liveness.status,
|
|
106
|
+
artifactPath: artifactRelativePath,
|
|
107
|
+
};
|
|
108
|
+
await writeDagRunState(located.runDir, state);
|
|
109
|
+
const runDir = located.lifecycle === "completed"
|
|
110
|
+
? located.runDir
|
|
111
|
+
: await transferDagRunDir(located.runDir, completedRunDir);
|
|
112
|
+
return {
|
|
113
|
+
action: parsed.action,
|
|
114
|
+
runId: state.runId,
|
|
115
|
+
status: state.status,
|
|
116
|
+
lifecycle: "completed",
|
|
117
|
+
reason: parsed.reason,
|
|
118
|
+
reconciliationArtifactPath: artifactRelativePath,
|
|
119
|
+
runDir,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -148,6 +148,9 @@ const dagNodeReportRowSchema = z
|
|
|
148
148
|
finishedAt: z.string().optional(),
|
|
149
149
|
decisionEnvelope: dagNodeDecisionEnvelopeSchema.optional(),
|
|
150
150
|
artifacts: dagNodeArtifactsReportSchema,
|
|
151
|
+
structuredArtifactPath: z.string().min(1).optional(),
|
|
152
|
+
structuredArtifactSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
153
|
+
structuredArtifactSchemaId: z.string().min(1).optional(),
|
|
151
154
|
})
|
|
152
155
|
.strict();
|
|
153
156
|
const dagRunReportEntrySchema = z
|
|
@@ -460,6 +463,15 @@ export async function buildDagRunReportEntry(input) {
|
|
|
460
463
|
finishedAt: node.finishedAt,
|
|
461
464
|
decisionEnvelope: node.decisionEnvelope,
|
|
462
465
|
artifacts: await buildNodeArtifactsReport(input.runDir, nodeId),
|
|
466
|
+
...(node.structuredArtifactPath
|
|
467
|
+
? { structuredArtifactPath: node.structuredArtifactPath }
|
|
468
|
+
: {}),
|
|
469
|
+
...(node.structuredArtifactSha256
|
|
470
|
+
? { structuredArtifactSha256: node.structuredArtifactSha256 }
|
|
471
|
+
: {}),
|
|
472
|
+
...(node.structuredArtifactSchemaId
|
|
473
|
+
? { structuredArtifactSchemaId: node.structuredArtifactSchemaId }
|
|
474
|
+
: {}),
|
|
463
475
|
});
|
|
464
476
|
}
|
|
465
477
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../application/evaluation/budget.js";
|
|
4
5
|
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
5
6
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
7
|
+
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
6
8
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
7
9
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
8
10
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
@@ -120,7 +122,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
120
122
|
: {}),
|
|
121
123
|
};
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
const state = {
|
|
124
126
|
version: 1,
|
|
125
127
|
title: spec.title,
|
|
126
128
|
runId,
|
|
@@ -147,6 +149,8 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
147
149
|
}
|
|
148
150
|
: {}),
|
|
149
151
|
};
|
|
152
|
+
initRunBudgetLedger(state, spec.budget);
|
|
153
|
+
return state;
|
|
150
154
|
}
|
|
151
155
|
export function assertFrozenEvaluationBinding(spec, state) {
|
|
152
156
|
const declared = spec.evaluation;
|
|
@@ -183,7 +187,7 @@ export async function runDag(spec, opts) {
|
|
|
183
187
|
controllerVersion: runningIdentity.packageVersion,
|
|
184
188
|
});
|
|
185
189
|
const { ranks } = topoSortToRanks(spec);
|
|
186
|
-
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
190
|
+
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
|
|
187
191
|
let runId = opts.runId;
|
|
188
192
|
if (runId) {
|
|
189
193
|
for (const warning of validateDagRunIdFormat(runId).warnings) {
|
|
@@ -268,6 +272,7 @@ export async function resumeDagRun(opts) {
|
|
|
268
272
|
throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
|
|
269
273
|
}
|
|
270
274
|
assertFrozenEvaluationBinding(spec, state);
|
|
275
|
+
assertFrozenBudget(spec.budget, state.budget, state.runId);
|
|
271
276
|
// Runtime contract + controller identity must be re-verified before executing
|
|
272
277
|
// any remaining node on resume; drift fails closed.
|
|
273
278
|
const runningIdentity = resolveRunningControllerIdentity();
|
|
@@ -311,7 +316,7 @@ export async function resumeDagRun(opts) {
|
|
|
311
316
|
catch (error) {
|
|
312
317
|
throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
|
|
313
318
|
}
|
|
314
|
-
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
319
|
+
const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
|
|
315
320
|
state.status = "running";
|
|
316
321
|
const resumedAt = new Date().toISOString();
|
|
317
322
|
state.runner = {
|
|
@@ -410,23 +415,43 @@ async function executeDagCheckpoint(input) {
|
|
|
410
415
|
tasksById,
|
|
411
416
|
meta: { runDir, runId: state.runId, spec },
|
|
412
417
|
}),
|
|
413
|
-
executeScheduledNode: (nodeId, executeNode, onPause) =>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
state
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
418
|
+
executeScheduledNode: async (nodeId, executeNode, onPause) => {
|
|
419
|
+
if (isHardBudgetBreached(state.budgetLedger))
|
|
420
|
+
return;
|
|
421
|
+
const preBreach = preflightBudgetOrBreach(state);
|
|
422
|
+
if (preBreach && isHardBudgetBreached(state.budgetLedger)) {
|
|
423
|
+
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
424
|
+
await persistState();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
await executeDagNode({
|
|
428
|
+
nodeId,
|
|
429
|
+
tasksById,
|
|
430
|
+
state,
|
|
431
|
+
spec,
|
|
432
|
+
cwd,
|
|
433
|
+
runDir,
|
|
434
|
+
executeNode,
|
|
435
|
+
executeDynamicNode,
|
|
436
|
+
observer: input.observer,
|
|
437
|
+
persistState,
|
|
438
|
+
onPause,
|
|
439
|
+
});
|
|
440
|
+
const node = state.nodes[nodeId];
|
|
441
|
+
if (node &&
|
|
442
|
+
(node.status === "FINISHED" || node.status === "ERROR")) {
|
|
443
|
+
recordFinishedNodeBudget(state, node);
|
|
444
|
+
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
445
|
+
await persistState();
|
|
446
|
+
}
|
|
447
|
+
},
|
|
426
448
|
});
|
|
427
449
|
if (pausedByNodeId) {
|
|
428
450
|
break;
|
|
429
451
|
}
|
|
452
|
+
if (isHardBudgetBreached(state.budgetLedger)) {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
430
455
|
const convergenceDecision = await runConvergencePassController({
|
|
431
456
|
spec,
|
|
432
457
|
state,
|
|
@@ -446,6 +471,7 @@ async function executeDagCheckpoint(input) {
|
|
|
446
471
|
}
|
|
447
472
|
state.finishedAt = new Date().toISOString();
|
|
448
473
|
const runDirBeforeTransfer = runDir;
|
|
474
|
+
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
449
475
|
if (pausedByNodeId) {
|
|
450
476
|
state.status = "paused";
|
|
451
477
|
await persistState();
|
|
@@ -466,6 +492,7 @@ async function executeDagCheckpoint(input) {
|
|
|
466
492
|
if (state.convergence) {
|
|
467
493
|
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
468
494
|
}
|
|
495
|
+
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
469
496
|
await persistState({ allowCompletedFactsWrite: true });
|
|
470
497
|
return {
|
|
471
498
|
title: spec.title,
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
import { isPauseOnHumanDecisionGate } from "./decision-envelope.js";
|
|
2
|
+
import { evaluateConditionExpression } from "./dynamic-runtime/condition.js";
|
|
2
3
|
export function isConditionSkippedReason(reason) {
|
|
3
4
|
return Boolean(reason?.startsWith("condition "));
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Dependency readiness for PENDING nodes.
|
|
8
|
+
*
|
|
9
|
+
* Default (`dependsPolicy: all`): any ERROR/SKIPPED dependency → skip (cascades
|
|
10
|
+
* exclusive condition branches). Unsettled → wait. All FINISHED → run.
|
|
11
|
+
*
|
|
12
|
+
* `all-or-condition-skip`: exclusive-branch tips that were condition-SKIPPED are
|
|
13
|
+
* soft; the node runs when every dep is FINISHED or soft condition-skip and at
|
|
14
|
+
* least one is FINISHED (OR-join after condition). Opt-in only — never default.
|
|
15
|
+
*/
|
|
16
|
+
function dependencyReadiness(task, nodes) {
|
|
17
|
+
if (task.depends_on.length === 0)
|
|
18
|
+
return "run";
|
|
19
|
+
const softConditionJoin = task.dependsPolicy === "all-or-condition-skip";
|
|
20
|
+
let hasFinished = false;
|
|
21
|
+
let hasPendingUpstream = false;
|
|
22
|
+
let hasHardBlock = false;
|
|
23
|
+
for (const depId of task.depends_on) {
|
|
9
24
|
const dep = nodes[depId];
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
if (!dep) {
|
|
26
|
+
hasHardBlock = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (dep.status === "FINISHED") {
|
|
30
|
+
hasFinished = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (dep.status === "PENDING" || dep.status === "RUNNING") {
|
|
34
|
+
hasPendingUpstream = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (dep.status === "ERROR") {
|
|
38
|
+
hasHardBlock = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (dep.status === "SKIPPED") {
|
|
42
|
+
if (softConditionJoin &&
|
|
43
|
+
isConditionSkippedReason(dep.skippedReason)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
hasHardBlock = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (hasPendingUpstream)
|
|
50
|
+
return "wait";
|
|
51
|
+
if (hasHardBlock)
|
|
52
|
+
return "skip";
|
|
53
|
+
if (hasFinished)
|
|
54
|
+
return "run";
|
|
55
|
+
return "skip";
|
|
12
56
|
}
|
|
13
57
|
function conditionSkippedByAncestor(task, nodes) {
|
|
14
58
|
return task.depends_on.some((depId) => isConditionSkippedReason(nodes[depId]?.skippedReason));
|
|
@@ -27,18 +71,44 @@ async function mapConcurrent(items, limit, fn) {
|
|
|
27
71
|
export async function executeDagRanksOnce(input) {
|
|
28
72
|
let pausedByNodeId;
|
|
29
73
|
for (const rank of input.ranks) {
|
|
30
|
-
const
|
|
74
|
+
const pending = rank.filter((id) => {
|
|
31
75
|
const node = input.state.nodes[id];
|
|
76
|
+
return node?.status === "PENDING";
|
|
77
|
+
});
|
|
78
|
+
const runnable = pending.filter((id) => {
|
|
32
79
|
const task = input.tasksById.get(id);
|
|
33
|
-
|
|
34
|
-
return false;
|
|
35
|
-
return !shouldSkipNode(node, task, input.state.nodes);
|
|
80
|
+
return dependencyReadiness(task, input.state.nodes) === "run";
|
|
36
81
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
82
|
+
const conditionSettled = [];
|
|
83
|
+
for (const id of runnable) {
|
|
84
|
+
const task = input.tasksById.get(id);
|
|
85
|
+
if (!task.runIf)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
if (!evaluateConditionExpression(task.runIf, input.state)) {
|
|
89
|
+
const node = input.state.nodes[id];
|
|
90
|
+
node.status = "SKIPPED";
|
|
91
|
+
node.skippedReason = `condition runIf did not match: ${task.runIf}`;
|
|
92
|
+
node.finishedAt = new Date().toISOString();
|
|
93
|
+
conditionSettled.push(id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const node = input.state.nodes[id];
|
|
98
|
+
node.status = "ERROR";
|
|
99
|
+
node.failureCategory = "invalid-output";
|
|
100
|
+
node.stderr = error instanceof Error ? error.message : String(error);
|
|
101
|
+
node.finishedAt = new Date().toISOString();
|
|
102
|
+
conditionSettled.push(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (conditionSettled.length > 0)
|
|
106
|
+
await input.persistState();
|
|
107
|
+
const conditionSkippedSet = new Set(conditionSettled);
|
|
108
|
+
const actuallyRunnable = runnable.filter((id) => !conditionSkippedSet.has(id));
|
|
109
|
+
const blocked = pending.filter((id) => {
|
|
39
110
|
const task = input.tasksById.get(id);
|
|
40
|
-
return (
|
|
41
|
-
shouldSkipNode(node, task, input.state.nodes));
|
|
111
|
+
return dependencyReadiness(task, input.state.nodes) === "skip";
|
|
42
112
|
});
|
|
43
113
|
for (const id of blocked) {
|
|
44
114
|
const node = input.state.nodes[id];
|
|
@@ -50,12 +120,12 @@ export async function executeDagRanksOnce(input) {
|
|
|
50
120
|
if (blocked.length > 0) {
|
|
51
121
|
await input.persistState();
|
|
52
122
|
}
|
|
53
|
-
const pauseGateRunnable =
|
|
123
|
+
const pauseGateRunnable = actuallyRunnable.filter((id) => {
|
|
54
124
|
const task = input.tasksById.get(id);
|
|
55
125
|
return isPauseOnHumanDecisionGate(task);
|
|
56
126
|
});
|
|
57
|
-
const regularRunnable =
|
|
58
|
-
const rankWriterNodeIds =
|
|
127
|
+
const regularRunnable = actuallyRunnable.filter((id) => !pauseGateRunnable.includes(id));
|
|
128
|
+
const rankWriterNodeIds = actuallyRunnable.filter((id) => {
|
|
59
129
|
const task = input.tasksById.get(id);
|
|
60
130
|
return (task?.executor === "pi" &&
|
|
61
131
|
task.toolProfile === "write" &&
|
|
@@ -3,9 +3,9 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
|
+
import { resolveContextPolicy } from "./context-policy.js";
|
|
6
7
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
7
|
-
import {
|
|
8
|
-
import { resolveDagNodeSkills } from "./skills.js";
|
|
8
|
+
import { resolveDagSkillInstructions, } from "./skill-instructions.js";
|
|
9
9
|
export const SKILL_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
10
10
|
export const SKILL_SNAPSHOT_RESOLVER_VERSION = 1;
|
|
11
11
|
export const SKILL_SNAPSHOT_REL_PATH = ".runtime/skill-snapshot.json";
|
|
@@ -117,11 +117,13 @@ function profileId(input) {
|
|
|
117
117
|
export function buildDagSkillSnapshotProfileRequest(spec, task) {
|
|
118
118
|
if (task.executor !== "pi")
|
|
119
119
|
return undefined;
|
|
120
|
+
const policy = resolveContextPolicy(spec);
|
|
121
|
+
const budget = policy.resolveSkillInstructionBudget(task);
|
|
120
122
|
const request = {
|
|
121
|
-
skills: [...
|
|
122
|
-
includeLearnedPatterns:
|
|
123
|
-
perSkillMaxChars:
|
|
124
|
-
totalMaxChars:
|
|
123
|
+
skills: [...policy.resolveSkills(spec, task)],
|
|
124
|
+
includeLearnedPatterns: budget.includeLearnedPatterns,
|
|
125
|
+
perSkillMaxChars: budget.perSkillMaxChars,
|
|
126
|
+
totalMaxChars: budget.totalMaxChars,
|
|
125
127
|
};
|
|
126
128
|
return { id: profileId(request), ...request };
|
|
127
129
|
}
|
|
@@ -511,7 +513,8 @@ function stripPromptText(instruction) {
|
|
|
511
513
|
return metadata;
|
|
512
514
|
}
|
|
513
515
|
export function buildNodePromptFromSnapshot(input) {
|
|
514
|
-
const
|
|
516
|
+
const policy = resolveContextPolicy(input.spec);
|
|
517
|
+
const skillNames = policy.resolveSkills(input.spec, input.task);
|
|
515
518
|
const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
|
|
516
519
|
return {
|
|
517
520
|
prompt: buildDagNodePromptEnvelope({
|
|
@@ -520,6 +523,7 @@ export function buildNodePromptFromSnapshot(input) {
|
|
|
520
523
|
upstream: input.upstream,
|
|
521
524
|
resolvedSkills: skillNames,
|
|
522
525
|
resolvedSkillInstructions,
|
|
526
|
+
maxUpstreamChars: policy.resolveMaxUpstreamChars(input.task),
|
|
523
527
|
}),
|
|
524
528
|
resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
|
|
525
529
|
};
|