CliRunnerEngine
CliRunnerEngine은 ProcessSupervisor를 통해 외부 CLI Agent 도구(예: Claude Code CLI, Codex CLI)의 서브프로세스 생명주기를 관리합니다. 일반 서브프로세스 모드와 PTY 터미널 모드를 모두 지원합니다.
아키텍처 다이어그램
graph TB
Router["AgentRouter / MagiService"] --> Engine["CliRunnerEngine"]
Engine --> Resolve["resolveCliConfig()"]
Engine --> Backend["resolveCliBackendConfig()"]
Engine --> Args["buildCliArgs()"]
Engine --> Env["buildEnv()"]
Engine --> Supervisor["ProcessSupervisor"]
Supervisor --> Decision{"프로세스 모드?"}
Decision -->|"mode: 'child'"| ChildAdapter["ChildAdapter<br/>child_process.spawn"]
Decision -->|"mode: 'pty'"| PtyAdapter["PtyAdapter<br/>node-pty"]
ChildAdapter --> RunRegistry["RunRegistry<br/>실행 상태 추적"]
PtyAdapter --> RunRegistry
Engine --> Parse["parseCliOutput()"]
Engine --> DB["DB 영속성<br/>메시지 + 세션 ID"]
Engine --> IPC["IPC 이벤트<br/>agent:event"]
ProcessSupervisor
ProcessSupervisor는 서브프로세스 생명주기 관리자입니다.
인터페이스
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;
}
프로세스 모드
Child 모드
interface SpawnChildInput {
mode: 'child';
argv: string[]; // 명령어 및 인수
input?: string; // stdin 입력
stdinMode?: 'inherit' | 'pipe-open' | 'pipe-closed';
windowsVerbatimArguments?: boolean;
// ...공통 필드
}
child_process.spawn을 사용하여 파이프를 통한 stdin/stdout 통신으로 서브프로세스를 생성합니다.
PTY 모드
interface SpawnPtyInput {
mode: 'pty';
shell?: string; // 기본값: powershell.exe (Windows) / /bin/bash (Unix)
args?: string[];
cols?: number; // 기본값 120
rows?: number; // 기본값 30
// ...공통 필드
}
node-pty를 사용하여 완전한 터미널 상호작용(색상, 커서, 줄 편집)을 지원하는 의사 터미널(pseudoterminal)을 생성합니다.
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;
}
종료 이유
type TerminationReason =
| 'manual-cancel' // 사용자가 수동으로 취소
| 'overall-timeout' // 전체 시간 초과
| 'no-output-timeout' // 출력 없음 시간 초과
| 'spawn-error' // 프로세스 생성 실패
| 'signal' // 시그널 수신
| 'exit'; // 정상 종료
실행 상태
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;
}
스코프 관리
scopeKey는 프로세스를 그룹화합니다. 동일한 스코프 내의 프로세스는 일괄 취소할 수 있습니다.
scopeKey = `cli:${backendId}:${cliSessionId}`
replaceExistingScope = true // 새 프로세스가 동일 스코프의 기존 프로세스를 자동으로 취소
타임아웃 처리
이중 타임아웃 메커니즘:
- 전체 타임아웃 (
timeoutMs) — 프로세스 전체 실행 시간 제한 - 출력 없음 타임아웃 (
noOutputTimeoutMs) — 프로세스 유휴 시간 제한
graph LR
Start["프로세스 시작"] --> Timer1["전체 타임아웃 타이머<br/>기본값 300초"]
Start --> Timer2["출력 없음 타이머<br/>동적 계산"]
Timer1 -->|timeout| Kill1["종료: overall-timeout"]
Timer2 -->|timeout| Kill2["종료: no-output-timeout"]
Output["출력 수신"] -->|reset| Timer2
출력 없음 타임아웃 계산 (cli-watchdog):
noOutputTimeoutMs = clamp(
overallTimeoutMs * ratio,
minMs,
maxMs,
)
| 파라미터 | 신규 (처음 실행) | 재개 (이어서 실행) |
|---|---|---|
| ratio | 0.8 | 0.3 |
| minMs | 180,000 (3분) | 60,000 (1분) |
| maxMs | 600,000 (10분) | 180,000 (3분) |
크로스 플랫폼 프로세스 종료
kill-tree.ts가 플랫폼에 관계없이 프로세스 트리 종료를 처리합니다.
- Windows —
taskkill /F /T /PID(프로세스 트리 강제 종료) - Unix — 프로세스 그룹 시그널:
SIGTERM→ 대기 →SIGKILL
CLI 백엔드
내장 백엔드
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',
}
백엔드 설정 결정
function resolveCliBackendConfig(
backendId: string,
overrides?: Partial<CliBackendConfig>,
): ResolvedCliBackend | null;
backendId로 내장 설정 조회- 사용자 오버라이드 설정 적용
- 병합된 설정 반환
CLI 인수 구성
function buildCliArgs(options: {
backend: CliBackendConfig;
baseArgs: string[];
modelId?: string;
sessionId?: string;
systemPrompt?: string;
isResume: boolean;
}): string[];
순서대로 연결: baseArgs → --model → --session-id → --append-system-prompt
출력 파싱
function parseCliOutput(
stdout: string,
outputMode: 'json' | 'jsonl' | 'text',
sessionIdFields?: string[],
): { text: string; sessionId?: string; usage?: object };
| 출력 모드 | 파싱 전략 |
|---|---|
json | 전체 출력을 JSON으로 파싱하여 result 또는 text 필드 추출 |
jsonl | JSON 줄을 파싱하여 내용 연결 |
text | 원시 텍스트 그대로 사용 |
세션 관리
CLI 세션 ID
CliRunnerEngine은 다중 턴 대화를 지원하는 CLI 도구의 세션 ID를 유지합니다.
sessionMode: 'always' | 'existing' | 'none'
| 모드 | 동작 |
|---|---|
always | 항상 세션 ID 사용 (없으면 새로 생성) |
existing | 기존 세션 ID만 사용 |
none | 세션 ID 사용 안 함 |
세션 ID는 chatSessions.cliSessionIds 필드에 저장됩니다(backendId별로 그룹화).
PTY 터미널
PTY 모드에서는 터미널 데이터가 모든 렌더러 창으로 실시간 스트리밍됩니다.
| IPC 이벤트 | 페이로드 | 설명 |
|---|---|---|
cli:ptyData | { sessionId, data } | 터미널 출력 데이터 |
cli:ptyExit | { sessionId, exitCode, reason } | 터미널 프로세스 종료 |
터미널 크기 조정을 지원합니다.
resizePty(dbSessionId: string, cols: number, rows: number): boolean;
IPC 이벤트
| 이벤트 | 설명 |
|---|---|
agent:event type=userMessage | 사용자 메시지 저장됨 |
agent:event type=processing | CLI 명령 실행 중 |
agent:event type=assistantMessage | CLI 출력 저장됨 |
agent:event type=result | 실행 결과 통계 |
agent:event type=error | 오류 메시지 |
cli:ptyData | PTY 터미널 데이터 스트림 |
cli:ptyExit | PTY 프로세스 종료 |
주요 파일
| 파일 | 경로 | 설명 |
|---|---|---|
| CliRunnerEngine | agent-core/engine/cli/CliRunnerEngine.ts | IEngine 구현체 |
| cli-backends | agent-core/engine/cli/cli-backends.ts | 내장 백엔드 설정 및 결정 로직 |
| cli-helpers | agent-core/engine/cli/cli-helpers.ts | 인수 구성 및 출력 파싱 |
| cli-watchdog | agent-core/engine/cli/cli-watchdog.ts | 출력 없음 타임아웃 계산 |
| cli/index | agent-core/engine/cli/index.ts | 모듈 내보내기 |
| supervisor | process/supervisor/supervisor.ts | ProcessSupervisor 구현체 |
| child-adapter | process/supervisor/child-adapter.ts | 자식 프로세스 어댑터 |
| pty-adapter | process/supervisor/pty-adapter.ts | PTY 어댑터 |
| kill-tree | process/supervisor/kill-tree.ts | 크로스 플랫폼 프로세스 종료 |
| run-registry | process/supervisor/run-registry.ts | 실행 상태 레지스트리 |
| types | process/supervisor/types.ts | 타입 정의 |
| CliEngineConfig | @shared/contracts/cli-types.ts | 공유 타입 |
모든 경로는 packages/desktop/app/main/services/를 기준으로 합니다.
확장 포인트
- 새 CLI 백엔드:
cli-backends.ts에 새CliBackendConfig추가 - 커스텀 출력 파싱:
cli-helpers.ts의parseCliOutput에 새 형식 추가 - 커스텀 타임아웃 전략:
CLI_FRESH_WATCHDOG_DEFAULTS/CLI_RESUME_WATCHDOG_DEFAULTS수정
관련 모듈
| 모듈 | 경로 | 관계 |
|---|---|---|
| EngineDispatcher | agent-core/engine/EngineDispatcher.ts | 엔진 등록 |
| ProcessSupervisor | process/supervisor/ | 서브프로세스 관리 |
| MagiService | agent-core/magi/MagiService.ts | 고수준 오케스트레이션 |