Skip to content

Production Considerations

Overview

Moving LLM agents from prototypes to production systems requires careful consideration of safety, reliability, monitoring, and maintenance. Deepagents and Claude SDK take fundamentally different approaches to production readiness.

Safety & Permissions

Deepagents: Implement Your Own

Permission Model

# Custom permission system
class PermissionManager:
    def __init__(self, user_role: str):
        self.user_role = user_role
        self.allowed_operations = self.load_permissions(user_role)

    def check_permission(self, tool_name: str, args: dict) -> bool:
        """Validate tool call before execution."""
        # Custom authorization logic
        if tool_name.startswith("write_") and self.user_role != "admin":
            return False

        if tool_name == "delete_object":
            return self.user_role == "admin"

        # Site-based access control
        if "site" in args:
            return args["site"] in self.allowed_operations.get("sites", [])

        return True

# Wrap tools with permission checks
class SafeNetBoxAgent:
    def __init__(self, permission_manager: PermissionManager):
        self.permissions = permission_manager

    async def execute_tool(self, tool_name: str, args: dict):
        # Enforce permissions
        if not self.permissions.check_permission(tool_name, args):
            raise PermissionDenied(
                f"User not authorized for {tool_name} with {args}"
            )

        return await self.run_tool(tool_name, args)

Responsibility: Developer implements all safety logic

Benefits: - Custom authorization models - Fine-grained access control - Domain-specific safety rules

Costs: - Significant development effort - Maintenance burden - Security vulnerability risk

Claude SDK: Built-In Controls

Declarative Permissions

# Permission configuration (built-in)
options = ClaudeAgentOptions(
    permission_mode="acceptEdits",  # Options: deny, ask, acceptEdits
    allowed_tools=[
        "netbox_get_objects",  # Read-only tools
        "netbox_get_object_by_id",
        "netbox_search_objects"
    ],  # Whitelist approach
    system_prompt={
        "type": "preset",
        "preset": "claude_code",
        "append": (
            "CRITICAL: Never modify production data without explicit user confirmation. "
            "Always use read-only operations unless specifically instructed."
        )
    }
)

Characteristics: - Declarative configuration - SDK enforces permissions - Audit trail automatic

Benefits: - Production-ready immediately - Battle-tested security - Consistent enforcement

Trade-offs: - Less flexibility for unusual requirements - Constrained to SDK permission model

Error Handling & Reliability

Deepagents: Custom Resilience

Retry Logic

class ResilientAgent:
    async def execute_with_retry(
        self,
        tool_func: Callable,
        *args,
        max_retries: int = 3,
        backoff: str = "exponential"
    ):
        """Custom retry strategy."""
        for attempt in range(max_retries):
            try:
                return await tool_func(*args)

            except TransientError as e:
                if attempt == max_retries - 1:
                    raise

                # Custom backoff calculation
                wait_time = self.calculate_backoff(attempt, backoff)
                logger.warning(f"Attempt {attempt + 1} failed, retrying in {wait_time}s")
                await asyncio.sleep(wait_time)

            except PermanentError:
                # Don't retry permanent failures
                raise

    def calculate_backoff(self, attempt: int, strategy: str) -> float:
        """Domain-specific backoff logic."""
        if strategy == "exponential":
            return min(2 ** attempt, 60)  # Cap at 60 seconds
        elif strategy == "linear":
            return 5 * (attempt + 1)
        else:
            return 1

Circuit Breaker Pattern

class CircuitBreakerAgent:
    def __init__(self, failure_threshold: int = 5, timeout: int = 60):
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.circuit_open = False
        self.circuit_opened_at = None

    async def execute_with_circuit_breaker(self, tool_func: Callable):
        """Prevent cascading failures."""
        # Check circuit state
        if self.circuit_open:
            if time.time() - self.circuit_opened_at > self.timeout:
                # Try half-open state
                self.circuit_open = False
                self.failure_count = 0
            else:
                raise CircuitBreakerOpen("Circuit breaker is open")

        try:
            result = await tool_func()
            self.failure_count = 0  # Reset on success
            return result

        except Exception as e:
            self.failure_count += 1

            if self.failure_count >= self.failure_threshold:
                self.circuit_open = True
                self.circuit_opened_at = time.time()
                logger.error("Circuit breaker opened due to repeated failures")

            raise

Benefits: - Complete control over resilience strategy - Domain-specific error handling - Custom recovery workflows

Costs: - Must implement all patterns - Complex testing requirements - Ongoing maintenance

Claude SDK: Managed Reliability

Automatic Error Handling

