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');
?>