aloop 0.1.1__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.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/prompts/__init__.py +1 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.1.dist-info/METADATA +252 -0
- aloop-0.1.1.dist-info/RECORD +66 -0
- aloop-0.1.1.dist-info/WHEEL +5 -0
- aloop-0.1.1.dist-info/entry_points.txt +2 -0
- aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.1.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/store/__init__.py +6 -0
- memory/store/memory_store.py +100 -0
- memory/store/yaml_file_memory_store.py +414 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- 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)}"
|