ripperdoc 0.2.3__py3-none-any.whl → 0.2.5__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 (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/__main__.py +0 -5
  3. ripperdoc/cli/cli.py +37 -16
  4. ripperdoc/cli/commands/__init__.py +2 -0
  5. ripperdoc/cli/commands/agents_cmd.py +12 -9
  6. ripperdoc/cli/commands/compact_cmd.py +7 -3
  7. ripperdoc/cli/commands/context_cmd.py +35 -15
  8. ripperdoc/cli/commands/doctor_cmd.py +27 -14
  9. ripperdoc/cli/commands/exit_cmd.py +1 -1
  10. ripperdoc/cli/commands/mcp_cmd.py +13 -8
  11. ripperdoc/cli/commands/memory_cmd.py +5 -5
  12. ripperdoc/cli/commands/models_cmd.py +47 -16
  13. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  14. ripperdoc/cli/commands/resume_cmd.py +1 -2
  15. ripperdoc/cli/commands/tasks_cmd.py +24 -13
  16. ripperdoc/cli/ui/rich_ui.py +523 -396
  17. ripperdoc/cli/ui/tool_renderers.py +298 -0
  18. ripperdoc/core/agents.py +172 -4
  19. ripperdoc/core/config.py +130 -6
  20. ripperdoc/core/default_tools.py +13 -2
  21. ripperdoc/core/permissions.py +20 -14
  22. ripperdoc/core/providers/__init__.py +31 -15
  23. ripperdoc/core/providers/anthropic.py +122 -8
  24. ripperdoc/core/providers/base.py +93 -15
  25. ripperdoc/core/providers/gemini.py +539 -96
  26. ripperdoc/core/providers/openai.py +371 -26
  27. ripperdoc/core/query.py +301 -62
  28. ripperdoc/core/query_utils.py +51 -7
  29. ripperdoc/core/skills.py +295 -0
  30. ripperdoc/core/system_prompt.py +79 -67
  31. ripperdoc/core/tool.py +15 -6
  32. ripperdoc/sdk/client.py +14 -1
  33. ripperdoc/tools/ask_user_question_tool.py +431 -0
  34. ripperdoc/tools/background_shell.py +82 -26
  35. ripperdoc/tools/bash_tool.py +356 -209
  36. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  37. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  38. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  39. ripperdoc/tools/file_edit_tool.py +53 -10
  40. ripperdoc/tools/file_read_tool.py +17 -7
  41. ripperdoc/tools/file_write_tool.py +49 -13
  42. ripperdoc/tools/glob_tool.py +10 -9
  43. ripperdoc/tools/grep_tool.py +182 -51
  44. ripperdoc/tools/ls_tool.py +6 -6
  45. ripperdoc/tools/mcp_tools.py +172 -413
  46. ripperdoc/tools/multi_edit_tool.py +49 -9
  47. ripperdoc/tools/notebook_edit_tool.py +57 -13
  48. ripperdoc/tools/skill_tool.py +205 -0
  49. ripperdoc/tools/task_tool.py +91 -9
  50. ripperdoc/tools/todo_tool.py +12 -12
  51. ripperdoc/tools/tool_search_tool.py +5 -6
  52. ripperdoc/utils/coerce.py +34 -0
  53. ripperdoc/utils/context_length_errors.py +252 -0
  54. ripperdoc/utils/file_watch.py +5 -4
  55. ripperdoc/utils/json_utils.py +4 -4
  56. ripperdoc/utils/log.py +3 -3
  57. ripperdoc/utils/mcp.py +82 -22
  58. ripperdoc/utils/memory.py +9 -6
  59. ripperdoc/utils/message_compaction.py +19 -16
  60. ripperdoc/utils/messages.py +73 -8
  61. ripperdoc/utils/path_ignore.py +677 -0
  62. ripperdoc/utils/permissions/__init__.py +7 -1
  63. ripperdoc/utils/permissions/path_validation_utils.py +5 -3
  64. ripperdoc/utils/permissions/shell_command_validation.py +496 -18
  65. ripperdoc/utils/prompt.py +1 -1
  66. ripperdoc/utils/safe_get_cwd.py +5 -2
  67. ripperdoc/utils/session_history.py +38 -19
  68. ripperdoc/utils/todo.py +6 -2
  69. ripperdoc/utils/token_estimation.py +34 -0
  70. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +14 -1
  71. ripperdoc-0.2.5.dist-info/RECORD +107 -0
  72. ripperdoc-0.2.3.dist-info/RECORD +0 -95
  73. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
  74. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
  76. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import random
6
7
  from abc import ABC, abstractmethod
7
- from dataclasses import dataclass
8
- from typing import Any, Awaitable, Callable, Dict, List, Optional
8
+ from dataclasses import dataclass, field
9
+ from typing import (
10
+ Any,
11
+ AsyncIterable,
12
+ AsyncIterator,
13
+ Awaitable,
14
+ Callable,
15
+ Dict,
16
+ Iterable,
17
+ List,
18
+ Optional,
19
+ )
9
20
 
10
21
  from ripperdoc.core.config import ModelProfile
11
22
  from ripperdoc.core.tool import Tool
@@ -24,6 +35,29 @@ class ProviderResponse:
24
35
  usage_tokens: Dict[str, int]
25
36
  cost_usd: float
26
37
  duration_ms: float
38
+ metadata: Dict[str, Any] = field(default_factory=dict)
39
+ # Error handling fields
40
+ is_error: bool = False
41
+ error_code: Optional[str] = None # e.g., "permission_denied", "context_length_exceeded"
42
+ error_message: Optional[str] = None
43
+
44
+ @classmethod
45
+ def create_error(
46
+ cls,
47
+ error_code: str,
48
+ error_message: str,
49
+ duration_ms: float = 0.0,
50
+ ) -> "ProviderResponse":
51
+ """Create an error response with a text block containing the error message."""
52
+ return cls(
53
+ content_blocks=[{"type": "text", "text": f"[API Error] {error_message}"}],
54
+ usage_tokens={},
55
+ cost_usd=0.0,
56
+ duration_ms=duration_ms,
57
+ is_error=True,
58
+ error_code=error_code,
59
+ error_message=error_message,
60
+ )
27
61
 
28
62
 
29
63
  class ProviderClient(ABC):
@@ -42,6 +76,7 @@ class ProviderClient(ABC):
42
76
  progress_callback: Optional[ProgressCallback],
43
77
  request_timeout: Optional[float],
44
78
  max_retries: int,
79
+ max_thinking_tokens: int,
45
80
  ) -> ProviderResponse:
