@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 +5 -0
- package/dist/agent.js +501 -465
- package/dist/index.d.ts +1 -1
- package/dist/tools/registry.d.ts +13 -1
- package/dist/tools/registry.js +17 -0
- package/dist/tools/types.d.ts +15 -0
- package/package.json +1 -1
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
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
//
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1594
|
-
emit({ type: 'llm_chunk', chunk });
|
|
1548
|
+
return sorted;
|
|
1595
1549
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1583
|
+
maxIterations,
|
|
1584
|
+
messages,
|
|
1608
1585
|
});
|
|
1609
|
-
if (
|
|
1610
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
|
1598
|
+
// Run beforeLLM hooks (can modify messages and tools)
|
|
1663
1599
|
if (this.hooksManager) {
|
|
1664
|
-
await this.hooksManager.
|
|
1600
|
+
const llmHookResult = await this.hooksManager.runBeforeLLM({
|
|
1665
1601
|
...hookContext,
|
|
1666
|
-
maxIterations,
|
|
1667
1602
|
messages,
|
|
1668
|
-
|
|
1669
|
-
completedWithText: true,
|
|
1603
|
+
tools,
|
|
1670
1604
|
});
|
|
1605
|
+
messages = llmHookResult.messages;
|
|
1606
|
+
tools = llmHookResult.tools;
|
|
1671
1607
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
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:
|
|
1740
|
+
aborted: true,
|
|
1754
1741
|
};
|
|
1755
1742
|
}
|
|
1756
|
-
emit({
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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: `
|
|
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 ?? '
|
|
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
|
-
|
|
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
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1865
|
-
result
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
-
|
|
1872
|
-
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
-
|
|
2034
|
+
if (aborted) {
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
// Run afterIteration hooks
|
|
1885
2038
|
if (this.hooksManager) {
|
|
1886
|
-
|
|
2039
|
+
await this.hooksManager.runAfterIteration({
|
|
1887
2040
|
...hookContext,
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2041
|
+
maxIterations,
|
|
2042
|
+
messages,
|
|
2043
|
+
toolCalls: iterationToolCalls,
|
|
2044
|
+
completedWithText: false,
|
|
1892
2045
|
});
|
|
1893
2046
|
}
|
|
1894
|
-
emit({ type: '
|
|
1895
|
-
//
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
//
|
|
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
|
-
|
|
1912
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2062
|
-
|
|
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
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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';
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/tools/registry.js
CHANGED
|
@@ -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}`,
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -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
|
}
|