@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
package/src/scheduler.ts DELETED
@@ -1,384 +0,0 @@
1
- // Minimal in-memory scheduler for the private orchestration kernel.
2
- // FIFO DAG-ready admission with one global active limit, declared-set
3
- // declared serialization, batch cancellation, and at most one mechanical redispatch.
4
- import { LIMITS, type RequestEnvelope } from "./contracts.ts";
5
-
6
- export type TerminalStatus = "succeeded" | "failed" | "cancelled";
7
-
8
- export interface ScheduledRequest {
9
- request: RequestEnvelope;
10
- prerequisites: string[];
11
- mechanicalRedispatch?: boolean;
12
- }
13
-
14
- export interface ScheduledOutcome<T = unknown> {
15
- id: string;
16
- status: TerminalStatus;
17
- attempts: number;
18
- value?: T;
19
- error?: string;
20
- }
21
-
22
- export interface BatchHandle<T = unknown> {
23
- batchId: string;
24
- result(id: string): Promise<ScheduledOutcome<T>>;
25
- done: Promise<ScheduledOutcome<T>[]>;
26
- }
27
-
28
- export interface SchedulerOptions<T = unknown> {
29
- limit: number;
30
- execute(
31
- request: RequestEnvelope,
32
- signal: AbortSignal,
33
- attempt: number,
34
- ): Promise<T>;
35
- }
36
-
37
- export interface ConflictDeclaration {
38
- taskId: string;
39
- requestId?: string;
40
- read: string[];
41
- write: string[];
42
- conflicts: string[];
43
- resources: string[];
44
- verificationLocks: string[];
45
- }
46
-
47
- type EntryState = "queued" | "running" | TerminalStatus;
48
-
49
- interface Entry<T> {
50
- request: RequestEnvelope;
51
- prerequisites: string[];
52
- mechanicalRedispatch: boolean;
53
- state: EntryState;
54
- attempts: number;
55
- cancelRequested: boolean;
56
- cancelReason: unknown;
57
- controller: AbortController | null;
58
- resolve(outcome: ScheduledOutcome<T>): void;
59
- promise: Promise<ScheduledOutcome<T>>;
60
- }
61
-
62
- function intersects(left: string[], right: string[]): boolean {
63
- return left.some((path) => right.includes(path));
64
- }
65
-
66
- function pathsOverlap(left: string, right: string): boolean {
67
- return (
68
- left === "." ||
69
- right === "." ||
70
- left === right ||
71
- left.startsWith(`${right}/`) ||
72
- right.startsWith(`${left}/`)
73
- );
74
- }
75
-
76
- function pathSetsOverlap(left: string[], right: string[]): boolean {
77
- return left.some((a) => right.some((b) => pathsOverlap(a, b)));
78
- }
79
-
80
- function conflictEdgeMatches(
81
- conflicts: string[],
82
- other: ConflictDeclaration,
83
- ): boolean {
84
- return conflicts.some(
85
- (edge) =>
86
- edge === other.taskId ||
87
- edge === other.requestId ||
88
- other.read.some((path) => pathsOverlap(edge, path)) ||
89
- other.write.some((path) => pathsOverlap(edge, path)),
90
- );
91
- }
92
-
93
- function requestConflictOf(request: RequestEnvelope): ConflictDeclaration {
94
- return {
95
- taskId: request.taskId ?? request.id,
96
- requestId: request.id,
97
- read: request.declared.read,
98
- write: request.declared.write,
99
- conflicts: request.declared.conflicts,
100
- resources: request.declared.resources,
101
- verificationLocks: request.declared.verificationLock
102
- ? [request.declared.verificationLock]
103
- : [],
104
- };
105
- }
106
-
107
- /** True when two pure declarations cannot be active at the same time. */
108
- export function declarationsConflict(
109
- left: ConflictDeclaration,
110
- right: ConflictDeclaration,
111
- ): boolean {
112
- const a = left;
113
- const b = right;
114
- if (a.taskId === b.taskId) return true;
115
- if (pathSetsOverlap(a.write, b.write)) return true;
116
- if (pathSetsOverlap(a.write, b.read) || pathSetsOverlap(a.read, b.write)) {
117
- return true;
118
- }
119
- if (conflictEdgeMatches(a.conflicts, right)) return true;
120
- if (conflictEdgeMatches(b.conflicts, left)) return true;
121
- if (intersects(a.resources, b.resources)) return true;
122
- return intersects(a.verificationLocks, b.verificationLocks);
123
- }
124
-
125
- function errorMessage(error: unknown): string {
126
- return error instanceof Error ? error.message : String(error);
127
- }
128
-
129
- class Batch<T> {
130
- readonly entries: Entry<T>[] = [];
131
- readonly byId = new Map<string, Entry<T>>();
132
-
133
- constructor(
134
- readonly batchId: string,
135
- requests: ScheduledRequest[],
136
- ) {
137
- for (const scheduled of requests) {
138
- let resolve: (outcome: ScheduledOutcome<T>) => void = () => {};
139
- const promise = new Promise<ScheduledOutcome<T>>((accept) => {
140
- resolve = accept;
141
- });
142
- const entry: Entry<T> = {
143
- request: scheduled.request,
144
- prerequisites: [...scheduled.prerequisites],
145
- mechanicalRedispatch: scheduled.mechanicalRedispatch === true,
146
- state: "queued",
147
- attempts: 0,
148
- cancelRequested: false,
149
- cancelReason: undefined,
150
- controller: null,
151
- resolve,
152
- promise,
153
- };
154
- this.entries.push(entry);
155
- this.byId.set(entry.request.id, entry);
156
- }
157
- }
158
- }
159
-
160
- export class Scheduler<T> {
161
- private readonly limit: number;
162
- private readonly execute: SchedulerOptions<T>["execute"];
163
- private readonly batches: Batch<T>[] = [];
164
- private readonly active = new Set<Entry<T>>();
165
-
166
- constructor(options: SchedulerOptions<T>) {
167
- this.limit = Math.max(
168
- 1,
169
- Math.min(options.limit, LIMITS.maxActiveChildSessions),
170
- );
171
- this.execute = options.execute;
172
- }
173
-
174
- schedule(batchId: string, requests: ScheduledRequest[]): BatchHandle<T> {
175
- if (requests.length > LIMITS.maxRequestsPerBatch) {
176
- throw new Error(
177
- `batch ${batchId} exceeds ${LIMITS.maxRequestsPerBatch} requests`,
178
- );
179
- }
180
- const batch = new Batch<T>(batchId, requests);
181
- this.batches.push(batch);
182
- for (const entry of batch.entries) {
183
- const missing = entry.prerequisites.find((id) => !batch.byId.has(id));
184
- if (missing !== undefined) {
185
- this.settle(batch, entry, {
186
- id: entry.request.id,
187
- status: "cancelled",
188
- attempts: 0,
189
- error: `unknown prerequisite: ${missing}`,
190
- });
191
- }
192
- }
193
- this.pump();
194
- const done = Promise.all(batch.entries.map((entry) => entry.promise));
195
- void done.then(() => this.removeBatch(batch));
196
- return {
197
- batchId,
198
- result: (id: string) => {
199
- const entry = batch.byId.get(id);
200
- if (!entry) throw new Error(`unknown scheduled request: ${id}`);
201
- return entry.promise;
202
- },
203
- done,
204
- };
205
- }
206
-
207
- private removeBatch(batch: Batch<T>): void {
208
- const index = this.batches.indexOf(batch);
209
- if (index >= 0) this.batches.splice(index, 1);
210
- }
211
-
212
- cancel(
213
- batchId: string,
214
- reason: unknown = new Error("batch cancelled"),
215
- ): void {
216
- const batch = this.batches.find(
217
- (candidate) => candidate.batchId === batchId,
218
- );
219
- if (batch) this.cancelBatch(batch, reason);
220
- }
221
-
222
- async cancelAll(
223
- reason: unknown = new Error("batch cancelled"),
224
- ): Promise<void> {
225
- const batches = [...this.batches];
226
- for (const batch of batches) this.requestCancellation(batch, reason);
227
- for (const batch of batches) this.settleQueuedCancellation(batch);
228
- await Promise.all(
229
- batches.flatMap((batch) => batch.entries.map((entry) => entry.promise)),
230
- );
231
- }
232
-
233
- private cancelBatch(batch: Batch<T>, reason: unknown): void {
234
- this.requestCancellation(batch, reason);
235
- this.settleQueuedCancellation(batch);
236
- }
237
-
238
- private requestCancellation(batch: Batch<T>, reason: unknown): void {
239
- for (const entry of batch.entries) {
240
- if (entry.state !== "queued" && entry.state !== "running") continue;
241
- if (!entry.cancelRequested) entry.cancelReason = reason;
242
- entry.cancelRequested = true;
243
- if (entry.state === "running") entry.controller?.abort(reason);
244
- }
245
- }
246
-
247
- private settleQueuedCancellation(batch: Batch<T>): void {
248
- for (const entry of batch.entries) {
249
- if (entry.state === "queued") {
250
- this.settle(batch, entry, this.cancelOutcome(entry));
251
- }
252
- }
253
- this.pump();
254
- }
255
-
256
- private pump(): void {
257
- for (const batch of this.batches) {
258
- for (const entry of batch.entries) {
259
- if (this.active.size >= this.limit) return;
260
- if (entry.state !== "queued" || entry.cancelRequested) continue;
261
- if (!this.prerequisitesSucceeded(batch, entry)) continue;
262
- if (this.hasActiveConflict(entry)) continue;
263
- this.start(batch, entry);
264
- }
265
- }
266
- }
267
-
268
- private prerequisitesSucceeded(batch: Batch<T>, entry: Entry<T>): boolean {
269
- return entry.prerequisites.every(
270
- (id) => batch.byId.get(id)?.state === "succeeded",
271
- );
272
- }
273
-
274
- private hasActiveConflict(entry: Entry<T>): boolean {
275
- for (const activeEntry of this.active) {
276
- if (
277
- declarationsConflict(
278
- requestConflictOf(entry.request),
279
- requestConflictOf(activeEntry.request),
280
- )
281
- ) {
282
- return true;
283
- }
284
- }
285
- return false;
286
- }
287
-
288
- private start(batch: Batch<T>, entry: Entry<T>): void {
289
- // Guarded start: a cancelled or settled queued closure never runs later.
290
- if (entry.state !== "queued" || entry.cancelRequested) return;
291
- entry.state = "running";
292
- entry.attempts += 1;
293
- entry.controller = new AbortController();
294
- this.active.add(entry);
295
- const attempt = entry.attempts;
296
- this.execute(entry.request, entry.controller.signal, attempt).then(
297
- (value) => {
298
- if (entry.cancelRequested) {
299
- this.settleCancelled(batch, entry);
300
- return;
301
- }
302
- this.settle(batch, entry, {
303
- id: entry.request.id,
304
- status: "succeeded",
305
- attempts: entry.attempts,
306
- value,
307
- });
308
- },
309
- (error) => {
310
- if (entry.cancelRequested) {
311
- this.settleCancelled(batch, entry);
312
- return;
313
- }
314
- if (entry.mechanicalRedispatch && attempt === 1) {
315
- this.redispatch(batch, entry);
316
- return;
317
- }
318
- this.settle(batch, entry, {
319
- id: entry.request.id,
320
- status: "failed",
321
- attempts: entry.attempts,
322
- error: errorMessage(error),
323
- });
324
- },
325
- );
326
- }
327
-
328
- private redispatch(batch: Batch<T>, entry: Entry<T>): void {
329
- // One identical mechanical redispatch in the retained active slot; the
330
- // attempt becomes 2 and a further failure settles, so no third attempt.
331
- entry.state = "queued";
332
- entry.controller = null;
333
- this.start(batch, entry);
334
- }
335
-
336
- private settleCancelled(batch: Batch<T>, entry: Entry<T>): void {
337
- this.settle(batch, entry, this.cancelOutcome(entry));
338
- }
339
-
340
- private cancelOutcome(entry: Entry<T>): ScheduledOutcome<T> {
341
- return {
342
- id: entry.request.id,
343
- status: "cancelled",
344
- attempts: entry.attempts,
345
- error: errorMessage(entry.cancelReason ?? new Error("batch cancelled")),
346
- };
347
- }
348
-
349
- private settle(
350
- batch: Batch<T>,
351
- entry: Entry<T>,
352
- outcome: ScheduledOutcome<T>,
353
- ): void {
354
- // Every scheduled request reaches exactly one terminal state.
355
- if (entry.state !== "queued" && entry.state !== "running") return;
356
- entry.state = outcome.status;
357
- this.active.delete(entry);
358
- entry.controller = null;
359
- entry.resolve(outcome);
360
- if (outcome.status !== "succeeded") {
361
- this.blockDependents(batch, entry.request.id, outcome.status);
362
- }
363
- this.pump();
364
- }
365
-
366
- private blockDependents(
367
- batch: Batch<T>,
368
- prerequisiteId: string,
369
- status: TerminalStatus,
370
- ): void {
371
- // A dependent can never start after a prerequisite failure or cancel;
372
- // independent siblings keep their own outcomes.
373
- for (const dependent of batch.entries) {
374
- if (dependent.state !== "queued") continue;
375
- if (!dependent.prerequisites.includes(prerequisiteId)) continue;
376
- this.settle(batch, dependent, {
377
- id: dependent.request.id,
378
- status: "cancelled",
379
- attempts: dependent.attempts,
380
- error: `blocked: prerequisite ${prerequisiteId} ${status}`,
381
- });
382
- }
383
- }
384
- }
@@ -1,158 +0,0 @@
1
- // Per-role subagent endpoint resolution from the cadence env file.
2
- // Three-tier whole-layer semantics: role layer, global layer, inherited.
3
-
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import path from "node:path";
7
- import { parseEnvFile } from "../skills/_shared/load-config.mjs";
8
- import { ROLES } from "./contracts.ts";
9
-
10
- export type SubagentRole = (typeof ROLES)[number];
11
-
12
- export type SubagentDialect =
13
- | "openai-completions"
14
- | "openai-responses"
15
- | "anthropic-messages";
16
-
17
- const LAYER_FIELDS = [
18
- "API_URL",
19
- "API_KEY",
20
- "MODEL",
21
- "API",
22
- "CONTEXT_WINDOW",
23
- "MAX_TOKENS",
24
- ] as const;
25
- type LayerField = (typeof LAYER_FIELDS)[number];
26
- type Layer = Partial<Record<LayerField, string>>;
27
-
28
- const GLOBAL_PREFIX = "SUBAGENT_";
29
- const DEFAULT_DIALECT: SubagentDialect = "openai-completions";
30
- const DIALECTS: readonly SubagentDialect[] = [
31
- "openai-completions",
32
- "openai-responses",
33
- "anthropic-messages",
34
- ];
35
-
36
- export const DEFAULT_CONTEXT_WINDOW = 256000;
37
- export const DEFAULT_MAX_TOKENS = 128000;
38
-
39
- export interface SubagentEndpoint {
40
- readonly url: string;
41
- readonly apiKey?: string;
42
- readonly model: string;
43
- readonly dialect: SubagentDialect;
44
- readonly contextWindow: number;
45
- readonly maxTokens: number;
46
- }
47
-
48
- export type SubagentEndpointResolution =
49
- | { kind: "inherited" }
50
- | { kind: "custom"; endpoint: Readonly<SubagentEndpoint> }
51
- | { kind: "invalid"; keys: string[]; file?: string };
52
-
53
- export function describeInvalidSubagentEndpoint(
54
- resolution: Extract<SubagentEndpointResolution, { kind: "invalid" }>,
55
- ): string {
56
- return resolution.keys.length > 0
57
- ? `invalid subagent endpoint configuration keys: ${resolution.keys.join(", ")}`
58
- : `invalid subagent endpoint configuration file: ${resolution.file ?? "unknown"}`;
59
- }
60
-
61
- function layerOf(values: Record<string, string>, prefix: string): Layer {
62
- const layer: Layer = {};
63
- for (const field of LAYER_FIELDS) {
64
- const value = values[prefix + field];
65
- if (value !== undefined && value !== "") layer[field] = value;
66
- }
67
- return layer;
68
- }
69
-
70
- function isCommitted(layer: Layer): boolean {
71
- return LAYER_FIELDS.some((field) => layer[field] !== undefined);
72
- }
73
-
74
- function isHttpUrl(value: string): boolean {
75
- try {
76
- const parsed = new URL(value);
77
- return parsed.protocol === "http:" || parsed.protocol === "https:";
78
- } catch {
79
- return false;
80
- }
81
- }
82
-
83
- function isPositiveInteger(value: string): boolean {
84
- const number = Number(value);
85
- return /^[0-9]+$/.test(value) && Number.isSafeInteger(number) && number > 0;
86
- }
87
-
88
- function offendingKeys(prefix: string, layer: Layer): string[] {
89
- const keys: string[] = [];
90
- if (layer.API_URL === undefined || !isHttpUrl(layer.API_URL))
91
- keys.push(`${prefix}API_URL`);
92
- if (layer.MODEL === undefined) keys.push(`${prefix}MODEL`);
93
- if (
94
- layer.API !== undefined &&
95
- !DIALECTS.includes(layer.API as SubagentDialect)
96
- )
97
- keys.push(`${prefix}API`);
98
- if (
99
- layer.CONTEXT_WINDOW !== undefined &&
100
- !isPositiveInteger(layer.CONTEXT_WINDOW)
101
- )
102
- keys.push(`${prefix}CONTEXT_WINDOW`);
103
- if (layer.MAX_TOKENS !== undefined && !isPositiveInteger(layer.MAX_TOKENS))
104
- keys.push(`${prefix}MAX_TOKENS`);
105
- return keys;
106
- }
107
-
108
- function selectEnvFile(cwd: string, home: string): string | null {
109
- const projectPath = path.join(cwd, ".pi", "cadence", ".env");
110
- if (existsSync(projectPath)) return projectPath;
111
- const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
112
- if (existsSync(userPath)) return userPath;
113
- return null;
114
- }
115
-
116
- export function resolveSubagentEndpoint(
117
- role: string,
118
- options?: { cwd?: string; home?: string },
119
- ): SubagentEndpointResolution {
120
- if (!(ROLES as readonly string[]).includes(role))
121
- return { kind: "invalid", keys: ["SUBAGENT_ROLE"] };
122
- const cwd = options?.cwd ?? process.cwd();
123
- const home = options?.home ?? homedir();
124
- const file = selectEnvFile(cwd, home);
125
- if (!file) return { kind: "inherited" };
126
- let values: Record<string, string>;
127
- try {
128
- values = parseEnvFile(readFileSync(file, "utf8"));
129
- } catch {
130
- return { kind: "invalid", keys: [], file };
131
- }
132
- const rolePrefix = `SUBAGENT_${role.toUpperCase().replaceAll("-", "_")}_`;
133
- const roleLayer = layerOf(values, rolePrefix);
134
- let layer: Layer | undefined = isCommitted(roleLayer) ? roleLayer : undefined;
135
- if (!layer) {
136
- const globalLayer = layerOf(values, GLOBAL_PREFIX);
137
- if (isCommitted(globalLayer)) layer = globalLayer;
138
- }
139
- if (!layer) return { kind: "inherited" };
140
- const prefix = layer === roleLayer ? rolePrefix : GLOBAL_PREFIX;
141
- const keys = offendingKeys(prefix, layer);
142
- if (keys.length > 0) return { kind: "invalid", keys, file };
143
- const endpoint: Readonly<SubagentEndpoint> = Object.freeze({
144
- url: layer.API_URL ?? "",
145
- ...(layer.API_KEY === undefined ? {} : { apiKey: layer.API_KEY }),
146
- model: layer.MODEL ?? "",
147
- dialect: (layer.API as SubagentDialect | undefined) ?? DEFAULT_DIALECT,
148
- contextWindow:
149
- layer.CONTEXT_WINDOW === undefined
150
- ? DEFAULT_CONTEXT_WINDOW
151
- : Number(layer.CONTEXT_WINDOW),
152
- maxTokens:
153
- layer.MAX_TOKENS === undefined
154
- ? DEFAULT_MAX_TOKENS
155
- : Number(layer.MAX_TOKENS),
156
- });
157
- return { kind: "custom", endpoint };
158
- }