@abelxiaoxing/cadence 1.0.14 → 1.1.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 (48) hide show
  1. package/README.md +39 -13
  2. package/agents/contract-reviewer.md +2 -2
  3. package/agents/design-explorer.md +1 -1
  4. package/agents/diagnosis-worker.md +6 -3
  5. package/agents/implementation-worker.md +14 -9
  6. package/config/.env.example +5 -17
  7. package/config/routes.example.json +42 -0
  8. package/package.json +3 -3
  9. package/prompts/abel-design.md +206 -228
  10. package/prompts/abel-diagnose.md +39 -18
  11. package/prompts/abel-implement.md +122 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +190 -168
  14. package/src/apply-transaction.ts +1427 -0
  15. package/src/artifact-store.ts +204 -0
  16. package/src/child-session.ts +136 -17
  17. package/src/contracts.ts +121 -696
  18. package/src/control-contracts.ts +246 -0
  19. package/src/delivery-compiler.ts +1118 -0
  20. package/src/design-control.ts +846 -0
  21. package/src/design-journal.ts +1569 -0
  22. package/src/file-snapshot.ts +5 -0
  23. package/src/implement-graph.ts +11 -4
  24. package/src/index.ts +1833 -118
  25. package/src/isolation-backend.ts +269 -0
  26. package/src/packet-runtime.ts +533 -0
  27. package/src/parent-provider.ts +23 -4
  28. package/src/route-policy.ts +483 -0
  29. package/src/run-state.ts +430 -0
  30. package/src/run-store.ts +990 -0
  31. package/src/safe-path.ts +11 -0
  32. package/src/scoped-tools.ts +36 -0
  33. package/src/state-root.ts +215 -0
  34. package/src/subagent-activity.ts +526 -27
  35. package/src/submit-tool.ts +274 -0
  36. package/src/task-ledger.ts +1380 -0
  37. package/src/verification-capability.ts +560 -45
  38. package/src/worker-broker.ts +703 -0
  39. package/src/workflow-engine.ts +7706 -0
  40. package/src/workspace-store.ts +544 -0
  41. package/src/candidate-preflight.ts +0 -1618
  42. package/src/drain.ts +0 -20
  43. package/src/patch.ts +0 -505
  44. package/src/result-store.ts +0 -312
  45. package/src/runtime.ts +0 -2436
  46. package/src/scheduler.ts +0 -384
  47. package/src/subagent-endpoint.ts +0 -158
  48. package/src/worker.ts +0 -204
