Skip to content

mini-swe-agent: a Base Agent, an acting agent and a deployment host - #49

Merged
haizhongzheng merged 2 commits into
Infini-AI-Lab:mainfrom
Michael-Jiahao-Zhang:feat/mini-swe-agent
Nov 9, 2026
Merged

haizhongzheng merged 2 commits into
Infini-AI-Lab:mainfrom
Michael-Jiahao-Zhang:feat/mini-swe-agent

Conversation

@Michael-Jiahao-Zhang

@Michael-Jiahao-Zhang Michael-Jiahao-Zhang commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Change

Adds mini-swe-agent (2.4.6) to Hermit on the agent layers. It is one of the further agent integrations of #37 §3, discussed with the maintainers beforehand.

The PR was rebuilt twice:

  • On the layer refactor. The first version had mini-swe-agent as an Acting Agent of its own; it is now a Base Agent used by the existing workspace Acting Agent.
  • On Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89. Pi arrived as a CLI Base Agent while this PR was open, so mini-swe-agent now follows the same contracts Pi does.

Layer 2: the mini-swe-agent Base Agent. MiniSweAgent is a CliAgent subclass in CLIS (hermit/agent/base/cli.py), beside Codex, Claude Code and Pi.

  • Process. One run is one task. It runs through mini-swe-agent's Python API, the way its batch runner runs one, in a process of its own: python -P -m hermit.agent.base.mini_swe_agent_run (-P keeps the workspace off its import path). That process writes the session as JSON events on stdout. CliAgent keeps them as the stream (agent-mini-swe-agent.jsonl) and reads them into the reply, the steps (mini_swe_agent_steps in trajectory.py) and the usage (parse_mini_swe_agent in records.py).

  • What it reuses from CliAgent. Isolation (none or the run's sandbox; not a container, as for Pi), the home (MSWEA_GLOBAL_CONFIG_DIR), the tool server, the clock and the retries of a provider that could not serve. Its hooks are the command, the home's settings file, served, errors, outcome, last_message, parse and steps.

  • Messages.

    • Hermit's prompts (mini_swe_agent.yaml) go over mini-swe-agent's own mini / mini_textbased config, then a config file's settings.
    • The system message ends with one block (adapter_block): the deployment's context (hermit.context in the workspace's mini-swe-agent.yaml), the call's tools and the call's system instructions.
    • The agent submits as mini-swe-agent's swebench config does (echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT && cat answer.txt); the reply is the submission, else the last assistant message.
  • Tools. The call's tools are one bash command, hermit-tool <name> '<JSON arguments>'. The process's environment (mini_swe_agent_env.py) answers it through the tool server's JSON entrance (HERMIT_TOOLS_URL).

  • The model's key.

    • It comes from the binding's variable and goes with each request. It is written to neither the stream nor the home's settings file (which holds only the names of env).
    • The commands get env and a few basic variables only.
    • On Linux the process is non-dumpable, so a command run as the same user cannot read its environment through /proc.
    • Without a model reference, mini-swe-agent's own .env is the sign-in file (home_files), as auth.json is Codex's and Pi's.
  • Failures (outcome, the contract Pi follows):

    What happens How it is reported
    No model Refused by the Base Agent before any process starts (ValueError)
    A config that names the model or the environment class, or a first model call refused for the settings (401, 404, or the 400 DeepSeek returns for a wrong key) Starts no session: stderr, exit 2, no event. Events are held back until the model answers once, so the acting agent raises rather than score every task 0
    A model call that breaks the session off later (a context overflow; a 400 is not retried), or a process cut off without mini-swe-agent's closing exit message (killed) failed: no answer is judged and the task is an error record (session_failed), as Pi's failed request is; its tokens count
    A provider that stops serving (rate limit, outage), even after replies Marked unavailable, not served: the task runs again (retries)
    The Base Agent's timeout The process group is stopped, and the running command, which runs in a session of its own, is killed by the process's SIGTERM handler
  • Refused. These are refused with Unsupported:

    • roles;
    • session (mini-swe-agent cannot resume);
    • login;
    • web_search: true;
    • with a model reference, a model class other than litellm / litellm_textbased, since another class would send the binding's key and URL to its own service.

Layer 3. agent.name: mini-swe-agent is the workspace Acting Agent (hermit/agent/acting/workspace.py) with backend mini-swe-agent, so prompt, files, mounts, prepare, answer, collect, context_query and retries work as for the other CLIs.

  • Deployment and tools. The adapter is deployed into each workspace by the new host. Its tools are served by the Base Agent's tool server, as a container session's are.
  • Refusals and config form. _check refuses a login and a container for it in the branch it now shares with Pi. The layered form is agent.base: mini-swe-agent with an agent.mini-swe-agent section.
  • Model binding. A model reference is bound as for the other CLIs: bind_agent_params kind mini-swe-agent, listed in CLI_KINDS, with NEEDS set to chat completions.

