tunacode-cli 0.0.28__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.28/src/tunacode_cli.egg-info → tunacode_cli-0.0.30}/PKG-INFO +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/pyproject.toml +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/commands.py +2 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/repl.py +36 -2
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_bridge.py +4 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/defaults.py +1 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/constants.py +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/main.py +225 -41
- tunacode_cli-0.0.30/src/tunacode/core/agents/orchestrator.py +213 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/readonly.py +18 -4
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/state.py +5 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/prompts/system.md +38 -8
- {tunacode_cli-0.0.28 → 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.28 → tunacode_cli-0.0.30/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/SOURCES.txt +2 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_fallback_responses.py +3 -1
- tunacode_cli-0.0.30/tests/test_file_reference_context_tracking.py +147 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_file_reference_expansion.py +56 -8
- tunacode_cli-0.0.28/src/tunacode/core/agents/orchestrator.py +0 -117
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/CLAUDE.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/LICENSE +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/MANIFEST.in +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/README.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/TUNACODE.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/setup.cfg +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/main.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_app.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/models.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/settings.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/context.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/planner_schema.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/background/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/background/manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/llm/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/llm/planner.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/agent_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/base.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/config_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/coordinator.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/environment_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/git_safety_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/tool_handler.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/exceptions.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/py.typed +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/services/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/services/mcp.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/base.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/bash.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/grep.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/read_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/run_command.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/update_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/write_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/types.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/completers.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/console.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/constants.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/decorators.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/input.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/keybindings.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/lexers.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/output.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/panels.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/prompt_manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/tool_ui.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/validators.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/bm25.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/diff_utils.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/file_utils.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/import_cache.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/ripgrep.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/system.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/user_configuration.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/requires.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/top_level.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_agent_initialization.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_architect_integration.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_architect_simple.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_background_manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_config_setup_async.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_fast_glob_search.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_json_tool_parsing.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_file_references.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_import.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_planning_visibility.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_react_thoughts.py +0 -0
- {tunacode_cli-0.0.28 → 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,49 +413,160 @@ 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
|
|
375
456
|
if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
|
|
376
457
|
patch_tool_messages("Task incomplete", state_manager=state_manager)
|
|
377
458
|
response_state.has_final_synthesis = True
|
|
459
|
+
|
|
460
|
+
# Extract context from the agent run
|
|
461
|
+
tool_calls_summary = []
|
|
462
|
+
files_modified = set()
|
|
463
|
+
commands_run = []
|
|
464
|
+
|
|
465
|
+
# Analyze message history for context
|
|
466
|
+
for msg in state_manager.session.messages:
|
|
467
|
+
if hasattr(msg, "parts"):
|
|
468
|
+
for part in msg.parts:
|
|
469
|
+
if hasattr(part, "part_kind") and part.part_kind == "tool-call":
|
|
470
|
+
tool_name = getattr(part, "tool_name", "unknown")
|
|
471
|
+
tool_calls_summary.append(tool_name)
|
|
472
|
+
|
|
473
|
+
# Track specific operations
|
|
474
|
+
if tool_name in ["write_file", "update_file"] and hasattr(part, "args"):
|
|
475
|
+
if "file_path" in part.args:
|
|
476
|
+
files_modified.add(part.args["file_path"])
|
|
477
|
+
elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
|
|
478
|
+
if "command" in part.args:
|
|
479
|
+
commands_run.append(part.args["command"])
|
|
480
|
+
|
|
481
|
+
# Build fallback response with context
|
|
378
482
|
fallback = FallbackResponse(
|
|
379
483
|
summary="Reached maximum iterations without producing a final response.",
|
|
380
|
-
progress=f"{i}
|
|
484
|
+
progress=f"Completed {i} iterations (limit: {max_iterations})",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Get verbosity setting
|
|
488
|
+
verbosity = state_manager.session.user_config.get("settings", {}).get(
|
|
489
|
+
"fallback_verbosity", "normal"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if verbosity in ["normal", "detailed"]:
|
|
493
|
+
# Add what was attempted
|
|
494
|
+
if tool_calls_summary:
|
|
495
|
+
tool_counts = {}
|
|
496
|
+
for tool in tool_calls_summary:
|
|
497
|
+
tool_counts[tool] = tool_counts.get(tool, 0) + 1
|
|
498
|
+
|
|
499
|
+
fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
|
|
500
|
+
for tool, count in sorted(tool_counts.items()):
|
|
501
|
+
fallback.issues.append(f" • {tool}: {count}x")
|
|
502
|
+
|
|
503
|
+
if verbosity == "detailed":
|
|
504
|
+
if files_modified:
|
|
505
|
+
fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
|
|
506
|
+
for f in sorted(files_modified)[:5]: # Limit to 5 files
|
|
507
|
+
fallback.issues.append(f" • {f}")
|
|
508
|
+
if len(files_modified) > 5:
|
|
509
|
+
fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
|
|
510
|
+
|
|
511
|
+
if commands_run:
|
|
512
|
+
fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
|
|
513
|
+
for cmd in commands_run[:3]: # Limit to 3 commands
|
|
514
|
+
# Truncate long commands
|
|
515
|
+
display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
|
|
516
|
+
fallback.issues.append(f" • {display_cmd}")
|
|
517
|
+
if len(commands_run) > 3:
|
|
518
|
+
fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
|
|
519
|
+
|
|
520
|
+
# Add helpful next steps
|
|
521
|
+
fallback.next_steps.append(
|
|
522
|
+
"The task may be too complex - try breaking it into smaller steps"
|
|
381
523
|
)
|
|
524
|
+
fallback.next_steps.append("Check the output above for any errors or partial progress")
|
|
525
|
+
if files_modified:
|
|
526
|
+
fallback.next_steps.append("Review modified files to see what changes were made")
|
|
527
|
+
|
|
528
|
+
# Create comprehensive output
|
|
529
|
+
output_parts = [fallback.summary, ""]
|
|
530
|
+
|
|
531
|
+
if fallback.progress:
|
|
532
|
+
output_parts.append(f"Progress: {fallback.progress}")
|
|
533
|
+
|
|
534
|
+
if fallback.issues:
|
|
535
|
+
output_parts.append("\nWhat happened:")
|
|
536
|
+
output_parts.extend(fallback.issues)
|
|
537
|
+
|
|
538
|
+
if fallback.next_steps:
|
|
539
|
+
output_parts.append("\nSuggested next steps:")
|
|
540
|
+
for step in fallback.next_steps:
|
|
541
|
+
output_parts.append(f" • {step}")
|
|
542
|
+
|
|
543
|
+
comprehensive_output = "\n".join(output_parts)
|
|
382
544
|
|
|
383
545
|
# Create a wrapper object that mimics AgentRun with the required attributes
|
|
384
546
|
class AgentRunWrapper:
|
|
385
547
|
def __init__(self, wrapped_run, fallback_result):
|
|
386
548
|
self._wrapped = wrapped_run
|
|
387
|
-
self.
|
|
549
|
+
self._result = fallback_result
|
|
388
550
|
self.response_state = response_state
|
|
389
551
|
|
|
390
|
-
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
|
+
|
|
391
561
|
# Delegate all other attributes to the wrapped object
|
|
392
|
-
|
|
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
|
+
)
|
|
393
568
|
|
|
394
|
-
return AgentRunWrapper(agent_run, SimpleResult(
|
|
569
|
+
return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
|
|
395
570
|
|
|
396
571
|
# For non-fallback cases, we still need to handle the response_state
|
|
397
572
|
# Create a minimal wrapper just to add response_state
|
|
@@ -400,8 +575,17 @@ async def process_request(
|
|
|
400
575
|
self._wrapped = wrapped_run
|
|
401
576
|
self.response_state = response_state
|
|
402
577
|
|
|
403
|
-
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
|
+
|
|
404
583
|
# Delegate all other attributes to the wrapped object
|
|
405
|
-
|
|
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
|
+
)
|
|
406
590
|
|
|
407
591
|
return AgentRunWithState(agent_run)
|