@astralyn/pi 0.82.0 → 0.82.2

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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +14 -3
  4. package/dist/core/agent-session.d.ts +4 -0
  5. package/dist/core/agent-session.d.ts.map +1 -1
  6. package/dist/core/agent-session.js +193 -43
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/bash-executor.d.ts.map +1 -1
  9. package/dist/core/bash-executor.js +22 -4
  10. package/dist/core/bash-executor.js.map +1 -1
  11. package/dist/core/extensions/loader.d.ts.map +1 -1
  12. package/dist/core/extensions/loader.js +6 -13
  13. package/dist/core/extensions/loader.js.map +1 -1
  14. package/dist/core/tools/bash.d.ts.map +1 -1
  15. package/dist/core/tools/bash.js +4 -3
  16. package/dist/core/tools/bash.js.map +1 -1
  17. package/dist/core/tools/output-accumulator.d.ts +13 -3
  18. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  19. package/dist/core/tools/output-accumulator.js +36 -6
  20. package/dist/core/tools/output-accumulator.js.map +1 -1
  21. package/dist/core/tools/tool-definition-wrapper.d.ts +7 -2
  22. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  23. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +25 -12
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/dist/utils/output-decoder.d.ts +52 -0
  28. package/dist/utils/output-decoder.d.ts.map +1 -0
  29. package/dist/utils/output-decoder.js +134 -0
  30. package/dist/utils/output-decoder.js.map +1 -0
  31. package/dist/utils/shell.d.ts +1 -0
  32. package/dist/utils/shell.d.ts.map +1 -1
  33. package/dist/utils/shell.js +21 -0
  34. package/dist/utils/shell.js.map +1 -1
  35. package/docs/compaction.md +8 -2
  36. package/docs/containerization.md +1 -1
  37. package/docs/development.md +32 -35
  38. package/docs/distribution/README.md +34 -0
  39. package/docs/distribution/architecture.md +102 -0
  40. package/docs/distribution/extensions/README.md +17 -0
  41. package/docs/distribution/extensions/deepwiki.md +62 -0
  42. package/docs/distribution/extensions/question.md +59 -0
  43. package/docs/distribution/extensions/rewind.md +85 -0
  44. package/docs/distribution/extensions/router.md +139 -0
  45. package/docs/distribution/extensions/statusline.md +110 -0
  46. package/docs/distribution/extensions/subagent.md +102 -0
  47. package/docs/distribution/maintenance.md +121 -0
  48. package/docs/distribution/release.md +85 -0
  49. package/docs/distribution/themes.md +58 -0
  50. package/docs/distribution/tool-presentation.md +91 -0
  51. package/docs/extensions.md +4 -2
  52. package/docs/index.md +1 -1
  53. package/docs/rpc.md +2 -2
  54. package/docs/themes.md +1 -1
  55. package/examples/extensions/custom-provider-anthropic/package-lock.json +3 -3
  56. package/examples/extensions/custom-provider-gitlab-duo/test.ts +1 -1
  57. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  58. package/examples/extensions/doom-overlay/doom/build.sh +0 -0
  59. package/examples/extensions/sandbox/package-lock.json +1 -1
  60. package/examples/extensions/with-deps/package-lock.json +2 -2
  61. package/npm-shrinkwrap.json +3481 -640
  62. package/package.json +30 -18
