@bastani/atomic 0.8.28-alpha.2 → 0.8.28-alpha.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/CHANGELOG.md +18 -0
  7. package/dist/builtin/workflows/README.md +1 -1
  8. package/dist/builtin/workflows/package.json +1 -1
  9. package/dist/builtin/workflows/src/authoring.d.ts +5 -2
  10. package/dist/builtin/workflows/src/extension/dispatcher.ts +2 -0
  11. package/dist/builtin/workflows/src/extension/index.ts +8 -0
  12. package/dist/builtin/workflows/src/extension/render-result.ts +5 -2
  13. package/dist/builtin/workflows/src/extension/workflow-schema.ts +18 -0
  14. package/dist/builtin/workflows/src/runs/background/status.ts +4 -0
  15. package/dist/builtin/workflows/src/runs/foreground/executor.ts +1251 -110
  16. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +34 -10
  17. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +10 -2
  18. package/dist/builtin/workflows/src/shared/persistence-restore.ts +28 -9
  19. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +9 -3
  20. package/dist/builtin/workflows/src/shared/store-types.ts +10 -3
  21. package/dist/builtin/workflows/src/shared/store.ts +29 -7
  22. package/dist/builtin/workflows/src/shared/types.ts +12 -10
  23. package/dist/builtin/workflows/src/tui/run-detail.ts +12 -0
  24. package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
  25. package/dist/builtin/workflows/src/tui/status-list.ts +15 -1
  26. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +1 -1
  27. package/dist/builtin/workflows/src/tui/widget.ts +12 -3
  28. package/dist/builtin/workflows/src/workflows/define-workflow.ts +3 -3
  29. package/dist/core/agent-session-services.d.ts +1 -0
  30. package/dist/core/agent-session-services.d.ts.map +1 -1
  31. package/dist/core/agent-session-services.js +1 -0
  32. package/dist/core/agent-session-services.js.map +1 -1
  33. package/dist/core/agent-session.d.ts +4 -0
  34. package/dist/core/agent-session.d.ts.map +1 -1
  35. package/dist/core/agent-session.js +12 -1
  36. package/dist/core/agent-session.js.map +1 -1
  37. package/dist/core/index.d.ts +1 -0
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js +1 -0
  40. package/dist/core/index.js.map +1 -1
  41. package/dist/core/sdk.d.ts +4 -2
  42. package/dist/core/sdk.d.ts.map +1 -1
  43. package/dist/core/sdk.js +1 -0
  44. package/dist/core/sdk.js.map +1 -1
  45. package/dist/core/tools/ask-user-question/state/inline-input.d.ts +28 -0
  46. package/dist/core/tools/ask-user-question/state/inline-input.d.ts.map +1 -0
  47. package/dist/core/tools/ask-user-question/state/inline-input.js +56 -0
  48. package/dist/core/tools/ask-user-question/state/inline-input.js.map +1 -0
  49. package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -1
  50. package/dist/core/tools/ask-user-question/state/key-router.js +30 -4
  51. package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -1
  52. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
  53. package/dist/core/tools/ask-user-question/state/questionnaire-session.js +9 -8
  54. package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
  55. package/dist/core/tools/ask-user-question/state/row-intent.d.ts +3 -2
  56. package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -1
  57. package/dist/core/tools/ask-user-question/state/row-intent.js +1 -1
  58. package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -1
  59. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +2 -0
  60. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
  61. package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
  62. package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
  63. package/dist/core/tools/ask-user-question/state/selectors/projections.js +2 -0
  64. package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
  65. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
  66. package/dist/core/tools/ask-user-question/state/state-reducer.js +36 -24
  67. package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
  68. package/dist/core/tools/ask-user-question/state/state.d.ts +8 -0
  69. package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
  70. package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
  71. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +6 -0
  72. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
  73. package/dist/core/tools/ask-user-question/tool/format-answer.js +19 -1
  74. package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
  75. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +3 -2
  76. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
  77. package/dist/core/tools/ask-user-question/tool/response-envelope.js +15 -3
  78. package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
  79. package/dist/core/tools/ask-user-question/tool/types.d.ts +2 -1
  80. package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -1
  81. package/dist/core/tools/ask-user-question/tool/types.js.map +1 -1
  82. package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +5 -2
  83. package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -1
  84. package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +2 -0
  85. package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -1
  86. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +1 -0
  87. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  88. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +2 -1
  89. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  90. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +3 -3
  91. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
  92. package/dist/core/tools/ask-user-question/view/props-adapter.js +11 -4
  93. package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
  94. package/dist/core/tools/bash-policy.d.ts +62 -0
  95. package/dist/core/tools/bash-policy.d.ts.map +1 -0
  96. package/dist/core/tools/bash-policy.js +1069 -0
  97. package/dist/core/tools/bash-policy.js.map +1 -0
  98. package/dist/core/tools/bash.d.ts +5 -0
  99. package/dist/core/tools/bash.d.ts.map +1 -1
  100. package/dist/core/tools/bash.js +7 -0
  101. package/dist/core/tools/bash.js.map +1 -1
  102. package/dist/core/tools/index.d.ts +1 -0
  103. package/dist/core/tools/index.d.ts.map +1 -1
  104. package/dist/core/tools/index.js +1 -0
  105. package/dist/core/tools/index.js.map +1 -1
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +1 -1
  109. package/dist/index.js.map +1 -1
  110. package/docs/sdk.md +42 -0
  111. package/docs/security.md +2 -0
  112. package/docs/workflows.md +127 -15
  113. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  * modules here. Public custom-TUI types are type-only imports from the same
7
7
  * extension-compatible surfaces used by Atomic extension UI.
8
8
  */
