@mariozechner/pi-coding-agent 0.55.3 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +1 -8
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +5 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +14 -4
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +167 -49
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/auth-storage.d.ts +1 -1
  14. package/dist/core/auth-storage.d.ts.map +1 -1
  15. package/dist/core/auth-storage.js +2 -1
  16. package/dist/core/auth-storage.js.map +1 -1
  17. package/dist/core/compaction/compaction.d.ts.map +1 -1
  18. package/dist/core/compaction/compaction.js +4 -1
  19. package/dist/core/compaction/compaction.js.map +1 -1
  20. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  21. package/dist/core/export-html/tool-renderer.js +6 -0
  22. package/dist/core/export-html/tool-renderer.js.map +1 -1
  23. package/dist/core/extensions/loader.d.ts.map +1 -1
  24. package/dist/core/extensions/loader.js +17 -3
  25. package/dist/core/extensions/loader.js.map +1 -1
  26. package/dist/core/extensions/runner.d.ts.map +1 -1
  27. package/dist/core/extensions/runner.js +1 -0
  28. package/dist/core/extensions/runner.js.map +1 -1
  29. package/dist/core/extensions/types.d.ts +8 -2
  30. package/dist/core/extensions/types.d.ts.map +1 -1
  31. package/dist/core/extensions/types.js.map +1 -1
  32. package/dist/core/model-registry.d.ts +1 -0
  33. package/dist/core/model-registry.d.ts.map +1 -1
  34. package/dist/core/model-registry.js +9 -6
  35. package/dist/core/model-registry.js.map +1 -1
  36. package/dist/core/model-resolver.d.ts.map +1 -1
  37. package/dist/core/model-resolver.js +35 -5
  38. package/dist/core/model-resolver.js.map +1 -1
  39. package/dist/core/sdk.d.ts +1 -1
  40. package/dist/core/sdk.d.ts.map +1 -1
  41. package/dist/core/sdk.js.map +1 -1
  42. package/dist/core/settings-manager.d.ts +3 -0
  43. package/dist/core/settings-manager.d.ts.map +1 -1
  44. package/dist/core/settings-manager.js +4 -0
  45. package/dist/core/settings-manager.js.map +1 -1
  46. package/dist/core/system-prompt.d.ts +4 -0
  47. package/dist/core/system-prompt.d.ts.map +1 -1
  48. package/dist/core/system-prompt.js +34 -12
  49. package/dist/core/system-prompt.js.map +1 -1
  50. package/dist/main.d.ts.map +1 -1
  51. package/dist/main.js +4 -4
  52. package/dist/main.js.map +1 -1
  53. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  54. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/extension-editor.js +8 -0
  56. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  57. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/login-dialog.js +1 -1
  59. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  60. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  61. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  63. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  64. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  65. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  66. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/session-selector.js +1 -1
  68. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  69. package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  70. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tool-execution.js +28 -3
  72. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  73. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  74. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/user-message.js +11 -0
  76. package/dist/modes/interactive/components/user-message.js.map +1 -1
  77. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  78. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.js +27 -26
  80. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  81. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  82. package/dist/modes/interactive/theme/theme.js +5 -0
  83. package/dist/modes/interactive/theme/theme.js.map +1 -1
  84. package/docs/custom-provider.md +10 -2
  85. package/docs/extensions.md +20 -1
  86. package/docs/providers.md +3 -1
  87. package/docs/settings.md +1 -0
  88. package/examples/extensions/README.md +1 -0
  89. package/examples/extensions/antigravity-image-gen.ts +3 -1
  90. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  91. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  92. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  93. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  94. package/examples/extensions/dynamic-tools.ts +74 -0
  95. package/examples/extensions/with-deps/package-lock.json +2 -2
  96. package/examples/extensions/with-deps/package.json +1 -1
  97. package/package.json +7 -5
