@abelxiaoxing/cadence 1.4.0 → 1.5.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.
Files changed (66) hide show
  1. package/README.md +130 -8
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/config/plan-draft.quick.example.json +78 -0
  8. package/package.json +10 -4
  9. package/prompts/abel-design.md +101 -44
  10. package/prompts/abel-implement.md +9 -0
  11. package/src/activity-contracts.ts +82 -0
  12. package/src/candidate-artifact.ts +143 -0
  13. package/src/candidate-patch.ts +31 -11
  14. package/src/candidate-workspace.ts +74 -0
  15. package/src/change-contract-schema.ts +191 -0
  16. package/src/change-contract.ts +5 -0
  17. package/src/change-verification.ts +920 -0
  18. package/src/child-budget.ts +58 -0
  19. package/src/child-model.ts +142 -0
  20. package/src/child-session.ts +289 -326
  21. package/src/contracts.ts +200 -76
  22. package/src/delivery-compiler.ts +105 -120
  23. package/src/delivery-revision.ts +42 -0
  24. package/src/design-control.ts +113 -4
  25. package/src/design-diagnostics.ts +150 -0
  26. package/src/durable-contracts.ts +213 -0
  27. package/src/durable-workflow.ts +148 -3570
  28. package/src/evidence-draft.ts +122 -0
  29. package/src/execution-profile.ts +38 -0
  30. package/src/implement-graph.ts +3 -0
  31. package/src/implement-plan.ts +70 -0
  32. package/src/index.ts +59 -522
  33. package/src/isolation-backend.ts +62 -40
  34. package/src/model-source.ts +22 -0
  35. package/src/operator-cli-entry.mjs +24 -0
  36. package/src/operator-cli.mjs +1421 -0
  37. package/src/operator-tools.ts +249 -0
  38. package/src/package-candidate.ts +227 -0
  39. package/src/package-verification.ts +86 -8
  40. package/src/package-workflow.ts +291 -0
  41. package/src/packet-runtime.ts +27 -48
  42. package/src/parent-provider.ts +86 -198
  43. package/src/phase-execution.ts +2248 -0
  44. package/src/pi-adapter.ts +19 -0
  45. package/src/plan-draft-summary.ts +149 -0
  46. package/src/plan-draft.ts +493 -0
  47. package/src/single-task-draft.ts +142 -0
  48. package/src/subagent-activity.ts +32 -66
  49. package/src/submit-tool.ts +20 -59
  50. package/src/transport-budget.ts +52 -0
  51. package/src/verification-capability.ts +5 -3
  52. package/src/verification-diagnostics.ts +76 -0
  53. package/src/verification-environment-io.ts +280 -0
  54. package/src/verification-environment.ts +56 -154
  55. package/src/verification-scan.ts +64 -0
  56. package/src/worker-broker.ts +39 -57
  57. package/src/workflow-policy.ts +14 -53
  58. package/src/workflow-recovery-policy.ts +95 -0
  59. package/src/workflow-scheduling.ts +105 -0
  60. package/src/workflow-state-machine.ts +105 -497
  61. package/src/workflow-status.ts +384 -0
  62. package/src/workspace-io-entry.mjs +19 -1
  63. package/src/workspace-io-worker.mjs +275 -50
  64. package/src/workspace-io.ts +6 -1
  65. package/src/empty-resource-loader.ts +0 -38
  66. package/src/parent-payload-bridge.ts +0 -523
@@ -5,11 +5,9 @@ import {
5
5
  type WorkerRole,
6
6
  type WorkerRoutePolicy,
7
7
  } from "./route-policy.ts";
8
+ import { TransportTimeout } from "./transport-budget.ts";
8
9
 
