tunacode-cli 0.0.29__py3-none-any.whl → 0.0.31__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 tunacode-cli might be problematic. Click here for more details.
- api/auth.py +13 -0
- api/users.py +8 -0
- tunacode/cli/commands.py +115 -233
- tunacode/cli/repl.py +53 -63
- tunacode/cli/textual_bridge.py +4 -1
- tunacode/constants.py +10 -1
- tunacode/core/agents/__init__.py +0 -4
- tunacode/core/agents/main.py +454 -49
- tunacode/core/code_index.py +479 -0
- tunacode/core/setup/git_safety_setup.py +7 -9
- tunacode/core/state.py +5 -0
- tunacode/core/tool_handler.py +18 -0
- tunacode/exceptions.py +13 -0
- tunacode/prompts/system.md +269 -30
- tunacode/tools/glob.py +288 -0
- tunacode/tools/grep.py +168 -195
- tunacode/tools/list_dir.py +190 -0
- tunacode/tools/read_file.py +9 -3
- tunacode/tools/read_file_async_poc.py +188 -0
- tunacode/utils/text_utils.py +14 -5
- tunacode/utils/token_counter.py +23 -0
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/METADATA +16 -7
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/RECORD +27 -24
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/top_level.txt +1 -0
- tunacode/core/agents/orchestrator.py +0 -213
- tunacode/core/agents/planner_schema.py +0 -9
- tunacode/core/agents/readonly.py +0 -65
- tunacode/core/llm/planner.py +0 -62
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.29.dist-info → tunacode_cli-0.0.31.dist-info}/licenses/LICENSE +0 -0
tunacode/tools/read_file.py
CHANGED
|
@@ -5,6 +5,7 @@ File reading tool for agent operations in the TunaCode application.
|
|
|
5
5
|
Provides safe file reading with size limits and proper error handling.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
8
9
|
import os
|
|
9
10
|
|
|
10
11
|
from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
|
|
@@ -40,9 +41,14 @@ class ReadFileTool(FileBasedTool):
|
|
|
40
41
|
await self.ui.error(err_msg)
|
|
41
42
|
raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=None)
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
# Run the blocking file I/O in a separate thread to avoid blocking the event loop
|
|
45
|
+
def _read_sync(path: str) -> str:
|
|
46
|
+
"""Synchronous helper to read file contents (runs in thread)."""
|
|
47
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
48
|
+
return f.read()
|
|
49
|
+
|
|
50
|
+
content: str = await asyncio.to_thread(_read_sync, filepath)
|
|
51
|
+
return content
|
|
46
52
|
|
|
47
53
|
async def _handle_error(self, error: Exception, filepath: str = None) -> ToolResult:
|
|
48
54
|
"""Handle errors with specific messages for common cases.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proof of Concept: Async-optimized read_file tool
|
|
3
|
+
|
|
4
|
+
This demonstrates how we can make the read_file tool truly async
|
|
5
|
+
by using asyncio.to_thread (Python 3.9+) or run_in_executor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
|
|
15
|
+
ERROR_FILE_TOO_LARGE, MAX_FILE_SIZE, MSG_FILE_SIZE_LIMIT)
|
|
16
|
+
from tunacode.exceptions import ToolExecutionError
|
|
17
|
+
from tunacode.tools.base import FileBasedTool
|
|
18
|
+
from tunacode.types import ToolResult
|
|
19
|
+
|
|
20
|
+
# Shared thread pool for I/O operations
|
|
21
|
+
# This avoids creating multiple thread pools
|
|
22
|
+
_IO_THREAD_POOL: Optional[ThreadPoolExecutor] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_io_thread_pool() -> ThreadPoolExecutor:
|
|
26
|
+
"""Get or create the shared I/O thread pool."""
|
|
27
|
+
global _IO_THREAD_POOL
|
|
28
|
+
if _IO_THREAD_POOL is None:
|
|
29
|
+
max_workers = min(32, (os.cpu_count() or 1) * 4)
|
|
30
|
+
_IO_THREAD_POOL = ThreadPoolExecutor(
|
|
31
|
+
max_workers=max_workers, thread_name_prefix="tunacode-io"
|
|
32
|
+
)
|
|
33
|
+
return _IO_THREAD_POOL
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncReadFileTool(FileBasedTool):
|
|
37
|
+
"""Async-optimized tool for reading file contents."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def tool_name(self) -> str:
|
|
41
|
+
return "Read"
|
|
42
|
+
|
|
43
|
+
async def _execute(self, filepath: str) -> ToolResult:
|
|
44
|
+
"""Read the contents of a file asynchronously.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
filepath: The path to the file to read.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ToolResult: The contents of the file or an error message.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
Exception: Any file reading errors
|
|
54
|
+
"""
|
|
55
|
+
# Check file size first (this is fast)
|
|
56
|
+
try:
|
|
57
|
+
file_size = os.path.getsize(filepath)
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
60
|
+
|
|
61
|
+
if file_size > MAX_FILE_SIZE:
|
|
62
|
+
err_msg = ERROR_FILE_TOO_LARGE.format(filepath=filepath) + MSG_FILE_SIZE_LIMIT
|
|
63
|
+
if self.ui:
|
|
64
|
+
await self.ui.error(err_msg)
|
|
65
|
+
raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=None)
|
|
66
|
+
|
|
67
|
+
# Read file asynchronously
|
|
68
|
+
content = await self._read_file_async(filepath)
|
|
69
|
+
return content
|
|
70
|
+
|
|
71
|
+
async def _read_file_async(self, filepath: str) -> str:
|
|
72
|
+
"""Read file contents without blocking the event loop."""
|
|
73
|
+
|
|
74
|
+
# Method 1: Using asyncio.to_thread (Python 3.9+)
|
|
75
|
+
if sys.version_info >= (3, 9):
|
|
76
|
+
|
|
77
|
+
def _read_sync():
|
|
78
|
+
with open(filepath, "r", encoding="utf-8") as file:
|
|
79
|
+
return file.read()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
return await asyncio.to_thread(_read_sync)
|
|
83
|
+
except Exception:
|
|
84
|
+
# Re-raise to be handled by _handle_error
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
# Method 2: Using run_in_executor (older Python versions)
|
|
88
|
+
else:
|
|
89
|
+
|
|
90
|
+
def _read_sync(path):
|
|
91
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
92
|
+
return file.read()
|
|
93
|
+
|
|
94
|
+
loop = asyncio.get_event_loop()
|
|
95
|
+
executor = get_io_thread_pool()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
return await loop.run_in_executor(executor, _read_sync, filepath)
|
|
99
|
+
except Exception:
|
|
100
|
+
# Re-raise to be handled by _handle_error
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
async def _handle_error(self, error: Exception, filepath: str = None) -> ToolResult:
|
|
104
|
+
"""Handle errors with specific messages for common cases.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ToolExecutionError: Always raised with structured error information
|
|
108
|
+
"""
|
|
109
|
+
if isinstance(error, FileNotFoundError):
|
|
110
|
+
err_msg = ERROR_FILE_NOT_FOUND.format(filepath=filepath)
|
|
111
|
+
elif isinstance(error, UnicodeDecodeError):
|
|
112
|
+
err_msg = (
|
|
113
|
+
ERROR_FILE_DECODE.format(filepath=filepath)
|
|
114
|
+
+ " "
|
|
115
|
+
+ ERROR_FILE_DECODE_DETAILS.format(error=error)
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# Use parent class handling for other errors
|
|
119
|
+
await super()._handle_error(error, filepath)
|
|
120
|
+
return # super() will raise, this is unreachable
|
|
121
|
+
|
|
122
|
+
if self.ui:
|
|
123
|
+
await self.ui.error(err_msg)
|
|
124
|
+
|
|
125
|
+
raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=error)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Create the async function that maintains the existing interface
|
|
129
|
+
async def read_file_async(filepath: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Read the contents of a file asynchronously without blocking the event loop.
|
|
132
|
+
|
|
133
|
+
This implementation uses thread pool execution to avoid blocking during file I/O,
|
|
134
|
+
allowing true parallel execution of multiple file reads.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
filepath: The path to the file to read.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
str: The contents of the file or an error message.
|
|
141
|
+
"""
|
|
142
|
+
tool = AsyncReadFileTool(None) # No UI for pydantic-ai compatibility
|
|
143
|
+
try:
|
|
144
|
+
return await tool.execute(filepath)
|
|
145
|
+
except ToolExecutionError as e:
|
|
146
|
+
# Return error message for pydantic-ai compatibility
|
|
147
|
+
return str(e)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Benchmarking utilities for testing
|
|
151
|
+
async def benchmark_read_performance():
|
|
152
|
+
"""Benchmark the performance difference between sync and async reads."""
|
|
153
|
+
import time
|
|
154
|
+
|
|
155
|
+
from tunacode.tools.read_file import read_file as read_file_sync
|
|
156
|
+
|
|
157
|
+
# Create some test files
|
|
158
|
+
test_files = []
|
|
159
|
+
for i in range(10):
|
|
160
|
+
filepath = f"/tmp/test_file_{i}.txt"
|
|
161
|
+
with open(filepath, "w") as f:
|
|
162
|
+
f.write("x" * 10000) # 10KB file
|
|
163
|
+
test_files.append(filepath)
|
|
164
|
+
|
|
165
|
+
# Test synchronous reads (sequential)
|
|
166
|
+
start_time = time.time()
|
|
167
|
+
for filepath in test_files:
|
|
168
|
+
await read_file_sync(filepath)
|
|
169
|
+
sync_time = time.time() - start_time
|
|
170
|
+
|
|
171
|
+
# Test async reads (parallel)
|
|
172
|
+
start_time = time.time()
|
|
173
|
+
tasks = [read_file_async(filepath) for filepath in test_files]
|
|
174
|
+
await asyncio.gather(*tasks)
|
|
175
|
+
async_time = time.time() - start_time
|
|
176
|
+
|
|
177
|
+
# Cleanup
|
|
178
|
+
for filepath in test_files:
|
|
179
|
+
os.unlink(filepath)
|
|
180
|
+
|
|
181
|
+
print(f"Synchronous reads: {sync_time:.3f}s")
|
|
182
|
+
print(f"Async reads: {async_time:.3f}s")
|
|
183
|
+
print(f"Speedup: {sync_time / async_time:.2f}x")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
# Run benchmark when executed directly
|
|
188
|
+
asyncio.run(benchmark_read_performance())
|
tunacode/utils/text_utils.py
CHANGED
|
@@ -6,7 +6,7 @@ Includes file extension to language mapping and key formatting functions.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
-
from typing import Set
|
|
9
|
+
from typing import List, Set, Tuple
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def key_to_title(key: str, uppercase_words: Set[str] = None) -> str:
|
|
@@ -50,14 +50,16 @@ def ext_to_lang(path: str) -> str:
|
|
|
50
50
|
return "text"
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def expand_file_refs(text: str) -> str:
|
|
53
|
+
def expand_file_refs(text: str) -> Tuple[str, List[str]]:
|
|
54
54
|
"""Expand @file references with file contents wrapped in code fences.
|
|
55
55
|
|
|
56
56
|
Args:
|
|
57
57
|
text: The input text potentially containing @file references.
|
|
58
58
|
|
|
59
59
|
Returns:
|
|
60
|
-
|
|
60
|
+
Tuple[str, List[str]]: A tuple containing:
|
|
61
|
+
- Text with any @file references replaced by the file's contents
|
|
62
|
+
- List of absolute paths of files that were successfully expanded
|
|
61
63
|
|
|
62
64
|
Raises:
|
|
63
65
|
ValueError: If a referenced file does not exist or is too large.
|
|
@@ -69,6 +71,7 @@ def expand_file_refs(text: str) -> str:
|
|
|
69
71
|
MSG_FILE_SIZE_LIMIT)
|
|
70
72
|
|
|
71
73
|
pattern = re.compile(r"@([\w./_-]+)")
|
|
74
|
+
expanded_files = []
|
|
72
75
|
|
|
73
76
|
def replacer(match: re.Match) -> str:
|
|
74
77
|
path = match.group(1)
|
|
@@ -81,7 +84,13 @@ def expand_file_refs(text: str) -> str:
|
|
|
81
84
|
with open(path, "r", encoding="utf-8") as f:
|
|
82
85
|
content = f.read()
|
|
83
86
|
|
|
87
|
+
# Track the absolute path of the file
|
|
88
|
+
abs_path = os.path.abspath(path)
|
|
89
|
+
expanded_files.append(abs_path)
|
|
90
|
+
|
|
84
91
|
lang = ext_to_lang(path)
|
|
85
|
-
|
|
92
|
+
# Add clear headers to indicate this is a file reference, not code to write
|
|
93
|
+
return f"\n=== FILE REFERENCE: {path} ===\n```{lang}\n{content}\n```\n=== END FILE REFERENCE: {path} ===\n"
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
expanded_text = pattern.sub(replacer, text)
|
|
96
|
+
return expanded_text, expanded_files
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Simple token counting utility for estimating message sizes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def estimate_tokens(text: str) -> int:
|
|
5
|
+
"""
|
|
6
|
+
Estimate token count using a simple character-based approximation.
|
|
7
|
+
|
|
8
|
+
This is a rough estimate: ~4 characters per token on average.
|
|
9
|
+
For more accurate counting, we would need tiktoken or similar.
|
|
10
|
+
"""
|
|
11
|
+
if not text:
|
|
12
|
+
return 0
|
|
13
|
+
|
|
14
|
+
# Simple approximation: ~4 characters per token
|
|
15
|
+
# This is roughly accurate for English text
|
|
16
|
+
return len(text) // 4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_token_count(count: int) -> str:
|
|
20
|
+
"""Format token count for display."""
|
|
21
|
+
if count >= 1000:
|
|
22
|
+
return f"{count:,}"
|
|
23
|
+
return str(count)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.31
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Author-email: larock22 <noreply@github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,7 @@ Requires-Dist: flake8; extra == "dev"
|
|
|
31
31
|
Requires-Dist: isort; extra == "dev"
|
|
32
32
|
Requires-Dist: pytest; extra == "dev"
|
|
33
33
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
34
35
|
Requires-Dist: textual-dev; extra == "dev"
|
|
35
36
|
Dynamic: license-file
|
|
36
37
|
|
|
@@ -44,7 +45,7 @@ Dynamic: license-file
|
|
|
44
45
|
|
|
45
46
|
**AI-powered CLI coding assistant**
|
|
46
47
|
|
|
47
|
-