# SDK handles transient failures automatically
async def query(self, message: str) -> AsyncIterator[StreamChunk]:
    """SDK manages retries and recovery."""
    try:
        await self.client.query(message)

        async for response in self.client.receive_response():
            # SDK retries transient failures internally
            yield response

    except Exception as e:
        # Only permanent failures reach application
        logger.error(f"Query failed: {e}")
        yield StreamChunk(type="error", content=str(e), completed=True)

Hook-Based Error Observation

def observe_errors(tool_name: str, error: Exception) -> dict:
    """Log errors but let SDK handle recovery."""
    logger.error(f"Tool {tool_name} failed: {error}")
    metrics.increment(f"tool_error.{tool_name}")

    # SDK continues with built-in recovery
    return {"error": str(error)}

options = ClaudeAgentOptions(
    hooks={"error": observe_errors}
)

Benefits: - Reliability out-of-box - No retry logic to maintain - Battle-tested patterns

Trade-offs: - Can't customize retry behavior - Limited control over recovery strategy

Monitoring & Observability

Deepagents: Build Custom Observability

Structured Logging

import structlog

class ObservableAgent:
    def __init__(self):
        self.logger = structlog.get_logger()

    async def execute_tool(self, tool_name: str, args: dict):
        # Structured logging with context
        log = self.logger.bind(
            tool=tool_name,
            args=args,
            user_id=self.user_id,
            session_id=self.session_id
        )

        log.info("tool_execution_started")
        start_time = time.time()

        try:
            result = await self.run_tool(tool_name, args)

            log.info(
                "tool_execution_completed",
                duration_ms=(time.time() - start_time) * 1000,
                result_size=len(json.dumps(result))
            )

            return result

        except Exception as e:
            log.error(
                "tool_execution_failed",
                error=str(e),
                error_type=type(e).__name__,
                duration_ms=(time.time() - start_time) * 1000
            )
            raise

Metrics & Tracing

from prometheus_client import Counter, Histogram, Gauge
from opentelemetry import trace

# Custom metrics
tool_calls = Counter("agent_tool_calls_total", "Total tool calls", ["tool_name", "status"])
tool_duration = Histogram("agent_tool_duration_seconds", "Tool execution time", ["tool_name"])
active_sessions = Gauge("agent_active_sessions", "Number of active sessions")

class InstrumentedAgent:
    def __init__(self):
        self.tracer = trace.get_tracer(__name__)

    async def execute_tool(self, tool_name: str, args: dict):
        """Fully instrumented tool execution."""
        # Distributed tracing
        with self.tracer.start_as_current_span("tool_execution") as span:
            span.set_attribute("tool.name", tool_name)
            span.set_attribute("tool.args", json.dumps(args))

            # Metrics
            start_time = time.time()

            try:
                result = await self.run_tool(tool_name, args)

                # Success metrics
                tool_calls.labels(tool_name=tool_name, status="success").inc()
                tool_duration.labels(tool_name=tool_name).observe(time.time() - start_time)

                return result

            except Exception as e:
                # Failure metrics
                tool_calls.labels(tool_name=tool_name, status="failure").inc()
                span.set_attribute("error", True)
                span.set_attribute("error.message", str(e))
                raise

Benefits: - Complete observability control - Custom metrics for domain - Integration with any monitoring stack

Costs: - Must build all instrumentation - Maintain monitoring infrastructure - Complex setup

Claude SDK: Hook-Based Observability

Observability via Hooks

# Centralized observability through hooks
import structlog

logger = structlog.get_logger()

def log_tool_use(tool_name: str, args: dict):
    """Pre-tool execution hook."""
    logger.info(
        "tool_called",
        tool=tool_name,
        args=args,
        timestamp=time.time()
    )
    return True  # Allow execution

def log_tool_result(tool_name: str, result: Any):
    """Post-tool execution hook."""
    logger.info(
        "tool_completed",
        tool=tool_name,
        result_size=len(str(result)),
        timestamp=time.time()
    )
    return result

def log_tool_error(tool_name: str, error: Exception):
    """Error hook."""
    logger.error(
        "tool_failed",
        tool=tool_name,
        error=str(error),
        error_type=type(error).__name__,
        timestamp=time.time()
    )
    return {"error": str(error)}

options = ClaudeAgentOptions(
    hooks={
        "pre_tool_use": log_tool_use,
        "post_tool_use": log_tool_result,
        "error": log_tool_error
    }
)

Benefits: - Simple hook implementation - SDK handles execution details - Consistent across all agents

Trade-offs: - Limited to hook interfaces - Can't instrument SDK internals - Less granular control

Deployment & Scaling

Deepagents: Deploy Like Normal Python

Containerization

# Standard Python containerization
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy agent code
COPY . .

