regcode 0.6.2__tar.gz → 0.6.3__tar.gz
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.
- {regcode-0.6.2 → regcode-0.6.3}/PKG-INFO +1 -1
- {regcode-0.6.2 → regcode-0.6.3}/pyproject.toml +1 -1
- {regcode-0.6.2 → regcode-0.6.3}/regcode/__init__.py +1 -1
- {regcode-0.6.2 → regcode-0.6.3}/regcode/main.py +270 -253
- {regcode-0.6.2 → regcode-0.6.3}/uv.lock +1 -1
- {regcode-0.6.2 → regcode-0.6.3}/.gitignore +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/.gitlab-ci.yml +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/.python-version +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/.qwen/settings.json +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/AGENTS.md +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/GOALS.md +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/README.md +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/config.yaml.template +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/main.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/cli.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/config.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/conversation_manager.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/dependencies.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/monty_sandbox.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/permissions.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/sandbox.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/__init__.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/base.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/builtins.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/registry.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/review_notes.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/web_crawl.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tools/web_search.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tracing.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/regcode/tui.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/__init__.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/conftest.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_api.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_cli.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_config.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_context_manager.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_dependencies.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_main.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_osv_cve.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_provider_extra.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_review_notes.py +0 -0
- {regcode-0.6.2 → regcode-0.6.3}/tests/test_web_search.py +0 -0
|
@@ -263,7 +263,7 @@ class Agent:
|
|
|
263
263
|
self._track_response_cost(response)
|
|
264
264
|
|
|
265
265
|
choice = response.choices[0] # type: ignore
|
|
266
|
-
message_obj = choice.message
|
|
266
|
+
message_obj = choice.message # type: ignore
|
|
267
267
|
response_message = {
|
|
268
268
|
"role": "assistant",
|
|
269
269
|
"content": message_obj.content or "",
|
|
@@ -513,231 +513,270 @@ class Agent:
|
|
|
513
513
|
return "".join(parts)
|
|
514
514
|
|
|
515
515
|
def _stream(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
516
|
+
self,
|
|
517
|
+
messages: list[dict],
|
|
518
|
+
tool_definitions: list[dict[str, Any]] | None = None,
|
|
519
|
+
on_text_chunk: Callable[[str], None] | None = None,
|
|
520
|
+
max_tool_calls: int = 10,
|
|
521
|
+
) -> str:
|
|
522
|
+
"""Stream response with tool support.
|
|
523
|
+
|
|
524
|
+
Runs the same tool-call loop as chat() but streams the final
|
|
525
|
+
text response chunk-by-chunk. Tool calls are executed normally
|
|
526
|
+
and their results are communicated via the callbacks registered
|
|
527
|
+
on __init__ (status_callback / result_callback).
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
messages: The message list for the completion.
|
|
531
|
+
tool_definitions: Tool definitions for the LLM.
|
|
532
|
+
on_text_chunk: Optional callback invoked for each text chunk
|
|
533
|
+
from the LLM stream. Useful for the CLI to print
|
|
534
|
+
progressively.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
The complete final text response.
|
|
538
|
+
"""
|
|
539
|
+
completion_kwargs = self.config.get_litellm_completion_kwargs()
|
|
540
|
+
completion_kwargs["messages"] = messages
|
|
541
|
+
if tool_definitions:
|
|
542
|
+
completion_kwargs["tools"] = tool_definitions
|
|
543
|
+
completion_kwargs["tool_choice"] = "auto"
|
|
543
544
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
545
|
+
full_text = ""
|
|
546
|
+
final_text = ""
|
|
547
|
+
response_message: dict[str, Any] = {}
|
|
548
|
+
total_tool_calls = 0
|
|
548
549
|
|
|
549
|
-
|
|
550
|
-
# When budget is exhausted, rebuild system prompt without tools
|
|
551
|
-
if self._tool_budget_exhausted:
|
|
550
|
+
for _ in range(max_tool_calls):
|
|
552
551
|
system_prompt_text = self._build_system_prompt_no_tools()
|
|
553
552
|
messages = [
|
|
554
553
|
{"role": "system", "content": system_prompt_text},
|
|
555
554
|
*messages[1:],
|
|
556
555
|
]
|
|
557
556
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if current_tool_defs:
|
|
563
|
-
completion_kwargs["tools"] = current_tool_defs
|
|
564
|
-
completion_kwargs["tool_choice"] = "auto"
|
|
565
|
-
elif self._tool_budget_exhausted:
|
|
566
|
-
completion_kwargs["tool_choice"] = "none"
|
|
567
|
-
# Remove tools key if present
|
|
568
|
-
completion_kwargs.pop("tools", None)
|
|
569
|
-
|
|
570
|
-
completion_kwargs["stream"] = True
|
|
571
|
-
completion_kwargs["messages"] = messages
|
|
572
|
-
|
|
573
|
-
response = litellm.completion(**completion_kwargs)
|
|
574
|
-
|
|
575
|
-
# Track cost and tokens from this response
|
|
576
|
-
self._track_response_cost(response)
|
|
577
|
-
|
|
578
|
-
# Stream the response — accumulate text and tool_calls
|
|
579
|
-
# across ALL chunks, not just the last one.
|
|
580
|
-
full_text = ""
|
|
581
|
-
# Accumulate delta tool calls by index so we can merge partial
|
|
582
|
-
# chunks (id in first chunk, function name in second, arguments
|
|
583
|
-
# accumulating in subsequent chunks) into a single per-call object.
|
|
584
|
-
accumulated_by_index: dict[int, dict[str, Any]] = {}
|
|
585
|
-
try:
|
|
586
|
-
for chunk in response:
|
|
587
|
-
delta = chunk.choices[0].delta # type: ignore
|
|
588
|
-
# Accumulate text content
|
|
589
|
-
if delta.content:
|
|
590
|
-
full_text += delta.content
|
|
591
|
-
if on_text_chunk:
|
|
592
|
-
on_text_chunk(delta.content)
|
|
593
|
-
# Accumulate tool calls from any chunk
|
|
594
|
-
if delta.tool_calls:
|
|
595
|
-
for tc_delta in delta.tool_calls:
|
|
596
|
-
idx = tc_delta.index # type: ignore
|
|
597
|
-
if idx not in accumulated_by_index:
|
|
598
|
-
accumulated_by_index[idx] = {
|
|
599
|
-
"id": tc_delta.id or "",
|
|
600
|
-
"type": tc_delta.type or "function",
|
|
601
|
-
"function_name": "",
|
|
602
|
-
"function_arguments": "",
|
|
603
|
-
}
|
|
604
|
-
acc = accumulated_by_index[idx]
|
|
605
|
-
if tc_delta.id is not None:
|
|
606
|
-
acc["id"] = tc_delta.id
|
|
607
|
-
if tc_delta.function is not None:
|
|
608
|
-
if tc_delta.function.name:
|
|
609
|
-
acc["function_name"] = tc_delta.function.name
|
|
610
|
-
if tc_delta.function.arguments:
|
|
611
|
-
acc["function_arguments"] += (
|
|
612
|
-
tc_delta.function.arguments
|
|
613
|
-
)
|
|
614
|
-
except Exception:
|
|
615
|
-
# Litellm's streaming iterator can crash on malformed responses
|
|
616
|
-
# or sentinel markers like [DONE]. If iteration fails, just use
|
|
617
|
-
# whatever text we accumulated so far.
|
|
618
|
-
pass
|
|
619
|
-
|
|
620
|
-
# Reconstruct tool call dicts from accumulated chunks and validate
|
|
621
|
-
valid_tool_calls = []
|
|
622
|
-
for idx, acc in accumulated_by_index.items():
|
|
623
|
-
if not acc.get("id") or not acc.get("function_name"):
|
|
624
|
-
continue
|
|
625
|
-
valid_tool_calls.append({
|
|
626
|
-
"id": acc["id"],
|
|
627
|
-
"type": acc["type"],
|
|
628
|
-
"index": idx,
|
|
629
|
-
"function": {
|
|
630
|
-
"name": acc["function_name"],
|
|
631
|
-
"arguments": acc["function_arguments"],
|
|
632
|
-
},
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
response_message = {
|
|
636
|
-
"role": "assistant",
|
|
637
|
-
"content": full_text or "",
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
# Attach only valid accumulated tool calls
|
|
641
|
-
if valid_tool_calls:
|
|
642
|
-
response_message["tool_calls"] = valid_tool_calls
|
|
643
|
-
|
|
644
|
-
messages.append(response_message)
|
|
645
|
-
|
|
646
|
-
if full_text:
|
|
647
|
-
final_text = full_text
|
|
557
|
+
current_tool_defs = (
|
|
558
|
+
tool_definitions if not self._tool_budget_exhausted else None
|
|
559
|
+
)
|
|
648
560
|
|
|
649
|
-
|
|
650
|
-
|
|
561
|
+
if current_tool_defs:
|
|
562
|
+
completion_kwargs["tools"] = current_tool_defs
|
|
563
|
+
completion_kwargs["tool_choice"] = "auto"
|
|
564
|
+
elif self._tool_budget_exhausted:
|
|
565
|
+
completion_kwargs["tool_choice"] = "none"
|
|
566
|
+
# Remove tools key if present
|
|
567
|
+
completion_kwargs.pop("tools", None)
|
|
568
|
+
|
|
569
|
+
completion_kwargs["stream"] = True
|
|
570
|
+
completion_kwargs["messages"] = messages
|
|
571
|
+
|
|
572
|
+
response = litellm.completion(**completion_kwargs)
|
|
573
|
+
|
|
574
|
+
# Track cost and tokens from this response
|
|
575
|
+
self._track_response_cost(response)
|
|
576
|
+
|
|
577
|
+
# Stream the response — accumulate text and tool_calls
|
|
578
|
+
# across ALL chunks, not just the last one.
|
|
579
|
+
full_text = ""
|
|
580
|
+
# Accumulate delta tool calls by index so we can merge partial
|
|
581
|
+
# chunks (id in first chunk, function name in second, arguments
|
|
582
|
+
# accumulating in subsequent chunks) into a single per-call object.
|
|
583
|
+
accumulated_by_index: dict[int, dict[str, Any]] = {}
|
|
584
|
+
try:
|
|
585
|
+
for chunk in response:
|
|
586
|
+
delta = chunk.choices[0].delta # type: ignore
|
|
587
|
+
# Accumulate text content
|
|
588
|
+
if delta.content:
|
|
589
|
+
full_text += delta.content
|
|
590
|
+
if on_text_chunk:
|
|
591
|
+
on_text_chunk(delta.content)
|
|
592
|
+
# Accumulate tool calls from any chunk
|
|
593
|
+
if delta.tool_calls:
|
|
594
|
+
for tc_delta in delta.tool_calls:
|
|
595
|
+
idx = tc_delta.index # type: ignore
|
|
596
|
+
if idx not in accumulated_by_index:
|
|
597
|
+
accumulated_by_index[idx] = {
|
|
598
|
+
"id": tc_delta.id or "",
|
|
599
|
+
"type": tc_delta.type or "function",
|
|
600
|
+
"function_name": "",
|
|
601
|
+
"function_arguments": "",
|
|
602
|
+
}
|
|
603
|
+
acc = accumulated_by_index[idx]
|
|
604
|
+
if tc_delta.id is not None:
|
|
605
|
+
acc["id"] = tc_delta.id
|
|
606
|
+
if tc_delta.function is not None:
|
|
607
|
+
if tc_delta.function.name:
|
|
608
|
+
acc["function_name"] = tc_delta.function.name
|
|
609
|
+
if tc_delta.function.arguments:
|
|
610
|
+
acc["function_arguments"] += (
|
|
611
|
+
tc_delta.function.arguments
|
|
612
|
+
)
|
|
613
|
+
except Exception:
|
|
614
|
+
# Litellm's streaming iterator can crash on malformed responses
|
|
615
|
+
# or sentinel markers like [DONE]. If iteration fails, just use
|
|
616
|
+
# whatever text we accumulated so far.
|
|
617
|
+
pass
|
|
618
|
+
|
|
619
|
+
# Reconstruct tool call dicts from accumulated chunks and validate
|
|
620
|
+
valid_tool_calls = []
|
|
621
|
+
for idx, acc in accumulated_by_index.items():
|
|
622
|
+
if not acc.get("id") or not acc.get("function_name"):
|
|
623
|
+
continue
|
|
624
|
+
valid_tool_calls.append({
|
|
625
|
+
"id": acc["id"],
|
|
626
|
+
"type": acc["type"],
|
|
627
|
+
"index": idx,
|
|
628
|
+
"function": {
|
|
629
|
+
"name": acc["function_name"],
|
|
630
|
+
"arguments": acc["function_arguments"],
|
|
631
|
+
},
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
response_message = {
|
|
635
|
+
"role": "assistant",
|
|
636
|
+
"content": full_text or "",
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
# Attach only valid accumulated tool calls
|
|
640
|
+
if valid_tool_calls:
|
|
641
|
+
response_message["tool_calls"] = valid_tool_calls
|
|
642
|
+
|
|
643
|
+
messages.append(response_message)
|
|
644
|
+
|
|
645
|
+
if full_text:
|
|
646
|
+
final_text = full_text
|
|
647
|
+
|
|
648
|
+
if self._tool_budget_exhausted:
|
|
649
|
+
break
|
|
651
650
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
651
|
+
if valid_tool_calls:
|
|
652
|
+
for tc_dict in valid_tool_calls:
|
|
653
|
+
tool_name = str(tc_dict["function"]["name"])
|
|
654
|
+
tool_call_obj = ToolCall(tool_name, {})
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
tool_args = json.loads(tc_dict["function"]["arguments"])
|
|
658
|
+
except (json.JSONDecodeError, AttributeError):
|
|
659
|
+
tool_args = {}
|
|
660
|
+
if self._status_callback is not None:
|
|
661
|
+
self._status_callback(
|
|
662
|
+
AgentStatus.TOOL_CALL, tool_call_obj
|
|
663
|
+
)
|
|
664
|
+
if self._result_callback is not None:
|
|
665
|
+
self._result_callback(
|
|
666
|
+
AgentStatus.TOOL_RESULT,
|
|
667
|
+
(
|
|
668
|
+
f"Error: Failed to parse arguments for "
|
|
669
|
+
f"tool '{tool_name}'."
|
|
670
|
+
),
|
|
671
|
+
True,
|
|
672
|
+
tool_call_obj,
|
|
673
|
+
)
|
|
674
|
+
messages.append({
|
|
675
|
+
"role": "tool",
|
|
676
|
+
"tool_call_id": tc_dict["id"],
|
|
677
|
+
"name": tool_name,
|
|
678
|
+
"content": (
|
|
679
|
+
f"Error: Failed to parse arguments for "
|
|
680
|
+
f"tool '{tool_name}'."
|
|
681
|
+
),
|
|
682
|
+
})
|
|
683
|
+
total_tool_calls += 1
|
|
684
|
+
continue
|
|
685
|
+
|
|
686
|
+
if self._allowed_dir is not None:
|
|
687
|
+
for arg_name in ["path", "file_path"]:
|
|
688
|
+
if arg_name in tool_args:
|
|
689
|
+
arg_path = Path(tool_args[arg_name])
|
|
690
|
+
try:
|
|
691
|
+
if arg_path.is_absolute():
|
|
692
|
+
raise ValueError(
|
|
693
|
+
"Access denied: "
|
|
694
|
+
"absolute paths are not allowed"
|
|
695
|
+
)
|
|
696
|
+
if ".." in arg_path.parts:
|
|
697
|
+
raise ValueError(
|
|
698
|
+
"Access denied: "
|
|
699
|
+
"paths with '..' are not allowed"
|
|
700
|
+
)
|
|
701
|
+
if not arg_path.is_absolute():
|
|
702
|
+
arg_path = Path(
|
|
703
|
+
self._allowed_dir
|
|
704
|
+
) / arg_path
|
|
705
|
+
arg_path = arg_path.resolve()
|
|
706
|
+
if not arg_path.is_relative_to(
|
|
707
|
+
Path(self._allowed_dir).resolve()
|
|
708
|
+
):
|
|
709
|
+
raise ValueError(
|
|
710
|
+
(
|
|
711
|
+
f"Access denied: "
|
|
712
|
+
f"'{arg_path}' is outside "
|
|
713
|
+
f"allowed directory "
|
|
714
|
+
f"'{self._allowed_dir}'"
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
except ValueError as e:
|
|
718
|
+
messages.append(
|
|
719
|
+
{
|
|
720
|
+
"role": "tool",
|
|
721
|
+
"tool_call_id": tc_dict["id"],
|
|
722
|
+
"name": tool_name,
|
|
723
|
+
"content": str(e),
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
continue
|
|
656
727
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
728
|
+
tool_call_obj = ToolCall(tool_name, tool_args)
|
|
729
|
+
if self._status_callback is not None:
|
|
730
|
+
self._status_callback(
|
|
731
|
+
AgentStatus.TOOL_CALL, tool_call_obj
|
|
732
|
+
)
|
|
733
|
+
result = self.registry.execute(tool_name, **tool_args)
|
|
734
|
+
result_str = str(result)
|
|
661
735
|
if self._status_callback is not None:
|
|
662
|
-
self._status_callback(
|
|
736
|
+
self._status_callback(
|
|
737
|
+
AgentStatus.TOOL_RESULT, tool_call_obj
|
|
738
|
+
)
|
|
663
739
|
if self._result_callback is not None:
|
|
740
|
+
is_error = (
|
|
741
|
+
"Error" in result_str
|
|
742
|
+
or "Traceback" in result_str
|
|
743
|
+
)
|
|
664
744
|
self._result_callback(
|
|
665
745
|
AgentStatus.TOOL_RESULT,
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
f"tool '{tool_name}'."
|
|
669
|
-
),
|
|
670
|
-
True,
|
|
746
|
+
result_str,
|
|
747
|
+
is_error,
|
|
671
748
|
tool_call_obj,
|
|
672
749
|
)
|
|
673
|
-
messages.append({
|
|
674
|
-
"role": "tool",
|
|
675
|
-
"tool_call_id": tc_dict["id"],
|
|
676
|
-
"name": tool_name,
|
|
677
|
-
"content": (
|
|
678
|
-
f"Error: Failed to parse arguments for "
|
|
679
|
-
f"tool '{tool_name}'."
|
|
680
|
-
),
|
|
681
|
-
})
|
|
682
|
-
total_tool_calls += 1
|
|
683
|
-
continue
|
|
684
750
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
)
|
|
700
|
-
if not arg_path.is_absolute():
|
|
701
|
-
arg_path = Path(self._allowed_dir) / arg_path
|
|
702
|
-
arg_path = arg_path.resolve()
|
|
703
|
-
if not arg_path.is_relative_to(
|
|
704
|
-
Path(self._allowed_dir).resolve()
|
|
705
|
-
):
|
|
706
|
-
raise ValueError(
|
|
707
|
-
f"Access denied: '{arg_path}' is outside "
|
|
708
|
-
f"allowed directory '{self._allowed_dir}'"
|
|
709
|
-
)
|
|
710
|
-
except ValueError as e:
|
|
711
|
-
messages.append(
|
|
712
|
-
{
|
|
713
|
-
"role": "tool",
|
|
714
|
-
"tool_call_id": tc_dict["id"],
|
|
715
|
-
"name": tool_name,
|
|
716
|
-
"content": str(e),
|
|
717
|
-
}
|
|
718
|
-
)
|
|
719
|
-
continue
|
|
720
|
-
|
|
721
|
-
tool_call_obj = ToolCall(tool_name, tool_args)
|
|
722
|
-
if self._status_callback is not None:
|
|
723
|
-
self._status_callback(AgentStatus.TOOL_CALL, tool_call_obj)
|
|
724
|
-
result = self.registry.execute(tool_name, **tool_args)
|
|
725
|
-
result_str = str(result)
|
|
726
|
-
if self._status_callback is not None:
|
|
727
|
-
self._status_callback(AgentStatus.TOOL_RESULT, tool_call_obj)
|
|
728
|
-
if self._result_callback is not None:
|
|
729
|
-
is_error = "Error" in result_str or "Traceback" in result_str
|
|
730
|
-
self._result_callback(
|
|
731
|
-
AgentStatus.TOOL_RESULT, result_str, is_error, tool_call_obj
|
|
732
|
-
)
|
|
733
|
-
|
|
734
|
-
self._tool_budget_remaining -= 1
|
|
735
|
-
if self._tool_budget_remaining <= 0:
|
|
736
|
-
self._tool_budget_exhausted = True
|
|
737
|
-
if self._status_callback is not None:
|
|
738
|
-
self._status_callback(
|
|
739
|
-
AgentStatus.BUDGET_EXHAUSTED, tool_call_obj
|
|
751
|
+
self._tool_budget_remaining -= 1
|
|
752
|
+
if self._tool_budget_remaining <= 0:
|
|
753
|
+
self._tool_budget_exhausted = True
|
|
754
|
+
if self._status_callback is not None:
|
|
755
|
+
self._status_callback(
|
|
756
|
+
AgentStatus.BUDGET_EXHAUSTED, tool_call_obj
|
|
757
|
+
)
|
|
758
|
+
messages.append(
|
|
759
|
+
{
|
|
760
|
+
"role": "tool",
|
|
761
|
+
"tool_call_id": tc_dict["id"],
|
|
762
|
+
"name": tool_name,
|
|
763
|
+
"content": str(result),
|
|
764
|
+
}
|
|
740
765
|
)
|
|
766
|
+
messages.append(
|
|
767
|
+
{
|
|
768
|
+
"role": "user",
|
|
769
|
+
"content": (
|
|
770
|
+
"You have exhausted your tool budget. "
|
|
771
|
+
"Provide your final answer in plain text only. "
|
|
772
|
+
"Do not use any special formatting, tags, or "
|
|
773
|
+
"code markers. Just the answer."
|
|
774
|
+
),
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
total_tool_calls += 1
|
|
778
|
+
continue
|
|
779
|
+
|
|
741
780
|
messages.append(
|
|
742
781
|
{
|
|
743
782
|
"role": "tool",
|
|
@@ -746,61 +785,39 @@ class Agent:
|
|
|
746
785
|
"content": str(result),
|
|
747
786
|
}
|
|
748
787
|
)
|
|
749
|
-
messages.append(
|
|
750
|
-
{
|
|
751
|
-
"role": "user",
|
|
752
|
-
"content": (
|
|
753
|
-
"You have exhausted your tool budget. "
|
|
754
|
-
"Provide your final answer in plain text only. "
|
|
755
|
-
"Do not use any special formatting, tags, or "
|
|
756
|
-
"code markers. Just the answer."
|
|
757
|
-
),
|
|
758
|
-
}
|
|
759
|
-
)
|
|
760
788
|
total_tool_calls += 1
|
|
761
|
-
|
|
789
|
+
self._update_context_progress(messages)
|
|
790
|
+
if total_tool_calls >= 30:
|
|
791
|
+
break
|
|
792
|
+
continue
|
|
762
793
|
|
|
794
|
+
# Model returned neither text content nor tool calls — re-prompt it
|
|
795
|
+
# to force an answer with the current context state.
|
|
796
|
+
if not full_text and (
|
|
797
|
+
not response_message.get("tool_calls") or
|
|
798
|
+
len(response_message.get("tool_calls", [])) == 0
|
|
799
|
+
):
|
|
800
|
+
if total_tool_calls >= 5:
|
|
801
|
+
# After 5 empty responses, just give up and return empty
|
|
802
|
+
break
|
|
763
803
|
messages.append(
|
|
764
804
|
{
|
|
765
|
-
"role": "
|
|
766
|
-
"
|
|
767
|
-
|
|
768
|
-
|
|
805
|
+
"role": "user",
|
|
806
|
+
"content": (
|
|
807
|
+
"You returned an empty response with no tool calls. "
|
|
808
|
+
"Please provide your answer now based on the current "
|
|
809
|
+
"conversation state. Respond in plain text only."
|
|
810
|
+
),
|
|
769
811
|
}
|
|
770
812
|
)
|
|
771
813
|
total_tool_calls += 1
|
|
772
|
-
|
|
773
|
-
if total_tool_calls >= 30:
|
|
774
|
-
break
|
|
775
|
-
continue
|
|
776
|
-
|
|
777
|
-
# Model returned neither text content nor tool calls — re-prompt it
|
|
778
|
-
# to force an answer with the current context state.
|
|
779
|
-
if not full_text and (
|
|
780
|
-
not response_message.get("tool_calls") or
|
|
781
|
-
len(response_message.get("tool_calls", [])) == 0
|
|
782
|
-
):
|
|
783
|
-
if total_tool_calls >= 5:
|
|
784
|
-
# After 5 empty responses, just give up and return empty
|
|
785
|
-
break
|
|
786
|
-
messages.append(
|
|
787
|
-
{
|
|
788
|
-
"role": "user",
|
|
789
|
-
"content": (
|
|
790
|
-
"You returned an empty response with no tool calls. "
|
|
791
|
-
"Please provide your answer now based on the current "
|
|
792
|
-
"conversation state. Respond in plain text only."
|
|
793
|
-
),
|
|
794
|
-
}
|
|
795
|
-
)
|
|
796
|
-
total_tool_calls += 1
|
|
797
|
-
continue
|
|
814
|
+
continue
|
|
798
815
|
|
|
799
|
-
|
|
816
|
+
break
|
|
800
817
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
818
|
+
self._update_context_progress(messages)
|
|
819
|
+
self._history = [m for m in messages if m.get("role") != "system"]
|
|
820
|
+
return final_text or response_message.get("content", "") or ""
|
|
804
821
|
|
|
805
822
|
def _update_context_progress(self, messages: list[dict[str, Any]]) -> None:
|
|
806
823
|
"""Estimate token usage and report progress via context_callback."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|