Skip to content

04: Agent Configuration and Best Practices

Chapter 04: Agent Configuration and Best Practices

Section titled “Chapter 04: Agent Configuration and Best Practices”

A well-configured agent is the difference between a prototype and a production system. In claude-php/claude-php-agent, configuration controls everything from retry behavior and timeout limits to logging verbosity and model selection. Getting these settings right from the start saves debugging time and ensures your agents behave predictably under load.

In Chapters 01–03, you learned the fundamentals: agentic patterns, loop strategies, and tool design. Now we’ll focus on making your agents production-ready. You’ll master configuration APIs, implement robust retry logic, add structured logging, handle errors gracefully, and build agents that are observable, maintainable, and reliable.

In this chapter you’ll:

  • Master agent configuration with fluent builders and sensible defaults
  • Implement retry strategies with exponential backoff and jitter
  • Add structured logging for debugging, monitoring, and audit trails
  • Handle errors and failures gracefully with circuit breakers and fallbacks
  • Apply production best practices from day one: timeouts, rate limits, validation
  • Build observable agents that emit metrics, traces, and diagnostic information

Estimated time: ~90 minutes

::: info Framework Version This chapter is based on claude-php/claude-php-agent v0.5+. Configuration patterns are tested against the actual framework. :::

::: info Code examples Complete, runnable examples for this chapter:

All files are in code/agentic-ai-php-developers/04-agent-configuration/. :::


Every agent in claude-php/claude-php-agent is built using a fluent configuration API. Understanding the available options and their defaults helps you make informed choices.

use ClaudeAgents\Agent;
use ClaudePhp\ClaudePhp;
$client = new ClaudePhp(apiKey: getenv('ANTHROPIC_API_KEY'));
$agent = Agent::create($client)
// Model selection
->model('claude-sonnet-4-5')
// Execution limits
->maxIterations(10)
->timeout(120) // seconds
// System instructions
->withSystemPrompt('You are a helpful assistant.')
// Temperature (0.0 = deterministic, 1.0 = creative)
->temperature(0.7)
// Max tokens for response
->maxTokens(4096)
// Loop strategy
->withLoopStrategy(new ReactLoop());

Agent configuration falls into several key areas:

CategoryOptionsPurpose
ExecutionmaxIterations, timeout, maxRetriesControl loop limits and retries
Modelmodel, temperature, maxTokensLLM behavior and constraints
ToolswithTool(), withTools(), toolChoiceAvailable tool set
MemorywithMemory(), context windowState management
ObservabilityLogger, callbacks, eventsDebugging and monitoring

Choosing the right model and parameters affects performance, cost, and output quality.

The claude-php/claude-php-agent framework supports all Claude models:

// Sonnet 4.5 (default) — Best balance of performance and cost
$agent->model('claude-sonnet-4-5');
// Opus 4.5 — Highest intelligence, best for complex reasoning
$agent->model('claude-opus-4-5');
// Haiku 4.5 — Fastest and cheapest, great for simple tasks
$agent->model('claude-haiku-4-5');

::: tip Model Selection Guide

  • Sonnet 4.5: Default for most tasks, excellent balance
  • Opus 4.5: Complex multi-step reasoning, code generation, analysis
  • Haiku 4.5: Simple tool routing, classification, high-volume tasks :::

Temperature controls randomness in responses:

// Deterministic, consistent outputs (0.0 - 0.3)
$agent->temperature(0.0); // Best for tool routing, classification
// Balanced creativity (0.4 - 0.7)
$agent->temperature(0.5); // General conversational tasks
// High creativity (0.8 - 1.0)
$agent->temperature(0.9); // Creative writing, brainstorming

::: warning Production Recommendation For production agents, use temperature 0.0–0.3 to ensure consistent, predictable behavior. :::

Control response length with maxTokens:

// Short responses (chat, tool routing)
$agent->maxTokens(1024);
// Medium responses (default)
$agent->maxTokens(4096);
// Long-form content (essays, code generation)
$agent->maxTokens(8192);

Prevent runaway loops and unbounded execution with sensible limits.

