Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ implements the **orchestrator → worker → reviewer pattern** (the AI agent
orchestration loop) as a deterministic harness with a closed feedback loop: a goal
is decomposed into subtasks, fanned out to worker subagents, aggregated, and run
through a review gate that loops until the work meets its success criteria. One
loop drives any LLM backend — Anthropic Claude, Claude Code, Codex, opencode, or
aider — through a single `Agent` interface, and it ships as both an **MCP server**
loop drives any LLM backend — Anthropic Claude, Claude Code, Codex, opencode, aider,
or GitHub Copilot — through a single `Agent` interface, and it ships as both an **MCP server**
and a plain CLI so any coding agent can call it.

The design principle: **the loop is a harness (deterministic code), not a skill.**
Expand Down Expand Up @@ -97,7 +97,7 @@ from agentloop.adapters import CliAgent

# Point the worker at a repo and let it actually edit files headlessly:
agent = CliAgent.claude_code(cwd="/path/to/repo", skip_permissions=True)
orch = Orchestrator(agent) # or .codex() / .opencode() / .aider() / .grok_build()
orch = Orchestrator(agent) # or .codex() / .opencode() / .aider() / .grok_build() / .copilot()
result = orch.run(goal="Add a /health endpoint + test", success_criteria="test passes")
```

Expand Down Expand Up @@ -156,7 +156,7 @@ python3 -m examples.run_with_cli_agent claude /path/to/repo
```

```bash
python3 -m examples.run_with_cli_agent claude # codex | opencode | aider | grok
python3 -m examples.run_with_cli_agent claude # codex | opencode | aider | grok | copilot
```

Custom CLI? It's just a command template (`{prompt}`, `{system}`, `{combined}`;
Expand Down Expand Up @@ -292,8 +292,8 @@ is unnecessary: `claude mcp add athena-loops -- agentloop-mcp`.
Then (restart the session first) ask the host agent to "use agentloop to
orchestrate: <goal>". The default `backend="auto"` picks the matching worker for
the caller (`codex` from Codex, `opencode` from OpenCode, `claude_code` from
Claude Code when detectable). Choose a concrete `backend` (`claude_code`,
`codex`, `mock`, …) to override that.
Claude Code, `copilot` from GitHub Copilot when detectable). Choose a concrete
`backend` (`claude_code`, `codex`, `copilot`, `mock`, …) to override that.

