---
title: "Plugin del agente"
description: "Una herramienta agente estilo ReAct. Extienda la herramienta para darle cualquier capacidad de PHP que pueda escribir."
---

El complemento del Agente le permite darle a un LLM **la capacidad de actuar** — para llamar consultas SQL, golpear puntos finales HTTP, leer archivos o hacer cualquier otra cosa que pueda expresar como un método PHP. Ejecuta un bucle estilo ReAct: piensa → llamada a herramienta → observa → repite → responde.

## Cinco líneas, un agente
```php
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(*)…)
```
## ¿Cómo funciona
```
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."
```
El protocolo utiliza un **sentinela de JSON con marcas** — `\`\`\`tool {...}\`\`\`` — que cualquier modelo puede producir. No se requiere una API nativa de llamada a herramientas, por lo que funciona en todos los modelos de Ollama sin necesidad de configuración adicional. (Los modelos que admiten llamadas a herramientas nativas pueden integrarse mediante una subclase que sobrescriba el método `parseToolCall()`.)

## Herramientas integradas

Nibiru envía tres:

| Herramienta | ¿Qué hace? |
|---|---|
| `Tools\PdoQuery` | Lectura única y de solo lectura `SELECT` contra la base de datos de la aplicación. Bloquea INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER. Devuelve hasta 50 filas como JSON. |
| `Tools\HttpGet` | Obtiene una URL HTTP/HTTPS con encabezados opcionales. Devuelve el cuerpo, truncado a 8 KB. |
| `Tools\FileRead` | Lee un archivo del proyecto por ruta relativa. Bloquea la traversión `..`. Devuelve hasta 8 KB. |
```php
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.'
);
```
El agente llamará a `file_read` con la ruta, observará el origen y responderá basándose en lo que realmente vio — no en lo que imaginó.

## Escribiendo una herramienta personalizada

Extienda `Tool`:
```php
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,
        ]);
    }
}
```
Luego conéctalo:
```php
$ai->agent()
   ->withTools([new \App\AiTools\StripeRefund(), new Tools\PdoQuery()])
   ->run('Refund order #4421 — they were charged twice.');
```
El agente utilizará `pdo_query` para encontrar la carga, luego llamará a `stripe_refund` con ese ID de carga.

## Examinando la traza
```php
$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";
}
```
Útil para la depuración, registros de auditoría o para construir una interfaz de usuario "muestra tu trabajo".

## Seguridad

- **`PdoQuery` bloquea escrituras.** Si necesitas acceso de escritura, escribe una subclase más privilegiada con un registro de auditoría. No levantes la restricción de solo SELECT en la herramienta incorporada.
- **`HttpGet` permite cualquier URL por defecto.** Bloquea mediante una lista blanca en `[AI] http_allowed_hosts[]` (planeado), o escribe una subclase `RestrictedHttpGet` que filtre las URLs.
- **`FileRead` bloquea `..`.** Está confinido a la raíz de la aplicación.
- **Iteraciones máximas.** `agent.max_iterations = 6` en el INI previene bucles sin control. Aumenta con cuidado.
- **Tiempo de espera de herramienta.** `agent.tool_timeout = 30` (segundos). Una herramienta que se atasque no mantendrá la solicitud para siempre.

## Trucos comunes

- **Olvidar `withTools()`.** Sin herramientas, el agente es solo un `Chat` regular.
- **Permitir que el agente vea secretos.** Nunca pongas claves API, contraseñas en bruto o datos personales identificables (PII) en la respuesta de una herramienta — el modelo recibe la cadena completa.
- **Salidas largas de herramientas.** Cada observación se anexa a la conversación. Una herramienta que vuelca 50 KB agotará rápidamente el contexto. Las herramientas integradas tienen un límite de 8 KB; haz lo mismo en tus herramientas personalizadas.
- **No hay llamada a herramienta en la respuesta = respuesta final.** Si el modelo produce una respuesta final que *parece* una llamada a herramienta pero no se valida, el agente la trata como final. Sé explícito en el aviso: "Salida una llamada a herramienta O una respuesta final, nunca ambas."