46
81
  """Execute a model call and return a normalized response."""
47
82
 
@@ -153,14 +188,54 @@ def sanitize_tool_history(normalized_messages: List[Dict[str, Any]]) -> List[Dic
153
188
  return sanitized
154
189
 
155
190
 
191
+ def _retry_delay_seconds(attempt: int, base_delay: float = 0.5, max_delay: float = 32.0) -> float:
192
+ """Calculate exponential backoff with jitter."""
193
+ capped_base: float = float(min(base_delay * (2 ** max(0, attempt - 1)), max_delay))
194
+ jitter: float = float(random.random() * 0.25 * capped_base)
195
+ return float(capped_base + jitter)
196
+
197
+
198
+ async def iter_with_timeout(
199
+ stream: Iterable[Any] | AsyncIterable[Any], timeout: Optional[float]
200
+ ) -> AsyncIterator[Any]:
201
+ """Yield items from an async or sync iterable, enforcing per-item timeout if provided."""
202
+ if timeout is None or timeout <= 0:
203
+ if hasattr(stream, "__aiter__"):
204
+ async for item in stream: # type: ignore[async-for]
205
+ yield item
206
+ else:
207
+ for item in stream:
208
+ yield item
209
+ return
210
+
211
+ if hasattr(stream, "__aiter__"):
212
+ aiter = stream.__aiter__() # type: ignore[attr-defined]
213
+ while True:
214
+ try:
215
+ yield await asyncio.wait_for(aiter.__anext__(), timeout=timeout) # type: ignore[attr-defined]
216
+ except StopAsyncIteration:
217
+ break
218
+ else:
219
+ iterator = iter(stream)
220
+ while True:
221
+ try:
222
+ next_item = await asyncio.wait_for(
223
+ asyncio.to_thread(next, iterator), timeout=timeout
224
+ )
225
+ except StopIteration:
226
+ break
227
+ yield next_item
228
+
229
+
156
230
  async def call_with_timeout_and_retries(
157
231
  coro_factory: Callable[[], Awaitable[Any]],
158
232
  request_timeout: Optional[float],
159
233
  max_retries: int,
160
234
  ) -> Any:
161
- """Run a coroutine with timeout and limited retries."""
235
+ """Run a coroutine with timeout and limited retries (exponential backoff)."""
162
236
  attempts = max(0, int(max_retries)) + 1
163
237
  last_error: Optional[Exception] = None
238
+
164
239
  for attempt in range(1, attempts + 1):
165
240
  try:
166
241
  if request_timeout and request_timeout > 0:
@@ -168,20 +243,23 @@ async def call_with_timeout_and_retries(
168
243
  return await coro_factory()
169
244
  except asyncio.TimeoutError as exc:
170
245
  last_error = exc
171
- logger.warning(
172
- "[provider_clients] Request timed out; retrying",
173
- extra={"attempt": attempt, "max_retries": attempts - 1},
174
- )
175
246
  if attempt == attempts:
176
- raise
177
- except Exception as exc:
178
- last_error = exc
179
- if attempt == attempts:
180
- raise
247
+ break
248
+ delay_seconds = _retry_delay_seconds(attempt)
181
249
  logger.warning(
182
- "[provider_clients] Request failed; retrying",
183
- extra={"attempt": attempt, "max_retries": attempts - 1, "error": str(exc)},
250
+ "[provider_clients] Request timed out; retrying",
251
+ extra={
252
+ "attempt": attempt,
253
+ "max_retries": attempts - 1,
254
+ "delay_seconds": round(delay_seconds, 3),
255
+ },
184
256
  )
257
+ await asyncio.sleep(delay_seconds)
258
+ except asyncio.CancelledError:
259
+ raise # Don't suppress task cancellation
260
+ except (RuntimeError, ValueError, TypeError, OSError, ConnectionError) as exc:
261
+ # Non-timeout errors are not retried; surface immediately.
262
+ raise exc
185
263
  if last_error:
186
- raise last_error
264
+ raise RuntimeError(f"Request timed out after {attempts} attempts") from last_error
187
265
  raise RuntimeError("Unexpected error executing request with retries")