code-puppy 0.0.124__py3-none-any.whl → 0.0.126__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.
@@ -590,3 +590,100 @@ def register_command_runner_tools(agent):
590
590
  - When encountering unexpected situations
591
591
  """
592
592
  return share_your_reasoning(context, reasoning, next_steps)
593
+
594
+
595
+ def register_agent_run_shell_command(agent):
596
+ """Register only the agent_run_shell_command tool."""
597
+
598
+ @agent.tool(strict=False)
599
+ def agent_run_shell_command(
600
+ context: RunContext, command: str = "", cwd: str = None, timeout: int = 60
601
+ ) -> ShellCommandOutput:
602
+ """Execute a shell command with comprehensive monitoring and safety features.
603
+
604
+ This tool provides robust shell command execution with streaming output,
605
+ timeout handling, user confirmation (when not in yolo mode), and proper
606
+ process lifecycle management. Commands are executed in a controlled
607
+ environment with cross-platform process group handling.
608
+
609
+ Args:
610
+ command: The shell command to execute. Cannot be empty or whitespace-only.
611
+ cwd: Working directory for command execution. If None,
612
+ uses the current working directory. Defaults to None.
613
+ timeout: Inactivity timeout in seconds. If no output is
614
+ produced for this duration, the process will be terminated.
615
+ Defaults to 60 seconds.
616
+
617
+ Returns:
618
+ ShellCommandOutput: A structured response containing:
619
+ - success (bool): True if command executed successfully (exit code 0)
620
+ - command (str | None): The executed command string
621
+ - error (str | None): Error message if execution failed
622
+ - stdout (str | None): Standard output from the command (last 1000 lines)
623
+ - stderr (str | None): Standard error from the command (last 1000 lines)
624
+ - exit_code (int | None): Process exit code
625
+ - execution_time (float | None): Total execution time in seconds
626
+ - timeout (bool | None): True if command was terminated due to timeout
627
+ - user_interrupted (bool | None): True if user killed the process
628
+
629
+ Examples:
630
+ >>> # Basic command execution
631
+ >>> result = agent_run_shell_command(ctx, "ls -la")
632
+ >>> print(result.stdout)
633
+
634
+ >>> # Command with working directory
635
+ >>> result = agent_run_shell_command(ctx, "npm test", "/path/to/project")
636
+ >>> if result.success:
637
+ ... print("Tests passed!")
638
+
639
+ >>> # Command with custom timeout
640
+ >>> result = agent_run_shell_command(ctx, "long_running_command", timeout=300)
641
+ >>> if result.timeout:
642
+ ... print("Command timed out")
643
+
644
+ Warning:
645
+ This tool can execute arbitrary shell commands. Exercise caution when
646
+ running untrusted commands, especially those that modify system state.
647
+ """
648
+ return run_shell_command(context, command, cwd, timeout)
649
+
650
+
651
+ def register_agent_share_your_reasoning(agent):
652
+ """Register only the agent_share_your_reasoning tool."""
653
+
654
+ @agent.tool(strict=False)
655
+ def agent_share_your_reasoning(
656
+ context: RunContext, reasoning: str = "", next_steps: str | None = None
657
+ ) -> ReasoningOutput:
658
+ """Share the agent's current reasoning and planned next steps with the user.
659
+
660
+ This tool provides transparency into the agent's decision-making process
661
+ by displaying the current reasoning and upcoming actions in a formatted,
662
+ user-friendly manner. It's essential for building trust and understanding
663
+ between the agent and user.
664
+
665
+ Args:
666
+ reasoning: The agent's current thought process, analysis, or
667
+ reasoning for the current situation. This should be clear,
668
+ comprehensive, and explain the 'why' behind decisions.
669
+ next_steps: Planned upcoming actions or steps
670
+ the agent intends to take. Can be None if no specific next steps
671
+ are determined. Defaults to None.
672
+
673
+ Returns:
674
+ ReasoningOutput: A simple response object containing:
675
+ - success (bool): Always True, indicating the reasoning was shared
676
+
677
+ Examples:
678
+ >>> reasoning = "I need to analyze the codebase structure first"
679
+ >>> next_steps = "First, I'll list the directory contents, then read key files"
680
+ >>> result = agent_share_your_reasoning(ctx, reasoning, next_steps)
681
+
682
+ Best Practice:
683
+ Use this tool frequently to maintain transparency. Call it:
684
+ - Before starting complex operations
685
+ - When changing strategy or approach
686
+ - To explain why certain decisions are being made
687
+ - When encountering unexpected situations
688
+ """
689
+ return share_your_reasoning(context, reasoning, next_steps)
@@ -347,16 +347,19 @@ def _edit_file(
347
347
  {"content": "full file contents", "overwrite": true}
348
348
  {"replacements": [ {"old_str": "foo", "new_str": "bar"}, ... ] }
349
349
  {"delete_snippet": "text to remove"}
350
+
350
351
  The function auto-detects the payload type and routes to the appropriate internal helper.
351
352
  """
