@abelxiaoxing/cadence 1.0.2 → 1.0.3
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +50 -21
- package/skills/abel-workflow/SKILL.md +57 -25
- package/src/candidate-preflight.ts +361 -56
- package/src/child-session.ts +92 -27
- package/src/contracts.ts +987 -38
- package/src/index.ts +50 -24
- package/src/parent-provider.ts +89 -23
- package/src/patch.ts +269 -41
- package/src/result-store.ts +254 -16
- package/src/runtime.ts +1281 -560
- package/src/scheduler.ts +59 -16
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +12 -2
- package/src/worker.ts +114 -195
package/src/scheduler.ts
CHANGED
|
@@ -34,6 +34,16 @@ export interface SchedulerOptions<T = unknown> {
|
|
|
34
34
|
): Promise<T>;
|
|
35
35
|
}
|
|
36
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
|
+
|
|
37
47
|
type EntryState = "queued" | "running" | TerminalStatus;
|
|
38
48
|
|
|
39
49
|
interface Entry<T> {
|
|
@@ -53,35 +63,63 @@ function intersects(left: string[], right: string[]): boolean {
|
|
|
53
63
|
return left.some((path) => right.includes(path));
|
|
54
64
|
}
|
|
55
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
|
+
|
|
56
80
|
function conflictEdgeMatches(
|
|
57
81
|
conflicts: string[],
|
|
58
|
-
other:
|
|
82
|
+
other: ConflictDeclaration,
|
|
59
83
|
): boolean {
|
|
60
84
|
return conflicts.some(
|
|
61
85
|
(edge) =>
|
|
62
86
|
edge === other.taskId ||
|
|
63
|
-
edge === other.
|
|
64
|
-
other.
|
|
65
|
-
other.
|
|
87
|
+
edge === other.requestId ||
|
|
88
|
+
other.read.some((path) => pathsOverlap(edge, path)) ||
|
|
89
|
+
other.write.some((path) => pathsOverlap(edge, path)),
|
|
66
90
|
);
|
|
67
91
|
}
|
|
68
92
|
|
|
69
|
-
|
|
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. */
|
|
70
108
|
export function declarationsConflict(
|
|
71
|
-
left:
|
|
72
|
-
right:
|
|
109
|
+
left: ConflictDeclaration,
|
|
110
|
+
right: ConflictDeclaration,
|
|
73
111
|
): boolean {
|
|
74
|
-
const a = left
|
|
75
|
-
const b = right
|
|
76
|
-
if (
|
|
77
|
-
if (
|
|
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
|
+
}
|
|
78
119
|
if (conflictEdgeMatches(a.conflicts, right)) return true;
|
|
79
120
|
if (conflictEdgeMatches(b.conflicts, left)) return true;
|
|
80
121
|
if (intersects(a.resources, b.resources)) return true;
|
|
81
|
-
return (
|
|
82
|
-
a.verificationLock !== undefined &&
|
|
83
|
-
a.verificationLock === b.verificationLock
|
|
84
|
-
);
|
|
122
|
+
return intersects(a.verificationLocks, b.verificationLocks);
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
function errorMessage(error: unknown): string {
|
|
@@ -235,7 +273,12 @@ export class Scheduler<T> {
|
|
|
235
273
|
|
|
236
274
|
private hasActiveConflict(entry: Entry<T>): boolean {
|
|
237
275
|
for (const activeEntry of this.active) {
|
|
238
|
-
if (
|
|
276
|
+
if (
|
|
277
|
+
declarationsConflict(
|
|
278
|
+
requestConflictOf(entry.request),
|
|
279
|
+
requestConflictOf(activeEntry.request),
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
239
282
|
return true;
|
|
240
283
|
}
|
|
241
284
|
}
|
package/src/subagent-activity.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
visibleWidth,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
import type {
|
|
13
|
-
DispatchResult,
|
|
14
13
|
RuntimeActivityEvent,
|
|
15
14
|
RuntimeActivityObserver,
|
|
16
15
|
RuntimeActivityState,
|
|
@@ -39,12 +38,12 @@ export interface EvidenceActivitySummary {
|
|
|
39
38
|
export interface DiffActivitySummary {
|
|
40
39
|
kind: "diff";
|
|
41
40
|
summary: string;
|
|
42
|
-
nextStep: string;
|
|
43
41
|
riskCount: number;
|
|
44
42
|
retained: boolean;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
|
|
46
|
+
export type ActivityTone = "success" | "warning" | "muted" | "error";
|
|
48
47
|
|
|
49
48
|
export interface ActivityDisplay {
|
|
50
49
|
version: 1;
|
|
@@ -54,6 +53,7 @@ export interface ActivityDisplay {
|
|
|
54
53
|
phase: string;
|
|
55
54
|
objective: string;
|
|
56
55
|
state: TerminalActivityState;
|
|
56
|
+
tone: ActivityTone;
|
|
57
57
|
elapsedMs: number;
|
|
58
58
|
summary?: ActivitySummary;
|
|
59
59
|
reason?: string;
|
|
@@ -217,14 +217,7 @@ function renderActivityLines(
|
|
|
217
217
|
formatElapsed(elapsed),
|
|
218
218
|
].join(" · ");
|
|
219
219
|
const second = ` ${sanitizeDisplayText(snapshot.objective, 240)}`;
|
|
220
|
-
const color =
|
|
221
|
-
snapshot.state === "completed"
|
|
222
|
-
? "success"
|
|
223
|
-
: snapshot.state === "failed" || snapshot.state === "timed-out"
|
|
224
|
-
? "error"
|
|
225
|
-
: snapshot.state === "cancelled"
|
|
226
|
-
? "warning"
|
|
227
|
-
: "accent";
|
|
220
|
+
const color = "tone" in snapshot ? snapshot.tone : "accent";
|
|
228
221
|
return [
|
|
229
222
|
line(styled(theme, color, first), width),
|
|
230
223
|
line(styled(theme, "muted", second), width),
|
|
@@ -311,7 +304,7 @@ export class ActivityInlineComponent implements Component {
|
|
|
311
304
|
lines[1] = line(
|
|
312
305
|
styled(
|
|
313
306
|
this.theme,
|
|
314
|
-
|
|
307
|
+
this.display.tone,
|
|
315
308
|
` reason: ${sanitizeFailureReason(this.display.reason)}`,
|
|
316
309
|
),
|
|
317
310
|
width,
|
|
@@ -323,7 +316,7 @@ export class ActivityInlineComponent implements Component {
|
|
|
323
316
|
const detail =
|
|
324
317
|
summary.kind === "evidence"
|
|
325
318
|
? ` evidence: ${summary.conclusions} conclusions · ${summary.citations} citations · ${summary.risks} risks · ${summary.blockingQuestions} blocking`
|
|
326
|
-
: ` diff: ${sanitizeDisplayText(summary.summary)} ·
|
|
319
|
+
: ` diff: ${sanitizeDisplayText(summary.summary)} · ${summary.riskCount} risks · retained: ${summary.retained ? "yes" : "no"}`;
|
|
327
320
|
return [lines[0] ?? "", line(styled(this.theme, "muted", detail), width)];
|
|
328
321
|
}
|
|
329
322
|
|
|
@@ -337,10 +330,14 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
337
330
|
}
|
|
338
331
|
|
|
339
332
|
export function summarizeDispatchResult(
|
|
340
|
-
result:
|
|
333
|
+
result: unknown,
|
|
341
334
|
): ActivitySummary | undefined {
|
|
342
|
-
|
|
343
|
-
|
|
335
|
+
const outer = asRecord(result);
|
|
336
|
+
if (!outer || outer.ok === false) return undefined;
|
|
337
|
+
const outcome = outer.ok === true ? asRecord(outer.result) : outer;
|
|
338
|
+
if (!outcome) return undefined;
|
|
339
|
+
const value =
|
|
340
|
+
outcome.kind === "candidate" ? asRecord(outcome.result) : outcome;
|
|
344
341
|
if (!value) return undefined;
|
|
345
342
|
if (
|
|
346
343
|
value.kind === "evidence" &&
|
|
@@ -360,20 +357,41 @@ export function summarizeDispatchResult(
|
|
|
360
357
|
if (
|
|
361
358
|
value.kind === "diff" &&
|
|
362
359
|
typeof value.summary === "string" &&
|
|
363
|
-
typeof value.nextStep === "string" &&
|
|
364
360
|
Array.isArray(value.risks)
|
|
365
361
|
) {
|
|
366
362
|
return {
|
|
367
363
|
kind: "diff",
|
|
368
364
|
summary: sanitizeDisplayText(value.summary),
|
|
369
|
-
nextStep: sanitizeDisplayText(value.nextStep),
|
|
370
365
|
riskCount: value.risks.length,
|
|
371
|
-
retained:
|
|
366
|
+
retained:
|
|
367
|
+
typeof outer.resultId === "string" ||
|
|
368
|
+
typeof outcome.resultId === "string",
|
|
372
369
|
};
|
|
373
370
|
}
|
|
374
371
|
return undefined;
|
|
375
372
|
}
|
|
376
373
|
|
|
374
|
+
function toneForResult(result: unknown): ActivityTone | undefined {
|
|
375
|
+
const outer = asRecord(result);
|
|
376
|
+
if (!outer || outer.ok === false) return undefined;
|
|
377
|
+
const value = outer.ok === true ? asRecord(outer.result) : outer;
|
|
378
|
+
switch (value?.kind) {
|
|
379
|
+
case "candidate":
|
|
380
|
+
case "applied":
|
|
381
|
+
case "completed":
|
|
382
|
+
return "success";
|
|
383
|
+
case "deferred":
|
|
384
|
+
case "retry":
|
|
385
|
+
case "checkpoint-required":
|
|
386
|
+
return "warning";
|
|
387
|
+
case "blocked":
|
|
388
|
+
case "cancelled":
|
|
389
|
+
return "muted";
|
|
390
|
+
default:
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
377
395
|
function terminalState(
|
|
378
396
|
event: RuntimeActivityEvent,
|
|
379
397
|
): event is RuntimeActivityEvent & {
|
|
@@ -396,6 +414,12 @@ function displayFromEvent(
|
|
|
396
414
|
phase: entry.phase,
|
|
397
415
|
objective: sanitizeDisplayText(entry.objective),
|
|
398
416
|
state: event.state as TerminalActivityState,
|
|
417
|
+
tone:
|
|
418
|
+
event.state === "failed" || event.state === "timed-out"
|
|
419
|
+
? "error"
|
|
420
|
+
: event.state === "cancelled"
|
|
421
|
+
? "muted"
|
|
422
|
+
: "success",
|
|
399
423
|
elapsedMs,
|
|
400
424
|
...(reason === undefined ? {} : { reason: sanitizeFailureReason(reason) }),
|
|
401
425
|
};
|
|
@@ -404,7 +428,7 @@ function displayFromEvent(
|
|
|
404
428
|
export function createActivityDisplay(
|
|
405
429
|
event: RuntimeActivityEvent,
|
|
406
430
|
elapsedMs: number,
|
|
407
|
-
result?:
|
|
431
|
+
result?: unknown,
|
|
408
432
|
): ActivityDisplay {
|
|
409
433
|
const entry: ActivityEntry = {
|
|
410
434
|
toolCallId: "",
|
|
@@ -419,7 +443,12 @@ export function createActivityDisplay(
|
|
|
419
443
|
};
|
|
420
444
|
const display = displayFromEvent(entry, event, elapsedMs);
|
|
421
445
|
const summary = result ? summarizeDispatchResult(result) : undefined;
|
|
422
|
-
|
|
446
|
+
const tone = result ? toneForResult(result) : undefined;
|
|
447
|
+
return {
|
|
448
|
+
...display,
|
|
449
|
+
...(tone === undefined ? {} : { tone }),
|
|
450
|
+
...(summary === undefined ? {} : { summary }),
|
|
451
|
+
};
|
|
423
452
|
}
|
|
424
453
|
|
|
425
454
|
function activityPartial(display: ActivityDisplay | ActivitySnapshot): unknown {
|
|
@@ -538,15 +567,17 @@ export class ActivityController {
|
|
|
538
567
|
this.syncUi();
|
|
539
568
|
}
|
|
540
569
|
|
|
541
|
-
finalize(
|
|
542
|
-
toolCallId: string,
|
|
543
|
-
result: DispatchResult,
|
|
544
|
-
): ActivityDisplay | undefined {
|
|
570
|
+
finalize(toolCallId: string, result: unknown): ActivityDisplay | undefined {
|
|
545
571
|
const display = this.terminals.get(toolCallId);
|
|
546
572
|
if (!display) return undefined;
|
|
547
573
|
this.terminals.delete(toolCallId);
|
|
548
574
|
const summary = summarizeDispatchResult(result);
|
|
549
|
-
|
|
575
|
+
const tone = toneForResult(result);
|
|
576
|
+
return {
|
|
577
|
+
...display,
|
|
578
|
+
...(tone === undefined ? {} : { tone }),
|
|
579
|
+
...(summary === undefined ? {} : { summary }),
|
|
580
|
+
};
|
|
550
581
|
}
|
|
551
582
|
|
|
552
583
|
getActiveEntries(): readonly ActivitySnapshot[] {
|
package/src/submit-tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import {
|
|
4
|
+
type CandidateFailure,
|
|
4
5
|
type DiffResult,
|
|
5
6
|
type EvidenceResult,
|
|
6
7
|
validateDiffResult,
|
|
@@ -36,7 +37,6 @@ const diffSchema = Type.Object({
|
|
|
36
37
|
diff: Type.String(),
|
|
37
38
|
expectedVerification: Type.String(),
|
|
38
39
|
risks: Type.Array(Type.String()),
|
|
39
|
-
nextStep: Type.String(),
|
|
40
40
|
contractCompliant: Type.Literal(true),
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -69,6 +69,7 @@ export function createSubmitTool(input: {
|
|
|
69
69
|
output: "evidence" | "diff";
|
|
70
70
|
}) {
|
|
71
71
|
let submitted: EvidenceResult | DiffResult | undefined;
|
|
72
|
+
let failure: CandidateFailure | undefined;
|
|
72
73
|
let attempts = 0;
|
|
73
74
|
let schema: "valid" | "invalid" = "invalid";
|
|
74
75
|
const identity: IdentityOutcome = {
|
|
@@ -94,6 +95,7 @@ export function createSubmitTool(input: {
|
|
|
94
95
|
identity.task = false;
|
|
95
96
|
identity.phase = false;
|
|
96
97
|
schema = "invalid";
|
|
98
|
+
failure = { kind: "artifact", code: "invalid-structural-result" };
|
|
97
99
|
throw new Error("submitted result is not an object");
|
|
98
100
|
}
|
|
99
101
|
if (value.id !== input.requestId) identity.request = false;
|
|
@@ -111,11 +113,17 @@ export function createSubmitTool(input: {
|
|
|
111
113
|
const matches =
|
|
112
114
|
identity.request && identity.role && identity.task && identity.phase;
|
|
113
115
|
if (!validation.ok || !matches) {
|
|
116
|
+
failure =
|
|
117
|
+
validation.failure ??
|
|
118
|
+
({ kind: "artifact", code: "invalid-structural-result" } as const);
|
|
114
119
|
throw new Error(
|
|
115
120
|
validation.reason ?? "submitted result identity does not match",
|
|
116
121
|
);
|
|
117
122
|
}
|
|
118
|
-
if (submitted)
|
|
123
|
+
if (submitted) {
|
|
124
|
+
failure = { kind: "artifact", code: "invalid-structural-result" };
|
|
125
|
+
throw new Error("duplicate structural submission");
|
|
126
|
+
}
|
|
119
127
|
submitted = structuredClone(value) as unknown as
|
|
120
128
|
| EvidenceResult
|
|
121
129
|
| DiffResult;
|
|
@@ -133,5 +141,7 @@ export function createSubmitTool(input: {
|
|
|
133
141
|
getAttempts: () => attempts,
|
|
134
142
|
getSchema: () => schema,
|
|
135
143
|
getIdentity: () => ({ ...identity }),
|
|
144
|
+
getFailure: () =>
|
|
145
|
+
failure === undefined ? undefined : structuredClone(failure),
|
|
136
146
|
};
|
|
137
147
|
}
|