-
Notifications
You must be signed in to change notification settings - Fork 0
Implement CLIRuntimeBase abstract class #5
Description
Context
RemoteClaw runs AI agent CLIs (Claude, Gemini, Codex, OpenCode) as subprocesses and parses their NDJSON streaming output into normalized AgentEvent objects. All four CLI runtimes share ~80% of their logic: spawn a subprocess, parse NDJSON lines from stdout, capture stderr, handle abort signals, manage watchdog timers.
CLIRuntimeBase is the abstract base class that encapsulates this shared subprocess machinery. Each concrete runtime (Claude, Gemini, Codex, OpenCode) extends it and only implements the CLI-specific parts: argument construction, event extraction, and environment setup.
Depends on: #3 (AgentRuntime interface — merged)
Architecture
AgentRuntime (interface, from types.ts)
^
| implements
|
CLIRuntimeBase (abstract class — this issue)
^
| extends
|
├── ClaudeCliRuntime (future issue)
├── GeminiCliRuntime (future issue)
├── CodexCliRuntime (future issue)
└── OpenCodeCliRuntime (future issue)
CLIRuntimeBase handles:
- Subprocess spawning via
child_process.spawn() - NDJSON line parsing from stdout
- Async event queue (yields
AgentEventitems) - Watchdog timer (configurable timeout)
- Abort signal propagation to child process
- Stderr capture for error classification
- Stdin delivery for long prompts
Subclasses handle:
buildArgs(params)— construct CLI-specific argumentsextractEvent(line)— parse provider-specific NDJSON intoAgentEventbuildEnv(params)— construct provider-specific environment variables
File Location
src/middleware/cli-runtime-base.ts (~150 lines estimated)
Implementation Details
Abstract Methods
/** Construct CLI-specific command-line arguments. */
protected abstract buildArgs(params: AgentExecuteParams): string[];
/** Parse a single NDJSON line into an AgentEvent (or null to skip). */
protected abstract extractEvent(line: string): AgentEvent | null;
/** Construct provider-specific environment variables. */
protected abstract buildEnv(params: AgentExecuteParams): Record<string, string>;Subprocess Spawning
- Use
child_process.spawn()(notexec) for streaming output - Set
cwdfromparams.workingDirectory - Merge
buildEnv(params)withprocess.env stdio: ["pipe", "pipe", "pipe"]— stdin for prompts, stdout for NDJSON, stderr for errors
Long Prompt Handling
Prompts exceeding 10K characters must be delivered via stdin (not CLI argument) to avoid OS argument length limits:
- Write prompt to
child.stdin, then close stdin - Subclasses indicate whether the CLI accepts stdin prompts via a class property or method
NDJSON Parsing
- Read stdout line-by-line (split on
\n) - Each line: attempt
JSON.parse(), pass toextractEvent() - Malformed lines: log warning, skip (not fatal)
- Yield parsed
AgentEventobjects via async generator
Watchdog Timer
- Configurable timeout (default: 5 minutes, override via constructor)
- Reset on each NDJSON line received (activity = alive)
- On timeout: kill child process, yield
AgentErrorEventwith timeout info
Abort Signal Propagation
- Wire
params.abortSignaltochild.kill('SIGTERM') - On abort: yield
AgentErrorEventwith abort info, thenAgentDoneEvent
Stderr Capture
- Accumulate stderr output into a buffer
- Expose to subclasses (or ErrorClassifier) after subprocess exits
- Don't treat stderr as fatal — some CLIs write progress/debug info to stderr
execute() — Async Generator
The public execute() method implements AgentRuntime.execute() as an async generator:
async *execute(params: AgentExecuteParams): AsyncIterable<AgentEvent> {
const args = this.buildArgs(params);
const env = this.buildEnv(params);
const child = spawn(this.command, args, { cwd: params.workingDirectory, env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
// ... wire abort signal, watchdog timer, stderr capture ...
// ... for each NDJSON line from stdout: extractEvent() → yield ...
// ... on subprocess exit: yield AgentDoneEvent with AgentRunResult ...
}Constructor
constructor(
/** CLI command name (e.g., "claude", "gemini", "codex", "opencode"). */
protected readonly command: string,
/** Subprocess timeout in milliseconds (default: 5 minutes). */
protected readonly timeoutMs: number = 300_000,
)Types Used (from src/middleware/types.ts)
AgentRuntime— interface this class implementsAgentExecuteParams— input parameter typeAgentEvent— discriminated union yielded fromexecute()AgentRunResult— final summary carried inAgentDoneEventAgentErrorEvent— emitted on timeout/abort/error
Acceptance Criteria
-
src/middleware/cli-runtime-base.tsexports abstract classCLIRuntimeBase - Class implements
AgentRuntimeinterface - Three abstract methods:
buildArgs(),extractEvent(),buildEnv() - Subprocess spawned via
child_process.spawn()with configurablecommand - NDJSON stdout parsing with malformed line handling (log + skip)
-
execute()is an async generator yieldingAgentEventobjects - Long prompts (>10K chars) delivered via stdin
- Working directory set via
spawn()cwdoption - Stderr captured into a buffer for downstream error classification
- Abort signal from
params.abortSignalkills child process - Watchdog timer kills process on inactivity timeout
- Unit tests for: NDJSON parsing, abort propagation, watchdog timeout, stdin prompt delivery
-
pnpm buildpasses