@abelxiaoxing/cadence 1.0.2 → 1.0.3
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/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +50 -21
- package/skills/abel-workflow/SKILL.md +57 -25
- package/src/candidate-preflight.ts +361 -56
- package/src/child-session.ts +92 -27
- package/src/contracts.ts +987 -38
- package/src/index.ts +50 -24
- package/src/parent-provider.ts +89 -23
- package/src/patch.ts +269 -41
- package/src/result-store.ts +254 -16
- package/src/runtime.ts +1281 -560
- package/src/scheduler.ts +59 -16
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +12 -2
- package/src/worker.ts +114 -195
package/src/runtime.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import { lstatSync, readdirSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Activation, type ActivationState } from "./activation.ts";
|
|
5
6
|
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
6
7
|
import type { BaselineEntry } from "./candidate-preflight.ts";
|
|
7
|
-
import { runChildSession } from "./child-session.ts";
|
|
8
|
+
import { runChildSession, UsageAggregator } from "./child-session.ts";
|
|
8
9
|
import {
|
|
9
10
|
ACTIONS,
|
|
11
|
+
type AgentsCheckpointAttempt,
|
|
12
|
+
type AgentsCheckpointRequest,
|
|
13
|
+
type CandidateFailure,
|
|
14
|
+
type ChildFailure,
|
|
10
15
|
type DiffResult,
|
|
16
|
+
type ImplementApplyOperation,
|
|
17
|
+
type ImplementationPhase,
|
|
18
|
+
type ImplementDiscardOperation,
|
|
19
|
+
type ImplementOutcome,
|
|
20
|
+
type ImplementRunRequest,
|
|
11
21
|
LIMITS,
|
|
12
|
-
type
|
|
13
|
-
type RecoveryNext,
|
|
14
|
-
type RecoveryRecord,
|
|
22
|
+
type PhaseAttempt,
|
|
15
23
|
type RequestEnvelope,
|
|
24
|
+
type RunRequest,
|
|
25
|
+
type TaskBoundary,
|
|
26
|
+
type TaskFailure,
|
|
27
|
+
validateAgentsCheckpointAttempt,
|
|
28
|
+
validateImplementApplyOperation,
|
|
29
|
+
validateImplementDiscardOperation,
|
|
30
|
+
validatePhaseAttemptAgainstBoundary,
|
|
16
31
|
validateRequestEnvelope,
|
|
17
32
|
} from "./contracts.ts";
|
|
18
33
|
import { drainStage } from "./drain.ts";
|
|
@@ -28,20 +43,178 @@ import {
|
|
|
28
43
|
} from "./file-snapshot.ts";
|
|
29
44
|
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
30
45
|
import { runtimeFromContext } from "./parent-provider.ts";
|
|
31
|
-
import { applyRetainedPatch } from "./patch.ts";
|
|
32
|
-
import { ResultStore } from "./result-store.ts";
|
|
33
|
-
import { Scheduler } from "./scheduler.ts";
|
|
46
|
+
import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
|
|
34
47
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
type BoundRetainedResult,
|
|
49
|
+
ResultStore,
|
|
50
|
+
type RetainedCandidateFacts,
|
|
51
|
+
type RetainedCandidateIdentity,
|
|
52
|
+
} from "./result-store.ts";
|
|
53
|
+
import { declarationsConflict, Scheduler } from "./scheduler.ts";
|
|
54
|
+
import {
|
|
55
|
+
type TaskRecord,
|
|
56
|
+
taskConflictOf,
|
|
57
|
+
taskRecordKey,
|
|
39
58
|
WorkerRegistry,
|
|
40
59
|
workerIdentity,
|
|
41
60
|
} from "./worker.ts";
|
|
42
61
|
|
|
43
62
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
44
63
|
|
|
64
|
+
function isFileSystemFailure(error: unknown): boolean {
|
|
65
|
+
return (
|
|
66
|
+
error instanceof Error &&
|
|
67
|
+
"code" in error &&
|
|
68
|
+
typeof (error as { code?: unknown }).code === "string"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalWorkspaceRoot(root: string): string | null {
|
|
73
|
+
try {
|
|
74
|
+
return realpathSync(root);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isImplementRunRequest(
|
|
81
|
+
request: RunRequest,
|
|
82
|
+
): request is ImplementRunRequest {
|
|
83
|
+
return request.stage === "abel-implement" && "kind" in request;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function targetsImplement(request: unknown): boolean {
|
|
87
|
+
return (
|
|
88
|
+
typeof request === "object" &&
|
|
89
|
+
request !== null &&
|
|
90
|
+
!Array.isArray(request) &&
|
|
91
|
+
(request as Record<string, unknown>).stage === "abel-implement"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cloneLegacyEnvelope(request: RequestEnvelope): RequestEnvelope {
|
|
96
|
+
return {
|
|
97
|
+
...request,
|
|
98
|
+
taskId: request.taskId ?? request.id,
|
|
99
|
+
roots: [...request.roots],
|
|
100
|
+
context: { ...request.context },
|
|
101
|
+
declared: {
|
|
102
|
+
...request.declared,
|
|
103
|
+
read: [...request.declared.read],
|
|
104
|
+
write: [...request.declared.write],
|
|
105
|
+
conflicts: [...request.declared.conflicts],
|
|
106
|
+
resources: [...request.declared.resources],
|
|
107
|
+
},
|
|
108
|
+
...(request.approvedDependencies === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { approvedDependencies: [...request.approvedDependencies] }),
|
|
111
|
+
...(request.impactClosure === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { impactClosure: structuredClone(request.impactClosure) }),
|
|
114
|
+
...(request.verification === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: {
|
|
117
|
+
verification: {
|
|
118
|
+
...request.verification,
|
|
119
|
+
argv: [...request.verification.argv],
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
...(request.snapshot === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { snapshot: structuredClone(request.snapshot) }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deriveImplementEnvelope(
|
|
129
|
+
record: TaskRecord,
|
|
130
|
+
attempt: PhaseAttempt,
|
|
131
|
+
): RequestEnvelope {
|
|
132
|
+
const boundary = record.boundary;
|
|
133
|
+
const phase = boundary.phases[attempt.phase];
|
|
134
|
+
if (!phase) throw new Error("task attempt phase is not declared");
|
|
135
|
+
return {
|
|
136
|
+
stage: "abel-implement",
|
|
137
|
+
role: "implementation-worker",
|
|
138
|
+
taskId: boundary.taskId,
|
|
139
|
+
id: attempt.requestId,
|
|
140
|
+
phase: attempt.phase,
|
|
141
|
+
objective: boundary.objective,
|
|
142
|
+
roots: [...boundary.roots],
|
|
143
|
+
context: { ...boundary.context },
|
|
144
|
+
declared: {
|
|
145
|
+
read: [...phase.read],
|
|
146
|
+
write: [...phase.write],
|
|
147
|
+
conflicts: [...boundary.scheduling.conflicts],
|
|
148
|
+
resources: [...boundary.scheduling.resources],
|
|
149
|
+
...(phase.verificationLock === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { verificationLock: phase.verificationLock }),
|
|
152
|
+
},
|
|
153
|
+
output: "diff",
|
|
154
|
+
agentsImpact: boundary.agents.impact,
|
|
155
|
+
...(boundary.agents.target === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { agentsTarget: boundary.agents.target }),
|
|
158
|
+
agentsManagedOnly: true,
|
|
159
|
+
approvedDependencies: [...boundary.approvedDependencies],
|
|
160
|
+
impactClosure: structuredClone(boundary.impactClosure),
|
|
161
|
+
verification: {
|
|
162
|
+
...phase.verification,
|
|
163
|
+
argv: [...phase.verification.argv],
|
|
164
|
+
},
|
|
165
|
+
snapshot: structuredClone(attempt.snapshot),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextDeclaredPhase(
|
|
170
|
+
boundary: TaskBoundary,
|
|
171
|
+
phase: PhaseAttempt["phase"],
|
|
172
|
+
): "green" | "refactor" | null {
|
|
173
|
+
if (phase === "red") return "green";
|
|
174
|
+
if (phase === "green" && boundary.phases.refactor) return "refactor";
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertAttemptAllowed(record: TaskRecord, attempt: PhaseAttempt): void {
|
|
179
|
+
switch (record.state.kind) {
|
|
180
|
+
case "candidate-pending":
|
|
181
|
+
throw new Error("current phase candidate is awaiting parent apply");
|
|
182
|
+
case "agents-checkpoint-pending":
|
|
183
|
+
throw new Error("AGENTS checkpoint is pending");
|
|
184
|
+
case "blocked":
|
|
185
|
+
if (attempt.phase !== record.state.phase) {
|
|
186
|
+
throw new Error("terminal task phase mismatch");
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
case "completed":
|
|
190
|
+
if (attempt.phase !== record.state.finalPhase) {
|
|
191
|
+
throw new Error("terminal task phase mismatch");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
case "ready":
|
|
195
|
+
if (attempt.phase !== record.state.phase) {
|
|
196
|
+
throw new Error("invalid implementation phase transition");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function taskPhase(record: TaskRecord): PhaseAttempt["phase"] {
|
|
202
|
+
return record.state.kind === "agents-checkpoint-pending" ||
|
|
203
|
+
record.state.kind === "completed"
|
|
204
|
+
? record.state.finalPhase
|
|
205
|
+
: record.state.phase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function taskLaunchIndex(record: TaskRecord): 0 | 1 {
|
|
209
|
+
return record.state.kind === "ready" ||
|
|
210
|
+
record.state.kind === "candidate-pending"
|
|
211
|
+
? record.state.launchIndex
|
|
212
|
+
: record.state.kind === "blocked" &&
|
|
213
|
+
record.state.failure.kind === "attempts-exhausted"
|
|
214
|
+
? 1
|
|
215
|
+
: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
45
218
|
interface PreparedPreflight {
|
|
46
219
|
snapshot: Bound;
|
|
47
220
|
baseline: BaselineEntry[];
|
|
@@ -194,7 +367,9 @@ function preparePreflight(
|
|
|
194
367
|
};
|
|
195
368
|
}
|
|
196
369
|
|
|
197
|
-
function cancellationError(
|
|
370
|
+
function cancellationError(
|
|
371
|
+
signal: AbortSignal,
|
|
372
|
+
): Extract<WrappedDispatchResult, { ok: false }> {
|
|
198
373
|
const reason = signal.reason;
|
|
199
374
|
return {
|
|
200
375
|
ok: false,
|
|
@@ -209,60 +384,140 @@ function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
|
209
384
|
|
|
210
385
|
type FailureClass = "transport" | "artifact" | "environment";
|
|
211
386
|
|
|
212
|
-
type
|
|
387
|
+
type InternalResultMetadata = {
|
|
213
388
|
failureKind?: RunFailureKind;
|
|
214
389
|
failureClass?: FailureClass;
|
|
390
|
+
failure?: ChildFailure | TaskFailure;
|
|
215
391
|
launchConsumed?: true;
|
|
216
392
|
};
|
|
217
393
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
394
|
+
type ChildDispatchResult = WrappedDispatchResult & InternalResultMetadata;
|
|
395
|
+
type InternalDispatchResult = (
|
|
396
|
+
| WrappedDispatchResult
|
|
397
|
+
| (ImplementOutcome & { usage?: Usage })
|
|
398
|
+
) &
|
|
399
|
+
InternalResultMetadata;
|
|
400
|
+
type InternalFailureResult = Extract<WrappedDispatchResult, { ok: false }> &
|
|
401
|
+
InternalResultMetadata;
|
|
402
|
+
|
|
403
|
+
interface OutcomeIdentity {
|
|
404
|
+
taskId: string;
|
|
405
|
+
requestId: string;
|
|
406
|
+
phase: ImplementationPhase;
|
|
222
407
|
}
|
|
223
408
|
|
|
224
|
-
function
|
|
225
|
-
identity:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
):
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
partialResultUsable: false,
|
|
241
|
-
independentResultsPreserved: true,
|
|
242
|
-
next,
|
|
243
|
-
};
|
|
409
|
+
function blockedOutcome(
|
|
410
|
+
identity: OutcomeIdentity,
|
|
411
|
+
failure: TaskFailure,
|
|
412
|
+
): ImplementOutcome {
|
|
413
|
+
return { kind: "blocked", ...identity, failure: structuredClone(failure) };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function cancelledOutcome(identity: OutcomeIdentity): ImplementOutcome {
|
|
417
|
+
return { kind: "cancelled", ...identity };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function retryOutcome(
|
|
421
|
+
identity: OutcomeIdentity,
|
|
422
|
+
scope: "worker" | "checkpoint",
|
|
423
|
+
cause: "artifact" | "stale",
|
|
424
|
+
): ImplementOutcome {
|
|
244
425
|
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
426
|
+
kind: "retry",
|
|
427
|
+
...identity,
|
|
428
|
+
scope,
|
|
429
|
+
cause,
|
|
430
|
+
remainingAttempts: 1,
|
|
249
431
|
};
|
|
250
432
|
}
|
|
251
433
|
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
434
|
+
function failureClassOf(failure: ChildFailure): FailureClass | undefined {
|
|
435
|
+
switch (failure.kind) {
|
|
436
|
+
case "artifact":
|
|
437
|
+
return "artifact";
|
|
438
|
+
case "environment":
|
|
439
|
+
return "environment";
|
|
440
|
+
case "transport":
|
|
441
|
+
return "transport";
|
|
442
|
+
case "stale":
|
|
443
|
+
case "approval-boundary":
|
|
444
|
+
case "cancelled":
|
|
445
|
+
case "result-limit":
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function artifactCorrectionEvidence(failure?: ChildFailure): string {
|
|
451
|
+
return failure?.kind === "artifact"
|
|
452
|
+
? `candidate:${failure.code}`
|
|
258
453
|
: "generated-artifact-rejection";
|
|
259
454
|
}
|
|
260
455
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
456
|
+
function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
457
|
+
if (result.failure) {
|
|
458
|
+
switch (result.failure.kind) {
|
|
459
|
+
case "artifact":
|
|
460
|
+
case "stale":
|
|
461
|
+
case "environment":
|
|
462
|
+
case "approval-boundary":
|
|
463
|
+
case "cancelled":
|
|
464
|
+
case "result-limit":
|
|
465
|
+
case "transport":
|
|
466
|
+
return result.failure;
|
|
467
|
+
case "attempts-exhausted":
|
|
468
|
+
case "checkpoint-attempts-exhausted":
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (result.failureKind === "cancelled") {
|
|
473
|
+
return { kind: "cancelled", code: "cancelled" };
|
|
474
|
+
}
|
|
475
|
+
switch (result.failureClass) {
|
|
476
|
+
case "artifact":
|
|
477
|
+
return { kind: "artifact", code: "invalid-diff" };
|
|
478
|
+
case "environment":
|
|
479
|
+
return { kind: "environment", code: "root-unavailable" };
|
|
480
|
+
default:
|
|
481
|
+
return {
|
|
482
|
+
kind: "transport",
|
|
483
|
+
code:
|
|
484
|
+
result.failureKind === "timed-out" ? "timeout" : "transport-failure",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function terminalTaskFailure(failure: ChildFailure): TaskFailure | undefined {
|
|
490
|
+
switch (failure.kind) {
|
|
491
|
+
case "approval-boundary":
|
|
492
|
+
case "environment":
|
|
493
|
+
case "result-limit":
|
|
494
|
+
return failure;
|
|
495
|
+
case "artifact":
|
|
496
|
+
case "stale":
|
|
497
|
+
case "cancelled":
|
|
498
|
+
case "transport":
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function attemptFailureCause(
|
|
504
|
+
failure: ChildFailure,
|
|
505
|
+
): "artifact" | "stale" | "transport" {
|
|
506
|
+
switch (failure.kind) {
|
|
507
|
+
case "artifact":
|
|
508
|
+
case "stale":
|
|
509
|
+
case "transport":
|
|
510
|
+
return failure.kind;
|
|
511
|
+
case "approval-boundary":
|
|
512
|
+
case "environment":
|
|
513
|
+
case "result-limit":
|
|
514
|
+
case "cancelled":
|
|
515
|
+
throw new Error("non-retryable failure reached the launch budget");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function assertNever(value: never): never {
|
|
520
|
+
throw new Error(`unhandled candidate failure: ${JSON.stringify(value)}`);
|
|
266
521
|
}
|
|
267
522
|
|
|
268
523
|
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
@@ -293,7 +548,7 @@ export type RuntimeActivityObserver = (
|
|
|
293
548
|
) => void | Promise<void>;
|
|
294
549
|
|
|
295
550
|
interface ScheduledRunResult {
|
|
296
|
-
dispatch:
|
|
551
|
+
dispatch: InternalDispatchResult;
|
|
297
552
|
failureKind?: RunFailureKind;
|
|
298
553
|
}
|
|
299
554
|
|
|
@@ -343,26 +598,30 @@ function isSafeBound(snapshot: unknown): snapshot is Bound {
|
|
|
343
598
|
return true;
|
|
344
599
|
}
|
|
345
600
|
|
|
346
|
-
export type
|
|
601
|
+
export type WrappedDispatchResult =
|
|
347
602
|
| {
|
|
348
603
|
ok: true;
|
|
349
604
|
action: string;
|
|
350
605
|
result?: unknown;
|
|
351
606
|
resultId?: string;
|
|
352
|
-
usage?:
|
|
607
|
+
usage?: Usage;
|
|
353
608
|
}
|
|
354
609
|
| {
|
|
355
610
|
ok: false;
|
|
356
611
|
notReady?: true;
|
|
357
612
|
error: string;
|
|
358
|
-
|
|
613
|
+
failure?: ChildFailure | TaskFailure;
|
|
614
|
+
usage?: Usage;
|
|
359
615
|
};
|
|
360
616
|
|
|
361
|
-
export
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
617
|
+
export type DispatchResult =
|
|
618
|
+
| WrappedDispatchResult
|
|
619
|
+
| (ImplementOutcome & { usage?: Usage });
|
|
620
|
+
|
|
621
|
+
export function isWrappedDispatchResult(
|
|
622
|
+
result: DispatchResult,
|
|
623
|
+
): result is WrappedDispatchResult {
|
|
624
|
+
return "ok" in result;
|
|
366
625
|
}
|
|
367
626
|
|
|
368
627
|
export interface RuntimeOptions {
|
|
@@ -378,6 +637,7 @@ interface StoredRunContext {
|
|
|
378
637
|
sequence: number;
|
|
379
638
|
runningEmitted: boolean;
|
|
380
639
|
terminalEmitted: boolean;
|
|
640
|
+
usage: UsageAggregator;
|
|
381
641
|
}
|
|
382
642
|
|
|
383
643
|
export class Runtime {
|
|
@@ -389,7 +649,7 @@ export class Runtime {
|
|
|
389
649
|
private batchSeq = 0;
|
|
390
650
|
private readonly parentPayloadBridge: ParentPayloadBridge;
|
|
391
651
|
private readonly registry = new WorkerRegistry();
|
|
392
|
-
private readonly
|
|
652
|
+
private readonly taskRecords = new WeakMap<RequestEnvelope, TaskRecord>();
|
|
393
653
|
private readonly runContexts = new WeakMap<
|
|
394
654
|
RequestEnvelope,
|
|
395
655
|
StoredRunContext
|
|
@@ -404,8 +664,57 @@ export class Runtime {
|
|
|
404
664
|
execute: async (request, signal) => {
|
|
405
665
|
const context = this.runContexts.get(request);
|
|
406
666
|
if (!context) throw new Error("scheduled run context is unavailable");
|
|
667
|
+
const task = this.taskRecords.get(request);
|
|
668
|
+
let candidateRollback:
|
|
669
|
+
| {
|
|
670
|
+
resultId: string;
|
|
671
|
+
requestId: string;
|
|
672
|
+
phase: ImplementationPhase;
|
|
673
|
+
launchIndex: 0 | 1;
|
|
674
|
+
priorState: TaskRecord["state"];
|
|
675
|
+
}
|
|
676
|
+
| undefined;
|
|
677
|
+
const rollback = () => {
|
|
678
|
+
const pending = candidateRollback;
|
|
679
|
+
if (!pending) return;
|
|
680
|
+
this.results.discard(pending.resultId);
|
|
681
|
+
if (
|
|
682
|
+
task?.state.kind === "candidate-pending" &&
|
|
683
|
+
task.state.resultId === pending.resultId &&
|
|
684
|
+
task.state.originRequestId === pending.requestId &&
|
|
685
|
+
task.state.phase === pending.phase &&
|
|
686
|
+
task.state.launchIndex === pending.launchIndex
|
|
687
|
+
) {
|
|
688
|
+
task.state = structuredClone(pending.priorState);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
signal.addEventListener("abort", rollback, { once: true });
|
|
407
692
|
this.notify(request, context, "running");
|
|
408
|
-
const result = await this.runScheduled(request, context
|
|
693
|
+
const result = await this.runScheduled(request, context, signal);
|
|
694
|
+
if (!isWrappedDispatchResult(result) && result.kind === "candidate") {
|
|
695
|
+
if (!task) throw new Error("candidate TaskRecord is unavailable");
|
|
696
|
+
if (task.state.kind !== "ready") {
|
|
697
|
+
throw new Error("candidate TaskRecord is not ready");
|
|
698
|
+
}
|
|
699
|
+
candidateRollback = {
|
|
700
|
+
resultId: result.resultId,
|
|
701
|
+
requestId: result.requestId,
|
|
702
|
+
phase: result.phase,
|
|
703
|
+
launchIndex: task.state.launchIndex,
|
|
704
|
+
priorState: structuredClone(task.state),
|
|
705
|
+
};
|
|
706
|
+
if (signal.aborted) {
|
|
707
|
+
rollback();
|
|
708
|
+
} else {
|
|
709
|
+
try {
|
|
710
|
+
this.rememberRetainedResult(result, request, task);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
this.results.discard(result.resultId);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
if (signal.aborted) rollback();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
409
718
|
return {
|
|
410
719
|
dispatch: result,
|
|
411
720
|
...(result.failureKind === undefined
|
|
@@ -418,13 +727,19 @@ export class Runtime {
|
|
|
418
727
|
|
|
419
728
|
async execute(
|
|
420
729
|
action: string,
|
|
421
|
-
params: {
|
|
730
|
+
params: {
|
|
731
|
+
request?: unknown;
|
|
732
|
+
resultId?: string;
|
|
733
|
+
requestId?: string;
|
|
734
|
+
rejection?: unknown;
|
|
735
|
+
agentsCheckpoint?: unknown;
|
|
736
|
+
},
|
|
422
737
|
ctx?: RunContext,
|
|
423
738
|
signal?: AbortSignal,
|
|
424
739
|
observer?: RuntimeActivityObserver,
|
|
425
740
|
): Promise<DispatchResult> {
|
|
426
741
|
if (!(ACTIONS as readonly string[]).includes(action)) {
|
|
427
|
-
|
|
742
|
+
throw new Error(`unknown action: ${String(action)}`);
|
|
428
743
|
}
|
|
429
744
|
if (!this.activation.isActive()) {
|
|
430
745
|
return { ok: false, notReady: true, error: "dispatcher is not active" };
|
|
@@ -432,13 +747,55 @@ export class Runtime {
|
|
|
432
747
|
if (action === "run")
|
|
433
748
|
return this.run(params.request, ctx, signal, observer);
|
|
434
749
|
if (action === "apply") {
|
|
435
|
-
if (!ctx
|
|
436
|
-
|
|
437
|
-
|
|
750
|
+
if (!ctx) throw new Error("apply requires context");
|
|
751
|
+
if (params.agentsCheckpoint !== undefined) {
|
|
752
|
+
if (params.resultId !== undefined) {
|
|
753
|
+
throw new Error("apply accepts either resultId or AGENTS checkpoint");
|
|
754
|
+
}
|
|
755
|
+
const validation = validateAgentsCheckpointAttempt(
|
|
756
|
+
params.agentsCheckpoint,
|
|
757
|
+
);
|
|
758
|
+
if (!validation.ok) {
|
|
759
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
760
|
+
}
|
|
761
|
+
return this.enqueueAgentsCheckpoint(ctx.cwd, validation.value, signal);
|
|
762
|
+
}
|
|
763
|
+
if (!params.resultId) {
|
|
764
|
+
throw new Error("apply requires resultId");
|
|
765
|
+
}
|
|
766
|
+
const retained = this.results.get(params.resultId);
|
|
767
|
+
if (
|
|
768
|
+
params.requestId !== undefined ||
|
|
769
|
+
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
770
|
+
) {
|
|
771
|
+
const validation = validateImplementApplyOperation(params);
|
|
772
|
+
if (!validation.ok) {
|
|
773
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
774
|
+
}
|
|
775
|
+
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
776
|
+
throw new Error("retained Implement result not found");
|
|
777
|
+
}
|
|
778
|
+
return this.enqueueApply(ctx.cwd, validation.value, signal);
|
|
779
|
+
}
|
|
780
|
+
return this.enqueueApply(ctx.cwd, { resultId: params.resultId }, signal);
|
|
438
781
|
}
|
|
439
782
|
if (action === "discard") {
|
|
440
|
-
if (!params.resultId)
|
|
441
|
-
|
|
783
|
+
if (!params.resultId) throw new Error("discard requires resultId");
|
|
784
|
+
const retained = this.results.get(params.resultId);
|
|
785
|
+
if (
|
|
786
|
+
params.requestId !== undefined ||
|
|
787
|
+
params.rejection !== undefined ||
|
|
788
|
+
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
789
|
+
) {
|
|
790
|
+
const validation = validateImplementDiscardOperation(params);
|
|
791
|
+
if (!validation.ok) {
|
|
792
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
793
|
+
}
|
|
794
|
+
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
795
|
+
throw new Error("retained Implement result not found");
|
|
796
|
+
}
|
|
797
|
+
return this.enqueueDiscard(validation.value);
|
|
798
|
+
}
|
|
442
799
|
return this.discardRetainedCandidate(params.resultId)
|
|
443
800
|
? { ok: true, action }
|
|
444
801
|
: { ok: false, error: "retained result not found" };
|
|
@@ -451,181 +808,493 @@ export class Runtime {
|
|
|
451
808
|
return { ok: true, action: "finish" };
|
|
452
809
|
}
|
|
453
810
|
|
|
454
|
-
private
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
811
|
+
private enqueueAgentsCheckpoint(
|
|
812
|
+
root: string,
|
|
813
|
+
attempt: AgentsCheckpointAttempt,
|
|
814
|
+
signal?: AbortSignal,
|
|
815
|
+
): Promise<DispatchResult> {
|
|
816
|
+
const sequence = ++this.applySeq;
|
|
817
|
+
return this.enqueueParentApply(() =>
|
|
818
|
+
this.performAgentsCheckpoint(root, attempt, signal),
|
|
819
|
+
).then((result) => {
|
|
820
|
+
if (
|
|
821
|
+
isWrappedDispatchResult(result) ||
|
|
822
|
+
result.kind !== "completed" ||
|
|
823
|
+
!result.result
|
|
824
|
+
) {
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
...result,
|
|
829
|
+
result: { ...result.result, sequence },
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private async performAgentsCheckpoint(
|
|
835
|
+
root: string,
|
|
836
|
+
attempt: AgentsCheckpointAttempt,
|
|
837
|
+
signal?: AbortSignal,
|
|
838
|
+
): Promise<DispatchResult> {
|
|
839
|
+
const workspaceRoot = canonicalWorkspaceRoot(root);
|
|
840
|
+
if (!workspaceRoot) {
|
|
841
|
+
throw new Error("AGENTS checkpoint workspace is unavailable");
|
|
842
|
+
}
|
|
843
|
+
const worker = this.registry.get(
|
|
844
|
+
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
845
|
+
);
|
|
846
|
+
if (!worker) {
|
|
847
|
+
if (
|
|
848
|
+
this.registry
|
|
849
|
+
.values()
|
|
850
|
+
.some((record) => record.workspaceRoot === workspaceRoot)
|
|
851
|
+
) {
|
|
852
|
+
throw new Error("AGENTS checkpoint identity mismatch");
|
|
853
|
+
}
|
|
854
|
+
throw new Error("logical Worker is unavailable for AGENTS checkpoint");
|
|
855
|
+
}
|
|
856
|
+
const agents = worker.boundary.agents;
|
|
857
|
+
if (agents.impact === "none" || !agents.target || !agents.managedOnly) {
|
|
858
|
+
throw new Error("AGENTS checkpoint contract mismatch");
|
|
859
|
+
}
|
|
860
|
+
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
861
|
+
throw new Error("AGENTS checkpoint is not pending");
|
|
862
|
+
}
|
|
460
863
|
if (
|
|
461
|
-
|
|
462
|
-
|
|
864
|
+
Object.keys(attempt.snapshot as Record<string, unknown>).length !== 1 ||
|
|
865
|
+
!Object.hasOwn(attempt.snapshot as object, agents.target)
|
|
463
866
|
) {
|
|
464
|
-
|
|
867
|
+
throw new Error("AGENTS checkpoint target mismatch");
|
|
465
868
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
worker.
|
|
469
|
-
|
|
470
|
-
|
|
869
|
+
const request: AgentsCheckpointRequest = {
|
|
870
|
+
stage: "abel-implement",
|
|
871
|
+
taskId: worker.boundary.taskId,
|
|
872
|
+
agentsImpact: agents.impact,
|
|
873
|
+
agentsTarget: agents.target,
|
|
874
|
+
agentsManagedOnly: true,
|
|
875
|
+
stableCheckpoint: true,
|
|
876
|
+
snapshot: attempt.snapshot,
|
|
877
|
+
diff: attempt.diff,
|
|
878
|
+
};
|
|
879
|
+
const result = await applyAgentsCheckpoint(workspaceRoot, request, signal);
|
|
880
|
+
if (result.ok) {
|
|
881
|
+
const finalPhase = worker.state.finalPhase;
|
|
882
|
+
worker.state = {
|
|
883
|
+
kind: "completed",
|
|
884
|
+
finalPhase,
|
|
885
|
+
};
|
|
886
|
+
const { ok: _ok, ...checkpoint } = result;
|
|
887
|
+
return {
|
|
888
|
+
kind: "completed",
|
|
889
|
+
requestId: attempt.requestId,
|
|
890
|
+
taskId: attempt.taskId,
|
|
891
|
+
finalPhase,
|
|
892
|
+
result: checkpoint,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return this.presentCheckpointFailure(attempt, worker, result.failure);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private presentCheckpointFailure(
|
|
899
|
+
attempt: AgentsCheckpointAttempt,
|
|
900
|
+
worker: TaskRecord,
|
|
901
|
+
failure: CandidateFailure,
|
|
902
|
+
): DispatchResult {
|
|
903
|
+
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
904
|
+
throw new Error("AGENTS checkpoint is not pending");
|
|
905
|
+
}
|
|
906
|
+
const phase = worker.state.finalPhase;
|
|
907
|
+
const identity = {
|
|
908
|
+
taskId: attempt.taskId,
|
|
909
|
+
requestId: attempt.requestId,
|
|
910
|
+
phase,
|
|
911
|
+
};
|
|
912
|
+
switch (failure.kind) {
|
|
913
|
+
case "artifact": {
|
|
914
|
+
if (worker.state.attemptIndex === 1) {
|
|
915
|
+
const taskFailure: TaskFailure = {
|
|
916
|
+
kind: "checkpoint-attempts-exhausted",
|
|
917
|
+
cause: "artifact",
|
|
918
|
+
};
|
|
919
|
+
worker.state = {
|
|
920
|
+
kind: "blocked",
|
|
921
|
+
phase,
|
|
922
|
+
failure: structuredClone(taskFailure),
|
|
923
|
+
};
|
|
924
|
+
return blockedOutcome(identity, taskFailure);
|
|
925
|
+
}
|
|
926
|
+
worker.state = {
|
|
927
|
+
kind: "agents-checkpoint-pending",
|
|
928
|
+
finalPhase: phase,
|
|
929
|
+
attemptIndex: 1,
|
|
930
|
+
};
|
|
931
|
+
return retryOutcome(identity, "checkpoint", "artifact");
|
|
932
|
+
}
|
|
933
|
+
case "stale": {
|
|
934
|
+
if (worker.state.attemptIndex === 1) {
|
|
935
|
+
const taskFailure: TaskFailure = {
|
|
936
|
+
kind: "checkpoint-attempts-exhausted",
|
|
937
|
+
cause: "stale",
|
|
938
|
+
};
|
|
939
|
+
worker.state = {
|
|
940
|
+
kind: "blocked",
|
|
941
|
+
phase,
|
|
942
|
+
failure: structuredClone(taskFailure),
|
|
943
|
+
};
|
|
944
|
+
return blockedOutcome(identity, taskFailure);
|
|
945
|
+
}
|
|
946
|
+
worker.state = {
|
|
947
|
+
kind: "agents-checkpoint-pending",
|
|
948
|
+
finalPhase: phase,
|
|
949
|
+
attemptIndex: 1,
|
|
950
|
+
};
|
|
951
|
+
return retryOutcome(identity, "checkpoint", "stale");
|
|
952
|
+
}
|
|
953
|
+
case "environment": {
|
|
954
|
+
const taskFailure: TaskFailure = failure;
|
|
955
|
+
worker.state = {
|
|
956
|
+
kind: "blocked",
|
|
957
|
+
phase,
|
|
958
|
+
failure: structuredClone(taskFailure),
|
|
959
|
+
};
|
|
960
|
+
return blockedOutcome(identity, taskFailure);
|
|
961
|
+
}
|
|
962
|
+
case "approval-boundary": {
|
|
963
|
+
const taskFailure: TaskFailure = failure;
|
|
964
|
+
worker.state = {
|
|
965
|
+
kind: "blocked",
|
|
966
|
+
phase,
|
|
967
|
+
failure: structuredClone(taskFailure),
|
|
968
|
+
};
|
|
969
|
+
return blockedOutcome(identity, taskFailure);
|
|
970
|
+
}
|
|
971
|
+
case "cancelled":
|
|
972
|
+
return cancelledOutcome(identity);
|
|
973
|
+
case "result-limit": {
|
|
974
|
+
const taskFailure: TaskFailure = failure;
|
|
975
|
+
worker.state = {
|
|
976
|
+
kind: "blocked",
|
|
977
|
+
phase,
|
|
978
|
+
failure: structuredClone(taskFailure),
|
|
979
|
+
};
|
|
980
|
+
return blockedOutcome(identity, taskFailure);
|
|
981
|
+
}
|
|
982
|
+
default:
|
|
983
|
+
return assertNever(failure);
|
|
471
984
|
}
|
|
472
|
-
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private resolveRetainedCandidate(
|
|
988
|
+
resultId: string,
|
|
989
|
+
operationRoot?: string,
|
|
990
|
+
): { retained: BoundRetainedResult; worker: TaskRecord } | undefined {
|
|
991
|
+
const candidate = this.results.get(resultId);
|
|
992
|
+
if (!candidate) return undefined;
|
|
993
|
+
if (!this.results.hasCandidateIdentity(resultId)) return undefined;
|
|
994
|
+
if (
|
|
995
|
+
candidate.stage !== "abel-implement" ||
|
|
996
|
+
candidate.canonicalRoot === undefined ||
|
|
997
|
+
candidate.changeId === undefined ||
|
|
998
|
+
candidate.taskId === undefined ||
|
|
999
|
+
candidate.originRequestId === undefined ||
|
|
1000
|
+
candidate.phase === undefined ||
|
|
1001
|
+
candidate.launchIndex === undefined
|
|
1002
|
+
) {
|
|
1003
|
+
throw new Error("retained candidate identity mismatch");
|
|
1004
|
+
}
|
|
1005
|
+
if (
|
|
1006
|
+
operationRoot !== undefined &&
|
|
1007
|
+
canonicalWorkspaceRoot(operationRoot) !== candidate.canonicalRoot
|
|
1008
|
+
) {
|
|
1009
|
+
throw new Error("retained candidate identity mismatch");
|
|
1010
|
+
}
|
|
1011
|
+
const worker = this.registry.get(
|
|
1012
|
+
taskRecordKey(
|
|
1013
|
+
candidate.canonicalRoot,
|
|
1014
|
+
candidate.changeId,
|
|
1015
|
+
candidate.taskId,
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
if (!worker) throw new Error("retained candidate identity mismatch");
|
|
1019
|
+
if (
|
|
1020
|
+
worker.state.kind !== "candidate-pending" ||
|
|
1021
|
+
worker.state.resultId !== resultId ||
|
|
1022
|
+
worker.state.originRequestId !== candidate.originRequestId ||
|
|
1023
|
+
worker.state.phase !== candidate.phase ||
|
|
1024
|
+
worker.state.launchIndex !== candidate.launchIndex
|
|
1025
|
+
) {
|
|
1026
|
+
throw new Error("retained candidate identity mismatch");
|
|
1027
|
+
}
|
|
1028
|
+
const phase = worker.boundary.phases[worker.state.phase];
|
|
1029
|
+
if (!phase || !isSafeBound(candidate.snapshot)) {
|
|
1030
|
+
throw new Error("retained candidate identity mismatch");
|
|
1031
|
+
}
|
|
1032
|
+
const expected: RetainedCandidateFacts = {
|
|
1033
|
+
stage: "abel-implement",
|
|
1034
|
+
canonicalRoot: worker.workspaceRoot,
|
|
1035
|
+
root: worker.workspaceRoot,
|
|
1036
|
+
changeId: worker.boundary.changeId,
|
|
1037
|
+
taskId: worker.boundary.taskId,
|
|
1038
|
+
originRequestId: worker.state.originRequestId,
|
|
1039
|
+
phase: worker.state.phase,
|
|
1040
|
+
launchIndex: worker.state.launchIndex,
|
|
1041
|
+
writeSet: [...phase.write],
|
|
1042
|
+
approvedDependencies: [...worker.boundary.approvedDependencies],
|
|
1043
|
+
snapshot: candidate.snapshot,
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
retained: this.results.resolveIdentity(resultId, expected),
|
|
1047
|
+
worker,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private discardRetainedCandidate(resultId: string): boolean {
|
|
1052
|
+
return this.results.discard(resultId);
|
|
473
1053
|
}
|
|
474
1054
|
|
|
475
1055
|
private enqueueApply(
|
|
476
1056
|
root: string,
|
|
477
|
-
|
|
1057
|
+
operation: ImplementApplyOperation | { resultId: string },
|
|
478
1058
|
signal?: AbortSignal,
|
|
479
1059
|
): Promise<DispatchResult> {
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
(
|
|
1060
|
+
const id = operation.resultId;
|
|
1061
|
+
const resolved = this.resolveRetainedCandidate(id, root);
|
|
1062
|
+
const requestId = "requestId" in operation ? operation.requestId : null;
|
|
1063
|
+
if (requestId !== null && !resolved) {
|
|
1064
|
+
throw new Error("retained Implement result not found");
|
|
1065
|
+
}
|
|
1066
|
+
if (resolved && requestId === null) {
|
|
1067
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1068
|
+
}
|
|
1069
|
+
const applyRoot = resolved?.retained.root ?? root;
|
|
1070
|
+
const run = this.enqueueParentApply(() =>
|
|
1071
|
+
applyRetainedPatch({ root: applyRoot, id, store: this.results, signal }),
|
|
488
1072
|
);
|
|
489
1073
|
const seq = ++this.applySeq;
|
|
490
|
-
return run.then(
|
|
491
|
-
(result)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1074
|
+
return run.then((result) => {
|
|
1075
|
+
if (result.ok) {
|
|
1076
|
+
const applied = { ...result.result, sequence: seq };
|
|
1077
|
+
if (resolved) {
|
|
1078
|
+
if (requestId === null)
|
|
1079
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1080
|
+
const { retained: identity, worker } = resolved;
|
|
1081
|
+
if (worker.state.kind === "candidate-pending") {
|
|
1082
|
+
const readyPhase = nextDeclaredPhase(
|
|
1083
|
+
worker.boundary,
|
|
1084
|
+
identity.phase,
|
|
1085
|
+
);
|
|
1086
|
+
if (readyPhase) {
|
|
1087
|
+
worker.state = {
|
|
1088
|
+
kind: "ready",
|
|
1089
|
+
phase: readyPhase,
|
|
1090
|
+
launchIndex: 0,
|
|
1091
|
+
};
|
|
1092
|
+
return {
|
|
1093
|
+
kind: "applied",
|
|
1094
|
+
requestId,
|
|
1095
|
+
taskId: identity.taskId,
|
|
1096
|
+
phase: identity.phase as "red" | "green",
|
|
1097
|
+
readyPhase,
|
|
1098
|
+
result: applied,
|
|
1099
|
+
} satisfies ImplementOutcome;
|
|
1100
|
+
} else if (worker.boundary.agents.impact === "none") {
|
|
1101
|
+
worker.state = {
|
|
1102
|
+
kind: "completed",
|
|
1103
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1104
|
+
};
|
|
1105
|
+
return {
|
|
1106
|
+
kind: "completed",
|
|
1107
|
+
requestId,
|
|
1108
|
+
taskId: identity.taskId,
|
|
1109
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1110
|
+
result: applied,
|
|
1111
|
+
} satisfies ImplementOutcome;
|
|
1112
|
+
} else {
|
|
1113
|
+
worker.state = {
|
|
1114
|
+
kind: "agents-checkpoint-pending",
|
|
1115
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1116
|
+
attemptIndex: 0,
|
|
1117
|
+
};
|
|
1118
|
+
return {
|
|
1119
|
+
kind: "checkpoint-required",
|
|
1120
|
+
requestId,
|
|
1121
|
+
taskId: identity.taskId,
|
|
1122
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1123
|
+
result: applied,
|
|
1124
|
+
} satisfies ImplementOutcome;
|
|
501
1125
|
}
|
|
502
1126
|
}
|
|
503
|
-
|
|
504
|
-
ok: true,
|
|
505
|
-
action: "apply",
|
|
506
|
-
result: { ...result, sequence: seq },
|
|
507
|
-
};
|
|
1127
|
+
throw new Error("retained candidate is not pending");
|
|
508
1128
|
}
|
|
509
|
-
return
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
result
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1129
|
+
return {
|
|
1130
|
+
ok: true,
|
|
1131
|
+
action: "apply",
|
|
1132
|
+
result: applied,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
if (!resolved) {
|
|
1136
|
+
return {
|
|
1137
|
+
ok: false,
|
|
1138
|
+
error: "candidate application failed",
|
|
1139
|
+
failure: result.failure,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
if (requestId === null)
|
|
1143
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1144
|
+
return this.presentApplyFailure(
|
|
1145
|
+
requestId,
|
|
1146
|
+
id,
|
|
1147
|
+
resolved.retained,
|
|
1148
|
+
resolved.worker,
|
|
1149
|
+
result.failure,
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private enqueueDiscard(
|
|
1155
|
+
operation: ImplementDiscardOperation,
|
|
1156
|
+
): Promise<DispatchResult> {
|
|
1157
|
+
return this.enqueueParentApply(async () => {
|
|
1158
|
+
const resolved = this.resolveRetainedCandidate(operation.resultId);
|
|
1159
|
+
if (!resolved || !this.results.discard(operation.resultId)) {
|
|
1160
|
+
throw new Error("retained Implement result not found");
|
|
1161
|
+
}
|
|
1162
|
+
const { retained: identity, worker } = resolved;
|
|
1163
|
+
const outcomeIdentity = {
|
|
1164
|
+
requestId: operation.requestId,
|
|
1165
|
+
taskId: identity.taskId,
|
|
1166
|
+
phase: identity.phase,
|
|
1167
|
+
};
|
|
1168
|
+
const rejection = operation.rejection;
|
|
1169
|
+
if (rejection.kind === "approval-boundary") {
|
|
1170
|
+
worker.state = {
|
|
1171
|
+
kind: "blocked",
|
|
1172
|
+
phase: identity.phase,
|
|
1173
|
+
failure: structuredClone(rejection),
|
|
1174
|
+
};
|
|
1175
|
+
return blockedOutcome(outcomeIdentity, rejection);
|
|
1176
|
+
}
|
|
1177
|
+
if (identity.launchIndex === 0) {
|
|
1178
|
+
worker.state = {
|
|
1179
|
+
kind: "ready",
|
|
1180
|
+
phase: identity.phase,
|
|
1181
|
+
launchIndex: 1,
|
|
1182
|
+
correction: structuredClone(rejection),
|
|
1183
|
+
};
|
|
1184
|
+
return retryOutcome(outcomeIdentity, "worker", "artifact");
|
|
1185
|
+
}
|
|
1186
|
+
const failure: TaskFailure = {
|
|
1187
|
+
kind: "attempts-exhausted",
|
|
1188
|
+
cause: "artifact",
|
|
1189
|
+
};
|
|
1190
|
+
worker.state = {
|
|
1191
|
+
kind: "blocked",
|
|
1192
|
+
phase: identity.phase,
|
|
1193
|
+
failure: structuredClone(failure),
|
|
1194
|
+
};
|
|
1195
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private enqueueParentApply<T>(operation: () => Promise<T>): Promise<T> {
|
|
1200
|
+
const run = this.applyTail.then(operation, operation);
|
|
1201
|
+
this.applyTail = run.then(
|
|
1202
|
+
() => undefined,
|
|
1203
|
+
() => undefined,
|
|
523
1204
|
);
|
|
1205
|
+
return run;
|
|
524
1206
|
}
|
|
525
1207
|
|
|
526
1208
|
private presentApplyFailure(
|
|
1209
|
+
requestId: string,
|
|
527
1210
|
resultId: string,
|
|
528
|
-
identity:
|
|
529
|
-
|
|
530
|
-
|
|
1211
|
+
identity: BoundRetainedResult,
|
|
1212
|
+
worker: TaskRecord,
|
|
1213
|
+
failure: CandidateFailure,
|
|
531
1214
|
): DispatchResult {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1215
|
+
const outcomeIdentity = {
|
|
1216
|
+
taskId: identity.taskId,
|
|
1217
|
+
requestId,
|
|
1218
|
+
phase: identity.phase,
|
|
1219
|
+
};
|
|
1220
|
+
switch (failure.kind) {
|
|
1221
|
+
case "environment": {
|
|
1222
|
+
this.results.discard(resultId);
|
|
1223
|
+
worker.state = {
|
|
1224
|
+
kind: "blocked",
|
|
542
1225
|
phase: identity.phase,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
identity.launchIndex,
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
const worker = this.registry.get(identity.taskId);
|
|
550
|
-
if (/candidate preflight rejected: design:/i.test(error)) {
|
|
551
|
-
this.results.discard(resultId);
|
|
552
|
-
this.retainedRuns.delete(resultId);
|
|
553
|
-
if (worker) {
|
|
554
|
-
worker.state = { kind: "ready" };
|
|
1226
|
+
failure: structuredClone(failure),
|
|
1227
|
+
};
|
|
1228
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
555
1229
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1230
|
+
case "approval-boundary": {
|
|
1231
|
+
this.results.discard(resultId);
|
|
1232
|
+
worker.state = {
|
|
1233
|
+
kind: "blocked",
|
|
560
1234
|
phase: identity.phase,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
if (
|
|
570
|
-
/^(?:candidate preflight rejected: stale:|stale file snapshot)/i.test(
|
|
571
|
-
error,
|
|
572
|
-
)
|
|
573
|
-
) {
|
|
574
|
-
this.results.discard(resultId);
|
|
575
|
-
this.retainedRuns.delete(resultId);
|
|
576
|
-
if (worker) {
|
|
1235
|
+
failure: structuredClone(failure),
|
|
1236
|
+
};
|
|
1237
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1238
|
+
}
|
|
1239
|
+
case "stale": {
|
|
1240
|
+
this.results.discard(resultId);
|
|
577
1241
|
if (identity.launchIndex === 0) {
|
|
578
|
-
worker.state = {
|
|
1242
|
+
worker.state = {
|
|
1243
|
+
kind: "ready",
|
|
1244
|
+
phase: identity.phase,
|
|
1245
|
+
launchIndex: 1,
|
|
1246
|
+
correction: structuredClone(failure),
|
|
1247
|
+
};
|
|
579
1248
|
} else {
|
|
580
|
-
|
|
1249
|
+
const taskFailure: TaskFailure = {
|
|
1250
|
+
kind: "attempts-exhausted",
|
|
1251
|
+
cause: "stale",
|
|
1252
|
+
};
|
|
1253
|
+
worker.state = {
|
|
1254
|
+
kind: "blocked",
|
|
1255
|
+
phase: identity.phase,
|
|
1256
|
+
failure: structuredClone(taskFailure),
|
|
1257
|
+
};
|
|
1258
|
+
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
581
1259
|
}
|
|
1260
|
+
return retryOutcome(outcomeIdentity, "worker", "stale");
|
|
582
1261
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1262
|
+
case "artifact": {
|
|
1263
|
+
this.results.discard(resultId);
|
|
1264
|
+
const correctionAvailable = identity.launchIndex === 0;
|
|
1265
|
+
if (!correctionAvailable) {
|
|
1266
|
+
const taskFailure: TaskFailure = {
|
|
1267
|
+
kind: "attempts-exhausted",
|
|
1268
|
+
cause: "artifact",
|
|
1269
|
+
};
|
|
1270
|
+
worker.state = {
|
|
1271
|
+
kind: "blocked",
|
|
588
1272
|
phase: identity.phase,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1273
|
+
failure: structuredClone(taskFailure),
|
|
1274
|
+
};
|
|
1275
|
+
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1276
|
+
}
|
|
1277
|
+
worker.state = {
|
|
1278
|
+
kind: "ready",
|
|
1279
|
+
phase: identity.phase,
|
|
1280
|
+
launchIndex: 1,
|
|
1281
|
+
correction: structuredClone(failure),
|
|
1282
|
+
};
|
|
1283
|
+
return retryOutcome(outcomeIdentity, "worker", "artifact");
|
|
596
1284
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
? {
|
|
610
|
-
kind: "artifact-correction-pending",
|
|
611
|
-
rejection: normalizedArtifactRejection(error),
|
|
612
|
-
}
|
|
613
|
-
: { kind: "blocked", reason: "artifact" };
|
|
1285
|
+
case "cancelled":
|
|
1286
|
+
return cancelledOutcome(outcomeIdentity);
|
|
1287
|
+
case "result-limit":
|
|
1288
|
+
this.results.discard(resultId);
|
|
1289
|
+
worker.state = {
|
|
1290
|
+
kind: "blocked",
|
|
1291
|
+
phase: identity.phase,
|
|
1292
|
+
failure: structuredClone(failure),
|
|
1293
|
+
};
|
|
1294
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1295
|
+
default:
|
|
1296
|
+
return assertNever(failure);
|
|
614
1297
|
}
|
|
615
|
-
return recoveryFailure(
|
|
616
|
-
{
|
|
617
|
-
taskId: identity.taskId,
|
|
618
|
-
id: identity.requestId,
|
|
619
|
-
phase: identity.phase,
|
|
620
|
-
},
|
|
621
|
-
"implementation-artifact-delivery-blocked",
|
|
622
|
-
correctionAvailable ? "correct-artifact" : "finish-unaffected",
|
|
623
|
-
identity.launchIndex,
|
|
624
|
-
"failed",
|
|
625
|
-
correctionAvailable
|
|
626
|
-
? "generated implementation artifact requires bounded correction"
|
|
627
|
-
: "artifact correction launch budget is exhausted",
|
|
628
|
-
);
|
|
629
1298
|
}
|
|
630
1299
|
|
|
631
1300
|
private async dispatchChild(
|
|
@@ -634,30 +1303,27 @@ export class Runtime {
|
|
|
634
1303
|
ctx: RunContext,
|
|
635
1304
|
signal: AbortSignal,
|
|
636
1305
|
artifactRejection?: string,
|
|
637
|
-
): Promise<
|
|
1306
|
+
): Promise<ChildDispatchResult> {
|
|
638
1307
|
if (signal.aborted)
|
|
639
1308
|
return {
|
|
640
1309
|
...cancellationError(signal),
|
|
641
1310
|
failureKind: "cancelled",
|
|
642
1311
|
};
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
error.message === "parent payload bridge is unavailable";
|
|
1312
|
+
const phase = await runtimeFromContext(
|
|
1313
|
+
ctx,
|
|
1314
|
+
this.parentPayloadBridge,
|
|
1315
|
+
signal,
|
|
1316
|
+
);
|
|
1317
|
+
if (!phase.ok) {
|
|
650
1318
|
return {
|
|
651
1319
|
ok: false,
|
|
652
|
-
error:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
?
|
|
658
|
-
:
|
|
659
|
-
? "transport"
|
|
660
|
-
: "environment",
|
|
1320
|
+
error: phase.error,
|
|
1321
|
+
failure: phase.failure,
|
|
1322
|
+
failureKind:
|
|
1323
|
+
phase.failure.kind === "cancelled" ? "cancelled" : "failed",
|
|
1324
|
+
...(phase.failure.kind === "cancelled"
|
|
1325
|
+
? {}
|
|
1326
|
+
: { failureClass: phase.failure.kind }),
|
|
661
1327
|
};
|
|
662
1328
|
}
|
|
663
1329
|
if (signal.aborted)
|
|
@@ -675,6 +1341,11 @@ export class Runtime {
|
|
|
675
1341
|
readSet: [...envelope.declared.read],
|
|
676
1342
|
writeSet: [...envelope.declared.write],
|
|
677
1343
|
verification: envelope.verification ?? null,
|
|
1344
|
+
agentsImpact: envelope.agentsImpact ?? "none",
|
|
1345
|
+
agentsTarget: envelope.agentsTarget ?? null,
|
|
1346
|
+
agentsManagedOnly: true,
|
|
1347
|
+
agentsWriteAllowed: false,
|
|
1348
|
+
impactClosure: envelope.impactClosure ?? null,
|
|
678
1349
|
};
|
|
679
1350
|
const systemPrompt = [
|
|
680
1351
|
agent.content,
|
|
@@ -707,22 +1378,13 @@ export class Runtime {
|
|
|
707
1378
|
return {
|
|
708
1379
|
...cancellationError(signal),
|
|
709
1380
|
failureKind: "cancelled",
|
|
1381
|
+
usage: child.usage,
|
|
710
1382
|
};
|
|
711
1383
|
if (!child.ok) {
|
|
712
|
-
const generatedArtifact =
|
|
713
|
-
child.classification.attempts > 0 ||
|
|
714
|
-
child.classification.finalCategory === "text-only" ||
|
|
715
|
-
child.classification.finalCategory === "mixed" ||
|
|
716
|
-
child.classification.finalCategory === "multiple-submit";
|
|
717
1384
|
return {
|
|
718
1385
|
...child,
|
|
719
1386
|
failureKind: child.failureKind,
|
|
720
|
-
failureClass:
|
|
721
|
-
child.failureKind === "timed-out" || child.transportFailure
|
|
722
|
-
? "transport"
|
|
723
|
-
: generatedArtifact
|
|
724
|
-
? "artifact"
|
|
725
|
-
: "transport",
|
|
1387
|
+
failureClass: failureClassOf(child.failure),
|
|
726
1388
|
};
|
|
727
1389
|
}
|
|
728
1390
|
if (envelope.output === "evidence") {
|
|
@@ -740,31 +1402,57 @@ export class Runtime {
|
|
|
740
1402
|
let prepared: PreparedPreflight | null;
|
|
741
1403
|
try {
|
|
742
1404
|
prepared = preparePreflight(ctx.cwd, envelope, requestSnapshot);
|
|
743
|
-
} catch {
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!isFileSystemFailure(error)) throw error;
|
|
744
1407
|
return {
|
|
745
1408
|
ok: false,
|
|
746
1409
|
error: "candidate preflight inputs are unavailable",
|
|
1410
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
747
1411
|
failureKind: "failed",
|
|
748
1412
|
failureClass: "environment",
|
|
749
1413
|
launchConsumed: true,
|
|
1414
|
+
usage: child.usage,
|
|
750
1415
|
};
|
|
751
1416
|
}
|
|
752
1417
|
if (envelope.verification && !prepared) {
|
|
753
1418
|
return {
|
|
754
1419
|
ok: false,
|
|
755
1420
|
error: "candidate preflight inputs are unavailable",
|
|
1421
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
756
1422
|
failureKind: "failed",
|
|
757
1423
|
failureClass: "environment",
|
|
758
1424
|
launchConsumed: true,
|
|
1425
|
+
usage: child.usage,
|
|
759
1426
|
};
|
|
760
1427
|
}
|
|
1428
|
+
const task = this.taskRecords.get(envelope);
|
|
1429
|
+
const retainedRoot = task?.workspaceRoot ?? ctx.cwd;
|
|
1430
|
+
const retainedSnapshot =
|
|
1431
|
+
prepared?.snapshot ??
|
|
1432
|
+
(isSafeBound(envelope.snapshot)
|
|
1433
|
+
? mergeBounds(
|
|
1434
|
+
snapshotFiles(retainedRoot, envelope.declared.write),
|
|
1435
|
+
envelope.snapshot,
|
|
1436
|
+
)
|
|
1437
|
+
: undefined);
|
|
761
1438
|
const resultId = this.results.retain({
|
|
762
1439
|
diff: diff.diff,
|
|
763
1440
|
writeSet: envelope.declared.write,
|
|
764
|
-
|
|
1441
|
+
approvedDependencies: envelope.approvedDependencies ?? [],
|
|
1442
|
+
root: retainedRoot,
|
|
1443
|
+
...(task
|
|
1444
|
+
? {
|
|
1445
|
+
stage: "abel-implement" as const,
|
|
1446
|
+
canonicalRoot: task.workspaceRoot,
|
|
1447
|
+
changeId: task.boundary.changeId,
|
|
1448
|
+
taskId: task.boundary.taskId,
|
|
1449
|
+
originRequestId: envelope.id,
|
|
1450
|
+
phase: taskPhase(task),
|
|
1451
|
+
launchIndex: taskLaunchIndex(task),
|
|
1452
|
+
}
|
|
1453
|
+
: {}),
|
|
765
1454
|
...(prepared
|
|
766
1455
|
? {
|
|
767
|
-
snapshot: prepared.snapshot,
|
|
768
1456
|
baseline: prepared.baseline,
|
|
769
1457
|
verification: envelope.verification,
|
|
770
1458
|
packageManifest: prepared.packageManifest,
|
|
@@ -772,13 +1460,8 @@ export class Runtime {
|
|
|
772
1460
|
dependencyTarget: prepared.dependencyTarget,
|
|
773
1461
|
}
|
|
774
1462
|
: {}),
|
|
1463
|
+
...(retainedSnapshot ? { snapshot: retainedSnapshot } : {}),
|
|
775
1464
|
});
|
|
776
|
-
if (!prepared && isSafeBound(envelope.snapshot)) {
|
|
777
|
-
const retained = this.results.get(resultId);
|
|
778
|
-
if (retained) {
|
|
779
|
-
retained.snapshot = mergeBounds(retained.snapshot, envelope.snapshot);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
1465
|
return {
|
|
783
1466
|
ok: true,
|
|
784
1467
|
action: "run",
|
|
@@ -794,37 +1477,136 @@ export class Runtime {
|
|
|
794
1477
|
signal?: AbortSignal,
|
|
795
1478
|
observer?: RuntimeActivityObserver,
|
|
796
1479
|
): Promise<DispatchResult> {
|
|
797
|
-
|
|
798
|
-
if (
|
|
1480
|
+
const implementRequest = targetsImplement(request);
|
|
1481
|
+
if (!ctx) {
|
|
1482
|
+
if (implementRequest) throw new Error("run requires extension context");
|
|
1483
|
+
return { ok: false, error: "run requires extension context" };
|
|
1484
|
+
}
|
|
799
1485
|
const validation = validateRequestEnvelope(request);
|
|
800
|
-
if (!validation.ok)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1486
|
+
if (!validation.ok) {
|
|
1487
|
+
if (implementRequest) {
|
|
1488
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
1489
|
+
}
|
|
1490
|
+
return { ok: false, error: validation.reason };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let envelope: RequestEnvelope;
|
|
1494
|
+
if (isImplementRunRequest(validation.value)) {
|
|
1495
|
+
const runRequest = validation.value;
|
|
1496
|
+
const attempt = runRequest.attempt;
|
|
1497
|
+
const workspaceRoot = canonicalWorkspaceRoot(ctx.cwd);
|
|
1498
|
+
if (
|
|
1499
|
+
runRequest.kind === "open-task" &&
|
|
1500
|
+
workspaceRoot &&
|
|
1501
|
+
this.registry.has(
|
|
1502
|
+
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
1503
|
+
)
|
|
1504
|
+
) {
|
|
1505
|
+
throw new Error("duplicate task open protocol error");
|
|
1506
|
+
}
|
|
1507
|
+
if (runRequest.kind === "open-task" && signal?.aborted) {
|
|
1508
|
+
return cancelledOutcome({
|
|
1509
|
+
taskId: attempt.taskId,
|
|
1510
|
+
requestId: attempt.requestId,
|
|
1511
|
+
phase: attempt.phase,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
if (!workspaceRoot) {
|
|
1515
|
+
throw new Error("implementation workspace is unavailable");
|
|
1516
|
+
}
|
|
1517
|
+
const key = taskRecordKey(
|
|
1518
|
+
workspaceRoot,
|
|
1519
|
+
attempt.changeId,
|
|
1520
|
+
attempt.taskId,
|
|
1521
|
+
);
|
|
1522
|
+
if (
|
|
1523
|
+
runRequest.kind === "open-task" &&
|
|
1524
|
+
this.registry
|
|
1525
|
+
.values()
|
|
1526
|
+
.some(
|
|
1527
|
+
(record) =>
|
|
1528
|
+
record.workspaceRoot === workspaceRoot &&
|
|
1529
|
+
record.state.kind !== "blocked" &&
|
|
1530
|
+
record.state.kind !== "completed" &&
|
|
1531
|
+
declarationsConflict(
|
|
1532
|
+
taskConflictOf(runRequest.boundary),
|
|
1533
|
+
record.conflict,
|
|
1534
|
+
),
|
|
1535
|
+
)
|
|
1536
|
+
) {
|
|
1537
|
+
return {
|
|
1538
|
+
kind: "deferred",
|
|
1539
|
+
taskId: attempt.taskId,
|
|
1540
|
+
requestId: attempt.requestId,
|
|
1541
|
+
reason: "task-conflict",
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
if (!ctx.model) {
|
|
1545
|
+
throw new Error("implementation model is unavailable");
|
|
1546
|
+
}
|
|
1547
|
+
const identity = workerIdentity(ctx.model);
|
|
1548
|
+
let record: TaskRecord;
|
|
1549
|
+
if (runRequest.kind === "open-task") {
|
|
1550
|
+
record = this.registry.open(
|
|
1551
|
+
runRequest.boundary,
|
|
1552
|
+
identity,
|
|
1553
|
+
workspaceRoot,
|
|
1554
|
+
attempt,
|
|
1555
|
+
);
|
|
1556
|
+
} else {
|
|
1557
|
+
const existing = this.registry.get(key);
|
|
1558
|
+
if (!existing) {
|
|
1559
|
+
throw new Error("task identity mismatch or task is not open");
|
|
1560
|
+
}
|
|
1561
|
+
if (existing.workerIdentity !== identity) {
|
|
1562
|
+
throw new Error("provider/model identity mismatch");
|
|
1563
|
+
}
|
|
1564
|
+
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
1565
|
+
existing.boundary,
|
|
1566
|
+
attempt,
|
|
1567
|
+
);
|
|
1568
|
+
if (attemptReason !== null) throw new Error(attemptReason);
|
|
1569
|
+
assertAttemptAllowed(existing, attempt);
|
|
1570
|
+
record = existing;
|
|
1571
|
+
}
|
|
1572
|
+
if (record.state.kind === "blocked") {
|
|
1573
|
+
return blockedOutcome(
|
|
1574
|
+
{
|
|
1575
|
+
taskId: record.boundary.taskId,
|
|
1576
|
+
requestId: attempt.requestId,
|
|
1577
|
+
phase: record.state.phase,
|
|
1578
|
+
},
|
|
1579
|
+
record.state.failure,
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (record.state.kind === "completed") {
|
|
1583
|
+
return {
|
|
1584
|
+
kind: "completed",
|
|
1585
|
+
taskId: record.boundary.taskId,
|
|
1586
|
+
requestId: attempt.requestId,
|
|
1587
|
+
finalPhase: record.state.finalPhase,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (signal?.aborted) {
|
|
1591
|
+
return cancelledOutcome({
|
|
1592
|
+
taskId: record.boundary.taskId,
|
|
1593
|
+
requestId: attempt.requestId,
|
|
1594
|
+
phase: taskPhase(record),
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
envelope = deriveImplementEnvelope(record, attempt);
|
|
1598
|
+
this.taskRecords.set(envelope, record);
|
|
1599
|
+
} else {
|
|
1600
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
1601
|
+
envelope = cloneLegacyEnvelope(validation.value);
|
|
1602
|
+
}
|
|
822
1603
|
const context: StoredRunContext = {
|
|
823
1604
|
ctx,
|
|
824
1605
|
observer,
|
|
825
1606
|
sequence: ++this.batchSeq,
|
|
826
1607
|
runningEmitted: false,
|
|
827
1608
|
terminalEmitted: false,
|
|
1609
|
+
usage: new UsageAggregator(),
|
|
828
1610
|
};
|
|
829
1611
|
this.runContexts.set(envelope, context);
|
|
830
1612
|
this.notify(envelope, context, "queued");
|
|
@@ -842,28 +1624,51 @@ export class Runtime {
|
|
|
842
1624
|
try {
|
|
843
1625
|
const outcome = await batch.result(envelope.id);
|
|
844
1626
|
if (outcome.status === "succeeded" && outcome.value) {
|
|
845
|
-
const internalResult = outcome.value.dispatch
|
|
846
|
-
failureKind?: RunFailureKind;
|
|
847
|
-
failureClass?: FailureClass;
|
|
848
|
-
};
|
|
1627
|
+
const internalResult = outcome.value.dispatch;
|
|
849
1628
|
const {
|
|
850
1629
|
failureKind,
|
|
851
1630
|
failureClass: _failureClass,
|
|
1631
|
+
launchConsumed: _launchConsumed,
|
|
852
1632
|
...publicResult
|
|
853
1633
|
} = internalResult;
|
|
854
|
-
|
|
1634
|
+
const dispatch = publicResult as DispatchResult;
|
|
1635
|
+
if (isWrappedDispatchResult(dispatch)) {
|
|
1636
|
+
if (!dispatch.ok) {
|
|
1637
|
+
const state: RuntimeActivityState = failureKind ?? "failed";
|
|
1638
|
+
this.notify(envelope, context, state);
|
|
1639
|
+
return dispatch;
|
|
1640
|
+
}
|
|
855
1641
|
this.notify(envelope, context, "completed");
|
|
856
|
-
return
|
|
1642
|
+
return dispatch;
|
|
857
1643
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1644
|
+
this.notify(
|
|
1645
|
+
envelope,
|
|
1646
|
+
context,
|
|
1647
|
+
dispatch.kind === "cancelled" ? "cancelled" : "completed",
|
|
1648
|
+
);
|
|
1649
|
+
return dispatch;
|
|
861
1650
|
}
|
|
862
1651
|
const state: RuntimeActivityState =
|
|
863
1652
|
outcome.status === "cancelled" ? "cancelled" : "failed";
|
|
1653
|
+
const task = this.taskRecords.get(envelope);
|
|
1654
|
+
if (task) {
|
|
1655
|
+
this.notify(envelope, context, state);
|
|
1656
|
+
if (outcome.status === "cancelled") {
|
|
1657
|
+
return {
|
|
1658
|
+
...cancelledOutcome({
|
|
1659
|
+
taskId: task.boundary.taskId,
|
|
1660
|
+
requestId: envelope.id,
|
|
1661
|
+
phase: taskPhase(task),
|
|
1662
|
+
}),
|
|
1663
|
+
...this.usageMetadata(context),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
throw new Error(outcome.error ?? `scheduled run ${outcome.status}`);
|
|
1667
|
+
}
|
|
864
1668
|
const terminal = {
|
|
865
1669
|
ok: false as const,
|
|
866
1670
|
error: outcome.error ?? `scheduled run ${outcome.status}`,
|
|
1671
|
+
...this.usageMetadata(context),
|
|
867
1672
|
};
|
|
868
1673
|
this.notify(envelope, context, state);
|
|
869
1674
|
return terminal;
|
|
@@ -916,192 +1721,96 @@ export class Runtime {
|
|
|
916
1721
|
|
|
917
1722
|
private async runScheduled(
|
|
918
1723
|
envelope: RequestEnvelope,
|
|
919
|
-
|
|
1724
|
+
context: StoredRunContext,
|
|
920
1725
|
signal: AbortSignal,
|
|
921
1726
|
): Promise<InternalDispatchResult> {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1727
|
+
const ctx = context.ctx;
|
|
1728
|
+
const task = this.taskRecords.get(envelope);
|
|
1729
|
+
if (!task && envelope.stage === "abel-implement") {
|
|
1730
|
+
throw new Error("implementation TaskRecord is unavailable");
|
|
1731
|
+
}
|
|
1732
|
+
if (signal.aborted) {
|
|
1733
|
+
if (task) {
|
|
1734
|
+
return cancelledOutcome({
|
|
1735
|
+
taskId: task.boundary.taskId,
|
|
1736
|
+
requestId: envelope.id,
|
|
1737
|
+
phase: taskPhase(task),
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
return { ...cancellationError(signal), failureKind: "cancelled" };
|
|
1741
|
+
}
|
|
928
1742
|
const agent = loadAgentDefinitions().find(
|
|
929
1743
|
(item) => item.role === envelope.role,
|
|
930
1744
|
);
|
|
931
|
-
if (!agent)
|
|
1745
|
+
if (!agent) {
|
|
1746
|
+
if (task) throw new Error("implementation agent definition is missing");
|
|
932
1747
|
return {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
),
|
|
1748
|
+
ok: false,
|
|
1749
|
+
error: "agent definition is unavailable",
|
|
1750
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
1751
|
+
failureKind: "failed",
|
|
1752
|
+
failureClass: "environment",
|
|
939
1753
|
};
|
|
940
|
-
if (!ctx.model)
|
|
941
|
-
return recoveryFailure(
|
|
942
|
-
envelope,
|
|
943
|
-
"environment-blocked",
|
|
944
|
-
"repair-environment",
|
|
945
|
-
0,
|
|
946
|
-
"failed",
|
|
947
|
-
"phase runtime is unavailable",
|
|
948
|
-
);
|
|
949
|
-
const identity = workerIdentity(ctx.model);
|
|
950
|
-
const contract = contractOf(envelope);
|
|
951
|
-
const existing = this.registry.get(taskId);
|
|
952
|
-
if (existing) {
|
|
953
|
-
if (existing.identity !== identity) {
|
|
954
|
-
return recoveryFailure(
|
|
955
|
-
envelope,
|
|
956
|
-
"environment-blocked",
|
|
957
|
-
"repair-environment",
|
|
958
|
-
existing.currentPhase.correctionIndex,
|
|
959
|
-
"failed",
|
|
960
|
-
"pinned provider/model identity differs",
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
if (!sameContract(existing.taskContract, contract)) {
|
|
964
|
-
return recoveryFailure(
|
|
965
|
-
envelope,
|
|
966
|
-
"design-required",
|
|
967
|
-
"return-to-design",
|
|
968
|
-
existing.currentPhase.correctionIndex,
|
|
969
|
-
"failed",
|
|
970
|
-
"immutable task contract changed",
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
if (
|
|
974
|
-
existing.state.kind === "blocked" &&
|
|
975
|
-
existing.state.reason === "artifact"
|
|
976
|
-
) {
|
|
977
|
-
return recoveryFailure(
|
|
978
|
-
envelope,
|
|
979
|
-
"implementation-artifact-delivery-blocked",
|
|
980
|
-
"finish-unaffected",
|
|
981
|
-
1,
|
|
982
|
-
"failed",
|
|
983
|
-
"artifact correction launch budget is exhausted",
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
if (
|
|
987
|
-
existing.state.kind === "blocked" &&
|
|
988
|
-
existing.state.reason === "mechanical"
|
|
989
|
-
) {
|
|
990
|
-
return recoveryFailure(
|
|
991
|
-
envelope,
|
|
992
|
-
"mechanical-redispatch-exhausted",
|
|
993
|
-
"finish-unaffected",
|
|
994
|
-
1,
|
|
995
|
-
"failed",
|
|
996
|
-
"phase launch budget is exhausted",
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
} else {
|
|
1000
|
-
this.registry.pin(contract, identity);
|
|
1001
|
-
}
|
|
1002
|
-
let pinned = this.registry.get(taskId);
|
|
1003
|
-
if (!pinned) {
|
|
1004
|
-
return recoveryFailure(
|
|
1005
|
-
envelope,
|
|
1006
|
-
"environment-blocked",
|
|
1007
|
-
"repair-environment",
|
|
1008
|
-
0,
|
|
1009
|
-
"failed",
|
|
1010
|
-
"logical Worker could not be pinned",
|
|
1011
|
-
);
|
|
1012
1754
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
pinned.state.kind === "artifact-correction-pending";
|
|
1016
|
-
const redispatchingStale = pinned.state.kind === "stale-redispatch-pending";
|
|
1017
|
-
let artifactRejection: string | undefined;
|
|
1018
|
-
const changedPhase =
|
|
1019
|
-
pinned.currentPhase.requestId !== envelope.id ||
|
|
1020
|
-
pinned.currentPhase.phase !== envelope.phase;
|
|
1021
|
-
if (pinned.state.kind === "candidate-pending") {
|
|
1755
|
+
if (!ctx.model) {
|
|
1756
|
+
if (task) throw new Error("implementation model is unavailable");
|
|
1022
1757
|
return {
|
|
1023
1758
|
ok: false,
|
|
1024
|
-
error: "
|
|
1759
|
+
error: "phase runtime is unavailable",
|
|
1760
|
+
failure: {
|
|
1761
|
+
kind: "environment",
|
|
1762
|
+
code: "sandbox-runtime-unavailable",
|
|
1763
|
+
},
|
|
1025
1764
|
failureKind: "failed",
|
|
1765
|
+
failureClass: "environment",
|
|
1026
1766
|
};
|
|
1027
1767
|
}
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
)
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
return {
|
|
1065
|
-
ok: false,
|
|
1066
|
-
error: "current phase candidate is not applied",
|
|
1067
|
-
failureKind: "failed",
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
const sameAppliedPhase = pinned.currentPhase.phase === envelope.phase;
|
|
1071
|
-
if (
|
|
1072
|
-
sameAppliedPhase &&
|
|
1073
|
-
!samePhaseContract(pinned.currentPhase, contract.currentPhase)
|
|
1074
|
-
) {
|
|
1075
|
-
return recoveryFailure(
|
|
1076
|
-
envelope,
|
|
1077
|
-
"design-required",
|
|
1078
|
-
"return-to-design",
|
|
1079
|
-
pinned.currentPhase.correctionIndex,
|
|
1080
|
-
"failed",
|
|
1081
|
-
"same-phase correction changed the current phase contract",
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
if (
|
|
1085
|
-
!sameAppliedPhase &&
|
|
1086
|
-
!isNextImplementationPhase(pinned.currentPhase.phase, envelope.phase)
|
|
1087
|
-
) {
|
|
1088
|
-
return recoveryFailure(
|
|
1089
|
-
envelope,
|
|
1090
|
-
"design-required",
|
|
1091
|
-
"return-to-design",
|
|
1092
|
-
pinned.currentPhase.correctionIndex,
|
|
1093
|
-
"failed",
|
|
1094
|
-
"invalid implementation phase transition",
|
|
1095
|
-
);
|
|
1768
|
+
if (!task) {
|
|
1769
|
+
const result = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
1770
|
+
this.captureUsage(context, "launch:0", result.usage);
|
|
1771
|
+
return this.withUsage(context, result);
|
|
1772
|
+
}
|
|
1773
|
+
if (task.workerIdentity !== workerIdentity(ctx.model)) {
|
|
1774
|
+
throw new Error("provider/model identity mismatch");
|
|
1775
|
+
}
|
|
1776
|
+
if (task.state.kind !== "ready") {
|
|
1777
|
+
throw new Error("implementation task is not launchable");
|
|
1778
|
+
}
|
|
1779
|
+
const phase = task.state.phase;
|
|
1780
|
+
const launchIndex = task.state.launchIndex;
|
|
1781
|
+
const correction = task.state.correction;
|
|
1782
|
+
const artifactRejection =
|
|
1783
|
+
correction?.kind === "artifact"
|
|
1784
|
+
? artifactCorrectionEvidence(correction)
|
|
1785
|
+
: undefined;
|
|
1786
|
+
const identity: OutcomeIdentity = {
|
|
1787
|
+
taskId: task.boundary.taskId,
|
|
1788
|
+
requestId: envelope.id,
|
|
1789
|
+
phase,
|
|
1790
|
+
};
|
|
1791
|
+
const block = (failure: TaskFailure): InternalDispatchResult => {
|
|
1792
|
+
task.state = {
|
|
1793
|
+
kind: "blocked",
|
|
1794
|
+
phase,
|
|
1795
|
+
failure: structuredClone(failure),
|
|
1796
|
+
};
|
|
1797
|
+
return blockedOutcome(identity, failure);
|
|
1798
|
+
};
|
|
1799
|
+
const candidate = (
|
|
1800
|
+
result: Extract<ChildDispatchResult, { ok: true }>,
|
|
1801
|
+
): InternalDispatchResult => {
|
|
1802
|
+
if (typeof result.resultId !== "string" || result.result === undefined) {
|
|
1803
|
+
throw new Error("implementation candidate result is incomplete");
|
|
1096
1804
|
}
|
|
1097
|
-
pinned = this.registry.setCurrentPhase(taskId, contract, 0) ?? pinned;
|
|
1098
|
-
} else if (pinned.state.kind === "phase-applied") {
|
|
1099
1805
|
return {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1806
|
+
kind: "candidate",
|
|
1807
|
+
...identity,
|
|
1808
|
+
resultId: result.resultId,
|
|
1809
|
+
result: result.result as DiffResult,
|
|
1103
1810
|
};
|
|
1104
|
-
}
|
|
1811
|
+
};
|
|
1812
|
+
const finish = (result: InternalDispatchResult) =>
|
|
1813
|
+
this.withUsage(context, result);
|
|
1105
1814
|
|
|
1106
1815
|
const first = await this.dispatchChild(
|
|
1107
1816
|
agent,
|
|
@@ -1110,149 +1819,161 @@ export class Runtime {
|
|
|
1110
1819
|
signal,
|
|
1111
1820
|
artifactRejection,
|
|
1112
1821
|
);
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1822
|
+
this.captureUsage(context, "launch:0", first.usage);
|
|
1823
|
+
if (signal.aborted) {
|
|
1824
|
+
if (first.ok && typeof first.resultId === "string") {
|
|
1825
|
+
this.results.discard(first.resultId);
|
|
1826
|
+
}
|
|
1827
|
+
return finish(cancelledOutcome(identity));
|
|
1828
|
+
}
|
|
1118
1829
|
if (first.ok) {
|
|
1119
|
-
|
|
1120
|
-
return first;
|
|
1830
|
+
return finish(candidate(first));
|
|
1121
1831
|
}
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
if (first.launchConsumed) this.consumeWorkerLaunch(pinned);
|
|
1126
|
-
return recoveryFailure(
|
|
1127
|
-
envelope,
|
|
1128
|
-
"environment-blocked",
|
|
1129
|
-
"repair-environment",
|
|
1130
|
-
launchIndex,
|
|
1131
|
-
first.failureKind,
|
|
1132
|
-
);
|
|
1832
|
+
const firstFailure = childFailureOf(first);
|
|
1833
|
+
if (firstFailure.kind === "cancelled") {
|
|
1834
|
+
return finish(cancelledOutcome(identity));
|
|
1133
1835
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
envelope,
|
|
1143
|
-
artifactBlocked
|
|
1144
|
-
? "implementation-artifact-delivery-blocked"
|
|
1145
|
-
: "mechanical-redispatch-exhausted",
|
|
1146
|
-
"finish-unaffected",
|
|
1147
|
-
1,
|
|
1148
|
-
first.failureKind,
|
|
1149
|
-
"phase launch budget is exhausted",
|
|
1836
|
+
const firstTerminal = terminalTaskFailure(firstFailure);
|
|
1837
|
+
if (firstTerminal) return finish(block(firstTerminal));
|
|
1838
|
+
if (launchIndex === 1) {
|
|
1839
|
+
return finish(
|
|
1840
|
+
block({
|
|
1841
|
+
kind: "attempts-exhausted",
|
|
1842
|
+
cause: attemptFailureCause(firstFailure),
|
|
1843
|
+
}),
|
|
1150
1844
|
);
|
|
1151
1845
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1846
|
+
if (firstFailure.kind === "artifact" || firstFailure.kind === "stale") {
|
|
1847
|
+
task.state = {
|
|
1848
|
+
kind: "ready",
|
|
1849
|
+
phase,
|
|
1850
|
+
launchIndex: 1,
|
|
1851
|
+
correction: structuredClone(firstFailure),
|
|
1157
1852
|
};
|
|
1158
|
-
return
|
|
1159
|
-
envelope,
|
|
1160
|
-
"implementation-artifact-delivery-blocked",
|
|
1161
|
-
"correct-artifact",
|
|
1162
|
-
0,
|
|
1163
|
-
first.failureKind,
|
|
1164
|
-
"generated implementation artifact requires bounded correction",
|
|
1165
|
-
);
|
|
1853
|
+
return finish(retryOutcome(identity, "worker", firstFailure.kind));
|
|
1166
1854
|
}
|
|
1167
1855
|
|
|
1168
|
-
|
|
1856
|
+
task.state = {
|
|
1857
|
+
kind: "ready",
|
|
1858
|
+
phase,
|
|
1859
|
+
launchIndex: 1,
|
|
1860
|
+
};
|
|
1169
1861
|
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1862
|
+
this.captureUsage(context, "launch:1", second.usage);
|
|
1863
|
+
if (signal.aborted) {
|
|
1864
|
+
if (second.ok && typeof second.resultId === "string") {
|
|
1865
|
+
this.results.discard(second.resultId);
|
|
1866
|
+
}
|
|
1867
|
+
return finish(cancelledOutcome(identity));
|
|
1868
|
+
}
|
|
1175
1869
|
if (second.ok) {
|
|
1176
|
-
|
|
1177
|
-
return second;
|
|
1870
|
+
return finish(candidate(second));
|
|
1178
1871
|
}
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1182
|
-
return recoveryFailure(
|
|
1183
|
-
envelope,
|
|
1184
|
-
"environment-blocked",
|
|
1185
|
-
"repair-environment",
|
|
1186
|
-
1,
|
|
1187
|
-
second.failureKind,
|
|
1188
|
-
);
|
|
1872
|
+
const secondFailure = childFailureOf(second);
|
|
1873
|
+
if (secondFailure.kind === "cancelled") {
|
|
1874
|
+
return finish(cancelledOutcome(identity));
|
|
1189
1875
|
}
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
artifactBlocked
|
|
1198
|
-
? "implementation-artifact-delivery-blocked"
|
|
1199
|
-
: "mechanical-redispatch-exhausted",
|
|
1200
|
-
"finish-unaffected",
|
|
1201
|
-
1,
|
|
1202
|
-
second.failureKind ?? first.failureKind,
|
|
1203
|
-
"phase launch budget is exhausted",
|
|
1876
|
+
const secondTerminal = terminalTaskFailure(secondFailure);
|
|
1877
|
+
if (secondTerminal) return finish(block(secondTerminal));
|
|
1878
|
+
return finish(
|
|
1879
|
+
block({
|
|
1880
|
+
kind: "attempts-exhausted",
|
|
1881
|
+
cause: attemptFailureCause(secondFailure),
|
|
1882
|
+
}),
|
|
1204
1883
|
);
|
|
1205
1884
|
}
|
|
1206
1885
|
|
|
1207
|
-
private
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1886
|
+
private captureUsage(
|
|
1887
|
+
context: StoredRunContext,
|
|
1888
|
+
id: string,
|
|
1889
|
+
usage: Usage | undefined,
|
|
1890
|
+
): void {
|
|
1891
|
+
if (usage) context.usage.add(id, usage);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private usageMetadata(context: StoredRunContext): { usage?: Usage } {
|
|
1895
|
+
return context.usage.hasUsage() ? { usage: context.usage.total() } : {};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
private withUsage<T extends InternalDispatchResult>(
|
|
1899
|
+
context: StoredRunContext,
|
|
1900
|
+
result: T,
|
|
1901
|
+
): T {
|
|
1902
|
+
return {
|
|
1903
|
+
...result,
|
|
1904
|
+
...this.usageMetadata(context),
|
|
1905
|
+
};
|
|
1214
1906
|
}
|
|
1215
1907
|
|
|
1216
1908
|
private rememberRetainedResult(
|
|
1217
|
-
result:
|
|
1909
|
+
result: Extract<ImplementOutcome, { kind: "candidate" }>,
|
|
1218
1910
|
envelope: RequestEnvelope,
|
|
1219
|
-
|
|
1911
|
+
task: TaskRecord,
|
|
1220
1912
|
): void {
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1913
|
+
if (task.state.kind !== "ready") {
|
|
1914
|
+
throw new Error("retained candidate identity mismatch");
|
|
1915
|
+
}
|
|
1916
|
+
const phaseState = task.state;
|
|
1917
|
+
if (
|
|
1918
|
+
result.requestId !== envelope.id ||
|
|
1919
|
+
result.taskId !== task.boundary.taskId ||
|
|
1920
|
+
result.phase !== phaseState.phase
|
|
1921
|
+
) {
|
|
1922
|
+
throw new Error("retained candidate identity mismatch");
|
|
1923
|
+
}
|
|
1924
|
+
const phase = task.boundary.phases[phaseState.phase];
|
|
1925
|
+
if (!phase || !isSafeBound(envelope.snapshot)) {
|
|
1926
|
+
throw new Error("retained candidate identity mismatch");
|
|
1927
|
+
}
|
|
1928
|
+
const identity: RetainedCandidateIdentity = {
|
|
1929
|
+
stage: "abel-implement",
|
|
1930
|
+
canonicalRoot: task.workspaceRoot,
|
|
1931
|
+
root: task.workspaceRoot,
|
|
1932
|
+
changeId: task.boundary.changeId,
|
|
1933
|
+
taskId: task.boundary.taskId,
|
|
1934
|
+
originRequestId: envelope.id,
|
|
1935
|
+
phase: phaseState.phase,
|
|
1936
|
+
launchIndex: phaseState.launchIndex,
|
|
1937
|
+
};
|
|
1938
|
+
if (!this.results.get(result.resultId)) {
|
|
1939
|
+
throw new Error("retained candidate is unavailable");
|
|
1940
|
+
}
|
|
1941
|
+
this.results.bindIdentity(result.resultId, identity);
|
|
1942
|
+
this.results.resolveIdentity(result.resultId, {
|
|
1943
|
+
...identity,
|
|
1944
|
+
writeSet: [...phase.write],
|
|
1945
|
+
approvedDependencies: [...task.boundary.approvedDependencies],
|
|
1946
|
+
snapshot: envelope.snapshot,
|
|
1227
1947
|
});
|
|
1228
|
-
|
|
1948
|
+
task.state = {
|
|
1949
|
+
kind: "candidate-pending",
|
|
1950
|
+
phase: phaseState.phase,
|
|
1951
|
+
launchIndex: phaseState.launchIndex,
|
|
1952
|
+
originRequestId: envelope.id,
|
|
1953
|
+
resultId: result.resultId,
|
|
1954
|
+
};
|
|
1229
1955
|
}
|
|
1230
1956
|
|
|
1231
1957
|
validateRequest(
|
|
1232
1958
|
envelope: unknown,
|
|
1233
|
-
): { ok: true; value:
|
|
1959
|
+
): { ok: true; value: RunRequest } | { ok: false; reason: string } {
|
|
1234
1960
|
return validateRequestEnvelope(envelope);
|
|
1235
1961
|
}
|
|
1236
1962
|
|
|
1237
1963
|
async drain(): Promise<void> {
|
|
1964
|
+
this.activation.drain();
|
|
1965
|
+
const scheduled = this.scheduler.cancelAll();
|
|
1966
|
+
const applied = this.applyTail;
|
|
1967
|
+
await Promise.all([scheduled, applied]);
|
|
1238
1968
|
this.parentPayloadBridge?.clear();
|
|
1239
|
-
const settled = this.scheduler.cancelAll();
|
|
1240
|
-
this.retainedRuns.clear();
|
|
1241
1969
|
drainStage({
|
|
1242
1970
|
results: this.results,
|
|
1243
1971
|
registry: this.registry,
|
|
1244
1972
|
activation: this.activation,
|
|
1245
1973
|
});
|
|
1246
|
-
await settled;
|
|
1247
1974
|
}
|
|
1248
1975
|
|
|
1249
1976
|
get state(): ActivationState {
|
|
1250
1977
|
return this.activation.state;
|
|
1251
1978
|
}
|
|
1252
1979
|
}
|
|
1253
|
-
|
|
1254
|
-
function isNextImplementationPhase(current: string, next: string): boolean {
|
|
1255
|
-
const phases = ["red", "green", "refactor"];
|
|
1256
|
-
const index = phases.indexOf(current);
|
|
1257
|
-
return index >= 0 && phases[index + 1] === next;
|
|
1258
|
-
}
|