|
| 1 | +# Adding a New Agent |
| 2 | + |
| 3 | +This guide walks through adding support for a new AI coding agent to AoE. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Adding an agent involves these files: |
| 8 | + |
| 9 | +| File | Purpose | |
| 10 | +|------|---------| |
| 11 | +| `src/agents.rs` | Agent registry entry (name, binary, detection, flags) | |
| 12 | +| `src/tmux/status_detection.rs` | Status detection function (pane parsing or stub) | |
| 13 | +| `src/hooks/mod.rs` | Hook installer (if the agent supports hooks) | |
| 14 | +| `src/session/instance.rs` | Wire hook installation + env prefix | |
| 15 | +| `src/session/container_config.rs` | Config mount for Docker sandbox | |
| 16 | +| `docker/Dockerfile` | Install agent in sandbox image | |
| 17 | +| `README.md`, `docs/` | Documentation updates | |
| 18 | + |
| 19 | +## Levels of Support |
| 20 | + |
| 21 | +Not all agents need everything. Here's what each level provides: |
| 22 | + |
| 23 | +### Level 1: Basic (minimum viable) |
| 24 | + |
| 25 | +- Agent appears in `aoe agents` list |
| 26 | +- Sessions can be created and launched |
| 27 | +- Status always shows "Idle" |
| 28 | + |
| 29 | +Requires: `AgentDef` entry + stub `detect_status` function. |
| 30 | + |
| 31 | +### Level 2: Status Detection via Pane Parsing |
| 32 | + |
| 33 | +- AoE reads the terminal output and infers running/waiting/idle |
| 34 | +- No agent-side configuration needed |
| 35 | +- Can be brittle if the agent's UI changes |
| 36 | + |
| 37 | +Requires: a `detect_<agent>_status(content: &str) -> Status` function that parses tmux pane content. |
| 38 | + |
| 39 | +Examples: OpenCode, Codex, Vibe, Copilot, Pi, Droid. |
| 40 | + |
| 41 | +### Level 3: Status Detection via Hooks |
| 42 | + |
| 43 | +- AoE installs hooks into the agent's config that write status to a file |
| 44 | +- Most reliable; survives UI changes |
| 45 | +- Requires the agent to support a hook/event system |
| 46 | + |
| 47 | +Requires: either using the generic `hook_config` (if the agent uses the same JSON format as Claude/Cursor/Gemini) or a custom `install_<agent>_hooks()` function. |
| 48 | + |
| 49 | +Examples: Claude, Cursor, Gemini (generic), Hermes (custom YAML), Kiro (custom JSON). |
| 50 | + |
| 51 | +### Level 4: Session Resume |
| 52 | + |
| 53 | +- AoE can restart a session and resume the prior conversation |
| 54 | +- Requires the agent to support a resume flag or session ID |
| 55 | + |
| 56 | +Requires: setting `resume_strategy` in the `AgentDef`. |
| 57 | + |
| 58 | +### Level 5: Docker Sandbox |
| 59 | + |
| 60 | +- Agent runs in an isolated container |
| 61 | +- Config is synced from host to container |
| 62 | + |
| 63 | +Requires: `AgentConfigMount` entry + Dockerfile installation. |
| 64 | + |
| 65 | +## Step-by-Step: Adding a New Agent |
| 66 | + |
| 67 | +### 1. Research the Agent |
| 68 | + |
| 69 | +Before writing code, gather: |
| 70 | + |
| 71 | +- **Binary name**: What command launches it? (e.g., `kiro-cli`, `claude`, `codex`) |
| 72 | +- **Detection**: How to check if it's installed? Usually `which <binary>`. |
| 73 | +- **YOLO/auto-approve flag**: Does it have a flag to skip permission prompts? |
| 74 | +- **Resume flag**: Can it resume a prior session? What flag? |
| 75 | +- **Hook support**: Does it have lifecycle hooks (preToolUse, stop, etc.)? |
| 76 | +- **Hook format**: If hooks exist, what's the config format? (JSON, YAML, TOML?) |
| 77 | +- **Config directory**: Where does it store config? (e.g., `~/.kiro`, `~/.claude`) |
| 78 | +- **Install command**: How do users install it? |
| 79 | + |
| 80 | +### 2. Add the Agent Definition (`src/agents.rs`) |
| 81 | + |
| 82 | +Add an entry to the `AGENTS` array: |
| 83 | + |
| 84 | +```rust |
| 85 | +AgentDef { |
| 86 | + name: "myagent", // canonical name |
| 87 | + binary: "myagent-cli", // binary to invoke |
| 88 | + aliases: &["my-agent"], // alternative names for resolve_tool_name |
| 89 | + detection: DetectionMethod::Which("myagent-cli"), |
| 90 | + yolo: Some(YoloMode::CliFlag("--auto-approve")), |
| 91 | + instruction_flag: None, // or Some("--system-prompt {}") |
| 92 | + set_default_command: false, // true if binary must be explicit in instance.command |
| 93 | + detect_status: status_detection::detect_myagent_status, |
| 94 | + container_env: &[], // env vars for container sessions |
| 95 | + hook_config: None, // or Some(AgentHookConfig { ... }) for Claude-format hooks |
| 96 | + resume_strategy: ResumeStrategy::Flag("--resume"), |
| 97 | + host_only: false, // true if sandbox/worktree not supported |
| 98 | + send_keys_enter_delay_ms: 0, |
| 99 | + install_hint: "npm install -g myagent-cli", |
| 100 | +}, |
| 101 | +``` |
| 102 | + |
| 103 | +### 3. Add Status Detection (`src/tmux/status_detection.rs`) |
| 104 | + |
| 105 | +For hook-based agents, add a stub: |
| 106 | + |
| 107 | +```rust |
| 108 | +/// MyAgent status is detected via hooks, not tmux pane parsing. |
| 109 | +pub fn detect_myagent_status(_content: &str) -> Status { |
| 110 | + Status::Idle |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +For pane-parsing agents, write a function that examines the terminal content: |
| 115 | + |
| 116 | +```rust |
| 117 | +pub fn detect_myagent_status(raw_content: &str) -> Status { |
| 118 | + let content = raw_content.to_lowercase(); |
| 119 | + if content.contains("waiting for input") { |
| 120 | + Status::Waiting |
| 121 | + } else if content.contains("processing") { |
| 122 | + Status::Running |
| 123 | + } else { |
| 124 | + Status::Idle |
| 125 | + } |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +### 4. Add Hook Installation (if applicable) |
| 130 | + |
| 131 | +If the agent supports hooks but uses a different format than Claude/Cursor/Gemini, add a custom installer in `src/hooks/mod.rs`. See `install_hermes_hooks` (YAML) or `install_kiro_hooks` (JSON) as examples. |
| 132 | + |
| 133 | +Then wire it into `install_agent_status_hooks()` in `src/session/instance.rs`: |
| 134 | + |
| 135 | +```rust |
| 136 | +} else if self.tool == "myagent" && !self.is_sandboxed() { |
| 137 | + if let Some(home) = dirs::home_dir() { |
| 138 | + let config_path = home.join(".myagent/hooks.json"); |
| 139 | + if let Err(e) = crate::hooks::install_myagent_hooks(&config_path) { |
| 140 | + tracing::warn!("Failed to install myagent hooks: {}", e); |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +And add the tool name to `status_hook_env_prefix()` so `AOE_INSTANCE_ID` is passed: |
| 147 | + |
| 148 | +```rust |
| 149 | +let has_hooks = agent.and_then(|a| a.hook_config.as_ref()).is_some() |
| 150 | + || tool == "settl" |
| 151 | + || tool == "hermes" |
| 152 | + || tool == "kiro" |
| 153 | + || tool == "myagent"; |
| 154 | +``` |
| 155 | + |
| 156 | +### 5. Add Container Config Mount (`src/session/container_config.rs`) |
| 157 | + |
| 158 | +```rust |
| 159 | +AgentConfigMount { |
| 160 | + tool_name: "myagent", |
| 161 | + host_rel: ".myagent", |
| 162 | + container_suffix: ".myagent", |
| 163 | + skip_entries: &["sandbox", "sessions", "cache"], |
| 164 | + seed_files: &[], |
| 165 | + copy_dirs: &[], |
| 166 | + keychain_credential: None, |
| 167 | + home_seed_files: &[], |
| 168 | + preserve_files: &[], |
| 169 | + clean_files: &[], |
| 170 | +}, |
| 171 | +``` |
| 172 | + |
| 173 | +### 6. Add to Dockerfile (`docker/Dockerfile`) |
| 174 | + |
| 175 | +```dockerfile |
| 176 | +# Install MyAgent |
| 177 | +RUN curl -fsSL https://myagent.dev/install | bash |
| 178 | +``` |
| 179 | + |
| 180 | +And add the config directory to the `mkdir -p` block. |
| 181 | + |
| 182 | +### 7. Update Tests |
| 183 | + |
| 184 | +In `src/agents.rs`, update: |
| 185 | +- `test_get_agent_known` |
| 186 | +- `test_agent_names` |
| 187 | +- `test_resolve_tool_name` |
| 188 | +- `test_settings_index_roundtrip` |
| 189 | +- `test_send_keys_enter_delay` |
| 190 | +- `test_install_hint_lookup` |
| 191 | + |
| 192 | +In `src/tmux/status_detection.rs`, add a test for your detection function. |
| 193 | + |
| 194 | +In `src/session/instance.rs`, add your agent to `test_status_hook_env_prefix_includes_hermes` (if hook-based). |
| 195 | + |
| 196 | +### 8. Update Documentation |
| 197 | + |
| 198 | +- `README.md`: features list + FAQ |
| 199 | +- `docs/index.md`: supported agents list |
| 200 | +- `docs/guides/sandbox.md`: sandbox overview + image table |
| 201 | +- `docker/Dockerfile.dev`: comment listing inherited agents |
| 202 | + |
| 203 | +### 9. Verify |
| 204 | + |
| 205 | +```bash |
| 206 | +cargo fmt |
| 207 | +cargo clippy -- -D warnings |
| 208 | +cargo test --lib agents |
| 209 | +cargo test --lib <youragent> |
| 210 | +cargo test --lib container_config |
| 211 | +cargo build |
| 212 | +./target/debug/aoe agents # verify detection works |
| 213 | +``` |
| 214 | + |
| 215 | +## Hook Format Reference |
| 216 | + |
| 217 | +### Claude/Cursor/Gemini (generic `hook_config`) |
| 218 | + |
| 219 | +```json |
| 220 | +{ |
| 221 | + "hooks": { |
| 222 | + "PreToolUse": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}], |
| 223 | + "Stop": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}] |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +Set `hook_config: Some(AgentHookConfig { ... })` in the agent def; the generic `install_hooks()` handles it. |
| 229 | + |
| 230 | +### Hermes (custom YAML) |
| 231 | + |
| 232 | +```yaml |
| 233 | +hooks: |
| 234 | + pre_tool_call: |
| 235 | + - command: "sh -c '...'" |
| 236 | +``` |
| 237 | +
|
| 238 | +### Kiro CLI (custom JSON agent config) |
| 239 | +
|
| 240 | +```json |
| 241 | +{ |
| 242 | + "name": "aoe-hooks", |
| 243 | + "tools": ["*"], |
| 244 | + "hooks": { |
| 245 | + "preToolUse": [{"command": "sh -c '...'"}], |
| 246 | + "stop": [{"command": "sh -c '...'"}] |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +## Common Pitfalls |
| 252 | + |
| 253 | +- **Forgetting `status_hook_env_prefix`**: Without `AOE_INSTANCE_ID`, hooks write nothing. Add your tool name to the check in `src/session/instance.rs`. |
| 254 | +- **Wrong hook format**: Each agent has its own schema; test that hooks actually fire by sending a message and checking `/tmp/aoe-hooks/*/status`. |
| 255 | +- **Sandbox hooks are separate**: Host hook installation (`install_agent_status_hooks`) doesn't cover sandbox sessions. You also need to wire hooks into `build_container_config` in `src/session/container_config.rs` so the sidecar volume is mounted and the agent config is materialized inside the container. |
| 256 | +- **Don't shell out in `install_*_hooks()`**: Keep hook installation as pure file IO. Any subprocess calls (like setting a default agent) should be in a separate function so `cargo test` doesn't mutate the developer's real environment. |
| 257 | +- **Use structured output when parsing agent CLIs**: If the agent CLI has `--format json` or similar, use it instead of substring matching on human-readable output. Human-readable formats change between versions. |
| 258 | +- **Waiting status needs a dedicated event**: Not all agents have an approval/permission event. If the agent doesn't expose one, document it as a limitation and consider filing upstream. |
| 259 | +- **Redundant Dockerfile ENV**: Check if PATH is already set before adding another layer. |
| 260 | +- **`set_default_command`**: Only needed for agents where the binary name alone isn't sufficient (e.g., opencode needs explicit command storage). |
0 commit comments