CliRunnerEngine
CliRunnerEngine управляет жизненным циклом подпроцессов внешних CLI-инструментов агента (например, Claude Code CLI, Codex CLI) через ProcessSupervisor. Поддерживает как обычный режим подпроцесса, так и режим 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["Сохранение в БД<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 по каналам (pipes).
Режим PTY
interface SpawnPtyInput {
mode: 'pty';
shell?: string; // по умолчанию: powershell.exe (Windows) / /bin/bash (Unix)
args?: string[];
cols?: number; // по умолчанию 120
rows?: number; // по умолчанию 30
// ...общие поля
}
Использует node-pty для создания псевдотерминала с полной интерактивностью (цвета, курсор, редактирование строки).
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;
}
Управление областями (Scope)
scopeKey группирует процессы; процессы в одной области можно отменить пакетно:
scopeKey = `cli:${backendId}:${cliSessionId}`
replaceExistingScope = true // новый процесс автоматически отменяет старые в той же области
Обработка таймаутов
Двойной механизм таймаутов:
- Общий таймаут (
timeoutMs) — ограничение общего времени выполнения процесса - Таймаут отсутствия вывода (
noOutputTimeoutMs) — ограничение времени простоя процесса
graph LR
Start["Запуск процесса"] --> Timer1["Таймер общего времени<br/>по умолчанию 300 с"]
Start --> Timer2["Таймер отсутствия вывода<br/>динамический расчёт"]
Timer1 -->|таймаут| Kill1["завершить: overall-timeout"]
Timer2 -->|таймаут| Kill2["завершить: no-output-timeout"]
Output["получен вывод"] -->|сбросить| Timer2
Расчёт таймаута отсутствия вывода (cli-watchdog):
noOutputTimeoutMs = clamp(
overallTimeoutMs * ratio,
minMs,
maxMs,
)
| Параметр | Первый запуск (Fresh) | Возобновление (Resume) |
|---|---|---|
| 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 | Использовать сырой текст напрямую |
Управление сессиями
ID CLI-сессии
CliRunnerEngine поддерживает ID сессии для CLI-инструментов с поддержкой многоходовых диалогов:
sessionMode: 'always' | 'existing' | 'none'
| Режим | Поведение |
|---|---|
always | всегда использовать ID сессии (генерировать новый, если отсутствует) |
existing | использовать только существующий ID сессии |
none | не использовать ID сессии |
ID сессии хранится в поле chatSessions.cliSessionIds (сгруппировано по backendId).
PTY-терминал
В режиме PTY данные терминала транслируются в реальном времени во все окна renderer:
| 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-бэкенд: добавить новый
CliBackendConfigвcli-backends.ts - Пользовательский разбор вывода: добавить новый формат в
parseCliOutputвcli-helpers.ts - Пользовательская стратегия таймаутов: изменить
CLI_FRESH_WATCHDOG_DEFAULTS/CLI_RESUME_WATCHDOG_DEFAULTS
Связанные модули
| Модуль | Путь | Связь |
|---|---|---|
| EngineDispatcher | agent-core/engine/EngineDispatcher.ts | регистрация движка |
| ProcessSupervisor | process/supervisor/ | управление подпроцессами |
| MagiService | agent-core/magi/MagiService.ts | высокоуровневая оркестрация |