@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/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: RequestEnvelope,
82
+ other: ConflictDeclaration,
59
83
  ): boolean {
60
84
  return conflicts.some(
61
85
  (edge) =>
62
86
  edge === other.taskId ||
63
- edge === other.id ||
64
- other.declared.read.includes(edge) ||
65
- other.declared.write.includes(edge),
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
- /** True when two declared envelopes cannot be active at the same time. */
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: RequestEnvelope,
72
- right: RequestEnvelope,
109
+ left: ConflictDeclaration,
110
+ right: ConflictDeclaration,
73
111
  ): boolean {
74
- const a = left.declared;
75
- const b = right.declared;
76
- if (intersects(a.write, b.write)) return true;
77
- if (intersects(a.write, b.read) || intersects(a.read, b.write)) return true;
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 (declarationsConflict(entry.request, activeEntry.request)) {
276
+ if (
277
+ declarationsConflict(
278
+ requestConflictOf(entry.request),
279
+ requestConflictOf(activeEntry.request),
280
+ )
281
+ ) {
239
282
  return true;
240
283
  }
241
284
  }
@@ -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
- "error",
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)} · next: ${sanitizeDisplayText(summary.nextStep)} · ${summary.riskCount} risks · retained: ${summary.retained ? "yes" : "no"}`;
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: DispatchResult,
333
+ result: unknown,
341
334
  ): ActivitySummary | undefined {
342
- if (!result.ok) return undefined;
343
- const value = asRecord(result.result);
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: typeof result.resultId === "string",
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?: DispatchResult,
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
- return summary ? { ...display, summary } : display;
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
- return summary ? { ...display, summary } : display;
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[] {
@@ -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) throw new Error("duplicate structural submission");
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
  }