Detener los bucles de los agentes de IA descontrolados: cuatro medidas de seguridad para entornos de producción
El capítulo trata sobre tokens y tiempo, verifica los objetivos mediante pruebas reales, utiliza herramientas de hash para detectar bucles de ping-pong y realiza un retroceso seguro cuando un agente autónomo se atasca.
Fuentes: Don’t Let Your AI Agents Loop Forever: An Engineering Guide to Termination Criteria
Una factura de API de 3,800 dólares generada por un agente autónomo que estuvo atascado durante doce horas en un bucle de corrección sintáctica es una experiencia que nadie debería tener que vivir. Aquí está cómo establecer reglas de detención de nivel profesional alrededor de un sistema basado en agentes.
El escenario del viernes por la tarde
Es viernes, a las 5:00 de la tarde. Un nuevo agente de programación entra en el entorno de pruebas. Su tarea es revisar las solicitudes de integración, reparar los tipos de TypeScript que fallan y volver a ejecutar las pruebas. El comando parece correcto, la computadora portátil se cierra y comienza el fin de semana.
Para la mañana del sábado, las alertas de PagerDuty y de facturación inunden el teléfono. El agente corrigió user.service.ts debido a un error de tipo y, con ello, dañó auth.service.ts. Al reparar ese archivo, se dañó nuevamente el primero, y los dos archivos siguieron anulándose mutuamente. Después de mil cuatrocientas llamadas a las herramientas, la factura por tokens asciende a 3,800 dólares y el conjunto de pruebas queda dañado.
La mayor parte de la atención en ingeniería se dirige a los prompts, la elección de herramientas y los gráficos de orquestación. En producción, la decisión con mayores consecuencias es cómo se detiene el agente, no cómo comienza.
Qué es un bucle agente
Un bucle agente es un ciclo de control en el que un LLM percibe el estado, elige una acción, llama a una herramienta, lee la salida y decide si continúa.
┌──────────────────────────────────────┐
│ Agent Perception Loop │
│ (Perceive → Plan → Act) │
└──────────────────┬───────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ 1. Success Guard │ │ 2. Resource Caps │ │ 3. Progress Guard │
│ (Programmatic Test)│ │ (Tokens/Turns/Time)│ │ (Loop/Hash Detect) │
└────────────────────┘ └────────────────────┘ └────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 4. Human Handoff / Safe Rollback │
└──────────────────────────────────────┘
Si el arnés que lo rodea carece de reglas de terminación explícitas y en capas, el agente terminará eventualmente en uno de tres modos de fallo:
- Ejecución interminable, al seguir realizando llamadas a herramientas repetidas o basadas en ilusiones.
- Salida anticipada, al detenerse y declarar victoria mientras el trabajo está a medio terminar o fallando silenciosamente.
- Agotamiento del presupuesto, al gastar toda la cantidad permitida de tokens antes de que se complete una tarea de múltiples pasos.
Existen cuatro tipos de criterios de detención que abarcan estos casos, y todo agente necesita los cuatro antes de poder ser desplegado.
1. Criterios de éxito: verificar el objetivo de manera determinista
El error común. Nunca acepte el veredicto del propio agente preguntándole al modelo de lenguaje si ha terminado. Los modelos tienden al sesgo de completitud y a halagar, y a menudo informarán de éxito cuando la construcción está defectuosa o el archivo de salida está vacío.
La solución. Trate la salida del modelo como una hipótesis no verificada, y haga que el bucle compare el estado actual con algo determinista:
- ejecute
npm testopytesten un proceso hijo y lea el código de salida - realice comprobaciones de tipo y análisis de formato con códigos de salida estrictos, como
tsc --noEmit - analice la salida mediante esquemas Zod o Pydantic
import { execSync } from 'node:child_process';
export interface VerificationResult {
success: boolean;
message: string;
stepFailed?: string;
}
/** execSync throws on non-zero exit; diagnostics may land on either stream. */
function runOrCapture(command: string, cwd: string): string | null {
try {
execSync(command, { cwd, stdio: 'pipe' });
return null;
} catch (err: unknown) {
const e = err as { stdout?: Buffer; stderr?: Buffer };
const out = e.stdout?.toString() ?? '';
const errOut = e.stderr?.toString() ?? '';
return [out, errOut].filter(Boolean).join('\n') || String(err);
}
}
export class GoalVerifier {
public static verify(workspacePath: string): VerificationResult {
const steps: Array<[string, string]> = [
['tsc', 'npx tsc --noEmit'],
['npm_test', 'npm test'],
];
for (const [stepId, command] of steps) {
const failure = runOrCapture(command, workspacePath);
if (failure !== null) {
return {
success: false,
message: `\`${command}\` failed:\n${failure}`,
stepFailed: stepId,
};
}
}
return { success: true, message: 'All typechecks and tests passed cleanly.' };
}
}
La regla general es sencilla. Si un revisor humano ejecutaría un comando para verificar una solicitud de integración, ese mismo comando debería convertirse en la condición de salida del bucle.
2. Límites de recursos y presupuesto: techo rígido
El problema. Cuando un agente se encuentra con un caso límite o un error inesperado en la herramienta, su comportamiento por defecto es intentarlo de nuevo con pequeñas variaciones en las instrucciones. Al no haber límites a nivel del motor, un bucle atascado puede consumir millones de tokens en cuestión de minutos.
La solución. Imponer techo rígido en tres dimensiones independientes: turnos, tokens y tiempo real. Contar los turnos y los tokens por separado, ya que un turno puede implicar varias llamadas al modelo y mezclarlos aumenta disimuladamente el recuento de turnos.
export interface ResourceLimits {
maxTurns: number; // e.g. 10 iterations
maxTotalTokens: number; // e.g. 100_000 input + output
timeoutMs: number; // e.g. 120_000 (2 minutes)
}
export class ResourceGuard {
private readonly startTime = Date.now();
private totalTokensUsed = 0;
private currentTurn = 0;
constructor(private readonly limits: ResourceLimits) {}
/** Call once per loop iteration, before the model call. */
public beginTurn(): void {
this.currentTurn += 1;
}
/** Call for every model call, including retries inside a turn. */
public recordUsage(tokens: number): void {
this.totalTokensUsed += tokens;
}
public getTurnCount(): number {
return this.currentTurn;
}
public checkShouldTerminate(): { terminate: boolean; reason?: string } {
if (this.currentTurn >= this.limits.maxTurns) {
return {
terminate: true,
reason: `Exceeded turn cap (${this.limits.maxTurns})`,
};
}
if (this.totalTokensUsed >= this.limits.maxTotalTokens) {
return {
terminate: true,
reason: `Exceeded token budget (${this.totalTokensUsed}/${this.limits.maxTotalTokens})`,
};
}
const elapsed = Date.now() - this.startTime;
if (elapsed >= this.limits.timeoutMs) {
return {
terminate: true,
reason: `Wall-clock timeout reached (${elapsed}ms/${this.limits.timeoutMs}ms)`,
};
}
return { terminate: false };
}
}
Los límites son una medida de seguridad, no parte del diseño. Si tu agente termina con frecuencia porque se alcanza maxTurns, el problema no está en los límites, sino en la forma en que está dividida la tarea.
3. Protectores de progreso: detectar agentes atascados o desviados
Hay tres patrones que aparecen una y otra vez:
- Ping-pong. El agente utiliza la Herramienta A, encuentra el Error X, usa la Herramienta B para solucionarlo, encuentra el Error Y y luego vuelve a usar la Herramienta A con los mismos parámetros.
- Alteraciones erráticas. Editar el archivo A daña el archivo B, y editar el archivo B daña el archivo A. El espacio de trabajo sigue cambiando mientras el progreso real se mantiene en cero.
- Desviación semántica. El agente se topa con una advertencia inesperada del analizador de código, dedica cinco turnos al formato y olvida el objetivo original.
Para detectar bloqueos antes de que se agoten los maxTurns, mantenga una ventana deslizante de hashes de las llamadas a herramientas junto con los hashes de las diferencias del espacio de trabajo. La aparición repetida de hashes idénticos indica que el agente está dando vueltas en círculos.
import { createHash } from 'node:crypto';
export interface ToolCall {
name: string;
args: Record<string, unknown>;
}
export class ProgressGuard {
private readonly recentActionHashes: string[] = [];
constructor(
private readonly windowSize = 5,
private readonly repeatThreshold = 3,
) {}
/** Stable stringify: key order must not change the hash. */
private hashToolCall(call: ToolCall): string {
const args = JSON.stringify(call.args, Object.keys(call.args).sort());
return createHash('sha256').update(`${call.name}:${args}`).digest('hex');
}
/** Returns true when the same call has appeared `repeatThreshold` times in the window. */
public trackAndCheckStuck(call: ToolCall): boolean {
const actionHash = this.hashToolCall(call);
const priorOccurrences = this.recentActionHashes.filter((h) => h === actionHash).length;
this.recentActionHashes.push(actionHash);
if (this.recentActionHashes.length > this.windowSize) {
this.recentActionHashes.shift();
}
return priorOccurrences + 1 >= this.repeatThreshold;
}
}
No toda repetición merece la misma sospecha. Una llamada de solo lectura realizada tres veces suele ser inofensiva, mientras que una operación de escritura repetida tres veces casi nunca lo es. Cuando están disponibles los metadatos de la herramienta, ajuste el umbral en consecuencia en lugar de tratar todas las llamadas de la misma manera.
4. Escalación humana y reversión segura
Cuando un bucle termina debido a un límite de presupuesto o un punto muerto, el peor resultado es un sistema parcialmente dañado. Un interruptor de circuito debe realizar tres cosas en orden: capturar datos de diagnóstico (qué herramienta falló y los últimos errores observados), devolver el espacio de trabajo a un estado conocido y escalar la situación a una persona con un resumen claro.
Tenga cuidado con el segundo paso. Ejecutar git reset --hard seguido de git clean -fd no se puede deshacer. git stash --include-untracked es más seguro, ya que mantiene el trabajo del agente disponible para inspección y aún se puede descartar posteriormente. Reserve el reset duro para entornos temporales desechables.
import { execSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
export class AgentEscalationRequiredError extends Error {
constructor(message: string, public readonly reportPath?: string) {
super(message);
this.name = 'AgentEscalationRequiredError';
}
}
export class AgentCircuitBreaker {
constructor(
private readonly workspaceDir: string,
private readonly reportDir: string, // keep reports OUTSIDE the workspace
) {}
public handleAbort(reason: string, history: unknown[] = []): never {
console.error(`[CIRCUIT BREAKER] Terminating agent loop: ${reason}`);
// 1. Park workspace changes recoverably.
try {
execSync('git stash push --include-untracked -m "agent-abort"', {
cwd: this.workspaceDir,
stdio: 'pipe',
});
} catch (err) {
console.error('git stash failed during abort; workspace left as-is:', err);
}
// 2. Write a diagnostic trace for human review.
const reportPath = this.writeFailureReport(reason, history);
// 3. Signal the orchestrator.
throw new AgentEscalationRequiredError(`Agent failed safely. Reason: ${reason}`, reportPath);
}
private writeFailureReport(reason: string, history: unknown[]): string {
const reportPath = join(this.reportDir, `agent_failure_${Date.now()}.json`);
writeFileSync(
reportPath,
JSON.stringify(
{
timestamp: new Date().toISOString(),
workspace: this.workspaceDir,
reason,
historyLength: history.length,
history: history.slice(-10),
},
null,
2,
),
'utf-8',
);
return reportPath;
}
}
Estudio de caso: un generador de aplicaciones abiertas
Una corrección de error cuenta con un conjunto de pruebas definido al que compararse. El caso más complicado es una instrucción abierta y subjetiva, por ejemplo: crear una aplicación web moderna localmente que gestione tareas, funcione de forma autónoma hasta completarlas, esté pulida y totalmente funcional, y decidir por uno mismo cuándo está lista para detenerse.
Palabras como “pulida” y “completada” son extremadamente ambiguas. Dejado a su aire, el agente o bien se detiene después de generar un simple index.html o pasa todo el tiempo ajustando los márgenes del CSS.
┌──────────────────────────────────────────────────────────┐
│ Vague prompt ("build a modern web app locally") │
└────────────────────────────┬─────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Phase 1: Dynamic spec synthesis (`ac-matrix.json`) │
└────────────────────────────┬─────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Phase 2: Multi-agent execution loop │
│ (Coder agent + design critic + headless E2E verifier) │
└────────────────────────────┬─────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Gate 1: Goal │ │ Gate 2: Resource │ │ Gate 3: Progress │
│ verification │ │ caps (turns/ │ │ guard (deadlock/ │
│ (build/E2E/ACs) │ │ token budget) │ │ repetition) │
└─────────┬────────┘ └─────────┬────────┘ └─────────┬────────┘
└───────────────────────┼───────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Gate 4: Safe exit OR circuit-breaker rollback │
└──────────────────────────────────────────────────────────┘
Sintetizando una especificación primero
En el primer turno, antes de escribir cualquier código, el agente genera una matriz explícita de criterios de aceptación y la guarda como ac-matrix.json. Agrupa los criterios en tres categorías:
- Criterios de funcionalidad, como la persistencia local en SQLite o JSON, operaciones CRUD y un botón para cambiar al modo oscuro.
- Criterios de ingeniería, como cero advertencias de TypeScript o lint y un servidor de desarrollo que se inicia sin errores en la consola.
- Criterios de diseño, como un layout responsive, tokens de diseño consistentes, estados vacíos e interacciones simples.
El mismo modelo que escribe la matriz se usará posteriormente para evaluar su propio desempeño según ella, por lo que debe considerarse como un contrato de alcance y no como un estándar objetivo. Su verdadero valor radica en que se fija temprano, antes de que el agente tenga motivos para cambiar los objetivos.
División de roles
Para evitar que un agente califique su propio trabajo como impecable, separa las tareas:
- Un subagente codificador crea las funcionalidades, gestiona las rutas y el estilo.
- Un verificador con navegador sin interfaz inicia el servidor de desarrollo, toma capturas de pantalla con Playwright y verifica errores en la consola.
- Un subagente crítico de diseño examina la interfaz de usuario según los estándares del sistema de diseño en cuanto a jerarquía, espaciado y contraste.
El marco de trabajo
El código a continuación asume la existencia de dependencias injetadas (llmAgent, executeTool, runHeadlessBrowserCheck y runDesignCriticAgent), las cuales deberías reemplazar por tu propio nivel de orquestación.
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { ResourceGuard, ProgressGuard, AgentCircuitBreaker } from './guards';
interface AcceptanceCriterion {
id: string;
description: string;
status: 'PENDING' | 'IN_PROGRESS' | 'DONE';
}
interface AcMatrix {
items: AcceptanceCriterion[];
}
export interface RunResult {
success: true;
turns: number;
summary: string;
}
export async function runOpenEndedWebAppGenerator(
userPrompt: string,
workspacePath: string,
reportDir: string,
devServerUrl = 'http://localhost:5173',
options = { maxTurns: 15, maxTotalTokens: 200_000, timeoutMs: 300_000 },
): Promise<RunResult> {
const resources = new ResourceGuard(options);
const progress = new ProgressGuard();
const circuitBreaker = new AgentCircuitBreaker(workspacePath, reportDir);
const acMatrixPath = join(workspacePath, 'ac-matrix.json');
let currentPrompt = userPrompt;
let designRetries = 0;
const maxDesignRetries = 3;
while (true) {
// GUARD 1: resource ceilings
const resourceCheck = resources.checkShouldTerminate();
if (resourceCheck.terminate) {
circuitBreaker.handleAbort(resourceCheck.reason!);
}
resources.beginTurn();
const turnResult = await llmAgent.step(currentPrompt);
resources.recordUsage(turnResult.tokensUsed);
// GUARD 2: deadlock detection (only meaningful when a tool was called)
let toolOutput = '(no tool call this turn)';
if (turnResult.toolCall) {
if (progress.trackAndCheckStuck(turnResult.toolCall)) {
circuitBreaker.handleAbort('Repeating tool call detected (stuck agent)');
}
toolOutput = await executeTool(turnResult.toolCall);
}
// GUARD 3: deterministic convergence check
const verification = await evaluateConvergence(workspacePath, acMatrixPath, devServerUrl);
if (verification.gateFailed === 'design') {
designRetries += 1;
if (designRetries > maxDesignRetries) {
circuitBreaker.handleAbort(
`Design gate never converged after ${maxDesignRetries} refinement passes`,
);
}
}
if (verification.isConverged) {
return {
success: true,
turns: resources.getTurnCount(),
summary: 'Web app built, tested, and design-reviewed cleanly.',
};
}
currentPrompt = `Tool output:\n${toolOutput}\n\nConvergence status:\n${verification.statusMessage}`;
}
}
interface ConvergenceResult {
isConverged: boolean;
statusMessage: string;
gateFailed?: 'build' | 'runtime' | 'acs' | 'design';
}
async function evaluateConvergence(
workspacePath: string,
acMatrixPath: string,
devServerUrl: string,
): Promise<ConvergenceResult> {
// Gate 1: build and typecheck
try {
execSync('npx tsc --noEmit && npm run build', { cwd: workspacePath, stdio: 'pipe' });
} catch (err: unknown) {
const e = err as { stdout?: Buffer; stderr?: Buffer };
const output = [e.stdout?.toString(), e.stderr?.toString()].filter(Boolean).join('\n');
return {
isConverged: false,
gateFailed: 'build',
statusMessage: `Gate 1 failed (build/typecheck):\n${output || String(err)}`,
};
}
// Gate 2: dev server and runtime health
const e2eResult = await runHeadlessBrowserCheck(devServerUrl);
if (!e2eResult.noConsoleErrors) {
return {
isConverged: false,
gateFailed: 'runtime',
statusMessage: `Gate 2 failed (console errors): ${e2eResult.errors.join(', ')}`,
};
}
// Gate 3: acceptance criteria fully complete
let acMatrix: AcMatrix;
try {
acMatrix = JSON.parse(readFileSync(acMatrixPath, 'utf-8')) as AcMatrix;
} catch {
return {
isConverged: false,
gateFailed: 'acs',
statusMessage: 'Gate 3 incomplete: `ac-matrix.json` missing or unparseable.',
};
}
const pending = acMatrix.items.filter((ac) => ac.status !== 'DONE');
if (pending.length > 0) {
return {
isConverged: false,
gateFailed: 'acs',
statusMessage: `Gate 3 incomplete: ${pending.length} ACs remaining (${pending
.map((a) => a.id)
.join(', ')})`,
};
}
// Gate 4: design audit (soft gate — see retry cap in the caller)
const criticVerdict = await runDesignCriticAgent(e2eResult.screenshots);
if (criticVerdict.score < 8.5) {
return {
isConverged: false,
gateFailed: 'design',
statusMessage: `Gate 4 incomplete (design ${criticVerdict.score}/10): ${criticVerdict.feedback}`,
};
}
return { isConverged: true, statusMessage: 'All four convergence gates passed.' };
}
Tenga en cuenta que las puertas de decisión no son iguales. Las dos primeras, el compilador y el navegador, son deterministas, ya que ninguno tiene opiniones. Las dos últimas son evaluadas por modelos, lo que significa que pueden oscilar hacia adelante y hacia atrás indefinidamente. Por eso, la puerta de diseño necesita su propio límite de intentos. Un umbral subjetivo sin límite en los intentos no es más que un bucle infinito más lento.
Un prompt maestro reutilizable
Para ejecutar este flujo en Claude Code, Copilot CLI, Cursor o Aider, la plantilla a continuación instruye al agente a que escriba su propia matriz de aceptación, realice iteraciones en ciclos de desarrollo, verifique los resultados localmente, revise la calidad del diseño y solo se detenga una vez que la aplicación esté terminada.
You are an autonomous lead software engineer, UX designer, and QA verifier. Your goal
is to build a production-quality web application locally, from scratch.
You must operate in a self-terminating agentic loop, running iteratively until the
application is complete, polished, functional, and verified.
================================================================================
1. TARGET APPLICATION SPECIFICATION
================================================================================
[DESCRIBE YOUR APP IDEA HERE — e.g. "A task management web app with local SQLite
persistence, a kanban board with drag-and-drop, priority tags, search/filter
controls, and a dark mode theme."]
================================================================================
2. EXECUTION PROTOCOL
================================================================================
PHASE 1 — DYNAMIC SPEC SYNTHESIS (TURN 1)
Before writing application code or installing dependencies:
1. Initialize the local project structure (e.g. Vite + React, Next.js, or Node).
2. Create `ac-matrix.json` in the workspace root defining explicit acceptance
criteria:
- Feature ACs: persistence, full CRUD, interactive components, error
handling, edge cases.
- Engineering ACs: strict TypeScript (`npx tsc --noEmit`), zero build
errors, zero linter warnings, dev server boots cleanly.
- Design ACs: visual hierarchy, responsive layout, dark/light toggle,
empty states, micro-interactions.
Format:
{
"project": "<app-name>",
"items": [
{ "id": "FEAT-1", "category": "feature", "description": "Local database persistence for tasks", "status": "PENDING" },
{ "id": "FEAT-2", "category": "feature", "description": "Drag-and-drop kanban re-ordering", "status": "PENDING" },
{ "id": "ENG-1", "category": "engineering", "description": "Clean TypeScript build, zero errors", "status": "PENDING" },
{ "id": "ENG-2", "category": "engineering", "description": "Dev server starts with 0 console errors","status": "PENDING" },
{ "id": "DSGN-1", "category": "design", "description": "Responsive UI with dark/light mode", "status": "PENDING" }
]
}
Once written, treat `ac-matrix.json` as frozen scope. Do not delete or weaken an
AC to make a gate pass. If an AC turns out to be genuinely infeasible, mark it
BLOCKED with a reason and surface it in the final summary.
PHASE 2 — AUTONOMOUS DEVELOPMENT LOOP
In each turn:
- Implement features, components, schemas, and routes incrementally.
- Run local validation after code changes (`npx tsc --noEmit`, `npm run build`).
- Update AC statuses (PENDING → IN_PROGRESS → DONE) as work is verified.
- Self-correction rule: if a command fails, read the exact error, fix the root
cause, and re-verify. Do not repeat the same failing command or edit more
than twice — change approach instead.
PHASE 3 — THE 4-GATE CONVERGENCE CHECK (MANDATORY)
Do not end execution or declare the project finished until all four gates pass
in the same turn:
Gate 1 — Compiler and build
`npx tsc --noEmit` and `npm run build` both exit 0 with zero errors.
Gate 2 — Dev server and runtime health
`npm run dev` boots cleanly with zero unhandled console or network errors.
Gate 3 — Acceptance criteria complete
Every item in `ac-matrix.json` has "status": "DONE".
Gate 4 — Design audit score >= 8.5/10
Audit visual hierarchy, color consistency, typography scale, spacing,
transitions, responsive behavior, and empty states. Score out of 10. If
below 8.5, refine and re-audit — but no more than 3 design passes total.
After 3 passes, stop and report the final score as-is.
================================================================================
3. FINAL COMPLETION OUTPUT
================================================================================
Only when all four gates pass, output:
- Final status: PROJECT COMPLETE & VERIFIED
- App summary and architecture overview
- Build and test commands executed, with exit codes
- Completed acceptance-criteria summary (including any BLOCKED items)
- Final design score (X/10) and UX highlights
- Instructions for running the app locally
Then stop.
Lista de verificación de preproducción
Antes de poner en marcha un bucle de agente, revíselo según estos criterios:
- Verificación de objetivo mediante una prueba determinística o un compilador, además de una comprobación externa del código de salida. Obligatorio.
- Límite de turnos, un límite fijo de iteraciones como
currentTurn >= maxTurns. Obligatorio. - Presupuesto de tokens, un límite acumulativo como
totalTokens >= tokenLimit. Obligatorio. - Tiempo de espera, un límite basado en el reloj como
elapsed >= timeoutMs. Obligatorio. - Protección de progreso, un detector de ventana deslizante para identificar firmas de herramientas duplicadas. Recomendado.
- Protección de estado, un hash de diferencias del espacio de trabajo que detecta cambios nulos. Recomendado.
- Solución de respaldo segura: un interruptor de circuito con capacidad de reversión mediante
git stashen caso de interrupción. Obligatorio.
Los límites antes que la inteligencia
Los agentes autónomos fiables se logran menos al hacer que el modelo sea más inteligente y más al definir límites operativos claros en su diseño. Cuatro elementos marcan la diferencia entre un experimento frágil y un sistema que se puede dejar funcionando todo el fin de semana: verificadores que no dependen del modelo, límites estrictos de recursos, hashing del progreso y mecanismos de reversión desde los cuales se puede recuperar.