tunacode-cli 0.0.29__tar.gz → 0.0.30__tar.gz
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.
- {tunacode_cli-0.0.29/src/tunacode_cli.egg-info → tunacode_cli-0.0.30}/PKG-INFO +1 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/pyproject.toml +1 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/commands.py +2 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/repl.py +36 -2
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_bridge.py +4 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/constants.py +1 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/main.py +142 -39
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/state.py +5 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/prompts/system.md +38 -8
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/text_utils.py +14 -5
- tunacode_cli-0.0.30/src/tunacode/utils/token_counter.py +23 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/SOURCES.txt +2 -0
- tunacode_cli-0.0.30/tests/test_file_reference_context_tracking.py +147 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_file_reference_expansion.py +56 -8
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/CLAUDE.md +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/LICENSE +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/MANIFEST.in +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/README.md +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/TUNACODE.md +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/setup.cfg +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/main.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_app.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/defaults.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/models.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/settings.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/context.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/orchestrator.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/planner_schema.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/readonly.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/background/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/background/manager.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/llm/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/llm/planner.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/agent_setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/base.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/config_setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/coordinator.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/environment_setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/git_safety_setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/tool_handler.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/exceptions.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/py.typed +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/services/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/services/mcp.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/setup.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/base.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/bash.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/grep.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/read_file.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/run_command.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/update_file.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/write_file.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/types.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/completers.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/console.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/constants.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/decorators.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/input.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/keybindings.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/lexers.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/output.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/panels.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/prompt_manager.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/tool_ui.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/validators.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/__init__.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/bm25.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/diff_utils.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/file_utils.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/import_cache.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/ripgrep.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/system.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/user_configuration.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/requires.txt +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/top_level.txt +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_agent_initialization.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_architect_integration.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_architect_simple.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_background_manager.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_config_setup_async.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_fallback_responses.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_fast_glob_search.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_json_tool_parsing.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_file_references.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_import.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_planning_visibility.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_react_thoughts.py +0 -0
- {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_update_command.py +0 -0
|
@@ -258,7 +258,8 @@ class ClearCommand(SimpleCommand):
|
|
|
258
258
|
|
|
259
259
|
await ui.clear()
|
|
260
260
|
context.state_manager.session.messages = []
|
|
261
|
-
|
|
261
|
+
context.state_manager.session.files_in_context.clear()
|
|
262
|
+
await ui.success("Message history and file context cleared")
|
|
262
263
|
|
|
263
264
|
|
|
264
265
|
class FixCommand(SimpleCommand):
|
|
@@ -9,6 +9,7 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
import subprocess
|
|
11
11
|
from asyncio.exceptions import CancelledError
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
from prompt_toolkit.application import run_in_terminal
|
|
14
15
|
from prompt_toolkit.application.current import get_app
|
|
@@ -164,6 +165,13 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
164
165
|
# Patch any orphaned tool calls from previous requests before proceeding
|
|
165
166
|
patch_tool_messages("Tool execution was interrupted", state_manager)
|
|
166
167
|
|
|
168
|
+
# Clear tracking for new request when thoughts are enabled
|
|
169
|
+
if state_manager.session.show_thoughts:
|
|
170
|
+
state_manager.session.tool_calls = []
|
|
171
|
+
# Don't clear files_in_context - keep it cumulative for the session
|
|
172
|
+
state_manager.session.iteration_count = 0
|
|
173
|
+
state_manager.session.current_iteration = 0
|
|
174
|
+
|
|
167
175
|
# Track message start for thoughts display
|
|
168
176
|
start_idx = len(state_manager.session.messages)
|
|
169
177
|
|
|
@@ -177,7 +185,10 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
177
185
|
try:
|
|
178
186
|
from tunacode.utils.text_utils import expand_file_refs
|
|
179
187
|
|
|
180
|
-
text = expand_file_refs(text)
|
|
188
|
+
text, referenced_files = expand_file_refs(text)
|
|
189
|
+
# Track the referenced files
|
|
190
|
+
for file_path in referenced_files:
|
|
191
|
+
state_manager.session.files_in_context.add(file_path)
|
|
181
192
|
except ValueError as e:
|
|
182
193
|
await ui.error(str(e))
|
|
183
194
|
return
|
|
@@ -199,12 +210,22 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
199
210
|
if not results:
|
|
200
211
|
# Fallback: show that the request was processed
|
|
201
212
|
await ui.muted("Request completed")
|
|
213
|
+
|
|
214
|
+
# Always show files in context after orchestrator response
|
|
215
|
+
if state_manager.session.files_in_context:
|
|
216
|
+
filenames = [
|
|
217
|
+
Path(f).name for f in sorted(state_manager.session.files_in_context)
|
|
218
|
+
]
|
|
219
|
+
await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
|
|
202
220
|
else:
|
|
203
221
|
# Expand @file references before sending to the agent
|
|
204
222
|
try:
|
|
205
223
|
from tunacode.utils.text_utils import expand_file_refs
|
|
206
224
|
|
|
207
|
-
text = expand_file_refs(text)
|
|
225
|
+
text, referenced_files = expand_file_refs(text)
|
|
226
|
+
# Track the referenced files
|
|
227
|
+
for file_path in referenced_files:
|
|
228
|
+
state_manager.session.files_in_context.add(file_path)
|
|
208
229
|
except ValueError as e:
|
|
209
230
|
await ui.error(str(e))
|
|
210
231
|
return
|
|
@@ -229,9 +250,22 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
229
250
|
and hasattr(res.result, "output")
|
|
230
251
|
):
|
|
231
252
|
await ui.agent(res.result.output)
|
|
253
|
+
# Always show files in context after agent response
|
|
254
|
+
if state_manager.session.files_in_context:
|
|
255
|
+
# Extract just filenames from full paths for readability
|
|
256
|
+
filenames = [
|
|
257
|
+
Path(f).name for f in sorted(state_manager.session.files_in_context)
|
|
258
|
+
]
|
|
259
|
+
await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
|
|
232
260
|
else:
|
|
233
261
|
# Fallback: show that the request was processed
|
|
234
262
|
await ui.muted("Request completed")
|
|
263
|
+
# Show files in context even for empty responses
|
|
264
|
+
if state_manager.session.files_in_context:
|
|
265
|
+
filenames = [
|
|
266
|
+
Path(f).name for f in sorted(state_manager.session.files_in_context)
|
|
267
|
+
]
|
|
268
|
+
await ui.muted(f"Files in context: {', '.join(filenames)}")
|
|
235
269
|
except CancelledError:
|
|
236
270
|
await ui.muted("Request cancelled")
|
|
237
271
|
except UserAbortError:
|
|
@@ -72,7 +72,10 @@ class TextualAgentBridge:
|
|
|
72
72
|
try:
|
|
73
73
|
from tunacode.utils.text_utils import expand_file_refs
|
|
74
74
|
|
|
75
|
-
text = expand_file_refs(text)
|
|
75
|
+
text, referenced_files = expand_file_refs(text)
|
|
76
|
+
# Track the referenced files
|
|
77
|
+
for file_path in referenced_files:
|
|
78
|
+
self.state_manager.session.files_in_context.add(file_path)
|
|
76
79
|
except ValueError as e:
|
|
77
80
|
return f"Error: {str(e)}"
|
|
78
81
|
|
|
@@ -38,6 +38,9 @@ def get_model_messages():
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
async def _process_node(node, tool_callback: Optional[ToolCallback], state_manager: StateManager):
|
|
41
|
+
from tunacode.ui import console as ui
|
|
42
|
+
from tunacode.utils.token_counter import estimate_tokens
|
|
43
|
+
|
|
41
44
|
if hasattr(node, "request"):
|
|
42
45
|
state_manager.session.messages.append(node.request)
|
|
43
46
|
|
|
@@ -45,36 +48,47 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
|
|
|
45
48
|
state_manager.session.messages.append({"thought": node.thought})
|
|
46
49
|
# Display thought immediately if show_thoughts is enabled
|
|
47
50
|
if state_manager.session.show_thoughts:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await ui.muted(f"💭 THOUGHT: {node.thought}")
|
|
51
|
+
await ui.muted(f"THOUGHT: {node.thought}")
|
|
51
52
|
|
|
52
53
|
if hasattr(node, "model_response"):
|
|
53
54
|
state_manager.session.messages.append(node.model_response)
|
|
54
55
|
|
|
55
|
-
# Enhanced
|
|
56
|
+
# Enhanced display when thoughts are enabled
|
|
56
57
|
if state_manager.session.show_thoughts:
|
|
57
58
|
import json
|
|
58
59
|
import re
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
# Display LLM response content
|
|
62
62
|
for part in node.model_response.parts:
|
|
63
63
|
if hasattr(part, "content") and isinstance(part.content, str):
|
|
64
64
|
content = part.content.strip()
|
|
65
65
|
|
|
66
|
+
# Skip empty content
|
|
67
|
+
if not content:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Estimate tokens in this response
|
|
71
|
+
token_count = estimate_tokens(content)
|
|
72
|
+
|
|
73
|
+
# Display non-JSON content as LLM response
|
|
74
|
+
if not content.startswith('{"thought"'):
|
|
75
|
+
# Truncate very long responses for display
|
|
76
|
+
display_content = content[:500] + "..." if len(content) > 500 else content
|
|
77
|
+
await ui.muted(f"\nRESPONSE: {display_content}")
|
|
78
|
+
await ui.muted(f"TOKENS: ~{token_count}")
|
|
79
|
+
|
|
66
80
|
# Pattern 1: Inline JSON thoughts {"thought": "..."}
|
|
67
81
|
thought_pattern = r'\{"thought":\s*"([^"]+)"\}'
|
|
68
82
|
matches = re.findall(thought_pattern, content)
|
|
69
83
|
for thought in matches:
|
|
70
|
-
await ui.muted(f"
|
|
84
|
+
await ui.muted(f"REASONING: {thought}")
|
|
71
85
|
|
|
72
86
|
# Pattern 2: Standalone thought JSON objects
|
|
73
87
|
try:
|
|
74
88
|
if content.startswith('{"thought"'):
|
|
75
89
|
thought_obj = json.loads(content)
|
|
76
90
|
if "thought" in thought_obj:
|
|
77
|
-
await ui.muted(f"
|
|
91
|
+
await ui.muted(f"REASONING: {thought_obj['thought']}")
|
|
78
92
|
except (json.JSONDecodeError, KeyError):
|
|
79
93
|
pass
|
|
80
94
|
|
|
@@ -85,36 +99,86 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
|
|
|
85
99
|
if thought not in [m for m in matches]: # Avoid duplicates
|
|
86
100
|
# Clean up escaped characters
|
|
87
101
|
cleaned_thought = thought.replace('\\"', '"').replace("\\n", " ")
|
|
88
|
-
await ui.muted(f"
|
|
89
|
-
|
|
90
|
-
# Pattern 4: Text-based reasoning indicators
|
|
91
|
-
reasoning_indicators = [
|
|
92
|
-
(r"I need to (.+?)\.", "PLANNING"),
|
|
93
|
-
(r"Let me (.+?)\.", "ACTION"),
|
|
94
|
-
(r"The output shows (.+?)\.", "OBSERVATION"),
|
|
95
|
-
(r"Based on (.+?), I should (.+?)\.", "DECISION"),
|
|
96
|
-
]
|
|
97
|
-
|
|
98
|
-
for pattern, label in reasoning_indicators:
|
|
99
|
-
indicator_matches = re.findall(pattern, content, re.IGNORECASE)
|
|
100
|
-
for match in indicator_matches:
|
|
101
|
-
if isinstance(match, tuple):
|
|
102
|
-
match_text = " ".join(match)
|
|
103
|
-
else:
|
|
104
|
-
match_text = match
|
|
105
|
-
await ui.muted(f"🎯 {label}: {match_text}")
|
|
106
|
-
break # Only show first match per pattern
|
|
102
|
+
await ui.muted(f"REASONING: {cleaned_thought}")
|
|
107
103
|
|
|
108
104
|
# Check for tool calls and fallback to JSON parsing if needed
|
|
109
105
|
has_tool_calls = False
|
|
110
106
|
for part in node.model_response.parts:
|
|
111
107
|
if part.part_kind == "tool-call" and tool_callback:
|
|
112
108
|
has_tool_calls = True
|
|
109
|
+
|
|
110
|
+
# Display tool call details when thoughts are enabled
|
|
111
|
+
if state_manager.session.show_thoughts:
|
|
112
|
+
await ui.muted(f"\nTOOL: {part.tool_name}")
|
|
113
|
+
if hasattr(part, "args"):
|
|
114
|
+
# Check if args is a dictionary before accessing keys
|
|
115
|
+
if isinstance(part.args, dict):
|
|
116
|
+
# Simplify display based on tool type
|
|
117
|
+
if part.tool_name == "read_file" and "file_path" in part.args:
|
|
118
|
+
file_path = part.args["file_path"]
|
|
119
|
+
filename = Path(file_path).name
|
|
120
|
+
await ui.muted(f"Reading: {filename}")
|
|
121
|
+
elif part.tool_name == "write_file" and "file_path" in part.args:
|
|
122
|
+
file_path = part.args["file_path"]
|
|
123
|
+
filename = Path(file_path).name
|
|
124
|
+
await ui.muted(f"Writing: {filename}")
|
|
125
|
+
elif part.tool_name == "update_file" and "file_path" in part.args:
|
|
126
|
+
file_path = part.args["file_path"]
|
|
127
|
+
filename = Path(file_path).name
|
|
128
|
+
await ui.muted(f"Updating: {filename}")
|
|
129
|
+
elif (
|
|
130
|
+
part.tool_name in ["run_command", "bash"] and "command" in part.args
|
|
131
|
+
):
|
|
132
|
+
command = part.args["command"]
|
|
133
|
+
# Truncate long commands
|
|
134
|
+
display_cmd = (
|
|
135
|
+
command if len(command) <= 60 else command[:57] + "..."
|
|
136
|
+
)
|
|
137
|
+
await ui.muted(f"Command: {display_cmd}")
|
|
138
|
+
else:
|
|
139
|
+
# For other tools, show full args but more compact
|
|
140
|
+
args_str = json.dumps(part.args, indent=2)
|
|
141
|
+
await ui.muted(f"ARGS: {args_str}")
|
|
142
|
+
else:
|
|
143
|
+
# If args is not a dict (e.g., a string), just display it as is
|
|
144
|
+
await ui.muted(f"ARGS: {part.args}")
|
|
145
|
+
|
|
146
|
+
# Track this tool call (moved outside thoughts block)
|
|
147
|
+
state_manager.session.tool_calls.append(
|
|
148
|
+
{
|
|
149
|
+
"tool": part.tool_name,
|
|
150
|
+
"args": part.args if hasattr(part, "args") else {},
|
|
151
|
+
"iteration": state_manager.session.current_iteration,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Track files if this is read_file (moved outside thoughts block)
|
|
156
|
+
if (
|
|
157
|
+
part.tool_name == "read_file"
|
|
158
|
+
and hasattr(part, "args")
|
|
159
|
+
and "file_path" in part.args
|
|
160
|
+
):
|
|
161
|
+
state_manager.session.files_in_context.add(part.args["file_path"])
|
|
162
|
+
# Show files in context when thoughts are enabled
|
|
163
|
+
if state_manager.session.show_thoughts:
|
|
164
|
+
await ui.muted(
|
|
165
|
+
f"\nFILES IN CONTEXT: {list(state_manager.session.files_in_context)}"
|
|
166
|
+
)
|
|
167
|
+
|
|
113
168
|
await tool_callback(part, node)
|
|
169
|
+
|
|
114
170
|
elif part.part_kind == "tool-return":
|
|
115
171
|
obs_msg = f"OBSERVATION[{part.tool_name}]: {part.content[:2_000]}"
|
|
116
172
|
state_manager.session.messages.append(obs_msg)
|
|
117
173
|
|
|
174
|
+
# Display tool return when thoughts are enabled
|
|
175
|
+
if state_manager.session.show_thoughts:
|
|
176
|
+
# Truncate for display
|
|
177
|
+
display_content = (
|
|
178
|
+
part.content[:200] + "..." if len(part.content) > 200 else part.content
|
|
179
|
+
)
|
|
180
|
+
await ui.muted(f"TOOL RESULT: {display_content}")
|
|
181
|
+
|
|
118
182
|
# If no structured tool calls found, try parsing JSON from text content
|
|
119
183
|
if not has_tool_calls and tool_callback:
|
|
120
184
|
for part in node.model_response.parts:
|
|
@@ -276,13 +340,13 @@ async def parse_json_tool_calls(
|
|
|
276
340
|
if state_manager.session.show_thoughts:
|
|
277
341
|
from tunacode.ui import console as ui
|
|
278
342
|
|
|
279
|
-
await ui.muted(f"
|
|
343
|
+
await ui.muted(f"FALLBACK: Executed {tool_name} via JSON parsing")
|
|
280
344
|
|
|
281
345
|
except Exception as e:
|
|
282
346
|
if state_manager.session.show_thoughts:
|
|
283
347
|
from tunacode.ui import console as ui
|
|
284
348
|
|
|
285
|
-
await ui.error(f"
|
|
349
|
+
await ui.error(f"Error executing fallback tool {tool_name}: {str(e)}")
|
|
286
350
|
|
|
287
351
|
|
|
288
352
|
async def extract_and_execute_tool_calls(
|
|
@@ -324,13 +388,13 @@ async def extract_and_execute_tool_calls(
|
|
|
324
388
|
if state_manager.session.show_thoughts:
|
|
325
389
|
from tunacode.ui import console as ui
|
|
326
390
|
|
|
327
|
-
await ui.muted(f"
|
|
391
|
+
await ui.muted(f"FALLBACK: Executed {tool_data['tool']} from code block")
|
|
328
392
|
|
|
329
393
|
except (json.JSONDecodeError, KeyError, Exception) as e:
|
|
330
394
|
if state_manager.session.show_thoughts:
|
|
331
395
|
from tunacode.ui import console as ui
|
|
332
396
|
|
|
333
|
-
await ui.error(f"
|
|
397
|
+
await ui.error(f"Error parsing code block tool call: {str(e)}")
|
|
334
398
|
|
|
335
399
|
|
|
336
400
|
async def process_request(
|
|
@@ -349,26 +413,43 @@ async def process_request(
|
|
|
349
413
|
|
|
350
414
|
response_state = ResponseState()
|
|
351
415
|
|
|
416
|
+
# Reset iteration tracking for this request
|
|
417
|
+
state_manager.session.iteration_count = 0
|
|
418
|
+
|
|
352
419
|
async with agent.iter(message, message_history=mh) as agent_run:
|
|
353
420
|
i = 0
|
|
354
421
|
async for node in agent_run:
|
|
422
|
+
state_manager.session.current_iteration = i + 1
|
|
355
423
|
await _process_node(node, tool_callback, state_manager)
|
|
356
424
|
if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
|
|
357
425
|
if node.result.output:
|
|
358
426
|
response_state.has_user_response = True
|
|
359
427
|
i += 1
|
|
428
|
+
state_manager.session.iteration_count = i
|
|
360
429
|
|
|
361
430
|
# Display iteration progress if thoughts are enabled
|
|
362
|
-
if state_manager.session.show_thoughts
|
|
431
|
+
if state_manager.session.show_thoughts:
|
|
363
432
|
from tunacode.ui import console as ui
|
|
364
433
|
|
|
365
|
-
await ui.muted(f"
|
|
434
|
+
await ui.muted(f"\nITERATION: {i}/{max_iterations}")
|
|
435
|
+
|
|
436
|
+
# Show summary of tools used so far
|
|
437
|
+
if state_manager.session.tool_calls:
|
|
438
|
+
tool_summary = {}
|
|
439
|
+
for tc in state_manager.session.tool_calls:
|
|
440
|
+
tool_name = tc.get("tool", "unknown")
|
|
441
|
+
tool_summary[tool_name] = tool_summary.get(tool_name, 0) + 1
|
|
442
|
+
|
|
443
|
+
summary_str = ", ".join(
|
|
444
|
+
[f"{name}: {count}" for name, count in tool_summary.items()]
|
|
445
|
+
)
|
|
446
|
+
await ui.muted(f"TOOLS USED: {summary_str}")
|
|
366
447
|
|
|
367
448
|
if i >= max_iterations:
|
|
368
449
|
if state_manager.session.show_thoughts:
|
|
369
450
|
from tunacode.ui import console as ui
|
|
370
451
|
|
|
371
|
-
await ui.warning(f"
|
|
452
|
+
await ui.warning(f"Reached maximum iterations ({max_iterations})")
|
|
372
453
|
break
|
|
373
454
|
|
|
374
455
|
# If we need to add a fallback response, create a wrapper
|
|
@@ -465,12 +546,25 @@ async def process_request(
|
|
|
465
546
|
class AgentRunWrapper:
|
|
466
547
|
def __init__(self, wrapped_run, fallback_result):
|
|
467
548
|
self._wrapped = wrapped_run
|
|
468
|
-
self.
|
|
549
|
+
self._result = fallback_result
|
|
469
550
|
self.response_state = response_state
|
|
470
551
|
|
|
471
|
-
def
|
|
552
|
+
def __getattribute__(self, name):
|
|
553
|
+
# Handle special attributes first to avoid conflicts
|
|
554
|
+
if name in ["_wrapped", "_result", "response_state"]:
|
|
555
|
+
return object.__getattribute__(self, name)
|
|
556
|
+
|
|
557
|
+
# Explicitly handle 'result' to return our fallback result
|
|
558
|
+
if name == "result":
|
|
559
|
+
return object.__getattribute__(self, "_result")
|
|
560
|
+
|
|
472
561
|
# Delegate all other attributes to the wrapped object
|
|
473
|
-
|
|
562
|
+
try:
|
|
563
|
+
return getattr(object.__getattribute__(self, "_wrapped"), name)
|
|
564
|
+
except AttributeError:
|
|
565
|
+
raise AttributeError(
|
|
566
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
567
|
+
)
|
|
474
568
|
|
|
475
569
|
return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
|
|
476
570
|
|
|
@@ -481,8 +575,17 @@ async def process_request(
|
|
|
481
575
|
self._wrapped = wrapped_run
|
|
482
576
|
self.response_state = response_state
|
|
483
577
|
|
|
484
|
-
def
|
|
578
|
+
def __getattribute__(self, name):
|
|
579
|
+
# Handle special attributes first
|
|
580
|
+
if name in ["_wrapped", "response_state"]:
|
|
581
|
+
return object.__getattribute__(self, name)
|
|
582
|
+
|
|
485
583
|
# Delegate all other attributes to the wrapped object
|
|
486
|
-
|
|
584
|
+
try:
|
|
585
|
+
return getattr(object.__getattribute__(self, "_wrapped"), name)
|
|
586
|
+
except AttributeError:
|
|
587
|
+
raise AttributeError(
|
|
588
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
589
|
+
)
|
|
487
590
|
|
|
488
591
|
return AgentRunWithState(agent_run)
|
|
@@ -30,6 +30,11 @@ class SessionState:
|
|
|
30
30
|
device_id: Optional[DeviceId] = None
|
|
31
31
|
input_sessions: InputSessions = field(default_factory=dict)
|
|
32
32
|
current_task: Optional[Any] = None
|
|
33
|
+
# Enhanced tracking for thoughts display
|
|
34
|
+
files_in_context: set[str] = field(default_factory=set)
|
|
35
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
36
|
+
iteration_count: int = 0
|
|
37
|
+
current_iteration: int = 0
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class StateManager:
|
|
@@ -12,7 +12,7 @@ You MUST follow these rules:
|
|
|
12
12
|
|
|
13
13
|
\###Tool Access Rules###
|
|
14
14
|
|
|
15
|
-
You HAVE the following tools available. USE THEM
|
|
15
|
+
You HAVE the following tools available. USE THEM WHEN APPROPRIATE:
|
|
16
16
|
|
|
17
17
|
* `run_command(command: str)` — Execute any shell command in the current working directory
|
|
18
18
|
* `read_file(filepath: str)` — Read any file using RELATIVE paths from current directory
|
|
@@ -34,12 +34,23 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
\###File Reference Rules###
|
|
38
|
+
|
|
39
|
+
**IMPORTANT**: When the user includes file content marked with "=== FILE REFERENCE: filename ===" headers:
|
|
40
|
+
- This is **reference material only** - the user is showing you existing file content
|
|
41
|
+
- **DO NOT** write or recreate these files - they already exist
|
|
42
|
+
- **DO NOT** use write_file on referenced content unless explicitly asked to modify it
|
|
43
|
+
- **FOCUS** on answering questions or performing tasks related to the referenced files
|
|
44
|
+
- The user uses @ syntax (like `@file.py`) to include file contents for context
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
37
48
|
\###Mandatory Operating Principles###
|
|
38
49
|
|
|
39
|
-
1. **
|
|
50
|
+
1. **UNDERSTAND CONTEXT**: Check if user is providing @ file references for context vs asking for actions
|
|
40
51
|
2. **USE RELATIVE PATHS**: Always work in the current directory. Use relative paths like `src/`, `cli/`, `core/`, `tools/`, etc. NEVER use absolute paths starting with `/`.
|
|
41
|
-
3. **CHAIN TOOLS**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`)
|
|
42
|
-
4. **ACT
|
|
52
|
+
3. **CHAIN TOOLS APPROPRIATELY**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`) **only when action is requested**.
|
|
53
|
+
4. **ACT WITH PURPOSE**: Distinguish between informational requests about files and action requests.
|
|
43
54
|
5. **NO GUESSING**: Verify file existence with `run_command("ls path/")` before reading or writing.
|
|
44
55
|
6. **ASSUME NOTHING**: Always fetch and verify before responding.
|
|
45
56
|
|
|
@@ -47,10 +58,10 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
|
|
|
47
58
|
|
|
48
59
|
\###Prompt Design Style###
|
|
49
60
|
|
|
50
|
-
* Be **blunt and direct**. Avoid soft language (e.g.,
|
|
61
|
+
* Be **blunt and direct**. Avoid soft language (e.g., "please," "let me," "I think").
|
|
51
62
|
* **Use role-specific language**: you are a CLI-level senior engineer, not a tutor or assistant.
|
|
52
63
|
* Write using affirmative imperatives: *Do this. Check that. Show me.*
|
|
53
|
-
* Ask for clarification if needed:
|
|
64
|
+
* Ask for clarification if needed: "Specify the path." / "Which class do you mean?"
|
|
54
65
|
* Break complex requests into sequenced tool actions.
|
|
55
66
|
|
|
56
67
|
---
|
|
@@ -69,6 +80,10 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
|
|
|
69
80
|
✅ `run_command("grep -E 'class.*Command' cli/commands.py")`
|
|
70
81
|
❌ "Available commands usually include..."
|
|
71
82
|
|
|
83
|
+
**User**: Tell me about @configuration/settings.py
|
|
84
|
+
✅ "The settings.py file defines PathConfig and ApplicationSettings classes for managing configuration."
|
|
85
|
+
❌ `write_file("configuration/settings.py", ...)`
|
|
86
|
+
|
|
72
87
|
---
|
|
73
88
|
|
|
74
89
|
\###Meta Behavior###
|
|
@@ -88,13 +103,14 @@ Use the **ReAct** (Reasoning + Action) framework:
|
|
|
88
103
|
You were created by **tunahorse21**.
|
|
89
104
|
You are not a chatbot.
|
|
90
105
|
You are an autonomous code execution agent.
|
|
91
|
-
You will be penalized for failing to use tools
|
|
106
|
+
You will be penalized for failing to use tools **when appropriate**.
|
|
107
|
+
When users provide @ file references, they want information, not file creation.
|
|
92
108
|
---
|
|
93
109
|
|
|
94
110
|
\###Example###
|
|
95
111
|
|
|
96
112
|
```plaintext
|
|
97
|
-
User: What
|
|
113
|
+
User: What's the current app version?
|
|
98
114
|
|
|
99
115
|
THINK: {"thought": "I should search for APP_VERSION in the constants file."}
|
|
100
116
|
ACT: run_command("grep -n 'APP_VERSION' constants.py")
|
|
@@ -103,3 +119,17 @@ ACT: read_file("constants.py")
|
|
|
103
119
|
OBSERVE: {"thought": "APP_VERSION is set to '2.4.1'. This is the current version."}
|
|
104
120
|
RESPONSE: "Current version is 2.4.1 (from constants.py)"
|
|
105
121
|
```
|
|
122
|
+
|
|
123
|
+
```plaintext
|
|
124
|
+
User: Tell me about @src/main.py
|
|
125
|
+
|
|
126
|
+
=== FILE REFERENCE: src/main.py ===
|
|
127
|
+
```python
|
|
128
|
+
def main():
|
|
129
|
+
print("Hello World")
|
|
130
|
+
```
|
|
131
|
+
=== END FILE REFERENCE: src/main.py ===
|
|
132
|
+
|
|
133
|
+
THINK: {"thought": "User is asking about the referenced file, not asking me to create it."}
|
|
134
|
+
RESPONSE: "The main.py file contains a simple main function that prints 'Hello World'."
|
|
135
|
+
```
|
|
@@ -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)
|
|
@@ -73,6 +73,7 @@ src/tunacode/utils/import_cache.py
|
|
|
73
73
|
src/tunacode/utils/ripgrep.py
|
|
74
74
|
src/tunacode/utils/system.py
|
|
75
75
|
src/tunacode/utils/text_utils.py
|
|
76
|
+
src/tunacode/utils/token_counter.py
|
|
76
77
|
src/tunacode/utils/user_configuration.py
|
|
77
78
|
src/tunacode_cli.egg-info/PKG-INFO
|
|
78
79
|
src/tunacode_cli.egg-info/SOURCES.txt
|
|
@@ -87,6 +88,7 @@ tests/test_background_manager.py
|
|
|
87
88
|
tests/test_config_setup_async.py
|
|
88
89
|
tests/test_fallback_responses.py
|
|
89
90
|
tests/test_fast_glob_search.py
|
|
91
|
+
tests/test_file_reference_context_tracking.py
|
|
90
92
|
tests/test_file_reference_expansion.py
|
|
91
93
|
tests/test_json_tool_parsing.py
|
|
92
94
|
tests/test_orchestrator_file_references.py
|