CliRunnerEngine
CliRunnerEngine manages subprocess lifecycle of external CLI Agent tools (such as Claude Code CLI, Codex CLI) via ProcessSupervisor. Supports both regular subprocess mode and PTY terminal mode.
Architecture Diagram
graph TB
Router["AgentRouter / MagiService"] --> Engine["CliRunnerEngine"]
Engine --> Resolve["resolveCliConfig()"]
Engine --> Backend["resolveCliBackendConfig()"]
Engine --> Args["buildCliArgs()"]
Engine --> Env["buildEnv()"]
Engine --> Supervisor["ProcessSupervisor"]
Supervisor --> Decision{"Process mode?"}
Decision -->|"mode: 'child'"| ChildAdapter["ChildAdapter<br/>child_process.spawn"]
Decision -->|"mode: 'pty'"| PtyAdapter["PtyAdapter<br/>node-pty"]
ChildAdapter --> RunRegistry["RunRegistry<br/>run state tracking"]
PtyAdapter --> RunRegistry
Engine --> Parse["parseCliOutput()"]
Engine --> DB["DB persistence<br/>messages + session ID"]
Engine --> IPC["IPC events<br/>agent:event"]
ProcessSupervisor
ProcessSupervisor is the subprocess lifecycle manager.
Interface
interface ProcessSupervisor {
spawn(input: SpawnInput): ManagedRun;
cancel(runId: string, reason?: TerminationReason): void;
cancelScope(scopeKey: string, reason?: TerminationReason): void;
getRecord(runId: string): RunRecord | undefined;
resizePty(runId: string, cols: number, rows: number): boolean;
}
Process Modes
Child Mode
interface SpawnChildInput {
mode: 'child';
argv: string[]; // command and arguments
input?: string; // stdin input
stdinMode?: 'inherit' | 'pipe-open' | 'pipe-closed';
windowsVerbatimArguments?: boolean;
// ...shared fields
}
Uses child_process.spawn to create subprocess with stdin/stdout communication via pipes.
PTY Mode
interface SpawnPtyInput {
mode: 'pty';
shell?: string; // default: powershell.exe (Windows) / /bin/bash (Unix)
args?: string[];
cols?: number; // default 120
rows?: number; // default 30
// ...shared fields
}
Uses node-pty to create pseudoterminal with full terminal interactivity (colors, cursor, line editing).
ManagedRun
interface ManagedRun {
runId: string;
pid?: number;
startedAtMs: number;
stdin?: ManagedRunStdin;
wait: () => Promise<RunExit>;
cancel: (reason?: TerminationReason) => void;
}
interface RunExit {
reason: TerminationReason;
exitCode: number | null;
exitSignal: string | null;
durationMs: number;
stdout: string;
stderr: string;
timedOut: boolean;
noOutputTimedOut: boolean;
}
Termination Reasons
type TerminationReason =
| 'manual-cancel' // user manual cancel
| 'overall-timeout' // total time timeout
| 'no-output-timeout' // no output timeout
| 'spawn-error' // spawn failure
| 'signal' // received signal
| 'exit'; // normal exit
Run State
type RunState = 'starting' | 'running' | 'exiting' | 'exited';
interface RunRecord {
runId: string;
sessionId: string;
backendId: string;
scopeKey?: string;
pid?: number;
startedAtMs: number;
lastOutputAtMs: number;
state: RunState;
terminationReason?: TerminationReason;
exitCode?: number | null;
}
Scope Management
scopeKey groups processes; processes within the same scope can be cancelled in batch:
scopeKey = `cli:${backendId}:${cliSessionId}`
replaceExistingScope = true // new process auto-cancels old ones in same scope
Timeout Handling
Dual timeout mechanism:
- Overall timeout (
timeoutMs) — process total runtime limit - No-output timeout (
noOutputTimeoutMs) — process idle time limit
graph LR
Start["Process start"] --> Timer1["Overall timeout timer<br/>default 300s"]
Start --> Timer2["No-output timer<br/>dynamic compute"]
Timer1 -->|timeout| Kill1["terminate: overall-timeout"]
Timer2 -->|timeout| Kill2["terminate: no-output-timeout"]
Output["output received"] -->|reset| Timer2
No-output timeout calculation (cli-watchdog):
noOutputTimeoutMs = clamp(
overallTimeoutMs * ratio,
minMs,
maxMs,
)
| Parameter | Fresh (first-time) | Resume (continuing) |
|---|---|---|
| ratio | 0.8 | 0.3 |
| minMs | 180,000 (3min) | 60,000 (1min) |
| maxMs | 600,000 (10min) | 180,000 (3min) |
Cross-platform Process Termination
kill-tree.ts handles cross-platform process tree termination:
- Windows —
taskkill /F /T /PID(force-terminate process tree) - Unix — process group signals:
SIGTERM→ wait →SIGKILL
CLI Backends
Built-in Backends
Claude Code CLI
{
command: 'claude',
args: ['--output-format', 'json', '--verbose', '--max-turns', '25'],
resumeArgs: ['--output-format', 'json', '--verbose', '--resume', '{sessionId}'],
output: 'json',
input: 'arg',
modelArg: '--model',
sessionArg: '--session-id',
sessionMode: 'always',
}
Codex CLI
{
command: 'codex',
args: ['exec', '--json', '--color', 'never', '--sandbox', 'workspace-write', '--skip-git-repo-check'],
output: 'jsonl',
input: 'arg',
modelArg: '--model',
sessionMode: 'none',
}
Backend Configuration Resolution
function resolveCliBackendConfig(
backendId: string,
overrides?: Partial<CliBackendConfig>,
): ResolvedCliBackend | null;
- Look up built-in config by
backendId - Apply user override config
- Return merged config
CLI Args Construction
function buildCliArgs(options: {
backend: CliBackendConfig;
baseArgs: string[];
modelId?: string;
sessionId?: string;
systemPrompt?: string;
isResume: boolean;
}): string[];
Concatenate in order: baseArgs → --model → --session-id → --append-system-prompt
Output Parsing
function parseCliOutput(
stdout: string,
outputMode: 'json' | 'jsonl' | 'text',
sessionIdFields?: string[],
): { text: string; sessionId?: string; usage?: object };
| Output mode | Parse strategy |
|---|---|
json | Parse entire output as JSON, extract result or text field |
jsonl | Parse JSON lines, concatenate content |
text | Use raw text directly |
Session Management
CLI Session ID
CliRunnerEngine maintains session ID for CLI tools that support multi-turn conversations:
sessionMode: 'always' | 'existing' | 'none'
| Mode | Behavior |
|---|---|
always | always use session ID (generate new if missing) |
existing | only use existing session ID |
none | do not use session ID |
Session ID is stored in chatSessions.cliSessionIds field (grouped by backendId).
PTY Terminal
In PTY mode, terminal data streams in real-time to all renderer windows:
| IPC Event | Payload | Description |
|---|---|---|
cli:ptyData | { sessionId, data } | terminal output data |
cli:ptyExit | { sessionId, exitCode, reason } | terminal process exit |
Supports terminal resize:
resizePty(dbSessionId: string, cols: number, rows: number): boolean;
IPC Events
| Event | Description |
|---|---|
agent:event type=userMessage | user message saved |
agent:event type=processing | executing CLI command |
agent:event type=assistantMessage | CLI output saved |
agent:event type=result | execution result stats |
agent:event type=error | error message |
cli:ptyData | PTY terminal data stream |
cli:ptyExit | PTY process exit |
Key Files
| File | Path | Description |
|---|---|---|
| CliRunnerEngine | agent-core/engine/cli/CliRunnerEngine.ts | IEngine implementation |
| cli-backends | agent-core/engine/cli/cli-backends.ts | built-in backend configs and resolution |
| cli-helpers | agent-core/engine/cli/cli-helpers.ts | args construction and output parsing |
| cli-watchdog | agent-core/engine/cli/cli-watchdog.ts | no-output timeout calculation |
| cli/index | agent-core/engine/cli/index.ts | module export |
| supervisor | process/supervisor/supervisor.ts | ProcessSupervisor implementation |
| child-adapter | process/supervisor/child-adapter.ts | child process adapter |
| pty-adapter | process/supervisor/pty-adapter.ts | PTY adapter |
| kill-tree | process/supervisor/kill-tree.ts | cross-platform process termination |
| run-registry | process/supervisor/run-registry.ts | run state registry |
| types | process/supervisor/types.ts | type definitions |
| CliEngineConfig | @shared/contracts/cli-types.ts | shared types |
All paths relative to packages/desktop/app/main/services/.
Extension Points
- New CLI backend: add new
CliBackendConfigincli-backends.ts - Custom output parsing: add new format in
parseCliOutputincli-helpers.ts - Custom timeout strategy: modify
CLI_FRESH_WATCHDOG_DEFAULTS/CLI_RESUME_WATCHDOG_DEFAULTS
Related Modules
| Module | Path | Relationship |
|---|---|---|
| EngineDispatcher | agent-core/engine/EngineDispatcher.ts | engine registration |
| ProcessSupervisor | process/supervisor/ | subprocess management |
| MagiService | agent-core/magi/MagiService.ts | high-level orchestration |