@mariozechner/pi-coding-agent 0.55.3 → 0.55.4

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +0 -8
  3. package/dist/core/agent-session.d.ts +9 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +141 -44
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  8. package/dist/core/export-html/tool-renderer.js +6 -0
  9. package/dist/core/export-html/tool-renderer.js.map +1 -1
  10. package/dist/core/extensions/loader.d.ts.map +1 -1
  11. package/dist/core/extensions/loader.js +3 -0
  12. package/dist/core/extensions/loader.js.map +1 -1
  13. package/dist/core/extensions/runner.d.ts.map +1 -1
  14. package/dist/core/extensions/runner.js +1 -0
  15. package/dist/core/extensions/runner.js.map +1 -1
  16. package/dist/core/extensions/types.d.ts +8 -2
  17. package/dist/core/extensions/types.d.ts.map +1 -1
  18. package/dist/core/extensions/types.js.map +1 -1
  19. package/dist/core/system-prompt.d.ts +4 -0
  20. package/dist/core/system-prompt.d.ts.map +1 -1
  21. package/dist/core/system-prompt.js +34 -12
  22. package/dist/core/system-prompt.js.map +1 -1
  23. package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  24. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  25. package/dist/modes/interactive/components/tool-execution.js +28 -3
  26. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  27. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  28. package/dist/modes/interactive/interactive-mode.js +0 -1
  29. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  30. package/docs/extensions.md +20 -1
  31. package/examples/extensions/README.md +1 -0
  32. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  33. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  34. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  35. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  36. package/examples/extensions/dynamic-tools.ts +74 -0
  37. package/examples/extensions/with-deps/package-lock.json +2 -2
  38. package/examples/extensions/with-deps/package.json +1 -1
  39. package/package.json +4 -4
