Saltar al contenido

Laravel AI SDK 1.1, publicada el 5 de octubre de 2026, permite dar Agent Skills a los agentes de tu aplicación: carpetas con un archivo SKILL.md con las instrucciones de una tarea, que el agente solo carga cuando la pregunta las necesita. Es el mismo formato que usan Claude Code y el resto de agentes de programación, pero aquí las skills son del agente que atiende a tus usuarios, no del que te ayuda a programar.

En esta entrada vamos a dar 2 skills a un agente de atención al cliente, ver qué recibe exactamente el modelo, resolver los 2 fallos que nos hemos encontrado con un modelo en local y medir cuántos tokens cuesta frente a escribirlo todo en las instrucciones del agente. Después cargaremos skills desde la base de datos y las cubriremos con tests. Todo está probado, en el momento de crear esta entrada, con Laravel AI SDK 1.2.0, Laravel 13.35 y PHP 8.5.

Qué es una skill para el SDK

El estándar Agent Skills define una skill como una carpeta con un archivo SKILL.md. El archivo empieza con un frontmatter, un bloque en YAML entre 2 líneas --- con el nombre y la descripción de la skill. Debajo van las instrucciones, en Markdown. La carpeta puede incluir más archivos, como documentos de referencia, que el agente lee solo si los necesita. Si quieres saber de dónde sale el estándar y cómo encaja con MCP y AGENTS.md, lo tienes en MCP, AGENTS.md y Agent Skills. Y si lo que buscas son las skills de tu agente de programación, las tienes en la lección Qué son las skills de Claude Code y cómo crear la primera.

La idea es que, en cada petición, el modelo solo ve el nombre y la descripción de cada skill, y carga el resto cuando la pregunta encaja con alguna. Así puedes tener muchas skills sin enviarlas todas en cada petición. Más abajo tienes la medición de lo que supone eso en tokens.

Requisitos

Las skills llegan con la versión 1.1.0 del SDK (notas de la versión), que añade la interfaz HasSkills y la tool LoadSkill. Una tool es una función de tu aplicación que el modelo puede pedir que se ejecute, y esta es la que usa el modelo para cargar cada skill. La 1.2.0, del 7 de octubre de 2026, no cambia nada de lo que vamos a ver. Tienes la referencia en la documentación oficial.

Partimos de un proyecto de Laravel 13 con Sail y el SDK configurado con Ollama como proveedor por defecto y el modelo qwen3:4b-instruct, tal como queda después de instalarlo y configurarlo en Laravel AI SDK: cómo integrar IA en Laravel. Ollama ejecuta los modelos de IA en tu propia máquina, y aquí el modelo corre en la CPU, sin tarjeta gráfica, con Ollama 0.34 en un AMD Ryzen 9 6900HX.

Crear las skills

El SDK busca las skills en resources/skills, con una carpeta por skill. Al terminar este apartado tendremos esta estructura:

resources/skills/
├── devoluciones/
│   ├── SKILL.md
│   └── references/
│       └── casos-especiales.md
└── envios/
    └── SKILL.md

Vamos a crear la primera skill, resources/skills/devoluciones/SKILL.md, con la política de devoluciones de la tienda:

---
name: devoluciones
description: Úsala cuando un cliente quiere devolver un pedido, pide un reembolso o pregunta por el plazo de devolución.
---

# Política de devoluciones

1. El cliente tiene 30 días naturales desde la entrega para devolver un pedido.
2. El producto tiene que llegar sin usar y con su embalaje original.
3. Reembolsamos el importe en el mismo método de pago, como mucho 14 días después de recibir el paquete.
4. Los productos personalizados, los pedidos de empresa y los artículos rebajados tienen reglas propias: lee `references/casos-especiales.md` antes de responder sobre ellos.

Responde siempre con el plazo concreto y con los pasos que tiene que seguir el cliente.

Si te fijas, el frontmatter solo lleva 2 campos. name es el nombre con el que el modelo pide la skill; si no lo pones, el SDK usa el nombre de la carpeta. description es lo único que el modelo ve antes de cargarla, así que tiene que decir cuándo se usa. Lo que va debajo son las instrucciones, que el modelo solo recibe cuando carga la skill.

La regla 4 remite a un archivo de la propia skill. Vamos a crearlo en resources/skills/devoluciones/references/casos-especiales.md:

# Casos especiales de devolución