@@ -63,6 +63,7 @@ export class AgentSession {
63
63
  // Event subscription state
64
64
  _unsubscribeAgent;
65
65
  _eventListeners = [];
66
+ _agentEventQueue = Promise.resolve();
66
67
  /** Tracks pending steering messages for UI display. Removed when delivered. */
67
68
  _steeringMessages = [];
68
69
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -72,6 +73,7 @@ export class AgentSession {
72
73
  // Compaction state
73
74
  _compactionAbortController = undefined;
74
75
  _autoCompactionAbortController = undefined;
76
+ _overflowRecoveryAttempted = false;
75
77
  // Branch summarization state
76
78
  _branchSummaryAbortController = undefined;
77
79
  // Retry state
@@ -101,6 +103,8 @@ export class AgentSession {
101
103
  _modelRegistry;
102
104
  // Tool registry for extension getTools/setTools
103
105
  _toolRegistry = new Map();
106
+ _toolPromptSnippets = new Map();
107
+ _toolPromptGuidelines = new Map();
104
108
  // Base system prompt (without extension appends) - used to apply fresh appends each turn
105
109
  _baseSystemPrompt = "";
106
110
  constructor(config) {
@@ -139,10 +143,47 @@ export class AgentSession {
139
143
  // Track last assistant message for auto-compaction check
140
144
  _lastAssistantMessage = undefined;
141
145
  /** Internal handler for agent events - shared by subscribe and reconnect */
142
- _handleAgentEvent = async (event) => {
146
+ _handleAgentEvent = (event) => {
147
+ // Create retry promise synchronously before queueing async processing.
148
+ // Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
149
+ // as soon as agent.prompt() resolves. If _retryPromise is created only inside
150
+ // _processAgentEvent, slow earlier queued events can delay agent_end processing
151
+ // and waitForRetry() can miss the in-flight retry.
152
+ this._createRetryPromiseForAgentEnd(event);
153
+ this._agentEventQueue = this._agentEventQueue.then(() => this._processAgentEvent(event), () => this._processAgentEvent(event));
154
+ // Keep queue alive if an event handler fails
155
+ this._agentEventQueue.catch(() => { });
156
+ };
157
+ _createRetryPromiseForAgentEnd(event) {
158
+ if (event.type !== "agent_end" || this._retryPromise) {
159
+ return;
160
+ }
161
+ const settings = this.settingsManager.getRetrySettings();
162
+ if (!settings.enabled) {
163
+ return;
164
+ }
165
+ const lastAssistant = this._findLastAssistantInMessages(event.messages);
166
+ if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
167
+ return;
168
+ }
169
+ this._retryPromise = new Promise((resolve) => {
170
+ this._retryResolve = resolve;
171
+ });
172
+ }
173
+ _findLastAssistantInMessages(messages) {
174
+ for (let i = messages.length - 1; i >= 0; i--) {
175
+ const message = messages[i];
176
+ if (message.role === "assistant") {
177
+ return message;
178
+ }
179
+ }
180
+ return undefined;
181
+ }
182
+ async _processAgentEvent(event) {
143
183
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
144
184
  // This ensures the UI sees the updated queue state
145
185
  if (event.type === "message_start" && event.message.role === "user") {
186
+ this._overflowRecoveryAttempted = false;
146
187
  const messageText = this._getUserMessageText(event.message);
147
188
  if (messageText) {
148
189
  // Check steering queue first
@@ -180,9 +221,12 @@ export class AgentSession {
180
221
  // Track assistant message for auto-compaction (checked on agent_end)
181
222
  if (event.message.role === "assistant") {
182
223
  this._lastAssistantMessage = event.message;
224
+ const assistantMsg = event.message;
225
+ if (assistantMsg.stopReason !== "error") {
226
+ this._overflowRecoveryAttempted = false;
227
+ }
183
228
  // Reset retry counter immediately on successful assistant response
184
229
  // This prevents accumulation across multiple LLM calls within a turn
185
- const assistantMsg = event.message;
186
230
  if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
187
231
  this._emit({
188
232
  type: "auto_retry_end",
@@ -206,7 +250,7 @@ export class AgentSession {
206
250
  }
207
251
  await this._checkCompaction(msg);
208
252
  }
209
- };
253
+ }
210
254
  /** Resolve the pending retry promise */
211
255
  _resolveRetry() {
212
256
  if (this._retryResolve) {
@@ -425,9 +469,11 @@ export class AgentSession {
425
469
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
426
470
  this.agent.setSystemPrompt(this._baseSystemPrompt);
427
471
  }
428
- /** Whether auto-compaction is currently running */
472
+ /** Whether compaction or branch summarization is currently running */
429
473
  get isCompacting() {
430
- return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
474
+ return (this._autoCompactionAbortController !== undefined ||
475
+ this._compactionAbortController !== undefined ||
476
+ this._branchSummaryAbortController !== undefined);
431
477
  }
432
478
  /** All messages including custom types like BashExecutionMessage */
433
479
  get messages() {
@@ -465,8 +511,42 @@ export class AgentSession {
465
511
  get promptTemplates() {
466
512
  return this._resourceLoader.getPrompts().prompts;
467
513
  }
514
+ _normalizePromptSnippet(text) {
515
+ if (!text)
516
+ return undefined;
517
+ const oneLine = text
518
+ .replace(/[\r\n]+/g, " ")
519
+ .replace(/\s+/g, " ")
520
+ .trim();
521
+ return oneLine.length > 0 ? oneLine : undefined;
522
+ }
523
+ _normalizePromptGuidelines(guidelines) {
524
+ if (!guidelines || guidelines.length === 0) {
525
+ return [];
526
+ }
527
+ const unique = new Set();
528
+ for (const guideline of guidelines) {
529
+ const normalized = guideline.trim();
530
+ if (normalized.length > 0) {
531
+ unique.add(normalized);
532
+ }
533
+ }
534
+ return Array.from(unique);
535
+ }
468
536
  _rebuildSystemPrompt(toolNames) {
469
- const validToolNames = toolNames.filter((name) => this._baseToolRegistry.has(name));
537
+ const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
538
+ const toolSnippets = {};
539
+ const promptGuidelines = [];
540
+ for (const name of validToolNames) {
541
+ const snippet = this._toolPromptSnippets.get(name);
542
+ if (snippet) {
543
+ toolSnippets[name] = snippet;
544
+ }
545
+ const toolGuidelines = this._toolPromptGuidelines.get(name);
546
+ if (toolGuidelines) {
547
+ promptGuidelines.push(...toolGuidelines);
548
+ }
549
+ }
470
550
  const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
471
551
  const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
472
552
  const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
@@ -479,6 +559,8 @@ export class AgentSession {
479
559
  customPrompt: loaderSystemPrompt,
480
560
  appendSystemPrompt,
481
561
  selectedTools: validToolNames,
562
+ toolSnippets,
563
+ promptGuidelines,
482
564
  });
483
565
  }
484
566
  // =========================================================================
@@ -984,8 +1066,11 @@ export class AgentSession {
984
1066
  this.agent.setModel(next.model);
985
1067
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
986
1068
  this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
987
- // Apply thinking level (setThinkingLevel clamps to model capabilities)
988
- this.setThinkingLevel(next.thinkingLevel);
1069
+ // Apply thinking level.
1070
+ // - Explicit scoped model thinking level overrides current session level
1071
+ // - Undefined scoped model thinking level inherits current session level
1072
+ // setThinkingLevel clamps to model capabilities.
1073
+ this.setThinkingLevel(next.thinkingLevel ?? this.thinkingLevel);
989
1074
  await this._emitModelSelect(next.model, currentModel, "cycle");
990
1075
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
991
1076
  }
@@ -1246,6 +1331,17 @@ export class AgentSession {
1246
1331
  const errorIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
1247
1332
  // Case 1: Overflow - LLM returned context overflow error
1248
1333
  if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
1334
+ if (this._overflowRecoveryAttempted) {
1335
+ this._emit({
1336
+ type: "auto_compaction_end",
1337
+ result: undefined,
1338
+ aborted: false,
1339
+ willRetry: false,
1340
+ errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
1341
+ });
1342
+ return;
1343
+ }
1344
+ this._overflowRecoveryAttempted = true;
1249
1345
  // Remove the error message from agent state (it IS saved to session for history,
1250
1346
  // but we don't want it in context for the retry)
1251
1347
  const messages = this.agent.state.messages;
@@ -1528,6 +1624,7 @@ export class AgentSession {
1528
1624
  getActiveTools: () => this.getActiveToolNames(),
1529
1625
  getAllTools: () => this.getAllTools(),
1530
1626
  setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
1627
+ refreshTools: () => this._refreshToolRegistry(),
1531
1628
  getCommands,
1532
1629
  setModel: async (model) => {
1533
1630
  const key = await this.modelRegistry.getApiKey(model);
@@ -1562,6 +1659,57 @@ export class AgentSession {
1562
1659
  getSystemPrompt: () => this.systemPrompt,
1563
1660
  });
1564
1661
  }
1662
+ _refreshToolRegistry(options) {
1663
+ const previousRegistryNames = new Set(this._toolRegistry.keys());
1664
+ const previousActiveToolNames = this.getActiveToolNames();
1665
+ const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1666
+ const allCustomTools = [
1667
+ ...registeredTools,
1668
+ ...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
1669
+ ];
1670
+ this._toolPromptSnippets = new Map(allCustomTools
1671
+ .map((registeredTool) => {
1672
+ const snippet = this._normalizePromptSnippet(registeredTool.definition.promptSnippet ?? registeredTool.definition.description);
1673
+ return snippet ? [registeredTool.definition.name, snippet] : undefined;
1674
+ })
1675
+ .filter((entry) => entry !== undefined));
1676
+ this._toolPromptGuidelines = new Map(allCustomTools
1677
+ .map((registeredTool) => {
1678
+ const guidelines = this._normalizePromptGuidelines(registeredTool.definition.promptGuidelines);
1679
+ return guidelines.length > 0 ? [registeredTool.definition.name, guidelines] : undefined;
1680
+ })
1681
+ .filter((entry) => entry !== undefined));
1682
+ const wrappedExtensionTools = this._extensionRunner
1683
+ ? wrapRegisteredTools(allCustomTools, this._extensionRunner)
1684
+ : [];
1685
+ const toolRegistry = new Map(this._baseToolRegistry);
1686
+ for (const tool of wrappedExtensionTools) {
1687
+ toolRegistry.set(tool.name, tool);
1688
+ }
1689
+ if (this._extensionRunner) {
1690
+ const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
1691
+ this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
1692
+ }
1693
+ else {
1694
+ this._toolRegistry = toolRegistry;
1695
+ }
1696
+ const nextActiveToolNames = options?.activeToolNames
1697
+ ? [...options.activeToolNames]
1698
+ : [...previousActiveToolNames];
1699
+ if (options?.includeAllExtensionTools) {
1700
+ for (const tool of wrappedExtensionTools) {
1701
+ nextActiveToolNames.push(tool.name);
1702
+ }
1703
+ }
1704
+ else if (!options?.activeToolNames) {
1705
+ for (const toolName of this._toolRegistry.keys()) {
1706
+ if (!previousRegistryNames.has(toolName)) {
1707
+ nextActiveToolNames.push(toolName);
1708
+ }
1709
+ }
1710
+ }
1711
+ this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
1712
+ }
1565
1713
  _buildRuntime(options) {
1566
1714
  const autoResizeImages = this.settingsManager.getImageAutoResize();
1567
1715
  const shellCommandPrefix = this.settingsManager.getShellCommandPrefix();
@@ -1591,47 +1739,14 @@ export class AgentSession {
1591
1739
  this._bindExtensionCore(this._extensionRunner);
1592
1740
  this._applyExtensionBindings(this._extensionRunner);
1593
1741
  }
1594
- const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1595
- const allCustomTools = [
1596
- ...registeredTools,
1597
- ...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
1598
- ];
1599
- const wrappedExtensionTools = this._extensionRunner
1600
- ? wrapRegisteredTools(allCustomTools, this._extensionRunner)
1601
- : [];
1602
- const toolRegistry = new Map(this._baseToolRegistry);
1603
- for (const tool of wrappedExtensionTools) {
1604
- toolRegistry.set(tool.name, tool);
1605
- }
1606
1742
  const defaultActiveToolNames = this._baseToolsOverride
1607
1743
  ? Object.keys(this._baseToolsOverride)
1608
1744
  : ["read", "bash", "edit", "write"];
1609
1745
  const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
1610
- const activeToolNameSet = new Set(baseActiveToolNames);
1611
- if (options.includeAllExtensionTools) {
1612
- for (const tool of wrappedExtensionTools) {
1613
- activeToolNameSet.add(tool.name);
1614
- }
1615
- }
1616
- const extensionToolNames = new Set(wrappedExtensionTools.map((tool) => tool.name));
1617
- const activeBaseTools = Array.from(activeToolNameSet)
1618
- .filter((name) => this._baseToolRegistry.has(name) && !extensionToolNames.has(name))
1619
- .map((name) => this._baseToolRegistry.get(name));
1620
- const activeExtensionTools = wrappedExtensionTools.filter((tool) => activeToolNameSet.has(tool.name));
1621
- const activeToolsArray = [...activeBaseTools, ...activeExtensionTools];
1622
- if (this._extensionRunner) {
1623
- const wrappedActiveTools = wrapToolsWithExtensions(activeToolsArray, this._extensionRunner);
1624
- this.agent.setTools(wrappedActiveTools);
1625
- const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
1626
- this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
1627
- }
1628
- else {
1629
- this.agent.setTools(activeToolsArray);
1630
- this._toolRegistry = toolRegistry;
1631
- }
1632
- const systemPromptToolNames = Array.from(activeToolNameSet).filter((name) => this._baseToolRegistry.has(name));
1633
- this._baseSystemPrompt = this._rebuildSystemPrompt(systemPromptToolNames);
1634
- this.agent.setSystemPrompt(this._baseSystemPrompt);
1746
+ this._refreshToolRegistry({
1747
+ activeToolNames: baseActiveToolNames,
1748
+ includeAllExtensionTools: options.includeAllExtensionTools,
1749
+ });
1635
1750
  }
1636
1751
  async reload() {
1637
1752
  const previousFlagValues = this._extensionRunner?.getFlagValues();
@@ -1677,15 +1792,18 @@ export class AgentSession {
1677
1792
  */
1678
1793
  async _handleRetryableError(message) {
1679
1794
  const settings = this.settingsManager.getRetrySettings();
1680
- if (!settings.enabled)
1795
+ if (!settings.enabled) {
1796
+ this._resolveRetry();
1681
1797
  return false;
1682
- this._retryAttempt++;
1683
- // Create retry promise on first attempt so waitForRetry() can await it
1684
- if (this._retryAttempt === 1 && !this._retryPromise) {
1798
+ }
1799
+ // Retry promise is created synchronously in _handleAgentEvent for agent_end.
1800
+ // Keep a defensive fallback here in case a future refactor bypasses that path.
1801
+ if (!this._retryPromise) {
1685
1802
  this._retryPromise = new Promise((resolve) => {
1686
1803
  this._retryResolve = resolve;
1687
1804
  });
1688
1805
  }
1806
+ this._retryAttempt++;
1689
1807
  if (this._retryAttempt > settings.maxRetries) {
1690
1808
  // Max retries exceeded, emit final failure and reset
1691
1809
  this._emit({