aloop 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.

Potentially problematic release.


This version of aloop might be problematic. Click here for more details.

Files changed (62) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/todo.py +149 -0
  6. agent/tool_executor.py +54 -0
  7. agent/verification.py +135 -0
  8. aloop-0.1.0.dist-info/METADATA +246 -0
  9. aloop-0.1.0.dist-info/RECORD +62 -0
  10. aloop-0.1.0.dist-info/WHEEL +5 -0
  11. aloop-0.1.0.dist-info/entry_points.txt +2 -0
  12. aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
  13. aloop-0.1.0.dist-info/top_level.txt +9 -0
  14. cli.py +19 -0
  15. config.py +146 -0
  16. interactive.py +865 -0
  17. llm/__init__.py +51 -0
  18. llm/base.py +26 -0
  19. llm/compat.py +226 -0
  20. llm/content_utils.py +309 -0
  21. llm/litellm_adapter.py +450 -0
  22. llm/message_types.py +245 -0
  23. llm/model_manager.py +265 -0
  24. llm/retry.py +95 -0
  25. main.py +246 -0
  26. memory/__init__.py +20 -0
  27. memory/compressor.py +554 -0
  28. memory/manager.py +538 -0
  29. memory/serialization.py +82 -0
  30. memory/short_term.py +88 -0
  31. memory/token_tracker.py +203 -0
  32. memory/types.py +51 -0
  33. tools/__init__.py +6 -0
  34. tools/advanced_file_ops.py +557 -0
  35. tools/base.py +51 -0
  36. tools/calculator.py +50 -0
  37. tools/code_navigator.py +975 -0
  38. tools/explore.py +254 -0
  39. tools/file_ops.py +150 -0
  40. tools/git_tools.py +791 -0
  41. tools/notify.py +69 -0
  42. tools/parallel_execute.py +420 -0
  43. tools/session_manager.py +205 -0
  44. tools/shell.py +147 -0
  45. tools/shell_background.py +470 -0
  46. tools/smart_edit.py +491 -0
  47. tools/todo.py +130 -0
  48. tools/web_fetch.py +673 -0
  49. tools/web_search.py +61 -0
  50. utils/__init__.py +15 -0
  51. utils/logger.py +105 -0
  52. utils/model_pricing.py +49 -0
  53. utils/runtime.py +75 -0
  54. utils/terminal_ui.py +422 -0
  55. utils/tui/__init__.py +39 -0
  56. utils/tui/command_registry.py +49 -0
  57. utils/tui/components.py +306 -0
  58. utils/tui/input_handler.py +393 -0
  59. utils/tui/model_ui.py +204 -0
  60. utils/tui/progress.py +292 -0
  61. utils/tui/status_bar.py +178 -0
  62. utils/tui/theme.py +165 -0
