Kill switch design для AI агентів (швидко зупинити writes) + Код

Коли агент робить шкоду, потрібен kill switch, який реально зупиняє: global + per-tenant, disable tools, і stop semantics без redeploy.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) “Pause” кнопки, які нічого не паузять
  4. 2) Якщо не enforce в tool gateway — воно протече
  5. 3) “Stop the run” недостатньо
  6. 4) Scope: global vs per-tenant
  7. Приклад реалізації (реальний код)
  8. Реальний інцидент (з цифрами)
  9. Компроміси
  10. Коли НЕ варто
  11. Чекліст (можна копіювати)
  12. Безпечний дефолтний конфіг (JSON/YAML)
  13. FAQ (3–5)
  14. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/3: Execution

Action is proposed as structured data (tool + args).

Проблема (з реального продакшену)

Агент робить неправильну річ.

Не “відповідь трохи off”. Неправильну, типу:

  • відправляє дублікати листів
  • створює тікети пачками
  • hammer’ить API, доки ти не в rate limit

І найважливіше: у тебе немає часу на “пофіксити prompt і задеплоїти”.

Потрібен kill switch, який:

  • працює зараз
  • аудитується (хто, коли, чому)
  • зупиняє side effects, а не лише UI

Якщо kill switch лише у фронтенді — це не kill switch. Це плацебо. Якщо kill switch — env var, це deploy. Інциденти не чекають deploy’ів.

Чому це ламається в продакшені

1) “Pause” кнопки, які нічого не паузять

Анти-дизайн:

  • UI ховає кнопку
  • API все одно крутить loop
  • tool gateway все одно робить writes

Якщо tool calls проходять — ти не зупинив інцидент. Ти його перейменував.

2) Якщо не enforce в tool gateway — воно протече

Якщо ти перевіряєш kill switch:

  • десь у роуті
  • але не в background jobs
  • і не в tool gateway

…ти пропустиш шлях.

3) “Stop the run” недостатньо

Є in-flight tool calls:

  • довгі HTTP
  • browser sessions
  • воркери, які вже виконують

Потрібні semantics:

  • stop new runs
  • stop new tool calls
  • опційно force-cancel in-flight (best-effort)

4) Scope: global vs per-tenant

Не хочеш зупиняти весь продукт через одного tenant’а. Хочеш:

  • global (ядерна опція)
  • per-tenant (хірургічно)
  • per-tool disable list (наприклад “без браузера сьогодні”)

Приклад реалізації (реальний код)

Цей патерн:

  • читає стан зі спільного store (pseudo)
  • перевіряє у двох місцях: loop + tool gateway
  • розрізняє “stop all” і “disable writes”
PYTHON
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class KillState:
  stop_all: bool = False
  disable_writes: bool = True
  disabled_tools: set[str] = None


class Killed(RuntimeError):
  pass


def load_kill_state(*, tenant_id: str) -> KillState:
  # Pseudo: Redis/DB/feature-flag service. Must be fast + reliable.
  # Split global + per-tenant state.
  global_state = read_flag("agent_kill_global")  # (pseudo)
  tenant_state = read_flag(f"agent_kill_tenant:{tenant_id}")  # (pseudo)
  disabled_tools = set(read_list("agent_disabled_tools"))  # (pseudo)

  return KillState(
      stop_all=bool(global_state or tenant_state),
      disable_writes=True,
      disabled_tools=disabled_tools,
  )


WRITE_TOOLS = {"email.send", "db.write", "ticket.create", "ticket.close"}


def guard_tool_call(*, kill: KillState, tool: str) -> None:
  if kill.stop_all:
      raise Killed("killed: stop_all")
  if tool in (kill.disabled_tools or set()):
      raise Killed(f"killed: tool_disabled:{tool}")
  if kill.disable_writes and tool in WRITE_TOOLS:
      raise Killed(f"killed: writes_disabled:{tool}")


def run(task: str, *, tenant_id: str, tools) -> dict[str, Any]:
  kill = load_kill_state(tenant_id=tenant_id)
  for _ in range(1000):
      if kill.stop_all:
          return {"status": "stopped", "stop_reason": "killed"}

      action = llm_decide(task)  # (pseudo)
      if action.kind != "tool":
          return {"status": "ok", "answer": action.final_answer}

      guard_tool_call(kill=kill, tool=action.name)
      obs = tools.call(action.name, action.args)  # (pseudo)
      task = update(task, action, obs)  # (pseudo)

  return {"status": "stopped", "stop_reason": "max_steps"}
