nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/__init__.py
ADDED
nc1709/agent/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NC1709 Agent Module
|
|
3
|
+
|
|
4
|
+
Provides an agentic architecture similar to Claude Code with:
|
|
5
|
+
- Tool-based execution (Read, Write, Edit, Grep, Glob, Bash, etc.)
|
|
6
|
+
- Sub-agent spawning for complex tasks
|
|
7
|
+
- MCP integration for extended capabilities
|
|
8
|
+
- Permission system for tool execution
|
|
9
|
+
- Real-time visual feedback
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .core import Agent, AgentConfig, create_agent
|
|
13
|
+
from .tools.base import Tool, ToolResult, ToolRegistry, ToolPermission, ToolParameter
|
|
14
|
+
from .permissions import PermissionManager, PermissionPolicy, PermissionConfig
|
|
15
|
+
from .mcp_bridge import MCPBridge, MCPTool, integrate_mcp_with_agent
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Core
|
|
19
|
+
"Agent",
|
|
20
|
+
"AgentConfig",
|
|
21
|
+
"create_agent",
|
|
22
|
+
# Tools
|
|
23
|
+
"Tool",
|
|
24
|
+
"ToolResult",
|
|
25
|
+
"ToolRegistry",
|
|
26
|
+
"ToolPermission",
|
|
27
|
+
"ToolParameter",
|
|
28
|
+
# Permissions
|
|
29
|
+
"PermissionManager",
|
|
30
|
+
"PermissionPolicy",
|
|
31
|
+
"PermissionConfig",
|
|
32
|
+
# MCP
|
|
33
|
+
"MCPBridge",
|
|
34
|
+
"MCPTool",
|
|
35
|
+
"integrate_mcp_with_agent",
|
|
36
|
+
]
|
nc1709/agent/core.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Core
|
|
3
|
+
|
|
4
|
+
The main Agent class that implements the agentic execution loop:
|
|
5
|
+
1. Receive user request
|
|
6
|
+
2. Plan and decide on tool calls
|
|
7
|
+
3. Execute tools with visual feedback
|
|
8
|
+
4. Process results and continue or complete
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
from .tools.base import Tool, ToolResult, ToolRegistry, ToolPermission, get_default_registry
|
|
18
|
+
from .tools.file_tools import register_file_tools
|
|
19
|
+
from .tools.search_tools import register_search_tools
|
|
20
|
+
from .tools.bash_tool import register_bash_tools
|
|
21
|
+
from .tools.task_tool import register_task_tools, TaskTool
|
|
22
|
+
from .tools.web_tools import register_web_tools
|
|
23
|
+
from .tools.notebook_tools import register_notebook_tools
|
|
24
|
+
|
|
25
|
+
# Import visual feedback components
|
|
26
|
+
try:
|
|
27
|
+
from ..cli_ui import (
|
|
28
|
+
ActionSpinner, Color, Icons,
|
|
29
|
+
status, thinking, success, error, warning, info,
|
|
30
|
+
log_action
|
|
31
|
+
)
|
|
32
|
+
HAS_CLI_UI = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_CLI_UI = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentState(Enum):
|
|
38
|
+
"""States of the agent execution"""
|
|
39
|
+
IDLE = "idle"
|
|
40
|
+
THINKING = "thinking"
|
|
41
|
+
EXECUTING_TOOL = "executing_tool"
|
|
42
|
+
WAITING_APPROVAL = "waiting_approval"
|
|
43
|
+
COMPLETED = "completed"
|
|
44
|
+
ERROR = "error"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AgentConfig:
|
|
49
|
+
"""Configuration for an Agent"""
|
|
50
|
+
max_iterations: int = 50
|
|
51
|
+
max_tool_retries: int = 3
|
|
52
|
+
max_same_error_retries: int = 2 # Max times to retry the same failing command
|
|
53
|
+
tool_permissions: Dict[str, ToolPermission] = field(default_factory=dict)
|
|
54
|
+
auto_approve_tools: List[str] = field(default_factory=lambda: [
|
|
55
|
+
"Read", "Glob", "Grep", "TodoWrite"
|
|
56
|
+
])
|
|
57
|
+
require_approval_tools: List[str] = field(default_factory=lambda: [
|
|
58
|
+
"Write", "Edit", "Bash", "Task", "WebFetch"
|
|
59
|
+
])
|
|
60
|
+
show_tool_output: bool = True
|
|
61
|
+
verbose: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ToolCall:
|
|
66
|
+
"""Represents a tool call parsed from LLM output"""
|
|
67
|
+
name: str
|
|
68
|
+
parameters: Dict[str, Any]
|
|
69
|
+
raw_text: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Agent:
|
|
73
|
+
"""
|
|
74
|
+
Main Agent class implementing the agentic execution loop.
|
|
75
|
+
|
|
76
|
+
The agent:
|
|
77
|
+
1. Takes a user request
|
|
78
|
+
2. Uses an LLM to decide what tools to call
|
|
79
|
+
3. Executes tools with permission checks
|
|
80
|
+
4. Provides visual feedback
|
|
81
|
+
5. Continues until task is complete
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
SYSTEM_PROMPT = """You are NC1709, an AI assistant with access to tools for completing tasks.
|
|
85
|
+
|
|
86
|
+
You have access to these tools:
|
|
87
|
+
{tools_description}
|
|
88
|
+
|
|
89
|
+
## How to use tools
|
|
90
|
+
|
|
91
|
+
To use a tool, include a tool call in your response using this exact format:
|
|
92
|
+
```tool
|
|
93
|
+
{{"tool": "ToolName", "parameters": {{"param1": "value1", "param2": "value2"}}}}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Guidelines
|
|
97
|
+
|
|
98
|
+
1. **Read before writing**: Always read files before modifying them
|
|
99
|
+
2. **Be precise**: Use exact file paths and specific parameters
|
|
100
|
+
3. **Explain your actions**: Briefly explain what you're doing and why
|
|
101
|
+
4. **Handle errors**: If a tool fails, try a DIFFERENT approach instead of repeating the same command
|
|
102
|
+
5. **Complete the task**: Keep working until the task is fully complete
|
|
103
|
+
6. **Ask if unclear**: Use AskUser if you need clarification
|
|
104
|
+
|
|
105
|
+
## Building New Projects - IMPORTANT
|
|
106
|
+
|
|
107
|
+
When creating a new project or application:
|
|
108
|
+
|
|
109
|
+
1. **Always create a project directory first**:
|
|
110
|
+
- Create a dedicated directory for the project (e.g., `my_project/`)
|
|
111
|
+
- ALL project files go inside this directory
|
|
112
|
+
- Example: For a "quantum simulator", create `quantum_simulator/` first
|
|
113
|
+
|
|
114
|
+
2. **Set up Python environment properly**:
|
|
115
|
+
- Create a virtual environment: `python3 -m venv <project_dir>/venv`
|
|
116
|
+
- Install packages using: `<project_dir>/venv/bin/pip install <package>`
|
|
117
|
+
- Run scripts using: `<project_dir>/venv/bin/python <script.py>`
|
|
118
|
+
- NEVER use bare `pip install` - always use the venv pip
|
|
119
|
+
|
|
120
|
+
3. **Project structure example**:
|
|
121
|
+
```
|
|
122
|
+
project_name/
|
|
123
|
+
├── venv/ # Virtual environment
|
|
124
|
+
├── requirements.txt # Dependencies
|
|
125
|
+
├── main.py # Entry point
|
|
126
|
+
├── app.py # Web app (if applicable)
|
|
127
|
+
├── static/ # Static files
|
|
128
|
+
└── templates/ # HTML templates
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
4. **After writing files, verify they exist and have content**
|
|
132
|
+
|
|
133
|
+
## Error Recovery
|
|
134
|
+
|
|
135
|
+
- If a command fails 2 times with the same error, TRY A DIFFERENT APPROACH
|
|
136
|
+
- Don't keep repeating `pip install` if it fails - check the Python environment
|
|
137
|
+
- Use absolute paths when relative paths fail
|
|
138
|
+
- If a module is not found, verify the correct Python interpreter is being used
|
|
139
|
+
|
|
140
|
+
## Current context
|
|
141
|
+
|
|
142
|
+
Working directory: {cwd}
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
llm=None,
|
|
148
|
+
config: AgentConfig = None,
|
|
149
|
+
registry: ToolRegistry = None,
|
|
150
|
+
parent_agent: "Agent" = None,
|
|
151
|
+
):
|
|
152
|
+
"""Initialize the agent
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
llm: LLM adapter for generating responses
|
|
156
|
+
config: Agent configuration
|
|
157
|
+
registry: Tool registry (creates default if None)
|
|
158
|
+
parent_agent: Parent agent if this is a sub-agent
|
|
159
|
+
"""
|
|
160
|
+
self.llm = llm
|
|
161
|
+
self.config = config or AgentConfig()
|
|
162
|
+
self.registry = registry or self._create_default_registry()
|
|
163
|
+
self.parent_agent = parent_agent
|
|
164
|
+
|
|
165
|
+
self.state = AgentState.IDLE
|
|
166
|
+
self.iteration_count = 0
|
|
167
|
+
self.conversation_history: List[Dict[str, str]] = []
|
|
168
|
+
self.tool_results: List[ToolResult] = []
|
|
169
|
+
|
|
170
|
+
# Visual feedback
|
|
171
|
+
self._spinner: Optional[ActionSpinner] = None
|
|
172
|
+
|
|
173
|
+
# Loop detection - track failed commands to avoid repeating
|
|
174
|
+
self._failed_commands: Dict[str, int] = {} # command_signature -> failure_count
|
|
175
|
+
self._last_error: Optional[str] = None
|
|
176
|
+
|
|
177
|
+
# Apply permission settings
|
|
178
|
+
self._apply_permission_config()
|
|
179
|
+
|
|
180
|
+
def _create_default_registry(self) -> ToolRegistry:
|
|
181
|
+
"""Create and populate default tool registry"""
|
|
182
|
+
registry = ToolRegistry()
|
|
183
|
+
|
|
184
|
+
# Register all built-in tools
|
|
185
|
+
register_file_tools(registry)
|
|
186
|
+
register_search_tools(registry)
|
|
187
|
+
register_bash_tools(registry)
|
|
188
|
+
task_tool = register_task_tools(registry, parent_agent=self)
|
|
189
|
+
register_web_tools(registry)
|
|
190
|
+
register_notebook_tools(registry)
|
|
191
|
+
|
|
192
|
+
return registry
|
|
193
|
+
|
|
194
|
+
def _apply_permission_config(self) -> None:
|
|
195
|
+
"""Apply permission configuration to registry"""
|
|
196
|
+
# Set auto-approve tools
|
|
197
|
+
for tool_name in self.config.auto_approve_tools:
|
|
198
|
+
self.registry.set_permission(tool_name, ToolPermission.AUTO)
|
|
199
|
+
|
|
200
|
+
# Set require-approval tools
|
|
201
|
+
for tool_name in self.config.require_approval_tools:
|
|
202
|
+
self.registry.set_permission(tool_name, ToolPermission.ASK)
|
|
203
|
+
|
|
204
|
+
# Apply custom overrides
|
|
205
|
+
for tool_name, permission in self.config.tool_permissions.items():
|
|
206
|
+
self.registry.set_permission(tool_name, permission)
|
|
207
|
+
|
|
208
|
+
def run(self, user_request: str) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Run the agent on a user request.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
user_request: The user's request or task
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Final response or result
|
|
217
|
+
"""
|
|
218
|
+
self.state = AgentState.THINKING
|
|
219
|
+
self.iteration_count = 0
|
|
220
|
+
self.conversation_history = []
|
|
221
|
+
self.tool_results = []
|
|
222
|
+
|
|
223
|
+
# Build system prompt with tools
|
|
224
|
+
import os
|
|
225
|
+
system_prompt = self.SYSTEM_PROMPT.format(
|
|
226
|
+
tools_description=self.registry.get_tools_prompt(),
|
|
227
|
+
cwd=os.getcwd(),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Add user request
|
|
231
|
+
self.conversation_history.append({
|
|
232
|
+
"role": "system",
|
|
233
|
+
"content": system_prompt,
|
|
234
|
+
})
|
|
235
|
+
self.conversation_history.append({
|
|
236
|
+
"role": "user",
|
|
237
|
+
"content": user_request,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
# Main execution loop
|
|
241
|
+
while self.iteration_count < self.config.max_iterations:
|
|
242
|
+
self.iteration_count += 1
|
|
243
|
+
|
|
244
|
+
if HAS_CLI_UI:
|
|
245
|
+
thinking(f"Iteration {self.iteration_count}...")
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
# Get LLM response
|
|
249
|
+
response = self._get_llm_response()
|
|
250
|
+
|
|
251
|
+
# Parse for tool calls
|
|
252
|
+
tool_calls = self._parse_tool_calls(response)
|
|
253
|
+
|
|
254
|
+
if not tool_calls:
|
|
255
|
+
# No tool calls - agent is done or providing final response
|
|
256
|
+
self.state = AgentState.COMPLETED
|
|
257
|
+
return self._clean_response(response)
|
|
258
|
+
|
|
259
|
+
# Execute tool calls
|
|
260
|
+
all_results = []
|
|
261
|
+
for tool_call in tool_calls:
|
|
262
|
+
result = self._execute_tool_call(tool_call)
|
|
263
|
+
all_results.append(result)
|
|
264
|
+
self.tool_results.append(result)
|
|
265
|
+
|
|
266
|
+
# Add results to conversation
|
|
267
|
+
results_text = self._format_tool_results(all_results)
|
|
268
|
+
self.conversation_history.append({
|
|
269
|
+
"role": "assistant",
|
|
270
|
+
"content": response,
|
|
271
|
+
})
|
|
272
|
+
self.conversation_history.append({
|
|
273
|
+
"role": "user",
|
|
274
|
+
"content": f"Tool results:\n{results_text}\n\nContinue with the task.",
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self.state = AgentState.ERROR
|
|
279
|
+
if HAS_CLI_UI:
|
|
280
|
+
error(f"Agent error: {e}")
|
|
281
|
+
return f"Error during execution: {e}"
|
|
282
|
+
|
|
283
|
+
# Max iterations reached
|
|
284
|
+
self.state = AgentState.COMPLETED
|
|
285
|
+
if HAS_CLI_UI:
|
|
286
|
+
warning(f"Reached maximum iterations ({self.config.max_iterations})")
|
|
287
|
+
return "Task incomplete - reached maximum iterations."
|
|
288
|
+
|
|
289
|
+
def _get_llm_response(self) -> str:
|
|
290
|
+
"""Get response from LLM"""
|
|
291
|
+
if self.llm is None:
|
|
292
|
+
raise ValueError("No LLM configured for agent")
|
|
293
|
+
|
|
294
|
+
# Build messages for LLM
|
|
295
|
+
messages = self.conversation_history.copy()
|
|
296
|
+
|
|
297
|
+
# Get completion
|
|
298
|
+
response = self.llm.chat(messages)
|
|
299
|
+
return response
|
|
300
|
+
|
|
301
|
+
def _parse_tool_calls(self, response: str) -> List[ToolCall]:
|
|
302
|
+
"""Parse tool calls from LLM response"""
|
|
303
|
+
tool_calls = []
|
|
304
|
+
|
|
305
|
+
# Pattern 1: ```tool ... ``` blocks
|
|
306
|
+
pattern = r"```tool\s*\n?(.*?)\n?```"
|
|
307
|
+
matches = re.findall(pattern, response, re.DOTALL)
|
|
308
|
+
|
|
309
|
+
for match in matches:
|
|
310
|
+
try:
|
|
311
|
+
data = json.loads(match.strip())
|
|
312
|
+
if "tool" in data:
|
|
313
|
+
tool_calls.append(ToolCall(
|
|
314
|
+
name=data["tool"],
|
|
315
|
+
parameters=data.get("parameters", {}),
|
|
316
|
+
raw_text=match,
|
|
317
|
+
))
|
|
318
|
+
except json.JSONDecodeError:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Pattern 2: JSON objects with "tool" key
|
|
322
|
+
json_pattern = r'\{[^{}]*"tool"\s*:\s*"[^"]+"\s*[^{}]*\}'
|
|
323
|
+
json_matches = re.findall(json_pattern, response)
|
|
324
|
+
|
|
325
|
+
for match in json_matches:
|
|
326
|
+
if match not in [tc.raw_text for tc in tool_calls]:
|
|
327
|
+
try:
|
|
328
|
+
data = json.loads(match)
|
|
329
|
+
if "tool" in data:
|
|
330
|
+
tool_calls.append(ToolCall(
|
|
331
|
+
name=data["tool"],
|
|
332
|
+
parameters=data.get("parameters", {}),
|
|
333
|
+
raw_text=match,
|
|
334
|
+
))
|
|
335
|
+
except json.JSONDecodeError:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
return tool_calls
|
|
339
|
+
|
|
340
|
+
def _get_command_signature(self, tool_call: ToolCall) -> str:
|
|
341
|
+
"""Get a signature for a tool call to detect repeated failures"""
|
|
342
|
+
import hashlib
|
|
343
|
+
sig_data = f"{tool_call.name}:{json.dumps(tool_call.parameters, sort_keys=True)}"
|
|
344
|
+
return hashlib.md5(sig_data.encode()).hexdigest()[:12]
|
|
345
|
+
|
|
346
|
+
def _check_loop_detection(self, tool_call: ToolCall) -> Optional[str]:
|
|
347
|
+
"""Check if this command has failed too many times
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Warning message if loop detected, None otherwise
|
|
351
|
+
"""
|
|
352
|
+
sig = self._get_command_signature(tool_call)
|
|
353
|
+
fail_count = self._failed_commands.get(sig, 0)
|
|
354
|
+
|
|
355
|
+
if fail_count >= self.config.max_same_error_retries:
|
|
356
|
+
return (
|
|
357
|
+
f"LOOP DETECTED: This command has failed {fail_count} times with the same error. "
|
|
358
|
+
f"You MUST try a DIFFERENT approach instead of repeating this command. "
|
|
359
|
+
f"Last error: {self._last_error}"
|
|
360
|
+
)
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def _record_failure(self, tool_call: ToolCall, error: str) -> None:
|
|
364
|
+
"""Record a command failure for loop detection"""
|
|
365
|
+
sig = self._get_command_signature(tool_call)
|
|
366
|
+
self._failed_commands[sig] = self._failed_commands.get(sig, 0) + 1
|
|
367
|
+
self._last_error = error
|
|
368
|
+
|
|
369
|
+
def _execute_tool_call(self, tool_call: ToolCall) -> ToolResult:
|
|
370
|
+
"""Execute a single tool call"""
|
|
371
|
+
tool = self.registry.get(tool_call.name)
|
|
372
|
+
|
|
373
|
+
if not tool:
|
|
374
|
+
return ToolResult(
|
|
375
|
+
success=False,
|
|
376
|
+
output="",
|
|
377
|
+
error=f"Unknown tool: {tool_call.name}",
|
|
378
|
+
tool_name=tool_call.name,
|
|
379
|
+
target=str(tool_call.parameters)[:30],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Loop detection - check if this command has failed too many times
|
|
383
|
+
loop_warning = self._check_loop_detection(tool_call)
|
|
384
|
+
if loop_warning:
|
|
385
|
+
if HAS_CLI_UI:
|
|
386
|
+
warning(f"Loop detected for {tool_call.name}")
|
|
387
|
+
return ToolResult(
|
|
388
|
+
success=False,
|
|
389
|
+
output="",
|
|
390
|
+
error=loop_warning,
|
|
391
|
+
tool_name=tool_call.name,
|
|
392
|
+
target=tool._get_target(**tool_call.parameters),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Check permission - special handling for Bash with safe commands
|
|
396
|
+
needs_approval = self.registry.needs_approval(tool_call.name)
|
|
397
|
+
|
|
398
|
+
# For Bash tool, check if command is safe (read-only)
|
|
399
|
+
if tool_call.name == "Bash" and "command" in tool_call.parameters:
|
|
400
|
+
from .tools.bash_tool import BashTool
|
|
401
|
+
if BashTool.is_safe_command(tool_call.parameters["command"]):
|
|
402
|
+
needs_approval = False
|
|
403
|
+
|
|
404
|
+
if needs_approval:
|
|
405
|
+
self.state = AgentState.WAITING_APPROVAL
|
|
406
|
+
approved = self._request_approval(tool_call)
|
|
407
|
+
if not approved:
|
|
408
|
+
return ToolResult(
|
|
409
|
+
success=False,
|
|
410
|
+
output="",
|
|
411
|
+
error="Tool execution denied by user",
|
|
412
|
+
tool_name=tool_call.name,
|
|
413
|
+
target=tool._get_target(**tool_call.parameters),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Execute tool
|
|
417
|
+
self.state = AgentState.EXECUTING_TOOL
|
|
418
|
+
|
|
419
|
+
if HAS_CLI_UI:
|
|
420
|
+
target = tool._get_target(**tool_call.parameters)
|
|
421
|
+
log_action(tool_call.name, target, "running")
|
|
422
|
+
|
|
423
|
+
result = tool.run(**tool_call.parameters)
|
|
424
|
+
|
|
425
|
+
# Track failures for loop detection
|
|
426
|
+
if not result.success:
|
|
427
|
+
self._record_failure(tool_call, result.error or "Unknown error")
|
|
428
|
+
|
|
429
|
+
if HAS_CLI_UI:
|
|
430
|
+
state = "success" if result.success else "error"
|
|
431
|
+
if self.config.verbose or not result.success:
|
|
432
|
+
log_action(tool_call.name, result.target, state)
|
|
433
|
+
if self.config.show_tool_output and result.output:
|
|
434
|
+
# Show truncated output
|
|
435
|
+
output = result.output[:500]
|
|
436
|
+
if len(result.output) > 500:
|
|
437
|
+
output += "..."
|
|
438
|
+
print(f" {Color.DIM}{output}{Color.RESET}")
|
|
439
|
+
|
|
440
|
+
return result
|
|
441
|
+
|
|
442
|
+
def _request_approval(self, tool_call: ToolCall) -> bool:
|
|
443
|
+
"""Request user approval for a tool call"""
|
|
444
|
+
tool = self.registry.get(tool_call.name)
|
|
445
|
+
target = tool._get_target(**tool_call.parameters) if tool else ""
|
|
446
|
+
|
|
447
|
+
print(f"\n{Color.YELLOW}⚠ Tool requires approval:{Color.RESET}")
|
|
448
|
+
print(f" {Color.BOLD}{tool_call.name}{Color.RESET}({Color.CYAN}{target}{Color.RESET})")
|
|
449
|
+
|
|
450
|
+
if tool_call.parameters:
|
|
451
|
+
print(f" Parameters: {json.dumps(tool_call.parameters, indent=2)[:200]}")
|
|
452
|
+
|
|
453
|
+
response = input(f"\n{Color.BOLD}Allow?{Color.RESET} [y/N/always]: ").strip().lower()
|
|
454
|
+
|
|
455
|
+
if response == "always":
|
|
456
|
+
self.registry.approve_for_session(tool_call.name)
|
|
457
|
+
return True
|
|
458
|
+
elif response in ["y", "yes"]:
|
|
459
|
+
return True
|
|
460
|
+
else:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
def _format_tool_results(self, results: List[ToolResult]) -> str:
|
|
464
|
+
"""Format tool results for conversation"""
|
|
465
|
+
parts = []
|
|
466
|
+
for result in results:
|
|
467
|
+
if result.success:
|
|
468
|
+
parts.append(f"✓ {result.tool_name}({result.target}):\n{result.output}")
|
|
469
|
+
else:
|
|
470
|
+
parts.append(f"✗ {result.tool_name}({result.target}) failed: {result.error}")
|
|
471
|
+
return "\n\n".join(parts)
|
|
472
|
+
|
|
473
|
+
def _clean_response(self, response: str) -> str:
|
|
474
|
+
"""Clean tool call markers from final response"""
|
|
475
|
+
# Remove tool blocks
|
|
476
|
+
response = re.sub(r"```tool\s*\n?.*?\n?```", "", response, flags=re.DOTALL)
|
|
477
|
+
# Remove JSON tool calls
|
|
478
|
+
response = re.sub(r'\{[^{}]*"tool"\s*:\s*"[^"]+"\s*[^{}]*\}', "", response)
|
|
479
|
+
return response.strip()
|
|
480
|
+
|
|
481
|
+
def get_tool_history(self) -> List[Dict[str, Any]]:
|
|
482
|
+
"""Get history of tool calls and results"""
|
|
483
|
+
return [
|
|
484
|
+
{
|
|
485
|
+
"tool": r.tool_name,
|
|
486
|
+
"target": r.target,
|
|
487
|
+
"success": r.success,
|
|
488
|
+
"duration_ms": r.duration_ms,
|
|
489
|
+
}
|
|
490
|
+
for r in self.tool_results
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def create_agent(llm=None, **config_kwargs) -> Agent:
|
|
495
|
+
"""Create an agent with default configuration
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
llm: LLM adapter
|
|
499
|
+
**config_kwargs: Configuration overrides
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Configured Agent instance
|
|
503
|
+
"""
|
|
504
|
+
config = AgentConfig(**config_kwargs)
|
|
505
|
+
return Agent(llm=llm, config=config)
|