feat: add runtime driver abstraction for platform-agnostic AI coding agent support#1060
Open
anchapin wants to merge 4 commits into
Open
feat: add runtime driver abstraction for platform-agnostic AI coding agent support#1060anchapin wants to merge 4 commits into
anchapin wants to merge 4 commits into
Conversation
…-agnostic AI coding agent support - Add AgentRuntimeDriver interface defining the contract for runtime implementations - Add RuntimeRegistry for managing driver registration and selection - Add CopilotDriver implementation wrapping existing @github/copilot-sdk - Add OpenCodeDriver stub implementation for future OpenCode support - Add RuntimeDefinition to SquadSDKConfig builder types - Add defineRuntime() builder function for runtime configuration - Export runtime and drivers modules from SDK index This enables Squad to work with alternative AI coding agent runtimes beyond GitHub Copilot by abstracting the runtime behind a driver interface. AI Tool: Manual implementation based on architectural planning Human Review: Required for driver implementations
- README.md: update platform badge to 'Multi-runtime', update description to reflect pluggable runtimes - SDK-First Mode docs: add defineRuntime() builder documentation with runtime table and config fields
- Add comprehensive Runtime Configuration guide in reference/ - Update Full SDK-First Config example to include runtime option - Add Runtime Configuration to See Also links
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new runtime driver abstraction layer to the Squad SDK so alternative AI coding agent runtimes (beyond GitHub Copilot) can be supported via pluggable drivers.
Changes:
- Introduces
AgentRuntimeDriver+ session/types contract and a singletonRuntimeRegistry. - Adds driver implementations:
CopilotDriver(wrapping@github/copilot-sdk) and anOpenCodeDriverskeleton. - Extends SDK-first config/builders with
RuntimeDefinition+defineRuntime()and documents runtime configuration.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/squad-sdk/src/runtime/driver.ts | Defines the runtime-agnostic driver/session interfaces and shared error types. |
| packages/squad-sdk/src/runtime/registry.ts | Adds a singleton registry for registering/selecting runtime drivers and creating sessions. |
| packages/squad-sdk/src/runtime/index.ts | Runtime barrel export for driver/registry. |
| packages/squad-sdk/src/drivers/copilot/driver.ts | Implements AgentRuntimeDriver using @github/copilot-sdk. |
| packages/squad-sdk/src/drivers/opencode/driver.ts | Adds an OpenCode driver scaffold (process + JSON-RPC assumptions). |
| packages/squad-sdk/src/drivers/index.ts | Drivers barrel export. |
| packages/squad-sdk/src/builders/types.ts | Adds RuntimeDefinition and runtime?: ... to SquadSDKConfig. |
| packages/squad-sdk/src/builders/index.ts | Adds defineRuntime() and validates config.runtime shape in defineSquad(). |
| packages/squad-sdk/src/index.ts | Exposes runtime/drivers modules via the SDK top-level barrel. |
| packages/squad-sdk/package.json | Version bump. |
| packages/squad-cli/package.json | Version bump. |
| package.json | Version bump. |
| docs/src/content/docs/sdk-first-mode.md | Documents defineRuntime() usage in SDK-first config. |
| docs/src/content/docs/reference/runtime.md | Adds runtime configuration reference documentation. |
| RUNTIME_REFRACTOR_PLAN.md | Adds a refactor plan document for runtime-agnostic architecture. |
| README.md | Updates positioning to “multi-runtime”. |
Comment on lines
+102
to
+108
| registerDriverFactory(name: string, factory: () => Promise<AgentRuntimeDriver>): void { | ||
| if (this.drivers.has(name)) { | ||
| console.warn(`Driver for runtime "${name}" already registered. Skipping factory registration.`); | ||
| return; | ||
| } | ||
| this.driverFactories.set(name, factory); | ||
| } |
Comment on lines
+59
to
+60
| export * from './runtime/index.js'; | ||
| export * from './drivers/index.js'; |
Comment on lines
+677
to
+680
| private async attemptReconnection(): Promise<void> { | ||
| if (this.state === 'reconnecting') { | ||
| throw new DriverError(this.name, 'Reconnection already in progress', 'reconnect', true); | ||
| } |
Comment on lines
+249
to
+308
| const span = tracer.startSpan('squad.driver.opencode.connect'); | ||
|
|
||
| this.state = 'connecting'; | ||
|
|
||
| try { | ||
| return new Promise<void>((resolve, reject) => { | ||
| const args = ['--stdio', '--json']; | ||
| if (this.options.cliArgs) { | ||
| args.push(...this.options.cliArgs); | ||
| } | ||
|
|
||
| const env = this.options.env as Record<string, string>; | ||
| this.process = spawn(this.options.cliPath!, args, { | ||
| cwd: this.options.cwd, | ||
| env, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
|
|
||
| this.process.on('error', (err) => { | ||
| this.state = 'error'; | ||
| span.recordException(err); | ||
| reject(new DriverConnectionError(this.name, `Failed to start OpenCode: ${err.message}`)); | ||
| }); | ||
|
|
||
| this.process.on('exit', (code) => { | ||
| if (code !== 0) { | ||
| this.state = 'error'; | ||
| } else { | ||
| this.state = 'disconnected'; | ||
| } | ||
| this.eventEmitter.emit('disconnected'); | ||
| }); | ||
|
|
||
| // Wait for ready signal | ||
| this.process.stdout?.on('data', (data: Buffer) => { | ||
| const output = data.toString(); | ||
| // OpenCode might send a "ready" message or similar | ||
| if (output.includes('ready') || output.includes('listening')) { | ||
| this.state = 'connected'; | ||
| span.setAttribute('connection.transport', 'stdio'); | ||
| resolve(); | ||
| } | ||
| }); | ||
|
|
||
| // Timeout after 10 seconds | ||
| setTimeout(() => { | ||
| if (this.state !== 'connected') { | ||
| this.process?.kill(); | ||
| reject(new DriverConnectionError(this.name, 'Connection timeout')); | ||
| } | ||
| }, 10000); | ||
| }); | ||
| } catch (err) { | ||
| this.state = 'error'; | ||
| span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) }); | ||
| span.recordException(err instanceof Error ? err : new Error(String(err))); | ||
| throw err; | ||
| } finally { | ||
| span.end(); | ||
| } |
Comment on lines
+123
to
+131
| Advanced users can implement custom drivers by implementing `AgentRuntimeDriver`: | ||
|
|
||
| ```typescript | ||
| interface AgentRuntimeDriver { | ||
| readonly name: string; | ||
| initialize(config: RuntimeConfig): Promise<void>; | ||
| createSession(options: SessionOptions): Promise<AgentSession>; | ||
| listSessions(): Promise<SessionInfo[]>; | ||
| dispose(): Promise<void>; |
Comment on lines
+255
to
+265
| const args = ['--stdio', '--json']; | ||
| if (this.options.cliArgs) { | ||
| args.push(...this.options.cliArgs); | ||
| } | ||
|
|
||
| const env = this.options.env as Record<string, string>; | ||
| this.process = spawn(this.options.cliPath!, args, { | ||
| cwd: this.options.cwd, | ||
| env, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); |
| */ | ||
| export function defineRuntime(config: RuntimeDefinition): RuntimeDefinition { | ||
| assertObject(config, 'defineRuntime'); | ||
| assertNonEmptyString(config.name, 'name', 'defineRuntime'); |
| */ | ||
| export function defineRuntime(config: RuntimeDefinition): RuntimeDefinition { | ||
| assertObject(config, 'defineRuntime'); | ||
| assertNonEmptyString(config.name, 'name', 'defineRuntime'); |
Comment on lines
+197
to
+199
| await this.sendRPC('session.close', { sessionId: this.sessionId }); | ||
| this.process.kill(); | ||
| this.pendingRequests.clear(); |
Comment on lines
+540
to
+582
| private sendRPC(method: string, params?: Record<string, unknown>): Promise<unknown> { | ||
| return new Promise((resolve, reject) => { | ||
| if (!this.process || !this.process.stdin) { | ||
| reject(new DriverConnectionError(this.name, 'Process not connected')); | ||
| return; | ||
| } | ||
|
|
||
| const id = Math.random().toString(36).slice(2); | ||
| const request = { id, method, params }; | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| pendingRequests.delete(id); | ||
| reject(new Error(`Request ${method} timed out`)); | ||
| }, 60000); | ||
|
|
||
| const pendingRequests = new Map<string, { | ||
| resolve: (value: unknown) => void; | ||
| reject: (error: Error) => void; | ||
| }>(); | ||
|
|
||
| pendingRequests.set(id, { resolve, reject }); | ||
|
|
||
| this.process.stdin.write(JSON.stringify(request) + '\n'); | ||
|
|
||
| this.process.stdout?.on('data', (data: Buffer) => { | ||
| for (const line of data.toString().split('\n').filter(Boolean)) { | ||
| try { | ||
| const response = JSON.parse(line) as OpenCodeRPCResponse; | ||
| if (response.id === id) { | ||
| clearTimeout(timeout); | ||
| if (response.error) { | ||
| reject(new Error(response.error.message)); | ||
| } else { | ||
| resolve(response.result); | ||
| } | ||
| pendingRequests.delete(id); | ||
| } | ||
| } catch { | ||
| // Ignore parse errors | ||
| } | ||
| } | ||
| }); | ||
| }); |
- Use 'opencode run --format json' for headless agent execution - Parse JSON events: step_start, text, step_finish for streaming - Implement session continuation with --continue flag - Add comprehensive error event handling - Fix stdio issue: use ['ignore', 'pipe', 'pipe'] instead of ['pipe', 'pipe', 'pipe'] - Update runtime.md documentation to reflect OpenCode is experimental
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add runtime driver abstraction layer enabling squad to work with alternative AI coding agent runtimes beyond GitHub Copilot.
Changes
AgentRuntimeDriverinterface defining the contract for runtime implementationsRuntimeRegistryfor managing driver registration and selectionCopilotDriverimplementation wrapping existing @github/copilot-sdkOpenCodeDriverstub implementation for future OpenCode supportRuntimeDefinitionto SquadSDKConfig builder typesdefineRuntime()builder function for runtime configurationMotivation
See issue #1058: Users should be able to choose OpenCode (or other AI coding agents) as the underlying runtime for squad agents, not just GitHub Copilot.
Testing
npm run lintpassesRelated Issues
Closes #1058