Skip to content

Advanced Function Calling ​

Function calling unlocks AI agents that don't just respond with text but take real actions in your application. This guide explores advanced patterns, error handling strategies, and complex orchestration techniques for building production-ready function calling systems.

Overview ​

What You'll Learn ​

This guide covers advanced function calling patterns beyond basic tool usage:

  • Multi-turn Conversations - Chain multiple function calls together
  • Parallel Execution - Execute multiple tools simultaneously
  • Error Handling - Gracefully handle failures and retry strategies
  • Conditional Logic - Dynamic tool selection based on context
  • State Management - Maintain state across function calls
  • Agent Orchestration - Build complex multi-step workflows
  • Security Patterns - Safely execute untrusted function calls
  • Performance Optimization - Cache, batch, and optimize tool execution

Prerequisites ​

This guide assumes you understand:

Multi-Turn Function Calling ​

Most real-world tasks require multiple function calls. The LLM calls functions, processes results, and decides what to do next.

Basic Multi-Turn Pattern ​

php
use Mindwave\Mindwave\Facades\LLM;
use Mindwave\Mindwave\LLM\FunctionCalling\FunctionBuilder;
use Mindwave\Mindwave\LLM\FunctionCalling\FunctionCall;

$functions = FunctionBuilder::make()
    ->addFunction(
        name: 'get_user',
        description: 'Retrieves user by email',
        closure: fn(string $email) => User::where('email', $email)->first()?->toJson() ?? 'User not found'
    )
    ->addFunction(
        name: 'get_orders',
        description: 'Retrieves orders for a user ID',
        closure: fn(int $userId) => Order::where('user_id', $userId)->get()->toJson()
    )
    ->addFunction(
        name: 'calculate_total',
        description: 'Calculates total amount from orders JSON',
        closure: function(string $ordersJson) {
            $orders = json_decode($ordersJson, true);
            return array_sum(array_column($orders, 'total'));
        }
    );

// Multi-turn execution
$messages = [
    ['role' => 'user', 'content' => 'What is the total order value for user john@example.com?']
];

$maxTurns = 10;
$turn = 0;

while ($turn < $maxTurns) {
    $response = LLM::driver('openai')->functionCall(
        json_encode($messages),
        $functions
    );

    // Check if we got a final text response
    if (is_string($response)) {
        echo "Final answer: {$response}";
        break;
    }

    // Execute the function
    if ($response instanceof FunctionCall) {
        Log::info("Turn {$turn}: Calling {$response->name}", $response->arguments);

        // Execute based on function name
        $result = match($response->name) {
            'get_user' => (fn() => User::where('email', $response->arguments['email'])->first()?->toJson() ?? 'User not found')(),
            'get_orders' => (fn() => Order::where('user_id', $response->arguments['userId'])->get()->toJson())(),
            'calculate_total' => (fn() => json_decode($response->arguments['ordersJson'], true) |> array_sum(array_column($_, 'total')))(),
            default => json_encode(['error' => 'Unknown function'])
        };

        // Add function call and result to conversation history
        $messages[] = [
            'role' => 'assistant',
            'function_call' => [
                'name' => $response->name,
                'arguments' => $response->rawArguments
            ]
        ];
        $messages[] = [
            'role' => 'function',
            'name' => $response->name,
            'content' => $result
        ];
    }

    $turn++;
}

if ($turn >= $maxTurns) {
    Log::warning('Max turns reached without completion');
}

How it works:

mermaid
sequenceDiagram
    participant User
    participant LLM
    participant get_user
    participant get_orders
    participant calculate_total

    User->>LLM: What is the total order value<br/>for john@example.com?

    Note over LLM: Turn 1: Need user data
    LLM->>get_user: get_user('john@example.com')
    get_user-->>LLM: {id: 123, name: "John", ...}

    Note over LLM: Turn 2: Now get orders
    LLM->>get_orders: get_orders(userId: 123)
    get_orders-->>LLM: [{order1}, {order2}, ...]

    Note over LLM: Turn 3: Calculate total
    LLM->>calculate_total: calculate_total(ordersJson)
    calculate_total-->>LLM: 1247.50

    Note over LLM: Turn 4: Format response
    LLM-->>User: John has spent $1,247.50<br/>across 3 orders

Process:

  1. User asks a complex question requiring multiple steps
  2. LLM calls get_user('john@example.com') → Gets user ID
  3. LLM calls get_orders(userId: 123) → Gets order data
  4. LLM calls calculate_total(ordersJson) → Computes total
  5. LLM returns final formatted answer to user

