tunacode-cli 0.0.70__py3-none-any.whl → 0.0.78.6__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/__init__.py +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/core/agents/main.py
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"""Module: tunacode.core.agents.main
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Refactored main agent functionality with focused responsibility classes.
|
|
4
4
|
Handles agent creation, configuration, and request processing.
|
|
5
5
|
|
|
6
6
|
CLAUDE_ANCHOR[main-agent-module]: Primary agent orchestration and lifecycle management
|
|
7
|
+
|
|
8
|
+
This is a refactored version of old main.py that:
|
|
9
|
+
- Eliminates StateFacade in favor of direct SessionState access
|
|
10
|
+
- Extracts intervention logic into focused classes
|
|
11
|
+
- Centralizes prompts in prompts.py module
|
|
7
12
|
"""
|
|
8
13
|
|
|
9
|
-
from
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import uuid
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional
|
|
10
20
|
|
|
11
21
|
from pydantic_ai import Agent
|
|
12
22
|
|
|
@@ -16,445 +26,587 @@ if TYPE_CHECKING:
|
|
|
16
26
|
from tunacode.core.logging.logger import get_logger
|
|
17
27
|
from tunacode.core.state import StateManager
|
|
18
28
|
from tunacode.exceptions import ToolBatchingJSONError, UserAbortError
|
|
19
|
-
from tunacode.services.mcp import
|
|
29
|
+
from tunacode.services.mcp import ( # re-exported by design
|
|
30
|
+
cleanup_mcp_servers,
|
|
31
|
+
get_mcp_servers,
|
|
32
|
+
register_mcp_agent,
|
|
33
|
+
)
|
|
34
|
+
from tunacode.tools.react import ReactTool
|
|
20
35
|
from tunacode.types import (
|
|
21
36
|
AgentRun,
|
|
22
37
|
ModelName,
|
|
23
38
|
ToolCallback,
|
|
24
39
|
UsageTrackerProtocol,
|
|
25
40
|
)
|
|
41
|
+
from tunacode.ui import console as ui
|
|
26
42
|
from tunacode.ui.tool_descriptions import get_batch_description
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
from .
|
|
30
|
-
AgentRunWithState,
|
|
31
|
-
AgentRunWrapper,
|
|
32
|
-
ResponseState,
|
|
33
|
-
SimpleResult,
|
|
34
|
-
ToolBuffer,
|
|
35
|
-
_process_node,
|
|
36
|
-
check_task_completion,
|
|
37
|
-
create_empty_response_message,
|
|
38
|
-
create_fallback_response,
|
|
39
|
-
create_progress_summary,
|
|
40
|
-
create_user_message,
|
|
41
|
-
execute_tools_parallel,
|
|
42
|
-
extract_and_execute_tool_calls,
|
|
43
|
-
format_fallback_output,
|
|
44
|
-
get_model_messages,
|
|
45
|
-
get_or_create_agent,
|
|
46
|
-
get_recent_tools_context,
|
|
47
|
-
get_tool_summary,
|
|
48
|
-
parse_json_tool_calls,
|
|
49
|
-
patch_tool_messages,
|
|
50
|
-
)
|
|
44
|
+
from . import agent_components as ac
|
|
45
|
+
from .prompts import format_clarification, format_iteration_limit, format_no_progress
|
|
51
46
|
|
|
52
|
-
# Import streaming types with fallback for older versions
|
|
53
|
-
try:
|
|
54
|
-
from pydantic_ai.messages import PartDeltaEvent, TextPartDelta
|
|
55
|
-
|
|
56
|
-
STREAMING_AVAILABLE = True
|
|
57
|
-
except ImportError:
|
|
58
|
-
PartDeltaEvent = None
|
|
59
|
-
TextPartDelta = None
|
|
60
|
-
STREAMING_AVAILABLE = False
|
|
61
|
-
|
|
62
|
-
# Configure logging
|
|
63
47
|
logger = get_logger(__name__)
|
|
64
48
|
|
|
65
49
|
__all__ = [
|
|
66
|
-
"ToolBuffer",
|
|
67
|
-
"check_task_completion",
|
|
68
|
-
"extract_and_execute_tool_calls",
|
|
69
|
-
"get_model_messages",
|
|
70
|
-
"parse_json_tool_calls",
|
|
71
|
-
"patch_tool_messages",
|
|
72
|
-
"get_mcp_servers",
|
|
73
|
-
"check_query_satisfaction",
|
|
74
50
|
"process_request",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"SimpleResult",
|
|
79
|
-
"AgentRunWrapper",
|
|
80
|
-
"AgentRunWithState",
|
|
81
|
-
"execute_tools_parallel",
|
|
51
|
+
"get_mcp_servers",
|
|
52
|
+
"cleanup_mcp_servers",
|
|
53
|
+
"register_mcp_agent",
|
|
82
54
|
"get_agent_tool",
|
|
55
|
+
"check_query_satisfaction",
|
|
83
56
|
]
|
|
84
57
|
|
|
85
58
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return Agent, Tool
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
async def check_query_satisfaction(
|
|
94
|
-
agent: Agent,
|
|
95
|
-
original_query: str,
|
|
96
|
-
response: str,
|
|
97
|
-
state_manager: StateManager,
|
|
98
|
-
) -> bool:
|
|
99
|
-
"""Check if the response satisfies the original query."""
|
|
100
|
-
return True # Agent uses TUNACODE_TASK_COMPLETE marker
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
async def process_request(
|
|
104
|
-
message: str,
|
|
105
|
-
model: ModelName,
|
|
106
|
-
state_manager: StateManager,
|
|
107
|
-
tool_callback: Optional[ToolCallback] = None,
|
|
108
|
-
streaming_callback: Optional[Callable[[str], Awaitable[None]]] = None,
|
|
109
|
-
usage_tracker: Optional[UsageTrackerProtocol] = None,
|
|
110
|
-
fallback_enabled: bool = True,
|
|
111
|
-
) -> AgentRun:
|
|
112
|
-
"""
|
|
113
|
-
Process a single request to the agent.
|
|
114
|
-
|
|
115
|
-
CLAUDE_ANCHOR[process-request-entry]: Main entry point for all agent requests
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
message: The user's request
|
|
119
|
-
model: The model to use
|
|
120
|
-
state_manager: State manager instance
|
|
121
|
-
tool_callback: Optional callback for tool execution
|
|
122
|
-
streaming_callback: Optional callback for streaming responses
|
|
123
|
-
usage_tracker: Optional usage tracker
|
|
124
|
-
fallback_enabled: Whether to enable fallback responses
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
AgentRun or wrapper with result
|
|
128
|
-
"""
|
|
129
|
-
# Get or create agent for the model
|
|
130
|
-
agent = get_or_create_agent(model, state_manager)
|
|
131
|
-
|
|
132
|
-
# Create a unique request ID for debugging
|
|
133
|
-
import uuid
|
|
134
|
-
|
|
135
|
-
request_id = str(uuid.uuid4())[:8]
|
|
136
|
-
|
|
137
|
-
# Reset state for new request
|
|
138
|
-
state_manager.session.current_iteration = 0
|
|
139
|
-
state_manager.session.iteration_count = 0
|
|
140
|
-
state_manager.session.tool_calls = []
|
|
59
|
+
@dataclass
|
|
60
|
+
class AgentConfig:
|
|
61
|
+
"""Configuration for agent behavior."""
|
|
141
62
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
63
|
+
max_iterations: int = 15
|
|
64
|
+
unproductive_limit: int = 3
|
|
65
|
+
forced_react_interval: int = 2
|
|
66
|
+
forced_react_limit: int = 5
|
|
67
|
+
debug_metrics: bool = False
|
|
145
68
|
|
|
146
|
-
# Create tool buffer for parallel execution
|
|
147
|
-
tool_buffer = ToolBuffer()
|
|
148
69
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
last_productive_iteration = 0
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class RequestContext:
|
|
72
|
+
"""Context for a single request."""
|
|
153
73
|
|
|
154
|
-
|
|
155
|
-
|
|
74
|
+
request_id: str
|
|
75
|
+
max_iterations: int
|
|
76
|
+
debug_metrics: bool
|
|
156
77
|
|
|
157
|
-
try:
|
|
158
|
-
# Get message history from session messages
|
|
159
|
-
# Create a copy of the message history to avoid modifying the original
|
|
160
|
-
message_history = list(state_manager.session.messages)
|
|
161
|
-
|
|
162
|
-
async with agent.iter(message, message_history=message_history) as agent_run:
|
|
163
|
-
# Process nodes iteratively
|
|
164
|
-
i = 1
|
|
165
|
-
async for node in agent_run:
|
|
166
|
-
state_manager.session.current_iteration = i
|
|
167
|
-
state_manager.session.iteration_count = i
|
|
168
|
-
|
|
169
|
-
# Handle token-level streaming for model request nodes
|
|
170
|
-
Agent, _ = get_agent_tool()
|
|
171
|
-
if streaming_callback and STREAMING_AVAILABLE and Agent.is_model_request_node(node):
|
|
172
|
-
async with node.stream(agent_run.ctx) as request_stream:
|
|
173
|
-
async for event in request_stream:
|
|
174
|
-
if isinstance(event, PartDeltaEvent) and isinstance(
|
|
175
|
-
event.delta, TextPartDelta
|
|
176
|
-
):
|
|
177
|
-
# Stream individual token deltas
|
|
178
|
-
if event.delta.content_delta and streaming_callback:
|
|
179
|
-
await streaming_callback(event.delta.content_delta)
|
|
180
|
-
|
|
181
|
-
empty_response, empty_reason = await _process_node(
|
|
182
|
-
node,
|
|
183
|
-
tool_callback,
|
|
184
|
-
state_manager,
|
|
185
|
-
tool_buffer,
|
|
186
|
-
streaming_callback,
|
|
187
|
-
usage_tracker,
|
|
188
|
-
response_state,
|
|
189
|
-
)
|
|
190
78
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if not hasattr(state_manager.session, "consecutive_empty_responses"):
|
|
194
|
-
state_manager.session.consecutive_empty_responses = 0
|
|
195
|
-
state_manager.session.consecutive_empty_responses += 1
|
|
196
|
-
|
|
197
|
-
if state_manager.session.consecutive_empty_responses >= 1:
|
|
198
|
-
force_action_content = create_empty_response_message(
|
|
199
|
-
message,
|
|
200
|
-
empty_reason,
|
|
201
|
-
state_manager.session.tool_calls,
|
|
202
|
-
i,
|
|
203
|
-
state_manager,
|
|
204
|
-
)
|
|
205
|
-
create_user_message(force_action_content, state_manager)
|
|
79
|
+
class EmptyResponseHandler:
|
|
80
|
+
"""Handles tracking and intervention for empty responses."""
|
|
206
81
|
|
|
207
|
-
|
|
208
|
-
|
|
82
|
+
def __init__(self, state_manager: StateManager) -> None:
|
|
83
|
+
self.state_manager = state_manager
|
|
209
84
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await ui.muted(" Injecting retry guidance prompt")
|
|
85
|
+
def track(self, is_empty: bool) -> None:
|
|
86
|
+
"""Track empty response and increment counter if empty."""
|
|
87
|
+
if is_empty:
|
|
88
|
+
current = getattr(self.state_manager.session, "consecutive_empty_responses", 0)
|
|
89
|
+
setattr(self.state_manager.session, "consecutive_empty_responses", current + 1)
|
|
90
|
+
else:
|
|
91
|
+
setattr(self.state_manager.session, "consecutive_empty_responses", 0)
|
|
218
92
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
state_manager.session.consecutive_empty_responses = 0
|
|
93
|
+
def should_intervene(self) -> bool:
|
|
94
|
+
"""Check if intervention is needed (>= 1 consecutive empty)."""
|
|
95
|
+
return getattr(self.state_manager.session, "consecutive_empty_responses", 0) >= 1
|
|
223
96
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
response_state.has_user_response = True
|
|
97
|
+
async def prompt_action(self, message: str, reason: str, iteration: int) -> None:
|
|
98
|
+
"""Delegate to agent_components.handle_empty_response."""
|
|
227
99
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
iteration_had_tools = True
|
|
234
|
-
break
|
|
235
|
-
|
|
236
|
-
if iteration_had_tools:
|
|
237
|
-
# Reset unproductive counter
|
|
238
|
-
unproductive_iterations = 0
|
|
239
|
-
last_productive_iteration = i
|
|
240
|
-
else:
|
|
241
|
-
# Increment unproductive counter
|
|
242
|
-
unproductive_iterations += 1
|
|
243
|
-
|
|
244
|
-
# After 3 unproductive iterations, force action
|
|
245
|
-
if unproductive_iterations >= 3 and not response_state.task_completed:
|
|
246
|
-
no_progress_content = f"""ALERT: No tools executed for {unproductive_iterations} iterations.
|
|
247
|
-
|
|
248
|
-
Last productive iteration: {last_productive_iteration}
|
|
249
|
-
Current iteration: {i}/{max_iterations}
|
|
250
|
-
Task: {message[:200]}...
|
|
100
|
+
# Create a minimal state-like object for compatibility
|
|
101
|
+
class StateProxy:
|
|
102
|
+
def __init__(self, sm: StateManager) -> None:
|
|
103
|
+
self.sm = sm
|
|
104
|
+
self.show_thoughts = bool(getattr(sm.session, "show_thoughts", False))
|
|
251
105
|
|
|
252
|
-
|
|
106
|
+
state_proxy = StateProxy(self.state_manager)
|
|
107
|
+
await ac.handle_empty_response(message, reason, iteration, state_proxy)
|
|
108
|
+
# Clear after intervention
|
|
109
|
+
setattr(self.state_manager.session, "consecutive_empty_responses", 0)
|
|
253
110
|
|
|
254
|
-
1. If task is COMPLETE: Start response with TUNACODE_TASK_COMPLETE
|
|
255
|
-
2. If task needs work: Execute a tool RIGHT NOW (grep, read_file, bash, etc.)
|
|
256
|
-
3. If stuck: Explain the specific blocker
|
|
257
111
|
|
|
258
|
-
|
|
112
|
+
class IterationManager:
|
|
113
|
+
"""Manages iteration tracking, productivity monitoring, and limit handling."""
|
|
259
114
|
|
|
260
|
-
|
|
115
|
+
def __init__(self, state_manager: StateManager, config: AgentConfig) -> None:
|
|
116
|
+
self.state_manager = state_manager
|
|
117
|
+
self.config = config
|
|
118
|
+
self.unproductive_iterations = 0
|
|
119
|
+
self.last_productive_iteration = 0
|
|
261
120
|
|
|
262
|
-
|
|
263
|
-
|
|
121
|
+
def track_productivity(self, had_tool_use: bool, iteration: int) -> None:
|
|
122
|
+
"""Track productivity based on tool usage."""
|
|
123
|
+
if had_tool_use:
|
|
124
|
+
self.unproductive_iterations = 0
|
|
125
|
+
self.last_productive_iteration = iteration
|
|
126
|
+
else:
|
|
127
|
+
self.unproductive_iterations += 1
|
|
264
128
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
129
|
+
def should_force_action(self, response_state: ac.ResponseState) -> bool:
|
|
130
|
+
"""Check if action should be forced due to unproductivity."""
|
|
131
|
+
return (
|
|
132
|
+
self.unproductive_iterations >= self.config.unproductive_limit
|
|
133
|
+
and not response_state.task_completed
|
|
134
|
+
)
|
|
268
135
|
|
|
269
|
-
|
|
136
|
+
async def handle_iteration_limit(
|
|
137
|
+
self, iteration: int, response_state: ac.ResponseState
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Handle reaching iteration limit."""
|
|
140
|
+
if iteration >= self.config.max_iterations and not response_state.task_completed:
|
|
141
|
+
_, tools_str = ac.create_progress_summary(
|
|
142
|
+
getattr(self.state_manager.session, "tool_calls", [])
|
|
143
|
+
)
|
|
144
|
+
limit_message = format_iteration_limit(self.config.max_iterations, iteration, tools_str)
|
|
145
|
+
ac.create_user_message(limit_message, self.state_manager)
|
|
146
|
+
|
|
147
|
+
if getattr(self.state_manager.session, "show_thoughts", False):
|
|
148
|
+
await ui.muted(
|
|
149
|
+
f"\nITERATION LIMIT: Awaiting user guidance at "
|
|
150
|
+
f"{self.config.max_iterations} iterations"
|
|
151
|
+
)
|
|
270
152
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
153
|
+
response_state.awaiting_user_guidance = True
|
|
154
|
+
|
|
155
|
+
def update_counters(self, iteration: int) -> None:
|
|
156
|
+
"""Update session iteration counters."""
|
|
157
|
+
self.state_manager.session.current_iteration = iteration
|
|
158
|
+
self.state_manager.session.iteration_count = iteration
|
|
159
|
+
|
|
160
|
+
async def force_action_if_unproductive(
|
|
161
|
+
self, message: str, iteration: int, response_state: ac.ResponseState
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Force action if unproductive iterations exceeded."""
|
|
164
|
+
if not self.should_force_action(response_state):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
no_progress_message = format_no_progress(
|
|
168
|
+
message,
|
|
169
|
+
self.unproductive_iterations,
|
|
170
|
+
self.last_productive_iteration,
|
|
171
|
+
iteration,
|
|
172
|
+
self.config.max_iterations,
|
|
173
|
+
)
|
|
174
|
+
ac.create_user_message(no_progress_message, self.state_manager)
|
|
274
175
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
176
|
+
if getattr(self.state_manager.session, "show_thoughts", False):
|
|
177
|
+
await ui.warning(
|
|
178
|
+
f"NO PROGRESS: {self.unproductive_iterations} iterations without tool usage"
|
|
179
|
+
)
|
|
278
180
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
from tunacode.ui import console as ui
|
|
181
|
+
# Reset after nudge
|
|
182
|
+
self.unproductive_iterations = 0
|
|
282
183
|
|
|
283
|
-
|
|
184
|
+
async def ask_for_clarification(self, iteration: int) -> None:
|
|
185
|
+
"""Ask user for clarification."""
|
|
186
|
+
_, tools_used_str = ac.create_progress_summary(
|
|
187
|
+
getattr(self.state_manager.session, "tool_calls", [])
|
|
188
|
+
)
|
|
189
|
+
original_query = getattr(self.state_manager.session, "original_query", "your request")
|
|
190
|
+
clarification_message = format_clarification(original_query, iteration, tools_used_str)
|
|
191
|
+
ac.create_user_message(clarification_message, self.state_manager)
|
|
192
|
+
|
|
193
|
+
if getattr(self.state_manager.session, "show_thoughts", False):
|
|
194
|
+
await ui.muted("\nSEEKING CLARIFICATION: Asking user for guidance on task progress")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ReactSnapshotManager:
|
|
198
|
+
"""Manages forced react snapshots and guidance injection."""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self, state_manager: StateManager, react_tool: ReactTool, config: AgentConfig
|
|
202
|
+
) -> None:
|
|
203
|
+
self.state_manager = state_manager
|
|
204
|
+
self.react_tool = react_tool
|
|
205
|
+
self.config = config
|
|
206
|
+
|
|
207
|
+
def should_snapshot(self, iteration: int) -> bool:
|
|
208
|
+
"""Check if snapshot should be taken."""
|
|
209
|
+
if iteration < self.config.forced_react_interval:
|
|
210
|
+
return False
|
|
211
|
+
if iteration % self.config.forced_react_interval != 0:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
forced_calls = getattr(self.state_manager.session, "react_forced_calls", 0)
|
|
215
|
+
return forced_calls < self.config.forced_react_limit
|
|
216
|
+
|
|
217
|
+
async def capture_snapshot(self, iteration: int, agent_run_ctx: Any, show_debug: bool) -> None:
|
|
218
|
+
"""Capture react snapshot and inject guidance."""
|
|
219
|
+
if not self.should_snapshot(iteration):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
await self.react_tool.execute(
|
|
224
|
+
action="think",
|
|
225
|
+
thoughts=f"Auto snapshot after iteration {iteration}",
|
|
226
|
+
next_action="continue",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Increment forced calls counter
|
|
230
|
+
forced_calls = getattr(self.state_manager.session, "react_forced_calls", 0)
|
|
231
|
+
self.state_manager.session.react_forced_calls = forced_calls + 1
|
|
232
|
+
|
|
233
|
+
# Build guidance from last tool call
|
|
234
|
+
timeline = self.state_manager.session.react_scratchpad.get("timeline", [])
|
|
235
|
+
latest = timeline[-1] if timeline else {"thoughts": "?", "next_action": "?"}
|
|
236
|
+
summary = latest.get("thoughts", "")
|
|
237
|
+
|
|
238
|
+
tool_calls = getattr(self.state_manager.session, "tool_calls", [])
|
|
239
|
+
if tool_calls:
|
|
240
|
+
last_tool = tool_calls[-1]
|
|
241
|
+
tool_name = last_tool.get("tool", "tool")
|
|
242
|
+
args = last_tool.get("args", {})
|
|
243
|
+
if isinstance(args, str):
|
|
244
|
+
try:
|
|
245
|
+
args = json.loads(args)
|
|
246
|
+
except (ValueError, TypeError):
|
|
247
|
+
args = {}
|
|
248
|
+
|
|
249
|
+
detail = ""
|
|
250
|
+
if tool_name == "grep" and isinstance(args, dict):
|
|
251
|
+
pattern = args.get("pattern")
|
|
252
|
+
detail = (
|
|
253
|
+
f"Review grep results for pattern '{pattern}'"
|
|
254
|
+
if pattern
|
|
255
|
+
else "Review grep results"
|
|
256
|
+
)
|
|
257
|
+
elif tool_name == "read_file" and isinstance(args, dict):
|
|
258
|
+
path = args.get("file_path") or args.get("filepath")
|
|
259
|
+
detail = (
|
|
260
|
+
f"Extract key notes from {path}" if path else "Summarize read_file output"
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
detail = f"Act on {tool_name} findings"
|
|
264
|
+
else:
|
|
265
|
+
detail = "Plan your first lookup"
|
|
266
|
+
|
|
267
|
+
guidance_entry = (
|
|
268
|
+
f"React snapshot {forced_calls + 1}/{self.config.forced_react_limit} "
|
|
269
|
+
f"at iteration {iteration}: {summary}. Next: {detail}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Append and trim guidance
|
|
273
|
+
self.state_manager.session.react_guidance.append(guidance_entry)
|
|
274
|
+
if len(self.state_manager.session.react_guidance) > self.config.forced_react_limit:
|
|
275
|
+
self.state_manager.session.react_guidance = (
|
|
276
|
+
self.state_manager.session.react_guidance[-self.config.forced_react_limit :]
|
|
277
|
+
)
|
|
284
278
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
279
|
+
# CRITICAL: Inject into agent_run.ctx.messages so next LLM call sees guidance
|
|
280
|
+
if agent_run_ctx is not None:
|
|
281
|
+
ctx_messages = getattr(agent_run_ctx, "messages", None)
|
|
282
|
+
if isinstance(ctx_messages, list):
|
|
283
|
+
ModelRequest, _, SystemPromptPart = ac.get_model_messages()
|
|
284
|
+
system_part = SystemPromptPart(
|
|
285
|
+
content=f"[React Guidance] {guidance_entry}",
|
|
286
|
+
part_kind="system-prompt",
|
|
287
|
+
)
|
|
288
|
+
# CLAUDE_ANCHOR[react-system-injection]
|
|
289
|
+
# Append synthetic system message so LLM receives react guidance next turn
|
|
290
|
+
ctx_messages.append(ModelRequest(parts=[system_part], kind="request"))
|
|
291
|
+
|
|
292
|
+
if show_debug:
|
|
293
|
+
await ui.muted("\n[react → LLM] BEGIN\n" + guidance_entry + "\n[react → LLM] END\n")
|
|
294
|
+
except Exception:
|
|
295
|
+
logger.debug("Forced react snapshot failed", exc_info=True)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class RequestOrchestrator:
|
|
299
|
+
"""Orchestrates the main request processing loop."""
|
|
300
|
+
|
|
301
|
+
def __init__(
|
|
302
|
+
self,
|
|
303
|
+
message: str,
|
|
304
|
+
model: ModelName,
|
|
305
|
+
state_manager: StateManager,
|
|
306
|
+
tool_callback: Optional[ToolCallback],
|
|
307
|
+
streaming_callback: Optional[Callable[[str], Awaitable[None]]],
|
|
308
|
+
usage_tracker: Optional[UsageTrackerProtocol],
|
|
309
|
+
) -> None:
|
|
310
|
+
self.message = message
|
|
311
|
+
self.model = model
|
|
312
|
+
self.state_manager = state_manager
|
|
313
|
+
self.tool_callback = tool_callback
|
|
314
|
+
self.streaming_callback = streaming_callback
|
|
315
|
+
self.usage_tracker = usage_tracker
|
|
316
|
+
|
|
317
|
+
# Initialize config from session settings
|
|
318
|
+
user_config = getattr(state_manager.session, "user_config", {}) or {}
|
|
319
|
+
settings = user_config.get("settings", {})
|
|
320
|
+
self.config = AgentConfig(
|
|
321
|
+
max_iterations=int(settings.get("max_iterations", 15)),
|
|
322
|
+
unproductive_limit=3,
|
|
323
|
+
forced_react_interval=2,
|
|
324
|
+
forced_react_limit=5,
|
|
325
|
+
debug_metrics=bool(settings.get("debug_metrics", False)),
|
|
326
|
+
)
|
|
292
327
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
328
|
+
# Initialize managers
|
|
329
|
+
self.empty_handler = EmptyResponseHandler(state_manager)
|
|
330
|
+
self.iteration_manager = IterationManager(state_manager, self.config)
|
|
331
|
+
self.react_manager = ReactSnapshotManager(
|
|
332
|
+
state_manager, ReactTool(state_manager=state_manager), self.config
|
|
333
|
+
)
|
|
296
334
|
|
|
297
|
-
|
|
335
|
+
def _init_request_context(self) -> RequestContext:
|
|
336
|
+
"""Initialize request context with ID and config."""
|
|
337
|
+
req_id = str(uuid.uuid4())[:8]
|
|
338
|
+
setattr(self.state_manager.session, "request_id", req_id)
|
|
298
339
|
|
|
299
|
-
|
|
340
|
+
return RequestContext(
|
|
341
|
+
request_id=req_id,
|
|
342
|
+
max_iterations=self.config.max_iterations,
|
|
343
|
+
debug_metrics=self.config.debug_metrics,
|
|
344
|
+
)
|
|
300
345
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
346
|
+
def _reset_session_state(self) -> None:
|
|
347
|
+
"""Reset/initialize fields needed for a new run."""
|
|
348
|
+
self.state_manager.session.current_iteration = 0
|
|
349
|
+
self.state_manager.session.iteration_count = 0
|
|
350
|
+
self.state_manager.session.tool_calls = []
|
|
351
|
+
self.state_manager.session.react_forced_calls = 0
|
|
352
|
+
self.state_manager.session.react_guidance = []
|
|
353
|
+
|
|
354
|
+
# Counter used by other subsystems; initialize if absent
|
|
355
|
+
if not hasattr(self.state_manager.session, "batch_counter"):
|
|
356
|
+
self.state_manager.session.batch_counter = 0
|
|
357
|
+
|
|
358
|
+
# Track empty response streaks
|
|
359
|
+
setattr(self.state_manager.session, "consecutive_empty_responses", 0)
|
|
360
|
+
|
|
361
|
+
setattr(self.state_manager.session, "original_query", "")
|
|
362
|
+
|
|
363
|
+
def _set_original_query_once(self, query: str) -> None:
|
|
364
|
+
"""Set original query if not already set."""
|
|
365
|
+
if not getattr(self.state_manager.session, "original_query", None):
|
|
366
|
+
setattr(self.state_manager.session, "original_query", query)
|
|
367
|
+
|
|
368
|
+
async def run(self) -> AgentRun:
|
|
369
|
+
"""Run the main request processing loop."""
|
|
370
|
+
ctx = self._init_request_context()
|
|
371
|
+
self._reset_session_state()
|
|
372
|
+
self._set_original_query_once(self.message)
|
|
373
|
+
|
|
374
|
+
# Acquire agent
|
|
375
|
+
agent = ac.get_or_create_agent(self.model, self.state_manager)
|
|
376
|
+
|
|
377
|
+
# Prepare history snapshot
|
|
378
|
+
message_history = list(getattr(self.state_manager.session, "messages", []))
|
|
379
|
+
|
|
380
|
+
# Per-request trackers
|
|
381
|
+
tool_buffer = ac.ToolBuffer()
|
|
382
|
+
response_state = ac.ResponseState()
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
async with agent.iter(self.message, message_history=message_history) as agent_run:
|
|
386
|
+
i = 1
|
|
387
|
+
async for node in agent_run:
|
|
388
|
+
self.iteration_manager.update_counters(i)
|
|
389
|
+
|
|
390
|
+
# Optional token streaming
|
|
391
|
+
await _maybe_stream_node_tokens(
|
|
392
|
+
node,
|
|
393
|
+
agent_run.ctx,
|
|
394
|
+
self.state_manager,
|
|
395
|
+
self.streaming_callback,
|
|
396
|
+
ctx.request_id,
|
|
397
|
+
i,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Core node processing
|
|
401
|
+
empty_response, empty_reason = await ac._process_node( # noqa: SLF001
|
|
402
|
+
node,
|
|
403
|
+
self.tool_callback,
|
|
404
|
+
self.state_manager,
|
|
405
|
+
tool_buffer,
|
|
406
|
+
self.streaming_callback,
|
|
407
|
+
self.usage_tracker,
|
|
408
|
+
response_state,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Handle empty response
|
|
412
|
+
self.empty_handler.track(empty_response)
|
|
413
|
+
if empty_response and self.empty_handler.should_intervene():
|
|
414
|
+
await self.empty_handler.prompt_action(self.message, empty_reason, i)
|
|
415
|
+
|
|
416
|
+
# Track whether we produced visible user output this iteration
|
|
417
|
+
if getattr(getattr(node, "result", None), "output", None):
|
|
418
|
+
response_state.has_user_response = True
|
|
304
419
|
|
|
305
|
-
|
|
306
|
-
|
|
420
|
+
# Productivity tracking
|
|
421
|
+
had_tool_use = _iteration_had_tool_use(node)
|
|
422
|
+
self.iteration_manager.track_productivity(had_tool_use, i)
|
|
307
423
|
|
|
308
|
-
|
|
424
|
+
# Force action if unproductive
|
|
425
|
+
await self.iteration_manager.force_action_if_unproductive(
|
|
426
|
+
self.message, i, response_state
|
|
427
|
+
)
|
|
309
428
|
|
|
310
|
-
|
|
311
|
-
|
|
429
|
+
# Force react snapshot
|
|
430
|
+
show_thoughts = bool(
|
|
431
|
+
getattr(self.state_manager.session, "show_thoughts", False)
|
|
432
|
+
)
|
|
433
|
+
await self.react_manager.capture_snapshot(i, agent_run.ctx, show_thoughts)
|
|
312
434
|
|
|
435
|
+
# Optional debug progress
|
|
436
|
+
if show_thoughts:
|
|
313
437
|
await ui.muted(
|
|
314
|
-
"\
|
|
438
|
+
f"\nITERATION: {i}/{ctx.max_iterations} (Request ID: {ctx.request_id})"
|
|
315
439
|
)
|
|
440
|
+
tool_summary = ac.get_tool_summary(
|
|
441
|
+
getattr(self.state_manager.session, "tool_calls", [])
|
|
442
|
+
)
|
|
443
|
+
if tool_summary:
|
|
444
|
+
summary_str = ", ".join(
|
|
445
|
+
f"{name}: {count}" for name, count in tool_summary.items()
|
|
446
|
+
)
|
|
447
|
+
await ui.muted(f"TOOLS USED: {summary_str}")
|
|
448
|
+
|
|
449
|
+
# Ask for clarification if agent requested it
|
|
450
|
+
if response_state.awaiting_user_guidance:
|
|
451
|
+
await self.iteration_manager.ask_for_clarification(i)
|
|
452
|
+
|
|
453
|
+
# Early completion
|
|
454
|
+
if response_state.task_completed:
|
|
455
|
+
if show_thoughts:
|
|
456
|
+
await ui.success("Task completed successfully")
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
# Handle iteration limit
|
|
460
|
+
await self.iteration_manager.handle_iteration_limit(i, response_state)
|
|
461
|
+
|
|
462
|
+
i += 1
|
|
463
|
+
|
|
464
|
+
await _finalize_buffered_tasks(tool_buffer, self.tool_callback, self.state_manager)
|
|
465
|
+
|
|
466
|
+
# Return wrapper that carries response_state
|
|
467
|
+
return ac.AgentRunWithState(agent_run, response_state)
|
|
468
|
+
|
|
469
|
+
except UserAbortError:
|
|
470
|
+
raise
|
|
471
|
+
except ToolBatchingJSONError as e:
|
|
472
|
+
logger.error("Tool batching JSON error [req=%s]: %s", ctx.request_id, e, exc_info=True)
|
|
473
|
+
ac.patch_tool_messages(
|
|
474
|
+
f"Tool batching failed: {str(e)[:100]}...", state_manager=self.state_manager
|
|
475
|
+
)
|
|
476
|
+
raise
|
|
477
|
+
except Exception as e:
|
|
478
|
+
# Attach request/iteration context for observability
|
|
479
|
+
safe_iter = getattr(self.state_manager.session, "current_iteration", "?")
|
|
480
|
+
logger.error(
|
|
481
|
+
"Error in process_request [req=%s iter=%s]: %s",
|
|
482
|
+
ctx.request_id,
|
|
483
|
+
safe_iter,
|
|
484
|
+
e,
|
|
485
|
+
exc_info=True,
|
|
486
|
+
)
|
|
487
|
+
ac.patch_tool_messages(
|
|
488
|
+
f"Request processing failed: {str(e)[:100]}...",
|
|
489
|
+
state_manager=self.state_manager,
|
|
490
|
+
)
|
|
491
|
+
raise
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# Utility functions
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
async def _maybe_stream_node_tokens(
|
|
498
|
+
node: Any,
|
|
499
|
+
agent_run_ctx: Any,
|
|
500
|
+
state_manager: StateManager,
|
|
501
|
+
streaming_cb: Optional[Callable[[str], Awaitable[None]]],
|
|
502
|
+
request_id: str,
|
|
503
|
+
iteration_index: int,
|
|
504
|
+
) -> None:
|
|
505
|
+
"""Stream tokens from model request nodes if callback provided.
|
|
316
506
|
|
|
317
|
-
|
|
507
|
+
Reference: main.py lines 146-161
|
|
508
|
+
"""
|
|
509
|
+
if not streaming_cb:
|
|
510
|
+
return
|
|
318
511
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
512
|
+
# Delegate to component streaming helper
|
|
513
|
+
if Agent.is_model_request_node(node): # type: ignore[attr-defined]
|
|
514
|
+
await ac.stream_model_request_node(
|
|
515
|
+
node, agent_run_ctx, state_manager, streaming_cb, request_id, iteration_index
|
|
516
|
+
)
|
|
323
517
|
|
|
324
|
-
await ui.success("Task completed successfully")
|
|
325
|
-
break
|
|
326
518
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
tools_str = tools_str if tools_str != "No tools used yet" else "No tools used"
|
|
519
|
+
def _iteration_had_tool_use(node: Any) -> bool:
|
|
520
|
+
"""Inspect the node to see if model responded with any tool-call parts.
|
|
330
521
|
|
|
331
|
-
|
|
522
|
+
Reference: main.py lines 164-171
|
|
523
|
+
"""
|
|
524
|
+
if hasattr(node, "model_response"):
|
|
525
|
+
for part in getattr(node.model_response, "parts", []):
|
|
526
|
+
# pydantic-ai annotates tool calls; be resilient to attr differences
|
|
527
|
+
if getattr(part, "part_kind", None) == "tool-call":
|
|
528
|
+
return True
|
|
529
|
+
return False
|
|
332
530
|
|
|
333
|
-
Progress summary:
|
|
334
|
-
- Tools used: {tools_str}
|
|
335
|
-
- Iterations completed: {i}
|
|
336
531
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
532
|
+
async def _finalize_buffered_tasks(
|
|
533
|
+
tool_buffer: ac.ToolBuffer,
|
|
534
|
+
tool_callback: Optional[ToolCallback],
|
|
535
|
+
state_manager: StateManager,
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Finalize and execute any buffered read-only tasks."""
|
|
538
|
+
if not tool_callback or not tool_buffer.has_tasks():
|
|
539
|
+
return
|
|
341
540
|
|
|
342
|
-
|
|
541
|
+
buffered_tasks = tool_buffer.flush()
|
|
343
542
|
|
|
344
|
-
|
|
543
|
+
# Update spinner message (best-effort)
|
|
544
|
+
try:
|
|
545
|
+
tool_names = [part.tool_name for part, _ in buffered_tasks]
|
|
546
|
+
batch_msg = get_batch_description(len(buffered_tasks), tool_names)
|
|
547
|
+
await ui.update_spinner_message(
|
|
548
|
+
f"[bold #00d7ff]{batch_msg}...[/bold #00d7ff]", state_manager
|
|
549
|
+
)
|
|
550
|
+
except Exception:
|
|
551
|
+
# UI is best-effort; never fail request because of display
|
|
552
|
+
logger.debug("UI batch prelude failed (non-fatal)", exc_info=True)
|
|
345
553
|
|
|
346
|
-
|
|
347
|
-
|
|
554
|
+
# Execute
|
|
555
|
+
await ac.execute_tools_parallel(buffered_tasks, tool_callback)
|
|
348
556
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
557
|
+
# Reset spinner message (best-effort)
|
|
558
|
+
try:
|
|
559
|
+
from tunacode.constants import UI_THINKING_MESSAGE # local import OK (rare path)
|
|
352
560
|
|
|
353
|
-
|
|
354
|
-
|
|
561
|
+
await ui.update_spinner_message(UI_THINKING_MESSAGE, state_manager)
|
|
562
|
+
except Exception:
|
|
563
|
+
logger.debug("UI batch epilogue failed (non-fatal)", exc_info=True)
|
|
355
564
|
|
|
356
|
-
# Increment iteration counter
|
|
357
|
-
i += 1
|
|
358
565
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
import time
|
|
566
|
+
def get_agent_tool() -> tuple[type[Agent], type["Tool"]]:
|
|
567
|
+
"""Return Agent and Tool classes without importing at module load time.
|
|
362
568
|
|
|
363
|
-
|
|
569
|
+
Reference: main.py lines 354-359
|
|
570
|
+
"""
|
|
571
|
+
from pydantic_ai import Agent as AgentCls
|
|
572
|
+
from pydantic_ai import Tool as ToolCls
|
|
364
573
|
|
|
365
|
-
|
|
366
|
-
start_time = time.time()
|
|
574
|
+
return AgentCls, ToolCls
|
|
367
575
|
|
|
368
|
-
# Update spinner message for final batch execution
|
|
369
|
-
tool_names = [part.tool_name for part, _ in buffered_tasks]
|
|
370
|
-
batch_msg = get_batch_description(len(buffered_tasks), tool_names)
|
|
371
|
-
await ui.update_spinner_message(
|
|
372
|
-
f"[bold #00d7ff]{batch_msg}...[/bold #00d7ff]", state_manager
|
|
373
|
-
)
|
|
374
576
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if hasattr(part, "args") and isinstance(part.args, dict):
|
|
384
|
-
if part.tool_name == "read_file" and "file_path" in part.args:
|
|
385
|
-
tool_desc += f" → {part.args['file_path']}"
|
|
386
|
-
elif part.tool_name == "grep" and "pattern" in part.args:
|
|
387
|
-
tool_desc += f" → pattern: '{part.args['pattern']}'"
|
|
388
|
-
if "include_files" in part.args:
|
|
389
|
-
tool_desc += f", files: '{part.args['include_files']}'"
|
|
390
|
-
elif part.tool_name == "list_dir" and "directory" in part.args:
|
|
391
|
-
tool_desc += f" → {part.args['directory']}"
|
|
392
|
-
elif part.tool_name == "glob" and "pattern" in part.args:
|
|
393
|
-
tool_desc += f" → pattern: '{part.args['pattern']}'"
|
|
394
|
-
await ui.muted(tool_desc)
|
|
395
|
-
await ui.muted("=" * 60)
|
|
396
|
-
|
|
397
|
-
await execute_tools_parallel(buffered_tasks, tool_callback)
|
|
398
|
-
|
|
399
|
-
elapsed_time = (time.time() - start_time) * 1000
|
|
400
|
-
sequential_estimate = len(buffered_tasks) * 100
|
|
401
|
-
speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
|
|
577
|
+
async def check_query_satisfaction(
|
|
578
|
+
agent: Agent,
|
|
579
|
+
original_query: str,
|
|
580
|
+
response: str,
|
|
581
|
+
state_manager: StateManager,
|
|
582
|
+
) -> bool:
|
|
583
|
+
"""Legacy hook for compatibility; completion still signaled via DONE marker."""
|
|
584
|
+
return True
|
|
402
585
|
|
|
403
|
-
await ui.muted(
|
|
404
|
-
f"✅ Final batch completed in {elapsed_time:.0f}ms "
|
|
405
|
-
f"(~{speedup:.1f}x faster than sequential)\n"
|
|
406
|
-
)
|
|
407
586
|
|
|
408
|
-
|
|
409
|
-
from tunacode.constants import UI_THINKING_MESSAGE
|
|
587
|
+
# Main entry point
|
|
410
588
|
|
|
411
|
-
await ui.update_spinner_message(UI_THINKING_MESSAGE, state_manager)
|
|
412
589
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
590
|
+
async def process_request(
|
|
591
|
+
message: str,
|
|
592
|
+
model: ModelName,
|
|
593
|
+
state_manager: StateManager,
|
|
594
|
+
tool_callback: Optional[ToolCallback] = None,
|
|
595
|
+
streaming_callback: Optional[Callable[[str], Awaitable[None]]] = None,
|
|
596
|
+
usage_tracker: Optional[UsageTrackerProtocol] = None,
|
|
597
|
+
) -> AgentRun:
|
|
598
|
+
"""
|
|
599
|
+
Process a single request to the agent.
|
|
422
600
|
|
|
423
|
-
|
|
424
|
-
"fallback_verbosity", "normal"
|
|
425
|
-
)
|
|
426
|
-
fallback = create_fallback_response(
|
|
427
|
-
i,
|
|
428
|
-
max_iterations,
|
|
429
|
-
state_manager.session.tool_calls,
|
|
430
|
-
state_manager.session.messages,
|
|
431
|
-
verbosity,
|
|
432
|
-
)
|
|
433
|
-
comprehensive_output = format_fallback_output(fallback)
|
|
601
|
+
CLAUDE_ANCHOR[process-request-entry]: Main entry point for all agent requests
|
|
434
602
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
except UserAbortError:
|
|
446
|
-
raise
|
|
447
|
-
except ToolBatchingJSONError as e:
|
|
448
|
-
logger.error(f"Tool batching JSON error: {e}", exc_info=True)
|
|
449
|
-
# Patch orphaned tool messages with error
|
|
450
|
-
patch_tool_messages(f"Tool batching failed: {str(e)[:100]}...", state_manager=state_manager)
|
|
451
|
-
# Re-raise to be handled by caller
|
|
452
|
-
raise
|
|
453
|
-
except Exception as e:
|
|
454
|
-
logger.error(f"Error in process_request: {e}", exc_info=True)
|
|
455
|
-
# Patch orphaned tool messages with generic error
|
|
456
|
-
patch_tool_messages(
|
|
457
|
-
f"Request processing failed: {str(e)[:100]}...", state_manager=state_manager
|
|
458
|
-
)
|
|
459
|
-
# Re-raise to be handled by caller
|
|
460
|
-
raise
|
|
603
|
+
"""
|
|
604
|
+
orchestrator = RequestOrchestrator(
|
|
605
|
+
message,
|
|
606
|
+
model,
|
|
607
|
+
state_manager,
|
|
608
|
+
tool_callback,
|
|
609
|
+
streaming_callback,
|
|
610
|
+
usage_tracker,
|
|
611
|
+
)
|
|
612
|
+
return await orchestrator.run()
|