Conductor: P/ACP Orchestrator
{{#rfd: proxying-acp}}
The Conductor (binary name: conductor) is the orchestrator for P/ACP proxy chains. It coordinates the flow of ACP messages through a chain of proxy components.
Overview
The conductor orchestrates proxy chains by sitting between every component. It spawns component processes and routes all messages, presenting itself as a normal ACP agent to the editor.
flowchart TB
Editor[Editor]
C[Conductor]
P1[Component 1]
P2[Component 2]
Editor <-->|ACP via stdio| C
C <-->|stdio| P1
C <-->|stdio| P2
Key insight: Components never talk directly to each other. The conductor routes ALL messages using the _proxy/successor/* protocol.
From the editor's perspective: Conductor is a normal ACP agent communicating over stdio.
From each component's perspective:
- Receives normal ACP messages from the conductor
- Sends
_proxy/successor/requestto conductor to forward messages TO successor - Receives
_proxy/successor/requestfrom conductor for messages FROM successor
See Architecture Overview for detailed conceptual and actual message flows.
Responsibilities
The conductor has four core responsibilities:
1. Process Management
- Spawns component processes based on command-line arguments
- Manages component lifecycle (startup, shutdown, error handling)
- For MVP: If any component crashes, shut down the entire chain
Command-line interface:
# Agent mode - manages proxy chain
conductor agent sparkle-acp claude-code-acp
# MCP mode - bridges stdio to TCP for MCP-over-ACP
conductor mcp 54321
Agent mode creates a chain: Editor → Conductor → sparkle-acp → claude-code-acp
MCP mode bridges MCP JSON-RPC (stdio) to raw JSON-RPC (TCP connection to main conductor)
2. Message Routing
The conductor routes ALL messages between components. No component talks directly to another.
Message ordering: The conductor preserves message send order by routing all forwarding decisions through a central event loop, preventing responses from overtaking notifications.
Message flow types:
- Editor → First Component: Conductor forwards normal ACP messages
- Component → Successor: Component sends
_proxy/successor/requestto conductor, which unwraps and forwards to next component - Successor → Component: Conductor wraps messages in
_proxy/successor/requestwhen sending FROM successor - Responses: Flow back via standard JSON-RPC response IDs
See Architecture Overview for detailed request/response flow diagrams.
3. Capability Management
The conductor manages proxy capability handshakes during initialization:
Normal Mode (conductor as root):
- Offers
proxy: trueto all components EXCEPT the last - Verifies each proxy component accepts the capability
- Last component (agent) receives standard ACP initialization
Proxy Mode (conductor as proxy):
- When conductor itself receives
proxy: trueduring initialization - Offers
proxy: trueto ALL components (including the last) - Enables tree-structured proxy chains
See Architecture Overview for detailed handshake flows and Proxy Mode below for hierarchical chain details.
4. MCP Bridge Adaptation
When components provide MCP servers with ACP transport ("url": "acp:$UUID"):
If agent has mcp_acp_transport capability:
- Pass through MCP server declarations unchanged
- Agent handles
_mcp/*messages natively
If agent lacks mcp_acp_transport capability:
- Bind TCP port for each ACP-transport MCP server
- Transform MCP server spec to use
conductor mcp $port - Spawn
conductor mcp $portbridge processes - Route MCP tool calls:
- Agent → stdio → bridge → TCP → conductor →
_mcp/*messages backward up chain - Component responses flow back: component → conductor → TCP → bridge → stdio → agent
- Agent → stdio → bridge → TCP → conductor →
See MCP Bridge for full implementation details.
Proxy Mode
The conductor can itself operate as a proxy component within a larger chain, enabling tree-structured proxy architectures.
How Proxy Mode Works
When the conductor receives an initialize request with the proxy capability:
- Detection: Conductor detects it's being used as a proxy component
- All components become proxies: Offers
proxy: trueto ALL managed components (including the last) - Successor forwarding: When the final component sends
_proxy/successor/request, conductor forwards to its own successor
Example: Hierarchical Chain
client → proxy1 → conductor (proxy mode) → final-agent
↓ manages
p1 → p2 → p3
Message flow when p3 forwards to successor:
- p3 sends
_proxy/successor/requestto conductor - Conductor recognizes it's in proxy mode
- Conductor sends
_proxy/successor/requestto proxy1 (its predecessor) - proxy1 routes to final-agent
Use Cases
Modular sub-chains: Group related proxies into a conductor-managed sub-chain that can be inserted anywhere
Conditional routing: A proxy can route to conductor-based sub-chains based on request type
Isolated environments: Each conductor manages its own component lifecycle while participating in larger chains
Implementation Notes
- Proxy mode is detected during initialization by checking for
proxy: truein incominginitializerequest - In normal mode: last component is agent (no proxy capability)
- In proxy mode: all components are proxies (all receive proxy capability)
- The conductor's own successor is determined by whoever initialized it
See Architecture Overview for conceptual diagrams.
Initialization Flow
sequenceDiagram
participant Editor
participant Conductor
participant Sparkle as Component1<br/>(Sparkle)
participant Agent as Component2<br/>(Agent)
Note over Conductor: Spawns both components at startup<br/>from CLI args
Editor->>Conductor: acp/initialize [I0]
Conductor->>Sparkle: acp/initialize (offers proxy capability) [I0]
Note over Sparkle: Sees proxy capability offer,<br/>knows it has a successor
Sparkle->>Conductor: _proxy/successor/request(acp/initialize) [I1]
Note over Conductor: Unwraps request,<br/>knows Agent is last in chain
Conductor->>Agent: acp/initialize (NO proxy capability - agent is last) [I1]
Agent-->>Conductor: initialize response (capabilities) [I1]
Conductor-->>Sparkle: _proxy/successor response [I1]
Note over Sparkle: Sees Agent's capabilities,<br/>prepares response
Sparkle-->>Conductor: initialize response (accepts proxy capability) [I0]
Note over Conductor: Verifies Sparkle accepted proxy.<br/>If not, would fail with error.
Conductor-->>Editor: initialize response [I0]
Key points:
- Conductor spawns ALL components at startup based on command-line args
- Sequential initialization: Conductor → Component1 → Component2 → ... → Agent
- Proxy capability handshake:
- Conductor offers
proxy: trueto non-last components (in InitializeRequest_meta) - Components must accept by responding with
proxy: true(in InitializeResponse_meta) - Last component (agent) is NOT offered proxy capability
- Conductor verifies acceptance and fails initialization if missing
- Conductor offers
- Components use
_proxy/successor/requestto initialize their successors - Capabilities flow back up the chain: Each component sees successor's capabilities before responding
- Message IDs: Preserved from editor (I0), new IDs for proxy messages (I1, I2, ...)
Implementation Architecture
The conductor uses an actor-based architecture with message passing via channels.
Core Components
- Main connection: Handles editor stdio and spawns the event loop
- Component connections: Each component has a bidirectional JSON-RPC connection
- Message router: Central actor that receives
ConductorMessageenums and routes appropriately - MCP bridge actors: Manage MCP-over-ACP connections
Message Ordering Invariant
Critical invariant: All messages (requests, responses, notifications) between any two endpoints must maintain their send order.
The conductor ensures this invariant by routing all message forwarding through its central message queue (ConductorMessage channel). This prevents faster message types (responses) from overtaking slower ones (notifications).
Why This Matters
Without ordering preservation, a race condition can occur:
- Agent sends
session/updatenotification - Agent responds to
session/promptrequest - Response takes a fast path (reply_actor with oneshot channels)
- Notification takes slower path (handler pipeline)
- Response arrives before notification → client loses notification data
Implementation
The conductor uses extension traits to route all forwarding through the central queue:
JrConnectionCxExt::send_proxied_message_via- Routes both requests and notificationsJrRequestCxExt::respond_via- Routes responses through the queueJrResponseExt::forward_response_via- Ensures response forwarding maintains order
All message forwarding in both directions (client-to-agent and agent-to-client) flows through the conductor's central event loop, which processes ConductorMessage enums sequentially. This serialization ensures messages arrive in the same order they were sent.
Message Routing Implementation
The conductor uses a recursive spawning pattern:
- Recursive chain building: Each component spawns the next, establishing connections
- Actor-based routing: All messages flow through a central conductor actor via channels
- Response routing: Uses JSON-RPC response IDs and request contexts to route back
- No explicit ID tracking: Context passing eliminates need for manual ID management
Key routing decisions:
- Normal mode: Last component gets normal ACP (no proxy capability)
- Proxy mode: All components get proxy capability, final component can forward to conductor's successor
- Bidirectional
_proxy/successor/*: Used for both TO successor (unwrap and forward) and FROM successor (wrap and deliver)
Concurrency Model
Built on Tokio async runtime:
- Async I/O: All stdio operations are non-blocking
- Message passing: Components communicate via mpsc channels
- Spawned tasks: Each connection handler runs as separate task
- Error propagation: Tasks send errors back to main actor via channels
See source code in src/sacp-conductor/src/conductor.rs for implementation details.
Error Handling
Component Crashes
If any component process exits or crashes:
- Log error to stderr
- Shut down entire Conductor process
- Exit with non-zero status
The editor will see the ACP connection close and can handle appropriately.
Invalid Messages
If Conductor receives malformed JSON-RPC:
- Log to stderr
- Continue processing (don't crash the chain)
- May result in downstream errors
Initialization Failures
If component fails to initialize:
- Log error
- Return error response to editor
- Shut down
Implementation Phases
Phase 1: Basic Routing (MVP)
- Design documented
- Parse command-line arguments (component list)
- Spawn components recursively (alternative to "spawn all at startup")
- Set up stdio pipes for all components
-
Message routing logic:
- Editor → Component1 forwarding
-
_proxy/successor/requestunwrapping and forwarding - Response routing via context passing (alternative to explicit ID tracking)
- Component → Editor message routing
-
Actor-based message passing architecture with
ConductorMessageenum - Error reporting from spawned tasks to conductor
-
PUNCH LIST - Remaining MVP items:
-
Fix typo:
ComnponentToItsClientMessage→ComponentToItsClientMessage -
Proxy capability handshake during initialization:
-
Offer
proxy: truein_metato non-last components duringacp/initialize -
Do NOT offer
proxyto last component (agent) -
Verify component accepts by checking for
proxy: truein InitializeResponse_meta - Fail initialization with error "component X is not a proxy" if handshake fails
-
Offer
- Add documentation/comments explaining recursive chain building
- Add logging (message routing, component startup, errors)
- Write tests (proxy capability handshake, basic routing, initialization, error handling)
- Component crash detection and chain shutdown
-
Fix typo:
Phase 2: Robust Error Handling
- Basic error reporting from async tasks
- Graceful component shutdown
- Retry logic for transient failures
- Health checks
- Timeout handling for hung requests
Phase 3: Observability
- Structured logging/tracing
- Performance metrics
- Debug mode with message inspection
Phase 4: Advanced Features
- Dynamic component loading
- Hot reload of components
- Multiple parallel chains
Testing Strategy
Unit Tests
- Message parsing and forwarding logic
- Capability modification
- Error handling paths
Integration Tests
- Full chain initialization
- Message flow through real components
- Component crash scenarios
- Malformed message handling
End-to-End Tests
- Real editor + Conductor + test components
- Sparkle + Claude Code integration
- Performance benchmarks
Open Questions
- Component discovery: How do we find component binaries? PATH? Configuration file?
- Configuration: Should Conductor support a config file for default chains?
- Logging: Structured logging format? Integration with existing Symposium logging?
- Metrics: Should Conductor expose metrics (message counts, latency)?
- Security: Do we need to validate/sandbox component processes?
Related Documentation
- P/ACP RFD - Full protocol specification
- Proxying ACP Server Trait - Component implementation guide
- Sparkle Component - Example P/ACP component