kubrick-cli 0.1.2__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.
@@ -0,0 +1,328 @@
1
+ """Planning phase for complex tasks with read-only exploration."""
2
+
3
+ from typing import Dict, List
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+ from rich.prompt import Prompt
9
+
10
+ console = Console()
11
+
12
+
13
+ # Read-only tools allowed in planning mode
14
+ PLANNING_ALLOWED_TOOLS = {
15
+ "read_file",
16
+ "list_files",
17
+ "search_files",
18
+ # run_bash is allowed but will be restricted to read-only commands
19
+ }
20
+
21
+ # Dangerous bash commands that are never allowed in planning
22
+ DANGEROUS_BASH_PATTERNS = [
23
+ "rm ",
24
+ "mv ",
25
+ "cp ",
26
+ "chmod",
27
+ "chown",
28
+ "sudo",
29
+ "> ",
30
+ ">>",
31
+ "|",
32
+ "git push",
33
+ "git commit",
34
+ ]
35
+
36
+
37
+ class PlanningPhase:
38
+ """
39
+ Handles the planning phase for complex tasks.
40
+
41
+ In planning mode:
42
+ - Agent can only use read-only tools
43
+ - Agent explores the codebase to understand structure
44
+ - Agent creates an implementation plan
45
+ - User approves/modifies/rejects the plan
46
+ - After approval, execution proceeds with full tools
47
+ """
48
+
49
+ def __init__(self, llm_client, tool_executor, agent_loop):
50
+ """
51
+ Initialize planning phase.
52
+
53
+ Args:
54
+ llm_client: LLM client instance
55
+ tool_executor: Tool executor instance
56
+ agent_loop: Agent loop instance for execution
57
+ """
58
+ self.llm_client = llm_client
59
+ self.tool_executor = tool_executor
60
+ self.agent_loop = agent_loop
61
+
62
+ def execute_planning(
63
+ self, user_message: str, base_messages: List[Dict]
64
+ ) -> str:
65
+ """
66
+ Execute the planning phase.
67
+
68
+ Args:
69
+ user_message: The user's original request
70
+ base_messages: Base conversation messages
71
+
72
+ Returns:
73
+ The generated plan text
74
+ """
75
+ console.print(
76
+ "\n[bold yellow]→ Entering PLANNING MODE[/bold yellow]"
77
+ )
78
+ console.print(
79
+ "[dim]Agent will explore the codebase with read-only tools and create a plan.[/dim]\n"
80
+ )
81
+
82
+ # Create planning messages with restricted system prompt
83
+ planning_messages = base_messages.copy()
84
+
85
+ # Add planning-specific system message
86
+ planning_messages.append(
87
+ {
88
+ "role": "system",
89
+ "content": """# PLANNING MODE
90
+
91
+ You are now in PLANNING MODE. Your task is to:
92
+ 1. EXPLORE the codebase using read-only tools
93
+ 2. DESIGN an implementation approach
94
+ 3. CREATE a detailed plan
95
+
96
+ # Available Tools (READ-ONLY)
97
+
98
+ You can ONLY use these tools:
99
+ - read_file: Read file contents
100
+ - list_files: List files matching patterns (supports recursive patterns like "**/*.py")
101
+ - search_files: Search for text in files
102
+ - run_bash: Run ONLY read-only commands (ls, find, cat, grep, tree, etc.)
103
+
104
+ You CANNOT use:
105
+ - write_file
106
+ - edit_file
107
+ - create_directory
108
+ - Any destructive bash commands
109
+
110
+ # How to Explore the Codebase
111
+
112
+ **IMPORTANT**: To get a complete understanding, you MUST explore systematically:
113
+
114
+ 1. **Start with directory structure**:
115
+ - Use `list_files` with pattern `**/*` to see ALL files and directories recursively
116
+ - Or use `run_bash` with command `find . -type f` to list all files
117
+ - Or use `run_bash` with command `tree` or `ls -R` to see the full structure
118
+
119
+ 2. **List files by type**:
120
+ - Python: `list_files` with pattern `**/*.py`
121
+ - JavaScript: `list_files` with pattern `**/*.js`
122
+ - All code files: Try multiple patterns to cover all file types
123
+
124
+ 3. **Read key files**:
125
+ - README files
126
+ - Configuration files (package.json, requirements.txt, pyproject.toml, etc.)
127
+ - Main entry points
128
+ - Important modules based on the task
129
+
130
+ 4. **Search for specific patterns**:
131
+ - Use `search_files` to find classes, functions, imports, etc.
132
+
133
+ **Example exploration workflow**:
134
+ ```tool_call
135
+ {
136
+ "tool": "list_files",
137
+ "parameters": {
138
+ "pattern": "**/*"
139
+ }
140
+ }
141
+ ```
142
+
143
+ Then read relevant files:
144
+ ```tool_call
145
+ {
146
+ "tool": "read_file",
147
+ "parameters": {
148
+ "file_path": "path/to/important/file.py"
149
+ }
150
+ }
151
+ ```
152
+
153
+ # Plan Format
154
+
155
+ When ready, create a plan in this format:
156
+
157
+ ## Implementation Plan
158
+
159
+ ### Overview
160
+ [Brief description of what you'll do]
161
+
162
+ ### Steps
163
+ 1. [First step]
164
+ 2. [Second step]
165
+ 3. [etc.]
166
+
167
+ ### Files to Modify
168
+ - file1.py: [what changes]
169
+ - file2.py: [what changes]
170
+
171
+ ### Risks
172
+ - [Potential issues or concerns]
173
+
174
+ # Completion
175
+
176
+ Say "PLAN_COMPLETE" when your plan is ready.""",
177
+ }
178
+ )
179
+
180
+ # Add user's original request
181
+ planning_messages.append(
182
+ {
183
+ "role": "user",
184
+ "content": f"Task: {user_message}\n\nPlease explore the codebase and create an implementation plan.",
185
+ }
186
+ )
187
+
188
+ # Run agent loop with read-only tool executor
189
+ original_executor = self.agent_loop.tool_executor
190
+ restricted_executor = RestrictedToolExecutor(
191
+ self.tool_executor, PLANNING_ALLOWED_TOOLS
192
+ )
193
+ self.agent_loop.tool_executor = restricted_executor
194
+
195
+ try:
196
+ # Run planning loop (max 10 iterations)
197
+ old_max = self.agent_loop.max_iterations
198
+ self.agent_loop.max_iterations = 10
199
+
200
+ # Create tool parser
201
+ from .main import KubrickCLI
202
+
203
+ temp_cli = KubrickCLI.__new__(KubrickCLI)
204
+ tool_parser = temp_cli.parse_tool_calls
205
+
206
+ result = self.agent_loop.run(
207
+ messages=planning_messages,
208
+ tool_parser=tool_parser,
209
+ display_callback=None,
210
+ )
211
+
212
+ self.agent_loop.max_iterations = old_max
213
+
214
+ # Extract the plan from the last assistant message
215
+ plan_text = ""
216
+ for msg in reversed(planning_messages):
217
+ if msg["role"] == "assistant":
218
+ plan_text = msg["content"]
219
+ break
220
+
221
+ return plan_text
222
+
223
+ finally:
224
+ # Restore original executor
225
+ self.agent_loop.tool_executor = original_executor
226
+
227
+ def get_user_approval(self, plan: str) -> Dict:
228
+ """
229
+ Present plan to user and get approval.
230
+
231
+ Args:
232
+ plan: The generated plan text
233
+
234
+ Returns:
235
+ Dict with 'approved' (bool) and optional 'modifications' (str)
236
+ """
237
+ # Display plan
238
+ console.print("\n" + "=" * 70)
239
+ console.print(
240
+ Panel(
241
+ Markdown(plan),
242
+ title="[bold cyan]Implementation Plan[/bold cyan]",
243
+ border_style="cyan",
244
+ )
245
+ )
246
+ console.print("=" * 70 + "\n")
247
+
248
+ # Get user decision
249
+ choice = Prompt.ask(
250
+ "[bold yellow]Approve this plan?[/bold yellow]",
251
+ choices=["approve", "modify", "reject"],
252
+ default="approve",
253
+ )
254
+
255
+ if choice == "approve":
256
+ console.print("[green]✓ Plan approved, proceeding with implementation[/green]")
257
+ return {"approved": True}
258
+
259
+ elif choice == "modify":
260
+ modifications = Prompt.ask(
261
+ "[yellow]What modifications would you like?[/yellow]"
262
+ )
263
+ console.print("[yellow]Plan modifications noted, will adjust[/yellow]")
264
+ return {"approved": True, "modifications": modifications}
265
+
266
+ else: # reject
267
+ console.print("[red]Plan rejected, cancelling task[/red]")
268
+ return {"approved": False}
269
+
270
+
271
+ class RestrictedToolExecutor:
272
+ """
273
+ Wraps ToolExecutor to restrict to read-only tools during planning.
274
+ """
275
+
276
+ def __init__(self, base_executor, allowed_tools: set):
277
+ """
278
+ Initialize restricted executor.
279
+
280
+ Args:
281
+ base_executor: Base ToolExecutor instance
282
+ allowed_tools: Set of allowed tool names
283
+ """
284
+ self.base_executor = base_executor
285
+ self.allowed_tools = allowed_tools
286
+
287
+ def execute(self, tool_name: str, parameters: Dict) -> Dict:
288
+ """
289
+ Execute tool with restrictions.
290
+
291
+ Args:
292
+ tool_name: Name of tool to execute
293
+ parameters: Tool parameters
294
+
295
+ Returns:
296
+ Result dict
297
+ """
298
+ # Check if tool is allowed
299
+ if tool_name not in self.allowed_tools:
300
+ return {
301
+ "success": False,
302
+ "error": f"Tool '{tool_name}' is not allowed in planning mode (read-only)",
303
+ }
304
+
305
+ # Special handling for run_bash - check for dangerous commands
306
+ if tool_name == "run_bash":
307
+ command = parameters.get("command", "")
308
+ if self._is_dangerous_command(command):
309
+ return {
310
+ "success": False,
311
+ "error": f"Bash command '{command}' is not allowed in planning mode (read-only)",
312
+ }
313
+
314
+ # Execute allowed tool
315
+ return self.base_executor.execute(tool_name, parameters)
316
+
317
+ def _is_dangerous_command(self, command: str) -> bool:
318
+ """Check if bash command is dangerous."""
319
+ command_lower = command.lower()
320
+ for pattern in DANGEROUS_BASH_PATTERNS:
321
+ if pattern in command_lower:
322
+ return True
323
+ return False
324
+
325
+ @property
326
+ def working_dir(self):
327
+ """Pass through working_dir property."""
328
+ return self.base_executor.working_dir
@@ -0,0 +1,172 @@
1
+ """Progress tracking and visualization for multi-step tasks."""
2
+
3
+ from typing import Optional
4
+
5
+ from rich.console import Console
6
+ from rich.progress import (
7
+ BarColumn,
8
+ Progress,
9
+ SpinnerColumn,
10
+ TaskID,
11
+ TextColumn,
12
+ TimeElapsedColumn,
13
+ )
14
+
15
+ console = Console()
16
+
17
+
18
+ class ProgressTracker:
19
+ """
20
+ Tracks and displays progress for multi-step agentic tasks.
21
+
22
+ Provides visual feedback during long-running operations.
23
+ """
24
+
25
+ def __init__(self, enabled: bool = True):
26
+ """
27
+ Initialize progress tracker.
28
+
29
+ Args:
30
+ enabled: Whether progress tracking is enabled
31
+ """
32
+ self.enabled = enabled
33
+ self.progress: Optional[Progress] = None
34
+ self.current_task: Optional[TaskID] = None
35
+ self.step_count = 0
36
+ self.total_steps = 0
37
+
38
+ def start(self, total_steps: int = None, description: str = "Working"):
39
+ """
40
+ Start progress tracking.
41
+
42
+ Args:
43
+ total_steps: Total number of steps (if known)
44
+ description: Initial description
45
+ """
46
+ if not self.enabled:
47
+ return
48
+
49
+ self.total_steps = total_steps or 0
50
+ self.step_count = 0
51
+
52
+ # Create progress bar
53
+ if total_steps:
54
+ # Determinate progress (with bar)
55
+ self.progress = Progress(
56
+ SpinnerColumn(),
57
+ TextColumn("[bold cyan]{task.description}"),
58
+ BarColumn(),
59
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
60
+ TimeElapsedColumn(),
61
+ console=console,
62
+ )
63
+ else:
64
+ # Indeterminate progress (spinner only)
65
+ self.progress = Progress(
66
+ SpinnerColumn(),
67
+ TextColumn("[bold cyan]{task.description}"),
68
+ TimeElapsedColumn(),
69
+ console=console,
70
+ )
71
+
72
+ self.progress.start()
73
+ self.current_task = self.progress.add_task(
74
+ description, total=total_steps
75
+ )
76
+
77
+ def update(
78
+ self, description: str = None, advance: int = 1, completed: int = None
79
+ ):
80
+ """
81
+ Update progress.
82
+
83
+ Args:
84
+ description: New description (optional)
85
+ advance: Amount to advance (default: 1)
86
+ completed: Set absolute completion (optional)
87
+ """
88
+ if not self.enabled or not self.progress:
89
+ return
90
+
91
+ if completed is not None:
92
+ self.step_count = completed
93
+ self.progress.update(self.current_task, completed=completed)
94
+ else:
95
+ self.step_count += advance
96
+ self.progress.update(self.current_task, advance=advance)
97
+
98
+ if description:
99
+ self.progress.update(self.current_task, description=description)
100
+
101
+ def update_description(self, description: str):
102
+ """
103
+ Update just the description.
104
+
105
+ Args:
106
+ description: New description
107
+ """
108
+ if not self.enabled or not self.progress:
109
+ return
110
+
111
+ self.progress.update(self.current_task, description=description)
112
+
113
+ def step(self, description: str):
114
+ """
115
+ Mark a step complete with new description.
116
+
117
+ Args:
118
+ description: Description of the new step
119
+ """
120
+ if not self.enabled or not self.progress:
121
+ # Still print step info even if progress disabled
122
+ console.print(f"[dim]→ {description}[/dim]")
123
+ return
124
+
125
+ self.step_count += 1
126
+ step_info = ""
127
+ if self.total_steps > 0:
128
+ step_info = f"[Step {self.step_count}/{self.total_steps}] "
129
+
130
+ self.progress.update(
131
+ self.current_task,
132
+ description=f"{step_info}{description}",
133
+ advance=1,
134
+ )
135
+
136
+ def complete(self, message: str = "Complete"):
137
+ """
138
+ Mark progress as complete.
139
+
140
+ Args:
141
+ message: Completion message
142
+ """
143
+ if not self.enabled or not self.progress:
144
+ return
145
+
146
+ if self.total_steps:
147
+ self.progress.update(
148
+ self.current_task, completed=self.total_steps
149
+ )
150
+
151
+ self.progress.update(self.current_task, description=message)
152
+ self.progress.stop()
153
+ self.progress = None
154
+ self.current_task = None
155
+
156
+ def stop(self):
157
+ """Stop progress tracking."""
158
+ if not self.enabled or not self.progress:
159
+ return
160
+
161
+ self.progress.stop()
162
+ self.progress = None
163
+ self.current_task = None
164
+
165
+ def __enter__(self):
166
+ """Context manager entry."""
167
+ return self
168
+
169
+ def __exit__(self, exc_type, exc_val, exc_tb):
170
+ """Context manager exit."""
171
+ if self.progress:
172
+ self.stop()
@@ -0,0 +1,6 @@
1
+ """Provider adapters for different LLM backends."""
2
+
3
+ from .base import ProviderAdapter, ProviderMetadata
4
+ from .factory import ProviderFactory
5
+
6
+ __all__ = ["ProviderAdapter", "ProviderMetadata", "ProviderFactory"]