Multi-Adapter Architecture Design
Archived design document. Preserved for historical context; not part of the current normative
reference/contract.
Overview
Extend agent-mux to support multiple adapter types beyond subprocess-based execution. This enables integration with sophisticated tools that provide HTTP APIs, WebSocket interfaces, or direct SDK access.
Current Architecture (Subprocess-Only)
interface AgentAdapter {
buildSpawnArgs(options: RunOptions): SpawnArgs;
parseEvent(line: string, context: ParseContext): AgentEvent | AgentEvent[] | null;
// ... session, auth, config methods
}
Limitations:
- Forces all tools into subprocess model
- Cannot leverage HTTP APIs, WebSocket streaming, or native SDKs
- Suboptimal for TUI-first tools (OpenCode), server-based tools (Codex app-server)
Proposed Architecture
1. Adapter Type Hierarchy
// Base interface (shared across all adapter types)
interface BaseAgentAdapter {
// Identity
readonly agent: AgentName;
readonly displayName: string;
readonly adapterType: 'subprocess' | 'remote' | 'programmatic';
// Capabilities (unchanged)
readonly capabilities: AgentCapabilities;
readonly models: ModelCapabilities[];
// Common functionality
detectAuth(): Promise<AuthState>;
getAuthGuidance(): AuthSetupGuidance;
sessionDir(cwd?: string): string;
parseSessionFile(filePath: string): Promise<Session>;
// ... other common methods
}
// Subprocess adapters (current model)
interface SubprocessAdapter extends BaseAgentAdapter {
readonly adapterType: 'subprocess';
readonly cliCommand: string;
buildSpawnArgs(options: RunOptions): SpawnArgs;
parseEvent(line: string, context: ParseContext): AgentEvent | AgentEvent[] | null;
}
// Remote adapters (HTTP, WebSocket, Unix sockets)
interface RemoteAdapter extends BaseAgentAdapter {
readonly adapterType: 'remote';
readonly connectionType: 'http' | 'websocket' | 'unix';
connect(options: RunOptions): Promise<RemoteConnection>;
disconnect(connection: RemoteConnection): Promise<void>;
// Optional: start/stop server if adapter manages it
startServer?(): Promise<ServerInfo>;
stopServer?(serverInfo: ServerInfo): Promise<void>;
}
// Programmatic adapters (direct SDK integration)
interface ProgrammaticAdapter extends BaseAgentAdapter {
readonly adapterType: 'programmatic';
execute(options: RunOptions): AsyncIterableIterator<AgentEvent>;
}
2. Connection Abstractions
// Remote connection interface
interface RemoteConnection {
readonly connectionId: string;
readonly connectionType: 'http' | 'websocket' | 'unix';
send(data: unknown): Promise<void>;
receive(): AsyncIterableIterator<AgentEvent>;
close(): Promise<void>;
}
// HTTP-specific connection
interface HttpConnection extends RemoteConnection {
readonly connectionType: 'http';
readonly baseUrl: string;
get(path: string, params?: Record<string, unknown>): Promise<unknown>;
post(path: string, data?: unknown): Promise<unknown>;
stream(path: string, data?: unknown): AsyncIterableIterator<AgentEvent>;
}
// WebSocket-specific connection
interface WebSocketConnection extends RemoteConnection {
readonly connectionType: 'websocket';
readonly websocketUrl: string;
subscribe(channel: string): AsyncIterableIterator<AgentEvent>;
unsubscribe(channel: string): Promise<void>;
}
3. Server Management
interface ServerInfo {
readonly serverId: string;
readonly serverType: string;
readonly endpoint: string;
readonly pid?: number;
readonly health?: 'starting' | 'healthy' | 'unhealthy';
}
interface ServerManager {
start(adapter: RemoteAdapter, options?: ServerOptions): Promise<ServerInfo>;
stop(serverId: string): Promise<void>;
health(serverId: string): Promise<ServerInfo>;
list(): Promise<ServerInfo[]>;
}
Implementation Plan
Phase 1: Core Architecture
- Create new interface hierarchy in
packages/core/src/adapter-types.ts - Extend AgentAdapter to be union type:
SubprocessAdapter | RemoteAdapter | ProgrammaticAdapter - Update BaseAgentAdapter in adapters package to implement
SubprocessAdapter - Maintain backward compatibility - all existing adapters continue working
Phase 2: Execution Engine Updates
- Update RunHandleImpl to route based on
adapter.adapterType - Create RemoteRunner for HTTP/WebSocket execution
- Create ProgrammaticRunner for direct SDK execution
- Add ServerManager for lifecycle management
- Update event streaming to handle different execution models
Phase 3: New Adapter Implementations
- opencode-http: HTTP server + REST API + SSE streaming
- codex-sdk: Direct SDK integration
- codex-websocket: WebSocket app-server integration
- claude-agent-sdk: Programmatic Claude interface
- pi-sdk: Enhanced programmatic Pi interface
Phase 4: Mock Infrastructure
- MockHttpServer: Simulate HTTP endpoints + SSE
- MockWebSocketServer: Simulate real-time connections
- MockSDK: Simulate direct SDK calls
- Enhanced scenarios: Support all adapter types
Adapter Naming Convention
Pattern: {tool}-{type} where type indicates the integration method:
opencode(subprocess, default)opencode-http(HTTP server)codex(subprocess, current)codex-sdk(programmatic SDK)codex-websocket(WebSocket app-server)claude-agent-sdk(programmatic)
Example Adapter Implementations
HTTP Adapter (OpenCode)
class OpenCodeHttpAdapter implements RemoteAdapter {
readonly adapterType = 'remote';
readonly connectionType = 'http';
readonly agent = 'opencode-http';
async connect(options: RunOptions): Promise<HttpConnection> {
// Start 'opencode serve' if needed
const serverInfo = await this.ensureServer();
// Create HTTP connection
return new OpenCodeHttpConnection({
baseUrl: serverInfo.endpoint,
sessionId: options.sessionId,
model: options.model,
});
}
private async ensureServer(): Promise<ServerInfo> {
// Check if server already running
// If not, start via 'opencode serve --port 0'
// Return connection details
}
}
class OpenCodeHttpConnection implements HttpConnection {
async *stream(path: string, data: unknown): AsyncIterableIterator<AgentEvent> {
// POST to /api/chat/stream with SSE
const response = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Accept': 'text/event-stream' }
});
for await (const chunk of response.body) {
yield this.parseServerSentEvent(chunk);
}
}
}
SDK Adapter (Codex)
class CodexSdkAdapter implements ProgrammaticAdapter {
readonly adapterType = 'programmatic';
readonly agent = 'codex-sdk';
async *execute(options: RunOptions): AsyncIterableIterator<AgentEvent> {
const sdk = new CodexSDK({
apiKey: process.env.OPENAI_API_KEY,
model: options.model || this.defaultModelId,
});
const stream = await sdk.chat.completions.create({
messages: [{ role: 'user', content: options.prompt }],
stream: true,
});
for await (const chunk of stream) {
yield this.parseCodexChunk(chunk);
}
}
}
Migration Strategy
- Backward Compatible: All existing adapters continue working unchanged
- Gradual Adoption: Add new adapter types without breaking existing functionality
- Clear Documentation: Document when to use each adapter type
- Mock Support: Ensure all adapter types have full mock coverage
Benefits
- Native Performance: Direct SDK integration eliminates subprocess overhead
- Real-time Streaming: WebSocket connections enable bidirectional communication
- Full Capabilities: HTTP APIs provide access to complete tool feature sets
- Flexible Integration: Choose the best integration method per tool
- Future-Proof: Support emerging tools with non-CLI interfaces
Claude-Specific Transport Note
Claude now spans multiple distinct integration surfaces that should not be collapsed into a single "server mode" mental model:
claudeCLI can provide a persistent structured subprocess transport through--print --input-format stream-json --output-format stream-json, with stdin carrying later user turns and stdout carrying structured events.claude-agent-sdkis a programmatic persistent transport with direct callback integration.claude-remote-controlis the server-managed Claude surface for Claude.ai / Claude app clients. Agent-mux can launch and observe the bridge honestly, but it does not advertise local stdin-driven chat semantics for it.- Claude channels are MCP-mediated push/reply integrations into a running Claude host session, not a standalone replacement for the CLI or SDK transports.
Agent-mux should model these surfaces honestly and only advertise the transport semantics each one actually supports.
Trade-offs
- Complexity: Multiple execution paths increase code complexity
- Resource Management: HTTP servers and connections need lifecycle management
- Testing: More sophisticated mocking required
- Dependencies: Programmatic adapters may require additional npm dependencies
Files to Create/Modify
New Files
packages/core/src/adapter-types.ts- New interface hierarchypackages/core/src/remote-runner.ts- HTTP/WebSocket executionpackages/core/src/programmatic-runner.ts- SDK executionpackages/core/src/server-manager.ts- Server lifecycle managementpackages/adapters/src/remote-adapter-base.ts- Base class for remote adapterspackages/adapters/src/programmatic-adapter-base.ts- Base class for SDK adapters
Modified Files
packages/core/src/adapter.ts- Update AgentAdapter union typepackages/core/src/run-handle-impl.ts- Route by adapter typepackages/adapters/src/base-adapter.ts- Implement SubprocessAdapterpackages/harness-mock/src/index.ts- Add mock infrastructure
This architecture enables agent-mux to evolve beyond subprocess-only integration while maintaining full backward compatibility and providing a clear path for supporting sophisticated AI tools with diverse integration requirements.