Deploy. hermit deploy --host mini-swe-agent (hermit/hosts/mini_swe_agent.py) follows the shared layout.

  • Workspace layout. The adapter is copied to hermit/<id>/, and the context names what mini-swe-agent cannot load (skills, roles, hooks, other files) through hermit.hosts.inputs.resources.
  • mini-swe-agent.yaml. It holds Hermit's prompts over mini-swe-agent's mini config, with the adapter's block in the system template as literal text. The block is a Jinja raw block, with every spelling of endraw escaped.
  • Tools. An adapter with tools names AdapterEnvironment (a Session in mini-swe-agent's process) as the environment class, plus an audit log.
  • Training and deployment read the same text. mini -c mini-swe-agent.yaml gets the system message the Base Agent sends (a test asserts it). The acting agent's own model_class / config are not deployed (documented).

Dependency. A mini-swe-agent optional extra (pip install "hermit-harness[mini-swe-agent]"). It is not a dependency group, because the groups are the methods' (METHOD_REQUIREMENTS). CI installs it so the tests run rather than skip. render_config locates the package rather than importing it, so a deploy or the Trainer never gets mini-swe-agent's import side effects.

Docs.

  • Tutorial and an example, docs/_examples/mini_swe_agent/: nine CSV questions, an exact-format judge, GEPA as the learner, DeepSeek.
  • mini-swe-agent added to the agent-layers tables, the adapter mapping table, the commands, task, configuration, config-fields, model-backends, installation and compatibility pages, the FAQ, the web page and the CHANGELOG.
  • Lines this PR edits that listed the CLIs without Pi now name it too.

Core changes. These are shared layers that every method group runs on. They extend existing tables and branches; no new mechanism.

  • hermit/agent/base/cli.py: the subclass in CLIS.
  • trajectory.py: the reader in READERS / STREAM_HOSTS / MARKS; it keys on session.started, which no other CLI emits.
  • records.py: the usage parser. parse_log now also dispatches Pi, which before fell through to Claude Code's reader.
  • acting/workspace.py, acting/agent.py: the backend, the deploy branch, the settings the Base Agent takes, and Pi's _check branch shared.
  • config.py, defaults.yaml: the agent.base value and its section.
  • model/backends.py: CLI_AGENTS, CLI_KINDS, NEEDS, the binding kind.
  • hermit/isolate.py: the config check that refuses a login for Pi now refuses it for mini-swe-agent too, with the agent's name. This is configuration validation only; what the sandbox shows or allows is unchanged.
  • .github/workflows/tests.yml: the extra.

Validation

Tests. tests/core/test_mini_swe_agent.py, 24 tests. The stand-in is the model: an OpenAI-compatible server with the API's payload shape. mini-swe-agent's loop, its environment and litellm run unchanged. Run time is about 70 s; the longest test is the 9 s timeout test, whose delay is what it holds.

What the 24 tests cover
  • The acting agent with the file adapter: context, resources, tools, an unknown tool, env, no key in any file, steps, tokens and cost.
  • The text-based model class; the step limit.
  • A failed call as a failed session; a process killed mid-session as a failed session.
  • Retry when the provider stops serving midway (a 429 after a reply).
  • A first-call 401, 404 and DeepSeek-style 400 raising as a session that never started.
  • A timeout killing the running command.
  • A workspace yaml.py that does not shadow the runner's imports.
  • The key not readable from /proc/$PPID/environ (Linux, non-root).
  • Refusals in the Base Agent, workspace._check and isolate's config check, including a bad config raising rather than scoring.
  • Deploy: the deployed config renders the Base Agent's system message for five endraw spellings; hermit-tool through mini-swe-agent's own get_environment; roles named; local environment without tools; redeploy; the other-host refusal; the CLI.
  • The packaged prompts, not a mini.yaml in the working directory.
  • The layered config.
  • A rollout in the task sandbox under bubblewrap: the model reached through the forwarded port, the run's data not shown.

The regression tests for the review findings fail without their fixes (checked by reverting each fix). test_context_tool.py adds the host to the not-portable refusal. test_layers, test_selection and test_packaging pass.

Test runs at this head, with litellm 1.108.3 and openai 3.46.0, the versions CI resolves with the other groups:

Where Result
macOS, pytest tests/core (before the last audit's small fixes) 600 passed, 60 skipped, 7 failed. The 7 fail identically on unmodified main on this machine: test_backends::test_check_reports_models_and_train_refuses_a_missing_dialect, test_cli_agent::test_a_stream_replaced_by_a_link_is_not_read_as_the_sessions, two test_harbor_runner, test_isolate::test_a_worker_whose_log_cannot_be_opened_leaves_no_descriptor_open, test_lean_swarm_launch, test_session_sandbox::test_a_server_that_cannot_start_is_reported
macOS, after those fixes test_mini_swe_agent.py, test_pi_agent.py, test_layers.py and test_selection.py pass (2 Linux-only skips)
Linux, Ubuntu 24.04, non-root test_mini_swe_agent.py: 23 passed, 1 skipped (bubblewrap); at b42cff9 also test_pi_agent.py and test_cli_agent.py (92 passed, skips for bubblewrap and the pi CLI)
Linux, tests/core Fails only in bubblewrap-dependent tests: on this host unprivileged user namespaces are restricted by AppArmor (bwrap: loopback: Failed RTM_NEWADDR), plus the one test_backends failure above
Linux, privileged root container with bubblewrap test_mini_swe_agent.py: 23 passed, the sandboxed rollout among them. The /proc test is skipped there, since root reads any process's environment

bash docs/build.sh (warnings as errors) passes.

End to end with DeepSeek (deepseek/deepseek-flash), the example as shipped.

Run eval --adapter none train eval --adapter final
Privileged container, sandbox.enforce=required (every task in the task sandbox, the learner in its own), this head 0.0 candidate accepted, validation 0.667 1.0, 0 errors
The same, at b42cff9 (the #89 rebase, before the last audit's fixes) 0.0 candidate accepted, validation 1.0 0.667, 0 errors
Linux host, sandbox.enabled=false (bubblewrap cannot run there), before the #89 rebase 0.0 1 step, 93.7k tokens; validation 1.0 0.667, 0 errors
  • The failed tasks broke the submission protocol: for example, the model chained its submit command after writing the file, then submitted an empty answer. That is the agent's failure.
  • hermit check with a wrong DEEPSEEK_API_KEY reports [PROBLEM] agent probe ... exited 2 before starting a session: ... Authentication Fails in about 5 s.
  • After a deploy of a trained checkout, mini -c mini-swe-agent.yaml -m deepseek/deepseek-flash -y --exit-immediately -t "..." answered {"answer": 448} (correct) in 3 calls.
  • No price for deepseek-flash is in the built-in backend, so costs are unknown.
One rollout's sandbox record (runs/mini-swe-agent/sandbox/rollout-1.json, privileged container, abridged)
{"role": "rollout", "program": "/opt/venv/bin/python3", "enforced": true, "network": "isolated",
 "read_only": ["/opt/venv/...", "/work/hermit", "/work/.demo/m/environment", "/work/.demo/m/runs/mini-swe-agent/rollout_adapter"],
 "read_write": [".../.logs/rollout/<task>", ".../.logs/rollout/<task>/home"],
 "allow": ["api.deepseek.com"],
 "probe": {"passed": true, "outside": {"1.1.1.1:443": "blocked (OSError)", "8.8.8.8:53": "blocked (OSError)"},
           "denied": {"/root/.bashrc": "absent", "/work/.demo/m/tasks.jsonl": "absent", "/work/.demo/m/train.txt": "absent",
                      "/work/.demo/m/val.txt": "absent", "/work/.demo/m/test.txt": "absent", "/work/.demo/m/judge.py": "absent"}},
 "environment": ["DEEPSEEK_API_KEY", "HOME", "HTTPS_PROXY", "MSWEA_GLOBAL_CONFIG_DIR", "PATH", "PYTHONPATH", "..."]}

Not verified:

  • Layer 2's own isolation: sandbox for mini-swe-agent: the acting agent reaches the sandbox through the task sandbox, where the inner isolation is none.
  • An unprivileged Linux account with working bubblewrap: the sandbox runs were as root in a privileged container. CI's Ubuntu runner covers it once its workflow is approved.
  • The non-dumpable protection on macOS: there ps eww shows a process's environment to the same user (documented).

Review

  • Tests and documentation cover the affected behavior, where applicable. See Validation, and Docs under Change.
  • Commands, defaults, and public interfaces are consistent. --host mini-swe-agent, agent.name / agent.base: mini-swe-agent and agent.mini-swe-agent.* are listed where the other CLIs are.
  • Shared logs and examples contain no credentials or private data. A test asserts that no file of a run holds the key; the sandbox record lists variable names only.
  • Documentation-only: not applicable.
  • Core changes. Listed under Change.
  • Implementation correctness and failures. Configuration flows agent.* → bind_agent_params → base_agent() settings → the home's JSON → the process. Errors, timeouts, retries and failed sessions are covered above, with tests.
  • Real execution paths and tests. The tests go through load_agent, Config.load and the Base Agent's run, the real mini-swe-agent and litellm, the deploy host and mini-swe-agent's own get_environment. The e2e runs go through hermit check/eval/train/deploy and the mini CLI.
  • Test checklist.
    • Tests sit in tests/core/, which --changed runs for these shared paths.
    • Names are sentences, and the file and the subtle tests have docstrings.
    • Stand-ins are at the model only, built from OpenAI's response shape.
    • Tests write to tmp_path and use no network beyond loopback.
    • They skip without the extra, bubblewrap, Linux or a non-root user.
  • Sandbox checklist.
    • Every process runs through the framework's sandbox: the acting agent and its mini-swe-agent process in the task sandbox (the record above), the learner in the evolver sandbox.
    • The one line in hermit/isolate.py is the config check's message (see Core changes). The sandbox itself is unchanged.
    • With the sandbox on, a config file must be declared in environment.read (documented).
  • This description. Rewritten for head after the Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 rebase and read against the code and run records.
  • Independent reviews. Three separate agent sessions reviewed against the general, test and sandbox checklists: once before the first push, and twice as post-pr-audit (at b5047fc, and at b42cff9 after the Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 rebase). Findings and fixes, with regression tests where testable:
    • (high) The process imported modules from the workspace; it now runs with -P.
    • (high) A provider failure after the first reply was scored as an answer; it is now not served, and retried.
    • (medium) Commands could read the key from the parent's environment; the process is non-dumpable on Linux.
    • (medium) Configuration errors and wrong keys or models became wrong answers; they now start no session, so the acting agent raises. Verified with DeepSeek, which answers a wrong key with a 400 that mini-swe-agent used to retry for about 4 minutes; a 400 is no longer retried.
    • (medium) A model reference with another model class sent the binding's key to that class's service; now refused.
    • (medium, Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 rebase audit) A local session log had been committed by mistake; it is removed.
    • (medium, Add Pi as a CLI Base Agent (acting agent and pi Evolver) #89 rebase audit) This description no longer matched the head; it is rewritten.
    • (low) A timeout left the running command alive on macOS; it is now killed.
    • (low) A process cut off after replying was scored as an empty answer; it is now a failed session.
    • (low) Duplicate error steps; RRSI's nested reader; render_config without importing mini-swe-agent; MSWEA_GLOBAL_CONFIG_DIR honoured; refusal messages naming the agent; docs listing Pi and mini-swe-agent alike; the task-sandbox note for config; tests for the merged refusals.
    • Kept as designed: at the step limit the reply is the last assistant message, as a CLI's last message is, which answer reads.

@Michael-Jiahao-Zhang Michael-Jiahao-Zhang changed the title mini-swe-agent: an acting agent and a deployment host mini-swe-agent: a Base Agent, an acting agent and a deployment host Nov 8, 2026
- `mini-swe-agent` is a CLI Base Agent beside Codex, Claude Code and Pi (`MiniSweAgent` in CLIS): one call runs one
  task through mini-swe-agent's Python API in a process of its own (hermit/agent/base/mini_swe_agent_run.py), which
  writes the session as JSON events, the stream CliAgent keeps and reads into the reply, the trajectory steps and
  the usage. Isolation, the home, the tool server, the clock and the retries of a provider that could not serve are
  CliAgent's. The call's tools are one bash command, `hermit-tool`, answered through the tool server's JSON
  entrance; the model's key goes with each request, the commands get only `env`, and on Linux the process is
  non-dumpable. A setting that fails every task (a config that names the model, a wrong key or model id found by
  the first call) starts no session; at the timeout the running command is killed too.
- `agent.name: mini-swe-agent` (or `agent.base: mini-swe-agent`) is the workspace Acting Agent with that backend,
  its model bound as the other CLIs' are; a reference takes the litellm model classes only.
- `hermit deploy --host mini-swe-agent` deploys as every host does (the copy under hermit/<id>/, what mini-swe-agent
  cannot load named in the context) and writes `mini-swe-agent.yaml`, whose system template gives `mini` the system
  message the Base Agent sends.
- The `mini-swe-agent` extra (installed by CI), a tutorial and an example, and the agent and host in the reference
  tables.
…n sandbox, core-only test collection)

- after a format error mini's own message told the model to end with the bare COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT,
  an empty submission: Hermit's format_error_template ends with `... && cat answer.txt`
- model_class is litellm or litellm_textbased in every case: the *_response classes' messages and the openrouter /
  portkey classes' errors are not litellm's (usage uncounted, a rate limit not retried)
- under isolation: sandbox the home names no host path, as Pi's: the config is read into the settings, the system
  instructions go on the command (past ARG_MAX in a file, Claude Code's _prompt_file, now CliAgent's)
- a thinking model's reasoning_content is a reasoning step of the trajectory
- tests/core collects without mini-swe-agent (jinja2 imported only when present)
- docs: the tutorial's time-limit sentence; agent.base / agent.<base> rows in config-fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@haizhongzheng haizhongzheng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. We reviewed it in seven rounds (our own read plus an independent Codex review each round) and pushed the fixes as one commit, 844897b, on this branch. Every fix has a test that fails without it.

Three-layer abstraction

It follows docs/framework/agent-layers.md:

  • Layer 2. mini-swe-agent is a Base Agent. It is a CliAgent subclass in CLIS, with its stream reader in trajectory.py.
  • Layer 1. Its model binding is in backends.py.
  • Layer 3. The workspace Acting Agent is reused.
  • Imports and the shared runner. Imports go one way only. MiniSweAgent uses only hooks that already exist and does not change the shared runner.

One small leak stays as it is, by our decision: _deploy in acting/workspace.py branches on backend == "mini-swe-agent" to choose who serves the tools.

What we patched (844897b)

Before After
Test collection tests/core/test_mini_swe_agent.py imported jinja2 at the top. With only pip install -e '.[test]' (AGENTS.md's setup), collection of tests/core stopped with an error. jinja2 is imported only when present, and the module is skipped without mini-swe-agent.
Empty submission After a format error, mini's inherited model.format_error_template told the model to end with a bare echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT. That gives an empty submission, which is likely the failure seen in the e2e run. mini_swe_agent.yaml overrides it for mini, ending with `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT && cat answer.txt`. render_config puts it in the model section.
Model classes Without a binding, any class was accepted. The *_response classes return Responses-API messages, so their usage was not counted and their steps were empty. The openrouter, portkey and requesty classes raise their own errors, so a rate limit was not retried and a wrong key was not a setup error. model_class must be litellm or litellm_textbased in every case. Another provider's model goes through litellm (openrouter/<model>).
isolation: sandbox The home's mini-swe-agent.json held host paths: the config file's path and the system text. The sandbox translates only the command, the environment and the prompt, so the session never started. The home names no host path, as for Pi. The config is read into the settings. The system instructions go on the command as --system, or --system-file past ARG_MAX, using Claude Code's _prompt_file, moved unchanged into CliAgent. An end-to-end bubblewrap test covers short and long instructions.
Reasoning reasoning_content was dropped, so the trajectory had no reasoning steps, unlike Codex, Claude Code and Pi. It is kept in the events and read as Step("reasoning").
Docs The tutorial said a time-limit stop returns the last assistant message; the reply is empty. config-fields.md omitted mini-swe-agent. Both corrected.

Tests: test_mini_swe_agent, test_layers, test_context_tool, test_base_agents, test_pi_agent and test_cli_agent give 148 passed and 2 skipped, with mini-swe-agent 2.4.6. In a core-only environment the mini-swe-agent tests are skipped.

Left for follow-ups (not blocking)

  • Retry accounting. A run the provider could not serve is retried, but only the last attempt's usage is kept. This is the shared Layer 3 retry loop for every CLI. mini meets it more often, since it retries after billed replies.
  • Background processes. A command's background child (server &) is not stopped at the end, timeout or cancel under isolation: none. Upstream's _run behaves the same, and a sandbox ends it.
  • Long system file. Past ARG_MAX, the system-instructions file keeps host paths: its contents are not translated. Claude Code's _prompt_file has the same behaviour.
  • deploy --python. It is not the interpreter the adapter's tools run in, because mini runs them in its own process. The deployment's launch command already uses that interpreter, but commands.md could say so.

Minor, recorded only:

  • ${VAR} in agent.params.config is not expanded.
  • The example judge accepts 195.0 as 195.
  • An empty submission falls back to the last assistant message.
  • Outputs of commands that run before a submission in the same response are not in the trajectory.
  • mini's MSWEA_GLOBAL_* limits can raise before a response's usage is recorded.
  • The JSON tool entrance is not passed through Tool.render. Adapter tools render identically.
  • Host paths inside a user config are not translated under the Layer 2 sandbox.

@haizhongzheng
haizhongzheng merged commit 6e421a4 into Infini-AI-Lab:main Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants