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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ All notable changes to SkillOpt are documented here. This project adheres to
## [Unreleased]

### Added
- **GitHub Copilot CLI backend**, in two forms: `copilot_chat` (usable as both
optimizer and target) and `copilot_exec` (target-only execution harness).
Because the Copilot CLI carries its own sign-in, `--backend copilot` selects
`copilot_chat` for both roles and runs a complete train/eval loop with **no
cloud API key**. Calls disable built-in MCP servers and custom instructions,
and never pass `--allow-all-tools`; `copilot_exec` requires an explicit
`copilot_exec_allow_all_tools` opt-in before granting unattended tool use.
The CLI reports no token counts, so usage totals are zero for these backends.
- A non-destructive Devin installer and SessionEnd activity marker, preserving
existing project hooks across repeated installation.
- Per-night SkillOpt-Sleep `evidence.jsonl` chains for reconstructing harvest,
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ https://github.com/user-attachments/assets/eb12d3bc-371c-467f-904d-91b61f339ed7
### Adding a new backend

A backend = a chat / exec target (e.g. `openai_chat`, `claude_chat`,
`qwen_chat`, `minimax_chat`, `openai_compatible`, `codex_exec`,
`claude_code_exec`, `cursor_exec`). If a provider implements the OpenAI Chat Completions
`qwen_chat`, `minimax_chat`, `copilot_chat`, `openai_compatible`, `codex_exec`,
`claude_code_exec`, `cursor_exec`, `copilot_exec`). If a provider implements the OpenAI Chat Completions
protocol, try the built-in `openai_compatible` backend before adding code. See
[`docs/guide/new-backend.md`](docs/guide/new-backend.md) for the full
contract. Chat backends add a `skillopt/model/<name>_backend.py` module;
Expand Down
9 changes: 9 additions & 0 deletions configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ model:
claude_code_exec_max_thinking_tokens: 16384
cursor_exec_path: "" # blank uses CURSOR_EXEC_PATH or cursor-agent
cursor_exec_sandbox: "" # blank uses CURSOR_EXEC_SANDBOX or enabled
# GitHub Copilot CLI. `--backend copilot` selects copilot_chat for BOTH roles,
# which is the only fully local (no cloud API key) configuration.
copilot_exec_path: "" # blank uses COPILOT_EXEC_PATH or copilot
copilot_exec_home: "" # blank leaves COPILOT_HOME untouched
copilot_exec_allow_all_tools: null # blank uses COPILOT_EXEC_ALLOW_ALL_TOOLS (default off);
# copilot_exec only, required for file-edit rollouts
copilot_chat_optimizer_model: "" # blank lets the CLI pick its default model
copilot_chat_target_model: ""
copilot_chat_timeout: 0 # 0 uses COPILOT_CHAT_TIMEOUT or the built-in default
codex_trace_to_optimizer: true
azure_openai_endpoint: "" # e.g. "https://your-resource.openai.azure.com/"
azure_openai_api_version: "2024-12-01-preview"
Expand Down
31 changes: 31 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ model:
| `claude_chat` | ✓ | ✓ | Claude Code CLI (`claude -p`) |
| `qwen_chat` | ✓ | ✓ | Qwen served through an OpenAI-compatible local endpoint |
| `minimax_chat` | ✓ | ✓ | MiniMax API |
| `copilot_chat` | ✓ | ✓ | GitHub Copilot CLI (`copilot -p`); alias `copilot` |
| `codex_exec` | — | ✓ | Codex CLI execution harness |
| `claude_code_exec` | — | ✓ | Claude Code CLI execution harness |
| `cursor_exec` | — | ✓ | Cursor Agent CLI execution harness |
| `copilot_exec` | — | ✓ | GitHub Copilot CLI execution harness |

`copilot_chat` is the only backend that fills **both** roles from a locally
authenticated CLI, so `--backend copilot` runs a complete training loop with no
cloud API key. Sign in once with `copilot` (GitHub Copilot CLI) beforehand.
Expect roughly 20-40 s per call: the CLI is an agent, not a completions
endpoint, so a full-size run is far slower than a hosted backend. It also
reports no token counts, so usage totals are zero.

