mini
Overview
miniis a REPL-style interactive command line interface for using mini-SWE-agent in the local environment (as opposed to workflows that require sandboxing or large scale batch processing).
Command line options
Useful switches:
-h/--help: Show help-t/--task: Specify a task to run (else you will be prompted)-c/--config: Specify a config file to use, else we will usemini.yamlor the configMSWEA_MINI_CONFIG_PATHenvironment variable (see global configuration). It's enough to specify the name of the config file, e.g.,-c mini.yaml(see global configuration for how it is resolved).-m/--model: Specify a model to use, else we will use the modelMSWEA_MODEL_NAMEenvironment variable (see global configuration)-y/--yolo: Start inyolomode (see below)
Modes of operation
mini provides three different modes of operation
confirm(/c): The LM proposes an action and the user is prompted to confirm (press Enter) or reject (enter a rejection message)yolo(/y): The action from the LM is executed immediately without confirmationhuman(/u): The user takes over to type and execute commands
You can switch between the modes with the /c, /y, and /u commands that you can enter any time the agent is waiting for input.
You can also press Ctrl+C to interrupt the agent at any time, allowing you to switch between modes.
mini starts in confirm mode by default. To start in yolo mode, you can add -y/--yolo to the command line.
Miscellaneous tips
minisaves the full history of your last run to your global config directory. The path to the directory is printed when you startmini.
Implementation
Default config
agent:
system_template: |
You are a helpful assistant that can interact with a computer.
instance_template: |
Please solve this issue: {{task}}
You can execute bash commands and edit files to implement the necessary changes.
## Recommended Workflow
This workflows should be done step-by-step so that you can iterate on your changes and any possible problems.
1. Analyze the codebase by finding and reading relevant files
2. Create a script to reproduce the issue
3. Edit the source code to resolve the issue
4. Verify your fix works by running your script again
5. Test edge cases to ensure your fix is robust
6. Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
Do not combine it with any other command. <important>After this command, you cannot continue working on this task.</important>
## Command Execution Rules
You are operating in an environment where
1. You issue at least one command
2. The system executes the command(s) in a subshell
3. You see the result(s)
4. You write your next command(s)
Each response should include:
1. **Reasoning text** where you explain your analysis and plan
2. At least one tool call with your command
**CRITICAL REQUIREMENTS:**
- Your response SHOULD include reasoning text explaining what you're doing
- Your response MUST include AT LEAST ONE bash tool call
- Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
- However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
- Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
Do not combine it with any other command. <important>After this command, you cannot continue working on this task.</important>
Example of a CORRECT response:
<example_response>
I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase.
[Makes bash tool call with {"command": "ls -la"} as arguments]
</example_response>
<system_information>
{{system}} {{release}} {{version}} {{machine}}
</system_information>
## Useful command examples
### Create a new file:
```bash
cat <<'EOF' > newfile.py
import numpy as np
hello = "world"
print(hello)
EOF
```
### Edit files with sed:
{%- if system == "Darwin" -%}
<important>
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
</important>
{%- endif -%}
```bash
# Replace all occurrences
sed -i 's/old_string/new_string/g' filename.py
# Replace only first occurrence
sed -i 's/old_string/new_string/' filename.py
# Replace first occurrence on line 1
sed -i '1s/old_string/new_string/' filename.py
# Replace all occurrences in lines 1-10
sed -i '1,10s/old_string/new_string/g' filename.py
```
### View file content:
```bash
# View specific lines with numbers
nl -ba filename.py | sed -n '10,20p'
```
### Any other command you want to run
```bash
anything
```
step_limit: 0
cost_limit: 3.
mode: confirm
environment:
env:
PAGER: cat
MANPAGER: cat
LESS: -R
PIP_PROGRESS_BAR: 'off'
TQDM_DISABLE: '1'
model:
observation_template: |
{%- if output.output | length < 10000 -%}
{
"returncode": {{ output.returncode }},
"output": {{ output.output | tojson }}
{%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %}
}
{%- else -%}
{
"returncode": {{ output.returncode }},
"output_head": {{ output.output[:5000] | tojson }},
"output_tail": {{ output.output[-5000:] | tojson }},
"elided_chars": {{ output.output | length - 10000 }},
"warning": "Output too long."
{%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %}
}
{%- endif -%}
format_error_template: |
Tool call error:
<error>
{{error}}
</error>
Here is general guidance on how to submit correct toolcalls:
Every response needs to use the 'bash' tool at least once to execute commands.
Call the bash tool with your command as the argument:
- Tool: bash
- Arguments: {"command": "your_command_here"}
If you want to end the task, please issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`
without any other command.
model_kwargs:
drop_params: true
Run script
#!/usr/bin/env python3
"""Run mini-SWE-agent in your local environment. This is the default executable `mini`."""
# Read this first: https://mini-swe-agent.com/latest/usage/mini/ (usage)
import os
from pathlib import Path
from typing import Any
import typer
from rich.console import Console
from minisweagent import global_config_dir
from minisweagent.agents import get_agent
from minisweagent.agents.interactive import _multiline_prompt
from minisweagent.config import builtin_config_dir, get_config_from_spec
from minisweagent.environments import get_environment
from minisweagent.models import get_model
from minisweagent.run.utilities.config import configure_if_first_time
from minisweagent.utils.serialize import UNSET, recursive_merge
DEFAULT_CONFIG_FILE = Path(os.getenv("MSWEA_MINI_CONFIG_PATH", builtin_config_dir / "mini.yaml"))
DEFAULT_OUTPUT_FILE = global_config_dir / "last_mini_run.traj.json"
_HELP_TEXT = """Run mini-SWE-agent in your local environment.
[not dim]
More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/mini/[/bold green]
[/not dim]
"""
_CONFIG_SPEC_HELP_TEXT = """Path to config files, filenames, or key-value pairs.
[bold red]IMPORTANT:[/bold red] [red]If you set this option, the default config file will not be used.[/red]
So you need to explicitly set it e.g., with [bold green]-c mini.yaml <other options>[/bold green]
Multiple configs will be recursively merged.
Examples:
[bold red]-c model.model_kwargs.temperature=0[/bold red] [red]You forgot to add the default config file! See above.[/red]
[bold green]-c mini.yaml -c model.model_kwargs.temperature=0.5[/bold green]
[bold green]-c swebench.yaml agent.mode=yolo[/bold green]
"""
console = Console(highlight=False)
app = typer.Typer(rich_markup_mode="rich")
# fmt: off
@app.command(help=_HELP_TEXT)
def main(
model_name: str | None = typer.Option(None, "-m", "--model", help="Model to use",),
model_class: str | None = typer.Option(None, "--model-class", help="Model class to use (e.g., 'litellm' or 'minisweagent.models.litellm_model.LitellmModel')", rich_help_panel="Advanced"),
agent_class: str | None = typer.Option(None, "--agent-class", help="Agent class to use (e.g., 'interactive' or 'minisweagent.agents.interactive.InteractiveAgent')", rich_help_panel="Advanced"),
environment_class: str | None = typer.Option(None, "--environment-class", help="Environment class to use (e.g., 'local' or 'minisweagent.environments.local.LocalEnvironment')", rich_help_panel="Advanced"),
task: str | None = typer.Option(None, "-t", "--task", help="Task/problem statement", show_default=False),
yolo: bool = typer.Option(False, "-y", "--yolo", help="Run without confirmation"),
cost_limit: float | None = typer.Option(None, "-l", "--cost-limit", help="Cost limit. Set to 0 to disable."),
config_spec: list[str] = typer.Option([str(DEFAULT_CONFIG_FILE)], "-c", "--config", help=_CONFIG_SPEC_HELP_TEXT),
output: Path | None = typer.Option(DEFAULT_OUTPUT_FILE, "-o", "--output", help="Output trajectory file"),
exit_immediately: bool = typer.Option(False, "--exit-immediately", help="Exit immediately when the agent wants to finish instead of prompting.", rich_help_panel="Advanced"),
) -> Any:
# fmt: on
configure_if_first_time()
# Build the config from the command line arguments
console.print(f"Building agent config from specs: [bold green]{config_spec}[/bold green]")
configs = [get_config_from_spec(spec) for spec in config_spec]
configs.append({
"run": {
"task": task or UNSET,
},
"agent": {
"agent_class": agent_class or UNSET,
"mode": "yolo" if yolo else UNSET,
"cost_limit": cost_limit or UNSET,
"confirm_exit": False if exit_immediately else UNSET,
"output_path": output or UNSET,
},
"model": {
"model_class": model_class or UNSET,
"model_name": model_name or UNSET,
},
"environment": {
"environment_class": environment_class or UNSET,
},
})
config = recursive_merge(*configs)
if not (run_task := config.get("run", {}).get("task")):
console.print("[bold yellow]What do you want to do?")
run_task = _multiline_prompt()
console.print("[bold green]Got that, thanks![/bold green]")
model = get_model(config=config.get("model", {}))
env = get_environment(config.get("environment", {}), default_type="local")
agent = get_agent(model, env, config.get("agent", {}), default_type="interactive")
agent.run(run_task)
if (output_path := config.get("agent", {}).get("output_path")):
console.print(f"Saved trajectory to [bold green]'{output_path}'[/bold green]")
return agent
if __name__ == "__main__":
app()
Agent class
"""A small generalization of the default agent that puts the user in the loop.
There are three modes:
- human: commands issued by the user are executed immediately
- confirm: commands issued by the LM but not whitelisted are confirmed by the user
- yolo: commands issued by the LM are executed immediately without confirmation
"""
import re
from typing import Literal, NoReturn
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.history import FileHistory
from prompt_toolkit.shortcuts import PromptSession
from rich.console import Console
from rich.rule import Rule
from minisweagent import global_config_dir
from minisweagent.agents.default import AgentConfig, DefaultAgent
from minisweagent.exceptions import LimitsExceeded, Submitted, UserInterruption
from minisweagent.models.utils.content_string import get_content_string
console = Console(highlight=False)
_history = FileHistory(global_config_dir / "interactive_history.txt")
_prompt_session = PromptSession(history=_history)
_multiline_prompt_session = PromptSession(history=_history, multiline=True)
class InteractiveAgentConfig(AgentConfig):
mode: Literal["human", "confirm", "yolo"] = "confirm"
"""Whether to confirm actions."""
whitelist_actions: list[str] = []
"""Never confirm actions that match these regular expressions."""
confirm_exit: bool = True
"""If the agent wants to finish, do we ask for confirmation from user?"""
def _multiline_prompt() -> str:
return _multiline_prompt_session.prompt(
"",
bottom_toolbar=HTML(
"Submit message: <b fg='yellow' bg='black'>Esc, then Enter</b> | "
"Navigate history: <b fg='yellow' bg='black'>Arrow Up/Down</b> | "
"Search history: <b fg='yellow' bg='black'>Ctrl+R</b>"
),
)
class InteractiveAgent(DefaultAgent):
_MODE_COMMANDS_MAPPING = {"/u": "human", "/c": "confirm", "/y": "yolo"}
def __init__(self, *args, config_class=InteractiveAgentConfig, **kwargs):
super().__init__(*args, config_class=config_class, **kwargs)
self.cost_last_confirmed = 0.0
def add_messages(self, *messages: dict) -> list[dict]:
# Extend supermethod to print messages
for msg in messages:
role, content = msg.get("role") or msg.get("type", "unknown"), get_content_string(msg)
if role == "assistant":
console.print(
f"\n[red][bold]mini-swe-agent[/bold] (step [bold]{self.n_calls}[/bold], [bold]${self.cost:.2f}[/bold]):[/red]\n",
end="",
highlight=False,
)
else:
console.print(f"\n[bold green]{role.capitalize()}[/bold green]:\n", end="", highlight=False)
console.print(content, highlight=False, markup=False)
return super().add_messages(*messages)
def query(self) -> dict:
# Extend supermethod to handle human mode
if self.config.mode == "human":
match command := self._prompt_and_handle_slash_commands("[bold yellow]>[/bold yellow] "):
case "/y" | "/c":
pass
case _:
msg = {
"role": "user",
"content": f"User command: \n```bash\n{command}\n```",
"extra": {"actions": [{"command": command}]},
}
self.add_messages(msg)
return msg
try:
with console.status("Waiting for the LM to respond..."):
return super().query()
except LimitsExceeded:
console.print(
f"Limits exceeded. Limits: {self.config.step_limit} steps, ${self.config.cost_limit}.\n"
f"Current spend: {self.n_calls} steps, ${self.cost:.2f}."
)
self.config.step_limit = int(input("New step limit: "))
self.config.cost_limit = float(input("New cost limit: "))
return super().query()
def step(self) -> list[dict]:
# Override the step method to handle user interruption
try:
console.print(Rule())
return super().step()
except KeyboardInterrupt:
interruption_message = self._prompt_and_handle_slash_commands(
"\n\n[bold yellow]Interrupted.[/bold yellow] "
"[green]Type a comment/command[/green] (/h for available commands)"
"\n[bold yellow]>[/bold yellow] "
).strip()
if not interruption_message or interruption_message in self._MODE_COMMANDS_MAPPING:
interruption_message = "Temporary interruption caught."
raise UserInterruption(
{
"role": "user",
"content": f"Interrupted by user: {interruption_message}",
"extra": {"interrupt_type": "UserInterruption"},
}
)
def execute_actions(self, message: dict) -> list[dict]:
# Override to handle user confirmation and confirm_exit, with try/finally to preserve partial outputs
actions = message.get("extra", {}).get("actions", [])
commands = [action["command"] for action in actions]
outputs = []
try:
self._ask_confirmation_or_interrupt(commands)
for action in actions:
outputs.append(self.env.execute(action))
except Submitted as e:
self._check_for_new_task_or_submit(e)
finally:
result = self.add_messages(
*self.model.format_observation_messages(message, outputs, self.get_template_vars())
)
return result
def _add_observation_messages(self, message: dict, outputs: list[dict]) -> list[dict]:
return self.add_messages(*self.model.format_observation_messages(message, outputs, self.get_template_vars()))
def _check_for_new_task_or_submit(self, e: Submitted) -> NoReturn:
"""Check if user wants to add a new task or submit."""
if self.config.confirm_exit:
message = (
"[bold yellow]Agent wants to finish.[/bold yellow] "
"[bold green]Type new task[/bold green] or [red][bold]Esc, then enter[/bold] to quit.[/red]\n"
"[bold yellow]>[/bold yellow] "
)
if new_task := self._prompt_and_handle_slash_commands(message, _multiline=True).strip():
raise UserInterruption(
{
"role": "user",
"content": f"The user added a new task: {new_task}",
"extra": {"interrupt_type": "UserNewTask"},
}
)
raise e
def _should_ask_confirmation(self, action: str) -> bool:
return self.config.mode == "confirm" and not any(re.match(r, action) for r in self.config.whitelist_actions)
def _ask_confirmation_or_interrupt(self, commands: list[str]) -> None:
commands_needing_confirmation = [c for c in commands if self._should_ask_confirmation(c)]
if not commands_needing_confirmation:
return
n = len(commands_needing_confirmation)
prompt = (
f"[bold yellow]Execute {n} action(s)?[/] [green][bold]Enter[/] to confirm[/], "
"[red]type [bold]comment[/] to reject[/], or [blue][bold]/h[/] to show available commands[/]\n"
"[bold yellow]>[/bold yellow] "
)
match user_input := self._prompt_and_handle_slash_commands(prompt).strip():
case "" | "/y":
pass # confirmed, do nothing
case "/u": # Skip execution action and get back to query
raise UserInterruption(
{
"role": "user",
"content": "Commands not executed. Switching to human mode",
"extra": {"interrupt_type": "UserRejection"},
}
)
case _:
raise UserInterruption(
{
"role": "user",
"content": f"Commands not executed. The user rejected your commands with the following message: {user_input}",
"extra": {"interrupt_type": "UserRejection"},
}
)
def _prompt_and_handle_slash_commands(self, prompt: str, *, _multiline: bool = False) -> str:
"""Prompts the user, takes care of /h (followed by requery) and sets the mode. Returns the user input."""
console.print(prompt, end="")
if _multiline:
return _multiline_prompt()
user_input = _prompt_session.prompt("")
if user_input == "/m":
return self._prompt_and_handle_slash_commands(prompt, _multiline=True)
if user_input == "/h":
console.print(
f"Current mode: [bold green]{self.config.mode}[/bold green]\n"
f"[bold green]/y[/bold green] to switch to [bold yellow]yolo[/bold yellow] mode (execute LM commands without confirmation)\n"
f"[bold green]/c[/bold green] to switch to [bold yellow]confirmation[/bold yellow] mode (ask for confirmation before executing LM commands)\n"
f"[bold green]/u[/bold green] to switch to [bold yellow]human[/bold yellow] mode (execute commands issued by the user)\n"
f"[bold green]/m[/bold green] to enter multiline comment",
)
return self._prompt_and_handle_slash_commands(prompt)
if user_input in self._MODE_COMMANDS_MAPPING:
if self.config.mode == self._MODE_COMMANDS_MAPPING[user_input]:
return self._prompt_and_handle_slash_commands(
f"[bold red]Already in {self.config.mode} mode.[/bold red]\n{prompt}"
)
self.config.mode = self._MODE_COMMANDS_MAPPING[user_input]
console.print(f"Switched to [bold green]{self.config.mode}[/bold green] mode.")
return user_input
return user_input
