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)}