@compilr-dev/agents 0.3.9 → 0.3.10

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.
package/dist/agent.d.ts CHANGED
@@ -1231,6 +1231,11 @@ export declare class Agent {
1231
1231
  * Get the context manager (if configured)
1232
1232
  */
1233
1233
  getContextManager(): ContextManager | undefined;
1234
+ /**
1235
+ * Get the tool registry instance.
1236
+ * Useful for setting up fallback handlers or inspecting registered tools.
1237
+ */
1238
+ getToolRegistry(): ToolRegistry;
1234
1239
  /**
1235
1240
  * Get context statistics
1236
1241
  */
package/dist/agent.js CHANGED
@@ -756,6 +756,13 @@ export class Agent {
756
756
  getContextManager() {
757
757
  return this.contextManager;
758
758
  }
759
+ /**
760
+ * Get the tool registry instance.
761
+ * Useful for setting up fallback handlers or inspecting registered tools.
762
+ */
763
+ getToolRegistry() {
764
+ return this.toolRegistry;
765
+ }
759
766
  /**
760
767
  * Get context statistics
761
768
  */
@@ -1525,247 +1532,237 @@ export class Agent {
1525
1532
  // Tool loop detection: track consecutive identical calls
1526
1533
  let lastToolCallHash = '';
1527
1534
  let consecutiveIdenticalCalls = 0;
1528
- // Hash function for tool call comparison
1529
- const hashToolCall = (name, input) => {
1530
- return `${name}:${JSON.stringify(input, Object.keys(input).sort())}`;
1531
- };
1532
- // Agentic loop
1533
- while (iterations < maxIterations) {
1534
- // Check for abort
1535
- if (signal?.aborted) {
1536
- aborted = true;
1537
- break;
1538
- }
1539
- iterations++;
1540
- emit({ type: 'iteration_start', iteration: iterations });
1541
- // Hook context for this iteration
1542
- const hookContext = {
1543
- sessionId: this._sessionId,
1544
- iteration: iterations,
1545
- signal,
1546
- metadata: {},
1547
- };
1548
- // Track tool calls for this iteration (for afterIteration hook)
1549
- const iterationToolCalls = [];
1550
- // Run beforeIteration hooks
1551
- if (this.hooksManager) {
1552
- const shouldContinue = await this.hooksManager.runBeforeIteration({
1553
- ...hookContext,
1554
- maxIterations,
1555
- messages,
1556
- });
1557
- if (!shouldContinue) {
1558
- emit({ type: 'iteration_end', iteration: iterations });
1559
- continue;
1560
- }
1561
- }
1562
- // Get tool definitions
1563
- let tools = this.toolRegistry.getDefinitions();
1564
- // Apply tool filter if specified (reduces token usage)
1565
- if (options?.toolFilter && options.toolFilter.length > 0) {
1566
- const filterSet = new Set(options.toolFilter);
1567
- tools = tools.filter((tool) => filterSet.has(tool.name));
1568
- }
1569
- // Run beforeLLM hooks (can modify messages and tools)
1570
- if (this.hooksManager) {
1571
- const llmHookResult = await this.hooksManager.runBeforeLLM({
1572
- ...hookContext,
1573
- messages,
1574
- tools,
1575
- });
1576
- messages = llmHookResult.messages;
1577
- tools = llmHookResult.tools;
1578
- }
1579
- // Call LLM
1580
- emit({ type: 'llm_start' });
1581
- const llmStartTime = Date.now();
1582
- const chunks = [];
1583
- try {
1584
- for await (const chunk of this.chatWithRetry(messages, {
1585
- ...chatOptions,
1586
- tools: tools.length > 0 ? tools : undefined,
1587
- }, emit, signal)) {
1588
- // Check for abort during streaming
1589
- if (signal?.aborted) {
1590
- aborted = true;
1591
- break;
1535
+ // Hash function for tool call comparison.
1536
+ // Uses a JSON replacer that recursively sorts object keys for stable hashing.
1537
+ // NOTE: A simple `JSON.stringify(input, Object.keys(input).sort())` only sorts
1538
+ // top-level keys — nested objects are serialized as `{}` because the replacer
1539
+ // array doesn't include their keys. This caused false positives for meta-tools
1540
+ // like `use_tool` where the actual arguments are nested inside `args`.
1541
+ const stableStringify = (obj) => {
1542
+ return JSON.stringify(obj, (_key, value) => {
1543
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1544
+ const sorted = {};
1545
+ for (const k of Object.keys(value).sort()) {
1546
+ sorted[k] = value[k];
1592
1547
  }
1593
- chunks.push(chunk);
1594
- emit({ type: 'llm_chunk', chunk });
1548
+ return sorted;
1595
1549
  }
1596
- }
1597
- catch (error) {
1550
+ return value;
1551
+ });
1552
+ };
1553
+ const hashToolCall = (name, input) => {
1554
+ return `${name}:${stableStringify(input)}`;
1555
+ };
1556
+ // Wrap agentic loop in try/finally to ensure conversation history is always
1557
+ // preserved, even when ToolLoopError or MaxIterationsError is thrown.
1558
+ // Without this, a thrown error skips the history append and the agent
1559
+ // loses all memory of the current run.
1560
+ try {
1561
+ // Agentic loop
1562
+ while (iterations < maxIterations) {
1563
+ // Check for abort
1598
1564
  if (signal?.aborted) {
1599
1565
  aborted = true;
1600
1566
  break;
1601
1567
  }
1602
- // Run onError hooks
1603
- if (this.hooksManager && error instanceof Error) {
1604
- const errorResult = await this.hooksManager.runOnError({
1568
+ iterations++;
1569
+ emit({ type: 'iteration_start', iteration: iterations });
1570
+ // Hook context for this iteration
1571
+ const hookContext = {
1572
+ sessionId: this._sessionId,
1573
+ iteration: iterations,
1574
+ signal,
1575
+ metadata: {},
1576
+ };
1577
+ // Track tool calls for this iteration (for afterIteration hook)
1578
+ const iterationToolCalls = [];
1579
+ // Run beforeIteration hooks
1580
+ if (this.hooksManager) {
1581
+ const shouldContinue = await this.hooksManager.runBeforeIteration({
1605
1582
  ...hookContext,
1606
- error,
1607
- phase: 'llm',
1583
+ maxIterations,
1584
+ messages,
1608
1585
  });
1609
- if (errorResult.handled) {
1610
- // Error was handled by hook, continue with next iteration
1586
+ if (!shouldContinue) {
1587
+ emit({ type: 'iteration_end', iteration: iterations });
1611
1588
  continue;
1612
1589
  }
1613
- if (errorResult.error) {
1614
- throw errorResult.error;
1615
- }
1616
1590
  }
1617
- throw error;
1618
- }
1619
- if (aborted) {
1620
- break;
1621
- }
1622
- // Process response
1623
- const { text, toolUses, usage, model } = this.processChunks(chunks);
1624
- emit({ type: 'llm_end', text, hasToolUses: toolUses.length > 0 });
1625
- // Record usage if available
1626
- if (usage && model) {
1627
- this.recordUsage(model, this.provider.name, {
1628
- inputTokens: usage.inputTokens,
1629
- outputTokens: usage.outputTokens,
1630
- totalTokens: usage.inputTokens + usage.outputTokens,
1631
- cacheReadTokens: usage.cacheReadTokens,
1632
- cacheCreationTokens: usage.cacheCreationTokens,
1633
- });
1634
- }
1635
- // Run afterLLM hooks
1636
- if (this.hooksManager) {
1637
- await this.hooksManager.runAfterLLM({
1638
- ...hookContext,
1639
- messages,
1640
- tools,
1641
- text,
1642
- toolUses,
1643
- usage: usage
1644
- ? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
1645
- : undefined,
1646
- model,
1647
- durationMs: Date.now() - llmStartTime,
1648
- });
1649
- }
1650
- // If no tool uses, we're done
1651
- if (toolUses.length === 0) {
1652
- finalResponse = text;
1653
- // Add final assistant response to history (only if non-empty)
1654
- // Empty responses can occur after silent tools like 'suggest'
1655
- if (text) {
1656
- const finalAssistantMsg = {
1657
- role: 'assistant',
1658
- content: text,
1659
- };
1660
- newMessages.push(finalAssistantMsg);
1591
+ // Get tool definitions
1592
+ let tools = this.toolRegistry.getDefinitions();
1593
+ // Apply tool filter if specified (reduces token usage)
1594
+ if (options?.toolFilter && options.toolFilter.length > 0) {
1595
+ const filterSet = new Set(options.toolFilter);
1596
+ tools = tools.filter((tool) => filterSet.has(tool.name));
1661
1597
  }
1662
- // Run afterIteration hooks
1598
+ // Run beforeLLM hooks (can modify messages and tools)
1663
1599
  if (this.hooksManager) {
1664
- await this.hooksManager.runAfterIteration({
1600
+ const llmHookResult = await this.hooksManager.runBeforeLLM({
1665
1601
  ...hookContext,
1666
- maxIterations,
1667
1602
  messages,
1668
- toolCalls: iterationToolCalls,
1669
- completedWithText: true,
1603
+ tools,
1670
1604
  });
1605
+ messages = llmHookResult.messages;
1606
+ tools = llmHookResult.tools;
1671
1607
  }
1672
- emit({ type: 'iteration_end', iteration: iterations });
1673
- break;
1674
- }
1675
- // Add assistant message with tool uses
1676
- const assistantMsg = {
1677
- role: 'assistant',
1678
- content: [
1679
- ...(text ? [{ type: 'text', text }] : []),
1680
- ...toolUses.map((tu) => ({
1681
- type: 'tool_use',
1682
- id: tu.id,
1683
- name: tu.name,
1684
- input: tu.input,
1685
- signature: tu.signature, // Gemini 3 thought signature (required for multi-turn)
1686
- })),
1687
- ],
1688
- };
1689
- messages.push(assistantMsg);
1690
- newMessages.push(assistantMsg);
1691
- // Execute tools and add results
1692
- // Check if we can parallelize - only parallelize tools marked as parallel-safe
1693
- const parallelTools = toolUses.filter((tu) => {
1694
- const tool = this.toolRegistry.get(tu.name);
1695
- return tool?.parallel === true;
1696
- });
1697
- const canParallelize = parallelTools.length > 1 && parallelTools.length === toolUses.length;
1698
- // Helper to execute a single tool with all checks
1699
- const executeSingleTool = async (toolUse) => {
1700
- // Check for abort
1701
- if (signal?.aborted) {
1702
- return {
1703
- result: { success: false, error: 'Aborted' },
1704
- toolResultMsg: {
1705
- role: 'user',
1706
- content: [
1707
- { type: 'tool_result', toolUseId: toolUse.id, content: 'Aborted', isError: true },
1708
- ],
1709
- },
1710
- skipped: true,
1711
- aborted: true,
1712
- };
1608
+ // Call LLM
1609
+ emit({ type: 'llm_start' });
1610
+ const llmStartTime = Date.now();
1611
+ const chunks = [];
1612
+ try {
1613
+ for await (const chunk of this.chatWithRetry(messages, {
1614
+ ...chatOptions,
1615
+ tools: tools.length > 0 ? tools : undefined,
1616
+ }, emit, signal)) {
1617
+ // Check for abort during streaming
1618
+ if (signal?.aborted) {
1619
+ aborted = true;
1620
+ break;
1621
+ }
1622
+ chunks.push(chunk);
1623
+ emit({ type: 'llm_chunk', chunk });
1624
+ }
1713
1625
  }
1714
- emit({
1715
- type: 'tool_start',
1716
- name: toolUse.name,
1717
- input: toolUse.input,
1718
- toolUseId: toolUse.id,
1719
- });
1720
- let result;
1721
- // Check permissions before execution
1722
- if (this.permissionManager) {
1723
- const permResult = await this.permissionManager.check(toolUse.name, toolUse.input);
1724
- if (permResult.askedUser) {
1725
- emit({ type: 'permission_asked', toolName: toolUse.name, level: permResult.level });
1626
+ catch (error) {
1627
+ if (signal?.aborted) {
1628
+ aborted = true;
1629
+ break;
1726
1630
  }
1727
- if (!permResult.allowed) {
1728
- emit({
1729
- type: 'permission_denied',
1730
- toolName: toolUse.name,
1731
- level: permResult.level,
1732
- reason: permResult.reason,
1631
+ // Run onError hooks
1632
+ if (this.hooksManager && error instanceof Error) {
1633
+ const errorResult = await this.hooksManager.runOnError({
1634
+ ...hookContext,
1635
+ error,
1636
+ phase: 'llm',
1733
1637
  });
1734
- result = {
1735
- success: false,
1736
- error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
1638
+ if (errorResult.handled) {
1639
+ // Error was handled by hook, continue with next iteration
1640
+ continue;
1641
+ }
1642
+ if (errorResult.error) {
1643
+ throw errorResult.error;
1644
+ }
1645
+ }
1646
+ throw error;
1647
+ }
1648
+ if (aborted) {
1649
+ break;
1650
+ }
1651
+ // Process response
1652
+ const { text, toolUses, usage, model } = this.processChunks(chunks);
1653
+ emit({ type: 'llm_end', text, hasToolUses: toolUses.length > 0 });
1654
+ // Record usage if available
1655
+ if (usage && model) {
1656
+ this.recordUsage(model, this.provider.name, {
1657
+ inputTokens: usage.inputTokens,
1658
+ outputTokens: usage.outputTokens,
1659
+ totalTokens: usage.inputTokens + usage.outputTokens,
1660
+ cacheReadTokens: usage.cacheReadTokens,
1661
+ cacheCreationTokens: usage.cacheCreationTokens,
1662
+ });
1663
+ }
1664
+ // Run afterLLM hooks
1665
+ if (this.hooksManager) {
1666
+ await this.hooksManager.runAfterLLM({
1667
+ ...hookContext,
1668
+ messages,
1669
+ tools,
1670
+ text,
1671
+ toolUses,
1672
+ usage: usage
1673
+ ? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
1674
+ : undefined,
1675
+ model,
1676
+ durationMs: Date.now() - llmStartTime,
1677
+ });
1678
+ }
1679
+ // If no tool uses, we're done
1680
+ if (toolUses.length === 0) {
1681
+ finalResponse = text;
1682
+ // Add final assistant response to history (only if non-empty)
1683
+ // Empty responses can occur after silent tools like 'suggest'
1684
+ if (text) {
1685
+ const finalAssistantMsg = {
1686
+ role: 'assistant',
1687
+ content: text,
1737
1688
  };
1738
- emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1689
+ newMessages.push(finalAssistantMsg);
1690
+ }
1691
+ // Run afterIteration hooks
1692
+ if (this.hooksManager) {
1693
+ await this.hooksManager.runAfterIteration({
1694
+ ...hookContext,
1695
+ maxIterations,
1696
+ messages,
1697
+ toolCalls: iterationToolCalls,
1698
+ completedWithText: true,
1699
+ });
1700
+ }
1701
+ emit({ type: 'iteration_end', iteration: iterations });
1702
+ break;
1703
+ }
1704
+ // Add assistant message with tool uses
1705
+ const assistantMsg = {
1706
+ role: 'assistant',
1707
+ content: [
1708
+ ...(text ? [{ type: 'text', text }] : []),
1709
+ ...toolUses.map((tu) => ({
1710
+ type: 'tool_use',
1711
+ id: tu.id,
1712
+ name: tu.name,
1713
+ input: tu.input,
1714
+ signature: tu.signature, // Gemini 3 thought signature (required for multi-turn)
1715
+ })),
1716
+ ],
1717
+ };
1718
+ messages.push(assistantMsg);
1719
+ newMessages.push(assistantMsg);
1720
+ // Execute tools and add results
1721
+ // Check if we can parallelize - only parallelize tools marked as parallel-safe
1722
+ const parallelTools = toolUses.filter((tu) => {
1723
+ const tool = this.toolRegistry.get(tu.name);
1724
+ return tool?.parallel === true;
1725
+ });
1726
+ const canParallelize = parallelTools.length > 1 && parallelTools.length === toolUses.length;
1727
+ // Helper to execute a single tool with all checks
1728
+ const executeSingleTool = async (toolUse) => {
1729
+ // Check for abort
1730
+ if (signal?.aborted) {
1739
1731
  return {
1740
- result,
1732
+ result: { success: false, error: 'Aborted' },
1741
1733
  toolResultMsg: {
1742
1734
  role: 'user',
1743
1735
  content: [
1744
- {
1745
- type: 'tool_result',
1746
- toolUseId: toolUse.id,
1747
- content: `Error: ${result.error ?? 'Permission denied'}`,
1748
- isError: true,
1749
- },
1736
+ { type: 'tool_result', toolUseId: toolUse.id, content: 'Aborted', isError: true },
1750
1737
  ],
1751
1738
  },
1752
1739
  skipped: true,
1753
- aborted: false,
1740
+ aborted: true,
1754
1741
  };
1755
1742
  }
1756
- emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
1757
- }
1758
- // Check guardrails before execution
1759
- if (this.guardrailManager) {
1760
- const { proceed, result: guardrailResult } = await this.guardrailManager.checkAndHandle(toolUse.name, toolUse.input);
1761
- if (guardrailResult.triggered) {
1762
- emit({ type: 'guardrail_triggered', result: guardrailResult });
1763
- if (!proceed) {
1764
- const message = guardrailResult.guardrail?.message ?? 'Operation blocked by guardrail';
1765
- emit({ type: 'guardrail_blocked', result: guardrailResult, message });
1743
+ emit({
1744
+ type: 'tool_start',
1745
+ name: toolUse.name,
1746
+ input: toolUse.input,
1747
+ toolUseId: toolUse.id,
1748
+ });
1749
+ let result;
1750
+ // Check permissions before execution
1751
+ if (this.permissionManager) {
1752
+ const permResult = await this.permissionManager.check(toolUse.name, toolUse.input);
1753
+ if (permResult.askedUser) {
1754
+ emit({ type: 'permission_asked', toolName: toolUse.name, level: permResult.level });
1755
+ }
1756
+ if (!permResult.allowed) {
1757
+ emit({
1758
+ type: 'permission_denied',
1759
+ toolName: toolUse.name,
1760
+ level: permResult.level,
1761
+ reason: permResult.reason,
1762
+ });
1766
1763
  result = {
1767
1764
  success: false,
1768
- error: `Guardrail blocked: ${message}`,
1765
+ error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
1769
1766
  };
1770
1767
  emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1771
1768
  return {
@@ -1776,7 +1773,7 @@ export class Agent {
1776
1773
  {
1777
1774
  type: 'tool_result',
1778
1775
  toolUseId: toolUse.id,
1779
- content: `Error: ${result.error ?? 'Blocked by guardrail'}`,
1776
+ content: `Error: ${result.error ?? 'Permission denied'}`,
1780
1777
  isError: true,
1781
1778
  },
1782
1779
  ],
@@ -1785,302 +1782,341 @@ export class Agent {
1785
1782
  aborted: false,
1786
1783
  };
1787
1784
  }
1788
- else if (guardrailResult.action === 'warn') {
1789
- const message = guardrailResult.guardrail?.message ?? 'Warning from guardrail';
1790
- emit({ type: 'guardrail_warning', result: guardrailResult, message });
1791
- }
1785
+ emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
1792
1786
  }
1793
- }
1794
- // Run beforeTool hooks (can skip or modify input)
1795
- let toolInput = toolUse.input;
1796
- if (this.hooksManager) {
1797
- const beforeToolResult = await this.hooksManager.runBeforeTool({
1798
- ...hookContext,
1799
- toolName: toolUse.name,
1800
- input: toolInput,
1801
- });
1802
- if (!beforeToolResult.proceed) {
1803
- result = beforeToolResult.skipResult ?? { success: false, error: 'Skipped by hook' };
1804
- emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1805
- return {
1806
- result,
1807
- toolResultMsg: {
1808
- role: 'user',
1809
- content: [
1810
- {
1811
- type: 'tool_result',
1812
- toolUseId: toolUse.id,
1813
- content: result.success
1814
- ? JSON.stringify(result.result)
1815
- : `Error: ${result.error ?? 'Unknown error'}`,
1816
- isError: !result.success,
1787
+ // Check guardrails before execution
1788
+ if (this.guardrailManager) {
1789
+ const { proceed, result: guardrailResult } = await this.guardrailManager.checkAndHandle(toolUse.name, toolUse.input);
1790
+ if (guardrailResult.triggered) {
1791
+ emit({ type: 'guardrail_triggered', result: guardrailResult });
1792
+ if (!proceed) {
1793
+ const message = guardrailResult.guardrail?.message ?? 'Operation blocked by guardrail';
1794
+ emit({ type: 'guardrail_blocked', result: guardrailResult, message });
1795
+ result = {
1796
+ success: false,
1797
+ error: `Guardrail blocked: ${message}`,
1798
+ };
1799
+ emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1800
+ return {
1801
+ result,
1802
+ toolResultMsg: {
1803
+ role: 'user',
1804
+ content: [
1805
+ {
1806
+ type: 'tool_result',
1807
+ toolUseId: toolUse.id,
1808
+ content: `Error: ${result.error ?? 'Blocked by guardrail'}`,
1809
+ isError: true,
1810
+ },
1811
+ ],
1817
1812
  },
1818
- ],
1813
+ skipped: true,
1814
+ aborted: false,
1815
+ };
1816
+ }
1817
+ else if (guardrailResult.action === 'warn') {
1818
+ const message = guardrailResult.guardrail?.message ?? 'Warning from guardrail';
1819
+ emit({ type: 'guardrail_warning', result: guardrailResult, message });
1820
+ }
1821
+ }
1822
+ }
1823
+ // Run beforeTool hooks (can skip or modify input)
1824
+ let toolInput = toolUse.input;
1825
+ if (this.hooksManager) {
1826
+ const beforeToolResult = await this.hooksManager.runBeforeTool({
1827
+ ...hookContext,
1828
+ toolName: toolUse.name,
1829
+ input: toolInput,
1830
+ });
1831
+ if (!beforeToolResult.proceed) {
1832
+ result = beforeToolResult.skipResult ?? { success: false, error: 'Skipped by hook' };
1833
+ emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1834
+ return {
1835
+ result,
1836
+ toolResultMsg: {
1837
+ role: 'user',
1838
+ content: [
1839
+ {
1840
+ type: 'tool_result',
1841
+ toolUseId: toolUse.id,
1842
+ content: result.success
1843
+ ? JSON.stringify(result.result)
1844
+ : `Error: ${result.error ?? 'Unknown error'}`,
1845
+ isError: !result.success,
1846
+ },
1847
+ ],
1848
+ },
1849
+ skipped: true,
1850
+ aborted: false,
1851
+ };
1852
+ }
1853
+ toolInput = beforeToolResult.input;
1854
+ }
1855
+ const toolStartTime = Date.now();
1856
+ try {
1857
+ // Get additional context from caller (e.g., for bash backgrounding)
1858
+ const additionalContext = getToolContext?.(toolUse.name, toolUse.id) ?? {};
1859
+ const toolContext = {
1860
+ toolUseId: toolUse.id,
1861
+ onOutput: (output, stream) => {
1862
+ emit({
1863
+ type: 'tool_output',
1864
+ toolUseId: toolUse.id,
1865
+ toolName: toolUse.name,
1866
+ output,
1867
+ stream,
1868
+ });
1819
1869
  },
1820
- skipped: true,
1821
- aborted: false,
1870
+ // Merge in additional context (abortSignal, onBackground, etc.)
1871
+ ...additionalContext,
1822
1872
  };
1873
+ result = await this.toolRegistry.execute(toolUse.name, toolInput, toolContext);
1823
1874
  }
1824
- toolInput = beforeToolResult.input;
1825
- }
1826
- const toolStartTime = Date.now();
1827
- try {
1828
- // Get additional context from caller (e.g., for bash backgrounding)
1829
- const additionalContext = getToolContext?.(toolUse.name, toolUse.id) ?? {};
1830
- const toolContext = {
1831
- toolUseId: toolUse.id,
1832
- onOutput: (output, stream) => {
1833
- emit({
1834
- type: 'tool_output',
1835
- toolUseId: toolUse.id,
1875
+ catch (error) {
1876
+ if (this.hooksManager && error instanceof Error) {
1877
+ const errorResult = await this.hooksManager.runOnError({
1878
+ ...hookContext,
1879
+ error,
1880
+ phase: 'tool',
1836
1881
  toolName: toolUse.name,
1837
- output,
1838
- stream,
1839
1882
  });
1840
- },
1841
- // Merge in additional context (abortSignal, onBackground, etc.)
1842
- ...additionalContext,
1843
- };
1844
- result = await this.toolRegistry.execute(toolUse.name, toolInput, toolContext);
1845
- }
1846
- catch (error) {
1847
- if (this.hooksManager && error instanceof Error) {
1883
+ if (errorResult.recovery) {
1884
+ result = errorResult.recovery;
1885
+ }
1886
+ else {
1887
+ result = {
1888
+ success: false,
1889
+ error: errorResult.error?.message ?? error.message,
1890
+ };
1891
+ }
1892
+ }
1893
+ else {
1894
+ result = {
1895
+ success: false,
1896
+ error: error instanceof Error ? error.message : String(error),
1897
+ };
1898
+ }
1899
+ }
1900
+ // Run onError hooks for failed tool results
1901
+ if (this.hooksManager && !result.success) {
1902
+ const syntheticError = new Error(result.error);
1848
1903
  const errorResult = await this.hooksManager.runOnError({
1849
1904
  ...hookContext,
1850
- error,
1905
+ error: syntheticError,
1851
1906
  phase: 'tool',
1852
1907
  toolName: toolUse.name,
1853
1908
  });
1854
1909
  if (errorResult.recovery) {
1855
1910
  result = errorResult.recovery;
1856
1911
  }
1857
- else {
1858
- result = {
1859
- success: false,
1860
- error: errorResult.error?.message ?? error.message,
1861
- };
1912
+ }
1913
+ // Run afterTool hooks (can modify result)
1914
+ if (this.hooksManager) {
1915
+ result = await this.hooksManager.runAfterTool({
1916
+ ...hookContext,
1917
+ toolName: toolUse.name,
1918
+ input: toolInput,
1919
+ result,
1920
+ durationMs: Date.now() - toolStartTime,
1921
+ });
1922
+ }
1923
+ emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1924
+ // Build tool result content
1925
+ let toolResultContent = result.success
1926
+ ? JSON.stringify(result.result)
1927
+ : `Error: ${result.error ?? 'Unknown error'}`;
1928
+ // Context management (only for sequential - parallel handles this after)
1929
+ if (!canParallelize && this.contextManager && this.autoContextManagement) {
1930
+ const estimatedTokens = this.contextManager.estimateTokens(toolResultContent);
1931
+ const preflight = this.contextManager.canAddContent(estimatedTokens, 'toolResults');
1932
+ if (!preflight.allowed) {
1933
+ if (preflight.action === 'reject') {
1934
+ const filtered = this.contextManager.filterContent(toolResultContent, 'tool_result');
1935
+ toolResultContent = filtered.content;
1936
+ }
1937
+ // Note: compact/summarize actions are complex with parallel execution
1938
+ // For now, only filter is applied during parallel; full context mgmt happens after
1862
1939
  }
1940
+ const finalTokens = this.contextManager.estimateTokens(toolResultContent);
1941
+ this.contextManager.addToCategory('toolResults', finalTokens);
1863
1942
  }
1864
- else {
1865
- result = {
1866
- success: false,
1867
- error: error instanceof Error ? error.message : String(error),
1868
- };
1943
+ return {
1944
+ result,
1945
+ toolResultMsg: {
1946
+ role: 'user',
1947
+ content: [
1948
+ {
1949
+ type: 'tool_result',
1950
+ toolUseId: toolUse.id,
1951
+ content: toolResultContent,
1952
+ isError: !result.success,
1953
+ },
1954
+ ],
1955
+ },
1956
+ skipped: false,
1957
+ aborted: false,
1958
+ };
1959
+ };
1960
+ // Execute tools - parallel if all are parallel-safe, otherwise sequential
1961
+ if (canParallelize) {
1962
+ // Parallel execution
1963
+ const results = await Promise.all(toolUses.map((tu) => executeSingleTool(tu)));
1964
+ for (let i = 0; i < toolUses.length; i++) {
1965
+ const toolUse = toolUses[i];
1966
+ const { result, toolResultMsg, aborted: wasAborted } = results[i];
1967
+ if (wasAborted) {
1968
+ aborted = true;
1969
+ break;
1970
+ }
1971
+ // Tool loop detection (still applies per-tool)
1972
+ if (this.maxConsecutiveToolCalls > 0) {
1973
+ const currentHash = hashToolCall(toolUse.name, toolUse.input);
1974
+ if (currentHash === lastToolCallHash) {
1975
+ consecutiveIdenticalCalls++;
1976
+ if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
1977
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
1978
+ }
1979
+ emit({
1980
+ type: 'tool_loop_warning',
1981
+ toolName: toolUse.name,
1982
+ consecutiveCalls: consecutiveIdenticalCalls,
1983
+ });
1984
+ }
1985
+ else {
1986
+ lastToolCallHash = currentHash;
1987
+ consecutiveIdenticalCalls = 1;
1988
+ }
1989
+ }
1990
+ const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
1991
+ toolCalls.push(toolCallEntry);
1992
+ iterationToolCalls.push(toolCallEntry);
1993
+ messages.push(toolResultMsg);
1994
+ newMessages.push(toolResultMsg);
1869
1995
  }
1870
1996
  }
1871
- // Run onError hooks for failed tool results
1872
- if (this.hooksManager && !result.success) {
1873
- const syntheticError = new Error(result.error);
1874
- const errorResult = await this.hooksManager.runOnError({
1875
- ...hookContext,
1876
- error: syntheticError,
1877
- phase: 'tool',
1878
- toolName: toolUse.name,
1879
- });
1880
- if (errorResult.recovery) {
1881
- result = errorResult.recovery;
1997
+ else {
1998
+ // Sequential execution (original loop, but using the helper)
1999
+ for (const toolUse of toolUses) {
2000
+ const { result, toolResultMsg, skipped, aborted: wasAborted, } = await executeSingleTool(toolUse);
2001
+ if (wasAborted) {
2002
+ aborted = true;
2003
+ break;
2004
+ }
2005
+ // Tool loop detection
2006
+ if (this.maxConsecutiveToolCalls > 0) {
2007
+ const currentHash = hashToolCall(toolUse.name, toolUse.input);
2008
+ if (currentHash === lastToolCallHash) {
2009
+ consecutiveIdenticalCalls++;
2010
+ if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
2011
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2012
+ }
2013
+ emit({
2014
+ type: 'tool_loop_warning',
2015
+ toolName: toolUse.name,
2016
+ consecutiveCalls: consecutiveIdenticalCalls,
2017
+ });
2018
+ }
2019
+ else {
2020
+ lastToolCallHash = currentHash;
2021
+ consecutiveIdenticalCalls = 1;
2022
+ }
2023
+ }
2024
+ const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
2025
+ toolCalls.push(toolCallEntry);
2026
+ iterationToolCalls.push(toolCallEntry);
2027
+ messages.push(toolResultMsg);
2028
+ newMessages.push(toolResultMsg);
2029
+ if (skipped) {
2030
+ continue;
2031
+ }
1882
2032
  }
1883
2033
  }
1884
- // Run afterTool hooks (can modify result)
2034
+ if (aborted) {
2035
+ break;
2036
+ }
2037
+ // Run afterIteration hooks
1885
2038
  if (this.hooksManager) {
1886
- result = await this.hooksManager.runAfterTool({
2039
+ await this.hooksManager.runAfterIteration({
1887
2040
  ...hookContext,
1888
- toolName: toolUse.name,
1889
- input: toolInput,
1890
- result,
1891
- durationMs: Date.now() - toolStartTime,
2041
+ maxIterations,
2042
+ messages,
2043
+ toolCalls: iterationToolCalls,
2044
+ completedWithText: false,
1892
2045
  });
1893
2046
  }
1894
- emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
1895
- // Build tool result content
1896
- let toolResultContent = result.success
1897
- ? JSON.stringify(result.result)
1898
- : `Error: ${result.error ?? 'Unknown error'}`;
1899
- // Context management (only for sequential - parallel handles this after)
1900
- if (!canParallelize && this.contextManager && this.autoContextManagement) {
1901
- const estimatedTokens = this.contextManager.estimateTokens(toolResultContent);
1902
- const preflight = this.contextManager.canAddContent(estimatedTokens, 'toolResults');
1903
- if (!preflight.allowed) {
1904
- if (preflight.action === 'reject') {
1905
- const filtered = this.contextManager.filterContent(toolResultContent, 'tool_result');
1906
- toolResultContent = filtered.content;
1907
- }
1908
- // Note: compact/summarize actions are complex with parallel execution
1909
- // For now, only filter is applied during parallel; full context mgmt happens after
2047
+ emit({ type: 'iteration_end', iteration: iterations });
2048
+ // Check if we're about to hit the iteration limit
2049
+ // If callback is defined, ask if we should continue
2050
+ if (iterations >= maxIterations && this.onIterationLimitReached) {
2051
+ emit({
2052
+ type: 'iteration_limit_reached',
2053
+ iteration: iterations,
2054
+ maxIterations,
2055
+ });
2056
+ const result = await this.onIterationLimitReached({
2057
+ iteration: iterations,
2058
+ maxIterations,
2059
+ toolCallCount: toolCalls.length,
2060
+ });
2061
+ if (typeof result === 'number' && result > 0) {
2062
+ // Extend the limit and continue
2063
+ maxIterations += result;
2064
+ emit({
2065
+ type: 'iteration_limit_extended',
2066
+ newMaxIterations: maxIterations,
2067
+ addedIterations: result,
2068
+ });
1910
2069
  }
1911
- const finalTokens = this.contextManager.estimateTokens(toolResultContent);
1912
- this.contextManager.addToCategory('toolResults', finalTokens);
1913
- }
1914
- return {
1915
- result,
1916
- toolResultMsg: {
1917
- role: 'user',
1918
- content: [
1919
- {
1920
- type: 'tool_result',
1921
- toolUseId: toolUse.id,
1922
- content: toolResultContent,
1923
- isError: !result.success,
1924
- },
1925
- ],
1926
- },
1927
- skipped: false,
1928
- aborted: false,
1929
- };
1930
- };
1931
- // Execute tools - parallel if all are parallel-safe, otherwise sequential
1932
- if (canParallelize) {
1933
- // Parallel execution
1934
- const results = await Promise.all(toolUses.map((tu) => executeSingleTool(tu)));
1935
- for (let i = 0; i < toolUses.length; i++) {
1936
- const toolUse = toolUses[i];
1937
- const { result, toolResultMsg, aborted: wasAborted } = results[i];
1938
- if (wasAborted) {
2070
+ else {
2071
+ // User chose to stop - set aborted to prevent error throw
1939
2072
  aborted = true;
1940
- break;
1941
- }
1942
- // Tool loop detection (still applies per-tool)
1943
- if (this.maxConsecutiveToolCalls > 0) {
1944
- const currentHash = hashToolCall(toolUse.name, toolUse.input);
1945
- if (currentHash === lastToolCallHash) {
1946
- consecutiveIdenticalCalls++;
1947
- if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
1948
- throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
1949
- }
1950
- emit({
1951
- type: 'tool_loop_warning',
1952
- toolName: toolUse.name,
1953
- consecutiveCalls: consecutiveIdenticalCalls,
1954
- });
1955
- }
1956
- else {
1957
- lastToolCallHash = currentHash;
1958
- consecutiveIdenticalCalls = 1;
1959
- }
2073
+ emit({ type: 'done', response: finalResponse });
1960
2074
  }
1961
- const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
1962
- toolCalls.push(toolCallEntry);
1963
- iterationToolCalls.push(toolCallEntry);
1964
- messages.push(toolResultMsg);
1965
- newMessages.push(toolResultMsg);
1966
2075
  }
1967
2076
  }
1968
- else {
1969
- // Sequential execution (original loop, but using the helper)
1970
- for (const toolUse of toolUses) {
1971
- const { result, toolResultMsg, skipped, aborted: wasAborted, } = await executeSingleTool(toolUse);
1972
- if (wasAborted) {
1973
- aborted = true;
1974
- break;
2077
+ // Check if we hit max iterations without completing
2078
+ if (!aborted && iterations >= maxIterations && finalResponse === '') {
2079
+ if (this.iterationLimitBehavior === 'summarize') {
2080
+ // Generate a summary response before throwing
2081
+ try {
2082
+ finalResponse = await this.generateIterationLimitSummary(messages, maxIterations, toolCalls);
2083
+ emit({ type: 'done', response: finalResponse });
1975
2084
  }
1976
- // Tool loop detection
1977
- if (this.maxConsecutiveToolCalls > 0) {
1978
- const currentHash = hashToolCall(toolUse.name, toolUse.input);
1979
- if (currentHash === lastToolCallHash) {
1980
- consecutiveIdenticalCalls++;
1981
- if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
1982
- throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
1983
- }
1984
- emit({
1985
- type: 'tool_loop_warning',
1986
- toolName: toolUse.name,
1987
- consecutiveCalls: consecutiveIdenticalCalls,
1988
- });
1989
- }
1990
- else {
1991
- lastToolCallHash = currentHash;
1992
- consecutiveIdenticalCalls = 1;
1993
- }
1994
- }
1995
- const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
1996
- toolCalls.push(toolCallEntry);
1997
- iterationToolCalls.push(toolCallEntry);
1998
- messages.push(toolResultMsg);
1999
- newMessages.push(toolResultMsg);
2000
- if (skipped) {
2001
- continue;
2085
+ catch {
2086
+ // If summary generation fails, still throw the error
2087
+ throw new MaxIterationsError(maxIterations);
2002
2088
  }
2003
2089
  }
2004
- }
2005
- if (aborted) {
2006
- break;
2007
- }
2008
- // Run afterIteration hooks
2009
- if (this.hooksManager) {
2010
- await this.hooksManager.runAfterIteration({
2011
- ...hookContext,
2012
- maxIterations,
2013
- messages,
2014
- toolCalls: iterationToolCalls,
2015
- completedWithText: false,
2016
- });
2017
- }
2018
- emit({ type: 'iteration_end', iteration: iterations });
2019
- // Check if we're about to hit the iteration limit
2020
- // If callback is defined, ask if we should continue
2021
- if (iterations >= maxIterations && this.onIterationLimitReached) {
2022
- emit({
2023
- type: 'iteration_limit_reached',
2024
- iteration: iterations,
2025
- maxIterations,
2026
- });
2027
- const result = await this.onIterationLimitReached({
2028
- iteration: iterations,
2029
- maxIterations,
2030
- toolCallCount: toolCalls.length,
2031
- });
2032
- if (typeof result === 'number' && result > 0) {
2033
- // Extend the limit and continue
2034
- maxIterations += result;
2035
- emit({
2036
- type: 'iteration_limit_extended',
2037
- newMaxIterations: maxIterations,
2038
- addedIterations: result,
2039
- });
2090
+ else if (this.iterationLimitBehavior === 'continue') {
2091
+ // Return partial result without throwing
2092
+ finalResponse =
2093
+ `[Agent reached maximum iterations (${String(maxIterations)}) without completing. ` +
2094
+ `${String(toolCalls.length)} tool calls were made.]`;
2040
2095
  }
2041
2096
  else {
2042
- // User chose to stop - set aborted to prevent error throw
2043
- aborted = true;
2044
- emit({ type: 'done', response: finalResponse });
2045
- }
2046
- }
2047
- }
2048
- // Check if we hit max iterations without completing
2049
- if (!aborted && iterations >= maxIterations && finalResponse === '') {
2050
- if (this.iterationLimitBehavior === 'summarize') {
2051
- // Generate a summary response before throwing
2052
- try {
2053
- finalResponse = await this.generateIterationLimitSummary(messages, maxIterations, toolCalls);
2054
- emit({ type: 'done', response: finalResponse });
2055
- }
2056
- catch {
2057
- // If summary generation fails, still throw the error
2097
+ // Default: throw error
2058
2098
  throw new MaxIterationsError(maxIterations);
2059
2099
  }
2060
2100
  }
2061
- else if (this.iterationLimitBehavior === 'continue') {
2062
- // Return partial result without throwing
2063
- finalResponse =
2064
- `[Agent reached maximum iterations (${String(maxIterations)}) without completing. ` +
2065
- `${String(toolCalls.length)} tool calls were made.]`;
2066
- }
2067
- else {
2068
- // Default: throw error
2069
- throw new MaxIterationsError(maxIterations);
2101
+ if (!aborted && !(iterations >= maxIterations && this.iterationLimitBehavior === 'summarize')) {
2102
+ emit({ type: 'done', response: finalResponse });
2070
2103
  }
2071
2104
  }
2072
- if (!aborted && !(iterations >= maxIterations && this.iterationLimitBehavior === 'summarize')) {
2073
- emit({ type: 'done', response: finalResponse });
2074
- }
2075
- // Context management: increment turn count and update token count
2076
- if (this.contextManager) {
2077
- this.contextManager.incrementTurn();
2078
- await this.contextManager.updateTokenCount(messages);
2105
+ finally {
2106
+ // CRITICAL: Always preserve conversation history, even on ToolLoopError
2107
+ // or MaxIterationsError. Without this, the agent loses all memory of the
2108
+ // current run and the user's next message starts with no context.
2109
+ if (newMessages.length > 0) {
2110
+ this.conversationHistory.push(...newMessages);
2111
+ }
2112
+ // Context management: increment turn count and update token count
2113
+ if (this.contextManager) {
2114
+ this.contextManager.incrementTurn();
2115
+ await this.contextManager.updateTokenCount(messages);
2116
+ }
2117
+ // Update internal state tracking
2118
+ this._currentIteration = iterations;
2079
2119
  }
2080
- // Append new messages to conversation history (persists for next run)
2081
- this.conversationHistory.push(...newMessages);
2082
- // Update internal state tracking
2083
- this._currentIteration = iterations;
2084
2120
  // Note: token tracking would require provider-specific token counting
2085
2121
  // Build result with optional context stats
2086
2122
  const result = {
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { PerplexityProvider, createPerplexityProvider } from './providers/index.
31
31
  export type { PerplexityProviderConfig } from './providers/index.js';
32
32
  export { OpenRouterProvider, createOpenRouterProvider } from './providers/index.js';
33
33
  export type { OpenRouterProviderConfig } from './providers/index.js';
34
- export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, SuggestInput, SuggestToolOptions, } from './tools/index.js';
34
+ export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, ToolFallbackHandler, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, SuggestInput, SuggestToolOptions, } from './tools/index.js';
35
35
  export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, DefaultToolRegistry, createToolRegistry, } from './tools/index.js';
36
36
  export { readFileTool, createReadFileTool, writeFileTool, createWriteFileTool, bashTool, createBashTool, execStream, detectFifoUsage, bashOutputTool, createBashOutputTool, killShellTool, createKillShellTool, ShellManager, getDefaultShellManager, setDefaultShellManager, grepTool, createGrepTool, globTool, createGlobTool, editTool, createEditTool, todoWriteTool, todoReadTool, createTodoTools, TodoStore, resetDefaultTodoStore, getDefaultTodoStore, createIsolatedTodoStore, cleanupTodoContextMessages, getTodoContextStats, webFetchTool, createWebFetchTool, createTaskTool, defaultAgentTypes, suggestTool, createSuggestTool, builtinTools, allBuiltinTools, TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
37
37
  export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, repairToolPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ToolRegistry - Manages available tools for an agent
3
3
  */
4
- import type { Tool, ToolDefinition, ToolRegistry as IToolRegistry, ToolExecutionResult, ToolExecutionContext } from './types.js';
4
+ import type { Tool, ToolDefinition, ToolRegistry as IToolRegistry, ToolExecutionResult, ToolExecutionContext, ToolFallbackHandler } from './types.js';
5
+ export type { ToolFallbackHandler } from './types.js';
5
6
  /**
6
7
  * Options for creating a DefaultToolRegistry
7
8
  */
@@ -15,6 +16,11 @@ export interface ToolRegistryOptions {
15
16
  * Per-tool timeout overrides (tool name -> timeout in ms)
16
17
  */
17
18
  toolTimeouts?: Record<string, number>;
19
+ /**
20
+ * Fallback handler for tools not found in the primary registry.
21
+ * Enables transparent routing to secondary registries (e.g., meta-tools).
22
+ */
23
+ fallbackHandler?: ToolFallbackHandler;
18
24
  }
19
25
  /**
20
26
  * Default implementation of ToolRegistry
@@ -23,7 +29,13 @@ export declare class DefaultToolRegistry implements IToolRegistry {
23
29
  private readonly tools;
24
30
  private readonly defaultTimeoutMs;
25
31
  private readonly toolTimeouts;
32
+ private fallbackHandler;
26
33
  constructor(options?: ToolRegistryOptions);
34
+ /**
35
+ * Set a fallback handler for tools not found in the primary registry.
36
+ * Enables transparent routing to secondary registries (e.g., meta-tools).
37
+ */
38
+ setFallbackHandler(handler: ToolFallbackHandler | null): void;
27
39
  /**
28
40
  * Register a tool
29
41
  */
@@ -13,9 +13,18 @@ export class DefaultToolRegistry {
13
13
  tools = new Map();
14
14
  defaultTimeoutMs;
15
15
  toolTimeouts;
16
+ fallbackHandler;
16
17
  constructor(options) {
17
18
  this.defaultTimeoutMs = options?.defaultTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
18
19
  this.toolTimeouts = options?.toolTimeouts ?? {};
20
+ this.fallbackHandler = options?.fallbackHandler ?? null;
21
+ }
22
+ /**
23
+ * Set a fallback handler for tools not found in the primary registry.
24
+ * Enables transparent routing to secondary registries (e.g., meta-tools).
25
+ */
26
+ setFallbackHandler(handler) {
27
+ this.fallbackHandler = handler;
19
28
  }
20
29
  /**
21
30
  * Register a tool
@@ -81,6 +90,14 @@ export class DefaultToolRegistry {
81
90
  async execute(name, input, contextOrTimeout, timeoutMs) {
82
91
  const tool = this.tools.get(name);
83
92
  if (!tool) {
93
+ // Try fallback handler (e.g., meta-tools registry)
94
+ if (this.fallbackHandler) {
95
+ const context = typeof contextOrTimeout === 'object' ? contextOrTimeout : undefined;
96
+ const fallbackResult = await this.fallbackHandler(name, input, context);
97
+ if (fallbackResult !== null) {
98
+ return fallbackResult;
99
+ }
100
+ }
84
101
  return {
85
102
  success: false,
86
103
  error: `Tool not found: ${name}`,
@@ -79,6 +79,16 @@ export interface Tool<T = object> {
79
79
  */
80
80
  silent?: boolean;
81
81
  }
82
+ /**
83
+ * Fallback handler for tools not found in the primary registry.
84
+ * Used by meta-tools to transparently route calls to a secondary registry.
85
+ *
86
+ * @param name - Tool name that was not found
87
+ * @param input - Tool input parameters
88
+ * @param context - Optional execution context
89
+ * @returns Result if handled, or null to return the default "not found" error
90
+ */
91
+ export type ToolFallbackHandler = (name: string, input: Record<string, unknown>, context?: ToolExecutionContext) => Promise<ToolExecutionResult | null>;
82
92
  /**
83
93
  * Tool registry for managing available tools
84
94
  */
@@ -102,4 +112,9 @@ export interface ToolRegistry {
102
112
  * @param context - Optional execution context for streaming
103
113
  */
104
114
  execute(name: string, input: Record<string, unknown>, context?: ToolExecutionContext): Promise<ToolExecutionResult>;
115
+ /**
116
+ * Set a fallback handler for tools not found in the primary registry.
117
+ * Enables transparent routing to secondary registries (e.g., meta-tools).
118
+ */
119
+ setFallbackHandler(handler: ToolFallbackHandler | null): void;
105
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",