regcode 0.1.0__tar.gz → 0.2.0__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.1.0 → regcode-0.2.0}/.gitignore +2 -1
- {regcode-0.1.0 → regcode-0.2.0}/PKG-INFO +2 -1
- {regcode-0.1.0 → regcode-0.2.0}/pyproject.toml +2 -1
- {regcode-0.1.0 → regcode-0.2.0}/regcode/cli.py +8 -35
- {regcode-0.1.0 → regcode-0.2.0}/regcode/main.py +63 -3
- {regcode-0.1.0 → regcode-0.2.0}/regcode/tools/__init__.py +2 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/tools/builtins.py +35 -34
- regcode-0.2.0/regcode/tui.py +660 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_main.py +316 -0
- {regcode-0.1.0 → regcode-0.2.0}/uv.lock +67 -1
- regcode-0.1.0/.env +0 -1
- regcode-0.1.0/regcode/tui.py +0 -331
- {regcode-0.1.0 → regcode-0.2.0}/.python-version +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/AGENTS.md +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/GOALS.md +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/README.md +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/config.yaml.template +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/main.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/__init__.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/config.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/conversation_manager.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/monty_sandbox.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/permissions.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/sandbox.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/tools/base.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/tools/registry.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/regcode/tools/review_notes.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/__init__.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/conftest.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_api.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_cli.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_config.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_context_manager.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_provider_extra.py +0 -0
- {regcode-0.1.0 → regcode-0.2.0}/tests/test_review_notes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Minimalistic coding agent with Python API and CLI
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: click>=8.0
|
|
@@ -10,6 +10,7 @@ Requires-Dist: pydantic-settings>=2.14.2
|
|
|
10
10
|
Requires-Dist: pytest>=8.0
|
|
11
11
|
Requires-Dist: pyyaml>=6.0
|
|
12
12
|
Requires-Dist: ruff>=0.8
|
|
13
|
+
Requires-Dist: textual>=8.2.7
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regcode"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Minimalistic coding agent with Python API and CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
"pytest>=8.0",
|
|
13
13
|
"ruff>=0.8",
|
|
14
14
|
"pydantic-monty>=0.0.18",
|
|
15
|
+
"textual>=8.2.7",
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
[build-system]
|
|
@@ -41,14 +41,14 @@ def cli(ctx):
|
|
|
41
41
|
help="Disable colored output",
|
|
42
42
|
)
|
|
43
43
|
def chat(model, config_path, stream, full, no_color):
|
|
44
|
-
"""Chat with the agent."""
|
|
44
|
+
"""Chat with the agent using a Textual TUI with sticky bottom bar."""
|
|
45
45
|
config = regcode.Config.load(config_path)
|
|
46
46
|
if model:
|
|
47
47
|
config.provider.model = model
|
|
48
48
|
if full:
|
|
49
49
|
config.permissions.append(AgentPermission.WRITE)
|
|
50
50
|
|
|
51
|
-
# Initialize the TUI
|
|
51
|
+
# Initialize the TUI (Textual-based)
|
|
52
52
|
force_color = not no_color and sys.stdout.isatty()
|
|
53
53
|
ui = ChatUI(force_color=force_color)
|
|
54
54
|
|
|
@@ -56,41 +56,14 @@ def chat(model, config_path, stream, full, no_color):
|
|
|
56
56
|
config=config,
|
|
57
57
|
status_callback=ui.handle_status,
|
|
58
58
|
result_callback=ui.handle_result,
|
|
59
|
+
context_callback=ui.set_context_progress,
|
|
59
60
|
)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if msg is None:
|
|
67
|
-
break # EOF
|
|
68
|
-
if msg.lower() in ("exit", "quit", "q"):
|
|
69
|
-
ui.print_exit()
|
|
70
|
-
break
|
|
71
|
-
if not msg:
|
|
72
|
-
continue
|
|
73
|
-
|
|
74
|
-
# Ensure agent response starts on a fresh line
|
|
75
|
-
click.echo()
|
|
76
|
-
|
|
77
|
-
# Always stream in TUI so text appears progressively rather than
|
|
78
|
-
# all at once after the full tool loop completes.
|
|
79
|
-
ui.start_streaming()
|
|
80
|
-
agent.chat(msg, stream=True, on_text_chunk=ui.print_text_chunk)
|
|
81
|
-
ui.end_streaming()
|
|
82
|
-
|
|
83
|
-
ui.print_round_complete()
|
|
84
|
-
|
|
85
|
-
except KeyboardInterrupt:
|
|
86
|
-
ui.print_interrupted()
|
|
87
|
-
break
|
|
88
|
-
except EOFError:
|
|
89
|
-
ui.print_exit()
|
|
90
|
-
break
|
|
91
|
-
except Exception as e:
|
|
92
|
-
ui.formatter.error(str(e))
|
|
93
|
-
break
|
|
62
|
+
# Attach agent to the Textual app
|
|
63
|
+
ui._app.run_agent(agent)
|
|
64
|
+
|
|
65
|
+
# Run the Textual app (replaces the old click-based chat loop)
|
|
66
|
+
ui.run()
|
|
94
67
|
|
|
95
68
|
|
|
96
69
|
@cli.command()
|
|
@@ -62,6 +62,7 @@ class Agent:
|
|
|
62
62
|
| None
|
|
63
63
|
)
|
|
64
64
|
| None = None,
|
|
65
|
+
context_callback: Callable[[int, int], None] | None = None,
|
|
65
66
|
) -> None:
|
|
66
67
|
self.config = config or Config.load()
|
|
67
68
|
self.system_prompt = self.config.system_prompt
|
|
@@ -69,6 +70,7 @@ class Agent:
|
|
|
69
70
|
self.registry = ToolRegistry()
|
|
70
71
|
self._status_callback = status_callback
|
|
71
72
|
self._result_callback = result_callback
|
|
73
|
+
self._context_callback = context_callback
|
|
72
74
|
self._allowed_dir = directory
|
|
73
75
|
self._tool_budget = self.config.tool_budget
|
|
74
76
|
self._tool_budget_remaining = self._tool_budget
|
|
@@ -76,6 +78,7 @@ class Agent:
|
|
|
76
78
|
|
|
77
79
|
# Context window manager
|
|
78
80
|
self.context_manager = ConversationManager()
|
|
81
|
+
self._context_window = self.config.agent.context_window
|
|
79
82
|
|
|
80
83
|
# Review note tools - only register when compaction is enabled
|
|
81
84
|
if self.config.agent.enable_compaction:
|
|
@@ -356,7 +359,6 @@ class Agent:
|
|
|
356
359
|
AgentStatus.TOOL_RESULT, result_str, is_error, tool_call_obj
|
|
357
360
|
)
|
|
358
361
|
|
|
359
|
-
# Deduct one tool budget from remaining budget
|
|
360
362
|
self._tool_budget_remaining -= 1
|
|
361
363
|
if self._tool_budget_remaining <= 0:
|
|
362
364
|
self._tool_budget_exhausted = True
|
|
@@ -372,8 +374,6 @@ class Agent:
|
|
|
372
374
|
"content": str(result),
|
|
373
375
|
}
|
|
374
376
|
)
|
|
375
|
-
# Add explicit message telling the agent to stop using
|
|
376
|
-
# tools and provide its final response
|
|
377
377
|
messages.append(
|
|
378
378
|
{
|
|
379
379
|
"role": "user",
|
|
@@ -399,13 +399,36 @@ class Agent:
|
|
|
399
399
|
}
|
|
400
400
|
)
|
|
401
401
|
total_tool_calls += 1
|
|
402
|
+
self._update_context_progress(messages)
|
|
402
403
|
# Safety: if model keeps calling tools without text, force break
|
|
403
404
|
if total_tool_calls >= 30:
|
|
404
405
|
break
|
|
405
406
|
continue
|
|
406
407
|
|
|
408
|
+
# Model returned neither text content nor tool calls — re-prompt it
|
|
409
|
+
# to force an answer with the current context state.
|
|
410
|
+
if not message_obj.content and (
|
|
411
|
+
not message_obj.tool_calls or len(message_obj.tool_calls) == 0
|
|
412
|
+
):
|
|
413
|
+
if total_tool_calls >= 5:
|
|
414
|
+
# After 5 empty responses, just give up and return empty
|
|
415
|
+
break
|
|
416
|
+
messages.append(
|
|
417
|
+
{
|
|
418
|
+
"role": "user",
|
|
419
|
+
"content": (
|
|
420
|
+
"You returned an empty response with no tool calls. "
|
|
421
|
+
"Please provide your answer now based on the current "
|
|
422
|
+
"conversation state. Respond in plain text only."
|
|
423
|
+
),
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
total_tool_calls += 1
|
|
427
|
+
continue
|
|
428
|
+
|
|
407
429
|
break
|
|
408
430
|
|
|
431
|
+
self._update_context_progress(messages)
|
|
409
432
|
self._history = [m for m in messages if m.get("role") != "system"]
|
|
410
433
|
return final_text or response_message.get("content", "") or ""
|
|
411
434
|
|
|
@@ -681,15 +704,52 @@ class Agent:
|
|
|
681
704
|
}
|
|
682
705
|
)
|
|
683
706
|
total_tool_calls += 1
|
|
707
|
+
self._update_context_progress(messages)
|
|
684
708
|
if total_tool_calls >= 30:
|
|
685
709
|
break
|
|
686
710
|
continue
|
|
687
711
|
|
|
712
|
+
# Model returned neither text content nor tool calls — re-prompt it
|
|
713
|
+
# to force an answer with the current context state.
|
|
714
|
+
if not full_text and (
|
|
715
|
+
not response_message.get("tool_calls") or
|
|
716
|
+
len(response_message.get("tool_calls", [])) == 0
|
|
717
|
+
):
|
|
718
|
+
if total_tool_calls >= 5:
|
|
719
|
+
# After 5 empty responses, just give up and return empty
|
|
720
|
+
break
|
|
721
|
+
messages.append(
|
|
722
|
+
{
|
|
723
|
+
"role": "user",
|
|
724
|
+
"content": (
|
|
725
|
+
"You returned an empty response with no tool calls. "
|
|
726
|
+
"Please provide your answer now based on the current "
|
|
727
|
+
"conversation state. Respond in plain text only."
|
|
728
|
+
),
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
total_tool_calls += 1
|
|
732
|
+
continue
|
|
733
|
+
|
|
688
734
|
break
|
|
689
735
|
|
|
736
|
+
self._update_context_progress(messages)
|
|
690
737
|
self._history = [m for m in messages if m.get("role") != "system"]
|
|
691
738
|
return final_text or response_message.get("content", "") or ""
|
|
692
739
|
|
|
740
|
+
def _update_context_progress(self, messages: list[dict[str, Any]]) -> None:
|
|
741
|
+
"""Estimate token usage and report progress via context_callback."""
|
|
742
|
+
if self._context_callback is None:
|
|
743
|
+
return
|
|
744
|
+
try:
|
|
745
|
+
token_count = litellm.token_counter(
|
|
746
|
+
model=self.config.provider.model,
|
|
747
|
+
messages=messages,
|
|
748
|
+
)
|
|
749
|
+
self._context_callback(token_count, self._context_window)
|
|
750
|
+
except Exception:
|
|
751
|
+
pass
|
|
752
|
+
|
|
693
753
|
def reset(self) -> None:
|
|
694
754
|
"""Clear conversation history."""
|
|
695
755
|
self._history = []
|
|
@@ -791,10 +791,18 @@ class SearchDirTool(BaseTool):
|
|
|
791
791
|
|
|
792
792
|
|
|
793
793
|
class RunPythonScriptTool(BaseTool):
|
|
794
|
-
"""
|
|
794
|
+
"""Execute a Python script file in an isolated Monty sandbox.
|
|
795
|
+
|
|
796
|
+
Reads the script file and executes its contents through the
|
|
797
|
+
Monty sandbox (Rust-based security, resource limits) rather than
|
|
798
|
+
spawning a subprocess on the host.
|
|
799
|
+
"""
|
|
795
800
|
|
|
796
801
|
name = "run_script"
|
|
797
|
-
description =
|
|
802
|
+
description = (
|
|
803
|
+
"Execute a Python script file in an isolated Monty sandbox "
|
|
804
|
+
"with Rust-based security and resource limits."
|
|
805
|
+
)
|
|
798
806
|
|
|
799
807
|
@property
|
|
800
808
|
def params(self) -> list[ToolParam]:
|
|
@@ -805,13 +813,6 @@ class RunPythonScriptTool(BaseTool):
|
|
|
805
813
|
"Path to the Python script file",
|
|
806
814
|
required=True,
|
|
807
815
|
),
|
|
808
|
-
ToolParam(
|
|
809
|
-
"args",
|
|
810
|
-
"string",
|
|
811
|
-
"Command-line arguments for the script",
|
|
812
|
-
required=False,
|
|
813
|
-
default="",
|
|
814
|
-
),
|
|
815
816
|
ToolParam(
|
|
816
817
|
"timeout",
|
|
817
818
|
"number",
|
|
@@ -823,52 +824,52 @@ class RunPythonScriptTool(BaseTool):
|
|
|
823
824
|
|
|
824
825
|
def execute(self, **kwargs: Any) -> ToolResult:
|
|
825
826
|
script_path = kwargs.get("script_path")
|
|
826
|
-
args = kwargs.get("args", "")
|
|
827
827
|
timeout = kwargs.get("timeout", 10)
|
|
828
828
|
if not script_path:
|
|
829
829
|
return ToolResult(
|
|
830
830
|
output="Missing required parameter: script_path",
|
|
831
831
|
error=True,
|
|
832
832
|
)
|
|
833
|
+
# Coerce timeout to float (LLM may send string like "15")
|
|
834
|
+
try:
|
|
835
|
+
timeout = float(timeout)
|
|
836
|
+
except (TypeError, ValueError):
|
|
837
|
+
timeout = 10.0
|
|
838
|
+
|
|
833
839
|
sandbox = MontySandbox()
|
|
834
840
|
try:
|
|
835
|
-
|
|
836
|
-
|
|
841
|
+
# Read script file from the host filesystem
|
|
842
|
+
p = Path(script_path)
|
|
843
|
+
if not p.exists():
|
|
837
844
|
return ToolResult(
|
|
838
845
|
output=f"Script not found: {script_path}",
|
|
839
846
|
error=True,
|
|
840
847
|
exit_code=1,
|
|
841
848
|
)
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
cwd=str(sandbox.sandbox_root),
|
|
851
|
-
capture_output=True,
|
|
852
|
-
text=True,
|
|
853
|
-
timeout=timeout,
|
|
854
|
-
)
|
|
849
|
+
try:
|
|
850
|
+
code = p.read_text()
|
|
851
|
+
except Exception as e:
|
|
852
|
+
return ToolResult(
|
|
853
|
+
output=f"Error reading script: {str(e)}",
|
|
854
|
+
error=True,
|
|
855
|
+
exit_code=1,
|
|
856
|
+
)
|
|
855
857
|
|
|
858
|
+
# Execute script contents through Monty sandbox (Rust-based isolation)
|
|
859
|
+
result = sandbox.run_python(code, timeout=timeout)
|
|
856
860
|
return ToolResult(
|
|
857
861
|
output=(
|
|
858
862
|
f"stdout: {result.stdout}\n"
|
|
859
863
|
f"stderr: {result.stderr}\n"
|
|
860
|
-
f"exit_code: {result.
|
|
864
|
+
f"exit_code: {result.exit_code}\n"
|
|
865
|
+
f"time: {result.execution_time:.2f}s\n"
|
|
866
|
+
f"result: {result.result}"
|
|
861
867
|
),
|
|
862
|
-
error=result.
|
|
868
|
+
error=not result.success,
|
|
863
869
|
stdout=result.stdout,
|
|
864
870
|
stderr=result.stderr,
|
|
865
|
-
exit_code=result.
|
|
866
|
-
|
|
867
|
-
except subprocess.TimeoutExpired:
|
|
868
|
-
return ToolResult(
|
|
869
|
-
output="SANDBOX_ERROR: Execution timed out",
|
|
870
|
-
error=True,
|
|
871
|
-
exit_code=-1,
|
|
871
|
+
exit_code=result.exit_code,
|
|
872
|
+
result=result.result,
|
|
872
873
|
)
|
|
873
874
|
finally:
|
|
874
875
|
sandbox.cleanup()
|