- Productos personalizados (grabados, fundas con nombre): no se pueden devolver, salvo que lleguen con un defecto de fabricación. En ese caso el cliente tiene 30 días para avisarnos y le enviamos uno nuevo.
- Pedidos de empresa (con factura a nombre de una sociedad): 15 días naturales desde la entrega, y el reembolso se hace por transferencia.
- Artículos rebajados: se pueden devolver en 30 días, pero el importe se devuelve como vale de compra, no en el método de pago.

Y una segunda skill, resources/skills/envios/SKILL.md, con los plazos de entrega:

---
name: envios
description: Úsala cuando un cliente pregunta cuánto tarda un pedido en llegar o a qué zonas enviamos.
---

# Envíos

| Zona | Plazo de entrega | Notas |
| --- | --- | --- |
| Península | 24 a 48 horas laborables | |
| Baleares | 3 a 5 días laborables | |
| Canarias, Ceuta y Melilla | 5 a 7 días laborables | El envío pasa por aduana y el cliente puede tener que pagar impuestos al recibirlo |

No enviamos fuera de España.

Dar las skills al agente

Vamos a crear el agente con Artisan:

php artisan make:agent SupportAgent
   INFO  Agent [app/Ai/Agents/SupportAgent.php] created successfully.  

El comando genera la clase con las interfaces de conversación y de tools, que este agente no necesita. Abre app/Ai/Agents/SupportAgent.php y sustituye su contenido por el siguiente:

<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasSkills;
use Laravel\Ai\Promptable;
use Stringable;

class SupportAgent implements Agent, HasSkills
{
    use Promptable;

    public function instructions(): Stringable|string
    {
        return 'Eres el asistente de atención al cliente de una tienda online. Responde en español y en 2 o 3 frases. '
            .'Si alguna skill encaja con la pregunta, cárgala antes de responder y usa solo lo que dice.';
    }

    public function skills(): iterable
    {
        return [
            resource_path('skills'),
        ];
    }
}

Lo único que hace falta es implementar HasSkills y devolver en skills() de dónde salen las skills, en este caso la carpeta resources/skills. Con eso, el SDK añade al agente la tool LoadSkill con esas skills. No hace falta implementar HasTools, y si el agente ya tiene sus propias tools, LoadSkill se añade a la lista.

Qué recibe el modelo

Antes de preguntar nada, vamos a ver qué le llega al modelo. El modelo conoce cada tool por su descripción y por el esquema de sus parámetros. Podemos ver los 2 creando la tool con las skills del agente:

use App\Ai\Agents\SupportAgent;
use Illuminate\JsonSchema\JsonSchemaTypeFactory;
use Laravel\Ai\Tools\LoadSkill;

$tool = new LoadSkill([...(new SupportAgent)->skills()]);

echo $tool->description();
echo json_encode(array_map(fn ($type) => $type->toArray(), $tool->schema(new JsonSchemaTypeFactory)), JSON_PRETTY_PRINT);

Esta es la descripción, con una línea por skill:

Load a skill's instructions before performing a task matching the skill's description. Pass a path to read one of the skill's bundled files instead.

Available skills:
- devoluciones: Úsala cuando un cliente quiere devolver un pedido, pide un reembolso o pregunta por el plazo de devolución.
- envios: Úsala cuando un cliente pregunta cuánto tarda un pedido en llegar o a qué zonas enviamos.

Y este es el esquema, con un parámetro name que solo admite los nombres de las skills que existen y otro opcional, path, para leer un archivo de la skill:

{
    "name": {
        "description": "The name of the skill to load.",
        "enum": [
            "devoluciones",
            "envios"
        ],
        "type": "string"
    },
    "path": {
        "description": "A bundled file path listed by the skill, read instead of the skill's instructions.",
        "type": "string"
    }
}

Cuando el modelo llama a la tool con el valor devoluciones en name, recibe las instrucciones sin el frontmatter, entre etiquetas, y la lista de archivos de la skill:

<skill_content name="devoluciones">
# Política de devoluciones

1. El cliente tiene 30 días naturales desde la entrega para devolver un pedido.
...

<skill_resources>
references/casos-especiales.md
</skill_resources>
Read one of these files by calling this tool again with the skill name and the file's path.
</skill_content>

Para leer un archivo, el modelo vuelve a llamar a la tool con el valor references/casos-especiales.md en path y recibe su contenido tal cual. El SDK solo deja leer archivos de la carpeta de la propia skill, y tampoco devuelve archivos binarios ni de más de 256 KB. Esto es lo que contesta si se le pide un archivo de otra skill o una skill que no existe:

File [../envios/SKILL.md] is not bundled with skill [devoluciones].
Skill [garantia] does not exist.

Probarlo con Ollama

El movimiento se demuestra andando. Vamos a preguntar por un envío:

$response = (new SupportAgent)->prompt('¿Cuánto tarda en llegar un pedido a Canarias?');

Para ver qué ha pasado, recorremos $response->steps y sacamos, de cada paso, sus llamadas a tools y los tokens de entrada de $step->usage->inputTokens:

Paso 1 (363 tokens de entrada)
  LoadSkill {"name":"envios"}
Paso 2 (512 tokens de entrada)
  Texto: Los pedidos a Canarias tardan entre 5 y 7 días laborables en llegar. Este plazo incluye el tránsito por aduana y el cliente podría tener que pagar impuestos al recibirlo.

Respuesta (23.4 s):
Los pedidos a Canarias tardan entre 5 y 7 días laborables en llegar. Este plazo incluye el tránsito por aduana y el cliente podría tener que pagar impuestos al recibirlo.

Cada paso es una petición al modelo. En el primero, el modelo solo ve las descripciones, elige envios y pide cargarla. En el segundo ya tiene la tabla de plazos y responde. Fíjate en los tokens de entrada: 363 en el primer paso y 512 en el segundo, cuando ya incluye el contenido de la skill.

El modelo no lee el archivo de referencia

Ahora una pregunta que necesita los casos especiales:

$response = (new SupportAgent)->prompt('Compré una funda personalizada con mi nombre hace 10 días y quiero devolverla. ¿Puedo?');
Paso 1 (375 tokens de entrada)
  LoadSkill {"name":"devoluciones"}
Paso 2 (596 tokens de entrada)
  Texto: Sí, puedes devolver la funda personalizada, siempre que esté sin usar y con su embalaje original. Debes enviarnos el producto dentro de los 30 días naturales desde la entrega.

Respuesta (12.4 s):
Sí, puedes devolver la funda personalizada, siempre que esté sin usar y con su embalaje original. Debes enviarnos el producto dentro de los 30 días naturales desde la entrega.

La respuesta es incorrecta: los productos personalizados no se pueden devolver. Hemos lanzado la misma pregunta otras 3 veces y el resultado fue el mismo en las 3, con la skill cargada y sin leer el archivo. El modelo ve en la respuesta de la tool que existe references/casos-especiales.md, y la regla 4 le dice que lo lea, pero no lo hace. Lo que funcionó fue escribir la regla como una orden, con la llamada que tiene que hacer. Abre resources/skills/devoluciones/SKILL.md y cambia la regla 4 por esta:

4. Si el pedido es de un producto personalizado, de una empresa o de un artículo rebajado, las reglas anteriores no valen. No respondas todavía: vuelve a llamar a esta herramienta con `path: references/casos-especiales.md` y responde con lo que diga ese archivo.

Respuesta vacía: el límite de pasos

Con la regla nueva, el modelo ya pide el archivo, pero la respuesta llega vacía:

Paso 1 (375 tokens de entrada)
  LoadSkill {"name":"devoluciones"}
Paso 2 (625 tokens de entrada)
  LoadSkill {"name":"devoluciones","path":"references/casos-especiales.md"}

Respuesta (9.8 s):

Lo hemos repetido 3 veces más y pasó lo mismo en las 3. No hay excepción ni aviso en el log: $response->text es una cadena vacía. La causa está en el código del SDK, en TextGenerationLoop. Si el agente no fija un máximo de pasos, el SDK lo calcula a partir del número de tools:

$count = ToolSearch::budget($tools);
$maxSteps = $count > 0 ? min((int) round($count * 1.5), self::DEFAULT_MAX_STEPS) : 5;

Es decir, 1,5 pasos por tool, redondeado y con un máximo de 25, más un paso extra si el agente aplica RepairToolCalls. Nuestro agente solo tiene una tool, LoadSkill, así que el máximo es 2: un paso para cargar la skill y otro para responder. Leer un archivo de referencia necesita un tercero, y el SDK termina en el segundo, cuando el modelo todavía está pidiendo el archivo. Con 2 tools, el máximo sería 3. La documentación del SDK no explica este cálculo, así que, si tus skills tienen archivos de referencia, revisa cuántos pasos puede dar el agente.