The current MiniMax adapter has one shared deployment. Set
`model.minimax_model` when MiniMax is the target; a mixed-backend run cannot
Expand Down Expand Up @@ -191,6 +200,12 @@ Model credentials are loaded from environment variables:
| `QWEN_CHAT_MODEL` | `qwen_chat` | Served model name for direct library use; train/eval YAML role models take precedence |
| `MINIMAX_BASE_URL` | `minimax_chat` | MiniMax-compatible base URL |
| `MINIMAX_API_KEY` | `minimax_chat` | MiniMax API key |
| `COPILOT_EXEC_PATH` | `copilot_chat`, `copilot_exec` | Optional path to `copilot`; defaults to `copilot` |
| `COPILOT_EXEC_HOME` | `copilot_chat`, `copilot_exec` | Optional `COPILOT_HOME` override isolating CLI config; sign-in state lives outside it |
| `COPILOT_EXEC_ALLOW_ALL_TOOLS` | `copilot_exec` | Opt in to `--allow-all-tools` for file-edit rollouts; `false` by default |
| `COPILOT_CHAT_OPTIMIZER_MODEL` | `copilot_chat` | Optional model ID passed as `--model` for optimizer calls |
| `COPILOT_CHAT_TARGET_MODEL` | `copilot_chat` | Optional model ID passed as `--model` for target calls |
| `COPILOT_CHAT_TIMEOUT` | `copilot_chat` | Per-call timeout in seconds |

`OPTIMIZER_` and `TARGET_` prefixes provide per-role overrides for the
Azure, OpenAI-compatible, and Qwen variable families. See the
Expand All @@ -209,6 +224,22 @@ Cursor sandbox by default and rejects file-edit rollouts if it is disabled;
read-only Ask-mode rollouts may explicitly disable it. SkillOpt does not approve
MCP servers automatically.

`copilot_chat` drives the GitHub Copilot CLI as a chat model for either role.
Because the CLI carries its own sign-in, selecting it for both roles
(`--backend copilot`) makes a run fully local:

```bash
copilot # sign in once, then exit
skillopt-train --cfg configs/train/default.yaml --backend copilot
```

Calls are made with built-in MCP servers and custom instructions disabled so
the model sees only the prompt SkillOpt sends, and `--allow-all-tools` is never
passed for chat calls. `copilot_exec` is the separate target-only harness that
runs the CLI as an agent inside a benchmark workspace; unlike the other exec
harnesses it does not grant unattended tool use unless
`COPILOT_EXEC_ALLOW_ALL_TOOLS` is set.

### Three OpenAI-compatible paths

- Research, generic provider: select `openai_compatible` and use
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,12 @@ not via a base class subclass. Supported values (as of this writing):
| `claude_chat` | ✓ | ✓ |
| `qwen_chat` | ✓ | ✓ |
| `minimax_chat` | ✓ | ✓ |
| `copilot_chat` | ✓ | ✓ |
| `openai_compatible` | ✓ | ✓ |
| `codex_exec` | ✓ | ✓ |
| `claude_code_exec` | — | ✓ |
| `cursor_exec` | — | ✓ |
| `copilot_exec` | — | ✓ |

See `skillopt/model/backend_config.py` for the live whitelist and
[`docs/reference/config.md`](./config.md) for the per-backend
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ selecting the generic OpenAI-compatible backend.
| `claude_chat` | ✓ | ✓ |
| `qwen_chat` | ✓ | ✓ |
| `minimax_chat` | ✓ | ✓ |
| `copilot_chat` | ✓ | ✓ |
| `codex_exec` | ✓ | ✓ |
| `claude_code_exec` | — | ✓ |
| `cursor_exec` | — | ✓ |
| `copilot_exec` | — | ✓ |