@@ -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. */
@@ -101,6 +102,8 @@ export class AgentSession {
101
102
  _modelRegistry;
102
103
  // Tool registry for extension getTools/setTools
103
104
  _toolRegistry = new Map();
105
+ _toolPromptSnippets = new Map();
106
+ _toolPromptGuidelines = new Map();
104
107
  // Base system prompt (without extension appends) - used to apply fresh appends each turn
105
108
  _baseSystemPrompt = "";
106
109
  constructor(config) {
@@ -139,7 +142,43 @@ export class AgentSession {
139
142
  // Track last assistant message for auto-compaction check
140
143
  _lastAssistantMessage = undefined;
141
144
  /** Internal handler for agent events - shared by subscribe and reconnect */
142
- _handleAgentEvent = async (event) => {
145
+ _handleAgentEvent = (event) => {
146
+ // Create retry promise synchronously before queueing async processing.
147
+ // Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
148
+ // as soon as agent.prompt() resolves. If _retryPromise is created only inside
149
+ // _processAgentEvent, slow earlier queued events can delay agent_end processing
150
+ // and waitForRetry() can miss the in-flight retry.
151
+ this._createRetryPromiseForAgentEnd(event);
152
+ this._agentEventQueue = this._agentEventQueue.then(() => this._processAgentEvent(event), () => this._processAgentEvent(event));
153
+ // Keep queue alive if an event handler fails
154
+ this._agentEventQueue.catch(() => { });
155
+ };
156
+ _createRetryPromiseForAgentEnd(event) {
157
+ if (event.type !== "agent_end" || this._retryPromise) {
158
+ return;
159
+ }
160
+ const settings = this.settingsManager.getRetrySettings();
161
+ if (!settings.enabled) {
162
+ return;
163
+ }
164
+ const lastAssistant = this._findLastAssistantInMessages(event.messages);
165
+ if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
166
+ return;
167
+ }
168
+ this._retryPromise = new Promise((resolve) => {
169
+ this._retryResolve = resolve;
170
+ });
171
+ }
172
+ _findLastAssistantInMessages(messages) {
173
+ for (let i = messages.length - 1; i >= 0; i--) {
174
+ const message = messages[i];
175
+ if (message.role === "assistant") {
176
+ return message;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+ async _processAgentEvent(event) {
143
182
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
144
183
  // This ensures the UI sees the updated queue state
145
184
  if (event.type === "message_start" && event.message.role === "user") {
@@ -206,7 +245,7 @@ export class AgentSession {
206
245
  }
207
246
  await this._checkCompaction(msg);
208
247
  }
209
- };
248
+ }
210
249
  /** Resolve the pending retry promise */
211
250
  _resolveRetry() {
212
251
  if (this._retryResolve) {
@@ -465,8 +504,42 @@ export class AgentSession {
465
504
  get promptTemplates() {
466
505
  return this._resourceLoader.getPrompts().prompts;
467
506
  }
507
+ _normalizePromptSnippet(text) {
508
+ if (!text)
509
+ return undefined;
510
+ const oneLine = text
511
+ .replace(/[\r\n]+/g, " ")
512
+ .replace(/\s+/g, " ")
513
+ .trim();
514
+ return oneLine.length > 0 ? oneLine : undefined;
515
+ }
516
+ _normalizePromptGuidelines(guidelines) {
517
+ if (!guidelines || guidelines.length === 0) {
518
+ return [];
519
+ }
520
+ const unique = new Set();
521
+ for (const guideline of guidelines) {
522
+ const normalized = guideline.trim();
523
+ if (normalized.length > 0) {
524
+ unique.add(normalized);
525
+ }
526
+ }
527
+ return Array.from(unique);
528
+ }
468
529
  _rebuildSystemPrompt(toolNames) {
469
- const validToolNames = toolNames.filter((name) => this._baseToolRegistry.has(name));
530
+ const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
531
+ const toolSnippets = {};
532
+ const promptGuidelines = [];
533
+ for (const name of validToolNames) {
534
+ const snippet = this._toolPromptSnippets.get(name);
535
+ if (snippet) {
536
+ toolSnippets[name] = snippet;
537
+ }
538
+ const toolGuidelines = this._toolPromptGuidelines.get(name);
539
+ if (toolGuidelines) {
540
+ promptGuidelines.push(...toolGuidelines);
541
+ }
542
+ }
470
543
  const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
471
544
  const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
472
545
  const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
@@ -479,6 +552,8 @@ export class AgentSession {
479
552
  customPrompt: loaderSystemPrompt,
480
553
  appendSystemPrompt,
481
554
  selectedTools: validToolNames,
555
+ toolSnippets,
556
+ promptGuidelines,
482
557
  });
483
558
  }
484
559
  // =========================================================================
@@ -1528,6 +1603,7 @@ export class AgentSession {
1528
1603
  getActiveTools: () => this.getActiveToolNames(),
1529
1604
  getAllTools: () => this.getAllTools(),
1530
1605
  setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
1606
+ refreshTools: () => this._refreshToolRegistry(),
1531
1607
  getCommands,
1532
1608
  setModel: async (model) => {
1533
1609
  const key = await this.modelRegistry.getApiKey(model);
@@ -1562,6 +1638,57 @@ export class AgentSession {
1562
1638
  getSystemPrompt: () => this.systemPrompt,
1563
1639
  });
1564
1640
  }
1641
+ _refreshToolRegistry(options) {
1642
+ const previousRegistryNames = new Set(this._toolRegistry.keys());
1643
+ const previousActiveToolNames = this.getActiveToolNames();
1644
+ const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1645
+ const allCustomTools = [
1646
+ ...registeredTools,
1647
+ ...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
1648
+ ];
1649
+ this._toolPromptSnippets = new Map(allCustomTools
1650
+ .map((registeredTool) => {
1651
+ const snippet = this._normalizePromptSnippet(registeredTool.definition.promptSnippet ?? registeredTool.definition.description);
1652
+ return snippet ? [registeredTool.definition.name, snippet] : undefined;
1653
+ })
1654
+ .filter((entry) => entry !== undefined));
1655
+ this._toolPromptGuidelines = new Map(allCustomTools
1656
+ .map((registeredTool) => {
1657
+ const guidelines = this._normalizePromptGuidelines(registeredTool.definition.promptGuidelines);
1658
+ return guidelines.length > 0 ? [registeredTool.definition.name, guidelines] : undefined;
1659
+ })
1660
+ .filter((entry) => entry !== undefined));
1661
+ const wrappedExtensionTools = this._extensionRunner
1662
+ ? wrapRegisteredTools(allCustomTools, this._extensionRunner)
1663
+ : [];
1664
+ const toolRegistry = new Map(this._baseToolRegistry);
1665
+ for (const tool of wrappedExtensionTools) {
1666
+ toolRegistry.set(tool.name, tool);
1667
+ }
1668
+ if (this._extensionRunner) {
1669
+ const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
1670
+ this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
1671
+ }
1672
+ else {
1673
+ this._toolRegistry = toolRegistry;
1674
+ }
1675
+ const nextActiveToolNames = options?.activeToolNames
1676
+ ? [...options.activeToolNames]
1677
+ : [...previousActiveToolNames];
1678
+ if (options?.includeAllExtensionTools) {
1679
+ for (const tool of wrappedExtensionTools) {
1680
+ nextActiveToolNames.push(tool.name);
1681
+ }
1682
+ }
1683
+ else if (!options?.activeToolNames) {
1684
+ for (const toolName of this._toolRegistry.keys()) {
1685
+ if (!previousRegistryNames.has(toolName)) {
1686
+ nextActiveToolNames.push(toolName);
1687
+ }
1688
+ }
1689
+ }
1690
+ this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
1691
+ }
1565
1692
  _buildRuntime(options) {
1566
1693
  const autoResizeImages = this.settingsManager.getImageAutoResize();
1567
1694
  const shellCommandPrefix = this.settingsManager.getShellCommandPrefix();
@@ -1591,47 +1718,14 @@ export class AgentSession {
1591
1718
  this._bindExtensionCore(this._extensionRunner);
1592
1719
  this._applyExtensionBindings(this._extensionRunner);
1593
1720
  }
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
1721
  const defaultActiveToolNames = this._baseToolsOverride
1607
1722
  ? Object.keys(this._baseToolsOverride)
1608
1723
  : ["read", "bash", "edit", "write"];
1609
1724
  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);
1725
+ this._refreshToolRegistry({
1726
+ activeToolNames: baseActiveToolNames,
1727
+ includeAllExtensionTools: options.includeAllExtensionTools,
1728
+ });
1635
1729
  }
1636
1730
  async reload() {
1637
1731
  const previousFlagValues = this._extensionRunner?.getFlagValues();
@@ -1677,15 +1771,18 @@ export class AgentSession {
1677
1771
  */
1678
1772
  async _handleRetryableError(message) {
1679
1773
  const settings = this.settingsManager.getRetrySettings();
1680
- if (!settings.enabled)
1774
+ if (!settings.enabled) {
1775
+ this._resolveRetry();
1681
1776
  return false;
1682
- this._retryAttempt++;
1683
- // Create retry promise on first attempt so waitForRetry() can await it
1684
- if (this._retryAttempt === 1 && !this._retryPromise) {
1777
+ }
1778
+ // Retry promise is created synchronously in _handleAgentEvent for agent_end.
1779
+ // Keep a defensive fallback here in case a future refactor bypasses that path.
1780
+ if (!this._retryPromise) {
1685
1781
  this._retryPromise = new Promise((resolve) => {
1686
1782
  this._retryResolve = resolve;
1687
1783
  });
1688
1784
  }
1785
+ this._retryAttempt++;
1689
1786
  if (this._retryAttempt > settings.maxRetries) {
1690
1787
  // Max retries exceeded, emit final failure and reset
1691
1788
  this._emit({