9
- import type { KeybindingsManager, Theme } from "@bastani/atomic";
9
+ import type { BashCommandPolicy, KeybindingsManager, Theme } from "@bastani/atomic";
10
10
  import type { Component, OverlayHandle, OverlayOptions, TUI } from "@earendil-works/pi-tui";
11
11
  import type { Static, TOptional, TSchema } from "typebox";
12
12
  export type { Static, TSchema };
@@ -31,10 +31,16 @@ export type WorkflowOutputMode = "inline" | "file-only";
31
31
  export type WorkflowContextMode = "fresh" | "fork";
32
32
  export type WorkflowThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
33
33
  export type WorkflowExecutionMode = "interactive" | "non_interactive";
34
- export type RunStatus = "pending" | "running" | "paused" | "completed" | "failed" | "killed";
34
+ export type WorkflowExitStatus = "completed" | "skipped" | "cancelled" | "blocked";
35
+ export type RunStatus = "pending" | "running" | "paused" | WorkflowExitStatus | "failed" | "killed";
35
36
  export type WorkflowDetailsMode = "named" | "single" | "parallel" | "chain" | "inspection" | "control";
36
- export type WorkflowDetailsStatus = "accepted" | "running" | "completed" | "failed" | "killed" | "noop";
37
+ export type WorkflowDetailsStatus = "accepted" | "running" | WorkflowExitStatus | "failed" | "killed" | "noop";
37
38
  export type WorkflowAction = "list" | "get" | "inputs" | "run" | "status" | "interrupt" | "resume";