MiniMax currently has one shared deployment. `model.minimax_model` is applied
when MiniMax is the target; mixed-backend runs cannot independently choose a
Expand Down Expand Up @@ -67,6 +69,11 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.
| `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap |
| `model.cursor_exec_path` | Cursor Agent executable path; default `cursor-agent` |
| `model.cursor_exec_sandbox` | Cursor sandbox mode: `enabled` (default) or `disabled`; file-edit rollouts require `enabled` |
| `model.copilot_exec_path` | GitHub Copilot CLI executable path; default `copilot` |
| `model.copilot_exec_home` | Optional `COPILOT_HOME` override isolating CLI config |
| `model.copilot_exec_allow_all_tools` | Opt in to `--allow-all-tools`; `false` by default, required for file-edit rollouts |
| `model.copilot_chat_optimizer_model` / `model.copilot_chat_target_model` | Optional per-role `--model` IDs for `copilot_chat` |
| `model.copilot_chat_timeout` | Per-call timeout in seconds for `copilot_chat` |

## Training (`train`)

Expand Down
69 changes: 52 additions & 17 deletions scripts/eval_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def parse_args() -> argparse.Namespace:
# Legacy flat overrides
p.add_argument("--env", type=str)
p.add_argument("--backend", type=str,
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "cursor", "cursor_exec", "minimax", "minimax_chat"])
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "cursor", "cursor_exec", "copilot", "copilot_chat", "copilot_exec", "minimax", "minimax_chat"])
p.add_argument("--optimizer_model", type=str)
p.add_argument("--target_model", type=str)
p.add_argument("--optimizer_backend", type=str)
Expand Down Expand Up @@ -184,6 +184,12 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--claude_code_exec_max_thinking_tokens", type=int)
p.add_argument("--cursor_exec_path", type=str)
p.add_argument("--cursor_exec_sandbox", type=str)
p.add_argument("--copilot_exec_path", type=str)
p.add_argument("--copilot_exec_home", type=str)
p.add_argument("--copilot_exec_allow_all_tools", type=_BOOL)
p.add_argument("--copilot_chat_optimizer_model", type=str)
p.add_argument("--copilot_chat_target_model", type=str)
p.add_argument("--copilot_chat_timeout", type=int)
p.add_argument("--minimax_base_url", type=str)
p.add_argument("--minimax_api_key", type=str)
p.add_argument("--minimax_model", type=str)
Expand Down Expand Up @@ -267,6 +273,12 @@ def main() -> None:
"claude_code_exec_max_thinking_tokens": "model.claude_code_exec_max_thinking_tokens",
"cursor_exec_path": "model.cursor_exec_path",
"cursor_exec_sandbox": "model.cursor_exec_sandbox",
"copilot_exec_path": "model.copilot_exec_path",
"copilot_exec_home": "model.copilot_exec_home",
"copilot_exec_allow_all_tools": "model.copilot_exec_allow_all_tools",
"copilot_chat_optimizer_model": "model.copilot_chat_optimizer_model",
"copilot_chat_target_model": "model.copilot_chat_target_model",
"copilot_chat_timeout": "model.copilot_chat_timeout",
"minimax_base_url": "model.minimax_base_url",
"minimax_api_key": "model.minimax_api_key",
"minimax_model": "model.minimax_model",
Expand Down Expand Up @@ -321,28 +333,41 @@ def _has_model_override(dotted_key: str, legacy_key: str) -> bool:
if explicit_backend is not None:
backend = normalize_backend_name(explicit_backend)
cfg["model_backend"] = backend

def _set_role(key: str, value: str) -> None:
"""Assign a role backend unless the operator named one explicitly.

``setdefault`` was a no-op here: configs/_base_/default.yaml always
sets both roles, so an explicit --backend was silently ignored.
"""
if not _has_model_override(f"model.{key}", key):
cfg[key] = value

if backend in {"claude", "claude_chat"}:
cfg.setdefault("optimizer_backend", "claude_chat")
cfg.setdefault("target_backend", "claude_chat")
_set_role("optimizer_backend", "claude_chat")
_set_role("target_backend", "claude_chat")
elif backend in {"codex", "codex_exec"}:
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
cfg["optimizer_backend"] = "codex_exec"
if not _has_model_override("model.target_backend", "target_backend"):
cfg["target_backend"] = "codex_exec"
_set_role("optimizer_backend", "codex_exec")
_set_role("target_backend", "codex_exec")
elif backend == "claude_code_exec":
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "claude_code_exec")
_set_role("optimizer_backend", "openai_chat")
_set_role("target_backend", "claude_code_exec")
elif backend == "cursor_exec":
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
cfg["optimizer_backend"] = "openai_chat"
if not _has_model_override("model.target_backend", "target_backend"):
cfg["target_backend"] = "cursor_exec"
_set_role("optimizer_backend", "openai_chat")
_set_role("target_backend", "cursor_exec")
elif backend in {"copilot", "copilot_chat"}:
# Both roles on the local CLI: the only fully local configuration.
_set_role("optimizer_backend", "copilot_chat")
_set_role("target_backend", "copilot_chat")
elif backend == "copilot_exec":
_set_role("optimizer_backend", "openai_chat")
_set_role("target_backend", "copilot_exec")
elif backend in {"minimax", "minimax_chat"}:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "minimax_chat")
_set_role("optimizer_backend", "openai_chat")
_set_role("target_backend", "minimax_chat")
else:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "openai_chat")
_set_role("optimizer_backend", "openai_chat")
_set_role("target_backend", "openai_chat")
else:
cfg.setdefault("optimizer_backend", "openai_chat")
cfg.setdefault("target_backend", "openai_chat")
Expand Down Expand Up @@ -449,6 +474,16 @@ def _has_model_override(dotted_key: str, legacy_key: str) -> bool:
path=cfg.get("cursor_exec_path") or None,
sandbox=cfg.get("cursor_exec_sandbox") or None,
)
configure_copilot_exec(
path=cfg.get("copilot_exec_path") or None,
home=cfg.get("copilot_exec_home") or None,
allow_all_tools=cfg.get("copilot_exec_allow_all_tools"),
)
configure_copilot_chat(
optimizer_model=cfg.get("copilot_chat_optimizer_model") or None,
target_model=cfg.get("copilot_chat_target_model") or None,
timeout=cfg.get("copilot_chat_timeout") or None,
)
configure_qwen_chat(
base_url=cfg.get("qwen_chat_base_url") or None,
api_key=cfg.get("qwen_chat_api_key") or None,
Expand Down
14 changes: 13 additions & 1 deletion scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def parse_args() -> argparse.Namespace:
# Legacy flat CLI overrides (still work, prefer --cfg-options for new usage)
p.add_argument("--env", type=str)
p.add_argument("--backend", type=str,
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "cursor", "cursor_exec", "qwen", "qwen_chat", "minimax", "minimax_chat"])
choices=["azure_openai", "codex", "codex_exec", "claude", "claude_chat", "claude_code_exec", "cursor", "cursor_exec", "copilot", "copilot_chat", "copilot_exec", "qwen", "qwen_chat", "minimax", "minimax_chat"])
p.add_argument("--optimizer_model", type=str)
p.add_argument("--target_model", type=str)
p.add_argument("--optimizer_backend", type=str)
Expand Down Expand Up @@ -207,6 +207,12 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--claude_code_exec_max_thinking_tokens", type=int)
p.add_argument("--cursor_exec_path", type=str)
p.add_argument("--cursor_exec_sandbox", type=str)
p.add_argument("--copilot_exec_path", type=str)
p.add_argument("--copilot_exec_home", type=str)
p.add_argument("--copilot_exec_allow_all_tools", type=_BOOL)
p.add_argument("--copilot_chat_optimizer_model", type=str)
p.add_argument("--copilot_chat_target_model", type=str)
p.add_argument("--copilot_chat_timeout", type=int)
p.add_argument("--codex_trace_to_optimizer", type=_BOOL)
p.add_argument("--skill_init", type=str)
p.add_argument("--num_epochs", type=int)
Expand Down Expand Up @@ -347,6 +353,12 @@ def parse_args() -> argparse.Namespace:
"claude_code_exec_max_thinking_tokens": "model.claude_code_exec_max_thinking_tokens",
"cursor_exec_path": "model.cursor_exec_path",
"cursor_exec_sandbox": "model.cursor_exec_sandbox",
"copilot_exec_path": "model.copilot_exec_path",
"copilot_exec_home": "model.copilot_exec_home",
"copilot_exec_allow_all_tools": "model.copilot_exec_allow_all_tools",
"copilot_chat_optimizer_model": "model.copilot_chat_optimizer_model",
"copilot_chat_target_model": "model.copilot_chat_target_model",
"copilot_chat_timeout": "model.copilot_chat_timeout",
"codex_trace_to_optimizer": "model.codex_trace_to_optimizer",
"num_epochs": "train.num_epochs",
"train_size": "train.train_size",
Expand Down
6 changes: 6 additions & 0 deletions skillopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
"model.claude_code_exec_max_thinking_tokens": "claude_code_exec_max_thinking_tokens",
"model.cursor_exec_path": "cursor_exec_path",
"model.cursor_exec_sandbox": "cursor_exec_sandbox",
"model.copilot_exec_path": "copilot_exec_path",
"model.copilot_exec_home": "copilot_exec_home",
"model.copilot_exec_allow_all_tools": "copilot_exec_allow_all_tools",
"model.copilot_chat_optimizer_model": "copilot_chat_optimizer_model",
"model.copilot_chat_target_model": "copilot_chat_target_model",
"model.copilot_chat_timeout": "copilot_chat_timeout",
"model.codex_trace_to_optimizer": "codex_trace_to_optimizer",
"model.azure_endpoint": "azure_endpoint",
"model.azure_api_version": "azure_api_version",
Expand Down
Loading