@abelxiaoxing/cadence 1.0.11 → 1.0.13
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/package.json +1 -1
- package/prompts/abel-design.md +19 -10
- package/prompts/abel-implement.md +19 -6
- package/skills/abel-workflow/SKILL.md +32 -10
- package/src/candidate-preflight.ts +9 -6
- package/src/child-session.ts +100 -28
- package/src/contracts.ts +438 -230
- package/src/file-snapshot.ts +11 -20
- package/src/implement-graph.ts +571 -0
- package/src/index.ts +14 -4
- package/src/parent-payload-bridge.ts +31 -7
- package/src/patch.ts +103 -21
- package/src/runtime.ts +445 -119
- package/src/safe-path.ts +66 -0
- package/src/scoped-tools.ts +278 -38
- package/src/subagent-activity.ts +80 -12
- package/src/submit-tool.ts +26 -13
- package/src/verification-capability.ts +52 -67
- package/src/worker.ts +49 -0
package/src/runtime.ts
CHANGED
|
@@ -17,12 +17,14 @@ import {
|
|
|
17
17
|
type ImplementApplyOperation,
|
|
18
18
|
type ImplementationPhase,
|
|
19
19
|
type ImplementDiscardOperation,
|
|
20
|
+
type ImplementGraphReadinessDiagnostic,
|
|
20
21
|
type ImplementOutcome,
|
|
21
22
|
type ImplementRunRequest,
|
|
22
23
|
LIMITS,
|
|
23
24
|
type PhaseAttempt,
|
|
24
25
|
type RequestEnvelope,
|
|
25
26
|
type RunRequest,
|
|
27
|
+
type SafeFailureDiagnostic,
|
|
26
28
|
type TaskBoundary,
|
|
27
29
|
type TaskFailure,
|
|
28
30
|
validateAgentsCheckpointAttempt,
|
|
@@ -43,6 +45,11 @@ import {
|
|
|
43
45
|
snapshotFile,
|
|
44
46
|
snapshotFiles,
|
|
45
47
|
} from "./file-snapshot.ts";
|
|
48
|
+
import {
|
|
49
|
+
assessImplementGraphReadiness,
|
|
50
|
+
hashImplementGraphBoundary,
|
|
51
|
+
type ImplementExecutionFacts,
|
|
52
|
+
} from "./implement-graph.ts";
|
|
46
53
|
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
47
54
|
import { customPhaseRuntime, runtimeFromContext } from "./parent-provider.ts";
|
|
48
55
|
import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
|
|
@@ -58,8 +65,8 @@ import {
|
|
|
58
65
|
resolveSubagentEndpoint,
|
|
59
66
|
type SubagentEndpoint,
|
|
60
67
|
} from "./subagent-endpoint.ts";
|
|
61
|
-
import { assessVerificationReadiness } from "./verification-capability.ts";
|
|
62
68
|
import {
|
|
69
|
+
type ImplementGraphRecord,
|
|
63
70
|
type TaskRecord,
|
|
64
71
|
taskConflictOf,
|
|
65
72
|
taskRecordKey,
|
|
@@ -100,7 +107,7 @@ function targetsImplement(request: unknown): boolean {
|
|
|
100
107
|
);
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
function
|
|
110
|
+
function cloneNonImplementEnvelope(request: RequestEnvelope): RequestEnvelope {
|
|
104
111
|
return {
|
|
105
112
|
...request,
|
|
106
113
|
taskId: request.taskId ?? request.id,
|
|
@@ -177,6 +184,39 @@ function nextDeclaredPhase(
|
|
|
177
184
|
return null;
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
function taskBoundaryOf(
|
|
188
|
+
graph: ImplementGraphRecord,
|
|
189
|
+
taskId: string,
|
|
190
|
+
): TaskBoundary | null {
|
|
191
|
+
const task = graph.boundary.tasks.find(
|
|
192
|
+
(candidate) => candidate.taskId === taskId,
|
|
193
|
+
);
|
|
194
|
+
return task
|
|
195
|
+
? ({
|
|
196
|
+
changeId: graph.boundary.changeId,
|
|
197
|
+
...structuredClone(task),
|
|
198
|
+
} satisfies TaskBoundary)
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function appliedPhasesOf(record: TaskRecord): ImplementationPhase[] {
|
|
203
|
+
const phases: ImplementationPhase[] = ["red", "green"];
|
|
204
|
+
if (record.boundary.phases.refactor) phases.push("refactor");
|
|
205
|
+
if (
|
|
206
|
+
record.state.kind === "completed" ||
|
|
207
|
+
record.state.kind === "agents-checkpoint-pending"
|
|
208
|
+
) {
|
|
209
|
+
return phases;
|
|
210
|
+
}
|
|
211
|
+
const current =
|
|
212
|
+
record.state.kind === "ready" ||
|
|
213
|
+
record.state.kind === "candidate-pending" ||
|
|
214
|
+
record.state.kind === "blocked"
|
|
215
|
+
? record.state.phase
|
|
216
|
+
: "red";
|
|
217
|
+
return phases.slice(0, phases.indexOf(current));
|
|
218
|
+
}
|
|
219
|
+
|
|
180
220
|
function assertAttemptAllowed(record: TaskRecord, attempt: PhaseAttempt): void {
|
|
181
221
|
switch (record.state.kind) {
|
|
182
222
|
case "candidate-pending":
|
|
@@ -411,25 +451,28 @@ interface OutcomeIdentity {
|
|
|
411
451
|
function blockedOutcome(
|
|
412
452
|
identity: OutcomeIdentity,
|
|
413
453
|
failure: TaskFailure,
|
|
414
|
-
): ImplementOutcome {
|
|
454
|
+
): Extract<ImplementOutcome, { kind: "blocked" }> {
|
|
415
455
|
return { kind: "blocked", ...identity, failure: structuredClone(failure) };
|
|
416
456
|
}
|
|
417
457
|
|
|
418
|
-
function cancelledOutcome(
|
|
458
|
+
function cancelledOutcome(
|
|
459
|
+
identity: OutcomeIdentity,
|
|
460
|
+
): Extract<ImplementOutcome, { kind: "cancelled" }> {
|
|
419
461
|
return { kind: "cancelled", ...identity };
|
|
420
462
|
}
|
|
421
463
|
|
|
422
464
|
function retryOutcome(
|
|
423
465
|
identity: OutcomeIdentity,
|
|
424
466
|
scope: "worker" | "checkpoint",
|
|
425
|
-
|
|
467
|
+
failure: Extract<ChildFailure, { kind: "artifact" | "stale" }>,
|
|
426
468
|
): ImplementOutcome {
|
|
427
469
|
return {
|
|
428
470
|
kind: "retry",
|
|
429
471
|
...identity,
|
|
430
472
|
scope,
|
|
431
|
-
cause,
|
|
473
|
+
cause: failure.kind,
|
|
432
474
|
remainingAttempts: 1,
|
|
475
|
+
lastFailure: safeFailureDiagnostic(failure),
|
|
433
476
|
};
|
|
434
477
|
}
|
|
435
478
|
|
|
@@ -478,7 +521,11 @@ function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
|
478
521
|
}
|
|
479
522
|
switch (result.failureClass) {
|
|
480
523
|
case "artifact":
|
|
481
|
-
return {
|
|
524
|
+
return {
|
|
525
|
+
kind: "artifact",
|
|
526
|
+
code: "invalid-diff",
|
|
527
|
+
stage: "phase-runtime",
|
|
528
|
+
};
|
|
482
529
|
case "environment":
|
|
483
530
|
return { kind: "environment", code: "root-unavailable" };
|
|
484
531
|
default:
|
|
@@ -486,6 +533,10 @@ function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
|
486
533
|
kind: "transport",
|
|
487
534
|
code:
|
|
488
535
|
result.failureKind === "timed-out" ? "timeout" : "transport-failure",
|
|
536
|
+
stage:
|
|
537
|
+
result.failureKind === "timed-out"
|
|
538
|
+
? "child-timeout"
|
|
539
|
+
: "child-provider-stream",
|
|
489
540
|
};
|
|
490
541
|
}
|
|
491
542
|
}
|
|
@@ -524,20 +575,28 @@ function attemptFailureCause(
|
|
|
524
575
|
|
|
525
576
|
function attemptsExhaustedFailure(failure: ChildFailure): TaskFailure {
|
|
526
577
|
const cause = attemptFailureCause(failure);
|
|
527
|
-
if (failure.kind !== "transport") {
|
|
528
|
-
return { kind: "attempts-exhausted", cause };
|
|
529
|
-
}
|
|
530
578
|
return {
|
|
531
579
|
kind: "attempts-exhausted",
|
|
532
580
|
cause,
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
581
|
+
attemptsUsed: 2,
|
|
582
|
+
lastFailure: safeFailureDiagnostic(
|
|
583
|
+
failure as Extract<
|
|
584
|
+
ChildFailure,
|
|
585
|
+
{ kind: "artifact" | "stale" | "transport" }
|
|
586
|
+
>,
|
|
587
|
+
),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function safeFailureDiagnostic(
|
|
592
|
+
failure: Extract<ChildFailure, { kind: "artifact" | "stale" | "transport" }>,
|
|
593
|
+
): SafeFailureDiagnostic {
|
|
594
|
+
return {
|
|
595
|
+
code: failure.code,
|
|
596
|
+
stage: failure.stage,
|
|
597
|
+
...("details" in failure && failure.details
|
|
598
|
+
? { details: structuredClone(failure.details) }
|
|
599
|
+
: {}),
|
|
541
600
|
};
|
|
542
601
|
}
|
|
543
602
|
|
|
@@ -891,6 +950,30 @@ export class Runtime {
|
|
|
891
950
|
) {
|
|
892
951
|
throw new Error("AGENTS checkpoint target mismatch");
|
|
893
952
|
}
|
|
953
|
+
const outputFailure = this.unavailableAppliedOutput(
|
|
954
|
+
worker,
|
|
955
|
+
worker.state.finalPhase,
|
|
956
|
+
true,
|
|
957
|
+
);
|
|
958
|
+
if (outputFailure) {
|
|
959
|
+
const failure: TaskFailure = {
|
|
960
|
+
kind: "graph-readiness",
|
|
961
|
+
diagnostic: outputFailure,
|
|
962
|
+
};
|
|
963
|
+
worker.state = {
|
|
964
|
+
kind: "blocked",
|
|
965
|
+
phase: worker.state.finalPhase,
|
|
966
|
+
failure,
|
|
967
|
+
};
|
|
968
|
+
return blockedOutcome(
|
|
969
|
+
{
|
|
970
|
+
requestId: attempt.requestId,
|
|
971
|
+
taskId: attempt.taskId,
|
|
972
|
+
phase: worker.state.phase,
|
|
973
|
+
},
|
|
974
|
+
failure,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
894
977
|
const request: AgentsCheckpointRequest = {
|
|
895
978
|
stage: "abel-implement",
|
|
896
979
|
taskId: worker.boundary.taskId,
|
|
@@ -940,6 +1023,8 @@ export class Runtime {
|
|
|
940
1023
|
const taskFailure: TaskFailure = {
|
|
941
1024
|
kind: "checkpoint-attempts-exhausted",
|
|
942
1025
|
cause: "artifact",
|
|
1026
|
+
attemptsUsed: 2,
|
|
1027
|
+
lastFailure: safeFailureDiagnostic(failure),
|
|
943
1028
|
};
|
|
944
1029
|
worker.state = {
|
|
945
1030
|
kind: "blocked",
|
|
@@ -953,13 +1038,15 @@ export class Runtime {
|
|
|
953
1038
|
finalPhase: phase,
|
|
954
1039
|
attemptIndex: 1,
|
|
955
1040
|
};
|
|
956
|
-
return retryOutcome(identity, "checkpoint",
|
|
1041
|
+
return retryOutcome(identity, "checkpoint", failure);
|
|
957
1042
|
}
|
|
958
1043
|
case "stale": {
|
|
959
1044
|
if (worker.state.attemptIndex === 1) {
|
|
960
1045
|
const taskFailure: TaskFailure = {
|
|
961
1046
|
kind: "checkpoint-attempts-exhausted",
|
|
962
1047
|
cause: "stale",
|
|
1048
|
+
attemptsUsed: 2,
|
|
1049
|
+
lastFailure: safeFailureDiagnostic(failure),
|
|
963
1050
|
};
|
|
964
1051
|
worker.state = {
|
|
965
1052
|
kind: "blocked",
|
|
@@ -973,7 +1060,7 @@ export class Runtime {
|
|
|
973
1060
|
finalPhase: phase,
|
|
974
1061
|
attemptIndex: 1,
|
|
975
1062
|
};
|
|
976
|
-
return retryOutcome(identity, "checkpoint",
|
|
1063
|
+
return retryOutcome(identity, "checkpoint", failure);
|
|
977
1064
|
}
|
|
978
1065
|
case "environment": {
|
|
979
1066
|
const taskFailure: TaskFailure = failure;
|
|
@@ -1102,7 +1189,18 @@ export class Runtime {
|
|
|
1102
1189
|
}
|
|
1103
1190
|
const applyRoot = resolved?.retained.root ?? root;
|
|
1104
1191
|
const run = this.enqueueParentApply(() =>
|
|
1105
|
-
applyRetainedPatch({
|
|
1192
|
+
applyRetainedPatch({
|
|
1193
|
+
root: applyRoot,
|
|
1194
|
+
id,
|
|
1195
|
+
store: this.results,
|
|
1196
|
+
...(resolved
|
|
1197
|
+
? {
|
|
1198
|
+
candidateOutputsAvailable: (checkoutRoot: string) =>
|
|
1199
|
+
this.candidateOutputsAvailable(resolved.worker, checkoutRoot),
|
|
1200
|
+
}
|
|
1201
|
+
: {}),
|
|
1202
|
+
signal,
|
|
1203
|
+
}),
|
|
1106
1204
|
);
|
|
1107
1205
|
const seq = ++this.applySeq;
|
|
1108
1206
|
return run.then((result) => {
|
|
@@ -1117,6 +1215,33 @@ export class Runtime {
|
|
|
1117
1215
|
worker.boundary,
|
|
1118
1216
|
identity.phase,
|
|
1119
1217
|
);
|
|
1218
|
+
const outputFailure = this.unavailableAppliedOutput(
|
|
1219
|
+
worker,
|
|
1220
|
+
identity.phase,
|
|
1221
|
+
readyPhase === null && worker.boundary.agents.impact === "none",
|
|
1222
|
+
);
|
|
1223
|
+
if (outputFailure) {
|
|
1224
|
+
const failure: TaskFailure = {
|
|
1225
|
+
kind: "graph-readiness",
|
|
1226
|
+
diagnostic: outputFailure,
|
|
1227
|
+
};
|
|
1228
|
+
worker.state = {
|
|
1229
|
+
kind: "blocked",
|
|
1230
|
+
phase: identity.phase,
|
|
1231
|
+
failure,
|
|
1232
|
+
};
|
|
1233
|
+
return {
|
|
1234
|
+
...blockedOutcome(
|
|
1235
|
+
{
|
|
1236
|
+
requestId,
|
|
1237
|
+
taskId: identity.taskId,
|
|
1238
|
+
phase: identity.phase,
|
|
1239
|
+
},
|
|
1240
|
+
failure,
|
|
1241
|
+
),
|
|
1242
|
+
result: applied,
|
|
1243
|
+
} satisfies ImplementOutcome;
|
|
1244
|
+
}
|
|
1120
1245
|
if (readyPhase) {
|
|
1121
1246
|
worker.state = {
|
|
1122
1247
|
kind: "ready",
|
|
@@ -1215,12 +1340,9 @@ export class Runtime {
|
|
|
1215
1340
|
launchIndex: 1,
|
|
1216
1341
|
correction: structuredClone(rejection),
|
|
1217
1342
|
};
|
|
1218
|
-
return retryOutcome(outcomeIdentity, "worker",
|
|
1343
|
+
return retryOutcome(outcomeIdentity, "worker", rejection);
|
|
1219
1344
|
}
|
|
1220
|
-
const failure
|
|
1221
|
-
kind: "attempts-exhausted",
|
|
1222
|
-
cause: "artifact",
|
|
1223
|
-
};
|
|
1345
|
+
const failure = attemptsExhaustedFailure(rejection);
|
|
1224
1346
|
worker.state = {
|
|
1225
1347
|
kind: "blocked",
|
|
1226
1348
|
phase: identity.phase,
|
|
@@ -1289,10 +1411,7 @@ export class Runtime {
|
|
|
1289
1411
|
correction: structuredClone(failure),
|
|
1290
1412
|
};
|
|
1291
1413
|
} else {
|
|
1292
|
-
const taskFailure
|
|
1293
|
-
kind: "attempts-exhausted",
|
|
1294
|
-
cause: "stale",
|
|
1295
|
-
};
|
|
1414
|
+
const taskFailure = attemptsExhaustedFailure(failure);
|
|
1296
1415
|
worker.state = {
|
|
1297
1416
|
kind: "blocked",
|
|
1298
1417
|
phase: identity.phase,
|
|
@@ -1300,16 +1419,13 @@ export class Runtime {
|
|
|
1300
1419
|
};
|
|
1301
1420
|
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1302
1421
|
}
|
|
1303
|
-
return retryOutcome(outcomeIdentity, "worker",
|
|
1422
|
+
return retryOutcome(outcomeIdentity, "worker", failure);
|
|
1304
1423
|
}
|
|
1305
1424
|
case "artifact": {
|
|
1306
1425
|
this.results.discard(resultId);
|
|
1307
1426
|
const correctionAvailable = identity.launchIndex === 0;
|
|
1308
1427
|
if (!correctionAvailable) {
|
|
1309
|
-
const taskFailure
|
|
1310
|
-
kind: "attempts-exhausted",
|
|
1311
|
-
cause: "artifact",
|
|
1312
|
-
};
|
|
1428
|
+
const taskFailure = attemptsExhaustedFailure(failure);
|
|
1313
1429
|
worker.state = {
|
|
1314
1430
|
kind: "blocked",
|
|
1315
1431
|
phase: identity.phase,
|
|
@@ -1323,7 +1439,7 @@ export class Runtime {
|
|
|
1323
1439
|
launchIndex: 1,
|
|
1324
1440
|
correction: structuredClone(failure),
|
|
1325
1441
|
};
|
|
1326
|
-
return retryOutcome(outcomeIdentity, "worker",
|
|
1442
|
+
return retryOutcome(outcomeIdentity, "worker", failure);
|
|
1327
1443
|
}
|
|
1328
1444
|
case "cancelled":
|
|
1329
1445
|
return cancelledOutcome(outcomeIdentity);
|
|
@@ -1533,6 +1649,184 @@ export class Runtime {
|
|
|
1533
1649
|
};
|
|
1534
1650
|
}
|
|
1535
1651
|
|
|
1652
|
+
private graphFacts(graph: ImplementGraphRecord): ImplementExecutionFacts {
|
|
1653
|
+
const records = this.registry
|
|
1654
|
+
.values()
|
|
1655
|
+
.filter(
|
|
1656
|
+
(record) =>
|
|
1657
|
+
record.workspaceRoot === graph.workspaceRoot &&
|
|
1658
|
+
record.boundary.changeId === graph.boundary.changeId,
|
|
1659
|
+
);
|
|
1660
|
+
return {
|
|
1661
|
+
completedTasks: [
|
|
1662
|
+
...new Set([
|
|
1663
|
+
...graph.completedTasks,
|
|
1664
|
+
...records
|
|
1665
|
+
.filter((record) => record.state.kind === "completed")
|
|
1666
|
+
.map((record) => record.boundary.taskId),
|
|
1667
|
+
]),
|
|
1668
|
+
],
|
|
1669
|
+
blockedTasks: [
|
|
1670
|
+
...new Set([
|
|
1671
|
+
...graph.blockedTasks,
|
|
1672
|
+
...records
|
|
1673
|
+
.filter((record) => record.state.kind === "blocked")
|
|
1674
|
+
.map((record) => record.boundary.taskId),
|
|
1675
|
+
]),
|
|
1676
|
+
],
|
|
1677
|
+
appliedPhases: records.flatMap((record) =>
|
|
1678
|
+
appliedPhasesOf(record).map((phase) => ({
|
|
1679
|
+
taskId: record.boundary.taskId,
|
|
1680
|
+
phase,
|
|
1681
|
+
})),
|
|
1682
|
+
),
|
|
1683
|
+
dependencyOwner: graph.workspaceRoot,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
private graphFor(worker: TaskRecord): ImplementGraphRecord {
|
|
1688
|
+
const graph = this.registry.getGraph(
|
|
1689
|
+
worker.workspaceRoot,
|
|
1690
|
+
worker.boundary.changeId,
|
|
1691
|
+
);
|
|
1692
|
+
if (!graph) throw new Error("Implement graph is unavailable");
|
|
1693
|
+
return graph;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private candidateOutputsAvailable(
|
|
1697
|
+
worker: TaskRecord,
|
|
1698
|
+
checkoutRoot: string,
|
|
1699
|
+
): boolean {
|
|
1700
|
+
const graph = this.graphFor(worker);
|
|
1701
|
+
const phase = taskPhase(worker);
|
|
1702
|
+
const produced = new Set(
|
|
1703
|
+
graph.boundary.outputs
|
|
1704
|
+
.filter(
|
|
1705
|
+
(output) =>
|
|
1706
|
+
output.producer.taskId === worker.boundary.taskId &&
|
|
1707
|
+
output.producer.phase === phase,
|
|
1708
|
+
)
|
|
1709
|
+
.map((output) => output.id),
|
|
1710
|
+
);
|
|
1711
|
+
if (produced.size === 0) return true;
|
|
1712
|
+
const readiness = assessImplementGraphReadiness(
|
|
1713
|
+
checkoutRoot,
|
|
1714
|
+
graph.boundary,
|
|
1715
|
+
{
|
|
1716
|
+
...this.graphFacts(graph),
|
|
1717
|
+
candidate: { taskId: worker.boundary.taskId, phase },
|
|
1718
|
+
},
|
|
1719
|
+
);
|
|
1720
|
+
return readiness.outputs.every(
|
|
1721
|
+
(output) =>
|
|
1722
|
+
!produced.has(output.outputId) || output.status === "available",
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private unavailableAppliedOutput(
|
|
1727
|
+
worker: TaskRecord,
|
|
1728
|
+
phase: ImplementationPhase,
|
|
1729
|
+
allTaskOutputs: boolean,
|
|
1730
|
+
): ImplementGraphReadinessDiagnostic | undefined {
|
|
1731
|
+
const graph = this.graphFor(worker);
|
|
1732
|
+
const facts = this.graphFacts(graph);
|
|
1733
|
+
const appliedPhases = [
|
|
1734
|
+
...(facts.appliedPhases ?? []),
|
|
1735
|
+
{ taskId: worker.boundary.taskId, phase },
|
|
1736
|
+
].filter(
|
|
1737
|
+
(entry, index, entries) =>
|
|
1738
|
+
entries.findIndex(
|
|
1739
|
+
(candidate) =>
|
|
1740
|
+
candidate.taskId === entry.taskId &&
|
|
1741
|
+
candidate.phase === entry.phase,
|
|
1742
|
+
) === index,
|
|
1743
|
+
);
|
|
1744
|
+
const readiness = assessImplementGraphReadiness(
|
|
1745
|
+
worker.workspaceRoot,
|
|
1746
|
+
graph.boundary,
|
|
1747
|
+
{ ...facts, appliedPhases },
|
|
1748
|
+
);
|
|
1749
|
+
const output = readiness.outputs.find(
|
|
1750
|
+
(entry) =>
|
|
1751
|
+
entry.producerTaskId === worker.boundary.taskId &&
|
|
1752
|
+
(allTaskOutputs || entry.producerPhase === phase) &&
|
|
1753
|
+
entry.status !== "available",
|
|
1754
|
+
);
|
|
1755
|
+
if (!output) return undefined;
|
|
1756
|
+
return (
|
|
1757
|
+
output.diagnostic ?? {
|
|
1758
|
+
kind: "graph-readiness",
|
|
1759
|
+
code: "producer-output-unavailable",
|
|
1760
|
+
outputId: output.outputId,
|
|
1761
|
+
producerTaskId: output.producerTaskId,
|
|
1762
|
+
producerPhase: output.producerPhase,
|
|
1763
|
+
}
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
private admitImplementGraph(
|
|
1768
|
+
request: Extract<ImplementRunRequest, { kind: "admit-graph" }>,
|
|
1769
|
+
workspaceRoot: string,
|
|
1770
|
+
): ImplementOutcome {
|
|
1771
|
+
if (this.registry.getGraph(workspaceRoot, request.graph.changeId)) {
|
|
1772
|
+
throw new Error("duplicate graph admission protocol error");
|
|
1773
|
+
}
|
|
1774
|
+
const actualHash = hashImplementGraphBoundary(request.graph);
|
|
1775
|
+
if (actualHash !== request.graphHash) {
|
|
1776
|
+
return {
|
|
1777
|
+
kind: "graph-rejected",
|
|
1778
|
+
changeId: request.graph.changeId,
|
|
1779
|
+
graphHash: request.graphHash,
|
|
1780
|
+
diagnostics: [
|
|
1781
|
+
{
|
|
1782
|
+
kind: "graph-readiness",
|
|
1783
|
+
code: "graph-hash-mismatch",
|
|
1784
|
+
},
|
|
1785
|
+
],
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
const readiness = assessImplementGraphReadiness(
|
|
1789
|
+
workspaceRoot,
|
|
1790
|
+
request.graph,
|
|
1791
|
+
{
|
|
1792
|
+
completedTasks: request.state.completedTasks,
|
|
1793
|
+
blockedTasks: request.state.blockedTasks,
|
|
1794
|
+
dependencyOwner: workspaceRoot,
|
|
1795
|
+
},
|
|
1796
|
+
);
|
|
1797
|
+
const unavailableOutputs = readiness.outputs.flatMap((output) =>
|
|
1798
|
+
output.status === "unavailable" && output.diagnostic
|
|
1799
|
+
? [output.diagnostic]
|
|
1800
|
+
: [],
|
|
1801
|
+
);
|
|
1802
|
+
if (!readiness.closure.executable || unavailableOutputs.length > 0) {
|
|
1803
|
+
return {
|
|
1804
|
+
kind: "graph-rejected",
|
|
1805
|
+
changeId: request.graph.changeId,
|
|
1806
|
+
graphHash: request.graphHash,
|
|
1807
|
+
diagnostics: [...readiness.closure.diagnostics, ...unavailableOutputs],
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
this.registry.admitGraph(
|
|
1811
|
+
request.graph,
|
|
1812
|
+
request.graphHash,
|
|
1813
|
+
workspaceRoot,
|
|
1814
|
+
request.state,
|
|
1815
|
+
);
|
|
1816
|
+
const red = readiness.phases.filter((phase) => phase.phase === "red");
|
|
1817
|
+
return {
|
|
1818
|
+
kind: "graph-admitted",
|
|
1819
|
+
changeId: request.graph.changeId,
|
|
1820
|
+
graphHash: request.graphHash,
|
|
1821
|
+
readyTasks: red
|
|
1822
|
+
.filter((phase) => phase.ready)
|
|
1823
|
+
.map((phase) => phase.taskId),
|
|
1824
|
+
blockedTasks: red
|
|
1825
|
+
.filter((phase) => !phase.ready && !phase.completed)
|
|
1826
|
+
.map((phase) => phase.taskId),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1536
1830
|
private async run(
|
|
1537
1831
|
request: unknown,
|
|
1538
1832
|
ctx?: RunContext,
|
|
@@ -1555,70 +1849,136 @@ export class Runtime {
|
|
|
1555
1849
|
let envelope: RequestEnvelope;
|
|
1556
1850
|
if (isImplementRunRequest(validation.value)) {
|
|
1557
1851
|
const runRequest = validation.value;
|
|
1558
|
-
const attempt = runRequest.attempt;
|
|
1559
1852
|
const workspaceRoot = canonicalWorkspaceRoot(ctx.cwd);
|
|
1560
|
-
if (
|
|
1561
|
-
runRequest.kind === "open-task" &&
|
|
1562
|
-
workspaceRoot &&
|
|
1563
|
-
this.registry.has(
|
|
1564
|
-
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
1565
|
-
)
|
|
1566
|
-
) {
|
|
1567
|
-
throw new Error("duplicate task open protocol error");
|
|
1568
|
-
}
|
|
1569
|
-
if (runRequest.kind === "open-task" && signal?.aborted) {
|
|
1570
|
-
return cancelledOutcome({
|
|
1571
|
-
taskId: attempt.taskId,
|
|
1572
|
-
requestId: attempt.requestId,
|
|
1573
|
-
phase: attempt.phase,
|
|
1574
|
-
});
|
|
1575
|
-
}
|
|
1576
1853
|
if (!workspaceRoot) {
|
|
1577
1854
|
throw new Error("implementation workspace is unavailable");
|
|
1578
1855
|
}
|
|
1856
|
+
if (runRequest.kind === "admit-graph") {
|
|
1857
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
1858
|
+
return this.admitImplementGraph(runRequest, workspaceRoot);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const attempt = runRequest.attempt;
|
|
1862
|
+
const graph = this.registry.getGraph(workspaceRoot, attempt.changeId);
|
|
1863
|
+
if (!graph) {
|
|
1864
|
+
throw new Error("Implement graph is not admitted");
|
|
1865
|
+
}
|
|
1866
|
+
const boundary = taskBoundaryOf(graph, attempt.taskId);
|
|
1867
|
+
if (!boundary) throw new Error("task identity mismatch");
|
|
1579
1868
|
const key = taskRecordKey(
|
|
1580
1869
|
workspaceRoot,
|
|
1581
1870
|
attempt.changeId,
|
|
1582
1871
|
attempt.taskId,
|
|
1583
1872
|
);
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1873
|
+
const existing = this.registry.get(key);
|
|
1874
|
+
let existingIdentity: string | undefined;
|
|
1875
|
+
if (existing) {
|
|
1876
|
+
if (!ctx.model) {
|
|
1877
|
+
throw new Error("implementation model is unavailable");
|
|
1878
|
+
}
|
|
1879
|
+
existingIdentity = workerIdentity(ctx.model);
|
|
1880
|
+
if (existing.workerIdentity !== existingIdentity) {
|
|
1881
|
+
throw new Error("provider/model identity mismatch");
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (existing?.state.kind === "blocked") {
|
|
1885
|
+
if (attempt.phase !== existing.state.phase) {
|
|
1886
|
+
throw new Error("terminal task phase mismatch");
|
|
1887
|
+
}
|
|
1888
|
+
return blockedOutcome(
|
|
1593
1889
|
{
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
...phases.green.write,
|
|
1598
|
-
...(phases.refactor?.write ?? []),
|
|
1599
|
-
]),
|
|
1600
|
-
],
|
|
1890
|
+
taskId: existing.boundary.taskId,
|
|
1891
|
+
requestId: attempt.requestId,
|
|
1892
|
+
phase: existing.state.phase,
|
|
1601
1893
|
},
|
|
1894
|
+
existing.state.failure,
|
|
1602
1895
|
);
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1896
|
+
}
|
|
1897
|
+
if (existing?.state.kind === "completed") {
|
|
1898
|
+
if (attempt.phase !== existing.state.finalPhase) {
|
|
1899
|
+
throw new Error("terminal task phase mismatch");
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
kind: "completed",
|
|
1903
|
+
taskId: existing.boundary.taskId,
|
|
1904
|
+
requestId: attempt.requestId,
|
|
1905
|
+
finalPhase: existing.state.finalPhase,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (graph.completedTasks.has(attempt.taskId)) {
|
|
1909
|
+
const finalPhase = boundary.phases.refactor ? "refactor" : "green";
|
|
1910
|
+
if (attempt.phase !== finalPhase) {
|
|
1911
|
+
throw new Error("terminal task phase mismatch");
|
|
1912
|
+
}
|
|
1913
|
+
return {
|
|
1914
|
+
kind: "completed",
|
|
1915
|
+
taskId: attempt.taskId,
|
|
1916
|
+
requestId: attempt.requestId,
|
|
1917
|
+
finalPhase,
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
1921
|
+
boundary,
|
|
1922
|
+
attempt,
|
|
1923
|
+
);
|
|
1924
|
+
if (attemptReason !== null) throw new Error(attemptReason);
|
|
1925
|
+
if (existing) assertAttemptAllowed(existing, attempt);
|
|
1926
|
+
else if (attempt.phase !== "red") {
|
|
1927
|
+
throw new Error("task must begin with Red");
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const readiness = assessImplementGraphReadiness(
|
|
1931
|
+
workspaceRoot,
|
|
1932
|
+
graph.boundary,
|
|
1933
|
+
this.graphFacts(graph),
|
|
1934
|
+
);
|
|
1935
|
+
const phaseReadiness = readiness.phases.find(
|
|
1936
|
+
(phase) =>
|
|
1937
|
+
phase.taskId === attempt.taskId && phase.phase === attempt.phase,
|
|
1938
|
+
);
|
|
1939
|
+
if (!phaseReadiness) throw new Error("task phase is not declared");
|
|
1940
|
+
if (!phaseReadiness.ready) {
|
|
1941
|
+
const dependencyDiagnostics = phaseReadiness.diagnostics.filter(
|
|
1942
|
+
(diagnostic) =>
|
|
1943
|
+
diagnostic.kind === "graph-readiness" &&
|
|
1944
|
+
diagnostic.code === "dependency-blocked",
|
|
1945
|
+
);
|
|
1946
|
+
if (dependencyDiagnostics.length > 0) {
|
|
1947
|
+
return {
|
|
1948
|
+
kind: "dependency-blocked",
|
|
1949
|
+
taskId: attempt.taskId,
|
|
1950
|
+
requestId: attempt.requestId,
|
|
1951
|
+
phase: attempt.phase,
|
|
1952
|
+
diagnostics: dependencyDiagnostics,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
const diagnostic = phaseReadiness.diagnostics[0];
|
|
1956
|
+
if (!diagnostic) throw new Error("task phase is not ready");
|
|
1957
|
+
if (diagnostic.kind === "design-readiness") {
|
|
1958
|
+
throw new Error(`Implement protocol error: ${diagnostic.code}`);
|
|
1959
|
+
}
|
|
1960
|
+
if (diagnostic.kind === "verification-adapter") {
|
|
1610
1961
|
return {
|
|
1611
1962
|
ok: false,
|
|
1612
|
-
error: `${diagnostic.kind}/${diagnostic.code}
|
|
1963
|
+
error: `${diagnostic.kind}/${diagnostic.code}`,
|
|
1613
1964
|
failure: {
|
|
1614
1965
|
kind: diagnostic.kind,
|
|
1615
1966
|
code: diagnostic.code,
|
|
1616
1967
|
},
|
|
1617
1968
|
};
|
|
1618
1969
|
}
|
|
1970
|
+
return blockedOutcome(
|
|
1971
|
+
{
|
|
1972
|
+
taskId: attempt.taskId,
|
|
1973
|
+
requestId: attempt.requestId,
|
|
1974
|
+
phase: attempt.phase,
|
|
1975
|
+
},
|
|
1976
|
+
{ kind: "graph-readiness", diagnostic },
|
|
1977
|
+
);
|
|
1619
1978
|
}
|
|
1979
|
+
|
|
1620
1980
|
if (
|
|
1621
|
-
|
|
1981
|
+
!existing &&
|
|
1622
1982
|
this.registry
|
|
1623
1983
|
.values()
|
|
1624
1984
|
.some(
|
|
@@ -1626,10 +1986,7 @@ export class Runtime {
|
|
|
1626
1986
|
record.workspaceRoot === workspaceRoot &&
|
|
1627
1987
|
record.state.kind !== "blocked" &&
|
|
1628
1988
|
record.state.kind !== "completed" &&
|
|
1629
|
-
declarationsConflict(
|
|
1630
|
-
taskConflictOf(runRequest.boundary),
|
|
1631
|
-
record.conflict,
|
|
1632
|
-
),
|
|
1989
|
+
declarationsConflict(taskConflictOf(boundary), record.conflict),
|
|
1633
1990
|
)
|
|
1634
1991
|
) {
|
|
1635
1992
|
return {
|
|
@@ -1642,9 +1999,9 @@ export class Runtime {
|
|
|
1642
1999
|
if (!ctx.model) {
|
|
1643
2000
|
throw new Error("implementation model is unavailable");
|
|
1644
2001
|
}
|
|
1645
|
-
const identity = workerIdentity(ctx.model);
|
|
2002
|
+
const identity = existingIdentity ?? workerIdentity(ctx.model);
|
|
1646
2003
|
let record: TaskRecord;
|
|
1647
|
-
if (
|
|
2004
|
+
if (!existing) {
|
|
1648
2005
|
const endpoint = resolveSubagentEndpoint("implementation-worker", {
|
|
1649
2006
|
cwd: ctx.cwd,
|
|
1650
2007
|
});
|
|
@@ -1655,7 +2012,7 @@ export class Runtime {
|
|
|
1655
2012
|
message: describeInvalidSubagentEndpoint(endpoint),
|
|
1656
2013
|
} as const;
|
|
1657
2014
|
record = this.registry.open(
|
|
1658
|
-
|
|
2015
|
+
boundary,
|
|
1659
2016
|
identity,
|
|
1660
2017
|
workspaceRoot,
|
|
1661
2018
|
attempt,
|
|
@@ -1677,46 +2034,15 @@ export class Runtime {
|
|
|
1677
2034
|
const subagentEndpoint: Readonly<SubagentEndpoint> | null =
|
|
1678
2035
|
endpoint.kind === "custom" ? endpoint.endpoint : null;
|
|
1679
2036
|
record = this.registry.open(
|
|
1680
|
-
|
|
2037
|
+
boundary,
|
|
1681
2038
|
identity,
|
|
1682
2039
|
workspaceRoot,
|
|
1683
2040
|
attempt,
|
|
1684
2041
|
subagentEndpoint,
|
|
1685
2042
|
);
|
|
1686
2043
|
} else {
|
|
1687
|
-
const existing = this.registry.get(key);
|
|
1688
|
-
if (!existing) {
|
|
1689
|
-
throw new Error("task identity mismatch or task is not open");
|
|
1690
|
-
}
|
|
1691
|
-
if (existing.workerIdentity !== identity) {
|
|
1692
|
-
throw new Error("provider/model identity mismatch");
|
|
1693
|
-
}
|
|
1694
|
-
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
1695
|
-
existing.boundary,
|
|
1696
|
-
attempt,
|
|
1697
|
-
);
|
|
1698
|
-
if (attemptReason !== null) throw new Error(attemptReason);
|
|
1699
|
-
assertAttemptAllowed(existing, attempt);
|
|
1700
2044
|
record = existing;
|
|
1701
2045
|
}
|
|
1702
|
-
if (record.state.kind === "blocked") {
|
|
1703
|
-
return blockedOutcome(
|
|
1704
|
-
{
|
|
1705
|
-
taskId: record.boundary.taskId,
|
|
1706
|
-
requestId: attempt.requestId,
|
|
1707
|
-
phase: record.state.phase,
|
|
1708
|
-
},
|
|
1709
|
-
record.state.failure,
|
|
1710
|
-
);
|
|
1711
|
-
}
|
|
1712
|
-
if (record.state.kind === "completed") {
|
|
1713
|
-
return {
|
|
1714
|
-
kind: "completed",
|
|
1715
|
-
taskId: record.boundary.taskId,
|
|
1716
|
-
requestId: attempt.requestId,
|
|
1717
|
-
finalPhase: record.state.finalPhase,
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1720
2046
|
if (signal?.aborted) {
|
|
1721
2047
|
return cancelledOutcome({
|
|
1722
2048
|
taskId: record.boundary.taskId,
|
|
@@ -1728,7 +2054,7 @@ export class Runtime {
|
|
|
1728
2054
|
this.taskRecords.set(envelope, record);
|
|
1729
2055
|
} else {
|
|
1730
2056
|
if (signal?.aborted) return cancellationError(signal);
|
|
1731
|
-
envelope =
|
|
2057
|
+
envelope = cloneNonImplementEnvelope(validation.value);
|
|
1732
2058
|
}
|
|
1733
2059
|
const context: StoredRunContext = {
|
|
1734
2060
|
ctx,
|
|
@@ -1986,7 +2312,7 @@ export class Runtime {
|
|
|
1986
2312
|
launchIndex: 1,
|
|
1987
2313
|
correction: structuredClone(firstFailure),
|
|
1988
2314
|
};
|
|
1989
|
-
return finish(retryOutcome(identity, "worker", firstFailure
|
|
2315
|
+
return finish(retryOutcome(identity, "worker", firstFailure));
|
|
1990
2316
|
}
|
|
1991
2317
|
|
|
1992
2318
|
task.state = {
|