39
+ export interface WorkflowExitOptions<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> {
40
+ readonly status?: WorkflowExitStatus;
41
+ readonly reason?: string;
42
+ readonly outputs?: Partial<TOutputs>;
43
+ }
38
44
  export interface WorkflowModelFallbackFields {
39
45
  /** Ordered model IDs to try after `model` fails; entries may use `:off|minimal|low|medium|high|xhigh` reasoning suffixes. */
40
46
  readonly fallbackModels?: readonly string[];
@@ -112,6 +118,7 @@ export interface StageOptions extends WorkflowModelFallbackFields {
112
118
  readonly noTools?: "all" | "builtin";
113
119
  readonly excludedTools?: readonly string[];
114
120
  readonly customTools?: readonly WorkflowCustomToolDefinition[];
121
+ readonly bashPolicy?: BashCommandPolicy;
115
122
  readonly cwd?: string;
116
123
  readonly agentDir?: string;
117
124
  readonly scopedModels?: readonly WorkflowScopedModel[];
@@ -332,12 +339,22 @@ export interface WorkflowRunChildOptions<TInputs extends WorkflowInputValues = W
332
339
  readonly inputs?: TInputs;
333
340
  readonly stageName?: string;
334
341
  }
335
- export interface WorkflowChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> extends WorkflowSerializableObject {
342
+ export interface WorkflowCompletedChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> extends WorkflowSerializableObject {
336
343
  readonly workflow: string;
337
344
  readonly runId: string;
338
345
  readonly status: "completed";
346
+ readonly exited: false;
339
347
  readonly outputs: TOutputs;
340
348
  }
349
+ export interface WorkflowExitedChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> extends WorkflowSerializableObject {
350
+ readonly workflow: string;
351
+ readonly runId: string;
352
+ readonly status: WorkflowExitStatus;
353
+ readonly exited: true;
354
+ readonly outputs: Partial<TOutputs>;
355
+ readonly exitReason?: string;
356
+ }
357
+ export type WorkflowChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> = WorkflowCompletedChildResult<TOutputs> | WorkflowExitedChildResult<TOutputs>;
341
358
  export type WorkflowCustomUiComponent = Component & {
342
359
  dispose?(): void;
343
360
  };
@@ -379,9 +396,10 @@ export interface WorkflowUIAdapter {
379
396
  editor(initial?: string): Promise<string>;
380
397
  custom?<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T>;
381
398
  }
382
- export interface WorkflowRunContext<TInputs extends WorkflowInputValues = WorkflowInputValues, TDefinitionBrand extends object = {}> {
399
+ export interface WorkflowRunContext<TInputs extends WorkflowInputValues = WorkflowInputValues, TDefinitionBrand extends object = {}, TOutputs extends WorkflowOutputValues = WorkflowOutputValues> {
383
400
  readonly inputs: Readonly<TInputs>;
384
401
  readonly cwd?: string;
402
+ exit(options?: WorkflowExitOptions<TOutputs>): never;
385
403
  stage(name: string, options?: StageOptions): StageContext;
386
404
  task(name: string, options: WorkflowTaskOptions): Promise<WorkflowTaskResult>;
387
405
  chain(steps: readonly WorkflowTaskStep[], options?: WorkflowChainOptions): Promise<WorkflowTaskResult[]>;
@@ -389,7 +407,7 @@ export interface WorkflowRunContext<TInputs extends WorkflowInputValues = Workfl
389
407
  workflow<TChildInputs extends WorkflowInputValues, TChildOutputs extends WorkflowOutputValues>(definition: WorkflowDefinition<TChildInputs, TChildOutputs> & TDefinitionBrand, options?: WorkflowRunChildOptions<TChildInputs>): Promise<WorkflowChildResult<TChildOutputs>>;
390
408
  readonly ui: WorkflowUIContext;
391
409
  }
392
- export type WorkflowRunFn<TInputs extends WorkflowInputValues = WorkflowInputValues, TOutputs extends WorkflowOutputValues = WorkflowOutputValues, TDefinitionBrand extends object = {}> = (ctx: WorkflowRunContext<TInputs, TDefinitionBrand>) => Promise<TOutputs> | TOutputs;
410
+ export type WorkflowRunFn<TInputs extends WorkflowInputValues = WorkflowInputValues, TOutputs extends WorkflowOutputValues = WorkflowOutputValues, TDefinitionBrand extends object = {}> = (ctx: WorkflowRunContext<TInputs, TDefinitionBrand, TOutputs>) => Promise<TOutputs> | TOutputs;
393
411
  export interface WorkflowRuntimeConfig {
394
412
  readonly maxDepth: number;
395
413
  readonly defaultConcurrency: number;
@@ -414,7 +432,7 @@ export interface WorkflowDefinition<TInputs extends WorkflowInputValues = Workfl
414
432
  readonly inputs: WorkflowInputSchemaMap;
415
433
  readonly outputs?: WorkflowOutputSchemaMap;
416
434
  readonly inputBindings?: WorkflowInputBindings;
417
- run(ctx: WorkflowRunContext<TInputs, TDefinitionBrand>): Promise<TOutputs> | TOutputs;
435
+ run(ctx: WorkflowRunContext<TInputs, TDefinitionBrand, TOutputs>): Promise<TOutputs> | TOutputs;
418
436
  }
419
437
  type DeclaredResolvedEntry<K extends string, S extends TSchema> = S extends TOptional<TSchema> ? {
420
438
  readonly [P in K]?: Static<S>;
@@ -437,7 +455,7 @@ export interface WorkflowBuilder<TInputs extends WorkflowInputValues = {}, TOutp
437
455
  input<K extends string, S extends TSchema>(key: K, schema: S): WorkflowBuilder<Simplify<TInputs & DeclaredResolvedEntry<K, S>>, TOutputs, Simplify<TRunInputs & DeclaredProvidedEntry<K, S>>, TDefinitionBrand, WorkflowDefinition<Simplify<TInputs & DeclaredResolvedEntry<K, S>>, TOutputs, Simplify<TRunInputs & DeclaredProvidedEntry<K, S>>, TDefinitionBrand> & TDefinitionBrand>;
438
456
  output<K extends string, S extends TSchema>(key: K, schema: S): WorkflowBuilder<TInputs, Simplify<TOutputs & (DeclaredResolvedEntry<K, S> & WorkflowOutputValues)>, TRunInputs, TDefinitionBrand, WorkflowDefinition<TInputs, Simplify<TOutputs & (DeclaredResolvedEntry<K, S> & WorkflowOutputValues)>, TRunInputs, TDefinitionBrand> & TDefinitionBrand>;
439
457
  worktreeFromInputs(binding: WorkflowWorktreeInputBinding): WorkflowBuilder<TInputs, TOutputs, TRunInputs, TDefinitionBrand, TCompiledDefinition>;
440
- run<TActualOutputs extends TOutputs>(fn: (ctx: WorkflowRunContext<TInputs, TDefinitionBrand>) => Promise<NoExtraOutputs<TOutputs, TActualOutputs>> | NoExtraOutputs<TOutputs, TActualOutputs>): CompletedWorkflowBuilder<TInputs, TOutputs, TRunInputs, TDefinitionBrand, TCompiledDefinition>;
458
+ run<TActualOutputs extends TOutputs>(fn: (ctx: WorkflowRunContext<TInputs, TDefinitionBrand, TOutputs>) => Promise<NoExtraOutputs<TOutputs, TActualOutputs>> | NoExtraOutputs<TOutputs, TActualOutputs>): CompletedWorkflowBuilder<TInputs, TOutputs, TRunInputs, TDefinitionBrand, TCompiledDefinition>;
441
459
  }
442
460
  export interface CompletedWorkflowBuilder<TInputs extends WorkflowInputValues = {}, TOutputs extends WorkflowOutputValues = {}, TRunInputs extends WorkflowInputValues = TInputs, TDefinitionBrand extends object = {}, TCompiledDefinition extends WorkflowDefinition<TInputs, TOutputs, TRunInputs, TDefinitionBrand> = WorkflowDefinition<TInputs, TOutputs, TRunInputs, TDefinitionBrand>> extends WorkflowBuilder<TInputs, TOutputs, TRunInputs, TDefinitionBrand, TCompiledDefinition> {
443
461
  description(text: string): CompletedWorkflowBuilder<TInputs, TOutputs, TRunInputs, TDefinitionBrand, TCompiledDefinition>;
@@ -501,7 +519,7 @@ export interface RunOpts {
501
519
  readonly onRunStart?: (snapshot: RunSnapshot) => void;
502
520
  readonly onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
503
521
  readonly onStageEnd?: (runId: string, snapshot: StageSnapshot) => void;
504
- readonly onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string) => void;
522
+ readonly onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string, exitReason?: string) => void;
505
523
  }
506
524
  export interface WorkflowProgressSummary extends WorkflowSerializableObject {
507
525
  readonly completed?: number;
@@ -530,6 +548,9 @@ export interface WorkflowDetails extends WorkflowSerializableObject {
530
548
  readonly intercom?: WorkflowIntercomSummary;
531
549
  readonly warnings?: readonly string[];
532
550
  readonly error?: string;
551
+ /** True when the run reached its terminal status through ctx.exit(). */
552
+ readonly exited?: boolean;
553
+ readonly exitReason?: string;
533
554
  }
534
555
  export type StageStatus = RunStatus | "skipped" | "awaiting_input" | "blocked";
535
556
  export interface StageSnapshot extends WorkflowSerializableObject {
@@ -542,8 +563,11 @@ export interface StageSnapshot extends WorkflowSerializableObject {
542
563
  export interface RunResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> extends WorkflowSerializableObject {
543
564
  readonly runId: string;
544
565
  readonly status: RunStatus;
545
- readonly result?: TOutputs;
566
+ readonly result?: Partial<TOutputs>;
546
567
  readonly error?: string;
568
+ /** True when the run reached its terminal status through ctx.exit(). */
569
+ readonly exited?: boolean;
570
+ readonly exitReason?: string;
547
571
  readonly stages: readonly StageSnapshot[];
548
572
  }
549
573
  export type ResolvedInputs<TInputs extends WorkflowInputValues = WorkflowInputValues> = Readonly<TInputs> & WorkflowSerializableObject;
@@ -29,12 +29,20 @@ function virtualStageId(runId: string, stageId: string, isRootRun: boolean): str
29
29
  return isRootRun ? stageId : `${runId}:${stageId}`;
30
30
  }
31
31
 
32
+ function isTerminalNonCompletedBoundary(stage: StageSnapshot): boolean {
33
+ return stage.status === "failed" || stage.status === "skipped";
34
+ }
35
+
32
36
  function childRunIdFor(stage: StageSnapshot): string | undefined {
33
- return stage.workflowChildRun?.runId ?? stage.workflowChild?.runId;
37
+ if (isTerminalNonCompletedBoundary(stage)) return undefined;
38
+ if (stage.status === "completed") return stage.workflowChild?.runId ?? stage.workflowChildRun?.runId;
39
+ return stage.workflowChildRun?.runId;
34
40
  }
35
41
 
36
42
  function childAliasFor(stage: StageSnapshot): string | undefined {
37
- return stage.workflowChildRun?.alias ?? stage.workflowChild?.alias;
43
+ if (isTerminalNonCompletedBoundary(stage)) return undefined;
44
+ if (stage.status === "completed") return stage.workflowChild?.alias ?? stage.workflowChildRun?.alias;
45
+ return stage.workflowChildRun?.alias;
38
46
  }
39
47
 
40
48
  function localTerminalStageIds(stages: readonly StageSnapshot[]): readonly string[] {
@@ -9,14 +9,14 @@
9
9
  import type { Store } from "./store.js";
10
10
  import type {
11
11
  RunSnapshot,
12
+ RunStatus,
12
13
  StageSnapshot,
13
14
  StageStatus,
14
- WorkflowChildReplaySnapshot,
15
15
  WorkflowFailureCode,
16
16
  WorkflowFailureDisposition,
17
17
  WorkflowFailureKind,
18
18
  } from "./store-types.js";
19
- import type { WorkflowInputValues, WorkflowOutputValues } from "./types.js";
19
+ import type { WorkflowExitStatus, WorkflowInputValues, WorkflowOutputValues } from "./types.js";
20
20
  import { workflowSerializableObjectSchema } from "./serializable.js";
21
21
  import { Value } from "typebox/value";
22
22
  import {
@@ -429,11 +429,12 @@ function serializableObjectOrEmpty(value: unknown): WorkflowOutputValues {
429
429
  return serializableObject(value) ?? {};
430
430
  }
431
431
 
432
- function isWorkflowChildReplayStatus(status: unknown): status is WorkflowChildReplaySnapshot["status"] {
433
- return status === "completed";
432
+ function isWorkflowChildReplayStatus(status: unknown): status is WorkflowExitStatus {
433
+ return status === "completed" || status === "skipped" || status === "cancelled" || status === "blocked";
434
434
  }
435
435
 
436
436
  function workflowChildMetadata(payload: Record<string, unknown>): Pick<StageSnapshot, "workflowChild"> {
437
+ if (payload["status"] !== "completed") return {};
437
438
  const workflowChild = payload["workflowChild"];
438
439
  if (!isRecord(workflowChild)) return {};
439
440
  const alias = workflowChild["alias"];
@@ -441,6 +442,8 @@ function workflowChildMetadata(payload: Record<string, unknown>): Pick<StageSnap
441
442
  const childRunId = workflowChild["runId"];
442
443
  const status = workflowChild["status"];
443
444
  const outputs = workflowChild["outputs"];
445
+ const exited = workflowChild["exited"];
446
+ const exitReason = workflowChild["exitReason"];
444
447
  if (
445
448
  typeof alias !== "string" ||
446
449
  typeof workflow !== "string" ||
@@ -470,7 +473,9 @@ function workflowChildMetadata(payload: Record<string, unknown>): Pick<StageSnap
470
473
  workflow,
471
474
  runId: childRunId,
472
475
  status,
476
+ ...(typeof exited === "boolean" ? { exited } : status !== "completed" || typeof exitReason === "string" ? { exited: true } : {}),
473
477
  outputs: clonedOutputs,
478
+ ...(typeof exitReason === "string" ? { exitReason } : {}),
474
479
  },
475
480
  };
476
481
  }
@@ -567,7 +572,12 @@ function restoreTerminalRuns(entries: readonly SessionEntry[], store: Store): vo
567
572
 
568
573
  const runMeta = findRunStartMetadata(entries, runId);
569
574
  const stages = _buildStageSnapshots(entries, runId);
570
- if (status === "completed" && stages.some((stage) => stage.status !== "completed")) continue;
575
+ const exited = end["exited"];
576
+ const exitReason = end["exitReason"];
577
+ const resumable = end["resumable"];
578
+ const restoredAuthorExit = isWorkflowExitTerminalStatus(status) &&
579
+ (exited === true || status !== "completed" || typeof exitReason === "string" || resumable === false);
580
+ if (status === "completed" && !restoredAuthorExit && stages.some((stage) => stage.status !== "completed")) continue;
571
581
  store.recordRunStart({
572
582
  id: runId,
573
583
  name: start.name,
@@ -583,6 +593,7 @@ function restoreTerminalRuns(entries: readonly SessionEntry[], store: Store): vo
583
593
  });
584
594
 
585
595
  const error = end["error"];
596
+ const result = serializableObject(end["result"]);
586
597
  const failureKind = end["failureKind"];
587
598
  const failureCode = end["failureCode"];
588
599
  const failureRecoverability = end["failureRecoverability"];
@@ -590,11 +601,10 @@ function restoreTerminalRuns(entries: readonly SessionEntry[], store: Store): vo
590
601
  const retryAfterMs = numericRetryAfterMs(end["retryAfterMs"]);
591
602
  const failureMessage = end["failureMessage"];
592
603
  const failedStageId = end["failedStageId"];
593
- const resumable = end["resumable"];
594
604
  store.recordRunEnd(
595
605
  runId,
596
606
  status,
597
- undefined,
607
+ result,
598
608
  typeof error === "string" ? error : undefined,
599
609
  {
600
610
  ...(typeof failureKind === "string" && isWorkflowFailureKind(failureKind) ? { failureKind } : {}),
@@ -604,15 +614,24 @@ function restoreTerminalRuns(entries: readonly SessionEntry[], store: Store): vo
604
614
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
605
615
  ...(typeof failureMessage === "string" ? { failureMessage } : {}),
606
616
  ...(typeof failedStageId === "string" ? { failedStageId } : {}),
607
- ...(typeof resumable === "boolean" ? { resumable } : {}),
617
+ ...(typeof resumable === "boolean" ? { resumable } : isWorkflowExitTerminalStatus(status) && restoredAuthorExit ? { resumable: false } : {}),
618
+ ...(restoredAuthorExit && isWorkflowExitTerminalStatus(status) ? { exited: true } : {}),
619
+ ...(typeof exitReason === "string" ? { exitReason } : {}),
608
620
  },
609
621
  );
610
622
  }
611
623
  }
612
624
 
613
- function restoreTerminalRunStatus(status: unknown): "completed" | "failed" | "killed" | undefined {
625
+ function isWorkflowExitTerminalStatus(status: RunStatus): status is WorkflowExitStatus {
626
+ return status === "completed" || status === "skipped" || status === "cancelled" || status === "blocked";
627
+ }
628
+
629
+ function restoreTerminalRunStatus(status: unknown): RunStatus | undefined {
614
630
  switch (status) {
615
631
  case "completed":
632
+ case "skipped":
633
+ case "cancelled":
634
+ case "blocked":
616
635
  case "failed":
617
636
  case "killed":
618
637
  return status;
@@ -6,7 +6,7 @@
6
6
  * through gracefully when the runtime doesn't support the method.
7
7
  */
8
8
 
9
- import type { WorkflowInputValues, WorkflowOutputValues } from "./types.js";
9
+ import type { WorkflowExitStatus, WorkflowInputValues, WorkflowOutputValues } from "./types.js";
10
10
  import type {
11
11
  WorkflowFailureCode,
12
12
  WorkflowFailureDisposition,
@@ -69,8 +69,10 @@ export interface WorkflowChildReplayPayload {
69
69
  readonly alias: string;
70
70
  readonly workflow: string;
71
71
  readonly runId: string;
72
- readonly status: "completed";
72
+ readonly status: WorkflowExitStatus;
73
+ readonly exited?: boolean;
73
74
  readonly outputs: WorkflowOutputValues;
75
+ readonly exitReason?: string;
74
76
  }
75
77
 
76
78
  export interface StageEndPayload {
@@ -98,6 +100,8 @@ export interface RunEndPayload {
98
100
  readonly status: string;
99
101
  readonly result?: WorkflowOutputValues;
100
102
  readonly error?: string;
103
+ readonly exited?: boolean;
104
+ readonly exitReason?: string;
101
105
  readonly failureKind?: string;
102
106
  readonly failureCode?: string;
103
107
  readonly failureRecoverability?: string;
@@ -201,7 +205,7 @@ export function appendStageEnd(
201
205
  ...(payload.replayKey !== undefined ? { replayKey: payload.replayKey } : {}),
202
206
  ...(payload.replayedFromStageId !== undefined ? { replayedFromStageId: payload.replayedFromStageId } : {}),
203
207
  ...(payload.replayed !== undefined ? { replayed: payload.replayed } : {}),
204
- ...(payload.workflowChild !== undefined ? { workflowChild: payload.workflowChild } : {}),
208
+ ...(payload.status === "completed" && payload.workflowChild !== undefined ? { workflowChild: payload.workflowChild } : {}),
205
209
  });
206
210
  if (opts?.emitMessage === true && payload.summary && typeof api.appendCustomMessageEntry === "function") {
207
211
  api.appendCustomMessageEntry(
@@ -236,6 +240,8 @@ export function appendRunEnd(api: PersistenceAPI, payload: RunEndPayload): void
236
240
  status: terminalPayload.status,
237
241
  ...(terminalPayload.result !== undefined ? { result: terminalPayload.result } : {}),
238
242
  ...(terminalPayload.error !== undefined ? { error: terminalPayload.error } : {}),
243
+ ...(terminalPayload.exited !== undefined ? { exited: terminalPayload.exited } : {}),
244
+ ...(terminalPayload.exitReason !== undefined ? { exitReason: terminalPayload.exitReason } : {}),
239
245
  ...(terminalPayload.failureKind !== undefined ? { failureKind: terminalPayload.failureKind } : {}),
240
246
  ...(terminalPayload.failureCode !== undefined ? { failureCode: terminalPayload.failureCode } : {}),
241
247
  ...(terminalPayload.failureRecoverability !== undefined ? { failureRecoverability: terminalPayload.failureRecoverability } : {}),
@@ -3,9 +3,9 @@
3
3
  * cross-ref: spec §5.5
4
4
  */
5
5
 
6
- import type { WorkflowInputValues, WorkflowOutputValues } from "./types.js";
6
+ import type { WorkflowExitStatus, WorkflowInputValues, WorkflowOutputValues } from "./types.js";
7
7
 
8
- export type RunStatus = "pending" | "running" | "paused" | "completed" | "failed" | "killed";
8
+ export type RunStatus = "pending" | "running" | "paused" | WorkflowExitStatus | "failed" | "killed";
9
9
  export type StageStatus =
10
10
  | "pending"
11
11
  | "running"
@@ -125,8 +125,11 @@ export interface WorkflowChildReplaySnapshot {
125
125
  readonly alias: string;
126
126
  readonly workflow: string;
127
127
  readonly runId: string;
128
- readonly status: "completed";
128
+ readonly status: WorkflowExitStatus;
129
+ /** True when the child reached this terminal status through ctx.exit(). */
130
+ readonly exited?: boolean;
129
131
  readonly outputs: WorkflowOutputValues;
132
+ readonly exitReason?: string;
130
133
  }
131
134
 
132
135
  export interface StageSnapshot {
@@ -243,6 +246,10 @@ export interface RunSnapshot {
243
246
  resumedAt?: number;
244
247
  result?: WorkflowOutputValues;
245
248
  error?: string;
249
+ /** True when the run reached its terminal status through ctx.exit(). */
250
+ exited?: boolean;
251
+ /** Optional author-supplied reason from ctx.exit(). */
252
+ exitReason?: string;
246
253
  /** Structured workflow failure category for failed runs. */
247
254
  failureKind?: WorkflowFailureKind;
248
255
  /** Specific additive workflow failure code within `failureKind`. */
@@ -25,8 +25,16 @@ import type {
25
25
  import { accumulatePausedDurationMs, elapsedRunMs } from "./timing.js";
26
26
  import { isTopLevelWorkflowRun } from "./run-visibility.js";
27
27
 
28
- /** Statuses that represent a terminal run state — cannot be overwritten. */
29
- const TERMINAL_STATUSES: ReadonlySet<RunStatus> = new Set(["completed", "failed", "killed"]);
28
+ /**
29
+ * Statuses that represent a terminal run state — cannot be overwritten.
30
+ *
31
+ * Note on `"blocked"`: here it is an author-selected `ctx.exit({ status: "blocked" })`
32
+ * outcome — terminal and non-resumable. This is deliberately distinct from retry-blocking,
33
+ * which does NOT use this run status: `recordRunBlocked()` keeps `run.status = "running"`
34
+ * and records the block via `blockedAt` / `failureDisposition: "active_blocked"` (resumable).
35
+ * The two never collide despite the shared word.
36
+ */
37
+ const TERMINAL_STATUSES: ReadonlySet<RunStatus> = new Set(["completed", "failed", "killed", "skipped", "cancelled", "blocked"]);
30
38
 
31
39
  function isTerminalStageStatus(status: StageStatus): boolean {
32
40
  return status === "completed" || status === "failed" || status === "skipped";
@@ -53,6 +61,8 @@ export interface RunEndMetadata {
53
61
  readonly failedStageId?: string;
54
62
  readonly resumable?: boolean;
55
63
  readonly retryAfterMs?: number;
64
+ readonly exited?: boolean;
65
+ readonly exitReason?: string;
56
66
  }
57
67
 
58
68
  export interface RunBlockedMetadata extends RunEndMetadata {
@@ -73,6 +83,8 @@ function clearRunFailureMetadata(run: RunSnapshot): void {
73
83
  delete run.resumable;
74
84
  delete run.retryAfterMs;
75
85
  delete run.blockedAt;
86
+ delete run.exited;
87
+ delete run.exitReason;
76
88
  }
77
89
 
78
90
  function clearStaleBlockedRunMetadata(run: RunSnapshot, metadata: RunEndMetadata | undefined): void {
@@ -84,6 +96,8 @@ function clearStaleBlockedRunMetadata(run: RunSnapshot, metadata: RunEndMetadata
84
96
  if (metadata?.failedStageId === undefined) delete run.failedStageId;
85
97
  if (metadata?.resumable === undefined) delete run.resumable;
86
98
  if (metadata?.retryAfterMs === undefined) delete run.retryAfterMs;
99
+ if (metadata?.exited === undefined) delete run.exited;
100
+ if (metadata?.exitReason === undefined) delete run.exitReason;
87
101
  }
88
102
 
89
103
  function applyRunEndMetadata(run: RunSnapshot, metadata: RunEndMetadata): void {
@@ -95,6 +109,8 @@ function applyRunEndMetadata(run: RunSnapshot, metadata: RunEndMetadata): void {
95
109
  if (metadata.failureMessage !== undefined) run.failureMessage = metadata.failureMessage;
96
110
  if (metadata.failedStageId !== undefined) run.failedStageId = metadata.failedStageId;
97
111
  if (metadata.resumable !== undefined) run.resumable = metadata.resumable;
112
+ if (metadata.exited !== undefined) run.exited = metadata.exited;
113
+ if (metadata.exitReason !== undefined) run.exitReason = metadata.exitReason;
98
114
  }
99
115
 
100
116
  export type StagePromptAnswerSource = "workflow_ui" | "workflow_tool";
@@ -138,8 +154,8 @@ export interface Store {
138
154
  /**
139
155
  * Records the end of a run.
140
156
  * Returns `true` if state changed, `false` if the run was not found or
141
- * already in a terminal state (completed | failed | killed).
142
- * `result` is only applied for status "completed".
157
+ * already in a terminal state (completed | failed | killed | skipped | cancelled | blocked).
158
+ * `result` is applied for intentional success/exit statuses (completed | skipped | cancelled | blocked).
143
159
  * `error` is only applied for status "failed" | "killed".
144
160
  */
145
161
  recordRunEnd(
@@ -531,8 +547,13 @@ export function createStore(): Store {
531
547
  if (stage.promptAnswerState !== undefined) existing.promptAnswerState = stage.promptAnswerState;
532
548
  if (stage.replayedFromStageId !== undefined) existing.replayedFromStageId = stage.replayedFromStageId;
533
549
  if (stage.replayed !== undefined) existing.replayed = stage.replayed;
534
- if (stage.workflowChildRun !== undefined) existing.workflowChildRun = { ...stage.workflowChildRun };
535
- if (stage.workflowChild !== undefined) existing.workflowChild = structuredClone(stage.workflowChild);
550
+ if (stage.status === "completed") {
551
+ if (stage.workflowChildRun !== undefined) existing.workflowChildRun = { ...stage.workflowChildRun };
552
+ if (stage.workflowChild !== undefined) existing.workflowChild = structuredClone(stage.workflowChild);
553
+ } else {
554
+ delete existing.workflowChildRun;
555
+ delete existing.workflowChild;
556
+ }
536
557
  delete existing.awaitingInputSince;
537
558
  delete existing.inputRequest;
538
559
  rejectStagePrompt(runId, existing, `atomic-workflows: stage ${stage.id} ended before prompt resolved`);
@@ -564,11 +585,12 @@ export function createStore(): Store {
564
585
  run.durationMs = elapsedRunMs(run, run.endedAt);
565
586
  const wasBlocked = run.blockedAt !== undefined || run.failureDisposition === "active_blocked";
566
587
  delete run.blockedAt;
567
- if (status === "completed") {
588
+ if (status === "completed" || status === "skipped" || status === "cancelled" || status === "blocked") {
568
589
  if (result !== undefined) {
569
590
  run.result = result;
570
591
  }
571
592
  clearRunFailureMetadata(run);
593
+ if (metadata !== undefined) applyRunEndMetadata(run, metadata);
572
594
  } else {
573
595
  if (wasBlocked && error === undefined) delete run.error;
574
596
  if ((status === "failed" || status === "killed") && error !== undefined) {
@@ -47,6 +47,8 @@ export type WorkflowSerializableValue = AuthoringContract.WorkflowSerializableVa
47
47
  export type WorkflowInputValues = AuthoringContract.WorkflowInputValues;
48
48
  export type WorkflowOutputValues = AuthoringContract.WorkflowOutputValues;
49
49
  export type WorkflowRunOutput = AuthoringContract.WorkflowRunOutput;
50
+ export type WorkflowExitStatus = AuthoringContract.WorkflowExitStatus;
51
+ export type WorkflowExitOptions<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> = AuthoringContract.WorkflowExitOptions<TOutputs>;
50
52
 
51
53
  // ---------------------------------------------------------------------------
52
54
  // Workflow input / output schemas
@@ -98,14 +100,9 @@ export interface WorkflowRunChildOptions<TInputs extends WorkflowInputValues = W
98
100
  readonly stageName?: string;
99
101
  }
100
102
 
101
- export interface WorkflowChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues>
102
- extends WorkflowSerializableObject {
103
- readonly workflow: string;
104
- readonly runId: string;
105
- readonly status: "completed";
106
- /** Child outputs, typed from the child workflow's declared `.output(...)` contract. */
107
- readonly outputs: TOutputs;
108
- }
103
+ export type WorkflowCompletedChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> = AuthoringContract.WorkflowCompletedChildResult<TOutputs>;
104
+ export type WorkflowExitedChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> = AuthoringContract.WorkflowExitedChildResult<TOutputs>;
105
+ export type WorkflowChildResult<TOutputs extends WorkflowOutputValues = WorkflowOutputValues> = AuthoringContract.WorkflowChildResult<TOutputs>;
109
106
 
110
107
  // ---------------------------------------------------------------------------
111
108
  // HIL (human-in-the-loop) primitives available inside run functions
@@ -329,11 +326,16 @@ export interface StageContext {
329
326
  // Workflow run context (top-level ctx passed to the run function)
330
327
  // ---------------------------------------------------------------------------
331
328
 
332
- export interface WorkflowRunContext<TInputs extends WorkflowInputValues = WorkflowInputValues> {
329
+ export interface WorkflowRunContext<
330
+ TInputs extends WorkflowInputValues = WorkflowInputValues,
331
+ TOutputs extends WorkflowOutputValues = WorkflowOutputValues,
332
+ > {
333
333
  /** Typed inputs provided by the caller, validated against the input schema. */
334
334
  readonly inputs: TInputs;
335
335
  /** Invocation working directory for workflow-owned artifacts. Defaults to the host process cwd when omitted. */
336
336
  readonly cwd?: string;
337
+ /** Intentionally end this workflow run from any call depth. */
338
+ exit(options?: WorkflowExitOptions<TOutputs>): never;
337
339
  /**
338
340
  * Create and register a named stage synchronously. Stage work starts when
339
341
  * a stage method such as prompt() or complete() is awaited; the executor
@@ -383,7 +385,7 @@ export type WorkflowRuntimeConfig = AuthoringContract.WorkflowRuntimeConfig;
383
385
  export type WorkflowRunFn<
384
386
  TInputs extends WorkflowInputValues = WorkflowInputValues,
385
387
  TOutputs extends WorkflowOutputValues = WorkflowOutputValues,
386
- > = (ctx: WorkflowRunContext<TInputs>) => ReturnType<AuthoringContract.WorkflowRunFn<TInputs, TOutputs>>;
388
+ > = (ctx: WorkflowRunContext<TInputs, TOutputs>) => ReturnType<AuthoringContract.WorkflowRunFn<TInputs, TOutputs>>;
387
389
 
388
390
  // ---------------------------------------------------------------------------
389
391
  // Compiled workflow definition
@@ -185,6 +185,9 @@ function summaryRows(detail: RunDetail, now: number): Array<[string, string | un
185
185
  } else {
186
186
  rows.push(["elapsed", fmtDuration(duration)]);
187
187
  }
188
+ if (detail.exitReason) {
189
+ rows.push(["reason", detail.exitReason]);
190
+ }
188
191
  if (detail.error) {
189
192
  rows.push(["error", detail.error.split("\n")[0] ?? ""]);
190
193
  }
@@ -303,6 +306,12 @@ function stateBadges(detail: RunDetail, theme: GraphTheme): FlatBandBadge[] {
303
306
  return [{ text: "❚❚ paused", fg: theme.warning }];
304
307
  case "completed":
305
308
  return [{ text: "✓ completed", fg: theme.success }];
309
+ case "skipped":
310
+ return [{ text: "⊘ skipped", fg: theme.dim }];
311
+ case "cancelled":
312
+ return [{ text: "⊘ cancelled", fg: theme.dim }];
313
+ case "blocked":
314
+ return [{ text: "↑ blocked", fg: theme.dim }];
306
315
  case "failed":
307
316
  return [{ text: "✗ failed", fg: theme.error }];
308
317
  case "killed":
@@ -318,6 +327,9 @@ function stateLabel(detail: RunDetail): string {
318
327
  case "running": return "● running";
319
328
  case "paused": return "❚❚ paused";
320
329
  case "completed": return "✓ completed";
330
+ case "skipped": return "⊘ skipped";
331
+ case "cancelled": return "⊘ cancelled";
332
+ case "blocked": return "↑ blocked";
321
333
  case "failed": return "✗ failed";
322
334
  case "killed": return "⊘ killed";
323
335
  case "pending":
@@ -30,6 +30,8 @@ export function statusColor(
30
30
  return theme.dim;
31
31
  case "skipped":
32
32
  return theme.dim;
33
+ case "cancelled":
34
+ return theme.dim;
33
35
  case "pending":
34
36
  default:
35
37
  return theme.dim;
@@ -47,6 +49,8 @@ export function statusIcon(status: StageStatus | RunStatus): string {
47
49
  return "↑";
48
50
  case "skipped":
49
51
  return "⊘";
52
+ case "cancelled":
53
+ return "⊘";
50
54
  case "running":
51
55
  return "●";
52
56
  case "paused":
@@ -161,6 +161,9 @@ function runAccent(run: RunSnapshot, theme?: GraphTheme): string {
161
161
  case "completed": return theme.success;
162
162
  case "running": return theme.warning;
163
163
  case "paused": return theme.warning;
164
+ case "skipped": return theme.dim;
165
+ case "cancelled": return theme.dim;
166
+ case "blocked": return theme.dim;
164
167
  case "failed": return theme.error;
165
168
  case "killed": return theme.error;
166
169
  case "pending":
@@ -173,6 +176,9 @@ function runTrailing(run: RunSnapshot, theme?: GraphTheme): { text: string; fg?:
173
176
  case "completed": return { text: "✓ completed", fg: theme?.success };
174
177
  case "running": return { text: "● running", fg: theme?.warning };
175
178
  case "paused": return { text: "❚❚ paused", fg: theme?.warning };
179
+ case "skipped": return { text: "⊘ skipped", fg: theme?.dim };
180
+ case "cancelled": return { text: "⊘ cancelled", fg: theme?.dim };
181
+ case "blocked": return { text: "↑ blocked", fg: theme?.dim };
176
182
  case "failed": return { text: "✗ failed", fg: theme?.error };
177
183
  case "killed": return { text: "⊘ killed", fg: theme?.error };
178
184
  case "pending":
@@ -226,7 +232,8 @@ function runCardMeta(run: RunSnapshot, now: number): string {
226
232
  return parts.join(" · ");
227
233
  }
228
234
 
229
- if (run.status === "completed") {
235
+ if (run.status === "completed" || run.status === "skipped" || run.status === "cancelled" || run.status === "blocked") {
236
+ if (run.exitReason !== undefined && run.exitReason.length > 0) parts.push(run.exitReason);
230
237
  if (!isChain && run.stages[0]) parts.push(run.stages[0].name);
231
238
  const dur = lastStageDuration(run, now);
232
239
  if (dur) parts.push(dur);
@@ -280,6 +287,9 @@ function stageCells(run: RunSnapshot): Array<{ status: StageStatus }> {
280
287
  function stageStatusFromRun(run: RunSnapshot): StageStatus {
281
288
  switch (run.status) {
282
289
  case "completed": return "completed";
290
+ case "skipped": return "skipped";
291
+ case "cancelled": return "skipped";
292
+ case "blocked": return "blocked";
283
293
  case "running": return "running";
284
294
  case "paused": return "paused";
285
295
  case "failed": return "failed";
@@ -320,6 +330,7 @@ function countBuckets(runs: readonly RunSnapshot[]): Counts {
320
330
  else if (r.status === "completed") c.completed++;
321
331
  else c.failed++;
322
332
  } else if (r.status === "completed") c.completed++;
333
+ else if (r.status === "skipped" || r.status === "cancelled" || r.status === "blocked") c.completed++;
323
334
  else c.failed++;
324
335
  }
325
336
  return c;
@@ -372,6 +383,9 @@ function emptyStateLine(theme?: GraphTheme): string {
372
383
  function statusIconForRun(run: RunSnapshot): string {
373
384
  switch (run.status) {
374
385
  case "completed": return "✓";
386
+ case "skipped": return "⊘";
387
+ case "cancelled": return "⊘";
388
+ case "blocked": return "↑";
375
389
  case "running": return "●";
376
390
  case "paused": return "❚❚";
377
391
  case "failed": return "✗";
@@ -171,7 +171,7 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
171
171
  readonly startedAt: number;
172
172
  }
173
173
 
174
- const terminalRunStatuses = new Set(["completed", "failed", "killed"]);
174
+ const terminalRunStatuses = new Set(["completed", "failed", "killed", "skipped", "cancelled", "blocked"]);
175
175
  const terminalStageStatuses = new Set(["completed", "failed", "skipped"]);
176
176
  const activeToolCalls = new Map<string, ActiveToolCall>();
177
177