|
2 | 2 |
|
3 | 3 | The `run` subcommand simplifies editor integration by reading agent configuration from a file rather than requiring command-line arguments. |
4 | 4 |
|
5 | | -## Motivation |
6 | | - |
7 | | -Without this mode, editor extensions must either: |
8 | | -- Hardcode specific agent commands, requiring extension updates to add new agents |
9 | | -- Expose complex configuration UI for specifying agent commands and proxy options |
10 | | - |
11 | | -With `run`, the extension simply runs: |
12 | | - |
13 | | -```bash |
14 | | -symposium-acp-agent run |
15 | | -``` |
16 | | - |
17 | | -The agent reads its configuration from `~/.symposium/config.jsonc`, and if no configuration exists, runs an interactive setup wizard. |
18 | | - |
19 | 5 | ## Configuration File |
20 | 6 |
|
21 | 7 | **Location:** `~/.symposium/config.jsonc` |
@@ -43,72 +29,233 @@ The file uses JSONC (JSON with comments) format: |
43 | 29 | | `agent` | string | Shell command to spawn the downstream agent. Parsed using shell word splitting. | |
44 | 30 | | `proxies` | array | List of proxy extensions with `name` and `enabled` fields. | |
45 | 31 |
|
46 | | -The `agent` string is parsed as shell words, so commands like `npx -y @zed-industries/claude-code-acp` work correctly. |
| 32 | +## Architecture: Three-Actor Pattern |
47 | 33 |
|
48 | | -## Runtime Behavior |
| 34 | +The `run` command uses a **ConfigAgent** that manages configuration and delegates sessions to conductors. The architecture uses three actors to separate concerns: |
49 | 35 |
|
50 | 36 | ``` |
51 | | -┌─────────────────────────────────────────┐ |
52 | | -│ run │ |
53 | | -└─────────────────┬───────────────────────┘ |
54 | | - │ |
55 | | - ▼ |
56 | | - ┌─────────────────┐ |
57 | | - │ Config exists? │ |
58 | | - └────────┬────────┘ |
59 | | - │ |
60 | | - ┌───────┴───────┐ |
61 | | - │ │ |
62 | | - ▼ ▼ |
63 | | - ┌──────────┐ ┌──────────────┐ |
64 | | - │ Yes │ │ No │ |
65 | | - └────┬─────┘ └──────┬───────┘ |
66 | | - │ │ |
67 | | - ▼ ▼ |
68 | | - Load config Run configuration |
69 | | - Run agent agent (setup wizard) |
| 37 | +Client (editor) |
| 38 | + │ |
| 39 | + ▼ |
| 40 | +┌─────────────────────────────────────────────────────────┐ |
| 41 | +│ ConfigAgent │ |
| 42 | +│ • Handles InitializeRequest │ |
| 43 | +│ • Intercepts /symposium:config slash command │ |
| 44 | +│ • Routes messages by session ID │ |
| 45 | +│ • Spawns ConfigModeActor for config UI │ |
| 46 | +└─────────────────────────────────────────────────────────┘ |
| 47 | + │ |
| 48 | + ▼ |
| 49 | +┌─────────────────────────────────────────────────────────┐ |
| 50 | +│ UberconductorActor │ |
| 51 | +│ • Manages conductor lifecycle │ |
| 52 | +│ • Groups sessions by config (deduplication) │ |
| 53 | +│ • Creates new conductors when config changes │ |
| 54 | +└─────────────────────────────────────────────────────────┘ |
| 55 | + │ |
| 56 | + ▼ |
| 57 | +┌─────────────────────────────────────────────────────────┐ |
| 58 | +│ ConductorActor (one per unique config) │ |
| 59 | +│ • Owns proxy chain + downstream agent process │ |
| 60 | +│ • Handles multiple sessions │ |
| 61 | +│ • Forwards messages bidirectionally │ |
| 62 | +└─────────────────────────────────────────────────────────┘ |
| 63 | + │ |
| 64 | + ▼ |
| 65 | +Downstream Agent (claude-code, etc.) |
| 66 | +``` |
| 67 | + |
| 68 | +### Why Three Actors? |
| 69 | + |
| 70 | +**ConfigAgent** owns the client connection and must respond to requests. But creating a conductor requires async initialization (spawning processes, capability negotiation). The three-actor pattern solves this: |
| 71 | + |
| 72 | +1. **ConfigAgent** - Stateful message router. Owns session-to-conductor mapping and responds to clients. |
| 73 | +2. **UberconductorActor** - Conductor factory. Manages conductor lifecycle and deduplicates by config. |
| 74 | +3. **ConductorActor** - Session handler. Owns the actual proxy chain and downstream agent. |
| 75 | + |
| 76 | +This separation ensures ConfigAgent never blocks waiting for conductor initialization. |
| 77 | + |
| 78 | +### New Session Flow |
| 79 | + |
| 80 | +When a client requests a new session: |
| 81 | + |
| 82 | +```mermaid |
| 83 | +sequenceDiagram |
| 84 | + participant Client |
| 85 | + participant ConfigAgent |
| 86 | + participant Uberconductor |
| 87 | + participant Conductor |
| 88 | + participant Agent as Downstream Agent |
| 89 | +
|
| 90 | + Client->>ConfigAgent: NewSessionRequest |
| 91 | + ConfigAgent->>ConfigAgent: Load config from disk |
| 92 | + ConfigAgent->>Uberconductor: new_session(config, request) |
| 93 | + |
| 94 | + opt No conductor for this config yet |
| 95 | + Uberconductor->>Conductor: spawn new conductor |
| 96 | + Conductor->>Agent: spawn downstream agent |
| 97 | + Agent-->>Conductor: InitializeResponse |
| 98 | + end |
| 99 | + |
| 100 | + Uberconductor->>Conductor: forward request |
| 101 | + Conductor->>Agent: NewSessionRequest |
| 102 | + Agent-->>Conductor: NewSessionResponse |
| 103 | + Conductor-->>ConfigAgent: NewSessionCreated(response, handle) |
| 104 | + Note over ConfigAgent: Store session→conductor mapping |
| 105 | + ConfigAgent-->>Client: NewSessionResponse |
70 | 106 | ``` |
71 | 107 |
|
72 | | -When a configuration file exists, `run` behaves equivalently to: |
| 108 | +Key insight: The conductor sends `NewSessionCreated` back to ConfigAgent *carrying the request context*. This ensures ConfigAgent stores the session mapping *before* responding to the client, avoiding race conditions. |
| 109 | + |
| 110 | +### Prompt Routing |
| 111 | + |
| 112 | +Once a session is established, prompts route through ConfigAgent: |
73 | 113 |
|
74 | | -```bash |
75 | | -symposium-acp-agent run-with \ |
76 | | - --proxy sparkle --proxy ferris --proxy cargo \ |
77 | | - --agent '{"name":"...","command":"npx",...}' |
| 114 | +```mermaid |
| 115 | +sequenceDiagram |
| 116 | + participant Client |
| 117 | + participant ConfigAgent |
| 118 | + participant Conductor |
| 119 | + participant Agent as Downstream Agent |
| 120 | +
|
| 121 | + Client->>ConfigAgent: PromptRequest(session_id) |
| 122 | + ConfigAgent->>ConfigAgent: Lookup session→conductor |
| 123 | + |
| 124 | + alt Is /symposium:config command |
| 125 | + ConfigAgent->>ConfigAgent: Enter config mode |
| 126 | + ConfigAgent-->>Client: PromptResponse |
| 127 | + else Normal prompt |
| 128 | + ConfigAgent->>Conductor: forward prompt |
| 129 | + Conductor->>Agent: PromptRequest |
| 130 | + Agent-->>Conductor: streaming notifications/requests |
| 131 | + Conductor->>ConfigAgent: MessageToClient |
| 132 | + ConfigAgent-->>Client: forward message |
| 133 | + Note over ConfigAgent: Repeats for each message from agent |
| 134 | + Conductor->>ConfigAgent: MessageToClient(PromptResponse) |
| 135 | + ConfigAgent-->>Client: PromptResponse |
| 136 | + end |
78 | 137 | ``` |
79 | 138 |
|
80 | | -## Configuration Agent |
| 139 | +All messages from conductors flow back through ConfigAgent as `MessageToClient`. This allows ConfigAgent to intercept and modify messages (e.g., injecting the `/symposium:config` command into `AvailableCommandsUpdate` notifications). |
81 | 140 |
|
82 | | -When no configuration file exists, Symposium runs a built-in configuration agent instead of a downstream AI agent. This agent: |
| 141 | +### Session-to-Conductor Mapping |
83 | 142 |
|
84 | | -1. Presents a numbered list of known agents (Claude Code, Gemini, Codex, Kiro CLI) |
85 | | -2. Waits for the user to type a number (1-N) |
86 | | -3. Saves the configuration file with all proxies enabled |
87 | | -4. Instructs the user to restart their editor |
| 143 | +The ConfigAgent groups sessions by configuration: |
88 | 144 |
|
89 | | -The configuration agent is a simple state machine that expects numeric input. Invalid input causes the prompt to repeat. |
| 145 | +- When a new session starts, ConfigAgent loads the current config from disk |
| 146 | +- UberconductorActor checks if a Conductor already exists for that config (compared by equality) |
| 147 | +- If not, a new Conductor is spawned, initialized, and the session is delegated to it |
90 | 148 |
|
91 | | -### Known Agents |
| 149 | +This means: |
| 150 | +- Multiple sessions with the same config share a Conductor |
| 151 | +- Config changes take effect on the next session, not immediately |
| 152 | +- Existing sessions continue with their original config |
92 | 153 |
|
93 | | -The configuration wizard offers these pre-configured agents: |
| 154 | +## Configuration Mode |
94 | 155 |
|
95 | | -| Name | Command | |
96 | | -|------|---------| |
97 | | -| Claude Code | `npx -y @zed-industries/claude-code-acp` | |
98 | | -| Gemini CLI | `npx -y -- @google/gemini-cli@latest --experimental-acp` | |
99 | | -| Codex | `npx -y @zed-industries/codex-acp` | |
100 | | -| Kiro CLI | `kiro-cli-chat acp` | |
| 156 | +Users can modify configuration at any time via the `/symposium:config` slash command. |
101 | 157 |
|
102 | | -Users can manually edit `~/.symposium/config.jsonc` to use other agents or modify proxy settings. |
| 158 | +### Entering Config Mode |
103 | 159 |
|
104 | | -## Implementation |
| 160 | +When ConfigAgent detects the config command in a prompt, it pauses the conductor before entering config mode. This ensures the conductor doesn't process any messages from the downstream agent while the user is configuring. |
| 161 | + |
| 162 | +```mermaid |
| 163 | +sequenceDiagram |
| 164 | + participant Client |
| 165 | + participant ConfigAgent |
| 166 | + participant Conductor |
| 167 | + participant ConfigMode as ConfigModeActor |
| 168 | +
|
| 169 | + Client->>ConfigAgent: PromptRequest("/symposium:config") |
| 170 | + ConfigAgent->>ConfigAgent: Detect config command |
| 171 | + ConfigAgent->>Conductor: Pause(resume_tx_sender) |
| 172 | + Conductor->>Conductor: Create oneshot channel |
| 173 | + Conductor-->>ConfigAgent: resume_tx |
| 174 | + Note over Conductor: Awaiting resume_rx (paused) |
| 175 | + ConfigAgent->>ConfigMode: spawn(config, resume_tx) |
| 176 | + ConfigAgent->>ConfigAgent: Store session state as Config{actor, return_to} |
| 177 | + ConfigMode-->>Client: Main menu (via notification) |
| 178 | + ConfigAgent-->>Client: PromptResponse(EndTurn) |
| 179 | +``` |
105 | 180 |
|
106 | | -The implementation consists of: |
| 181 | +The pause/resume protocol: |
| 182 | +1. ConfigAgent sends `Pause` to conductor with a channel to receive `resume_tx` |
| 183 | +2. Conductor creates a oneshot channel, sends `resume_tx` back, then awaits `resume_rx` |
| 184 | +3. ConfigModeActor holds `resume_tx` - when it exits, dropping `resume_tx` signals the conductor to resume |
| 185 | +4. While paused, the conductor processes no messages |
| 186 | + |
| 187 | +The session transitions from `Delegating{conductor}` to `Config{actor, return_to}`. The `return_to` field preserves the conductor handle so we can resume the session after config mode exits. |
| 188 | + |
| 189 | +### Config Mode UI |
| 190 | + |
| 191 | +ConfigModeActor presents an interactive "phone tree" menu: |
| 192 | + |
| 193 | +``` |
| 194 | +## Current Configuration |
| 195 | +
|
| 196 | +**Agent:** Claude Code |
| 197 | +
|
| 198 | +**Extensions:** |
| 199 | +1. [x] sparkle |
| 200 | +2. [x] ferris |
| 201 | +3. [x] cargo |
| 202 | +
|
| 203 | +## Commands |
| 204 | +
|
| 205 | +- `SAVE` - Save changes and exit |
| 206 | +- `CANCEL` - Discard changes and exit |
| 207 | +- `A` or `AGENT` - Change agent |
| 208 | +- `1`, `2`, `3` - Toggle extension on/off |
| 209 | +- `move X to Y` - Reorder extensions |
| 210 | +``` |
| 211 | + |
| 212 | +The actor uses **async control flow as a state machine**: nested async loops replace explicit state enums. Each submenu (like agent selection) is a function that awaits input and returns when complete. |
| 213 | + |
| 214 | +Commands return a `MenuAction` to control redisplay: |
| 215 | +- `Done` - Exit config mode entirely |
| 216 | +- `Redisplay` - Show the menu again (after successful command) |
| 217 | +- `Continue` - Don't redisplay (for invalid input, just wait for next) |
| 218 | + |
| 219 | +### Exiting Config Mode |
| 220 | + |
| 221 | +When the user saves or cancels, the ConfigModeActor exits and drops `resume_tx`, which signals the conductor to resume: |
| 222 | + |
| 223 | +```mermaid |
| 224 | +sequenceDiagram |
| 225 | + participant Client |
| 226 | + participant ConfigAgent |
| 227 | + participant ConfigMode as ConfigModeActor |
| 228 | + participant Conductor |
| 229 | +
|
| 230 | + Client->>ConfigAgent: PromptRequest("SAVE") |
| 231 | + ConfigAgent->>ConfigMode: send input |
| 232 | + ConfigMode->>ConfigMode: Save config to disk |
| 233 | + ConfigMode-->>ConfigAgent: ConfigModeOutput::Done{config} |
| 234 | + Note over ConfigMode: Actor exits, drops resume_tx |
| 235 | + Note over Conductor: resume_rx completes, resumes |
| 236 | + ConfigAgent->>ConfigAgent: Restore session to Delegating{return_to} |
| 237 | + ConfigAgent-->>Client: "Configuration saved. Returning to your session." |
| 238 | +``` |
| 239 | + |
| 240 | +## First-Time Setup |
| 241 | + |
| 242 | +When no configuration file exists, ConfigAgent enters `InitialSetup` state instead of delegating to a conductor. The setup flow: |
| 243 | + |
| 244 | +1. Fetches available agents from the [ACP Agent Registry](https://github.com/agentclientprotocol/registry) |
| 245 | +2. Presents a numbered list of agents |
| 246 | +3. User types a number to select |
| 247 | +4. Saves configuration with all proxies enabled by default |
| 248 | +5. Transitions to normal operation |
| 249 | + |
| 250 | +## Implementation |
107 | 251 |
|
108 | | -- **Config types:** `SymposiumUserConfig` and `ProxyEntry` structs in `src/symposium-acp-agent/src/config.rs` |
109 | | -- **Config loading:** `load()` reads from `~/.symposium/config.jsonc`, `save()` writes it |
110 | | -- **Configuration agent:** `ConfigurationAgent` implements the ACP `Component` trait |
111 | | -- **CLI integration:** `Run` variant in the `Command` enum |
| 252 | +| Component | Location | Purpose | |
| 253 | +|-----------|----------|---------| |
| 254 | +| `ConfigAgent` | `config_agent/mod.rs` | Message routing and session state | |
| 255 | +| `UberconductorActor` | `config_agent/uberconductor_actor.rs` | Conductor lifecycle management | |
| 256 | +| `ConductorActor` | `config_agent/conductor_actor.rs` | Proxy chain and agent process | |
| 257 | +| `ConfigModeActor` | `config_agent/config_mode_actor.rs` | Interactive config UI | |
| 258 | +| `SymposiumUserConfig` | `user_config.rs` | Config file parsing and persistence | |
112 | 259 |
|
113 | 260 | ### Dependencies |
114 | 261 |
|
|
0 commit comments