Conversation State Management ​

Track conversation state for complex interactions:

php
class ConversationManager
{
    protected array $messages = [];
    protected array $executedFunctions = [];
    protected int $totalCost = 0;

    public function __construct(
        protected FunctionBuilder $functions,
        protected string $driver = 'openai'
    ) {}

    public function execute(string $userMessage, int $maxTurns = 10): string
    {
        $this->messages[] = ['role' => 'user', 'content' => $userMessage];

        for ($turn = 0; $turn < $maxTurns; $turn++) {
            $response = LLM::driver($this->driver)
                ->functionCall(json_encode($this->messages), $this->functions);

            // Track costs (if tracing enabled)
            $this->totalCost += $this->estimateTokenCost($response);

            if (is_string($response)) {
                return $response; // Final answer
            }

            if ($response instanceof FunctionCall) {
                $result = $this->executeFunction($response);

                $this->executedFunctions[] = [
                    'name' => $response->name,
                    'arguments' => $response->arguments,
                    'result' => $result,
                    'turn' => $turn,
                ];

                $this->messages[] = [
                    'role' => 'assistant',
                    'function_call' => [
                        'name' => $response->name,
                        'arguments' => $response->rawArguments,
                    ],
                ];

                $this->messages[] = [
                    'role' => 'function',
                    'name' => $response->name,
                    'content' => $result,
                ];
            }
        }

        throw new \RuntimeException('Maximum turns exceeded without completion');
    }

    protected function executeFunction(FunctionCall $call): string
    {
        // Execute function through function builder
        $functions = $this->functions->build();

        foreach ($functions as $func) {
            if ($func['name'] === $call->name) {
                return ($func['function'])(...array_values($call->arguments));
            }
        }

        return json_encode(['error' => 'Function not found']);
    }

    public function getExecutionTrace(): array
    {
        return [
            'turns' => count($this->executedFunctions),
            'functions_called' => array_column($this->executedFunctions, 'name'),
            'estimated_cost' => $this->totalCost,
            'full_trace' => $this->executedFunctions,
        ];
    }

    protected function estimateTokenCost($response): int
    {
        // Implement token cost estimation
        return 0; // Placeholder
    }
}

// Usage
$manager = new ConversationManager($functions);
$answer = $manager->execute('What are the top 3 orders for john@example.com?');

echo $answer;
print_r($manager->getExecutionTrace());

Parallel Function Execution ​

Some LLMs can request multiple function calls simultaneously. Handle them efficiently:

Detecting Parallel Calls ​

php
$response = LLM::driver('openai')->functionCall($prompt, $functions);

// OpenAI may return multiple tool calls
if (is_array($response)) {
    // Parallel execution requested
    $results = [];

    foreach ($response as $call) {
        if ($call instanceof FunctionCall) {
            $results[] = $this->executeFunction($call);
        }
    }

    // Return all results to LLM
    return $results;
}

Concurrent Execution with Laravel ​

Execute multiple functions concurrently for better performance:

php
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\Arr;

class ParallelFunctionExecutor
{
    public function executeConcurrent(array $functionCalls): array
    {
        // Group by execution speed (fast vs slow)
        $fast = [];
        $slow = [];

        foreach ($functionCalls as $call) {
            if ($this->isFastFunction($call->name)) {
                $fast[] = $call;
            } else {
                $slow[] = $call;
            }
        }

        // Execute fast functions synchronously
        $fastResults = array_map(fn($call) => $this->execute($call), $fast);

        // Execute slow functions concurrently using queues
        $slowResults = $this->executeConcurrentlyWithQueues($slow);

        return array_merge($fastResults, $slowResults);
    }

    protected function executeConcurrentlyWithQueues(array $calls): array
    {
        if (empty($calls)) {
            return [];
        }

        $jobIds = [];

        foreach ($calls as $call) {
            $jobId = Str::uuid()->toString();
            $jobIds[] = $jobId;

            ExecuteFunctionJob::dispatch($call, $jobId);
        }

        // Poll for results (or use Redis pub/sub)
        return $this->waitForResults($jobIds);
    }

    protected function isFastFunction(string $name): bool
    {
        // Define which functions are fast (in-memory operations)
        return in_array($name, ['calculate_total', 'format_date', 'validate_input']);
    }
}

Promise-Based Parallel Execution ​

