10 - Stateful Tools in PHP - An Agent That Reads and Mutates Inventory A developer demonstrated a stateful PHP agent that reads and mutates inventory, using NanoAgent's FunctionTool to wrap a ProductDatabase class with search_products and place_order tools. The agent queries stock, decides, and decrements inventory in a single turn, with a system prompt instructing it not to guess stock levels. After the run, the Quantum Laptop stock dropped from 5 to 3, confirming the mutation executed. So far the tools have been read-only weather, search . The more interesting case is a tool that changes state - placing an order, updating a record, firing an action. The agent has to query state, decide , then mutate it, all in one turn. This example is an inventory agent: the user asks to buy something, the agent checks stock, then places the order. A plain PHP class stands in for a real database - an in-memory array of products, with methods to read and write it. This keeps the example runnable with no setup, while behaving exactly like a DB-backed store would. php class ProductDatabase { private array $products = 'p1' = 'name' = 'Quantum Laptop', 'price' = 1500, 'stock' = 5 , 'p2' = 'name' = 'Nano Phone', 'price' = 800, 'stock' = 0 , 'p3' = 'name' = 'AI Headset', 'price' = 300, 'stock' = 12 , ; public function search string $query : array { $out = ; foreach $this- products as $id = $p { if stripos $p 'name' , $query == false $out $id = $p; } return $out; } order is the mutating half: it validates the product exists and has enough stock before touching anything, then decrements the count and returns a human-readable result. public function order string $id, int $qty : string { if isset $this- products $id return "Error: Product not found."; if $this- products $id 'stock' < $qty return "Error: Insufficient stock."; $this- products $id 'stock' -= $qty; // <-- the mutation return "Success: Ordered $qty of {$this- products $id 'name' }. " . "New stock: {$this- products $id 'stock' }"; } public function getInventory : array { return $this- products; } } $db = new ProductDatabase ; The read tool is a thin wrapper around $db- search - no logic of its own, just a schema the model can call: php use NanoAgent\Agent; use NanoAgent\Tools\FunctionTool; $searchTool = new FunctionTool name: 'search products', description: 'Search for products by name in the store inventory.', parameters: 'type' = 'object', 'properties' = 'query' = 'type' = 'string', 'description' = 'Product name or keyword' , 'required' = 'query' , 'additionalProperties' = false , callable: fn array $args = $db- search $args 'query' ; The write tool is the same shape, just pointed at $db- order instead - the mutation itself lives entirely in the ProductDatabase class, not in the tool: php $orderTool = new FunctionTool name: 'place order', description: 'Place an order for a specific product by its ID.', parameters: 'type' = 'object', 'properties' = 'product id' = 'type' = 'string', 'description' = 'The unique product ID' , 'quantity' = 'type' = 'integer', 'description' = 'Units to purchase' , 'required' = 'product id', 'quantity' , 'additionalProperties' = false , callable: fn array $args = $db- order $args 'product id' , $args 'quantity' ; php $agent = new Agent llm: $llmConfig, systemPrompt: "You are a retail sales assistant. You have tools 'search products' and " . "'place order'. You MUST use these tools to check availability and place " . "orders. Do not guess stock levels.", tools: $searchTool, $orderTool ; $agent- enableActivityLogging ; $response = $agent- chat "I want to buy 2 Quantum Laptops. Check stock and order." ; echo $response; Check the database afterward and the stock is genuinely lower - this wasn't just a chat reply, the agent actually called place order and it actually ran: php var dump $db- getInventory 'p1' 'stock' ; // 5 - 3 The "do not guess stock levels" line in the prompt is load-bearing. Without it, a model might answer "yes, we have some" from memory. With it, the agent must call search products to read the real number, then place order to act on it. "Success: Ordered 2... New stock: 3" lets the model reason about the outcome and report it. A bare array gives it less to work with. "Error: Insufficient stock." is returned to the model, which can then tell the user "only 0 in stock" instead of your app 500ing. additionalProperties = false enableActivityLogging Swap ProductDatabase for real persistence and you've got an agent that can read your DB, make a decision, and write back to your DB - the core of most "agentic" business automation orders, tickets, inventory, bookings . The agent is the decision layer; your PHP functions are the hands. Part of the NanoAgent examples series. Landing + demos https://hammrouni.github.io/nanoagentphp/ .