tunacode-cli 0.0.30__py3-none-any.whl → 0.0.32__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 +113 -232
- tunacode/cli/repl.py +40 -84
- tunacode/constants.py +10 -1
- tunacode/core/agents/__init__.py +0 -4
- tunacode/core/agents/main.py +345 -43
- tunacode/core/code_index.py +479 -0
- tunacode/core/setup/git_safety_setup.py +7 -9
- tunacode/core/tool_handler.py +18 -0
- tunacode/exceptions.py +13 -0
- tunacode/prompts/system.md +237 -28
- 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_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/METADATA +16 -7
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/RECORD +23 -21
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.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.30.dist-info → tunacode_cli-0.0.32.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.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())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.32
|
|
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,14 +1,16 @@
|
|
|
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=HQGb-0hAm65zPpwvAmYoDMW-Ye_iMny4mNpeAxab_xU,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
15
|
tunacode/cli/textual_bridge.py,sha256=LvqiTtF0hu3gNujzpKaW9h-m6xzEP3OH2M8KL2pCwRc,6333
|
|
14
16
|
tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
|
|
@@ -16,32 +18,32 @@ tunacode/configuration/defaults.py,sha256=oLgmHprB3cTaFvT9dn_rgg206zoj09GRXRbI7M
|
|
|
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
|
|
21
|
+
tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,15661
|
|
19
22
|
tunacode/core/state.py,sha256=PHGCGjx_X03I5jO-T1JkREQm4cwYEXQty59JJlnk24c,1608
|
|
20
|
-
tunacode/core/tool_handler.py,sha256=
|
|
21
|
-
tunacode/core/agents/__init__.py,sha256=
|
|
22
|
-
tunacode/core/agents/main.py,sha256=
|
|
23
|
-
tunacode/core/agents/orchestrator.py,sha256=SSJyd4O_WZzcI5zygoUx1nhzRpfT6DrPsp2XtK2UVzY,8607
|
|
24
|
-
tunacode/core/agents/planner_schema.py,sha256=pu2ehQVALjiJ5HJD7EN6xuZHCknsrXO9z0xHuVdlKW8,345
|
|
25
|
-
tunacode/core/agents/readonly.py,sha256=jOG3CF5G1y9k4sBn7ChXN1GXWbmB_0pFGcaMMI4iaLs,2325
|
|
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
|
|
@@ -68,9 +70,9 @@ tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,1138
|
|
|
68
70
|
tunacode/utils/text_utils.py,sha256=zRBaorvtyd7HBEWtIfCH1Wce1L6rhsQwpORUEGBFMjA,2981
|
|
69
71
|
tunacode/utils/token_counter.py,sha256=nGCWwrHHFbKywqeDCEuJnADCkfJuzysWiB6cCltJOKI,648
|
|
70
72
|
tunacode/utils/user_configuration.py,sha256=IGvUH37wWtZ4M5xpukZEWYhtuKKyKcl6DaeObGXdleU,2610
|
|
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.
|
|
76
|
-
tunacode_cli-0.0.
|
|
73
|
+
tunacode_cli-0.0.32.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
74
|
+
tunacode_cli-0.0.32.dist-info/METADATA,sha256=uX02OQdTT5lrmAm6XFSO1IKEmv9uw9lkG1UGaVOcbcU,4023
|
|
75
|
+
tunacode_cli-0.0.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
76
|
+
tunacode_cli-0.0.32.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
77
|
+
tunacode_cli-0.0.32.dist-info/top_level.txt,sha256=GuU751acRvOhM5yLKFW0-gBg62JGh5zycDSq4tRFOYE,13
|
|
78
|
+
tunacode_cli-0.0.32.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")
|
tunacode/core/agents/readonly.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Read-only agent implementation for non-mutating operations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
from ...tools.grep import grep
|
|
8
|
-
from ...tools.read_file import read_file
|
|
9
|
-
from ...types import AgentRun, ModelName, ResponseState
|
|
10
|
-
from ..state import StateManager
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from ...types import PydanticAgent
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ReadOnlyAgent:
|
|
17
|
-
"""Agent configured with read-only tools for analysis tasks."""
|
|
18
|
-
|
|
19
|
-
def __init__(self, model: ModelName, state_manager: StateManager):
|
|
20
|
-
self.model = model
|
|
21
|
-
self.state_manager = state_manager
|
|
22
|
-
self._agent: PydanticAgent | None = None
|
|
23
|
-
|
|
24
|
-
def _get_agent(self) -> PydanticAgent:
|
|
25
|
-
"""Lazily create the agent with read-only tools."""
|
|
26
|
-
if self._agent is None:
|
|
27
|
-
from .main import get_agent_tool
|
|
28
|
-
|
|
29
|
-
Agent, Tool = get_agent_tool()
|
|
30
|
-
|
|
31
|
-
# Create agent with only read-only tools
|
|
32
|
-
self._agent = Agent(
|
|
33
|
-
model=self.model,
|
|
34
|
-
system_prompt="You are a read-only assistant. You can analyze and read files but cannot modify them.",
|
|
35
|
-
tools=[
|
|
36
|
-
Tool(read_file),
|
|
37
|
-
Tool(grep),
|
|
38
|
-
],
|
|
39
|
-
)
|
|
40
|
-
return self._agent
|
|
41
|
-
|
|
42
|
-
async def process_request(self, request: str) -> AgentRun:
|
|
43
|
-
"""Process a request using only read-only tools."""
|
|
44
|
-
agent = self._get_agent()
|
|
45
|
-
response_state = ResponseState()
|
|
46
|
-
|
|
47
|
-
# Use iter() like main.py does to get the full run context
|
|
48
|
-
async with agent.iter(request) as agent_run:
|
|
49
|
-
async for node in agent_run:
|
|
50
|
-
# Check if this node produced user-visible output
|
|
51
|
-
if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
|
|
52
|
-
if node.result.output:
|
|
53
|
-
response_state.has_user_response = True
|
|
54
|
-
|
|
55
|
-
# Wrap the agent run to include response_state
|
|
56
|
-
class AgentRunWithState:
|
|
57
|
-
def __init__(self, wrapped_run):
|
|
58
|
-
self._wrapped = wrapped_run
|
|
59
|
-
self.response_state = response_state
|
|
60
|
-
|
|
61
|
-
def __getattr__(self, name):
|
|
62
|
-
# Delegate all other attributes to the wrapped object
|
|
63
|
-
return getattr(self._wrapped, name)
|
|
64
|
-
|
|
65
|
-
return AgentRunWithState(agent_run)
|