Limit the number of loop iterations:

$agent->maxIterations(5); // Simple tasks (1-3 tool calls)
$agent->maxIterations(10); // Complex workflows (5-8 tool calls)
$agent->maxIterations(20); // Multi-stage planning (rare)

What happens when the limit is reached?

The agent returns the best answer it has at that point, along with metadata indicating the iteration limit was hit.

$result = $agent->run('Complex task...');
if ($result->hitIterationLimit()) {
echo "Agent reached max iterations without fully completing task\n";
// Handle partial completion
}

Set overall execution time limits:

// Quick tasks (API lookups, simple calculations)
$agent->timeout(30);
// Standard workflows
$agent->timeout(120);
// Long-running analysis
$agent->timeout(300);

::: danger Production Best Practice Always set timeouts in production to prevent hanging requests and resource exhaustion. :::


Network failures, rate limits, and transient errors are inevitable. Robust retry logic makes agents resilient.

use ClaudeAgents\Agent;
use ClaudeAgents\Retry\RetryStrategy;
$agent = Agent::create($client)
->maxRetries(3)
->retryStrategy(new ExponentialBackoff(
baseDelay: 1000, // 1 second
maxDelay: 30000, // 30 seconds
multiplier: 2.0
));

The most common retry pattern—delays double after each failure:

use ClaudeAgents\Retry\ExponentialBackoff;
// Retry 1: wait 1s
// Retry 2: wait 2s
// Retry 3: wait 4s
// Retry 4: wait 8s
$backoff = new ExponentialBackoff(
baseDelay: 1000,
maxDelay: 60000,
multiplier: 2.0
);
$agent->retryStrategy($backoff);

Jitter randomizes delays to prevent thundering herd problems:

use ClaudeAgents\Retry\ExponentialBackoffWithJitter;
// Adds ±25% randomness to each delay
$jitter = new ExponentialBackoffWithJitter(
baseDelay: 1000,
maxDelay: 30000,
multiplier: 2.0,
jitterFactor: 0.25 // ±25%
);
$agent->retryStrategy($jitter);

Example delays with jitter:

Retry 1: 750ms - 1250ms (1000ms ± 25%)
Retry 2: 1500ms - 2500ms (2000ms ± 25%)
Retry 3: 3000ms - 5000ms (4000ms ± 25%)

Only retry specific exceptions:

use ClaudeAgents\Retry\RetryPolicy;
use ClaudePhp\Exceptions\RateLimitException;
use ClaudePhp\Exceptions\ServerException;
$policy = RetryPolicy::create()
->retryOn([
RateLimitException::class,
ServerException::class,
])
->dontRetryOn([
InvalidRequestException::class, // Bad input, no point retrying
AuthenticationException::class, // Auth failed, fix config
]);
$agent->retryPolicy($policy);
<?php
use ClaudeAgents\Agent;
use ClaudeAgents\Retry\ExponentialBackoffWithJitter;
use ClaudeAgents\Retry\RetryPolicy;
use ClaudePhp\ClaudePhp;
use ClaudePhp\Exceptions\RateLimitException;
$client = new ClaudePhp(apiKey: getenv('ANTHROPIC_API_KEY'));
$agent = Agent::create($client)
->maxRetries(5)
->retryStrategy(new ExponentialBackoffWithJitter(
baseDelay: 1000,
maxDelay: 60000,
multiplier: 2.0,
jitterFactor: 0.25
))
->retryPolicy(
RetryPolicy::create()
->retryOn([RateLimitException::class])
->maxAttempts(5)
)
->onRetry(function ($attempt, $exception, $delay) {
echo sprintf(
"Retry %d after %dms due to: %s\n",
$attempt,
$delay,
$exception->getMessage()
);
});

Logs are your window into agent behavior. Structured logging makes debugging, monitoring, and auditing practical.

claude-php/claude-php-agent supports PSR-3 loggers (Monolog, etc.):