9
10
  export const ROUTE_ATTEMPT_BOUNDS = Object.freeze({
10
- connectMs: 10_000,
11
- firstResponseMs: 90_000,
12
- idleMs: 3 * 60_000,
13
11
  totalMs: 20 * 60_000,
14
12
  cooldownMs: 30_000,
15
13
  attemptsPerRoute: 2,
@@ -71,6 +69,7 @@ export interface RunWorkerAttemptInput<T> {
71
69
  export interface RouteAttemptContext {
72
70
  route: WorkerRoutePolicy;
73
71
  signal: AbortSignal;
72
+ onRequestStart(): void;
74
73
  onHeaders(): void;
75
74
  onProgress(): void;
76
75
  }
@@ -81,7 +80,7 @@ export interface BrokerAttemptEvidence {
81
80
  }
82
81
 
83
82
  export interface BrokerActivityUpdate {
84
- state: "connecting" | "waiting-first-response" | "running" | "retrying";
83
+ state: "preparing" | "waiting-first-response" | "running" | "retrying";
85
84
  attempt: number;
86
85
  maxAttempts: number;
87
86
  code?: string;
@@ -100,12 +99,7 @@ function emitBrokerActivity(
100
99
  }
101
100
 
102
101
  class RouteAttemptStop extends Error {
103
- readonly code:
104
- | "connect-timeout"
105
- | "first-response-timeout"
106
- | "idle-timeout"
107
- | "phase-timeout"
108
- | "cancelled";
102
+ readonly code: "cancelled";
109
103
 
110
104
  constructor(code: RouteAttemptStop["code"]) {
111
105
  super(code);
@@ -182,7 +176,10 @@ function capable(
182
176
  }
183
177
 
184
178
  function safeFailureCode(error: unknown): string {
185
- if (error instanceof RouteAttemptStop) return error.code;
179
+ if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
180
+ return error.code;
181
+ if (error instanceof Error && error.message === "child-provider-rate-limited")
182
+ return error.message;
186
183
  return "transport-failure";
187
184
  }
188
185
 
@@ -331,59 +328,39 @@ export class WorkerBroker {
331
328
  onActivity?: (state: "waiting-first-response" | "running") => void,
332
329
  ): Promise<T> {
333
330
  const controller = new AbortController();
334
- const timers = new Set<ReturnType<typeof setTimeout>>();
335
- let headersSeen = false;
336
- let connectTimer: ReturnType<typeof setTimeout> | undefined;
337
- let firstResponseTimer: ReturnType<typeof setTimeout> | undefined;
338
- let idleTimer: ReturnType<typeof setTimeout> | undefined;
339
-
340
- const stop = (code: RouteAttemptStop["code"]) => {
341
- if (!controller.signal.aborted)
342
- controller.abort(new RouteAttemptStop(code));
343
- };
344
- const schedule = (code: RouteAttemptStop["code"], milliseconds: number) => {
345
- const timer = setTimeout(() => stop(code), milliseconds);
346
- timers.add(timer);
347
- return timer;
348
- };
349
- const clear = (timer: ReturnType<typeof setTimeout> | undefined) => {
350
- if (timer !== undefined) {
351
- clearTimeout(timer);
352
- timers.delete(timer);
353
- }
354
- };
355
- const onParentAbort = () => stop("cancelled");
356
- if (parentSignal?.aborted) stop("cancelled");
331
+ let accepting = true;
332
+ let running = false;
333
+ const onParentAbort = () =>
334
+ controller.abort(new RouteAttemptStop("cancelled"));
335
+ if (parentSignal?.aborted) onParentAbort();
357
336
  else parentSignal?.addEventListener("abort", onParentAbort, { once: true });
358
-
359
- connectTimer = schedule("connect-timeout", ROUTE_ATTEMPT_BOUNDS.connectMs);
360
- schedule("phase-timeout", ROUTE_ATTEMPT_BOUNDS.totalMs);
337
+ const timer = setTimeout(
338
+ () => controller.abort(new TransportTimeout("attempt-timeout")),
339
+ ROUTE_ATTEMPT_BOUNDS.totalMs,
340
+ );
361
341
  let removeAbortRace: () => void = () => {};
362
342
  try {
363
- const execution = Promise.resolve().then(() =>
364
- execute({
343
+ const execution = Promise.resolve().then(() => {
344
+ controller.signal.throwIfAborted();
345
+ return execute({
365
346
  route: structuredClone(route),
366
347
  signal: controller.signal,
348
+ onRequestStart: () => {
349
+ if (!accepting || controller.signal.aborted) return;
350
+ running = false;
351
+ onActivity?.("waiting-first-response");
352
+ },
367
353
  onHeaders: () => {
368
- if (controller.signal.aborted || headersSeen) return;
369
- headersSeen = true;
370
- clear(connectTimer);
354
+ if (!accepting || controller.signal.aborted || running) return;
371
355
  onActivity?.("waiting-first-response");
372
- firstResponseTimer = schedule(
373
- "first-response-timeout",
374
- ROUTE_ATTEMPT_BOUNDS.firstResponseMs,
375
- );
376
356
  },
377
357
  onProgress: () => {
378
- if (controller.signal.aborted) return;
379
- clear(connectTimer);
380
- clear(firstResponseTimer);
381
- clear(idleTimer);
382
- idleTimer = schedule("idle-timeout", ROUTE_ATTEMPT_BOUNDS.idleMs);
358
+ if (!accepting || controller.signal.aborted) return;
359
+ running = true;
383
360
  onActivity?.("running");
384
361
  },
385
- }),
386
- );
362
+ });
363
+ });
387
364
  // A timed-out Provider may ignore its AbortSignal and settle later. The
388
365
  // broker owns the bounded result, so absorb that late settlement.
389
366
  void execution.catch(() => undefined);
@@ -403,7 +380,8 @@ export class WorkerBroker {
403
380
  return await Promise.race([execution, aborted]);
404
381
  } finally {
405
382
  removeAbortRace();
406
- for (const timer of timers) clearTimeout(timer);
383
+ accepting = false;
384
+ clearTimeout(timer);
407
385
  parentSignal?.removeEventListener("abort", onParentAbort);
408
386
  }
409
387
  }
@@ -483,10 +461,10 @@ export class WorkerBroker {
483
461
  ).length;
484
462
  const attempt = evidence.length + 1;
485
463
  emitBrokerActivity(input.onActivity, {
486
- state: "connecting",
464
+ state: "preparing",
487
465
  attempt,
488
466
  maxAttempts,
489
- wait: "connection",
467
+ wait: "worker-setup",
490
468
  });
491
469
  try {
492
470
  const value = await this.#attempt(
@@ -576,6 +554,7 @@ export class WorkerBroker {
576
554
  code:
577
555
  | "approval-boundary"
578
556
  | "route-not-declared"
557
+ | "endpoint-unavailable"
579
558
  | "route-capability-insufficient";
580
559
  } {
581
560
  const allowed = new Set(["runId", "role", "routeId", "requirements"]);
@@ -597,12 +576,15 @@ export class WorkerBroker {
597
576
  routeId: input.routeId,
598
577
  });
599
578
  if (!selection.ok) {
579
+ const route = this.#policy.routes[input.routeId];
600
580
  return {
601
581
  ok: false,
602
582
  code:
603
583
  selection.code === "route-not-declared"
604
584
  ? "route-not-declared"
605
- : "route-capability-insufficient",
585
+ : route && capable(route, input.role, input.requirements ?? {})
586
+ ? "endpoint-unavailable"
587
+ : "route-capability-insufficient",
606
588
  };
607
589
  }
608
590
  return {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
4
5
  import { compareCanonicalStrings } from "./canonical.ts";
5
6
  import {
6
7
  type ApprovalBoundaryCode,
@@ -18,7 +19,6 @@ import type { RoutePolicy } from "./route-policy.ts";
18
19
  import { canonicalJson } from "./run-state.ts";
19
20
  import type { OperationLease } from "./run-store.ts";
20
21
  import type { ResolvedStateRoot } from "./state-root.ts";
21
- import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
22
22
  import {
23
23
  type CandidateContextBoundary,
24
24
  type CandidateContextRef,
@@ -185,59 +185,20 @@ export interface RedArtifactCorrection {
185
185
  contextRequest?: WorkflowContextRequest;
186
186
  }
187
187
 
188
- export type RecoveryActionRequest = {
189
- kind: "affected-repair" | "cumulative-repair" | "red-correction";
190
- attempt: number;
191
- };
192
- export type RecoveryActionDecision =
193
- | { allowed: true }
194
- | {
195
- allowed: false;
196
- code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
197
- };
188
+ export type {
189
+ RecoveryActionDecision,
190
+ RecoveryActionRequest,
191
+ WorkflowRecoveryFact,
192
+ WorkflowRecoveryFeedback,
193
+ } from "./workflow-recovery-policy.ts";
194
+ export { decideRecoveryAction } from "./workflow-recovery-policy.ts";
198
195
 
199
- /** One policy for every nested recovery action; execution services supply facts only. */
200
- export function decideRecoveryAction(
201
- plan: ImplementPlan,
202
- request: RecoveryActionRequest,
203
- authority: { additionalAttempt?: boolean; verificationOnly?: boolean } = {},
204
- ): RecoveryActionDecision {
205
- const correction = request.kind === "red-correction";
206
- const limit =
207
- authority.additionalAttempt || authority.verificationOnly
208
- ? 1
209
- : correction
210
- ? plan.verification.artifactCorrection.maxAttempts
211
- : plan.verification.repair.maxAttempts;
212
- return Number.isSafeInteger(request.attempt) &&
213
- request.attempt > 0 &&
214
- request.attempt <= limit
215
- ? { allowed: true }
216
- : {
217
- allowed: false,
218
- code: correction
219
- ? "artifact-attempts-exhausted"
220
- : "repair-attempts-exhausted",
221
- };
222
- }
223
-
224
- export interface WorkflowRecoveryFeedback {
225
- code: string;
226
- attempt: number;
227
- maxAttempts: number;
228
- strategy:
229
- | "revise-candidate"
230
- | "refresh-candidate"
231
- | "repair-verification"
232
- | "compact-patch";
233
- failureIdentities?: string[];
234
- }
235
-
236
- export interface WorkflowRecoveryFact {
237
- key: string;
238
- failures: number;
239
- feedback: WorkflowRecoveryFeedback;
240
- }
196
+ import type {
197
+ RecoveryActionDecision,
198
+ RecoveryActionRequest,
199
+ WorkflowRecoveryFact,
200
+ WorkflowRecoveryFeedback,
201
+ } from "./workflow-recovery-policy.ts";
241
202
 
242
203
  export const APPROVAL_AUTHORITY_CATEGORIES = [
243
204
  "observable-behavior",
@@ -0,0 +1,95 @@
1
+ import type { ImplementPlan } from "./implement-plan.ts";
2
+
3
+ export type RecoveryActionRequest = {
4
+ kind: "affected-repair" | "cumulative-repair" | "red-correction";
5
+ attempt: number;
6
+ };
7
+ export type RecoveryActionDecision =
8
+ | { allowed: true }
9
+ | {
10
+ allowed: false;
11
+ code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
12
+ };
13
+
14
+ /** One policy for every nested recovery action; execution services supply facts only. */
15
+ export function decideRecoveryAction(
16
+ plan: ImplementPlan,
17
+ request: RecoveryActionRequest,
18
+ authority: { additionalAttempt?: boolean; verificationOnly?: boolean } = {},
19
+ ): RecoveryActionDecision {
20
+ const correction = request.kind === "red-correction";
21
+ const limit =
22
+ authority.additionalAttempt || authority.verificationOnly
23
+ ? 1
24
+ : correction
25
+ ? plan.verification.artifactCorrection.maxAttempts
26
+ : plan.verification.repair.maxAttempts;
27
+ return Number.isSafeInteger(request.attempt) &&
28
+ request.attempt > 0 &&
29
+ request.attempt <= limit
30
+ ? { allowed: true }
31
+ : {
32
+ allowed: false,
33
+ code: correction
34
+ ? "artifact-attempts-exhausted"
35
+ : "repair-attempts-exhausted",
36
+ };
37
+ }
38
+
39
+ export interface WorkflowRecoveryFeedback {
40
+ code: string;
41
+ attempt: number;
42
+ maxAttempts: number;
43
+ strategy:
44
+ | "revise-candidate"
45
+ | "refresh-candidate"
46
+ | "repair-verification"
47
+ | "compact-patch";
48
+ failureIdentities?: string[];
49
+ }
50
+
51
+ export interface WorkflowRecoveryFact {
52
+ key: string;
53
+ failures: number;
54
+ feedback: WorkflowRecoveryFeedback;
55
+ }
56
+
57
+ export function recoveryExhausted(
58
+ incident: WorkflowRecoveryFact | undefined,
59
+ maximum: number,
60
+ granted: boolean,
61
+ ): boolean {
62
+ return !granted && incident !== undefined && incident.failures >= maximum;
63
+ }
64
+
65
+ /** A grant is a recommendation until the state machine validates and records it in its lease transaction. */
66
+ export function assessRecoveryGrant(input: {
67
+ current: boolean;
68
+ incident?: WorkflowRecoveryFact;
69
+ failureSequence: number;
70
+ requestedSequence: number;
71
+ reason: string;
72
+ previous: { route?: string | null; context?: string | null };
73
+ observed: { route?: string | null; context?: string | null };
74
+ budget?: { used: number; max_work: number };
75
+ }): string | undefined {
76
+ if (
77
+ !input.current ||
78
+ !input.incident ||
79
+ input.incident.failures < input.incident.feedback.maxAttempts ||
80
+ input.failureSequence !== input.requestedSequence
81
+ )
82
+ return "recovery-request-stale";
83
+ if (
84
+ (input.reason === "route-changed" &&
85
+ (!input.previous.route ||
86
+ input.previous.route === input.observed.route)) ||
87
+ (input.reason === "context-extended" &&
88
+ (!input.previous.context ||
89
+ input.previous.context === input.observed.context))
90
+ )
91
+ return "recovery-evidence-unavailable";
92
+ if (!input.budget || input.budget.used >= input.budget.max_work)
93
+ return "change-work-budget-exhausted";
94
+ return undefined;
95
+ }
@@ -0,0 +1,105 @@
1
+ import type { PlanTaskDraft } from "./implement-plan.ts";
2
+ import { type EngineTaskRow, taskConflicts } from "./workflow-policy.ts";
3
+
4
+ function taskPlan(row: EngineTaskRow): PlanTaskDraft {
5
+ return JSON.parse(row.plan_json) as PlanTaskDraft;
6
+ }
7
+ export function hasTaskConflict(
8
+ row: EngineTaskRow,
9
+ rows: EngineTaskRow[],
10
+ selectedTaskIds: ReadonlySet<string>,
11
+ ): boolean {
12
+ const candidate = taskPlan(row);
13
+ return rows.some((other) => {
14
+ if (other.task_id === row.task_id || other.state === "verified") {
15
+ return false;
16
+ }
17
+ if (other.state === "queued") {
18
+ if (row.state !== "queued") return false;
19
+ const candidatePosition = row.queue_position ?? Number.MAX_SAFE_INTEGER;
20
+ const otherPosition = other.queue_position ?? Number.MAX_SAFE_INTEGER;
21
+ if (
22
+ otherPosition > candidatePosition ||
23
+ (otherPosition === candidatePosition &&
24
+ other.task_order > row.task_order)
25
+ ) {
26
+ return false;
27
+ }
28
+ }
29
+ if (other.task_order > row.task_order && other.state === "pending") {
30
+ return false;
31
+ }
32
+ if (other.state === "pending" && !dependenciesVerified(other, rows)) {
33
+ return false;
34
+ }
35
+ const conflict = taskConflicts(candidate, taskPlan(other));
36
+ return (
37
+ conflict.taskLifetime ||
38
+ (conflict.verification &&
39
+ ([
40
+ "pending",
41
+ "phase-ready",
42
+ "phase-running",
43
+ "validating",
44
+ "queued",
45
+ ].includes(other.state) ||
46
+ selectedTaskIds.has(other.task_id)))
47
+ );
48
+ });
49
+ }
50
+
51
+ export function dependenciesVerified(
52
+ row: EngineTaskRow,
53
+ rows: EngineTaskRow[],
54
+ ): boolean {
55
+ const byId = new Map(rows.map((candidate) => [candidate.task_id, candidate]));
56
+ return taskPlan(row).dependsOn.every(
57
+ (dependency) => byId.get(dependency)?.state === "verified",
58
+ );
59
+ }
60
+
61
+ /** Simulate ordered queue decisions without storage access or launch authority. */
62
+ export function selectRunnableTasks(input: {
63
+ rows: readonly EngineTaskRow[];
64
+ attempted: ReadonlySet<string>;
65
+ activeTasks: number;
66
+ capacity: number;
67
+ nextQueuePosition: number;
68
+ }) {
69
+ const rows = input.rows.map((row) => ({ ...row }));
70
+ const runnable: string[] = [];
71
+ const selected = new Set<string>();
72
+ const updates: Array<
73
+ | { kind: "pending"; taskId: string }
74
+ | { kind: "queue"; taskId: string; reason: "conflict" | "capacity" }
75
+ > = [];
76
+ let position = input.nextQueuePosition;
77
+ let capacityBlocked = false;
78
+ for (const row of rows) {
79
+ if (row.state === "verified" || input.attempted.has(row.task_id)) continue;
80
+ if (!dependenciesVerified(row, rows)) {
81
+ if (row.state !== "pending") {
82
+ updates.push({ kind: "pending", taskId: row.task_id });
83
+ row.state = "pending";
84
+ row.pause_code = null;
85
+ row.queue_position = null;
86
+ }
87
+ continue;
88
+ }
89
+ const conflict = hasTaskConflict(row, rows, selected);
90
+ const capacity = input.activeTasks + runnable.length >= input.capacity;
91
+ if (conflict || capacity) {
92
+ const reason = conflict ? "conflict" : "capacity";
93
+ updates.push({ kind: "queue", taskId: row.task_id, reason });
94
+ if (row.state !== "queued" || row.queue_position === null)
95
+ row.queue_position = position++;
96
+ row.state = "queued";
97
+ row.pause_code = reason === "capacity" ? "worker-capacity" : null;
98
+ if (!conflict) capacityBlocked = true;
99
+ continue;
100
+ }
101
+ runnable.push(row.task_id);
102
+ selected.add(row.task_id);
103
+ }
104
+ return { runnable, updates, capacityBlocked };
105
+ }