@abelxiaoxing/cadence 1.0.14 → 1.1.0

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 (46) hide show
  1. package/README.md +29 -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 +150 -228
  10. package/prompts/abel-diagnose.md +37 -18
  11. package/prompts/abel-implement.md +111 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +156 -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 +105 -696
  18. package/src/control-contracts.ts +259 -0
  19. package/src/delivery-compiler.ts +1088 -0
  20. package/src/file-snapshot.ts +5 -0
  21. package/src/implement-graph.ts +11 -4
  22. package/src/index.ts +1465 -133
  23. package/src/isolation-backend.ts +269 -0
  24. package/src/packet-runtime.ts +533 -0
  25. package/src/parent-provider.ts +23 -4
  26. package/src/route-policy.ts +483 -0
  27. package/src/run-state.ts +393 -0
  28. package/src/run-store.ts +918 -0
  29. package/src/safe-path.ts +11 -0
  30. package/src/scoped-tools.ts +36 -0
  31. package/src/state-root.ts +215 -0
  32. package/src/subagent-activity.ts +526 -27
  33. package/src/submit-tool.ts +274 -0
  34. package/src/task-ledger.ts +1380 -0
  35. package/src/verification-capability.ts +529 -42
  36. package/src/worker-broker.ts +703 -0
  37. package/src/workflow-engine.ts +7391 -0
  38. package/src/workspace-store.ts +544 -0
  39. package/src/candidate-preflight.ts +0 -1618
  40. package/src/drain.ts +0 -20
  41. package/src/patch.ts +0 -505
  42. package/src/result-store.ts +0 -312
  43. package/src/runtime.ts +0 -2436
  44. package/src/scheduler.ts +0 -384
  45. package/src/subagent-endpoint.ts +0 -158
  46. package/src/worker.ts +0 -204
@@ -10,10 +10,10 @@ import {
10
10
  visibleWidth,
11
11
  } from "@earendil-works/pi-tui";
12
12
  import type {
13
- RuntimeActivityEvent,
14
- RuntimeActivityObserver,
15
- RuntimeActivityState,
16
- } from "./runtime.ts";
13
+ PacketActivityEvent,
14
+ PacketActivityObserver,
15
+ PacketActivityState,
16
+ } from "./packet-runtime.ts";
17
17
 
18
18
  export const ACTIVITY_DETAILS_KEY = "activityDisplay" as const;
19
19
  export const ACTIVITY_WIDGET_KEY = "abel-subagents" as const;
@@ -22,10 +22,51 @@ export const ACTIVITY_REFRESH_MS = 100;
22
22
  export const ACTIVITY_WIDGET_MAX_LINES = 12;
23
23
  export const ACTIVITY_WIDGET_MIN_WIDTH = 3;
24
24
 
25
- export type TerminalActivityState = Exclude<
26
- RuntimeActivityState,
27
- "queued" | "running"
28
- >;
25
+ export const WORKFLOW_ACTIVITY_STATES = [
26
+ "queued",
27
+ "connecting",
28
+ "waiting-first-response",
29
+ "running",
30
+ "validating",
31
+ "retrying",
32
+ "verifying",
33
+ "paused",
34
+ "approval-needed",
35
+ "applying",
36
+ "recovering",
37
+ "operation-cancelled",
38
+ "discarded",
39
+ "rejected",
40
+ "completed",
41
+ ] as const;
42
+
43
+ export type WorkflowActivityState = (typeof WORKFLOW_ACTIVITY_STATES)[number];
44
+ export type ActivityState = PacketActivityState | WorkflowActivityState;
45
+ export type TerminalActivityState =
46
+ | Exclude<
47
+ PacketActivityState,
48
+ | "queued"
49
+ | "connecting"
50
+ | "waiting-first-response"
51
+ | "running"
52
+ | "retrying"
53
+ >
54
+ | WorkflowActivityState;
55
+
56
+ export interface WorkflowActivityUpdate {
57
+ state: WorkflowActivityState;
58
+ stage?: "abel-design" | "abel-implement";
59
+ runId?: string;
60
+ change?: string;
61
+ taskId?: string;
62
+ phase?: string;
63
+ objective?: string;
64
+ code?: string;
65
+ attempt?: number;
66
+ maxAttempts?: number;
67
+ wait?: string;
68
+ legalCommands?: string[];
69
+ }
29
70
 
