@bastani/atomic 0.8.26-alpha.5 → 0.8.26-alpha.6

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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  5. package/dist/builtin/mcp/package.json +1 -1
  6. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  7. package/dist/builtin/subagents/package.json +1 -1
  8. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +48 -10
  9. package/dist/builtin/subagents/src/runs/foreground/execution.ts +30 -9
  10. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
  11. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
  12. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  13. package/dist/builtin/web-access/package.json +1 -1
  14. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  15. package/dist/builtin/workflows/package.json +1 -1
  16. package/dist/builtin/workflows/src/extension/index.ts +10 -2
  17. package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
  18. package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
  19. package/dist/builtin/workflows/src/runs/foreground/executor.ts +441 -15
  20. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +69 -8
  21. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
  22. package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
  23. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
  24. package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
  25. package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
  26. package/dist/builtin/workflows/src/shared/store.ts +99 -11
  27. package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
  28. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
  29. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
  30. package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
  31. package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
  32. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
  33. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
  34. package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
  35. package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
  36. package/package.json +1 -1
@@ -52,6 +52,9 @@ import type {
52
52
  RunSnapshot,
53
53
  WorkflowOverlayAdapter,
54
54
  WorkflowFailureKind,
55
+ WorkflowFailureCode,
56
+ WorkflowFailureRecoverability,
57
+ WorkflowFailureDisposition,
55
58
  PendingPrompt,
56
59
  PromptKind,
57
60
  WorkflowChildReplaySnapshot,
@@ -82,6 +85,7 @@ import {
82
85
  appendStageStart,
83
86
  appendStageEnd,
84
87
  appendRunEnd,
88
+ appendRunBlocked,
85
89
  } from "../../shared/persistence-session-entries.js";
86
90
  import { buildModelCandidatesFromCatalog, validateWorkflowModels, workflowModelId } from "../shared/model-fallback.js";
87
91
  import { validateInputs, type ValidationError } from "../shared/validate-inputs.js";
@@ -437,6 +441,22 @@ export function readinessResultMeansAdvance(result: unknown): boolean {
437
441
  );
438
442
  }
439
443
 
444
+ /**
445
+ * True when a raw tool-result record from an `ask_user_question` call carries a
446
+ * `details.answers[].kind === "chat"` entry. Used by the readiness-gate watcher
447
+ * to skip `confirmReadiness` and return control directly to the stage composer.
448
+ */
449
+ export function toolResultHasChatAnswer(result: unknown): boolean {
450
+ if (result === null || typeof result !== "object") return false;
451
+ const details = (result as Record<string, unknown>)["details"];
452
+ if (details === null || typeof details !== "object") return false;
453
+ const answers = (details as Record<string, unknown>)["answers"];
454
+ if (!Array.isArray(answers)) return false;
455
+ return answers.some(
456
+ (a) => a !== null && typeof a === "object" && (a as Record<string, unknown>)["kind"] === "chat",
457
+ );
458
+ }
459
+
440
460
  let cachedReadinessGateTool: ReturnType<typeof createAskUserQuestionToolDefinition> | undefined;
