@bastani/atomic 0.8.27-alpha.1 → 0.8.28-alpha.1

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 (112) hide show
  1. package/CHANGELOG.md +24 -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 +2 -2
  6. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  7. package/dist/builtin/subagents/package.json +1 -1
  8. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  9. package/dist/builtin/web-access/package.json +1 -1
  10. package/dist/builtin/workflows/CHANGELOG.md +20 -0
  11. package/dist/builtin/workflows/README.md +11 -9
  12. package/dist/builtin/workflows/package.json +1 -1
  13. package/dist/builtin/workflows/src/authoring.d.ts +5 -2
  14. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +3 -1
  15. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +17 -25
  16. package/dist/builtin/workflows/src/extension/index.ts +133 -18
  17. package/dist/builtin/workflows/src/extension/render-result.ts +22 -2
  18. package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -3
  19. package/dist/builtin/workflows/src/runs/foreground/executor.ts +210 -16
  20. package/dist/builtin/workflows/src/sdk-surface.ts +1 -1
  21. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +42 -5
  22. package/dist/builtin/workflows/src/shared/store-types.ts +8 -2
  23. package/dist/builtin/workflows/src/shared/store.ts +51 -0
  24. package/dist/builtin/workflows/src/shared/types.ts +14 -4
  25. package/dist/builtin/workflows/src/tui/graph-view.ts +4 -1
  26. package/dist/builtin/workflows/src/tui/prompt-card.ts +6 -0
  27. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +11 -1
  28. package/dist/core/agent-session.d.ts +4 -4
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +147 -31
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/auth-guidance.d.ts +10 -1
  33. package/dist/core/auth-guidance.d.ts.map +1 -1
  34. package/dist/core/auth-guidance.js +26 -1
  35. package/dist/core/auth-guidance.js.map +1 -1
  36. package/dist/core/compaction/branch-summarization.d.ts +2 -2
  37. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  38. package/dist/core/compaction/branch-summarization.js +7 -7
  39. package/dist/core/compaction/branch-summarization.js.map +1 -1
  40. package/dist/core/compaction/compaction.d.ts +4 -84
  41. package/dist/core/compaction/compaction.d.ts.map +1 -1
  42. package/dist/core/compaction/compaction.js +3 -479
  43. package/dist/core/compaction/compaction.js.map +1 -1
  44. package/dist/core/compaction/context-compaction.d.ts.map +1 -1
  45. package/dist/core/compaction/context-compaction.js +39 -82
  46. package/dist/core/compaction/context-compaction.js.map +1 -1
  47. package/dist/core/compaction/index.d.ts +1 -1
  48. package/dist/core/compaction/index.d.ts.map +1 -1
  49. package/dist/core/compaction/index.js +1 -1
  50. package/dist/core/compaction/index.js.map +1 -1
  51. package/dist/core/extensions/types.d.ts +10 -8
  52. package/dist/core/extensions/types.d.ts.map +1 -1
  53. package/dist/core/extensions/types.js.map +1 -1
  54. package/dist/core/index.d.ts +1 -1
  55. package/dist/core/index.d.ts.map +1 -1
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/messages.d.ts +1 -11
  58. package/dist/core/messages.d.ts.map +1 -1
  59. package/dist/core/messages.js +10 -25
  60. package/dist/core/messages.js.map +1 -1
  61. package/dist/core/session-manager.d.ts +5 -8
  62. package/dist/core/session-manager.d.ts.map +1 -1
  63. package/dist/core/session-manager.js +12 -76
  64. package/dist/core/session-manager.js.map +1 -1
  65. package/dist/core/settings-manager.d.ts +0 -3
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +0 -4
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/index.d.ts +3 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +3 -3
  72. package/dist/index.js.map +1 -1
  73. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -5
  74. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/chat-message-renderer.js +5 -9
  76. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  77. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/chat-session-host.js +0 -3
  79. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  80. package/dist/modes/interactive/components/index.d.ts +0 -1
  81. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/index.js +0 -1
  83. package/dist/modes/interactive/components/index.js.map +1 -1
  84. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  85. package/dist/modes/interactive/interactive-mode.js +4 -27
  86. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  87. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  88. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  89. package/dist/modes/rpc/rpc-client.js +2 -2
  90. package/dist/modes/rpc/rpc-client.js.map +1 -1
  91. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-mode.js +1 -1
  93. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  94. package/dist/modes/rpc/rpc-types.d.ts +0 -1
  95. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  96. package/dist/modes/rpc/rpc-types.js.map +1 -1
  97. package/docs/compaction.md +210 -181
  98. package/docs/extensions.md +31 -20
  99. package/docs/json.md +3 -4
  100. package/docs/session-format.md +12 -21
  101. package/docs/sessions.md +3 -1
  102. package/docs/settings.md +2 -5
  103. package/docs/workflows.md +11 -9
  104. package/examples/extensions/README.md +1 -1
  105. package/examples/extensions/custom-compaction.ts +43 -106
  106. package/examples/extensions/handoff.ts +6 -44
  107. package/examples/extensions/trigger-compact.ts +5 -4
  108. package/package.json +5 -5
  109. package/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
  110. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +0 -1
  111. package/dist/modes/interactive/components/compaction-summary-message.js +0 -43
  112. package/dist/modes/interactive/components/compaction-summary-message.js.map +0 -1
