tunacode-cli 0.0.28__py3-none-any.whl → 0.0.29__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.
@@ -22,6 +22,7 @@ DEFAULT_USER_CONFIG: UserConfig = {
22
22
  "tool_ignore": [TOOL_READ_FILE],
23
23
  "guide_file": GUIDE_FILE_NAME,
24
24
  "fallback_response": True,
25
+ "fallback_verbosity": "normal", # Options: minimal, normal, detailed
25
26
  },
26
27
  "mcpServers": {},
27
28
  }
tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.28"
10
+ APP_VERSION = "0.0.29"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -375,10 +375,91 @@ async def process_request(
375
375
  if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
376
376
  patch_tool_messages("Task incomplete", state_manager=state_manager)
377
377
  response_state.has_final_synthesis = True
378
+
379
+ # Extract context from the agent run
380
+ tool_calls_summary = []
381
+ files_modified = set()
382
+ commands_run = []
383
+
384
+ # Analyze message history for context
385
+ for msg in state_manager.session.messages:
386
+ if hasattr(msg, "parts"):
387
+ for part in msg.parts:
388
+ if hasattr(part, "part_kind") and part.part_kind == "tool-call":
389
+ tool_name = getattr(part, "tool_name", "unknown")
390
+ tool_calls_summary.append(tool_name)
391
+
392
+ # Track specific operations
393
+ if tool_name in ["write_file", "update_file"] and hasattr(part, "args"):
394
+ if "file_path" in part.args:
395
+ files_modified.add(part.args["file_path"])
396
+ elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
397
+ if "command" in part.args:
398
+ commands_run.append(part.args["command"])
399
+
400
+ # Build fallback response with context
378
401
  fallback = FallbackResponse(
379
402
  summary="Reached maximum iterations without producing a final response.",
380
- progress=f"{i}/{max_iterations} iterations completed",
403
+ progress=f"Completed {i} iterations (limit: {max_iterations})",
404
+ )
405
+
406
+ # Get verbosity setting
407
+ verbosity = state_manager.session.user_config.get("settings", {}).get(
408
+ "fallback_verbosity", "normal"
409
+ )
410
+
411
+ if verbosity in ["normal", "detailed"]:
412
+ # Add what was attempted
413
+ if tool_calls_summary:
414
+ tool_counts = {}
415
+ for tool in tool_calls_summary:
416
+ tool_counts[tool] = tool_counts.get(tool, 0) + 1
417
+
418
+ fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
419
+ for tool, count in sorted(tool_counts.items()):
420
+ fallback.issues.append(f" • {tool}: {count}x")
421
+
422
+ if verbosity == "detailed":
423
+ if files_modified:
424
+ fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
425
+ for f in sorted(files_modified)[:5]: # Limit to 5 files
426
+ fallback.issues.append(f" • {f}")
427
+ if len(files_modified) > 5:
428
+ fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
429
+
430
+ if commands_run:
431
+ fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
432
+ for cmd in commands_run[:3]: # Limit to 3 commands
433
+ # Truncate long commands
434
+ display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
435
+ fallback.issues.append(f" • {display_cmd}")
436
+ if len(commands_run) > 3:
437
+ fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
438
+
439
+ # Add helpful next steps
440
+ fallback.next_steps.append(
441
+ "The task may be too complex - try breaking it into smaller steps"
381
442
  )
443
+ fallback.next_steps.append("Check the output above for any errors or partial progress")
444
+ if files_modified:
445
+ fallback.next_steps.append("Review modified files to see what changes were made")
446
+
447
+ # Create comprehensive output
448
+ output_parts = [fallback.summary, ""]
449
+
450
+ if fallback.progress:
451
+ output_parts.append(f"Progress: {fallback.progress}")
452
+
453
+ if fallback.issues:
454
+ output_parts.append("\nWhat happened:")
455
+ output_parts.extend(fallback.issues)
456
+
457
+ if fallback.next_steps:
458
+ output_parts.append("\nSuggested next steps:")
459
+ for step in fallback.next_steps:
460
+ output_parts.append(f" • {step}")
461
+
462
+ comprehensive_output = "\n".join(output_parts)
382
463
 
383
464
  # Create a wrapper object that mimics AgentRun with the required attributes
384
465
  class AgentRunWrapper:
@@ -391,7 +472,7 @@ async def process_request(
391
472
  # Delegate all other attributes to the wrapped object
392
473
  return getattr(self._wrapped, name)
393
474
 
394
- return AgentRunWrapper(agent_run, SimpleResult(fallback.summary))
475
+ return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
395
476
 
396
477
  # For non-fallback cases, we still need to handle the response_state
397
478
  # Create a minimal wrapper just to add response_state
@@ -13,7 +13,7 @@ import asyncio
13
13
  import itertools
14
14
  from typing import List
15
15
 
16
- from ...types import AgentRun, ModelName
16
+ from ...types import AgentRun, FallbackResponse, ModelName, ResponseState
17
17
  from ..llm.planner import make_plan
18
18
  from ..state import StateManager
19
19
  from . import main as agent_main
@@ -69,6 +69,9 @@ class OrchestratorAgent:
69
69
  console = Console()
70
70
  model = model or self.state.session.current_model
71
71
 
72
+ # Track response state across all sub-tasks
73
+ response_state = ResponseState()
74
+
72
75
  # Show orchestrator is starting
73
76
  console.print(
74
77
  "\n[cyan]Orchestrator Mode: Analyzing request and creating execution plan...[/cyan]"
@@ -80,10 +83,28 @@ class OrchestratorAgent:
80
83
  console.print(f"\n[cyan]Executing plan with {len(tasks)} tasks...[/cyan]")
81
84
 
82
85
  results: List[AgentRun] = []
86
+ task_progress = []
87
+
83
88
  for mutate_flag, group in itertools.groupby(tasks, key=lambda t: t.mutate):
84
89
  if mutate_flag:
85
90
  for t in group:
86
- results.append(await self._run_sub_task(t, model))
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
87
108
  else:
88
109
  # Show parallel execution
89
110
  task_list = list(group)
@@ -92,26 +113,101 @@ class OrchestratorAgent:
92
113
  f"\n[dim][Parallel Execution] Running {len(task_list)} read-only tasks concurrently...[/dim]"
93
114
  )
94
115
  coros = [self._run_sub_task(t, model) for t in task_list]
95
- results.extend(await asyncio.gather(*coros))
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
96
134
 
97
135
  console.print("\n[green]Orchestrator completed all tasks successfully![/green]")
98
136
 
99
- has_output = any(
137
+ # Check if we need a fallback response
138
+ has_any_output = any(
100
139
  hasattr(r, "result") and r.result and getattr(r.result, "output", None) for r in results
101
140
  )
102
141
 
103
- if results and not has_output:
104
- lines = [f"Task {i + 1} completed" for i in range(len(results))]
105
- summary = "\n".join(lines)
142
+ fallback_enabled = self.state.session.user_config.get("settings", {}).get(
143
+ "fallback_response", True
144
+ )
106
145
 
107
- class SynthResult:
108
- def __init__(self, output: str):
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):
109
202
  self.output = output
203
+ self.response_state = response_state
110
204
 
111
- class SynthRun:
112
- def __init__(self):
113
- self.result = SynthResult(summary)
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
114
209
 
115
- results.append(SynthRun())
210
+ response_state.has_final_synthesis = True
211
+ results.append(FallbackRun(synthesis, response_state))
116
212
 
117
213
  return results
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from ...tools.grep import grep
8
8
  from ...tools.read_file import read_file
9
- from ...types import AgentRun, ModelName
9
+ from ...types import AgentRun, ModelName, ResponseState
10
10
  from ..state import StateManager
11
11
 
12
12
  if TYPE_CHECKING:
@@ -42,10 +42,24 @@ class ReadOnlyAgent:
42
42
  async def process_request(self, request: str) -> AgentRun:
43
43
  """Process a request using only read-only tools."""
44
44
  agent = self._get_agent()
45
+ response_state = ResponseState()
45
46
 
46
47
  # Use iter() like main.py does to get the full run context
47
48
  async with agent.iter(request) as agent_run:
48
- async for _ in agent_run:
49
- pass # Let it complete
49
+ async for node in agent_run:
50
+ # Check if this node produced user-visible output
51
+ if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
52
+ if node.result.output:
53
+ response_state.has_user_response = True
50
54
 
51
- return agent_run
55
+ # Wrap the agent run to include response_state
56
+ class AgentRunWithState:
57
+ def __init__(self, wrapped_run):
58
+ self._wrapped = wrapped_run
59
+ self.response_state = response_state
60
+
61
+ def __getattr__(self, name):
62
+ # Delegate all other attributes to the wrapped object
63
+ return getattr(self._wrapped, name)
64
+
65
+ return AgentRunWithState(agent_run)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.28
3
+ Version: 0.0.29
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=br_qbqbT1sFcUxA2mpTclUyQn-jsVMr2hrsReyBP8Rc,3799
2
+ tunacode/constants.py,sha256=yChcPTCk4tc0Q1JArwWxIZ2Zxi9S-vLgKiBOdMml6eI,3799
3
3
  tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
4
4
  tunacode/exceptions.py,sha256=_Dyj6cC0868dMABekdQHXCg5XhucJumbGUMXaSDzgB4,2645
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -12,17 +12,17 @@ tunacode/cli/repl.py,sha256=sXtRHYameAlMjlee82ix8P2JjRyWLrdFfHwxfaMKPb8,13722
12
12
  tunacode/cli/textual_app.py,sha256=14-Nt0IIETmyHBrNn9uwSF3EwCcutwTp6gdoKgNm0sY,12593
13
13
  tunacode/cli/textual_bridge.py,sha256=CTuf5Yjg5occQa7whyopS_erbJdS2UpWqaX_TVJ2D2A,6140
14
14
  tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
15
- tunacode/configuration/defaults.py,sha256=9JqGMdKsCT1uCmNVydXHEP1gsvqSMDSEbe8DUZcV_KI,723
15
+ tunacode/configuration/defaults.py,sha256=oLgmHprB3cTaFvT9dn_rgg206zoj09GRXRbI7MYijxA,801
16
16
  tunacode/configuration/models.py,sha256=XPobkLM_TzKTuMIWhK-svJfGRGFT9r2LhKEM6rv6QHk,3756
17
17
  tunacode/configuration/settings.py,sha256=lm2ov8rG1t4C5JIXMOhIKik5FAsjpuLVYtFmnE1ZQ3k,995
18
18
  tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  tunacode/core/state.py,sha256=n1claG-gVVDMBCCS8cDmgas4XbKLJJwKRc-8CtXeTS8,1376
20
20
  tunacode/core/tool_handler.py,sha256=OKx7jM8pml6pSEnoARu33_uBY8awJBqvhbVeBn6T4ow,1804
21
21
  tunacode/core/agents/__init__.py,sha256=TiXwymGRNMuOqQaRLpNCAt7bZArg8rkadIRs4Nw21SQ,275
22
- tunacode/core/agents/main.py,sha256=bfm_TS1Z9rpy3phIUb7d1lLbYK99xJwrtBy4PWPSEOE,16673
23
- tunacode/core/agents/orchestrator.py,sha256=t_eXtCO_QkIz687uT88RpDCmaE0-UkrUaczuTEJOrYQ,4154
22
+ tunacode/core/agents/main.py,sha256=FCExe7pQ57z-GQh9p1RHEgaxHdMotJS21htdnHB-VK0,20612
23
+ tunacode/core/agents/orchestrator.py,sha256=SSJyd4O_WZzcI5zygoUx1nhzRpfT6DrPsp2XtK2UVzY,8607
24
24
  tunacode/core/agents/planner_schema.py,sha256=pu2ehQVALjiJ5HJD7EN6xuZHCknsrXO9z0xHuVdlKW8,345
25
- tunacode/core/agents/readonly.py,sha256=NOPfqPWu53fJy77k5uqwKWmZ6yzqnDOZpBeQjGy0AG8,1624
25
+ tunacode/core/agents/readonly.py,sha256=jOG3CF5G1y9k4sBn7ChXN1GXWbmB_0pFGcaMMI4iaLs,2325
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
@@ -67,9 +67,9 @@ tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
67
67
  tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
68
68
  tunacode/utils/text_utils.py,sha256=B9M1cuLTm_SSsr15WNHF6j7WdLWPvWzKZV0Lvfgdbjg,2458
69
69
  tunacode/utils/user_configuration.py,sha256=IGvUH37wWtZ4M5xpukZEWYhtuKKyKcl6DaeObGXdleU,2610
70
- tunacode_cli-0.0.28.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
71
- tunacode_cli-0.0.28.dist-info/METADATA,sha256=QAmjxfmzJELeyebnE3H5wCkgGILFP7COYWRcKkl6JFA,3619
72
- tunacode_cli-0.0.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- tunacode_cli-0.0.28.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
74
- tunacode_cli-0.0.28.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
75
- tunacode_cli-0.0.28.dist-info/RECORD,,
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,,