37.4 Termination conditions and budgets

Overview and links for this section of the guide.

Why Budgets Matter

Without budgets, an agent can:

  • Run indefinitely, burning money
  • Get stuck in infinite loops
  • Expand scope beyond the original task
  • Consume your entire API quota

Budgets are your kill switch.

Types of Budgets

Budget Type What It Limits Typical Values
Step Budget Number of agent iterations 5-20 steps
Token Budget Total tokens consumed 50K-500K tokens
Time Budget Wall-clock execution time 30s-5min
Cost Budget Dollar amount spent $0.10-$5.00
Tool Budget Number of tool calls 10-50 calls
// budget-config.ts
interface AgentBudget {
  maxSteps: number;
  maxTokens: number;
  maxDurationMs: number;
  maxCostUsd: number;
  maxToolCalls: number;
  
  // Per-tool limits
  toolLimits?: {
    [toolName: string]: number;
  };
}

const DEFAULT_BUDGET: AgentBudget = {
  maxSteps: 15,
  maxTokens: 100000,
  maxDurationMs: 120000,  // 2 minutes
  maxCostUsd: 1.00,
  maxToolCalls: 30,
  toolLimits: {
    'write_file': 5,
    'run_command': 10,
    'search_web': 3
  }
};

Termination Conditions

An agent should stop when any of these are true:

// termination.ts
interface TerminationCheck {
  shouldTerminate: boolean;
  reason: string;
  graceful: boolean;  // Did we finish the task?
}

function checkTermination(state: AgentState, budget: AgentBudget): TerminationCheck {
  // 1. Task completed successfully
  if (state.goalAchieved) {
    return { shouldTerminate: true, reason: 'Goal achieved', graceful: true };
  }
  
  // 2. Model says it's done
  if (state.lastAction === 'finish') {
    return { shouldTerminate: true, reason: 'Agent declared completion', graceful: true };
  }
  
  // 3. Step budget exceeded
  if (state.steps >= budget.maxSteps) {
    return { shouldTerminate: true, reason: `Step limit (${budget.maxSteps}) reached`, graceful: false };
  }
  
  // 4. Token budget exceeded
  if (state.tokensUsed >= budget.maxTokens) {
    return { shouldTerminate: true, reason: `Token limit (${budget.maxTokens}) reached`, graceful: false };
  }
  
  // 5. Time budget exceeded
  const elapsed = Date.now() - state.startTime;
  if (elapsed >= budget.maxDurationMs) {
    return { shouldTerminate: true, reason: `Time limit (${budget.maxDurationMs}ms) reached`, graceful: false };
  }
  
  // 6. Loop detection
  if (detectLoop(state.history)) {
    return { shouldTerminate: true, reason: 'Loop detected', graceful: false };
  }
  
  // 7. Consecutive failures
  if (state.consecutiveFailures >= 3) {
    return { shouldTerminate: true, reason: 'Too many consecutive failures', graceful: false };
  }
  
  return { shouldTerminate: false, reason: '', graceful: false };
}

function detectLoop(history: AgentStep[]): boolean {
  if (history.length < 4) return false;
  
  const recent = history.slice(-4);
  const actions = recent.map(s => s.action?.tool).join(',');
  const prevActions = history.slice(-8, -4).map(s => s.action?.tool).join(',');
  
  return actions === prevActions;
}

Implementation

// budgeted-agent.ts
export class BudgetedAgent {
  private budget: AgentBudget;
  private state: AgentState;
  
  async run(task: string): Promise {
    this.state = {
      steps: 0,
      tokensUsed: 0,
      startTime: Date.now(),
      history: [],
      consecutiveFailures: 0,
      goalAchieved: false
    };
    
    while (true) {
      // Check termination BEFORE each step
      const termCheck = checkTermination(this.state, this.budget);
      if (termCheck.shouldTerminate) {
        return {
          success: termCheck.graceful,
          reason: termCheck.reason,
          history: this.state.history
        };
      }
      
      try {
        // Execute one step
        const step = await this.executeStep(task);
        
        // Track usage
        this.state.steps++;
        this.state.tokensUsed += step.tokensUsed;
        this.state.history.push(step);
        this.state.consecutiveFailures = 0;
        
        // Check if goal achieved
        if (step.action === 'finish') {
          this.state.goalAchieved = true;
        }
      } catch (error) {
        this.state.consecutiveFailures++;
        this.state.history.push({
          thought: 'Error occurred',
          action: null,
          observation: error.message
        });
      }
    }
  }
  
  // Real-time budget reporting
  getBudgetStatus(): BudgetStatus {
    const elapsed = Date.now() - this.state.startTime;
    return {
      steps: { used: this.state.steps, limit: this.budget.maxSteps },
      tokens: { used: this.state.tokensUsed, limit: this.budget.maxTokens },
      time: { used: elapsed, limit: this.budget.maxDurationMs },
      percentUsed: Math.max(
        this.state.steps / this.budget.maxSteps,
        this.state.tokensUsed / this.budget.maxTokens,
        elapsed / this.budget.maxDurationMs
      ) * 100
    };
  }
}
Always Fail Safe

When a budget is exceeded, terminate cleanly. Return partial results, explain what happened, and don't leave the system in a broken state.

Where to go next