tunacode-cli 0.0.17__py3-none-any.whl → 0.0.19__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.
- tunacode/cli/commands.py +73 -41
- tunacode/cli/main.py +29 -26
- tunacode/cli/repl.py +91 -37
- tunacode/cli/textual_app.py +69 -66
- tunacode/cli/textual_bridge.py +33 -32
- tunacode/configuration/settings.py +2 -9
- tunacode/constants.py +2 -4
- tunacode/context.py +1 -1
- tunacode/core/agents/__init__.py +12 -0
- tunacode/core/agents/main.py +89 -63
- tunacode/core/agents/orchestrator.py +99 -0
- tunacode/core/agents/planner_schema.py +9 -0
- tunacode/core/agents/readonly.py +51 -0
- tunacode/core/background/__init__.py +0 -0
- tunacode/core/background/manager.py +36 -0
- tunacode/core/llm/__init__.py +0 -0
- tunacode/core/llm/planner.py +63 -0
- tunacode/core/setup/config_setup.py +79 -44
- tunacode/core/setup/coordinator.py +20 -13
- tunacode/core/setup/git_safety_setup.py +35 -49
- tunacode/core/state.py +2 -9
- tunacode/exceptions.py +0 -2
- tunacode/prompts/system.txt +179 -69
- tunacode/tools/__init__.py +10 -1
- tunacode/tools/base.py +1 -1
- tunacode/tools/bash.py +5 -5
- tunacode/tools/grep.py +210 -250
- tunacode/tools/read_file.py +2 -8
- tunacode/tools/run_command.py +4 -11
- tunacode/tools/update_file.py +2 -6
- tunacode/ui/completers.py +32 -31
- tunacode/ui/console.py +3 -3
- tunacode/ui/input.py +8 -5
- tunacode/ui/keybindings.py +1 -3
- tunacode/ui/lexers.py +16 -16
- tunacode/ui/output.py +2 -2
- tunacode/ui/panels.py +8 -8
- tunacode/ui/prompt_manager.py +19 -7
- tunacode/utils/import_cache.py +11 -0
- tunacode/utils/user_configuration.py +24 -2
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/METADATA +68 -11
- tunacode_cli-0.0.19.dist-info/RECORD +75 -0
- tunacode_cli-0.0.17.dist-info/RECORD +0 -67
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/top_level.txt +0 -0
tunacode/cli/commands.py
CHANGED
|
@@ -6,7 +6,6 @@ from enum import Enum
|
|
|
6
6
|
from typing import Any, Dict, List, Optional, Type
|
|
7
7
|
|
|
8
8
|
from .. import utils
|
|
9
|
-
from ..configuration.models import ModelRegistry
|
|
10
9
|
from ..exceptions import ValidationError
|
|
11
10
|
from ..types import CommandArgs, CommandContext, CommandResult, ProcessRequestCallback
|
|
12
11
|
from ..ui import console as ui
|
|
@@ -116,9 +115,9 @@ class YoloCommand(SimpleCommand):
|
|
|
116
115
|
state = context.state_manager.session
|
|
117
116
|
state.yolo = not state.yolo
|
|
118
117
|
if state.yolo:
|
|
119
|
-
await ui.success("
|
|
118
|
+
await ui.success("All tools are now active ⚡ Please proceed with caution.\n")
|
|
120
119
|
else:
|
|
121
|
-
await ui.info("
|
|
120
|
+
await ui.info("Tool confirmations re-enabled for safety.\n")
|
|
122
121
|
|
|
123
122
|
|
|
124
123
|
class DumpCommand(SimpleCommand):
|
|
@@ -168,6 +167,39 @@ class ThoughtsCommand(SimpleCommand):
|
|
|
168
167
|
await ui.success(f"Thought display {status}")
|
|
169
168
|
|
|
170
169
|
|
|
170
|
+
class ArchitectCommand(SimpleCommand):
|
|
171
|
+
"""Toggle architect mode for task planning and orchestration."""
|
|
172
|
+
|
|
173
|
+
def __init__(self):
|
|
174
|
+
super().__init__(
|
|
175
|
+
CommandSpec(
|
|
176
|
+
name="architect",
|
|
177
|
+
aliases=["/architect"],
|
|
178
|
+
description="Toggle architect mode (task planning & orchestration)",
|
|
179
|
+
category=CommandCategory.DEBUG,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
184
|
+
state = context.state_manager.session
|
|
185
|
+
if args:
|
|
186
|
+
arg = args[0].lower()
|
|
187
|
+
if arg in {"on", "1", "true"}:
|
|
188
|
+
state.architect_mode = True
|
|
189
|
+
elif arg in {"off", "0", "false"}:
|
|
190
|
+
state.architect_mode = False
|
|
191
|
+
else:
|
|
192
|
+
await ui.error("Usage: /architect [on|off]")
|
|
193
|
+
return
|
|
194
|
+
else:
|
|
195
|
+
state.architect_mode = not getattr(state, 'architect_mode', False)
|
|
196
|
+
status = "ON" if state.architect_mode else "OFF"
|
|
197
|
+
if state.architect_mode:
|
|
198
|
+
await ui.success(f"Architect mode {status} - Requests will be planned before execution")
|
|
199
|
+
else:
|
|
200
|
+
await ui.success(f"Architect mode {status} - Using direct execution")
|
|
201
|
+
|
|
202
|
+
|
|
171
203
|
class IterationsCommand(SimpleCommand):
|
|
172
204
|
"""Configure maximum agent iterations for ReAct reasoning."""
|
|
173
205
|
|
|
@@ -189,12 +221,12 @@ class IterationsCommand(SimpleCommand):
|
|
|
189
221
|
if new_limit < 1 or new_limit > 50:
|
|
190
222
|
await ui.error("Iterations must be between 1 and 50")
|
|
191
223
|
return
|
|
192
|
-
|
|
224
|
+
|
|
193
225
|
# Update the user config
|
|
194
226
|
if "settings" not in state.user_config:
|
|
195
227
|
state.user_config["settings"] = {}
|
|
196
228
|
state.user_config["settings"]["max_iterations"] = new_limit
|
|
197
|
-
|
|
229
|
+
|
|
198
230
|
await ui.success(f"Maximum iterations set to {new_limit}")
|
|
199
231
|
await ui.muted("Higher values allow more complex reasoning but may be slower")
|
|
200
232
|
except ValueError:
|
|
@@ -221,8 +253,9 @@ class ClearCommand(SimpleCommand):
|
|
|
221
253
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
222
254
|
# Patch any orphaned tool calls before clearing
|
|
223
255
|
from tunacode.core.agents.main import patch_tool_messages
|
|
256
|
+
|
|
224
257
|
patch_tool_messages("Conversation cleared", context.state_manager)
|
|
225
|
-
|
|
258
|
+
|
|
226
259
|
await ui.clear()
|
|
227
260
|
context.state_manager.session.messages = []
|
|
228
261
|
await ui.success("Message history cleared")
|
|
@@ -243,17 +276,17 @@ class FixCommand(SimpleCommand):
|
|
|
243
276
|
|
|
244
277
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
245
278
|
from tunacode.core.agents.main import patch_tool_messages
|
|
246
|
-
|
|
279
|
+
|
|
247
280
|
# Count current messages
|
|
248
281
|
before_count = len(context.state_manager.session.messages)
|
|
249
|
-
|
|
282
|
+
|
|
250
283
|
# Patch orphaned tool calls
|
|
251
284
|
patch_tool_messages("Tool call resolved by /fix command", context.state_manager)
|
|
252
|
-
|
|
285
|
+
|
|
253
286
|
# Count after patching
|
|
254
287
|
after_count = len(context.state_manager.session.messages)
|
|
255
288
|
patched_count = after_count - before_count
|
|
256
|
-
|
|
289
|
+
|
|
257
290
|
if patched_count > 0:
|
|
258
291
|
await ui.success(f"Fixed {patched_count} orphaned tool call(s)")
|
|
259
292
|
await ui.muted("You can now continue the conversation normally")
|
|
@@ -269,36 +302,37 @@ class ParseToolsCommand(SimpleCommand):
|
|
|
269
302
|
CommandSpec(
|
|
270
303
|
name="parsetools",
|
|
271
304
|
aliases=["/parsetools"],
|
|
272
|
-
description=
|
|
305
|
+
description=(
|
|
306
|
+
"Parse JSON tool calls from last response when structured calling fails"
|
|
307
|
+
),
|
|
273
308
|
category=CommandCategory.DEBUG,
|
|
274
309
|
)
|
|
275
310
|
)
|
|
276
311
|
|
|
277
312
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
278
313
|
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
279
|
-
|
|
314
|
+
|
|
280
315
|
# Find the last model response in messages
|
|
281
316
|
messages = context.state_manager.session.messages
|
|
282
317
|
if not messages:
|
|
283
318
|
await ui.error("No message history found")
|
|
284
319
|
return
|
|
285
|
-
|
|
320
|
+
|
|
286
321
|
# Look for the most recent response with text content
|
|
287
322
|
found_content = False
|
|
288
323
|
for msg in reversed(messages):
|
|
289
|
-
if hasattr(msg,
|
|
324
|
+
if hasattr(msg, "parts"):
|
|
290
325
|
for part in msg.parts:
|
|
291
|
-
if hasattr(part,
|
|
326
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
292
327
|
# Create tool callback
|
|
293
328
|
from tunacode.cli.repl import _tool_handler
|
|
329
|
+
|
|
294
330
|
def tool_callback_with_state(part, node):
|
|
295
331
|
return _tool_handler(part, node, context.state_manager)
|
|
296
|
-
|
|
332
|
+
|
|
297
333
|
try:
|
|
298
334
|
await extract_and_execute_tool_calls(
|
|
299
|
-
part.content,
|
|
300
|
-
tool_callback_with_state,
|
|
301
|
-
context.state_manager
|
|
335
|
+
part.content, tool_callback_with_state, context.state_manager
|
|
302
336
|
)
|
|
303
337
|
await ui.success("JSON tool parsing completed")
|
|
304
338
|
found_content = True
|
|
@@ -306,7 +340,7 @@ class ParseToolsCommand(SimpleCommand):
|
|
|
306
340
|
except Exception as e:
|
|
307
341
|
await ui.error(f"Failed to parse tools: {str(e)}")
|
|
308
342
|
return
|
|
309
|
-
|
|
343
|
+
|
|
310
344
|
if not found_content:
|
|
311
345
|
await ui.error("No parseable content found in recent messages")
|
|
312
346
|
|
|
@@ -326,7 +360,7 @@ class RefreshConfigCommand(SimpleCommand):
|
|
|
326
360
|
|
|
327
361
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
328
362
|
from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
329
|
-
|
|
363
|
+
|
|
330
364
|
# Update current session config with latest defaults
|
|
331
365
|
for key, value in DEFAULT_USER_CONFIG.items():
|
|
332
366
|
if key not in context.state_manager.session.user_config:
|
|
@@ -336,9 +370,11 @@ class RefreshConfigCommand(SimpleCommand):
|
|
|
336
370
|
for subkey, subvalue in value.items():
|
|
337
371
|
if subkey not in context.state_manager.session.user_config[key]:
|
|
338
372
|
context.state_manager.session.user_config[key][subkey] = subvalue
|
|
339
|
-
|
|
373
|
+
|
|
340
374
|
# Show updated max_iterations
|
|
341
|
-
max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
|
|
375
|
+
max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
|
|
376
|
+
"max_iterations", 20
|
|
377
|
+
)
|
|
342
378
|
await ui.success(f"Configuration refreshed - max iterations: {max_iterations}")
|
|
343
379
|
|
|
344
380
|
|
|
@@ -507,23 +543,20 @@ class UpdateCommand(SimpleCommand):
|
|
|
507
543
|
)
|
|
508
544
|
|
|
509
545
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
546
|
+
import shutil
|
|
510
547
|
import subprocess
|
|
511
548
|
import sys
|
|
512
|
-
import shutil
|
|
513
549
|
|
|
514
550
|
await ui.info("Checking for TunaCode updates...")
|
|
515
551
|
|
|
516
552
|
# Detect installation method
|
|
517
553
|
installation_method = None
|
|
518
|
-
|
|
554
|
+
|
|
519
555
|
# Check if installed via pipx
|
|
520
556
|
if shutil.which("pipx"):
|
|
521
557
|
try:
|
|
522
558
|
result = subprocess.run(
|
|
523
|
-
["pipx", "list"],
|
|
524
|
-
capture_output=True,
|
|
525
|
-
text=True,
|
|
526
|
-
timeout=10
|
|
559
|
+
["pipx", "list"], capture_output=True, text=True, timeout=10
|
|
527
560
|
)
|
|
528
561
|
if "tunacode" in result.stdout.lower():
|
|
529
562
|
installation_method = "pipx"
|
|
@@ -534,10 +567,10 @@ class UpdateCommand(SimpleCommand):
|
|
|
534
567
|
if not installation_method:
|
|
535
568
|
try:
|
|
536
569
|
result = subprocess.run(
|
|
537
|
-
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
538
|
-
capture_output=True,
|
|
539
|
-
text=True,
|
|
540
|
-
timeout=10
|
|
570
|
+
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
571
|
+
capture_output=True,
|
|
572
|
+
text=True,
|
|
573
|
+
timeout=10,
|
|
541
574
|
)
|
|
542
575
|
if result.returncode == 0:
|
|
543
576
|
installation_method = "pip"
|
|
@@ -556,10 +589,7 @@ class UpdateCommand(SimpleCommand):
|
|
|
556
589
|
if installation_method == "pipx":
|
|
557
590
|
await ui.info("Updating via pipx...")
|
|
558
591
|
result = subprocess.run(
|
|
559
|
-
["pipx", "upgrade", "tunacode"],
|
|
560
|
-
capture_output=True,
|
|
561
|
-
text=True,
|
|
562
|
-
timeout=60
|
|
592
|
+
["pipx", "upgrade", "tunacode"], capture_output=True, text=True, timeout=60
|
|
563
593
|
)
|
|
564
594
|
else: # pip
|
|
565
595
|
await ui.info("Updating via pip...")
|
|
@@ -567,16 +597,16 @@ class UpdateCommand(SimpleCommand):
|
|
|
567
597
|
[sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
|
|
568
598
|
capture_output=True,
|
|
569
599
|
text=True,
|
|
570
|
-
timeout=60
|
|
600
|
+
timeout=60,
|
|
571
601
|
)
|
|
572
602
|
|
|
573
603
|
if result.returncode == 0:
|
|
574
604
|
await ui.success("TunaCode updated successfully!")
|
|
575
605
|
await ui.muted("Restart TunaCode to use the new version")
|
|
576
|
-
|
|
606
|
+
|
|
577
607
|
# Show update output if available
|
|
578
608
|
if result.stdout.strip():
|
|
579
|
-
output_lines = result.stdout.strip().split(
|
|
609
|
+
output_lines = result.stdout.strip().split("\n")
|
|
580
610
|
for line in output_lines[-5:]: # Show last 5 lines
|
|
581
611
|
if line.strip():
|
|
582
612
|
await ui.muted(f" {line}")
|
|
@@ -723,6 +753,7 @@ class CommandRegistry:
|
|
|
723
753
|
YoloCommand,
|
|
724
754
|
DumpCommand,
|
|
725
755
|
ThoughtsCommand,
|
|
756
|
+
ArchitectCommand,
|
|
726
757
|
IterationsCommand,
|
|
727
758
|
ClearCommand,
|
|
728
759
|
FixCommand,
|
|
@@ -798,8 +829,9 @@ class CommandRegistry:
|
|
|
798
829
|
return await command.execute(args, context)
|
|
799
830
|
else:
|
|
800
831
|
# Ambiguous - show possibilities
|
|
832
|
+
matches_str = ", ".join(sorted(set(matches)))
|
|
801
833
|
raise ValidationError(
|
|
802
|
-
f"Ambiguous command '{command_name}'. Did you mean: {
|
|
834
|
+
f"Ambiguous command '{command_name}'. Did you mean: {matches_str}?"
|
|
803
835
|
)
|
|
804
836
|
|
|
805
837
|
def find_matching_commands(self, partial_command: str) -> List[str]:
|
tunacode/cli/main.py
CHANGED
|
@@ -24,36 +24,39 @@ state_manager = StateManager()
|
|
|
24
24
|
def main(
|
|
25
25
|
version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
|
|
26
26
|
run_setup: bool = typer.Option(False, "--setup", help="Run setup process."),
|
|
27
|
-
baseurl: str = typer.Option(
|
|
27
|
+
baseurl: str = typer.Option(
|
|
28
|
+
None, "--baseurl", help="API base URL (e.g., https://openrouter.ai/api/v1)"
|
|
29
|
+
),
|
|
28
30
|
model: str = typer.Option(None, "--model", help="Default model to use (e.g., openai/gpt-4)"),
|
|
29
31
|
key: str = typer.Option(None, "--key", help="API key for the provider"),
|
|
30
32
|
):
|
|
31
33
|
"""🚀 Start TunaCode - Your AI-powered development assistant"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
asyncio.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
|
|
35
|
+
async def async_main():
|
|
36
|
+
if version:
|
|
37
|
+
await ui.version()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
await ui.banner()
|
|
41
|
+
|
|
42
|
+
# Start update check in background
|
|
43
|
+
update_task = asyncio.to_thread(check_for_updates)
|
|
44
|
+
|
|
45
|
+
cli_config = {}
|
|
46
|
+
if baseurl or model or key:
|
|
47
|
+
cli_config = {"baseurl": baseurl, "model": model, "key": key}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
await setup(run_setup, state_manager, cli_config)
|
|
51
|
+
await repl(state_manager)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
await ui.error(str(e))
|
|
54
|
+
|
|
55
|
+
has_update, latest_version = await update_task
|
|
56
|
+
if has_update:
|
|
57
|
+
await ui.update_available(latest_version)
|
|
58
|
+
|
|
59
|
+
asyncio.run(async_main())
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
if __name__ == "__main__":
|
tunacode/cli/repl.py
CHANGED
|
@@ -6,6 +6,8 @@ Handles user input, command processing, and agent interaction in an interactive
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
9
11
|
from asyncio.exceptions import CancelledError
|
|
10
12
|
|
|
11
13
|
from prompt_toolkit.application import run_in_terminal
|
|
@@ -15,6 +17,7 @@ from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
|
15
17
|
from tunacode.configuration.settings import ApplicationSettings
|
|
16
18
|
from tunacode.core.agents import main as agent
|
|
17
19
|
from tunacode.core.agents.main import patch_tool_messages
|
|
20
|
+
from tunacode.core.agents.orchestrator import OrchestratorAgent
|
|
18
21
|
from tunacode.core.tool_handler import ToolHandler
|
|
19
22
|
from tunacode.exceptions import AgentError, UserAbortError, ValidationError
|
|
20
23
|
from tunacode.ui import console as ui
|
|
@@ -158,41 +161,69 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
158
161
|
True, state_manager.session.spinner, state_manager
|
|
159
162
|
)
|
|
160
163
|
try:
|
|
161
|
-
# Expand @file references before sending to the agent
|
|
162
|
-
try:
|
|
163
|
-
from tunacode.utils.text_utils import expand_file_refs
|
|
164
|
-
|
|
165
|
-
text = expand_file_refs(text)
|
|
166
|
-
except ValueError as e:
|
|
167
|
-
await ui.error(str(e))
|
|
168
|
-
return
|
|
169
|
-
|
|
170
164
|
# Patch any orphaned tool calls from previous requests before proceeding
|
|
171
165
|
patch_tool_messages("Tool execution was interrupted", state_manager)
|
|
172
|
-
|
|
166
|
+
|
|
167
|
+
# Track message start for thoughts display
|
|
168
|
+
start_idx = len(state_manager.session.messages)
|
|
169
|
+
|
|
173
170
|
# Create a partial function that includes state_manager
|
|
174
171
|
def tool_callback_with_state(part, node):
|
|
175
172
|
return _tool_handler(part, node, state_manager)
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
174
|
+
# Check if architect mode is enabled
|
|
175
|
+
if getattr(state_manager.session, 'architect_mode', False):
|
|
176
|
+
# Expand @file references before sending to the orchestrator
|
|
177
|
+
try:
|
|
178
|
+
from tunacode.utils.text_utils import expand_file_refs
|
|
179
|
+
|
|
180
|
+
text = expand_file_refs(text)
|
|
181
|
+
except ValueError as e:
|
|
182
|
+
await ui.error(str(e))
|
|
183
|
+
return
|
|
184
|
+
# Use orchestrator for planning and execution
|
|
185
|
+
orchestrator = OrchestratorAgent(state_manager)
|
|
186
|
+
results = await orchestrator.run(text, state_manager.session.current_model)
|
|
187
|
+
|
|
188
|
+
if output:
|
|
189
|
+
# Process results from all sub-agents
|
|
190
|
+
for res in results:
|
|
191
|
+
# Check if result exists and has output
|
|
192
|
+
if hasattr(res, "result") and res.result is not None and hasattr(res.result, "output"):
|
|
193
|
+
await ui.agent(res.result.output)
|
|
194
|
+
|
|
195
|
+
if not results:
|
|
196
|
+
# Fallback: show that the request was processed
|
|
197
|
+
await ui.muted("Request completed")
|
|
198
|
+
else:
|
|
199
|
+
# Expand @file references before sending to the agent
|
|
200
|
+
try:
|
|
201
|
+
from tunacode.utils.text_utils import expand_file_refs
|
|
202
|
+
|
|
203
|
+
text = expand_file_refs(text)
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
await ui.error(str(e))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Use normal agent processing
|
|
209
|
+
res = await agent.process_request(
|
|
210
|
+
state_manager.session.current_model,
|
|
211
|
+
text,
|
|
212
|
+
state_manager,
|
|
213
|
+
tool_callback=tool_callback_with_state,
|
|
214
|
+
)
|
|
215
|
+
if output:
|
|
216
|
+
if state_manager.session.show_thoughts:
|
|
217
|
+
new_msgs = state_manager.session.messages[start_idx:]
|
|
218
|
+
for msg in new_msgs:
|
|
219
|
+
if isinstance(msg, dict) and "thought" in msg:
|
|
220
|
+
await ui.muted(f"THOUGHT: {msg['thought']}")
|
|
221
|
+
# Check if result exists and has output
|
|
222
|
+
if hasattr(res, "result") and res.result is not None and hasattr(res.result, "output"):
|
|
223
|
+
await ui.agent(res.result.output)
|
|
224
|
+
else:
|
|
225
|
+
# Fallback: show that the request was processed
|
|
226
|
+
await ui.muted("Request completed")
|
|
196
227
|
except CancelledError:
|
|
197
228
|
await ui.muted("Request cancelled")
|
|
198
229
|
except UserAbortError:
|
|
@@ -204,29 +235,28 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
204
235
|
except Exception as e:
|
|
205
236
|
# Check if this might be a tool calling failure that we can recover from
|
|
206
237
|
error_str = str(e).lower()
|
|
207
|
-
if any(keyword in error_str for keyword in [
|
|
238
|
+
if any(keyword in error_str for keyword in ["tool", "function", "call", "schema"]):
|
|
208
239
|
# Try to extract and execute tool calls from the last response
|
|
209
240
|
if state_manager.session.messages:
|
|
210
241
|
last_msg = state_manager.session.messages[-1]
|
|
211
|
-
if hasattr(last_msg,
|
|
242
|
+
if hasattr(last_msg, "parts"):
|
|
212
243
|
for part in last_msg.parts:
|
|
213
|
-
if hasattr(part,
|
|
244
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
214
245
|
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
246
|
+
|
|
215
247
|
try:
|
|
216
248
|
# Create a partial function that includes state_manager
|
|
217
249
|
def tool_callback_with_state(part, node):
|
|
218
250
|
return _tool_handler(part, node, state_manager)
|
|
219
|
-
|
|
251
|
+
|
|
220
252
|
await extract_and_execute_tool_calls(
|
|
221
|
-
part.content,
|
|
222
|
-
tool_callback_with_state,
|
|
223
|
-
state_manager
|
|
253
|
+
part.content, tool_callback_with_state, state_manager
|
|
224
254
|
)
|
|
225
255
|
await ui.warning("🔧 Recovered using JSON tool parsing")
|
|
226
256
|
return # Successfully recovered
|
|
227
257
|
except Exception:
|
|
228
258
|
pass # Fallback failed, continue with normal error handling
|
|
229
|
-
|
|
259
|
+
|
|
230
260
|
# Wrap unexpected exceptions in AgentError for better tracking
|
|
231
261
|
agent_error = AgentError(f"Agent processing failed: {str(e)}")
|
|
232
262
|
agent_error.__cause__ = e # Preserve the original exception chain
|
|
@@ -271,6 +301,30 @@ async def repl(state_manager: StateManager):
|
|
|
271
301
|
break
|
|
272
302
|
continue
|
|
273
303
|
|
|
304
|
+
if line.startswith("!"):
|
|
305
|
+
command = line[1:].strip()
|
|
306
|
+
|
|
307
|
+
# Show tool-style header for bash commands
|
|
308
|
+
cmd_display = command if command else "Interactive shell"
|
|
309
|
+
await ui.panel("Tool(bash)", f"Command: {cmd_display}", border_style="yellow")
|
|
310
|
+
|
|
311
|
+
def run_shell():
|
|
312
|
+
try:
|
|
313
|
+
if command:
|
|
314
|
+
result = subprocess.run(command, shell=True, capture_output=False)
|
|
315
|
+
if result.returncode != 0:
|
|
316
|
+
# Use print directly since we're in a terminal context
|
|
317
|
+
print(f"\nCommand exited with code {result.returncode}")
|
|
318
|
+
else:
|
|
319
|
+
shell = os.environ.get("SHELL", "bash")
|
|
320
|
+
subprocess.run(shell)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print(f"\nShell command failed: {str(e)}")
|
|
323
|
+
|
|
324
|
+
await run_in_terminal(run_shell)
|
|
325
|
+
await ui.line()
|
|
326
|
+
continue
|
|
327
|
+
|
|
274
328
|
# Check if another task is already running
|
|
275
329
|
if state_manager.session.current_task and not state_manager.session.current_task.done():
|
|
276
330
|
await ui.muted("Agent is busy, press Ctrl+C to interrupt.")
|