Phantom Tools return control to your application instead of executing automatically, enabling external handling, user confirmation, and API integration.
Phantom Tools follow the same interface as regular tools but instead of automatic execution, they return the tool call details to your application — giving you full control over when and how the tool is executed.
When the LLM calls a regular tool, LarAgent automatically executes it and feeds the result back to the LLM. Phantom Tools break this cycle — they return the tool call details to your application, allowing you to:
When the LLM decides to call a phantom tool, the agent returns an array with tool_calls instead of a text response (or ToolCallMessage when using ->returnMessage()):
Copy
use LarAgent\Message;use LarAgent\Messages\ToolCallMessage;$agent = PaymentAgent::for('user-123');$response = $agent->respond('Charge $50 for the subscription');// Response is an array when phantom tools are calledif (is_array($response) && isset($response['tool_calls'])) { foreach ($response['tool_calls'] as $toolCall) { $id = $toolCall['id']; // Tool call ID $name = $toolCall['function']['name']; // 'process_payment' $args = json_decode($toolCall['function']['arguments'], true); // ['amount' => 5000, 'currency' => 'USD'] // Handle the tool execution externally $result = match($name) { 'process_payment' => $this->paymentService->charge($args), 'refund_payment' => $this->paymentService->refund($args), default => ['error' => 'Unknown tool'], }; // Add tool result to chat history and continue conversation $agent->addMessage(Message::toolResult(json_encode($result), $id, $name)); } // Continue the conversation with tool results $finalResponse = $agent->respond();}
Use ->returnMessage() before respond() to get a ToolCallMessage instance instead of an array, giving you access to the getToolCalls() method with ToolCall objects.
When parallel tool calls are enabled, the LLM may request multiple phantom tools at once:
Copy
use LarAgent\Message;$agent = PaymentAgent::for('user-123');$response = $agent->respond('Charge $50 and send a receipt email');if (is_array($response) && isset($response['tool_calls'])) { // Process all tool calls foreach ($response['tool_calls'] as $toolCall) { $id = $toolCall['id']; $name = $toolCall['function']['name']; $args = json_decode($toolCall['function']['arguments'], true); $result = $this->executeExternally($name, $args); // Add each tool result to the chat history (result must be a string) $agent->addMessage(Message::toolResult(json_encode($result), $id, $name)); } // Continue conversation after all tool results are added $finalResponse = $agent->respond();}
You can use both regular and phantom tools in the same agent:
Copy
use LarAgent\Agent;use LarAgent\PhantomTool;use LarAgent\Attributes\Tool;class AssistantAgent extends Agent{ // Regular tool - executes automatically #[Tool('Get the current time')] public function getTime(): string { return now()->toIso8601String(); } // Regular tool - executes automatically #[Tool('Search for products')] public function searchProducts(string $query): array { return Product::search($query)->take(5)->get()->toArray(); } public function registerTools() { return [ // Phantom tool - returns for external handling PhantomTool::create('place_order', 'Place an order for products') ->addProperty('product_ids', 'array', 'Product IDs to order') ->addProperty('quantities', 'array', 'Quantities for each product') ->setRequired('product_ids') ->setRequired('quantities'), ]; }}
When mixing regular and phantom tools: if the LLM calls both types in parallel, regular tools execute first, then the phantom tool call is returned. The conversation only returns to you when all regular tool executions are complete and at least one phantom tool was called.
Use regular tools for safe, read-only operations and phantom tools for actions that modify state, cost money, or require confirmation.
Phantom tools support the same property definitions as regular tools:
Copy
PhantomTool::create('create_invoice', 'Create and send an invoice') ->addProperty('customer_id', 'string', 'Customer ID') ->addProperty('items', 'array', 'Line items for the invoice') ->addProperty('due_date', 'string', 'Due date in ISO 8601 format') ->addProperty('send_email', 'boolean', 'Whether to send email notification') ->setRequired('customer_id') ->setRequired('items');