code-puppy 0.0.134__py3-none-any.whl → 0.0.136__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.
- code_puppy/agent.py +15 -17
- code_puppy/agents/agent_manager.py +320 -9
- code_puppy/agents/base_agent.py +58 -2
- code_puppy/agents/runtime_manager.py +68 -42
- code_puppy/command_line/command_handler.py +82 -33
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy/command_line/mcp/base.py +35 -0
- code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy/command_line/mcp/stop_command.py +79 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +259 -0
- code_puppy/command_line/model_picker_completion.py +21 -4
- code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- code_puppy/main.py +23 -17
- code_puppy/mcp/__init__.py +42 -16
- code_puppy/mcp/async_lifecycle.py +51 -49
- code_puppy/mcp/blocking_startup.py +125 -113
- code_puppy/mcp/captured_stdio_server.py +63 -70
- code_puppy/mcp/circuit_breaker.py +63 -47
- code_puppy/mcp/config_wizard.py +169 -136
- code_puppy/mcp/dashboard.py +79 -71
- code_puppy/mcp/error_isolation.py +147 -100
- code_puppy/mcp/examples/retry_example.py +55 -42
- code_puppy/mcp/health_monitor.py +152 -141
- code_puppy/mcp/managed_server.py +100 -97
- code_puppy/mcp/manager.py +168 -156
- code_puppy/mcp/registry.py +148 -110
- code_puppy/mcp/retry_manager.py +63 -61
- code_puppy/mcp/server_registry_catalog.py +271 -225
- code_puppy/mcp/status_tracker.py +80 -80
- code_puppy/mcp/system_tools.py +47 -52
- code_puppy/messaging/message_queue.py +20 -13
- code_puppy/messaging/renderers.py +30 -15
- code_puppy/state_management.py +103 -0
- code_puppy/tui/app.py +64 -7
- code_puppy/tui/components/chat_view.py +3 -3
- code_puppy/tui/components/human_input_modal.py +12 -8
- code_puppy/tui/screens/__init__.py +2 -2
- code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
code_puppy/mcp/retry_manager.py
CHANGED
|
@@ -8,10 +8,11 @@ communication with intelligent backoff strategies to prevent overwhelming failed
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
10
|
import random
|
|
11
|
-
from dataclasses import dataclass, field
|
|
12
|
-
from datetime import datetime, timedelta
|
|
13
|
-
from typing import Any, Callable, Dict, Optional
|
|
14
11
|
from collections import defaultdict
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any, Callable, Dict, Optional
|
|
15
|
+
|
|
15
16
|
import httpx
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -20,12 +21,13 @@ logger = logging.getLogger(__name__)
|
|
|
20
21
|
@dataclass
|
|
21
22
|
class RetryStats:
|
|
22
23
|
"""Statistics for retry operations per server."""
|
|
24
|
+
|
|
23
25
|
total_retries: int = 0
|
|
24
26
|
successful_retries: int = 0
|
|
25
27
|
failed_retries: int = 0
|
|
26
28
|
average_attempts: float = 0.0
|
|
27
29
|
last_retry: Optional[datetime] = None
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
def calculate_average(self, new_attempts: int) -> None:
|
|
30
32
|
"""Update the average attempts calculation."""
|
|
31
33
|
if self.total_retries == 0:
|
|
@@ -38,53 +40,53 @@ class RetryStats:
|
|
|
38
40
|
class RetryManager:
|
|
39
41
|
"""
|
|
40
42
|
Manages retry logic for MCP server operations with various backoff strategies.
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
Supports different backoff strategies and intelligent retry decisions based on
|
|
43
45
|
error types. Tracks retry statistics per server for monitoring.
|
|
44
46
|
"""
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
def __init__(self):
|
|
47
49
|
"""Initialize the retry manager."""
|
|
48
50
|
self._stats: Dict[str, RetryStats] = defaultdict(RetryStats)
|
|
49
51
|
self._lock = asyncio.Lock()
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
async def retry_with_backoff(
|
|
52
54
|
self,
|
|
53
55
|
func: Callable,
|
|
54
56
|
max_attempts: int = 3,
|
|
55
57
|
strategy: str = "exponential",
|
|
56
|
-
server_id: str = "unknown"
|
|
58
|
+
server_id: str = "unknown",
|
|
57
59
|
) -> Any:
|
|
58
60
|
"""
|
|
59
61
|
Execute a function with retry logic and backoff strategy.
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
Args:
|
|
62
64
|
func: The async function to execute
|
|
63
65
|
max_attempts: Maximum number of retry attempts
|
|
64
66
|
strategy: Backoff strategy ('fixed', 'linear', 'exponential', 'exponential_jitter')
|
|
65
67
|
server_id: ID of the server for tracking stats
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
Returns:
|
|
68
70
|
The result of the function call
|
|
69
|
-
|
|
71
|
+
|
|
70
72
|
Raises:
|
|
71
73
|
The last exception encountered if all retries fail
|
|
72
74
|
"""
|
|
73
75
|
last_exception = None
|
|
74
|
-
|
|
76
|
+
|
|
75
77
|
for attempt in range(max_attempts):
|
|
76
78
|
try:
|
|
77
79
|
result = await func()
|
|
78
|
-
|
|
80
|
+
|
|
79
81
|
# Record successful retry if this wasn't the first attempt
|
|
80
82
|
if attempt > 0:
|
|
81
83
|
await self.record_retry(server_id, attempt + 1, success=True)
|
|
82
|
-
|
|
84
|
+
|
|
83
85
|
return result
|
|
84
|
-
|
|
86
|
+
|
|
85
87
|
except Exception as e:
|
|
86
88
|
last_exception = e
|
|
87
|
-
|
|
89
|
+
|
|
88
90
|
# Check if this error is retryable
|
|
89
91
|
if not self.should_retry(e):
|
|
90
92
|
logger.info(
|
|
@@ -92,73 +94,73 @@ class RetryManager:
|
|
|
92
94
|
)
|
|
93
95
|
await self.record_retry(server_id, attempt + 1, success=False)
|
|
94
96
|
raise e
|
|
95
|
-
|
|
97
|
+
|
|
96
98
|
# If this is the last attempt, don't wait
|
|
97
99
|
if attempt == max_attempts - 1:
|
|
98
100
|
await self.record_retry(server_id, max_attempts, success=False)
|
|
99
101
|
break
|
|
100
|
-
|
|
102
|
+
|
|
101
103
|
# Calculate backoff delay
|
|
102
104
|
delay = self.calculate_backoff(attempt + 1, strategy)
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
logger.warning(
|
|
105
107
|
f"Attempt {attempt + 1}/{max_attempts} failed for server {server_id}: "
|
|
106
108
|
f"{type(e).__name__}: {e}. Retrying in {delay:.2f}s"
|
|
107
109
|
)
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
# Wait before retrying
|
|
110
112
|
await asyncio.sleep(delay)
|
|
111
|
-
|
|
113
|
+
|
|
112
114
|
# All attempts failed
|
|
113
115
|
logger.error(
|
|
114
116
|
f"All {max_attempts} attempts failed for server {server_id}. "
|
|
115
117
|
f"Last error: {type(last_exception).__name__}: {last_exception}"
|
|
116
118
|
)
|
|
117
119
|
raise last_exception
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
def calculate_backoff(self, attempt: int, strategy: str) -> float:
|
|
120
122
|
"""
|
|
121
123
|
Calculate backoff delay based on attempt number and strategy.
|
|
122
|
-
|
|
124
|
+
|
|
123
125
|
Args:
|
|
124
126
|
attempt: The current attempt number (1-based)
|
|
125
127
|
strategy: The backoff strategy to use
|
|
126
|
-
|
|
128
|
+
|
|
127
129
|
Returns:
|
|
128
130
|
Delay in seconds
|
|
129
131
|
"""
|
|
130
132
|
if strategy == "fixed":
|
|
131
133
|
return 1.0
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
elif strategy == "linear":
|
|
134
136
|
return float(attempt)
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
elif strategy == "exponential":
|
|
137
139
|
return 2.0 ** (attempt - 1)
|
|
138
|
-
|
|
140
|
+
|
|
139
141
|
elif strategy == "exponential_jitter":
|
|
140
142
|
base_delay = 2.0 ** (attempt - 1)
|
|
141
143
|
jitter = random.uniform(-0.25, 0.25) # ±25% jitter
|
|
142
144
|
return max(0.1, base_delay * (1 + jitter))
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
else:
|
|
145
147
|
logger.warning(f"Unknown backoff strategy: {strategy}, using exponential")
|
|
146
148
|
return 2.0 ** (attempt - 1)
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
def should_retry(self, error: Exception) -> bool:
|
|
149
151
|
"""
|
|
150
152
|
Determine if an error is retryable.
|
|
151
|
-
|
|
153
|
+
|
|
152
154
|
Args:
|
|
153
155
|
error: The exception to evaluate
|
|
154
|
-
|
|
156
|
+
|
|
155
157
|
Returns:
|
|
156
158
|
True if the error is retryable, False otherwise
|
|
157
159
|
"""
|
|
158
160
|
# Network timeouts and connection errors are retryable
|
|
159
161
|
if isinstance(error, (asyncio.TimeoutError, ConnectionError, OSError)):
|
|
160
162
|
return True
|
|
161
|
-
|
|
163
|
+
|
|
162
164
|
# HTTP errors
|
|
163
165
|
if isinstance(error, httpx.HTTPError):
|
|
164
166
|
if isinstance(error, httpx.TimeoutException):
|
|
@@ -167,7 +169,7 @@ class RetryManager:
|
|
|
167
169
|
return True
|
|
168
170
|
elif isinstance(error, httpx.ReadError):
|
|
169
171
|
return True
|
|
170
|
-
elif hasattr(error,
|
|
172
|
+
elif hasattr(error, "response") and error.response is not None:
|
|
171
173
|
status_code = error.response.status_code
|
|
172
174
|
# 5xx server errors are retryable
|
|
173
175
|
if 500 <= status_code < 600:
|
|
@@ -180,28 +182,31 @@ class RetryManager:
|
|
|
180
182
|
if status_code == 408:
|
|
181
183
|
return True
|
|
182
184
|
return False
|
|
183
|
-
|
|
185
|
+
|
|
184
186
|
# JSON decode errors might be transient
|
|
185
187
|
if isinstance(error, ValueError) and "json" in str(error).lower():
|
|
186
188
|
return True
|
|
187
|
-
|
|
189
|
+
|
|
188
190
|
# Authentication and authorization errors are not retryable
|
|
189
191
|
error_str = str(error).lower()
|
|
190
|
-
if any(
|
|
192
|
+
if any(
|
|
193
|
+
term in error_str
|
|
194
|
+
for term in ["unauthorized", "forbidden", "authentication", "permission"]
|
|
195
|
+
):
|
|
191
196
|
return False
|
|
192
|
-
|
|
197
|
+
|
|
193
198
|
# Schema validation errors are not retryable
|
|
194
199
|
if "schema" in error_str or "validation" in error_str:
|
|
195
200
|
return False
|
|
196
|
-
|
|
201
|
+
|
|
197
202
|
# By default, consider other errors as potentially retryable
|
|
198
203
|
# This is conservative but helps handle unknown transient issues
|
|
199
204
|
return True
|
|
200
|
-
|
|
205
|
+
|
|
201
206
|
async def record_retry(self, server_id: str, attempts: int, success: bool) -> None:
|
|
202
207
|
"""
|
|
203
208
|
Record retry statistics for a server.
|
|
204
|
-
|
|
209
|
+
|
|
205
210
|
Args:
|
|
206
211
|
server_id: ID of the server
|
|
207
212
|
attempts: Number of attempts made
|
|
@@ -211,21 +216,21 @@ class RetryManager:
|
|
|
211
216
|
stats = self._stats[server_id]
|
|
212
217
|
stats.total_retries += 1
|
|
213
218
|
stats.last_retry = datetime.now()
|
|
214
|
-
|
|
219
|
+
|
|
215
220
|
if success:
|
|
216
221
|
stats.successful_retries += 1
|
|
217
222
|
else:
|
|
218
223
|
stats.failed_retries += 1
|
|
219
|
-
|
|
224
|
+
|
|
220
225
|
stats.calculate_average(attempts)
|
|
221
|
-
|
|
226
|
+
|
|
222
227
|
async def get_retry_stats(self, server_id: str) -> RetryStats:
|
|
223
228
|
"""
|
|
224
229
|
Get retry statistics for a server.
|
|
225
|
-
|
|
230
|
+
|
|
226
231
|
Args:
|
|
227
232
|
server_id: ID of the server
|
|
228
|
-
|
|
233
|
+
|
|
229
234
|
Returns:
|
|
230
235
|
RetryStats object with current statistics
|
|
231
236
|
"""
|
|
@@ -237,13 +242,13 @@ class RetryManager:
|
|
|
237
242
|
successful_retries=stats.successful_retries,
|
|
238
243
|
failed_retries=stats.failed_retries,
|
|
239
244
|
average_attempts=stats.average_attempts,
|
|
240
|
-
last_retry=stats.last_retry
|
|
245
|
+
last_retry=stats.last_retry,
|
|
241
246
|
)
|
|
242
|
-
|
|
247
|
+
|
|
243
248
|
async def get_all_stats(self) -> Dict[str, RetryStats]:
|
|
244
249
|
"""
|
|
245
250
|
Get retry statistics for all servers.
|
|
246
|
-
|
|
251
|
+
|
|
247
252
|
Returns:
|
|
248
253
|
Dictionary mapping server IDs to their retry statistics
|
|
249
254
|
"""
|
|
@@ -254,22 +259,22 @@ class RetryManager:
|
|
|
254
259
|
successful_retries=stats.successful_retries,
|
|
255
260
|
failed_retries=stats.failed_retries,
|
|
256
261
|
average_attempts=stats.average_attempts,
|
|
257
|
-
last_retry=stats.last_retry
|
|
262
|
+
last_retry=stats.last_retry,
|
|
258
263
|
)
|
|
259
264
|
for server_id, stats in self._stats.items()
|
|
260
265
|
}
|
|
261
|
-
|
|
266
|
+
|
|
262
267
|
async def clear_stats(self, server_id: str) -> None:
|
|
263
268
|
"""
|
|
264
269
|
Clear retry statistics for a server.
|
|
265
|
-
|
|
270
|
+
|
|
266
271
|
Args:
|
|
267
272
|
server_id: ID of the server
|
|
268
273
|
"""
|
|
269
274
|
async with self._lock:
|
|
270
275
|
if server_id in self._stats:
|
|
271
276
|
del self._stats[server_id]
|
|
272
|
-
|
|
277
|
+
|
|
273
278
|
async def clear_all_stats(self) -> None:
|
|
274
279
|
"""Clear retry statistics for all servers."""
|
|
275
280
|
async with self._lock:
|
|
@@ -283,7 +288,7 @@ _retry_manager_instance: Optional[RetryManager] = None
|
|
|
283
288
|
def get_retry_manager() -> RetryManager:
|
|
284
289
|
"""
|
|
285
290
|
Get the global retry manager instance (singleton pattern).
|
|
286
|
-
|
|
291
|
+
|
|
287
292
|
Returns:
|
|
288
293
|
The global RetryManager instance
|
|
289
294
|
"""
|
|
@@ -298,24 +303,21 @@ async def retry_mcp_call(
|
|
|
298
303
|
func: Callable,
|
|
299
304
|
server_id: str,
|
|
300
305
|
max_attempts: int = 3,
|
|
301
|
-
strategy: str = "exponential_jitter"
|
|
306
|
+
strategy: str = "exponential_jitter",
|
|
302
307
|
) -> Any:
|
|
303
308
|
"""
|
|
304
309
|
Convenience function for retrying MCP calls with sensible defaults.
|
|
305
|
-
|
|
310
|
+
|
|
306
311
|
Args:
|
|
307
312
|
func: The async function to execute
|
|
308
313
|
server_id: ID of the server for tracking
|
|
309
314
|
max_attempts: Maximum retry attempts
|
|
310
315
|
strategy: Backoff strategy
|
|
311
|
-
|
|
316
|
+
|
|
312
317
|
Returns:
|
|
313
318
|
The result of the function call
|
|
314
319
|
"""
|
|
315
320
|
retry_manager = get_retry_manager()
|
|
316
321
|
return await retry_manager.retry_with_backoff(
|
|
317
|
-
func=func,
|
|
318
|
-
|
|
319
|
-
strategy=strategy,
|
|
320
|
-
server_id=server_id
|
|
321
|
-
)
|
|
322
|
+
func=func, max_attempts=max_attempts, strategy=strategy, server_id=server_id
|
|
323
|
+
)
|