# Run agent service
CMD ["python", "-m", "uvicorn", "agent_api:app", "--host", "0.0.0.0", "--port", "8000"]

Horizontal Scaling

# Stateless agent design enables scaling
class StatelessNetBoxAgent:
    def __init__(self, netbox_url: str, token: str):
        # No shared state between instances
        self.netbox_client = NetBoxClient(netbox_url, token)

    async def query(self, user_query: str) -> dict:
        # Each request is independent
        result = await self.process_query(user_query)
        return result

# Deploy multiple instances behind load balancer
# requests distributed across agent pool

Benefits: - Standard Python deployment - Easy horizontal scaling - No special infrastructure

Claude SDK: MCP Server Requirements

Deployment Architecture

# Requires MCP server process management
# docker-compose.yml
version: "3.8"
services:
  agent-api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - NETBOX_URL=${NETBOX_URL}
      - NETBOX_TOKEN=${NETBOX_TOKEN}
    depends_on:
      - mcp-server

  mcp-server:
    build: ./mcp-netbox
    environment:
      - NETBOX_URL=${NETBOX_URL}
      - NETBOX_TOKEN=${NETBOX_TOKEN}
    # MCP servers communicate via stdio

Considerations: - Manage multiple processes (agent + MCP servers) - stdio communication requires process orchestration - More complex deployment topology

Maintenance & Updates

Deepagents: Maintain Custom Code

Monthly Maintenance Tasks: - Update custom orchestration logic - Fix bugs in permission system - Optimize custom caching strategies - Update error handling as needed - Keep instrumentation working - Test resilience patterns

Estimated Effort: 8-12 hours/month

Claude SDK: Leverage SDK Updates

Monthly Maintenance Tasks: - Update SDK version - Review SDK changelog - Test with new SDK features - Update hook implementations if needed

Estimated Effort: 2-4 hours/month

SDK Benefits: - Security patches automatic - Performance improvements free - New features available - Breaking changes documented

Production Readiness Checklist

Deepagents

  • [ ] Permission system implemented
  • [ ] Authentication & authorization
  • [ ] Rate limiting
  • [ ] Retry logic with backoff
  • [ ] Circuit breaker pattern
  • [ ] Comprehensive error handling
  • [ ] Structured logging
  • [ ] Metrics & monitoring
  • [ ] Distributed tracing
  • [ ] Health check endpoints
  • [ ] Graceful shutdown
  • [ ] Resource cleanup
  • [ ] Unit tests (>80% coverage)
  • [ ] Integration tests
  • [ ] Load testing
  • [ ] Security audit
  • [ ] Deployment automation
  • [ ] Rollback procedures
  • [ ] Runbook documentation

Timeline: 4-6 weeks for production readiness

Claude SDK

  • [ ] Configure permission mode
  • [ ] Define allowed tools whitelist
  • [ ] Implement lifecycle hooks
  • [ ] Add error observation
  • [ ] Configure MCP servers
  • [ ] Test session management
  • [ ] Deploy MCP server infrastructure
  • [ ] Integration tests
  • [ ] Security review

Timeline: 1-2 weeks for production readiness

Cost of Ownership (TCO)

First Year Comparison

Cost Category Deepagents Claude SDK
Initial Development 4-6 weeks 1-2 weeks
Production Hardening 2-4 weeks Included
Monthly Maintenance 8-12 hours 2-4 hours
Security Updates Developer responsibility SDK managed
Bug Fixes Developer responsibility SDK managed
Feature Updates Developer effort SDK releases
Team Training Custom architecture Standard patterns
Knowledge Transfer Complex Straightforward

Break-Even Analysis

Claude SDK cheaper if: - Standard features sufficient - Team prefers managed solutions - Maintenance capacity limited

Deepagents cheaper if: - Significant customization needed - Custom features justify investment - Long-term maintenance acceptable

Recommendations

For Production Systems

Start with Claude SDK if: - Time-to-market critical - Team has limited LLM agent experience - Standard capabilities sufficient - Maintenance minimization valued

Choose Deepagents if: - Novel agent behaviors required - Custom orchestration critical - Team has infrastructure expertise - Flexibility justifies maintenance cost

Migration Strategy

Hybrid Approach: 1. Start: Claude SDK for fast deployment 2. Identify: Limitations in SDK approach 3. Extract: Critical paths needing customization 4. Implement: Custom tools as MCP servers 5. Optional: Migrate to Deepagents if SDK too constraining

Conclusion

Production considerations heavily favor Claude SDK for standard use cases: - Faster time-to-production - Lower maintenance burden - Better security posture out-of-box - Managed reliability

Choose Deepagents when customization needs justify the additional effort and ongoing maintenance costs.

See Selection Guide for final decision criteria.