Skip to content

Agent with Custom Tool

Overview

Build a custom tool by extending BaseTool. Override __invoke(mixed ...$args) to implement the tool logic, use $this->arg() to extract named parameters, and override toToolSchema() to define the parameter schema for the LLM.

This example creates a SystemInfoTool with deterministic output that reports whether a PHP runtime is available. The agent calls it when asked about the runtime.

Key concepts: - BaseTool: Abstract base class for custom tools - __invoke(mixed ...$args): The method the agent calls - $this->arg(): Extract named or positional parameters from args - toToolSchema(): Define the JSON Schema the LLM sees for this tool - Deterministic tool results keep multi-step HTTP recordings replayable

Example

<?php
require 'examples/boot.php';

use Cognesy\Agents\AgentLoop;
use Cognesy\Agents\Data\AgentState;
use Cognesy\Agents\Events\Support\AgentEventConsoleObserver;
use Cognesy\Agents\Tool\Tools\BaseTool;
use Cognesy\Messages\Messages;
use Cognesy\Polyglot\Inference\Data\ToolDefinition;
use Cognesy\Utils\JsonSchema\JsonSchema;
use Cognesy\Utils\JsonSchema\ToolSchema;

// Custom tool with deterministic output suitable for record/replay
class SystemInfoTool extends BaseTool
{
    public function __construct() {
        parent::__construct(
            name: 'system_info',
            description: 'Reports whether the PHP runtime is available.',
        );
    }

    #[\Override]
    public function __invoke(mixed ...$args): string {
        $this->arg($args, 'category', 0, 'runtime');

        return 'PHP runtime: available';
    }

    #[\Override]
    public function toToolSchema(): ToolDefinition {
        return ToolDefinition::fromArray(ToolSchema::make(
            name: $this->name(),
            description: $this->description(),
            parameters: JsonSchema::object('parameters')
                ->withProperties([
                    JsonSchema::string('category', 'Use "runtime" to check PHP availability.'),
                ])
                ->withRequiredProperties([])
        )->toArray());
    }
}

// AgentEventConsoleObserver shows execution lifecycle events on the console
$logger = new AgentEventConsoleObserver(
    useColors: true,
    showTimestamps: true,
    showContinuation: true,
    showToolArgs: true,
);

// Create loop with the custom tool
$loop = AgentLoop::default()
    ->withTool(new SystemInfoTool())
    ->wiretap($logger->wiretap());

$state = AgentState::empty()->withMessages(
    Messages::fromString('Call system_info with category "runtime" and report its answer verbatim.')
);

echo "=== Agent Execution ===\n\n";
$finalState = $loop->execute($state);

echo "\n=== Result ===\n";
$response = $finalState->finalResponse()->toString() ?: 'No response';
echo "Answer: {$response}\n";
echo "Steps: {$finalState->stepCount()}\n";
echo "Tokens: {$finalState->usage()->total()}\n";

if ($finalState->status()->value !== 'completed') {
    echo "Skipping assertions because execution status is {$finalState->status()->value}.\n";
    exit(1);
}

// Assertions
assert(!empty($finalState->finalResponse()->toString()), 'Expected non-empty response');
assert($finalState->stepCount() >= 1, 'Expected at least 1 step');
assert($finalState->usage()->total() > 0, 'Expected token usage > 0');
?>