nc1709 1.15.4__py3-none-any.whl → 1.18.8__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 +1 -1
- nc1709/agent/core.py +172 -19
- nc1709/agent/permissions.py +2 -2
- nc1709/agent/tools/bash_tool.py +295 -8
- nc1709/cli.py +435 -19
- nc1709/cli_ui.py +137 -52
- nc1709/conversation_logger.py +416 -0
- nc1709/llm_adapter.py +62 -4
- nc1709/plugins/agents/database_agent.py +695 -0
- nc1709/plugins/agents/django_agent.py +11 -4
- nc1709/plugins/agents/docker_agent.py +11 -4
- nc1709/plugins/agents/fastapi_agent.py +11 -4
- nc1709/plugins/agents/git_agent.py +11 -4
- nc1709/plugins/agents/nextjs_agent.py +11 -4
- nc1709/plugins/agents/ollama_agent.py +574 -0
- nc1709/plugins/agents/test_agent.py +702 -0
- nc1709/prompts/unified_prompt.py +156 -14
- nc1709/requirements_tracker.py +526 -0
- nc1709/thinking_messages.py +337 -0
- nc1709/version_check.py +6 -2
- nc1709/web/server.py +63 -3
- nc1709/web/templates/index.html +819 -140
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/METADATA +10 -7
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/RECORD +28 -22
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/WHEEL +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/entry_points.txt +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/licenses/LICENSE +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/top_level.txt +0 -0
nc1709/__init__.py
CHANGED
nc1709/agent/core.py
CHANGED
|
@@ -27,7 +27,7 @@ try:
|
|
|
27
27
|
from ..cli_ui import (
|
|
28
28
|
ActionSpinner, Color, Icons,
|
|
29
29
|
status, thinking, success, error, warning, info,
|
|
30
|
-
log_action
|
|
30
|
+
log_action, log_output
|
|
31
31
|
)
|
|
32
32
|
HAS_CLI_UI = True
|
|
33
33
|
except ImportError:
|
|
@@ -50,6 +50,7 @@ class AgentConfig:
|
|
|
50
50
|
max_iterations: int = 50
|
|
51
51
|
max_tool_retries: int = 3
|
|
52
52
|
max_same_error_retries: int = 2 # Max times to retry the same failing command
|
|
53
|
+
max_alternating_loops: int = 3 # Max alternating pattern repetitions (e.g., Read→Edit→Read→Edit)
|
|
53
54
|
tool_permissions: Dict[str, ToolPermission] = field(default_factory=dict)
|
|
54
55
|
auto_approve_tools: List[str] = field(default_factory=lambda: [
|
|
55
56
|
"Read", "Glob", "Grep", "TodoWrite"
|
|
@@ -93,13 +94,23 @@ To use a tool, include a tool call in your response using this exact format:
|
|
|
93
94
|
{{"tool": "ToolName", "parameters": {{"param1": "value1", "param2": "value2"}}}}
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
## CRITICAL: When to STOP
|
|
98
|
+
|
|
99
|
+
You MUST stop and provide a final summary (without tool calls) when:
|
|
100
|
+
- A command/script runs successfully (exit code 0)
|
|
101
|
+
- The requested file has been created/edited
|
|
102
|
+
- The task is complete
|
|
103
|
+
- You've already run the same command successfully - DO NOT run it again
|
|
104
|
+
|
|
105
|
+
NEVER repeat the same successful tool call. If `python example.py` succeeds once, the task is DONE.
|
|
106
|
+
|
|
96
107
|
## Guidelines
|
|
97
108
|
|
|
98
109
|
1. **Read before writing**: Always read files before modifying them
|
|
99
110
|
2. **Be precise**: Use exact file paths and specific parameters
|
|
100
111
|
3. **Explain your actions**: Briefly explain what you're doing and why
|
|
101
112
|
4. **Handle errors**: If a tool fails, try a DIFFERENT approach instead of repeating the same command
|
|
102
|
-
5. **
|
|
113
|
+
5. **Know when to stop**: Once a task succeeds, STOP and summarize - don't repeat it
|
|
103
114
|
6. **Ask if unclear**: Use AskUser if you need clarification
|
|
104
115
|
|
|
105
116
|
## Building New Projects - IMPORTANT
|
|
@@ -174,6 +185,10 @@ Working directory: {cwd}
|
|
|
174
185
|
self._failed_commands: Dict[str, int] = {} # command_signature -> failure_count
|
|
175
186
|
self._last_error: Optional[str] = None
|
|
176
187
|
|
|
188
|
+
# Advanced loop detection
|
|
189
|
+
self._tool_sequence: List[str] = [] # Track tool names for pattern detection
|
|
190
|
+
self._successful_commands: set = set() # Track commands that already succeeded
|
|
191
|
+
|
|
177
192
|
# Apply permission settings
|
|
178
193
|
self._apply_permission_config()
|
|
179
194
|
|
|
@@ -219,6 +234,7 @@ Working directory: {cwd}
|
|
|
219
234
|
self.iteration_count = 0
|
|
220
235
|
self.conversation_history = []
|
|
221
236
|
self.tool_results = []
|
|
237
|
+
self._recent_tool_calls = [] # Track recent calls for loop detection
|
|
222
238
|
|
|
223
239
|
# Build system prompt with tools
|
|
224
240
|
import os
|
|
@@ -256,6 +272,17 @@ Working directory: {cwd}
|
|
|
256
272
|
self.state = AgentState.COMPLETED
|
|
257
273
|
return self._clean_response(response)
|
|
258
274
|
|
|
275
|
+
# Detect loops - same tool call made 3+ times
|
|
276
|
+
current_call_sig = [(tc.name, str(tc.arguments)) for tc in tool_calls]
|
|
277
|
+
self._recent_tool_calls.append(current_call_sig)
|
|
278
|
+
if len(self._recent_tool_calls) >= 3:
|
|
279
|
+
last_three = self._recent_tool_calls[-3:]
|
|
280
|
+
if last_three[0] == last_three[1] == last_three[2]:
|
|
281
|
+
if HAS_CLI_UI:
|
|
282
|
+
warning("Detected loop - same tool called 3 times. Stopping.")
|
|
283
|
+
self.state = AgentState.COMPLETED
|
|
284
|
+
return "Task completed (detected repetitive tool calls)."
|
|
285
|
+
|
|
259
286
|
# Execute tool calls
|
|
260
287
|
all_results = []
|
|
261
288
|
for tool_call in tool_calls:
|
|
@@ -269,9 +296,68 @@ Working directory: {cwd}
|
|
|
269
296
|
"role": "assistant",
|
|
270
297
|
"content": response,
|
|
271
298
|
})
|
|
299
|
+
|
|
300
|
+
# Check if all tools succeeded - if so, strongly hint task may be done
|
|
301
|
+
all_succeeded = all(r.success for r in all_results)
|
|
302
|
+
has_bash = any(tc.name == "Bash" for tc in tool_calls)
|
|
303
|
+
has_write = any(tc.name in ["Write", "Edit"] for tc in tool_calls)
|
|
304
|
+
|
|
305
|
+
# Check for silent success (command succeeded but no output)
|
|
306
|
+
silent_success = all_succeeded and all(
|
|
307
|
+
not r.output.strip() for r in all_results
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if all_succeeded and has_bash:
|
|
311
|
+
# Bash command succeeded - task is likely complete
|
|
312
|
+
if silent_success:
|
|
313
|
+
follow_up = (
|
|
314
|
+
f"Tool results:\n{results_text}\n\n"
|
|
315
|
+
"The command completed successfully with no output (exit code 0). "
|
|
316
|
+
"This typically means the operation succeeded. "
|
|
317
|
+
"Provide a final summary of what was accomplished WITHOUT any more tool calls."
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
follow_up = (
|
|
321
|
+
f"Tool results:\n{results_text}\n\n"
|
|
322
|
+
"The command executed successfully. "
|
|
323
|
+
"If this completes the user's request, provide a final summary WITHOUT any tool calls. "
|
|
324
|
+
"Only use more tools if there are additional steps needed."
|
|
325
|
+
)
|
|
326
|
+
elif all_succeeded and has_write:
|
|
327
|
+
# File was written/edited successfully
|
|
328
|
+
follow_up = (
|
|
329
|
+
f"Tool results:\n{results_text}\n\n"
|
|
330
|
+
"The file was successfully created/modified. "
|
|
331
|
+
"If this completes the task, provide a summary. "
|
|
332
|
+
"Do NOT read the file back unless the user asked to verify it."
|
|
333
|
+
)
|
|
334
|
+
elif all_succeeded:
|
|
335
|
+
follow_up = (
|
|
336
|
+
f"Tool results:\n{results_text}\n\n"
|
|
337
|
+
"All tools succeeded. Provide a summary if the task is complete, or continue with the next step."
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
# Some failed - analyze failure type
|
|
341
|
+
failed_results = [r for r in all_results if not r.success]
|
|
342
|
+
loop_detected = any("LOOP" in (r.error or "") or "REDUNDANT" in (r.error or "") for r in failed_results)
|
|
343
|
+
|
|
344
|
+
if loop_detected:
|
|
345
|
+
follow_up = (
|
|
346
|
+
f"Tool results:\n{results_text}\n\n"
|
|
347
|
+
"A loop or redundant operation was detected. "
|
|
348
|
+
"STOP and either: 1) Provide a final summary if the task is done, or "
|
|
349
|
+
"2) Explain what's blocking progress and ask the user for guidance."
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
follow_up = (
|
|
353
|
+
f"Tool results:\n{results_text}\n\n"
|
|
354
|
+
"Some tools failed. Try a DIFFERENT approach - don't repeat the same command. "
|
|
355
|
+
"If you've tried multiple approaches without success, ask the user for help."
|
|
356
|
+
)
|
|
357
|
+
|
|
272
358
|
self.conversation_history.append({
|
|
273
359
|
"role": "user",
|
|
274
|
-
"content":
|
|
360
|
+
"content": follow_up,
|
|
275
361
|
})
|
|
276
362
|
|
|
277
363
|
except Exception as e:
|
|
@@ -350,16 +436,62 @@ Working directory: {cwd}
|
|
|
350
436
|
Warning message if loop detected, None otherwise
|
|
351
437
|
"""
|
|
352
438
|
sig = self._get_command_signature(tool_call)
|
|
353
|
-
fail_count = self._failed_commands.get(sig, 0)
|
|
354
439
|
|
|
440
|
+
# Check 1: Same command failed too many times
|
|
441
|
+
fail_count = self._failed_commands.get(sig, 0)
|
|
355
442
|
if fail_count >= self.config.max_same_error_retries:
|
|
356
443
|
return (
|
|
357
444
|
f"LOOP DETECTED: This command has failed {fail_count} times with the same error. "
|
|
358
445
|
f"You MUST try a DIFFERENT approach instead of repeating this command. "
|
|
359
446
|
f"Last error: {self._last_error}"
|
|
360
447
|
)
|
|
448
|
+
|
|
449
|
+
# Check 2: Trying to repeat a command that already succeeded
|
|
450
|
+
if sig in self._successful_commands:
|
|
451
|
+
return (
|
|
452
|
+
f"REDUNDANT COMMAND: This exact command already succeeded. "
|
|
453
|
+
f"Do NOT repeat it. If the task is complete, provide a summary. "
|
|
454
|
+
f"If you need different output, modify the command parameters."
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
def _check_alternating_pattern(self) -> Optional[str]:
|
|
460
|
+
"""Detect alternating tool patterns like Read→Edit→Read→Edit
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Warning message if pattern detected, None otherwise
|
|
464
|
+
"""
|
|
465
|
+
seq = self._tool_sequence
|
|
466
|
+
if len(seq) < 4:
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
# Check for 2-tool alternating pattern (e.g., Read→Edit→Read→Edit)
|
|
470
|
+
if len(seq) >= 6:
|
|
471
|
+
last_six = seq[-6:]
|
|
472
|
+
if (last_six[0] == last_six[2] == last_six[4] and
|
|
473
|
+
last_six[1] == last_six[3] == last_six[5] and
|
|
474
|
+
last_six[0] != last_six[1]):
|
|
475
|
+
return (
|
|
476
|
+
f"ALTERNATING LOOP DETECTED: Pattern {last_six[0]}→{last_six[1]} repeated 3 times. "
|
|
477
|
+
f"You appear to be stuck in a loop. Stop and summarize what you've accomplished, "
|
|
478
|
+
f"or try a completely different approach."
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Check for single-tool repetition (already covered elsewhere but double-check)
|
|
482
|
+
if len(seq) >= 4 and seq[-1] == seq[-2] == seq[-3] == seq[-4]:
|
|
483
|
+
return (
|
|
484
|
+
f"REPETITION DETECTED: {seq[-1]} called 4 times in a row. "
|
|
485
|
+
f"Stop repeating and either complete the task or try a different approach."
|
|
486
|
+
)
|
|
487
|
+
|
|
361
488
|
return None
|
|
362
489
|
|
|
490
|
+
def _record_success(self, tool_call: ToolCall) -> None:
|
|
491
|
+
"""Record a successful command to prevent redundant repeats"""
|
|
492
|
+
sig = self._get_command_signature(tool_call)
|
|
493
|
+
self._successful_commands.add(sig)
|
|
494
|
+
|
|
363
495
|
def _record_failure(self, tool_call: ToolCall, error: str) -> None:
|
|
364
496
|
"""Record a command failure for loop detection"""
|
|
365
497
|
sig = self._get_command_signature(tool_call)
|
|
@@ -379,7 +511,10 @@ Working directory: {cwd}
|
|
|
379
511
|
target=str(tool_call.parameters)[:30],
|
|
380
512
|
)
|
|
381
513
|
|
|
382
|
-
#
|
|
514
|
+
# Track tool sequence for alternating pattern detection
|
|
515
|
+
self._tool_sequence.append(tool_call.name)
|
|
516
|
+
|
|
517
|
+
# Loop detection - check if this command has failed too many times or already succeeded
|
|
383
518
|
loop_warning = self._check_loop_detection(tool_call)
|
|
384
519
|
if loop_warning:
|
|
385
520
|
if HAS_CLI_UI:
|
|
@@ -392,6 +527,19 @@ Working directory: {cwd}
|
|
|
392
527
|
target=tool._get_target(**tool_call.parameters),
|
|
393
528
|
)
|
|
394
529
|
|
|
530
|
+
# Check for alternating tool patterns
|
|
531
|
+
alt_warning = self._check_alternating_pattern()
|
|
532
|
+
if alt_warning:
|
|
533
|
+
if HAS_CLI_UI:
|
|
534
|
+
warning("Alternating pattern detected")
|
|
535
|
+
return ToolResult(
|
|
536
|
+
success=False,
|
|
537
|
+
output="",
|
|
538
|
+
error=alt_warning,
|
|
539
|
+
tool_name=tool_call.name,
|
|
540
|
+
target=tool._get_target(**tool_call.parameters),
|
|
541
|
+
)
|
|
542
|
+
|
|
395
543
|
# Check permission - special handling for Bash with safe commands
|
|
396
544
|
needs_approval = self.registry.needs_approval(tool_call.name)
|
|
397
545
|
|
|
@@ -422,20 +570,19 @@ Working directory: {cwd}
|
|
|
422
570
|
|
|
423
571
|
result = tool.run(**tool_call.parameters)
|
|
424
572
|
|
|
425
|
-
# Track
|
|
426
|
-
if
|
|
573
|
+
# Track results for loop detection
|
|
574
|
+
if result.success:
|
|
575
|
+
self._record_success(tool_call)
|
|
576
|
+
else:
|
|
427
577
|
self._record_failure(tool_call, result.error or "Unknown error")
|
|
428
578
|
|
|
429
579
|
if HAS_CLI_UI:
|
|
430
|
-
|
|
431
|
-
if self.config.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if len(result.output) > 500:
|
|
437
|
-
output += "..."
|
|
438
|
-
print(f" {Color.DIM}{output}{Color.RESET}")
|
|
580
|
+
# Show output using Claude Code style (corner indentation)
|
|
581
|
+
if self.config.show_tool_output:
|
|
582
|
+
if result.success and result.output:
|
|
583
|
+
log_output(result.output, is_error=False, max_lines=15)
|
|
584
|
+
elif not result.success and result.error:
|
|
585
|
+
log_output(result.error, is_error=True, max_lines=10)
|
|
439
586
|
|
|
440
587
|
return result
|
|
441
588
|
|
|
@@ -444,11 +591,17 @@ Working directory: {cwd}
|
|
|
444
591
|
tool = self.registry.get(tool_call.name)
|
|
445
592
|
target = tool._get_target(**tool_call.parameters) if tool else ""
|
|
446
593
|
|
|
447
|
-
|
|
448
|
-
print(f"
|
|
594
|
+
# Claude Code style approval prompt
|
|
595
|
+
print(f"\n{Color.YELLOW}{Icons.BULLET}{Color.RESET} {Color.BOLD}{tool_call.name}{Color.RESET}({Color.CYAN}{target}{Color.RESET})")
|
|
449
596
|
|
|
450
597
|
if tool_call.parameters:
|
|
451
|
-
|
|
598
|
+
# Show parameters with corner indentation
|
|
599
|
+
params_str = json.dumps(tool_call.parameters, indent=2)
|
|
600
|
+
for i, line in enumerate(params_str.split('\n')[:5]):
|
|
601
|
+
if i == 0:
|
|
602
|
+
print(f" {Color.DIM}{Icons.CORNER}{Color.RESET} {Color.DIM}{line}{Color.RESET}")
|
|
603
|
+
else:
|
|
604
|
+
print(f" {Color.DIM}{line}{Color.RESET}")
|
|
452
605
|
|
|
453
606
|
response = input(f"\n{Color.BOLD}Allow?{Color.RESET} [y/N/always]: ").strip().lower()
|
|
454
607
|
|
nc1709/agent/permissions.py
CHANGED
|
@@ -32,7 +32,7 @@ class PermissionRule:
|
|
|
32
32
|
@dataclass
|
|
33
33
|
class PermissionConfig:
|
|
34
34
|
"""Configuration for the permissions system"""
|
|
35
|
-
policy: PermissionPolicy = PermissionPolicy.
|
|
35
|
+
policy: PermissionPolicy = PermissionPolicy.PERMISSIVE # Default to permissive for better UX
|
|
36
36
|
custom_rules: List[PermissionRule] = field(default_factory=list)
|
|
37
37
|
blocked_tools: Set[str] = field(default_factory=set)
|
|
38
38
|
session_approvals: Set[str] = field(default_factory=set)
|
|
@@ -61,7 +61,7 @@ class PermissionManager:
|
|
|
61
61
|
},
|
|
62
62
|
PermissionPolicy.PERMISSIVE: {
|
|
63
63
|
"default": ToolPermission.AUTO,
|
|
64
|
-
"ask": ["
|
|
64
|
+
"ask": ["Write", "Edit"], # Only ask for file modifications, auto-approve Bash/Task
|
|
65
65
|
},
|
|
66
66
|
PermissionPolicy.TRUST: {
|
|
67
67
|
"default": ToolPermission.AUTO,
|