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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/__main__.py +0 -5
- ripperdoc/cli/cli.py +37 -16
- ripperdoc/cli/commands/__init__.py +2 -0
- ripperdoc/cli/commands/agents_cmd.py +12 -9
- ripperdoc/cli/commands/compact_cmd.py +7 -3
- ripperdoc/cli/commands/context_cmd.py +35 -15
- ripperdoc/cli/commands/doctor_cmd.py +27 -14
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/mcp_cmd.py +13 -8
- ripperdoc/cli/commands/memory_cmd.py +5 -5
- ripperdoc/cli/commands/models_cmd.py +47 -16
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +1 -2
- ripperdoc/cli/commands/tasks_cmd.py +24 -13
- ripperdoc/cli/ui/rich_ui.py +523 -396
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/agents.py +172 -4
- ripperdoc/core/config.py +130 -6
- ripperdoc/core/default_tools.py +13 -2
- ripperdoc/core/permissions.py +20 -14
- ripperdoc/core/providers/__init__.py +31 -15
- ripperdoc/core/providers/anthropic.py +122 -8
- ripperdoc/core/providers/base.py +93 -15
- ripperdoc/core/providers/gemini.py +539 -96
- ripperdoc/core/providers/openai.py +371 -26
- ripperdoc/core/query.py +301 -62
- ripperdoc/core/query_utils.py +51 -7
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +79 -67
- ripperdoc/core/tool.py +15 -6
- ripperdoc/sdk/client.py +14 -1
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +82 -26
- ripperdoc/tools/bash_tool.py +356 -209
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +53 -10
- ripperdoc/tools/file_read_tool.py +17 -7
- ripperdoc/tools/file_write_tool.py +49 -13
- ripperdoc/tools/glob_tool.py +10 -9
- ripperdoc/tools/grep_tool.py +182 -51
- ripperdoc/tools/ls_tool.py +6 -6
- ripperdoc/tools/mcp_tools.py +172 -413
- ripperdoc/tools/multi_edit_tool.py +49 -9
- ripperdoc/tools/notebook_edit_tool.py +57 -13
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +91 -9
- ripperdoc/tools/todo_tool.py +12 -12
- ripperdoc/tools/tool_search_tool.py +5 -6
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/file_watch.py +5 -4
- ripperdoc/utils/json_utils.py +4 -4
- ripperdoc/utils/log.py +3 -3
- ripperdoc/utils/mcp.py +82 -22
- ripperdoc/utils/memory.py +9 -6
- ripperdoc/utils/message_compaction.py +19 -16
- ripperdoc/utils/messages.py +73 -8
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/permissions/__init__.py +7 -1
- ripperdoc/utils/permissions/path_validation_utils.py +5 -3
- ripperdoc/utils/permissions/shell_command_validation.py +496 -18
- ripperdoc/utils/prompt.py +1 -1
- ripperdoc/utils/safe_get_cwd.py +5 -2
- ripperdoc/utils/session_history.py +38 -19
- ripperdoc/utils/todo.py +6 -2
- ripperdoc/utils/token_estimation.py +34 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +14 -1
- ripperdoc-0.2.5.dist-info/RECORD +107 -0
- ripperdoc-0.2.3.dist-info/RECORD +0 -95
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/top_level.txt +0 -0
ripperdoc/core/providers/base.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
177
|
-
|
|
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
|
|
183
|
-
extra={
|
|
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")
|