@@ -20,9 +20,9 @@ import { stripFrontmatter } from "../utils/frontmatter.js";
20
20
  import { resolvePath } from "../utils/paths.js";
21
21
  import { sleep } from "../utils/sleep.js";
22
22
  import { ATOMIC_GUIDE_COMMAND_NAME, ATOMIC_GUIDE_HELP_CHOICES, atomicGuideModeForChoice, getAtomicGuideMessage, isAtomicGuideHelpChoice, normalizeAtomicGuideMode, } from "./atomic-guide-command.js";
23
- import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
23
+ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage, formatUnresolvedModelMessage, } from "./auth-guidance.js";
24
24
  import { executeBashWithOperations } from "./bash-executor.js";
25
- import { calculateContextTokens, collectEntriesForBranchSummary, contextCompact as runContextCompact, estimateContextTokens, generateBranchSummary, prepareContextCompaction, shouldCompact, } from "./compaction/index.js";
25
+ import { calculateContextTokens, collectEntriesForBranchSummary, contextCompact as runContextCompact, estimateContextTokens, generateBranchSummary, prepareContextCompaction, shouldCompact, validateContextDeletionRequest, } from "./compaction/index.js";
26
26
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
27
27
  import { exportSessionToHtml } from "./export-html/index.js";
28
28
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -35,6 +35,15 @@ import { buildSystemPrompt } from "./system-prompt.js";
35
35
  import { createLocalBashOperations } from "./tools/bash.js";
36
36
  import { createAllToolDefinitions, defaultToolNames } from "./tools/index.js";
