ripperdoc 0.2.10__py3-none-any.whl → 0.3.0__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 +164 -57
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +3 -7
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +61 -5
- ripperdoc/cli/commands/resume_cmd.py +1 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +4 -4
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +13 -8
- ripperdoc/cli/ui/rich_ui.py +451 -32
- ripperdoc/cli/ui/spinner.py +68 -5
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +18 -11
- ripperdoc/core/agents.py +4 -0
- ripperdoc/core/config.py +235 -0
- ripperdoc/core/default_tools.py +1 -0
- ripperdoc/core/hooks/llm_callback.py +0 -1
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +82 -5
- ripperdoc/core/providers/openai.py +55 -9
- ripperdoc/core/query.py +349 -108
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +8 -3
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +49 -5
- ripperdoc/tools/bash_tool.py +75 -9
- ripperdoc/tools/file_edit_tool.py +98 -29
- ripperdoc/tools/file_read_tool.py +139 -8
- ripperdoc/tools/file_write_tool.py +46 -3
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +9 -15
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +33 -8
- ripperdoc/utils/file_watch.py +12 -6
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +9 -3
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +2 -2
- ripperdoc/utils/messages.py +177 -32
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +9 -3
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +1 -3
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +1 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +49 -17
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/RECORD +68 -61
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -408
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/core/query.py
CHANGED
|
@@ -48,6 +48,7 @@ from ripperdoc.utils.file_watch import (
|
|
|
48
48
|
ChangedFileNotice,
|
|
49
49
|
detect_changed_files,
|
|
50
50
|
)
|
|
51
|
+
from ripperdoc.utils.pending_messages import PendingMessageQueue
|
|
51
52
|
from ripperdoc.utils.log import get_logger
|
|
52
53
|
from ripperdoc.utils.messages import (
|
|
53
54
|
AssistantMessage,
|
|
@@ -67,6 +68,10 @@ logger = get_logger()
|
|
|
67
68
|
|
|
68
69
|
DEFAULT_REQUEST_TIMEOUT_SEC = float(os.getenv("RIPPERDOC_API_TIMEOUT", "120"))
|
|
69
70
|
MAX_LLM_RETRIES = int(os.getenv("RIPPERDOC_MAX_RETRIES", "10"))
|
|
71
|
+
# Timeout for individual tool execution (can be overridden per tool if needed)
|
|
72
|
+
DEFAULT_TOOL_TIMEOUT_SEC = float(os.getenv("RIPPERDOC_TOOL_TIMEOUT", "300")) # 5 minutes
|
|
73
|
+
# Timeout for concurrent tool execution (total for all tools)
|
|
74
|
+
DEFAULT_CONCURRENT_TOOL_TIMEOUT_SEC = float(os.getenv("RIPPERDOC_CONCURRENT_TOOL_TIMEOUT", "600")) # 10 minutes
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
def infer_thinking_mode(model_profile: ModelProfile) -> Optional[str]:
|
|
@@ -85,6 +90,9 @@ def infer_thinking_mode(model_profile: ModelProfile) -> Optional[str]:
|
|
|
85
90
|
# Use explicit config if set
|
|
86
91
|
explicit_mode = model_profile.thinking_mode
|
|
87
92
|
if explicit_mode:
|
|
93
|
+
# "none", "disabled", "off" means thinking is explicitly disabled
|
|
94
|
+
if explicit_mode.lower() in ("disabled", "off"):
|
|
95
|
+
return None
|
|
88
96
|
return explicit_mode
|
|
89
97
|
|
|
90
98
|
# Auto-detect based on API base and model name
|
|
@@ -145,8 +153,10 @@ async def _check_tool_permissions(
|
|
|
145
153
|
if isinstance(decision, PermissionResult):
|
|
146
154
|
return decision.result, decision.message, decision.updated_input
|
|
147
155
|
if isinstance(decision, dict) and "result" in decision:
|
|
148
|
-
return
|
|
149
|
-
"
|
|
156
|
+
return (
|
|
157
|
+
bool(decision.get("result")),
|
|
158
|
+
decision.get("message"),
|
|
159
|
+
decision.get("updated_input"),
|
|
150
160
|
)
|
|
151
161
|
if isinstance(decision, tuple) and len(decision) == 2:
|
|
152
162
|
return bool(decision[0]), decision[1], None
|
|
@@ -210,6 +220,11 @@ async def _run_tool_use_generator(
|
|
|
210
220
|
context: Dict[str, str],
|
|
211
221
|
) -> AsyncGenerator[Union[UserMessage, ProgressMessage], None]:
|
|
212
222
|
"""Execute a single tool_use and yield progress/results."""
|
|
223
|
+
logger.debug(
|
|
224
|
+
"[query] _run_tool_use_generator ENTER: tool='%s' tool_use_id=%s",
|
|
225
|
+
tool_name,
|
|
226
|
+
tool_use_id,
|
|
227
|
+
)
|
|
213
228
|
# Get tool input as dict for hooks
|
|
214
229
|
tool_input_dict = (
|
|
215
230
|
parsed_input.model_dump()
|
|
@@ -240,8 +255,14 @@ async def _run_tool_use_generator(
|
|
|
240
255
|
)
|
|
241
256
|
# Re-parse the input with the updated values
|
|
242
257
|
try:
|
|
243
|
-
|
|
244
|
-
|
|
258
|
+
# Ensure updated_input is a dict, not a Pydantic model
|
|
259
|
+
updated_input = pre_result.updated_input
|
|
260
|
+
if hasattr(updated_input, "model_dump"):
|
|
261
|
+
updated_input = updated_input.model_dump()
|
|
262
|
+
elif not isinstance(updated_input, dict):
|
|
263
|
+
updated_input = {"value": str(updated_input)}
|
|
264
|
+
parsed_input = tool.input_schema(**updated_input)
|
|
265
|
+
tool_input_dict = updated_input
|
|
245
266
|
except (ValueError, TypeError) as exc:
|
|
246
267
|
logger.warning(
|
|
247
268
|
f"[query] Failed to apply updated input from hook: {exc}",
|
|
@@ -261,26 +282,51 @@ async def _run_tool_use_generator(
|
|
|
261
282
|
tool_output = None
|
|
262
283
|
|
|
263
284
|
try:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
logger.debug("[query] _run_tool_use_generator: BEFORE tool.call() for '%s'", tool_name)
|
|
286
|
+
# Wrap tool execution with timeout to prevent hangs
|
|
287
|
+
try:
|
|
288
|
+
async with asyncio.timeout(DEFAULT_TOOL_TIMEOUT_SEC):
|
|
289
|
+
async for output in tool.call(parsed_input, tool_context):
|
|
290
|
+
logger.debug(
|
|
291
|
+
"[query] _run_tool_use_generator: tool='%s' yielded output type=%s",
|
|
292
|
+
tool_name,
|
|
293
|
+
type(output).__name__,
|
|
294
|
+
)
|
|
295
|
+
if isinstance(output, ToolProgress):
|
|
296
|
+
yield create_progress_message(
|
|
297
|
+
tool_use_id=tool_use_id,
|
|
298
|
+
sibling_tool_use_ids=sibling_ids,
|
|
299
|
+
content=output.content,
|
|
300
|
+
is_subagent_message=getattr(output, 'is_subagent_message', False),
|
|
301
|
+
)
|
|
302
|
+
logger.debug(
|
|
303
|
+
f"[query] Progress from tool_use_id={tool_use_id}: {output.content}"
|
|
304
|
+
)
|
|
305
|
+
elif isinstance(output, ToolResult):
|
|
306
|
+
tool_output = output.data
|
|
307
|
+
result_content = output.result_for_assistant or str(output.data)
|
|
308
|
+
result_msg = tool_result_message(
|
|
309
|
+
tool_use_id, result_content, tool_use_result=output.data
|
|
310
|
+
)
|
|
311
|
+
yield result_msg
|
|
312
|
+
logger.debug(
|
|
313
|
+
f"[query] Tool completed tool_use_id={tool_use_id} name={tool_name} "
|
|
314
|
+
f"result_len={len(result_content)}"
|
|
315
|
+
)
|
|
316
|
+
except asyncio.TimeoutError:
|
|
317
|
+
logger.error(
|
|
318
|
+
f"[query] Tool '{tool_name}' timed out after {DEFAULT_TOOL_TIMEOUT_SEC}s",
|
|
319
|
+
extra={"tool": tool_name, "tool_use_id": tool_use_id},
|
|
320
|
+
)
|
|
321
|
+
yield tool_result_message(
|
|
322
|
+
tool_use_id,
|
|
323
|
+
f"Tool '{tool_name}' timed out after {DEFAULT_TOOL_TIMEOUT_SEC:.0f} seconds",
|
|
324
|
+
is_error=True,
|
|
325
|
+
)
|
|
326
|
+
return # Exit early on timeout
|
|
327
|
+
logger.debug("[query] _run_tool_use_generator: AFTER tool.call() loop for '%s'", tool_name)
|
|
283
328
|
except CancelledError:
|
|
329
|
+
logger.debug("[query] _run_tool_use_generator: tool='%s' CANCELLED", tool_name)
|
|
284
330
|
raise # Don't suppress task cancellation
|
|
285
331
|
except (RuntimeError, ValueError, TypeError, OSError, IOError, AttributeError, KeyError) as exc:
|
|
286
332
|
logger.warning(
|
|
@@ -299,13 +345,15 @@ async def _run_tool_use_generator(
|
|
|
299
345
|
if post_result.additional_context:
|
|
300
346
|
_append_hook_context(context, f"PostToolUse:{tool_name}", post_result.additional_context)
|
|
301
347
|
if post_result.system_message:
|
|
302
|
-
_append_hook_context(
|
|
303
|
-
context, f"PostToolUse:{tool_name}:system", post_result.system_message
|
|
304
|
-
)
|
|
348
|
+
_append_hook_context(context, f"PostToolUse:{tool_name}:system", post_result.system_message)
|
|
305
349
|
if post_result.should_block:
|
|
306
350
|
reason = post_result.block_reason or post_result.stop_reason or "Blocked by hook."
|
|
307
351
|
yield create_user_message(f"PostToolUse hook blocked: {reason}")
|
|
308
352
|
|
|
353
|
+
logger.debug(
|
|
354
|
+
"[query] _run_tool_use_generator DONE: tool='%s' tool_use_id=%s", tool_name, tool_use_id
|
|
355
|
+
)
|
|
356
|
+
|
|
309
357
|
|
|
310
358
|
def _group_tool_calls_by_concurrency(prepared_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
311
359
|
"""Group consecutive tool calls by their concurrency safety."""
|
|
@@ -337,9 +385,18 @@ async def _execute_tools_in_parallel(
|
|
|
337
385
|
items: List[Dict[str, Any]], tool_results: List[UserMessage]
|
|
338
386
|
) -> AsyncGenerator[Union[UserMessage, ProgressMessage], None]:
|
|
339
387
|
"""Run tool generators concurrently."""
|
|
340
|
-
|
|
341
|
-
|
|
388
|
+
logger.debug("[query] _execute_tools_in_parallel ENTER: %d items", len(items))
|
|
389
|
+
valid_items = [call for call in items if call.get("generator")]
|
|
390
|
+
generators = [call["generator"] for call in valid_items]
|
|
391
|
+
tool_names = [call.get("tool_name", "unknown") for call in valid_items]
|
|
392
|
+
logger.debug(
|
|
393
|
+
"[query] _execute_tools_in_parallel: %d valid generators, tools=%s",
|
|
394
|
+
len(generators),
|
|
395
|
+
tool_names,
|
|
396
|
+
)
|
|
397
|
+
async for message in _run_concurrent_tool_uses(generators, tool_names, tool_results):
|
|
342
398
|
yield message
|
|
399
|
+
logger.debug("[query] _execute_tools_in_parallel DONE")
|
|
343
400
|
|
|
344
401
|
|
|
345
402
|
async def _run_tools_concurrently(
|
|
@@ -371,46 +428,164 @@ async def _run_tools_serially(
|
|
|
371
428
|
|
|
372
429
|
async def _run_concurrent_tool_uses(
|
|
373
430
|
generators: List[AsyncGenerator[Union[UserMessage, ProgressMessage], None]],
|
|
431
|
+
tool_names: List[str],
|
|
374
432
|
tool_results: List[UserMessage],
|
|
375
433
|
) -> AsyncGenerator[Union[UserMessage, ProgressMessage], None]:
|
|
376
|
-
"""Drain multiple tool generators concurrently and stream outputs."""
|
|
434
|
+
"""Drain multiple tool generators concurrently and stream outputs with overall timeout."""
|
|
435
|
+
logger.debug(
|
|
436
|
+
"[query] _run_concurrent_tool_uses ENTER: %d generators, tools=%s, timeout=%s",
|
|
437
|
+
len(generators),
|
|
438
|
+
tool_names,
|
|
439
|
+
DEFAULT_CONCURRENT_TOOL_TIMEOUT_SEC,
|
|
440
|
+
)
|
|
377
441
|
if not generators:
|
|
442
|
+
logger.debug("[query] _run_concurrent_tool_uses: no generators, returning")
|
|
378
443
|
return
|
|
379
|
-
yield # Make this a proper async generator that yields nothing
|
|
444
|
+
yield # Make this a proper async generator that yields nothing (unreachable but required)
|
|
380
445
|
|
|
381
446
|
queue: asyncio.Queue[Optional[Union[UserMessage, ProgressMessage]]] = asyncio.Queue()
|
|
382
447
|
|
|
383
|
-
async def _consume(
|
|
448
|
+
async def _consume(
|
|
449
|
+
gen: AsyncGenerator[Union[UserMessage, ProgressMessage], None],
|
|
450
|
+
gen_index: int,
|
|
451
|
+
tool_name: str,
|
|
452
|
+
) -> Optional[Exception]:
|
|
453
|
+
"""Consume a tool generator and return any exception that occurred."""
|
|
454
|
+
logger.debug(
|
|
455
|
+
"[query] _consume START: tool='%s' index=%d gen=%s",
|
|
456
|
+
tool_name,
|
|
457
|
+
gen_index,
|
|
458
|
+
type(gen).__name__,
|
|
459
|
+
)
|
|
460
|
+
captured_exception: Optional[Exception] = None
|
|
461
|
+
message_count = 0
|
|
384
462
|
try:
|
|
463
|
+
logger.debug("[query] _consume: entering async for loop for '%s'", tool_name)
|
|
385
464
|
async for message in gen:
|
|
465
|
+
message_count += 1
|
|
466
|
+
msg_type = type(message).__name__
|
|
467
|
+
logger.debug(
|
|
468
|
+
"[query] _consume: tool='%s' received message #%d type=%s",
|
|
469
|
+
tool_name,
|
|
470
|
+
message_count,
|
|
471
|
+
msg_type,
|
|
472
|
+
)
|
|
386
473
|
await queue.put(message)
|
|
474
|
+
logger.debug("[query] _consume: tool='%s' put message to queue", tool_name)
|
|
475
|
+
logger.debug(
|
|
476
|
+
"[query] _consume: tool='%s' async for loop finished, total messages=%d",
|
|
477
|
+
tool_name,
|
|
478
|
+
message_count,
|
|
479
|
+
)
|
|
387
480
|
except asyncio.CancelledError:
|
|
481
|
+
logger.debug("[query] _consume: tool='%s' was CANCELLED", tool_name)
|
|
388
482
|
raise # Don't suppress cancellation
|
|
389
483
|
except (StopAsyncIteration, GeneratorExit):
|
|
484
|
+
logger.debug("[query] _consume: tool='%s' StopAsyncIteration/GeneratorExit", tool_name)
|
|
390
485
|
pass # Normal generator termination
|
|
391
|
-
except
|
|
486
|
+
except Exception as exc:
|
|
487
|
+
# Capture exception for reporting to caller
|
|
488
|
+
captured_exception = exc
|
|
392
489
|
logger.warning(
|
|
393
|
-
"[query] Error while consuming tool
|
|
490
|
+
"[query] Error while consuming tool '%s' (task %d): %s: %s",
|
|
491
|
+
tool_name,
|
|
492
|
+
gen_index,
|
|
394
493
|
type(exc).__name__,
|
|
395
494
|
exc,
|
|
396
495
|
)
|
|
397
496
|
finally:
|
|
497
|
+
logger.debug("[query] _consume FINALLY: tool='%s' putting None to queue", tool_name)
|
|
398
498
|
await queue.put(None)
|
|
499
|
+
logger.debug("[query] _consume DONE: tool='%s' messages=%d", tool_name, message_count)
|
|
500
|
+
return captured_exception
|
|
399
501
|
|
|
400
|
-
|
|
502
|
+
logger.debug("[query] _run_concurrent_tool_uses: creating %d tasks", len(generators))
|
|
503
|
+
tasks = [
|
|
504
|
+
asyncio.create_task(_consume(gen, i, tool_names[i])) for i, gen in enumerate(generators)
|
|
505
|
+
]
|
|
401
506
|
active = len(tasks)
|
|
507
|
+
logger.debug("[query] _run_concurrent_tool_uses: %d tasks created, entering while loop", active)
|
|
402
508
|
|
|
403
509
|
try:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
510
|
+
# Add overall timeout for entire concurrent execution
|
|
511
|
+
async with asyncio.timeout(DEFAULT_CONCURRENT_TOOL_TIMEOUT_SEC):
|
|
512
|
+
while active:
|
|
513
|
+
logger.debug(
|
|
514
|
+
"[query] _run_concurrent_tool_uses: waiting for queue.get(), active=%d", active
|
|
515
|
+
)
|
|
516
|
+
try:
|
|
517
|
+
message = await asyncio.wait_for(
|
|
518
|
+
queue.get(), timeout=DEFAULT_CONCURRENT_TOOL_TIMEOUT_SEC
|
|
519
|
+
)
|
|
520
|
+
except asyncio.TimeoutError:
|
|
521
|
+
logger.error(
|
|
522
|
+
"[query] Concurrent tool execution timed out waiting for messages"
|
|
523
|
+
)
|
|
524
|
+
# Cancel all remaining tasks
|
|
525
|
+
for task in tasks:
|
|
526
|
+
if not task.done():
|
|
527
|
+
task.cancel()
|
|
528
|
+
raise
|
|
529
|
+
|
|
530
|
+
logger.debug(
|
|
531
|
+
"[query] _run_concurrent_tool_uses: got message type=%s, active=%d",
|
|
532
|
+
type(message).__name__ if message else "None",
|
|
533
|
+
active,
|
|
534
|
+
)
|
|
535
|
+
if message is None:
|
|
536
|
+
active -= 1
|
|
537
|
+
logger.debug(
|
|
538
|
+
"[query] _run_concurrent_tool_uses: None received, active now=%d", active
|
|
539
|
+
)
|
|
540
|
+
continue
|
|
541
|
+
if isinstance(message, UserMessage):
|
|
542
|
+
tool_results.append(message)
|
|
543
|
+
yield message
|
|
544
|
+
logger.debug("[query] _run_concurrent_tool_uses: while loop finished, all tools done")
|
|
545
|
+
except asyncio.TimeoutError:
|
|
546
|
+
logger.error(
|
|
547
|
+
f"[query] Concurrent tool execution timed out after {DEFAULT_CONCURRENT_TOOL_TIMEOUT_SEC}s",
|
|
548
|
+
extra={"tool_names": tool_names},
|
|
549
|
+
)
|
|
550
|
+
# Ensure all tasks are cancelled
|
|
551
|
+
for task in tasks:
|
|
552
|
+
if not task.done():
|
|
553
|
+
task.cancel()
|
|
554
|
+
raise
|
|
412
555
|
finally:
|
|
413
|
-
|
|
556
|
+
# Wait for all tasks and collect any exceptions
|
|
557
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
558
|
+
exceptions_found: List[tuple[int, str, BaseException]] = []
|
|
559
|
+
for i, result in enumerate(results):
|
|
560
|
+
if isinstance(result, asyncio.CancelledError):
|
|
561
|
+
continue
|
|
562
|
+
elif isinstance(result, Exception):
|
|
563
|
+
# Exception from gather itself (shouldn't happen with return_exceptions=True)
|
|
564
|
+
exceptions_found.append((i, tool_names[i], result))
|
|
565
|
+
elif result is not None:
|
|
566
|
+
# Exception returned by _consume
|
|
567
|
+
exceptions_found.append((i, tool_names[i], result))
|
|
568
|
+
|
|
569
|
+
# Log all exceptions for debugging
|
|
570
|
+
for i, name, exc in exceptions_found:
|
|
571
|
+
logger.warning(
|
|
572
|
+
"[query] Concurrent tool '%s' (task %d) failed: %s: %s",
|
|
573
|
+
name,
|
|
574
|
+
i,
|
|
575
|
+
type(exc).__name__,
|
|
576
|
+
exc,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Re-raise first exception if any occurred, so caller knows something failed
|
|
580
|
+
if exceptions_found:
|
|
581
|
+
first_name = exceptions_found[0][1]
|
|
582
|
+
first_exc = exceptions_found[0][2]
|
|
583
|
+
logger.error(
|
|
584
|
+
"[query] %d tool(s) failed during concurrent execution, first error in '%s': %s",
|
|
585
|
+
len(exceptions_found),
|
|
586
|
+
first_name,
|
|
587
|
+
first_exc,
|
|
588
|
+
)
|
|
414
589
|
|
|
415
590
|
|
|
416
591
|
class ToolRegistry:
|
|
@@ -483,6 +658,9 @@ class ToolRegistry:
|
|
|
483
658
|
"""Activate deferred tools by name."""
|
|
484
659
|
activated: List[str] = []
|
|
485
660
|
missing: List[str] = []
|
|
661
|
+
|
|
662
|
+
# First pass: collect tools to activate (no mutations)
|
|
663
|
+
to_activate: List[str] = []
|
|
486
664
|
for raw_name in names:
|
|
487
665
|
name = (raw_name or "").strip()
|
|
488
666
|
if not name:
|
|
@@ -491,12 +669,17 @@ class ToolRegistry:
|
|
|
491
669
|
continue
|
|
492
670
|
tool = self._tool_map.get(name)
|
|
493
671
|
if tool:
|
|
494
|
-
|
|
495
|
-
self._active_set.add(name)
|
|
496
|
-
self._deferred.discard(name)
|
|
497
|
-
activated.append(name)
|
|
672
|
+
to_activate.append(name)
|
|
498
673
|
else:
|
|
499
674
|
missing.append(name)
|
|
675
|
+
|
|
676
|
+
# Second pass: atomically update all data structures
|
|
677
|
+
if to_activate:
|
|
678
|
+
self._active.extend(to_activate)
|
|
679
|
+
self._active_set.update(to_activate)
|
|
680
|
+
self._deferred.difference_update(to_activate)
|
|
681
|
+
activated.extend(to_activate)
|
|
682
|
+
|
|
500
683
|
return activated, missing
|
|
501
684
|
|
|
502
685
|
def iter_named_tools(self) -> Iterable[tuple[str, Tool[Any, Any]]]:
|
|
@@ -560,14 +743,6 @@ def _apply_skill_context_updates(
|
|
|
560
743
|
class QueryContext:
|
|
561
744
|
"""Context for a query session."""
|
|
562
745
|
|
|
563
|
-
# Thresholds for memory warnings
|
|
564
|
-
MESSAGE_COUNT_WARNING_THRESHOLD = int(
|
|
565
|
-
os.getenv("RIPPERDOC_MESSAGE_WARNING_THRESHOLD", "500")
|
|
566
|
-
)
|
|
567
|
-
MESSAGE_COUNT_CRITICAL_THRESHOLD = int(
|
|
568
|
-
os.getenv("RIPPERDOC_MESSAGE_CRITICAL_THRESHOLD", "1000")
|
|
569
|
-
)
|
|
570
|
-
|
|
571
746
|
def __init__(
|
|
572
747
|
self,
|
|
573
748
|
tools: List[Tool[Any, Any]],
|
|
@@ -580,6 +755,9 @@ class QueryContext:
|
|
|
580
755
|
stop_hook: str = "stop",
|
|
581
756
|
file_cache_max_entries: int = 500,
|
|
582
757
|
file_cache_max_memory_mb: float = 50.0,
|
|
758
|
+
pending_message_queue: Optional[PendingMessageQueue] = None,
|
|
759
|
+
max_turns: Optional[int] = None,
|
|
760
|
+
permission_mode: str = "default",
|
|
583
761
|
) -> None:
|
|
584
762
|
self.tool_registry = ToolRegistry(tools)
|
|
585
763
|
self.max_thinking_tokens = max_thinking_tokens
|
|
@@ -587,6 +765,9 @@ class QueryContext:
|
|
|
587
765
|
self.model = model
|
|
588
766
|
self.verbose = verbose
|
|
589
767
|
self.abort_controller = asyncio.Event()
|
|
768
|
+
self.pending_message_queue: PendingMessageQueue = (
|
|
769
|
+
pending_message_queue if pending_message_queue is not None else PendingMessageQueue()
|
|
770
|
+
)
|
|
590
771
|
# Use BoundedFileCache instead of plain Dict to prevent unbounded growth
|
|
591
772
|
self.file_state_cache: BoundedFileCache = BoundedFileCache(
|
|
592
773
|
max_entries=file_cache_max_entries,
|
|
@@ -596,7 +777,8 @@ class QueryContext:
|
|
|
596
777
|
self.resume_ui = resume_ui
|
|
597
778
|
self.stop_hook = stop_hook
|
|
598
779
|
self.stop_hook_active = False
|
|
599
|
-
self.
|
|
780
|
+
self.max_turns = max_turns
|
|
781
|
+
self.permission_mode = permission_mode
|
|
600
782
|
|
|
601
783
|
@property
|
|
602
784
|
def tools(self) -> List[Tool[Any, Any]]:
|
|
@@ -616,36 +798,6 @@ class QueryContext:
|
|
|
616
798
|
"""Return all known tools (active + deferred)."""
|
|
617
799
|
return self.tool_registry.all_tools
|
|
618
800
|
|
|
619
|
-
def check_message_count(self, message_count: int) -> None:
|
|
620
|
-
"""Check message count and log warnings if thresholds are exceeded.
|
|
621
|
-
|
|
622
|
-
This helps detect potential memory issues in long sessions.
|
|
623
|
-
"""
|
|
624
|
-
if message_count >= self.MESSAGE_COUNT_CRITICAL_THRESHOLD:
|
|
625
|
-
if self._last_message_warning_count < self.MESSAGE_COUNT_CRITICAL_THRESHOLD:
|
|
626
|
-
logger.warning(
|
|
627
|
-
"[query] Critical: Message history is very large. "
|
|
628
|
-
"Consider compacting or starting a new session.",
|
|
629
|
-
extra={
|
|
630
|
-
"message_count": message_count,
|
|
631
|
-
"threshold": self.MESSAGE_COUNT_CRITICAL_THRESHOLD,
|
|
632
|
-
"file_cache_stats": self.file_state_cache.stats(),
|
|
633
|
-
},
|
|
634
|
-
)
|
|
635
|
-
self._last_message_warning_count = message_count
|
|
636
|
-
elif message_count >= self.MESSAGE_COUNT_WARNING_THRESHOLD:
|
|
637
|
-
# Only warn once per threshold crossing
|
|
638
|
-
if self._last_message_warning_count < self.MESSAGE_COUNT_WARNING_THRESHOLD:
|
|
639
|
-
logger.info(
|
|
640
|
-
"[query] Message history growing large; automatic compaction may trigger soon",
|
|
641
|
-
extra={
|
|
642
|
-
"message_count": message_count,
|
|
643
|
-
"threshold": self.MESSAGE_COUNT_WARNING_THRESHOLD,
|
|
644
|
-
"file_cache_stats": self.file_state_cache.stats(),
|
|
645
|
-
},
|
|
646
|
-
)
|
|
647
|
-
self._last_message_warning_count = message_count
|
|
648
|
-
|
|
649
801
|
def get_memory_stats(self) -> Dict[str, Any]:
|
|
650
802
|
"""Return memory usage statistics for monitoring."""
|
|
651
803
|
return {
|
|
@@ -654,6 +806,14 @@ class QueryContext:
|
|
|
654
806
|
"active_tool_count": len(self.tool_registry.active_tools),
|
|
655
807
|
}
|
|
656
808
|
|
|
809
|
+
def drain_pending_messages(self) -> List[UserMessage]:
|
|
810
|
+
"""Drain queued messages waiting to be injected into the conversation."""
|
|
811
|
+
return self.pending_message_queue.drain()
|
|
812
|
+
|
|
813
|
+
def enqueue_user_message(self, text: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
814
|
+
"""Queue a user-style message to inject once the current loop finishes."""
|
|
815
|
+
self.pending_message_queue.enqueue_text(text, metadata=metadata)
|
|
816
|
+
|
|
657
817
|
|
|
658
818
|
async def query_llm(
|
|
659
819
|
messages: List[Union[UserMessage, AssistantMessage, ProgressMessage]],
|
|
@@ -751,6 +911,7 @@ async def query_llm(
|
|
|
751
911
|
error_msg = create_assistant_message(
|
|
752
912
|
content=str(exc),
|
|
753
913
|
duration_ms=duration_ms,
|
|
914
|
+
model=model_profile.model,
|
|
754
915
|
)
|
|
755
916
|
error_msg.is_api_error_message = True
|
|
756
917
|
return error_msg
|
|
@@ -765,6 +926,7 @@ async def query_llm(
|
|
|
765
926
|
"Check your model configuration and provider dependencies."
|
|
766
927
|
),
|
|
767
928
|
duration_ms=duration_ms,
|
|
929
|
+
model=model_profile.model,
|
|
768
930
|
)
|
|
769
931
|
error_msg.is_api_error_message = True
|
|
770
932
|
return error_msg
|
|
@@ -805,6 +967,7 @@ async def query_llm(
|
|
|
805
967
|
content=provider_response.content_blocks,
|
|
806
968
|
duration_ms=provider_response.duration_ms,
|
|
807
969
|
metadata=metadata,
|
|
970
|
+
model=model_profile.model,
|
|
808
971
|
)
|
|
809
972
|
error_msg.is_api_error_message = True
|
|
810
973
|
return error_msg
|
|
@@ -862,7 +1025,10 @@ async def query_llm(
|
|
|
862
1025
|
)
|
|
863
1026
|
|
|
864
1027
|
error_msg = create_assistant_message(
|
|
865
|
-
content=content,
|
|
1028
|
+
content=content,
|
|
1029
|
+
duration_ms=duration_ms,
|
|
1030
|
+
metadata=error_metadata,
|
|
1031
|
+
model=model_profile.model,
|
|
866
1032
|
)
|
|
867
1033
|
error_msg.is_api_error_message = True
|
|
868
1034
|
return error_msg
|
|
@@ -912,7 +1078,7 @@ async def _run_query_iteration(
|
|
|
912
1078
|
Yields:
|
|
913
1079
|
Messages (progress, assistant, tool results) as they are generated
|
|
914
1080
|
"""
|
|
915
|
-
logger.
|
|
1081
|
+
logger.info(f"[query] Starting iteration {iteration}/{MAX_QUERY_ITERATIONS}")
|
|
916
1082
|
|
|
917
1083
|
# Check for file changes at the start of each iteration
|
|
918
1084
|
change_notices = detect_changed_files(query_context.file_state_cache)
|
|
@@ -942,15 +1108,19 @@ async def _run_query_iteration(
|
|
|
942
1108
|
if not chunk:
|
|
943
1109
|
return
|
|
944
1110
|
try:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
content=chunk,
|
|
950
|
-
)
|
|
1111
|
+
msg = create_progress_message(
|
|
1112
|
+
tool_use_id="stream",
|
|
1113
|
+
sibling_tool_use_ids=set(),
|
|
1114
|
+
content=chunk,
|
|
951
1115
|
)
|
|
952
|
-
|
|
953
|
-
|
|
1116
|
+
try:
|
|
1117
|
+
progress_queue.put_nowait(msg)
|
|
1118
|
+
except asyncio.QueueFull:
|
|
1119
|
+
# Queue full - wait with timeout instead of dropping immediately
|
|
1120
|
+
try:
|
|
1121
|
+
await asyncio.wait_for(progress_queue.put(msg), timeout=0.5)
|
|
1122
|
+
except asyncio.TimeoutError:
|
|
1123
|
+
logger.warning("[query] Progress queue full after timeout, dropping chunk")
|
|
954
1124
|
except (RuntimeError, ValueError) as exc:
|
|
955
1125
|
logger.warning("[query] Failed to enqueue stream progress chunk: %s", exc)
|
|
956
1126
|
|
|
@@ -969,6 +1139,8 @@ async def _run_query_iteration(
|
|
|
969
1139
|
)
|
|
970
1140
|
)
|
|
971
1141
|
|
|
1142
|
+
logger.debug("[query] Created query_llm task, waiting for response...")
|
|
1143
|
+
|
|
972
1144
|
assistant_message: Optional[AssistantMessage] = None
|
|
973
1145
|
|
|
974
1146
|
# Wait for LLM response while yielding progress
|
|
@@ -979,7 +1151,7 @@ async def _run_query_iteration(
|
|
|
979
1151
|
await assistant_task
|
|
980
1152
|
except CancelledError:
|
|
981
1153
|
pass
|
|
982
|
-
yield create_assistant_message(INTERRUPT_MESSAGE)
|
|
1154
|
+
yield create_assistant_message(INTERRUPT_MESSAGE, model=model_profile.model)
|
|
983
1155
|
result.should_stop = True
|
|
984
1156
|
return
|
|
985
1157
|
if assistant_task.done():
|
|
@@ -1024,7 +1196,7 @@ async def _run_query_iteration(
|
|
|
1024
1196
|
|
|
1025
1197
|
# Check for abort
|
|
1026
1198
|
if query_context.abort_controller.is_set():
|
|
1027
|
-
yield create_assistant_message(INTERRUPT_MESSAGE)
|
|
1199
|
+
yield create_assistant_message(INTERRUPT_MESSAGE, model=model_profile.model)
|
|
1028
1200
|
result.should_stop = True
|
|
1029
1201
|
return
|
|
1030
1202
|
|
|
@@ -1043,8 +1215,14 @@ async def _run_query_iteration(
|
|
|
1043
1215
|
)
|
|
1044
1216
|
|
|
1045
1217
|
if not tool_use_blocks:
|
|
1046
|
-
logger.debug(
|
|
1218
|
+
logger.debug(
|
|
1219
|
+
"[query] No tool_use blocks; running stop hook and returning response to user."
|
|
1220
|
+
)
|
|
1047
1221
|
stop_hook = query_context.stop_hook
|
|
1222
|
+
logger.debug(
|
|
1223
|
+
f"[query] stop_hook={stop_hook}, stop_hook_active={query_context.stop_hook_active}"
|
|
1224
|
+
)
|
|
1225
|
+
logger.debug("[query] BEFORE calling hook_manager.run_stop_async")
|
|
1048
1226
|
stop_result = (
|
|
1049
1227
|
await hook_manager.run_subagent_stop_async(
|
|
1050
1228
|
stop_hook_active=query_context.stop_hook_active
|
|
@@ -1052,10 +1230,14 @@ async def _run_query_iteration(
|
|
|
1052
1230
|
if stop_hook == "subagent"
|
|
1053
1231
|
else await hook_manager.run_stop_async(stop_hook_active=query_context.stop_hook_active)
|
|
1054
1232
|
)
|
|
1233
|
+
logger.debug("[query] AFTER calling hook_manager.run_stop_async")
|
|
1234
|
+
logger.debug("[query] Checking additional_context")
|
|
1055
1235
|
if stop_result.additional_context:
|
|
1056
1236
|
_append_hook_context(context, f"{stop_hook}:context", stop_result.additional_context)
|
|
1237
|
+
logger.debug("[query] Checking system_message")
|
|
1057
1238
|
if stop_result.system_message:
|
|
1058
1239
|
_append_hook_context(context, f"{stop_hook}:system", stop_result.system_message)
|
|
1240
|
+
logger.debug("[query] Checking should_block")
|
|
1059
1241
|
if stop_result.should_block:
|
|
1060
1242
|
reason = stop_result.block_reason or stop_result.stop_reason or "Blocked by hook."
|
|
1061
1243
|
result.tool_results = [create_user_message(f"{stop_hook} hook blocked: {reason}")]
|
|
@@ -1064,6 +1246,7 @@ async def _run_query_iteration(
|
|
|
1064
1246
|
query_context.stop_hook_active = True
|
|
1065
1247
|
result.should_stop = False
|
|
1066
1248
|
return
|
|
1249
|
+
logger.debug("[query] Setting should_stop=True and returning")
|
|
1067
1250
|
query_context.stop_hook_active = False
|
|
1068
1251
|
result.should_stop = True
|
|
1069
1252
|
return
|
|
@@ -1084,6 +1267,17 @@ async def _run_query_iteration(
|
|
|
1084
1267
|
tool_use_id = getattr(tool_use, "tool_use_id", None) or getattr(tool_use, "id", None) or ""
|
|
1085
1268
|
tool_input = getattr(tool_use, "input", {}) or {}
|
|
1086
1269
|
|
|
1270
|
+
# Handle case where input is a Pydantic model instead of a dict
|
|
1271
|
+
# This can happen when the API response contains structured tool input objects
|
|
1272
|
+
# Always try to convert if it has model_dump or dict methods
|
|
1273
|
+
if tool_input and hasattr(tool_input, "model_dump"):
|
|
1274
|
+
tool_input = tool_input.model_dump()
|
|
1275
|
+
elif tool_input and hasattr(tool_input, "dict") and callable(getattr(tool_input, "dict")):
|
|
1276
|
+
tool_input = tool_input.dict()
|
|
1277
|
+
elif tool_input and not isinstance(tool_input, dict):
|
|
1278
|
+
# Last resort: convert unknown type to string representation
|
|
1279
|
+
tool_input = {"value": str(tool_input)}
|
|
1280
|
+
|
|
1087
1281
|
tool, missing_msg = _resolve_tool(query_context.tool_registry, tool_name, tool_use_id)
|
|
1088
1282
|
if missing_msg:
|
|
1089
1283
|
logger.warning(f"[query] Tool '{tool_name}' not found for tool_use_id={tool_use_id}")
|
|
@@ -1101,6 +1295,7 @@ async def _run_query_iteration(
|
|
|
1101
1295
|
)
|
|
1102
1296
|
|
|
1103
1297
|
tool_context = ToolUseContext(
|
|
1298
|
+
message_id=tool_use_id, # Set message_id for parent_tool_use_id tracking
|
|
1104
1299
|
yolo_mode=query_context.yolo_mode,
|
|
1105
1300
|
verbose=query_context.verbose,
|
|
1106
1301
|
permission_checker=can_use_tool_fn,
|
|
@@ -1110,6 +1305,7 @@ async def _run_query_iteration(
|
|
|
1110
1305
|
abort_signal=query_context.abort_controller,
|
|
1111
1306
|
pause_ui=query_context.pause_ui,
|
|
1112
1307
|
resume_ui=query_context.resume_ui,
|
|
1308
|
+
pending_message_queue=query_context.pending_message_queue,
|
|
1113
1309
|
)
|
|
1114
1310
|
|
|
1115
1311
|
validation = await tool.validate_input(parsed_input, tool_context)
|
|
@@ -1142,7 +1338,13 @@ async def _run_query_iteration(
|
|
|
1142
1338
|
break
|
|
1143
1339
|
if updated_input:
|
|
1144
1340
|
try:
|
|
1145
|
-
|
|
1341
|
+
# Ensure updated_input is a dict, not a Pydantic model
|
|
1342
|
+
normalized_input = updated_input
|
|
1343
|
+
if hasattr(normalized_input, "model_dump"):
|
|
1344
|
+
normalized_input = normalized_input.model_dump()
|
|
1345
|
+
elif not isinstance(normalized_input, dict):
|
|
1346
|
+
normalized_input = {"value": str(normalized_input)}
|
|
1347
|
+
parsed_input = tool.input_schema(**normalized_input)
|
|
1146
1348
|
except ValidationError as ve:
|
|
1147
1349
|
detail_text = format_pydantic_errors(ve)
|
|
1148
1350
|
error_msg = tool_result_message(
|
|
@@ -1166,6 +1368,7 @@ async def _run_query_iteration(
|
|
|
1166
1368
|
|
|
1167
1369
|
prepared_calls.append(
|
|
1168
1370
|
{
|
|
1371
|
+
"tool_name": tool_name,
|
|
1169
1372
|
"is_concurrency_safe": tool.is_concurrency_safe(),
|
|
1170
1373
|
"generator": _run_tool_use_generator(
|
|
1171
1374
|
tool,
|
|
@@ -1229,7 +1432,7 @@ async def _run_query_iteration(
|
|
|
1229
1432
|
|
|
1230
1433
|
# Check for abort after tools
|
|
1231
1434
|
if query_context.abort_controller.is_set():
|
|
1232
|
-
yield create_assistant_message(INTERRUPT_MESSAGE_FOR_TOOL_USE)
|
|
1435
|
+
yield create_assistant_message(INTERRUPT_MESSAGE_FOR_TOOL_USE, model=model_profile.model)
|
|
1233
1436
|
result.tool_results = tool_results
|
|
1234
1437
|
result.should_stop = True
|
|
1235
1438
|
return
|
|
@@ -1253,6 +1456,26 @@ async def query(
|
|
|
1253
1456
|
3. Executes tools
|
|
1254
1457
|
4. Continues the conversation in a loop until no more tool calls
|
|
1255
1458
|
|
|
1459
|
+
Args:
|
|
1460
|
+
messages: Conversation history
|
|
1461
|
+
system_prompt: Base system prompt
|
|
1462
|
+
context: Additional context dictionary
|
|
1463
|
+
query_context: Query configuration
|
|
1464
|
+
can_use_tool_fn: Optional function to check tool permissions
|
|
1465
|
+
|
|
1466
|
+
Yields:
|
|
1467
|
+
Messages (user, assistant, progress) as they are generated
|
|
1468
|
+
"""
|
|
1469
|
+
# Resolve model once for use in messages (e.g., max iterations, errors)
|
|
1470
|
+
model_profile = resolve_model_profile(query_context.model)
|
|
1471
|
+
"""Execute a query with tool support.
|
|
1472
|
+
|
|
1473
|
+
This is the main query loop that:
|
|
1474
|
+
1. Sends messages to the AI
|
|
1475
|
+
2. Handles tool use responses
|
|
1476
|
+
3. Executes tools
|
|
1477
|
+
4. Continues the conversation in a loop until no more tool calls
|
|
1478
|
+
|
|
1256
1479
|
Args:
|
|
1257
1480
|
messages: Conversation history
|
|
1258
1481
|
system_prompt: Base system prompt
|
|
@@ -1270,16 +1493,22 @@ async def query(
|
|
|
1270
1493
|
"tool_count": len(query_context.tools),
|
|
1271
1494
|
"yolo_mode": query_context.yolo_mode,
|
|
1272
1495
|
"model_pointer": query_context.model,
|
|
1496
|
+
"max_turns": query_context.max_turns,
|
|
1497
|
+
"permission_mode": query_context.permission_mode,
|
|
1273
1498
|
},
|
|
1274
1499
|
)
|
|
1275
1500
|
# Work on a copy so external mutations (e.g., UI appending messages while consuming)
|
|
1276
1501
|
# do not interfere with the loop or normalization.
|
|
1277
1502
|
messages = list(messages)
|
|
1278
1503
|
|
|
1279
|
-
# Check initial message count for memory warnings
|
|
1280
|
-
query_context.check_message_count(len(messages))
|
|
1281
|
-
|
|
1282
1504
|
for iteration in range(1, MAX_QUERY_ITERATIONS + 1):
|
|
1505
|
+
# Inject any pending messages queued by background events or user interjections
|
|
1506
|
+
pending_messages = query_context.drain_pending_messages()
|
|
1507
|
+
if pending_messages:
|
|
1508
|
+
messages.extend(pending_messages)
|
|
1509
|
+
for pending in pending_messages:
|
|
1510
|
+
yield pending
|
|
1511
|
+
|
|
1283
1512
|
result = IterationResult()
|
|
1284
1513
|
|
|
1285
1514
|
async for msg in _run_query_iteration(
|
|
@@ -1294,6 +1523,20 @@ async def query(
|
|
|
1294
1523
|
yield msg
|
|
1295
1524
|
|
|
1296
1525
|
if result.should_stop:
|
|
1526
|
+
# Before stopping, check if new pending messages arrived during this iteration.
|
|
1527
|
+
trailing_pending = query_context.drain_pending_messages()
|
|
1528
|
+
if trailing_pending:
|
|
1529
|
+
# type: ignore[operator,list-item]
|
|
1530
|
+
next_messages = (
|
|
1531
|
+
messages + [result.assistant_message] + result.tool_results
|
|
1532
|
+
if result.assistant_message is not None
|
|
1533
|
+
else messages + result.tool_results # type: ignore[operator]
|
|
1534
|
+
) # type: ignore[operator]
|
|
1535
|
+
next_messages = next_messages + trailing_pending # type: ignore[operator,list-item]
|
|
1536
|
+
for pending in trailing_pending:
|
|
1537
|
+
yield pending
|
|
1538
|
+
messages = next_messages
|
|
1539
|
+
continue
|
|
1297
1540
|
return
|
|
1298
1541
|
|
|
1299
1542
|
# Update messages for next iteration
|
|
@@ -1302,9 +1545,6 @@ async def query(
|
|
|
1302
1545
|
else:
|
|
1303
1546
|
messages = messages + result.tool_results # type: ignore[operator]
|
|
1304
1547
|
|
|
1305
|
-
# Check message count after each iteration for memory warnings
|
|
1306
|
-
query_context.check_message_count(len(messages))
|
|
1307
|
-
|
|
1308
1548
|
logger.debug(
|
|
1309
1549
|
f"[query] Continuing loop with {len(messages)} messages after tools; "
|
|
1310
1550
|
f"tool_results_count={len(result.tool_results)}"
|
|
@@ -1316,5 +1556,6 @@ async def query(
|
|
|
1316
1556
|
)
|
|
1317
1557
|
yield create_assistant_message(
|
|
1318
1558
|
f"Reached maximum query iterations ({MAX_QUERY_ITERATIONS}). "
|
|
1319
|
-
"Please continue the conversation to proceed."
|
|
1559
|
+
"Please continue the conversation to proceed.",
|
|
1560
|
+
model=model_profile.model,
|
|
1320
1561
|
)
|