├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── agent-aider-worktree ├── aider_multi_agent ├── bin └── agent ├── c ├── demos └── dopamine_demo.py ├── info.md ├── project_agent ├── prompt_cycle ├── setup.py ├── spec.md ├── src ├── __init__.py ├── agent │ ├── core.py │ ├── execution.py │ ├── plan.py │ ├── repository.py │ └── task.py ├── agent_main.py ├── config.py ├── interface │ ├── __init__.py │ ├── actions.py │ ├── chat.py │ ├── cli.py │ ├── commands.py │ ├── display.py │ ├── input.py │ ├── input_handler.py │ ├── interface.py │ └── vim_input.py ├── run_agent.py ├── spec.md ├── task_list.md └── utils │ ├── __init__.py │ ├── feedback.py │ ├── file_ops.py │ ├── helpers.py │ ├── input_schema.py │ ├── shell.py │ ├── web_search.py │ ├── xml_operations.py │ ├── xml_schema.py │ └── xml_tools.py ├── start_agent ├── tests ├── conftest.py ├── test_commands.py ├── test_core.py ├── test_display.py ├── test_dummy.py ├── test_feedback.py ├── test_file_ops.py ├── test_interface.py ├── test_task.py ├── test_web_search.py ├── test_xml_schema.py └── test_xml_tools.py ├── wallpapercycle └── watch-git-diff /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | .aider* 173 | 174 | context.txt 175 | custom_aider_history.md 176 | aider_history.md -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | # Load all checks except those we disable below 3 | disable= 4 | 5 | [MESSAGES CONTROL] 6 | # Disable these specific checks 7 | disable= 8 | missing-docstring, 9 | too-few-public-methods, 10 | too-many-arguments, 11 | too-many-locals, 12 | too-many-instance-attributes, 13 | too-many-public-methods, 14 | too-many-branches, 15 | too-many-statements, 16 | duplicate-code, 17 | protected-access, 18 | redefined-builtin, 19 | broad-except, 20 | fixme, 21 | invalid-name, 22 | line-too-long, 23 | import-error, 24 | wrong-import-position, 25 | unnecessary-dunder-call, 26 | consider-using-f-string, 27 | unspecified-encoding, 28 | unnecessary-lambda-assignment, 29 | use-dict-literal, 30 | use-list-literal 31 | 32 | [REPORTS] 33 | # Output configuration 34 | output-format=colorized 35 | reports=no 36 | 37 | [FORMAT] 38 | # Formatting options 39 | max-line-length=120 40 | indent-string=' ' 41 | single-line-if-stmt=no 42 | ignore-long-lines=^\s*(# )??$ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tom Dörr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Path Scripts 2 | 3 | A collection of powerful CLI tools for automating development workflows and task management. These scripts are designed to be added to your system PATH for easy command-line access. 4 | 5 | Repository: https://github.com/tom-doerr/path_scripts 6 | 7 | ## Tools Overview 8 | 9 | ### 1. Agent Aider Worktree 10 | 11 | Automates code improvements using AI agents in isolated git worktrees. 12 | 13 | #### Key Features 14 | - Creates isolated git worktrees for safe experimentation 15 | - Runs AI-powered code analysis and improvements 16 | - Automatically merges changes back to main branch 17 | - Handles merge conflicts intelligently 18 | - Test-driven development workflow 19 | - Exponential retry strategy for complex tasks 20 | 21 | #### Installation 22 | ```bash 23 | # Clone and install 24 | git clone https://github.com/tom-doerr/path_scripts.git 25 | cd path_scripts 26 | pip install -r requirements.txt 27 | 28 | # Add to PATH (optional) 29 | ln -s $PWD/agent-aider-worktree ~/.local/bin/ 30 | ``` 31 | 32 | #### Usage 33 | ```bash 34 | # Basic usage 35 | agent-aider-worktree "Add user authentication" 36 | 37 | # With custom iterations and model 38 | agent-aider-worktree --max-iterations 20 --model claude-3-opus "Refactor database code" 39 | 40 | # From a subdirectory 41 | agent-aider-worktree --exponential-retries "Fix login form validation" 42 | ``` 43 | 44 | #### Command Line Options 45 | | Option | Description | Default | 46 | |--------|-------------|---------| 47 | | `--path` | Repository path | Current directory | 48 | | `--model` | AI model to use | deepseek-reasoner | 49 | | `--weak-model` | Secondary model | deepseek | 50 | | `--max-iterations` | Maximum iterations | 10 | 51 | | `--inner-loop` | Inner loop iterations | 10 | 52 | | `--exponential-retries` | Use exponential retry strategy | False | 53 | | `--no-push` | Skip pushing changes | False | 54 | | `--read` | Additional files to analyze | [] | 55 | 56 | ### 2. Aider Multi-Agent System 57 | 58 | Runs multiple AI agents in parallel for distributed code analysis. 59 | 60 | #### Key Features 61 | - Parallel agent execution in tmux sessions 62 | - Configurable models and iterations 63 | - Session persistence and recovery 64 | - Centralized management interface 65 | 66 | #### Usage 67 | ```bash 68 | # Start 3 parallel agents 69 | aider_multi_agent -n 3 70 | 71 | # Use specific model 72 | aider_multi_agent -m claude-3-opus -n 2 73 | 74 | # Kill all sessions 75 | aider_multi_agent -k 76 | ``` 77 | 78 | #### Options 79 | | Option | Description | Default | 80 | |--------|-------------|---------| 81 | | `-n SESSIONS` | Number of agents | 1 | 82 | | `-m MODEL` | AI model | r1 | 83 | | `-w WEAK_MODEL` | Secondary model | gemini-2.0-flash-001 | 84 | | `-i ITERATIONS` | Max iterations | 1000 | 85 | | `-k` | Kill all sessions | - | 86 | 87 | ### 3. Task Management (c) 88 | 89 | Streamlined interface for Taskwarrior with smart filtering. 90 | 91 | #### Installation 92 | ```bash 93 | # Install to PATH 94 | cp c ~/.local/bin/ 95 | chmod +x ~/.local/bin/c 96 | 97 | # Configure task script path 98 | export TASK_SCRIPT_PATH="/path/to/show_tw_tasks.py" 99 | ``` 100 | 101 | #### Usage 102 | ```bash 103 | # Show pending tasks 104 | c 105 | 106 | # Filter by tag and report 107 | c "+work" "active" 108 | 109 | # Custom filters 110 | c -n "+project:home" "next" 111 | ``` 112 | 113 | ## Dependencies 114 | 115 | - Python 3.8+ 116 | - Git 2.25+ 117 | - tmux 118 | - Taskwarrior (for task management) 119 | - Required Python packages in requirements.txt 120 | 121 | ## Contributing 122 | 123 | 1. Fork the repository 124 | 2. Create a feature branch 125 | 3. Submit a pull request 126 | 127 | ## License 128 | 129 | MIT License - See LICENSE file for details 130 | -------------------------------------------------------------------------------- /aider_multi_agent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define prompts 4 | PROMPTS=( 5 | "please look at context.txt. are there issues? if so what are they? where do they occur and why do they occur? please go through each issue and update the report.md and make sure we have for each issue documented why it occurs, how we can fix it or what pytest tests we could add to find out why it occurs" 6 | "please go through each line in the spec and check if the code fulfills the requirement and why it does or does not fulfill it. document it in detail in the report.md" 7 | "are there features in the codebase that are not part of the specs in spec.md? what are they? can we remove them? should we remove them? how much additional code do they cause? do they add a lot of complexity? are they really useful and would be appreciated? please document them in report.md" 8 | "consider the current code architecture. could it be restructured someway? what are alternatives? what are alternatives that would be easy to implement? can we restructure it in a way that would reduce total lines of code? can we restructure it in a way that reduces complexity or makes it easier to work with in general? those do not need to be large things, even tiny changes can have an impact. please add report.md with your findings" 9 | "please look through the content in report.md. what are the highest priority items in there? can you identify dependencies between tasks? would implementing some of those tasks make other tasks easier? please update the report with a list of tasks in a markdown table with each task containing task description, task-id, priority(1-9), the tasks it depends on, the tasks that depend on it (just enter the task ids for those two in the table). do not make the task list ordered since reprioritizing tasks is harder if reordering is required as well." 10 | "please go through report.md and refactor it. are there items that should be grouped together? should some sections be restructered?" 11 | "please go through report.md and check if there are duplicate section or if there is duplicate content. please remove any duplication" 12 | "please go through report.md and create a plan for how we can takle the highest priority items. please update the report.md with your plan, do not implement it just yet" 13 | "please go through report.md and work on the highest priority tasks" 14 | "please go through report.md and check if the plan was successfully implemented. remove the plan if it was completed and mark the tasks as done. if there are issues with the implementation update the report.md accordingly" 15 | ) 16 | 17 | show_help() { 18 | echo "Usage: aider_multi_agent [options]" 19 | echo 20 | echo "Options:" 21 | echo " -h, --help Show this help message and exit" 22 | echo " -i ITERATIONS Number of iterations to run (default: 1000)" 23 | echo " -m MODEL Model to use (default: r1)" 24 | echo " -w WEAK_MODEL Weak model to use (default: gemini-2.0-flash-001)" 25 | echo " -n SESSIONS Number of tmux sessions to create (default: 1)" 26 | echo " -k Kill all running aider_multi_agent tmux sessions" 27 | echo 28 | echo "This script runs multiple aider agents through a series of prompts to analyze and improve code." 29 | echo 30 | echo "To start multiple sessions:" 31 | echo " aider_multi_agent -n 3 # Starts 3 vertical splits" 32 | echo 33 | echo "To stop all sessions:" 34 | echo " aider_multi_agent -k" 35 | } 36 | 37 | # Main script 38 | if [[ "$1" == "-h" || "$1" == "--help" ]]; then 39 | show_help 40 | exit 0 41 | fi 42 | 43 | # Parse arguments 44 | ITERATIONS=1000 45 | MODEL="r1" 46 | WEAK_MODEL="gemini-2.0-flash-001" 47 | SESSIONS=1 48 | KILL_MODE=false 49 | 50 | while getopts "i:m:w:n:kh" opt; do 51 | case $opt in 52 | i) ITERATIONS=$OPTARG ;; 53 | m) MODEL=$OPTARG ;; 54 | w) WEAK_MODEL=$OPTARG ;; 55 | n) SESSIONS=$OPTARG ;; 56 | k) KILL_MODE=true ;; 57 | h) show_help; exit 0 ;; 58 | *) show_help; exit 1 ;; 59 | esac 60 | done 61 | 62 | # Kill all sessions if -k flag is set 63 | if $KILL_MODE; then 64 | echo "Killing all aider_multi_agent sessions..." 65 | tmux list-sessions -F '#{session_name}' | grep '^aider_multi_agent_' | while read -r session; do 66 | tmux kill-session -t "$session" 67 | done 68 | exit 0 69 | fi 70 | 71 | # Create tmux session 72 | SESSION_NAME="aider_multi_agent_$(date +%s)" 73 | CUSTOM_HISTORY_FILE=custom_aider_history.md 74 | 75 | # Create new tmux session with horizontal splits 76 | if [ "$SESSIONS" -gt 1 ]; then 77 | echo "Creating $SESSIONS tmux sessions..." 78 | # Get current working directory 79 | CURRENT_DIR=$(pwd) 80 | 81 | # Create initial session with first window 82 | tmux new-session -d -s "$SESSION_NAME" -n "Session 1" "cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL" 83 | 84 | # Create additional windows 85 | for ((s=2; s<=SESSIONS; s++)); do 86 | # Create new window 87 | tmux new-window -t "$SESSION_NAME" -n "Session $s" "cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL" 88 | done 89 | 90 | # Split each window horizontally after brief delay 91 | for ((s=1; s<=SESSIONS; s++)); do 92 | tmux split-window -h -t "$SESSION_NAME:$s" "sleep 0.1; cd '$CURRENT_DIR' && ./$0 -i $ITERATIONS -m $MODEL -w $WEAK_MODEL" 93 | tmux select-layout -t "$SESSION_NAME:$s" even-horizontal 94 | sleep 0.1 # Allow time for window creation 95 | done 96 | 97 | # Select first window and attach 98 | tmux select-window -t "$SESSION_NAME:1" 99 | tmux attach-session -t "$SESSION_NAME" 100 | exit 0 101 | fi 102 | 103 | # Normal execution for single session 104 | for ((i=1; i<=ITERATIONS; i++)); do 105 | echo "Iteration $i"' ========================= '$(date)' ============================' 106 | 107 | for prompt in "${PROMPTS[@]}"; do 108 | rm -f $CUSTOM_HISTORY_FILE 109 | for run in {1..3}; do 110 | aider --yes-always \ 111 | --read "${PWD}/plex.md" \ 112 | --read "${HOME}/git/dotfiles/instruction.md" \ 113 | --read "${PWD}/spec.md" \ 114 | --read "${PWD}/context.txt" \ 115 | --edit-format diff \ 116 | --model "openrouter/deepseek/deepseek-r1" \ 117 | --no-show-model-warnings \ 118 | --weak-model $WEAK_MODEL \ 119 | --architect \ 120 | --chat-history-file $CUSTOM_HISTORY_FILE \ 121 | --restore-chat-history \ 122 | report.md **/*py \ 123 | --message "$prompt" 124 | done 125 | done 126 | 127 | sleep 1 128 | done 129 | -------------------------------------------------------------------------------- /bin/agent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from src.interface.cli import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /c: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Default filters as single string without extra quoting 4 | DEFAULT_FILTERS="+PENDING -bu" 5 | 6 | # Handle arguments 7 | if [[ $# -eq 0 ]]; then 8 | echo "Usage: c [OPTIONS] [FILTERS...] [REPORT_NAME]" 9 | echo "Options:" 10 | echo " -n, --no_default_filters Disable default filters (+PENDING -bu)" 11 | exit 1 12 | fi 13 | 14 | # Check for no-default-filters flag 15 | NO_DEFAULTS=false 16 | if [[ "$1" == "-n" || "$1" == "--no_default_filters" ]]; then 17 | NO_DEFAULTS=true 18 | shift 19 | fi 20 | 21 | # Build arguments 22 | ARGS=("--once") 23 | if ! $NO_DEFAULTS; then 24 | ARGS+=("$DEFAULT_FILTERS") 25 | fi 26 | 27 | # Combine remaining arguments into single string and add to ARGS 28 | USER_ARGS="$*" 29 | ARGS+=("$USER_ARGS") 30 | 31 | # Pass all arguments as individual quoted elements 32 | "/home/tom/git/scripts/show_tw_tasks.py" "${ARGS[@]}" 33 | -------------------------------------------------------------------------------- /demos/dopamine_demo.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | import sys 3 | import os 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | from src.utils.feedback import DopamineReward 6 | 7 | def demo_dopamine_optimization(): 8 | """Demo dopamine-based prompt optimization""" 9 | console = Console() 10 | reward = DopamineReward(console) 11 | 12 | # Initial state 13 | console.print(f"\nStarting dopamine level: {reward.dopamine_level:.1f}") 14 | 15 | # Simulate positive interaction 16 | console.print("\n[bold]Test 1: Positive feedback[/bold]") 17 | feedback = reward.reward_for_xml_response("", "Perfect! Exactly what I needed!") 18 | console.print(f"Reward: {feedback}") 19 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}") 20 | 21 | # Simulate negative interaction 22 | console.print("\n[bold]Test 2: Negative feedback[/bold]") 23 | feedback = reward.reward_for_xml_response("", "Wrong! This is completely incorrect.") 24 | console.print(f"Reward: {feedback}") 25 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}") 26 | 27 | # Simulate mixed interaction 28 | console.print("\n[bold]Test 3: Mixed feedback[/bold]") 29 | feedback = reward.reward_for_xml_response("", "Partially correct but needs improvement") 30 | console.print(f"Reward: {feedback}") 31 | console.print(f"New dopamine level: {reward.dopamine_level:.1f}") 32 | 33 | if __name__ == "__main__": 34 | demo_dopamine_optimization() 35 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | import os 2 | from camel.agents import ChatAgent 3 | from camel.memory import ChatHistoryMemory 4 | from camel.messages import BaseMessage 5 | from camel.models import ModelFactory 6 | 7 | # Step 1: Define a function to read files 8 | def read_file(file_path): 9 | if not os.path.exists(file_path): 10 | raise FileNotFoundError(f"File {file_path} not found") 11 | with open(file_path, 'r') as file: 12 | return file.read() 13 | 14 | # Step 2: Initialize the CAMEL agent with memory 15 | def create_spec_review_agent(): 16 | # Configure the memory to retain context 17 | memory = ChatHistoryMemory(window_size=10) # Retains last 10 messages 18 | 19 | # Use OpenAI's GPT-4 as the model (replace with your API key) 20 | model = ModelFactory.create(model_type="openai", model_config={"api_key": "your_openai_api_key"}) 21 | 22 | # Initialize the ChatAgent 23 | agent = ChatAgent(model=model, memory=memory) 24 | return agent 25 | 26 | # Step 3: Function to review specs and code 27 | def review_specs_and_code(agent, spec_file_path, code_file_path): 28 | # Read the spec and code files 29 | spec_content = read_file(spec_file_path) 30 | code_content = read_file(code_file_path) 31 | 32 | # Create an instruction message for the agent 33 | instruction = ( 34 | "You are a code review assistant. Your task is to:\n" 35 | "1. Review the specifications in the provided spec.md file.\n" 36 | "2. Analyze the code in the provided main.py file.\n" 37 | "3. Identify and list any specifications that are violated by the code.\n" 38 | "Return the result in the format: 'Violations: [list of violated specs]'.\n\n" 39 | f"Here is the spec content:\n{spec_content}\n\n" 40 | f"Here is the code content:\n{code_content}" 41 | ) 42 | 43 | # Create a user message 44 | user_msg = BaseMessage.make_user_message(role_name="User", content=instruction) 45 | 46 | # Record the message to memory 47 | agent.record_message(user_msg) 48 | 49 | # Generate a response based on the memory context 50 | response = agent.step() 51 | 52 | # Extract the agent's response content 53 | return response.content 54 | 55 | # Step 4: Main execution 56 | def main(): 57 | # File paths (adjust these to your actual file locations) 58 | spec_file_path = "spec.md" 59 | code_file_path = "main.py" 60 | 61 | # Create the agent 62 | agent = create_spec_review_agent() 63 | 64 | # Review specs and code 65 | result = review_specs_and_code(agent, spec_file_path, code_file_path) 66 | 67 | # Print the result 68 | print("Agent's Review Result:") 69 | print(result) 70 | 71 | # Optionally, retrieve the memory context for debugging 72 | context = agent.memory.get_context() 73 | print("\nMemory Context:") 74 | print(context) 75 | 76 | # Example spec.md content (save this as spec.md) 77 | """ 78 | # Project Specifications 79 | 1. The function `add_numbers` must take two parameters: `a` and `b`. 80 | 2. The function `add_numbers` must return the sum of `a` and `b`. 81 | 3. The code must include a function called `greet_user` that prints "Hello, User!". 82 | """ 83 | 84 | # Example main.py content (save this as main.py) 85 | """ 86 | def add_numbers(a, b, c): 87 | return a + b + c 88 | 89 | def say_hello(): 90 | print("Hi there!") 91 | """ 92 | 93 | if __name__ == "__main__": 94 | main() 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | from camel.agents import ChatAgent 112 | from camel.memory import LongtermAgentMemory, ScoreBasedContextCreator 113 | from camel.storage import InMemoryKeyValueStorage, QdrantStorage 114 | 115 | # Step 1: Initialize storage backends 116 | chat_history_storage = InMemoryKeyValueStorage() 117 | vector_db_storage = QdrantStorage(path="./vector_db", prefer_grpc=True) 118 | 119 | # Step 2: Initialize memory with context creator 120 | memory = LongtermAgentMemory( 121 | context_creator=ScoreBasedContextCreator(), 122 | chat_history_block=chat_history_storage, 123 | vector_db_block=vector_db_storage, 124 | retrieve_limit=3 125 | ) 126 | 127 | # Step 3: Initialize the agent with memory 128 | agent = ChatAgent(model="gpt-4", memory=memory) 129 | 130 | # Step 4: Retrieve memory context (using retrieve() for LongtermAgentMemory) 131 | context = agent.memory.retrieve() 132 | print("Combined memory context:", context) 133 | 134 | # Step 5: Record a new message 135 | new_user_msg = BaseMessage.make_user_message(role_name="User", content="What's the weather like today?") 136 | agent.record_message(new_user_msg) 137 | 138 | # Step 6: Retrieve updated context 139 | updated_context = agent.memory.retrieve() 140 | print("Updated combined memory context:", updated_context) 141 | -------------------------------------------------------------------------------- /project_agent: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Unified project maintenance agent 4 | # Combines task list management, reporting, and development tasks 5 | 6 | typeset -A CONFIG=( 7 | MODEL "r1" 8 | MAX_ITERATIONS 1000 9 | BASE_SLEEP 1 10 | TASK_SLEEP 60 11 | REPORT_SLEEP 60 12 | MODE "all" 13 | EDIT_FORMAT "diff" 14 | INSTANCES 1 15 | ) 16 | 17 | # Define a function to build AIDER_ARGS after parsing arguments 18 | build_aider_args() { 19 | AIDER_ARGS=( 20 | --read spec.md 21 | --read issues.txt 22 | --model ${CONFIG[MODEL]} 23 | --no-auto-lint 24 | --no-auto-test 25 | --edit-format ${CONFIG[EDIT_FORMAT]} 26 | --no-browser 27 | --no-suggest-shell-commands 28 | --no-detect-urls 29 | --subtree-only 30 | --architect 31 | --yes-always 32 | ) 33 | } 34 | 35 | update_task_list() { 36 | echo "\n=== Updating Task List ===" 37 | local worker_msg="" 38 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}." 39 | 40 | aider ${AIDER_ARGS} task_list.md --message \ 41 | "Review project and update task_list.md with prioritized, non-duplicated tasks.$worker_msg" 42 | sleep ${CONFIG[TASK_SLEEP]} 43 | } 44 | 45 | update_project_report() { 46 | echo "\n=== Updating Project Report ===" 47 | local worker_msg="" 48 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}." 49 | 50 | aider ${AIDER_ARGS} project_report.md --message \ 51 | "Update project_report.md with current status, critical path, architecture issues, simplification opportunities. No long-term planning. Ensure accuracy and no duplication.$worker_msg" 52 | sleep ${CONFIG[REPORT_SLEEP]} 53 | } 54 | 55 | handle_development_tasks() { 56 | echo "\n=== Handling Development Tasks ===" 57 | local worker_msg="" 58 | [[ -n "${CONFIG[WORKER_ID]}" ]] && worker_msg=" You are worker ${CONFIG[WORKER_ID]}." 59 | 60 | aider ${AIDER_ARGS} --auto-test --message \ 61 | "Review spec.md and issues.txt and work on the highest priority items. Only work on pylint issues if the code is rated below 9. If you don't work on linting, go through spec.md. Quote each requirement and tell me if the code fulfills it. If it doesn't fulfill it, create one or more edit blocks to fix the issue before moving on to the next spec requirement. $worker_msg" 62 | sleep ${CONFIG[BASE_SLEEP]} 63 | } 64 | 65 | parse_arguments() { 66 | while [[ $# -gt 0 ]]; do 67 | case $1 in 68 | --tasks) 69 | CONFIG[MODE]="tasks" 70 | ;; 71 | --report) 72 | CONFIG[MODE]="report" 73 | ;; 74 | --dev) 75 | CONFIG[MODE]="dev" 76 | ;; 77 | --all) 78 | CONFIG[MODE]="all" 79 | ;; 80 | -i|--iterations) 81 | CONFIG[MAX_ITERATIONS]=$2 82 | shift 83 | ;; 84 | --edit-format) 85 | CONFIG[EDIT_FORMAT]="$2" 86 | shift 87 | ;; 88 | -n|--instances) 89 | CONFIG[INSTANCES]=$2 90 | shift 91 | ;; 92 | --worker-id) 93 | CONFIG[WORKER_ID]=$2 94 | shift 95 | ;; 96 | --model) 97 | CONFIG[MODEL]="$2" 98 | shift 99 | ;; 100 | --help) 101 | help 102 | exit 0 103 | ;; 104 | *) 105 | echo "Unknown option: $1" 106 | help 107 | exit 1 108 | ;; 109 | esac 110 | shift 111 | done 112 | } 113 | 114 | run_cycle() { 115 | local cycle=$1 116 | echo "\n\n=== Cycle $cycle/${CONFIG[MAX_ITERATIONS]} ===" 117 | 118 | case $CONFIG[MODE] in 119 | "all") 120 | if (( cycle % 60 == 0 )); then update_task_list; fi 121 | if (( cycle % 60 == 30 )); then update_project_report; fi 122 | handle_development_tasks 123 | ;; 124 | "tasks") 125 | update_task_list 126 | ;; 127 | "report") 128 | update_project_report 129 | ;; 130 | "dev") 131 | handle_development_tasks 132 | ;; 133 | esac 134 | } 135 | 136 | help() { 137 | echo "Project Maintenance Agent - Unified Development Orchestrator" 138 | echo "Version: 1.2.0 | License: MIT | Model: ${CONFIG[MODEL]}" 139 | echo "Usage: ./project_agent [OPTIONS]" 140 | 141 | echo "\nOPERATIONAL MODES:" 142 | echo " --tasks Focus only on task list maintenance (task_list.md)" 143 | echo " --report Generate project status reports (project_report.md)" 144 | echo " --dev Execute development tasks only (code changes)" 145 | echo " --all Full operational mode (default)" 146 | 147 | echo "\nOPTIONS:" 148 | echo " -i, --iterations Set execution cycles (default: ${CONFIG[MAX_ITERATIONS]})" 149 | echo " --edit-format (diff|whole) Edit format (default: ${CONFIG[EDIT_FORMAT]})" 150 | echo " -n, --instances Run N parallel background instances (default: ${CONFIG[INSTANCES]})" 151 | echo " --model Set the AI model to use (default: ${CONFIG[MODEL]})" 152 | echo " --help Show this help menu" 153 | 154 | echo "\nCONFIGURATION DEFAULTS:" 155 | echo " Max Iterations: ${CONFIG[MAX_ITERATIONS]}" 156 | echo " Base Sleep: ${CONFIG[BASE_SLEEP]}s" 157 | echo " Task Sleep: ${CONFIG[TASK_SLEEP]}s" 158 | echo " Report Sleep: ${CONFIG[REPORT_SLEEP]}s" 159 | 160 | echo "\nEXAMPLES:" 161 | echo " ./project_agent --tasks -i 5 # Refresh task list 5 times" 162 | echo " ./project_agent --report # Generate status report" 163 | echo " ./project_agent --dev # Continuous development mode" 164 | echo " ./project_agent --all -i 100 # Full operation for 100 cycles" 165 | echo " ./project_agent --model gpt-4 # Use GPT-4 model" 166 | 167 | echo "\nNOTES:" 168 | echo " - Task priorities update automatically based on project state" 169 | echo " - Reports include architecture analysis and simplification opportunities" 170 | echo " - Development mode prefers isolated, low-complexity changes" 171 | echo " - Use 'aider --help' for details on the underlying AI agent" 172 | } 173 | 174 | launch_instance() { 175 | local instance_id=$1 176 | # Use direct command execution instead of nested instance management 177 | kitty --title "Agent $instance_id" zsh -ic "project_agent --${CONFIG[MODE]} --worker-id $instance_id --model ${CONFIG[MODEL]} || echo 'Agent failed - press enter to exit'; read" & 178 | } 179 | 180 | main() { 181 | parse_arguments "$@" 182 | build_aider_args 183 | 184 | if [[ ${CONFIG[INSTANCES]} -gt 1 ]]; then 185 | for ((instance=1; instance<=${CONFIG[INSTANCES]}; instance++)); do 186 | # Set worker ID for each instance 187 | launch_instance $instance "$@" 188 | done 189 | exit 0 190 | fi 191 | 192 | for ((i=1; i<=${CONFIG[MAX_ITERATIONS]}; i++)); do 193 | run_cycle $i 194 | done 195 | } 196 | 197 | main "$@" 198 | sleep 3 199 | -------------------------------------------------------------------------------- /prompt_cycle: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configuration 4 | MODEL="r1" # Default model 5 | EDITOR_MODEL="" # Editor model (if different from main model) 6 | PROMPTS_FILE="prompts.txt" # File containing prompts to cycle through 7 | ITERATIONS=1000 8 | SLEEP_TIME=1 # Sleep time between iterations in seconds 9 | GLOBAL_PROMPT="" # Global prompt to prepend to all messages 10 | 11 | # Check if rich-cli is installed 12 | if ! command -v rich &> /dev/null; then 13 | echo "Installing rich-cli for better formatting..." 14 | pip install rich-cli 15 | fi 16 | 17 | # Default prompts 18 | DEFAULT_PROMPTS=( 19 | "improve code structure and organization" 20 | "add more error handling and edge cases" 21 | "optimize performance where possible" 22 | "improve documentation and comments" 23 | "refactor for better readability" 24 | "add unit tests for critical functions" 25 | "implement additional features" 26 | "fix potential bugs and issues" 27 | ) 28 | 29 | # Create default prompts file if it doesn't exist 30 | if [ ! -f "$PROMPTS_FILE" ]; then 31 | echo "Creating default prompts file: $PROMPTS_FILE" 32 | printf "%s\n" "${DEFAULT_PROMPTS[@]}" > "$PROMPTS_FILE" 33 | fi 34 | 35 | # Initial check if prompts file exists and is not empty 36 | if [ ! -s "$PROMPTS_FILE" ]; then 37 | echo "Error: Prompts file is empty or doesn't exist: $PROMPTS_FILE" 38 | exit 1 39 | fi 40 | 41 | # Initial count of prompts 42 | PROMPT_COUNT=$(wc -l < "$PROMPTS_FILE") 43 | echo "Found $PROMPT_COUNT prompts in $PROMPTS_FILE" 44 | echo "Note: Changes to $PROMPTS_FILE will be detected automatically on each iteration" 45 | 46 | # Function to display usage information 47 | usage() { 48 | echo "Usage: $0 [options] [file1 [file2 ...]]" 49 | echo "Options:" 50 | echo " -m, --model MODEL Set the model (default: $MODEL)" 51 | echo " -e, --editor-model MODEL Set a specific editor model (optional)" 52 | echo " -p, --prompts FILE Set the prompts file (default: $PROMPTS_FILE)" 53 | echo " -i, --iterations NUM Set number of iterations (default: $ITERATIONS)" 54 | echo " -s, --sleep SECONDS Set sleep time between iterations (default: $SLEEP_TIME)" 55 | echo " -n, --no-files Run without specifying files (architect mode can add files)" 56 | echo " -g, --global-prompt TEXT Set a global prompt to prepend to all messages" 57 | echo " -h, --help Display this help message" 58 | exit 1 59 | } 60 | 61 | # Parse command line arguments 62 | FILE_PATTERNS=() 63 | NO_FILES=false 64 | READ_FILES=() 65 | while [[ $# -gt 0 ]]; do 66 | case $1 in 67 | -m|--model) 68 | MODEL="$2" 69 | shift 2 70 | ;; 71 | -e|--editor-model) 72 | EDITOR_MODEL="$2" 73 | shift 2 74 | ;; 75 | -p|--prompts) 76 | PROMPTS_FILE="$2" 77 | # Create default prompts file if specified file doesn't exist 78 | if [ ! -f "$PROMPTS_FILE" ]; then 79 | echo "Creating specified prompts file: $PROMPTS_FILE" 80 | printf "%s\n" "${DEFAULT_PROMPTS[@]}" > "$PROMPTS_FILE" 81 | fi 82 | shift 2 83 | ;; 84 | -g|--global-prompt) 85 | GLOBAL_PROMPT="$2" 86 | shift 2 87 | ;; 88 | -n|--no-files) 89 | NO_FILES=true 90 | shift 91 | ;; 92 | -r|--read) 93 | if [ -f "$2" ]; then 94 | READ_FILES+=("$2") 95 | else 96 | echo "Warning: Read file '$2' does not exist and will be ignored." 97 | fi 98 | shift 2 99 | ;; 100 | -i|--iterations) 101 | ITERATIONS="$2" 102 | shift 2 103 | ;; 104 | -s|--sleep) 105 | SLEEP_TIME="$2" 106 | shift 2 107 | ;; 108 | -h|--help) 109 | usage 110 | ;; 111 | -*) 112 | echo "Unknown option: $1" 113 | usage 114 | ;; 115 | *) 116 | FILE_PATTERNS+=("$1") 117 | shift 118 | ;; 119 | esac 120 | done 121 | 122 | # Default read files if none specified 123 | if [ ${#READ_FILES[@]} -eq 0 ]; then 124 | for DEFAULT_READ in "plex.md" "context.txt" "spec.md"; do 125 | if [ -f "$DEFAULT_READ" ]; then 126 | READ_FILES+=("$DEFAULT_READ") 127 | fi 128 | done 129 | fi 130 | 131 | # Check if at least one file pattern is provided or --no-files flag is set 132 | if [ ${#FILE_PATTERNS[@]} -eq 0 ] && [ "$NO_FILES" = false ]; then 133 | echo "Error: No files specified. Use --no-files flag if you want to run without specifying files." 134 | usage 135 | fi 136 | 137 | # Initial check for file patterns 138 | if [ ${#FILE_PATTERNS[@]} -gt 0 ] && [ "$NO_FILES" = false ]; then 139 | # Check if at least one pattern matches something 140 | FOUND_FILES=false 141 | for PATTERN in "${FILE_PATTERNS[@]}"; do 142 | if compgen -G "$PATTERN" > /dev/null; then 143 | FOUND_FILES=true 144 | break 145 | fi 146 | done 147 | 148 | if [ "$FOUND_FILES" = false ]; then 149 | echo "Warning: None of the specified file patterns match any files." 150 | echo "Files will be included if they appear later during execution." 151 | fi 152 | fi 153 | 154 | # Function to handle script interruption 155 | cleanup() { 156 | echo -e "\nScript interrupted. Exiting gracefully..." 157 | exit 0 158 | } 159 | 160 | # Set up trap for CTRL+C 161 | trap cleanup SIGINT SIGTERM 162 | 163 | # Main loop 164 | for i in $(seq 1 $ITERATIONS); do 165 | # Get valid prompts (non-empty lines) 166 | mapfile -t VALID_PROMPTS < <(grep -v '^\s*$' "$PROMPTS_FILE") 167 | PROMPT_COUNT=${#VALID_PROMPTS[@]} 168 | 169 | if [ "$PROMPT_COUNT" -eq 0 ]; then 170 | echo "Warning: No valid prompts found in $PROMPTS_FILE. Using default prompt." 171 | CURRENT_PROMPT="improve code" 172 | else 173 | # Calculate which prompt to use (random start then cycle) 174 | PROMPT_INDEX=$(( (RANDOM + i - 1) % PROMPT_COUNT )) 175 | CURRENT_PROMPT="${VALID_PROMPTS[$PROMPT_INDEX]}" 176 | fi 177 | 178 | # Combine global prompt with current prompt if global prompt is set 179 | FULL_PROMPT="$CURRENT_PROMPT" 180 | if [ -n "$GLOBAL_PROMPT" ]; then 181 | FULL_PROMPT="$GLOBAL_PROMPT $CURRENT_PROMPT" 182 | fi 183 | 184 | # Display the current prompt with rich formatting 185 | echo -e "\n" 186 | if command -v rich &> /dev/null; then 187 | rich --print "[bold blue]Iteration $i - $(date)[/bold blue]" 188 | rich --print "[bold green]====================================================[/bold green]" 189 | if [ -n "$GLOBAL_PROMPT" ]; then 190 | rich --print "[bold magenta]GLOBAL:[/bold magenta] [white]$GLOBAL_PROMPT[/white]" 191 | fi 192 | rich --print "[bold yellow]PROMPT:[/bold yellow] [bold white]$CURRENT_PROMPT[/bold white]" 193 | rich --print "[bold green]====================================================[/bold green]" 194 | else 195 | echo "Iteration $i - $(date)" 196 | echo "====================================================" 197 | if [ -n "$GLOBAL_PROMPT" ]; then 198 | echo "GLOBAL: $GLOBAL_PROMPT" 199 | fi 200 | echo "PROMPT: $CURRENT_PROMPT" 201 | echo "====================================================" 202 | fi 203 | 204 | # Build read arguments 205 | READ_ARGS="" 206 | for READ_FILE in "${READ_FILES[@]}"; do 207 | READ_ARGS="$READ_ARGS --read \"$READ_FILE\"" 208 | done 209 | 210 | # Build read arguments 211 | READ_ARGS=() 212 | for READ_FILE in "${READ_FILES[@]}"; do 213 | READ_ARGS+=(--read "$READ_FILE") 214 | done 215 | 216 | # Build the base command 217 | AIDER_CMD=(aider --model "$MODEL" --subtree-only "${READ_ARGS[@]}" 218 | --yes-always --no-show-model-warnings 219 | --weak-model 'openrouter/google/gemini-2.0-flash-001') 220 | 221 | # Add editor model if specified 222 | if [ -n "$EDITOR_MODEL" ]; then 223 | AIDER_CMD+=(--editor-model "$EDITOR_MODEL") 224 | fi 225 | 226 | # Add message 227 | AIDER_CMD+=(--message "$FULL_PROMPT") 228 | 229 | # Add files if needed - resolve globs on each iteration 230 | if [ "$NO_FILES" = false ]; then 231 | FILES=() 232 | for PATTERN in "${FILE_PATTERNS[@]}"; do 233 | # Use compgen to expand globs 234 | while IFS= read -r FILE; do 235 | if [ -f "$FILE" ]; then 236 | FILES+=("$FILE") 237 | fi 238 | done < <(compgen -G "$PATTERN" 2>/dev/null || echo "") 239 | done 240 | 241 | if [ ${#FILES[@]} -gt 0 ]; then 242 | AIDER_CMD+=("${FILES[@]}") 243 | 244 | # Display the files being processed 245 | if command -v rich &> /dev/null; then 246 | rich --print "[cyan]Processing files:[/cyan] [white]${FILES[*]}[/white]" 247 | else 248 | echo "Processing files: ${FILES[*]}" 249 | fi 250 | else 251 | echo "Warning: No files match the specified patterns at this iteration." 252 | fi 253 | fi 254 | 255 | # Execute the command 256 | "${AIDER_CMD[@]}" 257 | 258 | if command -v rich &> /dev/null; then 259 | rich --print "[dim]Sleeping for $SLEEP_TIME seconds...[/dim]" 260 | else 261 | echo "Sleeping for $SLEEP_TIME seconds..." 262 | fi 263 | sleep $SLEEP_TIME 264 | done 265 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="path-scripts", 5 | version="0.1.0", 6 | packages=find_packages(), 7 | install_requires=[ 8 | "rich<13.0.0,>=12.6.0", # Compatible with textual 0.1.18 9 | "litellm", 10 | "textual==0.1.18", # Pin to the installed version 11 | "lxml", # For better XML handling 12 | "requests", # For web search 13 | ], 14 | entry_points={ 15 | "console_scripts": [ 16 | "agent=src.interface.cli:main", 17 | ], 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | - has a cli interface 2 | - uses rich to create good looking tables 3 | - shows the ping jitter to locations in a table 4 | - by default without args analyses and shows ping jitter to new york as a demo 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # Make src a proper package 2 | -------------------------------------------------------------------------------- /src/agent/core.py: -------------------------------------------------------------------------------- 1 | """Core agent functionality.""" 2 | 3 | import threading 4 | import shutil 5 | from typing import Dict, Optional, Callable, Any, List, Tuple 6 | import litellm 7 | from rich.console import Console 8 | from .plan import ( 9 | generate_plan, 10 | update_plan, 11 | check_dependencies, 12 | apply_plan_updates, 13 | ) 14 | from .task import execute_task 15 | from ..utils.xml_tools import format_xml_response 16 | 17 | 18 | class Agent: 19 | """Main agent class for handling model interactions and reasoning.""" 20 | 21 | def __init__(self, model_name: str = "openrouter/deepseek/deepseek-r1"): 22 | self.console = Console() 23 | self.model_name = model_name 24 | self.plan_tree = None 25 | self.plan_lock = threading.Lock() # Thread safety for plan_tree access 26 | self.repository_info: Dict[str, Any] = {} 27 | self.config = { 28 | "stream_reasoning": True, 29 | "verbose": True, 30 | "rate_limit": 5, # Requests per minute 31 | } 32 | self.stream_callback: Optional[Callable[[str, bool], None]] = None 33 | 34 | def initialize(self, repo_path: str = ".") -> None: 35 | """Initialize the agent with repository information""" 36 | 37 | self.repository_info = {"path": repo_path} 38 | print(f"Agent initialized for repository: {repo_path}") 39 | 40 | def _get_terminal_height(self) -> int: 41 | """Get terminal height using shutil""" 42 | try: 43 | return shutil.get_terminal_size().lines 44 | except Exception: 45 | return 40 # Fallback default 46 | 47 | def stream_reasoning(self, prompt: str) -> str: 48 | """Stream the reasoning process from the model and return the final response""" 49 | messages = [{"role": "user", "content": prompt}] 50 | 51 | # Get terminal height and add that many newlines to preserve history 52 | terminal_height = self._get_terminal_height() 53 | print("\n" * terminal_height) 54 | 55 | if not self.config["stream_reasoning"]: 56 | # Non-streaming mode 57 | try: 58 | response = litellm.completion( 59 | model=self.model_name, 60 | messages=messages, 61 | timeout=60, # Add timeout to prevent hanging 62 | ) 63 | return response.choices[0].message.content 64 | except Exception as e: 65 | print(f"Error in non-streaming mode: {e}") 66 | return f"Error: {str(e)}" 67 | 68 | # Streaming mode 69 | full_response = "" 70 | reasoning_output = "" 71 | 72 | try: 73 | response = litellm.completion( 74 | model=self.model_name, messages=messages, stream=True, timeout=60 75 | ) 76 | 77 | for chunk in response: 78 | self.handle_response_content(chunk, full_response, reasoning_output) 79 | 80 | self.finalize_response(reasoning_output) 81 | 82 | return full_response 83 | 84 | except KeyboardInterrupt: 85 | print("\n\nOperation cancelled by user") 86 | return full_response 87 | except Exception as e: 88 | print(f"\nError during streaming: {e}") 89 | return full_response or f"Error: {str(e)}" 90 | 91 | def handle_response_content(self, chunk, full_response: str, reasoning_output: str) -> None: 92 | """Handle different types of response content.""" 93 | if hasattr(chunk.choices[0].delta, "content"): 94 | self.handle_regular_content(chunk, full_response) 95 | elif hasattr(chunk.choices[0].delta, "reasoning_content"): 96 | self.handle_reasoning_content(chunk, reasoning_output) 97 | 98 | def handle_regular_content(self, chunk, full_response: str) -> None: 99 | """Process regular content chunks.""" 100 | content = chunk.choices[0].delta.content 101 | if content: 102 | clean_content = content.replace("\r", "").replace("\b", "") 103 | if self.stream_callback and callable(self.stream_callback): 104 | self.stream_callback(clean_content, False) # pylint: disable=not-callable 105 | else: 106 | print(clean_content, end="", flush=True) 107 | full_response += clean_content 108 | 109 | def handle_reasoning_content(self, chunk, reasoning_output: str) -> None: 110 | """Process reasoning content chunks.""" 111 | reasoning = chunk.choices[0].delta.reasoning_content 112 | if reasoning: 113 | clean_reasoning = reasoning.replace("\r", "").replace("\b", "") 114 | if self.stream_callback and callable(self.stream_callback): 115 | self.stream_callback(clean_reasoning, True) # pylint: disable=not-callable 116 | else: 117 | self.console.print(f"[yellow]{clean_reasoning}[/yellow]", 118 | end="", highlight=False) 119 | reasoning_output += clean_reasoning 120 | 121 | def finalize_response(self, reasoning_output: str) -> None: 122 | """Save reasoning output to file.""" 123 | if reasoning_output: 124 | try: 125 | with open("last_reasoning.txt", "w") as f: 126 | f.write(reasoning_output) 127 | except Exception as e: 128 | print(f"Warning: Could not save reasoning to file: {e}") 129 | 130 | # Plan management methods 131 | def generate_plan(self, spec: str) -> str: 132 | """Generate a plan tree based on the specification""" 133 | return generate_plan(self, spec) 134 | 135 | def update_plan( 136 | self, 137 | task_id: str, 138 | new_status: str, 139 | notes: Optional[str] = None, 140 | progress: Optional[str] = None, 141 | ) -> str: 142 | """Update the status of a task in the plan""" 143 | return update_plan(self, task_id, new_status, notes, progress) 144 | 145 | def display_plan_tree(self) -> str: 146 | """Display the current plan tree""" 147 | if not self.plan_tree: 148 | return format_xml_response({"error": "No plan exists"}) 149 | return format_xml_response({"plan": self.plan_tree}) 150 | 151 | def apply_plan_updates(self, plan_update_xml: str) -> None: 152 | """Apply updates to the plan tree based on the plan_update XML""" 153 | apply_plan_updates(self, plan_update_xml) 154 | 155 | def check_dependencies(self, task_id: str) -> Tuple[bool, List[str]]: 156 | """Check if all dependencies for a task are completed""" 157 | return check_dependencies(self, task_id) 158 | 159 | def execute_task(self, task_id: str) -> str: 160 | """Execute a specific task from the plan""" 161 | return execute_task(self, task_id) 162 | -------------------------------------------------------------------------------- /src/agent/execution.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Task execution functionality.""" 3 | 4 | import xml.etree.ElementTree as ET 5 | from ..utils.xml_tools import extract_xml_from_response, format_xml_response 6 | from .plan import check_dependencies, apply_plan_updates 7 | 8 | 9 | def execute_task(agent, task_id: str) -> str: 10 | """ 11 | Execute a specific task from the plan. 12 | 13 | Args: 14 | agent: The agent instance 15 | task_id: The ID of the task to execute 16 | 17 | Returns: 18 | Formatted XML response with execution results 19 | """ 20 | if not agent.plan_tree: 21 | return format_xml_response({"error": "No plan exists"}) 22 | 23 | try: 24 | # Parse the plan tree 25 | parser = ET.XMLParser(resolve_entities=False) 26 | root = ET.fromstring(agent.plan_tree, parser=parser) 27 | 28 | # Find the task with the given ID 29 | task_element = root.find(f".//task[@id='{task_id}']") 30 | if task_element is None: 31 | return format_xml_response({"error": f"Task {task_id} not found"}) 32 | 33 | # Get task details 34 | description = task_element.get("description", "") 35 | current_status = task_element.get("status", "pending") 36 | 37 | # Check if task is already completed 38 | if current_status == "completed": 39 | return format_xml_response( 40 | { 41 | "warning": f"Task {task_id} is already marked as completed", 42 | "task": { 43 | "id": task_id, 44 | "description": description, 45 | "status": current_status, 46 | }, 47 | } 48 | ) 49 | 50 | # Check dependencies 51 | deps_met, missing_deps = check_dependencies(agent, task_id) 52 | if not deps_met: 53 | return format_xml_response( 54 | { 55 | "error": "Dependencies not met", 56 | "task": {"id": task_id, "description": description}, 57 | "missing_dependencies": missing_deps, 58 | } 59 | ) 60 | 61 | # Update task status to in-progress 62 | task_element.set("status", "in-progress") 63 | task_element.set("progress", "10") # Start with 10% progress 64 | agent.plan_tree = ET.tostring(root, encoding="unicode") 65 | 66 | print(f"Executing task {task_id}: {description}") 67 | print("Status updated to: in-progress (10%)") 68 | 69 | # Get parent task information for context 70 | parent_info = "" 71 | for potential_parent in root.findall(".//task"): 72 | for child in potential_parent.findall("./task"): 73 | if child.get("id") == task_id: 74 | parent_id = potential_parent.get("id") 75 | parent_desc = potential_parent.get("description") 76 | parent_info = f"This task is part of: {parent_id} - {parent_desc}" 77 | break 78 | if parent_info: 79 | break 80 | 81 | # Generate actions for this task 82 | prompt = f""" 83 | I need to execute the following task: 84 | 85 | TASK ID: {task_id} 86 | DESCRIPTION: {description} 87 | {parent_info} 88 | 89 | REPOSITORY INFORMATION: 90 | {agent.repository_info} 91 | 92 | CURRENT PLAN: 93 | {agent.plan_tree} 94 | 95 | Generate the necessary actions to complete this task. The actions should be in XML format: 96 | 97 | 98 | 99 | # Python code here 100 | 101 | 102 | 103 | def old_function(): 104 | def new_function(): 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Your response text here 114 | 115 | 116 | 117 | 118 | # Python code here 119 | 120 | 121 | 122 | def old_function(): 123 | def new_function(): 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | def old_function(): 133 | def new_function(): 134 | 135 | 136 | 137 | 138 | 139 | echo "Hello World" 140 | rm -rf some_directory 141 | 142 | 143 | 144 | 145 | 146 | Old information to replace 147 | Updated information 148 | 149 | New information to remember 150 | 151 | 152 | 153 | 154 | Status message explaining what's done or what's needed 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Think step by step about what needs to be done to complete this task. 166 | Focus on creating actions that are specific, concrete, and directly implement the task. 167 | """ 168 | 169 | # Update progress to 30% - planning phase 170 | task_element.set("progress", "30") 171 | agent.plan_tree = ET.tostring(root, encoding="unicode") 172 | print("Progress updated to: 30% (planning phase)") 173 | 174 | response = agent.stream_reasoning(prompt) 175 | 176 | # Update progress to 50% - actions generated 177 | task_element.set("progress", "50") 178 | agent.plan_tree = ET.tostring(root, encoding="unicode") 179 | print("Progress updated to: 50% (actions generated)") 180 | 181 | # Extract actions XML from the response 182 | actions_xml = extract_xml_from_response(response, "actions") 183 | plan_update_xml = extract_xml_from_response(response, "plan_update") 184 | 185 | # Apply plan updates if present 186 | if plan_update_xml: 187 | apply_plan_updates(agent, plan_update_xml) 188 | 189 | if actions_xml: 190 | # Update progress to 70% - ready for execution 191 | task_element.set("progress", "70") 192 | agent.plan_tree = ET.tostring(root, encoding="unicode") 193 | print("Progress updated to: 70% (ready for execution)") 194 | 195 | return format_xml_response( 196 | { 197 | "task": { 198 | "id": task_id, 199 | "description": description, 200 | "progress": "70", 201 | }, 202 | "actions": actions_xml, 203 | "plan_update": plan_update_xml if plan_update_xml else None, 204 | } 205 | ) 206 | 207 | # Update task status to failed 208 | task_element.set("status", "failed") 209 | task_element.set("notes", "Failed to generate actions") 210 | task_element.set("progress", "0") 211 | agent.plan_tree = ET.tostring(root, encoding="unicode") 212 | print(f"Task {task_id} failed: Could not generate actions") 213 | 214 | return format_xml_response( 215 | { 216 | "error": "Failed to generate actions for task", 217 | "task": { 218 | "id": task_id, 219 | "description": description, 220 | "status": "failed", 221 | }, 222 | } 223 | ) 224 | 225 | except Exception as e: 226 | return format_xml_response({"error": f"Error executing task: {str(e)}"}) 227 | 228 | 229 | if __name__ == "__main__": 230 | # Simple test when run directly 231 | print("Task execution module - run through the agent interface") 232 | -------------------------------------------------------------------------------- /src/agent/plan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Plan generation and management functionality.""" 3 | 4 | import json 5 | import xml.etree.ElementTree as ET 6 | from typing import Dict, Any, List, Tuple, Optional 7 | 8 | from ..utils.xml_tools import extract_xml_from_response, format_xml_response 9 | 10 | 11 | def generate_plan(agent, spec: str, formatted_message: str = None) -> str: 12 | """ 13 | Generate a plan tree based on the specification. 14 | 15 | Args: 16 | agent: The agent instance 17 | spec: The specification text 18 | 19 | Returns: 20 | Formatted XML response containing the plan 21 | """ 22 | # If no formatted message is provided, create one 23 | if not formatted_message: 24 | # Get system information for the prompt 25 | from src.interface.display import get_system_info 26 | 27 | system_info = get_system_info() 28 | 29 | # Import the input schema formatter 30 | from src.utils.input_schema import format_input_message 31 | 32 | # Format the message with XML tags using the schema 33 | formatted_message = format_input_message( 34 | message=f"Generate a plan based on the following specification:\n\n{spec}", 35 | system_info=system_info, 36 | ) 37 | 38 | prompt = f""" 39 | 40 | 41 | Based on the following specification, create a hierarchical plan as an XML tree. 42 | Think step by step about the dependencies between tasks and how to break down the problem effectively. 43 | 44 | 45 | 46 | {spec} 47 | 48 | 49 | {json.dumps(agent.repository_info, indent=2)} 50 | 51 | 52 | Each task should have: 53 | - A unique id 54 | - A clear description 55 | - A status (pending, in-progress, completed, failed) 56 | - A complexity estimate (low, medium, high) 57 | - Dependencies (depends_on attribute with comma-separated task IDs) 58 | - Progress indicator (0-100) 59 | - Subtasks where appropriate 60 | 61 | Example structure: 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | """ 77 | 78 | response = agent.stream_reasoning(prompt) 79 | 80 | # Extract XML from the response 81 | xml_content = extract_xml_from_response(response, "plan") 82 | if xml_content: 83 | agent.plan_tree = xml_content 84 | return format_xml_response({"plan": xml_content}) 85 | else: 86 | return format_xml_response({"error": "Failed to generate plan"}) 87 | 88 | 89 | def update_plan( 90 | agent, 91 | task_id: str, 92 | new_status: str, 93 | notes: Optional[str] = None, 94 | progress: Optional[str] = None, 95 | ) -> str: 96 | """ 97 | Update the status of a task in the plan. 98 | 99 | Args: 100 | agent: The agent instance 101 | task_id: The ID of the task to update 102 | new_status: The new status for the task 103 | notes: Optional notes to add to the task 104 | progress: Optional progress value (0-100) 105 | 106 | Returns: 107 | Formatted XML response with the updated plan 108 | """ 109 | if not agent.plan_tree: 110 | return format_xml_response({"error": "No plan exists"}) 111 | 112 | try: 113 | # Parse the plan tree 114 | parser = ET.XMLParser(resolve_entities=False) 115 | root = ET.fromstring(agent.plan_tree, parser=parser) 116 | 117 | # Find the task with the given ID 118 | task_found = False 119 | for task in root.findall(".//task[@id='{}']".format(task_id)): 120 | task.set("status", new_status) 121 | if notes: 122 | task.set("notes", notes) 123 | if progress and progress.isdigit() and 0 <= int(progress) <= 100: 124 | task.set("progress", progress) 125 | task_found = True 126 | 127 | if not task_found: 128 | return format_xml_response({"error": f"Task {task_id} not found"}) 129 | 130 | # Update the plan tree 131 | agent.plan_tree = ET.tostring(root, encoding="unicode") 132 | 133 | return format_xml_response( 134 | { 135 | "plan": agent.plan_tree, 136 | "status": f"Updated task {task_id} to {new_status}", 137 | } 138 | ) 139 | 140 | except Exception as e: 141 | return format_xml_response({"error": f"Error updating plan: {str(e)}"}) 142 | 143 | 144 | def check_dependencies(agent, task_id: str) -> Tuple[bool, List[str]]: 145 | """ 146 | Check if all dependencies for a task are completed. 147 | 148 | Args: 149 | agent: The agent instance 150 | task_id: The ID of the task to check 151 | 152 | Returns: 153 | Tuple of (dependencies_met, list_of_missing_dependencies) 154 | """ 155 | if not agent.plan_tree: 156 | return False, ["No plan exists"] 157 | 158 | try: 159 | # Parse the plan tree 160 | root = ET.fromstring(agent.plan_tree) 161 | 162 | # Find the task with the given ID 163 | task_element = root.find(f".//task[@id='{task_id}']") 164 | if task_element is None: 165 | return False, [f"Task {task_id} not found"] 166 | 167 | # Get dependencies 168 | depends_on = task_element.get("depends_on", "") 169 | if not depends_on: 170 | return True, [] # No dependencies 171 | 172 | # Check each dependency 173 | dependency_ids = [dep.strip() for dep in depends_on.split(",") if dep.strip()] 174 | incomplete_deps = [] 175 | 176 | for dep_id in dependency_ids: 177 | dep_element = root.find(f".//task[@id='{dep_id}']") 178 | if dep_element is None: 179 | incomplete_deps.append(f"Dependency {dep_id} not found") 180 | continue 181 | 182 | status = dep_element.get("status", "") 183 | if status != "completed": 184 | desc = dep_element.get("description", "") 185 | incomplete_deps.append( 186 | f"Dependency {dep_id} ({desc}) is not completed (status: {status})" 187 | ) 188 | 189 | return len(incomplete_deps) == 0, incomplete_deps 190 | 191 | except Exception as e: 192 | return False, [f"Error checking dependencies: {str(e)}"] 193 | 194 | 195 | def apply_plan_updates(agent, plan_update_xml: str) -> None: 196 | """ 197 | Apply updates to the plan tree based on the plan_update XML. 198 | 199 | Args: 200 | agent: The agent instance 201 | plan_update_xml: XML string containing plan updates 202 | """ 203 | if not agent.plan_tree: 204 | return 205 | 206 | try: 207 | # Parse the plan tree and updates 208 | plan_root = ET.fromstring(agent.plan_tree) 209 | parser = ET.XMLParser(resolve_entities=False) 210 | updates_root = ET.fromstring(plan_update_xml, parser=parser) 211 | 212 | # Track changes for reporting 213 | changes = [] 214 | 215 | # Process add_task elements 216 | for add_task in updates_root.findall("./add_task"): 217 | parent_id = add_task.get("parent_id") 218 | 219 | # Find the parent task 220 | parent = plan_root.find(f".//task[@id='{parent_id}']") 221 | if parent is not None: 222 | # Create a new task element 223 | new_task = ET.Element("task") 224 | 225 | # Copy all attributes from add_task to new_task 226 | for attr, value in add_task.attrib.items(): 227 | if attr != "parent_id": # Skip the parent_id attribute 228 | new_task.set(attr, value) 229 | 230 | # Add the new task to the parent 231 | parent.append(new_task) 232 | changes.append( 233 | f"Added new task {new_task.get('id')}: {new_task.get('description')}" 234 | ) 235 | 236 | # Process modify_task elements 237 | for modify_task in updates_root.findall("./modify_task"): 238 | task_id = modify_task.get("id") 239 | 240 | # Find the task to modify 241 | task = plan_root.find(f".//task[@id='{task_id}']") 242 | if task is not None: 243 | old_desc = task.get("description", "") 244 | # Update attributes 245 | for attr, value in modify_task.attrib.items(): 246 | if attr != "id": # Skip the id attribute 247 | task.set(attr, value) 248 | new_desc = task.get("description", "") 249 | if old_desc != new_desc: 250 | changes.append(f"Modified task {task_id}: {old_desc} -> {new_desc}") 251 | else: 252 | changes.append(f"Updated attributes for task {task_id}") 253 | 254 | # Process remove_task elements 255 | for remove_task in updates_root.findall("./remove_task"): 256 | task_id = remove_task.get("id") 257 | 258 | # Find the task to remove 259 | task = plan_root.find(f".//task[@id='{task_id}']") 260 | if task is not None: 261 | desc = task.get("description", "") 262 | # ElementTree in Python doesn't have getparent() method 263 | # We need to find the parent manually 264 | for potential_parent in plan_root.findall(".//task"): 265 | for child in potential_parent.findall("./task"): 266 | if child.get("id") == task_id: 267 | potential_parent.remove(child) 268 | changes.append(f"Removed task {task_id}: {desc}") 269 | break 270 | 271 | # Update the plan tree 272 | agent.plan_tree = ET.tostring(plan_root, encoding="unicode") 273 | 274 | # Report changes 275 | if changes: 276 | print("\nPlan has been updated by the agent:") 277 | for change in changes: 278 | print(f"- {change}") 279 | 280 | except Exception as e: 281 | print(f"Error applying plan updates: {e}") 282 | 283 | 284 | if __name__ == "__main__": 285 | # Simple test when run directly 286 | print("Plan management module - run through the agent interface") 287 | -------------------------------------------------------------------------------- /src/agent/repository.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Repository analysis functionality.""" 3 | 4 | import os 5 | import subprocess 6 | from typing import Dict, Any 7 | 8 | 9 | def analyze_repository(repo_path: str = ".") -> Dict[str, Any]: 10 | """ 11 | Analyze the repository structure and return information. 12 | 13 | Args: 14 | repo_path: Path to the repository 15 | 16 | Returns: 17 | Dictionary containing repository information 18 | """ 19 | repo_info = {"files": [], "directories": [], "git_info": {}} 20 | 21 | # Get list of files (excluding .git directory) 22 | try: 23 | result = subprocess.run( 24 | ["git", "ls-files"], 25 | cwd=repo_path, 26 | capture_output=True, 27 | text=True, 28 | check=True, 29 | ) 30 | repo_info["files"] = result.stdout.strip().split("\n") 31 | except subprocess.CalledProcessError: 32 | # Fallback if git command fails 33 | for root, dirs, files in os.walk(repo_path): 34 | if ".git" in root: 35 | continue 36 | for file in files: 37 | full_path = os.path.join(root, file) 38 | rel_path = os.path.relpath(full_path, repo_path) 39 | repo_info["files"].append(rel_path) 40 | for dir in dirs: 41 | if dir != ".git": 42 | full_path = os.path.join(root, dir) 43 | rel_path = os.path.relpath(full_path, repo_path) 44 | repo_info["directories"].append(rel_path) 45 | 46 | # Get git info if available 47 | try: 48 | result = subprocess.run( 49 | ["git", "branch", "--show-current"], 50 | cwd=repo_path, 51 | capture_output=True, 52 | text=True, 53 | check=True, 54 | ) 55 | repo_info["git_info"]["current_branch"] = result.stdout.strip() 56 | except subprocess.CalledProcessError: 57 | repo_info["git_info"]["current_branch"] = "unknown" 58 | 59 | return repo_info 60 | -------------------------------------------------------------------------------- /src/agent/task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Task execution functionality.""" 3 | 4 | import json 5 | import xml.etree.ElementTree as ET 6 | import json 7 | import xml.etree.ElementTree as ET 8 | from src.utils.xml_tools import extract_xml_from_response, format_xml_response 9 | 10 | 11 | def execute_task(agent, task_id: str) -> str: 12 | """ 13 | Execute a specific task from the plan. 14 | 15 | Args: 16 | agent: The agent instance 17 | task_id: The ID of the task to execute 18 | 19 | Returns: 20 | Formatted XML response with execution results 21 | """ 22 | if not hasattr(agent, 'plan_tree') or not agent.plan_tree: 23 | return format_xml_response({"error": "No plan exists"}) 24 | 25 | try: 26 | # Parse the plan tree 27 | root = ET.fromstring(agent.plan_tree) 28 | 29 | # Find the task with the given ID 30 | task_element = root.find(f".//task[@id='{task_id}']") 31 | if task_element is None: 32 | return format_xml_response({"error": f"Task {task_id} not found"}) 33 | 34 | # Return basic task info 35 | return format_xml_response({ 36 | "task": { 37 | "id": task_id, 38 | "description": task_element.get("description", ""), 39 | "status": task_element.get("status", "pending") 40 | } 41 | }) 42 | 43 | except Exception as e: 44 | return format_xml_response({"error": f"Error executing task: {str(e)}"}) 45 | if task_element is None: 46 | return format_xml_response({"error": f"Task {task_id} not found"}) 47 | 48 | # Get task details 49 | description = task_element.get("description", "") 50 | current_status = task_element.get("status", "pending") 51 | 52 | # Check if task is already completed 53 | if current_status == "completed": 54 | return format_xml_response( 55 | { 56 | "warning": f"Task {task_id} is already marked as completed", 57 | "task": { 58 | "id": task_id, 59 | "description": description, 60 | "status": current_status, 61 | }, 62 | } 63 | ) 64 | 65 | # Check dependencies 66 | from src.agent.plan import check_dependencies 67 | 68 | deps_met, missing_deps = check_dependencies(agent, task_id) 69 | if not deps_met: 70 | return format_xml_response( 71 | { 72 | "error": "Dependencies not met", 73 | "task": {"id": task_id, "description": description}, 74 | "missing_dependencies": missing_deps, 75 | } 76 | ) 77 | 78 | # Update task status to in-progress 79 | task_element.set("status", "in-progress") 80 | task_element.set("progress", "10") # Start with 10% progress 81 | agent.plan_tree = ET.tostring(root, encoding="unicode") 82 | 83 | print(f"Executing task {task_id}: {description}") 84 | print("Status updated to: in-progress (10%)") 85 | 86 | # Get parent task information for context 87 | parent_info = "" 88 | for potential_parent in root.findall(".//task"): 89 | for child in potential_parent.findall("./task"): 90 | if child.get("id") == task_id: 91 | parent_id = potential_parent.get("id") 92 | parent_desc = potential_parent.get("description") 93 | parent_info = f"This task is part of: {parent_id} - {parent_desc}" 94 | break 95 | if parent_info: 96 | break 97 | 98 | # Generate actions for this task 99 | prompt = f""" 100 | I need to execute the following task: 101 | 102 | TASK ID: {task_id} 103 | DESCRIPTION: {description} 104 | {parent_info} 105 | 106 | REPOSITORY INFORMATION: 107 | {json.dumps(agent.repository_info, indent=2)} 108 | 109 | CURRENT PLAN: 110 | {agent.plan_tree} 111 | 112 | Generate the necessary actions to complete this task. The actions should be in XML format: 113 | 114 | 115 | 116 | # Python code here 117 | 118 | 119 | 120 | def old_function(): 121 | def new_function(): 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Your response text here 131 | 132 | 133 | 134 | 135 | # Python code here 136 | 137 | 138 | 139 | def old_function(): 140 | def new_function(): 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | def old_function(): 150 | def new_function(): 151 | 152 | 153 | 154 | 155 | 156 | echo "Hello World" 157 | rm -rf some_directory 158 | 159 | 160 | 161 | 162 | 163 | Old information to replace 164 | Updated information 165 | 166 | New information to remember 167 | 168 | 169 | 170 | 171 | Status message explaining what's done or what's needed 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | Think step by step about what needs to be done to complete this task. 183 | Focus on creating actions that are specific, concrete, and directly implement the task. 184 | """ 185 | 186 | # Update progress to 30% - planning phase 187 | task_element.set("progress", "30") 188 | agent.plan_tree = ET.tostring(root, encoding="unicode") 189 | print("Progress updated to: 30% (planning phase)") 190 | 191 | response = agent.stream_reasoning(prompt) 192 | 193 | # Update progress to 50% - actions generated 194 | task_element.set("progress", "50") 195 | agent.plan_tree = ET.tostring(root, encoding="unicode") 196 | print("Progress updated to: 50% (actions generated)") 197 | 198 | # Extract actions XML from the response 199 | actions_xml = extract_xml_from_response(response, "actions") 200 | plan_update_xml = extract_xml_from_response(response, "plan_update") 201 | 202 | # Apply plan updates if present 203 | if plan_update_xml: 204 | from src.agent.plan import apply_plan_updates 205 | 206 | apply_plan_updates(agent, plan_update_xml) 207 | 208 | if not actions_xml: 209 | # Update task status to failed 210 | task_element.set("status", "failed") 211 | task_element.set("notes", "Failed to generate actions") 212 | task_element.set("progress", "0") 213 | agent.plan_tree = ET.tostring(root, encoding="unicode") 214 | print(f"Task {task_id} failed: Could not generate actions") 215 | 216 | # Generate dopamine reward for failure 217 | if hasattr(agent, "dopamine_reward"): 218 | dopamine = agent.dopamine_reward.generate_reward(30) 219 | else: 220 | from src.utils.feedback import DopamineReward 221 | 222 | agent.dopamine_reward = DopamineReward(agent.console) 223 | dopamine = agent.dopamine_reward.generate_reward(30) 224 | 225 | return format_xml_response( 226 | { 227 | "error": "Failed to generate actions for task", 228 | "task": { 229 | "id": task_id, 230 | "description": description, 231 | "status": "failed", 232 | }, 233 | "dopamine": dopamine, 234 | } 235 | ) 236 | 237 | # Update progress to 70% - ready for execution 238 | task_element.set("progress", "70") 239 | agent.plan_tree = ET.tostring(root, encoding="unicode") 240 | print("Progress updated to: 70% (ready for execution)") 241 | 242 | # Generate dopamine reward for successful action generation 243 | if hasattr(agent, "dopamine_reward"): 244 | dopamine = agent.dopamine_reward.generate_reward(75) 245 | else: 246 | from utils.feedback import DopamineReward 247 | 248 | agent.dopamine_reward = DopamineReward(agent.console) 249 | dopamine = agent.dopamine_reward.generate_reward(75) 250 | 251 | return format_xml_response( 252 | { 253 | "task": { 254 | "id": task_id, 255 | "description": description, 256 | "progress": "70", 257 | }, 258 | "actions": actions_xml, 259 | "plan_update": plan_update_xml if plan_update_xml else None, 260 | "dopamine": dopamine, 261 | } 262 | ) 263 | 264 | except Exception as e: 265 | return format_xml_response({"error": f"Error executing task: {str(e)}"}) 266 | -------------------------------------------------------------------------------- /src/agent_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import json 6 | from typing import Dict, List, Optional, Any, Tuple 7 | import litellm 8 | from rich.console import Console 9 | 10 | # Import refactored modules 11 | from src.agent.repository import analyze_repository 12 | from src.agent.plan import ( 13 | generate_plan, 14 | update_plan, 15 | check_dependencies, 16 | apply_plan_updates, 17 | ) 18 | from src.agent.task import execute_task 19 | from src.utils.xml_operations import ( 20 | extract_xml_from_response, 21 | format_xml_response, 22 | pretty_format_xml, 23 | ) 24 | from src.utils.xml_tools import extract_xml_from_response as extract_xml_alt 25 | from src.utils.feedback import DopamineReward 26 | 27 | from src.agent.core import Agent 28 | from typing import List, Optional 29 | import sys 30 | import litellm 31 | from rich.console import Console 32 | 33 | 34 | def main(): 35 | """Stream the reasoning process from the model and return the final response""" 36 | messages = [{"role": "user", "content": prompt}] 37 | 38 | # Print the full message being sent to the model 39 | print("\n=== Message Sent to Model ===\n") 40 | print(f"Model: {self.model_name}") 41 | print(prompt) 42 | print("\n=== End Message ===\n") 43 | 44 | # Get terminal height and add that many newlines to preserve history 45 | terminal_height = self._get_terminal_height() 46 | print("\n" * terminal_height) 47 | 48 | if not self.config["stream_reasoning"]: 49 | # Non-streaming mode 50 | try: 51 | response = litellm.completion( 52 | model=self.model_name, 53 | messages=messages, 54 | timeout=60, # Add timeout to prevent hanging 55 | ) 56 | return response.choices[0].message.content 57 | except Exception as e: 58 | print(f"Error in non-streaming mode: {e}") 59 | return f"Error: {str(e)}" 60 | 61 | # Streaming mode 62 | full_response = "" 63 | reasoning_output = "" 64 | 65 | try: 66 | # Add timeout to prevent hanging 67 | response = litellm.completion( 68 | model=self.model_name, messages=messages, stream=True, timeout=60 69 | ) 70 | 71 | # Track if we're in the reasoning phase 72 | reasoning_phase = True 73 | 74 | for chunk in response: 75 | # Handle regular content 76 | if hasattr(chunk.choices[0], "delta") and hasattr( 77 | chunk.choices[0].delta, "content" 78 | ): 79 | content = chunk.choices[0].delta.content 80 | if content: 81 | # We've transitioned to regular content 82 | reasoning_phase = False 83 | 84 | # Clean content of control characters 85 | clean_content = content.replace("\r", "").replace("\b", "") 86 | 87 | if self.stream_callback: 88 | self.stream_callback(clean_content, is_reasoning=False) 89 | else: 90 | # Print without any special formatting 91 | print(clean_content, end="", flush=True) 92 | full_response += clean_content 93 | 94 | # Handle reasoning content separately (for deepseek models) 95 | if hasattr(chunk.choices[0], "delta") and hasattr( 96 | chunk.choices[0].delta, "reasoning_content" 97 | ): 98 | reasoning = chunk.choices[0].delta.reasoning_content 99 | if reasoning: 100 | # Clean up control chars and handle newlines 101 | clean_reasoning = reasoning.replace("\r", "").replace("\b", "") 102 | 103 | # Use callback if available, otherwise use console 104 | if self.stream_callback: 105 | self.stream_callback(clean_reasoning, is_reasoning=True) 106 | else: 107 | # Use yellow color for reasoning 108 | self.console.print( 109 | f"[yellow]{clean_reasoning}[/yellow]", 110 | end="", 111 | highlight=False, 112 | ) 113 | reasoning_output += clean_reasoning 114 | 115 | print("\n") 116 | 117 | # Save reasoning to a file for reference 118 | if reasoning_output: 119 | try: 120 | with open("last_reasoning.txt", "w") as f: 121 | f.write(reasoning_output) 122 | except Exception as e: 123 | print(f"Warning: Could not save reasoning to file: {e}") 124 | 125 | return full_response 126 | 127 | except KeyboardInterrupt: 128 | print("\n\nOperation cancelled by user") 129 | return full_response 130 | except Exception as e: 131 | print(f"\nError during streaming: {e}") 132 | return full_response or f"Error: {str(e)}" 133 | 134 | 135 | # These methods are now imported from utils.xml_operations 136 | 137 | 138 | def main(): 139 | """Main function to handle command line arguments and run the agent""" 140 | # Check for model argument 141 | model_name = "openrouter/deepseek/deepseek-r1" # Default model 142 | 143 | # Look for --model or -m flag 144 | for i, arg in enumerate(sys.argv): 145 | if arg in ["--model", "-m"] and i + 1 < len(sys.argv): 146 | model_name = sys.argv[i + 1] 147 | # Remove these arguments 148 | sys.argv.pop(i) 149 | sys.argv.pop(i) 150 | break 151 | 152 | agent = Agent(model_name) 153 | 154 | if len(sys.argv) < 2: 155 | print("Usage: ./agent [--model MODEL_NAME] [arguments]") 156 | print("Commands:") 157 | print(" init - Initialize the agent") 158 | print( 159 | " plan [spec_file] - Generate a plan from specification (default: spec.md)" 160 | ) 161 | print(" display - Display the current plan") 162 | print( 163 | " update [--notes=text] [--progress=0-100] - Update task status" 164 | ) 165 | print(" execute - Execute a specific task") 166 | print(" review - Review code against specifications") 167 | print("\nOptions:") 168 | print( 169 | " --model, -m MODEL_NAME - Specify the model to use (default: openrouter/deepseek/deepseek-r1)" 170 | ) 171 | sys.exit(1) 172 | 173 | command = sys.argv[1] 174 | 175 | if command == "init": 176 | agent.initialize() 177 | print("Agent initialized successfully") 178 | 179 | elif command == "plan": 180 | # Use spec.md by default if no file specified 181 | spec_file = sys.argv[2] if len(sys.argv) > 2 else "spec.md" 182 | print(f"Using specification file: {spec_file}") 183 | try: 184 | with open(spec_file, "r") as f: 185 | spec = f.read() 186 | 187 | print(f"Using model: {agent.model_name}") 188 | agent.initialize() 189 | 190 | try: 191 | result = agent.generate_plan(spec) 192 | print(result) 193 | 194 | # Save the plan to a file 195 | with open("agent_plan.xml", "w") as f: 196 | f.write(result) 197 | print("Plan saved to agent_plan.xml") 198 | except KeyboardInterrupt: 199 | print("\nOperation cancelled by user") 200 | sys.exit(1) 201 | 202 | except FileNotFoundError: 203 | print(f"Error: Specification file '{spec_file}' not found") 204 | sys.exit(1) 205 | 206 | elif command == "display": 207 | # Load the plan from file 208 | try: 209 | with open("agent_plan.xml", "r") as f: 210 | xml_content = f.read() 211 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan") 212 | 213 | result = agent.display_plan_tree() 214 | print(result) 215 | 216 | except FileNotFoundError: 217 | print("Error: No plan file found. Generate a plan first.") 218 | sys.exit(1) 219 | 220 | elif command == "update": 221 | if len(sys.argv) < 4: 222 | print("Error: Missing task_id or status") 223 | sys.exit(1) 224 | 225 | task_id = sys.argv[2] 226 | status = sys.argv[3] 227 | 228 | # Check for progress and notes flags 229 | progress = None 230 | notes = None 231 | 232 | for i, arg in enumerate(sys.argv[4:], 4): 233 | if arg.startswith("--progress="): 234 | progress = arg.split("=")[1] 235 | elif arg.startswith("--notes="): 236 | notes = arg.split("=")[1] 237 | elif i == 4 and not arg.startswith("--"): 238 | # For backward compatibility, treat the fourth argument as notes 239 | notes = arg 240 | 241 | # Load the plan from file 242 | try: 243 | with open("agent_plan.xml", "r") as f: 244 | xml_content = f.read() 245 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan") 246 | 247 | result = agent.update_plan(task_id, status, notes, progress) 248 | print(result) 249 | 250 | # Save the updated plan 251 | with open("agent_plan.xml", "w") as f: 252 | f.write(result) 253 | 254 | except FileNotFoundError: 255 | print("Error: No plan file found. Generate a plan first.") 256 | sys.exit(1) 257 | 258 | elif command == "execute": 259 | if len(sys.argv) < 3: 260 | print("Error: Missing task_id") 261 | sys.exit(1) 262 | 263 | task_id = sys.argv[2] 264 | 265 | # Load the plan from file 266 | try: 267 | with open("agent_plan.xml", "r") as f: 268 | xml_content = f.read() 269 | agent.plan_tree = agent.extract_xml_from_response(xml_content, "plan") 270 | 271 | try: 272 | result = agent.execute_task(task_id) 273 | print(result) 274 | 275 | # Save the actions to a file 276 | with open(f"agent_actions_{task_id}.xml", "w") as f: 277 | f.write(result) 278 | print(f"Actions saved to agent_actions_{task_id}.xml") 279 | 280 | # Save the updated plan 281 | with open("agent_plan.xml", "w") as f: 282 | f.write(agent.format_xml_response({"plan": agent.plan_tree})) 283 | except KeyboardInterrupt: 284 | print("\nOperation cancelled by user") 285 | sys.exit(1) 286 | 287 | except FileNotFoundError: 288 | print("Error: No plan file found. Generate a plan first.") 289 | sys.exit(1) 290 | 291 | elif command == "interactive": 292 | print(f"Starting interactive mode with model: {agent.model_name}") 293 | agent.initialize() 294 | 295 | while True: 296 | try: 297 | user_input = input("\n> ") 298 | if user_input.lower() in ["exit", "quit", "q"]: 299 | break 300 | 301 | # Simple command processing 302 | if user_input.startswith("/"): 303 | parts = user_input[1:].split() 304 | cmd = parts[0] if parts else "" 305 | 306 | if cmd == "plan" and len(parts) > 1: 307 | spec_file = parts[1] 308 | try: 309 | with open(spec_file, "r") as f: 310 | spec = f.read() 311 | result = agent.generate_plan(spec) 312 | print(result) 313 | except FileNotFoundError: 314 | print(f"Error: File '{spec_file}' not found") 315 | 316 | elif cmd == "display": 317 | result = agent.display_plan_tree() 318 | print(result) 319 | 320 | elif cmd == "execute" and len(parts) > 1: 321 | task_id = parts[1] 322 | result = agent.execute_task(task_id) 323 | print(result) 324 | 325 | elif cmd == "help": 326 | print("Available commands:") 327 | print( 328 | " /plan - Generate a plan from specification" 329 | ) 330 | print(" /display - Display the current plan") 331 | print(" /execute - Execute a specific task") 332 | print(" /help - Show this help") 333 | print(" exit, quit, q - Exit interactive mode") 334 | 335 | else: 336 | print("Unknown command. Type /help for available commands.") 337 | 338 | # Treat as a prompt to the model 339 | else: 340 | response = agent.stream_reasoning(user_input) 341 | # No need to print response as it's already streamed 342 | 343 | except KeyboardInterrupt: 344 | print("\nUse 'exit' or 'q' to quit") 345 | except Exception as e: 346 | print(f"Error: {e}") 347 | 348 | else: 349 | print(f"Error: Unknown command '{command}'") 350 | sys.exit(1) 351 | 352 | 353 | if __name__ == "__main__": 354 | main() 355 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration management for the agent. 3 | """ 4 | 5 | import os 6 | import json 7 | from typing import Dict, Any, Optional 8 | 9 | DEFAULT_CONFIG = { 10 | "vim_mode": True, 11 | "stream_reasoning": True, 12 | "verbose": True, 13 | "default_model": "openrouter/deepseek/deepseek-r1", 14 | "history_size": 100, 15 | "model_aliases": { 16 | "flash": "openrouter/google/gemini-2.0-flash-001", 17 | "r1": "deepseek/deepseek-reasoner", 18 | "claude": "openrouter/anthropic/claude-3.7-sonnet", 19 | }, 20 | } 21 | 22 | CONFIG_PATH = os.path.expanduser("~/.config/agent/config.json") 23 | 24 | 25 | def load_config() -> Dict[str, Any]: 26 | """ 27 | Load configuration from file or return default. 28 | 29 | Returns: 30 | Configuration dictionary 31 | """ 32 | if os.path.exists(CONFIG_PATH): 33 | try: 34 | with open(CONFIG_PATH, "r") as f: 35 | config = json.load(f) 36 | # Merge with defaults for any missing keys 37 | return {**DEFAULT_CONFIG, **config} 38 | except Exception: 39 | return DEFAULT_CONFIG 40 | else: 41 | return DEFAULT_CONFIG 42 | 43 | 44 | def save_config(config: Dict[str, Any]) -> bool: 45 | """ 46 | Save configuration to file. 47 | 48 | Args: 49 | config: Configuration dictionary 50 | 51 | Returns: 52 | True if successful, False otherwise 53 | """ 54 | try: 55 | # Ensure directory exists 56 | os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) 57 | 58 | with open(CONFIG_PATH, "w") as f: 59 | json.dump(config, f, indent=2) 60 | return True 61 | except Exception: 62 | return False 63 | 64 | 65 | def update_config(key: str, value: Any) -> bool: 66 | """ 67 | Update a specific configuration value. 68 | 69 | Args: 70 | key: Configuration key 71 | value: New value 72 | 73 | Returns: 74 | True if successful, False otherwise 75 | """ 76 | config = load_config() 77 | config[key] = value 78 | return save_config(config) 79 | 80 | 81 | def get_config_value(key: str, default: Optional[Any] = None) -> Any: 82 | """ 83 | Get a specific configuration value. 84 | 85 | Args: 86 | key: Configuration key 87 | default: Default value if key not found 88 | 89 | Returns: 90 | Configuration value or default 91 | """ 92 | config = load_config() 93 | return config.get(key, default) 94 | -------------------------------------------------------------------------------- /src/interface/__init__.py: -------------------------------------------------------------------------------- 1 | """Interface package initialization.""" 2 | from .display import ( 3 | get_system_info, 4 | display_welcome, 5 | display_help, 6 | display_models, 7 | display_plan_tree, 8 | display_from_top, 9 | ) 10 | -------------------------------------------------------------------------------- /src/interface/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command-line interface for the agent. 3 | """ 4 | 5 | import os 6 | import sys 7 | import datetime 8 | from typing import List, Dict, Any 9 | from rich.console import Console 10 | from rich.panel import Panel 11 | from rich.markdown import Markdown 12 | 13 | from src.agent.core import Agent 14 | from src.interface.display import display_welcome, get_system_info 15 | from src.interface.commands import process_command 16 | from src.interface.input import process_user_input, save_chat_history 17 | from src.interface.input_handler import get_user_input 18 | from src.config import load_config 19 | 20 | 21 | def main(): 22 | """Main entry point for the agent CLI.""" 23 | # Initialize console 24 | console = Console() 25 | 26 | # Display welcome message 27 | system_info = get_system_info() 28 | display_welcome(console, system_info) 29 | 30 | # Initialize agent 31 | agent = Agent() 32 | 33 | # Load configuration 34 | config = load_config() 35 | 36 | # Set up history 37 | history_dir = os.path.expanduser("~/.config/agent/history") 38 | os.makedirs(history_dir, exist_ok=True) 39 | history_file = os.path.join(history_dir, "chat_history.json") 40 | 41 | # Initialize chat history 42 | chat_history = [] 43 | try: 44 | if os.path.exists(history_file): 45 | with open(history_file, "r") as f: 46 | import json 47 | 48 | chat_history = json.load(f) 49 | except Exception as e: 50 | console.print(f"[yellow]Could not load chat history: {e}[/yellow]") 51 | 52 | # Command history for up/down navigation 53 | command_history = [] 54 | 55 | # Multiline input mode flag and buffer 56 | multiline_input_mode = False 57 | multiline_input_buffer = [] 58 | 59 | # Main input loop 60 | while True: 61 | try: 62 | # Handle multiline input mode 63 | if multiline_input_mode: 64 | line = get_user_input(console, "... ", command_history, config=config) 65 | if line.strip() == "/end": 66 | multiline_input_mode = False 67 | if multiline_input_buffer: 68 | full_input = "\n".join(multiline_input_buffer) 69 | console.print( 70 | f"[dim]Processing {len(multiline_input_buffer)} lines of input...[/dim]" 71 | ) 72 | process_user_input( 73 | agent, full_input, chat_history, history_file, console 74 | ) 75 | multiline_input_buffer.clear() 76 | else: 77 | console.print("[yellow]No input to process[/yellow]") 78 | else: 79 | multiline_input_buffer.append(line) 80 | continue 81 | 82 | # Get user input 83 | user_input = get_user_input(console, "> ", command_history, config=config) 84 | 85 | # Add to command history if not empty 86 | if user_input.strip() and ( 87 | not command_history or user_input != command_history[-1] 88 | ): 89 | command_history.append(user_input) 90 | # Trim history if needed 91 | if len(command_history) > config.get("history_size", 100): 92 | command_history.pop(0) 93 | 94 | # Check for empty input 95 | if not user_input.strip(): 96 | continue 97 | 98 | # Check for slash commands 99 | if user_input.startswith("/"): 100 | # Split the command and arguments 101 | parts = user_input[1:].split() 102 | process_command( 103 | agent, 104 | parts, 105 | chat_history, 106 | history_file, 107 | console, 108 | multiline_input_mode, 109 | multiline_input_buffer, 110 | ) 111 | else: 112 | # Process as regular input to the model 113 | process_user_input( 114 | agent, user_input, chat_history, history_file, console 115 | ) 116 | 117 | except KeyboardInterrupt: 118 | console.print("\n[bold yellow]Operation cancelled by user[/bold yellow]") 119 | continue 120 | except EOFError: 121 | console.print("\n[bold blue]Exiting...[/bold blue]") 122 | break 123 | except Exception as e: 124 | console.print(f"[bold red]Error:[/bold red] {str(e)}") 125 | import traceback 126 | 127 | console.print(traceback.format_exc()) 128 | -------------------------------------------------------------------------------- /src/interface/display.py: -------------------------------------------------------------------------------- 1 | """Display formatting for the agent interface.""" 2 | 3 | import platform 4 | from typing import Dict 5 | from rich.console import Console 6 | from rich.panel import Panel 7 | 8 | 9 | def get_system_info() -> Dict[str, str]: 10 | """Get basic system information.""" 11 | return { 12 | "platform": platform.platform(), 13 | "python": platform.python_version(), 14 | "shell": platform.system(), 15 | } 16 | 17 | 18 | def display_welcome(console: Console, system_info: Dict[str, str] = None): 19 | """Display welcome message.""" 20 | if system_info is None: 21 | system_info = get_system_info() 22 | 23 | console.print( 24 | Panel.fit( 25 | "[bold blue]Agent Interface[/bold blue]\n" 26 | f"Running on: {system_info['platform']} | Python {system_info['python']}", 27 | title="Welcome", 28 | border_style="blue", 29 | ) 30 | ) 31 | 32 | 33 | def display_help(console: Console): 34 | """Display available commands.""" 35 | console.print("[bold]Available Commands:[/bold]") 36 | console.print("- /help: Show this help") 37 | console.print("- /exit: Exit the interface") 38 | console.print("- /models: Show available models") 39 | console.print("- /plan: Generate or display plan") 40 | console.print("- /execute: Execute a task") 41 | 42 | 43 | def display_models(agent, console: Console): 44 | """Display available models.""" 45 | model_name = agent.model_name if agent else "No model selected" 46 | console.print(f"[bold blue]Current model:[/bold blue] {model_name}") 47 | 48 | 49 | def display_plan_tree(console: Console, xml_content: str): 50 | """Display plan tree from XML.""" 51 | if not xml_content: 52 | console.print("[bold red]Error: No plan content[/bold red]") 53 | return 54 | 55 | console.print("[bold blue]Plan Tree:[/bold blue]") 56 | console.print(xml_content) 57 | 58 | 59 | def display_from_top(console: Console, content: str, _preserve_history: bool = True): 60 | """ 61 | Display content without clearing terminal history. 62 | 63 | Args: 64 | console: Rich console instance 65 | content: Content to display 66 | preserve_history: Not used, kept for backward compatibility 67 | """ 68 | console.print(content) 69 | -------------------------------------------------------------------------------- /src/interface/input.py: -------------------------------------------------------------------------------- 1 | """ 2 | Input handling for the agent interface. 3 | """ 4 | 5 | import os 6 | import datetime 7 | import json 8 | import xml.etree.ElementTree as ET 9 | from typing import List, Dict, Any, Optional, Callable 10 | from rich.console import Console 11 | 12 | from src.interface.display import get_system_info 13 | from src.interface.chat import process_chat_response 14 | 15 | 16 | def process_user_input( 17 | agent, 18 | user_input: str, 19 | chat_history: List[Dict[str, Any]], 20 | history_file: str, 21 | console: Console, 22 | ): 23 | """Process user input and send to the model.""" 24 | 25 | # Add user message to history 26 | timestamp = datetime.datetime.now().isoformat() 27 | chat_history.append({"role": "user", "content": user_input, "timestamp": timestamp}) 28 | save_chat_history(chat_history, history_file) 29 | 30 | # Format history for the prompt 31 | formatted_history = _format_history_for_prompt(chat_history) 32 | 33 | # Get persistent memory 34 | memory_content = _load_persistent_memory() 35 | 36 | # Get system information 37 | system_info = get_system_info() 38 | 39 | # Import the input schema formatter 40 | from src.utils.input_schema import format_input_message 41 | 42 | # Format the message with XML tags using the schema 43 | formatted_input = format_input_message( 44 | message=user_input, 45 | system_info=system_info, 46 | memory=memory_content, 47 | history=formatted_history, 48 | ) 49 | 50 | # Construct a prompt that instructs the model to respond in XML format 51 | from src.interface.chat import process_chat_message 52 | 53 | prompt = process_chat_message( 54 | formatted_input, 55 | formatted_history, 56 | memory_content, 57 | system_info, 58 | getattr(agent, "config", {}), 59 | ) 60 | 61 | try: 62 | # Set a callback to handle streaming in the interface 63 | def stream_callback(content, is_reasoning=False): 64 | if is_reasoning: 65 | # Use yellow color for reasoning tokens 66 | console.print(f"[yellow]{content}[/yellow]", end="") 67 | else: 68 | # Use rich for normal content 69 | console.print(content, end="", highlight=False) 70 | 71 | # Pass the callback to the agent 72 | agent.stream_callback = stream_callback 73 | response = agent.stream_reasoning(prompt) 74 | 75 | # Process the response 76 | process_chat_response( 77 | agent, 78 | console, 79 | response, 80 | chat_history, 81 | _update_persistent_memory, 82 | _get_terminal_height, 83 | _load_persistent_memory, 84 | _format_history_for_prompt, 85 | lambda: save_chat_history(chat_history, history_file), 86 | ) 87 | except KeyboardInterrupt: 88 | console.print("\n[bold yellow]Operation cancelled by user[/bold yellow]") 89 | 90 | 91 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str): 92 | """Save chat history to file.""" 93 | try: 94 | # Ensure directory exists 95 | os.makedirs(os.path.dirname(history_file), exist_ok=True) 96 | 97 | with open(history_file, "w") as f: 98 | json.dump(chat_history, f, indent=2) 99 | except Exception as e: 100 | print(f"Could not save chat history: {e}") 101 | 102 | 103 | def _format_history_for_prompt(chat_history: List[Dict[str, Any]]) -> str: 104 | """Format chat history for inclusion in the prompt.""" 105 | # Limit history to last 10 messages to avoid context overflow 106 | recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history 107 | 108 | formatted_history = [] 109 | for msg in recent_history: 110 | role = msg["role"] 111 | content = msg["content"] 112 | timestamp = msg.get("timestamp", "") 113 | 114 | # Format as XML 115 | entry = f'' 116 | 117 | # For assistant messages, try to extract just the message part to keep history cleaner 118 | if role == "assistant": 119 | from src.utils.xml_tools import extract_xml_from_response 120 | 121 | message_xml = extract_xml_from_response(content, "message") 122 | if message_xml: 123 | try: 124 | root = ET.fromstring(message_xml) 125 | message_text = root.text if root.text else "" 126 | entry += f"{message_text}" 127 | except ET.ParseError: 128 | entry += f"{content}" 129 | else: 130 | entry += f"{content}" 131 | else: 132 | entry += f"{content}" 133 | 134 | entry += "" 135 | formatted_history.append(entry) 136 | 137 | return "\n".join(formatted_history) 138 | 139 | 140 | def _load_persistent_memory() -> str: 141 | """Load memory from file.""" 142 | memory_file = "agent_memory.xml" 143 | try: 144 | if os.path.exists(memory_file): 145 | with open(memory_file, "r") as f: 146 | return f.read() 147 | else: 148 | # Create default memory structure - simple and flexible 149 | default_memory = ( 150 | "\n \n" 151 | ) 152 | with open(memory_file, "w") as f: 153 | f.write(default_memory) 154 | return default_memory 155 | except Exception as e: 156 | print(f"Could not load memory: {e}") 157 | return "" 158 | 159 | 160 | def _update_persistent_memory(memory_updates_xml): 161 | """Update memory based on model's instructions.""" 162 | if not memory_updates_xml: 163 | return 164 | 165 | try: 166 | memory_file = "agent_memory.xml" 167 | current_memory = _load_persistent_memory() 168 | 169 | # Parse the updates 170 | updates_root = ET.fromstring(memory_updates_xml) 171 | 172 | # Parse current memory 173 | try: 174 | memory_root = ET.fromstring(current_memory) 175 | except ET.ParseError: 176 | # If parsing fails, create a new memory structure 177 | memory_root = ET.Element("memory") 178 | 179 | # Process edits - simple search/replace approach 180 | for edit in updates_root.findall("./edit"): 181 | search_elem = edit.find("search") 182 | replace_elem = edit.find("replace") 183 | 184 | if search_elem is not None and replace_elem is not None: 185 | search_text = search_elem.text if search_elem.text else "" 186 | replace_text = replace_elem.text if replace_elem.text else "" 187 | 188 | # Convert memory to string for search/replace 189 | memory_str = ET.tostring(memory_root, encoding="unicode") 190 | 191 | if search_text in memory_str: 192 | # Replace the text 193 | memory_str = memory_str.replace(search_text, replace_text) 194 | 195 | # Parse the updated memory 196 | memory_root = ET.fromstring(memory_str) 197 | 198 | # Process additions - just add text directly to memory 199 | for append in updates_root.findall("./append"): 200 | append_text = append.text if append.text else "" 201 | if append_text: 202 | # Append to existing memory text 203 | if memory_root.text is None: 204 | memory_root.text = append_text 205 | else: 206 | memory_root.text += "\n" + append_text 207 | 208 | # Save the updated memory 209 | from src.utils.xml_tools import pretty_format_xml 210 | 211 | updated_memory = pretty_format_xml(ET.tostring(memory_root, encoding="unicode")) 212 | with open(memory_file, "w") as f: 213 | f.write(updated_memory) 214 | 215 | print("Memory updated") 216 | 217 | except Exception as e: 218 | print(f"Error updating memory: {e}") 219 | 220 | 221 | def _get_terminal_height() -> int: 222 | """Get the terminal height for proper screen clearing.""" 223 | try: 224 | import shutil 225 | 226 | terminal_size = shutil.get_terminal_size() 227 | return terminal_size.lines 228 | except Exception: 229 | # Fallback to a reasonable default if we can't get the terminal size 230 | return 40 231 | 232 | 233 | def _format_history_for_prompt(chat_history: List[Dict[str, Any]]) -> str: 234 | """ 235 | Format chat history for inclusion in the prompt. 236 | 237 | Args: 238 | chat_history: List of chat history entries 239 | 240 | Returns: 241 | Formatted history string 242 | """ 243 | formatted_history = [] 244 | 245 | # Get the last few messages (up to 10) 246 | recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history 247 | 248 | for entry in recent_history: 249 | role = entry.get("role", "unknown") 250 | content = entry.get("content", "") 251 | formatted_history.append(f'{content}') 252 | 253 | return "\n".join(formatted_history) 254 | 255 | 256 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str): 257 | """ 258 | Save chat history to file. 259 | 260 | Args: 261 | chat_history: List of chat history entries 262 | history_file: Path to the history file 263 | """ 264 | try: 265 | # Ensure directory exists 266 | os.makedirs(os.path.dirname(history_file), exist_ok=True) 267 | 268 | with open(history_file, "w") as f: 269 | import json 270 | 271 | json.dump(chat_history, f, indent=2) 272 | except Exception as e: 273 | print(f"Could not save chat history: {e}") 274 | 275 | 276 | from src.utils.helpers import load_persistent_memory 277 | 278 | def _update_persistent_memory(memory_updates_xml): 279 | """ 280 | Update the persistent memory with the provided updates. 281 | 282 | Args: 283 | memory_updates_xml: XML string containing memory updates 284 | """ 285 | try: 286 | # Parse the memory updates 287 | updates_root = ET.fromstring(memory_updates_xml) 288 | 289 | # Load the current memory 290 | memory_content = _load_persistent_memory() 291 | memory_root = ET.fromstring(memory_content) 292 | 293 | # Process edits 294 | for edit in updates_root.findall("./edit"): 295 | search = edit.find("./search") 296 | replace = edit.find("./replace") 297 | 298 | if search is not None and replace is not None: 299 | search_text = search.text if search.text else "" 300 | replace_text = replace.text if replace.text else "" 301 | 302 | # Convert memory to string for search/replace 303 | memory_str = ET.tostring(memory_root, encoding="unicode") 304 | memory_str = memory_str.replace(search_text, replace_text) 305 | 306 | # Parse back to XML 307 | memory_root = ET.fromstring(memory_str) 308 | 309 | # Process appends 310 | for append in updates_root.findall("./append"): 311 | append_text = append.text if append.text else "" 312 | 313 | # Create a temporary root to parse the append text 314 | try: 315 | # Try to parse as XML first 316 | append_elem = ET.fromstring(f"{append_text}") 317 | for child in append_elem: 318 | memory_root.append(child) 319 | except ET.ParseError: 320 | # If not valid XML, add as text node to a new element 321 | new_elem = ET.SubElement(memory_root, "entry") 322 | new_elem.text = append_text 323 | new_elem.set("timestamp", datetime.datetime.now().isoformat()) 324 | 325 | # Save the updated memory 326 | with open("agent_memory.xml", "w") as f: 327 | f.write(ET.tostring(memory_root, encoding="unicode")) 328 | 329 | except ET.ParseError as e: 330 | print(f"Could not parse memory updates XML: {e}") 331 | except Exception as e: 332 | print(f"Error updating memory: {e}") 333 | 334 | 335 | -------------------------------------------------------------------------------- /src/interface/input_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Input handler for the agent CLI with Vim-like functionality. 3 | """ 4 | 5 | from typing import List, Dict, Any, Optional 6 | from rich.console import Console 7 | 8 | # Try to import the Vim input module, fall back to regular input if not available 9 | try: 10 | from src.interface.vim_input import get_vim_input 11 | 12 | VIM_INPUT_AVAILABLE = True 13 | except ImportError: 14 | VIM_INPUT_AVAILABLE = False 15 | 16 | 17 | def get_user_input( 18 | console: Console, 19 | prompt: str = "> ", 20 | history: List[str] = None, 21 | vim_mode: bool = True, 22 | config: Dict[str, Any] = None, 23 | ) -> str: 24 | """ 25 | Get input from the user with optional Vim-like interface. 26 | 27 | Args: 28 | console: Rich console instance 29 | prompt: Input prompt to display 30 | history: Command history 31 | vim_mode: Whether to use Vim-like input mode 32 | config: Configuration dictionary 33 | 34 | Returns: 35 | User input string 36 | """ 37 | # Check config for vim_mode setting if provided 38 | if config is not None and "vim_mode" in config: 39 | vim_mode = config["vim_mode"] 40 | 41 | try: 42 | if vim_mode and VIM_INPUT_AVAILABLE: 43 | console.print(f"[dim]{prompt}[/dim]", end="") 44 | return get_vim_input(console, history or []) 45 | else: 46 | # Fall back to regular input 47 | return console.input(prompt) 48 | except Exception as e: 49 | console.print(f"[bold red]Error getting input: {str(e)}[/bold red]") 50 | # Fall back to basic input on error 51 | return input(prompt) 52 | 53 | 54 | def process_input_with_history( 55 | input_text: str, history: List[str], max_history: int = 100 56 | ) -> None: 57 | """ 58 | Process input and update history. 59 | 60 | Args: 61 | input_text: The input text to process 62 | history: The history list to update 63 | max_history: Maximum history items to keep 64 | """ 65 | if input_text and (not history or input_text != history[-1]): 66 | history.append(input_text) 67 | 68 | # Trim history if needed 69 | if len(history) > max_history: 70 | history.pop(0) 71 | -------------------------------------------------------------------------------- /src/interface/interface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Make sure this file is executable with: chmod +x interface.py 3 | 4 | import sys 5 | import threading 6 | import shutil 7 | from typing import List, Dict, Any, Tuple, Optional 8 | import xml.etree.ElementTree as ET 9 | from rich.console import Console 10 | from rich.prompt import Prompt 11 | 12 | # Import other interface modules 13 | from src.interface.commands import process_command 14 | from src.interface.display import display_welcome, get_system_info 15 | from src.interface.actions import ( 16 | execute_action, 17 | execute_file_edit, 18 | execute_shell_command, 19 | ) 20 | from src.interface.input import process_user_input, save_chat_history 21 | from src.agent.core import Agent 22 | 23 | 24 | class AgentInterface: 25 | def __init__(self): 26 | self.console = Console() 27 | from src.config import load_config 28 | 29 | config = load_config() 30 | model_name = config.get("default_model", "openrouter/deepseek/deepseek-r1") 31 | self.agent = Agent(model_name=model_name) 32 | self.current_plan = None 33 | self.model_aliases = { 34 | "flash": "openrouter/google/gemini-2.0-flash-001", 35 | "r1": "deepseek/deepseek-reasoner", 36 | "claude": "openrouter/anthropic/claude-3.7-sonnet", 37 | } 38 | self.chat_history = [] 39 | self.history_file = "chat_history.json" 40 | self.system_info = get_system_info() 41 | self.multiline_input_buffer = [] 42 | self.multiline_input_mode = False 43 | self.load_chat_history() 44 | 45 | def load_chat_history(self): 46 | """Load chat history from file if it exists""" 47 | try: 48 | if os.path.exists(self.history_file): 49 | with open(self.history_file, "r") as f: 50 | self.chat_history = json.load(f) 51 | self.console.print( 52 | f"[dim]Loaded {len(self.chat_history)} previous messages[/dim]" 53 | ) 54 | except Exception as e: 55 | self.console.print(f"[dim]Could not load chat history: {e}[/dim]") 56 | self.chat_history = [] 57 | 58 | def save_chat_history(self): 59 | """Save chat history to file""" 60 | from src.interface.input import save_chat_history 61 | 62 | save_chat_history(self.chat_history, self.history_file) 63 | 64 | # These methods have been moved to the input module 65 | 66 | def _get_terminal_height(self) -> int: 67 | """Get the terminal height for proper screen clearing""" 68 | try: 69 | import shutil 70 | 71 | terminal_size = shutil.get_terminal_size() 72 | return terminal_size.lines 73 | except Exception: 74 | # Fallback to a reasonable default if we can't get the terminal size 75 | return 40 76 | 77 | def run_command(self, command: List[str], is_slash_command: bool = True): 78 | """Run a command and handle the result""" 79 | process_command( 80 | self.agent, 81 | command, 82 | self.chat_history, 83 | self.history_file, 84 | self.console, 85 | self.multiline_input_mode, 86 | self.multiline_input_buffer, 87 | ) 88 | 89 | def chat_with_model(self, message: str): 90 | """Send a message directly to the model and handle the response""" 91 | # Initialize agent if not already done 92 | if not self.agent.repository_info: 93 | with self.console.status("[bold blue]Initializing agent...[/bold blue]"): 94 | self.agent.initialize() 95 | 96 | # Use the process_user_input function from input module 97 | process_user_input( 98 | self.agent, message, self.chat_history, self.history_file, self.console 99 | ) 100 | 101 | def run_interactive(self): 102 | """Run the interface in interactive mode""" 103 | display_welcome(self.console, self.system_info) 104 | 105 | while True: 106 | try: 107 | # Handle multiline input mode 108 | if self.multiline_input_mode: 109 | user_input = Prompt.ask("\n[bold yellow]paste>[/bold yellow]") 110 | 111 | # Check for end of multiline input 112 | if user_input.lower() in ["/end", "/done", "/finish"]: 113 | self.multiline_input_mode = False 114 | 115 | # Process the collected input 116 | if self.multiline_input_buffer: 117 | full_input = "\n".join(self.multiline_input_buffer) 118 | self.console.print( 119 | f"[dim]Processing {len(self.multiline_input_buffer)} lines of input...[/dim]" 120 | ) 121 | self.chat_with_model(full_input) 122 | self.multiline_input_buffer = [] 123 | else: 124 | self.console.print("[yellow]No input to process[/yellow]") 125 | else: 126 | # Add to buffer 127 | self.multiline_input_buffer.append(user_input) 128 | self.console.print( 129 | f"[dim]Line {len(self.multiline_input_buffer)} added[/dim]" 130 | ) 131 | else: 132 | # Normal single-line input mode 133 | user_input = Prompt.ask("\n[bold blue]>[/bold blue]") 134 | 135 | if user_input.lower() in [ 136 | "exit", 137 | "quit", 138 | "q", 139 | "/exit", 140 | "/quit", 141 | "/q", 142 | ]: 143 | self.console.print("[bold blue]Exiting...[/bold blue]") 144 | sys.exit(0) 145 | 146 | # Check if this is a paste command 147 | if user_input.lower() == "/paste": 148 | self.multiline_input_mode = True 149 | self.multiline_input_buffer = [] 150 | self.console.print( 151 | "[bold yellow]Entering multiline paste mode. Type /end when finished.[/bold yellow]" 152 | ) 153 | continue 154 | 155 | # Check if this is a slash command 156 | if user_input.startswith("/"): 157 | # Remove the slash and split into command parts 158 | command = user_input[1:].strip().split() 159 | self.run_command(command) 160 | else: 161 | # Check if this might be a multiline paste 162 | if "\n" in user_input: 163 | lines = user_input.split("\n") 164 | self.console.print( 165 | f"[dim]Detected multiline paste with {len(lines)} lines[/dim]" 166 | ) 167 | self.chat_with_model(user_input) 168 | else: 169 | # Treat as direct chat with the model 170 | self.chat_with_model(user_input) 171 | 172 | except KeyboardInterrupt: 173 | if self.multiline_input_mode: 174 | self.console.print( 175 | "\n[bold yellow]Cancelling multiline input[/bold yellow]" 176 | ) 177 | self.multiline_input_mode = False 178 | self.multiline_input_buffer = [] 179 | else: 180 | self.console.print("\n[bold yellow]Exiting...[/bold yellow]") 181 | sys.exit(0) 182 | except EOFError: # Handle Ctrl+D 183 | self.console.print("\n[bold yellow]Exiting...[/bold yellow]") 184 | sys.exit(0) 185 | except Exception as e: 186 | self.console.print(f"[bold red]Error:[/bold red] {e}") 187 | 188 | 189 | def main(): 190 | """Main function to run the agent interface""" 191 | interface = AgentInterface() 192 | interface.run_interactive() 193 | 194 | 195 | if __name__ == "__main__": 196 | main() 197 | -------------------------------------------------------------------------------- /src/interface/vim_input.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vim-like input interface for the agent CLI using Textual. 3 | """ 4 | 5 | from enum import Enum 6 | from typing import List, Dict, Any, Callable, Optional 7 | 8 | from rich.console import Console 9 | from textual.app import App 10 | from textual.widgets import Input 11 | from textual.events import Key 12 | 13 | # Import necessary modules for input processing 14 | from src.interface.display import get_system_info 15 | 16 | 17 | class Mode(Enum): 18 | NORMAL = "normal" 19 | INSERT = "insert" 20 | 21 | 22 | class VimInput(App): 23 | """A Vim-like input widget using Textual.""" 24 | 25 | def __init__( 26 | self, 27 | console: Console, 28 | history: List[str] = None, 29 | on_submit: Callable[[str], None] = None, 30 | ): 31 | super().__init__() 32 | self.console = console 33 | self.history = history or [] 34 | self.history_index = len(self.history) 35 | self.on_submit = on_submit 36 | self.mode = Mode.INSERT 37 | self.result: Optional[str] = None 38 | 39 | def compose(self): 40 | """Create child widgets.""" 41 | self.input = Input(placeholder="Enter command (Esc for normal mode)") 42 | yield self.input 43 | 44 | def on_mount(self): 45 | """Called when app is mounted.""" 46 | self.input.focus() 47 | 48 | def on_key(self, event: Key): 49 | """Handle key events.""" 50 | # Handle mode switching 51 | if event.key == "escape": 52 | self.mode = Mode.NORMAL 53 | self.input.placeholder = "[Normal Mode] j/k: history, i: insert, :/search" 54 | return 55 | 56 | if self.mode == Mode.NORMAL: 57 | self._handle_normal_mode(event) 58 | # In insert mode, let default handlers work 59 | 60 | def _handle_normal_mode(self, event: Key): 61 | """Handle keys in normal mode.""" 62 | key = event.key 63 | 64 | if key == "i": 65 | # Switch to insert mode 66 | self.mode = Mode.INSERT 67 | self.input.placeholder = "Enter command (Esc for normal mode)" 68 | 69 | elif key == "a": 70 | # Append (move cursor to end and switch to insert) 71 | self.mode = Mode.INSERT 72 | self.input.placeholder = "Enter command (Esc for normal mode)" 73 | # Move cursor to end 74 | self.input.cursor_position = len(self.input.value) 75 | 76 | elif key == "0": 77 | # Move to beginning of line 78 | self.input.cursor_position = 0 79 | 80 | elif key == "$": 81 | # Move to end of line 82 | self.input.cursor_position = len(self.input.value) 83 | 84 | elif key == "j": 85 | # Navigate down in history 86 | if self.history and self.history_index < len(self.history) - 1: 87 | self.history_index += 1 88 | self.input.value = self.history[self.history_index] 89 | 90 | elif key == "k": 91 | # Navigate up in history 92 | if self.history and self.history_index > 0: 93 | self.history_index -= 1 94 | self.input.value = self.history[self.history_index] 95 | 96 | elif key == "d": 97 | # dd to clear line 98 | if getattr(self, "_last_key", None) == "d": 99 | self.input.value = "" 100 | self._last_key = "d" 101 | return 102 | 103 | elif key == "x": 104 | # Delete character under cursor 105 | if self.input.value and self.input.cursor_position < len(self.input.value): 106 | pos = self.input.cursor_position 107 | self.input.value = self.input.value[:pos] + self.input.value[pos + 1 :] 108 | 109 | elif key == "enter": 110 | # Submit in normal mode too 111 | self.result = self.input.value 112 | self.exit() 113 | 114 | # Store last key for combinations like dd 115 | self._last_key = key 116 | 117 | def on_input_submitted(self): 118 | """Handle input submission.""" 119 | self.result = self.input.value 120 | self.exit() 121 | 122 | 123 | def get_vim_input( 124 | console: Console, history: List[str] = None, on_submit: Callable[[str], None] = None 125 | ) -> str: 126 | """ 127 | Display a Vim-like input prompt and return the entered text. 128 | 129 | Args: 130 | console: Rich console instance 131 | history: Command history list 132 | on_submit: Callback for when input is submitted 133 | 134 | Returns: 135 | The entered text 136 | """ 137 | app = VimInput(console, history, on_submit) 138 | app.run() 139 | return app.result or "" 140 | -------------------------------------------------------------------------------- /src/run_agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def main(): 5 | """Entry point for the agent interface""" 6 | from src.interface.interface import main as interface_main 7 | 8 | interface_main() 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /src/spec.md: -------------------------------------------------------------------------------- 1 | - no actions visible under the model suggests the following actions 2 | - don't think we need an action xml tag, all of those are kind of actions 3 | 4 | 5 | ### Project Initialization 6 | - Write a `spec.md` for each project to define requirements. 7 | - Use `aider` to generate structured output based on the spec. 8 | 9 | ### Task Management 10 | - Split tasks into small, manageable pieces to increase success rates. 11 | - Use `pytest` for testing and to identify failures. 12 | - Include feedback from test results in prompts for better coordination. 13 | 14 | ### Monitoring and Coordination 15 | - Use `tmux` for session management, starting new sessions with custom endings to avoid conflicts. 16 | - Display and refresh stats constantly for easy monitoring. 17 | - Coordinate with `agent-aider-worktree` without modifying it to preserve its integrity. 18 | 19 | ### Planning and Execution 20 | - Create a plan tree using XML to structure tasks. 21 | - Execute tasks based on the generated plan. 22 | - Track task dependencies and show progress. 23 | - Allow the agent to modify its own plan as needed. 24 | - Enable the agent to indicate task completion or request further input from the user. 25 | 26 | ### User Interaction 27 | - Implement an interactive mode with chat functionality for direct user interaction. 28 | - Require user confirmation for critical actions to ensure control. 29 | - Handle multi-line inputs properly to process pasted content effectively. 30 | 31 | ### Technical Implementation 32 | - Stream and display reasoning with proper formatting for transparency. 33 | - Manage terminal output to preserve history and avoid overwriting existing text. 34 | - Integrate with different models and APIs for flexibility. 35 | - Use `deepseekr1` for reasoning and evaluation of results. 36 | - Handle API overload issues, possibly by using native DeepSeek APIs when necessary. 37 | 38 | ### Code Maintenance 39 | - Keep complexity low to ensure maintainability. 40 | - Refactor code into smaller, independent modules for easier testing and development. 41 | - Fix bugs related to API overload and module imports. 42 | - Avoid modifying existing code like `agent-aider-worktree`; duplicate functionality if needed. 43 | 44 | ### Enhancements 45 | - Include system information (e.g., date, time, timezone) in the context for better awareness. 46 | - Implement multi-step execution to handle complex tasks over multiple interactions. 47 | - Maintain conversation history to provide context for the agent. 48 | - Add a vim-like interface for command navigation (e.g., using `j`, `k` to move through commands). 49 | - Implement persistent memory for the agent to retain and modify information over time. 50 | - Allow the agent to edit files using XML tags (e.g., search and replace with filename specifications). 51 | - Enable the agent to update its plan based on new information learned during execution. 52 | 53 | ### Context Management 54 | - Format input messages to the model in XML to set a consistent example. 55 | - Provide an XML schema for the agent’s responses to clarify expected output structure. 56 | - Exclude reasoning tokens from the model’s context to avoid confusion, as they weren’t part of training. 57 | - Truncate shell command outputs to the last 5000 characters per command to manage context size. 58 | - Log command execution details (e.g., auto-run, confirmed, rejected) for valuable context. 59 | 60 | ### Debugging 61 | - Print the complete message sent to the model for debugging purposes. 62 | - Remove unnecessary status messages (e.g., "generating plan", "end of reasoning") and rely on color-coding. 63 | 64 | ### Alternative Approaches 65 | - Explore using `litellm` with `deepseekr1` as an alternative to `aider` for building the agent. 66 | 67 | -------------------------------------------------------------------------------- /src/task_list.md: -------------------------------------------------------------------------------- 1 | # Agent Development Task List 2 | 3 | ## High Priority Tasks 4 | 5 | 1. **Implement Direct Model Chat** - Enhance the chat interface to allow direct interaction with the model. 6 | - Priority: HIGH 7 | - Status: Completed 8 | - Notes: Successfully implemented direct model chat functionality 9 | 10 | 2. **Fix Command Line Interface** - Make the agent command work globally. 11 | - Priority: HIGH 12 | - Status: Completed 13 | - Notes: Fixed setup.py to use entry_points instead of scripts for better compatibility 14 | 15 | ## Medium Priority Tasks 16 | 17 | 1. **Allow Agent to Modify Its Own Plan** - Enable the agent to update its plan based on new information. 18 | - Priority: HIGH 19 | - Status: Basic implementation 20 | - Notes: Need to improve the plan modification logic and validation 21 | 22 | 2. **Show Progress as Tasks are Completed** - Better visual feedback for task completion. 23 | - Priority: MEDIUM 24 | - Status: Basic implementation exists 25 | - Notes: Add progress bars and better status indicators 26 | 27 | 3. **Improve Memory Management** - Enhance the persistent memory system with better organization. 28 | - Priority: MEDIUM 29 | - Status: Not started 30 | - Notes: Add categorization, tagging, and priority levels to memory items 31 | 32 | 4. **Add Model Selection UI** - Create a better UI for selecting and switching between models. 33 | - Priority: MEDIUM 34 | - Status: Not started 35 | - Notes: Should show available models and allow easy switching 36 | 37 | ## Low Priority Tasks 38 | 39 | 1. **Implement Parallel Task Execution** - Allow multiple non-dependent tasks to be executed in parallel. 40 | - Priority: LOW 41 | - Status: Not started 42 | - Notes: Will require significant refactoring of execution logic 43 | 44 | 2. **Add Export/Import Functionality** - Allow plans to be exported and imported in different formats. 45 | - Priority: LOW 46 | - Status: Not started 47 | - Notes: Consider supporting JSON, YAML, and Markdown formats 48 | 49 | 3. **Add Vim-like Interface** - Implement vim-like navigation and editing in the interface. 50 | - Priority: LOW 51 | - Status: Mostly complete 52 | - Notes: Implemented mode switching (normal/insert), history navigation with j/k, cursor movement (0, $), and text manipulation commands (dd, x). Added configuration option to enable/disable. 53 | 54 | 4. **Add Textual UI Integration** - Consider integrating with Textual for a more advanced TUI. 55 | - Priority: LOW 56 | - Status: Not started 57 | - Notes: Would provide better layout and interaction capabilities 58 | 59 | 5. **Refactor Code for Maintainability** - Improve code organization and reduce complexity. 60 | - Priority: MEDIUM 61 | - Status: In progress 62 | - Notes: Added configuration management, improved input handling, and enhanced Vim interface 63 | 64 | 6. **Add Command Aliases** - Allow users to define custom aliases for common commands. 65 | - Priority: LOW 66 | - Status: Not started 67 | - Notes: Would improve user experience for frequent users 68 | 69 | 7. **Implement Undo Functionality** - Allow undoing actions and edits. 70 | - Priority: LOW 71 | - Status: Not started 72 | - Notes: Would require tracking action history and implementing reverse operations 73 | 74 | ## Completed Tasks 75 | 76 | 1. **Multi-step Execution** - Allow the agent to run for multiple steps with context preservation. 77 | - Priority: HIGH 78 | - Status: Completed 79 | - Notes: Implemented ability to continue execution with command output context 80 | 81 | 2. **Standardize XML Input Schema** - Create a consistent schema for input messages to the model. 82 | - Priority: HIGH 83 | - Status: Completed 84 | - Notes: Created input_schema.py with standardized format for all model interactions 85 | 86 | 3. **XML-Formatted Prompts** - Make all prompts to the model fully XML-formatted. 87 | - Priority: HIGH 88 | - Status: Completed 89 | - Notes: Restructured all prompts to use consistent XML formatting for better model understanding 90 | 91 | 2. **File Editing Functionality** - Add ability to edit files using search/replace. 92 | - Priority: HIGH 93 | - Status: Completed 94 | - Notes: Implemented file editing with search/replace functionality 95 | 96 | 3. **Add System Information Display** - Show relevant system information in the interface. 97 | - Priority: MEDIUM 98 | - Status: Completed 99 | - Notes: Added platform, Python version, and shell information to the welcome screen 100 | 101 | 4. **Add File Editing Confirmation** - Add confirmation for each file edit before execution. 102 | - Priority: HIGH 103 | - Status: Completed 104 | - Notes: Added confirmation dialog showing diff before applying changes 105 | 106 | 4. **Generate Output Without Clearing Terminal** - Ensure output preserves terminal history. 107 | - Priority: HIGH 108 | - Status: Completed 109 | - Notes: Removed terminal clearing functionality to preserve all previous output 110 | 111 | 5. **Fix System Info Display** - Ensure system information is properly displayed without errors. 112 | - Priority: HIGH 113 | - Status: Completed 114 | - Notes: Added error handling for system information retrieval 115 | 116 | 6. **Improve Multiline Input Handling** - Better handling of pasted multiline content. 117 | - Priority: MEDIUM 118 | - Status: Completed 119 | - Notes: Added dedicated paste mode and improved multiline detection 120 | 121 | 7. **Add Task Dependency Tracking** - Improve tracking of task dependencies and status updates. 122 | - Priority: HIGH 123 | - Status: Completed 124 | - Notes: Enhanced dependency resolution and automatic status updates 125 | 126 | 8. **Add Structured XML Input Format** - Allow users to send structured XML input to the agent. 127 | - Priority: HIGH 128 | - Status: Completed 129 | - Notes: Users can now send XML-formatted messages that match the response format 130 | 131 | 9. **Add Persistent Memory** - Give the agent ability to maintain and update persistent memory. 132 | - Priority: HIGH 133 | - Status: Completed 134 | - Notes: Agent can now store and update information across sessions 135 | 136 | 10. **Add Execution Status Tracking** - Allow the agent to indicate if a task is complete or needs user input. 137 | - Priority: MEDIUM 138 | - Status: Completed 139 | - Notes: Added execution_status XML tag to indicate completion status and user input needs 140 | 141 | 11. **Print Full Model Messages** - Show the complete messages being sent to the model. 142 | - Priority: MEDIUM 143 | - Status: Completed 144 | - Notes: Added display of full prompts for better debugging and transparency 145 | 146 | 12. **Preserve Terminal History** - Ensure terminal history is preserved when scrolling up. 147 | - Priority: MEDIUM 148 | - Status: Completed 149 | - Notes: Added newlines to preserve history when generating new output 150 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Package initialization for utils module 2 | # Package initialization for utils module 3 | -------------------------------------------------------------------------------- /src/utils/feedback.py: -------------------------------------------------------------------------------- 1 | """Feedback mechanisms for the agent, including dopamine rewards.""" 2 | 3 | from typing import Optional 4 | import random 5 | from rich.console import Console 6 | 7 | 8 | class DopamineReward: 9 | """Manages dopamine rewards for the agent based on performance.""" 10 | 11 | def __init__(self, console: Optional[Console] = None): 12 | self.console = console or Console() 13 | self.dopamine_level = 50.0 # Track as float for more precise calculations 14 | self.history = [] 15 | 16 | def generate_reward(self, quality_score: Optional[int] = None) -> str: 17 | """ 18 | Generate a dopamine reward message based on performance quality. 19 | 20 | Args: 21 | quality_score: Optional score from 0-100 indicating quality of performance 22 | If None, will use current dopamine level as base 23 | 24 | Returns: 25 | A dopamine reward message 26 | 27 | Examples: 28 | >>> reward = DopamineReward(Console()) 29 | >>> reward.generate_reward(95) 30 | '🌟 [bold green]DOPAMINE SURGE![/bold green] Exceptional work!' 31 | >>> reward.generate_reward(65) 32 | '🙂 [blue]DOPAMINE TRICKLE[/blue] Good progress.' 33 | """ 34 | if quality_score is None: 35 | # Use current dopamine level with small random variation 36 | quality_score = max(0, min(100, self.dopamine_level + random.randint(-10, 10))) 37 | 38 | self.dopamine_level = max(0, min(100, quality_score)) # Update dopamine level directly 39 | 40 | if quality_score >= 90: 41 | return "🌟 [bold green]DOPAMINE SURGE![/bold green] Exceptional work!" 42 | if quality_score >= 75: 43 | return "😊 [green]DOPAMINE BOOST![/green] Great job!" 44 | if quality_score >= 60: 45 | return "🙂 [blue]DOPAMINE TRICKLE[/blue] Good progress." 46 | if quality_score >= 40: 47 | return "😐 [yellow]DOPAMINE NEUTRAL[/yellow] Acceptable." 48 | if quality_score >= 20: 49 | return "😕 [orange]DOPAMINE DIP[/orange] Could be better." 50 | return "😟 [red]DOPAMINE LOW[/red] Needs improvement." 51 | 52 | def reward_for_xml_response(self, _response: str, observation: str) -> str: 53 | """ 54 | Analyze the XML response and observation to determine a reward. 55 | Updates dopamine level based on feedback quality. 56 | 57 | Args: 58 | response: The XML response from the agent 59 | observation: The user's observation/feedback 60 | 61 | Returns: 62 | A dopamine reward message 63 | """ 64 | # Simple heuristic based on positive/negative words in observation 65 | positive_words = [ 66 | "good", "great", "excellent", "perfect", "nice", "helpful", 67 | "useful", "correct", "right", "well", "thanks", "thank" 68 | ] 69 | negative_words = [ 70 | "bad", "wrong", "incorrect", "error", "mistake", "useless", 71 | "unhelpful", "poor", "terrible", "fail", "failed", "not working" 72 | ] 73 | 74 | observation_lower = observation.lower() 75 | positive_count = sum(1 for word in positive_words if word in observation_lower) 76 | negative_count = sum(1 for word in negative_words if word in observation_lower) 77 | 78 | # Calculate score and update dopamine level 79 | if positive_count + negative_count == 0: 80 | return self.generate_reward(None) # Neutral 81 | 82 | score = 100 * positive_count / (positive_count + negative_count) 83 | self._update_dopamine_level(score) 84 | return self.generate_reward(score) 85 | 86 | def _update_dopamine_level(self, score: float): 87 | """Update dopamine level using a moving average with decay factor.""" 88 | # Keep last 5 scores for smoothing 89 | self.history = (self.history + [score])[-5:] 90 | # Calculate weighted average with decay 91 | weights = [0.5**i for i in range(len(self.history),0,-1)] 92 | weighted_avg = sum(w*s for w,s in zip(weights, self.history)) / sum(weights) 93 | # Update dopamine level (clamped between 0-100) 94 | self.dopamine_level = max(0, min(100, weighted_avg)) 95 | return self.dopamine_level 96 | 97 | def get_current_dopamine_level(self) -> float: 98 | """Get the current dopamine level for prompt optimization.""" 99 | return self.dopamine_level 100 | -------------------------------------------------------------------------------- /src/utils/file_ops.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """File operation utilities.""" 3 | 4 | import os 5 | from typing import Tuple 6 | 7 | 8 | def read_file(path: str) -> Tuple[bool, str]: 9 | """ 10 | Read a file and return its contents. 11 | 12 | Args: 13 | path: Path to the file 14 | 15 | Returns: 16 | Tuple of (success, content_or_error_message) 17 | """ 18 | try: 19 | if not os.path.exists(path): 20 | return False, f"File not found: {path}" 21 | if not os.path.isfile(path): 22 | return False, f"Path is not a file: {path}" 23 | 24 | with open(path, "r", encoding='utf-8') as f: 25 | file_content = f.read() 26 | return True, file_content 27 | except FileNotFoundError: 28 | return False, f"File not found: {path}" 29 | except PermissionError: 30 | return False, f"Permission denied: {path}" 31 | except Exception as e: 32 | return False, f"Error reading file: {str(e)}" 33 | 34 | 35 | def write_file( 36 | path: str, file_content: str, create_dirs: bool = True 37 | ) -> Tuple[bool, str]: 38 | """ 39 | Write content to a file. 40 | 41 | Args: 42 | path: Path to the file 43 | content: Content to write 44 | create_dirs: Whether to create parent directories if they don't exist 45 | 46 | Returns: 47 | Tuple of (success, message) 48 | """ 49 | try: 50 | if os.path.exists(path) and not os.path.isfile(path): 51 | return False, f"Path exists but is not a file: {path}" 52 | 53 | # Create parent directories if needed 54 | if create_dirs: 55 | directory = os.path.dirname(path) 56 | if directory and not os.path.exists(directory): 57 | os.makedirs(directory, exist_ok=True) 58 | 59 | with open(path, "w", encoding='utf-8') as f: 60 | f.write(file_content) 61 | 62 | # Make executable if it's a Python file or has no extension 63 | if path.endswith(".py") or not os.path.splitext(path)[1]: 64 | os.chmod(path, 0o755) 65 | return True, f"File written and made executable: {path}" 66 | 67 | return True, f"File written: {path}" 68 | except PermissionError: 69 | return False, f"Permission denied: {path}" 70 | except Exception as e: 71 | return False, f"Error writing file: {str(e)}" 72 | 73 | 74 | def edit_file(path: str, search_text: str, replace_text: str) -> Tuple[bool, str]: 75 | """ 76 | Edit a file by replacing text. 77 | 78 | Args: 79 | path: Path to the file 80 | search: Text to search for 81 | replace: Text to replace with 82 | 83 | Returns: 84 | Tuple of (success, message) 85 | """ 86 | try: 87 | if not os.path.exists(path): 88 | return False, f"File not found: {path}" 89 | if not os.path.isfile(path): 90 | return False, f"Path is not a file: {path}" 91 | 92 | with open(path, "r", encoding='utf-8') as f: 93 | existing_content = f.read() 94 | 95 | if search_text not in existing_content: 96 | return False, f"Search text not found in {path}" 97 | 98 | new_content = existing_content.replace(search_text, replace_text, 1) 99 | 100 | with open(path, "w", encoding='utf-8') as f: 101 | f.write(new_content) 102 | 103 | return True, f"File edited: {path}" 104 | except PermissionError: 105 | return False, f"Permission denied: {path}" 106 | except Exception as e: 107 | return False, f"Error editing file: {str(e)}" 108 | 109 | 110 | def append_to_file(path: str, text_to_append: str) -> Tuple[bool, str]: 111 | """ 112 | Append content to a file. 113 | 114 | Args: 115 | path: Path to the file 116 | content: Content to append 117 | 118 | Returns: 119 | Tuple of (success, message) 120 | """ 121 | try: 122 | if not os.path.exists(path): 123 | return False, f"File not found: {path}" 124 | if not os.path.isfile(path): 125 | return False, f"Path is not a file: {path}" 126 | 127 | with open(path, "a", encoding='utf-8') as f: 128 | f.write(text_to_append) 129 | 130 | return True, f"Content appended to: {path}" 131 | except PermissionError: 132 | return False, f"Permission denied: {path}" 133 | except Exception as e: 134 | return False, f"Error appending to file: {str(e)}" 135 | 136 | 137 | if __name__ == "__main__": 138 | # Simple test when run directly 139 | import tempfile 140 | 141 | # Create a temporary file 142 | with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as temp: 143 | temp_path = temp.name 144 | 145 | # Test write 146 | success, msg = write_file(temp_path, "Hello, world!") 147 | print(f"Write: {success} - {msg}") 148 | 149 | # Test read 150 | success, content = read_file(temp_path) 151 | print(f"Read: {success} - {content}") 152 | 153 | # Test edit 154 | success, msg = edit_file(temp_path, "Hello", "Goodbye") 155 | print(f"Edit: {success} - {msg}") 156 | 157 | # Test append 158 | success, msg = append_to_file(temp_path, "\nAppended text") 159 | print(f"Append: {success} - {msg}") 160 | 161 | # Read final content 162 | success, content = read_file(temp_path) 163 | print(f"Final content: {content}") 164 | 165 | # Clean up 166 | os.unlink(temp_path) 167 | -------------------------------------------------------------------------------- /src/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """Common helper functions reused across the codebase.""" 2 | 3 | import os 4 | import json 5 | from typing import List, Dict, Any 6 | 7 | def load_persistent_memory() -> str: 8 | """ 9 | Load memory from file. 10 | 11 | Returns: 12 | Memory content as string 13 | """ 14 | memory_file = "agent_memory.xml" 15 | try: 16 | if os.path.exists(memory_file): 17 | with open(memory_file, "r") as f: 18 | return f.read() 19 | # Create default memory structure 20 | default_memory = "\n \n" 21 | with open(memory_file, "w") as f: 22 | f.write(default_memory) 23 | return default_memory 24 | except Exception as e: 25 | print(f"Could not load memory: {e}") 26 | return "" 27 | 28 | def save_chat_history(chat_history: List[Dict[str, Any]], history_file: str) -> None: 29 | """ 30 | Save chat history to file. 31 | 32 | Args: 33 | chat_history: List of chat history entries 34 | history_file: Path to the history file 35 | """ 36 | try: 37 | os.makedirs(os.path.dirname(history_file), exist_ok=True) 38 | with open(history_file, "w") as f: 39 | json.dump(chat_history, f, indent=2) 40 | except Exception as e: 41 | print(f"Could not save chat history: {e}") 42 | 43 | def get_terminal_height() -> int: 44 | """ 45 | Get terminal height in lines. 46 | 47 | Returns: 48 | Terminal height or 40 as default 49 | """ 50 | try: 51 | import shutil 52 | return shutil.get_terminal_size().lines 53 | except Exception: 54 | return 40 55 | -------------------------------------------------------------------------------- /src/utils/input_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | XML schema definitions for input messages to the model. 3 | """ 4 | 5 | from typing import Dict, Any, Optional, List 6 | 7 | INPUT_SCHEMA = """ 8 | 9 | 10 | 11 | 12 | 13 | Operating system and version 14 | Python version 15 | User's shell 16 | Current date 17 | 18 | User's timezone 19 | Current working directory 20 | 21 | 22 | 23 | User's input text 24 | 25 | 26 | 27 | Previously executed command 28 | Command output (truncated if very long) 29 | Command exit code 30 | Time taken to execute the command 31 | 32 | 33 | 34 | Current plan XML 35 | 36 | 37 | 38 | List of files in repository 39 | List of git branches 40 | Current git branch 41 | Git status output 42 | 43 | 44 | 45 | Persistent memory content 46 | 47 | 48 | 49 | Previous user message 50 | Previous assistant response 51 | 52 | 53 | 54 | 55 | Type of error that occurred 56 | Error message 57 | Stack trace if available 58 | 59 | 60 | 61 | """ 62 | 63 | RESPONSE_SCHEMA = """ 64 | Response text 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | """ 75 | 76 | 77 | def get_input_schema() -> str: 78 | """ 79 | Get the XML schema for input messages. 80 | 81 | Returns: 82 | XML schema string 83 | """ 84 | return INPUT_SCHEMA 85 | 86 | 87 | def get_response_schema() -> str: 88 | """ 89 | Get the XML schema for response messages. 90 | 91 | Returns: 92 | XML schema string 93 | """ 94 | return RESPONSE_SCHEMA 95 | 96 | 97 | def get_schema() -> str: 98 | """ 99 | Get the XML schema for response messages. 100 | 101 | Returns: 102 | XML schema string 103 | """ 104 | return RESPONSE_SCHEMA # Kept for backward compatibility 105 | 106 | 107 | def format_input_message( 108 | message: str, 109 | system_info: Dict[str, str], 110 | execution_context: Optional[Dict[str, Any]] = None, 111 | plan: Optional[str] = None, 112 | repository_info: Optional[Dict[str, Any]] = None, 113 | memory: Optional[str] = None, 114 | history: Optional[str] = None, 115 | error_context: Optional[Dict[str, str]] = None, 116 | ) -> str: 117 | """ 118 | Format an input message according to the XML schema. 119 | 120 | Args: 121 | message: User message 122 | system_info: System information dictionary 123 | execution_context: Optional execution context from previous commands 124 | plan: Optional plan XML 125 | repository_info: Optional repository information 126 | memory: Optional persistent memory content 127 | history: Optional conversation history 128 | error_context: Optional error information 129 | 130 | Returns: 131 | Formatted XML input message 132 | """ 133 | # Start with basic structure 134 | xml_parts = [ 135 | "", 136 | " ", 137 | ] 138 | 139 | # Add system info 140 | for key, value in system_info.items(): 141 | xml_parts.append(f" <{key}>{value}") 142 | 143 | xml_parts.append(" ") 144 | xml_parts.append(f" {message}") 145 | 146 | # Add execution context if provided 147 | if execution_context: 148 | xml_parts.append(" ") 149 | xml_parts.append( 150 | f" {execution_context.get('command', '')}" 151 | ) 152 | xml_parts.append(f" {execution_context.get('output', '')}") 153 | xml_parts.append( 154 | f" {execution_context.get('exit_code', 0)}" 155 | ) 156 | if "execution_time" in execution_context: 157 | xml_parts.append( 158 | f" {execution_context['execution_time']}" 159 | ) 160 | xml_parts.append(" ") 161 | 162 | # Add plan if provided 163 | if plan: 164 | xml_parts.append(f" {plan}") 165 | 166 | # Add repository info if provided 167 | if repository_info: 168 | xml_parts.append(" ") 169 | if "files" in repository_info: 170 | xml_parts.append(f" {repository_info['files']}") 171 | if "branches" in repository_info: 172 | xml_parts.append(f" {repository_info['branches']}") 173 | if "current_branch" in repository_info: 174 | xml_parts.append( 175 | f" {repository_info['current_branch']}" 176 | ) 177 | if "status" in repository_info: 178 | xml_parts.append(f" {repository_info['status']}") 179 | xml_parts.append(" ") 180 | 181 | # Add memory if provided 182 | if memory: 183 | xml_parts.append(f" {memory}") 184 | 185 | # Add history if provided 186 | if history: 187 | xml_parts.append(" ") 188 | xml_parts.append(f" {history}") 189 | xml_parts.append(" ") 190 | 191 | # Add error context if provided 192 | if error_context: 193 | xml_parts.append(" ") 194 | if "error_type" in error_context: 195 | xml_parts.append( 196 | f" {error_context['error_type']}" 197 | ) 198 | if "error_message" in error_context: 199 | xml_parts.append( 200 | f" {error_context['error_message']}" 201 | ) 202 | if "traceback" in error_context: 203 | xml_parts.append(f" {error_context['traceback']}") 204 | xml_parts.append(" ") 205 | 206 | xml_parts.append("") 207 | 208 | return "\n".join(xml_parts) 209 | 210 | 211 | def escape_xml_content(content: str) -> str: 212 | """ 213 | Escape special characters in XML content. 214 | 215 | Args: 216 | content: The string to escape 217 | 218 | Returns: 219 | Escaped string safe for XML inclusion 220 | """ 221 | if not content: 222 | return "" 223 | 224 | # Replace special characters with their XML entities 225 | replacements = { 226 | "&": "&", 227 | "<": "<", 228 | ">": ">", 229 | '"': """, 230 | "'": "'", 231 | } 232 | 233 | for char, entity in replacements.items(): 234 | content = content.replace(char, entity) 235 | 236 | return content 237 | 238 | 239 | def format_response_message( 240 | message: Optional[str] = None, 241 | actions: Optional[List[Dict[str, Any]]] = None, 242 | file_edits: Optional[List[Dict[str, Any]]] = None, 243 | shell_commands: Optional[List[Dict[str, str]]] = None, 244 | memory_updates: Optional[Dict[str, Any]] = None, 245 | plan_updates: Optional[List[Dict[str, Any]]] = None, 246 | execution_status: Optional[Dict[str, Any]] = None, 247 | error: Optional[Dict[str, str]] = None, 248 | ) -> str: 249 | """ 250 | Format a response message according to the XML schema. 251 | 252 | Args: 253 | message: Optional message to the user 254 | actions: Optional list of actions to execute 255 | file_edits: Optional list of file edits 256 | shell_commands: Optional list of shell commands 257 | memory_updates: Optional memory updates 258 | plan_updates: Optional plan updates 259 | execution_status: Optional execution status 260 | error: Optional error information 261 | 262 | Returns: 263 | Formatted XML response message 264 | """ 265 | xml_parts = [""] 266 | 267 | # Add message if provided 268 | if message: 269 | xml_parts.append(f" {escape_xml_content(message)}") 270 | 271 | # Add actions if provided 272 | if actions and len(actions) > 0: 273 | xml_parts.append(" ") 274 | for action in actions: 275 | action_type = action.get("type", "") 276 | if action_type == "create_file": 277 | xml_parts.append( 278 | f" " 279 | ) 280 | xml_parts.append( 281 | f" {escape_xml_content(action.get('content', ''))}" 282 | ) 283 | xml_parts.append(" ") 284 | elif action_type == "modify_file": 285 | xml_parts.append( 286 | f" " 287 | ) 288 | for change in action.get("changes", []): 289 | xml_parts.append(" ") 290 | xml_parts.append( 291 | f" {escape_xml_content(change.get('original', ''))}" 292 | ) 293 | xml_parts.append( 294 | f" {escape_xml_content(change.get('new', ''))}" 295 | ) 296 | xml_parts.append(" ") 297 | xml_parts.append(" ") 298 | elif action_type == "run_command": 299 | xml_parts.append( 300 | f" " 301 | ) 302 | xml_parts.append(" ") 303 | xml_parts.append(" ") 304 | 305 | # Add file edits if provided 306 | if file_edits and len(file_edits) > 0: 307 | xml_parts.append(" ") 308 | for edit in file_edits: 309 | xml_parts.append(f" ") 310 | xml_parts.append( 311 | f" {escape_xml_content(edit.get('search', ''))}" 312 | ) 313 | xml_parts.append( 314 | f" {escape_xml_content(edit.get('replace', ''))}" 315 | ) 316 | xml_parts.append(" ") 317 | xml_parts.append(" ") 318 | 319 | # Add shell commands if provided 320 | if shell_commands and len(shell_commands) > 0: 321 | xml_parts.append(" ") 322 | for cmd in shell_commands: 323 | safe = cmd.get("safe_to_autorun", False) 324 | xml_parts.append( 325 | f" {escape_xml_content(cmd.get('command', ''))}" 326 | ) 327 | xml_parts.append(" ") 328 | 329 | # Add memory updates if provided 330 | if memory_updates: 331 | xml_parts.append(" ") 332 | if "edits" in memory_updates: 333 | for edit in memory_updates["edits"]: 334 | xml_parts.append(" ") 335 | xml_parts.append( 336 | f" {escape_xml_content(edit.get('search', ''))}" 337 | ) 338 | xml_parts.append( 339 | f" {escape_xml_content(edit.get('replace', ''))}" 340 | ) 341 | xml_parts.append(" ") 342 | if "append" in memory_updates: 343 | xml_parts.append( 344 | f" {escape_xml_content(memory_updates['append'])}" 345 | ) 346 | xml_parts.append(" ") 347 | 348 | # Add plan updates if provided 349 | if plan_updates and len(plan_updates) > 0: 350 | xml_parts.append(" ") 351 | for task in plan_updates: 352 | if "id" in task and "status" in task: 353 | xml_parts.append( 354 | f" {escape_xml_content(task.get('description', ''))}" 355 | ) 356 | elif "id" in task and task.get("is_new", False): 357 | depends = task.get("depends_on", "") 358 | xml_parts.append( 359 | f" {escape_xml_content(task.get('description', ''))}" 360 | ) 361 | xml_parts.append(" ") 362 | 363 | # Add execution status if provided 364 | if execution_status: 365 | complete = execution_status.get("complete", False) 366 | needs_input = execution_status.get("needs_user_input", False) 367 | xml_parts.append( 368 | f' ' 369 | ) 370 | if "message" in execution_status: 371 | xml_parts.append( 372 | f" {escape_xml_content(execution_status['message'])}" 373 | ) 374 | if "progress" in execution_status: 375 | percent = execution_status.get("progress", {}).get("percent", 0) 376 | progress_text = escape_xml_content( 377 | execution_status.get("progress", {}).get("text", "") 378 | ) 379 | xml_parts.append( 380 | f' {progress_text}' 381 | ) 382 | xml_parts.append(" ") 383 | 384 | # Add error if provided 385 | if error: 386 | xml_parts.append(" ") 387 | if "type" in error: 388 | xml_parts.append(f" {escape_xml_content(error['type'])}") 389 | if "message" in error: 390 | xml_parts.append( 391 | f" {escape_xml_content(error['message'])}" 392 | ) 393 | if "suggestion" in error: 394 | xml_parts.append( 395 | f" {escape_xml_content(error['suggestion'])}" 396 | ) 397 | xml_parts.append(" ") 398 | 399 | xml_parts.append("") 400 | 401 | return "\n".join(xml_parts) 402 | -------------------------------------------------------------------------------- /src/utils/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Shell command execution utilities.""" 3 | 4 | import subprocess 5 | from typing import Tuple, Dict, Any 6 | 7 | 8 | def execute_command(command: str, cwd: str = None) -> Tuple[bool, Dict[str, Any]]: 9 | """ 10 | Execute a shell command and return the result. 11 | 12 | Args: 13 | command: The command to execute 14 | cwd: Working directory for the command 15 | 16 | Returns: 17 | Tuple of (success, result_dict) 18 | """ 19 | result = { 20 | "command": command, 21 | "returncode": None, 22 | "stdout": "", 23 | "stderr": "", 24 | "success": False, 25 | } 26 | 27 | try: 28 | process = subprocess.Popen( 29 | command, 30 | shell=True, 31 | stdout=subprocess.PIPE, 32 | stderr=subprocess.PIPE, 33 | text=True, 34 | cwd=cwd, 35 | ) 36 | 37 | # Collect output while streaming 38 | output_lines = [] 39 | 40 | # Stream output in real-time 41 | for line in process.stdout: 42 | output_lines.append(line.rstrip()) 43 | 44 | # Wait for process to complete 45 | process.wait() 46 | 47 | # Get stderr if any 48 | stderr = process.stderr.read() 49 | 50 | # Truncate output to last 5000 characters 51 | full_output = "\n".join(output_lines) 52 | if len(full_output) > 5000: 53 | truncated_output = "... (output truncated) ...\n" + full_output[-5000:] 54 | else: 55 | truncated_output = full_output 56 | 57 | result["stdout"] = truncated_output 58 | result["stderr"] = stderr 59 | result["returncode"] = process.returncode 60 | result["success"] = process.returncode == 0 61 | 62 | return result["success"], result 63 | 64 | except Exception as e: 65 | result["stderr"] = str(e) 66 | return False, result 67 | 68 | 69 | def is_command_safe(command: str) -> bool: 70 | """ 71 | Check if a command is safe to run automatically. 72 | 73 | Args: 74 | command: The command to check 75 | 76 | Returns: 77 | True if the command is considered safe, False otherwise 78 | """ 79 | # List of dangerous commands or patterns 80 | dangerous_patterns = [ 81 | "rm -rf", 82 | "rm -r", 83 | "rmdir", 84 | "dd", 85 | "> /dev/", 86 | "mkfs", 87 | "fdisk", 88 | "format", 89 | "chmod -R", 90 | "chown -R", 91 | ":(){:|:&};:", # Fork bomb 92 | "wget", 93 | "curl", 94 | "> /etc/", 95 | "> ~/.ssh/", 96 | "sudo", 97 | "su", 98 | "shutdown", 99 | "reboot", 100 | "halt", 101 | "mv /* ", 102 | "find / -delete", 103 | ] 104 | 105 | # Check for dangerous patterns 106 | for pattern in dangerous_patterns: 107 | if pattern in command: 108 | return False 109 | 110 | # List of safe commands 111 | safe_commands = [ 112 | "ls", 113 | "dir", 114 | "echo", 115 | "cat", 116 | "head", 117 | "tail", 118 | "pwd", 119 | "cd", 120 | "mkdir", 121 | "touch", 122 | "grep", 123 | "find", 124 | "wc", 125 | "sort", 126 | "uniq", 127 | "git status", 128 | "git log", 129 | "git branch", 130 | "git diff", 131 | "python", 132 | "python3", 133 | "pip", 134 | "pip3", 135 | "pytest", 136 | "npm test", 137 | "npm run", 138 | "ps", 139 | "top", 140 | "htop", 141 | "df", 142 | "du", 143 | ] 144 | 145 | # Check if command starts with a safe command 146 | for safe in safe_commands: 147 | if command.startswith(safe): 148 | return True 149 | 150 | # By default, consider commands unsafe 151 | return False 152 | 153 | 154 | if __name__ == "__main__": 155 | # Simple test when run directly 156 | test_commands = [ 157 | "echo 'Hello, world!'", 158 | "ls -la", 159 | "rm -rf /", # Should be unsafe 160 | "sudo apt-get update", # Should be unsafe 161 | "git status", 162 | ] 163 | 164 | print("Safety check:") 165 | for cmd in test_commands: 166 | print(f"{cmd}: {'Safe' if is_command_safe(cmd) else 'Unsafe'}") 167 | 168 | print("\nExecution test (safe commands only):") 169 | for cmd in test_commands: 170 | if is_command_safe(cmd): 171 | success, result = execute_command(cmd) 172 | print(f"{cmd}: {'Success' if success else 'Failed'}") 173 | print(f" stdout: {result['stdout'][:50]}...") 174 | if result["stderr"]: 175 | print(f" stderr: {result['stderr']}") 176 | -------------------------------------------------------------------------------- /src/utils/web_search.py: -------------------------------------------------------------------------------- 1 | """Web search functionality using DuckDuckGo API.""" 2 | 3 | import requests 4 | from typing import List, Dict 5 | 6 | 7 | def search_web(query: str) -> List[Dict[str, str]]: 8 | """ 9 | Perform a web search using DuckDuckGo Instant Answer API. 10 | 11 | Args: 12 | query: Search query string 13 | 14 | Returns: 15 | List of search results with title, link and snippet 16 | """ 17 | if not query.strip(): 18 | return [] 19 | 20 | try: 21 | response = requests.get( 22 | "https://api.duckduckgo.com/", 23 | params={ 24 | "q": query, 25 | "format": "json", 26 | "no_html": 1, 27 | "skip_disambig": 1, 28 | }, 29 | timeout=5, 30 | ) 31 | response.raise_for_status() 32 | 33 | data = response.json() 34 | results = [] 35 | 36 | # Extract basic results from RelatedTopics 37 | for topic in data.get("RelatedTopics", []): 38 | if "FirstURL" in topic and "Text" in topic: 39 | results.append({ 40 | "title": topic["Text"].split(" - ")[0], 41 | "link": topic["FirstURL"], 42 | "snippet": topic["Text"] 43 | }) 44 | 45 | return results[:3] # Return top 3 results to keep it simple 46 | 47 | except requests.RequestException: 48 | return [] # Fail silently for now 49 | -------------------------------------------------------------------------------- /src/utils/xml_operations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """XML operations for the agent.""" 3 | 4 | import xml.etree.ElementTree as ET 5 | import xml.dom.minidom as minidom 6 | from typing import Dict, Any, Optional 7 | 8 | 9 | def extract_xml_from_response(response: str, tag_name: str) -> Optional[str]: 10 | """ 11 | Extract XML content for a specific tag from the response. 12 | 13 | Args: 14 | response: The response text 15 | tag_name: The tag name to extract 16 | 17 | Returns: 18 | The XML content or None if not found 19 | """ 20 | try: 21 | # Look for XML content in the response 22 | start_tag = f"<{tag_name}" 23 | end_tag = f"" 24 | 25 | start_index = response.find(start_tag) 26 | end_index = response.find(end_tag, start_index) + len(end_tag) 27 | 28 | if start_index != -1 and end_index != -1: 29 | return response[start_index:end_index] 30 | return None 31 | except Exception as e: 32 | print(f"Error extracting XML: {e}") 33 | return None 34 | 35 | 36 | def format_xml_response(content_dict: Dict[str, Any]) -> str: 37 | """ 38 | Format various content pieces into an XML response. 39 | 40 | Args: 41 | content_dict: Dictionary of content to format 42 | 43 | Returns: 44 | Formatted XML string 45 | """ 46 | root = ET.Element("agent-response") 47 | 48 | for key, value in content_dict.items(): 49 | if value is None: 50 | continue 51 | 52 | if ( 53 | isinstance(value, str) 54 | and value.strip().startswith("<") 55 | and value.strip().endswith(">") 56 | ): 57 | # This is already XML content, parse it and add as a subtree 58 | try: 59 | # Parse the XML string 60 | element = ET.fromstring(value) 61 | root.append(element) 62 | except ET.ParseError: 63 | # If parsing fails, add as text 64 | child = ET.SubElement(root, key) 65 | child.text = value 66 | else: 67 | # Add as regular element 68 | child = ET.SubElement(root, key) 69 | if isinstance(value, dict): 70 | child.text = str(value) 71 | else: 72 | child.text = str(value) 73 | 74 | # Convert to string with pretty formatting 75 | xml_str = ET.tostring(root, encoding="unicode") 76 | 77 | # Use a custom function to format XML more cleanly 78 | return pretty_format_xml(xml_str) 79 | 80 | 81 | def pretty_format_xml(xml_string: str) -> str: 82 | """ 83 | Format XML string in a cleaner way than minidom. 84 | 85 | Args: 86 | xml_string: Raw XML string 87 | 88 | Returns: 89 | Formatted XML string with proper indentation 90 | """ 91 | try: 92 | # Parse the XML 93 | root = ET.fromstring(xml_string) 94 | 95 | # Function to recursively format XML 96 | def format_elem(elem, level=0): 97 | indent = " " * level 98 | result = [] 99 | 100 | # Add opening tag with attributes 101 | attrs = " ".join([f'{k}="{v}"' for k, v in elem.attrib.items()]) 102 | tag_open = f"{indent}<{elem.tag}{' ' + attrs if attrs else ''}>" 103 | 104 | # Check if element has children or text 105 | children = list(elem) 106 | if children or (elem.text and elem.text.strip()): 107 | result.append(tag_open) 108 | 109 | # Add text if present 110 | if elem.text and elem.text.strip(): 111 | text_lines = elem.text.strip().split("\n") 112 | if len(text_lines) > 1: 113 | # Multi-line text 114 | result.append("") 115 | for line in text_lines: 116 | result.append(f"{indent} {line}") 117 | result.append("") 118 | else: 119 | # Single line text 120 | result.append(f"{indent} {elem.text.strip()}") 121 | 122 | # Add children 123 | for child in children: 124 | result.extend(format_elem(child, level + 1)) 125 | 126 | # Add closing tag 127 | result.append(f"{indent}") 128 | else: 129 | # Empty element 130 | result.append(f"{tag_open}") 131 | 132 | return result 133 | 134 | # Format the XML 135 | formatted = format_elem(root) 136 | return "\n".join(formatted) 137 | 138 | except ET.ParseError: 139 | # Fallback to minidom if our custom formatter fails 140 | try: 141 | pretty_xml = minidom.parseString(xml_string).toprettyxml(indent=" ") 142 | lines = [line for line in pretty_xml.split("\n") if line.strip()] 143 | return "\n".join(lines) 144 | except: 145 | # If all else fails, return the original string 146 | return xml_string 147 | -------------------------------------------------------------------------------- /src/utils/xml_schema.py: -------------------------------------------------------------------------------- 1 | """XML schema definitions for agent responses.""" 2 | 3 | RESPONSE_SCHEMA = """ 4 | 5 | 6 | 7 | 8 | Your response text here. Can include markdown formatting. 9 | 10 | 11 | 12 | 13 | 14 | # Python code here 15 | 16 | 17 | 18 | 19 | 20 | def old_function(): 21 | def new_function(): 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | def old_function(): 34 | def new_function(): 35 | 36 | 37 | 38 | # New file content here 39 | 40 | 41 | 42 | 43 | 44 | echo "Hello World" 45 | rm -rf some_directory 46 | 47 | 48 | 49 | 50 | 51 | Old information to replace 52 | Updated information 53 | 54 | New information to remember 55 | 56 | 57 | 58 | 59 | Status message explaining what's done or what's needed 60 | 61 | 62 | 63 | """ 64 | 65 | 66 | def get_schema(): 67 | """Return the XML schema for agent responses.""" 68 | return RESPONSE_SCHEMA 69 | -------------------------------------------------------------------------------- /src/utils/xml_tools.py: -------------------------------------------------------------------------------- 1 | """XML parsing and formatting utilities.""" 2 | 3 | import xml.etree.ElementTree as ET 4 | from xml.dom import minidom 5 | import json 6 | from typing import Optional, Dict, Any 7 | 8 | 9 | def extract_xml_from_response(response: str, tag_name: str) -> Optional[str]: 10 | """Extract the first XML section with the specified tag from a response string. 11 | 12 | Args: 13 | response: String containing potential XML content 14 | tag_name: Name of the root XML tag to look for 15 | 16 | Returns: 17 | Extracted XML string or None if not found 18 | """ 19 | try: 20 | start_tag = f"<{tag_name}" 21 | end_tag = f"" 22 | 23 | start_index = response.find(start_tag) 24 | if start_index == -1: 25 | return None 26 | 27 | end_index = response.find(end_tag, start_index) 28 | if end_index == -1: 29 | return None 30 | 31 | return response[start_index : end_index + len(end_tag)] 32 | except Exception: 33 | return None 34 | 35 | 36 | def validate_xml(xml_str: str) -> bool: 37 | """Basic XML validation.""" 38 | try: 39 | ET.fromstring(xml_str) 40 | return True 41 | except ET.ParseError: 42 | return False 43 | 44 | 45 | def escape_xml_content(content: str) -> str: 46 | """Escape special XML characters.""" 47 | return ( 48 | content.replace("&", "&") 49 | .replace("<", "<") 50 | .replace(">", ">") 51 | .replace('"', """) 52 | .replace("'", "'") 53 | ) 54 | 55 | 56 | def format_xml_response(content_dict: Dict[str, Any]) -> str: 57 | """ 58 | Format various content pieces into an XML response. 59 | 60 | Args: 61 | content_dict: Dictionary of content to format 62 | 63 | Returns: 64 | Formatted XML string 65 | """ 66 | root = ET.Element("agent-response") 67 | 68 | for key, value in content_dict.items(): 69 | if value is None: 70 | continue 71 | 72 | if ( 73 | isinstance(value, str) 74 | and value.strip().startswith("<") 75 | and value.strip().endswith(">") 76 | ): 77 | # This is already XML content, parse it and add as a subtree 78 | try: 79 | # Parse the XML string 80 | element = ET.fromstring(value) 81 | root.append(element) 82 | except ET.ParseError: 83 | # If parsing fails, add as text 84 | child = ET.SubElement(root, key) 85 | child.text = value 86 | else: 87 | # Add as regular element 88 | child = ET.SubElement(root, key) 89 | if isinstance(value, dict): 90 | child.text = json.dumps(value) 91 | else: 92 | child.text = str(value) 93 | 94 | # Convert to string with pretty formatting 95 | xml_str = ET.tostring(root, encoding="unicode") 96 | 97 | # Use a custom function to format XML more cleanly 98 | return pretty_format_xml(xml_str) 99 | 100 | 101 | def pretty_format_xml(xml_string: str) -> str: 102 | """Format XML string with consistent indentation. 103 | 104 | Args: 105 | xml_string: Raw XML string to format 106 | 107 | Returns: 108 | Beautifully formatted XML string. Returns original string if parsing fails. 109 | """ 110 | try: 111 | # Parse the XML safely 112 | parser = ET.XMLParser() 113 | root = ET.fromstring(xml_string, parser=parser) 114 | 115 | # Function to recursively format XML 116 | def format_elem(elem, level=0): 117 | indent = " " * level 118 | result = [] 119 | 120 | # Add opening tag with attributes 121 | attrs = " ".join([f'{k}="{v}"' for k, v in elem.attrib.items()]) 122 | tag_open = f"{indent}<{elem.tag}{' ' + attrs if attrs else ''}>" 123 | 124 | # Check if element has children or text 125 | children = list(elem) 126 | if children or (elem.text and elem.text.strip()): 127 | result.append(tag_open) 128 | 129 | # Add text if present 130 | if elem.text and elem.text.strip(): 131 | text_lines = elem.text.strip().split("\n") 132 | if len(text_lines) > 1: 133 | # Multi-line text 134 | result.append("") 135 | for line in text_lines: 136 | result.append(f"{indent} {line}") 137 | result.append("") 138 | else: 139 | # Single line text 140 | result.append(f"{indent} {elem.text.strip()}") 141 | 142 | # Add children 143 | for child in children: 144 | result.extend(format_elem(child, level + 1)) 145 | 146 | # Add closing tag 147 | result.append(f"{indent}") 148 | else: 149 | # Empty element 150 | result.append(f"{tag_open}") 151 | 152 | return result 153 | 154 | # Format the XML 155 | formatted = format_elem(root) 156 | return "\n".join(formatted) 157 | 158 | except ET.ParseError: 159 | # Fallback to minidom if our custom formatter fails 160 | try: 161 | 162 | pretty_xml = minidom.parseString(xml_string).toprettyxml(indent=" ") 163 | lines = [line for line in pretty_xml.split("\n") if line.strip()] 164 | return "\n".join(lines) 165 | except Exception: 166 | # If all else fails, return the original string 167 | return xml_string 168 | 169 | 170 | if __name__ == "__main__": 171 | # Simple test when run directly 172 | test_xml = """Text""" 173 | print(pretty_format_xml(test_xml)) 174 | -------------------------------------------------------------------------------- /start_agent: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Default model if not specified 4 | MODEL="r1" 5 | 6 | # Parse command line arguments 7 | while [[ $# -gt 0 ]]; do 8 | case $1 in 9 | --model) 10 | MODEL="$2" 11 | shift 2 12 | ;; 13 | *) 14 | echo "Unknown option: $1" 15 | echo "Usage: $0 [--model MODEL_NAME]" 16 | exit 1 17 | ;; 18 | esac 19 | done 20 | 21 | cd src 22 | for i in {1..1000} 23 | do 24 | echo "$i - $(date) =====================================================" 25 | aider --architect --model $MODEL --subtree-only --read plex.md --read context.txt --yes-always --no-show-model-warnings --weak-model 'openrouter/google/gemini-2.0-flash-001' --message 'if there are errors, work on fixing the errors. if there are no errors, work on cleaning up the code a little bit. If you see that something is not defined or implemented, please work on implementing until there are no errors. The tests need to work as they are right now. Do not edit tests. Do not add tests to the chat. Do not edit the linting rules. Do not run any commands. Do not try to install anything. Do not mock any functionality, actually implement it. Is there any functionality that is not yet implemented? Replace all mocking with actual implementations. Only use small search replace blocks. You can however use many search replace blocks.' **/*.py 26 | sleep 1 27 | done 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | # Add project root to Python path 5 | project_root = str(Path(__file__).parent.parent) 6 | if project_root not in sys.path: 7 | sys.path.insert(0, project_root) 8 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | """Tests for command processing.""" 2 | from unittest.mock import Mock 3 | from src.interface.commands import process_command # pylint: disable=no-name-in-module 4 | 5 | def test_process_help_command(): 6 | """Test help command displays help.""" 7 | mock_console = Mock() 8 | process_command( 9 | agent=Mock(), 10 | command=["help"], 11 | chat_history=[], 12 | history_file="test.json", 13 | console=mock_console, 14 | multiline_input_mode=False, 15 | multiline_input_buffer=[] 16 | ) 17 | mock_console.print.assert_called() # Verify help was displayed 18 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | """Tests for core agent functionality.""" 2 | 3 | from src.agent.core import Agent # pylint: disable=no-name-in-module 4 | 5 | 6 | def test_agent_initialization(): 7 | """Test agent initializes with correct repository info.""" 8 | test_repo_path = "/test/path" 9 | agent = Agent() 10 | 11 | # Test initial state before initialization 12 | assert not agent.repository_info # Should be empty 13 | 14 | agent.initialize(test_repo_path) 15 | 16 | # Test state after initialization 17 | assert "path" in agent.repository_info 18 | assert agent.repository_info["path"] == test_repo_path 19 | 20 | 21 | def test_agent_initial_state(): 22 | """Test agent starts with empty repository info.""" 23 | agent = Agent() 24 | assert ( 25 | agent.repository_info == {} 26 | ), "Repository info should be empty before initialization" 27 | 28 | def test_initial_plan_tree_is_none(): 29 | """Test plan tree is None after initialization.""" 30 | agent = Agent() 31 | assert agent.plan_tree is None, "Plan tree should be None initially" 32 | -------------------------------------------------------------------------------- /tests/test_display.py: -------------------------------------------------------------------------------- 1 | """Tests for display functionality.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | # Add project root to Python path 7 | project_root = str(Path(__file__).parent.parent) 8 | if project_root not in sys.path: 9 | sys.path.insert(0, project_root) 10 | 11 | from rich.console import Console 12 | from src.interface.display import ( 13 | get_system_info, 14 | display_welcome, 15 | display_help, 16 | display_models, 17 | display_plan_tree, 18 | display_from_top, 19 | ) 20 | 21 | 22 | def test_get_system_info_returns_dict(): 23 | """Test get_system_info returns a dictionary with expected keys.""" 24 | info = get_system_info() 25 | assert isinstance(info, dict) 26 | assert "platform" in info 27 | assert "python" in info 28 | assert "shell" in info 29 | 30 | 31 | def test_display_welcome_prints(capsys): 32 | """Test display_welcome prints output.""" 33 | console = Console() 34 | display_welcome(console) 35 | captured = capsys.readouterr() 36 | assert "Welcome" in captured.out 37 | assert "Agent Interface" in captured.out 38 | 39 | 40 | def test_display_help_prints_commands(capsys): 41 | """Test display_help prints available commands.""" 42 | console = Console() 43 | display_help(console) 44 | captured = capsys.readouterr() 45 | assert "/help" in captured.out 46 | assert "/exit" in captured.out 47 | 48 | 49 | def test_display_models_shows_current_model(capsys): 50 | """Test display_models shows current model.""" 51 | console = Console() 52 | display_models(None, console) # Pass None as agent since we just test output 53 | captured = capsys.readouterr() 54 | assert "Current model" in captured.out 55 | 56 | 57 | def test_display_plan_tree_handles_empty_xml(capsys): 58 | """Test display_plan_tree handles empty XML gracefully.""" 59 | console = Console() 60 | display_plan_tree(console, "") 61 | captured = capsys.readouterr() 62 | assert "No plan" in captured.out or "Error" in captured.out 63 | 64 | 65 | def test_display_from_top_prints_content(capsys): 66 | """Test display_from_top prints content without clearing.""" 67 | console = Console() 68 | test_content = "Test output" 69 | display_from_top(console, test_content) 70 | captured = capsys.readouterr() 71 | assert test_content in captured.out 72 | 73 | 74 | def test_display_plan_tree_shows_xml_content(capsys): 75 | """Test display_plan_tree shows XML content.""" 76 | console = Console() 77 | test_xml = "Test" 78 | display_plan_tree(console, test_xml) 79 | captured = capsys.readouterr() 80 | assert "Test" in captured.out 81 | 82 | 83 | def test_display_welcome_includes_system_info(capsys): 84 | """Test display_welcome includes system information.""" 85 | console = Console() 86 | display_welcome(console) 87 | captured = capsys.readouterr() 88 | info = get_system_info() 89 | assert info["platform"] in captured.out 90 | assert info["python"] in captured.out 91 | 92 | 93 | def test_display_help_includes_all_commands(capsys): 94 | """Test display_help includes all expected commands.""" 95 | console = Console() 96 | display_help(console) 97 | captured = capsys.readouterr() 98 | expected_commands = ["/help", "/exit", "/models", "/plan", "/execute"] 99 | for cmd in expected_commands: 100 | assert cmd in captured.out 101 | 102 | 103 | def test_display_models_handles_missing_agent(capsys): 104 | """Test display_models handles missing agent gracefully.""" 105 | console = Console() 106 | display_models(None, console) 107 | captured = capsys.readouterr() 108 | assert "Current model" in captured.out 109 | -------------------------------------------------------------------------------- /tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | def test_dummy(): 2 | """Simple dummy test that always passes""" 3 | assert True, "This test should always pass" 4 | -------------------------------------------------------------------------------- /tests/test_feedback.py: -------------------------------------------------------------------------------- 1 | """Tests for feedback functionality.""" 2 | 3 | from rich.console import Console 4 | from src.utils.feedback import DopamineReward # pylint: disable=no-name-in-module 5 | 6 | 7 | def test_initial_score_neutral(): 8 | """Test initial score is neutral.""" 9 | reward = DopamineReward(Console()) 10 | assert "NEUTRAL" in reward.generate_reward() 11 | 12 | 13 | def test_positive_feedback_high_score(): 14 | """Test high score generates positive feedback.""" 15 | reward = DopamineReward(Console()) 16 | feedback = reward.generate_reward(95) 17 | assert "SURGE" in feedback 18 | 19 | 20 | def test_negative_feedback_low_score(): 21 | """Test low score generates negative feedback.""" 22 | reward = DopamineReward(Console()) 23 | feedback = reward.generate_reward(15) 24 | assert "LOW" in feedback 25 | 26 | 27 | def test_mixed_feedback_mid_score(): 28 | """Test mid-range score generates mixed feedback.""" 29 | reward = DopamineReward(Console()) 30 | feedback = reward.generate_reward(65) 31 | assert "TRICKLE" in feedback # 65 should be in the TRICKLE range 32 | 33 | 34 | def test_positive_feedback_edge_case(): 35 | """Test edge case for positive feedback.""" 36 | reward = DopamineReward(Console()) 37 | feedback = reward.generate_reward(75) 38 | assert "BOOST" in feedback 39 | 40 | 41 | def test_negative_feedback_edge_case(): 42 | """Test edge case for negative feedback.""" 43 | reward = DopamineReward(Console()) 44 | feedback = reward.generate_reward(39) 45 | assert "DIP" in feedback 46 | 47 | 48 | def test_reward_with_positive_observation(): 49 | """Test reward generation with positive user feedback affects dopamine level.""" 50 | reward = DopamineReward(Console()) 51 | initial_level = reward.dopamine_level 52 | reward.reward_for_xml_response("", "Good job! This is perfect!") 53 | assert reward.dopamine_level > initial_level 54 | 55 | 56 | def test_reward_with_negative_observation(): 57 | """Test reward generation with negative user feedback affects dopamine level.""" 58 | reward = DopamineReward(Console()) 59 | initial_level = reward.dopamine_level 60 | reward.reward_for_xml_response("", "Bad result! Wrong and useless!") 61 | assert reward.dopamine_level < initial_level 62 | 63 | 64 | def test_reward_with_neutral_observation(): 65 | """Test reward generation with mixed feedback.""" 66 | reward = DopamineReward(Console()) 67 | feedback = reward.reward_for_xml_response("", "OK but could be better") 68 | assert "NEUTRAL" in feedback 69 | 70 | 71 | def test_empty_feedback_defaults_neutral(): 72 | """Test empty feedback defaults to neutral.""" 73 | reward = DopamineReward(Console()) 74 | feedback = reward.reward_for_xml_response("", "") 75 | assert "NEUTRAL" in feedback 76 | 77 | 78 | def test_default_reward_score(): 79 | """Test reward generation with default scoring.""" 80 | reward = DopamineReward(Console()) 81 | feedback = reward.generate_reward() 82 | assert "DOPAMINE" in feedback # Should handle None score 83 | -------------------------------------------------------------------------------- /tests/test_file_ops.py: -------------------------------------------------------------------------------- 1 | """Tests for file operations.""" 2 | 3 | import os 4 | import tempfile 5 | from src.utils.file_ops import read_file, write_file, edit_file, append_to_file 6 | 7 | def test_read_file_success(): 8 | """Test reading an existing file.""" 9 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: 10 | f.write("test content") 11 | path = f.name 12 | 13 | success, content = read_file(path) 14 | assert success is True 15 | assert content == "test content" 16 | os.unlink(path) 17 | 18 | def test_read_file_not_found(): 19 | """Test reading a non-existent file.""" 20 | success, content = read_file("/nonexistent/file") 21 | assert success is False 22 | assert "not found" in content.lower() 23 | 24 | def test_write_file_new(): 25 | """Test writing to a new file.""" 26 | with tempfile.TemporaryDirectory() as tmpdir: 27 | path = os.path.join(tmpdir, "new.txt") 28 | success, _ = write_file(path, "new content") 29 | assert success is True 30 | assert os.path.exists(path) 31 | with open(path) as f: 32 | assert f.read() == "new content" 33 | 34 | def test_edit_file_success(): 35 | """Test editing a file.""" 36 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: 37 | f.write("old content") 38 | path = f.name 39 | 40 | success, _ = edit_file(path, "old", "new") 41 | assert success is True 42 | with open(path) as f: 43 | assert f.read() == "new content" 44 | os.unlink(path) 45 | 46 | def test_append_to_file(): 47 | """Test appending to a file.""" 48 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: 49 | f.write("original") 50 | path = f.name 51 | 52 | success, _ = append_to_file(path, "\nappended") 53 | assert success is True 54 | with open(path) as f: 55 | assert f.read() == "original\nappended" 56 | os.unlink(path) 57 | -------------------------------------------------------------------------------- /tests/test_interface.py: -------------------------------------------------------------------------------- 1 | """Tests for interface components.""" 2 | from rich.console import Console 3 | from src.interface.display import get_system_info, display_welcome # pylint: disable=no-name-in-module 4 | 5 | def test_get_system_info(): 6 | """Test system info returns expected fields.""" 7 | info = get_system_info() 8 | assert isinstance(info, dict) 9 | for key in ["platform", "python", "shell"]: 10 | assert key in info 11 | assert isinstance(info[key], str) 12 | 13 | def test_display_welcome(): 14 | """Test welcome message displays without errors.""" 15 | console = Console() 16 | display_welcome(console) 17 | # Basic smoke test - just verify function runs without exceptions 18 | -------------------------------------------------------------------------------- /tests/test_task.py: -------------------------------------------------------------------------------- 1 | """Tests for task execution functionality.""" 2 | 3 | from src.agent.task import execute_task 4 | from src.agent.core import Agent 5 | 6 | 7 | def test_execute_task_with_no_plan(): 8 | """Test executing a task when no plan exists.""" 9 | agent = Agent() 10 | 11 | # Execute task with no plan 12 | result = execute_task(agent, "task1") 13 | 14 | # Verify error response 15 | assert "error" in result 16 | assert "No plan exists" in result 17 | -------------------------------------------------------------------------------- /tests/test_web_search.py: -------------------------------------------------------------------------------- 1 | """Tests for web search functionality.""" 2 | 3 | import requests 4 | from src.utils.web_search import search_web # pylint: disable=no-name-in-module 5 | 6 | 7 | def test_search_web_success(): 8 | """Test successful web search returns results.""" 9 | query = "Python programming language" 10 | results = search_web(query) 11 | 12 | assert isinstance(results, list) 13 | if results: # Only check structure if we got results 14 | for result in results: 15 | assert "title" in result 16 | assert "link" in result 17 | assert "snippet" in result 18 | 19 | 20 | def test_search_web_empty_query(): 21 | """Test empty query returns empty results.""" 22 | results = search_web("") 23 | assert results == [] 24 | 25 | 26 | def test_search_web_error_handling(monkeypatch): 27 | """Test error handling when API fails.""" 28 | def mock_get(*args, **kwargs): 29 | raise requests.RequestException("API Error") 30 | 31 | monkeypatch.setattr(requests, "get", mock_get) 32 | results = search_web("test") 33 | assert results == [] 34 | -------------------------------------------------------------------------------- /tests/test_xml_schema.py: -------------------------------------------------------------------------------- 1 | """Tests for XML schema functionality.""" 2 | 3 | import xml.etree.ElementTree as ET 4 | from src.utils.xml_schema import get_schema # pylint: disable=no-name-in-module 5 | from src.utils.xml_tools import validate_xml # pylint: disable=no-name-in-module 6 | 7 | 8 | def test_schema_is_valid_xml(): 9 | """Test that the schema itself is valid XML.""" 10 | schema = get_schema() 11 | assert validate_xml(schema), "Schema should be valid XML" 12 | 13 | 14 | def test_schema_contains_required_elements(): 15 | """Test that the schema contains required elements.""" 16 | schema = get_schema() 17 | required_elements = { 18 | "response", 19 | "actions", 20 | "file_edits", 21 | "shell_commands", 22 | "memory_updates", 23 | "execution_status", 24 | } 25 | 26 | for element in required_elements: 27 | assert f"<{element}" in schema, f"Schema should contain {element} element" 28 | assert f"" in schema, f"Schema should close {element} element" 29 | 30 | 31 | def test_schema_example_structures(): 32 | """Test that example structures in schema are valid""" 33 | schema = get_schema() 34 | assert 'type="create_file"' in schema, "Should contain file creation example" 35 | assert 'type="modify_file"' in schema, "Should contain file modification example" 36 | assert 'type="run_command"' in schema, "Should contain command execution example" 37 | 38 | 39 | def test_get_schema_returns_string(): 40 | """Test that get_schema returns a non-empty string.""" 41 | schema = get_schema() 42 | assert isinstance(schema, str), "Schema should be a string" 43 | assert len(schema) > 100, "Schema should be a meaningful length string" 44 | 45 | 46 | def test_execution_status_structure(): 47 | """Test execution_status element has required attributes""" 48 | schema = get_schema() 49 | root = ET.fromstring(schema) 50 | status_elem = root.find(".//execution_status") 51 | 52 | assert status_elem is not None, "execution_status element missing" 53 | assert "complete" in status_elem.attrib, "Missing complete attribute" 54 | assert ( 55 | "needs_user_input" in status_elem.attrib 56 | ), "Missing needs_user_input attribute" 57 | 58 | def test_schema_root_element(): 59 | """Test schema contains root xml_schema element""" 60 | schema = get_schema() 61 | root = ET.fromstring(schema) 62 | assert root.tag == "xml_schema", "Schema should have xml_schema root element" 63 | -------------------------------------------------------------------------------- /tests/test_xml_tools.py: -------------------------------------------------------------------------------- 1 | """Tests for XML tools functionality.""" 2 | 3 | from src.utils.xml_tools import ( # pylint: disable=no-name-in-module 4 | extract_xml_from_response, 5 | pretty_format_xml, 6 | validate_xml, 7 | escape_xml_content, 8 | ) 9 | 10 | SAMPLE_XML = """ 11 | Test content 12 | 123 13 | """ 14 | 15 | 16 | def test_extract_xml_simple(): 17 | """Test basic XML extraction.""" 18 | wrapped_xml = f"Some text before {SAMPLE_XML} some text after" 19 | result = extract_xml_from_response(wrapped_xml, "response") 20 | assert result.strip() == SAMPLE_XML.strip() 21 | 22 | 23 | def test_extract_xml_no_match(): 24 | """Test XML extraction when no match exists.""" 25 | result = extract_xml_from_response("No XML here", "response") 26 | assert result is None 27 | 28 | 29 | def test_extract_xml_nested_tag(): 30 | """Test extraction of nested XML tags.""" 31 | nested_xml = ( 32 | """BeforeNestedAfter""" 33 | ) 34 | result = extract_xml_from_response(nested_xml, "response") 35 | assert "Nested" in result 36 | 37 | 38 | def test_extract_xml_multiple_matches(): 39 | """Test extraction returns first match when multiple exist.""" 40 | multi_xml = "FirstSecond" 41 | result = extract_xml_from_response(multi_xml, "response") 42 | assert "First" in result and "Second" not in result 43 | 44 | 45 | def test_pretty_format_xml(): 46 | """Test XML formatting produces indented output.""" 47 | compressed_xml = ( 48 | "Test1" 49 | ) 50 | formatted = pretty_format_xml(compressed_xml) 51 | # Check indentation 52 | assert formatted == ( 53 | "\n" 54 | " \n" 55 | " Test\n" 56 | " \n" 57 | " \n" 58 | " \n" 59 | " 1\n" 60 | " \n" 61 | " \n" 62 | "" 63 | ) 64 | 65 | 66 | def test_pretty_format_invalid_xml(): 67 | """Test formatting handles invalid XML gracefully.""" 68 | invalid_xml = "test" 69 | formatted = pretty_format_xml(invalid_xml) 70 | assert invalid_xml in formatted # Should return original string 71 | 72 | 73 | def test_extract_xml_with_whitespace(): 74 | """Test XML extraction with surrounding whitespace.""" 75 | wrapped_xml = "\n\n \n Content\n \n\n" 76 | result = extract_xml_from_response(wrapped_xml, "response") 77 | assert result.strip() == "\n Content\n " 78 | 79 | 80 | def test_validate_good_xml(): 81 | """Test validation of properly formatted XML.""" 82 | valid_xml = "text" 83 | assert validate_xml(valid_xml) is True 84 | 85 | 86 | def test_escape_xml_content(): 87 | """Test XML content escaping.""" 88 | test_cases = [ 89 | ('Hello "World" & Co. <3 >', "Hello "World" & Co. <3 >"), 90 | ('content', "<tag>content</tag>"), 91 | ('', ''), 92 | ('No special chars', 'No special chars'), 93 | ('&&&&&', "&&&&&") 94 | ] 95 | 96 | for original, expected in test_cases: 97 | assert escape_xml_content(original) == expected, f"Failed for: {original}" 98 | 99 | 100 | def test_validate_bad_xml(): 101 | """Test validation detects malformed XML.""" 102 | invalid_xml = "text" 103 | assert validate_xml(invalid_xml) is False 104 | -------------------------------------------------------------------------------- /watch-git-diff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # watch-git-diff - Continuously display the last git commit diff with color 4 | # Usage: watch-git-diff [check_interval_seconds] [additional_git_diff_args] 5 | 6 | # Default check interval in seconds 7 | INTERVAL=${1:-2} 8 | shift 2>/dev/null 9 | 10 | # Check if we're in a git repository 11 | if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 12 | echo "Error: Not in a git repository" 13 | exit 1 14 | fi 15 | 16 | # Get the initial HEAD commit hash 17 | LAST_KNOWN_COMMIT=$(git rev-parse HEAD 2>/dev/null) 18 | 19 | # Use a more compatible approach for terminal handling 20 | setup_display() { 21 | # Function to clear screen without flickering 22 | clear_screen() { 23 | clear 24 | } 25 | } 26 | 27 | # Function to check if there's a new commit 28 | check_for_new_commit() { 29 | # Get current HEAD commit 30 | CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null) 31 | 32 | # Compare with last known commit 33 | if [[ "$CURRENT_COMMIT" != "$LAST_KNOWN_COMMIT" ]]; then 34 | LAST_KNOWN_COMMIT=$CURRENT_COMMIT 35 | return 0 # New commit detected 36 | fi 37 | 38 | return 1 # No new commit 39 | } 40 | 41 | # Function to display the diff with a header 42 | show_diff() { 43 | clear_screen 44 | 45 | # Get the last commit hash and message 46 | LAST_COMMIT=$(git log -1 --pretty=format:"%h - %s (%cr) by %an") 47 | 48 | # Print header with timestamp 49 | echo -e "\033[1;36m=== Last Commit Diff (Updated: $(date '+%Y-%m-%d %H:%M:%S')) ===\033[0m" 50 | echo -e "\033[1;33m$LAST_COMMIT\033[0m" 51 | echo -e "\033[1;36m=======================================================\033[0m" 52 | echo "" 53 | 54 | # Show the diff with color 55 | git --no-pager diff HEAD~1 HEAD --color=always "$@" 56 | 57 | echo "" 58 | echo -e "\033[1;36m=== Press Ctrl+C or 'q' to exit ===\033[0m" 59 | } 60 | 61 | # Main execution 62 | echo "Starting git diff watch, checking for new commits every $INTERVAL seconds..." 63 | setup_display 64 | 65 | # Show initial diff 66 | show_diff "$@" 67 | 68 | # Function to check for 'q' keypress without blocking 69 | check_for_quit() { 70 | # Check if input is available (non-blocking) 71 | if read -t 0.1 -N 1 key; then 72 | if [[ "$key" == "q" ]]; then 73 | echo -e "\nUser pressed 'q'. Exiting..." 74 | exit 0 75 | fi 76 | fi 77 | } 78 | 79 | # Set terminal to read input without requiring Enter key 80 | old_stty_settings=$(stty -g) 81 | stty -icanon min 1 time 0 82 | 83 | # Ensure terminal settings are restored on exit 84 | cleanup() { 85 | stty "$old_stty_settings" 86 | echo -e "\nExiting git diff watch" 87 | exit 0 88 | } 89 | trap cleanup INT TERM EXIT 90 | 91 | # Main loop 92 | while true; do 93 | # Check for new commits 94 | if check_for_new_commit; then 95 | # New commit detected, update the display 96 | show_diff "$@" 97 | echo -e "\033[1;32mNew commit detected, updated diff.\033[0m" >&2 98 | fi 99 | 100 | # Update status on the same line (overwrite previous status) 101 | echo -ne "\r\033[K\033[1;36mLast check: $(date '+%H:%M:%S') | Press Ctrl+C to exit or 'q' to quit\033[0m" 102 | 103 | # Check for 'q' keypress 104 | check_for_quit 105 | 106 | # Wait before next check (with smaller intervals to check for keypress) 107 | for ((i=0; i<$INTERVAL*10; i++)); do 108 | check_for_quit 109 | sleep 0.1 110 | done 111 | done 112 | --------------------------------------------------------------------------------