37
37
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
38
+ function deepFreeze(value) {
39
+ if (value && typeof value === "object") {
40
+ Object.freeze(value);
41
+ for (const nested of Object.values(value)) {
42
+ deepFreeze(nested);
43
+ }
44
+ }
45
+ return value;
46
+ }
38
47
  /**
39
48
  * Parse a skill block from message text.
40
49
  * Returns null if the text doesn't contain a skill block.
@@ -357,7 +366,7 @@ export class AgentSession {
357
366
  // Regular LLM message - persist as SessionMessageEntry
358
367
  this.sessionManager.appendMessage(event.message);
359
368
  }
360
- // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
369
+ // Other message types (bashExecution, branchSummary) are persisted elsewhere
361
370
  // Track assistant message for auto-compaction (checked on agent_end)
362
371
  if (event.message.role === "assistant") {
363
372
  this._lastAssistantMessage = event.message;
@@ -824,6 +833,15 @@ export class AgentSession {
824
833
  if (!this.model) {
825
834
  throw new Error(formatNoModelSelectedMessage());
826
835
  }
836
+ // Defensive guard: a model that never resolved to a real provider
837
+ // (for example an unknown/unresolved model id that reached this path
838
+ // as a bare string) has no `provider`, which would otherwise fail deep
839
+ // in auth resolution as the confusing "No API key found for undefined".
840
+ // Surface a clear, accurate "unknown model" error instead.
841
+ const resolvedProvider = this.model.provider;
842
+ if (typeof resolvedProvider !== "string" || resolvedProvider.length === 0) {
843
+ throw new Error(formatUnresolvedModelMessage(this.model));
844
+ }
827
845
  if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
828
846
  const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
829
847
  if (isOAuth) {
@@ -1483,6 +1501,9 @@ export class AgentSession {
1483
1501
  if (!this.model) {
1484
1502
  throw new Error(formatNoModelSelectedMessage());
1485
1503
  }
1504
+ // Capture the narrowed model now (control-flow narrowing holds immediately after the
1505
+ // guard) so the lazy planner-fallback closure below can use a non-undefined model.
1506
+ const model = this.model;
1486
1507
  const pathEntries = this.sessionManager.getBranch();
1487
1508
  const settings = this.settingsManager.getCompactionSettings();
1488
1509
  const mode = options.mode ?? "standard";
@@ -1490,28 +1511,118 @@ export class AgentSession {
1490
1511
  if (!preparation) {
1491
1512
  return undefined;
1492
1513
  }
1493
- const validated = await runContextCompact(preparation, this.model, options.apiKey, options.headers, options.abortController.signal, this.thinkingLevel, mode);
1514
+ // Planner fallback used when no extension supplies a deletionRequest. Auth is resolved
1515
+ // lazily here so extension-provided deletion requests keep working offline. Returns
1516
+ // undefined when auth is unavailable (auto-mode resolvers), signaling a no-op compaction.
1517
+ const runPlanner = async () => {
1518
+ const auth = await options.resolvePlannerAuth();
1519
+ if (!auth)
1520
+ return undefined;
1521
+ return runContextCompact(preparation, model, auth.apiKey, auth.headers, options.abortController.signal, this.thinkingLevel, mode);
1522
+ };
1523
+ // Emit session_before_compact to allow extensions to cancel or provide a deletion request.
1524
+ // This happens BEFORE any auth resolution so local extension deletion requests work
1525
+ // without configured API credentials.
1526
+ let fromExtension = false;
1527
+ let validated;
1528
+ if (this._extensionRunner.hasHandlers("session_before_compact")) {
1529
+ // Deep-clone the preparation only when a before-compact handler actually exists. Extensions
1530
+ // receive an isolated, frozen snapshot so they cannot mutate protection metadata
1531
+ // (protectedEntryIds, entry .protected flags, etc.) on the internal preparation used for
1532
+ // validation. Building it lazily avoids deep-cloning the transcript — largest exactly when
1533
+ // compaction fires — on the common no-extension path.
1534
+ let extensionPreparation;
1535
+ try {
1536
+ extensionPreparation = deepFreeze(structuredClone(preparation));
1537
+ }
1538
+ catch (error) {
1539
+ // structuredClone only throws if an entry carries a non-cloneable value (a function or a
1540
+ // class instance). Transcript entries are plain data today, so this guards a latent
1541
+ // invariant: surface a clear error instead of letting a raw DataCloneError abort an
1542
+ // otherwise-viable compaction.
1543
+ throw new Error(`Failed to snapshot transcript for compaction extensions: ${error instanceof Error ? error.message : String(error)}`);
1544
+ }
1545
+ const hookResult = (await this._extensionRunner.emit({
1546
+ type: "session_before_compact",
1547
+ reason: options.reason,
1548
+ mode,
1549
+ preparation: extensionPreparation,
1550
+ branchEntries: pathEntries,
1551
+ signal: options.abortController.signal,
1552
+ }));
1553
+ if (hookResult?.cancel) {
1554
+ throw new Error("Compaction cancelled");
1555
+ }
1556
+ if (hookResult?.deletionRequest) {
1557
+ const extensionDeletionRequest = hookResult.deletionRequest;
1558
+ // Reject empty deletion requests before any side effects (backup, append, rebuild).
1559
+ if (!Array.isArray(extensionDeletionRequest.deletions) || extensionDeletionRequest.deletions.length === 0) {
1560
+ throw new Error("No safe context deletions proposed by extension");
1561
+ }
1562
+ // Validate against the internal transcript snapshot, not the extension-facing clone.
1563
+ // Auth is NOT resolved here — local extension deletion requests work offline.
1564
+ validated = validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript, { mode });
1565
+ // Reject if reconciliation reduced deletions to zero.
1566
+ if (validated.deletedTargets.length === 0) {
1567
+ throw new Error("No safe context deletions proposed by extension");
1568
+ }
1569
+ fromExtension = true;
1570
+ }
1571
+ }
1572
+ // Planner fallback shared by both paths: no before-compact handler at all, or a handler that
1573
+ // observed without supplying a deletionRequest. Resolves auth lazily; undefined means auth is
1574
+ // unavailable (auto-mode resolvers), so compaction is a no-op.
1575
+ if (!validated) {
1576
+ const plannerResult = await runPlanner();
1577
+ if (!plannerResult) {
1578
+ return undefined;
1579
+ }
1580
+ validated = plannerResult;
1581
+ }
1494
1582
  if (options.abortController.signal.aborted) {
1495
1583
  throw new Error("Compaction cancelled");
1496
1584
  }
1497
1585
  const backupPath = this.sessionManager.writeBackupSnapshot(options.backupLabel);
1498
- this.sessionManager.appendContextCompaction(validated.deletedTargets, validated.protectedEntryIds, validated.stats, backupPath);
1586
+ const compactionEntryId = this.sessionManager.appendContextCompaction(validated.deletedTargets, validated.protectedEntryIds, validated.stats, backupPath);
1499
1587
  const sessionContext = this.sessionManager.buildSessionContext();
1500
1588
  this.agent.state.messages = sessionContext.messages;
1501
- return {
1589
+ const result = {
1502
1590
  ...validated,
1503
1591
  promptVersion: 1,
1504
1592
  ...(backupPath ? { backupPath } : {}),
1505
1593
  };
1594
+ // Emit session_compact so extensions can observe the validated result. This is a pure
1595
+ // observation hook fired AFTER the compaction has been committed (backup written,
1596
+ // context_compaction entry persisted, active context rebuilt). A misbehaving observer must
1597
+ // never turn a successful, already-persisted compaction into a reported failure, so any
1598
+ // throw is routed to the non-fatal extension-error channel and compaction still reports
1599
+ // success.
1600
+ const contextCompactionEntry = this.sessionManager.getEntry(compactionEntryId);
1601
+ try {
1602
+ await this._extensionRunner.emit({
1603
+ type: "session_compact",
1604
+ reason: options.reason,
1605
+ mode,
1606
+ result,
1607
+ contextCompactionEntry,
1608
+ fromExtension,
1609
+ });
1610
+ }
1611
+ catch (error) {
1612
+ this._extensionRunner.emitError({
1613
+ extensionPath: "<session_compact>",
1614
+ event: "session_compact",
1615
+ error: error instanceof Error ? error.message : String(error),
1616
+ stack: error instanceof Error ? error.stack : undefined,
1617
+ });
1618
+ }
1619
+ return result;
1506
1620
  }
1507
1621
  /**
1508
1622
  * Manually compact the session context using deletion-only verbatim context compaction.
1509
- * Aborts current agent operation first. Custom summary instructions are not accepted.
1623
+ * Aborts current agent operation first.
1510
1624
  */
