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.
Files changed (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -97
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -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, 'response') and error.response is not None:
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(term in error_str for term in ["unauthorized", "forbidden", "authentication", "permission"]):
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
- max_attempts=max_attempts,
319
- strategy=strategy,
320
- server_id=server_id
321
- )
322
+ func=func, max_attempts=max_attempts, strategy=strategy, server_id=server_id
323
+ )