use ClaudeAgents\Agent;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
// Create logger
$logger = new Logger('agent');
$logger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG));
$logger->pushHandler(new RotatingFileHandler('/var/log/agent.log', 7, Logger::INFO));
// Attach to agent
$agent = Agent::create($client)
->withLogger($logger);

The framework logs key events at different levels:

LevelEvents
DEBUGTool input/output, loop iterations, LLM requests
INFOAgent start/stop, tool execution, task completion
WARNINGRetries, rate limits, validation errors
ERRORTool failures, API errors, unhandled exceptions
CRITICALSystem failures, circuit breaker open

Add context to every log entry:

use Monolog\Processor\UidProcessor;
use Monolog\Processor\WebProcessor;
$logger = new Logger('agent');
// Add unique request ID
$logger->pushProcessor(new UidProcessor());
// Add web context ($_SERVER vars)
$logger->pushProcessor(new WebProcessor());
// Add custom context
$logger->pushProcessor(function ($record) {
$record['extra']['environment'] = getenv('APP_ENV');
$record['extra']['service'] = 'agentic-api';
return $record;
});
$agent = Agent::create($client)->withLogger($logger);
{
"message": "Agent task completed",
"context": {
"task": "Calculate quarterly revenue",
"iterations": 4,
"tools_used": ["database_query", "calculator"],
"duration_ms": 2341
},
"level": "INFO",
"datetime": "2026-02-01T10:23:45.123456+00:00",
"extra": {
"uid": "f4a3b2c1",
"environment": "production",
"service": "agentic-api"
}
}

Hook into agent lifecycle events:

$agent = Agent::create($client)
->onToolCall(function ($tool, $input) use ($logger) {
$logger->debug('Tool called', [
'tool' => $tool->name,
'input' => $input,
]);
})
->onToolResult(function ($tool, $result) use ($logger) {
$logger->debug('Tool completed', [
'tool' => $tool->name,
'success' => $result->isSuccess(),
'execution_time_ms' => $result->executionTime(),
]);
})
->onIterationComplete(function ($iteration, $state) use ($logger) {
$logger->info('Iteration completed', [
'iteration' => $iteration,
'tokens_used' => $state->tokensUsed(),
]);
});

Robust error handling prevents cascading failures and provides clear diagnostics.