30
71
  export interface EvidenceActivitySummary {
31
72
  kind: "evidence";
@@ -43,7 +84,7 @@ export interface DiffActivitySummary {
43
84
  }
44
85
 
45
86
  export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
46
- export type ActivityTone = "success" | "warning" | "muted" | "error";
87
+ export type ActivityTone = "accent" | "success" | "warning" | "muted" | "error";
47
88
 
48
89
  export interface ActivityDisplay {
49
90
  version: 1;
@@ -57,6 +98,13 @@ export interface ActivityDisplay {
57
98
  elapsedMs: number;
58
99
  summary?: ActivitySummary;
59
100
  reason?: string;
101
+ runId?: string;
102
+ taskId?: string;
103
+ code?: string;
104
+ nextAction?: string;
105
+ attempt?: number;
106
+ maxAttempts?: number;
107
+ wait?: string;
60
108
  }
61
109
 
62
110
  export interface ActivitySnapshot {
@@ -65,11 +113,19 @@ export interface ActivitySnapshot {
65
113
  role: string;
66
114
  phase: string;
67
115
  objective: string;
68
- state: "queued" | "running";
116
+ state: ActivityState;
69
117
  sequence: number;
70
118
  startedAt: number;
71
119
  elapsedMs: number;
72
120
  spinnerFrame?: number;
121
+ tone?: ActivityTone;
122
+ runId?: string;
123
+ taskId?: string;
124
+ code?: string;
125
+ nextAction?: string;
126
+ attempt?: number;
127
+ maxAttempts?: number;
128
+ wait?: string;
73
129
  }
74
130
 
75
131
  interface ActivityEntry extends ActivitySnapshot {
@@ -131,6 +187,26 @@ export function sanitizeDisplayText(value: unknown, maxLength = 240): string {
131
187
  return safe.slice(0, Math.max(0, maxLength));
132
188
  }
133
189
 
190
+ const SAFE_WORKFLOW_CODE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
191
+ const SAFE_CONTROL_ACTIONS = new Set([
192
+ "start",
193
+ "status",
194
+ "resume",
195
+ "rebind",
196
+ "cancel",
197
+ "discard",
198
+ ]);
199
+
200
+ export function sanitizeWorkflowCode(value: unknown): string {
201
+ const code = typeof value === "string" ? value : "";
202
+ return SAFE_WORKFLOW_CODE.test(code) ? code : "workflow-state-unavailable";
203
+ }
204
+
205
+ function sanitizeControlAction(value: unknown): string {
206
+ const action = typeof value === "string" ? value : "";
207
+ return SAFE_CONTROL_ACTIONS.has(action) ? action : "status";
208
+ }
209
+
134
210
  const SAFE_FAILURE_REASONS = new Set([
135
211
  "subagent failed",
136
212
  "subagent cancelled",
@@ -159,18 +235,32 @@ export function formatElapsed(milliseconds: number): string {
159
235
 
160
236
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
161
237
 
162
- function stateGlyph(state: RuntimeActivityState, spinnerFrame = 0): string {
238
+ function stateGlyph(state: ActivityState, spinnerFrame = 0): string {
163
239
  switch (state) {
164
240
  case "queued":
165
241
  return "…";
242
+ case "connecting":
243
+ case "waiting-first-response":
166
244
  case "running":
245
+ case "validating":
246
+ case "retrying":
247
+ case "verifying":
248
+ case "applying":
249
+ case "recovering":
167
250
  return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] ?? "⠋";
168
251
  case "completed":
169
252
  return "✓";
170
253
  case "failed":
254
+ case "rejected":
171
255
  return "✗";
172
256
  case "cancelled":
257
+ case "operation-cancelled":
173
258
  return "⊘";
259
+ case "discarded":
260
+ return "◇";
261
+ case "paused":
262
+ case "approval-needed":
263
+ return "!";
174
264
  case "timed-out":
175
265
  return "⌛";
176
266
  }
@@ -183,10 +273,40 @@ function line(value: string, width: number): string {
183
273
  : truncateToWidth(bounded, boundedWidth(width), "");
184
274
  }
185
275
 
186
- function stateText(state: RuntimeActivityState): string {
276
+ function stateText(state: ActivityState): string {
187
277
  return state;
188
278
  }
189
279
 
280
+ function activityMetadata(
281
+ snapshot: ActivitySnapshot | ActivityDisplay,
282
+ ): string[] {
283
+ const metadata: string[] = [];
284
+ if (snapshot.taskId) {
285
+ metadata.push(`task ${sanitizeDisplayText(snapshot.taskId, 80)}`);
286
+ }
287
+ if (snapshot.code) {
288
+ metadata.push(`code ${sanitizeWorkflowCode(snapshot.code)}`);
289
+ }
290
+ if (
291
+ Number.isSafeInteger(snapshot.attempt) &&
292
+ (snapshot.attempt as number) > 0
293
+ ) {
294
+ metadata.push(
295
+ Number.isSafeInteger(snapshot.maxAttempts) &&
296
+ (snapshot.maxAttempts as number) >= (snapshot.attempt as number)
297
+ ? `attempt ${snapshot.attempt}/${snapshot.maxAttempts}`
298
+ : `attempt ${snapshot.attempt}`,
299
+ );
300
+ }
301
+ if (snapshot.wait) {
302
+ metadata.push(`wait ${sanitizeDisplayText(snapshot.wait, 80)}`);
303
+ }
304
+ if (snapshot.nextAction) {
305
+ metadata.push(`next ${sanitizeControlAction(snapshot.nextAction)}`);
306
+ }
307
+ return metadata;
308
+ }
309
+
190
310
  function styled(
191
311
  theme: ActivityTheme | undefined,
192
312
  color: ActivityColor,
@@ -221,8 +341,10 @@ function renderActivityLines(
221
341
  stateText(snapshot.state),
222
342
  formatElapsed(elapsed),
223
343
  ].join(" · ");
224
- const second = ` ${sanitizeDisplayText(snapshot.objective, 240)}`;
225
- const color = "tone" in snapshot ? snapshot.tone : "accent";
344
+ const metadata = activityMetadata(snapshot);
345
+ const objective = sanitizeDisplayText(snapshot.objective, 240);
346
+ const second = ` ${[objective, ...metadata].filter(Boolean).join(" · ")}`;
347
+ const color = "tone" in snapshot && snapshot.tone ? snapshot.tone : "accent";
226
348
  return [
227
349
  line(styled(theme, color, first), width),
228
350
  line(styled(theme, "muted", second), width),
@@ -246,7 +368,7 @@ export function renderActivityWidgetLines(
246
368
  lines.push(...renderActivityLines(entry, width, undefined, _now));
247
369
  }
248
370
  if (hidden.length > 0) {
249
- const running = hidden.filter((entry) => entry.state === "running").length;
371
+ const running = hidden.filter((entry) => entry.state !== "queued").length;
250
372
  const queued = hidden.filter((entry) => entry.state === "queued").length;
251
373
  const overflow = [
252
374
  `+${hidden.length} more (${running} running, ${queued} queued)`,
@@ -334,6 +456,243 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
334
456
  : undefined;
335
457
  }
336
458
 
459
+ function workflowStateOf(
460
+ result: Record<string, unknown>,
461
+ ): WorkflowActivityState {
462
+ const pause = asRecord(result.pause);
463
+ const operation = asRecord(result.operation);
464
+ if (
465
+ pause?.code === "operation-cancelled" ||
466
+ operation?.kind === "operation-cancelled" ||
467
+ result.state === "operation-cancelled"
468
+ ) {
469
+ return "operation-cancelled";
470
+ }
471
+ const state = result.state;
472
+ switch (state) {
473
+ case "created":
474
+ case "ready":
475
+ case "queued":
476
+ return "queued";
477
+ case "connecting":
478
+ return "connecting";
479
+ case "waiting-first-response":
480
+ return "waiting-first-response";
481
+ case "running":
482
+ return "running";
483
+ case "validating-delivery":
484
+ case "validating":
485
+ return "validating";
486
+ case "retryable":
487
+ case "retrying":
488
+ return "retrying";
489
+ case "verifying":
490
+ case "change-verifying":
491
+ case "ready-to-apply":
492
+ return "verifying";
493
+ case "paused":
494
+ return "paused";
495
+ case "approval-needed":
496
+ return "approval-needed";
497
+ case "applying":
498
+ return "applying";
499
+ case "recovering":
500
+ return "recovering";
501
+ case "discarded":
502
+ return "discarded";
503
+ case "rejected":
504
+ return "rejected";
505
+ case "completed":
506
+ return result.completed === true ? "completed" : "rejected";
507
+ default:
508
+ return "running";
509
+ }
510
+ }
511
+
512
+ function workflowTone(state: WorkflowActivityState): ActivityTone {
513
+ switch (state) {
514
+ case "completed":
515
+ return "success";
516
+ case "rejected":
517
+ return "error";
518
+ case "retrying":
519
+ case "paused":
520
+ case "approval-needed":
521
+ return "warning";
522
+ case "operation-cancelled":
523
+ case "discarded":
524
+ return "muted";
525
+ default:
526
+ return "accent";
527
+ }
528
+ }
529
+
530
+ function legalWorkflowCommands(result: Record<string, unknown>): string[] {
531
+ return Array.isArray(result.legalCommands)
532
+ ? result.legalCommands.filter(
533
+ (command): command is string =>
534
+ typeof command === "string" && SAFE_CONTROL_ACTIONS.has(command),
535
+ )
536
+ : [];
537
+ }
538
+
539
+ function nextWorkflowAction(
540
+ state: WorkflowActivityState,
541
+ commands: readonly string[],
542
+ ): string | undefined {
543
+ const preferred =
544
+ state === "paused" ||
545
+ state === "approval-needed" ||
546
+ state === "retrying" ||
547
+ state === "operation-cancelled"
548
+ ? ["resume", "status", "discard"]
549
+ : state === "completed" || state === "discarded" || state === "rejected"
550
+ ? ["status"]
551
+ : ["status", "cancel", "discard"];
552
+ return preferred.find((command) => commands.includes(command)) ?? commands[0];
553
+ }
554
+
555
+ function firstWorkflowTask(result: Record<string, unknown>):
556
+ | {
557
+ taskId?: string;
558
+ phase?: string;
559
+ }
560
+ | undefined {
561
+ if (!Array.isArray(result.tasks)) return undefined;
562
+ const records = result.tasks
563
+ .map(asRecord)
564
+ .filter((task): task is Record<string, unknown> => task !== undefined);
565
+ const task =
566
+ records.find((candidate) => candidate.state !== "verified") ?? records[0];
567
+ return task
568
+ ? {
569
+ ...(typeof task.taskId === "string" ? { taskId: task.taskId } : {}),
570
+ ...(typeof task.phase === "string" ? { phase: task.phase } : {}),
571
+ }
572
+ : undefined;
573
+ }
574
+
575
+ function positiveInteger(value: unknown): number | undefined {
576
+ return Number.isSafeInteger(value) && (value as number) > 0
577
+ ? (value as number)
578
+ : undefined;
579
+ }
580
+
581
+ function workflowWait(
582
+ state: WorkflowActivityState,
583
+ result: Record<string, unknown>,
584
+ ): string | undefined {
585
+ if (typeof result.wait === "string") {
586
+ return sanitizeDisplayText(result.wait, 80);
587
+ }
588
+ const approval = asRecord(result.approval);
589
+ if (
590
+ state === "approval-needed" &&
591
+ (approval?.gate === "gate-a" || approval?.gate === "gate-b")
592
+ ) {
593
+ return approval.gate;
594
+ }
595
+ if (state === "connecting") return "connection";
596
+ if (state === "waiting-first-response") return "first-response";
597
+ if (state === "retrying") return "bounded-policy";
598
+ const queue = Array.isArray(result.queue)
599
+ ? asRecord(result.queue[0])
600
+ : undefined;
601
+ const position = positiveInteger(queue?.position);
602
+ return state === "queued" && position ? `queue-${position}` : undefined;
603
+ }
604
+
605
+ function workflowCode(result: Record<string, unknown>): string | undefined {
606
+ const pause = asRecord(result.pause);
607
+ const raw =
608
+ typeof result.code === "string"
609
+ ? result.code
610
+ : typeof pause?.code === "string"
611
+ ? pause.code
612
+ : undefined;
613
+ return raw === undefined ? undefined : sanitizeWorkflowCode(raw);
614
+ }
615
+
616
+ export function projectWorkflowActivity(
617
+ args: unknown,
618
+ result: unknown,
619
+ elapsedMs: number,
620
+ ): ActivityDisplay {
621
+ const command = asRecord(args) ?? {};
622
+ const payload = asRecord(result) ?? {};
623
+ const state = workflowStateOf(payload);
624
+ const task = firstWorkflowTask(payload);
625
+ const stage =
626
+ payload.stage === "abel-design" || payload.stage === "abel-implement"
627
+ ? payload.stage
628
+ : command.stage === "abel-design" || command.stage === "abel-implement"
629
+ ? command.stage
630
+ : "abel-control";
631
+ const change =
632
+ typeof payload.change === "string"
633
+ ? payload.change
634
+ : typeof command.change === "string"
635
+ ? command.change
636
+ : undefined;
637
+ const runId = typeof payload.runId === "string" ? payload.runId : undefined;
638
+ const taskId =
639
+ typeof payload.taskId === "string" ? payload.taskId : task?.taskId;
640
+ const phase =
641
+ typeof payload.phase === "string"
642
+ ? payload.phase
643
+ : (task?.phase ??
644
+ (typeof command.command === "string" ? command.command : "control"));
645
+ const objective =
646
+ typeof payload.objective === "string"
647
+ ? payload.objective
648
+ : change
649
+ ? `change ${change}`
650
+ : `${String(command.command ?? "control")} workflow run`;
651
+ const legalCommands = legalWorkflowCommands(payload);
652
+ const policy = asRecord(payload.policy);
653
+ const attempt = positiveInteger(payload.attempt ?? policy?.attempt);
654
+ const maxAttempts = positiveInteger(
655
+ payload.maxAttempts ?? policy?.maxAttempts,
656
+ );
657
+ const code =
658
+ state === "rejected" && payload.state === "completed"
659
+ ? "completion-state-inconsistent"
660
+ : workflowCode(payload);
661
+ return {
662
+ version: 1,
663
+ kind: "activityDisplay",
664
+ requestId: sanitizeDisplayText(
665
+ runId ?? taskId ?? change ?? command.operationId ?? "pending",
666
+ 128,
667
+ ),
668
+ role: sanitizeDisplayText(stage, 80),
669
+ phase: sanitizeDisplayText(phase, 40),
670
+ objective: sanitizeDisplayText(objective, 240),
671
+ state,
672
+ tone: workflowTone(state),
673
+ elapsedMs: Math.max(0, elapsedMs),
674
+ ...(runId ? { runId: sanitizeDisplayText(runId, 128) } : {}),
675
+ ...(taskId ? { taskId: sanitizeDisplayText(taskId, 128) } : {}),
676
+ ...(code ? { code } : {}),
677
+ ...(attempt ? { attempt } : {}),
678
+ ...(maxAttempts ? { maxAttempts } : {}),
679
+ ...(workflowWait(state, payload)
680
+ ? { wait: workflowWait(state, payload) }
681
+ : {}),
682
+ ...(nextWorkflowAction(state, legalCommands)
683
+ ? { nextAction: nextWorkflowAction(state, legalCommands) }
684
+ : {}),
685
+ };
686
+ }
687
+
688
+ function activityResultFromUpdate(update: WorkflowActivityUpdate): unknown {
689
+ return {
690
+ ...update,
691
+ ...(update.state === "completed" ? { completed: true } : {}),
692
+ ...(update.code ? { pause: { code: update.code } } : {}),
693
+ };
694
+ }
695
+
337
696
  export function summarizeDispatchResult(
338
697
  result: unknown,
339
698
  ): ActivitySummary | undefined {
@@ -413,16 +772,22 @@ function toneForResult(result: unknown): ActivityTone | undefined {
413
772
  }
414
773
 
415
774
  function terminalState(
416
- event: RuntimeActivityEvent,
417
- ): event is RuntimeActivityEvent & {
775
+ event: PacketActivityEvent,
776
+ ): event is PacketActivityEvent & {
418
777
  state: TerminalActivityState;
419
778
  } {
420
- return !["queued", "running"].includes(event.state);
779
+ return ![
780
+ "queued",
781
+ "connecting",
782
+ "waiting-first-response",
783
+ "running",
784
+ "retrying",
785
+ ].includes(event.state);
421
786
  }
422
787
 
423
788
  function displayFromEvent(
424
789
  entry: ActivityEntry,
425
- event: RuntimeActivityEvent,
790
+ event: PacketActivityEvent,
426
791
  elapsedMs: number,
427
792
  ): ActivityDisplay {
428
793
  const reason = event.failureReason;
@@ -434,6 +799,10 @@ function displayFromEvent(
434
799
  phase: entry.phase,
435
800
  objective: sanitizeDisplayText(entry.objective),
436
801
  state: event.state as TerminalActivityState,
802
+ ...(entry.attempt ? { attempt: entry.attempt } : {}),
803
+ ...(entry.maxAttempts ? { maxAttempts: entry.maxAttempts } : {}),
804
+ ...(entry.code ? { code: entry.code } : {}),
805
+ ...(entry.wait ? { wait: entry.wait } : {}),
437
806
  tone:
438
807
  event.state === "failed" || event.state === "timed-out"
439
808
  ? "error"
@@ -446,7 +815,7 @@ function displayFromEvent(
446
815
  }
447
816
 
448
817
  export function createActivityDisplay(
449
- event: RuntimeActivityEvent,
818
+ event: PacketActivityEvent,
450
819
  elapsedMs: number,
451
820
  result?: unknown,
452
821
  ): ActivityDisplay {
@@ -456,10 +825,14 @@ export function createActivityDisplay(
456
825
  role: event.role,
457
826
  phase: event.phase,
458
827
  objective: event.objective,
459
- state: event.state === "queued" ? "queued" : "running",
828
+ state: event.state,
460
829
  sequence: event.sequence,
461
830
  startedAt: 0,
462
831
  elapsedMs,
832
+ ...(event.attempt ? { attempt: event.attempt } : {}),
833
+ ...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
834
+ ...(event.code ? { code: event.code } : {}),
835
+ ...(event.wait ? { wait: event.wait } : {}),
463
836
  };
464
837
  const display = displayFromEvent(entry, event, elapsedMs);
465
838
  const summary = result ? summarizeDispatchResult(result) : undefined;
@@ -496,6 +869,7 @@ export class ActivityController {
496
869
  private timer?: TimerHandle | unknown;
497
870
  private requestRender?: () => void;
498
871
  private widgetInstalled = false;
872
+ private controlSequence = 0;
499
873
  private readonly now: () => number;
500
874
  private readonly setIntervalFn: (
501
875
  callback: () => void,
@@ -535,17 +909,113 @@ export class ActivityController {
535
909
  this.clearUi();
536
910
  }
537
911
 
912
+ beginWorkflow(
913
+ toolCallId: string,
914
+ args: unknown,
915
+ onUpdate?: (result: unknown) => void,
916
+ ): void {
917
+ if (!this.accepting || this.entries.has(toolCallId)) return;
918
+ const display = projectWorkflowActivity(args, { state: "queued" }, 0);
919
+ const entry: ActivityEntry = {
920
+ toolCallId,
921
+ requestId: display.requestId,
922
+ role: display.role,
923
+ phase: display.phase,
924
+ objective: display.objective,
925
+ state: display.state,
926
+ sequence: ++this.controlSequence,
927
+ startedAt: this.now(),
928
+ elapsedMs: 0,
929
+ spinnerFrame: this.spinnerFrame,
930
+ tone: display.tone,
931
+ onUpdate,
932
+ };
933
+ this.entries.set(toolCallId, entry);
934
+ this.emit(entry, entry);
935
+ this.syncUi();
936
+ }
937
+
938
+ updateWorkflow(
939
+ toolCallId: string,
940
+ args: unknown,
941
+ update: WorkflowActivityUpdate,
942
+ ): void {
943
+ const entry = this.entries.get(toolCallId);
944
+ if (!this.accepting || !entry) return;
945
+ const display = projectWorkflowActivity(
946
+ args,
947
+ activityResultFromUpdate(update),
948
+ Math.max(0, this.now() - entry.startedAt),
949
+ );
950
+ Object.assign(entry, {
951
+ requestId: display.requestId,
952
+ role: display.role,
953
+ phase: display.phase,
954
+ objective: display.objective,
955
+ state: display.state,
956
+ elapsedMs: display.elapsedMs,
957
+ tone: display.tone,
958
+ runId: display.runId,
959
+ taskId: display.taskId,
960
+ code: display.code,
961
+ nextAction: display.nextAction,
962
+ attempt: display.attempt,
963
+ maxAttempts: display.maxAttempts,
964
+ wait: display.wait,
965
+ });
966
+ this.emit(entry, entry);
967
+ this.syncUi();
968
+ }
969
+
970
+ finalizeWorkflow(
971
+ toolCallId: string,
972
+ args: unknown,
973
+ result: unknown,
974
+ ): ActivityDisplay | undefined {
975
+ const entry = this.entries.get(toolCallId);
976
+ if (!entry) return undefined;
977
+ const display = projectWorkflowActivity(
978
+ args,
979
+ result,
980
+ Math.max(0, this.now() - entry.startedAt),
981
+ );
982
+ this.emit(entry, display);
983
+ this.entries.delete(toolCallId);
984
+ this.syncUi();
985
+ return display;
986
+ }
987
+
988
+ failWorkflow(toolCallId: string): void {
989
+ const entry = this.entries.get(toolCallId);
990
+ if (!entry) return;
991
+ const display: ActivityDisplay = {
992
+ version: 1,
993
+ kind: "activityDisplay",
994
+ requestId: entry.requestId,
995
+ role: entry.role,
996
+ phase: entry.phase,
997
+ objective: entry.objective,
998
+ state: "failed",
999
+ tone: "error",
1000
+ elapsedMs: Math.max(0, this.now() - entry.startedAt),
1001
+ reason: "subagent failed",
1002
+ };
1003
+ this.emit(entry, display);
1004
+ this.entries.delete(toolCallId);
1005
+ this.syncUi();
1006
+ }
1007
+
538
1008
  observe(
539
1009
  toolCallId: string,
540
1010
  onUpdate?: (result: unknown) => void,
541
- ): RuntimeActivityObserver {
1011
+ ): PacketActivityObserver {
542
1012
  return (event) => this.accept(toolCallId, onUpdate, event);
543
1013
  }
544
1014
 
545
1015
  accept(
546
1016
  toolCallId: string,
547
1017
  onUpdate: ((result: unknown) => void) | undefined,
548
- event: RuntimeActivityEvent,
1018
+ event: PacketActivityEvent,
549
1019
  ): void {
550
1020
  if (!this.accepting || this.terminals.has(toolCallId)) return;
551
1021
  let entry = this.entries.get(toolCallId);
@@ -556,17 +1026,25 @@ export class ActivityController {
556
1026
  role: event.role,
557
1027
  phase: event.phase,
558
1028
  objective: sanitizeDisplayText(event.objective),
559
- state: event.state === "queued" ? "queued" : "running",
1029
+ state: event.state,
560
1030
  sequence: event.sequence,
561
1031
  startedAt: this.now(),
562
1032
  elapsedMs: 0,
563
1033
  spinnerFrame: this.spinnerFrame,
1034
+ ...(event.attempt ? { attempt: event.attempt } : {}),
1035
+ ...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
1036
+ ...(event.code ? { code: event.code } : {}),
1037
+ ...(event.wait ? { wait: event.wait } : {}),
564
1038
  onUpdate,
565
1039
  };
566
1040
  this.entries.set(toolCallId, entry);
567
1041
  } else {
568
1042
  entry.onUpdate = onUpdate ?? entry.onUpdate;
569
- if (event.state === "running") entry.state = "running";
1043
+ entry.state = event.state;
1044
+ entry.attempt = event.attempt;
1045
+ entry.maxAttempts = event.maxAttempts;
1046
+ entry.code = event.code;
1047
+ entry.wait = event.wait;
570
1048
  }
571
1049
  if (terminalState(event)) {
572
1050
  const display = displayFromEvent(
@@ -701,7 +1179,7 @@ export class ActivityController {
701
1179
  this.startTimer();
702
1180
  try {
703
1181
  const running = [...this.entries.values()].filter(
704
- (entry) => entry.state === "running",
1182
+ (entry) => entry.state !== "queued",
705
1183
  ).length;
706
1184
  const queued = this.entries.size - running;
707
1185
  this.ui.setStatus(
@@ -772,7 +1250,11 @@ export function renderActivityResult(
772
1250
  return new ActivityInlineComponent(display, theme);
773
1251
  }
774
1252
  if (failed) {
775
- return new Text(`${dispatchLabel(args?.action)} failed`, 0, 0);
1253
+ return new Text(
1254
+ `${dispatchLabel(args?.action ?? args?.command)} failed`,
1255
+ 0,
1256
+ 0,
1257
+ );
776
1258
  }
777
1259
  const content = asRecord(result)?.content;
778
1260
  const text = Array.isArray(content)
@@ -838,6 +1320,23 @@ export function renderActivityCall(
838
1320
  context?: ActivityRenderContext,
839
1321
  ): Component {
840
1322
  const value = asRecord(args);
1323
+ if (typeof value?.command === "string" && value.version === 2) {
1324
+ if (context?.executionStarted || context?.isError) return new Text("");
1325
+ const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
1326
+ const display: ActivitySnapshot = {
1327
+ toolCallId: "call",
1328
+ requestId: projected.requestId,
1329
+ role: projected.role,
1330
+ phase: projected.phase,
1331
+ objective: projected.objective,
1332
+ state: projected.state,
1333
+ sequence: 0,
1334
+ startedAt: Date.now(),
1335
+ elapsedMs: 0,
1336
+ tone: projected.tone,
1337
+ };
1338
+ return new ActivityInlineComponent(display, theme);
1339
+ }
841
1340
  if (value?.action !== "run") {
842
1341
  return new Text(context?.isError ? "" : dispatchLabel(value?.action), 0, 0);
843
1342
  }