Before you build a backend from scratch, let's confirm the boundary is clear:
generate() takes chat messages and returns text + usage metadata. Everything else — rollout orchestration, scoring, reflection — lives in the env adapter layer, not the backend. Your backend just wraps your agent's API.rollout and evaluate live in the env adapter, not the backend. The backend is pure model interface: generate(messages, temperature, max_tokens) → ModelResponse. That's it. Re-read the new-backend guide — the required interface table makes it explicit.
Implement a SkillOpt ModelBackend for your preferred agent — pi, Kiro, or
another CLI agent. You'll read the shipped claude_code_exec backend as your
template, implement the contract, register it, and run a training loop through your own
agent.
The best reference is the codex_exec backend — it wraps the Codex CLI as a subprocess:
# Find it in the installed package
python -c "import skillopt; print(skillopt.__path__[0])"
# Read the exec-style backend
cat $(python -c "import skillopt; print(skillopt.__path__[0])")/model/codex_exec.py
Note the pattern:
__init__ — store config, validate the agent CLI is availablegenerate() — format messages into a prompt, invoke the agent CLI as a subprocess, capture output, return ModelResponseCreate ~/skillopt-repo/skillopt/model/pi_exec.py (or kiro_exec.py):
import subprocess
import json
from skillopt.model.base import ModelBackend, ModelResponse
class PiExecBackend(ModelBackend):
"""Execute tasks via the pi coding agent CLI."""
def __init__(self, cfg: dict):
super().__init__(cfg)
self.model_name = cfg.get("model_name", "pi")
self.timeout = int(cfg.get("timeout", 120))
# Verify pi is available
subprocess.run(["pi", "--version"], capture_output=True, check=True)
async def generate(
self,
messages: list[dict],
temperature: float = 0.7,
max_tokens: int = 4096,
**kwargs,
) -> ModelResponse:
# Format messages into a single prompt for the CLI
prompt = self._format_messages(messages)
# Run pi with the skill as system prompt + task as user message
result = subprocess.run(
["pi", "--non-interactive", "--prompt", prompt],
capture_output=True,
text=True,
timeout=self.timeout,
)
content = result.stdout.strip()
# Approximate token count
token_est = len(content.split()) * 1.3
return ModelResponse(
content=content,
usage={
"prompt_tokens": int(len(prompt.split()) * 1.3),
"completion_tokens": int(token_est),
},
model=self.model_name,
)
def _format_messages(self, messages: list[dict]) -> str:
"""Collapse chat messages into a single prompt string."""
parts = []
for msg in messages:
if msg["role"] == "system":
parts.append(f"[System]\n{msg['content']}\n")
elif msg["role"] == "user":
parts.append(f"[Task]\n{msg['content']}\n")
return "\n".join(parts)
The subprocess call above is a placeholder. Check your agent's actual CLI:
pi --non-interactive --prompt "..." (check pi --help)plugins/copilot/mcp_server.py) is your template — register a SkillOpt MCP tool that Kiro can call.claude_code_exec — just use it.Add to skillopt/model/__init__.py:
from .pi_exec import PiExecBackend
BACKEND_REGISTRY = {
# ... existing backends ...
"pi_exec": PiExecBackend,
}
Before running full training, verify the backend works in isolation:
python -c "
import asyncio
from skillopt.model.pi_exec import PiExecBackend
backend = PiExecBackend({'model_name': 'pi', 'timeout': 60})
async def test():
resp = await backend.generate(
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': 'What is 2+2?'}
]
)
print(f'Content: {resp.content}')
print(f'Usage: {resp.usage}')
asyncio.run(test())
"
If this prints a response, your backend contract is satisfied.
Update your config to use the new backend:
# In configs/my_env/default.yaml
model:
backend: pi_exec
model_name: pi
timeout: 120
Then run:
python scripts/train.py --config configs/my_env/default.yaml
You're now training a skill for your agent, using your agent as the execution target, on your tasks. This is the full mission: SkillOpt optimizing a skill document that makes your daily agent measurably better.
If you prefer Kiro over pi, the approach is different. Kiro doesn't have a simple
subprocess CLI — it's an MCP-native agent. The pattern from
plugins/copilot/mcp_server.py is:
skillopt-sleep tools
The missing piece for Kiro is a transcript harvester — Kiro doesn't
yet write ATIF-v1.7 session logs the way Devin does. You'd need to write a harvester
that reads Kiro's session state and converts it to SkillOpt's expected format. The
Devin plugin's harvester (plugins/devin/harvester.py) is the reference.
No one has shipped a Kiro backend yet. You'd be the first. The engineering is straightforward (MCP server + transcript conversion), but expect to debug format mismatches. File issues on the SkillOpt repo — the maintainers are responsive and interested in new integrations.
~/.pi/skills/. For Kiro: put it in .kiro/steering/. For Claude Code: project-level CLAUDE.md. The agent gets better for free.CLAUDE.md, .kiro/steering/, ~/.pi/skills/). Zero inference overhead, zero infrastructure.Once you have a validated best_skill.md, deploy it:
| Agent | Deploy location |
|---|---|
| pi | ~/.pi/skills/best_skill.md |
| Kiro | .kiro/steering/skillopt-trained.md |
| Claude Code | Append to project CLAUDE.md |
| Codex | ~/.codex/instructions.md |
Then schedule nightly sleep cycles to keep improving it:
skillopt-sleep schedule # installs a cron entry for this project
Add a New Model Backend — Microsoft Research. The full 130-line guide: ModelBackend ABC, generate() contract, registration, and the required vs optional methods table. Plus skim plugins/copilot/mcp_server.py if you're going the Kiro/MCP route.