codepp 0.0.437__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,337 @@
1
+ """
2
+ Retry manager for MCP server communication with various backoff strategies.
3
+
4
+ This module provides retry logic for handling transient failures in MCP server
5
+ communication with intelligent backoff strategies to prevent overwhelming failed servers.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import random
11
+ import threading
12
+ from collections import defaultdict
13
+ from dataclasses import dataclass
14
+ from datetime import datetime
15
+ from typing import Any, Callable, Dict, Optional
16
+
17
+ import httpx
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class RetryStats:
24
+ """Statistics for retry operations per server."""
25
+
26
+ total_retries: int = 0
27
+ successful_retries: int = 0
28
+ failed_retries: int = 0
29
+ average_attempts: float = 0.0
30
+ last_retry: Optional[datetime] = None
31
+
32
+ def calculate_average(self, new_attempts: int) -> None:
33
+ """Update the average attempts calculation."""
34
+ if self.total_retries == 0:
35
+ self.average_attempts = float(new_attempts)
36
+ else:
37
+ # Calculate new average: (old_average * old_count + new_value) / new_count
38
+ total_attempts = (self.average_attempts * self.total_retries) + new_attempts
39
+ self.average_attempts = total_attempts / (self.total_retries + 1)
40
+
41
+
42
+ class RetryManager:
43
+ """
44
+ Manages retry logic for MCP server operations with various backoff strategies.
45
+
46
+ Supports different backoff strategies and intelligent retry decisions based on
47
+ error types. Tracks retry statistics per server for monitoring.
48
+
49
+ Note: This class is designed for async-only usage. The ``_stats`` dict is
50
+ protected by an ``asyncio.Lock`` for coroutine-safe access.
51
+ """
52
+
53
+ def __init__(self):
54
+ """Initialize the retry manager."""
55
+ self._stats: Dict[str, RetryStats] = defaultdict(RetryStats)
56
+ self._lock: Optional[asyncio.Lock] = None
57
+
58
+ def _get_lock(self) -> asyncio.Lock:
59
+ """Lazily create the asyncio.Lock to avoid issues with event loop timing."""
60
+ if self._lock is None:
61
+ self._lock = asyncio.Lock()
62
+ return self._lock
63
+
64
+ async def retry_with_backoff(
65
+ self,
66
+ func: Callable,
67
+ max_attempts: int = 3,
68
+ strategy: str = "exponential",
69
+ server_id: str = "unknown",
70
+ ) -> Any:
71
+ """
72
+ Execute a function with retry logic and backoff strategy.
73
+
74
+ Args:
75
+ func: The async function to execute
76
+ max_attempts: Maximum number of retry attempts
77
+ strategy: Backoff strategy ('fixed', 'linear', 'exponential', 'exponential_jitter')
78
+ server_id: ID of the server for tracking stats
79
+
80
+ Returns:
81
+ The result of the function call
82
+
83
+ Raises:
84
+ The last exception encountered if all retries fail
85
+ """
86
+ last_exception = None
87
+
88
+ for attempt in range(max_attempts):
89
+ try:
90
+ result = await func()
91
+
92
+ # Record successful retry if this wasn't the first attempt
93
+ if attempt > 0:
94
+ await self.record_retry(server_id, attempt + 1, success=True)
95
+
96
+ return result
97
+
98
+ except Exception as e:
99
+ last_exception = e
100
+
101
+ # Check if this error is retryable
102
+ if not self.should_retry(e):
103
+ logger.info(
104
+ f"Non-retryable error for server {server_id}: {type(e).__name__}: {e}"
105
+ )
106
+ await self.record_retry(server_id, attempt + 1, success=False)
107
+ raise
108
+
109
+ # If this is the last attempt, don't wait
110
+ if attempt == max_attempts - 1:
111
+ await self.record_retry(server_id, max_attempts, success=False)
112
+ break
113
+
114
+ # Calculate backoff delay
115
+ delay = self.calculate_backoff(attempt + 1, strategy)
116
+
117
+ logger.warning(
118
+ f"Attempt {attempt + 1}/{max_attempts} failed for server {server_id}: "
119
+ f"{type(e).__name__}: {e}. Retrying in {delay:.2f}s"
120
+ )
121
+
122
+ # Wait before retrying
123
+ await asyncio.sleep(delay)
124
+
125
+ # All attempts failed
126
+ logger.error(
127
+ f"All {max_attempts} attempts failed for server {server_id}. "
128
+ f"Last error: {type(last_exception).__name__}: {last_exception}"
129
+ )
130
+ raise last_exception
131
+
132
+ def calculate_backoff(self, attempt: int, strategy: str) -> float:
133
+ """
134
+ Calculate backoff delay based on attempt number and strategy.
135
+
136
+ Args:
137
+ attempt: The current attempt number (1-based)
138
+ strategy: The backoff strategy to use
139
+
140
+ Returns:
141
+ Delay in seconds
142
+ """
143
+ if strategy == "fixed":
144
+ return 1.0
145
+
146
+ elif strategy == "linear":
147
+ return float(attempt)
148
+
149
+ elif strategy == "exponential":
150
+ return 2.0 ** (attempt - 1)
151
+
152
+ elif strategy == "exponential_jitter":
153
+ base_delay = 2.0 ** (attempt - 1)
154
+ jitter = random.uniform(-0.25, 0.25) # ±25% jitter
155
+ return max(0.1, base_delay * (1 + jitter))
156
+
157
+ else:
158
+ logger.warning(f"Unknown backoff strategy: {strategy}, using exponential")
159
+ return 2.0 ** (attempt - 1)
160
+
161
+ def should_retry(self, error: Exception) -> bool:
162
+ """
163
+ Determine if an error is retryable.
164
+
165
+ Args:
166
+ error: The exception to evaluate
167
+
168
+ Returns:
169
+ True if the error is retryable, False otherwise
170
+ """
171
+ # Network timeouts and connection errors are retryable
172
+ if isinstance(error, (asyncio.TimeoutError, ConnectionError, OSError)):
173
+ return True
174
+
175
+ # HTTP errors
176
+ if isinstance(error, httpx.HTTPError):
177
+ if isinstance(error, httpx.TimeoutException):
178
+ return True
179
+ elif isinstance(error, httpx.ConnectError):
180
+ return True
181
+ elif isinstance(error, httpx.ReadError):
182
+ return True
183
+ elif hasattr(error, "response") and error.response is not None:
184
+ status_code = error.response.status_code
185
+ # 5xx server errors are retryable
186
+ if 500 <= status_code < 600:
187
+ return True
188
+ # Rate limit errors are retryable (with longer backoff)
189
+ if status_code == 429:
190
+ return True
191
+ # 4xx client errors are generally not retryable
192
+ # except for specific cases like 408 (timeout)
193
+ if status_code == 408:
194
+ return True
195
+ return False
196
+
197
+ # JSON decode errors might be transient
198
+ if isinstance(error, ValueError) and "json" in str(error).lower():
199
+ return True
200
+
201
+ # Authentication and authorization errors are not retryable
202
+ error_str = str(error).lower()
203
+ if any(
204
+ term in error_str
205
+ for term in ["unauthorized", "forbidden", "authentication", "permission"]
206
+ ):
207
+ return False
208
+
209
+ # Schema validation errors are not retryable
210
+ if "schema" in error_str or "validation" in error_str:
211
+ return False
212
+
213
+ # By default, consider other errors as potentially retryable
214
+ # This is conservative but helps handle unknown transient issues
215
+ return True
216
+
217
+ async def record_retry(self, server_id: str, attempts: int, success: bool) -> None:
218
+ """
219
+ Record retry statistics for a server.
220
+
221
+ Args:
222
+ server_id: ID of the server
223
+ attempts: Number of attempts made
224
+ success: Whether the retry was successful
225
+ """
226
+ async with self._get_lock():
227
+ stats = self._stats[server_id]
228
+ stats.last_retry = datetime.now()
229
+
230
+ if success:
231
+ stats.successful_retries += 1
232
+ else:
233
+ stats.failed_retries += 1
234
+
235
+ stats.calculate_average(attempts)
236
+ stats.total_retries += 1
237
+
238
+ async def get_retry_stats(self, server_id: str) -> RetryStats:
239
+ """
240
+ Get retry statistics for a server.
241
+
242
+ Args:
243
+ server_id: ID of the server
244
+
245
+ Returns:
246
+ RetryStats object with current statistics
247
+ """
248
+ async with self._get_lock():
249
+ # Return a copy to avoid external modification
250
+ stats = self._stats[server_id]
251
+ return RetryStats(
252
+ total_retries=stats.total_retries,
253
+ successful_retries=stats.successful_retries,
254
+ failed_retries=stats.failed_retries,
255
+ average_attempts=stats.average_attempts,
256
+ last_retry=stats.last_retry,
257
+ )
258
+
259
+ async def get_all_stats(self) -> Dict[str, RetryStats]:
260
+ """
261
+ Get retry statistics for all servers.
262
+
263
+ Returns:
264
+ Dictionary mapping server IDs to their retry statistics
265
+ """
266
+ async with self._get_lock():
267
+ return {
268
+ server_id: RetryStats(
269
+ total_retries=stats.total_retries,
270
+ successful_retries=stats.successful_retries,
271
+ failed_retries=stats.failed_retries,
272
+ average_attempts=stats.average_attempts,
273
+ last_retry=stats.last_retry,
274
+ )
275
+ for server_id, stats in self._stats.items()
276
+ }
277
+
278
+ async def clear_stats(self, server_id: str) -> None:
279
+ """
280
+ Clear retry statistics for a server.
281
+
282
+ Args:
283
+ server_id: ID of the server
284
+ """
285
+ async with self._get_lock():
286
+ if server_id in self._stats:
287
+ del self._stats[server_id]
288
+
289
+ async def clear_all_stats(self) -> None:
290
+ """Clear retry statistics for all servers."""
291
+ async with self._get_lock():
292
+ self._stats.clear()
293
+
294
+
295
+ # Global retry manager instance
296
+ _retry_manager_lock = threading.Lock()
297
+ _retry_manager_instance: Optional[RetryManager] = None
298
+
299
+
300
+ def get_retry_manager() -> RetryManager:
301
+ """
302
+ Get the global retry manager instance (singleton pattern).
303
+
304
+ Returns:
305
+ The global RetryManager instance
306
+ """
307
+ global _retry_manager_instance
308
+ if _retry_manager_instance is None:
309
+ with _retry_manager_lock:
310
+ if _retry_manager_instance is None:
311
+ _retry_manager_instance = RetryManager()
312
+ return _retry_manager_instance
313
+
314
+
315
+ # Convenience function for common retry patterns
316
+ async def retry_mcp_call(
317
+ func: Callable,
318
+ server_id: str,
319
+ max_attempts: int = 3,
320
+ strategy: str = "exponential_jitter",
321
+ ) -> Any:
322
+ """
323
+ Convenience function for retrying MCP calls with sensible defaults.
324
+
325
+ Args:
326
+ func: The async function to execute
327
+ server_id: ID of the server for tracking
328
+ max_attempts: Maximum retry attempts
329
+ strategy: Backoff strategy
330
+
331
+ Returns:
332
+ The result of the function call
333
+ """
334
+ retry_manager = get_retry_manager()
335
+ return await retry_manager.retry_with_backoff(
336
+ func=func, max_attempts=max_attempts, strategy=strategy, server_id=server_id
337
+ )