La solución es fijar el máximo con el atributo #[MaxSteps]. En app/Ai/Agents/SupportAgent.php, añade el atributo a la clase:

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasSkills;
use Laravel\Ai\Promptable;
use Stringable;

#[MaxSteps(4)]
class SupportAgent implements Agent, HasSkills

Con 4 pasos hay sitio para cargar la skill, leer el archivo y responder, y queda uno de margen. Ahora la respuesta es correcta, en 3 pasos, y lo fue también en otras 3 ejecuciones:

Paso 1 (375 tokens de entrada)
  LoadSkill {"name":"devoluciones"}
Paso 2 (625 tokens de entrada)
  LoadSkill {"name":"devoluciones","path":"references/casos-especiales.md"}
Paso 3 (807 tokens de entrada)
  Texto: Lo siento, pero no puedes devolver una funda personalizada con tu nombre. Esta categoría de productos no se devuelve, salvo que tenga un defecto de fabricación. En ese caso, deberías contactarnos para informar sobre el problema.

Respuesta (20.7 s):
Lo siento, pero no puedes devolver una funda personalizada con tu nombre. Esta categoría de productos no se devuelve, salvo que tenga un defecto de fabricación. En ese caso, deberías contactarnos para informar sobre el problema.

Las 2 soluciones hacen falta a la vez. Con la regla nueva y 2 pasos, la respuesta llega vacía. Y con la regla original y 4 pasos, el modelo sigue sin leer el archivo: lo hemos probado también 3 veces, y las 3 respondió que la funda se podía devolver.

Skills o instrucciones: cuántos tokens cuesta cada una

La alternativa a las skills es escribirlo todo en instructions(). Para compararlas, hemos creado un segundo agente, sin tools, con el texto de las 2 skills y de los casos especiales en sus instrucciones, y hemos lanzado cada pregunta 3 veces con cada agente. Después hemos añadido una tercera skill, una guía de tallas de 40 prendas, y hemos repetido la medición. La tabla recoge los tokens de entrada sumando todos los pasos, que salieron iguales en las 3 ejecuciones:

PreguntaCon skillsTodo en las instrucciones
Envío a Canarias, 2 skills875 (2 pasos)438 (1 paso)
Funda personalizada, 2 skills1.807 (3 pasos)450 (1 paso)
Envío a Canarias, con la guía de tallas939 (2 pasos)1.734 (1 paso)
Talla de sudadera, con la guía de tallas2.154 (2 pasos)1.746 (1 paso)

Con 2 skills cortas, las skills salen peor: cada skill cargada es un paso más, y en cada paso se vuelve a enviar todo lo anterior. Lo que cambia con la guía de tallas es el coste fijo. Con skills, cada una añade a cada petición solo su línea de descripción: el primer paso pasa de 363 a 395 tokens. Con todo en las instrucciones, cada petición lleva la guía entera, aunque la pregunta sea de envíos, y pasa de 438 a 1.734.

Así que las skills compensan cuando tienes mucho contenido y cada pregunta usa una parte pequeña. Si son 2 o 3 políticas cortas, es más barato y más rápido escribirlas en las instrucciones: en la medición, la mediana del envío a Canarias fue de 8,1 segundos con skills y de 3,3 sin ellas. Los segundos cambian mucho de una ejecución a otra, porque dependen de la máquina y de la caché de Ollama, que reutiliza el principio de los prompts repetidos. El primer ejemplo, el de arriba, tardó 23,4 segundos. Por eso comparamos tokens y no tiempos.

Skills desde la base de datos

Además de carpetas, skills() acepta objetos Skill creados en PHP y closures que devuelven skills. Con una closure, cada cliente de tu aplicación puede tener sus propias skills guardadas en la base de datos. Vamos a crear el modelo con su migración:

php artisan make:model Playbook -m

Abre la migración que se acaba de crear y añade las columnas de la skill. Los límites de longitud son los del estándar: 64 caracteres para el nombre y 1.024 para la descripción.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('playbooks', function (Blueprint $table) {
            $table->id();
            $table->string('name', 64)->unique();
            $table->string('description', 1024);
            $table->text('instructions');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('playbooks');
    }
};

En el modelo, app/Models/Playbook.php, vamos a añadir un método que convierte cada fila en una Skill:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Laravel\Ai\Skills\Skill;

#[Fillable(['name', 'description', 'instructions'])]
class Playbook extends Model
{
    public function toSkill(): Skill
    {
        return new Skill($this->name, $this->description, $this->instructions);
    }
}