@@ -0,0 +1,204 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ constants,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ renameSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ const SHA256 = /^[a-f0-9]{64}$/u;
18
+
19
+ export interface ArtifactIdentity {
20
+ hash: string;
21
+ bytes: number;
22
+ }
23
+
24
+ function digest(bytes: Uint8Array): string {
25
+ return createHash("sha256").update(bytes).digest("hex");
26
+ }
27
+
28
+ function requireHash(hash: string): void {
29
+ if (!SHA256.test(hash)) throw new Error("artifact-hash-invalid");
30
+ }
31
+
32
+ function ensureDirectory(directory: string): void {
33
+ const stat = lstatSync(directory, { throwIfNoEntry: false });
34
+ if (stat) {
35
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
36
+ throw new Error("artifact-store-path-unsafe");
37
+ }
38
+ chmodSync(directory, 0o700);
39
+ return;
40
+ }
41
+ let cursor = path.dirname(directory);
42
+ while (cursor !== path.dirname(cursor)) {
43
+ const ancestor = lstatSync(cursor, { throwIfNoEntry: false });
44
+ if (ancestor) {
45
+ if (ancestor.isSymbolicLink() || !ancestor.isDirectory()) {
46
+ throw new Error("artifact-store-path-unsafe");
47
+ }
48
+ break;
49
+ }
50
+ cursor = path.dirname(cursor);
51
+ }
52
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
53
+ const created = lstatSync(directory);
54
+ if (created.isSymbolicLink() || !created.isDirectory()) {
55
+ throw new Error("artifact-store-path-unsafe");
56
+ }
57
+ chmodSync(directory, 0o700);
58
+ }
59
+
60
+ function syncDirectory(directory: string): void {
61
+ const descriptor = openSync(directory, constants.O_RDONLY);
62
+ try {
63
+ fsyncSync(descriptor);
64
+ } finally {
65
+ closeSync(descriptor);
66
+ }
67
+ }
68
+
69
+ function writeAtomic(target: string, bytes: Uint8Array): void {
70
+ const directory = path.dirname(target);
71
+ ensureDirectory(directory);
72
+ const temporary = path.join(
73
+ directory,
74
+ `.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`,
75
+ );
76
+ const descriptor = openSync(
77
+ temporary,
78
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
79
+ 0o600,
80
+ );
81
+ try {
82
+ writeFileSync(descriptor, bytes);
83
+ fsyncSync(descriptor);
84
+ } finally {
85
+ closeSync(descriptor);
86
+ }
87
+ try {
88
+ renameSync(temporary, target);
89
+ chmodSync(target, 0o600);
90
+ syncDirectory(directory);
91
+ } catch (error) {
92
+ try {
93
+ unlinkSync(temporary);
94
+ } catch {
95
+ // The rename may already have consumed the temporary file.
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /** Durable content-addressed bytes used by workspace revisions and sealed results. */
102
+ export class ArtifactStore {
103
+ readonly root: string;
104
+ readonly #blobs: string;
105
+ readonly #references: string;
106
+
107
+ constructor(root: string) {
108
+ if (!path.isAbsolute(root)) throw new Error("artifact-store-root-invalid");
109
+ this.root = path.resolve(root);
110
+ this.#blobs = path.join(this.root, "blobs");
111
+ this.#references = path.join(this.root, "references");
112
+ ensureDirectory(this.root);
113
+ ensureDirectory(this.#blobs);
114
+ ensureDirectory(this.#references);
115
+ }
116
+
117
+ #blobPath(hash: string): string {
118
+ requireHash(hash);
119
+ return path.join(this.#blobs, hash.slice(0, 2), hash);
120
+ }
121
+
122
+ #referencePath(hash: string): string {
123
+ requireHash(hash);
124
+ return path.join(this.#references, hash.slice(0, 2), `${hash}.ref`);
125
+ }
126
+
127
+ put(input: Uint8Array): ArtifactIdentity {
128
+ if (!(input instanceof Uint8Array))
129
+ throw new Error("artifact-bytes-invalid");
130
+ const bytes = Buffer.from(input);
131
+ const hash = digest(bytes);
132
+ const target = this.#blobPath(hash);
133
+ const existing = lstatSync(target, { throwIfNoEntry: false });
134
+ if (existing) {
135
+ if (existing.isSymbolicLink() || !existing.isFile()) {
136
+ throw new Error("artifact-store-path-unsafe");
137
+ }
138
+ const current = readFileSync(target);
139
+ if (current.length !== bytes.length || digest(current) !== hash) {
140
+ throw new Error("artifact-integrity-invalid");
141
+ }
142
+ } else {
143
+ writeAtomic(target, bytes);
144
+ const stored = readFileSync(target);
145
+ if (stored.length !== bytes.length || digest(stored) !== hash) {
146
+ throw new Error("artifact-integrity-invalid");
147
+ }
148
+ }
149
+ return Object.freeze({ hash, bytes: bytes.length });
150
+ }
151
+
152
+ read(hash: string): Uint8Array {
153
+ const target = this.#blobPath(hash);
154
+ const stat = lstatSync(target, { throwIfNoEntry: false });
155
+ if (!stat?.isFile() || stat.isSymbolicLink()) {
156
+ throw new Error("artifact-unavailable");
157
+ }
158
+ const bytes = readFileSync(target);
159
+ if (digest(bytes) !== hash) throw new Error("artifact-integrity-invalid");
160
+ return Buffer.from(bytes);
161
+ }
162
+
163
+ referenceCount(hash: string): number {
164
+ requireHash(hash);
165
+ const referencePath = this.#referencePath(hash);
166
+ const stat = lstatSync(referencePath, { throwIfNoEntry: false });
167
+ if (!stat) return 0;
168
+ if (!stat.isFile() || stat.isSymbolicLink()) {
169
+ throw new Error("artifact-reference-invalid");
170
+ }
171
+ const raw = readFileSync(referencePath, "utf8");
172
+ if (!/^(?:0|[1-9][0-9]*)\n$/u.test(raw)) {
173
+ throw new Error("artifact-reference-invalid");
174
+ }
175
+ const count = Number(raw.trim());
176
+ if (!Number.isSafeInteger(count))
177
+ throw new Error("artifact-reference-invalid");
178
+ return count;
179
+ }
180
+
181
+ #setReferenceCount(hash: string, count: number): number {
182
+ if (!Number.isSafeInteger(count) || count < 0) {
183
+ throw new Error("artifact-reference-invalid");
184
+ }
185
+ writeAtomic(this.#referencePath(hash), Buffer.from(`${count}\n`));
186
+ return count;
187
+ }
188
+
189
+ retain(hash: string): number {
190
+ this.read(hash);
191
+ const current = this.referenceCount(hash);
192
+ if (current === Number.MAX_SAFE_INTEGER) {
193
+ throw new Error("artifact-reference-overflow");
194
+ }
195
+ return this.#setReferenceCount(hash, current + 1);
196
+ }
197
+
198
+ release(hash: string): number {
199
+ this.read(hash);
200
+ const current = this.referenceCount(hash);
201
+ if (current === 0) throw new Error("artifact-reference-underflow");
202
+ return this.#setReferenceCount(hash, current - 1);
203
+ }
204
+ }
@@ -16,12 +16,21 @@ import type {
16
16
  SafeFailureDetails,
17
17
  } from "./contracts.ts";
18
18
  import { EmptyResourceLoader } from "./empty-resource-loader.ts";
19
- import { createScopedTools, TOOL_LIMITS } from "./scoped-tools.ts";
20
19
  import {
20
+ createScopedTools,
21
+ type Observation,
22
+ ScopedObservationCollector,
23
+ TOOL_LIMITS,
24
+ } from "./scoped-tools.ts";
25
+ import {
26
+ type CandidateArtifactSubmission,
27
+ type CandidateArtifactSubmissionResult,
28
+ createCandidateArtifactTool,
21
29
  createSubmitTool,
22
30
  type FinalCategory,
23
31
  type SubmitClassification,
24
32
  } from "./submit-tool.ts";
33
+ import { serializeTaskLedgerProjection } from "./task-ledger.ts";
25
34
 
26
35
  function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
27
36
  signal.throwIfAborted();
@@ -106,6 +115,7 @@ function wrapScopedTools(
106
115
  roots: string[],
107
116
  cwd: string,
108
117
  allowedPaths?: string[],
118
+ observer?: (observation: Observation) => void,
109
119
  ) {
110
120
  const order = ["read", "grep", "find", "ls"];
111
121
  return createScopedTools({
@@ -117,6 +127,7 @@ function wrapScopedTools(
117
127
  ),
118
128
  }
119
129
  : {}),
130
+ ...(observer ? { observer } : {}),
120
131
  })
121
132
  .sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
122
133
  .map((scoped) =>
@@ -175,6 +186,20 @@ function finalDeliveryContent(message: AssistantMessage | undefined) {
175
186
  );
176
187
  }
177
188
 
189
+ function hasStructuralSubmit(message: AssistantMessage | undefined) {
190
+ return finalDeliveryContent(message).some(
191
+ (content) =>
192
+ content.type === "toolCall" && content.name === "abel_submit_result",
193
+ );
194
+ }
195
+
196
+ function structuralSubmitCount(message: AssistantMessage | undefined) {
197
+ return finalDeliveryContent(message).filter(
198
+ (content) =>
199
+ content.type === "toolCall" && content.name === "abel_submit_result",
200
+ ).length;
201
+ }
202
+
178
203
  function isSingleStructuralSubmit(message: AssistantMessage | undefined) {
179
204
  const content = finalDeliveryContent(message);
180
205
  return (
@@ -251,12 +276,13 @@ function withSubmitDetails(
251
276
  export type ChildSessionResult =
252
277
  | {
253
278
  ok: true;
254
- result: EvidenceResult | DiffResult;
279
+ result: EvidenceResult | DiffResult | CandidateArtifactSubmissionResult;
255
280
  toolNames: string[];
256
281
  submitCount: number;
257
282
  disposeCount: number;
258
283
  usage: Usage;
259
284
  classification: SubmitClassification;
285
+ observations?: { observations: Observation[]; truncated: boolean };
260
286
  }
261
287
  | {
262
288
  ok: false;
@@ -267,6 +293,7 @@ export type ChildSessionResult =
267
293
  disposeCount: number;
268
294
  usage: Usage;
269
295
  classification: SubmitClassification;
296
+ observations?: { observations: Observation[]; truncated: boolean };
270
297
  };
271
298
 
272
299
  export async function runChildSession(input: {
@@ -284,19 +311,49 @@ export async function runChildSession(input: {
284
311
  timeoutMs: number;
285
312
  signal?: AbortSignal;
286
313
  failureOverride?: () => ChildFailure | undefined;
314
+ ledgerProjection?: unknown;
315
+ captureObservations?: boolean;
316
+ candidateArtifact?: CandidateArtifactSubmission;
317
+ onStreamStart?: () => void;
318
+ onStreamProgress?: () => void;
287
319
  }): Promise<ChildSessionResult> {
288
- const submit = createSubmitTool({
289
- requestId: input.requestId,
290
- taskId: input.taskId,
291
- role: input.role,
292
- phase: input.phase ?? "red",
293
- output: input.output,
294
- });
295
- const readTools = wrapScopedTools(input.roots, input.cwd, input.allowedPaths);
320
+ const segmentedCandidate = input.candidateArtifact !== undefined;
321
+ const submit = input.candidateArtifact
322
+ ? createCandidateArtifactTool(input.candidateArtifact)
323
+ : createSubmitTool({
324
+ requestId: input.requestId,
325
+ taskId: input.taskId,
326
+ role: input.role,
327
+ phase: input.phase ?? "red",
328
+ output: input.output,
329
+ });
330
+ const observationCollector = input.captureObservations
331
+ ? new ScopedObservationCollector()
332
+ : undefined;
333
+ const readTools = wrapScopedTools(
334
+ input.roots,
335
+ input.cwd,
336
+ input.allowedPaths,
337
+ observationCollector?.observe,
338
+ );
296
339
  const customTools = [...readTools, submit.tool];
297
340
  const toolNames = customTools.map((tool) => tool.name);
298
341
  let disposeCount = 0;
299
342
  const usage = new UsageAggregator();
343
+ const effectivePrompt =
344
+ input.ledgerProjection === undefined
345
+ ? input.systemPrompt
346
+ : [
347
+ input.systemPrompt,
348
+ "",
349
+ "<abel-task-ledger-projection>",
350
+ serializeTaskLedgerProjection(input.ledgerProjection),
351
+ "</abel-task-ledger-projection>",
352
+ ].join("\n");
353
+ const observationMetadata = () =>
354
+ observationCollector
355
+ ? { observations: observationCollector.projection() }
356
+ : {};
300
357
  const abort = new AbortController();
301
358
  const forwardCancellation = () =>
302
359
  abort.abort(input.signal?.reason ?? new Error("child phase cancelled"));
@@ -316,6 +373,24 @@ export async function runChildSession(input: {
316
373
  let unsubscribe: (() => void) | undefined;
317
374
  let abortSettlement: Promise<void> | undefined;
318
375
  let assistantSequence = 0;
376
+ let streamStarted = false;
377
+ const notifyStreamStart = () => {
378
+ if (streamStarted) return;
379
+ streamStarted = true;
380
+ try {
381
+ input.onStreamStart?.();
382
+ } catch {
383
+ // Stream observation cannot influence child execution.
384
+ }
385
+ };
386
+ const notifyStreamProgress = () => {
387
+ notifyStreamStart();
388
+ try {
389
+ input.onStreamProgress?.();
390
+ } catch {
391
+ // Stream observation cannot influence child execution.
392
+ }
393
+ };
319
394
  const disposeOnce = () => {
320
395
  if (session) {
321
396
  session.dispose();
@@ -332,7 +407,9 @@ export async function runChildSession(input: {
332
407
  finalCategory = "no-final-assistant";
333
408
  } else if (isSingleStructuralSubmit(last)) {
334
409
  finalCategory =
335
- submit.getAttempts() > 1 ? "multiple-submit" : "single-submit-only";
410
+ !segmentedCandidate && submit.getAttempts() > 1
411
+ ? "multiple-submit"
412
+ : "single-submit-only";
336
413
  } else {
337
414
  const content = finalDeliveryContent(last);
338
415
  finalCategory =
@@ -383,8 +460,11 @@ export async function runChildSession(input: {
383
460
  }
384
461
  return undefined;
385
462
  };
386
- const noStructuralSubmit = (): ChildFailure =>
387
- submit.getAttempts() === 0
463
+ const noStructuralSubmit = (): ChildFailure => {
464
+ const final = session?.messages
465
+ .filter((message) => message.role === "assistant")
466
+ .at(-1);
467
+ return submit.getAttempts() === 0 && !hasStructuralSubmit(final)
388
468
  ? {
389
469
  kind: "artifact",
390
470
  code: "child-no-structural-submit",
@@ -395,6 +475,7 @@ export async function runChildSession(input: {
395
475
  code: "invalid-structural-result",
396
476
  stage: "structural-submit",
397
477
  };
478
+ };
398
479
  try {
399
480
  abort.signal.throwIfAborted();
400
481
  const creation = createAgentSession({
@@ -404,7 +485,7 @@ export async function runChildSession(input: {
404
485
  thinkingLevel: "off",
405
486
  tools: toolNames,
406
487
  customTools,
407
- resourceLoader: new EmptyResourceLoader(input.systemPrompt),
488
+ resourceLoader: new EmptyResourceLoader(effectivePrompt),
408
489
  sessionManager: SessionManager.inMemory(input.cwd),
409
490
  settingsManager: SettingsManager.inMemory({
410
491
  compaction: { enabled: false },
@@ -419,7 +500,33 @@ export async function runChildSession(input: {
419
500
  .catch(() => undefined);
420
501
  throw error;
421
502
  }
503
+ const childAgent = session.agent;
504
+ if (childAgent) {
505
+ const previousShouldStopAfterTurn = childAgent.shouldStopAfterTurn;
506
+ childAgent.shouldStopAfterTurn = async (turn, signal) => {
507
+ if (
508
+ segmentedCandidate
509
+ ? submit.getResult() !== undefined
510
+ : hasStructuralSubmit(turn.message)
511
+ ) {
512
+ return true;
513
+ }
514
+ return (await previousShouldStopAfterTurn?.(turn, signal)) ?? false;
515
+ };
516
+ }
422
517
  unsubscribe = session.subscribe((event) => {
518
+ if (
519
+ event.type === "message_start" &&
520
+ event.message.role === "assistant"
521
+ ) {
522
+ notifyStreamStart();
523
+ }
524
+ if (
525
+ event.type === "message_update" &&
526
+ event.message.role === "assistant"
527
+ ) {
528
+ notifyStreamProgress();
529
+ }
423
530
  if (event.type === "message_end" && event.message.role === "assistant") {
424
531
  usage.add(`assistant:${assistantSequence++}`, event.message.usage);
425
532
  }
@@ -438,7 +545,7 @@ export async function runChildSession(input: {
438
545
  throw abort.signal.reason;
439
546
  }
440
547
  await Promise.race([
441
- session.prompt(input.systemPrompt, { expandPromptTemplates: false }),
548
+ session.prompt(effectivePrompt, { expandPromptTemplates: false }),
442
549
  new Promise<never>((_, reject) =>
443
550
  abort.signal.addEventListener(
444
551
  "abort",
@@ -453,7 +560,11 @@ export async function runChildSession(input: {
453
560
  const result = submit.getResult();
454
561
  const attempts = submit.getAttempts();
455
562
  const classification = classifySession();
456
- if (!result || attempts !== 1) {
563
+ const segmentedComplete =
564
+ segmentedCandidate &&
565
+ result !== undefined &&
566
+ (result.kind === "sealed-candidate" || result.kind === "context-request");
567
+ if (!result || (segmentedCandidate ? !segmentedComplete : attempts !== 1)) {
457
568
  const failure = withSubmitDetails(
458
569
  submit.getFailure() ??
459
570
  input.failureOverride?.() ??
@@ -472,11 +583,16 @@ export async function runChildSession(input: {
472
583
  disposeCount,
473
584
  usage: usage.total(),
474
585
  classification,
586
+ ...observationMetadata(),
475
587
  };
476
588
  }
477
589
  const assistants = session.messages.filter((m) => m.role === "assistant");
478
590
  const final = assistants.at(-1);
479
- if (!isSingleStructuralSubmit(final)) {
591
+ if (
592
+ !(segmentedCandidate
593
+ ? segmentedComplete && structuralSubmitCount(final) === 1
594
+ : isSingleStructuralSubmit(final))
595
+ ) {
480
596
  const failure = {
481
597
  kind: "artifact",
482
598
  code: "invalid-structural-result",
@@ -493,6 +609,7 @@ export async function runChildSession(input: {
493
609
  disposeCount,
494
610
  usage: usage.total(),
495
611
  classification,
612
+ ...observationMetadata(),
496
613
  };
497
614
  }
498
615
  disposeOnce();
@@ -504,6 +621,7 @@ export async function runChildSession(input: {
504
621
  disposeCount,
505
622
  usage: usage.total(),
506
623
  classification,
624
+ ...observationMetadata(),
507
625
  };
508
626
  } catch (error) {
509
627
  if (abort.signal.aborted && session) {
@@ -553,6 +671,7 @@ export async function runChildSession(input: {
553
671
  disposeCount,
554
672
  usage: usage.total(),
555
673
  classification,
674
+ ...observationMetadata(),
556
675
  };
557
676
  } finally {
558
677
  clearTimeout(timer);