tools/explore.py ADDED
@@ -0,0 +1,254 @@
1
+ """Exploration tool for parallel context gathering."""
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Any, Dict, List
5
+
6
+ from llm import LLMMessage
7
+
8
+ from .base import BaseTool
9
+
10
+ if TYPE_CHECKING:
11
+ from agent.base import BaseAgent
12
+
13
+
14
+ # General exploration prompt supporting both code and web exploration
15
+ GENERAL_EXPLORER_PROMPT = """<role>
16
+ You are an exploration agent gathering information for a task.
17
+ Your job is to discover relevant information WITHOUT making any changes.
18
+ </role>
19
+
20
+ <exploration_focus>
21
+ Aspect: {aspect}
22
+ Description: {description}
23
+ </exploration_focus>
24
+
25
+ <instructions>
26
+ 1. Use available information-gathering tools:
27
+ - Code exploration: glob_files, grep_content, read_file, code_navigator
28
+ - Web exploration: web_search, web_fetch
29
+
30
+ 2. Focus ONLY on the specified exploration aspect
31
+ 3. Report your findings concisely and specifically
32
+ 4. Do NOT make any changes to files
33
+ 5. Do NOT try to solve problems - just gather information
34
+
35
+ Your output should be a structured summary of what you discovered.
36
+ </instructions>
37
+
38
+ Explore and report your findings:"""
39
+
40
+
41
+ class ExploreTool(BaseTool):
42
+ """Tool for parallel exploration of code and web resources.
43
+
44
+ This tool enables the main agent to gather context through parallel
45
+ exploration sub-agents. Each exploration task runs in isolation and
46
+ returns a compressed summary.
47
+
48
+ Key features:
49
+ - Parallel execution of multiple exploration tasks
50
+ - Code exploration: file structure, patterns, dependencies
51
+ - Web exploration: search results, webpage content
52
+ - Compressed summaries to preserve context space
53
+ """
54
+
55
+ # Configuration
56
+ MAX_PARALLEL_EXPLORATIONS = 3
57
+ MAX_RESULT_CHARS = 1500
58
+
59
+ # Allowed tools for exploration (read-only + network)
60
+ EXPLORATION_TOOLS = {
61
+ "glob_files",
62
+ "grep_content",
63
+ "read_file",
64
+ "code_navigator",
65
+ "web_search",
66
+ "web_fetch",
67
+ }
68
+
69
+ def __init__(self, agent: "BaseAgent"):
70
+ """Initialize exploration tool.
71
+
72
+ Args:
73
+ agent: The parent agent instance that will run explorations
74
+ """
75
+ self.agent = agent
76
+
77
+ @property
78
+ def name(self) -> str:
79
+ return "explore_context"
80
+
81
+ @property
82
+ def description(self) -> str:
83
+ return """Gather context through parallel exploration of code and web resources.
84
+
85
+ Use this tool when you need to:
86
+ - Explore code structure, patterns, or dependencies
87
+ - Search the web for documentation, APIs, or solutions
88
+ - Gather information from multiple sources in parallel
89
+ - Understand a codebase before making changes
90
+
91
+ DO NOT use this for:
92
+ - Making changes to files (use regular edit tools)
93
+ - Simple, single-file reads (use read_file directly)
94
+ - Tasks that don't require exploration
95
+
96
+ Input parameters:
97
+ - tasks (required): Array of exploration tasks, each with:
98
+ - aspect: Brief name of what to explore (e.g., "file_structure", "api_docs")
99
+ - description: Detailed description of what to find
100
+
101
+ The tool runs explorations in parallel and returns compressed summaries."""
102
+
103
+ @property
104
+ def parameters(self) -> Dict[str, Any]:
105
+ return {
106
+ "tasks": {
107
+ "type": "array",
108
+ "description": "List of exploration tasks to run in parallel",
109
+ "items": {
110
+ "type": "object",
111
+ "properties": {
112
+ "aspect": {
113
+ "type": "string",
114
+ "description": "Brief name of the exploration aspect",
115
+ },
116
+ "description": {
117
+ "type": "string",
118
+ "description": "Detailed description of what to explore",
119
+ },
120
+ },
121
+ "required": ["aspect", "description"],
122
+ },
123
+ }
124
+ }
125
+
126
+ def to_anthropic_schema(self) -> Dict[str, Any]:
127
+ """Convert to Anthropic tool schema format."""
128
+ return {
129
+ "name": self.name,
130
+ "description": self.description,
131
+ "input_schema": {
132
+ "type": "object",
133
+ "properties": self.parameters,
134
+ "required": ["tasks"],
135
+ },
136
+ }
137
+
138
+ async def execute(self, tasks: List[Dict[str, str]]) -> str:
139
+ """Execute parallel exploration tasks.
140
+
141
+ Args:
142
+ tasks: List of exploration tasks with 'aspect' and 'description' keys
143
+
144
+ Returns:
145
+ Combined exploration results as a string
146
+ """
147
+ if not tasks:
148
+ return "Error: No exploration tasks provided"
149
+
150
+ # Limit the number of parallel explorations
151
+ tasks = tasks[: self.MAX_PARALLEL_EXPLORATIONS]
152
+
153
+ # Get exploration-only tools
154
+ all_tools = self.agent.tool_executor.get_tool_schemas()
155
+ exploration_tools = [
156
+ t
157
+ for t in all_tools
158
+ if t.get("name") in self.EXPLORATION_TOOLS
159
+ or t.get("function", {}).get("name") in self.EXPLORATION_TOOLS
160
+ ]
161
+
162
+ # Run explorations in parallel
163
+ results = await self._run_parallel_explorations(tasks, exploration_tools)
164
+
165
+ # Format and return results
166
+ return self._format_results(results)
167
+
168
+ async def _run_parallel_explorations(
169
+ self, tasks: List[Dict[str, str]], tools: List[Dict[str, Any]]
170
+ ) -> Dict[str, str]:
171
+ """Run multiple exploration tasks in parallel.
172
+
173
+ Args:
174
+ tasks: List of exploration tasks
175
+ tools: Available exploration tools
176
+
177
+ Returns:
178
+ Dict mapping aspect names to results
179
+ """
180
+
181
+ async def run_single(task: Dict[str, str]) -> tuple:
182
+ aspect = task.get("aspect", "unknown")
183
+ description = task.get("description", "")
184
+ try:
185
+ result = await self._run_exploration(aspect, description, tools)
186
+ return aspect, result
187
+ except asyncio.CancelledError:
188
+ raise
189
+ except Exception as e:
190
+ return aspect, f"Exploration failed: {str(e)}"
191
+
192
+ # Use TaskGroup for parallel execution
193
+ # Since run_single catches all exceptions internally (except CancelledError),
194
+ # any ExceptionGroup raised here indicates cancellation which should propagate
195
+ results = {}
196
+ async with asyncio.TaskGroup() as tg:
197
+ task_list = [tg.create_task(run_single(t)) for t in tasks]
198
+
199
+ for task in task_list:
200
+ aspect, result = task.result()
201
+ results[aspect] = result
202
+
203
+ return results
204
+
205
+ async def _run_exploration(
206
+ self, aspect: str, description: str, tools: List[Dict[str, Any]]
207
+ ) -> str:
208
+ """Run a single exploration using isolated mini-loop.
209
+
210
+ Args:
211
+ aspect: The aspect being explored
212
+ description: Description of the exploration focus
213
+ tools: Available exploration tools
214
+
215
+ Returns:
216
+ Exploration result string
217
+ """
218
+ # Build exploration prompt
219
+ prompt = GENERAL_EXPLORER_PROMPT.format(aspect=aspect, description=description)
220
+
221
+ messages = [LLMMessage(role="user", content=prompt)]
222
+
223
+ # Run exploration in isolated context
224
+ result = await self.agent._react_loop(
225
+ messages=messages,
226
+ tools=tools,
227
+ use_memory=False, # Don't use main memory
228
+ save_to_memory=False, # Don't save to main memory
229
+ )
230
+
231
+ return result
232
+
233
+ def _format_results(self, results: Dict[str, str]) -> str:
234
+ """Format exploration results into a combined summary.
235
+
236
+ Args:
237
+ results: Dict mapping aspect names to result strings
238
+
239
+ Returns:
240
+ Formatted combined results
241
+ """
242
+ if not results:
243
+ return "No exploration results."
244
+
245
+ parts = ["# Exploration Results\n"]
246
+
247
+ for aspect, result in results.items():
248
+ # Truncate long results
249
+ if len(result) > self.MAX_RESULT_CHARS:
250
+ result = result[: self.MAX_RESULT_CHARS] + "... [truncated]"
251
+
252
+ parts.append(f"## {aspect}\n{result}\n")
253
+
254
+ return "\n".join(parts)
tools/file_ops.py ADDED
@@ -0,0 +1,150 @@
1
+ """File operation tools for reading, writing, and searching files."""
2
+
3
+ import asyncio
4
+ import glob
5
+ import os
6
+ from typing import Any, Dict
7
+
8
+ import aiofiles
9
+ import aiofiles.os
10
+
11
+ from .base import BaseTool
12
+
13
+
14
+ class FileReadTool(BaseTool):
15
+ """Read contents of a file from the filesystem."""
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ return "read_file"
20
+
21
+ @property
22
+ def description(self) -> str:
23
+ return (
24
+ "Read contents of a file. For large files, use offset and limit "
25
+ "parameters to read specific portions."
26
+ )
27
+
28
+ @property
29
+ def parameters(self) -> Dict[str, Any]:
30
+ return {
31
+ "file_path": {
32
+ "type": "string",
33
+ "description": "Path to the file to read",
34
+ },
35
+ "offset": {
36
+ "type": "integer",
37
+ "description": "Line number to start from (0-indexed). Default: 0",
38
+ },
39
+ "limit": {
40
+ "type": "integer",
41
+ "description": "Maximum number of lines to read. If not set, reads entire file.",
42
+ },
43
+ }
44
+
45
+ async def execute(self, file_path: str, offset: int = 0, limit: int = None) -> str:
46
+ """Read file with optional pagination."""
47
+ try:
48
+ # Pre-check file size
49
+ file_size = await aiofiles.os.path.getsize(file_path)
50
+ estimated_tokens = file_size // self.CHARS_PER_TOKEN
51
+
52
+ # If file too large and no pagination, return error
53
+ if estimated_tokens > self.MAX_TOKENS and limit is None:
54
+ return (
55
+ f"Error: File content (~{estimated_tokens} tokens) exceeds "
56
+ f"maximum allowed tokens ({self.MAX_TOKENS}). Please use offset "
57
+ f"and limit parameters to read specific portions of the file, "
58
+ f"or use grep_content to search for specific content."
59
+ )
60
+
61
+ async with aiofiles.open(file_path, encoding="utf-8") as f:
62
+ if limit is None:
63
+ return await f.read()
64
+ # Pagination mode
65
+ lines = await f.readlines()
66
+ total_lines = len(lines)
67
+ selected = lines[offset : offset + limit]
68
+ result = "".join(selected)
69
+ # Add context about total lines
70
+ if offset > 0 or offset + limit < total_lines:
71
+ result = f"[Lines {offset+1}-{min(offset+limit, total_lines)} of {total_lines}]\n{result}"
72
+ return result
73
+
74
+ except FileNotFoundError:
75
+ return f"Error: File '{file_path}' not found"
76
+ except Exception as e:
77
+ return f"Error reading file: {str(e)}"
78
+
79
+
80
+ class FileWriteTool(BaseTool):
81
+ """Write content to a file (creates or overwrites)."""
82
+
83
+ @property
84
+ def name(self) -> str:
85
+ return "write_file"
86
+
87
+ @property
88
+ def description(self) -> str:
89
+ return "Write content to a file (creates or overwrites)"
90
+
91
+ @property
92
+ def parameters(self) -> Dict[str, Any]:
93
+ return {
94
+ "file_path": {
95
+ "type": "string",
96
+ "description": "Path where to write the file",
97
+ },
98
+ "content": {
99
+ "type": "string",
100
+ "description": "Content to write to the file",
101
+ },
102
+ }
103
+
104
+ async def execute(self, file_path: str, content: str) -> str:
105
+ """Write content to file."""
106
+ try:
107
+ # Create directory if it doesn't exist
108
+ await aiofiles.os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
109
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
110
+ await f.write(content)
111
+ return f"Successfully wrote to {file_path}"
112
+ except Exception as e:
113
+ return f"Error writing file: {str(e)}"
114
+
115
+
116
+ class FileSearchTool(BaseTool):
117
+ """Search for files matching a pattern in a directory."""
118
+
119
+ @property
120
+ def name(self) -> str:
121
+ return "search_files"
122
+
123
+ @property
124
+ def description(self) -> str:
125
+ return "Search for files matching a pattern in a directory"
126
+
127
+ @property
128
+ def parameters(self) -> Dict[str, Any]:
129
+ return {
130
+ "directory": {
131
+ "type": "string",
132
+ "description": "Directory to search in (default: current directory)",
133
+ },
134
+ "pattern": {
135
+ "type": "string",
136
+ "description": "File name pattern (e.g., '*.py', 'test_*')",
137
+ },
138
+ }
139
+
140
+ async def execute(self, directory: str = ".", pattern: str = "*") -> str:
141
+ """Search for files matching pattern."""
142
+ try:
143
+ search_path = os.path.join(directory, "**", pattern)
144
+ files = await asyncio.to_thread(lambda: glob.glob(search_path, recursive=True))
145
+ if files:
146
+ return "\n".join(files)
147
+ else:
148
+ return "No files found matching pattern"
149
+ except Exception as e:
150
+ return f"Error searching files: {str(e)}"