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.

@@ -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
- with open(filepath, "r", encoding="utf-8") as file:
44
- content = file.read()
45
- return content
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())
@@ -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
- Text with any @file references replaced by the file's contents.
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
- return f"```{lang}\n{content}\n```"
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
- return pattern.sub(replacer, text)
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.29
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
- ![Demo](demo.gif)
48
+ ![Demo](docs/assets/demo.gif)
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
+ ![Parallel Execution Performance](docs/assets/parrelel_work_3x.png)
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**](documentation/FEATURES.md) - All features, tools, and commands
109
- - [**Advanced Configuration**](documentation/ADVANCED-CONFIG.md) - Provider setup, MCP, customization
110
- - [**Architecture**](documentation/ARCHITECTURE.md) - Source code organization and design
111
- - [**Development**](documentation/DEVELOPMENT.md) - Contributing and development setup
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=yChcPTCk4tc0Q1JArwWxIZ2Zxi9S-vLgKiBOdMml6eI,3799
4
+ tunacode/constants.py,sha256=PMXSbKvpMm9j7EjXpaaFmvBXKdptNi3IfV4E-bxQmBM,4074
3
5
  tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
4
- tunacode/exceptions.py,sha256=_Dyj6cC0868dMABekdQHXCg5XhucJumbGUMXaSDzgB4,2645
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=OD3ZnNd5_TjJSAIuDT4rNgruTH_OOuAaR5OAoa8poFQ,34062
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=sXtRHYameAlMjlee82ix8P2JjRyWLrdFfHwxfaMKPb8,13722
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=CTuf5Yjg5occQa7whyopS_erbJdS2UpWqaX_TVJ2D2A,6140
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/state.py,sha256=n1claG-gVVDMBCCS8cDmgas4XbKLJJwKRc-8CtXeTS8,1376
20
- tunacode/core/tool_handler.py,sha256=OKx7jM8pml6pSEnoARu33_uBY8awJBqvhbVeBn6T4ow,1804
21
- tunacode/core/agents/__init__.py,sha256=TiXwymGRNMuOqQaRLpNCAt7bZArg8rkadIRs4Nw21SQ,275
22
- tunacode/core/agents/main.py,sha256=FCExe7pQ57z-GQh9p1RHEgaxHdMotJS21htdnHB-VK0,20612
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
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=T7hwIf3u3Tq3QtIdUAfuHI6vclMfm2Sqcml5l6x02oA,6799
37
- tunacode/prompts/system.md,sha256=vjXE23JWEz87BSH05On6ILuaBBYXFJu96U5_79K2rec,4024
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/grep.py,sha256=N0Iu5MuSAVGBueu6PoE4Pt1x3oMIlX5aaZyejQwTlU8,26492
44
- tunacode/tools/read_file.py,sha256=Cz1-7sdQwOdaqkVvkVpORiBdNtncCVlP9e9cu37ya80,2988
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=B9M1cuLTm_SSsr15WNHF6j7WdLWPvWzKZV0Lvfgdbjg,2458
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.29.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
71
- tunacode_cli-0.0.29.dist-info/METADATA,sha256=QAJvK_x13W-wzNNNLr0WmSo3Xo1kHuskr3N9OFSGZxc,3619
72
- tunacode_cli-0.0.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- tunacode_cli-0.0.29.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
74
- tunacode_cli-0.0.29.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
75
- tunacode_cli-0.0.29.dist-info/RECORD,,
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")