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 CHANGED
@@ -5,7 +5,7 @@ Author: Lafzusa Corp
5
5
  License: Proprietary
6
6
  """
7
7
 
8
- __version__ = "1.15.4"
8
+ __version__ = "1.18.8"
9
9
  __author__ = "Lafzusa Corp"
10
10
 
11
11
  from .cli import main
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. **Complete the task**: Keep working until the task is fully complete
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": f"Tool results:\n{results_text}\n\nContinue with the task.",
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
- # Loop detection - check if this command has failed too many times
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 failures for loop detection
426
- if not result.success:
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
- 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}")
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
- 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})")
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
- print(f" Parameters: {json.dumps(tool_call.parameters, indent=2)[:200]}")
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
 
@@ -32,7 +32,7 @@ class PermissionRule:
32
32
  @dataclass
33
33
  class PermissionConfig:
34
34
  """Configuration for the permissions system"""
35
- policy: PermissionPolicy = PermissionPolicy.NORMAL
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": ["Bash", "Write", "Edit", "Task", "BackgroundBash"],
64
+ "ask": ["Write", "Edit"], # Only ask for file modifications, auto-approve Bash/Task
65
65
  },
66
66
  PermissionPolicy.TRUST: {
67
67
  "default": ToolPermission.AUTO,