For agents that *don't* speak MCP but can run a shell, there's a plain CLI over
the same contract:
Expand Down
14 changes: 14 additions & 0 deletions agentloop/adapters/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,17 @@ def grok_build(
if model:
cmd += ["-m", model]
return cls(cmd, **kw)

@classmethod
def copilot(
cls, *, model: Optional[str] = None, skip_permissions: bool = False, **kw
) -> "CliAgent":
# `-s` prints only the final answer (clean stdout); `--no-ask-user` keeps
# the agent autonomous instead of stopping to ask. `--allow-all-tools`
# auto-approves tool use for headless runs (the skip-permissions analogue).
cmd = ["copilot", "-p", "{combined}", "-s", "--no-ask-user"]
if skip_permissions:
cmd.append("--allow-all-tools")
if model:
cmd += ["--model", model]
return cls(cmd, **kw)
2 changes: 1 addition & 1 deletion agentloop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def build_parser() -> argparse.ArgumentParser:
help="let CLI workers use tools without prompting (needs --cwd)")
r.add_argument("--no-isolate", action="store_true",
help="do NOT run in a throwaway worktree when --cwd is set")
r.add_argument("--model", help="model override for claude_code / grok_build / claude_api / grok_api")
r.add_argument("--model", help="model override for claude_code / grok_build / copilot / claude_api / grok_api")
r.add_argument("--json", action="store_true", help="emit full result as JSON")
r.add_argument("--progress", action="store_true",
help="stream per-iteration NDJSON to stderr")
Expand Down
13 changes: 9 additions & 4 deletions agentloop/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"opencode": CliAgent.opencode,
"aider": CliAgent.aider,
"grok_build": CliAgent.grok_build,
"copilot": CliAgent.copilot,
}
BACKENDS = ["mock", "claude_api", "grok_api", *_CLI_PRESETS]
DEFAULT_BACKEND = "auto"
Expand Down Expand Up @@ -96,6 +97,7 @@ def _backend_status(name: str) -> dict[str, Any]:
"opencode": "opencode",
"aider": "aider",
"grok_build": "grok",
"copilot": "copilot",
}[name]
path = shutil.which(executable)
status: dict[str, Any] = {
Expand Down Expand Up @@ -130,6 +132,7 @@ def _caller_backend(caller_agent: Optional[str] = None) -> Optional[str]:
"grok_api": "grok_api",
"xai_api": "grok_api",
"xai": "grok_api",
"copilot": "copilot",
}
if hint in aliases:
return aliases[hint]
Expand All @@ -143,6 +146,8 @@ def _caller_backend(caller_agent: Optional[str] = None) -> Optional[str]:
("CLAUDE_CODE", "claude_code"),
("CLAUDE_SESSION_ID", "claude_code"),
("GROK_AGENT", "grok_build"),
("COPILOT_CLI", "copilot"),
("COPILOT_AGENT_SESSION_ID", "copilot"),
)
for env_name, backend in env_hints:
if os.environ.get(env_name):
Expand Down Expand Up @@ -246,7 +251,7 @@ def _build_agent(backend: str, cwd: Optional[str], skip_permissions: bool,
if cwd:
kw["cwd"] = cwd
kw["skip_permissions"] = skip_permissions
if backend in ("claude_code", "grok_build") and model:
if backend in ("claude_code", "grok_build", "copilot") and model:
kw["model"] = model
return _CLI_PRESETS[backend](**kw)
raise ValueError(f"unknown backend {backend!r}; choose from {BACKENDS}")
Expand Down Expand Up @@ -813,16 +818,16 @@ async def orchestrate(
orchestrator proposes criteria itself.
backend: Worker engine — "auto" (default: same agent family as the
caller when detectable), "claude_code", "codex", "opencode",
"aider", "grok_build", "claude_api", "grok_api", or "mock".
"aider", "grok_build", "copilot", "claude_api", "grok_api", or "mock".
caller_agent: Optional caller identity hint for backend="auto", e.g.
"codex", "opencode", "claude", or "grok". Explicit backend overrides it.
"codex", "opencode", "copilot", "claude", or "grok". Explicit backend overrides it.
cwd: Repo to work in. Required for coding tasks that edit files.
max_iterations: Cap on decompose->review cycles (termination guard).
skip_permissions: Let CLI workers use tools without prompting. Only
meaningful with `cwd`; the run is isolated in a worktree.
isolate: When `cwd` is set, run in a throwaway git worktree/branch so
the caller's checkout is untouched (recommended).
model: Optional model override for claude_code / grok_build / claude_api / grok_api.
model: Optional model override for claude_code / grok_build / copilot / claude_api / grok_api.
timeout: OPTIONAL seconds to cap EACH worker CLI subprocess call. None
(default) = no per-call cap. Leave unset for normal runs; set it
only to force a genuinely stuck worker to fail instead of hanging.
Expand Down
2 changes: 2 additions & 0 deletions examples/run_with_cli_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
python3 -m examples.run_with_cli_agent claude [REPO_DIR]
python3 -m examples.run_with_cli_agent codex [REPO_DIR]
python3 -m examples.run_with_cli_agent grok [REPO_DIR]
python3 -m examples.run_with_cli_agent copilot [REPO_DIR]
python3 -m examples.run_with_cli_agent opencode

Pass REPO_DIR to run the worker against a real repo. The run happens in an
Expand All @@ -30,6 +31,7 @@
"opencode": CliAgent.opencode,
"aider": CliAgent.aider,
"grok": CliAgent.grok_build,
"copilot": CliAgent.copilot,
}

GOAL = "Add a /health endpoint that returns {status: ok} and a test for it."
Expand Down
9 changes: 9 additions & 0 deletions tests/test_cli_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ def test_skip_permissions_adds_bypass_flag():
assert "--yes-always" in CliAgent.aider(skip_permissions=True).command
assert "--always-approve" in CliAgent.grok_build(skip_permissions=True).command
assert "--always-approve" not in CliAgent.grok_build().command
assert "--allow-all-tools" in CliAgent.copilot(skip_permissions=True).command
assert "--allow-all-tools" not in CliAgent.copilot().command


def test_grok_build_preset_shape():
Expand All @@ -146,6 +148,13 @@ def test_grok_build_preset_shape():
assert cmd[-2:] == ["-m", "grok-build-0.1"]


def test_copilot_preset_shape():
cmd = CliAgent.copilot(model="gpt-5.4").command
assert cmd[:2] == ["copilot", "-p"]
assert "-s" in cmd and "--no-ask-user" in cmd
assert cmd[-2:] == ["--model", "gpt-5.4"]


def test_build_agent_passes_skip_permissions_to_opencode():
from agentloop.mcp_server import _build_agent
agent = _build_agent("opencode", cwd=None, skip_permissions=True,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def test_auto_backend_uses_caller_agent_hint(monkeypatch):
assert _resolve_backend("auto", caller_agent="opencode") == "opencode"
assert _resolve_backend("auto", caller_agent="claude") == "claude_code"
assert _resolve_backend("auto", caller_agent="grok") == "grok_build"
assert _resolve_backend("auto", caller_agent="copilot") == "copilot"


def test_auto_backend_uses_environment_hint(monkeypatch):
Expand All @@ -149,6 +150,16 @@ def test_auto_backend_uses_grok_environment_hint(monkeypatch):
assert _resolve_backend("auto") == "grok_build"


def test_auto_backend_uses_copilot_environment_hint(monkeypatch):
for name in (
"OPENCODE", "OPENCODE_RUN_ID", "CODEX_RUN_ID", "CODEX_SESSION_ID",
"CLAUDECODE", "CLAUDE_CODE", "CLAUDE_SESSION_ID", "GROK_AGENT",
):
monkeypatch.delenv(name, raising=False)
monkeypatch.setenv("COPILOT_CLI", "1")
assert _resolve_backend("auto") == "copilot"


def test_explicit_backend_overrides_caller_hint():
assert _resolve_backend("codex", caller_agent="opencode") == "codex"

Expand All @@ -158,6 +169,7 @@ def test_backend_aliases():
assert _resolve_backend("grok-build") == "grok_build"
assert _resolve_backend("xai") == "grok_api"
assert _resolve_backend("grok-api") == "grok_api"
assert _resolve_backend("copilot") == "copilot"


def test_orchestrate_impl_reports_cli_loop_failure_after_intake_fallback(monkeypatch):
Expand Down Expand Up @@ -218,6 +230,14 @@ def test_isolate_runs_in_worktree_and_reports_it():
def test_backends_listed():
assert "mock" in BACKENDS and "claude_code" in BACKENDS
assert "grok_api" in BACKENDS and "grok_build" in BACKENDS
assert "copilot" in BACKENDS


def test_build_agent_forwards_model_to_copilot():
from agentloop.mcp_server import _build_agent
agent = _build_agent("copilot", cwd=None, skip_permissions=True,
model="gpt-5.4", timeout=None)
assert "--model" in agent.command and "gpt-5.4" in agent.command


def test_doctor_reports_backends_and_timeout_guidance(tmp_path):
Expand Down