For truly parallel execution within a single request:

php
use React\Promise\Promise;
use React\Promise\PromiseInterface;

class PromiseBasedExecutor
{
    public function executeParallel(array $functionCalls): array
    {
        $promises = [];

        foreach ($functionCalls as $call) {
            $promises[] = $this->executeAsync($call);
        }

        // Wait for all promises to resolve
        return \React\Promise\all($promises)->then(function ($results) {
            return $results;
        })->wait();
    }

    protected function executeAsync(FunctionCall $call): PromiseInterface
    {
        return new Promise(function ($resolve, $reject) use ($call) {
            try {
                $result = $this->execute($call);
                $resolve($result);
            } catch (\Exception $e) {
                $reject($e);
            }
        });
    }
}

Error Handling and Recovery ​

Production systems need robust error handling for function calls.

Function Call Parsing Errors ​

When the LLM returns malformed JSON in function call arguments, Mindwave throws a MindwaveParseException instead of silently returning null:

php
use Mindwave\Mindwave\Exceptions\MindwaveParseException;

try {
    $result = Mindwave::llm()
        ->model('gpt-4o')
        ->functionCall($prompt, $functions);

    if ($result instanceof FunctionCall) {
        // Arguments are guaranteed to be valid parsed JSON
        $output = $result->arguments;
    }

} catch (MindwaveParseException $e) {
    // LLM returned invalid JSON in function arguments
    Log::warning('Function call argument parsing failed', [
        'raw_text' => $e->getRawText(),
        'error' => $e->getMessage(),
    ]);
}

Graceful Error Handling ​

