Паттерн аварийного выключателя
Updated Jul 2026
Последний рубеж обороны
Даже при наличии всех пяти ограждений существуют сценарии, когда сама обвязка выходит из строя. Агент может зависнуть на системном вызове. Процесс обвязки может получить segfault. Сетевой разрыв может заморозить вызов LLM навсегда. Аварийный выключатель -- это внешняя страховочная сеть, которая убивает тестовый прогон, когда сама обвязка перестаёт отвечать.
Реализация в CI/CD
Аварийный выключатель находится вне агента и обвязки. Он настраивается на уровне CI-раннера:
GitHub Actions
- name: Run agentic tests
timeout-minutes: 15 # Hard CI-level timeout
env:
AGENT_MAX_STEPS: 30
AGENT_MAX_TOKENS: 50000
AGENT_ALLOWED_DOMAINS: "staging.myapp.com,api.staging.myapp.com"
run: |
python -m pytest tests/agentic/ \
--timeout=300 \ # Per-test timeout (pytest-timeout)
--agent-budget=$AGENT_MAX_TOKENS
Многоуровневая структура таймаутов:
Layer 1: Per-action timeout (30s)
→ Catches: hung browser, slow element lookup
→ Set by: the harness (action-level)
Layer 2: Per-test timeout (300s / 5 min)
→ Catches: stuck agent loop, slow LLM response chain
→ Set by: pytest-timeout plugin
Layer 3: CI job timeout (15 min)
→ Catches: harness hang, process deadlock, zombie processes
→ Set by: GitHub Actions timeout-minutes
Layer 4: CI workflow timeout (60 min)
→ Catches: entire workflow stuck (multiple jobs)
→ Set by: GitHub Actions workflow-level timeout
GitLab CI
agentic_tests:
timeout: 15 minutes
variables:
AGENT_MAX_STEPS: "30"
AGENT_MAX_TOKENS: "50000"
script:
- python -m pytest tests/agentic/ --timeout=300
after_script:
- python scripts/cleanup_agent_processes.py
Jenkins
pipeline {
options {
timeout(time: 15, unit: 'MINUTES')
}
stages {
stage('Agentic Tests') {
steps {
sh '''
timeout 300 python -m pytest tests/agentic/ \
--timeout=120
'''
}
post {
always {
sh 'pkill -f "vibe-check daemon" || true'
sh 'pkill -f "chromedriver" || true'
}
}
}
}
}
Очистка процессов
Когда аварийный выключатель срабатывает, он убивает процесс резко. Это может оставить зомби-процессы (экземпляры Chrome, демон-процессы, временные файлы). Всегда реализуйте очистку:
#!/usr/bin/env python3
"""cleanup_agent_processes.py — Run after agentic tests to clean up."""
import subprocess
import os
import signal
import glob
def cleanup():
# Kill any remaining Chrome processes started by tests
subprocess.run(["pkill", "-f", "chrome.*--test-type"], capture_output=True)
# Kill any vibe-check daemon processes
subprocess.run(["pkill", "-f", "vibe-check daemon"], capture_output=True)
# Kill any chromedriver processes
subprocess.run(["pkill", "-f", "chromedriver"], capture_output=True)
# Remove temporary screenshot files
for f in glob.glob("/tmp/agent_screenshot_*.png"):
os.remove(f)
# Remove stale lock files
for f in glob.glob("/tmp/agent_lock_*"):
os.remove(f)
# Remove shared state files from agent runs
for f in glob.glob("/tmp/agent_state_*.json"):
os.remove(f)
print("Agent cleanup complete")
if __name__ == "__main__":
cleanup()
Мониторинг heartbeat
Для длительных агентных сессий (ночное исследовательское тестирование) реализуйте heartbeat:
import threading
import time
import sys
class HeartbeatMonitor:
"""Kills the process if the agent stops making progress."""
def __init__(self, max_idle_seconds: int = 120):
self.max_idle = max_idle_seconds
self.last_heartbeat = time.time()
self._running = True
self._thread = threading.Thread(target=self._monitor, daemon=True)
self._thread.start()
def beat(self):
"""Call this every time the agent takes an action."""
self.last_heartbeat = time.time()
def _monitor(self):
while self._running:
idle = time.time() - self.last_heartbeat
if idle > self.max_idle:
print(f"DEAD MAN'S SWITCH: No heartbeat for {idle:.0f}s. "
f"Killing process.", file=sys.stderr)
os._exit(1) # Hard exit — bypass cleanup
time.sleep(10) # Check every 10 seconds
def stop(self):
self._running = False
# Usage in the harness
monitor = HeartbeatMonitor(max_idle_seconds=120)
class HarnessWithHeartbeat(ConstrainedTestHarness):
def execute(self, objective):
for step in range(self.config.max_steps):
monitor.beat() # Signal that we are still making progress
action = self.agent.next_action()
result = self.agent.execute_action(action)
if result.is_terminal:
monitor.stop()
return result
Полный стек безопасности
+----------------------------------------------------------+
| Layer 4: CI Workflow Timeout (60 min) |
| +------------------------------------------------------+|
| | Layer 3: CI Job Timeout (15 min) ||
| | +--------------------------------------------------+||
| | | Layer 2: Per-Test Timeout (300s) |||
| | | +----------------------------------------------+|||
| | | | Layer 1: Per-Action Timeout (30s) ||||
| | | | +------------------------------------------+||||
| | | | | Guardrails: steps, tokens, domains, |||||
| | | | | actions — enforced by the harness |||||
| | | | +------------------------------------------+||||
| | | +----------------------------------------------+|||
| | +--------------------------------------------------+||
| +------------------------------------------------------+|
+----------------------------------------------------------+
Каждый уровень ловит сбои, которые внутренний уровень пропускает:
- Ограждения ловят нарушения агентом (слишком много шагов, неправильный домен)
- Таймаут на действие ловит зависшие браузерные операции
- Таймаут на тест ловит застрявшие циклы агента
- Таймаут задачи CI ловит зависание обвязки/процесса
- Таймаут рабочего процесса CI ловит сбои на уровне инфраструктуры
Антипаттерн: отсутствие выключателя
# DANGEROUS: no timeouts at any level
- name: Run agentic tests
run: python -m pytest tests/agentic/
Эта задача может работать бесконечно. Если агент входит в бесконечный цикл и обвязка не обнаруживает это, CI-раннер заблокирован до ручного завершения. В общем CI-окружении это блокирует другие команды.
Всегда устанавливайте как минимум два уровня таймаута: один в тестовом фреймворке и один в CI-раннере.
Ключевой вывод
Паттерн аварийного выключателя гарантирует, что агентные тесты всегда завершаются, даже когда агент, обвязка и тестовый фреймворк не могут самостоятельно завершиться. Реализуйте его как многоуровневые таймауты (действие, тест, задача, рабочий процесс) плюс очистку процессов в пост-скриптах CI. Таймаут CI -- это самая внешняя страховочная сеть, единственная гарантия того, что ваш пайплайн не зависнет навсегда.