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