regcode 0.1.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.
regcode/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from regcode.config import Config
2
+ from regcode.main import Agent, AgentStatus, ToolCall
3
+
4
+ __all__ = ["Agent", "Config", "AgentStatus", "ToolCall"]
5
+ __version__ = "0.1.0"
regcode/cli.py ADDED
@@ -0,0 +1,180 @@
1
+ import sys
2
+
3
+ import click
4
+ import yaml
5
+
6
+ import regcode
7
+ from regcode.permissions import AgentPermission
8
+ from regcode.tui import ChatUI
9
+
10
+
11
+ def _confirm(prompt_text):
12
+ """Ask yes/no question, accepting y/n (case insensitive)."""
13
+ answer = click.prompt(
14
+ prompt_text, type=click.Choice(["y", "n"], case_sensitive=False), default="y"
15
+ )
16
+ return answer == "y"
17
+
18
+
19
+ @click.group(invoke_without_command=True)
20
+ @click.pass_context
21
+ def cli(ctx):
22
+ """RegCode - Minimalistic Coding Agent"""
23
+ if ctx.invoked_subcommand is None:
24
+ ctx.invoke(chat, full=True)
25
+
26
+
27
+ @cli.command()
28
+ @click.option("--model", default=None, help="Model to use (e.g. openai/gpt-4o)")
29
+ @click.option(
30
+ "--config", "config_path", default="config.yaml", help="Path to config.yaml"
31
+ )
32
+ @click.option("--stream", is_flag=True, help="Stream response")
33
+ @click.option(
34
+ "--full",
35
+ is_flag=True,
36
+ help="Enable full agent mode (read, write, execute permissions)",
37
+ )
38
+ @click.option(
39
+ "--no-color",
40
+ is_flag=True,
41
+ help="Disable colored output",
42
+ )
43
+ def chat(model, config_path, stream, full, no_color):
44
+ """Chat with the agent."""
45
+ config = regcode.Config.load(config_path)
46
+ if model:
47
+ config.provider.model = model
48
+ if full:
49
+ config.permissions.append(AgentPermission.WRITE)
50
+
51
+ # Initialize the TUI
52
+ force_color = not no_color and sys.stdout.isatty()
53
+ ui = ChatUI(force_color=force_color)
54
+
55
+ agent = regcode.Agent(
56
+ config=config,
57
+ status_callback=ui.handle_status,
58
+ result_callback=ui.handle_result,
59
+ )
60
+
61
+ ui.print_welcome(regcode.__version__)
62
+
63
+ while True:
64
+ try:
65
+ msg = ui.get_user_input()
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
94
+
95
+
96
+ @cli.command()
97
+ @click.option(
98
+ "--output", "output_path", default="config.yaml", help="Path for config.yaml"
99
+ )
100
+ def configure(output_path):
101
+ """Interactive config generator for config.yaml."""
102
+ config = regcode.Config()
103
+
104
+ # --- Agent config ---
105
+ yes = _confirm("Configure agent settings?")
106
+ if yes:
107
+ context_window = click.prompt(
108
+ "Context window size",
109
+ type=int,
110
+ default=config.agent.context_window,
111
+ )
112
+ compaction_threshold = click.prompt(
113
+ "Compaction threshold",
114
+ type=float,
115
+ default=config.agent.compaction_threshold,
116
+ )
117
+ enable_compaction = _confirm("Enable compaction")
118
+
119
+ config.agent.context_window = context_window
120
+ config.agent.compaction_threshold = compaction_threshold
121
+ config.agent.enable_compaction = enable_compaction
122
+ else:
123
+ click.echo(" Using default agent settings.")
124
+
125
+ # --- Provider config ---
126
+ yes = _confirm("Configure API provider?")
127
+ if yes:
128
+ model = click.prompt("Model", default=config.provider.model)
129
+ base_url = click.prompt("Base URL", default=config.provider.base_url)
130
+ api_key = click.prompt(
131
+ "API key",
132
+ default=config.provider.api_key,
133
+ hide_input=True,
134
+ )
135
+ temperature = click.prompt(
136
+ "Temperature", type=float, default=config.provider.temperature
137
+ )
138
+ max_tokens = click.prompt(
139
+ "Max tokens", type=int, default=config.provider.max_tokens
140
+ )
141
+
142
+ config.provider.model = model
143
+ config.provider.base_url = base_url
144
+ config.provider.api_key = api_key
145
+ config.provider.temperature = temperature
146
+ config.provider.max_tokens = max_tokens
147
+ else:
148
+ click.echo(" Using default provider settings.")
149
+
150
+ # --- Write config.yaml ---
151
+ data = {
152
+ "agent": {
153
+ "context_window": config.agent.context_window,
154
+ "compaction_threshold": config.agent.compaction_threshold,
155
+ "enable_compaction": config.agent.enable_compaction,
156
+ },
157
+ "provider": {
158
+ "base_url": config.provider.base_url,
159
+ "api_key": config.provider.api_key,
160
+ "model": config.provider.model,
161
+ "temperature": config.provider.temperature,
162
+ "max_tokens": config.provider.max_tokens,
163
+ },
164
+ "permissions": [p.value for p in config.permissions],
165
+ "tool_budget": config.tool_budget,
166
+ }
167
+
168
+ with open(output_path, "w") as f:
169
+ yaml.dump(data, f, default_flow_style=False)
170
+ click.echo(f"\nconfig.yaml written to {output_path}")
171
+
172
+
173
+ @cli.command()
174
+ def version():
175
+ """Print version."""
176
+ click.echo(f"RegCode v{regcode.__version__}")
177
+
178
+
179
+ if __name__ == "__main__":
180
+ cli(prog_name="regcode")
regcode/config.py ADDED
@@ -0,0 +1,153 @@
1
+ import os
2
+ from pathlib import Path
3
+ from textwrap import dedent
4
+ from typing import Any
5
+
6
+ import yaml
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from regcode.permissions import AgentPermission
10
+
11
+ default_system_prompt = dedent(
12
+ """
13
+ # RegCode coding agent
14
+
15
+ ## Instructions
16
+ You are a coding agent. You have access to tools for reading and writing files,
17
+ executing code, and running tests. You can also use a sandboxed environment for
18
+ running untrusted code. You should use these tools to help you understand and
19
+ modify the codebase in the current directory.
20
+
21
+ When you need to read or write files, execute code, or run tests, you should use
22
+ the appropriate tool. You should not attempt to read or write files directly, or
23
+ execute code directly. You should also not attempt to run tests directly.
24
+
25
+ You should always be careful when executing code, as it may have side effects.
26
+ You should also be careful when reading or writing files, as it may affect the
27
+ state of the codebase.
28
+
29
+ Your goal is to help the user understand and modify the codebase in the current
30
+ directory. You should provide clear and concise explanations of the code and
31
+ suggest modifications that will improve the codebase.
32
+
33
+ Make sure to provide thorough explanations of your reasoning and the steps you
34
+ are taking.
35
+
36
+ ## Review Format
37
+ Start directly with your review content. Do not address user or
38
+ use fillers like "Now I have enough information to provide a review." or
39
+ "Here is my review:"
40
+ """
41
+ )
42
+
43
+
44
+ class AgentConfig(BaseModel):
45
+ model_config = ConfigDict(extra="allow")
46
+ context_window: int = 128000
47
+ compaction_threshold: float = 0.8 # Compact when context > 80% of window
48
+ enable_compaction: bool = True
49
+
50
+
51
+ class ProviderConfig(BaseModel):
52
+ model_config = ConfigDict(extra="allow")
53
+ base_url: str = "https://api.openai.com/v1"
54
+ api_key: str = ""
55
+ temperature: float = 1
56
+ model: str = "openai/gpt-4o"
57
+ max_tokens: int = 4096
58
+ # Optional litellm completion kwargs (passed through to litellm.completion)
59
+ # See https://docs.litellm.ai/docs/completion/input for full list
60
+ extra_headers: dict | None = None
61
+ stop: list[str] | None = None
62
+ top_p: float | None = None
63
+ frequency_penalty: float | None = None
64
+ presence_penalty: float | None = None
65
+ logit_bias: dict | None = None
66
+ response_format: type[BaseModel] | None = None
67
+ seed: int | None = None
68
+ service_tier: str | None = None
69
+
70
+
71
+ class SandboxConfig(BaseModel):
72
+ """Configuration for the Monty sandbox."""
73
+
74
+ model_config = ConfigDict(extra="allow")
75
+ use_monty: bool = True
76
+ max_duration_secs: float = 10.0
77
+ max_memory_mb: int = 64
78
+ max_recursion_depth: int = 100
79
+ type_check: bool = True
80
+ allowed_imports: list[str] = []
81
+
82
+
83
+ class Config(BaseModel):
84
+ model_config = ConfigDict(extra="allow")
85
+
86
+ agent: AgentConfig = AgentConfig()
87
+ provider: ProviderConfig = ProviderConfig()
88
+ system_prompt: str = default_system_prompt
89
+ tools: dict = {}
90
+ sandbox: SandboxConfig = SandboxConfig()
91
+ tool_budget: int = 20
92
+ permissions: list[AgentPermission] = [AgentPermission.READ, AgentPermission.EXECUTE]
93
+
94
+ @classmethod
95
+ def load(cls, path: str | None = None) -> "Config":
96
+ # get default home dir for user first
97
+ if path is None:
98
+ alt_config_path = Path.home() / ".regcode" / "config.yaml"
99
+ if alt_config_path.exists():
100
+ _path = alt_config_path
101
+ else:
102
+ _path = Path("config.yaml")
103
+ else:
104
+ _path = Path(path)
105
+ if not _path.exists():
106
+ return cls()
107
+ with open(_path) as f:
108
+ data = yaml.safe_load(f)
109
+ # Expand env vars in values
110
+ data = _expand_env_vars(data)
111
+ return cls(**data)
112
+
113
+ def get_litellm_completion_kwargs(self) -> dict[str, Any]:
114
+ """Build litellm.completion kwargs from this config.
115
+
116
+ Combines model, provider, and agent config into a single dict
117
+ ready to pass to litellm.completion().
118
+
119
+ Args:
120
+ extra_kwargs: Additional kwargs to merge on top of the
121
+ config-derived values (e.g. 'messages', 'tools', 'stream').
122
+
123
+ Returns:
124
+ Dict of litellm.completion keyword arguments.
125
+ """
126
+ extra_kwargs = {}
127
+ if hasattr(self.provider, "model_extra"):
128
+ extra_kwargs = self.provider.model_extra
129
+ result = self.provider.model_dump()
130
+ if extra_kwargs:
131
+ result.update(extra_kwargs)
132
+ # Agent-level config overrides provider defaults for known keys
133
+ if self.agent.context_window is not None:
134
+ result["context_window"] = self.agent.context_window
135
+ if hasattr(self.agent, "compaction_threshold"):
136
+ result["compaction_threshold"] = self.agent.compaction_threshold
137
+ if hasattr(self.agent, "enable_compaction"):
138
+ result["enable_compaction"] = self.agent.enable_compaction
139
+ # Apply agent-level extra fields (e.g. model, max_tokens, temperature
140
+ # from YAML agent section) as overrides over provider defaults
141
+ if hasattr(self.agent, "model_extra") and self.agent.model_extra:
142
+ result.update(self.agent.model_extra)
143
+ return result
144
+
145
+
146
+ def _expand_env_vars(obj):
147
+ if isinstance(obj, str):
148
+ return os.path.expandvars(obj)
149
+ if isinstance(obj, dict):
150
+ return {k: _expand_env_vars(v) for k, v in obj.items()}
151
+ if isinstance(obj, list):
152
+ return [_expand_env_vars(v) for v in obj]
153
+ return obj
@@ -0,0 +1,154 @@
1
+ """Context window management with automatic compaction and review notes.
2
+
3
+ Handles context window limits by triggering compaction when the context
4
+ approaches the maximum size. Maintains review notes across compaction
5
+ for persistent findings about large repos.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import litellm
13
+
14
+ from regcode.config import Config
15
+
16
+
17
+ class ConversationManager:
18
+ """Manages conversation history with automatic compaction and review notes."""
19
+
20
+ def __init__(
21
+ self,
22
+ ) -> None:
23
+ c = Config.load()
24
+ self.c = c
25
+ self.context_window = c.agent.context_window
26
+ self.max_tokens = c.provider.max_tokens
27
+ self.compaction_threshold = c.agent.compaction_threshold
28
+
29
+ # Conversation messages
30
+ self.messages: list[dict[str, Any]] = []
31
+
32
+ # Review notes persist across compaction
33
+ self._review_notes: list[dict[str, Any]] = []
34
+
35
+ # Cached token estimate (invalidate on message/compaction changes)
36
+ self._cached_token_estimate: int | None = None
37
+
38
+ def add_message(self, message: dict[str, Any]) -> None:
39
+ """Add a message to the context."""
40
+ self.messages.append(message)
41
+ self._cached_token_estimate = None
42
+
43
+ def should_compact(self) -> bool:
44
+ """Check if context needs compaction."""
45
+ if self._cached_token_estimate is None:
46
+ self._cached_token_estimate = self._estimate_tokens()
47
+ # Ensure available space is always at least 1 to avoid edge cases
48
+ # where max_tokens >= context_window would make threshold zero.
49
+ available_space = max(1, self.context_window - self.max_tokens)
50
+ return self._cached_token_estimate > available_space * self.compaction_threshold
51
+
52
+ def compact(self) -> None:
53
+ """Compact the context by keeping the first message, review notes,
54
+ and the most recent messages.
55
+
56
+ This preserves user instructions and accumulated review notes while
57
+ discarding older conversation history to stay within token limits.
58
+ """
59
+ if not self.messages:
60
+ return
61
+
62
+ # Keep the first message (system prompt / user instruction),
63
+ # but strip any previously embedded review notes section so they
64
+ # don't appear twice when _build_system_prompt() re-appends them.
65
+ first_msg = self.messages[0]
66
+ if first_msg.get("role") == "system" and isinstance(
67
+ first_msg.get("content"), str
68
+ ):
69
+ first_msg = {
70
+ "role": first_msg["role"],
71
+ "content": self._strip_review_notes(first_msg["content"]),
72
+ }
73
+
74
+ # Keep the last N messages instead of just 1.
75
+ # Cap at total_remaining so we never include more than available,
76
+ # and use min with total_remaining to handle small histories safely.
77
+ total_remaining = len(self.messages) - 1
78
+ if total_remaining <= 0:
79
+ recent_count = 0
80
+ else:
81
+ # Retain at least 5 recent messages, or 1/3 of history, whichever
82
+ # is larger, capped at total_remaining.
83
+ recent_count = min(max(5, total_remaining // 3), total_remaining)
84
+ # Get the last N messages (these don't include index 0 since we
85
+ # already extracted first_msg)
86
+ recent_msgs = self.messages[-recent_count:] if recent_count > 0 else []
87
+
88
+ # Build compacted list: first message + recent messages
89
+ self.messages = [first_msg]
90
+ self.messages.extend(recent_msgs)
91
+
92
+ # Invalidate cached token estimate since message count changed
93
+ self._cached_token_estimate = None
94
+
95
+ @staticmethod
96
+ def _strip_review_notes(text: str) -> str:
97
+ """Remove the 'Prior Review Notes' section from a system prompt.
98
+
99
+ This prevents review notes from being duplicated after compaction
100
+ since they will be re-appended by _build_system_prompt() on the
101
+ next chat call via get_review_notes().
102
+ """
103
+ marker = "\n\n### Prior Review Notes\n"
104
+ if marker not in text:
105
+ return text
106
+ # Find the marker and truncate the text at that point,
107
+ # then strip any trailing whitespace left behind
108
+ idx = text.index(marker)
109
+ return text[:idx].rstrip()
110
+
111
+ def _estimate_tokens(self) -> int:
112
+ """Estimate token count from messages using litellm token counter.
113
+
114
+ Uses litellm's built-in token counting which accurately counts tokens
115
+ for the specific model, including system prompt and tool definitions.
116
+ """
117
+ return litellm.token_counter(
118
+ model=self.c.provider.model,
119
+ messages=self.messages,
120
+ )
121
+
122
+ def add_review_note(
123
+ self,
124
+ title: str,
125
+ content: str,
126
+ importance: str = "medium",
127
+ ) -> None:
128
+ """Add a review note (persists across compaction)."""
129
+ self._review_notes.append({
130
+ "title": title,
131
+ "content": content,
132
+ "importance": importance,
133
+ })
134
+
135
+ def get_review_notes(self) -> list[dict[str, Any]]:
136
+ """Get all review notes."""
137
+ return list(self._review_notes)
138
+
139
+ def clear_review_notes(self) -> None:
140
+ """Clear all review notes."""
141
+ self._review_notes = []
142
+
143
+ def reset(self) -> None:
144
+ """Clear all messages and review notes."""
145
+ self.messages = []
146
+ self._review_notes = []
147
+
148
+ def get_messages(self) -> list[dict[str, Any]]:
149
+ """Get current messages."""
150
+ return list(self.messages)
151
+
152
+
153
+ # Backwards-compatible alias for import paths that use 'context_manager'
154
+ ContextManager = ConversationManager