Files
grok-bot c5d495f830 refactor: modularize for coding agents (runtime, agents, boundaries)
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.
2026-08-06 17:27:17 +02:00

9.4 KiB
Raw Permalink Blame History

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

  1. Install Grok Build CLI and authenticate (see Grok product docs).
  2. Ensure grok is on PATH, or pass --grok-bin /path/to/grok.
  3. Prefer a project with a clear goal; use --cwd for 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