353
+ # Extract file_path from payload
354
+ file_path = os.path.abspath(payload.file_path)
355
+
352
356
  # Use provided group_id or generate one if not provided
353
357
  if group_id is None:
354
- group_id = generate_group_id("edit_file", payload.file_path)
358
+ group_id = generate_group_id("edit_file", file_path)
355
359
 
356
360
  emit_info(
357
361
  "\n[bold white on blue] EDIT FILE [/bold white on blue]", message_group=group_id
358
362
  )
359
- file_path = os.path.abspath(payload.file_path)
360
363
  try:
361
364
  if isinstance(payload, DeleteSnippetPayload):
362
365
  return delete_snippet_from_file(
@@ -449,9 +452,7 @@ def register_file_modifications_tools(agent):
449
452
  """Attach file-editing tools to *agent* with mandatory diff rendering."""
450
453
 
451
454
  @agent.tool(retries=5)
452
- def edit_file(
453
- context: RunContext, payload: EditFilePayload | str = ""
454
- ) -> Dict[str, Any]:
455
+ def edit_file(context: RunContext, payload: EditFilePayload) -> Dict[str, Any]:
455
456
  """Comprehensive file editing tool supporting multiple modification strategies.
456
457
 
457
458
  This is the primary file modification tool that supports three distinct editing
@@ -477,8 +478,9 @@ def register_file_modifications_tools(agent):
477
478
  DeleteSnippetPayload:
478
479
  - delete_snippet (str): Exact text snippet to remove from file
479
480
 
480
- file_path (str): Path to the target file. Can be relative or absolute.
481
- File will be created if it doesn't exist (for ContentPayload).
481
+ file_path (str): Path to the target file. Can be relative or absolute.
482
+ File will be created if it doesn't exist (for ContentPayload).
483
+
482
484
  Returns:
483
485
  Dict[str, Any]: Operation result containing:
484
486
  - success (bool): True if operation completed successfully
@@ -498,16 +500,16 @@ def register_file_modifications_tools(agent):
498
500
  Examples:
499
501
  >>> # Create new file
500
502
  >>> payload = ContentPayload(file_path="foo.py", content="print('Hello World')")
501
- >>> result = edit_file(payload)
503
+ >>> result = edit_file(context, payload)
502
504
 
503
505
  >>> # Replace specific text
504
506
  >>> replacements = [Replacement(old_str="foo", new_str="bar")]
505
507
  >>> payload = ReplacementsPayload(file_path="foo.py", replacements=replacements)
506
- >>> result = edit_file(payload)
508
+ >>> result = edit_file(context, payload)
507
509
 
508
510
  >>> # Delete code block
509
511
  >>> payload = DeleteSnippetPayload(file_path="foo.py", delete_snippet="# TODO: remove this")
510
- >>> result = edit_file(payload)
512
+ >>> result = edit_file(context, payload)
511
513
 
512
514
  Warning:
513
515
  - Always verify file contents after modification
@@ -549,7 +551,7 @@ def register_file_modifications_tools(agent):
549
551
  return result
550
552
 
551
553
  @agent.tool(retries=5)
552
- def delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
554
+ def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
553
555
  """Safely delete files with comprehensive logging and diff generation.
554
556
 
555
557
  This tool provides safe file deletion with automatic diff generation to show
@@ -606,3 +608,166 @@ def register_file_modifications_tools(agent):
606
608
  if "diff" in result:
607
609
  del result["diff"]
608
610
  return result
611
+
612
+
613
+ def register_edit_file(agent):
614
+ """Register only the edit_file tool."""
615
+
616
+ @agent.tool(strict=False)
617
+ def edit_file(
618
+ context: RunContext,
619
+ payload: EditFilePayload | str = "",
620
+ ) -> Dict[str, Any]:
621
+ """Comprehensive file editing tool supporting multiple modification strategies.
622
+
623
+ This is the primary file modification tool that supports three distinct editing
624
+ approaches: full content replacement, targeted text replacements, and snippet
625
+ deletion. It provides robust diff generation, error handling, and automatic
626
+ retry capabilities for reliable file operations.
627
+
628
+ Args:
629
+ context (RunContext): The PydanticAI runtime context for the agent.
630
+ payload: One of three payload types:
631
+
632
+ ContentPayload:
633
+ - content (str): Full file content to write
634
+ - overwrite (bool, optional): Whether to overwrite existing files.
635
+ Defaults to False (safe mode).
636
+
637
+ ReplacementsPayload:
638
+ - replacements (List[Replacement]): List of text replacements where
639
+ each Replacement contains:
640
+ - old_str (str): Exact text to find and replace
641
+ - new_str (str): Replacement text
642
+
643
+ DeleteSnippetPayload:
644
+ - delete_snippet (str): Exact text snippet to remove from file
645
+
646
+ Returns:
647
+ Dict[str, Any]: Operation result containing:
648
+ - success (bool): True if operation completed successfully
649
+ - path (str): Absolute path to the modified file
650
+ - message (str): Human-readable description of changes
651
+ - changed (bool): True if file content was actually modified
652
+ - diff (str, optional): Unified diff showing changes made
653
+ - error (str, optional): Error message if operation failed
654
+
655
+ Examples:
656
+ >>> # Create new file with content
657
+ >>> payload = {"file_path": "hello.py", "content": "print('Hello!')"}
658
+ >>> result = edit_file(ctx, payload)
659
+
660
+ >>> # Replace text in existing file
661
+ >>> payload = {
662
+ ... "file_path": "config.py",
663
+ ... "replacements": [
664
+ ... {"old_str": "debug = False", "new_str": "debug = True"}
665
+ ... ]
666
+ ... }
667
+ >>> result = edit_file(ctx, payload)
668
+
669
+ >>> # Delete snippet from file
670
+ >>> payload = {
671
+ ... "file_path": "main.py",
672
+ ... "delete_snippet": "# TODO: remove this comment"
673
+ ... }
674
+ >>> result = edit_file(ctx, payload)
675
+
676
+ Best Practices:
677
+ - Use replacements for targeted changes (most efficient)
678
+ - Use content payload only for new files or complete rewrites
679
+ - Always check the 'success' field before assuming changes worked
680
+ - Review the 'diff' field to understand what changed
681
+ - Use delete_snippet for removing specific code blocks
682
+ """
683
+ # Handle string payload parsing (for models that send JSON strings)
684
+ if isinstance(payload, str):
685
+ # Fallback for weird models that just can't help but send json strings...
686
+ payload = json.loads(json_repair.repair_json(payload))
687
+ if "replacements" in payload and "file_path" in payload:
688
+ payload = ReplacementsPayload(**payload)
689
+ elif "delete_snippet" in payload and "file_path" in payload:
690
+ payload = DeleteSnippetPayload(**payload)
691
+ elif "content" in payload and "file_path" in payload:
692
+ payload = ContentPayload(**payload)
693
+ else:
694
+ file_path = "Unknown"
695
+ if "file_path" in payload:
696
+ file_path = payload["file_path"]
697
+ # Diagnose what's missing
698
+ missing = []
699
+ if "file_path" not in payload:
700
+ missing.append("file_path")
701
+
702
+ payload_type = "unknown"
703
+ if "content" in payload:
704
+ payload_type = "content"
705
+ elif "replacements" in payload:
706
+ payload_type = "replacements"
707
+ elif "delete_snippet" in payload:
708
+ payload_type = "delete_snippet"
709
+ else:
710
+ missing.append("content/replacements/delete_snippet")
711
+
712
+ missing_str = ", ".join(missing) if missing else "none"
713
+ return {
714
+ "success": False,
715
+ "path": file_path,
716
+ "message": f"Invalid payload for {payload_type} operation. Missing required fields: {missing_str}. Payload keys: {list(payload.keys())}",
717
+ "changed": False,
718
+ }
719
+
720
+ # Call _edit_file which will extract file_path from payload and handle group_id generation
721
+ result = _edit_file(context, payload)
722
+ if "diff" in result:
723
+ del result["diff"]
724
+ return result
725
+
726
+
727
+ def register_delete_file(agent):
728
+ """Register only the delete_file tool."""
729
+
730
+ @agent.tool(strict=False)
731
+ def delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
732
+ """Safely delete files with comprehensive logging and diff generation.
733
+
734
+ This tool provides safe file deletion with automatic diff generation to show
735
+ exactly what content was removed. It includes proper error handling and
736
+ automatic retry capabilities for reliable operation.
737
+
738
+ Args:
739
+ context (RunContext): The PydanticAI runtime context for the agent.
740
+ file_path (str): Path to the file to delete. Can be relative or absolute.
741
+ Must be an existing regular file (not a directory).
742
+
743
+ Returns:
744
+ Dict[str, Any]: Operation result containing:
745
+ - success (bool): True if file was successfully deleted
746
+ - path (str): Absolute path to the deleted file
747
+ - message (str): Human-readable description of the operation
748
+ - changed (bool): True if file was actually removed
749
+ - error (str, optional): Error message if deletion failed
750
+
751
+ Examples:
752
+ >>> # Delete a specific file
753
+ >>> result = delete_file(ctx, "temp_file.txt")
754
+ >>> if result['success']:
755
+ ... print(f"Deleted: {result['path']}")
756
+
757
+ >>> # Handle deletion errors
758
+ >>> result = delete_file(ctx, "missing.txt")
759
+ >>> if not result['success']:
760
+ ... print(f"Error: {result.get('error', 'Unknown error')}")
761
+
762
+ Best Practices:
763
+ - Always verify file exists before attempting deletion
764
+ - Check 'success' field to confirm operation completed
765
+ - Use list_files first to confirm file paths
766
+ - Cannot delete directories (use shell commands for that)
767
+ """
768
+ # Generate group_id for delete_file tool execution
769
+ group_id = generate_group_id("delete_file", file_path)
770
+ result = _delete_file(context, file_path, message_group=group_id)
771
+ if "diff" in result:
772
+ del result["diff"]
773
+ return result
@@ -173,13 +173,14 @@ def _list_files(
173
173
  if rel_path == ".":
174
174
  rel_path = ""
175
175
  if rel_path:
176
- os.path.join(directory, rel_path)
176
+ dir_path = os.path.join(directory, rel_path)
177
177
  results.append(
178
178
  ListedFile(
179
179
  **{
180
180
  "path": rel_path,
181
181
  "type": "directory",
182
182
  "size": 0,
183
+ "full_path": dir_path,
183
184
  "depth": depth,
184
185
  }
185
186
  )
@@ -187,6 +188,7 @@ def _list_files(
187
188
  folder_structure[rel_path] = {
188
189
  "path": rel_path,
189
190
  "depth": depth,
191
+ "full_path": dir_path,
190
192
  }
191
193
  for file in files:
192
194
  file_path = os.path.join(root, file)
@@ -199,6 +201,7 @@ def _list_files(
199
201
  "path": rel_file_path,
200
202
  "type": "file",
201
203
  "size": size,
204
+ "full_path": file_path,
202
205
  "depth": depth,
203
206
  }
204
207
  results.append(ListedFile(**file_info))
@@ -621,3 +624,170 @@ def register_file_operations_tools(agent):
621
624
  - For case-insensitive search, try multiple variants manually
622
625
  """
623
626
  return _grep(context, search_string, directory)
627
+
628
+
629
+ def register_list_files(agent):
630
+ """Register only the list_files tool."""
631
+
632
+ @agent.tool(strict=False)
633
+ def list_files(
634
+ context: RunContext, directory: str = ".", recursive: bool = True
635
+ ) -> ListFileOutput:
636
+ """List files and directories with intelligent filtering and safety features.
637
+
638
+ This tool provides comprehensive directory listing with smart home directory
639
+ detection, project-aware recursion, and token-safe output. It automatically
640
+ ignores common build artifacts, cache directories, and other noise while
641
+ providing rich file metadata and visual formatting.
642
+
643
+ Args:
644
+ context (RunContext): The PydanticAI runtime context for the agent.
645
+ directory (str, optional): Path to the directory to list. Can be relative
646
+ or absolute. Defaults to "." (current directory).
647
+ recursive (bool, optional): Whether to recursively list subdirectories.
648
+ Automatically disabled for home directories unless they contain
649
+ project indicators. Defaults to True.
650
+
651
+ Returns:
652
+ ListFileOutput: A structured response containing:
653
+ - files (List[ListedFile]): List of files and directories found, where
654
+ each ListedFile contains:
655
+ - path (str | None): Relative path from the listing directory
656
+ - type (str | None): "file" or "directory"
657
+ - size (int): File size in bytes (0 for directories)
658
+ - full_path (str | None): Absolute path to the item
659
+ - depth (int | None): Nesting depth from the root directory
660
+ - error (str | None): Error message if listing failed
661
+
662
+ Examples:
663
+ >>> # List current directory
664
+ >>> result = list_files(ctx)
665
+ >>> for file in result.files:
666
+ ... print(f"{file.type}: {file.path} ({file.size} bytes)")
667
+
668
+ >>> # List specific directory non-recursively
669
+ >>> result = list_files(ctx, "/path/to/project", recursive=False)
670
+ >>> print(f"Found {len(result.files)} items")
671
+
672
+ >>> # Handle potential errors
673
+ >>> result = list_files(ctx, "/nonexistent/path")
674
+ >>> if result.error:
675
+ ... print(f"Error: {result.error}")
676
+
677
+ Best Practices:
678
+ - Always use this before reading/modifying files
679
+ - Use non-recursive for quick directory overviews
680
+ - Check for errors in the response
681
+ - Combine with grep to find specific file patterns
682
+ """
683
+ return _list_files(context, directory, recursive)
684
+
685
+
686
+ def register_read_file(agent):
687
+ """Register only the read_file tool."""
688
+
689
+ @agent.tool(strict=False)
690
+ def read_file(
691
+ context: RunContext,
692
+ file_path: str = "",
693
+ start_line: int | None = None,
694
+ num_lines: int | None = None,
695
+ ) -> ReadFileOutput:
696
+ """Read file contents with optional line-range selection and token safety.
697
+
698
+ This tool provides safe file reading with automatic token counting and
699
+ optional line-range selection for handling large files efficiently.
700
+ It protects against reading excessively large files that could overwhelm
701
+ the agent's context window.
702
+
703
+ Args:
704
+ context (RunContext): The PydanticAI runtime context for the agent.
705
+ file_path (str): Path to the file to read. Can be relative or absolute.
706
+ Cannot be empty.
707
+ start_line (int | None, optional): Starting line number for partial reads
708
+ (1-based indexing). If specified, num_lines must also be provided.
709
+ Defaults to None (read entire file).
710
+ num_lines (int | None, optional): Number of lines to read starting from
711
+ start_line. Must be specified if start_line is provided.
712
+ Defaults to None (read to end of file).
713
+
714
+ Returns:
715
+ ReadFileOutput: A structured response containing:
716
+ - content (str | None): The file contents or error message
717
+ - num_tokens (int): Estimated token count (constrained to < 10,000)
718
+ - error (str | None): Error message if reading failed
719
+
720
+ Examples:
721
+ >>> # Read entire file
722
+ >>> result = read_file(ctx, "example.py")
723
+ >>> print(f"Read {result.num_tokens} tokens")
724
+ >>> print(result.content)
725
+
726
+ >>> # Read specific line range
727
+ >>> result = read_file(ctx, "large_file.py", start_line=10, num_lines=20)
728
+ >>> print("Lines 10-29:", result.content)
729
+
730
+ >>> # Handle errors
731
+ >>> result = read_file(ctx, "missing.txt")
732
+ >>> if result.error:
733
+ ... print(f"Error: {result.error}")
734
+
735
+ Best Practices:
736
+ - Always check for errors before using content
737
+ - Use line ranges for large files to avoid token limits
738
+ - Monitor num_tokens to stay within context limits
739
+ - Combine with list_files to find files first
740
+ """
741
+ return _read_file(context, file_path, start_line, num_lines)
742
+
743
+
744
+ def register_grep(agent):
745
+ """Register only the grep tool."""
746
+
747
+ @agent.tool(strict=False)
748
+ def grep(
749
+ context: RunContext, search_string: str = "", directory: str = "."
750
+ ) -> GrepOutput:
751
+ """Recursively search for text patterns across files with intelligent filtering.
752
+
753
+ This tool provides powerful text searching across directory trees with
754
+ automatic filtering of irrelevant files, binary detection, and match limiting
755
+ for performance. It's essential for code exploration and finding specific
756
+ patterns or references.
757
+
758
+ Args:
759
+ context (RunContext): The PydanticAI runtime context for the agent.
760
+ search_string (str): The text pattern to search for. Performs exact
761
+ string matching (not regex). Cannot be empty.
762
+ directory (str, optional): Root directory to start the recursive search.
763
+ Can be relative or absolute. Defaults to "." (current directory).
764
+
765
+ Returns:
766
+ GrepOutput: A structured response containing:
767
+ - matches (List[MatchInfo]): List of matches found, where each
768
+ MatchInfo contains:
769
+ - file_path (str | None): Absolute path to the file containing the match
770
+ - line_number (int | None): Line number where match was found (1-based)
771
+ - line_content (str | None): Full line content containing the match
772
+
773
+ Examples:
774
+ >>> # Search for function definitions
775
+ >>> result = grep(ctx, "def my_function")
776
+ >>> for match in result.matches:
777
+ ... print(f"{match.file_path}:{match.line_number}: {match.line_content}")
778
+
779
+ >>> # Search in specific directory
780
+ >>> result = grep(ctx, "TODO", "/path/to/project/src")
781
+ >>> print(f"Found {len(result.matches)} TODO items")
782
+
783
+ >>> # Search for imports
784
+ >>> result = grep(ctx, "import pandas")
785
+ >>> files_using_pandas = {match.file_path for match in result.matches}
786
+
787
+ Best Practices:
788
+ - Use specific search terms to avoid too many results
789
+ - Search is case-sensitive; try variations if needed
790
+ - Combine with read_file to examine matches in detail
791
+ - For case-insensitive search, try multiple variants manually
792
+ """
793
+ return _grep(context, search_string, directory)