ripperdoc 0.2.7__py3-none-any.whl → 0.2.9__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/cli/cli.py +33 -115
- ripperdoc/cli/commands/__init__.py +70 -6
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +610 -0
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +57 -37
- ripperdoc/cli/commands/resume_cmd.py +6 -4
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +64 -8
- ripperdoc/cli/ui/interrupt_handler.py +3 -4
- ripperdoc/cli/ui/message_display.py +5 -3
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +196 -77
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +412 -0
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +301 -0
- ripperdoc/core/hooks/events.py +535 -0
- ripperdoc/core/hooks/executor.py +496 -0
- ripperdoc/core/hooks/integration.py +344 -0
- ripperdoc/core/hooks/manager.py +745 -0
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +548 -68
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +140 -39
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +30 -20
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +8 -4
- ripperdoc/tools/file_read_tool.py +9 -5
- ripperdoc/tools/file_write_tool.py +9 -5
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +11 -7
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +38 -12
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.7.dist-info/RECORD +0 -113
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import copy
|
|
7
7
|
import inspect
|
|
8
|
+
import json
|
|
8
9
|
import os
|
|
9
10
|
import time
|
|
10
11
|
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, cast
|
|
@@ -240,9 +241,7 @@ async def _async_build_tool_declarations(tools: List[Tool[Any, Any]]) -> List[Di
|
|
|
240
241
|
description=description,
|
|
241
242
|
parameters_json_schema=parameters_schema,
|
|
242
243
|
)
|
|
243
|
-
declarations.append(
|
|
244
|
-
func_decl.model_dump(mode="json", exclude_none=True)
|
|
245
|
-
)
|
|
244
|
+
declarations.append(func_decl.model_dump(mode="json", exclude_none=True))
|
|
246
245
|
else:
|
|
247
246
|
declarations.append(
|
|
248
247
|
{
|
|
@@ -385,6 +384,17 @@ class GeminiClient(ProviderClient):
|
|
|
385
384
|
) -> ProviderResponse:
|
|
386
385
|
start_time = time.time()
|
|
387
386
|
|
|
387
|
+
logger.debug(
|
|
388
|
+
"[gemini_client] Preparing request",
|
|
389
|
+
extra={
|
|
390
|
+
"model": model_profile.model,
|
|
391
|
+
"tool_mode": tool_mode,
|
|
392
|
+
"stream": stream,
|
|
393
|
+
"max_thinking_tokens": max_thinking_tokens,
|
|
394
|
+
"num_tools": len(tools),
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
388
398
|
try:
|
|
389
399
|
client = await self._client(model_profile)
|
|
390
400
|
except asyncio.CancelledError:
|
|
@@ -392,6 +402,15 @@ class GeminiClient(ProviderClient):
|
|
|
392
402
|
except Exception as exc:
|
|
393
403
|
duration_ms = (time.time() - start_time) * 1000
|
|
394
404
|
error_code, error_message = _classify_gemini_error(exc)
|
|
405
|
+
logger.debug(
|
|
406
|
+
"[gemini_client] Exception details during init",
|
|
407
|
+
extra={
|
|
408
|
+
"model": model_profile.model,
|
|
409
|
+
"exception_type": type(exc).__name__,
|
|
410
|
+
"exception_str": str(exc),
|
|
411
|
+
"error_code": error_code,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
395
414
|
logger.error(
|
|
396
415
|
"[gemini_client] Initialization failed",
|
|
397
416
|
extra={
|
|
@@ -422,7 +441,12 @@ class GeminiClient(ProviderClient):
|
|
|
422
441
|
from google.genai import types as genai_types # type: ignore
|
|
423
442
|
|
|
424
443
|
config["thinking_config"] = genai_types.ThinkingConfig(**thinking_config)
|
|
425
|
-
except (
|
|
444
|
+
except (
|
|
445
|
+
ImportError,
|
|
446
|
+
ModuleNotFoundError,
|
|
447
|
+
TypeError,
|
|
448
|
+
ValueError,
|
|
449
|
+
): # pragma: no cover - fallback when SDK not installed
|
|
426
450
|
config["thinking_config"] = thinking_config
|
|
427
451
|
if declarations:
|
|
428
452
|
config["tools"] = [{"function_declarations": declarations}]
|
|
@@ -432,6 +456,23 @@ class GeminiClient(ProviderClient):
|
|
|
432
456
|
"contents": contents,
|
|
433
457
|
"config": config,
|
|
434
458
|
}
|
|
459
|
+
|
|
460
|
+
logger.debug(
|
|
461
|
+
"[gemini_client] Request parameters",
|
|
462
|
+
extra={
|
|
463
|
+
"model": model_profile.model,
|
|
464
|
+
"config": json.dumps(
|
|
465
|
+
{k: v for k, v in config.items() if k != "system_instruction"},
|
|
466
|
+
ensure_ascii=False,
|
|
467
|
+
default=str,
|
|
468
|
+
)[:1000],
|
|
469
|
+
"num_declarations": len(declarations),
|
|
470
|
+
"thinking_config": json.dumps(thinking_config, ensure_ascii=False)
|
|
471
|
+
if thinking_config
|
|
472
|
+
else None,
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
435
476
|
usage_tokens: Dict[str, int] = {}
|
|
436
477
|
collected_text: List[str] = []
|
|
437
478
|
function_calls: List[Dict[str, Any]] = []
|
|
@@ -483,6 +524,10 @@ class GeminiClient(ProviderClient):
|
|
|
483
524
|
|
|
484
525
|
try:
|
|
485
526
|
if stream:
|
|
527
|
+
logger.debug(
|
|
528
|
+
"[gemini_client] Initiating stream request",
|
|
529
|
+
extra={"model": model_profile.model},
|
|
530
|
+
)
|
|
486
531
|
stream_resp = await _call_generate(streaming=True)
|
|
487
532
|
|
|
488
533
|
# Normalize streams into an async iterator to avoid StopIteration surfacing through
|
|
@@ -523,7 +568,8 @@ class GeminiClient(ProviderClient):
|
|
|
523
568
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
524
569
|
logger.warning(
|
|
525
570
|
"[gemini_client] Stream callback failed: %s: %s",
|
|
526
|
-
type(cb_exc).__name__,
|
|
571
|
+
type(cb_exc).__name__,
|
|
572
|
+
cb_exc,
|
|
527
573
|
)
|
|
528
574
|
if text_chunk:
|
|
529
575
|
collected_text.append(text_chunk)
|
|
@@ -552,6 +598,15 @@ class GeminiClient(ProviderClient):
|
|
|
552
598
|
except Exception as exc:
|
|
553
599
|
duration_ms = (time.time() - start_time) * 1000
|
|
554
600
|
error_code, error_message = _classify_gemini_error(exc)
|
|
601
|
+
logger.debug(
|
|
602
|
+
"[gemini_client] Exception details",
|
|
603
|
+
extra={
|
|
604
|
+
"model": model_profile.model,
|
|
605
|
+
"exception_type": type(exc).__name__,
|
|
606
|
+
"exception_str": str(exc),
|
|
607
|
+
"error_code": error_code,
|
|
608
|
+
},
|
|
609
|
+
)
|
|
555
610
|
logger.error(
|
|
556
611
|
"[gemini_client] API call failed",
|
|
557
612
|
extra={
|
|
@@ -595,6 +650,16 @@ class GeminiClient(ProviderClient):
|
|
|
595
650
|
**(usage_tokens or {}),
|
|
596
651
|
)
|
|
597
652
|
|
|
653
|
+
logger.debug(
|
|
654
|
+
"[gemini_client] Response content blocks",
|
|
655
|
+
extra={
|
|
656
|
+
"model": model_profile.model,
|
|
657
|
+
"content_blocks": json.dumps(content_blocks, ensure_ascii=False)[:1000],
|
|
658
|
+
"usage_tokens": json.dumps(usage_tokens, ensure_ascii=False),
|
|
659
|
+
"metadata": json.dumps(response_metadata, ensure_ascii=False)[:500],
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
598
663
|
logger.info(
|
|
599
664
|
"[gemini_client] Response received",
|
|
600
665
|
extra={
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import time
|
|
7
8
|
from typing import Any, Dict, List, Optional, cast
|
|
8
9
|
from uuid import uuid4
|
|
@@ -94,7 +95,7 @@ def _detect_openai_vendor(model_profile: ModelProfile) -> str:
|
|
|
94
95
|
if "generativelanguage.googleapis.com" in base or name.startswith("gemini"):
|
|
95
96
|
return "gemini_openai"
|
|
96
97
|
if "gpt-5" in name:
|
|
97
|
-
return "
|
|
98
|
+
return "openai"
|
|
98
99
|
return "openai"
|
|
99
100
|
|
|
100
101
|
|
|
@@ -130,7 +131,7 @@ def _build_thinking_kwargs(
|
|
|
130
131
|
if effort:
|
|
131
132
|
top_level["reasoning_effort"] = effort
|
|
132
133
|
extra_body.setdefault("reasoning", {"effort": effort})
|
|
133
|
-
elif vendor == "
|
|
134
|
+
elif vendor == "openai":
|
|
134
135
|
if effort:
|
|
135
136
|
extra_body["reasoning"] = {"effort": effort}
|
|
136
137
|
else:
|
|
@@ -178,6 +179,15 @@ class OpenAIClient(ProviderClient):
|
|
|
178
179
|
except Exception as exc:
|
|
179
180
|
duration_ms = (time.time() - start_time) * 1000
|
|
180
181
|
error_code, error_message = _classify_openai_error(exc)
|
|
182
|
+
logger.debug(
|
|
183
|
+
"[openai_client] Exception details",
|
|
184
|
+
extra={
|
|
185
|
+
"model": model_profile.model,
|
|
186
|
+
"exception_type": type(exc).__name__,
|
|
187
|
+
"exception_str": str(exc),
|
|
188
|
+
"error_code": error_code,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
181
191
|
logger.error(
|
|
182
192
|
"[openai_client] API call failed",
|
|
183
193
|
extra={
|
|
@@ -213,6 +223,18 @@ class OpenAIClient(ProviderClient):
|
|
|
213
223
|
openai_messages: List[Dict[str, object]] = [
|
|
214
224
|
{"role": "system", "content": system_prompt}
|
|
215
225
|
] + sanitize_tool_history(list(normalized_messages))
|
|
226
|
+
|
|
227
|
+
logger.debug(
|
|
228
|
+
"[openai_client] Preparing request",
|
|
229
|
+
extra={
|
|
230
|
+
"model": model_profile.model,
|
|
231
|
+
"tool_mode": tool_mode,
|
|
232
|
+
"stream": stream,
|
|
233
|
+
"max_thinking_tokens": max_thinking_tokens,
|
|
234
|
+
"num_tools": len(openai_tools),
|
|
235
|
+
"num_messages": len(openai_messages),
|
|
236
|
+
},
|
|
237
|
+
)
|
|
216
238
|
collected_text: List[str] = []
|
|
217
239
|
streamed_tool_calls: Dict[int, Dict[str, Optional[str]]] = {}
|
|
218
240
|
streamed_tool_text: List[str] = []
|
|
@@ -228,6 +250,16 @@ class OpenAIClient(ProviderClient):
|
|
|
228
250
|
model_profile, max_thinking_tokens
|
|
229
251
|
)
|
|
230
252
|
|
|
253
|
+
logger.debug(
|
|
254
|
+
"[openai_client] Request parameters",
|
|
255
|
+
extra={
|
|
256
|
+
"model": model_profile.model,
|
|
257
|
+
"thinking_extra_body": json.dumps(thinking_extra_body, ensure_ascii=False),
|
|
258
|
+
"thinking_top_level": json.dumps(thinking_top_level, ensure_ascii=False),
|
|
259
|
+
"messages_preview": json.dumps(openai_messages[:2], ensure_ascii=False)[:500],
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
|
|
231
263
|
async with AsyncOpenAI(
|
|
232
264
|
api_key=model_profile.api_key, base_url=model_profile.api_base
|
|
233
265
|
) as client:
|
|
@@ -246,6 +278,16 @@ class OpenAIClient(ProviderClient):
|
|
|
246
278
|
}
|
|
247
279
|
if thinking_extra_body:
|
|
248
280
|
stream_kwargs["extra_body"] = thinking_extra_body
|
|
281
|
+
logger.debug(
|
|
282
|
+
"[openai_client] Initiating stream request",
|
|
283
|
+
extra={
|
|
284
|
+
"model": model_profile.model,
|
|
285
|
+
"stream_kwargs": json.dumps(
|
|
286
|
+
{k: v for k, v in stream_kwargs.items() if k != "messages"},
|
|
287
|
+
ensure_ascii=False,
|
|
288
|
+
),
|
|
289
|
+
},
|
|
290
|
+
)
|
|
249
291
|
stream_coro = client.chat.completions.create( # type: ignore[call-overload]
|
|
250
292
|
**stream_kwargs
|
|
251
293
|
)
|
|
@@ -303,7 +345,8 @@ class OpenAIClient(ProviderClient):
|
|
|
303
345
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
304
346
|
logger.warning(
|
|
305
347
|
"[openai_client] Stream callback failed: %s: %s",
|
|
306
|
-
type(cb_exc).__name__,
|
|
348
|
+
type(cb_exc).__name__,
|
|
349
|
+
cb_exc,
|
|
307
350
|
)
|
|
308
351
|
|
|
309
352
|
# Tool call deltas for native tool mode
|
|
@@ -333,7 +376,8 @@ class OpenAIClient(ProviderClient):
|
|
|
333
376
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
334
377
|
logger.warning(
|
|
335
378
|
"[openai_client] Stream callback failed: %s: %s",
|
|
336
|
-
type(cb_exc).__name__,
|
|
379
|
+
type(cb_exc).__name__,
|
|
380
|
+
cb_exc,
|
|
337
381
|
)
|
|
338
382
|
|
|
339
383
|
if idx not in announced_tool_indexes and state.get("name"):
|
|
@@ -344,7 +388,8 @@ class OpenAIClient(ProviderClient):
|
|
|
344
388
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
345
389
|
logger.warning(
|
|
346
390
|
"[openai_client] Stream callback failed: %s: %s",
|
|
347
|
-
type(cb_exc).__name__,
|
|
391
|
+
type(cb_exc).__name__,
|
|
392
|
+
cb_exc,
|
|
348
393
|
)
|
|
349
394
|
|
|
350
395
|
streamed_tool_calls[idx] = state
|
|
@@ -467,6 +512,16 @@ class OpenAIClient(ProviderClient):
|
|
|
467
512
|
if stream_reasoning_details:
|
|
468
513
|
response_metadata["reasoning_details"] = stream_reasoning_details
|
|
469
514
|
|
|
515
|
+
logger.debug(
|
|
516
|
+
"[openai_client] Response content blocks",
|
|
517
|
+
extra={
|
|
518
|
+
"model": model_profile.model,
|
|
519
|
+
"content_blocks": json.dumps(content_blocks, ensure_ascii=False)[:1000],
|
|
520
|
+
"usage_tokens": json.dumps(usage_tokens, ensure_ascii=False),
|
|
521
|
+
"metadata": json.dumps(response_metadata, ensure_ascii=False)[:500],
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
|
|
470
525
|
logger.info(
|
|
471
526
|
"[openai_client] Response received",
|
|
472
527
|
extra={
|
ripperdoc/core/query.py
CHANGED
|
@@ -26,9 +26,10 @@ from typing import (
|
|
|
26
26
|
|
|
27
27
|
from pydantic import ValidationError
|
|
28
28
|
|
|
29
|
-
from ripperdoc.core.config import provider_protocol
|
|
29
|
+
from ripperdoc.core.config import ModelProfile, provider_protocol
|
|
30
30
|
from ripperdoc.core.providers import ProviderClient, get_provider_client
|
|
31
31
|
from ripperdoc.core.permissions import PermissionResult
|
|
32
|
+
from ripperdoc.core.hooks.manager import hook_manager
|
|
32
33
|
from ripperdoc.core.query_utils import (
|
|
33
34
|
build_full_system_prompt,
|
|
34
35
|
determine_tool_mode,
|
|
@@ -64,6 +65,42 @@ DEFAULT_REQUEST_TIMEOUT_SEC = float(os.getenv("RIPPERDOC_API_TIMEOUT", "120"))
|
|
|
64
65
|
MAX_LLM_RETRIES = int(os.getenv("RIPPERDOC_MAX_RETRIES", "10"))
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
def infer_thinking_mode(model_profile: ModelProfile) -> Optional[str]:
|
|
69
|
+
"""Infer thinking mode from ModelProfile if not explicitly configured.
|
|
70
|
+
|
|
71
|
+
This function checks the model_profile.thinking_mode first. If it's set,
|
|
72
|
+
returns that value. Otherwise, auto-detects based on api_base and model name.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
model_profile: The model profile to analyze
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Thinking mode string ("deepseek", "qwen", "openrouter", "gemini_openai")
|
|
79
|
+
or None if no thinking mode should be applied.
|
|
80
|
+
"""
|
|
81
|
+
# Use explicit config if set
|
|
82
|
+
explicit_mode = model_profile.thinking_mode
|
|
83
|
+
if explicit_mode:
|
|
84
|
+
return explicit_mode
|
|
85
|
+
|
|
86
|
+
# Auto-detect based on API base and model name
|
|
87
|
+
base = (model_profile.api_base or "").lower()
|
|
88
|
+
name = (model_profile.model or "").lower()
|
|
89
|
+
|
|
90
|
+
if "deepseek" in base or name.startswith("deepseek"):
|
|
91
|
+
return "deepseek"
|
|
92
|
+
if "dashscope" in base or "qwen" in name:
|
|
93
|
+
return "qwen"
|
|
94
|
+
if "openrouter.ai" in base:
|
|
95
|
+
return "openrouter"
|
|
96
|
+
if "generativelanguage.googleapis.com" in base or name.startswith("gemini"):
|
|
97
|
+
return "gemini_openai"
|
|
98
|
+
if "openai" in base:
|
|
99
|
+
return "openai"
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
67
104
|
def _resolve_tool(
|
|
68
105
|
tool_registry: "ToolRegistry", tool_name: str, tool_use_id: str
|
|
69
106
|
) -> tuple[Optional[Tool[Any, Any]], Optional[UserMessage]]:
|
|
@@ -109,7 +146,7 @@ async def _check_tool_permissions(
|
|
|
109
146
|
return bool(decision[0]), decision[1]
|
|
110
147
|
return bool(decision), None
|
|
111
148
|
|
|
112
|
-
if query_context.
|
|
149
|
+
if not query_context.yolo_mode and tool.needs_permissions(parsed_input):
|
|
113
150
|
loop = asyncio.get_running_loop()
|
|
114
151
|
input_preview = (
|
|
115
152
|
parsed_input.model_dump()
|
|
@@ -154,6 +191,53 @@ async def _run_tool_use_generator(
|
|
|
154
191
|
tool_context: ToolUseContext,
|
|
155
192
|
) -> AsyncGenerator[Union[UserMessage, ProgressMessage], None]:
|
|
156
193
|
"""Execute a single tool_use and yield progress/results."""
|
|
194
|
+
# Get tool input as dict for hooks
|
|
195
|
+
tool_input_dict = (
|
|
196
|
+
parsed_input.model_dump()
|
|
197
|
+
if hasattr(parsed_input, "model_dump")
|
|
198
|
+
else dict(parsed_input)
|
|
199
|
+
if isinstance(parsed_input, dict)
|
|
200
|
+
else {}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Run PreToolUse hooks
|
|
204
|
+
pre_result = await hook_manager.run_pre_tool_use_async(
|
|
205
|
+
tool_name, tool_input_dict, tool_use_id=tool_use_id
|
|
206
|
+
)
|
|
207
|
+
if pre_result.should_block:
|
|
208
|
+
block_reason = pre_result.block_reason or f"Blocked by hook: {tool_name}"
|
|
209
|
+
logger.info(
|
|
210
|
+
f"[query] Tool {tool_name} blocked by PreToolUse hook",
|
|
211
|
+
extra={"tool_use_id": tool_use_id, "reason": block_reason},
|
|
212
|
+
)
|
|
213
|
+
yield tool_result_message(tool_use_id, f"Hook blocked: {block_reason}", is_error=True)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Handle updated input from hooks
|
|
217
|
+
if pre_result.updated_input:
|
|
218
|
+
logger.debug(
|
|
219
|
+
f"[query] PreToolUse hook modified input for {tool_name}",
|
|
220
|
+
extra={"tool_use_id": tool_use_id},
|
|
221
|
+
)
|
|
222
|
+
# Re-parse the input with the updated values
|
|
223
|
+
try:
|
|
224
|
+
parsed_input = tool.input_schema(**pre_result.updated_input)
|
|
225
|
+
tool_input_dict = pre_result.updated_input
|
|
226
|
+
except (ValueError, TypeError) as exc:
|
|
227
|
+
logger.warning(
|
|
228
|
+
f"[query] Failed to apply updated input from hook: {exc}",
|
|
229
|
+
extra={"tool_use_id": tool_use_id},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Add hook context if provided
|
|
233
|
+
if pre_result.additional_context:
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"[query] PreToolUse hook added context for {tool_name}",
|
|
236
|
+
extra={"context": pre_result.additional_context[:100]},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
tool_output = None
|
|
240
|
+
|
|
157
241
|
try:
|
|
158
242
|
async for output in tool.call(parsed_input, tool_context):
|
|
159
243
|
if isinstance(output, ToolProgress):
|
|
@@ -164,6 +248,7 @@ async def _run_tool_use_generator(
|
|
|
164
248
|
)
|
|
165
249
|
logger.debug(f"[query] Progress from tool_use_id={tool_use_id}: {output.content}")
|
|
166
250
|
elif isinstance(output, ToolResult):
|
|
251
|
+
tool_output = output.data
|
|
167
252
|
result_content = output.result_for_assistant or str(output.data)
|
|
168
253
|
result_msg = tool_result_message(
|
|
169
254
|
tool_use_id, result_content, tool_use_result=output.data
|
|
@@ -178,11 +263,18 @@ async def _run_tool_use_generator(
|
|
|
178
263
|
except (RuntimeError, ValueError, TypeError, OSError, IOError, AttributeError, KeyError) as exc:
|
|
179
264
|
logger.warning(
|
|
180
265
|
"Error executing tool '%s': %s: %s",
|
|
181
|
-
tool_name,
|
|
266
|
+
tool_name,
|
|
267
|
+
type(exc).__name__,
|
|
268
|
+
exc,
|
|
182
269
|
extra={"tool": tool_name, "tool_use_id": tool_use_id},
|
|
183
270
|
)
|
|
184
271
|
yield tool_result_message(tool_use_id, f"Error executing tool: {str(exc)}", is_error=True)
|
|
185
272
|
|
|
273
|
+
# Run PostToolUse hooks
|
|
274
|
+
await hook_manager.run_post_tool_use_async(
|
|
275
|
+
tool_name, tool_input_dict, tool_response=tool_output, tool_use_id=tool_use_id
|
|
276
|
+
)
|
|
277
|
+
|
|
186
278
|
|
|
187
279
|
def _group_tool_calls_by_concurrency(prepared_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
188
280
|
"""Group consecutive tool calls by their concurrency safety."""
|
|
@@ -267,7 +359,8 @@ async def _run_concurrent_tool_uses(
|
|
|
267
359
|
except (RuntimeError, ValueError, TypeError) as exc:
|
|
268
360
|
logger.warning(
|
|
269
361
|
"[query] Error while consuming tool generator: %s: %s",
|
|
270
|
-
type(exc).__name__,
|
|
362
|
+
type(exc).__name__,
|
|
363
|
+
exc,
|
|
271
364
|
)
|
|
272
365
|
finally:
|
|
273
366
|
await queue.put(None)
|
|
@@ -320,7 +413,8 @@ class ToolRegistry:
|
|
|
320
413
|
except (TypeError, AttributeError) as exc:
|
|
321
414
|
logger.warning(
|
|
322
415
|
"[tool_registry] Tool.defer_loading failed: %s: %s",
|
|
323
|
-
type(exc).__name__,
|
|
416
|
+
type(exc).__name__,
|
|
417
|
+
exc,
|
|
324
418
|
extra={"tool": getattr(tool, "name", None)},
|
|
325
419
|
)
|
|
326
420
|
deferred = False
|
|
@@ -407,7 +501,8 @@ def _apply_skill_context_updates(
|
|
|
407
501
|
except (KeyError, ValueError, TypeError) as exc:
|
|
408
502
|
logger.warning(
|
|
409
503
|
"[query] Failed to activate tools listed in skill output: %s: %s",
|
|
410
|
-
type(exc).__name__,
|
|
504
|
+
type(exc).__name__,
|
|
505
|
+
exc,
|
|
411
506
|
)
|
|
412
507
|
|
|
413
508
|
model_hint = data.get("model")
|
|
@@ -437,7 +532,7 @@ class QueryContext:
|
|
|
437
532
|
self,
|
|
438
533
|
tools: List[Tool[Any, Any]],
|
|
439
534
|
max_thinking_tokens: int = 0,
|
|
440
|
-
|
|
535
|
+
yolo_mode: bool = False,
|
|
441
536
|
model: str = "main",
|
|
442
537
|
verbose: bool = False,
|
|
443
538
|
pause_ui: Optional[Callable[[], None]] = None,
|
|
@@ -445,7 +540,7 @@ class QueryContext:
|
|
|
445
540
|
) -> None:
|
|
446
541
|
self.tool_registry = ToolRegistry(tools)
|
|
447
542
|
self.max_thinking_tokens = max_thinking_tokens
|
|
448
|
-
self.
|
|
543
|
+
self.yolo_mode = yolo_mode
|
|
449
544
|
self.model = model
|
|
450
545
|
self.verbose = verbose
|
|
451
546
|
self.abort_controller = asyncio.Event()
|
|
@@ -518,8 +613,22 @@ async def query_llm(
|
|
|
518
613
|
else:
|
|
519
614
|
messages_for_model = messages
|
|
520
615
|
|
|
616
|
+
# Get thinking_mode for provider-specific handling
|
|
617
|
+
# Apply when thinking is enabled (max_thinking_tokens > 0) OR when using a
|
|
618
|
+
# reasoning model like deepseek-reasoner which has thinking enabled by default
|
|
619
|
+
thinking_mode: Optional[str] = None
|
|
620
|
+
if protocol == "openai":
|
|
621
|
+
model_name = (model_profile.model or "").lower()
|
|
622
|
+
# DeepSeek Reasoner models have thinking enabled by default
|
|
623
|
+
is_reasoning_model = "reasoner" in model_name or "r1" in model_name
|
|
624
|
+
if max_thinking_tokens > 0 or is_reasoning_model:
|
|
625
|
+
thinking_mode = infer_thinking_mode(model_profile)
|
|
626
|
+
|
|
521
627
|
normalized_messages: List[Dict[str, Any]] = normalize_messages_for_api(
|
|
522
|
-
messages_for_model,
|
|
628
|
+
messages_for_model,
|
|
629
|
+
protocol=protocol,
|
|
630
|
+
tool_mode=tool_mode,
|
|
631
|
+
thinking_mode=thinking_mode,
|
|
523
632
|
)
|
|
524
633
|
logger.info(
|
|
525
634
|
"[query_llm] Preparing model request",
|
|
@@ -530,6 +639,7 @@ async def query_llm(
|
|
|
530
639
|
"normalized_messages": len(normalized_messages),
|
|
531
640
|
"tool_count": len(tools),
|
|
532
641
|
"max_thinking_tokens": max_thinking_tokens,
|
|
642
|
+
"thinking_mode": thinking_mode,
|
|
533
643
|
"tool_mode": tool_mode,
|
|
534
644
|
},
|
|
535
645
|
)
|
|
@@ -613,7 +723,8 @@ async def query_llm(
|
|
|
613
723
|
# Return error message
|
|
614
724
|
logger.warning(
|
|
615
725
|
"Error querying AI model: %s: %s",
|
|
616
|
-
type(e).__name__,
|
|
726
|
+
type(e).__name__,
|
|
727
|
+
e,
|
|
617
728
|
extra={
|
|
618
729
|
"model": getattr(model_profile, "model", None),
|
|
619
730
|
"model_pointer": model,
|
|
@@ -624,12 +735,12 @@ async def query_llm(
|
|
|
624
735
|
)
|
|
625
736
|
duration_ms = (time.time() - start_time) * 1000
|
|
626
737
|
context_error = detect_context_length_error(e)
|
|
627
|
-
|
|
738
|
+
error_metadata: Optional[Dict[str, Any]] = None
|
|
628
739
|
content = f"Error querying AI model: {str(e)}"
|
|
629
740
|
|
|
630
741
|
if context_error:
|
|
631
742
|
content = f"The request exceeded the model's context window. {context_error.message}"
|
|
632
|
-
|
|
743
|
+
error_metadata = {
|
|
633
744
|
"context_length_exceeded": True,
|
|
634
745
|
"context_length_provider": context_error.provider,
|
|
635
746
|
"context_length_error_code": context_error.error_code,
|
|
@@ -645,7 +756,7 @@ async def query_llm(
|
|
|
645
756
|
)
|
|
646
757
|
|
|
647
758
|
error_msg = create_assistant_message(
|
|
648
|
-
content=content, duration_ms=duration_ms, metadata=
|
|
759
|
+
content=content, duration_ms=duration_ms, metadata=error_metadata
|
|
649
760
|
)
|
|
650
761
|
error_msg.is_api_error_message = True
|
|
651
762
|
return error_msg
|
|
@@ -704,9 +815,7 @@ async def _run_query_iteration(
|
|
|
704
815
|
|
|
705
816
|
model_profile = resolve_model_profile(query_context.model)
|
|
706
817
|
tool_mode = determine_tool_mode(model_profile)
|
|
707
|
-
tools_for_model: List[Tool[Any, Any]] = (
|
|
708
|
-
[] if tool_mode == "text" else query_context.all_tools()
|
|
709
|
-
)
|
|
818
|
+
tools_for_model: List[Tool[Any, Any]] = [] if tool_mode == "text" else query_context.all_tools()
|
|
710
819
|
|
|
711
820
|
full_system_prompt = build_full_system_prompt(
|
|
712
821
|
system_prompt, context, tool_mode, query_context.all_tools()
|
|
@@ -778,7 +887,7 @@ async def _run_query_iteration(
|
|
|
778
887
|
done, pending = await asyncio.wait(
|
|
779
888
|
{assistant_task, waiter},
|
|
780
889
|
return_when=asyncio.FIRST_COMPLETED,
|
|
781
|
-
timeout=0.1 # Check abort_controller every 100ms
|
|
890
|
+
timeout=0.1, # Check abort_controller every 100ms
|
|
782
891
|
)
|
|
783
892
|
if not done:
|
|
784
893
|
# Timeout - cancel waiter and continue loop to check abort_controller
|
|
@@ -836,8 +945,7 @@ async def _run_query_iteration(
|
|
|
836
945
|
tool_results: List[UserMessage] = []
|
|
837
946
|
permission_denied = False
|
|
838
947
|
sibling_ids = set(
|
|
839
|
-
getattr(t, "tool_use_id", None) or getattr(t, "id", None) or ""
|
|
840
|
-
for t in tool_use_blocks
|
|
948
|
+
getattr(t, "tool_use_id", None) or getattr(t, "id", None) or "" for t in tool_use_blocks
|
|
841
949
|
)
|
|
842
950
|
prepared_calls: List[Dict[str, Any]] = []
|
|
843
951
|
|
|
@@ -845,18 +953,12 @@ async def _run_query_iteration(
|
|
|
845
953
|
tool_name = tool_use.name
|
|
846
954
|
if not tool_name:
|
|
847
955
|
continue
|
|
848
|
-
tool_use_id = (
|
|
849
|
-
getattr(tool_use, "tool_use_id", None) or getattr(tool_use, "id", None) or ""
|
|
850
|
-
)
|
|
956
|
+
tool_use_id = getattr(tool_use, "tool_use_id", None) or getattr(tool_use, "id", None) or ""
|
|
851
957
|
tool_input = getattr(tool_use, "input", {}) or {}
|
|
852
958
|
|
|
853
|
-
tool, missing_msg = _resolve_tool(
|
|
854
|
-
query_context.tool_registry, tool_name, tool_use_id
|
|
855
|
-
)
|
|
959
|
+
tool, missing_msg = _resolve_tool(query_context.tool_registry, tool_name, tool_use_id)
|
|
856
960
|
if missing_msg:
|
|
857
|
-
logger.warning(
|
|
858
|
-
f"[query] Tool '{tool_name}' not found for tool_use_id={tool_use_id}"
|
|
859
|
-
)
|
|
961
|
+
logger.warning(f"[query] Tool '{tool_name}' not found for tool_use_id={tool_use_id}")
|
|
860
962
|
tool_results.append(missing_msg)
|
|
861
963
|
yield missing_msg
|
|
862
964
|
continue
|
|
@@ -870,7 +972,7 @@ async def _run_query_iteration(
|
|
|
870
972
|
)
|
|
871
973
|
|
|
872
974
|
tool_context = ToolUseContext(
|
|
873
|
-
|
|
975
|
+
yolo_mode=query_context.yolo_mode,
|
|
874
976
|
verbose=query_context.verbose,
|
|
875
977
|
permission_checker=can_use_tool_fn,
|
|
876
978
|
tool_registry=query_context.tool_registry,
|
|
@@ -883,8 +985,7 @@ async def _run_query_iteration(
|
|
|
883
985
|
validation = await tool.validate_input(parsed_input, tool_context)
|
|
884
986
|
if not validation.result:
|
|
885
987
|
logger.debug(
|
|
886
|
-
f"[query] Validation failed for tool_use_id={tool_use_id}: "
|
|
887
|
-
f"{validation.message}"
|
|
988
|
+
f"[query] Validation failed for tool_use_id={tool_use_id}: {validation.message}"
|
|
888
989
|
)
|
|
889
990
|
result_msg = tool_result_message(
|
|
890
991
|
tool_use_id,
|
|
@@ -895,18 +996,15 @@ async def _run_query_iteration(
|
|
|
895
996
|
yield result_msg
|
|
896
997
|
continue
|
|
897
998
|
|
|
898
|
-
if query_context.
|
|
999
|
+
if not query_context.yolo_mode or can_use_tool_fn is not None:
|
|
899
1000
|
allowed, denial_message = await _check_tool_permissions(
|
|
900
1001
|
tool, parsed_input, query_context, can_use_tool_fn
|
|
901
1002
|
)
|
|
902
1003
|
if not allowed:
|
|
903
1004
|
logger.debug(
|
|
904
|
-
f"[query] Permission denied for tool_use_id={tool_use_id}: "
|
|
905
|
-
f"{denial_message}"
|
|
906
|
-
)
|
|
907
|
-
denial_text = (
|
|
908
|
-
denial_message or f"User aborted the tool invocation: {tool_name}"
|
|
1005
|
+
f"[query] Permission denied for tool_use_id={tool_use_id}: {denial_message}"
|
|
909
1006
|
)
|
|
1007
|
+
denial_text = denial_message or f"User aborted the tool invocation: {tool_name}"
|
|
910
1008
|
denial_msg = tool_result_message(tool_use_id, denial_text, is_error=True)
|
|
911
1009
|
tool_results.append(denial_msg)
|
|
912
1010
|
yield denial_msg
|
|
@@ -1016,7 +1114,7 @@ async def query(
|
|
|
1016
1114
|
extra={
|
|
1017
1115
|
"message_count": len(messages),
|
|
1018
1116
|
"tool_count": len(query_context.tools),
|
|
1019
|
-
"
|
|
1117
|
+
"yolo_mode": query_context.yolo_mode,
|
|
1020
1118
|
"model_pointer": query_context.model,
|
|
1021
1119
|
},
|
|
1022
1120
|
)
|
|
@@ -1042,7 +1140,10 @@ async def query(
|
|
|
1042
1140
|
return
|
|
1043
1141
|
|
|
1044
1142
|
# Update messages for next iteration
|
|
1045
|
-
|
|
1143
|
+
if result.assistant_message is not None:
|
|
1144
|
+
messages = messages + [result.assistant_message] + result.tool_results # type: ignore[operator]
|
|
1145
|
+
else:
|
|
1146
|
+
messages = messages + result.tool_results # type: ignore[operator]
|
|
1046
1147
|
logger.debug(
|
|
1047
1148
|
f"[query] Continuing loop with {len(messages)} messages after tools; "
|
|
1048
1149
|
f"tool_results_count={len(result.tool_results)}"
|
ripperdoc/core/query_utils.py
CHANGED
|
@@ -462,11 +462,13 @@ def log_openai_messages(normalized_messages: List[Dict[str, Any]]) -> None:
|
|
|
462
462
|
role = message.get("role")
|
|
463
463
|
tool_calls = message.get("tool_calls")
|
|
464
464
|
tool_call_id = message.get("tool_call_id")
|
|
465
|
+
has_reasoning = "reasoning_content" in message and message.get("reasoning_content")
|
|
465
466
|
ids = [tc.get("id") for tc in tool_calls] if tool_calls else []
|
|
466
467
|
summary_parts.append(
|
|
467
468
|
f"{idx}:{role}"
|
|
468
469
|
+ (f" tool_calls={ids}" if ids else "")
|
|
469
470
|
+ (f" tool_call_id={tool_call_id}" if tool_call_id else "")
|
|
471
|
+
+ (" +reasoning" if has_reasoning else "")
|
|
470
472
|
)
|
|
471
473
|
logger.debug(f"[query_llm] OpenAI normalized messages: {' | '.join(summary_parts)}")
|
|
472
474
|
|