|
|
48
|
+

|
|
48
49
|
|
|
49
50
|
</div>
|
|
50
51
|
|
|
@@ -75,7 +76,7 @@ tunacode --model "anthropic:claude-3.5-sonnet" --key "sk-ant-your-anthropic-key"
|
|
|
75
76
|
tunacode --model "openrouter:openai/gpt-4o" --key "sk-or-your-openrouter-key"
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
Your config is saved to `~/.config/tunacode.json`
|
|
79
|
+
Your config is saved to `~/.config/tunacode.json` (edit directly with `nvim ~/.config/tunacode.json`)
|
|
79
80
|
|
|
80
81
|
## Start Coding
|
|
81
82
|
|
|
@@ -96,6 +97,14 @@ tunacode
|
|
|
96
97
|
| `!<command>` | Run shell command |
|
|
97
98
|
| `exit` | Exit TunaCode |
|
|
98
99
|
|
|
100
|
+
## Performance
|
|
101
|
+
|
|
102
|
+
TunaCode leverages parallel execution for read-only operations, achieving **3x faster** file operations:
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
Multiple file reads, directory listings, and searches execute concurrently using async I/O, making code exploration significantly faster.
|
|
107
|
+
|
|
99
108
|
## Safety First
|
|
100
109
|
|
|
101
110
|
⚠️ **Important**: TunaCode can modify your codebase. Always:
|
|
@@ -105,10 +114,10 @@ tunacode
|
|
|
105
114
|
|
|
106
115
|
## Documentation
|
|
107
116
|
|
|
108
|
-
- [**Features**](
|
|
109
|
-
- [**Advanced Configuration**](
|
|
110
|
-
- [**Architecture**](
|
|
111
|
-
- [**Development**](
|
|
117
|
+
- [**Features**](docs/FEATURES.md) - All features, tools, and commands
|
|
118
|
+
- [**Advanced Configuration**](docs/ADVANCED-CONFIG.md) - Provider setup, MCP, customization
|
|
119
|
+
- [**Architecture**](docs/ARCHITECTURE.md) - Source code organization and design
|
|
120
|
+
- [**Development**](docs/DEVELOPMENT.md) - Contributing and development setup
|
|
112
121
|
|
|
113
122
|
## Links
|
|
114
123
|
|
|
@@ -1,47 +1,49 @@
|
|
|
1
|
+
api/auth.py,sha256=_ysF1RCXvtJR1S35lbYQZexES1lif4J6VVzEyqNK74Q,303
|
|
2
|
+
api/users.py,sha256=WRcy1Vsr4cEC8CW2qeN3XrA9EMyIk47ufpMyvQ4nLuw,193
|
|
1
3
|
tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
tunacode/constants.py,sha256=
|
|
4
|
+
tunacode/constants.py,sha256=PMXSbKvpMm9j7EjXpaaFmvBXKdptNi3IfV4E-bxQmBM,4074
|
|
3
5
|
tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
|
|
4
|
-
tunacode/exceptions.py,sha256=
|
|
6
|
+
tunacode/exceptions.py,sha256=mTWXuWyr1k16CGLWN2tsthDGi7lbx1JK0ekIqogYDP8,3105
|
|
5
7
|
tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
8
|
tunacode/setup.py,sha256=dYn0NeAxtNIDSogWEmGSyjb9wsr8AonZ8vAo5sw9NIw,1909
|
|
7
9
|
tunacode/types.py,sha256=BciT-uxnQ44iC-4QiDY72OD23LOtqSyMOuK_N0ttlaA,7676
|
|
8
10
|
tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
|
|
9
|
-
tunacode/cli/commands.py,sha256=
|
|
11
|
+
tunacode/cli/commands.py,sha256=w1bqLuxkKoavvXrpARFi5qg3yfYx_MA-1COyPj9RJGw,29884
|
|
10
12
|
tunacode/cli/main.py,sha256=PIcFnfmIoI_pmK2y-zB_ouJbzR5fbSI7zsKQNPB_J8o,2406
|
|
11
|
-
tunacode/cli/repl.py,sha256=
|
|
13
|
+
tunacode/cli/repl.py,sha256=AYVU-k8yKa8LjJg3s3vu5LrHyMUIShtgUlyakwb-rh4,13583
|
|
12
14
|
tunacode/cli/textual_app.py,sha256=14-Nt0IIETmyHBrNn9uwSF3EwCcutwTp6gdoKgNm0sY,12593
|
|
13
|
-
tunacode/cli/textual_bridge.py,sha256=
|
|
15
|
+
tunacode/cli/textual_bridge.py,sha256=LvqiTtF0hu3gNujzpKaW9h-m6xzEP3OH2M8KL2pCwRc,6333
|
|
14
16
|
tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
|
|
15
17
|
tunacode/configuration/defaults.py,sha256=oLgmHprB3cTaFvT9dn_rgg206zoj09GRXRbI7MYijxA,801
|
|
16
18
|
tunacode/configuration/models.py,sha256=XPobkLM_TzKTuMIWhK-svJfGRGFT9r2LhKEM6rv6QHk,3756
|
|
17
19
|
tunacode/configuration/settings.py,sha256=lm2ov8rG1t4C5JIXMOhIKik5FAsjpuLVYtFmnE1ZQ3k,995
|
|
18
20
|
tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
tunacode/core/
|
|
20
|
-
tunacode/core/
|
|
21
|
-
tunacode/core/
|
|
22
|
-
tunacode/core/agents/
|
|
23
|
-
tunacode/core/agents/
|
|
24
|
-
tunacode/core/agents/planner_schema.py,sha256=pu2ehQVALjiJ5HJD7EN6xuZHCknsrXO9z0xHuVdlKW8,345
|
|
25
|
-
tunacode/core/agents/readonly.py,sha256=jOG3CF5G1y9k4sBn7ChXN1GXWbmB_0pFGcaMMI4iaLs,2325
|
|
21
|
+
tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,15661
|
|
22
|
+
tunacode/core/state.py,sha256=PHGCGjx_X03I5jO-T1JkREQm4cwYEXQty59JJlnk24c,1608
|
|
23
|
+
tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
|
|
24
|
+
tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
|
|
25
|
+
tunacode/core/agents/main.py,sha256=HuDVLFp18MWONjrAtAPZgigVpAGB8KmbA-Idx0ElAQs,37736
|
|
26
26
|
tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
|
|
28
28
|
tunacode/core/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
tunacode/core/llm/planner.py,sha256=G1oxRFynYisWspcWr6KvVuiWHmElozhhVds6Wj4yrLk,2111
|
|
30
29
|
tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
|
|
31
30
|
tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
|
|
32
31
|
tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
|
|
33
32
|
tunacode/core/setup/config_setup.py,sha256=A1MEbkZq2FvVg9FNlLD9_JO_vlr0ixP-6Ay0-2W7VvQ,14330
|
|
34
33
|
tunacode/core/setup/coordinator.py,sha256=oVTN2xIeJERXitVJpkIk9tDGLs1D1bxIRmaogJwZJFI,2049
|
|
35
34
|
tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
|
|
36
|
-
tunacode/core/setup/git_safety_setup.py,sha256=
|
|
37
|
-
tunacode/prompts/system.md,sha256=
|
|
35
|
+
tunacode/core/setup/git_safety_setup.py,sha256=CRIqrQt0QUJQRS344njty_iCqTorrDhHlXRuET7w0Tk,6714
|
|
36
|
+
tunacode/prompts/system.md,sha256=qnTXKEVsQY7o54B2kFNsdkbbCHhxdqVXytUQbhKy7PM,12027
|
|
38
37
|
tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
|
|
39
38
|
tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
|
|
40
39
|
tunacode/tools/__init__.py,sha256=ECBuUWWF1JjHW42CCceaPKgVTQyuljbz3RlhuA2fe2s,314
|
|
41
40
|
tunacode/tools/base.py,sha256=TF71ZE66-W-7GLY8QcPpPJ5CVjod6FHL1caBOTCssvU,7044
|
|
42
41
|
tunacode/tools/bash.py,sha256=mgZqugfDFevZ4BETuUv90pzXvtq7qKGUGFiuDxzmytk,8766
|
|
43
|
-
tunacode/tools/
|
|
44
|
-
tunacode/tools/
|
|
42
|
+
tunacode/tools/glob.py,sha256=TSgVK79ewZgGw8ucYkkiHgVqRgkw-wZrhP8j52nm_gQ,10334
|
|
43
|
+
tunacode/tools/grep.py,sha256=jboEVA2ATv0YI8zg9dF89emZ_HWy2vVtsQ_-hDhlr7g,26337
|
|
44
|
+
tunacode/tools/list_dir.py,sha256=1kNqzYCNlcA5rqXIEVqcjQy6QxlLZLj5AG6YIECfwio,7217
|
|
45
|
+
tunacode/tools/read_file.py,sha256=BqHxPspZBYotz5wtjuP-zve61obsx98z5TU-aw5BJHg,3273
|
|
46
|
+
tunacode/tools/read_file_async_poc.py,sha256=0rSfYCmoNcvWk8hB1z86l32-tomSc9yOM4tR4nrty_o,6267
|
|
45
47
|
tunacode/tools/run_command.py,sha256=kYg_Re397OmZdKtUSjpNfYYNDBjd0vsS1xMK0yP181I,3776
|
|
46
48
|
tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
|
|
47
49
|
tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
|
|
@@ -65,11 +67,12 @@ tunacode/utils/file_utils.py,sha256=AXiAJ_idtlmXEi9pMvwtfPy9Ys3yK-F4K7qb_NpwonU,
|
|
|
65
67
|
tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
|
|
66
68
|
tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
|
|
67
69
|
tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
|
|
68
|
-
tunacode/utils/text_utils.py,sha256=
|
|
70
|
+
tunacode/utils/text_utils.py,sha256=zRBaorvtyd7HBEWtIfCH1Wce1L6rhsQwpORUEGBFMjA,2981
|
|
71
|
+
tunacode/utils/token_counter.py,sha256=nGCWwrHHFbKywqeDCEuJnADCkfJuzysWiB6cCltJOKI,648
|
|
69
72
|
tunacode/utils/user_configuration.py,sha256=IGvUH37wWtZ4M5xpukZEWYhtuKKyKcl6DaeObGXdleU,2610
|
|
70
|
-
tunacode_cli-0.0.
|
|
71
|
-
tunacode_cli-0.0.
|
|
72
|
-
tunacode_cli-0.0.
|
|
73
|
-
tunacode_cli-0.0.
|
|
74
|
-
tunacode_cli-0.0.
|
|
75
|
-
tunacode_cli-0.0.
|
|
73
|
+
tunacode_cli-0.0.31.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
74
|
+
tunacode_cli-0.0.31.dist-info/METADATA,sha256=xVgvlGILVePW9LUKCUCgYslTMzxrkSFvPcgu2kfrsGg,4023
|
|
75
|
+
tunacode_cli-0.0.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
76
|
+
tunacode_cli-0.0.31.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
77
|
+
tunacode_cli-0.0.31.dist-info/top_level.txt,sha256=GuU751acRvOhM5yLKFW0-gBg62JGh5zycDSq4tRFOYE,13
|
|
78
|
+
tunacode_cli-0.0.31.dist-info/RECORD,,
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
"""Agent orchestration scaffolding.
|
|
2
|
-
|
|
3
|
-
This module defines an ``OrchestratorAgent`` class that demonstrates how
|
|
4
|
-
higher level planning and delegation could be layered on top of the existing
|
|
5
|
-
``process_request`` workflow. The goal is to keep orchestration logic isolated
|
|
6
|
-
from the core agent implementation while reusing all current tooling and state
|
|
7
|
-
handling provided by ``main.process_request``.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import itertools
|
|
14
|
-
from typing import List
|
|
15
|
-
|
|
16
|
-
from ...types import AgentRun, FallbackResponse, ModelName, ResponseState
|
|
17
|
-
from ..llm.planner import make_plan
|
|
18
|
-
from ..state import StateManager
|
|
19
|
-
from . import main as agent_main
|
|
20
|
-
from .planner_schema import Task
|
|
21
|
-
from .readonly import ReadOnlyAgent
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class OrchestratorAgent:
|
|
25
|
-
"""Plan and run a sequence of sub-agent tasks."""
|
|
26
|
-
|
|
27
|
-
def __init__(self, state_manager: StateManager):
|
|
28
|
-
self.state = state_manager
|
|
29
|
-
|
|
30
|
-
async def plan(self, request: str, model: ModelName) -> List[Task]:
|
|
31
|
-
"""Plan tasks for a user request using the planner LLM."""
|
|
32
|
-
|
|
33
|
-
return await make_plan(request, model, self.state)
|
|
34
|
-
|
|
35
|
-
async def _run_sub_task(self, task: Task, model: ModelName) -> AgentRun:
|
|
36
|
-
"""Execute a single task using an appropriate sub-agent."""
|
|
37
|
-
from rich.console import Console
|
|
38
|
-
|
|
39
|
-
console = Console()
|
|
40
|
-
|
|
41
|
-
# Show which task is being executed
|
|
42
|
-
task_type = "WRITE" if task.mutate else "READ"
|
|
43
|
-
console.print(f"\n[dim][Task {task.id}] {task_type}[/dim]")
|
|
44
|
-
console.print(f"[dim] → {task.description}[/dim]")
|
|
45
|
-
|
|
46
|
-
if task.mutate:
|
|
47
|
-
agent_main.get_or_create_agent(model, self.state)
|
|
48
|
-
result = await agent_main.process_request(model, task.description, self.state)
|
|
49
|
-
else:
|
|
50
|
-
agent = ReadOnlyAgent(model, self.state)
|
|
51
|
-
result = await agent.process_request(task.description)
|
|
52
|
-
|
|
53
|
-
console.print(f"[dim][Task {task.id}] Complete[/dim]")
|
|
54
|
-
return result
|
|
55
|
-
|
|
56
|
-
async def run(self, request: str, model: ModelName | None = None) -> List[AgentRun]:
|
|
57
|
-
"""Plan and execute a user request.
|
|
58
|
-
|
|
59
|
-
Parameters
|
|
60
|
-
----------
|
|
61
|
-
request:
|
|
62
|
-
The high level user request to process.
|
|
63
|
-
model:
|
|
64
|
-
Optional model name to use for sub agents. Defaults to the current
|
|
65
|
-
session model.
|
|
66
|
-
"""
|
|
67
|
-
from rich.console import Console
|
|
68
|
-
|
|
69
|
-
console = Console()
|
|
70
|
-
model = model or self.state.session.current_model
|
|
71
|
-
|
|
72
|
-
# Track response state across all sub-tasks
|
|
73
|
-
response_state = ResponseState()
|
|
74
|
-
|
|
75
|
-
# Show orchestrator is starting
|
|
76
|
-
console.print(
|
|
77
|
-
"\n[cyan]Orchestrator Mode: Analyzing request and creating execution plan...[/cyan]"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
tasks = await self.plan(request, model)
|
|
81
|
-
|
|
82
|
-
# Show execution is starting
|
|
83
|
-
console.print(f"\n[cyan]Executing plan with {len(tasks)} tasks...[/cyan]")
|
|
84
|
-
|
|
85
|
-
results: List[AgentRun] = []
|
|
86
|
-
task_progress = []
|
|
87
|
-
|
|
88
|
-
for mutate_flag, group in itertools.groupby(tasks, key=lambda t: t.mutate):
|
|
89
|
-
if mutate_flag:
|
|
90
|
-
for t in group:
|
|
91
|
-
result = await self._run_sub_task(t, model)
|
|
92
|
-
results.append(result)
|
|
93
|
-
|
|
94
|
-
# Track task progress
|
|
95
|
-
task_progress.append(
|
|
96
|
-
{
|
|
97
|
-
"task": t,
|
|
98
|
-
"completed": True,
|
|
99
|
-
"had_output": hasattr(result, "result")
|
|
100
|
-
and result.result
|
|
101
|
-
and getattr(result.result, "output", None),
|
|
102
|
-
}
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
# Check if this task produced user-visible output
|
|
106
|
-
if hasattr(result, "response_state"):
|
|
107
|
-
response_state.has_user_response |= result.response_state.has_user_response
|
|
108
|
-
else:
|
|
109
|
-
# Show parallel execution
|
|
110
|
-
task_list = list(group)
|
|
111
|
-
if len(task_list) > 1:
|
|
112
|
-
console.print(
|
|
113
|
-
f"\n[dim][Parallel Execution] Running {len(task_list)} read-only tasks concurrently...[/dim]"
|
|
114
|
-
)
|
|
115
|
-
coros = [self._run_sub_task(t, model) for t in task_list]
|
|
116
|
-
parallel_results = await asyncio.gather(*coros)
|
|
117
|
-
results.extend(parallel_results)
|
|
118
|
-
|
|
119
|
-
# Track parallel task progress
|
|
120
|
-
for t, result in zip(task_list, parallel_results):
|
|
121
|
-
task_progress.append(
|
|
122
|
-
{
|
|
123
|
-
"task": t,
|
|
124
|
-
"completed": True,
|
|
125
|
-
"had_output": hasattr(result, "result")
|
|
126
|
-
and result.result
|
|
127
|
-
and getattr(result.result, "output", None),
|
|
128
|
-
}
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
# Check if this task produced user-visible output
|
|
132
|
-
if hasattr(result, "response_state"):
|
|
133
|
-
response_state.has_user_response |= result.response_state.has_user_response
|
|
134
|
-
|
|
135
|
-
console.print("\n[green]Orchestrator completed all tasks successfully![/green]")
|
|
136
|
-
|
|
137
|
-
# Check if we need a fallback response
|
|
138
|
-
has_any_output = any(
|
|
139
|
-
hasattr(r, "result") and r.result and getattr(r.result, "output", None) for r in results
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
fallback_enabled = self.state.session.user_config.get("settings", {}).get(
|
|
143
|
-
"fallback_response", True
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Use has_any_output as the primary check since response_state might not be set for all agents
|
|
147
|
-
if not has_any_output and fallback_enabled:
|
|
148
|
-
# Generate a detailed fallback response
|
|
149
|
-
completed_count = sum(1 for tp in task_progress if tp["completed"])
|
|
150
|
-
output_count = sum(1 for tp in task_progress if tp["had_output"])
|
|
151
|
-
|
|
152
|
-
fallback = FallbackResponse(
|
|
153
|
-
summary="Orchestrator completed all tasks but no final response was generated.",
|
|
154
|
-
progress=f"Executed {completed_count}/{len(tasks)} tasks successfully",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Add task details based on verbosity
|
|
158
|
-
verbosity = self.state.session.user_config.get("settings", {}).get(
|
|
159
|
-
"fallback_verbosity", "normal"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
if verbosity in ["normal", "detailed"]:
|
|
163
|
-
# List what was done
|
|
164
|
-
if task_progress:
|
|
165
|
-
fallback.issues.append(f"Tasks executed: {completed_count}")
|
|
166
|
-
if output_count == 0:
|
|
167
|
-
fallback.issues.append("No tasks produced visible output")
|
|
168
|
-
|
|
169
|
-
if verbosity == "detailed":
|
|
170
|
-
# Add task descriptions
|
|
171
|
-
for i, tp in enumerate(task_progress, 1):
|
|
172
|
-
task_type = "WRITE" if tp["task"].mutate else "READ"
|
|
173
|
-
status = "✓" if tp["completed"] else "✗"
|
|
174
|
-
fallback.issues.append(
|
|
175
|
-
f"{status} Task {i} [{task_type}]: {tp['task'].description}"
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
# Suggest next steps
|
|
179
|
-
fallback.next_steps.append("Review the task execution above for any errors")
|
|
180
|
-
fallback.next_steps.append(
|
|
181
|
-
"Try running individual tasks separately for more detailed output"
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Create synthesized response
|
|
185
|
-
synthesis_parts = [fallback.summary, ""]
|
|
186
|
-
|
|
187
|
-
if fallback.progress:
|
|
188
|
-
synthesis_parts.append(f"Progress: {fallback.progress}")
|
|
189
|
-
|
|
190
|
-
if fallback.issues:
|
|
191
|
-
synthesis_parts.append("\nDetails:")
|
|
192
|
-
synthesis_parts.extend(f" • {issue}" for issue in fallback.issues)
|
|
193
|
-
|
|
194
|
-
if fallback.next_steps:
|
|
195
|
-
synthesis_parts.append("\nNext steps:")
|
|
196
|
-
synthesis_parts.extend(f" • {step}" for step in fallback.next_steps)
|
|
197
|
-
|
|
198
|
-
synthesis = "\n".join(synthesis_parts)
|
|
199
|
-
|
|
200
|
-
class FallbackResult:
|
|
201
|
-
def __init__(self, output: str, response_state: ResponseState):
|
|
202
|
-
self.output = output
|
|
203
|
-
self.response_state = response_state
|
|
204
|
-
|
|
205
|
-
class FallbackRun:
|
|
206
|
-
def __init__(self, synthesis: str, response_state: ResponseState):
|
|
207
|
-
self.result = FallbackResult(synthesis, response_state)
|
|
208
|
-
self.response_state = response_state
|
|
209
|
-
|
|
210
|
-
response_state.has_final_synthesis = True
|
|
211
|
-
results.append(FallbackRun(synthesis, response_state))
|
|
212
|
-
|
|
213
|
-
return results
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Task(BaseModel):
|
|
5
|
-
"""Single sub-task generated by the planner."""
|
|
6
|
-
|
|
7
|
-
id: int = Field(..., description="1-based task index in execution order")
|
|
8
|
-
description: str = Field(..., description="What the sub-agent must do")
|
|
9
|
-
mutate: bool = Field(..., description="True if the task changes code")
|