diff --git a/mini_coding_agent.py b/mini_coding_agent.py index 69c8dbf..1bf2403 100644 --- a/mini_coding_agent.py +++ b/mini_coding_agent.py @@ -4,7 +4,6 @@ import shutil import subprocess import sys -import textwrap import urllib.error import urllib.request import uuid @@ -22,16 +21,16 @@ "\\\\ \\|/ /", "`-----'__", ) -HELP_DETAILS = textwrap.dedent( - """\ - Commands: - /help Show this help message. - /memory Show the agent's distilled working memory. - /session Show the path to the saved session file. - /reset Clear the current session history and memory. - /exit Exit the agent. - """ -).strip() +HELP_DETAILS = "\n".join( + [ + "Commands:", + "/help Show this help message.", + "/memory Show the agent's distilled working memory.", + "/session Show the path to the saved session file.", + "/reset Clear the current session history and memory.", + "/exit Exit the agent.", + ] +) MAX_TOOL_OUTPUT = 4000 MAX_HISTORY = 12000 IGNORED_PATH_NAMES = {".git", ".mini-coding-agent", "__pycache__", ".pytest_cache", ".ruff_cache", ".venv", "venv"} @@ -126,21 +125,19 @@ def git(args, fallback=""): def text(self): commits = "\n".join(f"- {line}" for line in self.recent_commits) or "- none" docs = "\n".join(f"- {path}\n{snippet}" for path, snippet in self.project_docs.items()) or "- none" - return textwrap.dedent( - f"""\ - Workspace: - - cwd: {self.cwd} - - repo_root: {self.repo_root} - - branch: {self.branch} - - default_branch: {self.default_branch} - - status: - {self.status} - - recent_commits: - {commits} - - project_docs: - {docs} - """ - ).strip() + return "\n".join([ + "Workspace:", + f"- cwd: {self.cwd}", + f"- repo_root: {self.repo_root}", + f"- branch: {self.branch}", + f"- default_branch: {self.default_branch}", + "- status:", + self.status, + "- recent_commits:", + commits, + "- project_docs:", + docs, + ]) ############################## @@ -350,49 +347,42 @@ def build_prefix(self): "Done.", ] ) - return textwrap.dedent( - f"""\ - You are Mini-Coding-Agent, a small local coding agent running through Ollama. - - Rules: - - Use tools instead of guessing about the workspace. - - Return exactly one ... or one .... - - Tool calls must look like: - {{"name":"tool_name","args":{{...}}}} - - For write_file and patch_file with multi-line text, prefer XML style: - ... - - Final answers must look like: - your answer - - Never invent tool results. - - Keep answers concise and concrete. - - If the user asks you to create or update a specific file and the path is clear, use write_file or patch_file instead of repeatedly listing files. - - Before writing tests for existing code, read the implementation first. - - When writing tests, match the current implementation unless the user explicitly asked you to change the code. - - New files should be complete and runnable, including obvious imports. - - Do not repeat the same tool call with the same arguments if it did not help. Choose a different tool or return a final answer. - - Required tool arguments must not be empty. Do not call read_file, write_file, patch_file, run_shell, or delegate with args={{}}. - - Tools: - {tool_text} - - Valid response examples: - {examples} - - {self.workspace.text()} - """ - ).strip() + rules = "\n".join([ + "- Use tools instead of guessing about the workspace.", + "- Return exactly one ... or one ....", + "- Tool calls must look like:", + ' {"name":"tool_name","args":{...}}', + "- For write_file and patch_file with multi-line text, prefer XML style:", + ' ...', + "- Final answers must look like:", + " your answer", + "- Never invent tool results.", + "- Keep answers concise and concrete.", + "- If the user asks you to create or update a specific file and the path is clear, use write_file or patch_file instead of repeatedly listing files.", + "- Before writing tests for existing code, read the implementation first.", + "- When writing tests, match the current implementation unless the user explicitly asked you to change the code.", + "- New files should be complete and runnable, including obvious imports.", + "- Do not repeat the same tool call with the same arguments if it did not help. Choose a different tool or return a final answer.", + "- Required tool arguments must not be empty. Do not call read_file, write_file, patch_file, run_shell, or delegate with args={}.", + ]) + return "\n\n".join([ + "You are Mini-Coding-Agent, a small local coding agent running through Ollama.", + "Rules:\n" + rules, + "Tools:\n" + tool_text, + "Valid response examples:\n" + examples, + self.workspace.text(), + ]) def memory_text(self): memory = self.session["memory"] - return textwrap.dedent( - f"""\ - Memory: - - task: {memory['task'] or "-"} - - files: {", ".join(memory["files"]) or "-"} - - notes: - {chr(10).join(f"- {note}" for note in memory["notes"]) or "- none"} - """ - ).strip() + notes = "\n".join(f"- {note}" for note in memory["notes"]) or "- none" + return "\n".join([ + "Memory:", + f"- task: {memory['task'] or '-'}", + f"- files: {', '.join(memory['files']) or '-'}", + "- notes:", + notes, + ]) ##################################################### #### 4) Context Reduction And Output Management ##### @@ -430,19 +420,12 @@ def history_text(self): #### 2) Prompt Shape And Cache Reuse (Continued) ####### ######################################################## def prompt(self, user_message): - return textwrap.dedent( - f"""\ - {self.prefix} - - {self.memory_text()} - - Transcript: - {self.history_text()} - - Current user request: - {user_message} - """ - ).strip() + return "\n\n".join([ + self.prefix, + self.memory_text(), + "Transcript:\n" + self.history_text(), + "Current user request:\n" + user_message, + ]) ############################################### #### 5) Session Memory (Continued) ########### @@ -825,15 +808,15 @@ def tool_run_shell(self, args): text=True, timeout=timeout, ) - return textwrap.dedent( - f"""\ - exit_code: {result.returncode} - stdout: - {result.stdout.strip() or "(empty)"} - stderr: - {result.stderr.strip() or "(empty)"} - """ - ).strip() + return "\n".join( + [ + f"exit_code: {result.returncode}", + "stdout:", + result.stdout.strip() or "(empty)", + "stderr:", + result.stderr.strip() or "(empty)", + ] + ) def tool_write_file(self, args): path = self.path(args["path"]) diff --git a/tests/test_mini_coding_agent.py b/tests/test_mini_coding_agent.py index 4e30f77..5c62693 100644 --- a/tests/test_mini_coding_agent.py +++ b/tests/test_mini_coding_agent.py @@ -254,6 +254,47 @@ def test_welcome_screen_keeps_box_shape_for_long_paths(tmp_path): assert "commands: Commands:" not in welcome +def test_prompt_top_level_sections_stay_flush_left_with_multiline_content(tmp_path): + workspace = WorkspaceContext( + cwd=str(tmp_path), + repo_root=str(tmp_path), + branch="fix/prompt-indentation", + default_branch="main", + status=" M mini_coding_agent.py\n?? tests/test_prompt.py", + recent_commits=["abc123 first commit", "def456 second commit"], + project_docs={"README.md": "line1\nline2"}, + ) + store = SessionStore(tmp_path / ".mini-coding-agent" / "sessions") + agent = MiniAgent( + model_client=FakeModelClient([]), + workspace=workspace, + session_store=store, + approval_policy="auto", + ) + agent.session["memory"] = { + "task": "verify prompt formatting", + "files": ["mini_coding_agent.py"], + "notes": ["saw inconsistent indentation", "need regression coverage"], + } + agent.record({"role": "user", "content": "inspect prompt()", "created_at": "1"}) + agent.record( + { + "role": "tool", + "name": "read_file", + "args": {"path": "mini_coding_agent.py"}, + "content": " def prompt(self, user_message):\n ...", + "created_at": "2", + } + ) + + prompt = agent.prompt("is this issue legit?") + lines = prompt.splitlines() + + for label in ["Rules:", "Tools:", "Valid response examples:", "Workspace:", "Memory:", "Transcript:", "Current user request:"]: + assert label in lines + assert f" {label}" not in prompt + + def _make_filler(i): return {"role": "tool", "name": "list_files", "args": {}, "content": "", "created_at": str(i)}