Tutorial

How to Build Retry-Safe Agent Tool Calls

Handle API errors in AI agent tool calls gracefully. Retry logic, fallbacks, circuit breakers, and error classification.

AI agents fail silently when API calls error out, producing hallucinated results instead of real data. This tutorial builds retry-safe tool call wrappers with exponential backoff, error classification, circuit breakers, and graceful fallbacks. Your agent never hallucinates a search result again.

Prerequisites

  • Python 3.8+
  • requests library
  • A Scavio API key from scavio.dev
  • An existing agent with tool calls

Walkthrough

Step 1: Build retry wrapper with exponential backoff

Wrap API calls with automatic retry and backoff for transient errors.

Python
import os, requests, json, time
from functools import wraps

API_KEY = os.environ['SCAVIO_API_KEY']
SH = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}

def retry_with_backoff(max_retries=3, base_delay=1.0, max_delay=10.0):
    """Decorator: retry failed calls with exponential backoff."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except requests.exceptions.Timeout:
                    last_error = 'timeout'
                    delay = min(base_delay * (2 ** attempt), max_delay)
                    print(f'  Retry {attempt+1}/{max_retries} after timeout ({delay:.1f}s)')
                    time.sleep(delay)
                except requests.exceptions.HTTPError as e:
                    status = e.response.status_code if e.response else 0
                    if status == 429:  # Rate limited
                        delay = min(base_delay * (2 ** attempt), max_delay)
                        print(f'  Retry {attempt+1}/{max_retries} after 429 ({delay:.1f}s)')
                        time.sleep(delay)
                    elif status >= 500:  # Server error
                        delay = min(base_delay * (2 ** attempt), max_delay)
                        print(f'  Retry {attempt+1}/{max_retries} after {status} ({delay:.1f}s)')
                        time.sleep(delay)
                    else:
                        raise  # Don't retry 4xx errors (except 429)
                except Exception as e:
                    last_error = str(e)
                    break
            return {'error': last_error or 'max_retries_exceeded', 'results': []}
        return wrapper
    return decorator

@retry_with_backoff(max_retries=3)
def safe_search(query, **kwargs):
    resp = requests.post('https://api.scavio.dev/api/v1/search',
        headers=SH, json={'query': query, 'country_code': 'us', **kwargs}, timeout=10)
    resp.raise_for_status()
    return resp.json()

result = safe_search('python web framework 2026')
print(f'Results: {len(result.get("organic_results", []))}')

Step 2: Add circuit breaker pattern

Stop calling a failing API after too many errors to avoid wasting budget.

Python
class CircuitBreaker:
    def __init__(self, failure_threshold=5, reset_timeout=60):
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.last_failure_time = 0
        self.state = 'closed'  # closed=normal, open=blocking, half_open=testing

    def call(self, func, *args, **kwargs):
        if self.state == 'open':
            if time.time() - self.last_failure_time > self.reset_timeout:
                self.state = 'half_open'
                print(f'  Circuit half-open: testing...')
            else:
                remaining = self.reset_timeout - (time.time() - self.last_failure_time)
                return {'error': f'circuit_open (retry in {remaining:.0f}s)', 'results': []}
        try:
            result = func(*args, **kwargs)
            if self.state == 'half_open':
                self.state = 'closed'
                self.failure_count = 0
                print(f'  Circuit closed: service recovered')
            return result
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.failure_count >= self.failure_threshold:
                self.state = 'open'
                print(f'  Circuit OPEN: {self.failure_count} failures. Blocking calls for {self.reset_timeout}s.')
            return {'error': str(e), 'results': []}

search_breaker = CircuitBreaker(failure_threshold=3, reset_timeout=30)

def guarded_search(query):
    return search_breaker.call(safe_search, query)

# Test normal operation
result = guarded_search('test query')
print(f'Circuit state: {search_breaker.state}')
print(f'Failures: {search_breaker.failure_count}')

Step 3: Classify errors and provide agent-friendly messages

Give the agent clear error context so it can decide what to do next.

Python
def classify_error(error):
    """Convert raw errors into agent-friendly classifications."""
    error_str = str(error).lower()
    if 'timeout' in error_str:
        return {'type': 'transient', 'action': 'retry',
                'message': 'Search timed out. Retry with simpler query.'}
    if '429' in error_str or 'rate' in error_str:
        return {'type': 'rate_limit', 'action': 'wait',
                'message': 'Rate limited. Wait 10 seconds before next call.'}
    if '401' in error_str or '403' in error_str:
        return {'type': 'auth', 'action': 'stop',
                'message': 'API key invalid or expired. Cannot search.'}
    if '5' in error_str[:1]:
        return {'type': 'server', 'action': 'retry',
                'message': 'Server error. Retry in 5 seconds.'}
    if 'circuit' in error_str:
        return {'type': 'circuit_open', 'action': 'fallback',
                'message': 'Search API unavailable. Use cached results or skip.'}
    return {'type': 'unknown', 'action': 'log',
            'message': f'Unexpected error: {error_str[:100]}'}

def agent_safe_search(query):
    """Search with full error handling for agent use."""
    result = guarded_search(query)
    if 'error' in result and result['error']:
        classified = classify_error(result['error'])
        print(f'  Error: [{classified["type"]}] {classified["message"]}')
        print(f'  Action: {classified["action"]}')
        return {'status': 'error', **classified, 'results': []}
    results = result.get('organic_results', [])
    return {'status': 'ok', 'results': results, 'count': len(results)}

# Test
result = agent_safe_search('best ai framework 2026')
print(f'Status: {result["status"]}, Results: {result.get("count", 0)}')

Python Example

Python
import os, requests, time
SH = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}

def safe_search(query, retries=3):
    for i in range(retries):
        try:
            resp = requests.post('https://api.scavio.dev/api/v1/search',
                headers=SH, json={'query': query, 'country_code': 'us'}, timeout=10)
            resp.raise_for_status()
            return resp.json().get('organic_results', [])
        except Exception as e:
            if i < retries - 1:
                time.sleep(2 ** i)
            else:
                return []

results = safe_search('test query')
print(f'Results: {len(results)} (with retry safety)')

JavaScript Example

JavaScript
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
async function safeSearch(query, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const resp = await fetch('https://api.scavio.dev/api/v1/search', {
        method: 'POST', headers: SH,
        body: JSON.stringify({ query, country_code: 'us' })
      });
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      return (await resp.json()).organic_results || [];
    } catch (e) {
      if (i < retries - 1) await new Promise(r => setTimeout(r, 2 ** i * 1000));
    }
  }
  return [];
}
const results = await safeSearch('test query');
console.log(`Results: ${results.length}`);

Expected Output

JSON
Results: 10
Circuit state: closed
Failures: 0

Status: ok, Results: 10

# On failure:
  Error: [transient] Search timed out. Retry with simpler query.
  Action: retry

# After repeated failures:
  Circuit OPEN: 3 failures. Blocking calls for 30s.
  Error: [circuit_open] Search API unavailable. Use cached results or skip.
  Action: fallback

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Python 3.8+. requests library. A Scavio API key from scavio.dev. An existing agent with tool calls. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Handle API errors in AI agent tool calls gracefully. Retry logic, fallbacks, circuit breakers, and error classification.