cd /news/ai-agents/10-stateful-tools-in-php-an-agent-th… · home › topics › ai-agents › article
[ARTICLE · art-140621] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

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.

by read3 min views1 publishedSep 27, 2026

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.

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:

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:

$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:

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @nanoagent 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/10-stateful-tools-in…] indexed:0 read:3min 2026-09-27 · —