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”
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"}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:
- enforce kill switch у tool gateway (disable writes)
- per-tenant stop
- audit logs коли kill state блокує tool call
- 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)
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)
Використовується в патернах
Пов’язані відмови
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 лінків)
- Foundations: What makes an agent production-ready · Why agents fail in production
- Failure: Cascading tool failures · Tool spam loops
- Governance: Budget controls · Tool permissions
- Production stack: Production agent stack