Ejecuta las migraciones con php artisan migrate. Ahora abre app/Ai/Agents/SupportAgent.php y añade la closure a skills(). Este es el agente completo:

<?php

namespace App\Ai\Agents;

use App\Models\Playbook;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasSkills;
use Laravel\Ai\Promptable;
use Stringable;

#[MaxSteps(4)]
class SupportAgent implements Agent, HasSkills
{
    use Promptable;

    public function instructions(): Stringable|string
    {
        return 'Eres el asistente de atención al cliente de una tienda online. Responde en español y en 2 o 3 frases. '
            .'Si alguna skill encaja con la pregunta, cárgala antes de responder y usa solo lo que dice.';
    }

    public function skills(): iterable
    {
        return [
            resource_path('skills'),
            fn () => Playbook::all()->map->toSkill(),
        ];
    }
}

Para probarlo, vamos a guardar 2 filas en la tabla: una skill garantia y otra que se llama envios, igual que la de la carpeta, con otras instrucciones. La descripción de la tool y lo que devuelve al pedir envios quedan así:

Available skills:
- devoluciones: Úsala cuando un cliente quiere devolver un pedido, pide un reembolso o pregunta por el plazo de devolución.
- envios: Úsala cuando un cliente pregunta cuánto tarda un pedido en llegar o a qué zonas enviamos.
- garantia: Úsala cuando un cliente pregunta por la garantía de un producto o quiere repararlo.

<skill_content name="envios">
# Envíos
...
No enviamos fuera de España.
</skill_content>

Cuando 2 skills tienen el mismo nombre, gana la de la fuente que aparece antes en skills(). Aquí es la carpeta, así que la fila envios de la base de datos se ignora. Si quieres que un cliente pueda sustituir una skill de la aplicación, pon la closure delante.

Después vamos a hacer 2 preguntas sobre la garantía, contando las consultas a la tabla playbooks con DB::listen():

== prompt 1: ¿Cuántos años de garantía tienen los productos?
Tools: {"name":"garantia"}
Pasos: 2
Respuesta: Todos los productos tienen 3 años de garantía desde la fecha de compra. Para activarla, contacta con soporte y envía el número de pedido junto con una foto del defecto.
Consultas a playbooks hasta ahora: 1

== prompt 2: ¿Qué tengo que enviar para usar la garantía?
Tools: {"name":"garantia"}
Pasos: 2
Respuesta: Para usar la garantía, debes escribir a soporte con el número de pedido y una foto del defecto. La garantía dura 3 años desde la fecha de compra.
Consultas a playbooks hasta ahora: 2

La closure se ejecuta una vez en cada prompt, al preparar la petición, aunque el prompt tenga varios pasos. Si la consulta es pesada, puedes cachear su resultado dentro de la closure.

Ten en cuenta que las instrucciones de una skill llegan al modelo como texto que tiene que seguir. Si las escriben tus usuarios, cualquiera de ellos puede cambiar el comportamiento del agente, y eso es inyección de prompts. Cómo proteger un agente de ese ataque lo tienes en la lección Inyección de prompts: qué es y cómo proteger las tools de Laravel AI SDK.

Errores en el frontmatter

El frontmatter es YAML, y en YAML los dos puntos seguidos de un espacio separan una clave de su valor. Una descripción como esta, sin comillas, rompe el archivo:

description: Úsala cuando un cliente pregunta por la garantía: plazos, reparaciones y cambios.

Y no falla solo esa skill. El SDK lee todas las skills al preparar cada petición, así que cualquier pregunta al agente, también la del envío a Canarias, lanza la excepción antes de llegar al modelo:

   Symfony\Component\Yaml\Exception\ParseException 

  A colon cannot be used in an unquoted mapping value in "/var/www/html/resources/skills/garantia/SKILL.md" at line 2 (near "description: Úsala cuando un cliente pregunta por la garantía: plazos, reparaciones y cambios.").

La línea 2 que indica el mensaje es la del frontmatter, no la del archivo. La solución es poner la descripción entre comillas:

description: 'Úsala cuando un cliente pregunta por la garantía: plazos, reparaciones y cambios.'

El caso contrario es más difícil de ver. Una skill sin description no da ningún error: el SDK la ignora y el modelo nunca sabe que existe. Con una carpeta garantia sin descripción, la lista de skills de la tool sigue teniendo solo devoluciones y envios. El SDK tampoco comprueba las reglas del estándar para el nombre (minúsculas, cifras y guiones, y el mismo nombre que la carpeta), así que si compartes las skills con otros agentes, tendrás que comprobarlo tú.

