@abelxiaoxing/cadence 1.3.0 → 1.4.1

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 (76) hide show
  1. package/README.md +172 -13
  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/package.json +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. 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,
@@ -21,6 +19,8 @@ export interface RouteRequirements {
21
19
  dialects?: RouteDialect[];
22
20
  minContextWindow?: number;
23
21
  minOutputTokens?: number;
22
+ preferredContextWindow?: number;
23
+ preferredOutputTokens?: number;
24
24
  }
25
25
 
26
26
  interface RouteHealth {
@@ -56,6 +56,7 @@ export interface WorkerBrokerOptions {
56
56
 
57
57
  export interface RunWorkerAttemptInput<T> {
58
58
  runId: string;
59
+ taskId?: string;
59
60
  operationId: string;
60
61
  role: WorkerRole;
61
62
  requirements?: RouteRequirements;
@@ -68,6 +69,7 @@ export interface RunWorkerAttemptInput<T> {
68
69
  export interface RouteAttemptContext {
69
70
  route: WorkerRoutePolicy;
70
71
  signal: AbortSignal;
72
+ onRequestStart(): void;
71
73
  onHeaders(): void;
72
74
  onProgress(): void;
73
75
  }
@@ -78,7 +80,7 @@ export interface BrokerAttemptEvidence {
78
80
  }
79
81
 
80
82
  export interface BrokerActivityUpdate {
81
- state: "connecting" | "waiting-first-response" | "running" | "retrying";
83
+ state: "preparing" | "waiting-first-response" | "running" | "retrying";
82
84
  attempt: number;
83
85
  maxAttempts: number;
84
86
  code?: string;
@@ -97,12 +99,7 @@ function emitBrokerActivity(
97
99
  }
98
100
 
99
101
  class RouteAttemptStop extends Error {
100
- readonly code:
101
- | "connect-timeout"
102
- | "first-response-timeout"
103
- | "idle-timeout"
104
- | "phase-timeout"
105
- | "cancelled";
102
+ readonly code: "cancelled";
106
103
 
107
104
  constructor(code: RouteAttemptStop["code"]) {
108
105
  super(code);
@@ -126,7 +123,13 @@ function validRequirements(value: unknown): value is RouteRequirements {
126
123
  if (
127
124
  Object.keys(candidate).some(
128
125
  (key) =>
129
- !["dialects", "minContextWindow", "minOutputTokens"].includes(key),
126
+ ![
127
+ "dialects",
128
+ "minContextWindow",
129
+ "minOutputTokens",
130
+ "preferredContextWindow",
131
+ "preferredOutputTokens",
132
+ ].includes(key),
130
133
  )
131
134
  ) {
132
135
  return false;
@@ -147,7 +150,12 @@ function validRequirements(value: unknown): value is RouteRequirements {
147
150
  (candidate.minContextWindow as number) > 0)) &&
148
151
  (candidate.minOutputTokens === undefined ||
149
152
  (Number.isSafeInteger(candidate.minOutputTokens) &&
150
- (candidate.minOutputTokens as number) > 0))
153
+ (candidate.minOutputTokens as number) > 0)) &&
154
+ [candidate.preferredContextWindow, candidate.preferredOutputTokens].every(
155
+ (value) =>
156
+ value === undefined ||
157
+ (Number.isSafeInteger(value) && (value as number) > 0),
158
+ )
151
159
  );
152
160
  }
153
161
 
@@ -168,7 +176,8 @@ function capable(
168
176
  }
169
177
 
170
178
  function safeFailureCode(error: unknown): string {
171
- if (error instanceof RouteAttemptStop) return error.code;
179
+ if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
180
+ return error.code;
172
181
  return "transport-failure";
173
182
  }
174
183
 
@@ -176,7 +185,6 @@ export class WorkerBroker {
176
185
  readonly #policy: RoutePolicy;
177
186
  readonly #now: () => number;
178
187
  readonly #healthStore: RouteHealthStore;
179
- readonly #health = new Map<string, RouteHealth>();
180
188
 
181
189
  constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
182
190
  this.#policy = structuredClone(policy);
@@ -187,7 +195,6 @@ export class WorkerBroker {
187
195
  #routeHealth(routeId: string): RouteHealth {
188
196
  const route = this.#policy.routes[routeId];
189
197
  return (
190
- this.#health.get(routeId) ??
191
198
  (route ? this.#healthStore.get(route.fingerprint) : undefined) ?? {
192
199
  state: "healthy",
193
200
  }
@@ -197,7 +204,6 @@ export class WorkerBroker {
197
204
  #setRouteHealth(routeId: string, health: RouteHealth): void {
198
205
  const route = this.#policy.routes[routeId];
199
206
  if (!route) throw new Error("route-not-declared");
200
- this.#health.set(routeId, structuredClone(health));
201
207
  this.#healthStore.set(route.fingerprint, health);
202
208
  }
203
209
 
@@ -229,14 +235,13 @@ export class WorkerBroker {
229
235
  });
230
236
  }
231
237
 
232
- select(input: {
233
- role: WorkerRole;
234
- dialects?: RouteDialect[];
235
- minContextWindow?: number;
236
- minOutputTokens?: number;
237
- exclude?: readonly string[];
238
- routeId?: string;
239
- }):
238
+ select(
239
+ input: RouteRequirements & {
240
+ role: WorkerRole;
241
+ exclude?: readonly string[];
242
+ routeId?: string;
243
+ },
244
+ ):
240
245
  | { ok: true; route: WorkerRoutePolicy; health: RouteHealthState }
241
246
  | { ok: false; code: "endpoint-unavailable" | "route-not-declared" } {
242
247
  const requirements: RouteRequirements = {
@@ -247,6 +252,12 @@ export class WorkerBroker {
247
252
  ...(input.minOutputTokens === undefined
248
253
  ? {}
249
254
  : { minOutputTokens: input.minOutputTokens }),
255
+ ...(input.preferredContextWindow === undefined
256
+ ? {}
257
+ : { preferredContextWindow: input.preferredContextWindow }),
258
+ ...(input.preferredOutputTokens === undefined
259
+ ? {}
260
+ : { preferredOutputTokens: input.preferredOutputTokens }),
250
261
  };
251
262
  if (!isRole(input.role) || !validRequirements(requirements)) {
252
263
  return { ok: false, code: "endpoint-unavailable" };
@@ -261,7 +272,22 @@ export class WorkerBroker {
261
272
  return { ok: false, code: "route-not-declared" };
262
273
  }
263
274
  const excluded = new Set(input.exclude ?? []);
264
- for (const routeId of candidates) {
275
+ // Stable preference tiers preserve declared order within each tier. Health,
276
+ // hard minima and explicit pins still determine eligibility.
277
+ const preferred = (routeId: string) => {
278
+ const route = this.#policy.routes[routeId];
279
+ return route &&
280
+ route.capabilities.contextWindow >=
281
+ (requirements.preferredContextWindow ?? 0) &&
282
+ route.capabilities.maxTokens >=
283
+ (requirements.preferredOutputTokens ?? 0)
284
+ ? 1
285
+ : 0;
286
+ };
287
+ const ordered = [...candidates].sort(
288
+ (left, right) => preferred(right) - preferred(left),
289
+ );
290
+ for (const routeId of ordered) {
265
291
  if (excluded.has(routeId)) continue;
266
292
  const route = this.#policy.routes[routeId];
267
293
  if (!route || !capable(route, input.role, requirements)) continue;
@@ -300,59 +326,39 @@ export class WorkerBroker {
300
326
  onActivity?: (state: "waiting-first-response" | "running") => void,
301
327
  ): Promise<T> {
302
328
  const controller = new AbortController();
303
- const timers = new Set<ReturnType<typeof setTimeout>>();
304
- let headersSeen = false;
305
- let connectTimer: ReturnType<typeof setTimeout> | undefined;
306
- let firstResponseTimer: ReturnType<typeof setTimeout> | undefined;
307
- let idleTimer: ReturnType<typeof setTimeout> | undefined;
308
-
309
- const stop = (code: RouteAttemptStop["code"]) => {
310
- if (!controller.signal.aborted)
311
- controller.abort(new RouteAttemptStop(code));
312
- };
313
- const schedule = (code: RouteAttemptStop["code"], milliseconds: number) => {
314
- const timer = setTimeout(() => stop(code), milliseconds);
315
- timers.add(timer);
316
- return timer;
317
- };
318
- const clear = (timer: ReturnType<typeof setTimeout> | undefined) => {
319
- if (timer !== undefined) {
320
- clearTimeout(timer);
321
- timers.delete(timer);
322
- }
323
- };
324
- const onParentAbort = () => stop("cancelled");
325
- if (parentSignal?.aborted) stop("cancelled");
329
+ let accepting = true;
330
+ let running = false;
331
+ const onParentAbort = () =>
332
+ controller.abort(new RouteAttemptStop("cancelled"));
333
+ if (parentSignal?.aborted) onParentAbort();
326
334
  else parentSignal?.addEventListener("abort", onParentAbort, { once: true });
327
-
328
- connectTimer = schedule("connect-timeout", ROUTE_ATTEMPT_BOUNDS.connectMs);
329
- schedule("phase-timeout", ROUTE_ATTEMPT_BOUNDS.totalMs);
335
+ const timer = setTimeout(
336
+ () => controller.abort(new TransportTimeout("attempt-timeout")),
337
+ ROUTE_ATTEMPT_BOUNDS.totalMs,
338
+ );
330
339
  let removeAbortRace: () => void = () => {};
331
340
  try {
332
- const execution = Promise.resolve().then(() =>
333
- execute({
341
+ const execution = Promise.resolve().then(() => {
342
+ controller.signal.throwIfAborted();
343
+ return execute({
334
344
  route: structuredClone(route),
335
345
  signal: controller.signal,
346
+ onRequestStart: () => {
347
+ if (!accepting || controller.signal.aborted) return;
348
+ running = false;
349
+ onActivity?.("waiting-first-response");
350
+ },
336
351
  onHeaders: () => {
337
- if (controller.signal.aborted || headersSeen) return;
338
- headersSeen = true;
339
- clear(connectTimer);
352
+ if (!accepting || controller.signal.aborted || running) return;
340
353
  onActivity?.("waiting-first-response");
341
- firstResponseTimer = schedule(
342
- "first-response-timeout",
343
- ROUTE_ATTEMPT_BOUNDS.firstResponseMs,
344
- );
345
354
  },
346
355
  onProgress: () => {
347
- if (controller.signal.aborted) return;
348
- clear(connectTimer);
349
- clear(firstResponseTimer);
350
- clear(idleTimer);
351
- idleTimer = schedule("idle-timeout", ROUTE_ATTEMPT_BOUNDS.idleMs);
356
+ if (!accepting || controller.signal.aborted) return;
357
+ running = true;
352
358
  onActivity?.("running");
353
359
  },
354
- }),
355
- );
360
+ });
361
+ });
356
362
  // A timed-out Provider may ignore its AbortSignal and settle later. The
357
363
  // broker owns the bounded result, so absorb that late settlement.
358
364
  void execution.catch(() => undefined);
@@ -372,7 +378,8 @@ export class WorkerBroker {
372
378
  return await Promise.race([execution, aborted]);
373
379
  } finally {
374
380
  removeAbortRace();
375
- for (const timer of timers) clearTimeout(timer);
381
+ accepting = false;
382
+ clearTimeout(timer);
376
383
  parentSignal?.removeEventListener("abort", onParentAbort);
377
384
  }
378
385
  }
@@ -452,10 +459,10 @@ export class WorkerBroker {
452
459
  ).length;
453
460
  const attempt = evidence.length + 1;
454
461
  emitBrokerActivity(input.onActivity, {
455
- state: "connecting",
462
+ state: "preparing",
456
463
  attempt,
457
464
  maxAttempts,
458
- wait: "connection",
465
+ wait: "worker-setup",
459
466
  });
460
467
  try {
461
468
  const value = await this.#attempt(
@@ -545,6 +552,7 @@ export class WorkerBroker {
545
552
  code:
546
553
  | "approval-boundary"
547
554
  | "route-not-declared"
555
+ | "endpoint-unavailable"
548
556
  | "route-capability-insufficient";
549
557
  } {
550
558
  const allowed = new Set(["runId", "role", "routeId", "requirements"]);
@@ -566,12 +574,15 @@ export class WorkerBroker {
566
574
  routeId: input.routeId,
567
575
  });
568
576
  if (!selection.ok) {
577
+ const route = this.#policy.routes[input.routeId];
569
578
  return {
570
579
  ok: false,
571
580
  code:
572
581
  selection.code === "route-not-declared"
573
582
  ? "route-not-declared"
574
- : "route-capability-insufficient",
583
+ : route && capable(route, input.role, input.requirements ?? {})
584
+ ? "endpoint-unavailable"
585
+ : "route-capability-insufficient",
575
586
  };
576
587
  }
577
588
  return {
@@ -606,7 +617,8 @@ export class WorkerBroker {
606
617
  }
607
618
 
608
619
  /**
609
- * Adds stable run/role route bindings to the stateless route-attempt broker.
620
+ * Adds run/role defaults and independent task bindings to route attempts.
621
+ * All bound brokers consult the same health store.
610
622
  * Attempt credentials and provider objects remain disposable and are never
611
623
  * retained by this facade.
612
624
  */
@@ -622,16 +634,21 @@ export class RunWorkerBroker {
622
634
 
623
635
  constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
624
636
  this.#policy = structuredClone(policy);
625
- this.#options = options;
626
- this.#defaultBroker = new WorkerBroker(this.#policy, options);
637
+ this.#options = {
638
+ ...options,
639
+ healthStore: options.healthStore ?? new MemoryRouteHealthStore(),
640
+ };
641
+ this.#defaultBroker = new WorkerBroker(this.#policy, this.#options);
627
642
  }
628
643
 
629
- #bindingKey(runId: string, role: WorkerRole): string {
630
- return `${runId}\0${role}`;
644
+ #bindingKey(runId: string, role: WorkerRole, taskId?: string): string {
645
+ return JSON.stringify([runId, role, taskId ?? null]);
631
646
  }
632
647
 
633
- #brokerFor(runId: string, role: WorkerRole): WorkerBroker {
634
- const binding = this.#bindings.get(this.#bindingKey(runId, role));
648
+ #brokerFor(runId: string, role: WorkerRole, taskId?: string): WorkerBroker {
649
+ const binding =
650
+ this.#bindings.get(this.#bindingKey(runId, role, taskId)) ??
651
+ this.#bindings.get(this.#bindingKey(runId, role));
635
652
  if (!binding) return this.#defaultBroker;
636
653
  const routeId = binding.routeId;
637
654
  const key = `${role}\0${routeId}`;
@@ -657,6 +674,8 @@ export class RunWorkerBroker {
657
674
  if (
658
675
  typeof input.runId !== "string" ||
659
676
  input.runId.length === 0 ||
677
+ (input.taskId !== undefined &&
678
+ (typeof input.taskId !== "string" || input.taskId.length === 0)) ||
660
679
  typeof input.operationId !== "string" ||
661
680
  input.operationId.length === 0
662
681
  ) {
@@ -667,7 +686,11 @@ export class RunWorkerBroker {
667
686
  attempts: [],
668
687
  };
669
688
  }
670
- const result = await this.#brokerFor(input.runId, input.role).run({
689
+ const result = await this.#brokerFor(
690
+ input.runId,
691
+ input.role,
692
+ input.taskId,
693
+ ).run({
671
694
  operationId: input.operationId,
672
695
  role: input.role,
673
696
  ...(input.requirements ? { requirements: input.requirements } : {}),
@@ -679,39 +702,54 @@ export class RunWorkerBroker {
679
702
  if (result.ok) {
680
703
  const route = this.#policy.routes[result.routeId];
681
704
  if (!route) throw new Error("broker-route-result-invalid");
682
- this.#bindings.set(this.#bindingKey(input.runId, input.role), {
683
- routeId: route.id,
684
- fingerprint: route.fingerprint,
685
- });
705
+ this.#bindings.set(
706
+ this.#bindingKey(input.runId, input.role, input.taskId),
707
+ {
708
+ routeId: route.id,
709
+ fingerprint: route.fingerprint,
710
+ },
711
+ );
686
712
  }
687
713
  return result;
688
714
  }
689
715
 
690
716
  rebind(input: {
691
717
  runId: string;
718
+ taskId?: string;
692
719
  role: WorkerRole;
693
720
  routeId: string;
694
721
  requirements?: RouteRequirements;
695
722
  }) {
696
- const result = this.#defaultBroker.rebind(input);
723
+ const { taskId, ...routeInput } = input;
724
+ if (
725
+ taskId !== undefined &&
726
+ (typeof taskId !== "string" || taskId.length === 0)
727
+ ) {
728
+ return { ok: false as const, code: "approval-boundary" as const };
729
+ }
730
+ const result = this.#defaultBroker.rebind(routeInput);
697
731
  if (result.ok) {
698
- this.#bindings.set(this.#bindingKey(input.runId, input.role), {
699
- routeId: result.route.id,
700
- fingerprint: result.route.fingerprint,
701
- });
732
+ this.#bindings.set(
733
+ this.#bindingKey(input.runId, input.role, input.taskId),
734
+ {
735
+ routeId: result.route.id,
736
+ fingerprint: result.route.fingerprint,
737
+ },
738
+ );
702
739
  }
703
740
  return result;
704
741
  }
705
742
 
706
743
  resumeBinding(input: {
707
744
  runId: string;
745
+ taskId?: string;
708
746
  role: WorkerRole;
709
747
  routeId: string;
710
748
  expectedFingerprint?: string;
711
749
  requirements?: RouteRequirements;
712
750
  }) {
713
751
  const binding = this.#bindings.get(
714
- this.#bindingKey(input.runId, input.role),
752
+ this.#bindingKey(input.runId, input.role, input.taskId),
715
753
  );
716
754
  const route = this.#policy.routes[input.routeId];
717
755
  if (!binding) {
@@ -724,6 +762,7 @@ export class RunWorkerBroker {
724
762
  }
725
763
  return this.rebind({
726
764
  runId: input.runId,
765
+ ...(input.taskId === undefined ? {} : { taskId: input.taskId }),
727
766
  role: input.role,
728
767
  routeId: input.routeId,
729
768
  ...(input.requirements ? { requirements: input.requirements } : {}),
@@ -751,8 +790,12 @@ export class RunWorkerBroker {
751
790
  this.#boundBrokers.clear();
752
791
  }
753
792
 
754
- binding(runId: string, role: WorkerRole): string | undefined {
755
- return this.#bindings.get(this.#bindingKey(runId, role))?.routeId;
793
+ binding(
794
+ runId: string,
795
+ role: WorkerRole,
796
+ taskId?: string,
797
+ ): string | undefined {
798
+ return this.#bindings.get(this.#bindingKey(runId, role, taskId))?.routeId;
756
799
  }
757
800
 
758
801
  status(): Record<string, unknown> {