Skip to content
Nibiru docsv0.9.2

Agent plugin

A ReAct-style tool-using agent. Extend Tool to give it any PHP capability you can write.

Stable Reading time ~ 3 min Edit on GitHub

The Agent plugin lets you give an LLM the ability to act — to call SQL queries, hit HTTP endpoints, read files, or do anything else you can express as a PHP method. It runs a ReAct-style loop: think → tool-call → observe → repeat → answer.

use Nibiru\Module\Ai\Ai;
use Nibiru\Module\Ai\Plugin\Tools\PdoQuery;
$ai = new Ai();
echo $ai->agent()
->withTools([new PdoQuery()])
->run('How many active users do we have?');
// → "We have 1,247 active users." (after the agent ran SELECT count(*)…)
user task
LLM gets system prompt with tool definitions
LLM emits ```tool {"tool":"pdo_query","args":{"sql":"SELECT…"}}```
Agent runs the tool, captures result
LLM gets observation, decides: more tools or final answer?
"FINAL: 1,247 active users."

The protocol uses a fenced-JSON sentinel\“tool {…}```— that any model can produce. No native tool-calling API required, so it works on every Ollama model out of the box. (Models that support native tool-calling can be plugged in via a subclass that overridesparseToolCall()`.)

Nibiru ships three:

ToolWhat it does
Tools\PdoQuerySingle read-only SELECT against the app DB. Blocks INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER. Returns up to 50 rows as JSON.
Tools\HttpGetGET an HTTP/HTTPS URL with optional headers. Returns body, truncated to 8 KB.
Tools\FileReadRead a project file by relative path. Blocks .. traversal. Returns up to 8 KB.
use Nibiru\Module\Ai\Plugin\Tools;
$agent = $ai->agent()->withTools([
new Tools\PdoQuery(),
new Tools\HttpGet(),
new Tools\FileRead(),
]);
// Multi-step task
echo $agent->run(
'Read application/controller/loginController.php and tell me '
. 'whether it implements rate limiting.'
);

The agent will call file_read with the path, observe the source, and answer based on what it actually saw — not on what it imagines.

Extend Tool:

namespace App\AiTools;
use Nibiru\Module\Ai\Plugin\Tool;
class StripeRefund extends Tool
{
public function name(): string { return 'stripe_refund'; }
public function description(): string {
return 'Issue a Stripe refund for a charge ID.';
}
public function schema(): array {
return [
'charge_id' => [
'type' => 'string',
'description' => 'A Stripe charge ID, e.g. ch_3K…',
'required' => true,
],
'amount_cents' => [
'type' => 'integer',
'description' => 'Amount to refund in cents. Omit for full refund.',
'required' => false,
],
];
}
public function execute(array $args): mixed {
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));
$refund = $stripe->refunds->create(array_filter([
'charge' => $args['charge_id'],
'amount' => $args['amount_cents'] ?? null,
]));
return json_encode([
'refund_id' => $refund->id,
'status' => $refund->status,
'amount' => $refund->amount,
]);
}
}

Then plug it in:

$ai->agent()
->withTools([new \App\AiTools\StripeRefund(), new Tools\PdoQuery()])
->run('Refund order #4421 — they were charged twice.');

The agent will use pdo_query to find the charge, then call stripe_refund with that charge ID.

$agent = $ai->agent()->withTools([new Tools\PdoQuery()]);
$answer = $agent->run('How many products in the gold-plating category?');
foreach ($agent->trace() as $step) {
echo "Step {$step['step']}: action={$step['action']}\n obs={$step['observation']}\n";
}

Useful for debugging, audit trails, or building a “show your work” UI.

  • PdoQuery blocks writes. If you want write access, write a more privileged subclass with an audit trail. Don’t lift the SELECT-only restriction in the built-in tool.
  • HttpGet allows any URL by default. Lock down via an allowlist in [AI] http_allowed_hosts[] (planned), or write a RestrictedHttpGet subclass that filters URLs.
  • FileRead blocks ... It’s confined to the application root.
  • Max iterations. agent.max_iterations = 6 in the INI prevents runaway loops. Raise carefully.
  • Tool timeout. agent.tool_timeout = 30 (seconds). A tool that hangs won’t hold the request forever.
  • Forgetting withTools(). Without tools, the agent is just a regular Chat.
  • Letting the agent see secrets. Never put API keys, raw passwords, or PII into a tool’s response — the model receives the full string.
  • Long tool outputs. Each observation is appended to the conversation. A tool that dumps 50 KB will exhaust context fast. The built-in tools cap at 8 KB; do the same in your custom tools.
  • No tool-call in reply = final answer. If the model produces a final answer that looks like a tool call but doesn’t validate, the agent treats it as final. Be explicit in the prompt: “Output a tool call OR a final answer, never both.”