|
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
15 | 15 |
|
| 16 | +import asyncio |
16 | 17 | import logging |
17 | 18 | import re |
18 | 19 | import shutil |
@@ -69,6 +70,47 @@ async def awrap_model_call(self, request, handler): |
69 | 70 | return self._patch(await handler(request)) |
70 | 71 |
|
71 | 72 |
|
| 73 | +class ToolRetryMiddleware(AgentMiddleware): |
| 74 | + """Retries failed tool calls with exponential backoff. |
| 75 | +
|
| 76 | + Provides uniform retry coverage for all tools. Some tools (e.g., Tavily) |
| 77 | + have their own internal retry; this middleware wraps the outer call so |
| 78 | + tools without retry (knowledge layer, paper search) are also covered. |
| 79 | + """ |
| 80 | + |
| 81 | + def __init__( |
| 82 | + self, |
| 83 | + max_retries: int = 16, |
| 84 | + backoff_factor: float = 2.0, |
| 85 | + initial_delay: float = 1.0, |
| 86 | + ): |
| 87 | + self.max_retries = max_retries |
| 88 | + self.backoff_factor = backoff_factor |
| 89 | + self.initial_delay = initial_delay |
| 90 | + |
| 91 | + async def awrap_tool_call(self, request, handler): |
| 92 | + """Retry tool calls on failure with exponential backoff.""" |
| 93 | + delay = self.initial_delay |
| 94 | + last_exception = None |
| 95 | + for attempt in range(self.max_retries + 1): |
| 96 | + try: |
| 97 | + return await handler(request) |
| 98 | + except Exception as e: |
| 99 | + last_exception = e |
| 100 | + if attempt < self.max_retries: |
| 101 | + tool_name = request.tool_call.get("name", "?") if hasattr(request, "tool_call") else "?" |
| 102 | + logger.warning( |
| 103 | + "Tool %s failed (attempt %d/%d): %s", |
| 104 | + tool_name, |
| 105 | + attempt + 1, |
| 106 | + self.max_retries + 1, |
| 107 | + e, |
| 108 | + ) |
| 109 | + await asyncio.sleep(delay) |
| 110 | + delay *= self.backoff_factor |
| 111 | + raise last_exception |
| 112 | + |
| 113 | + |
72 | 114 | def strip_pattern(text: str, pattern: re.Pattern[str] | None) -> str: |
73 | 115 | """Remove all regex matches from *text*. |
74 | 116 |
|
|
0 commit comments