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.

Files changed (47) hide show
  1. tunacode/cli/commands.py +73 -41
  2. tunacode/cli/main.py +29 -26
  3. tunacode/cli/repl.py +91 -37
  4. tunacode/cli/textual_app.py +69 -66
  5. tunacode/cli/textual_bridge.py +33 -32
  6. tunacode/configuration/settings.py +2 -9
  7. tunacode/constants.py +2 -4
  8. tunacode/context.py +1 -1
  9. tunacode/core/agents/__init__.py +12 -0
  10. tunacode/core/agents/main.py +89 -63
  11. tunacode/core/agents/orchestrator.py +99 -0
  12. tunacode/core/agents/planner_schema.py +9 -0
  13. tunacode/core/agents/readonly.py +51 -0
  14. tunacode/core/background/__init__.py +0 -0
  15. tunacode/core/background/manager.py +36 -0
  16. tunacode/core/llm/__init__.py +0 -0
  17. tunacode/core/llm/planner.py +63 -0
  18. tunacode/core/setup/config_setup.py +79 -44
  19. tunacode/core/setup/coordinator.py +20 -13
  20. tunacode/core/setup/git_safety_setup.py +35 -49
  21. tunacode/core/state.py +2 -9
  22. tunacode/exceptions.py +0 -2
  23. tunacode/prompts/system.txt +179 -69
  24. tunacode/tools/__init__.py +10 -1
  25. tunacode/tools/base.py +1 -1
  26. tunacode/tools/bash.py +5 -5
  27. tunacode/tools/grep.py +210 -250
  28. tunacode/tools/read_file.py +2 -8
  29. tunacode/tools/run_command.py +4 -11
  30. tunacode/tools/update_file.py +2 -6
  31. tunacode/ui/completers.py +32 -31
  32. tunacode/ui/console.py +3 -3
  33. tunacode/ui/input.py +8 -5
  34. tunacode/ui/keybindings.py +1 -3
  35. tunacode/ui/lexers.py +16 -16
  36. tunacode/ui/output.py +2 -2
  37. tunacode/ui/panels.py +8 -8
  38. tunacode/ui/prompt_manager.py +19 -7
  39. tunacode/utils/import_cache.py +11 -0
  40. tunacode/utils/user_configuration.py +24 -2
  41. {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/METADATA +68 -11
  42. tunacode_cli-0.0.19.dist-info/RECORD +75 -0
  43. tunacode_cli-0.0.17.dist-info/RECORD +0 -67
  44. {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/WHEEL +0 -0
  45. {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/entry_points.txt +0 -0
  46. {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/licenses/LICENSE +0 -0
  47. {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/top_level.txt +0 -0
@@ -9,8 +9,22 @@ import re
9
9
  from datetime import datetime, timezone
10
10
  from typing import Optional
11
11
 
12
- from pydantic_ai import Agent, Tool
13
- from pydantic_ai.messages import ModelRequest, ToolReturnPart
12
+ # Lazy import for Agent and Tool
13
+
14
+
15
+ def get_agent_tool():
16
+ import importlib
17
+
18
+ pydantic_ai = importlib.import_module("pydantic_ai")
19
+ return pydantic_ai.Agent, pydantic_ai.Tool
20
+
21
+
22
+ def get_model_messages():
23
+ import importlib
24
+
25
+ messages = importlib.import_module("pydantic_ai.messages")
26
+ return messages.ModelRequest, messages.ToolReturnPart
27
+
14
28
 
15
29
  from tunacode.core.state import StateManager
16
30
  from tunacode.services.mcp import get_mcp_servers
@@ -20,15 +34,8 @@ from tunacode.tools.read_file import read_file
20
34
  from tunacode.tools.run_command import run_command
21
35
  from tunacode.tools.update_file import update_file
22
36
  from tunacode.tools.write_file import write_file
23
- from tunacode.types import (
24
- AgentRun,
25
- ErrorMessage,
26
- ModelName,
27
- PydanticAgent,
28
- ToolCallback,
29
- ToolCallId,
30
- ToolName,
31
- )
37
+ from tunacode.types import (AgentRun, ErrorMessage, ModelName, PydanticAgent, ToolCallback,
38
+ ToolCallId, ToolName)
32
39
 
33
40
 
34
41
  async def _process_node(node, tool_callback: Optional[ToolCallback], state_manager: StateManager):
@@ -40,63 +47,65 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
40
47
  # Display thought immediately if show_thoughts is enabled
41
48
  if state_manager.session.show_thoughts:
42
49
  from tunacode.ui import console as ui
50
+
43
51
  await ui.muted(f"💭 THOUGHT: {node.thought}")
44
52
 
45
53
  if hasattr(node, "model_response"):
46
54
  state_manager.session.messages.append(node.model_response)
47
-
55
+
48
56
  # Enhanced ReAct thought processing
49
57
  if state_manager.session.show_thoughts:
50
- from tunacode.ui import console as ui
51
58
  import json
52
59
  import re
53
-
60
+
61
+ from tunacode.ui import console as ui
62
+
54
63
  for part in node.model_response.parts:
55
- if hasattr(part, 'content') and isinstance(part.content, str):
64
+ if hasattr(part, "content") and isinstance(part.content, str):
56
65
  content = part.content.strip()
57
-
66
+
58
67
  # Pattern 1: Inline JSON thoughts {"thought": "..."}
59
68
  thought_pattern = r'\{"thought":\s*"([^"]+)"\}'
60
69
  matches = re.findall(thought_pattern, content)
61
70
  for thought in matches:
62
71
  await ui.muted(f"💭 REASONING: {thought}")
63
-
72
+
64
73
  # Pattern 2: Standalone thought JSON objects
65
74
  try:
66
75
  if content.startswith('{"thought"'):
67
76
  thought_obj = json.loads(content)
68
- if 'thought' in thought_obj:
77
+ if "thought" in thought_obj:
69
78
  await ui.muted(f"💭 REASONING: {thought_obj['thought']}")
70
79
  except (json.JSONDecodeError, KeyError):
71
80
  pass
72
-
81
+
73
82
  # Pattern 3: Multi-line thoughts with context
74
83
  multiline_pattern = r'\{"thought":\s*"([^"]+(?:\\.[^"]*)*?)"\}'
75
84
  multiline_matches = re.findall(multiline_pattern, content, re.DOTALL)
76
85
  for thought in multiline_matches:
77
86
  if thought not in [m for m in matches]: # Avoid duplicates
78
87
  # Clean up escaped characters
79
- cleaned_thought = thought.replace('\\"', '"').replace('\\n', ' ')
88
+ cleaned_thought = thought.replace('\\"', '"').replace("\\n", " ")
80
89
  await ui.muted(f"💭 REASONING: {cleaned_thought}")
81
-
90
+
82
91
  # Pattern 4: Text-based reasoning indicators
83
92
  reasoning_indicators = [
84
- (r'I need to (.+?)\.', 'PLANNING'),
85
- (r'Let me (.+?)\.', 'ACTION'),
86
- (r'The output shows (.+?)\.', 'OBSERVATION'),
87
- (r'Based on (.+?), I should (.+?)\.', 'DECISION')
93
+ (r"I need to (.+?)\.", "PLANNING"),
94
+ (r"Let me (.+?)\.", "ACTION"),
95
+ (r"The output shows (.+?)\.", "OBSERVATION"),
96
+ (r"Based on (.+?), I should (.+?)\.", "DECISION"),
88
97
  ]
89
-
98
+
90
99
  for pattern, label in reasoning_indicators:
91
100
  indicator_matches = re.findall(pattern, content, re.IGNORECASE)
92
101
  for match in indicator_matches:
93
102
  if isinstance(match, tuple):
94
- match_text = ' '.join(match)
103
+ match_text = " ".join(match)
95
104
  else:
96
105
  match_text = match
97
106
  await ui.muted(f"🎯 {label}: {match_text}")
98
107
  break # Only show first match per pattern
99
-
108
+
100
109
  # Check for tool calls and fallback to JSON parsing if needed
101
110
  has_tool_calls = False
102
111
  for part in node.model_response.parts:
@@ -106,28 +115,32 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
106
115
  elif part.part_kind == "tool-return":
107
116
  obs_msg = f"OBSERVATION[{part.tool_name}]: {part.content[:2_000]}"
108
117
  state_manager.session.messages.append(obs_msg)
109
-
118
+
110
119
  # If no structured tool calls found, try parsing JSON from text content
111
120
  if not has_tool_calls and tool_callback:
112
121
  for part in node.model_response.parts:
113
- if hasattr(part, 'content') and isinstance(part.content, str):
122
+ if hasattr(part, "content") and isinstance(part.content, str):
114
123
  await extract_and_execute_tool_calls(part.content, tool_callback, state_manager)
115
124
 
116
125
 
117
126
  def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
118
127
  if model not in state_manager.session.agents:
119
128
  max_retries = state_manager.session.user_config["settings"]["max_retries"]
120
-
129
+
130
+ # Lazy import Agent and Tool
131
+ Agent, Tool = get_agent_tool()
132
+
121
133
  # Load system prompt
122
134
  import os
123
135
  from pathlib import Path
124
- prompt_path = Path(__file__).parent.parent.parent / "prompts" / "system.txt"
136
+
137
+ prompt_path = Path(__file__).parent.parent.parent / "prompts" / "system.md"
125
138
  try:
126
139
  with open(prompt_path, "r", encoding="utf-8") as f:
127
140
  system_prompt = f.read().strip()
128
141
  except FileNotFoundError:
129
142
  system_prompt = None
130
-
143
+
131
144
  state_manager.session.agents[model] = Agent(
132
145
  model=model,
133
146
  system_prompt=system_prompt,
@@ -186,6 +199,8 @@ def patch_tool_messages(
186
199
  # Identify orphaned tools (those without responses and not being retried)
187
200
  for tool_call_id, tool_name in list(tool_calls.items()):
188
201
  if tool_call_id not in tool_returns and tool_call_id not in retry_prompts:
202
+ # Import ModelRequest and ToolReturnPart lazily
203
+ ModelRequest, ToolReturnPart = get_model_messages()
189
204
  messages.append(
190
205
  ModelRequest(
191
206
  parts=[
@@ -202,39 +217,41 @@ def patch_tool_messages(
202
217
  )
203
218
 
204
219
 
205
- async def parse_json_tool_calls(text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager):
220
+ async def parse_json_tool_calls(
221
+ text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager
222
+ ):
206
223
  """
207
224
  Parse JSON tool calls from text when structured tool calling fails.
208
225
  Fallback for when API providers don't support proper tool calling.
209
226
  """
210
227
  if not tool_callback:
211
228
  return
212
-
229
+
213
230
  # Pattern for JSON tool calls: {"tool": "tool_name", "args": {...}}
214
231
  # Find potential JSON objects and parse them
215
232
  potential_jsons = []
216
233
  brace_count = 0
217
234
  start_pos = -1
218
-
235
+
219
236
  for i, char in enumerate(text):
220
- if char == '{':
237
+ if char == "{":
221
238
  if brace_count == 0:
222
239
  start_pos = i
223
240
  brace_count += 1
224
- elif char == '}':
241
+ elif char == "}":
225
242
  brace_count -= 1
226
243
  if brace_count == 0 and start_pos != -1:
227
- potential_json = text[start_pos:i+1]
244
+ potential_json = text[start_pos : i + 1]
228
245
  try:
229
246
  parsed = json.loads(potential_json)
230
- if isinstance(parsed, dict) and 'tool' in parsed and 'args' in parsed:
231
- potential_jsons.append((parsed['tool'], parsed['args']))
247
+ if isinstance(parsed, dict) and "tool" in parsed and "args" in parsed:
248
+ potential_jsons.append((parsed["tool"], parsed["args"]))
232
249
  except json.JSONDecodeError:
233
250
  pass
234
251
  start_pos = -1
235
-
252
+
236
253
  matches = potential_jsons
237
-
254
+
238
255
  for tool_name, args in matches:
239
256
  try:
240
257
  # Create a mock tool call object
@@ -243,66 +260,73 @@ async def parse_json_tool_calls(text: str, tool_callback: Optional[ToolCallback]
243
260
  self.tool_name = tool_name
244
261
  self.args = args
245
262
  self.tool_call_id = f"fallback_{datetime.now().timestamp()}"
246
-
263
+
247
264
  class MockNode:
248
265
  pass
249
-
266
+
250
267
  # Execute the tool through the callback
251
268
  mock_call = MockToolCall(tool_name, args)
252
269
  mock_node = MockNode()
253
-
270
+
254
271
  await tool_callback(mock_call, mock_node)
255
-
272
+
256
273
  if state_manager.session.show_thoughts:
257
274
  from tunacode.ui import console as ui
275
+
258
276
  await ui.muted(f"🔧 FALLBACK: Executed {tool_name} via JSON parsing")
259
-
277
+
260
278
  except Exception as e:
261
279
  if state_manager.session.show_thoughts:
262
280
  from tunacode.ui import console as ui
281
+
263
282
  await ui.error(f"❌ Error executing fallback tool {tool_name}: {str(e)}")
264
283
 
265
284
 
266
- async def extract_and_execute_tool_calls(text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager):
285
+ async def extract_and_execute_tool_calls(
286
+ text: str, tool_callback: Optional[ToolCallback], state_manager: StateManager
287
+ ):
267
288
  """
268
289
  Extract tool calls from text content and execute them.
269
290
  Supports multiple formats for maximum compatibility.
270
291
  """
271
292
  if not tool_callback:
272
293
  return
273
-
294
+
274
295
  # Format 1: {"tool": "name", "args": {...}}
275
296
  await parse_json_tool_calls(text, tool_callback, state_manager)
276
-
297
+
277
298
  # Format 2: Tool calls in code blocks
278
299
  code_block_pattern = r'```json\s*(\{(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*"tool"(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*\})\s*```'
279
300
  code_matches = re.findall(code_block_pattern, text, re.MULTILINE | re.DOTALL)
280
-
301
+
281
302
  for match in code_matches:
282
303
  try:
283
304
  tool_data = json.loads(match)
284
- if 'tool' in tool_data and 'args' in tool_data:
305
+ if "tool" in tool_data and "args" in tool_data:
306
+
285
307
  class MockToolCall:
286
308
  def __init__(self, tool_name: str, args: dict):
287
309
  self.tool_name = tool_name
288
310
  self.args = args
289
311
  self.tool_call_id = f"codeblock_{datetime.now().timestamp()}"
290
-
312
+
291
313
  class MockNode:
292
314
  pass
293
-
294
- mock_call = MockToolCall(tool_data['tool'], tool_data['args'])
315
+
316
+ mock_call = MockToolCall(tool_data["tool"], tool_data["args"])
295
317
  mock_node = MockNode()
296
-
318
+
297
319
  await tool_callback(mock_call, mock_node)
298
-
320
+
299
321
  if state_manager.session.show_thoughts:
300
322
  from tunacode.ui import console as ui
323
+
301
324
  await ui.muted(f"🔧 FALLBACK: Executed {tool_data['tool']} from code block")
302
-
325
+
303
326
  except (json.JSONDecodeError, KeyError, Exception) as e:
304
327
  if state_manager.session.show_thoughts:
305
328
  from tunacode.ui import console as ui
329
+
306
330
  await ui.error(f"❌ Error parsing code block tool call: {str(e)}")
307
331
 
308
332
 
@@ -316,22 +340,24 @@ async def process_request(
316
340
  mh = state_manager.session.messages.copy()
317
341
  # Get max iterations from config (default: 20)
318
342
  max_iterations = state_manager.session.user_config.get("settings", {}).get("max_iterations", 20)
319
-
343
+
320
344
  async with agent.iter(message, message_history=mh) as agent_run:
321
345
  i = 0
322
346
  async for node in agent_run:
323
347
  await _process_node(node, tool_callback, state_manager)
324
348
  i += 1
325
-
349
+
326
350
  # Display iteration progress if thoughts are enabled
327
351
  if state_manager.session.show_thoughts and i > 1:
328
352
  from tunacode.ui import console as ui
353
+
329
354
  await ui.muted(f"🔄 Iteration {i}/{max_iterations}")
330
-
355
+
331
356
  if i >= max_iterations:
332
357
  if state_manager.session.show_thoughts:
333
358
  from tunacode.ui import console as ui
359
+
334
360
  await ui.warning(f"⚠️ Reached maximum iterations ({max_iterations})")
335
361
  break
336
-
362
+
337
363
  return agent_run
@@ -0,0 +1,99 @@
1
+ """Agent orchestration scaffolding.
2
+
3
+ This module defines an ``OrchestratorAgent`` class that demonstrates how
4
+ higher level planning and delegation could be layered on top of the existing
5
+ ``process_request`` workflow. The goal is to keep orchestration logic isolated
6
+ from the core agent implementation while reusing all current tooling and state
7
+ handling provided by ``main.process_request``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import itertools
14
+ from typing import List
15
+
16
+ from ...types import AgentRun, ModelName
17
+ from ..llm.planner import make_plan
18
+ from ..state import StateManager
19
+ from . import main as agent_main
20
+ from .planner_schema import Task
21
+ from .readonly import ReadOnlyAgent
22
+
23
+
24
+ class OrchestratorAgent:
25
+ """Plan and run a sequence of sub-agent tasks."""
26
+
27
+ def __init__(self, state_manager: StateManager):
28
+ self.state = state_manager
29
+
30
+ async def plan(self, request: str, model: ModelName) -> List[Task]:
31
+ """Plan tasks for a user request using the planner LLM."""
32
+
33
+ return await make_plan(request, model, self.state)
34
+
35
+ async def _run_sub_task(self, task: Task, model: ModelName) -> AgentRun:
36
+ """Execute a single task using an appropriate sub-agent."""
37
+ from rich.console import Console
38
+
39
+ console = Console()
40
+
41
+ # Show which task is being executed
42
+ task_type = "WRITE" if task.mutate else "READ"
43
+ console.print(f"\n[dim][Task {task.id}] {task_type}[/dim]")
44
+ console.print(f"[dim] → {task.description}[/dim]")
45
+
46
+ if task.mutate:
47
+ agent_main.get_or_create_agent(model, self.state)
48
+ result = await agent_main.process_request(model, task.description, self.state)
49
+ else:
50
+ agent = ReadOnlyAgent(model, self.state)
51
+ result = await agent.process_request(task.description)
52
+
53
+ console.print(f"[dim][Task {task.id}] Complete[/dim]")
54
+ return result
55
+
56
+ async def run(self, request: str, model: ModelName | None = None) -> List[AgentRun]:
57
+ """Plan and execute a user request.
58
+
59
+ Parameters
60
+ ----------
61
+ request:
62
+ The high level user request to process.
63
+ model:
64
+ Optional model name to use for sub agents. Defaults to the current
65
+ session model.
66
+ """
67
+ from rich.console import Console
68
+
69
+ console = Console()
70
+ model = model or self.state.session.current_model
71
+
72
+ # Show orchestrator is starting
73
+ console.print(
74
+ "\n[cyan]Orchestrator Mode: Analyzing request and creating execution plan...[/cyan]"
75
+ )
76
+
77
+ tasks = await self.plan(request, model)
78
+
79
+ # Show execution is starting
80
+ console.print(f"\n[cyan]Executing plan with {len(tasks)} tasks...[/cyan]")
81
+
82
+ results: List[AgentRun] = []
83
+ for mutate_flag, group in itertools.groupby(tasks, key=lambda t: t.mutate):
84
+ if mutate_flag:
85
+ for t in group:
86
+ results.append(await self._run_sub_task(t, model))
87
+ else:
88
+ # Show parallel execution
89
+ task_list = list(group)
90
+ if len(task_list) > 1:
91
+ console.print(
92
+ f"\n[dim][Parallel Execution] Running {len(task_list)} read-only tasks concurrently...[/dim]"
93
+ )
94
+ coros = [self._run_sub_task(t, model) for t in task_list]
95
+ results.extend(await asyncio.gather(*coros))
96
+
97
+ console.print("\n[green]Orchestrator completed all tasks successfully![/green]")
98
+
99
+ return results
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class Task(BaseModel):
5
+ """Single sub-task generated by the planner."""
6
+
7
+ id: int = Field(..., description="1-based task index in execution order")
8
+ description: str = Field(..., description="What the sub-agent must do")
9
+ mutate: bool = Field(..., description="True if the task changes code")
@@ -0,0 +1,51 @@
1
+ """Read-only agent implementation for non-mutating operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ...tools.grep import grep
8
+ from ...tools.read_file import read_file
9
+ from ...types import AgentRun, ModelName
10
+ from ..state import StateManager
11
+
12
+ if TYPE_CHECKING:
13
+ from ...types import PydanticAgent
14
+
15
+
16
+ class ReadOnlyAgent:
17
+ """Agent configured with read-only tools for analysis tasks."""
18
+
19
+ def __init__(self, model: ModelName, state_manager: StateManager):
20
+ self.model = model
21
+ self.state_manager = state_manager
22
+ self._agent: PydanticAgent | None = None
23
+
24
+ def _get_agent(self) -> PydanticAgent:
25
+ """Lazily create the agent with read-only tools."""
26
+ if self._agent is None:
27
+ from .main import get_agent_tool
28
+
29
+ Agent, Tool = get_agent_tool()
30
+
31
+ # Create agent with only read-only tools
32
+ self._agent = Agent(
33
+ model=self.model,
34
+ system_prompt="You are a read-only assistant. You can analyze and read files but cannot modify them.",
35
+ tools=[
36
+ Tool(read_file),
37
+ Tool(grep),
38
+ ],
39
+ )
40
+ return self._agent
41
+
42
+ async def process_request(self, request: str) -> AgentRun:
43
+ """Process a request using only read-only tools."""
44
+ agent = self._get_agent()
45
+
46
+ # Use iter() like main.py does to get the full run context
47
+ async with agent.iter(request) as agent_run:
48
+ async for _ in agent_run:
49
+ pass # Let it complete
50
+
51
+ return agent_run
File without changes
@@ -0,0 +1,36 @@
1
+ """Asynchronous background task management utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import uuid
7
+ from collections import defaultdict
8
+ from typing import Awaitable, Callable, Dict, List
9
+
10
+
11
+ class BackgroundTaskManager:
12
+ """Simple manager for background asyncio tasks."""
13
+
14
+ def __init__(self) -> None:
15
+ self.tasks: Dict[str, asyncio.Task] = {}
16
+ self.listeners: Dict[str, List[Callable[[asyncio.Task], None]]] = defaultdict(list)
17
+
18
+ def spawn(self, coro: Awaitable, *, name: str | None = None) -> str:
19
+ task_id = name or uuid.uuid4().hex[:8]
20
+ task = asyncio.create_task(coro, name=task_id)
21
+ self.tasks[task_id] = task
22
+ task.add_done_callback(self._notify)
23
+ return task_id
24
+
25
+ def _notify(self, task: asyncio.Task) -> None:
26
+ for cb in self.listeners.get(task.get_name(), []):
27
+ cb(task)
28
+
29
+ async def shutdown(self) -> None:
30
+ for task in self.tasks.values():
31
+ task.cancel()
32
+ if self.tasks:
33
+ await asyncio.gather(*self.tasks.values(), return_exceptions=True)
34
+
35
+
36
+ BG_MANAGER = BackgroundTaskManager()
File without changes
@@ -0,0 +1,63 @@
1
+ import json
2
+ from typing import List
3
+
4
+ from ...types import ModelName
5
+ from ..agents.planner_schema import Task
6
+ from ..state import StateManager
7
+
8
+ _SYSTEM = """You are a senior software project planner.
9
+
10
+ Your job is to break down a USER_REQUEST into a logical sequence of tasks.
11
+
12
+ Guidelines:
13
+ 1. Use the FEWEST tasks possible - combine related operations
14
+ 2. Order tasks logically - reads before writes, understand before modify
15
+ 3. Mark read-only tasks (reading files, searching, analyzing) as mutate: false
16
+ 4. Mark modifying tasks (writing, updating, creating files) as mutate: true
17
+ 5. Write clear, actionable descriptions
18
+
19
+ Each task MUST have:
20
+ - id: Sequential number starting from 1
21
+ - description: Clear description of what needs to be done
22
+ - mutate: true if the task modifies files/code, false if it only reads/analyzes
23
+
24
+ Return ONLY a valid JSON array of tasks, no other text.
25
+ Example:
26
+ [
27
+ {"id": 1, "description": "Read the main.py file to understand the structure", "mutate": false},
28
+ {"id": 2, "description": "Update the function to handle edge cases", "mutate": true}
29
+ ]"""
30
+
31
+
32
+ async def make_plan(request: str, model: ModelName, state_manager: StateManager) -> List[Task]:
33
+ """Generate an execution plan from a user request using TunaCode's LLM infrastructure."""
34
+ # Lazy import to avoid circular dependencies
35
+ from rich.console import Console
36
+
37
+ from ..agents.main import get_agent_tool
38
+
39
+ console = Console()
40
+ Agent, _ = get_agent_tool()
41
+
42
+ # Show planning is starting
43
+ console.print("\n[dim][Planning] Breaking down request into tasks...[/dim]")
44
+
45
+ # Create a simple planning agent
46
+ planner = Agent(
47
+ model=model,
48
+ system_prompt=_SYSTEM,
49
+ result_type=List[Task],
50
+ )
51
+
52
+ # Get the plan from the agent
53
+ result = await planner.run(request)
54
+ tasks = result.data
55
+
56
+ # Display the plan
57
+ console.print(f"[dim][Planning] Generated {len(tasks)} tasks:[/dim]")
58
+ for task in tasks:
59
+ task_type = "WRITE" if task.mutate else "READ"
60
+ console.print(f"[dim] Task {task.id}: {task_type} - {task.description}[/dim]")
61
+ console.print("")
62
+
63
+ return tasks