1511
- async compact(customInstructions) {
1512
- if (customInstructions?.trim()) {
1513
- throw new Error("Custom compaction instructions are not supported; use /compact without arguments");
1514
- }
1625
+ async compact() {
1515
1626
  this._disconnectFromAgent();
1516
1627
  await this.abort();
1517
1628
  this._compactionAbortController = new AbortController();
@@ -1520,12 +1631,14 @@ export class AgentSession {
1520
1631
  if (!this.model) {
1521
1632
  throw new Error(formatNoModelSelectedMessage());
1522
1633
  }
1523
- const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
1634
+ // Auth is resolved lazily: only called when the planner fallback is needed.
1635
+ // Extensions that provide a deletionRequest work without configured credentials.
1636
+ const model = this.model;
1524
1637
  const result = await this._applyContextVerbatimCompaction({
1525
- apiKey,
1526
- headers,
1638
+ resolvePlannerAuth: () => this._getRequiredRequestAuth(model),
1527
1639
  abortController: this._compactionAbortController,
1528
1640
  backupLabel: "compact",
1641
+ reason: "manual",
1529
1642
  });
1530
1643
  if (!result) {
1531
1644
  throw new Error("Nothing to compact (session too small)");
@@ -1570,12 +1683,14 @@ export class AgentSession {
1570
1683
  if (!this.model) {
1571
1684
  throw new Error(formatNoModelSelectedMessage());
1572
1685
  }
1573
- const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
1686
+ // Auth is resolved lazily: only called when the planner fallback is needed.
1687
+ // Extensions that provide a deletionRequest work without configured credentials.
1688
+ const model = this.model;
1574
1689
  const result = await this._applyContextVerbatimCompaction({
1575
- apiKey,
1576
- headers,
1690
+ resolvePlannerAuth: () => this._getRequiredRequestAuth(model),
1577
1691
  abortController: this._compactionAbortController,
1578
1692
  backupLabel: "context-compact",
1693
+ reason: "manual",
1579
1694
  });
1580
1695
  if (!result) {
1581
1696
  throw new Error("Nothing to context-compact (session too small)");
@@ -1754,23 +1869,24 @@ export class AgentSession {
1754
1869
  });
1755
1870
  return;
1756
1871
  }
1757
- const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
1758
- if (!authResult.ok || !authResult.apiKey) {
1759
- this._emit({
1760
- type: "compaction_end",
1761
- reason,
1762
- result: undefined,
1763
- aborted: false,
1764
- willRetry: false,
1765
- });
1766
- return;
1767
- }
1872
+ // Auth is resolved lazily: only called when the planner fallback is needed.
1873
+ // This allows extension-provided deletion requests to run before auth is checked,
1874
+ // enabling local extension compaction even when API credentials are unavailable.
1875
+ // Auto-mode resolver returns undefined (rather than throwing) when auth is missing,
1876
+ // so compaction silently no-ops if the planner would be needed but credentials are absent.
1877
+ const model = this.model;
1768
1878
  const result = await this._applyContextVerbatimCompaction({
1769
- apiKey: authResult.apiKey,
1770
- headers: authResult.headers,
1879
+ resolvePlannerAuth: async () => {
1880
+ const authResult = await this._modelRegistry.getApiKeyAndHeaders(model);
1881
+ if (!authResult.ok || !authResult.apiKey) {
1882
+ return undefined;
1883
+ }
1884
+ return { apiKey: authResult.apiKey, headers: authResult.headers };
1885
+ },
1771
1886
  abortController: this._autoCompactionAbortController,
1772
1887
  backupLabel: reason === "overflow" ? "overflow-auto-compact" : "auto-compact",
1773
1888
  mode: reason === "overflow" ? "critical_overflow" : "standard",
1889
+ reason,
1774
1890
  });
1775
1891
  if (!result) {
1776
1892
  this._emit({
@@ -1974,7 +2090,7 @@ export class AgentSession {
1974
2090
  compact: (options) => {
1975
2091
  void (async () => {
1976
2092
  try {
1977
- const result = await this.compact(options?.customInstructions);
2093
+ const result = await this.compact();
1978
2094
  options?.onComplete?.(result);
1979
2095
  }
1980
2096
  catch (error) {