Skip to main content

Building Tools

This page walks through the two recommended paths for creating tools in the Agents package. Most projects only need one of these:
  • FunctionTool::fromCallable() — wrap any callable and get typed parameters with auto-generated schema.
  • BaseTool — extend a base class for tools that need access to agent state or custom behavior.
For lower-level patterns like ContextAwareTool, SimpleTool, and custom descriptors, see Building Tools: Advanced Patterns. FunctionTool is the fastest path to a working tool. It uses PHP reflection to extract the tool name from the function name, the description from the #[Description] attribute, and the parameter schema from typed arguments. There is nothing to configure manually.

Basic Usage

The generated tool will have the name get_weather, the description from the function-level #[Description] attribute, and a JSON schema with a required city string parameter documented with its own description.

Closures and Anonymous Functions

Closures work too, though the generated tool name will be less meaningful. You can use #[Description] on both the closure and its parameters:

Multiple Parameters and Types

FunctionTool supports all common PHP types. Optional parameters (those with default values) are not marked as required in the generated schema:

Using Static or Instance Methods

Any callable works — static methods, instance methods, and invokable objects:

How Schema Generation Works

When you call FunctionTool::fromCallable(), the factory:
  1. Uses CallableSchemaFactory to extract the function name, description, and parameter types via reflection.
  2. Converts the schema to a JSON Schema array via SchemaFactory.
  3. Caches the JSON schema on the FunctionTool instance so reflection only happens once.
  4. Wraps the callable in a Closure for consistent invocation.
The generated schema follows the OpenAI function-calling format and includes parameter types, descriptions, and required/optional status derived from PHP defaults.

Accessing the Underlying Callable

If you need to retrieve the original callable (for example, for testing), use the function() method:

BaseTool (State-Aware Class Tool)

Use BaseTool when you need a class-based tool that can access the current AgentState during execution. This is the right choice when your tool needs to read conversation history, check execution metadata, or interact with other parts of the agent’s runtime context.

Basic Usage

Every BaseTool subclass must implement __invoke(mixed ...$args). Because SimpleTool (the root of the hierarchy) declares __invoke with a variadic mixed signature, all subclasses must keep this exact signature. Use $this->arg() to extract named or positional parameters from the args array.

Why Override toToolSchema()?

BaseTool includes reflective schema support via the HasReflectiveSchema trait, which can auto-generate a schema from the __invoke method signature. However, because __invoke must use the mixed ...$args signature, the auto-generated schema will describe a single variadic mixed parameter — not useful for production prompts. You should almost always override toToolSchema() to declare the parameters the LLM should provide.

Defining Parameters with JsonSchema

The JsonSchema class provides a fluent API for building parameter schemas without writing raw arrays. It supports all JSON Schema types:
Available JsonSchema factory methods include: string(), integer(), number(), boolean(), enum(), array(), object(), and any(). Each accepts a name, description, and optional configuration like nullability.

Extracting Arguments with $this->arg()

The arg() helper resolves arguments by trying three sources in order: named key, positional index, then default value. This means your tool works correctly whether the LLM passes arguments by name (the typical case) or by position in tests:
The lookup order is: $args['query'] first, then $args[0], then the default ''.

Accessing Agent State

BaseTool extends StateAwareTool, so the current AgentState is available as $this->agentState during execution. The framework injects the state automatically before each invocation — you do not need to set it yourself:
State is read-only from the tool’s perspective. The framework clones the tool and injects the state before each call, so tools are safe to use across multiple invocations without shared mutable state.

Constructor Defaults

The BaseTool constructor accepts optional name and description parameters. If name is omitted, it defaults to the fully qualified class name. If description is omitted, it defaults to an empty string:

Custom Metadata and Instructions

BaseTool provides default implementations of metadata() and instructions() that derive values from the tool name and description. Override them when your tool needs richer documentation for tool registries or browsing:
The default metadata() implementation supports automatic namespace extraction from dotted tool names (e.g., file.read extracts namespace file) and automatic summary extraction from the first sentence of the description. The instructions() method returns the full specification including the reflective parameter schema. This two-level design supports the ToolsTool registry pattern where agents can discover tools without loading their complete documentation.

The __invoke Signature Constraint

A common question is why BaseTool subclasses cannot declare typed parameters on __invoke. The answer is a PHP language constraint: SimpleTool (the abstract root of the hierarchy) declares abstract public function __invoke(mixed ...$args): mixed, and PHP does not allow child classes to narrow the parameter types of an inherited method signature. This means you cannot write:
Instead, use $this->arg() to extract named or positional parameters:
If you want typed parameters with compile-time safety and auto-generated schema, use FunctionTool::fromCallable() instead.

Testing Your Tools

FakeTool for Loop Testing

When writing tests for agent behavior, use FakeTool to create tools with predetermined responses. This lets you test the agent loop without real tool implementations:

Testing FunctionTool Directly

You can invoke a FunctionTool directly without the agent loop:

Testing BaseTool Subclasses

Instantiate the tool and call it directly. If the tool reads $this->agentState, inject a state first:

Which Approach Should I Use?

For the vast majority of use cases, FunctionTool is the right choice. Reach for BaseTool when you need AgentState access, and ContextAwareTool only when you also need the raw ToolCall for correlation or tracing.

Next Steps

  • Tools — full reference for the tool system, contracts, and execution lifecycle
  • Building Tools: Advanced PatternsContextAwareTool, SimpleTool, custom descriptors, and schema strategies