Kill switch design para agentes IA (parar writes ya) + Código

Cuando tu agente hace daño, necesitas un kill switch real: global + por tenant, disable a nivel tool, y stop semantics que funcionen sin redeploy.
En esta página
  1. El problema (en producción)
  2. Por qué esto se rompe en producción
  3. 1) “Pause” buttons que no pausan nada
  4. 2) Si no se enforcea en el tool gateway, se filtra
  5. 3) “Stop the run” no alcanza
  6. 4) Scope: global vs por tenant
  7. Ejemplo de implementación (código real)
  8. Incidente real (con números)
  9. Trade-offs
  10. Cuándo NO usarlo
  11. Checklist (copiar/pegar)
  12. Config segura por defecto (JSON/YAML)
  13. FAQ (3–5)
  14. Páginas relacionadas (3–6 links)
Flujo interactivo
Escenario:
Paso 1/3: Execution

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

El problema (en producción)

Tu agente está haciendo lo incorrecto.

No “la respuesta no es perfecta”. Incorrecto tipo:

  • mandar emails duplicados
  • crear tickets en masa
  • martillar una API hasta que te rate-limit

Y ahora la parte importante: no tienes tiempo de “arreglar el prompt y redeploy”.

Necesitas un kill switch que:

  • funcione ahora
  • sea auditable (quién lo activó, cuándo, por qué)
  • pare los side effects, no solo la UI

Si tu kill switch vive en el frontend, no es kill switch. Es placebo. Si es una env var, es un deploy. Los incidentes no esperan deploys.

Por qué esto se rompe en producción

1) “Pause” buttons que no pausan nada

Anti-diseño:

  • la UI oculta el botón
  • la API sigue ejecutando el loop
  • el tool gateway sigue permitiendo writes

Si los tool calls siguen pasando, no paraste el incidente. Lo renombraste.

2) Si no se enforcea en el tool gateway, se filtra

Si checkeas kill switch:

  • en una ruta
  • pero no en background jobs
  • y no en el tool gateway

…te va a faltar un camino.

3) “Stop the run” no alcanza

Hay tool calls en vuelo:

  • HTTP largos
  • sesiones de browser
  • workers ya ejecutando

Necesitas semántica:

  • stop new runs
  • stop new tool calls
  • opcional force-cancel in-flight (best-effort)

4) Scope: global vs por tenant

No quieres apagar todo el producto porque un tenant está en loop. Quieres:

  • global (nuclear)
  • por tenant (quirúrgico)
  • disable list por tool (ej: “sin browser hoy”)

Ejemplo de implementación (código real)

Este patrón:

  • lee estado desde un store compartido (pseudo)
  • check en dos lugares: loop + tool gateway
  • distingue “stop all” vs “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);
}

Incidente real (con números)

Teníamos un agente que redactaba y enviaba emails de seguimiento. Estaba detrás de un tool “send_email” y (oops) sin approval gate aún.

Un cambio de prompt interpretó “follow up” como “send now”.

Impacto en 22 minutos:

  • 117 emails enviados (algunos duplicados)
  • ~4 horas de damage control con clientes
  • el modelo no estaba “hackeado” — estaba equivocado, ruidoso

El kill switch que creíamos tener era un toggle UI. Los workers background lo ignoraban.

Fix:

  1. kill switch enforced en tool gateway (writes disabled)
  2. stop por tenant
  3. audit logs cuando el kill state bloquea un tool call
  4. runbook: kill switch primero, preguntas después

Trade-offs

  • Un kill switch baja disponibilidad durante incidentes. Mejor que writes irreversibles.
  • Hay que testear el kill path. Un kill switch sin tests falla en el peor momento.
  • Leer estado compartido agrega latencia; mantenlo rápido y cachea corto (segundos, no minutos).

Cuándo NO usarlo

  • No uses kill switch como reemplazo de governance real (permissions, approvals, budgets).
  • No lo hagas solo client-side.
  • No dependas de kill switch para el flujo normal. Es para parar el sangrado.

Checklist (copiar/pegar)

  • [ ] Global kill switch (stop new runs)
  • [ ] Por tenant (quirúrgico)
  • [ ] Enforced en tool gateway (stop side effects)
  • [ ] Modo disable writes (read-only degrade)
  • [ ] Disable list por tool
  • [ ] Audit logs (bloqueos + acciones de operador)
  • [ ] Runbook testeado: flip, verify, drain, recover

Config segura por defecto (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)

¿El kill switch debe parar todo o solo writes?
Default: deshabilitar writes. Parar todo es nuclear cuando ya no puedes confiar en el loop.
¿Dónde enforceo el kill switch?
En el tool gateway y en el run loop. Si no bloquea tool calls, no es real.
¿Puedo cachear el estado?
Sí, pero TTL en segundos. Los incidentes se miden en segundos, no minutos.
¿Necesito kill switch por tenant?
Si eres multi-tenant: sí. Si no, el incidente de un cliente se vuelve outage para todos.

P: ¿El kill switch debe parar todo o solo writes?
R: Default: deshabilitar writes. Parar todo es nuclear cuando ya no puedes confiar en el loop.

P: ¿Dónde enforceo el kill switch?
R: En el tool gateway y en el run loop. Si no bloquea tool calls, no es real.

P: ¿Puedo cachear el estado?
R: Sí, pero TTL en segundos. Los incidentes se miden en segundos, no minutos.

P: ¿Necesito kill switch por tenant?
R: Si eres multi-tenant: sí. Si no, el incidente de un cliente se vuelve outage para todos.

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 6 min de lecturaActualizado Mar, 2026Dificultad: ★★★
Implementar en OnceOnly
Budgets + permissions you can enforce at the boundary.
Usar en 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 }
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.