Tests sin llamar al modelo

Las 2 cosas se pueden comprobar con un test, sin llamar al modelo. Con Pest instalado, como en el apartado de tests de Laravel AI SDK: cómo integrar IA en Laravel, vamos a crear el test:

php artisan make:test SkillsTest --pest

Abre tests/Feature/SkillsTest.php y sustituye su contenido por el siguiente:

<?php

use App\Ai\Agents\SupportAgent;
use App\Models\Playbook;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Skills\Skill;
use Laravel\Ai\Tools\LoadSkill;
use Laravel\Ai\Tools\Request;

uses(RefreshDatabase::class);

test('cada carpeta de resources/skills es una skill válida', function (string $directory) {
    $skill = Skill::fromDirectory($directory);

    expect($skill)->not->toBeNull()
        ->and($skill->name)->toBe(basename($directory))
        ->and($skill->name)->toMatch('/^[a-z0-9]+(-[a-z0-9]+)*$/')
        ->and(mb_strlen($skill->description))->toBeLessThanOrEqual(1024);
})->with(fn () => collect(glob(dirname(__DIR__, 2).'/resources/skills/*', GLOB_ONLYDIR))
    ->mapWithKeys(fn (string $directory) => [basename($directory) => [$directory]])
    ->all());

test('el agente de soporte ofrece las skills de los archivos y de la base de datos', function () {
    Playbook::create([
        'name' => 'garantia',
        'description' => 'Úsala cuando un cliente pregunta por la garantía de un producto o quiere repararlo.',
        'instructions' => 'Todos los productos tienen 3 años de garantía desde la fecha de compra.',
    ]);

    $tool = new LoadSkill([...(new SupportAgent)->skills()]);

    expect($tool->description())
        ->toContain('- devoluciones:')
        ->toContain('- envios:')
        ->toContain('- garantia:')
        ->and($tool->handle(new Request(['name' => 'devoluciones', 'path' => 'references/casos-especiales.md'])))
        ->toContain('Productos personalizados');
});

El primer test usa un dataset de Pest, que ejecuta el mismo test con cada valor de una lista, en este caso con cada carpeta de resources/skills. Comprueba que el SDK puede leer la skill, que el nombre coincide con la carpeta y que cumple las reglas del estándar. Si falta la descripción, fromDirectory() devuelve null, y si el YAML no es válido, lanza la excepción. El segundo comprueba que el agente ofrece las skills de los archivos y de la base de datos y que el archivo de referencia se puede leer.

Si te fijas, la ruta de las skills se construye con dirname(__DIR__, 2) y no con resource_path(). Este dataset se calcula cuando la aplicación de Laravel todavía no ha arrancado, así que con resource_path() el test falla así:

   FAILED  Tests\Feature\SkillsTest > cada carpeta de resources/skills es una skill válida                      Error
  Call to undefined method Illuminate\Container\Container::resourcePath()

Vamos a ejecutar los tests:

php artisan test tests/Feature/SkillsTest.php
   PASS  Tests\Feature\SkillsTest
  ✓ cada carpeta de resources/skills es una skill válida with dataset "devoluciones"                             0.85s  
  ✓ cada carpeta de resources/skills es una skill válida with dataset "envios"                                   0.01s  
  ✓ el agente de soporte ofrece las skills de los archivos y de la base de datos                                 0.02s  

  Tests:    3 passed (14 assertions)
  Duration: 0.93s

Y si alguien añade una skill sin descripción, el test lo detecta antes de que llegue a producción:

  ✓ cada carpeta de resources/skills es una skill válida with dataset "devoluciones"                             0.80s
  ✓ cada carpeta de resources/skills es una skill válida with dataset "envios"                                   0.01s
  ⨯ cada carpeta de resources/skills es una skill válida with dataset "garantia"                                 0.01s
  ✓ el agente de soporte ofrece las skills de los archivos y de la base de datos                                 0.02s
  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\SkillsTest > cada carpeta de resources/skills es una skill válida with dataset "garantia"
  Expecting null not to be null .

A partir de aquí, añadir una tarea nueva al agente es crear una carpeta con su SKILL.md, sin tocar la clase, siempre que la tarea no necesite más pasos de los que le has dado. Espero que te sirva. Si quieres seguir con Laravel AI SDK y aprender a proteger un agente con tools, no te pierdas el curso Desarrollo seguro con IA en Laravel.

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje