Skip to content

feat: add runtime driver abstraction for platform-agnostic AI coding agent support#1060

Open
anchapin wants to merge 4 commits into
bradygaster:devfrom
anchapin:alex/opencode-runtime-driver-v2
Open

feat: add runtime driver abstraction for platform-agnostic AI coding agent support#1060
anchapin wants to merge 4 commits into
bradygaster:devfrom
anchapin:alex/opencode-runtime-driver-v2

Conversation

@anchapin
Copy link
Copy Markdown

@anchapin anchapin commented May 1, 2026

Summary

Add runtime driver abstraction layer enabling squad to work with alternative AI coding agent runtimes beyond GitHub Copilot.

Changes

  • AgentRuntimeDriver interface defining the contract for runtime implementations
  • RuntimeRegistry for managing driver registration and selection
  • CopilotDriver implementation wrapping existing @github/copilot-sdk
  • OpenCodeDriver stub implementation for future OpenCode support
  • RuntimeDefinition to SquadSDKConfig builder types
  • defineRuntime() builder function for runtime configuration

Motivation

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

  • TypeScript compilation passes
  • Unit tests pass
  • npm run lint passes

Related Issues

Closes #1058

…-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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 singleton RuntimeRegistry.
  • Adds driver implementations: CopilotDriver (wrapping @github/copilot-sdk) and an OpenCodeDriver skeleton.
  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add OpenCode CLI as an alternative AI coding agent runtime

3 participants