DEV Community

Preecha
Preecha

Posted on

How to Stop Babysitting AI Agents ?

TL;DR

To stop babysitting AI agents, build three layers: guardrails to prevent catastrophic failures, observability to show what happened, and checkpoints to pause for human approval when risk is high. Set them up once so agents can run for hours instead of minutes. API contracts can also act as guardrails by rejecting invalid requests before they reach your backend.

Try Apidog today

Introduction

Last week, I watched a developer spend four hours supervising an AI agent that was supposed to save time. Every few minutes, they interrupted it, fixed a mistake, and restarted the run. By the end, writing the code manually would have been faster.

This is the babysitting problem: the tools and models work, but teams never get beyond constant supervision.

Most agent setups treat an LLM like a junior developer who needs instructions at every step. A better model is an extremely fast intern that can confidently make the wrong decision unless you enforce boundaries.

If your agent calls APIs, define strict request and response schemas. A validated API contract gives the agent a map: it can only send data that matches the contract.

By the end of this guide, you will have:

  • A practical model for agent autonomy
  • Guardrail, observability, and checkpoint patterns
  • Copyable Python and TypeScript examples
  • A checklist for deciding whether an agent can run unsupervised

Why agents need constant supervision

Agents tend to fail in predictable ways. Identify these failure modes first, then add controls for each one.

Failure mode 1: Scope creep

You ask an agent to “add authentication to an API endpoint.” It adds authentication, then rate limiting, refactors the database schema, and deletes files it considers unused.

The issue is not necessarily that the agent misunderstood the first task. It kept going because it had no enforced definition of “done.”

Fix: define allowed files, allowed operations, and explicit completion criteria.

Failure mode 2: Wrong abstractions

An agent asked to “improve error handling” might add try/catch blocks everywhere. That is technically valid, but it can make code unreadable, produce inconsistent logs, and still miss important error cases.

The agent interpreted the request literally but missed the implementation standard.

Fix: provide examples, tests, linting rules, and acceptance criteria that enforce the intended pattern.

Failure mode 3: Cascading failures

A small mistake in step 1 can affect every decision that follows. A typo in a function name becomes broken imports, broken tests, and eventually a broken API.

This is dangerous because each individual action may look reasonable. The problem appears only after the accumulated changes are evaluated.

Fix: add checkpoints and validation after meaningful milestones.

Failure mode 4: Resource exhaustion

An unsupervised agent can retry failed requests indefinitely, create unlimited sub-agents, or continue generating code until it hits a billing or token limit.

Fix: enforce limits on steps, tokens, execution time, and external API calls.

The autonomy framework: guardrails, observability, and checkpoints

Use three layers:

  1. Guardrails prevent failures.
  2. Observability detects and explains failures.
  3. Checkpoints let humans recover from risky decisions.

Think of guardrails as the foundation. Logs tell you what happened above that foundation. Checkpoints are the controlled exits when automation needs a human decision.

Layer 1: Guardrails

Guardrails are constraints enforced in code. Prompts are guidance; guardrails are rules the agent cannot bypass.

Restrict file access

Do not rely on a prompt such as:

agent.run("Only modify files in the src/ directory")
Enter fullscreen mode Exit fullscreen mode

Instead, validate every file operation before it happens.

import os
from pathlib import Path

ALLOWED_DIRECTORIES = {"src", "tests", "docs"}

def validate_file_path(path: str) -> bool:
    """Return True only when the path is inside an allowed directory."""
    abs_path = Path(path).resolve()

    return any(
        str(abs_path).startswith(str(Path(directory).resolve()))
        for directory in ALLOWED_DIRECTORIES
    )

def agent_write_file(path: str, content: str) -> None:
    """Write only to approved directories."""
    if not validate_file_path(path):
        raise ValueError(
            f"Cannot write to {path}: outside allowed directories"
        )

    with open(path, "w") as file:
        file.write(content)
Enter fullscreen mode Exit fullscreen mode

Use wrapper functions like agent_write_file() for every tool exposed to your agent. Do not provide raw filesystem access if the task does not require it.

Validate API requests against schemas

When agents call APIs, invalid payloads can cause downstream failures. Validate requests before sending them.

For example, define a contract for a user-creation endpoint:

// apidog-schema.ts
export const CreateUserSchema = {
  type: "object",
  required: ["email", "name"],
  properties: {
    email: { type: "string", format: "email" },
    name: { type: "string", minLength: 1, maxLength: 100 },
    role: { type: "string", enum: ["user", "admin", "guest"] }
  },
  additionalProperties: false
};
Enter fullscreen mode Exit fullscreen mode

Validate agent-generated data before an API call:

function validateRequest(schema: object, data: unknown): void {
  const valid = ajv.validate(schema, data);

  if (!valid) {
    throw new Error(
      `Invalid request: ${JSON.stringify(ajv.errors)}`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

A schema prevents the agent from adding unexpected fields, using invalid enum values, or omitting required properties.

Enforce budgets

Every agent run needs a budget. Set limits for execution time, steps, tokens, and API calls.

import time
from dataclasses import dataclass

@dataclass
class AgentBudget:
    max_steps: int = 50
    max_tokens: int = 100_000
    max_time_seconds: int = 600
    max_api_calls: int = 100

class BudgetEnforcer:
    def __init__(self, budget: AgentBudget):
        self.budget = budget
        self.start_time = time.time()
        self.steps = 0
        self.tokens_used = 0
        self.api_calls = 0

    def check(self) -> bool:
        """Raise an error when a budget limit is exceeded."""
        elapsed = time.time() - self.start_time

        if self.steps >= self.budget.max_steps:
            raise RuntimeError(f"Step limit reached: {self.steps}")

        if self.tokens_used >= self.budget.max_tokens:
            raise RuntimeError(f"Token limit reached: {self.tokens_used}")

        if elapsed >= self.budget.max_time_seconds:
            raise RuntimeError(f"Time limit reached: {elapsed:.0f}s")

        if self.api_calls >= self.budget.max_api_calls:
            raise RuntimeError(f"API call limit reached: {self.api_calls}")

        return True

    def record_step(self, tokens: int, api_calls: int = 0) -> None:
        self.steps += 1
        self.tokens_used += tokens
        self.api_calls += api_calls
        self.check()
Enter fullscreen mode Exit fullscreen mode

Call record_step() after every model turn or tool execution. If the budget is exceeded, stop the run instead of hoping the agent eventually stops itself.

Layer 2: Observability

When an agent runs for hours, you need to understand its behavior without reading every prompt and response manually.

At minimum, capture:

  • Decisions and their reasoning
  • Tool calls and results
  • Files changed
  • API calls
  • Errors
  • Budget usage
  • Confidence scores

Add structured logs

Write machine-readable logs as the agent runs. JSON Lines (.jsonl) works well because each event is independently parseable.

import json
from datetime import datetime
from typing import Any

class AgentLogger:
    def __init__(self, log_file: str = "agent_trace.jsonl"):
        self.log_file = log_file
        self.entries = []

    def log(self, event: str, data: dict[str, Any] | None = None) -> None:
        entry = {
            "timestamp": datetime.utcnow().isoformat(),
            "event": event,
            "data": data or {}
        }

        self.entries.append(entry)

        # Persist immediately so crashes do not lose the trace.
        with open(self.log_file, "a") as file:
            file.write(json.dumps(entry) + "\n")

    def log_decision(
        self,
        decision: str,
        reasoning: str,
        confidence: float
    ) -> None:
        self.log("decision", {
            "decision": decision,
            "reasoning": reasoning,
            "confidence": confidence
        })

    def log_action(
        self,
        action: str,
        params: dict,
        result: str
    ) -> None:
        self.log("action", {
            "action": action,
            "params": params,
            "result": result[:200]
        })

    def log_error(self, error: str, context: dict) -> None:
        self.log("error", {
            "error": error,
            "context": context
        })
Enter fullscreen mode Exit fullscreen mode

Log decisions and actions at the point they occur:

logger = AgentLogger()

logger.log_decision(
    decision="Add rate limiting to API",
    reasoning="Current endpoint has no protection against abuse",
    confidence=0.85
)

logger.log_action(
    action="write_file",
    params={"path": "src/middleware/rate-limit.ts"},
    result="Successfully wrote 45 lines"
)
Enter fullscreen mode Exit fullscreen mode

Avoid logging secrets, API tokens, or full user data. Redact sensitive values before persisting logs.

Track aggregate metrics

Logs explain individual events. Metrics help you find patterns across a full run.

from collections import Counter
from dataclasses import dataclass, field

@dataclass
class AgentMetrics:
    actions_taken: Counter = field(default_factory=Counter)
    files_modified: list[str] = field(default_factory=list)
    api_calls: dict[str, int] = field(default_factory=dict)
    errors: list[str] = field(default_factory=list)
    decisions_by_confidence: dict[str, int] = field(
        default_factory=lambda: {
            "high (>0.9)": 0,
            "medium (0.7-0.9)": 0,
            "low (<0.7)": 0
        }
    )

    def record_action(self, action: str) -> None:
        self.actions_taken[action] += 1

    def record_file_modification(self, path: str) -> None:
        if path not in self.files_modified:
            self.files_modified.append(path)

    def record_api_call(self, endpoint: str) -> None:
        self.api_calls[endpoint] = self.api_calls.get(endpoint, 0) + 1

    def record_error(self, error: str) -> None:
        self.errors.append(error)

    def record_decision(self, confidence: float) -> None:
        if confidence > 0.9:
            self.decisions_by_confidence["high (>0.9)"] += 1
        elif confidence >= 0.7:
            self.decisions_by_confidence["medium (0.7-0.9)"] += 1
        else:
            self.decisions_by_confidence["low (<0.7)"] += 1

    def summary(self) -> str:
        return f"""
Agent Metrics Summary
=====================
Actions: {dict(self.actions_taken)}
Files modified: {len(self.files_modified)}
API calls: {self.api_calls}
Errors: {len(self.errors)}
Decisions by confidence: {self.decisions_by_confidence}
"""
Enter fullscreen mode Exit fullscreen mode

Review these metrics after each supervised run. Look for repeated failures, high retry counts, unexpected file modifications, or too many low-confidence decisions.

Layer 3: Checkpoints

Checkpoints are automatic pauses where an agent waits for human approval. They let you review risky actions without monitoring every low-risk operation.

Good checkpoint triggers include:

  • Before deleting files
  • Before deploying
  • Before committing or merging code
  • Before calling production APIs
  • After a fixed number of steps
  • When confidence is below a threshold
  • When a budget is nearly exhausted

Implement checkpoint handling

from dataclasses import dataclass
from enum import Enum

class CheckpointTrigger(Enum):
    BEFORE_FILE_WRITE = "before_file_write"
    BEFORE_API_CALL = "before_api_call"
    BEFORE_GIT_COMMIT = "before_git_commit"
    BEFORE_DELETE = "before_delete"
    AFTER_N_STEPS = "after_n_steps"

@dataclass
class Checkpoint:
    trigger: CheckpointTrigger
    description: str
    data: dict
    requires_approval: bool = True

class CheckpointManager:
    def __init__(
        self,
        auto_approve: set[CheckpointTrigger] | None = None
    ):
        self.auto_approve = auto_approve or set()
        self.pending: list[Checkpoint] = []

    def create_checkpoint(
        self,
        trigger: CheckpointTrigger,
        description: str,
        data: dict
    ) -> bool:
        """Return True when approved and False when execution should pause."""
        if trigger in self.auto_approve:
            return True

        checkpoint = Checkpoint(
            trigger=trigger,
            description=description,
            data=data
        )

        self.pending.append(checkpoint)

        # Replace this with notification and approval handling
        # in a production implementation.
        return False

    def approve(self, checkpoint_id: int) -> None:
        if 0 <= checkpoint_id < len(self.pending):
            self.pending.pop(checkpoint_id)

    def reject(self, checkpoint_id: int) -> None:
        raise RuntimeError(
            f"Checkpoint rejected: {self.pending[checkpoint_id]}"
        )
Enter fullscreen mode Exit fullscreen mode

Use checkpoints before destructive actions:

checkpoints = CheckpointManager(
    auto_approve={CheckpointTrigger.BEFORE_FILE_WRITE}
)

approved = checkpoints.create_checkpoint(
    trigger=CheckpointTrigger.BEFORE_DELETE,
    description="About to delete src/legacy/ directory",
    data={
        "path": "src/legacy/",
        "files": ["old_handler.ts", "deprecated.ts"]
    }
)

if not approved:
    agent.pause("Waiting for approval to delete files")
Enter fullscreen mode Exit fullscreen mode

The goal is not to approve every file write. The goal is to require approval where the cost of being wrong is high.

Build API guardrails with Apidog

API calls are a common failure point for agents. A malformed request can trigger validation errors, corrupt data, or create downstream failures.

Use an API contract workflow:

  1. Import or define your OpenAPI specification in Apidog.
  2. Generate client code with validation.
  3. Give the agent the generated client instead of raw HTTP access.
  4. Treat validation failures as agent errors that require correction or escalation.

Instead of allowing direct fetch() calls:

const response = await fetch("/api/users", {
  method: "POST",
  body: JSON.stringify(data)
});
Enter fullscreen mode Exit fullscreen mode

Give the agent a validated API client:

import { UsersApi } from "./generated/apidog-client";

const usersApi = new UsersApi();

const response = await usersApi.createUser({
  email: "user@example.com",
  name: "Test User",
  role: "user"
});
Enter fullscreen mode Exit fullscreen mode

This makes the API layer part of your guardrail system. The agent must provide data that matches the contract before the request is sent.

Proven patterns

Pattern 1: The approval sandwich

For high-risk operations, request approval both before and after execution.

def risky_operation(agent, operation):
    # Approve the intent.
    if not agent.checkpoint(f"About to: {operation.description}"):
        return "Cancelled by user"

    result = operation.execute()

    # Approve the outcome.
    if not agent.checkpoint(
        f"Verify result of: {operation.description}"
    ):
        operation.rollback()
        return "Rolled back by user"

    return result
Enter fullscreen mode Exit fullscreen mode

Use this for migrations, production changes, mass file deletion, and security-sensitive operations.

Pattern 2: Confidence thresholds

Do not let an agent execute low-confidence decisions without escalation.

MIN_CONFIDENCE = 0.75

def agent_decide(options: list[dict]) -> dict:
    best = max(options, key=lambda option: option.get("confidence", 0))

    if best["confidence"] < MIN_CONFIDENCE:
        return {
            "action": "escalate",
            "reason": (
                f"Best option has confidence "
                f"{best['confidence']:.2f} < {MIN_CONFIDENCE}"
            ),
            "options": options
        }

    return best
Enter fullscreen mode Exit fullscreen mode

Treat model-reported confidence as a signal, not a guarantee. Combine it with deterministic checks such as tests, schemas, and linting.

Pattern 3: Idempotent operations

Design actions so they can run multiple times without creating additional side effects.

import hashlib
import os

def idempotent_write(path: str, content: str) -> bool:
    """Write only when file content has changed."""
    content_hash = hashlib.sha256(content.encode()).hexdigest()

    existing_hash = None
    if os.path.exists(path):
        with open(path, "r") as file:
            existing_hash = hashlib.sha256(
                file.read().encode()
            ).hexdigest()

    if content_hash == existing_hash:
        logger.log_action(
            "write_file",
            {"path": path},
            "Skipped - no changes"
        )
        return False

    with open(path, "w") as file:
        file.write(content)

    logger.log_action(
        "write_file",
        {"path": path},
        f"Wrote {len(content)} bytes"
    )
    return True
Enter fullscreen mode Exit fullscreen mode

Idempotency makes retries safer. If an agent crashes after an action and restarts, it should not duplicate work or create inconsistent state.

Common mistakes to avoid

Trusting prompts as constraints

“Do not delete files” is an instruction. File permissions and restricted tools are constraints.

Skipping rollback plans

If an agent makes a mistake, you need a way to undo it. Use Git branches, commits, backups, or transactional operations before allowing destructive actions.

Ignoring confidence scores

Most LLMs can provide confidence estimates when prompted. Low confidence should trigger a checkpoint, a request for more context, or a human escalation.

Over-monitoring

If a human watches every action, the system is not autonomous. Start with many checkpoints, then remove them gradually for proven low-risk actions.

Under-specifying success

“Fix the bug” has no measurable finish condition. Define success criteria instead:

  • The failing test passes.
  • The full test suite passes.
  • No schema validation errors occur.
  • No files outside the allowed directories changed.

Choosing an autonomy level

Approach Autonomy Risk Best for
Manual coding None Low Complex, critical work
Pair programming with AI Low Low Learning and exploration
Supervised agents Medium Medium Routine tasks
Autonomous agents with guardrails High Controlled Bulk operations and migrations
Fully autonomous agents Very high High Trusted, well-tested workflows

For most teams, autonomous agents with guardrails is the practical target. It provides most of the time savings while keeping risk controlled.

Real-world use cases

Codebase migration

An agent migrates API endpoints from REST to GraphQL.

  • Guardrails prevent unauthorized schema changes.
  • Checkpoints require approval before deleting old endpoints.
  • Tests and API validation verify each migration batch.

Documentation generation

An agent generates API documentation from code.

  • File permissions limit it to approved source directories.
  • Structured logs record generated pages and source files.
  • A checkpoint pauses the workflow before publication.

Test coverage improvements

An agent identifies untested code and writes missing tests.

  • Budget limits prevent runaway test generation.
  • Confidence thresholds flag uncertain tests for review.
  • CI verifies that tests pass before changes are merged.

Implementation checklist

Before running an agent unsupervised, verify the following:

  • [ ] The agent can only access required files and tools.
  • [ ] File writes, deletes, and deploys are validated in code.
  • [ ] API calls use validated schemas or generated clients.
  • [ ] Step, token, time, and API-call budgets are enforced.
  • [ ] Every significant action is logged.
  • [ ] Metrics summarize actions, errors, files, and API calls.
  • [ ] Risky operations create checkpoints.
  • [ ] A rollback strategy exists for every destructive action.
  • [ ] Success conditions are explicit and testable.
  • [ ] The workflow has completed multiple supervised runs successfully.

Wrapping up

AI agents fail in predictable ways: scope creep, wrong abstractions, cascading failures, and resource exhaustion.

The practical solution is a three-layer system:

  • Guardrails prevent invalid or destructive actions.
  • Observability provides logs and metrics instead of manual watching.
  • Checkpoints put humans at high-risk decision points.
  • API schemas add an extra guardrail for agents that call backend services.

Your next steps:

  1. Pick one repetitive AI-assisted task.
  2. Define what the agent must never do.
  3. Enforce those rules in code.
  4. Add structured logs and basic metrics.
  5. Add checkpoints for destructive or expensive operations.
  6. Run the agent for 30 minutes and review the trace.
  7. Remove checkpoints only after low-risk behavior is proven.

The goal is not to remove humans from the loop. It is to put humans in the right part of the loop: approving high-level decisions instead of correcting low-level mistakes.

FAQ

What is the difference between an AI agent and an AI assistant?

An assistant responds to a request and waits for the next instruction. An agent receives a goal, plans steps, uses tools, and continues until it completes the work, hits a limit, or reaches a checkpoint.

How do I know whether an agent is ready to run autonomously?

Run it in supervised mode for 10 sessions and track every intervention. If interventions drop below two per session and only involve minor clarifications rather than corrections or rollbacks, the workflow may be ready for reduced supervision. If interventions are frequent, add more guardrails.

What is the biggest risk with autonomous agents?

Cascading failures. A small early mistake can affect every later action while the agent continues because each individual step appears reasonable. Checkpoints and milestone validation stop these cascades early.

Can I use these patterns with any LLM?

Yes. Guardrails, observability, and checkpoints are model-agnostic. The implementation may differ by provider or agent framework, but the architecture applies to Claude, GPT-4, Gemini, and other models.

How much does observability slow down an agent?

Structured logging usually adds negligible overhead. The meaningful delay comes from checkpoints that wait for human input. Use checkpoints at high-risk moments rather than after every action.

What if the agent makes a decision I disagree with?

Reject the checkpoint, roll back the operation if needed, and update the agent’s instructions, tests, examples, or constraints. Repeated disagreement usually means the task is under-specified or the guardrails do not encode your preferred implementation standard.

Should I start with supervised or autonomous agents?

Start supervised. Require checkpoints for every significant action, then gradually auto-approve low-risk actions after repeated successful runs. This creates confidence incrementally and reduces the risk of a first-run failure.

How does Apidog help with AI agents?

Apidog can generate validated API clients from defined schemas. When an agent uses those clients, malformed requests can be rejected before they reach your backend. That prevents failures caused by invalid data shapes, missing fields, or unsupported values.

Top comments (0)