@@ -70,6 +70,8 @@ function estimateMessagesTokens(messages) {
70
70
  // ============================================================================
71
71
  // Constants
72
72
  // ============================================================================
73
+ const CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD = "Estimated retained context remains above the configured auto-compaction threshold. Reduce compaction.keepRecentTokens, remove large retained content, or switch to a model with a larger context window before continuing.";
74
+ const AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER = "Stopped before the next provider request because auto-compaction did not produce a safe context. Review the compaction warning before continuing.";
73
75
  /** Standard thinking levels */
74
76
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
75
77
  // ============================================================================
@@ -95,6 +97,7 @@ export class AgentSession {
95
97
  // Compaction state
96
98
  _compactionAbortController = undefined;
97
99
  _autoCompactionAbortController = undefined;
100
+ _stopAfterTurnRequested = false;
98
101
  _overflowRecoveryAttempted = false;
99
102
  // Branch summarization state
100
103
  _branchSummaryAbortController = undefined;
@@ -189,8 +192,8 @@ export class AgentSession {
189
192
  }
190
193
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
191
194
  }
192
- async _getSummarizationRequestAuth(model) {
193
- if (this.agent.streamFunction === streamSimple) {
195
+ async _getSummarizationRequestAuth(model, streamFunction = this.agent.streamFunction) {
196
+ if (streamFunction === streamSimple) {
194
197
  return this._getRequiredRequestAuth(model);
195
198
  }
196
199
  try {
@@ -264,8 +267,25 @@ export class AgentSession {
264
267
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
265
268
  : undefined);
266
269
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
267
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
268
- const previousContext = previousSnapshot?.context ?? turn.context;
270
+ const compaction = await this._maybeCompactBeforeNextToolTurn(turn, signal);
271
+ if (compaction.stopErrorMessage) {
272
+ // Agent does not expose the low-level graceful turn-stop hook. Throwing here
273
+ // exits before queue polling or another provider request; the run records a
274
+ // normal error/aborted lifecycle instead of risking an oversized context.
275
+ this._stopAfterTurnRequested = true;
276
+ throw new Error(compaction.stopErrorMessage);
277
+ }
278
+ const effectiveTurn = compaction.messages === undefined
279
+ ? turn
280
+ : {
281
+ ...turn,
282
+ context: {
283
+ ...turn.context,
284
+ messages: compaction.messages,
285
+ },
286
+ };
287
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.(effectiveTurn, signal);
288
+ const previousContext = previousSnapshot?.context ?? effectiveTurn.context;
269
289
  return {
270
290
  ...previousSnapshot,
271
291
  context: {
@@ -743,6 +763,7 @@ export class AgentSession {
743
763
  // =========================================================================
744
764
  async _runAgentPrompt(messages) {
745
765
  this._isAgentRunActive = true;
766
+ this._stopAfterTurnRequested = false;
746
767
  try {
747
768
  await this.agent.prompt(messages);
748
769
  while (await this._handlePostAgentRun()) {
@@ -759,6 +780,12 @@ export class AgentSession {
759
780
  const msg = this._lastAssistantMessage;
760
781
  this._lastAssistantMessage = undefined;
761
782
  if (!msg) {
783
+ this._stopAfterTurnRequested = false;
784
+ return false;
785
+ }
786
+ const stopAfterTurnRequested = this._stopAfterTurnRequested;
787
+ this._stopAfterTurnRequested = false;
788
+ if (stopAfterTurnRequested) {
762
789
  return false;
763
790
  }
764
791
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
@@ -776,6 +803,10 @@ export class AgentSession {
776
803
  if (await this._checkCompaction(msg)) {
777
804
  return true;
778
805
  }
806
+ if (this._stopAfterTurnRequested) {
807
+ this._stopAfterTurnRequested = false;
808
+ return false;
809
+ }
779
810
  // The agent loop drains both queues before emitting agent_end. Any messages
780
811
  // here were queued by agent_end extension handlers and need a continuation.
781
812
  return this.agent.hasQueuedMessages();
@@ -861,6 +892,13 @@ export class AgentSession {
861
892
  if (lastAssistant) {
862
893
  await this._checkCompaction(lastAssistant, false);
863
894
  }
895
+ const compactionSettings = this.settingsManager.getCompactionSettings();
896
+ // A compaction entry can be valid while its retained suffix is still too large.
897
+ // Refuse the existing context here rather than discovering that via provider overflow.
898
+ if (compactionSettings.enabled &&
899
+ shouldCompact(this._estimateContextTokensForCompaction(this.agent.state.messages), this.model.contextWindow, compactionSettings)) {
900
+ throw new Error(CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD);
901
+ }
864
902
  // Build messages array (custom message if any, then user message)
865
903
  messages = [];
866
904
  // Add user message
@@ -1164,6 +1202,7 @@ export class AgentSession {
1164
1202
  */
1165
1203
  async abort() {
1166
1204
  this.abortRetry();
1205
+ this.abortCompaction();
1167
1206
  this.agent.abort();
1168
1207
  await this.waitForIdle();
1169
1208
  }
@@ -1494,6 +1533,49 @@ export class AgentSession {
1494
1533
  abortBranchSummary() {
1495
1534
  this._branchSummaryAbortController?.abort();
1496
1535
  }
1536
+ _estimateContextTokensForCompaction(messages) {
1537
+ const estimate = estimateContextTokens(messages);
1538
+ const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1539
+ if (compactionEntry && estimate.lastUsageIndex !== null) {
1540
+ const usageMessage = messages[estimate.lastUsageIndex];
1541
+ if (usageMessage?.role === "assistant" &&
1542
+ usageMessage.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
1543
+ // A retained pre-compaction usage describes the old, larger context. Fall back
1544
+ // to message estimates rather than immediately compacting again from stale data.
1545
+ return estimateMessagesTokens(messages);
1546
+ }
1547
+ }
1548
+ return estimate.tokens;
1549
+ }
1550
+ async _maybeCompactBeforeNextToolTurn(turn, signal) {
1551
+ if (signal?.aborted) {
1552
+ return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
1553
+ }
1554
+ if (turn.toolResults.length === 0) {
1555
+ return {};
1556
+ }
1557
+ const settings = this.settingsManager.getCompactionSettings();
1558
+ if (!settings.enabled) {
1559
+ return {};
1560
+ }
1561
+ const contextTokens = this._estimateContextTokensForCompaction(turn.context.messages);
1562
+ const contextWindow = this.model?.contextWindow ?? 0;
1563
+ if (!shouldCompact(contextTokens, contextWindow, settings)) {
1564
+ return {};
1565
+ }
1566
+ const outcome = await this._runAutoCompactionWithOutcome("threshold", false, signal);
1567
+ if (!outcome.compacted) {
1568
+ return { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER };
1569
+ }
1570
+ return {
1571
+ messages: this.agent.state.messages.slice(),
1572
+ ...(signal?.aborted === true
1573
+ ? { stopErrorMessage: AUTO_COMPACTION_STOPPED_BEFORE_NEXT_PROVIDER }
1574
+ : outcome.mustStop === true
1575
+ ? { stopErrorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD }
1576
+ : {}),
1577
+ };
1578
+ }
1497
1579
  /**
1498
1580
  * Check if compaction is needed and run it.
1499
1581
  * Called after agent_end and before prompt submission.
@@ -1536,6 +1618,7 @@ export class AgentSession {
1536
1618
  return await this._runAutoCompaction("overflow", false);
1537
1619
  }
1538
1620
  if (this._overflowRecoveryAttempted) {
1621
+ this._stopAfterTurnRequested = true;
1539
1622
  this._emit({
1540
1623
  type: "compaction_end",
1541
1624
  reason: "overflow",
@@ -1589,31 +1672,57 @@ export class AgentSession {
1589
1672
  * Internal: Run auto-compaction with events.
1590
1673
  */
1591
1674
  async _runAutoCompaction(reason, willRetry) {
1675
+ const outcome = await this._runAutoCompactionWithOutcome(reason, willRetry);
1676
+ if (!outcome.compacted || outcome.mustStop) {
1677
+ this._stopAfterTurnRequested = true;
1678
+ }
1679
+ return outcome.shouldContinue;
1680
+ }
1681
+ async _runAutoCompactionWithOutcome(reason, willRetry, parentSignal) {
1592
1682
  const settings = this.settingsManager.getCompactionSettings();
1593
1683
  let started = false;
1684
+ let controller;
1685
+ let removeParentAbortListener;
1594
1686
  try {
1595
- if (!this.model) {
1596
- return false;
1597
- }
1598
- let apiKey;
1599
- let headers;
1600
- let env;
1601
- if (this.agent.streamFunction === streamSimple) {
1602
- ({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
1603
- }
1604
- else {
1605
- ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
1687
+ if (!this.model ||
1688
+ this._autoCompactionAbortController ||
1689
+ this._compactionAbortController ||
1690
+ parentSignal?.aborted) {
1691
+ return { compacted: false, shouldContinue: false };
1692
+ }
1693
+ const model = this.model;
1694
+ const streamFunction = this.agent.streamFunction;
1695
+ const thinkingLevel = this.thinkingLevel;
1696
+ const retrySettings = this.settingsManager.getRetrySettings();
1697
+ controller = new AbortController();
1698
+ if (parentSignal) {
1699
+ const abortFromParent = () => controller?.abort();
1700
+ parentSignal.addEventListener("abort", abortFromParent, { once: true });
1701
+ removeParentAbortListener = () => parentSignal.removeEventListener("abort", abortFromParent);
1702
+ }
1703
+ if (controller.signal.aborted) {
1704
+ return { compacted: false, shouldContinue: false };
1705
+ }
1706
+ this._autoCompactionAbortController = controller;
1707
+ started = true;
1708
+ this._emit({ type: "compaction_start", reason });
1709
+ if (controller.signal.aborted) {
1710
+ this._emit({
1711
+ type: "compaction_end",
1712
+ reason,
1713
+ result: undefined,
1714
+ aborted: true,
1715
+ willRetry: false,
1716
+ });
1717
+ return { compacted: false, shouldContinue: false };
1606
1718
  }
1719
+ let extensionCompaction;
1720
+ let fromExtension = false;
1607
1721
  const pathEntries = this.sessionManager.getBranch();
1608
1722
  const preparation = prepareCompaction(pathEntries, settings);
1609
1723
  if (!preparation) {
1610
- return false;
1724
+ throw new Error("Nothing to compact while preserving the configured recent context.");
1611
1725
  }
1612
- this._emit({ type: "compaction_start", reason });
1613
- this._autoCompactionAbortController = new AbortController();
1614
- started = true;
1615
- let extensionCompaction;
1616
- let fromExtension = false;
1617
1726
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
1618
1727
  const extensionResult = (await this._extensionRunner.emit({
1619
1728
  type: "session_before_compact",
@@ -1622,7 +1731,7 @@ export class AgentSession {
1622
1731
  customInstructions: undefined,
1623
1732
  reason,
1624
1733
  willRetry,
1625
- signal: this._autoCompactionAbortController.signal,
1734
+ signal: controller.signal,
1626
1735
  }));
1627
1736
  if (extensionResult?.cancel) {
1628
1737
  this._emit({
@@ -1632,7 +1741,7 @@ export class AgentSession {
1632
1741
  aborted: true,
1633
1742
  willRetry: false,
1634
1743
  });
1635
- return false;
1744
+ return { compacted: false, shouldContinue: false };
1636
1745
  }
1637
1746
  if (extensionResult?.compaction) {
1638
1747
  extensionCompaction = extensionResult.compaction;
@@ -1653,15 +1762,34 @@ export class AgentSession {
1653
1762
  details = extensionCompaction.details;
1654
1763
  }
1655
1764
  else {
1765
+ let apiKey;
1766
+ let headers;
1767
+ let env;
1768
+ if (streamFunction === streamSimple) {
1769
+ ({ apiKey, headers, env } = await this._getRequiredRequestAuth(model));
1770
+ }
1771
+ else {
1772
+ ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(model, streamFunction));
1773
+ }
1774
+ if (controller.signal.aborted) {
1775
+ this._emit({
1776
+ type: "compaction_end",
1777
+ reason,
1778
+ result: undefined,
1779
+ aborted: true,
1780
+ willRetry: false,
1781
+ });
1782
+ return { compacted: false, shouldContinue: false };
1783
+ }
1656
1784
  // Generate compaction result
1657
- const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
1785
+ const compactResult = await compact(preparation, model, apiKey, headers, undefined, controller.signal, thinkingLevel, streamFunction, env, retrySettings, this._summarizationRetryCallbacks({ source: "compaction", reason }));
1658
1786
  summary = compactResult.summary;
1659
1787
  firstKeptEntryId = compactResult.firstKeptEntryId;
1660
1788
  tokensBefore = compactResult.tokensBefore;
1661
1789
  usage = compactResult.usage;
1662
1790
  details = compactResult.details;
1663
1791
  }
1664
- if (this._autoCompactionAbortController.signal.aborted) {
1792
+ if (controller.signal.aborted) {
1665
1793
  this._emit({
1666
1794
  type: "compaction_end",
1667
1795
  reason,
@@ -1669,13 +1797,22 @@ export class AgentSession {
1669
1797
  aborted: true,
1670
1798
  willRetry: false,
1671
1799
  });
1672
- return false;
1800
+ return { compacted: false, shouldContinue: false };
1673
1801
  }
1674
1802
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
1675
1803
  const newEntries = this.sessionManager.getEntries();
1676
1804
  const sessionContext = this.sessionManager.buildSessionContext();
1677
1805
  this.agent.state.messages = sessionContext.messages;
1678
- const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
1806
+ if (willRetry) {
1807
+ const messages = this.agent.state.messages;
1808
+ const lastMsg = messages[messages.length - 1];
1809
+ if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
1810
+ this.agent.state.messages = messages.slice(0, -1);
1811
+ }
1812
+ }
1813
+ const estimatedTokensAfter = this._estimateContextTokensForCompaction(this.agent.state.messages);
1814
+ const contextStillOverThreshold = shouldCompact(estimatedTokensAfter, model.contextWindow, settings);
1815
+ const effectiveWillRetry = willRetry && !contextStillOverThreshold;
1679
1816
  // Get the saved compaction entry for the extension event
1680
1817
  const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1681
1818
  if (this._extensionRunner && savedCompactionEntry) {
@@ -1684,7 +1821,7 @@ export class AgentSession {
1684
1821
  compactionEntry: savedCompactionEntry,
1685
1822
  fromExtension,
1686
1823
  reason,
1687
- willRetry,
1824
+ willRetry: effectiveWillRetry,
1688
1825
  });
1689
1826
  }
1690
1827
  const result = {
@@ -1695,37 +1832,50 @@ export class AgentSession {
1695
1832
  usage,
1696
1833
  details,
1697
1834
  };
1698
- this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
1699
- if (willRetry) {
1700
- const messages = this.agent.state.messages;
1701
- const lastMsg = messages[messages.length - 1];
1702
- if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
1703
- this.agent.state.messages = messages.slice(0, -1);
1704
- }
1705
- return true;
1835
+ this._emit({
1836
+ type: "compaction_end",
1837
+ reason,
1838
+ result,
1839
+ aborted: false,
1840
+ willRetry: effectiveWillRetry,
1841
+ ...(contextStillOverThreshold ? { errorMessage: CONTEXT_REMAINS_OVER_COMPACTION_THRESHOLD } : {}),
1842
+ });
1843
+ if (contextStillOverThreshold) {
1844
+ return { compacted: true, shouldContinue: false, mustStop: true };
1845
+ }
1846
+ if (effectiveWillRetry) {
1847
+ return { compacted: true, shouldContinue: true };
1706
1848
  }
1707
1849
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
1708
1850
  // Continue once so queued messages are delivered.
1709
- return this.agent.hasQueuedMessages();
1851
+ return { compacted: true, shouldContinue: this.agent.hasQueuedMessages() };
1710
1852
  }
1711
1853
  catch (error) {
1854
+ const aborted = controller?.signal.aborted || (error instanceof Error && error.name === "AbortError");
1712
1855
  const errorMessage = error instanceof Error ? error.message : "compaction failed";
1713
1856
  if (started) {
1714
1857
  this._emit({
1715
1858
  type: "compaction_end",
1716
1859
  reason,
1717
1860
  result: undefined,
1718
- aborted: false,
1861
+ aborted,
1719
1862
  willRetry: false,
1720
- errorMessage: reason === "overflow"
1721
- ? `Context overflow recovery failed: ${errorMessage}`
1722
- : `Auto-compaction failed: ${errorMessage}`,
1863
+ ...(aborted
1864
+ ? {}
1865
+ : {
1866
+ errorMessage: reason === "overflow"
1867
+ ? `Context overflow recovery failed: ${errorMessage}`
1868
+ : `Auto-compaction failed: ${errorMessage}`,
1869
+ }),
1723
1870
  });
1724
1871
  }
1725
- return false;
1872
+ return { compacted: false, shouldContinue: false };
1726
1873
  }
1727
1874
  finally {
1728
- this._autoCompactionAbortController = undefined;
1875
+ removeParentAbortListener?.();
1876
+ if (this._autoCompactionAbortController === controller) {
1877
+ this._autoCompactionAbortController = undefined;
1878
+ }
1729
1879
  }
1730
1880
  }
1731
1881
  /**