@abelxiaoxing/cadence 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +393 -0
- package/src/run-store.ts +918 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
package/src/runtime.ts
DELETED
|
@@ -1,2436 +0,0 @@
|
|
|
1
|
-
import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Activation, type ActivationState } from "./activation.ts";
|
|
6
|
-
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
7
|
-
import type { BaselineEntry } from "./candidate-preflight.ts";
|
|
8
|
-
import { runChildSession, UsageAggregator } from "./child-session.ts";
|
|
9
|
-
import {
|
|
10
|
-
ACTIONS,
|
|
11
|
-
type AgentsCheckpointAttempt,
|
|
12
|
-
type AgentsCheckpointRequest,
|
|
13
|
-
type CandidateFailure,
|
|
14
|
-
type ChildFailure,
|
|
15
|
-
cloneVerificationContract,
|
|
16
|
-
type DiffResult,
|
|
17
|
-
type ImplementApplyOperation,
|
|
18
|
-
type ImplementationPhase,
|
|
19
|
-
type ImplementDiscardOperation,
|
|
20
|
-
type ImplementGraphReadinessDiagnostic,
|
|
21
|
-
type ImplementOutcome,
|
|
22
|
-
type ImplementRunRequest,
|
|
23
|
-
LIMITS,
|
|
24
|
-
type PhaseAttempt,
|
|
25
|
-
type RequestEnvelope,
|
|
26
|
-
type RunRequest,
|
|
27
|
-
type SafeFailureDiagnostic,
|
|
28
|
-
type TaskBoundary,
|
|
29
|
-
type TaskFailure,
|
|
30
|
-
validateAgentsCheckpointAttempt,
|
|
31
|
-
validateImplementApplyOperation,
|
|
32
|
-
validateImplementDiscardOperation,
|
|
33
|
-
validatePhaseAttemptAgainstBoundary,
|
|
34
|
-
validateRequestEnvelope,
|
|
35
|
-
verificationInputPaths,
|
|
36
|
-
} from "./contracts.ts";
|
|
37
|
-
import { drainStage } from "./drain.ts";
|
|
38
|
-
import {
|
|
39
|
-
type Bound,
|
|
40
|
-
type DirBound,
|
|
41
|
-
type FileBound,
|
|
42
|
-
isCurrent,
|
|
43
|
-
mergeBounds,
|
|
44
|
-
snapshotDirManifest,
|
|
45
|
-
snapshotFile,
|
|
46
|
-
snapshotFiles,
|
|
47
|
-
} from "./file-snapshot.ts";
|
|
48
|
-
import {
|
|
49
|
-
assessImplementGraphReadiness,
|
|
50
|
-
hashImplementGraphBoundary,
|
|
51
|
-
type ImplementExecutionFacts,
|
|
52
|
-
} from "./implement-graph.ts";
|
|
53
|
-
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
54
|
-
import { customPhaseRuntime, runtimeFromContext } from "./parent-provider.ts";
|
|
55
|
-
import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
|
|
56
|
-
import {
|
|
57
|
-
type BoundRetainedResult,
|
|
58
|
-
ResultStore,
|
|
59
|
-
type RetainedCandidateFacts,
|
|
60
|
-
type RetainedCandidateIdentity,
|
|
61
|
-
} from "./result-store.ts";
|
|
62
|
-
import { declarationsConflict, Scheduler } from "./scheduler.ts";
|
|
63
|
-
import {
|
|
64
|
-
describeInvalidSubagentEndpoint,
|
|
65
|
-
resolveSubagentEndpoint,
|
|
66
|
-
type SubagentEndpoint,
|
|
67
|
-
} from "./subagent-endpoint.ts";
|
|
68
|
-
import {
|
|
69
|
-
type ImplementGraphRecord,
|
|
70
|
-
type TaskRecord,
|
|
71
|
-
taskConflictOf,
|
|
72
|
-
taskRecordKey,
|
|
73
|
-
WorkerRegistry,
|
|
74
|
-
workerIdentity,
|
|
75
|
-
} from "./worker.ts";
|
|
76
|
-
|
|
77
|
-
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
78
|
-
|
|
79
|
-
function isFileSystemFailure(error: unknown): boolean {
|
|
80
|
-
return (
|
|
81
|
-
error instanceof Error &&
|
|
82
|
-
"code" in error &&
|
|
83
|
-
typeof (error as { code?: unknown }).code === "string"
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function canonicalWorkspaceRoot(root: string): string | null {
|
|
88
|
-
try {
|
|
89
|
-
return realpathSync(root);
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isImplementRunRequest(
|
|
96
|
-
request: RunRequest,
|
|
97
|
-
): request is ImplementRunRequest {
|
|
98
|
-
return request.stage === "abel-implement" && "kind" in request;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function targetsImplement(request: unknown): boolean {
|
|
102
|
-
return (
|
|
103
|
-
typeof request === "object" &&
|
|
104
|
-
request !== null &&
|
|
105
|
-
!Array.isArray(request) &&
|
|
106
|
-
(request as Record<string, unknown>).stage === "abel-implement"
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function cloneNonImplementEnvelope(request: RequestEnvelope): RequestEnvelope {
|
|
111
|
-
return {
|
|
112
|
-
...request,
|
|
113
|
-
taskId: request.taskId ?? request.id,
|
|
114
|
-
roots: [...request.roots],
|
|
115
|
-
context: { ...request.context },
|
|
116
|
-
declared: {
|
|
117
|
-
...request.declared,
|
|
118
|
-
read: [...request.declared.read],
|
|
119
|
-
write: [...request.declared.write],
|
|
120
|
-
conflicts: [...request.declared.conflicts],
|
|
121
|
-
resources: [...request.declared.resources],
|
|
122
|
-
},
|
|
123
|
-
...(request.approvedDependencies === undefined
|
|
124
|
-
? {}
|
|
125
|
-
: { approvedDependencies: [...request.approvedDependencies] }),
|
|
126
|
-
...(request.impactClosure === undefined
|
|
127
|
-
? {}
|
|
128
|
-
: { impactClosure: structuredClone(request.impactClosure) }),
|
|
129
|
-
...(request.verification === undefined
|
|
130
|
-
? {}
|
|
131
|
-
: {
|
|
132
|
-
verification: cloneVerificationContract(request.verification),
|
|
133
|
-
}),
|
|
134
|
-
...(request.snapshot === undefined
|
|
135
|
-
? {}
|
|
136
|
-
: { snapshot: structuredClone(request.snapshot) }),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function deriveImplementEnvelope(
|
|
141
|
-
record: TaskRecord,
|
|
142
|
-
attempt: PhaseAttempt,
|
|
143
|
-
): RequestEnvelope {
|
|
144
|
-
const boundary = record.boundary;
|
|
145
|
-
const phase = boundary.phases[attempt.phase];
|
|
146
|
-
if (!phase) throw new Error("task attempt phase is not declared");
|
|
147
|
-
return {
|
|
148
|
-
stage: "abel-implement",
|
|
149
|
-
role: "implementation-worker",
|
|
150
|
-
taskId: boundary.taskId,
|
|
151
|
-
id: attempt.requestId,
|
|
152
|
-
phase: attempt.phase,
|
|
153
|
-
objective: boundary.objective,
|
|
154
|
-
roots: [...boundary.roots],
|
|
155
|
-
context: { ...boundary.context },
|
|
156
|
-
declared: {
|
|
157
|
-
read: [...phase.read],
|
|
158
|
-
write: [...phase.write],
|
|
159
|
-
conflicts: [...boundary.scheduling.conflicts],
|
|
160
|
-
resources: [...boundary.scheduling.resources],
|
|
161
|
-
...(phase.verificationLock === undefined
|
|
162
|
-
? {}
|
|
163
|
-
: { verificationLock: phase.verificationLock }),
|
|
164
|
-
},
|
|
165
|
-
output: "diff",
|
|
166
|
-
agentsImpact: boundary.agents.impact,
|
|
167
|
-
...(boundary.agents.target === undefined
|
|
168
|
-
? {}
|
|
169
|
-
: { agentsTarget: boundary.agents.target }),
|
|
170
|
-
agentsManagedOnly: true,
|
|
171
|
-
approvedDependencies: [...boundary.approvedDependencies],
|
|
172
|
-
impactClosure: structuredClone(boundary.impactClosure),
|
|
173
|
-
verification: cloneVerificationContract(phase.verification),
|
|
174
|
-
snapshot: structuredClone(attempt.snapshot),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function nextDeclaredPhase(
|
|
179
|
-
boundary: TaskBoundary,
|
|
180
|
-
phase: PhaseAttempt["phase"],
|
|
181
|
-
): "green" | "refactor" | null {
|
|
182
|
-
if (phase === "red") return "green";
|
|
183
|
-
if (phase === "green" && boundary.phases.refactor) return "refactor";
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function taskBoundaryOf(
|
|
188
|
-
graph: ImplementGraphRecord,
|
|
189
|
-
taskId: string,
|
|
190
|
-
): TaskBoundary | null {
|
|
191
|
-
const task = graph.boundary.tasks.find(
|
|
192
|
-
(candidate) => candidate.taskId === taskId,
|
|
193
|
-
);
|
|
194
|
-
return task
|
|
195
|
-
? ({
|
|
196
|
-
changeId: graph.boundary.changeId,
|
|
197
|
-
...structuredClone(task),
|
|
198
|
-
} satisfies TaskBoundary)
|
|
199
|
-
: null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function appliedPhasesOf(record: TaskRecord): ImplementationPhase[] {
|
|
203
|
-
const phases: ImplementationPhase[] = ["red", "green"];
|
|
204
|
-
if (record.boundary.phases.refactor) phases.push("refactor");
|
|
205
|
-
if (
|
|
206
|
-
record.state.kind === "completed" ||
|
|
207
|
-
record.state.kind === "agents-checkpoint-pending"
|
|
208
|
-
) {
|
|
209
|
-
return phases;
|
|
210
|
-
}
|
|
211
|
-
const current =
|
|
212
|
-
record.state.kind === "ready" ||
|
|
213
|
-
record.state.kind === "candidate-pending" ||
|
|
214
|
-
record.state.kind === "blocked"
|
|
215
|
-
? record.state.phase
|
|
216
|
-
: "red";
|
|
217
|
-
return phases.slice(0, phases.indexOf(current));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function assertAttemptAllowed(record: TaskRecord, attempt: PhaseAttempt): void {
|
|
221
|
-
switch (record.state.kind) {
|
|
222
|
-
case "candidate-pending":
|
|
223
|
-
throw new Error("current phase candidate is awaiting parent apply");
|
|
224
|
-
case "agents-checkpoint-pending":
|
|
225
|
-
throw new Error("AGENTS checkpoint is pending");
|
|
226
|
-
case "blocked":
|
|
227
|
-
if (attempt.phase !== record.state.phase) {
|
|
228
|
-
throw new Error("terminal task phase mismatch");
|
|
229
|
-
}
|
|
230
|
-
return;
|
|
231
|
-
case "completed":
|
|
232
|
-
if (attempt.phase !== record.state.finalPhase) {
|
|
233
|
-
throw new Error("terminal task phase mismatch");
|
|
234
|
-
}
|
|
235
|
-
return;
|
|
236
|
-
case "ready":
|
|
237
|
-
if (attempt.phase !== record.state.phase) {
|
|
238
|
-
throw new Error("invalid implementation phase transition");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function taskPhase(record: TaskRecord): PhaseAttempt["phase"] {
|
|
244
|
-
return record.state.kind === "agents-checkpoint-pending" ||
|
|
245
|
-
record.state.kind === "completed"
|
|
246
|
-
? record.state.finalPhase
|
|
247
|
-
: record.state.phase;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function taskLaunchIndex(record: TaskRecord): 0 | 1 {
|
|
251
|
-
return record.state.kind === "ready" ||
|
|
252
|
-
record.state.kind === "candidate-pending"
|
|
253
|
-
? record.state.launchIndex
|
|
254
|
-
: record.state.kind === "blocked" &&
|
|
255
|
-
record.state.failure.kind === "attempts-exhausted"
|
|
256
|
-
? 1
|
|
257
|
-
: 0;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
interface PreparedPreflight {
|
|
261
|
-
snapshot: Bound;
|
|
262
|
-
baseline: BaselineEntry[];
|
|
263
|
-
packageManifest: FileBound;
|
|
264
|
-
lockfile: FileBound;
|
|
265
|
-
dependencyTarget: FileBound | DirBound;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function addBaselineFile(
|
|
269
|
-
root: string,
|
|
270
|
-
relative: string,
|
|
271
|
-
executable: boolean,
|
|
272
|
-
entries: Map<string, BaselineEntry>,
|
|
273
|
-
observed: Bound,
|
|
274
|
-
): boolean {
|
|
275
|
-
const bound = snapshotFile(root, relative);
|
|
276
|
-
if (!bound) return false;
|
|
277
|
-
observed[relative] = bound;
|
|
278
|
-
entries.set(relative, {
|
|
279
|
-
path: relative,
|
|
280
|
-
kind: "file",
|
|
281
|
-
sha256: bound.sha256,
|
|
282
|
-
bytes: bound.bytes,
|
|
283
|
-
executable,
|
|
284
|
-
});
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function addDirectoryFiles(
|
|
289
|
-
root: string,
|
|
290
|
-
relative: string,
|
|
291
|
-
entries: Map<string, BaselineEntry>,
|
|
292
|
-
observed: Bound,
|
|
293
|
-
): boolean {
|
|
294
|
-
const directory = path.resolve(root, relative);
|
|
295
|
-
for (const name of readdirSync(directory).sort()) {
|
|
296
|
-
const child = path.posix.join(relative, name);
|
|
297
|
-
const stat = lstatSync(path.resolve(root, child));
|
|
298
|
-
if (stat.isSymbolicLink()) return false;
|
|
299
|
-
if (stat.isDirectory()) {
|
|
300
|
-
if (!addDirectoryFiles(root, child, entries, observed)) return false;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (!stat.isFile()) return false;
|
|
304
|
-
if (
|
|
305
|
-
!addBaselineFile(
|
|
306
|
-
root,
|
|
307
|
-
child,
|
|
308
|
-
Boolean(stat.mode & 0o111),
|
|
309
|
-
entries,
|
|
310
|
-
observed,
|
|
311
|
-
)
|
|
312
|
-
)
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function preparePreflight(
|
|
319
|
-
root: string,
|
|
320
|
-
envelope: RequestEnvelope,
|
|
321
|
-
requestSnapshot: Bound,
|
|
322
|
-
): PreparedPreflight | null {
|
|
323
|
-
if (!envelope.verification) return null;
|
|
324
|
-
const packageManifest = snapshotFile(root, "package.json");
|
|
325
|
-
const lockPath = [
|
|
326
|
-
"bun.lock",
|
|
327
|
-
"bun.lockb",
|
|
328
|
-
"package-lock.json",
|
|
329
|
-
"pnpm-lock.yaml",
|
|
330
|
-
"yarn.lock",
|
|
331
|
-
].find((candidate) => snapshotFile(root, candidate) !== null);
|
|
332
|
-
const lockfile = lockPath ? snapshotFile(root, lockPath) : null;
|
|
333
|
-
const dependencyTarget =
|
|
334
|
-
snapshotDirManifest(root, "node_modules") ??
|
|
335
|
-
snapshotFile(root, "node_modules");
|
|
336
|
-
if (!packageManifest || !lockPath || !lockfile || !dependencyTarget)
|
|
337
|
-
return null;
|
|
338
|
-
if (!isCurrent(root, requestSnapshot)) {
|
|
339
|
-
return {
|
|
340
|
-
snapshot: mergeBounds(
|
|
341
|
-
snapshotFiles(root, ["package.json", lockPath]),
|
|
342
|
-
{ node_modules: dependencyTarget },
|
|
343
|
-
requestSnapshot,
|
|
344
|
-
),
|
|
345
|
-
baseline: [],
|
|
346
|
-
packageManifest,
|
|
347
|
-
lockfile,
|
|
348
|
-
dependencyTarget,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const paths = new Set([
|
|
353
|
-
...Object.keys(requestSnapshot),
|
|
354
|
-
...envelope.declared.read,
|
|
355
|
-
...envelope.declared.write,
|
|
356
|
-
"package.json",
|
|
357
|
-
lockPath,
|
|
358
|
-
...verificationInputPaths(envelope.verification),
|
|
359
|
-
]);
|
|
360
|
-
const entries = new Map<string, BaselineEntry>();
|
|
361
|
-
const observed: Bound = {};
|
|
362
|
-
for (const relative of [...paths].sort()) {
|
|
363
|
-
const absolute = path.resolve(root, relative);
|
|
364
|
-
if (
|
|
365
|
-
absolute === root ||
|
|
366
|
-
!absolute.startsWith(`${path.resolve(root)}${path.sep}`)
|
|
367
|
-
) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
371
|
-
if (!stat) {
|
|
372
|
-
observed[relative] = { kind: "absent", absent: true };
|
|
373
|
-
entries.set(relative, { path: relative, kind: "deleted" });
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (stat.isDirectory()) {
|
|
377
|
-
if (relative === "node_modules") continue;
|
|
378
|
-
try {
|
|
379
|
-
if (!addDirectoryFiles(root, relative, entries, observed)) return null;
|
|
380
|
-
} catch {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
386
|
-
if (
|
|
387
|
-
!addBaselineFile(
|
|
388
|
-
root,
|
|
389
|
-
relative,
|
|
390
|
-
Boolean(stat.mode & 0o111),
|
|
391
|
-
entries,
|
|
392
|
-
observed,
|
|
393
|
-
)
|
|
394
|
-
)
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
const baseline = [...entries.values()].sort((left, right) =>
|
|
398
|
-
left.path.localeCompare(right.path),
|
|
399
|
-
);
|
|
400
|
-
const dependencyBound: Bound = {
|
|
401
|
-
node_modules: dependencyTarget,
|
|
402
|
-
};
|
|
403
|
-
return {
|
|
404
|
-
snapshot: mergeBounds(observed, dependencyBound, requestSnapshot),
|
|
405
|
-
baseline,
|
|
406
|
-
packageManifest,
|
|
407
|
-
lockfile,
|
|
408
|
-
dependencyTarget,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function cancellationError(
|
|
413
|
-
signal: AbortSignal,
|
|
414
|
-
): Extract<WrappedDispatchResult, { ok: false }> {
|
|
415
|
-
const reason = signal.reason;
|
|
416
|
-
return {
|
|
417
|
-
ok: false,
|
|
418
|
-
error:
|
|
419
|
-
reason instanceof Error
|
|
420
|
-
? reason.message
|
|
421
|
-
: typeof reason === "string"
|
|
422
|
-
? reason
|
|
423
|
-
: "child phase cancelled",
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
type FailureClass = "transport" | "artifact" | "environment";
|
|
428
|
-
|
|
429
|
-
type InternalResultMetadata = {
|
|
430
|
-
failureKind?: RunFailureKind;
|
|
431
|
-
failureClass?: FailureClass;
|
|
432
|
-
failure?: ChildFailure | TaskFailure;
|
|
433
|
-
launchConsumed?: true;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
type ChildDispatchResult = WrappedDispatchResult & InternalResultMetadata;
|
|
437
|
-
type InternalDispatchResult = (
|
|
438
|
-
| WrappedDispatchResult
|
|
439
|
-
| (ImplementOutcome & { usage?: Usage })
|
|
440
|
-
) &
|
|
441
|
-
InternalResultMetadata;
|
|
442
|
-
type InternalFailureResult = Extract<WrappedDispatchResult, { ok: false }> &
|
|
443
|
-
InternalResultMetadata;
|
|
444
|
-
|
|
445
|
-
interface OutcomeIdentity {
|
|
446
|
-
taskId: string;
|
|
447
|
-
requestId: string;
|
|
448
|
-
phase: ImplementationPhase;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function blockedOutcome(
|
|
452
|
-
identity: OutcomeIdentity,
|
|
453
|
-
failure: TaskFailure,
|
|
454
|
-
): Extract<ImplementOutcome, { kind: "blocked" }> {
|
|
455
|
-
return { kind: "blocked", ...identity, failure: structuredClone(failure) };
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function cancelledOutcome(
|
|
459
|
-
identity: OutcomeIdentity,
|
|
460
|
-
): Extract<ImplementOutcome, { kind: "cancelled" }> {
|
|
461
|
-
return { kind: "cancelled", ...identity };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function retryOutcome(
|
|
465
|
-
identity: OutcomeIdentity,
|
|
466
|
-
scope: "worker" | "checkpoint",
|
|
467
|
-
failure: Extract<ChildFailure, { kind: "artifact" | "stale" }>,
|
|
468
|
-
): ImplementOutcome {
|
|
469
|
-
return {
|
|
470
|
-
kind: "retry",
|
|
471
|
-
...identity,
|
|
472
|
-
scope,
|
|
473
|
-
cause: failure.kind,
|
|
474
|
-
remainingAttempts: 1,
|
|
475
|
-
lastFailure: safeFailureDiagnostic(failure),
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function failureClassOf(failure: ChildFailure): FailureClass | undefined {
|
|
480
|
-
switch (failure.kind) {
|
|
481
|
-
case "artifact":
|
|
482
|
-
return "artifact";
|
|
483
|
-
case "environment":
|
|
484
|
-
return "environment";
|
|
485
|
-
case "transport":
|
|
486
|
-
return "transport";
|
|
487
|
-
case "stale":
|
|
488
|
-
case "approval-boundary":
|
|
489
|
-
case "verification-adapter":
|
|
490
|
-
case "cancelled":
|
|
491
|
-
case "result-limit":
|
|
492
|
-
return undefined;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function artifactCorrectionEvidence(failure?: ChildFailure): string {
|
|
497
|
-
return failure?.kind === "artifact"
|
|
498
|
-
? `candidate:${failure.code}`
|
|
499
|
-
: "generated-artifact-rejection";
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
503
|
-
if (result.failure) {
|
|
504
|
-
switch (result.failure.kind) {
|
|
505
|
-
case "artifact":
|
|
506
|
-
case "stale":
|
|
507
|
-
case "environment":
|
|
508
|
-
case "approval-boundary":
|
|
509
|
-
case "verification-adapter":
|
|
510
|
-
case "cancelled":
|
|
511
|
-
case "result-limit":
|
|
512
|
-
case "transport":
|
|
513
|
-
return result.failure;
|
|
514
|
-
case "attempts-exhausted":
|
|
515
|
-
case "checkpoint-attempts-exhausted":
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (result.failureKind === "cancelled") {
|
|
520
|
-
return { kind: "cancelled", code: "cancelled" };
|
|
521
|
-
}
|
|
522
|
-
switch (result.failureClass) {
|
|
523
|
-
case "artifact":
|
|
524
|
-
return {
|
|
525
|
-
kind: "artifact",
|
|
526
|
-
code: "invalid-diff",
|
|
527
|
-
stage: "phase-runtime",
|
|
528
|
-
};
|
|
529
|
-
case "environment":
|
|
530
|
-
return { kind: "environment", code: "root-unavailable" };
|
|
531
|
-
default:
|
|
532
|
-
return {
|
|
533
|
-
kind: "transport",
|
|
534
|
-
code:
|
|
535
|
-
result.failureKind === "timed-out" ? "timeout" : "transport-failure",
|
|
536
|
-
stage:
|
|
537
|
-
result.failureKind === "timed-out"
|
|
538
|
-
? "child-timeout"
|
|
539
|
-
: "child-provider-stream",
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function terminalTaskFailure(failure: ChildFailure): TaskFailure | undefined {
|
|
545
|
-
switch (failure.kind) {
|
|
546
|
-
case "approval-boundary":
|
|
547
|
-
case "environment":
|
|
548
|
-
case "verification-adapter":
|
|
549
|
-
case "result-limit":
|
|
550
|
-
return failure;
|
|
551
|
-
case "artifact":
|
|
552
|
-
case "stale":
|
|
553
|
-
case "cancelled":
|
|
554
|
-
case "transport":
|
|
555
|
-
return undefined;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function attemptFailureCause(
|
|
560
|
-
failure: ChildFailure,
|
|
561
|
-
): "artifact" | "stale" | "transport" {
|
|
562
|
-
switch (failure.kind) {
|
|
563
|
-
case "artifact":
|
|
564
|
-
case "stale":
|
|
565
|
-
case "transport":
|
|
566
|
-
return failure.kind;
|
|
567
|
-
case "approval-boundary":
|
|
568
|
-
case "environment":
|
|
569
|
-
case "verification-adapter":
|
|
570
|
-
case "result-limit":
|
|
571
|
-
case "cancelled":
|
|
572
|
-
throw new Error("non-retryable failure reached the launch budget");
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function attemptsExhaustedFailure(failure: ChildFailure): TaskFailure {
|
|
577
|
-
const cause = attemptFailureCause(failure);
|
|
578
|
-
return {
|
|
579
|
-
kind: "attempts-exhausted",
|
|
580
|
-
cause,
|
|
581
|
-
attemptsUsed: 2,
|
|
582
|
-
lastFailure: safeFailureDiagnostic(
|
|
583
|
-
failure as Extract<
|
|
584
|
-
ChildFailure,
|
|
585
|
-
{ kind: "artifact" | "stale" | "transport" }
|
|
586
|
-
>,
|
|
587
|
-
),
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function safeFailureDiagnostic(
|
|
592
|
-
failure: Extract<ChildFailure, { kind: "artifact" | "stale" | "transport" }>,
|
|
593
|
-
): SafeFailureDiagnostic {
|
|
594
|
-
return {
|
|
595
|
-
code: failure.code,
|
|
596
|
-
stage: failure.stage,
|
|
597
|
-
...("details" in failure && failure.details
|
|
598
|
-
? { details: structuredClone(failure.details) }
|
|
599
|
-
: {}),
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function assertNever(value: never): never {
|
|
604
|
-
throw new Error(`unhandled candidate failure: ${JSON.stringify(value)}`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
608
|
-
export type RuntimeFailureReason =
|
|
609
|
-
| "subagent failed"
|
|
610
|
-
| "subagent cancelled"
|
|
611
|
-
| "phase timed out";
|
|
612
|
-
export type RuntimeActivityState =
|
|
613
|
-
| "queued"
|
|
614
|
-
| "running"
|
|
615
|
-
| "completed"
|
|
616
|
-
| "failed"
|
|
617
|
-
| "cancelled"
|
|
618
|
-
| "timed-out";
|
|
619
|
-
|
|
620
|
-
export interface RuntimeActivityEvent {
|
|
621
|
-
state: RuntimeActivityState;
|
|
622
|
-
requestId: string;
|
|
623
|
-
role: string;
|
|
624
|
-
phase: string;
|
|
625
|
-
objective: string;
|
|
626
|
-
sequence: number;
|
|
627
|
-
failureReason?: RuntimeFailureReason;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export type RuntimeActivityObserver = (
|
|
631
|
-
event: RuntimeActivityEvent,
|
|
632
|
-
) => void | Promise<void>;
|
|
633
|
-
|
|
634
|
-
interface ScheduledRunResult {
|
|
635
|
-
dispatch: InternalDispatchResult;
|
|
636
|
-
failureKind?: RunFailureKind;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function isSafeBound(snapshot: unknown): snapshot is Bound {
|
|
640
|
-
if (
|
|
641
|
-
typeof snapshot !== "object" ||
|
|
642
|
-
snapshot === null ||
|
|
643
|
-
Array.isArray(snapshot)
|
|
644
|
-
)
|
|
645
|
-
return false;
|
|
646
|
-
for (const [path, value] of Object.entries(
|
|
647
|
-
snapshot as Record<string, unknown>,
|
|
648
|
-
)) {
|
|
649
|
-
if (
|
|
650
|
-
!path ||
|
|
651
|
-
path.startsWith("/") ||
|
|
652
|
-
path === "." ||
|
|
653
|
-
path === ".." ||
|
|
654
|
-
path.includes("/../") ||
|
|
655
|
-
path.endsWith("/..") ||
|
|
656
|
-
path.includes("\\")
|
|
657
|
-
)
|
|
658
|
-
return false;
|
|
659
|
-
if (typeof value !== "object" || value === null) return false;
|
|
660
|
-
const entry = value as Record<string, unknown>;
|
|
661
|
-
if (entry.kind === "file") {
|
|
662
|
-
if (
|
|
663
|
-
typeof entry.sha256 !== "string" ||
|
|
664
|
-
!SHA256_HEX.test(entry.sha256) ||
|
|
665
|
-
typeof entry.bytes !== "number" ||
|
|
666
|
-
!Number.isSafeInteger(entry.bytes) ||
|
|
667
|
-
entry.bytes < 0
|
|
668
|
-
)
|
|
669
|
-
return false;
|
|
670
|
-
} else if (entry.kind === "dir") {
|
|
671
|
-
if (
|
|
672
|
-
typeof entry.manifest !== "string" ||
|
|
673
|
-
!SHA256_HEX.test(entry.manifest)
|
|
674
|
-
)
|
|
675
|
-
return false;
|
|
676
|
-
} else if (entry.kind === "absent") {
|
|
677
|
-
if (entry.absent !== true) return false;
|
|
678
|
-
} else {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return true;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
export type WrappedDispatchResult =
|
|
686
|
-
| {
|
|
687
|
-
ok: true;
|
|
688
|
-
action: string;
|
|
689
|
-
result?: unknown;
|
|
690
|
-
resultId?: string;
|
|
691
|
-
usage?: Usage;
|
|
692
|
-
}
|
|
693
|
-
| {
|
|
694
|
-
ok: false;
|
|
695
|
-
notReady?: true;
|
|
696
|
-
error: string;
|
|
697
|
-
failure?: ChildFailure | TaskFailure;
|
|
698
|
-
usage?: Usage;
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
export type DispatchResult =
|
|
702
|
-
| WrappedDispatchResult
|
|
703
|
-
| (ImplementOutcome & { usage?: Usage });
|
|
704
|
-
|
|
705
|
-
export function isWrappedDispatchResult(
|
|
706
|
-
result: DispatchResult,
|
|
707
|
-
): result is WrappedDispatchResult {
|
|
708
|
-
return "ok" in result;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
export interface RuntimeOptions {
|
|
712
|
-
activation?: Activation;
|
|
713
|
-
parentPayloadBridge: ParentPayloadBridge;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
type RunContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
|
|
717
|
-
|
|
718
|
-
interface StoredRunContext {
|
|
719
|
-
ctx: RunContext;
|
|
720
|
-
observer?: RuntimeActivityObserver;
|
|
721
|
-
sequence: number;
|
|
722
|
-
runningEmitted: boolean;
|
|
723
|
-
terminalEmitted: boolean;
|
|
724
|
-
usage: UsageAggregator;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
export class Runtime {
|
|
728
|
-
readonly activation: Activation;
|
|
729
|
-
readonly limits = LIMITS;
|
|
730
|
-
readonly results = new ResultStore();
|
|
731
|
-
private applyTail: Promise<void> = Promise.resolve();
|
|
732
|
-
private applySeq = 0;
|
|
733
|
-
private batchSeq = 0;
|
|
734
|
-
private readonly parentPayloadBridge: ParentPayloadBridge;
|
|
735
|
-
private readonly registry = new WorkerRegistry();
|
|
736
|
-
private readonly taskRecords = new WeakMap<RequestEnvelope, TaskRecord>();
|
|
737
|
-
private readonly runContexts = new WeakMap<
|
|
738
|
-
RequestEnvelope,
|
|
739
|
-
StoredRunContext
|
|
740
|
-
>();
|
|
741
|
-
private readonly scheduler: Scheduler<ScheduledRunResult>;
|
|
742
|
-
|
|
743
|
-
constructor(opts: RuntimeOptions) {
|
|
744
|
-
this.activation = opts.activation ?? new Activation();
|
|
745
|
-
this.parentPayloadBridge = opts.parentPayloadBridge;
|
|
746
|
-
this.scheduler = new Scheduler({
|
|
747
|
-
limit: LIMITS.maxActiveChildSessions,
|
|
748
|
-
execute: async (request, signal) => {
|
|
749
|
-
const context = this.runContexts.get(request);
|
|
750
|
-
if (!context) throw new Error("scheduled run context is unavailable");
|
|
751
|
-
const task = this.taskRecords.get(request);
|
|
752
|
-
let candidateRollback:
|
|
753
|
-
| {
|
|
754
|
-
resultId: string;
|
|
755
|
-
requestId: string;
|
|
756
|
-
phase: ImplementationPhase;
|
|
757
|
-
launchIndex: 0 | 1;
|
|
758
|
-
priorState: TaskRecord["state"];
|
|
759
|
-
}
|
|
760
|
-
| undefined;
|
|
761
|
-
const rollback = () => {
|
|
762
|
-
const pending = candidateRollback;
|
|
763
|
-
if (!pending) return;
|
|
764
|
-
this.results.discard(pending.resultId);
|
|
765
|
-
if (
|
|
766
|
-
task?.state.kind === "candidate-pending" &&
|
|
767
|
-
task.state.resultId === pending.resultId &&
|
|
768
|
-
task.state.originRequestId === pending.requestId &&
|
|
769
|
-
task.state.phase === pending.phase &&
|
|
770
|
-
task.state.launchIndex === pending.launchIndex
|
|
771
|
-
) {
|
|
772
|
-
task.state = structuredClone(pending.priorState);
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
signal.addEventListener("abort", rollback, { once: true });
|
|
776
|
-
this.notify(request, context, "running");
|
|
777
|
-
const result = await this.runScheduled(request, context, signal);
|
|
778
|
-
if (!isWrappedDispatchResult(result) && result.kind === "candidate") {
|
|
779
|
-
if (!task) throw new Error("candidate TaskRecord is unavailable");
|
|
780
|
-
if (task.state.kind !== "ready") {
|
|
781
|
-
throw new Error("candidate TaskRecord is not ready");
|
|
782
|
-
}
|
|
783
|
-
candidateRollback = {
|
|
784
|
-
resultId: result.resultId,
|
|
785
|
-
requestId: result.requestId,
|
|
786
|
-
phase: result.phase,
|
|
787
|
-
launchIndex: task.state.launchIndex,
|
|
788
|
-
priorState: structuredClone(task.state),
|
|
789
|
-
};
|
|
790
|
-
if (signal.aborted) {
|
|
791
|
-
rollback();
|
|
792
|
-
} else {
|
|
793
|
-
try {
|
|
794
|
-
this.rememberRetainedResult(result, request, task);
|
|
795
|
-
} catch (error) {
|
|
796
|
-
this.results.discard(result.resultId);
|
|
797
|
-
throw error;
|
|
798
|
-
}
|
|
799
|
-
if (signal.aborted) rollback();
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
return {
|
|
803
|
-
dispatch: result,
|
|
804
|
-
...(result.failureKind === undefined
|
|
805
|
-
? {}
|
|
806
|
-
: { failureKind: result.failureKind }),
|
|
807
|
-
};
|
|
808
|
-
},
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
async execute(
|
|
813
|
-
action: string,
|
|
814
|
-
params: {
|
|
815
|
-
request?: unknown;
|
|
816
|
-
resultId?: string;
|
|
817
|
-
requestId?: string;
|
|
818
|
-
rejection?: unknown;
|
|
819
|
-
agentsCheckpoint?: unknown;
|
|
820
|
-
},
|
|
821
|
-
ctx?: RunContext,
|
|
822
|
-
signal?: AbortSignal,
|
|
823
|
-
observer?: RuntimeActivityObserver,
|
|
824
|
-
): Promise<DispatchResult> {
|
|
825
|
-
if (!(ACTIONS as readonly string[]).includes(action)) {
|
|
826
|
-
throw new Error(`unknown action: ${String(action)}`);
|
|
827
|
-
}
|
|
828
|
-
if (!this.activation.isActive()) {
|
|
829
|
-
return { ok: false, notReady: true, error: "dispatcher is not active" };
|
|
830
|
-
}
|
|
831
|
-
if (action === "run")
|
|
832
|
-
return this.run(params.request, ctx, signal, observer);
|
|
833
|
-
if (action === "apply") {
|
|
834
|
-
if (!ctx) throw new Error("apply requires context");
|
|
835
|
-
if (params.agentsCheckpoint !== undefined) {
|
|
836
|
-
if (params.resultId !== undefined) {
|
|
837
|
-
throw new Error("apply accepts either resultId or AGENTS checkpoint");
|
|
838
|
-
}
|
|
839
|
-
const validation = validateAgentsCheckpointAttempt(
|
|
840
|
-
params.agentsCheckpoint,
|
|
841
|
-
);
|
|
842
|
-
if (!validation.ok) {
|
|
843
|
-
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
844
|
-
}
|
|
845
|
-
return this.enqueueAgentsCheckpoint(ctx.cwd, validation.value, signal);
|
|
846
|
-
}
|
|
847
|
-
if (!params.resultId) {
|
|
848
|
-
throw new Error("apply requires resultId");
|
|
849
|
-
}
|
|
850
|
-
const retained = this.results.get(params.resultId);
|
|
851
|
-
if (
|
|
852
|
-
params.requestId !== undefined ||
|
|
853
|
-
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
854
|
-
) {
|
|
855
|
-
const validation = validateImplementApplyOperation(params);
|
|
856
|
-
if (!validation.ok) {
|
|
857
|
-
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
858
|
-
}
|
|
859
|
-
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
860
|
-
throw new Error("retained Implement result not found");
|
|
861
|
-
}
|
|
862
|
-
return this.enqueueApply(ctx.cwd, validation.value, signal);
|
|
863
|
-
}
|
|
864
|
-
return this.enqueueApply(ctx.cwd, { resultId: params.resultId }, signal);
|
|
865
|
-
}
|
|
866
|
-
if (action === "discard") {
|
|
867
|
-
if (!params.resultId) throw new Error("discard requires resultId");
|
|
868
|
-
const retained = this.results.get(params.resultId);
|
|
869
|
-
if (
|
|
870
|
-
params.requestId !== undefined ||
|
|
871
|
-
params.rejection !== undefined ||
|
|
872
|
-
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
873
|
-
) {
|
|
874
|
-
const validation = validateImplementDiscardOperation(params);
|
|
875
|
-
if (!validation.ok) {
|
|
876
|
-
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
877
|
-
}
|
|
878
|
-
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
879
|
-
throw new Error("retained Implement result not found");
|
|
880
|
-
}
|
|
881
|
-
return this.enqueueDiscard(validation.value);
|
|
882
|
-
}
|
|
883
|
-
return this.discardRetainedCandidate(params.resultId)
|
|
884
|
-
? { ok: true, action }
|
|
885
|
-
: { ok: false, error: "retained result not found" };
|
|
886
|
-
}
|
|
887
|
-
if (action === "cancel") {
|
|
888
|
-
await this.scheduler.cancelAll();
|
|
889
|
-
return { ok: true, action };
|
|
890
|
-
}
|
|
891
|
-
await this.drain();
|
|
892
|
-
return { ok: true, action: "finish" };
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
private enqueueAgentsCheckpoint(
|
|
896
|
-
root: string,
|
|
897
|
-
attempt: AgentsCheckpointAttempt,
|
|
898
|
-
signal?: AbortSignal,
|
|
899
|
-
): Promise<DispatchResult> {
|
|
900
|
-
const sequence = ++this.applySeq;
|
|
901
|
-
return this.enqueueParentApply(() =>
|
|
902
|
-
this.performAgentsCheckpoint(root, attempt, signal),
|
|
903
|
-
).then((result) => {
|
|
904
|
-
if (
|
|
905
|
-
isWrappedDispatchResult(result) ||
|
|
906
|
-
result.kind !== "completed" ||
|
|
907
|
-
!result.result
|
|
908
|
-
) {
|
|
909
|
-
return result;
|
|
910
|
-
}
|
|
911
|
-
return {
|
|
912
|
-
...result,
|
|
913
|
-
result: { ...result.result, sequence },
|
|
914
|
-
};
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
private async performAgentsCheckpoint(
|
|
919
|
-
root: string,
|
|
920
|
-
attempt: AgentsCheckpointAttempt,
|
|
921
|
-
signal?: AbortSignal,
|
|
922
|
-
): Promise<DispatchResult> {
|
|
923
|
-
const workspaceRoot = canonicalWorkspaceRoot(root);
|
|
924
|
-
if (!workspaceRoot) {
|
|
925
|
-
throw new Error("AGENTS checkpoint workspace is unavailable");
|
|
926
|
-
}
|
|
927
|
-
const worker = this.registry.get(
|
|
928
|
-
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
929
|
-
);
|
|
930
|
-
if (!worker) {
|
|
931
|
-
if (
|
|
932
|
-
this.registry
|
|
933
|
-
.values()
|
|
934
|
-
.some((record) => record.workspaceRoot === workspaceRoot)
|
|
935
|
-
) {
|
|
936
|
-
throw new Error("AGENTS checkpoint identity mismatch");
|
|
937
|
-
}
|
|
938
|
-
throw new Error("logical Worker is unavailable for AGENTS checkpoint");
|
|
939
|
-
}
|
|
940
|
-
const agents = worker.boundary.agents;
|
|
941
|
-
if (agents.impact === "none" || !agents.target || !agents.managedOnly) {
|
|
942
|
-
throw new Error("AGENTS checkpoint contract mismatch");
|
|
943
|
-
}
|
|
944
|
-
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
945
|
-
throw new Error("AGENTS checkpoint is not pending");
|
|
946
|
-
}
|
|
947
|
-
if (
|
|
948
|
-
Object.keys(attempt.snapshot as Record<string, unknown>).length !== 1 ||
|
|
949
|
-
!Object.hasOwn(attempt.snapshot as object, agents.target)
|
|
950
|
-
) {
|
|
951
|
-
throw new Error("AGENTS checkpoint target mismatch");
|
|
952
|
-
}
|
|
953
|
-
const outputFailure = this.unavailableAppliedOutput(
|
|
954
|
-
worker,
|
|
955
|
-
worker.state.finalPhase,
|
|
956
|
-
true,
|
|
957
|
-
);
|
|
958
|
-
if (outputFailure) {
|
|
959
|
-
const failure: TaskFailure = {
|
|
960
|
-
kind: "graph-readiness",
|
|
961
|
-
diagnostic: outputFailure,
|
|
962
|
-
};
|
|
963
|
-
worker.state = {
|
|
964
|
-
kind: "blocked",
|
|
965
|
-
phase: worker.state.finalPhase,
|
|
966
|
-
failure,
|
|
967
|
-
};
|
|
968
|
-
return blockedOutcome(
|
|
969
|
-
{
|
|
970
|
-
requestId: attempt.requestId,
|
|
971
|
-
taskId: attempt.taskId,
|
|
972
|
-
phase: worker.state.phase,
|
|
973
|
-
},
|
|
974
|
-
failure,
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
const request: AgentsCheckpointRequest = {
|
|
978
|
-
stage: "abel-implement",
|
|
979
|
-
taskId: worker.boundary.taskId,
|
|
980
|
-
agentsImpact: agents.impact,
|
|
981
|
-
agentsTarget: agents.target,
|
|
982
|
-
agentsManagedOnly: true,
|
|
983
|
-
stableCheckpoint: true,
|
|
984
|
-
snapshot: attempt.snapshot,
|
|
985
|
-
diff: attempt.diff,
|
|
986
|
-
};
|
|
987
|
-
const result = await applyAgentsCheckpoint(workspaceRoot, request, signal);
|
|
988
|
-
if (result.ok) {
|
|
989
|
-
const finalPhase = worker.state.finalPhase;
|
|
990
|
-
worker.state = {
|
|
991
|
-
kind: "completed",
|
|
992
|
-
finalPhase,
|
|
993
|
-
};
|
|
994
|
-
const { ok: _ok, ...checkpoint } = result;
|
|
995
|
-
return {
|
|
996
|
-
kind: "completed",
|
|
997
|
-
requestId: attempt.requestId,
|
|
998
|
-
taskId: attempt.taskId,
|
|
999
|
-
finalPhase,
|
|
1000
|
-
result: checkpoint,
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
return this.presentCheckpointFailure(attempt, worker, result.failure);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
private presentCheckpointFailure(
|
|
1007
|
-
attempt: AgentsCheckpointAttempt,
|
|
1008
|
-
worker: TaskRecord,
|
|
1009
|
-
failure: CandidateFailure,
|
|
1010
|
-
): DispatchResult {
|
|
1011
|
-
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
1012
|
-
throw new Error("AGENTS checkpoint is not pending");
|
|
1013
|
-
}
|
|
1014
|
-
const phase = worker.state.finalPhase;
|
|
1015
|
-
const identity = {
|
|
1016
|
-
taskId: attempt.taskId,
|
|
1017
|
-
requestId: attempt.requestId,
|
|
1018
|
-
phase,
|
|
1019
|
-
};
|
|
1020
|
-
switch (failure.kind) {
|
|
1021
|
-
case "artifact": {
|
|
1022
|
-
if (worker.state.attemptIndex === 1) {
|
|
1023
|
-
const taskFailure: TaskFailure = {
|
|
1024
|
-
kind: "checkpoint-attempts-exhausted",
|
|
1025
|
-
cause: "artifact",
|
|
1026
|
-
attemptsUsed: 2,
|
|
1027
|
-
lastFailure: safeFailureDiagnostic(failure),
|
|
1028
|
-
};
|
|
1029
|
-
worker.state = {
|
|
1030
|
-
kind: "blocked",
|
|
1031
|
-
phase,
|
|
1032
|
-
failure: structuredClone(taskFailure),
|
|
1033
|
-
};
|
|
1034
|
-
return blockedOutcome(identity, taskFailure);
|
|
1035
|
-
}
|
|
1036
|
-
worker.state = {
|
|
1037
|
-
kind: "agents-checkpoint-pending",
|
|
1038
|
-
finalPhase: phase,
|
|
1039
|
-
attemptIndex: 1,
|
|
1040
|
-
};
|
|
1041
|
-
return retryOutcome(identity, "checkpoint", failure);
|
|
1042
|
-
}
|
|
1043
|
-
case "stale": {
|
|
1044
|
-
if (worker.state.attemptIndex === 1) {
|
|
1045
|
-
const taskFailure: TaskFailure = {
|
|
1046
|
-
kind: "checkpoint-attempts-exhausted",
|
|
1047
|
-
cause: "stale",
|
|
1048
|
-
attemptsUsed: 2,
|
|
1049
|
-
lastFailure: safeFailureDiagnostic(failure),
|
|
1050
|
-
};
|
|
1051
|
-
worker.state = {
|
|
1052
|
-
kind: "blocked",
|
|
1053
|
-
phase,
|
|
1054
|
-
failure: structuredClone(taskFailure),
|
|
1055
|
-
};
|
|
1056
|
-
return blockedOutcome(identity, taskFailure);
|
|
1057
|
-
}
|
|
1058
|
-
worker.state = {
|
|
1059
|
-
kind: "agents-checkpoint-pending",
|
|
1060
|
-
finalPhase: phase,
|
|
1061
|
-
attemptIndex: 1,
|
|
1062
|
-
};
|
|
1063
|
-
return retryOutcome(identity, "checkpoint", failure);
|
|
1064
|
-
}
|
|
1065
|
-
case "environment": {
|
|
1066
|
-
const taskFailure: TaskFailure = failure;
|
|
1067
|
-
worker.state = {
|
|
1068
|
-
kind: "blocked",
|
|
1069
|
-
phase,
|
|
1070
|
-
failure: structuredClone(taskFailure),
|
|
1071
|
-
};
|
|
1072
|
-
return blockedOutcome(identity, taskFailure);
|
|
1073
|
-
}
|
|
1074
|
-
case "approval-boundary": {
|
|
1075
|
-
const taskFailure: TaskFailure = failure;
|
|
1076
|
-
worker.state = {
|
|
1077
|
-
kind: "blocked",
|
|
1078
|
-
phase,
|
|
1079
|
-
failure: structuredClone(taskFailure),
|
|
1080
|
-
};
|
|
1081
|
-
return blockedOutcome(identity, taskFailure);
|
|
1082
|
-
}
|
|
1083
|
-
case "verification-adapter": {
|
|
1084
|
-
const taskFailure: TaskFailure = failure;
|
|
1085
|
-
worker.state = {
|
|
1086
|
-
kind: "blocked",
|
|
1087
|
-
phase,
|
|
1088
|
-
failure: structuredClone(taskFailure),
|
|
1089
|
-
};
|
|
1090
|
-
return blockedOutcome(identity, taskFailure);
|
|
1091
|
-
}
|
|
1092
|
-
case "cancelled":
|
|
1093
|
-
return cancelledOutcome(identity);
|
|
1094
|
-
case "result-limit": {
|
|
1095
|
-
const taskFailure: TaskFailure = failure;
|
|
1096
|
-
worker.state = {
|
|
1097
|
-
kind: "blocked",
|
|
1098
|
-
phase,
|
|
1099
|
-
failure: structuredClone(taskFailure),
|
|
1100
|
-
};
|
|
1101
|
-
return blockedOutcome(identity, taskFailure);
|
|
1102
|
-
}
|
|
1103
|
-
default:
|
|
1104
|
-
return assertNever(failure);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
private resolveRetainedCandidate(
|
|
1109
|
-
resultId: string,
|
|
1110
|
-
operationRoot?: string,
|
|
1111
|
-
): { retained: BoundRetainedResult; worker: TaskRecord } | undefined {
|
|
1112
|
-
const candidate = this.results.get(resultId);
|
|
1113
|
-
if (!candidate) return undefined;
|
|
1114
|
-
if (!this.results.hasCandidateIdentity(resultId)) return undefined;
|
|
1115
|
-
if (
|
|
1116
|
-
candidate.stage !== "abel-implement" ||
|
|
1117
|
-
candidate.canonicalRoot === undefined ||
|
|
1118
|
-
candidate.changeId === undefined ||
|
|
1119
|
-
candidate.taskId === undefined ||
|
|
1120
|
-
candidate.originRequestId === undefined ||
|
|
1121
|
-
candidate.phase === undefined ||
|
|
1122
|
-
candidate.launchIndex === undefined
|
|
1123
|
-
) {
|
|
1124
|
-
throw new Error("retained candidate identity mismatch");
|
|
1125
|
-
}
|
|
1126
|
-
if (
|
|
1127
|
-
operationRoot !== undefined &&
|
|
1128
|
-
canonicalWorkspaceRoot(operationRoot) !== candidate.canonicalRoot
|
|
1129
|
-
) {
|
|
1130
|
-
throw new Error("retained candidate identity mismatch");
|
|
1131
|
-
}
|
|
1132
|
-
const worker = this.registry.get(
|
|
1133
|
-
taskRecordKey(
|
|
1134
|
-
candidate.canonicalRoot,
|
|
1135
|
-
candidate.changeId,
|
|
1136
|
-
candidate.taskId,
|
|
1137
|
-
),
|
|
1138
|
-
);
|
|
1139
|
-
if (!worker) throw new Error("retained candidate identity mismatch");
|
|
1140
|
-
if (
|
|
1141
|
-
worker.state.kind !== "candidate-pending" ||
|
|
1142
|
-
worker.state.resultId !== resultId ||
|
|
1143
|
-
worker.state.originRequestId !== candidate.originRequestId ||
|
|
1144
|
-
worker.state.phase !== candidate.phase ||
|
|
1145
|
-
worker.state.launchIndex !== candidate.launchIndex
|
|
1146
|
-
) {
|
|
1147
|
-
throw new Error("retained candidate identity mismatch");
|
|
1148
|
-
}
|
|
1149
|
-
const phase = worker.boundary.phases[worker.state.phase];
|
|
1150
|
-
if (!phase || !isSafeBound(candidate.snapshot)) {
|
|
1151
|
-
throw new Error("retained candidate identity mismatch");
|
|
1152
|
-
}
|
|
1153
|
-
const expected: RetainedCandidateFacts = {
|
|
1154
|
-
stage: "abel-implement",
|
|
1155
|
-
canonicalRoot: worker.workspaceRoot,
|
|
1156
|
-
root: worker.workspaceRoot,
|
|
1157
|
-
changeId: worker.boundary.changeId,
|
|
1158
|
-
taskId: worker.boundary.taskId,
|
|
1159
|
-
originRequestId: worker.state.originRequestId,
|
|
1160
|
-
phase: worker.state.phase,
|
|
1161
|
-
launchIndex: worker.state.launchIndex,
|
|
1162
|
-
writeSet: [...phase.write],
|
|
1163
|
-
approvedDependencies: [...worker.boundary.approvedDependencies],
|
|
1164
|
-
snapshot: candidate.snapshot,
|
|
1165
|
-
};
|
|
1166
|
-
return {
|
|
1167
|
-
retained: this.results.resolveIdentity(resultId, expected),
|
|
1168
|
-
worker,
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
private discardRetainedCandidate(resultId: string): boolean {
|
|
1173
|
-
return this.results.discard(resultId);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
private enqueueApply(
|
|
1177
|
-
root: string,
|
|
1178
|
-
operation: ImplementApplyOperation | { resultId: string },
|
|
1179
|
-
signal?: AbortSignal,
|
|
1180
|
-
): Promise<DispatchResult> {
|
|
1181
|
-
const id = operation.resultId;
|
|
1182
|
-
const resolved = this.resolveRetainedCandidate(id, root);
|
|
1183
|
-
const requestId = "requestId" in operation ? operation.requestId : null;
|
|
1184
|
-
if (requestId !== null && !resolved) {
|
|
1185
|
-
throw new Error("retained Implement result not found");
|
|
1186
|
-
}
|
|
1187
|
-
if (resolved && requestId === null) {
|
|
1188
|
-
throw new Error("Implement apply operation identity is missing");
|
|
1189
|
-
}
|
|
1190
|
-
const applyRoot = resolved?.retained.root ?? root;
|
|
1191
|
-
const run = this.enqueueParentApply(() =>
|
|
1192
|
-
applyRetainedPatch({
|
|
1193
|
-
root: applyRoot,
|
|
1194
|
-
id,
|
|
1195
|
-
store: this.results,
|
|
1196
|
-
...(resolved
|
|
1197
|
-
? {
|
|
1198
|
-
candidateOutputsAvailable: (checkoutRoot: string) =>
|
|
1199
|
-
this.candidateOutputsAvailable(resolved.worker, checkoutRoot),
|
|
1200
|
-
}
|
|
1201
|
-
: {}),
|
|
1202
|
-
signal,
|
|
1203
|
-
}),
|
|
1204
|
-
);
|
|
1205
|
-
const seq = ++this.applySeq;
|
|
1206
|
-
return run.then((result) => {
|
|
1207
|
-
if (result.ok) {
|
|
1208
|
-
const applied = { ...result.result, sequence: seq };
|
|
1209
|
-
if (resolved) {
|
|
1210
|
-
if (requestId === null)
|
|
1211
|
-
throw new Error("Implement apply operation identity is missing");
|
|
1212
|
-
const { retained: identity, worker } = resolved;
|
|
1213
|
-
if (worker.state.kind === "candidate-pending") {
|
|
1214
|
-
const readyPhase = nextDeclaredPhase(
|
|
1215
|
-
worker.boundary,
|
|
1216
|
-
identity.phase,
|
|
1217
|
-
);
|
|
1218
|
-
const outputFailure = this.unavailableAppliedOutput(
|
|
1219
|
-
worker,
|
|
1220
|
-
identity.phase,
|
|
1221
|
-
readyPhase === null && worker.boundary.agents.impact === "none",
|
|
1222
|
-
);
|
|
1223
|
-
if (outputFailure) {
|
|
1224
|
-
const failure: TaskFailure = {
|
|
1225
|
-
kind: "graph-readiness",
|
|
1226
|
-
diagnostic: outputFailure,
|
|
1227
|
-
};
|
|
1228
|
-
worker.state = {
|
|
1229
|
-
kind: "blocked",
|
|
1230
|
-
phase: identity.phase,
|
|
1231
|
-
failure,
|
|
1232
|
-
};
|
|
1233
|
-
return {
|
|
1234
|
-
...blockedOutcome(
|
|
1235
|
-
{
|
|
1236
|
-
requestId,
|
|
1237
|
-
taskId: identity.taskId,
|
|
1238
|
-
phase: identity.phase,
|
|
1239
|
-
},
|
|
1240
|
-
failure,
|
|
1241
|
-
),
|
|
1242
|
-
result: applied,
|
|
1243
|
-
} satisfies ImplementOutcome;
|
|
1244
|
-
}
|
|
1245
|
-
if (readyPhase) {
|
|
1246
|
-
worker.state = {
|
|
1247
|
-
kind: "ready",
|
|
1248
|
-
phase: readyPhase,
|
|
1249
|
-
launchIndex: 0,
|
|
1250
|
-
};
|
|
1251
|
-
return {
|
|
1252
|
-
kind: "applied",
|
|
1253
|
-
requestId,
|
|
1254
|
-
taskId: identity.taskId,
|
|
1255
|
-
phase: identity.phase as "red" | "green",
|
|
1256
|
-
readyPhase,
|
|
1257
|
-
result: applied,
|
|
1258
|
-
} satisfies ImplementOutcome;
|
|
1259
|
-
} else if (worker.boundary.agents.impact === "none") {
|
|
1260
|
-
worker.state = {
|
|
1261
|
-
kind: "completed",
|
|
1262
|
-
finalPhase: identity.phase as "green" | "refactor",
|
|
1263
|
-
};
|
|
1264
|
-
return {
|
|
1265
|
-
kind: "completed",
|
|
1266
|
-
requestId,
|
|
1267
|
-
taskId: identity.taskId,
|
|
1268
|
-
finalPhase: identity.phase as "green" | "refactor",
|
|
1269
|
-
result: applied,
|
|
1270
|
-
} satisfies ImplementOutcome;
|
|
1271
|
-
} else {
|
|
1272
|
-
worker.state = {
|
|
1273
|
-
kind: "agents-checkpoint-pending",
|
|
1274
|
-
finalPhase: identity.phase as "green" | "refactor",
|
|
1275
|
-
attemptIndex: 0,
|
|
1276
|
-
};
|
|
1277
|
-
return {
|
|
1278
|
-
kind: "checkpoint-required",
|
|
1279
|
-
requestId,
|
|
1280
|
-
taskId: identity.taskId,
|
|
1281
|
-
finalPhase: identity.phase as "green" | "refactor",
|
|
1282
|
-
result: applied,
|
|
1283
|
-
} satisfies ImplementOutcome;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
throw new Error("retained candidate is not pending");
|
|
1287
|
-
}
|
|
1288
|
-
return {
|
|
1289
|
-
ok: true,
|
|
1290
|
-
action: "apply",
|
|
1291
|
-
result: applied,
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
if (!resolved) {
|
|
1295
|
-
return {
|
|
1296
|
-
ok: false,
|
|
1297
|
-
error: "candidate application failed",
|
|
1298
|
-
failure: result.failure,
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
if (requestId === null)
|
|
1302
|
-
throw new Error("Implement apply operation identity is missing");
|
|
1303
|
-
return this.presentApplyFailure(
|
|
1304
|
-
requestId,
|
|
1305
|
-
id,
|
|
1306
|
-
resolved.retained,
|
|
1307
|
-
resolved.worker,
|
|
1308
|
-
result.failure,
|
|
1309
|
-
);
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
private enqueueDiscard(
|
|
1314
|
-
operation: ImplementDiscardOperation,
|
|
1315
|
-
): Promise<DispatchResult> {
|
|
1316
|
-
return this.enqueueParentApply(async () => {
|
|
1317
|
-
const resolved = this.resolveRetainedCandidate(operation.resultId);
|
|
1318
|
-
if (!resolved || !this.results.discard(operation.resultId)) {
|
|
1319
|
-
throw new Error("retained Implement result not found");
|
|
1320
|
-
}
|
|
1321
|
-
const { retained: identity, worker } = resolved;
|
|
1322
|
-
const outcomeIdentity = {
|
|
1323
|
-
requestId: operation.requestId,
|
|
1324
|
-
taskId: identity.taskId,
|
|
1325
|
-
phase: identity.phase,
|
|
1326
|
-
};
|
|
1327
|
-
const rejection = operation.rejection;
|
|
1328
|
-
if (rejection.kind === "approval-boundary") {
|
|
1329
|
-
worker.state = {
|
|
1330
|
-
kind: "blocked",
|
|
1331
|
-
phase: identity.phase,
|
|
1332
|
-
failure: structuredClone(rejection),
|
|
1333
|
-
};
|
|
1334
|
-
return blockedOutcome(outcomeIdentity, rejection);
|
|
1335
|
-
}
|
|
1336
|
-
if (identity.launchIndex === 0) {
|
|
1337
|
-
worker.state = {
|
|
1338
|
-
kind: "ready",
|
|
1339
|
-
phase: identity.phase,
|
|
1340
|
-
launchIndex: 1,
|
|
1341
|
-
correction: structuredClone(rejection),
|
|
1342
|
-
};
|
|
1343
|
-
return retryOutcome(outcomeIdentity, "worker", rejection);
|
|
1344
|
-
}
|
|
1345
|
-
const failure = attemptsExhaustedFailure(rejection);
|
|
1346
|
-
worker.state = {
|
|
1347
|
-
kind: "blocked",
|
|
1348
|
-
phase: identity.phase,
|
|
1349
|
-
failure: structuredClone(failure),
|
|
1350
|
-
};
|
|
1351
|
-
return blockedOutcome(outcomeIdentity, failure);
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
private enqueueParentApply<T>(operation: () => Promise<T>): Promise<T> {
|
|
1356
|
-
const run = this.applyTail.then(operation, operation);
|
|
1357
|
-
this.applyTail = run.then(
|
|
1358
|
-
() => undefined,
|
|
1359
|
-
() => undefined,
|
|
1360
|
-
);
|
|
1361
|
-
return run;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
private presentApplyFailure(
|
|
1365
|
-
requestId: string,
|
|
1366
|
-
resultId: string,
|
|
1367
|
-
identity: BoundRetainedResult,
|
|
1368
|
-
worker: TaskRecord,
|
|
1369
|
-
failure: CandidateFailure,
|
|
1370
|
-
): DispatchResult {
|
|
1371
|
-
const outcomeIdentity = {
|
|
1372
|
-
taskId: identity.taskId,
|
|
1373
|
-
requestId,
|
|
1374
|
-
phase: identity.phase,
|
|
1375
|
-
};
|
|
1376
|
-
switch (failure.kind) {
|
|
1377
|
-
case "environment": {
|
|
1378
|
-
this.results.discard(resultId);
|
|
1379
|
-
worker.state = {
|
|
1380
|
-
kind: "blocked",
|
|
1381
|
-
phase: identity.phase,
|
|
1382
|
-
failure: structuredClone(failure),
|
|
1383
|
-
};
|
|
1384
|
-
return blockedOutcome(outcomeIdentity, failure);
|
|
1385
|
-
}
|
|
1386
|
-
case "approval-boundary": {
|
|
1387
|
-
this.results.discard(resultId);
|
|
1388
|
-
worker.state = {
|
|
1389
|
-
kind: "blocked",
|
|
1390
|
-
phase: identity.phase,
|
|
1391
|
-
failure: structuredClone(failure),
|
|
1392
|
-
};
|
|
1393
|
-
return blockedOutcome(outcomeIdentity, failure);
|
|
1394
|
-
}
|
|
1395
|
-
case "verification-adapter": {
|
|
1396
|
-
this.results.discard(resultId);
|
|
1397
|
-
worker.state = {
|
|
1398
|
-
kind: "blocked",
|
|
1399
|
-
phase: identity.phase,
|
|
1400
|
-
failure: structuredClone(failure),
|
|
1401
|
-
};
|
|
1402
|
-
return blockedOutcome(outcomeIdentity, failure);
|
|
1403
|
-
}
|
|
1404
|
-
case "stale": {
|
|
1405
|
-
this.results.discard(resultId);
|
|
1406
|
-
if (identity.launchIndex === 0) {
|
|
1407
|
-
worker.state = {
|
|
1408
|
-
kind: "ready",
|
|
1409
|
-
phase: identity.phase,
|
|
1410
|
-
launchIndex: 1,
|
|
1411
|
-
correction: structuredClone(failure),
|
|
1412
|
-
};
|
|
1413
|
-
} else {
|
|
1414
|
-
const taskFailure = attemptsExhaustedFailure(failure);
|
|
1415
|
-
worker.state = {
|
|
1416
|
-
kind: "blocked",
|
|
1417
|
-
phase: identity.phase,
|
|
1418
|
-
failure: structuredClone(taskFailure),
|
|
1419
|
-
};
|
|
1420
|
-
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1421
|
-
}
|
|
1422
|
-
return retryOutcome(outcomeIdentity, "worker", failure);
|
|
1423
|
-
}
|
|
1424
|
-
case "artifact": {
|
|
1425
|
-
this.results.discard(resultId);
|
|
1426
|
-
const correctionAvailable = identity.launchIndex === 0;
|
|
1427
|
-
if (!correctionAvailable) {
|
|
1428
|
-
const taskFailure = attemptsExhaustedFailure(failure);
|
|
1429
|
-
worker.state = {
|
|
1430
|
-
kind: "blocked",
|
|
1431
|
-
phase: identity.phase,
|
|
1432
|
-
failure: structuredClone(taskFailure),
|
|
1433
|
-
};
|
|
1434
|
-
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1435
|
-
}
|
|
1436
|
-
worker.state = {
|
|
1437
|
-
kind: "ready",
|
|
1438
|
-
phase: identity.phase,
|
|
1439
|
-
launchIndex: 1,
|
|
1440
|
-
correction: structuredClone(failure),
|
|
1441
|
-
};
|
|
1442
|
-
return retryOutcome(outcomeIdentity, "worker", failure);
|
|
1443
|
-
}
|
|
1444
|
-
case "cancelled":
|
|
1445
|
-
return cancelledOutcome(outcomeIdentity);
|
|
1446
|
-
case "result-limit":
|
|
1447
|
-
this.results.discard(resultId);
|
|
1448
|
-
worker.state = {
|
|
1449
|
-
kind: "blocked",
|
|
1450
|
-
phase: identity.phase,
|
|
1451
|
-
failure: structuredClone(failure),
|
|
1452
|
-
};
|
|
1453
|
-
return blockedOutcome(outcomeIdentity, failure);
|
|
1454
|
-
default:
|
|
1455
|
-
return assertNever(failure);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
private async dispatchChild(
|
|
1460
|
-
agent: { role: string; content: string },
|
|
1461
|
-
envelope: RequestEnvelope,
|
|
1462
|
-
ctx: RunContext,
|
|
1463
|
-
signal: AbortSignal,
|
|
1464
|
-
artifactRejection?: string,
|
|
1465
|
-
): Promise<ChildDispatchResult> {
|
|
1466
|
-
if (signal.aborted)
|
|
1467
|
-
return {
|
|
1468
|
-
...cancellationError(signal),
|
|
1469
|
-
failureKind: "cancelled",
|
|
1470
|
-
};
|
|
1471
|
-
const task = this.taskRecords.get(envelope);
|
|
1472
|
-
const endpoint = task
|
|
1473
|
-
? task.subagentEndpoint === null
|
|
1474
|
-
? ({ kind: "inherited" } as const)
|
|
1475
|
-
: ({ kind: "custom", endpoint: task.subagentEndpoint } as const)
|
|
1476
|
-
: resolveSubagentEndpoint(envelope.role, { cwd: ctx.cwd });
|
|
1477
|
-
if (endpoint.kind === "invalid") {
|
|
1478
|
-
const message = describeInvalidSubagentEndpoint(endpoint);
|
|
1479
|
-
return {
|
|
1480
|
-
ok: false,
|
|
1481
|
-
error: message,
|
|
1482
|
-
failure: {
|
|
1483
|
-
kind: "environment",
|
|
1484
|
-
code: "invalid-subagent-endpoint",
|
|
1485
|
-
message,
|
|
1486
|
-
},
|
|
1487
|
-
failureKind: "failed",
|
|
1488
|
-
failureClass: "environment",
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
const phase =
|
|
1492
|
-
endpoint.kind === "custom"
|
|
1493
|
-
? await customPhaseRuntime(endpoint.endpoint, signal)
|
|
1494
|
-
: await runtimeFromContext(ctx, this.parentPayloadBridge, signal);
|
|
1495
|
-
if (!phase.ok) {
|
|
1496
|
-
return {
|
|
1497
|
-
ok: false,
|
|
1498
|
-
error: phase.error,
|
|
1499
|
-
failure: phase.failure,
|
|
1500
|
-
failureKind:
|
|
1501
|
-
phase.failure.kind === "cancelled" ? "cancelled" : "failed",
|
|
1502
|
-
...(phase.failure.kind === "cancelled"
|
|
1503
|
-
? {}
|
|
1504
|
-
: { failureClass: phase.failure.kind }),
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
if (signal.aborted)
|
|
1508
|
-
return {
|
|
1509
|
-
...cancellationError(signal),
|
|
1510
|
-
failureKind: "cancelled",
|
|
1511
|
-
};
|
|
1512
|
-
const roots = envelope.roots.map(
|
|
1513
|
-
(root) => new URL(root, `file://${ctx.cwd}/`).pathname,
|
|
1514
|
-
);
|
|
1515
|
-
const phaseContract = {
|
|
1516
|
-
taskId: envelope.taskId ?? envelope.id,
|
|
1517
|
-
requestId: envelope.id,
|
|
1518
|
-
phase: envelope.phase,
|
|
1519
|
-
readSet: [...envelope.declared.read],
|
|
1520
|
-
writeSet: [...envelope.declared.write],
|
|
1521
|
-
verification: envelope.verification ?? null,
|
|
1522
|
-
agentsImpact: envelope.agentsImpact ?? "none",
|
|
1523
|
-
agentsTarget: envelope.agentsTarget ?? null,
|
|
1524
|
-
agentsManagedOnly: true,
|
|
1525
|
-
agentsWriteAllowed: false,
|
|
1526
|
-
impactClosure: envelope.impactClosure ?? null,
|
|
1527
|
-
};
|
|
1528
|
-
const systemPrompt = [
|
|
1529
|
-
agent.content,
|
|
1530
|
-
envelope.objective,
|
|
1531
|
-
envelope.context.agents,
|
|
1532
|
-
envelope.context.contract,
|
|
1533
|
-
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1534
|
-
...(artifactRejection
|
|
1535
|
-
? [`<artifact-rejection>${artifactRejection}</artifact-rejection>`]
|
|
1536
|
-
: []),
|
|
1537
|
-
].join("\n\n");
|
|
1538
|
-
const child = await runChildSession({
|
|
1539
|
-
cwd: ctx.cwd,
|
|
1540
|
-
modelRuntime: phase.modelRuntime,
|
|
1541
|
-
model: phase.model,
|
|
1542
|
-
systemPrompt,
|
|
1543
|
-
requestId: envelope.id,
|
|
1544
|
-
taskId: envelope.taskId,
|
|
1545
|
-
role: envelope.role,
|
|
1546
|
-
phase: envelope.phase,
|
|
1547
|
-
output: envelope.output as "evidence" | "diff",
|
|
1548
|
-
roots,
|
|
1549
|
-
allowedPaths: [
|
|
1550
|
-
...new Set([...envelope.declared.read, ...envelope.declared.write]),
|
|
1551
|
-
],
|
|
1552
|
-
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1553
|
-
signal,
|
|
1554
|
-
failureOverride: phase.failureOverride,
|
|
1555
|
-
});
|
|
1556
|
-
if (signal.aborted)
|
|
1557
|
-
return {
|
|
1558
|
-
...cancellationError(signal),
|
|
1559
|
-
failureKind: "cancelled",
|
|
1560
|
-
usage: child.usage,
|
|
1561
|
-
};
|
|
1562
|
-
if (!child.ok) {
|
|
1563
|
-
return {
|
|
1564
|
-
...child,
|
|
1565
|
-
failureKind: child.failureKind,
|
|
1566
|
-
failureClass: failureClassOf(child.failure),
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
if (envelope.output === "evidence") {
|
|
1570
|
-
return {
|
|
1571
|
-
ok: true,
|
|
1572
|
-
action: "run",
|
|
1573
|
-
result: child.result,
|
|
1574
|
-
usage: child.usage,
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
const diff = child.result as DiffResult;
|
|
1578
|
-
const requestSnapshot = isSafeBound(envelope.snapshot)
|
|
1579
|
-
? envelope.snapshot
|
|
1580
|
-
: {};
|
|
1581
|
-
let prepared: PreparedPreflight | null;
|
|
1582
|
-
try {
|
|
1583
|
-
prepared = preparePreflight(ctx.cwd, envelope, requestSnapshot);
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
if (!isFileSystemFailure(error)) throw error;
|
|
1586
|
-
return {
|
|
1587
|
-
ok: false,
|
|
1588
|
-
error: "candidate preflight inputs are unavailable",
|
|
1589
|
-
failure: { kind: "environment", code: "root-unavailable" },
|
|
1590
|
-
failureKind: "failed",
|
|
1591
|
-
failureClass: "environment",
|
|
1592
|
-
launchConsumed: true,
|
|
1593
|
-
usage: child.usage,
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
if (envelope.verification && !prepared) {
|
|
1597
|
-
return {
|
|
1598
|
-
ok: false,
|
|
1599
|
-
error: "candidate preflight inputs are unavailable",
|
|
1600
|
-
failure: { kind: "environment", code: "root-unavailable" },
|
|
1601
|
-
failureKind: "failed",
|
|
1602
|
-
failureClass: "environment",
|
|
1603
|
-
launchConsumed: true,
|
|
1604
|
-
usage: child.usage,
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
const retainedRoot = task?.workspaceRoot ?? ctx.cwd;
|
|
1608
|
-
const retainedSnapshot =
|
|
1609
|
-
prepared?.snapshot ??
|
|
1610
|
-
(isSafeBound(envelope.snapshot)
|
|
1611
|
-
? mergeBounds(
|
|
1612
|
-
snapshotFiles(retainedRoot, envelope.declared.write),
|
|
1613
|
-
envelope.snapshot,
|
|
1614
|
-
)
|
|
1615
|
-
: undefined);
|
|
1616
|
-
const resultId = this.results.retain({
|
|
1617
|
-
diff: diff.diff,
|
|
1618
|
-
writeSet: envelope.declared.write,
|
|
1619
|
-
approvedDependencies: envelope.approvedDependencies ?? [],
|
|
1620
|
-
root: retainedRoot,
|
|
1621
|
-
...(task
|
|
1622
|
-
? {
|
|
1623
|
-
stage: "abel-implement" as const,
|
|
1624
|
-
canonicalRoot: task.workspaceRoot,
|
|
1625
|
-
changeId: task.boundary.changeId,
|
|
1626
|
-
taskId: task.boundary.taskId,
|
|
1627
|
-
originRequestId: envelope.id,
|
|
1628
|
-
phase: taskPhase(task),
|
|
1629
|
-
launchIndex: taskLaunchIndex(task),
|
|
1630
|
-
}
|
|
1631
|
-
: {}),
|
|
1632
|
-
...(prepared
|
|
1633
|
-
? {
|
|
1634
|
-
baseline: prepared.baseline,
|
|
1635
|
-
verification: envelope.verification,
|
|
1636
|
-
packageManifest: prepared.packageManifest,
|
|
1637
|
-
lockfile: prepared.lockfile,
|
|
1638
|
-
dependencyTarget: prepared.dependencyTarget,
|
|
1639
|
-
}
|
|
1640
|
-
: {}),
|
|
1641
|
-
...(retainedSnapshot ? { snapshot: retainedSnapshot } : {}),
|
|
1642
|
-
});
|
|
1643
|
-
return {
|
|
1644
|
-
ok: true,
|
|
1645
|
-
action: "run",
|
|
1646
|
-
result: diff,
|
|
1647
|
-
resultId,
|
|
1648
|
-
usage: child.usage,
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
private graphFacts(graph: ImplementGraphRecord): ImplementExecutionFacts {
|
|
1653
|
-
const records = this.registry
|
|
1654
|
-
.values()
|
|
1655
|
-
.filter(
|
|
1656
|
-
(record) =>
|
|
1657
|
-
record.workspaceRoot === graph.workspaceRoot &&
|
|
1658
|
-
record.boundary.changeId === graph.boundary.changeId,
|
|
1659
|
-
);
|
|
1660
|
-
return {
|
|
1661
|
-
completedTasks: [
|
|
1662
|
-
...new Set([
|
|
1663
|
-
...graph.completedTasks,
|
|
1664
|
-
...records
|
|
1665
|
-
.filter((record) => record.state.kind === "completed")
|
|
1666
|
-
.map((record) => record.boundary.taskId),
|
|
1667
|
-
]),
|
|
1668
|
-
],
|
|
1669
|
-
blockedTasks: [
|
|
1670
|
-
...new Set([
|
|
1671
|
-
...graph.blockedTasks,
|
|
1672
|
-
...records
|
|
1673
|
-
.filter((record) => record.state.kind === "blocked")
|
|
1674
|
-
.map((record) => record.boundary.taskId),
|
|
1675
|
-
]),
|
|
1676
|
-
],
|
|
1677
|
-
appliedPhases: records.flatMap((record) =>
|
|
1678
|
-
appliedPhasesOf(record).map((phase) => ({
|
|
1679
|
-
taskId: record.boundary.taskId,
|
|
1680
|
-
phase,
|
|
1681
|
-
})),
|
|
1682
|
-
),
|
|
1683
|
-
dependencyOwner: graph.workspaceRoot,
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
private graphFor(worker: TaskRecord): ImplementGraphRecord {
|
|
1688
|
-
const graph = this.registry.getGraph(
|
|
1689
|
-
worker.workspaceRoot,
|
|
1690
|
-
worker.boundary.changeId,
|
|
1691
|
-
);
|
|
1692
|
-
if (!graph) throw new Error("Implement graph is unavailable");
|
|
1693
|
-
return graph;
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
private candidateOutputsAvailable(
|
|
1697
|
-
worker: TaskRecord,
|
|
1698
|
-
checkoutRoot: string,
|
|
1699
|
-
): boolean {
|
|
1700
|
-
const graph = this.graphFor(worker);
|
|
1701
|
-
const phase = taskPhase(worker);
|
|
1702
|
-
const produced = new Set(
|
|
1703
|
-
graph.boundary.outputs
|
|
1704
|
-
.filter(
|
|
1705
|
-
(output) =>
|
|
1706
|
-
output.producer.taskId === worker.boundary.taskId &&
|
|
1707
|
-
output.producer.phase === phase,
|
|
1708
|
-
)
|
|
1709
|
-
.map((output) => output.id),
|
|
1710
|
-
);
|
|
1711
|
-
if (produced.size === 0) return true;
|
|
1712
|
-
const readiness = assessImplementGraphReadiness(
|
|
1713
|
-
checkoutRoot,
|
|
1714
|
-
graph.boundary,
|
|
1715
|
-
{
|
|
1716
|
-
...this.graphFacts(graph),
|
|
1717
|
-
candidate: { taskId: worker.boundary.taskId, phase },
|
|
1718
|
-
},
|
|
1719
|
-
);
|
|
1720
|
-
return readiness.outputs.every(
|
|
1721
|
-
(output) =>
|
|
1722
|
-
!produced.has(output.outputId) || output.status === "available",
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
private unavailableAppliedOutput(
|
|
1727
|
-
worker: TaskRecord,
|
|
1728
|
-
phase: ImplementationPhase,
|
|
1729
|
-
allTaskOutputs: boolean,
|
|
1730
|
-
): ImplementGraphReadinessDiagnostic | undefined {
|
|
1731
|
-
const graph = this.graphFor(worker);
|
|
1732
|
-
const facts = this.graphFacts(graph);
|
|
1733
|
-
const appliedPhases = [
|
|
1734
|
-
...(facts.appliedPhases ?? []),
|
|
1735
|
-
{ taskId: worker.boundary.taskId, phase },
|
|
1736
|
-
].filter(
|
|
1737
|
-
(entry, index, entries) =>
|
|
1738
|
-
entries.findIndex(
|
|
1739
|
-
(candidate) =>
|
|
1740
|
-
candidate.taskId === entry.taskId &&
|
|
1741
|
-
candidate.phase === entry.phase,
|
|
1742
|
-
) === index,
|
|
1743
|
-
);
|
|
1744
|
-
const readiness = assessImplementGraphReadiness(
|
|
1745
|
-
worker.workspaceRoot,
|
|
1746
|
-
graph.boundary,
|
|
1747
|
-
{ ...facts, appliedPhases },
|
|
1748
|
-
);
|
|
1749
|
-
const output = readiness.outputs.find(
|
|
1750
|
-
(entry) =>
|
|
1751
|
-
entry.producerTaskId === worker.boundary.taskId &&
|
|
1752
|
-
(allTaskOutputs || entry.producerPhase === phase) &&
|
|
1753
|
-
entry.status !== "available",
|
|
1754
|
-
);
|
|
1755
|
-
if (!output) return undefined;
|
|
1756
|
-
return (
|
|
1757
|
-
output.diagnostic ?? {
|
|
1758
|
-
kind: "graph-readiness",
|
|
1759
|
-
code: "producer-output-unavailable",
|
|
1760
|
-
outputId: output.outputId,
|
|
1761
|
-
producerTaskId: output.producerTaskId,
|
|
1762
|
-
producerPhase: output.producerPhase,
|
|
1763
|
-
}
|
|
1764
|
-
);
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
private admitImplementGraph(
|
|
1768
|
-
request: Extract<ImplementRunRequest, { kind: "admit-graph" }>,
|
|
1769
|
-
workspaceRoot: string,
|
|
1770
|
-
): ImplementOutcome {
|
|
1771
|
-
if (this.registry.getGraph(workspaceRoot, request.graph.changeId)) {
|
|
1772
|
-
throw new Error("duplicate graph admission protocol error");
|
|
1773
|
-
}
|
|
1774
|
-
const actualHash = hashImplementGraphBoundary(request.graph);
|
|
1775
|
-
if (actualHash !== request.graphHash) {
|
|
1776
|
-
return {
|
|
1777
|
-
kind: "graph-rejected",
|
|
1778
|
-
changeId: request.graph.changeId,
|
|
1779
|
-
graphHash: request.graphHash,
|
|
1780
|
-
diagnostics: [
|
|
1781
|
-
{
|
|
1782
|
-
kind: "graph-readiness",
|
|
1783
|
-
code: "graph-hash-mismatch",
|
|
1784
|
-
},
|
|
1785
|
-
],
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
const readiness = assessImplementGraphReadiness(
|
|
1789
|
-
workspaceRoot,
|
|
1790
|
-
request.graph,
|
|
1791
|
-
{
|
|
1792
|
-
completedTasks: request.state.completedTasks,
|
|
1793
|
-
blockedTasks: request.state.blockedTasks,
|
|
1794
|
-
dependencyOwner: workspaceRoot,
|
|
1795
|
-
},
|
|
1796
|
-
);
|
|
1797
|
-
const unavailableOutputs = readiness.outputs.flatMap((output) =>
|
|
1798
|
-
output.status === "unavailable" && output.diagnostic
|
|
1799
|
-
? [output.diagnostic]
|
|
1800
|
-
: [],
|
|
1801
|
-
);
|
|
1802
|
-
if (!readiness.closure.executable || unavailableOutputs.length > 0) {
|
|
1803
|
-
return {
|
|
1804
|
-
kind: "graph-rejected",
|
|
1805
|
-
changeId: request.graph.changeId,
|
|
1806
|
-
graphHash: request.graphHash,
|
|
1807
|
-
diagnostics: [...readiness.closure.diagnostics, ...unavailableOutputs],
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
this.registry.admitGraph(
|
|
1811
|
-
request.graph,
|
|
1812
|
-
request.graphHash,
|
|
1813
|
-
workspaceRoot,
|
|
1814
|
-
request.state,
|
|
1815
|
-
);
|
|
1816
|
-
const red = readiness.phases.filter((phase) => phase.phase === "red");
|
|
1817
|
-
return {
|
|
1818
|
-
kind: "graph-admitted",
|
|
1819
|
-
changeId: request.graph.changeId,
|
|
1820
|
-
graphHash: request.graphHash,
|
|
1821
|
-
readyTasks: red
|
|
1822
|
-
.filter((phase) => phase.ready)
|
|
1823
|
-
.map((phase) => phase.taskId),
|
|
1824
|
-
blockedTasks: red
|
|
1825
|
-
.filter((phase) => !phase.ready && !phase.completed)
|
|
1826
|
-
.map((phase) => phase.taskId),
|
|
1827
|
-
};
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
private async run(
|
|
1831
|
-
request: unknown,
|
|
1832
|
-
ctx?: RunContext,
|
|
1833
|
-
signal?: AbortSignal,
|
|
1834
|
-
observer?: RuntimeActivityObserver,
|
|
1835
|
-
): Promise<DispatchResult> {
|
|
1836
|
-
const implementRequest = targetsImplement(request);
|
|
1837
|
-
if (!ctx) {
|
|
1838
|
-
if (implementRequest) throw new Error("run requires extension context");
|
|
1839
|
-
return { ok: false, error: "run requires extension context" };
|
|
1840
|
-
}
|
|
1841
|
-
const validation = validateRequestEnvelope(request);
|
|
1842
|
-
if (!validation.ok) {
|
|
1843
|
-
if (implementRequest) {
|
|
1844
|
-
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
1845
|
-
}
|
|
1846
|
-
return { ok: false, error: validation.reason };
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
let envelope: RequestEnvelope;
|
|
1850
|
-
if (isImplementRunRequest(validation.value)) {
|
|
1851
|
-
const runRequest = validation.value;
|
|
1852
|
-
const workspaceRoot = canonicalWorkspaceRoot(ctx.cwd);
|
|
1853
|
-
if (!workspaceRoot) {
|
|
1854
|
-
throw new Error("implementation workspace is unavailable");
|
|
1855
|
-
}
|
|
1856
|
-
if (runRequest.kind === "admit-graph") {
|
|
1857
|
-
if (signal?.aborted) return cancellationError(signal);
|
|
1858
|
-
return this.admitImplementGraph(runRequest, workspaceRoot);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
const attempt = runRequest.attempt;
|
|
1862
|
-
const graph = this.registry.getGraph(workspaceRoot, attempt.changeId);
|
|
1863
|
-
if (!graph) {
|
|
1864
|
-
throw new Error("Implement graph is not admitted");
|
|
1865
|
-
}
|
|
1866
|
-
const boundary = taskBoundaryOf(graph, attempt.taskId);
|
|
1867
|
-
if (!boundary) throw new Error("task identity mismatch");
|
|
1868
|
-
const key = taskRecordKey(
|
|
1869
|
-
workspaceRoot,
|
|
1870
|
-
attempt.changeId,
|
|
1871
|
-
attempt.taskId,
|
|
1872
|
-
);
|
|
1873
|
-
const existing = this.registry.get(key);
|
|
1874
|
-
let existingIdentity: string | undefined;
|
|
1875
|
-
if (existing) {
|
|
1876
|
-
if (!ctx.model) {
|
|
1877
|
-
throw new Error("implementation model is unavailable");
|
|
1878
|
-
}
|
|
1879
|
-
existingIdentity = workerIdentity(ctx.model);
|
|
1880
|
-
if (existing.workerIdentity !== existingIdentity) {
|
|
1881
|
-
throw new Error("provider/model identity mismatch");
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
if (existing?.state.kind === "blocked") {
|
|
1885
|
-
if (attempt.phase !== existing.state.phase) {
|
|
1886
|
-
throw new Error("terminal task phase mismatch");
|
|
1887
|
-
}
|
|
1888
|
-
return blockedOutcome(
|
|
1889
|
-
{
|
|
1890
|
-
taskId: existing.boundary.taskId,
|
|
1891
|
-
requestId: attempt.requestId,
|
|
1892
|
-
phase: existing.state.phase,
|
|
1893
|
-
},
|
|
1894
|
-
existing.state.failure,
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
if (existing?.state.kind === "completed") {
|
|
1898
|
-
if (attempt.phase !== existing.state.finalPhase) {
|
|
1899
|
-
throw new Error("terminal task phase mismatch");
|
|
1900
|
-
}
|
|
1901
|
-
return {
|
|
1902
|
-
kind: "completed",
|
|
1903
|
-
taskId: existing.boundary.taskId,
|
|
1904
|
-
requestId: attempt.requestId,
|
|
1905
|
-
finalPhase: existing.state.finalPhase,
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
if (graph.completedTasks.has(attempt.taskId)) {
|
|
1909
|
-
const finalPhase = boundary.phases.refactor ? "refactor" : "green";
|
|
1910
|
-
if (attempt.phase !== finalPhase) {
|
|
1911
|
-
throw new Error("terminal task phase mismatch");
|
|
1912
|
-
}
|
|
1913
|
-
return {
|
|
1914
|
-
kind: "completed",
|
|
1915
|
-
taskId: attempt.taskId,
|
|
1916
|
-
requestId: attempt.requestId,
|
|
1917
|
-
finalPhase,
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
1921
|
-
boundary,
|
|
1922
|
-
attempt,
|
|
1923
|
-
);
|
|
1924
|
-
if (attemptReason !== null) throw new Error(attemptReason);
|
|
1925
|
-
if (existing) assertAttemptAllowed(existing, attempt);
|
|
1926
|
-
else if (attempt.phase !== "red") {
|
|
1927
|
-
throw new Error("task must begin with Red");
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
const readiness = assessImplementGraphReadiness(
|
|
1931
|
-
workspaceRoot,
|
|
1932
|
-
graph.boundary,
|
|
1933
|
-
this.graphFacts(graph),
|
|
1934
|
-
);
|
|
1935
|
-
const phaseReadiness = readiness.phases.find(
|
|
1936
|
-
(phase) =>
|
|
1937
|
-
phase.taskId === attempt.taskId && phase.phase === attempt.phase,
|
|
1938
|
-
);
|
|
1939
|
-
if (!phaseReadiness) throw new Error("task phase is not declared");
|
|
1940
|
-
if (!phaseReadiness.ready) {
|
|
1941
|
-
const dependencyDiagnostics = phaseReadiness.diagnostics.filter(
|
|
1942
|
-
(diagnostic) =>
|
|
1943
|
-
diagnostic.kind === "graph-readiness" &&
|
|
1944
|
-
diagnostic.code === "dependency-blocked",
|
|
1945
|
-
);
|
|
1946
|
-
if (dependencyDiagnostics.length > 0) {
|
|
1947
|
-
return {
|
|
1948
|
-
kind: "dependency-blocked",
|
|
1949
|
-
taskId: attempt.taskId,
|
|
1950
|
-
requestId: attempt.requestId,
|
|
1951
|
-
phase: attempt.phase,
|
|
1952
|
-
diagnostics: dependencyDiagnostics,
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
const diagnostic = phaseReadiness.diagnostics[0];
|
|
1956
|
-
if (!diagnostic) throw new Error("task phase is not ready");
|
|
1957
|
-
if (diagnostic.kind === "design-readiness") {
|
|
1958
|
-
throw new Error(`Implement protocol error: ${diagnostic.code}`);
|
|
1959
|
-
}
|
|
1960
|
-
if (diagnostic.kind === "verification-adapter") {
|
|
1961
|
-
return {
|
|
1962
|
-
ok: false,
|
|
1963
|
-
error: `${diagnostic.kind}/${diagnostic.code}`,
|
|
1964
|
-
failure: {
|
|
1965
|
-
kind: diagnostic.kind,
|
|
1966
|
-
code: diagnostic.code,
|
|
1967
|
-
},
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
return blockedOutcome(
|
|
1971
|
-
{
|
|
1972
|
-
taskId: attempt.taskId,
|
|
1973
|
-
requestId: attempt.requestId,
|
|
1974
|
-
phase: attempt.phase,
|
|
1975
|
-
},
|
|
1976
|
-
{ kind: "graph-readiness", diagnostic },
|
|
1977
|
-
);
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
1981
|
-
!existing &&
|
|
1982
|
-
this.registry
|
|
1983
|
-
.values()
|
|
1984
|
-
.some(
|
|
1985
|
-
(record) =>
|
|
1986
|
-
record.workspaceRoot === workspaceRoot &&
|
|
1987
|
-
record.state.kind !== "blocked" &&
|
|
1988
|
-
record.state.kind !== "completed" &&
|
|
1989
|
-
declarationsConflict(taskConflictOf(boundary), record.conflict),
|
|
1990
|
-
)
|
|
1991
|
-
) {
|
|
1992
|
-
return {
|
|
1993
|
-
kind: "deferred",
|
|
1994
|
-
taskId: attempt.taskId,
|
|
1995
|
-
requestId: attempt.requestId,
|
|
1996
|
-
reason: "task-conflict",
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
if (!ctx.model) {
|
|
2000
|
-
throw new Error("implementation model is unavailable");
|
|
2001
|
-
}
|
|
2002
|
-
const identity = existingIdentity ?? workerIdentity(ctx.model);
|
|
2003
|
-
let record: TaskRecord;
|
|
2004
|
-
if (!existing) {
|
|
2005
|
-
const endpoint = resolveSubagentEndpoint("implementation-worker", {
|
|
2006
|
-
cwd: ctx.cwd,
|
|
2007
|
-
});
|
|
2008
|
-
if (endpoint.kind === "invalid") {
|
|
2009
|
-
const failure = {
|
|
2010
|
-
kind: "environment",
|
|
2011
|
-
code: "invalid-subagent-endpoint",
|
|
2012
|
-
message: describeInvalidSubagentEndpoint(endpoint),
|
|
2013
|
-
} as const;
|
|
2014
|
-
record = this.registry.open(
|
|
2015
|
-
boundary,
|
|
2016
|
-
identity,
|
|
2017
|
-
workspaceRoot,
|
|
2018
|
-
attempt,
|
|
2019
|
-
);
|
|
2020
|
-
record.state = {
|
|
2021
|
-
kind: "blocked",
|
|
2022
|
-
phase: attempt.phase,
|
|
2023
|
-
failure,
|
|
2024
|
-
};
|
|
2025
|
-
return blockedOutcome(
|
|
2026
|
-
{
|
|
2027
|
-
taskId: attempt.taskId,
|
|
2028
|
-
requestId: attempt.requestId,
|
|
2029
|
-
phase: attempt.phase,
|
|
2030
|
-
},
|
|
2031
|
-
failure,
|
|
2032
|
-
);
|
|
2033
|
-
}
|
|
2034
|
-
const subagentEndpoint: Readonly<SubagentEndpoint> | null =
|
|
2035
|
-
endpoint.kind === "custom" ? endpoint.endpoint : null;
|
|
2036
|
-
record = this.registry.open(
|
|
2037
|
-
boundary,
|
|
2038
|
-
identity,
|
|
2039
|
-
workspaceRoot,
|
|
2040
|
-
attempt,
|
|
2041
|
-
subagentEndpoint,
|
|
2042
|
-
);
|
|
2043
|
-
} else {
|
|
2044
|
-
record = existing;
|
|
2045
|
-
}
|
|
2046
|
-
if (signal?.aborted) {
|
|
2047
|
-
return cancelledOutcome({
|
|
2048
|
-
taskId: record.boundary.taskId,
|
|
2049
|
-
requestId: attempt.requestId,
|
|
2050
|
-
phase: taskPhase(record),
|
|
2051
|
-
});
|
|
2052
|
-
}
|
|
2053
|
-
envelope = deriveImplementEnvelope(record, attempt);
|
|
2054
|
-
this.taskRecords.set(envelope, record);
|
|
2055
|
-
} else {
|
|
2056
|
-
if (signal?.aborted) return cancellationError(signal);
|
|
2057
|
-
envelope = cloneNonImplementEnvelope(validation.value);
|
|
2058
|
-
}
|
|
2059
|
-
const context: StoredRunContext = {
|
|
2060
|
-
ctx,
|
|
2061
|
-
observer,
|
|
2062
|
-
sequence: ++this.batchSeq,
|
|
2063
|
-
runningEmitted: false,
|
|
2064
|
-
terminalEmitted: false,
|
|
2065
|
-
usage: new UsageAggregator(),
|
|
2066
|
-
};
|
|
2067
|
-
this.runContexts.set(envelope, context);
|
|
2068
|
-
this.notify(envelope, context, "queued");
|
|
2069
|
-
const batchId = `runtime-${context.sequence}`;
|
|
2070
|
-
const batch = this.scheduler.schedule(batchId, [
|
|
2071
|
-
{ request: envelope, prerequisites: [] },
|
|
2072
|
-
]);
|
|
2073
|
-
const cancelBatch = () =>
|
|
2074
|
-
this.scheduler.cancel(
|
|
2075
|
-
batchId,
|
|
2076
|
-
signal?.reason ?? new Error("tool call cancelled"),
|
|
2077
|
-
);
|
|
2078
|
-
if (signal?.aborted) cancelBatch();
|
|
2079
|
-
else signal?.addEventListener("abort", cancelBatch, { once: true });
|
|
2080
|
-
try {
|
|
2081
|
-
const outcome = await batch.result(envelope.id);
|
|
2082
|
-
if (outcome.status === "succeeded" && outcome.value) {
|
|
2083
|
-
const internalResult = outcome.value.dispatch;
|
|
2084
|
-
const {
|
|
2085
|
-
failureKind,
|
|
2086
|
-
failureClass: _failureClass,
|
|
2087
|
-
launchConsumed: _launchConsumed,
|
|
2088
|
-
...publicResult
|
|
2089
|
-
} = internalResult;
|
|
2090
|
-
const dispatch = publicResult as DispatchResult;
|
|
2091
|
-
if (isWrappedDispatchResult(dispatch)) {
|
|
2092
|
-
if (!dispatch.ok) {
|
|
2093
|
-
const state: RuntimeActivityState = failureKind ?? "failed";
|
|
2094
|
-
this.notify(envelope, context, state);
|
|
2095
|
-
return dispatch;
|
|
2096
|
-
}
|
|
2097
|
-
this.notify(envelope, context, "completed");
|
|
2098
|
-
return dispatch;
|
|
2099
|
-
}
|
|
2100
|
-
this.notify(
|
|
2101
|
-
envelope,
|
|
2102
|
-
context,
|
|
2103
|
-
dispatch.kind === "cancelled" ? "cancelled" : "completed",
|
|
2104
|
-
);
|
|
2105
|
-
return dispatch;
|
|
2106
|
-
}
|
|
2107
|
-
const state: RuntimeActivityState =
|
|
2108
|
-
outcome.status === "cancelled" ? "cancelled" : "failed";
|
|
2109
|
-
const task = this.taskRecords.get(envelope);
|
|
2110
|
-
if (task) {
|
|
2111
|
-
this.notify(envelope, context, state);
|
|
2112
|
-
if (outcome.status === "cancelled") {
|
|
2113
|
-
return {
|
|
2114
|
-
...cancelledOutcome({
|
|
2115
|
-
taskId: task.boundary.taskId,
|
|
2116
|
-
requestId: envelope.id,
|
|
2117
|
-
phase: taskPhase(task),
|
|
2118
|
-
}),
|
|
2119
|
-
...this.usageMetadata(context),
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
throw new Error(outcome.error ?? `scheduled run ${outcome.status}`);
|
|
2123
|
-
}
|
|
2124
|
-
const terminal = {
|
|
2125
|
-
ok: false as const,
|
|
2126
|
-
error: outcome.error ?? `scheduled run ${outcome.status}`,
|
|
2127
|
-
...this.usageMetadata(context),
|
|
2128
|
-
};
|
|
2129
|
-
this.notify(envelope, context, state);
|
|
2130
|
-
return terminal;
|
|
2131
|
-
} finally {
|
|
2132
|
-
signal?.removeEventListener("abort", cancelBatch);
|
|
2133
|
-
this.runContexts.delete(envelope);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
private notify(
|
|
2138
|
-
envelope: RequestEnvelope,
|
|
2139
|
-
context: StoredRunContext,
|
|
2140
|
-
state: RuntimeActivityState,
|
|
2141
|
-
): void {
|
|
2142
|
-
if (!context.observer || context.terminalEmitted) return;
|
|
2143
|
-
if (state === "running") {
|
|
2144
|
-
if (context.runningEmitted) return;
|
|
2145
|
-
context.runningEmitted = true;
|
|
2146
|
-
}
|
|
2147
|
-
if (
|
|
2148
|
-
state === "completed" ||
|
|
2149
|
-
state === "failed" ||
|
|
2150
|
-
state === "cancelled" ||
|
|
2151
|
-
state === "timed-out"
|
|
2152
|
-
) {
|
|
2153
|
-
context.terminalEmitted = true;
|
|
2154
|
-
}
|
|
2155
|
-
const event: RuntimeActivityEvent = {
|
|
2156
|
-
state,
|
|
2157
|
-
requestId: envelope.id,
|
|
2158
|
-
role: envelope.role,
|
|
2159
|
-
phase: envelope.phase,
|
|
2160
|
-
objective: envelope.objective,
|
|
2161
|
-
sequence: context.sequence,
|
|
2162
|
-
...(state === "cancelled"
|
|
2163
|
-
? { failureReason: "subagent cancelled" as const }
|
|
2164
|
-
: state === "timed-out"
|
|
2165
|
-
? { failureReason: "phase timed out" as const }
|
|
2166
|
-
: state === "failed"
|
|
2167
|
-
? { failureReason: "subagent failed" as const }
|
|
2168
|
-
: {}),
|
|
2169
|
-
};
|
|
2170
|
-
try {
|
|
2171
|
-
const result = context.observer(event);
|
|
2172
|
-
if (result instanceof Promise) void result.catch(() => undefined);
|
|
2173
|
-
} catch {
|
|
2174
|
-
// Presentation observers cannot affect orchestration.
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
private async runScheduled(
|
|
2179
|
-
envelope: RequestEnvelope,
|
|
2180
|
-
context: StoredRunContext,
|
|
2181
|
-
signal: AbortSignal,
|
|
2182
|
-
): Promise<InternalDispatchResult> {
|
|
2183
|
-
const ctx = context.ctx;
|
|
2184
|
-
const task = this.taskRecords.get(envelope);
|
|
2185
|
-
if (!task && envelope.stage === "abel-implement") {
|
|
2186
|
-
throw new Error("implementation TaskRecord is unavailable");
|
|
2187
|
-
}
|
|
2188
|
-
if (signal.aborted) {
|
|
2189
|
-
if (task) {
|
|
2190
|
-
return cancelledOutcome({
|
|
2191
|
-
taskId: task.boundary.taskId,
|
|
2192
|
-
requestId: envelope.id,
|
|
2193
|
-
phase: taskPhase(task),
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
return { ...cancellationError(signal), failureKind: "cancelled" };
|
|
2197
|
-
}
|
|
2198
|
-
const agent = loadAgentDefinitions().find(
|
|
2199
|
-
(item) => item.role === envelope.role,
|
|
2200
|
-
);
|
|
2201
|
-
if (!agent) {
|
|
2202
|
-
if (task) throw new Error("implementation agent definition is missing");
|
|
2203
|
-
return {
|
|
2204
|
-
ok: false,
|
|
2205
|
-
error: "agent definition is unavailable",
|
|
2206
|
-
failure: { kind: "environment", code: "root-unavailable" },
|
|
2207
|
-
failureKind: "failed",
|
|
2208
|
-
failureClass: "environment",
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
if (!ctx.model) {
|
|
2212
|
-
if (task) throw new Error("implementation model is unavailable");
|
|
2213
|
-
return {
|
|
2214
|
-
ok: false,
|
|
2215
|
-
error: "phase runtime is unavailable",
|
|
2216
|
-
failure: {
|
|
2217
|
-
kind: "environment",
|
|
2218
|
-
code: "sandbox-runtime-unavailable",
|
|
2219
|
-
},
|
|
2220
|
-
failureKind: "failed",
|
|
2221
|
-
failureClass: "environment",
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
|
-
if (!task) {
|
|
2225
|
-
const first = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
2226
|
-
this.captureUsage(context, "launch:0", first.usage);
|
|
2227
|
-
const retryInvalidDesignResult =
|
|
2228
|
-
!signal.aborted &&
|
|
2229
|
-
envelope.stage === "abel-design" &&
|
|
2230
|
-
!first.ok &&
|
|
2231
|
-
first.failure?.kind === "artifact" &&
|
|
2232
|
-
(first.failure.code === "invalid-structural-result" ||
|
|
2233
|
-
first.failure.code === "child-no-structural-submit");
|
|
2234
|
-
if (!retryInvalidDesignResult) return this.withUsage(context, first);
|
|
2235
|
-
|
|
2236
|
-
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
2237
|
-
this.captureUsage(context, "launch:1", second.usage);
|
|
2238
|
-
return this.withUsage(context, second);
|
|
2239
|
-
}
|
|
2240
|
-
if (task.workerIdentity !== workerIdentity(ctx.model)) {
|
|
2241
|
-
throw new Error("provider/model identity mismatch");
|
|
2242
|
-
}
|
|
2243
|
-
if (task.state.kind !== "ready") {
|
|
2244
|
-
throw new Error("implementation task is not launchable");
|
|
2245
|
-
}
|
|
2246
|
-
const phase = task.state.phase;
|
|
2247
|
-
const launchIndex = task.state.launchIndex;
|
|
2248
|
-
const correction = task.state.correction;
|
|
2249
|
-
const artifactRejection =
|
|
2250
|
-
correction?.kind === "artifact"
|
|
2251
|
-
? artifactCorrectionEvidence(correction)
|
|
2252
|
-
: undefined;
|
|
2253
|
-
const identity: OutcomeIdentity = {
|
|
2254
|
-
taskId: task.boundary.taskId,
|
|
2255
|
-
requestId: envelope.id,
|
|
2256
|
-
phase,
|
|
2257
|
-
};
|
|
2258
|
-
const block = (failure: TaskFailure): InternalDispatchResult => {
|
|
2259
|
-
task.state = {
|
|
2260
|
-
kind: "blocked",
|
|
2261
|
-
phase,
|
|
2262
|
-
failure: structuredClone(failure),
|
|
2263
|
-
};
|
|
2264
|
-
return blockedOutcome(identity, failure);
|
|
2265
|
-
};
|
|
2266
|
-
const candidate = (
|
|
2267
|
-
result: Extract<ChildDispatchResult, { ok: true }>,
|
|
2268
|
-
): InternalDispatchResult => {
|
|
2269
|
-
if (typeof result.resultId !== "string" || result.result === undefined) {
|
|
2270
|
-
throw new Error("implementation candidate result is incomplete");
|
|
2271
|
-
}
|
|
2272
|
-
return {
|
|
2273
|
-
kind: "candidate",
|
|
2274
|
-
...identity,
|
|
2275
|
-
resultId: result.resultId,
|
|
2276
|
-
result: result.result as DiffResult,
|
|
2277
|
-
};
|
|
2278
|
-
};
|
|
2279
|
-
const finish = (result: InternalDispatchResult) =>
|
|
2280
|
-
this.withUsage(context, result);
|
|
2281
|
-
|
|
2282
|
-
const first = await this.dispatchChild(
|
|
2283
|
-
agent,
|
|
2284
|
-
envelope,
|
|
2285
|
-
ctx,
|
|
2286
|
-
signal,
|
|
2287
|
-
artifactRejection,
|
|
2288
|
-
);
|
|
2289
|
-
this.captureUsage(context, "launch:0", first.usage);
|
|
2290
|
-
if (signal.aborted) {
|
|
2291
|
-
if (first.ok && typeof first.resultId === "string") {
|
|
2292
|
-
this.results.discard(first.resultId);
|
|
2293
|
-
}
|
|
2294
|
-
return finish(cancelledOutcome(identity));
|
|
2295
|
-
}
|
|
2296
|
-
if (first.ok) {
|
|
2297
|
-
return finish(candidate(first));
|
|
2298
|
-
}
|
|
2299
|
-
const firstFailure = childFailureOf(first);
|
|
2300
|
-
if (firstFailure.kind === "cancelled") {
|
|
2301
|
-
return finish(cancelledOutcome(identity));
|
|
2302
|
-
}
|
|
2303
|
-
const firstTerminal = terminalTaskFailure(firstFailure);
|
|
2304
|
-
if (firstTerminal) return finish(block(firstTerminal));
|
|
2305
|
-
if (launchIndex === 1) {
|
|
2306
|
-
return finish(block(attemptsExhaustedFailure(firstFailure)));
|
|
2307
|
-
}
|
|
2308
|
-
if (firstFailure.kind === "artifact" || firstFailure.kind === "stale") {
|
|
2309
|
-
task.state = {
|
|
2310
|
-
kind: "ready",
|
|
2311
|
-
phase,
|
|
2312
|
-
launchIndex: 1,
|
|
2313
|
-
correction: structuredClone(firstFailure),
|
|
2314
|
-
};
|
|
2315
|
-
return finish(retryOutcome(identity, "worker", firstFailure));
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
task.state = {
|
|
2319
|
-
kind: "ready",
|
|
2320
|
-
phase,
|
|
2321
|
-
launchIndex: 1,
|
|
2322
|
-
};
|
|
2323
|
-
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
2324
|
-
this.captureUsage(context, "launch:1", second.usage);
|
|
2325
|
-
if (signal.aborted) {
|
|
2326
|
-
if (second.ok && typeof second.resultId === "string") {
|
|
2327
|
-
this.results.discard(second.resultId);
|
|
2328
|
-
}
|
|
2329
|
-
return finish(cancelledOutcome(identity));
|
|
2330
|
-
}
|
|
2331
|
-
if (second.ok) {
|
|
2332
|
-
return finish(candidate(second));
|
|
2333
|
-
}
|
|
2334
|
-
const secondFailure = childFailureOf(second);
|
|
2335
|
-
if (secondFailure.kind === "cancelled") {
|
|
2336
|
-
return finish(cancelledOutcome(identity));
|
|
2337
|
-
}
|
|
2338
|
-
const secondTerminal = terminalTaskFailure(secondFailure);
|
|
2339
|
-
if (secondTerminal) return finish(block(secondTerminal));
|
|
2340
|
-
return finish(block(attemptsExhaustedFailure(secondFailure)));
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
private captureUsage(
|
|
2344
|
-
context: StoredRunContext,
|
|
2345
|
-
id: string,
|
|
2346
|
-
usage: Usage | undefined,
|
|
2347
|
-
): void {
|
|
2348
|
-
if (usage) context.usage.add(id, usage);
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
private usageMetadata(context: StoredRunContext): { usage?: Usage } {
|
|
2352
|
-
return context.usage.hasUsage() ? { usage: context.usage.total() } : {};
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
private withUsage<T extends InternalDispatchResult>(
|
|
2356
|
-
context: StoredRunContext,
|
|
2357
|
-
result: T,
|
|
2358
|
-
): T {
|
|
2359
|
-
return {
|
|
2360
|
-
...result,
|
|
2361
|
-
...this.usageMetadata(context),
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
private rememberRetainedResult(
|
|
2366
|
-
result: Extract<ImplementOutcome, { kind: "candidate" }>,
|
|
2367
|
-
envelope: RequestEnvelope,
|
|
2368
|
-
task: TaskRecord,
|
|
2369
|
-
): void {
|
|
2370
|
-
if (task.state.kind !== "ready") {
|
|
2371
|
-
throw new Error("retained candidate identity mismatch");
|
|
2372
|
-
}
|
|
2373
|
-
const phaseState = task.state;
|
|
2374
|
-
if (
|
|
2375
|
-
result.requestId !== envelope.id ||
|
|
2376
|
-
result.taskId !== task.boundary.taskId ||
|
|
2377
|
-
result.phase !== phaseState.phase
|
|
2378
|
-
) {
|
|
2379
|
-
throw new Error("retained candidate identity mismatch");
|
|
2380
|
-
}
|
|
2381
|
-
const phase = task.boundary.phases[phaseState.phase];
|
|
2382
|
-
if (!phase || !isSafeBound(envelope.snapshot)) {
|
|
2383
|
-
throw new Error("retained candidate identity mismatch");
|
|
2384
|
-
}
|
|
2385
|
-
const identity: RetainedCandidateIdentity = {
|
|
2386
|
-
stage: "abel-implement",
|
|
2387
|
-
canonicalRoot: task.workspaceRoot,
|
|
2388
|
-
root: task.workspaceRoot,
|
|
2389
|
-
changeId: task.boundary.changeId,
|
|
2390
|
-
taskId: task.boundary.taskId,
|
|
2391
|
-
originRequestId: envelope.id,
|
|
2392
|
-
phase: phaseState.phase,
|
|
2393
|
-
launchIndex: phaseState.launchIndex,
|
|
2394
|
-
};
|
|
2395
|
-
if (!this.results.get(result.resultId)) {
|
|
2396
|
-
throw new Error("retained candidate is unavailable");
|
|
2397
|
-
}
|
|
2398
|
-
this.results.bindIdentity(result.resultId, identity);
|
|
2399
|
-
this.results.resolveIdentity(result.resultId, {
|
|
2400
|
-
...identity,
|
|
2401
|
-
writeSet: [...phase.write],
|
|
2402
|
-
approvedDependencies: [...task.boundary.approvedDependencies],
|
|
2403
|
-
snapshot: envelope.snapshot,
|
|
2404
|
-
});
|
|
2405
|
-
task.state = {
|
|
2406
|
-
kind: "candidate-pending",
|
|
2407
|
-
phase: phaseState.phase,
|
|
2408
|
-
launchIndex: phaseState.launchIndex,
|
|
2409
|
-
originRequestId: envelope.id,
|
|
2410
|
-
resultId: result.resultId,
|
|
2411
|
-
};
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
validateRequest(
|
|
2415
|
-
envelope: unknown,
|
|
2416
|
-
): { ok: true; value: RunRequest } | { ok: false; reason: string } {
|
|
2417
|
-
return validateRequestEnvelope(envelope);
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
async drain(): Promise<void> {
|
|
2421
|
-
this.activation.drain();
|
|
2422
|
-
const scheduled = this.scheduler.cancelAll();
|
|
2423
|
-
const applied = this.applyTail;
|
|
2424
|
-
await Promise.all([scheduled, applied]);
|
|
2425
|
-
this.parentPayloadBridge?.clear();
|
|
2426
|
-
drainStage({
|
|
2427
|
-
results: this.results,
|
|
2428
|
-
registry: this.registry,
|
|
2429
|
-
activation: this.activation,
|
|
2430
|
-
});
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
get state(): ActivationState {
|
|
2434
|
-
return this.activation.state;
|
|
2435
|
-
}
|
|
2436
|
-
}
|