use ClaudeAgents\Exceptions\AgentException;
use ClaudeAgents\Exceptions\ToolExecutionException;
use ClaudePhp\Exceptions\ApiException;
try {
$result = $agent->run('Process user request');
if ($result->isSuccess()) {
return $result->getAnswer();
} else {
// Agent completed but didn't succeed
$logger->warning('Agent task failed', [
'reason' => $result->getError()
]);
}
} catch (ToolExecutionException $e) {
// A tool failed to execute
$logger->error('Tool execution failed', [
'tool' => $e->getToolName(),
'error' => $e->getMessage(),
]);
} catch (ApiException $e) {
// Claude API error (rate limit, network, etc.)
$logger->error('API error', [
'status_code' => $e->getStatusCode(),
'error' => $e->getMessage(),
]);
} catch (AgentException $e) {
// Generic agent framework error
$logger->error('Agent error', ['error' => $e->getMessage()]);
} catch (\Exception $e) {
// Unexpected error
$logger->critical('Unexpected error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}

Provide fallback behavior when agents fail:

function processRequest(string $userInput): string
{
try {
$result = $agent->run($userInput);
return $result->getAnswer();
} catch (RateLimitException $e) {
// Rate limited—queue for later
queueForRetry($userInput);
return "Your request is being processed. Please check back in a moment.";
} catch (ToolExecutionException $e) {
// Tool failed—try simpler approach
return fallbackSimpleResponse($userInput);
} catch (\Exception $e) {
// Unknown error—safe default
logError($e);
return "I'm having trouble processing your request. Please try again.";
}
}

Handle errors within tool handlers:

use ClaudeAgents\Tools\Tool;
use ClaudeAgents\Tools\ToolResult;
$databaseTool = Tool::create('query_database')
->description('Query the user database')
->parameter('query', 'string', 'SQL query to execute')
->required('query')
->handler(function (array $input) use ($db, $logger) {
try {
$result = $db->query($input['query']);
return ToolResult::success([
'rows' => $result->fetchAll(),
'count' => $result->rowCount(),
]);
} catch (\PDOException $e) {
$logger->error('Database query failed', [
'query' => $input['query'],
'error' => $e->getMessage(),
]);
return ToolResult::error(
'Database query failed: ' . $e->getMessage()
);
}
});

Prevent cascading failures by temporarily disabling failing components.

A circuit breaker monitors failures and “opens” (stops trying) after a threshold, giving the system time to recover.

States:

  1. Closed — Normal operation, requests flow through
  2. Open — Too many failures, all requests fail fast
  3. Half-Open — Test if the system has recovered
class CircuitBreaker
{
private int $failures = 0;
private string $state = 'closed'; // closed, open, half-open
private ?int $openedAt = null;
public function __construct(
private int $failureThreshold = 5,
private int $recoveryTimeout = 60, // seconds
) {}
public function call(callable $fn): mixed
{
if ($this->state === 'open') {
if (time() - $this->openedAt > $this->recoveryTimeout) {
$this->state = 'half-open';
} else {
throw new \Exception('Circuit breaker is open');
}
}
try {
$result = $fn();
$this->onSuccess();
return $result;
} catch (\Exception $e) {
$this->onFailure();
throw $e;
}
}
private function onSuccess(): void
{
$this->failures = 0;
$this->state = 'closed';
}
private function onFailure(): void
{
$this->failures++;
if ($this->failures >= $this->failureThreshold) {
$this->state = 'open';
$this->openedAt = time();
}
}
}
$breaker = new CircuitBreaker(
failureThreshold: 3,
recoveryTimeout: 30
);
try {
$result = $breaker->call(fn() => $agent->run($userInput));
echo $result->getAnswer();
} catch (\Exception $e) {
if ($e->getMessage() === 'Circuit breaker is open') {
// Fail fast—don't even try
return "Service temporarily unavailable";
}
throw $e;
}

Protect APIs and control costs by limiting request rates.

class RateLimiter
{
private array $requests = [];
public function __construct(
private int $maxRequests,
private int $windowSeconds,
) {}
public function allowRequest(string $key): bool
{
$now = time();
$windowStart = $now - $this->windowSeconds;
// Remove old requests
$this->requests[$key] = array_filter(
$this->requests[$key] ?? [],
fn($timestamp) => $timestamp > $windowStart
);
// Check limit
if (count($this->requests[$key]) >= $this->maxRequests) {
return false;
}
// Allow request
$this->requests[$key][] = $now;
return true;
}
}
function rateLimitedAgentRun(
Agent $agent,
string $input,
RateLimiter $limiter,
string $userId
): AgentResult {
if (!$limiter->allowRequest($userId)) {
throw new \Exception('Rate limit exceeded for user: ' . $userId);
}
return $agent->run($input);
}
// Usage
$limiter = new RateLimiter(maxRequests: 10, windowSeconds: 60);
try {
$result = rateLimitedAgentRun($agent, $userInput, $limiter, $userId);
echo $result->getAnswer();
} catch (\Exception $e) {
echo "Rate limit exceeded. Please try again in a moment.";
}

Putting it all together—a fully configured production agent:

<?php
use ClaudeAgents\Agent;
use ClaudeAgents\Loops\ReactLoop;
use ClaudeAgents\Retry\ExponentialBackoffWithJitter;
use ClaudeAgents\Retry\RetryPolicy;
use ClaudePhp\ClaudePhp;
use ClaudePhp\Exceptions\RateLimitException;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Processor\UidProcessor;
// Configure logger
$logger = new Logger('agent');
$logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
$logger->pushHandler(new RotatingFileHandler(
filename: '/var/log/agent.log',
maxFiles: 30,
level: Logger::DEBUG
));
$logger->pushProcessor(new UidProcessor());
// Create Claude client
$client = new ClaudePhp(
apiKey: getenv('ANTHROPIC_API_KEY'),
timeout: 120
);
// Configure agent
$agent = Agent::create($client)
// Model and parameters
->model('claude-sonnet-4-5')
->temperature(0.1) // Low temperature for consistency
->maxTokens(4096)
// Execution limits
->maxIterations(10)
->timeout(120)
// Retry strategy
->maxRetries(5)
->retryStrategy(new ExponentialBackoffWithJitter(
baseDelay: 1000,
maxDelay: 60000,
multiplier: 2.0,
jitterFactor: 0.25
))
->retryPolicy(
RetryPolicy::create()
->retryOn([RateLimitException::class])
->maxAttempts(5)
)
// Logging
->withLogger($logger)
// Loop strategy
->withLoopStrategy(new ReactLoop())
// System prompt
->withSystemPrompt(
'You are a helpful assistant. Be concise and accurate. ' .
'Use tools when needed. Always cite sources.'
)
// Lifecycle callbacks
->onToolCall(function ($tool, $input) use ($logger) {
$logger->info('Tool called', [
'tool' => $tool->name,
'input_size' => strlen(json_encode($input)),
]);
})
->onIterationComplete(function ($iteration, $state) use ($logger) {
$logger->debug('Iteration completed', [
'iteration' => $iteration,
'tokens_used' => $state->tokensUsed(),
]);
});
// Error handling wrapper
function runAgentSafely(Agent $agent, string $input): string
{
global $logger;
try {
$result = $agent->run($input);
if ($result->isSuccess()) {
$logger->info('Agent task completed successfully', [
'iterations' => $result->iterations(),
'tokens' => $result->tokensUsed(),
]);
return $result->getAnswer();
} else {
$logger->warning('Agent task failed', [
'error' => $result->getError()
]);
return "I couldn't complete that task: " . $result->getError();
}
} catch (\Exception $e) {
$logger->error('Agent execution failed', [
'error' => $e->getMessage(),
'class' => get_class($e),
]);
return "An error occurred. Please try again.";
}
}
// Usage
$response = runAgentSafely($agent, 'What is the weather in San Francisco?');
echo $response . "\n";

Adjust settings based on environment:

$isProduction = getenv('APP_ENV') === 'production';
$agent = Agent::create($client)
->temperature($isProduction ? 0.1 : 0.5)
->maxIterations($isProduction ? 10 : 20)
->timeout($isProduction ? 120 : 300)
->withLogger($isProduction ? $prodLogger : $devLogger);

Store configuration in environment variables or config files:

config/agent.php
return [
'model' => getenv('AGENT_MODEL') ?: 'claude-sonnet-4-5',
'temperature' => (float) getenv('AGENT_TEMPERATURE') ?: 0.1,
'max_iterations' => (int) getenv('AGENT_MAX_ITERATIONS') ?: 10,
'timeout' => (int) getenv('AGENT_TIMEOUT') ?: 120,
'max_retries' => (int) getenv('AGENT_MAX_RETRIES') ?: 5,
];
// Usage
$config = require 'config/agent.php';
$agent = Agent::create($client)
->model($config['model'])
->temperature($config['temperature'])
->maxIterations($config['max_iterations'])
->timeout($config['timeout'])
->maxRetries($config['max_retries']);

Validate configuration before creating agents:

function validateAgentConfig(array $config): void
{
assert($config['temperature'] >= 0.0 && $config['temperature'] <= 1.0, 'Invalid temperature');
assert($config['max_iterations'] > 0, 'Max iterations must be positive');
assert($config['timeout'] > 0, 'Timeout must be positive');
assert(in_array($config['model'], [
'claude-sonnet-4-5',
'claude-opus-4-5',
'claude-haiku-4-5',
]), 'Invalid model');
}
$config = require 'config/agent.php';
validateAgentConfig($config);

Track agent performance and health in production.

MetricWhat it MeasuresAlert Threshold
LatencyTime to complete tasksp95 > 10s
Error RateFailed tasks / total tasks> 5%
Token UsageTokens per request> 8000
Iteration CountLoops per task> 15
Retry RateRetries / total requests> 10%
Tool FailuresTool errors / tool calls> 2%
class MetricsCollector
{
private array $metrics = [];
public function record(string $name, float $value, array $tags = []): void
{
$this->metrics[] = [
'name' => $name,
'value' => $value,
'tags' => $tags,
'timestamp' => microtime(true),
];
}
public function timing(string $name, callable $fn, array $tags = []): mixed
{
$start = microtime(true);
$result = $fn();
$duration = (microtime(true) - $start) * 1000; // ms
$this->record($name, $duration, $tags);
return $result;
}
public function flush(): void
{
// Send to monitoring system (Prometheus, DataDog, etc.)
foreach ($this->metrics as $metric) {
// Implementation depends on your monitoring stack
}
$this->metrics = [];
}
}
function monitoredAgentRun(
Agent $agent,
string $input,
MetricsCollector $metrics
): AgentResult {
return $metrics->timing('agent.run', function () use ($agent, $input, $metrics) {
try {
$result = $agent->run($input);
// Record success metrics
$metrics->record('agent.success', 1);
$metrics->record('agent.iterations', $result->iterations());
$metrics->record('agent.tokens', $result->tokensUsed());
return $result;
} catch (\Exception $e) {
// Record error metrics
$metrics->record('agent.error', 1, [
'error_type' => get_class($e)
]);
throw $e;
}
}, ['model' => $agent->getModel()]);
}

Validate your configuration with automated tests.

use PHPUnit\Framework\TestCase;
class AgentConfigurationTest extends TestCase
{
public function testProductionConfiguration(): void
{
$config = require __DIR__ . '/../config/agent.php';
// Validate temperature
$this->assertGreaterThanOrEqual(0.0, $config['temperature']);
$this->assertLessThanOrEqual(1.0, $config['temperature']);
// Validate limits
$this->assertGreaterThan(0, $config['max_iterations']);
$this->assertLessThanOrEqual(20, $config['max_iterations']);
// Validate timeout
$this->assertGreaterThan(0, $config['timeout']);
$this->assertLessThanOrEqual(300, $config['timeout']);
}
public function testAgentCreationWithConfig(): void
{
$config = require __DIR__ . '/../config/agent.php';
$client = new ClaudePhp(apiKey: 'test-key');
$agent = Agent::create($client)
->model($config['model'])
->temperature($config['temperature'])
->maxIterations($config['max_iterations']);
$this->assertInstanceOf(Agent::class, $agent);
}
}

In this chapter, you learned how to configure agents for production:

Configuration fundamentals — Model selection, execution limits, temperature
Retry strategies — Exponential backoff, jitter, selective retries
Structured logging — PSR-3 integration, context, lifecycle callbacks
Error handling — Try-catch patterns, graceful degradation, tool errors
Circuit breakers — Preventing cascading failures
Rate limiting — Protecting APIs and controlling costs
Production templates — Complete agent setup with all best practices
Monitoring — Metrics, observability, performance tracking

With these patterns, your agents are ready for production workloads.


Create a production-ready agent with:

  • Model: claude-sonnet-4-5
  • Temperature: 0.1
  • Max iterations: 10
  • Timeout: 120s
  • Retry strategy with exponential backoff
  • Structured logging with Monolog
  • Error handling with graceful fallbacks

Starter code:

// Your implementation here
$agent = Agent::create($client)
// Add configuration...
;

Build a rate limiter that allows 10 requests per minute per user.

Requirements:

  • Track requests by user ID
  • Return false when limit exceeded
  • Clean up old request timestamps

Wrap agent execution with a circuit breaker:

  • Open after 3 consecutive failures
  • Recovery timeout: 30 seconds
  • Test with a half-open state

Track these metrics:

  • Average response time
  • Error rate
  • Token usage per request
  • Iteration count distribution

Now that your agents are production-ready, it’s time to build the core primitives. In Chapter 05: Tool Routing and Execution Pipelines, you’ll create a tool router that safely dispatches tool calls, logs executions, standardizes errors, and implements retries.

Continue to Chapter 05