Skip to main content

Building Tools: Advanced Patterns

Most projects only need Building Tools. Use this page when you are writing custom capabilities and need lower-level control.

Class Hierarchy

ClassAddsTypical use
SimpleToolDescriptor + result wrapper + $this->arg()Full manual control
ReflectiveSchemaToolReflective toToolSchema() from __invoke()Auto schema from signatures
FunctionToolWraps callable + cached reflective schemaTyped callable tools
StateAwareToolwithAgentState() / $this->agentStateRead current execution state
BaseToolStateAwareTool + reflective schema + default metadata/instructionsState-aware class tools
ContextAwareToolStateAwareTool + withToolCall() / $this->toolCallNeed raw tool call context

ContextAwareTool: Access ToolCall and State

Use ContextAwareTool when you need call metadata (for tracing/correlation) in addition to state.
use Cognesy\Agents\Tool\ToolDescriptor;
use Cognesy\Agents\Tool\Tools\ContextAwareTool;
use Cognesy\Utils\JsonSchema\JsonSchema;
use Cognesy\Utils\JsonSchema\ToolSchema;

final class AuditingTool extends ContextAwareTool
{
    public function __construct()
    {
        parent::__construct(new ToolDescriptor(
            name: 'audit_input',
            description: 'Record tool call metadata and input.',
        ));
    }

    public function __invoke(mixed ...$args): string
    {
        $input = (string) $this->arg($args, 'input', 0, '');
        $callId = (string) ($this->toolCall?->id() ?? '');
        $callId = $callId !== '' ? $callId : 'unknown';

        return "call_id={$callId}; input={$input}";
    }

    public function toToolSchema(): array
    {
        return ToolSchema::make(
            name: $this->name(),
            description: $this->description(),
            parameters: JsonSchema::object('parameters')
                ->withProperties([
                    JsonSchema::string('input', 'Input text to audit'),
                ])
                ->withRequiredProperties(['input'])
        )->toArray();
    }
}
// @doctest id="ff2d"

SimpleTool: Full Control

Use SimpleTool when you want to manage everything yourself (descriptor, schema, behavior).
use Cognesy\Agents\Tool\ToolDescriptor;
use Cognesy\Agents\Tool\Tools\SimpleTool;
use Cognesy\Utils\JsonSchema\JsonSchema;
use Cognesy\Utils\JsonSchema\ToolSchema;

final class EchoTool extends SimpleTool
{
    public function __construct()
    {
        parent::__construct(new ToolDescriptor(
            name: 'echo_text',
            description: 'Echo back input text.',
        ));
    }

    public function __invoke(mixed ...$args): string
    {
        return (string) $this->arg($args, 'text', 0, '');
    }

    public function toToolSchema(): array
    {
        return ToolSchema::make(
            name: $this->name(),
            description: $this->description(),
            parameters: JsonSchema::object('parameters')
                ->withProperties([
                    JsonSchema::string('text', 'Text to echo'),
                ])
                ->withRequiredProperties(['text'])
        )->toArray();
    }
}
// @doctest id="f6aa"

Descriptors as Separate Classes

When tool docs get large, move them into a dedicated descriptor class. This keeps runtime logic short and reuses documentation cleanly.
use Cognesy\Agents\Tool\ToolDescriptor;

final readonly class BashLikeDescriptor extends ToolDescriptor
{
    public function __construct()
    {
        parent::__construct(
            name: 'my_tool',
            description: 'Run a controlled operation.',
            metadata: [
                'namespace' => 'system',
                'tags' => ['ops'],
            ],
            instructions: [
                'parameters' => [
                    'input' => 'Operation input.',
                ],
                'returns' => 'Operation output string.',
            ],
        );
    }
}
// @doctest id="5811"

Schema Strategy Matrix

ClassSchema sourceWhat to do
FunctionToolCallable reflection (fromCallable)Usually no override
BaseToolReflection of __invoke(mixed ...$args)Usually override toToolSchema() for explicit params
ContextAwareToolNone by defaultImplement toToolSchema()
StateAwareToolNone by defaultImplement toToolSchema()
SimpleToolNone by defaultImplement toToolSchema()
BaseTool includes reflective schema support, but because __invoke must keep mixed ...$args, the generated schema is often too generic for production prompts.

Parameter Extraction with $this->arg()

Use $this->arg() to support named and positional arguments in one line:
$path = (string) $this->arg($args, 'path', 0, '');
// @doctest id="7d92"
Lookup order is: named key, positional index, then default.