JAVASCRIPT
const WRITE_TOOLS = new Set(["email.send", "db.write", "ticket.create", "ticket.close"]);

export class Killed extends Error {}

export function loadKillState({ tenantId }) {
// Pseudo: feature-flag store. Must be fast + reliable.
const globalStop = readFlag("agent_kill_global"); // (pseudo)
const tenantStop = readFlag("agent_kill_tenant:" + tenantId); // (pseudo)
const disabledTools = new Set(readList("agent_disabled_tools")); // (pseudo)
return { stopAll: Boolean(globalStop || tenantStop), disableWrites: true, disabledTools };
}

export function guardToolCall({ kill, tool }) {
if (kill.stopAll) throw new Killed("killed: stop_all");
if (kill.disabledTools && kill.disabledTools.has(tool)) throw new Killed("killed: tool_disabled:" + tool);
if (kill.disableWrites && WRITE_TOOLS.has(tool)) throw new Killed("killed: writes_disabled:" + tool);
}

Реальний інцидент (з цифрами)

У нас був агент, який писав і відправляв follow-up листи. Він був за tool “send_email” і (oops) ще без approval gate.

Зміна prompt’а інтерпретувала “follow up” як “send now”.

Імпакт за 22 хвилини:

  • 117 листів відправлено (частина дублікати)
  • ~4 години damage control з клієнтами
  • модель не була “зламана” — вона просто була гучно неправильна

Kill switch, який ми думали мали, був UI toggle. Background workers його ігнорували.

Fix:

  1. enforce kill switch у tool gateway (disable writes)
  2. per-tenant stop
  3. audit logs коли kill state блокує tool call
  4. runbook: kill switch спочатку, питання потім

Компроміси

  • Kill switch знижує availability під час інциденту. Це краще, ніж незворотні writes.
  • Треба тестити kill path. Непротестований kill switch падає в найгірший момент.
  • Shared-state reads додають latency; кешуй коротко (секунди, не хвилини).

Коли НЕ варто

  • Не заміняй kill switch’ем реальну governance (permissions, approvals, budgets).
  • Не роби kill switch тільки client-side.
  • Не покладайся на kill switch як на нормальний flow. Це stop-the-bleeding.

Чекліст (можна копіювати)

  • [ ] Global kill switch (stop new runs)
  • [ ] Per-tenant kill switch (surgical stop)
  • [ ] Enforced у tool gateway (зупиняє side effects)
  • [ ] Disable writes mode (read-only degrade)
  • [ ] Tool disable list
  • [ ] Audit logs (blocks + дії оператора)
  • [ ] Тестований runbook: flip, verify, drain, recover

Безпечний дефолтний конфіг (JSON/YAML)

YAML
kill_switch:
  global_flag: "agent_kill_global"
  per_tenant_flag_prefix: "agent_kill_tenant:"
  mode_when_enabled: "disable_writes"
  disabled_tools_key: "agent_disabled_tools"
  cache_ttl_s: 2

FAQ (3–5)

Kill switch має зупиняти все чи тільки writes?
Default: disable writes. ‘Stop everything’ — ядерна опція, якщо loop взагалі не можна довіряти.
Де enforce kill switch?
У tool gateway і в run loop. Якщо він не блокує tool calls — це не kill switch.
Можна кешувати kill state?
Так, але TTL у секундах. Інциденти — це секунди, не хвилини.
Потрібен per-tenant kill switch?
Якщо ти multi-tenant — так. Інакше інцидент одного клієнта стане outage для всіх.

Q: Kill switch має зупиняти все чи тільки writes?
A: Default: disable writes. “Stop everything” — ядерна опція, якщо loop взагалі не можна довіряти.

Q: Де enforce kill switch?
A: У tool gateway і в run loop. Якщо він не блокує tool calls — це не kill switch.

Q: Можна кешувати kill state?
A: Так, але TTL у секундах. Інциденти — це секунди, не хвилини.

Q: Потрібен per-tenant kill switch?
A: Якщо ти multi-tenant — так. Інакше інцидент одного клієнта стане outage для всіх.

Пов’язані сторінки (3–6 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 6 хв читанняОновлено Бер, 2026Складність: ★★★
Реалізувати в OnceOnly
Budgets + permissions you can enforce at the boundary.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.