441
461
  function readinessGateTool(): ReturnType<typeof createAskUserQuestionToolDefinition> {
442
462
  return (cachedReadinessGateTool ??= createAskUserQuestionToolDefinition());
@@ -1118,7 +1138,13 @@ function workflowDetailsFromRun(
1118
1138
  mode,
1119
1139
  action: "run",
1120
1140
  runId: runResult.runId,
1121
- status: runResult.status === "killed" ? "killed" : runResult.status === "failed" ? "failed" : "completed",
1141
+ status: runResult.status === "completed"
1142
+ ? "completed"
1143
+ : runResult.status === "failed"
1144
+ ? "failed"
1145
+ : runResult.status === "killed"
1146
+ ? "killed"
1147
+ : "running",
1122
1148
  ...(options.context !== undefined ? { context: options.context } : {}),
1123
1149
  results: [...results],
1124
1150
  output: runResult.result,
@@ -1399,9 +1425,13 @@ function appendRunEndWhenRecorded(
1399
1425
  readonly result?: WorkflowOutputValues;
1400
1426
  readonly error?: string;
1401
1427
  readonly failureKind?: WorkflowFailureKind;
1428
+ readonly failureCode?: WorkflowFailureCode;
1429
+ readonly failureRecoverability?: WorkflowFailureRecoverability;
1430
+ readonly failureDisposition?: WorkflowFailureDisposition;
1402
1431
  readonly failureMessage?: string;
1403
1432
  readonly failedStageId?: string;
1404
1433
  readonly resumable?: boolean;
1434
+ readonly retryAfterMs?: number;
1405
1435
  readonly ts: number;
1406
1436
  },
1407
1437
  ): void {
@@ -1412,32 +1442,272 @@ function appendRunEndWhenRecorded(
1412
1442
  interface RunFailureMetadata {
1413
1443
  readonly errorMessage: string;
1414
1444
  readonly failureKind: WorkflowFailureKind;
1445
+ readonly failureCode?: WorkflowFailureCode;
1446
+ readonly failureRecoverability?: WorkflowFailureRecoverability;
1447
+ readonly failureDisposition?: WorkflowFailureDisposition;
1415
1448
  readonly failureMessage: string;
1416
1449
  readonly failedStageId?: string;
1417
1450
  readonly resumable: boolean;
1451
+ readonly retryAfterMs?: number;
1418
1452
  }
1419
1453
 
1420
1454
  function applyFailureToStage(stage: StageSnapshot, failure: WorkflowFailure): void {
1421
1455
  stage.status = "failed";
1422
1456
  stage.error = failure.userMessage;
1423
1457
  stage.failureKind = failure.kind;
1458
+ stage.failureCode = failure.code;
1459
+ stage.failureRecoverability = failure.recoverability;
1460
+ stage.failureDisposition = failure.disposition;
1461
+ stage.retryAfterMs = failure.retryAfterMs;
1424
1462
  stage.failureMessage = failure.message;
1425
1463
  }
1426
1464
 
1427
- function runFailureMetadata(err: unknown, stages: readonly StageSnapshot[]): RunFailureMetadata {
1428
- const classified = classifyWorkflowFailure(err);
1465
+ function runFailureMetadata(
1466
+ failure: WorkflowFailure,
1467
+ stages: readonly StageSnapshot[],
1468
+ ): RunFailureMetadata {
1429
1469
  const failedStage = stages.find((stage) => stage.status === "failed");
1430
- const failureKind = failedStage?.failureKind ?? classified.kind;
1470
+ const failureKind = failedStage?.failureKind ?? failure.kind;
1471
+ const failureCode = failedStage?.failureCode ?? failure.code;
1472
+ const failureRecoverability = failedStage?.failureRecoverability ?? failure.recoverability;
1473
+ const failureDisposition = failedStage?.failureDisposition ?? failure.disposition;
1474
+ const retryAfterMs = failedStage?.retryAfterMs ?? failure.retryAfterMs;
1475
+
1476
+ return {
1477
+ errorMessage: failedStage?.error ?? failure.userMessage,
1478
+ failureKind,
1479
+ ...(failureCode !== undefined ? { failureCode } : {}),
1480
+ failureRecoverability,
1481
+ failureDisposition,
1482
+ failureMessage: failedStage?.failureMessage ?? failure.message,
1483
+ ...(failedStage !== undefined ? { failedStageId: failedStage.id } : {}),
1484
+ resumable: failure.resumable,
1485
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
1486
+ };
1487
+ }
1488
+
1489
+ interface SelectedRunFailureMetadata extends RunFailureMetadata {
1490
+ readonly failedStageIds: readonly string[];
1491
+ }
1492
+
1493
+ function stageDispositionResumable(
1494
+ disposition: WorkflowFailureDisposition | undefined,
1495
+ fallback: boolean,
1496
+ ): boolean {
1497
+ if (disposition === "terminal_killed") return false;
1498
+ if (disposition === "active_blocked") return true;
1499
+ return fallback;
1500
+ }
1501
+
1502
+ function runFailureMetadataFromStage(
1503
+ fallbackFailure: WorkflowFailure,
1504
+ stage: StageSnapshot,
1505
+ ): RunFailureMetadata {
1506
+ const failureKind = stage.failureKind ?? fallbackFailure.kind;
1507
+ const failureCode = stage.failureCode ?? fallbackFailure.code;
1508
+ const failureRecoverability = stage.failureRecoverability ?? fallbackFailure.recoverability;
1509
+ const failureDisposition = stage.failureDisposition ?? fallbackFailure.disposition;
1510
+ const retryAfterMs = stage.retryAfterMs ?? fallbackFailure.retryAfterMs;
1431
1511
 
1432
1512
  return {
1433
- errorMessage: classified.userMessage,
1513
+ errorMessage: stage.error ?? fallbackFailure.userMessage,
1434
1514
  failureKind,
1435
- failureMessage: failedStage?.failureMessage ?? classified.message,
1515
+ ...(failureCode !== undefined ? { failureCode } : {}),
1516
+ failureRecoverability,
1517
+ failureDisposition,
1518
+ failureMessage: stage.failureMessage ?? fallbackFailure.message,
1519
+ failedStageId: stage.id,
1520
+ resumable: stageDispositionResumable(failureDisposition, fallbackFailure.resumable),
1521
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
1522
+ };
1523
+ }
1524
+
1525
+ function runFailureMetadataFromFailure(
1526
+ failure: WorkflowFailure,
1527
+ failedStage: StageSnapshot | undefined,
1528
+ ): RunFailureMetadata {
1529
+ return {
1530
+ errorMessage: failedStage?.error ?? failure.userMessage,
1531
+ failureKind: failure.kind,
1532
+ ...(failure.code !== undefined ? { failureCode: failure.code } : {}),
1533
+ failureRecoverability: failure.recoverability,
1534
+ failureDisposition: failure.disposition,
1535
+ failureMessage: failedStage?.failureMessage ?? failure.message,
1436
1536
  ...(failedStage !== undefined ? { failedStageId: failedStage.id } : {}),
1437
- resumable: classified.resumable,
1537
+ resumable: failure.resumable,
1538
+ ...(failure.retryAfterMs !== undefined ? { retryAfterMs: failure.retryAfterMs } : {}),
1438
1539
  };
1439
1540
  }
1440
1541
 
1542
+ function executorAggregateErrorItems(error: unknown): readonly unknown[] {
1543
+ const nativeErrors = error instanceof AggregateError ? error.errors as unknown : undefined;
1544
+ const errors = nativeErrors ?? (error !== null && typeof error === "object"
1545
+ ? (error as Record<string, unknown>)["errors"]
1546
+ : undefined);
1547
+ return Array.isArray(errors) ? errors : [];
1548
+ }
1549
+
1550
+ function isAggregateWrapper(error: unknown): boolean {
1551
+ return executorAggregateErrorItems(error).length > 0;
1552
+ }
1553
+
1554
+ function aggregateInnerFailures(
1555
+ error: unknown,
1556
+ classifyFailure: (error: unknown) => WorkflowFailure,
1557
+ ): readonly WorkflowFailure[] {
1558
+ return executorAggregateErrorItems(error).map((innerError) => classifyFailure(innerError));
1559
+ }
1560
+
1561
+ type StageFailureCandidate = {
1562
+ readonly source: "stage";
1563
+ readonly stage: StageSnapshot;
1564
+ readonly disposition: WorkflowFailureDisposition;
1565
+ readonly recoverability: WorkflowFailureRecoverability;
1566
+ };
1567
+
1568
+ type AggregateFailureCandidate = {
1569
+ readonly source: "aggregate";
1570
+ readonly failure: WorkflowFailure;
1571
+ readonly disposition: WorkflowFailureDisposition;
1572
+ readonly recoverability: WorkflowFailureRecoverability;
1573
+ };
1574
+
1575
+ type OuterFailureCandidate = {
1576
+ readonly source: "outer";
1577
+ readonly failure: WorkflowFailure;
1578
+ readonly disposition: WorkflowFailureDisposition;
1579
+ readonly recoverability: WorkflowFailureRecoverability;
1580
+ };
1581
+
1582
+ type FailureCandidate = StageFailureCandidate | AggregateFailureCandidate | OuterFailureCandidate;
1583
+
1584
+ function stageFailureCandidate(stage: StageSnapshot): StageFailureCandidate {
1585
+ return {
1586
+ source: "stage",
1587
+ stage,
1588
+ disposition: stage.failureDisposition ?? "terminal_failed",
1589
+ recoverability: stage.failureRecoverability ?? "unknown",
1590
+ };
1591
+ }
1592
+
1593
+ function aggregateFailureCandidate(failure: WorkflowFailure): AggregateFailureCandidate {
1594
+ return {
1595
+ source: "aggregate",
1596
+ failure,
1597
+ disposition: failure.disposition,
1598
+ recoverability: failure.recoverability,
1599
+ };
1600
+ }
1601
+
1602
+ function outerFailureCandidate(failure: WorkflowFailure): OuterFailureCandidate {
1603
+ return {
1604
+ source: "outer",
1605
+ failure,
1606
+ disposition: failure.disposition,
1607
+ recoverability: failure.recoverability,
1608
+ };
1609
+ }
1610
+
1611
+ function isRecoverableActiveBlockedCandidate(candidate: FailureCandidate): boolean {
1612
+ return candidate.disposition === "active_blocked" && candidate.recoverability === "recoverable";
1613
+ }
1614
+
1615
+ function runFailureMetadataFromCandidate(
1616
+ fallbackFailure: WorkflowFailure,
1617
+ candidate: FailureCandidate,
1618
+ thrownError: unknown,
1619
+ ): RunFailureMetadata {
1620
+ let metadata: RunFailureMetadata;
1621
+ switch (candidate.source) {
1622
+ case "stage":
1623
+ metadata = runFailureMetadataFromStage(fallbackFailure, candidate.stage);
1624
+ break;
1625
+ case "aggregate":
1626
+ case "outer":
1627
+ metadata = runFailureMetadataFromFailure(candidate.failure, undefined);
1628
+ break;
1629
+ }
1630
+
1631
+ if (candidate.disposition === "terminal_failed" && isAggregateWrapper(thrownError)) {
1632
+ return { ...metadata, errorMessage: fallbackFailure.userMessage };
1633
+ }
1634
+
1635
+ return metadata;
1636
+ }
1637
+
1638
+ function failedStageIdsForCandidate(
1639
+ candidate: FailureCandidate,
1640
+ failedStages: readonly StageSnapshot[],
1641
+ ): readonly string[] {
1642
+ switch (candidate.source) {
1643
+ case "aggregate":
1644
+ return failedStages.map((stage) => stage.id);
1645
+ case "outer":
1646
+ return [];
1647
+ case "stage":
1648
+ return failedStages
1649
+ .filter((stage) => (stage.failureDisposition ?? "terminal_failed") === candidate.disposition)
1650
+ .map((stage) => stage.id);
1651
+ }
1652
+ }
1653
+
1654
+ function selectedMetadata(
1655
+ metadata: RunFailureMetadata,
1656
+ failedStageIds: readonly string[],
1657
+ ): SelectedRunFailureMetadata {
1658
+ return {
1659
+ ...metadata,
1660
+ failedStageIds,
1661
+ };
1662
+ }
1663
+
1664
+ function selectRunFailureDisposition(input: {
1665
+ readonly outerFailure: WorkflowFailure;
1666
+ readonly thrownError: unknown;
1667
+ readonly stages: readonly StageSnapshot[];
1668
+ readonly classifyFailure: (error: unknown) => WorkflowFailure;
1669
+ }): SelectedRunFailureMetadata {
1670
+ const failedStages = input.stages.filter((stage) => stage.status === "failed");
1671
+ const failedStageIds = failedStages.map((stage) => stage.id);
1672
+ const aggregateFailures = aggregateInnerFailures(input.thrownError, input.classifyFailure);
1673
+ const candidates: readonly FailureCandidate[] = [
1674
+ ...failedStages.map(stageFailureCandidate),
1675
+ ...aggregateFailures.map(aggregateFailureCandidate),
1676
+ outerFailureCandidate(input.outerFailure),
1677
+ ];
1678
+ // Candidate precedence mirrors lifecycle severity: terminal killed is non-resumable
1679
+ // and wins first, terminal failed wins over recoverable blocks, and active-blocked
1680
+ // is only preserved when every observed failure is recoverable active-blocked.
1681
+ const terminalKilledCandidate = candidates.find((candidate) => candidate.disposition === "terminal_killed");
1682
+ if (terminalKilledCandidate !== undefined) {
1683
+ return selectedMetadata(
1684
+ runFailureMetadataFromCandidate(input.outerFailure, terminalKilledCandidate, input.thrownError),
1685
+ failedStageIdsForCandidate(terminalKilledCandidate, failedStages),
1686
+ );
1687
+ }
1688
+
1689
+ const terminalFailedCandidate = candidates.find((candidate) => candidate.disposition === "terminal_failed");
1690
+ if (terminalFailedCandidate !== undefined) {
1691
+ return selectedMetadata(
1692
+ runFailureMetadataFromCandidate(input.outerFailure, terminalFailedCandidate, input.thrownError),
1693
+ failedStageIdsForCandidate(terminalFailedCandidate, failedStages),
1694
+ );
1695
+ }
1696
+
1697
+ const recoverableBlockedCandidate = candidates.find(isRecoverableActiveBlockedCandidate);
1698
+ if (
1699
+ recoverableBlockedCandidate !== undefined &&
1700
+ candidates.every(isRecoverableActiveBlockedCandidate)
1701
+ ) {
1702
+ return selectedMetadata(
1703
+ runFailureMetadataFromCandidate(input.outerFailure, recoverableBlockedCandidate, input.thrownError),
1704
+ failedStageIds,
1705
+ );
1706
+ }
1707
+
1708
+ return selectedMetadata(runFailureMetadata(input.outerFailure, input.stages), failedStageIds);
1709
+ }
1710
+
1441
1711
  function stageReplayFields(stage: StageSnapshot): Partial<Pick<StageSnapshot, "replayKey" | "replayedFromStageId" | "replayed">> {
1442
1712
  return {
1443
1713
  ...(stage.replayKey !== undefined ? { replayKey: stage.replayKey } : {}),
@@ -1649,6 +1919,9 @@ function finalizeKilled(
1649
1919
  const errorMessage = "workflow killed";
1650
1920
  const metadata = {
1651
1921
  failureKind: "cancelled" as const,
1922
+ failureCode: "cancelled" as const,
1923
+ failureRecoverability: "non_recoverable" as const,
1924
+ failureDisposition: "terminal_killed" as const,
1652
1925
  failureMessage: errorMessage,
1653
1926
  resumable: false,
1654
1927
  };
@@ -1669,6 +1942,80 @@ function finalizeKilled(
1669
1942
  };
1670
1943
  }
1671
1944
 
1945
+ function finalizeKilledByFailure(
1946
+ runId: string,
1947
+ runSnapshot: RunSnapshot,
1948
+ activeStore: Store,
1949
+ persistence: WorkflowPersistencePort | undefined,
1950
+ onRunEnd: RunOpts["onRunEnd"],
1951
+ metadata: RunFailureMetadata,
1952
+ ): RunResult {
1953
+ const recorded = activeStore.recordRunEnd(runId, "killed", undefined, metadata.errorMessage, metadata);
1954
+ onRunEnd?.(runId, "killed", undefined, metadata.errorMessage);
1955
+ appendRunEndWhenRecorded(persistence, recorded, {
1956
+ runId,
1957
+ status: "killed",
1958
+ error: metadata.errorMessage,
1959
+ failureKind: metadata.failureKind,
1960
+ ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
1961
+ ...(metadata.failureRecoverability !== undefined ? { failureRecoverability: metadata.failureRecoverability } : {}),
1962
+ ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
1963
+ failureMessage: metadata.failureMessage,
1964
+ ...(metadata.failedStageId !== undefined ? { failedStageId: metadata.failedStageId } : {}),
1965
+ resumable: false,
1966
+ ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
1967
+ ts: Date.now(),
1968
+ });
1969
+ return {
1970
+ runId,
1971
+ status: "killed",
1972
+ error: metadata.errorMessage,
1973
+ stages: [...runSnapshot.stages],
1974
+ };
1975
+ }
1976
+
1977
+ function recordActiveBlockedFailure(
1978
+ runId: string,
1979
+ runSnapshot: RunSnapshot,
1980
+ activeStore: Store,
1981
+ persistence: WorkflowPersistencePort | undefined,
1982
+ metadata: RunFailureMetadata & { readonly failureRecoverability: "recoverable"; readonly failedStageId: string },
1983
+ ): RunResult {
1984
+ const blockedAt = Date.now();
1985
+ const recorded = activeStore.recordRunBlocked(runId, metadata.errorMessage, {
1986
+ failureKind: metadata.failureKind,
1987
+ ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
1988
+ failureRecoverability: "recoverable",
1989
+ ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
1990
+ failureMessage: metadata.failureMessage,
1991
+ failedStageId: metadata.failedStageId,
1992
+ resumable: true,
1993
+ ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
1994
+ blockedAt,
1995
+ });
1996
+ if (recorded && persistence !== undefined) {
1997
+ appendRunBlocked(persistence, {
1998
+ runId,
1999
+ failedStageId: metadata.failedStageId,
2000
+ error: metadata.errorMessage,
2001
+ failureKind: metadata.failureKind,
2002
+ ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
2003
+ failureMessage: metadata.failureMessage,
2004
+ failureRecoverability: "recoverable",
2005
+ ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
2006
+ ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
2007
+ resumable: true,
2008
+ ts: blockedAt,
2009
+ });
2010
+ }
2011
+ return {
2012
+ runId,
2013
+ status: "running",
2014
+ error: metadata.errorMessage,
2015
+ stages: [...runSnapshot.stages],
2016
+ };
2017
+ }
2018
+
1672
2019
  // ---------------------------------------------------------------------------
1673
2020
  // Main executor
1674
2021
  // ---------------------------------------------------------------------------
@@ -1933,6 +2280,15 @@ export async function run<TInputs extends WorkflowInputValues>(
1933
2280
  } : {}),
1934
2281
  };
1935
2282
 
2283
+ const classifiedFailures = new Map<unknown, WorkflowFailure>();
2284
+ const classifyExecutorFailure = (error: unknown): WorkflowFailure => {
2285
+ const cached = classifiedFailures.get(error);
2286
+ if (cached !== undefined) return cached;
2287
+ const classified = classifyWorkflowFailure(error);
2288
+ classifiedFailures.set(error, classified);
2289
+ return classified;
2290
+ };
2291
+
1936
2292
  activeStore.recordRunStart(runSnapshot);
1937
2293
  // When the caller already has a controller registered (the detached runner
1938
2294
  // pre-registers before calling run() so abort() can hit the run during
@@ -2045,6 +2401,13 @@ export async function run<TInputs extends WorkflowInputValues>(
2045
2401
  activeStore.recordStageBlocked(runId, stage.id, blockedBy);
2046
2402
  };
2047
2403
 
2404
+ const blockKnownNonTerminalDescendants = (failedStageId: string): void => {
2405
+ for (const descendant of descendantsOf(failedStageId)) {
2406
+ if (isTerminalStage(descendant) || descendant.status === "paused" || descendant.status === "blocked") continue;
2407
+ blockStageUntilCascadeRelease(descendant, failedStageId);
2408
+ }
2409
+ };
2410
+
2048
2411
  const markCascadePaused = (stageId: string, ownerStageId: string): void => {
2049
2412
  let owners = cascadePauseOwners.get(stageId);
2050
2413
  if (!owners) {
@@ -2210,7 +2573,11 @@ export async function run<TInputs extends WorkflowInputValues>(
2210
2573
  durationMs: stageSnapshot.durationMs,
2211
2574
  ...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
2212
2575
  ...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
2576
+ ...(stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {}),
2577
+ ...(stageSnapshot.failureRecoverability !== undefined ? { failureRecoverability: stageSnapshot.failureRecoverability } : {}),
2578
+ ...(stageSnapshot.failureDisposition !== undefined ? { failureDisposition: stageSnapshot.failureDisposition } : {}),
2213
2579
  ...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
2580
+ ...(stageSnapshot.retryAfterMs !== undefined ? { retryAfterMs: stageSnapshot.retryAfterMs } : {}),
2214
2581
  ...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
2215
2582
  ...stageReplayFields(stageSnapshot),
2216
2583
  ...(stageSnapshot.workflowChild !== undefined ? { workflowChild: stageSnapshot.workflowChild } : {}),
@@ -2230,10 +2597,7 @@ export async function run<TInputs extends WorkflowInputValues>(
2230
2597
  stageSnapshot.result = summaryOrError;
2231
2598
  if (workflowChild !== undefined) stageSnapshot.workflowChild = workflowChild;
2232
2599
  } else {
2233
- const failure = classifyWorkflowFailure(failureError);
2234
- stageSnapshot.error = failure.userMessage;
2235
- stageSnapshot.failureKind = failure.kind;
2236
- stageSnapshot.failureMessage = failure.message;
2600
+ applyFailureToStage(stageSnapshot, classifyExecutorFailure(failureError));
2237
2601
  }
2238
2602
  stageSnapshot.endedAt = Date.now();
2239
2603
  stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
@@ -2348,7 +2712,11 @@ export async function run<TInputs extends WorkflowInputValues>(
2348
2712
  durationMs: stageSnapshot.durationMs,
2349
2713
  ...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
2350
2714
  ...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
2715
+ ...(stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {}),
2716
+ ...(stageSnapshot.failureRecoverability !== undefined ? { failureRecoverability: stageSnapshot.failureRecoverability } : {}),
2717
+ ...(stageSnapshot.failureDisposition !== undefined ? { failureDisposition: stageSnapshot.failureDisposition } : {}),
2351
2718
  ...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
2719
+ ...(stageSnapshot.retryAfterMs !== undefined ? { retryAfterMs: stageSnapshot.retryAfterMs } : {}),
2352
2720
  ...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
2353
2721
  ...stageReplayFields(stageSnapshot),
2354
2722
  });
@@ -2416,7 +2784,7 @@ export async function run<TInputs extends WorkflowInputValues>(
2416
2784
  stageSnapshot.skippedReason = "run-aborted";
2417
2785
  finalizePromptStage("skipped");
2418
2786
  } else {
2419
- applyFailureToStage(stageSnapshot, classifyWorkflowFailure(err));
2787
+ applyFailureToStage(stageSnapshot, classifyExecutorFailure(err));
2420
2788
  finalizePromptStage("failed");
2421
2789
  }
2422
2790
  throw err;
@@ -2636,6 +3004,10 @@ export async function run<TInputs extends WorkflowInputValues>(
2636
3004
  // after a turn that asked the user a question ends, the workflow must
2637
3005
  // confirm readiness before completing/advancing the stage.
2638
3006
  let askUserQuestionObservedThisTurn = false;
3007
+ // Set when the completed ask_user_question call carried a chat answer.
3008
+ // When true the readiness gate is bypassed — the stage stays in the
3009
+ // composer without showing an extra confirmation UI (#1264).
3010
+ let chatAnswerObservedThisTurn = false;
2639
3011
  const hasActiveAskUserQuestion = (): boolean =>
2640
3012
  activeAskUserQuestionCalls.size > 0 || activeAskUserQuestionAnonymousCalls > 0;
2641
3013
  const unsubscribeAskUserQuestionWatcher = innerCtx.subscribe((event) => {
@@ -2667,6 +3039,12 @@ export async function run<TInputs extends WorkflowInputValues>(
2667
3039
  return;
2668
3040
  }
2669
3041
 
3042
+ // If the completed call carried a chat answer, remember it so the
3043
+ // readiness gate can bypass confirmReadiness for this turn (#1264).
3044
+ if (toolResultHasChatAnswer((event as Record<string, unknown>)["result"])) {
3045
+ chatAnswerObservedThisTurn = true;
3046
+ }
3047
+
2670
3048
  if (!hasActiveAskUserQuestion()) {
2671
3049
  activeStore.recordStageAwaitingInput(runId, stageId, false);
2672
3050
  stageUiBroker.clearStagePrompt(runId, stageId);
@@ -2802,7 +3180,11 @@ export async function run<TInputs extends WorkflowInputValues>(
2802
3180
  durationMs: stageSnapshot.durationMs,
2803
3181
  ...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
2804
3182
  ...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
3183
+ ...(stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {}),
3184
+ ...(stageSnapshot.failureRecoverability !== undefined ? { failureRecoverability: stageSnapshot.failureRecoverability } : {}),
3185
+ ...(stageSnapshot.failureDisposition !== undefined ? { failureDisposition: stageSnapshot.failureDisposition } : {}),
2805
3186
  ...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
3187
+ ...(stageSnapshot.retryAfterMs !== undefined ? { retryAfterMs: stageSnapshot.retryAfterMs } : {}),
2806
3188
  ...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
2807
3189
  ...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
2808
3190
  ...stageReplayFields(stageSnapshot),
@@ -2964,6 +3346,7 @@ export async function run<TInputs extends WorkflowInputValues>(
2964
3346
  try {
2965
3347
  // Run the stage's initial agent turn.
2966
3348
  askUserQuestionObservedThisTurn = false;
3349
+ chatAnswerObservedThisTurn = false;
2967
3350
  result = await raceAbort(call(), ownController.signal);
2968
3351
 
2969
3352
  // Per-turn readiness gate (#1099). When an agent turn ENDS (control
@@ -2986,11 +3369,17 @@ export async function run<TInputs extends WorkflowInputValues>(
2986
3369
  });
2987
3370
  try {
2988
3371
  while (askUserQuestionObservedThisTurn) {
2989
- if ((await confirmReadiness()) === "advance") break;
3372
+ // Chat answer: bypass the confirmation UI and stay in the composer
3373
+ // without asking again (#1264).
3374
+ const decision = chatAnswerObservedThisTurn
3375
+ ? "stay"
3376
+ : await confirmReadiness();
3377
+ if (decision === "advance") break;
2990
3378
  if (ownController.signal.aborted) break;
2991
3379
  // Stay: return control to the user and await their next
2992
3380
  // composer-driven turn end before re-checking.
2993
3381
  askUserQuestionObservedThisTurn = false;
3382
+ chatAnswerObservedThisTurn = false;
2994
3383
  await raceAbort(
2995
3384
  new Promise<void>((resolve) => {
2996
3385
  resolveNextTurnEnd = resolve;
@@ -3033,7 +3422,7 @@ export async function run<TInputs extends WorkflowInputValues>(
3033
3422
  return result;
3034
3423
  } catch (err) {
3035
3424
  if (!ownController.signal.aborted && !skippedForParallelFailFast) {
3036
- applyFailureToStage(stageSnapshot, classifyWorkflowFailure(err));
3425
+ applyFailureToStage(stageSnapshot, classifyExecutorFailure(err));
3037
3426
  }
3038
3427
  throw err;
3039
3428
  } finally {
@@ -3396,7 +3785,40 @@ export async function run<TInputs extends WorkflowInputValues>(
3396
3785
  return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
3397
3786
  }
3398
3787
 
3399
- const metadata = runFailureMetadata(err, runSnapshot.stages);
3788
+ const failure = classifyExecutorFailure(err);
3789
+ const metadata = selectRunFailureDisposition({
3790
+ outerFailure: failure,
3791
+ thrownError: err,
3792
+ stages: runSnapshot.stages,
3793
+ classifyFailure: classifyExecutorFailure,
3794
+ });
3795
+
3796
+ if (metadata.failureDisposition === "terminal_killed") {
3797
+ for (const failedStageId of metadata.failedStageIds) {
3798
+ blockKnownNonTerminalDescendants(failedStageId);
3799
+ }
3800
+ return finalizeKilledByFailure(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd, {
3801
+ ...metadata,
3802
+ resumable: false,
3803
+ });
3804
+ }
3805
+
3806
+ if (
3807
+ metadata.failureDisposition === "active_blocked" &&
3808
+ metadata.failedStageId !== undefined &&
3809
+ metadata.failureRecoverability === "recoverable"
3810
+ ) {
3811
+ for (const failedStageId of metadata.failedStageIds) {
3812
+ blockKnownNonTerminalDescendants(failedStageId);
3813
+ }
3814
+ return recordActiveBlockedFailure(runId, runSnapshot, activeStore, opts.persistence, {
3815
+ ...metadata,
3816
+ failureRecoverability: "recoverable",
3817
+ failedStageId: metadata.failedStageId,
3818
+ resumable: true,
3819
+ });
3820
+ }
3821
+
3400
3822
  const recorded = activeStore.recordRunEnd(runId, "failed", undefined, metadata.errorMessage, metadata);
3401
3823
  opts.onRunEnd?.(runId, "failed", undefined, metadata.errorMessage);
3402
3824
 
@@ -3405,9 +3827,13 @@ export async function run<TInputs extends WorkflowInputValues>(
3405
3827
  status: "failed",
3406
3828
  error: metadata.errorMessage,
3407
3829
  failureKind: metadata.failureKind,
3830
+ ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
3831
+ ...(metadata.failureRecoverability !== undefined ? { failureRecoverability: metadata.failureRecoverability } : {}),
3832
+ ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
3408
3833
  failureMessage: metadata.failureMessage,
3409
3834
  ...(metadata.failedStageId !== undefined ? { failedStageId: metadata.failedStageId } : {}),
3410
3835
  resumable: metadata.resumable,
3836
+ ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
3411
3837
  ts: Date.now(),
3412
3838
  });
3413
3839