Extract clm.runtime and clm.agents; keep session core vendor-free; inject describe_live_event into the outer loop; enforce import boundaries and mirror unit tests by package.
9.4 KiB
Pluggable Coding Agents
CLM’s outer control loop steers a configurable coding agent. The loop never
branches on vendor names: it talks to the framework-agnostic Agent protocol
(step / inject_context / reset_with_seed) and optional capabilities such as
ExternalSessionCapable for dual session IDs.
| Layer | Role | Vendor-free? | Package |
|---|---|---|---|
Protocol Agent |
Execution surface | yes | clm.session |
Capabilities (e.g. ExternalSessionCapable, LiveEventCapable) |
Cross-cutting optional features | yes | clm.session |
Config agent_kind + AgentSettings |
Selection & parameters | yes (keys generic) | clm.models |
Factory / registry build_agent |
Kind → instance (no vendor imports) | yes | clm.session.factory |
| Builtin registration | register_builtin_agents() on import |
— | clm.agents |
| Adapter impl | Grok / Simulated / OpenAI-compatible | no | clm.agents (+ clm.agents.grok) |
| CLI | --agent + generic flags + escape hatch |
thin mapping | clm.cli |
Default offline agent: simulated (JSON script). First live adapter:
grok-build (headless Grok Build CLI).
Choose an agent
CLI
# Offline (default — backcompat)
clm run --agent simulated --script tests/fixtures/sample_transcript.json \
--goal "demo" --profile coding
# Same without --agent (defaults to simulated; --script still required)
clm run --goal "demo" --script tests/fixtures/sample_transcript.json
# Grok Build (live; needs grok CLI + auth)
clm run --agent grok-build --goal "Implement feature X" \
--cwd . --profile coding --yolo \
--model grok-build --max-turns-per-step 8 --path .clm
# Long goals from a file (mutually exclusive with --goal)
clm run --agent grok-build --goal-file ./GOAL.md \
--cwd . --profile coding --yolo --path .clm
Generic flags (all coding agents)
| Flag | Config field | Notes |
|---|---|---|
--agent |
agent_kind |
Registry key |
--goal / --goal-file |
session goal | String or UTF-8 file; not both |
--cwd |
agent.cwd |
Working directory |
--model |
agent.model |
Adapter-specific model id |
--max-turns-per-step |
agent.max_turns_per_step |
Turns inside one outer-loop step |
--rules |
agent.rules |
Extra rules string |
--yolo |
agent.yolo |
Opt-in always-approve (unsafe) |
--script |
agent.script_path |
Only simulated (required there; forbidden elsewhere) |
--agent-option KEY=VALUE |
agent.options[k] |
Escape hatch for vendor keys |
--grok-bin PATH |
agent.options["grok_bin"] |
Convenience alias for grok-build |
Library / config
Always import clm.agents (or call register_builtin_agents()) before
build_agent so builtins (simulated, grok-build) are on the registry.
Session factory itself does not side-effect-register vendors.
from pathlib import Path
from clm.models.config import AgentSettings, ClmConfig
import clm.agents # registers simulated + grok-build (idempotent)
from clm.session.factory import build_agent
# or: from clm.agents import GrokBuildAgent, SimulatedAgent, register_builtin_agents
cfg = ClmConfig(
agent_kind="grok-build",
agent=AgentSettings(
cwd=Path("."),
model="grok-build",
max_turns_per_step=8,
yolo=True,
options={"grok_bin": "grok", "timeout_s": 3600},
),
)
agent = build_agent(cfg, goal="Implement feature X")
# Wire TrackingWorkingSession + OuterControlLoop (see clm.cli.run).
# For live doing= labels with grok-build, pass describe_live_event=
# clm.agents.grok.stream.describe_stream_event into OuterControlLoop.
Unknown agent_kind raises ValueError listing registered keys.
Register your own agent
Implement the Agent protocol (and optionally ExternalSessionCapable), then
register a factory:
from clm.models.config import ClmConfig
from clm.session.agent import Agent
from clm.session.factory import register_agent
def _build_my_agent(
config: ClmConfig,
*,
goal: str | None = None,
script_steps=None,
runner=None,
**_: object,
) -> Agent:
# Read only config.agent / config.agent.options — never branch in the loop.
return MyAgent(goal=goal, cwd=config.agent.cwd, **config.agent.options)
register_agent("my-agent", _build_my_agent)
Then:
clm run --agent my-agent --goal "…" --agent-option api_key_env=MY_KEY
Do not add isinstance(MyAgent) checks in OuterControlLoop. Prefer
capability protocols (ExternalSessionCapable) for cross-cutting behaviour
such as syncing SessionState.external_session_id.
Grok Build (first live adapter)
Prerequisites
- Install Grok Build CLI and authenticate (see Grok product docs).
- Ensure
grokis onPATH, or pass--grok-bin /path/to/grok. - Prefer a project with a clear goal; use
--cwdfor the workspace root.
Example
clm init --path .clm
clm run --agent grok-build \
--goal "Add a unit test for parse_agent_options and keep the suite green" \
--cwd . --profile coding --yolo \
--model grok-build --max-turns-per-step 8 \
--max-steps 20 --path .clm
How CLM maps control actions
| CLM action | Grok adapter behaviour (MVP) |
|---|---|
| CONTINUE | Next headless grok -p (with --resume after first step) |
| COMPACT | CLM compaction pipeline + inject_context prompt prefix — not Grok /compact |
| HANDOFF | CLM manifest; bind_external_session(None) + seed → new Grok session |
Live progress via streaming-json (ACP session updates)
By default grok-build uses Grok headless --output-format streaming-json:
NDJSON lines derived from the agent’s native ACP session updates (tool_call,
tool_call_update, plan, thought, text, usage, end, …).
| Layer | Role |
|---|---|
LiveEventCapable |
Vendor-free sink on session: set_live_event_handler |
GrokBuildAgent (clm.agents.grok) |
Streams when stream=True (default) or a sink is attached |
OuterControlLoop |
Attaches sink during agent.step; optional injected describe_live_event → doing= / STATUS ticks |
| Composition root (CLI) | For grok-build, wires clm.agents.grok.stream.describe_stream_event (loop never imports grok) |
| Fallback | Workspace mtime probe if no stream activity yet |
Disable streaming (one-shot json only):
clm run --agent grok-build --goal "…" --agent-option stream=false …
Notes field after a streamed step includes stream=acp. This is not full
bidirectional ACP (grok agent / permissions over JSON-RPC); tool approvals
still use --yolo / permission mode in headless. Full ACP client is a later
registry kind.
Completion is detected via a text marker in the agent reply:
CLM_TASK_STATUS: completed
# or: failed | blocked | waiting | phase_boundary
Dual session IDs
- CLM session:
.clm/sessions/<id>/(state.json, metrics, decisions). - Grok session: vendor store (e.g.
~/.grok/sessions/<uuid>).
Mapping is stored in SessionState.external_session_id and synced by
TrackingWorkingSession when the agent implements ExternalSessionCapable.
--resume <clm-id> rebinds the external id before the next step.
YOLO and permissions
Automation typically needs --yolo (or adapter rules) so headless Grok does not
block on permission prompts. Default is off (safer). Treat YOLO as an
explicit opt-in for trusted local runs.
Thresholds vs Grok auto-compact
Grok may auto-compact near ~85% of its window. Choose CLM soft thresholds so CLM COMPACT still fires first when you want verified, audit-friendly compaction. If Grok compacted first, CLM still sees reduced context on the next step via the resumed external session — but you lose CLM’s verify/handoff trail for that shrink.
Troubleshooting
Generic
| Symptom | Check |
|---|---|
Unknown agent_kind … Registered: … |
Typo in --agent; call registered_agents() or install the package that registers the kind |
--script is required with --agent simulated |
Pass a JSON fixture path |
--script is only valid with --agent simulated |
Drop --script for live agents |
| No progress / max-steps hit | Raise --max-steps; confirm agent status markers / script length |
| Resume fails with terminal status | Session already completed/failed; start a new --session-id |
Grok Build
| Symptom | Check |
|---|---|
Grok binary not found |
Install CLI or --grok-bin / --agent-option grok_bin=… |
| Permission / interactive prompts hang | Use --yolo for automation, or tighten --rules |
| Auth errors | Log in with the Grok CLI before CLM run |
| External id missing after run | Ensure adapter implements ExternalSessionCapable; inspect state.json |
| Live smoke in CI | Not supported — inject FakeGrokCliRunner / unit tests only |
Related
- Implementation plan:
docs/superpowers/plans/2026-08-05-configurable-coding-agents.md - Getting started (offline Simulated):
docs/getting-started.md - Architecture:
docs/architecture.md - Module pages:
docs/modules/agents.md,docs/modules/session.md - Source: session core
src/clm/session/(protocol, capabilities, factory, working/tracking); adapterssrc/clm/agents/(simulated,openai_compatible,grok/*); composition helperssrc/clm/runtime/