php
class ResilientFunctionExecutor
{
    public function execute(FunctionCall $call): string
    {
        try {
            // Validate arguments before execution
            $this->validateArguments($call);

            // Execute with timeout
            $result = $this->executeWithTimeout($call, timeout: 30);

            // Validate result format
            $this->validateResult($result);

            return $result;

        } catch (ValidationException $e) {
            // Return validation error to LLM
            return json_encode([
                'error' => 'validation_failed',
                'message' => $e->getMessage(),
                'hint' => 'Please check the parameter format and try again'
            ]);

        } catch (TimeoutException $e) {
            // Function took too long
            return json_encode([
                'error' => 'timeout',
                'message' => 'Function execution exceeded time limit',
                'hint' => 'Try breaking the request into smaller parts'
            ]);

        } catch (\Exception $e) {
            // Log unexpected errors
            Log::error('Function execution failed', [
                'function' => $call->name,
                'arguments' => $call->arguments,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return json_encode([
                'error' => 'execution_failed',
                'message' => 'An unexpected error occurred',
                'function' => $call->name,
            ]);
        }
    }

    protected function validateArguments(FunctionCall $call): void
    {
        // Implement argument validation based on function schema
        $schema = $this->getFunctionSchema($call->name);

        foreach ($schema['required'] ?? [] as $param) {
            if (!isset($call->arguments[$param])) {
                throw new ValidationException("Missing required parameter: {$param}");
            }
        }
    }

    protected function executeWithTimeout(FunctionCall $call, int $timeout): string
    {
        // Use pcntl or process timeout for long-running operations
        $start = time();
        $result = $this->doExecute($call);

        if (time() - $start > $timeout) {
            throw new TimeoutException('Execution time exceeded');
        }

        return $result;
    }

    protected function validateResult(string $result): void
    {
        // Ensure result is valid JSON
        $decoded = json_decode($result, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new ValidationException('Result is not valid JSON');
        }
    }
}

Retry Strategies ​

Implement intelligent retry logic for transient failures:

php
class RetryableFunctionExecutor
{
    protected int $maxRetries = 3;
    protected int $baseDelay = 1000; // milliseconds

    public function executeWithRetry(FunctionCall $call): string
    {
        $attempt = 0;
        $lastException = null;

        while ($attempt < $this->maxRetries) {
            try {
                return $this->execute($call);

            } catch (TransientException $e) {
                // Retry on transient errors (network, rate limits, etc.)
                $lastException = $e;
                $attempt++;

                if ($attempt < $this->maxRetries) {
                    $delay = $this->calculateBackoff($attempt);
                    Log::warning("Function call failed, retrying in {$delay}ms", [
                        'function' => $call->name,
                        'attempt' => $attempt,
                        'error' => $e->getMessage(),
                    ]);

                    usleep($delay * 1000);
                }

            } catch (\Exception $e) {
                // Don't retry on permanent errors
                throw $e;
            }
        }

        // All retries exhausted
        throw new \RuntimeException(
            "Function execution failed after {$this->maxRetries} attempts",
            0,
            $lastException
        );
    }

    protected function calculateBackoff(int $attempt): int
    {
        // Exponential backoff with jitter
        $exponential = $this->baseDelay * (2 ** ($attempt - 1));
        $jitter = rand(0, (int)($exponential * 0.1));

        return $exponential + $jitter;
    }

    protected function isTransientError(\Exception $e): bool
    {
        // Determine if error is retryable
        return $e instanceof NetworkException
            || $e instanceof RateLimitException
            || $e instanceof TimeoutException
            || str_contains($e->getMessage(), 'temporary');
    }
}

Circuit Breaker Pattern ​

Prevent cascading failures when a function repeatedly fails:

mermaid
stateDiagram-v2
    [*] --> Closed

    Closed --> Closed: Success<br/>(reset failure count)
    Closed --> Open: Failures ≥ threshold<br/>(5 failures)

    Open --> HalfOpen: After recovery timeout<br/>(60 seconds)
    Open --> Open: Request blocked

    HalfOpen --> Closed: Success<br/>(service recovered)
    HalfOpen --> Open: Failure<br/>(still broken)

    note right of Closed
        Normal operation
        All requests pass through
    end note

    note right of Open
        Circuit tripped
        All requests blocked
        Returns fallback response
    end note

    note right of HalfOpen
        Testing recovery
        Limited requests allowed
    end note

Implementation:

php
use Illuminate\Support\Facades\Cache;

class CircuitBreaker
{
    protected int $failureThreshold = 5;
    protected int $recoveryTimeout = 60; // seconds

    public function execute(string $functionName, callable $callable): mixed
    {
        $key = "circuit_breaker:{$functionName}";

        // Check if circuit is open
        if (Cache::has("{$key}:open")) {
            throw new CircuitBreakerOpenException(
                "Circuit breaker is open for function: {$functionName}"
            );
        }

        try {
            $result = $callable();

            // Reset failure count on success
            Cache::forget("{$key}:failures");

            return $result;

        } catch (\Exception $e) {
            // Increment failure count
            $failures = Cache::increment("{$key}:failures", 1, now()->addMinutes(5));

            if ($failures >= $this->failureThreshold) {
                // Open circuit
                Cache::put("{$key}:open", true, $this->recoveryTimeout);

                Log::error("Circuit breaker opened for function: {$functionName}", [
                    'failures' => $failures,
                    'recovery_timeout' => $this->recoveryTimeout,
                ]);
            }

            throw $e;
        }
    }
}

// Usage
$breaker = new CircuitBreaker();

try {
    $result = $breaker->execute('external_api_call', function() use ($call) {
        return $this->executeFunction($call);
    });
} catch (CircuitBreakerOpenException $e) {
    // Provide fallback response
    return json_encode([
        'error' => 'service_unavailable',
        'message' => 'This function is temporarily unavailable',
        'retry_after' => 60,
    ]);
}

Conditional Logic and Dynamic Tool Selection ​

Build agents that adapt tool usage based on context and state.

Context-Aware Function Selection ​

php
class ContextAwareFunctionBuilder
{
    public function buildForUser(User $user): FunctionBuilder
    {
        $functions = FunctionBuilder::make();

        // Base functions available to all users
        $functions->addFunction(
            name: 'get_profile',
            description: 'Get user profile information',
            closure: fn() => $user->toJson()
        );

        // Admin-only functions
        if ($user->isAdmin()) {
            $functions->addFunction(
                name: 'delete_user',
                description: 'Delete a user account',
                closure: fn(int $userId) => User::destroy($userId)
            );

            $functions->addFunction(
                name: 'view_all_orders',
                description: 'View all orders in the system',
                closure: fn() => Order::all()->toJson()
            );
        }

        // Premium user functions
        if ($user->isPremium()) {
            $functions->addFunction(
                name: 'export_data',
                description: 'Export user data in various formats',
                closure: fn(string $format) => $this->exportData($user, $format)
            );
        }

        // Rate-limited functions
        if ($this->checkRateLimit($user)) {
            $functions->addFunction(
                name: 'generate_report',
                description: 'Generate detailed analytics report',
                closure: fn(array $params) => $this->generateReport($user, $params)
            );
        }

        return $functions;
    }
}

State-Based Tool Availability ​

php
class StatefulAgent
{
    protected array $state = [];
    protected array $availableTools = [];

    public function __construct(protected FunctionBuilder $baseFunctions)
    {
        $this->availableTools = $this->baseFunctions->build();
    }

    public function execute(string $message): string
    {
        // Update available tools based on current state
        $this->updateAvailableTools();

        $functions = $this->getCurrentFunctions();

        $response = LLM::driver('openai')->functionCall($message, $functions);

        if ($response instanceof FunctionCall) {
            $result = $this->executeAndUpdateState($response);
            return $result;
        }

        return $response;
    }

    protected function updateAvailableTools(): void
    {
        // Example: Only allow checkout after cart has items
        if (!empty($this->state['cart'])) {
            $this->availableTools[] = [
                'name' => 'checkout',
                'description' => 'Process order and payment',
                'function' => fn() => $this->checkout(),
            ];
        }

        // Example: Only allow refund after purchase
        if (isset($this->state['order_id'])) {
            $this->availableTools[] = [
                'name' => 'request_refund',
                'description' => 'Request refund for order',
                'function' => fn() => $this->requestRefund($this->state['order_id']),
            ];
        }
    }

    protected function executeAndUpdateState(FunctionCall $call): string
    {
        $result = $this->execute($call);

        // Update state based on function execution
        match($call->name) {
            'add_to_cart' => $this->state['cart'][] = $call->arguments['product_id'],
            'checkout' => $this->state['order_id'] = json_decode($result, true)['order_id'],
            'clear_cart' => $this->state['cart'] = [],
            default => null,
        };

        return $result;
    }
}

Advanced Patterns ​

Function Call Chaining with Pipelines ​

Create reusable function call pipelines:

php
class FunctionPipeline
{
    protected array $stages = [];

    public function add(string $name, callable $function): self
    {
        $this->stages[] = compact('name', 'function');
        return $this;
    }

    public function execute($input): mixed
    {
        $output = $input;

        foreach ($this->stages as $stage) {
            Log::info("Pipeline stage: {$stage['name']}");

            $output = ($stage['function'])($output);

            if ($output instanceof PipelineBreak) {
                return $output->value;
            }
        }

        return $output;
    }
}

// Usage: Data enrichment pipeline
$pipeline = (new FunctionPipeline())
    ->add('fetch_user', fn($email) => User::where('email', $email)->first())
    ->add('fetch_orders', fn($user) => Order::where('user_id', $user->id)->get())
    ->add('calculate_lifetime_value', fn($orders) => $orders->sum('total'))
    ->add('format_currency', fn($value) => '$' . number_format($value, 2));

$ltv = $pipeline->execute('john@example.com');
// Output: "$1,234.56"

Function Call Memoization ​

Cache expensive function results:

php
class MemoizedFunctionExecutor
{
    public function execute(FunctionCall $call): string
    {
        $cacheKey = $this->getCacheKey($call);
        $ttl = $this->getTtl($call->name);

        return Cache::remember($cacheKey, $ttl, function() use ($call) {
            return $this->doExecute($call);
        });
    }

    protected function getCacheKey(FunctionCall $call): string
    {
        return sprintf(
            'func:%s:%s',
            $call->name,
            md5(json_encode($call->arguments))
        );
    }

    protected function getTtl(string $functionName): int
    {
        // Different TTLs for different functions
        return match($functionName) {
            'get_weather' => 1800,      // 30 minutes
            'get_stock_price' => 60,    // 1 minute
            'get_user' => 300,          // 5 minutes
            default => 600,             // 10 minutes
        };
    }
}

Hierarchical Function Calling ​

Organize functions into hierarchies for complex domains:

php
class HierarchicalFunctionExecutor
{
    protected array $namespaces = [];

    public function registerNamespace(string $namespace, array $functions): void
    {
        $this->namespaces[$namespace] = $functions;
    }

    public function execute(string $qualifiedName, array $arguments): string
    {
        [$namespace, $function] = explode('.', $qualifiedName, 2);

        if (!isset($this->namespaces[$namespace][$function])) {
            throw new \RuntimeException("Function not found: {$qualifiedName}");
        }

        return ($this->namespaces[$namespace][$function])(...$arguments);
    }
}

// Usage
$executor = new HierarchicalFunctionExecutor();

$executor->registerNamespace('user', [
    'get' => fn($id) => User::find($id)->toJson(),
    'create' => fn($data) => User::create($data)->toJson(),
    'delete' => fn($id) => User::destroy($id),
]);

$executor->registerNamespace('order', [
    'get' => fn($id) => Order::find($id)->toJson(),
    'create' => fn($data) => Order::create($data)->toJson(),
    'cancel' => fn($id) => Order::find($id)->update(['status' => 'cancelled']),
]);

// Call: user.get(123) or order.create({...})
$result = $executor->execute('user.get', [123]);

Security Considerations ​

Secure your function calling implementations against abuse and attacks.

Input Validation and Sanitization ​

php
class SecureFunctionExecutor
{
    protected array $allowedFunctions = [];

    public function execute(FunctionCall $call): string
    {
        // 1. Whitelist validation
        if (!$this->isAllowedFunction($call->name)) {
            throw new SecurityException("Function not allowed: {$call->name}");
        }

        // 2. Sanitize inputs
        $sanitized = $this->sanitizeArguments($call->arguments);

        // 3. Validate argument types
        $this->validateArgumentTypes($call->name, $sanitized);

        // 4. Check permissions
        $this->checkPermissions($call->name);

        // 5. Rate limiting
        $this->enforceRateLimit($call->name);

        // 6. Execute safely
        return $this->executeInSandbox($call->name, $sanitized);
    }

    protected function sanitizeArguments(array $arguments): array
    {
        return array_map(function ($value) {
            if (is_string($value)) {
                // Prevent SQL injection
                $value = strip_tags($value);

                // Prevent path traversal
                $value = str_replace(['../', '..\\'], '', $value);

                // Prevent command injection
                $value = escapeshellarg($value);
            }

            return $value;
        }, $arguments);
    }

    protected function enforceRateLimit(string $functionName): void
    {
        $key = 'rate_limit:' . request()->ip() . ':' . $functionName;

        if (RateLimiter::tooManyAttempts($key, 10)) {
            throw new RateLimitException('Too many requests for function: ' . $functionName);
        }

        RateLimiter::hit($key, 60);
    }
}

Sandboxed Execution ​

Execute untrusted functions in isolated environments:

php
class SandboxedExecutor
{
    public function execute(FunctionCall $call): string
    {
        // Execute in separate process with timeout
        $process = new Process([
            'php',
            base_path('scripts/execute_function.php'),
            $call->name,
            json_encode($call->arguments)
        ], timeout: 30);

        $process->run();

        if (!$process->isSuccessful()) {
            throw new \RuntimeException('Sandboxed execution failed: ' . $process->getErrorOutput());
        }

        return $process->getOutput();
    }
}

Audit Logging ​

Track all function executions for security and compliance:

php
class AuditedFunctionExecutor
{
    public function execute(FunctionCall $call): string
    {
        $auditId = Str::uuid();

        // Log before execution
        Log::info('Function execution started', [
            'audit_id' => $auditId,
            'function' => $call->name,
            'arguments' => $this->sanitizeForLogging($call->arguments),
            'user_id' => auth()->id(),
            'ip' => request()->ip(),
            'timestamp' => now()->toIso8601String(),
        ]);

        try {
            $result = $this->doExecute($call);

            // Log success
            Log::info('Function execution completed', [
                'audit_id' => $auditId,
                'function' => $call->name,
                'success' => true,
            ]);

            return $result;

        } catch (\Exception $e) {
            // Log failure
            Log::error('Function execution failed', [
                'audit_id' => $auditId,
                'function' => $call->name,
                'error' => $e->getMessage(),
                'success' => false,
            ]);

            throw $e;
        }
    }

    protected function sanitizeForLogging(array $arguments): array
    {
        // Remove sensitive data from logs
        $sensitive = ['password', 'token', 'api_key', 'secret'];

        return array_map(function ($key, $value) use ($sensitive) {
            if (in_array($key, $sensitive)) {
                return '[REDACTED]';
            }
            return $value;
        }, array_keys($arguments), $arguments);
    }
}

Performance Optimization ​

Optimize function calling for production workloads.

Function Result Caching ​

php
class CachingFunctionExecutor
{
    protected array $cacheConfig = [];

    public function setCacheConfig(string $functionName, int $ttl, ?callable $keyGenerator = null): void
    {
        $this->cacheConfig[$functionName] = compact('ttl', 'keyGenerator');
    }

    public function execute(FunctionCall $call): string
    {
        if (!isset($this->cacheConfig[$call->name])) {
            return $this->doExecute($call);
        }

        $config = $this->cacheConfig[$call->name];
        $key = $config['keyGenerator']
            ? ($config['keyGenerator'])($call)
            : $this->defaultCacheKey($call);

        return Cache::remember($key, $config['ttl'], function() use ($call) {
            return $this->doExecute($call);
        });
    }

    protected function defaultCacheKey(FunctionCall $call): string
    {
        return sprintf(
            'func_cache:%s:%s',
            $call->name,
            md5(json_encode($call->arguments))
        );
    }
}

// Configuration
$executor = new CachingFunctionExecutor();

$executor->setCacheConfig(
    'get_weather',
    ttl: 1800,
    keyGenerator: fn($call) => "weather:{$call->arguments['city']}"
);

$executor->setCacheConfig('get_stock_price', ttl: 60);

Batch Function Execution ​

Execute multiple independent functions in batches:

php
class BatchFunctionExecutor
{
    public function executeBatch(array $functionCalls): array
    {
        // Group by function name for potential optimization
        $grouped = collect($functionCalls)->groupBy(fn($call) => $call->name);

        $results = [];

        foreach ($grouped as $functionName => $calls) {
            if ($this->supportsBatchExecution($functionName)) {
                // Execute all at once
                $batchResults = $this->executeBatchOptimized($functionName, $calls);
                $results = array_merge($results, $batchResults);
            } else {
                // Execute individually
                foreach ($calls as $call) {
                    $results[] = $this->execute($call);
                }
            }
        }

        return $results;
    }

    protected function executeBatchOptimized(string $functionName, $calls): array
    {
        // Example: Batch database queries
        if ($functionName === 'get_user') {
            $ids = collect($calls)->pluck('arguments.id')->toArray();
            $users = User::whereIn('id', $ids)->get()->keyBy('id');

            return collect($calls)->map(function($call) use ($users) {
                $user = $users->get($call->arguments['id']);
                return $user ? $user->toJson() : json_encode(['error' => 'User not found']);
            })->toArray();
        }

        // Fallback to individual execution
        return array_map(fn($call) => $this->execute($call), $calls);
    }
}

Lazy Function Evaluation ​

Defer expensive function execution until results are needed:

php
class LazyFunctionCall
{
    protected ?string $result = null;

    public function __construct(
        protected FunctionCall $call,
        protected FunctionExecutor $executor
    ) {}

    public function get(): string
    {
        if ($this->result === null) {
            $this->result = $this->executor->execute($this->call);
        }

        return $this->result;
    }

    public function isEvaluated(): bool
    {
        return $this->result !== null;
    }
}

// Usage
$lazyCall = new LazyFunctionCall($functionCall, $executor);

// Result not computed yet
if (some_condition()) {
    // Only evaluate if needed
    $result = $lazyCall->get();
}

Best Practices ​

1. Clear Function Boundaries ​

Design functions with single, well-defined purposes:

php
// Bad: Function does too much
$functions->addFunction(
    name: 'manage_user',
    description: 'Creates, updates, or deletes users',
    closure: fn($action, $data) => match($action) {
        'create' => User::create($data),
        'update' => User::find($data['id'])->update($data),
        'delete' => User::destroy($data['id']),
    }
);

// Good: Separate functions for each action
$functions->addFunction(
    name: 'create_user',
    description: 'Creates a new user account',
    closure: fn(array $data) => User::create($data)->toJson()
);

$functions->addFunction(
    name: 'update_user',
    description: 'Updates an existing user account',
    closure: fn(int $id, array $data) => User::find($id)->update($data)
);

$functions->addFunction(
    name: 'delete_user',
    description: 'Deletes a user account',
    closure: fn(int $id) => User::destroy($id)
);

2. Idempotent Functions ​

Design functions to be safely retryable:

php
// Bad: Non-idempotent
$functions->addFunction(
    name: 'increment_counter',
    closure: fn() => Counter::increment()
);

// Good: Idempotent with unique key
$functions->addFunction(
    name: 'increment_counter',
    closure: function(string $requestId) {
        if (Cache::has("counter_increment:{$requestId}")) {
            return Cache::get("counter_increment:{$requestId}");
        }

        $newValue = Counter::increment();
        Cache::put("counter_increment:{$requestId}", $newValue, 3600);

        return $newValue;
    }
);

3. Comprehensive Error Messages ​

Provide helpful error messages for the LLM:

php
try {
    $user = User::where('email', $email)->firstOrFail();
} catch (ModelNotFoundException $e) {
    return json_encode([
        'success' => false,
        'error' => 'user_not_found',
        'message' => "No user found with email: {$email}",
        'suggestion' => 'Try checking the email address for typos or use the search_users function to find similar emails',
        'available_actions' => ['search_users', 'list_all_users']
    ]);
}

4. Function Documentation ​

Document functions thoroughly for better LLM understanding:

php
$functions->addFunction(
    name: 'search_products',
    description: 'Searches for products in the catalog.

        Parameters:
        - query: Search term (required)
        - category: Filter by category (optional)
        - minPrice: Minimum price filter (optional)
        - maxPrice: Maximum price filter (optional)
        - inStock: Only show in-stock items (optional, default: false)

        Returns: JSON array of matching products with id, name, price, and stock.

        Examples:
        - search_products(query="laptop", category="electronics")
        - search_products(query="shirt", minPrice=20, maxPrice=50, inStock=true)',
    closure: function(string $query, ?string $category = null, ?float $minPrice = null, ?float $maxPrice = null, bool $inStock = false) {
        return Product::search($query, compact('category', 'minPrice', 'maxPrice', 'inStock'));
    }
);

5. Monitor and Alert ​

Track function execution metrics:

php
class MonitoredFunctionExecutor
{
    public function execute(FunctionCall $call): string
    {
        $start = microtime(true);
        $success = false;

        try {
            $result = $this->doExecute($call);
            $success = true;
            return $result;

        } catch (\Exception $e) {
            throw $e;

        } finally {
            $duration = microtime(true) - $start;

            // Record metrics
            Metrics::record('function.execution.duration', $duration, [
                'function' => $call->name,
                'success' => $success,
            ]);

            // Alert on slow executions
            if ($duration > 5.0) {
                Alert::send("Slow function execution: {$call->name} took {$duration}s");
            }

            // Alert on high failure rates
            $failureRate = $this->getRecentFailureRate($call->name);
            if ($failureRate > 0.2) {
                Alert::send("High failure rate for {$call->name}: {$failureRate}%");
            }
        }
    }
}

Common Pitfalls ​

1. Unbounded Recursion ​

Prevent infinite loops in multi-turn conversations:

php
// Bad: No turn limit
while (true) {
    $response = LLM::functionCall($prompt, $functions);
    if (is_string($response)) break;
    // Could loop forever
}

// Good: Bounded with max turns
$maxTurns = 10;
for ($i = 0; $i < $maxTurns; $i++) {
    $response = LLM::functionCall($prompt, $functions);
    if (is_string($response)) break;
}

if ($i >= $maxTurns) {
    Log::warning('Max turns reached without completion');
}

2. Missing Error Handling ​

Always handle function execution failures:

php
// Bad: Unhandled exceptions
$result = $function($arguments);

// Good: Comprehensive error handling
try {
    $result = $function($arguments);
} catch (ValidationException $e) {
    return json_encode(['error' => 'validation_failed', 'details' => $e->errors()]);
} catch (\Exception $e) {
    Log::error('Function failed', ['error' => $e->getMessage()]);
    return json_encode(['error' => 'execution_failed']);
}

3. Exposing Sensitive Data ​

Never return sensitive information in function results:

php
// Bad: Exposes passwords
fn($email) => User::where('email', $email)->first()->toJson()

// Good: Filters sensitive fields
fn($email) => User::where('email', $email)->first()->only(['id', 'name', 'email'])->toJson()

4. Not Validating LLM Inputs ​

Never trust LLM-generated parameters:

php
// Bad: Direct usage
$userId = $arguments['user_id'];
User::find($userId)->delete();

// Good: Validation and authorization
$userId = (int) $arguments['user_id'];

if ($userId <= 0) {
    throw new ValidationException('Invalid user ID');
}

$user = User::findOrFail($userId);

if (!auth()->user()->can('delete', $user)) {
    throw new AuthorizationException('Not authorized to delete this user');
}

$user->delete();

5. Ignoring Rate Limits ​

Implement rate limiting to prevent abuse:

php
use Illuminate\Support\Facades\RateLimiter;

function executeFunction(FunctionCall $call): string
{
    $key = 'func:' . auth()->id() . ':' . $call->name;

    if (RateLimiter::tooManyAttempts($key, 10)) {
        throw new RateLimitException('Too many function calls');
    }

    RateLimiter::hit($key, 60);

    return $this->doExecute($call);
}