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”
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);
}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:
- kill switch enforced en tool gateway (writes disabled)
- stop por tenant
- audit logs cuando el kill state bloquea un tool call
- 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)
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)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
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.
Páginas relacionadas (3–6 links)
- 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