Agent Sandbox Registry

agent-sandbox

Decision: Plug in coding-agent backends behind a protocol boundary (Qwen Code stays the default)

无中文版

.agents/notes/implemented/architecture/2026-09-29-pluggable-coding-agent-backend.md

Decision: Plug in coding-agent backends behind a protocol boundary (Qwen Code stays the default)

Status: implemented Implemented: 2026-09-29 Related: 2026-09-29-nl-create-qwen-code-integration.md, 2026-09-29-single-question-clarification.md

Problem

The natural-language create path (abx create "<description>") was hard-wired to Qwen Code at every level of src/agent_sandbox/cli/commands/sandbox.py:

The product direction is that the sandbox's built-in agent may later be Qoder CLI, Codex, Claude Code, or a user-defined command — and the host-side generation agent may differ from the in-sandbox agent. Adding a second backend must not mean surgery on the create pipeline and a pile of near-duplicate messages.

Decision

Introduce one seam, no plugin framework: src/agent_sandbox/agent/coding_agent.py.

Boundary contract

CodingAgentBackend (a typing.Protocol) — what the create pipeline needs:

Member Purpose
name, display_name identifier + every user-facing message
config_key, env_var credential storage (abx config set <config_key>) and environment variable
base_url_config_key, model_config_key optional model endpoint overrides
credential_prompt interactive prompt label when a key must be entered
credential_error, not_installed_error the E-code error types the CLI raises for this backend
quick_setup_lines(reason=...) the Quick Setup guide ("not-installed" / "no-credentials")
find_binary() / install(on_progress=...) locate or install the host executable
resolve_credentials(stored_api_key=..., llm_api_key=..., stored_base_url=..., stored_model=...) resolve model credentials (raises credential_error)
prepare_workdir(description) (workdir, template_name) shared by clarification and generation
assess(prompt, *, workdir, binary, env, session_id=None, resume=None) one structured completeness-assessment round; None = unavailable → the CLI degrades to direct generation
generate(description, *, workdir, template_name, resume_session=None, binary=None, env=None, on_progress=None) produce Dockerfile + template.yaml

CodingAgentCredentials is a one-method protocol (as_env() -> dict[str, str]) so a backend may inject whatever variables its CLI needs.

resolve_coding_agent_backend(name=None) returns a fresh instance from the module-level _BACKENDS mapping; None selects DEFAULT_CODING_AGENT ("qwen-code"). Unknown names raise ValueError with the available names — the flow never silently falls back to a different agent than requested.

Adapter

QwenCodeBackend is the only shipped backend. It carries the class variables above (so every CLI message is derived, not duplicated) and every method delegates to the verified modules — imported lazily inside the method:

The lazy imports exist specifically so the existing patch points on those source modules keep intercepting every call — the whole CLI test suite (tests/test_cli/test_create_nl.py) monkeypatches there, never on the adapter.

Orchestration

sandbox.py now talks to CodingAgentBackend only:

_clarify_requirements still imports agent_sandbox.agent.clarify directly for the loop's agent-neutral surface (threshold, prompt builders, ClarifyOutcome shape, round budget); the model-dependent parts go through backend.assess. The displayed degradation warning interpolates backend.display_name.

Preserved behaviour

Migration points (deliberately NOT implemented)

Recorded so the next step is mechanical, not exploratory:

  1. No plugin registry / entry-point discovery. The shipped set is the static _BACKENDS mapping. Add a class here to ship a new built-in backend; entry-point loading is only worth it when third-party plugins exist.
  2. No --coding-agent selector. resolve_coding_agent_backend() is always called with the default. Wiring the flag is one option + passing its value at the top of _generate_and_deploy_template.
  3. The clarify/codegen protocols speak the Qwen headless dialect (--json-schema, --session-id, --resume, structured_result payload; sessions keyed by working directory). A second backend implements assess/generate natively (e.g. Claude Code --output-format json), or those two methods get factored into shared helpers once a real second implementation shows the common shape.
  4. abx config init / abx config set still name the Qwen keys in the guided wizard. The backend class variables are the single source for the create path only; generalising the wizard is follow-up work.
  5. In-sandbox agent axis untouched. The example templates (qwen-code, qoder) and the envd-side deployment in api/deploy.py are about what runs inside the sandbox and are out of scope here — this boundary is the host-side generation agent only.

Dependencies

None new — typing.Protocol and ClassVar are stdlib.

Test Strategy

Acceptance criteria

Files changed

Consequences