Skip to content

Commit 1af00b1

Browse files
authored
Merge pull request #106 from nikomatsakis/config-agent
feat: ConfigAgent architecture for run mode
2 parents 5c8d06e + 8030a2b commit 1af00b1

File tree

18 files changed

+2211
-980
lines changed

18 files changed

+2211
-980
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ jobs:
6565
restore-keys: |
6666
${{ runner.os }}-cargo-
6767
68+
- name: Install elizacp
69+
run: cargo install --force elizacp
70+
6871
- name: Run CI tests
6972
run: cargo ci test
7073

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Release Ferris
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
build:
9+
if: startsWith(github.ref_name, 'symposium-ferris-v')
10+
permissions:
11+
contents: write
12+
uses: symposium-dev/package-agent-extension/.github/workflows/build.yml@v1
13+
with:
14+
manifest: src/symposium-ferris/Cargo.toml
15+
musl: true
16+
secrets: inherit

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

md/design/run-mode.md

Lines changed: 209 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,6 @@
22

33
The `run` subcommand simplifies editor integration by reading agent configuration from a file rather than requiring command-line arguments.
44

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-
195
## Configuration File
206

217
**Location:** `~/.symposium/config.jsonc`
@@ -43,72 +29,233 @@ The file uses JSONC (JSON with comments) format:
4329
| `agent` | string | Shell command to spawn the downstream agent. Parsed using shell word splitting. |
4430
| `proxies` | array | List of proxy extensions with `name` and `enabled` fields. |
4531

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
4733

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:
4935

5036
```
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
70106
```
71107

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:
73113

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
78137
```
79138

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).
81140

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
83142

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:
88144

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
90148

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
92153

93-
The configuration wizard offers these pre-configured agents:
154+
## Configuration Mode
94155

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.
101157

102-
Users can manually edit `~/.symposium/config.jsonc` to use other agents or modify proxy settings.
158+
### Entering Config Mode
103159

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+
```
105180

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
107251

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 |
112259

113260
### Dependencies
114261

0 commit comments

Comments
 (0)