Skip to content

Building Agents

This guide is for developers building their own agents to distribute as ChargePanda modules.


Anatomy of an agent

An agent is a PHP class that extends AbstractAgent. It declares:

  • What the input form looks like (inputSchema)
  • What system prompt to send the LLM (instructions)
  • How the output should be rendered (outputFormat)
  • Which plan entitlement key gates access (planKey)

Everything else — provider selection, API key injection, queuing, billing, status polling — is handled by AiAgentsCore.


Minimal example

php
<?php

namespace Modules\MyModule\Agents;

use Modules\AiAgentsCore\Support\AbstractAgent;

class BlogOutlineAgent extends AbstractAgent
{
    public function slug(): string        { return 'blog-outline'; }
    public function displayName(): string { return 'Blog Outline Generator'; }
    public function description(): string { return 'Generate a structured blog post outline from a topic.'; }
    public function category(): string    { return 'Content'; }
    public function planKey(): string     { return 'my_module'; } // matches PlanEntitlement key

    public function inputSchema(): array
    {
        return [
            [
                'name'     => 'topic',
                'label'    => 'Blog Topic',
                'type'     => 'text',
                'required' => true,
                'rules'    => 'required|string|max:200',
            ],
            [
                'name'     => 'tone',
                'label'    => 'Tone',
                'type'     => 'select',
                'options'  => ['Professional', 'Casual', 'Technical'],
                'required' => false,
            ],
        ];
    }

    public function instructions(): string
    {
        return 'You are an expert content strategist. Generate a clear, structured blog post outline.';
    }
}

Registering the agent

In your module's ModuleServiceProvider::boot():

php
use Modules\MyModule\Agents\BlogOutlineAgent;

public function boot(): void
{
    // ... other boot calls

    $this->registerAgent(BlogOutlineAgent::class);
}

registerAgent() is a helper on AbstractModuleServiceProvider — pass the fully-qualified class name, not an instance. The agent is now available to AiAgentsCore's runner, billing, and UI systems.


Input schema reference

Each field in inputSchema() is an array with these keys:

KeyRequiredDescription
nameYesField name (snake_case). Becomes the key in input_data.
labelYesHuman-readable label shown in the form.
typeYesOne of: text, textarea, email, url, number, select, multiselect, toggle, hidden
requiredNoAdds a required indicator to the label. Defaults to false.
rulesNoLaravel validation rules string (e.g. 'required|string|max:500')
optionsNoArray of options for select and multiselect types
placeholderNoPlaceholder text for text inputs
helpNoHelp text shown below the field
defaultNoDefault value pre-filled in the form

Output formats

Override outputFormat() to control how the run result is rendered:

FormatoutputFormat()How it works
Plain text'text'Default. Output is displayed as pre-formatted text.
HTML'html'Output is rendered as HTML in the result view. Sanitised before display.
Structured'structured'LLM must return valid JSON. Parsed and stored as a PHP array. Rendered as a table or card by the view.

For structured output, the LLM response should be a JSON object. AiAgentsCore strips markdown code fences automatically.


Custom prompt building

By default, buildPrompt() formats each input field as "Label: value\n". Override it for a more tailored prompt:

php
public function buildPrompt(array $inputs): string
{
    return <<<PROMPT
    Write a blog outline about: {$inputs['topic']}
    Tone: {$inputs['tone'] ?? 'Professional'}

    Return a numbered outline with 5-7 main sections and 3 bullet points each.
    PROMPT;
}

Cross-field validation with InputSchema

inputSchema() handles per-field validation via the rules key. For validation that spans multiple fields, use the InputSchema utility class inside buildPrompt() — it throws a ValidationException that the runner catches and returns to the user before a job is created.

php
use Modules\AiAgentsCore\Support\InputSchema;

public function buildPrompt(array $inputs): string
{
    // Require at least one of two fields
    InputSchema::requireAtLeastOne(
        $inputs,
        ['url', 'content'],
        'Provide a URL or paste the page content directly.'
    );

    // Require two fields to be different values
    InputSchema::requireDistinct(
        $inputs,
        'your_url',
        'competitor_url',
        'Your URL and the competitor URL must be different.'
    );

    return "Analyse: {$inputs['url']} vs {$inputs['competitor_url']}";
}
MethodWhen to use
InputSchema::requireAtLeastOne($inputs, $fields, $message)User must fill in one of several optional fields (e.g. URL or pasted content)
InputSchema::requireDistinct($inputs, $fieldA, $fieldB, $message)Two fields must not be identical (e.g. comparing a URL to itself)

Using tools

Agents can use tools — actions executed by the PHP side (not the LLM) to fetch data. Implement tools() to return tool instances recognised by the Laravel AI SDK:

php
public function tools(): iterable
{
    return [
        new \Modules\MyModule\Tools\FetchPageMetaTool(),
    ];
}

When the LLM decides to call a tool, the SDK executes it and feeds the result back. AiAgentsCore records each tool call (name, input, output, duration) in the agent_tool_calls table.

See the Laravel AI SDK documentation for how to implement tool classes.


Plan entitlement key

planKey() returns the PlanEntitlement.key that gates access to this agent. All agents in the same module typically share one key (e.g. my_module) so a single plan entitlement covers the whole module.

If your module has agents at different access tiers (basic vs. premium), use different keys and seed separate entitlement rows.


Agent metadata

Override these methods to customise how your agent appears in the UI:

php
public function icon(): string        { return 'ti-pencil'; }       // Admin icon (Themify)
public function materialIcon(): string { return 'edit_note'; }       // Account icon (Material Symbols)
public function estimatedSeconds(): int { return 15; }               // Shown as estimated time
public function tags(): array         { return ['content', 'seo']; } // Future filtering
public function runsPerUnit(): int    { return 1; }                  // Cost weight (1 run = 1 unit)

Seeding entitlements on activation

When your module activates, seed the plans for your product. Listen to ModuleActivated:

php
// ModuleServiceProvider::boot()
use App\Events\ModuleActivated;
use Modules\MyModule\Listeners\RunMyModuleSetupSeeder;

$this->app['events']->listen(ModuleActivated::class, RunMyModuleSetupSeeder::class);
php
// RunMyModuleSetupSeeder::handle()
public function handle(ModuleActivated $event): void
{
    if ($event->module->slug !== 'my-module') {
        return;
    }

    // Guard against re-running
    if (setting('my_module.setup_complete')) {
        return;
    }

    // Create product, plans, entitlements...
    // See SeoAgents module for a complete example.

    setting(['my_module.setup_complete' => '1']);
}

Events fired during a run

Listen to these in your module for side-effects (notifications, logging, webhooks):

EventWhenPayload
AgentRunDispatchedAfter job is queued$run
AgentRunStartedWorker begins execution$run
AgentToolCalledAfter each tool call$run, $toolCallRecord
AgentRunCompletedRun finished successfully$run
AgentRunFailedRun failed or timed out$run, $exception
php
use Modules\AiAgentsCore\Events\AgentRunCompleted;

$this->app['events']->listen(AgentRunCompleted::class, function ($event) {
    // $event->run is a fully populated AgentRun model
});

Released under the Commercial License.