Tools
Tools are the primary mechanism through which an agent interacts with the outside world. When you give an agent a set of tools, the LLM decides which tool to call, with what arguments, and when. The agent loop orchestrates this cycle automatically: the LLM requests a tool call, the framework executes it, feeds the result back, and the LLM continues reasoning until it produces a final response. This page covers the full tool API — from creating and registering tools, through the contracts that govern them, to the execution lifecycle and error handling. For practical step-by-step guidance on building your own tools, see Building Tools.Creating Tools With FunctionTool
The fastest way to create a tool is to wrap any PHP callable withFunctionTool::fromCallable(). The tool name, description, and parameter schema are all generated automatically from the function signature using reflection:
#[Description] attribute on the function provides the tool description that the LLM sees. The same attribute on parameters documents individual arguments in the generated JSON schema. Named functions produce meaningful tool names; closures work too, but you should prefer named functions for clarity.
Tip: FunctionTool is the recommended starting point for most projects. It handles schema generation, argument passing, and result wrapping with zero boilerplate.
The Tools Collection
Tools are collected in the immutableTools value object. Pass any number of ToolInterface implementations to its constructor, and the collection indexes them by name:
Querying the Collection
TheTools collection provides a rich query API for inspecting registered tools at runtime:
descriptions() method returns an array of compact summaries (name and description) for each tool. The toToolSchema() method returns the full OpenAI-compatible function-calling schema array that gets sent to the LLM as part of the inference request.
Immutable Mutators
TheTools collection is immutable. Every mutation returns a new instance, leaving the original unchanged:
Registering Multiple Tools
Pass multiple tools to theTools constructor. The LLM chooses which tool to call on each turn:
Attaching Tools to an Agent
There are two ways to give tools to an agent: directly on theAgentLoop, or through the AgentBuilder capability system.
Direct Assignment
TheAgentLoop provides withTools() (replacing the entire collection) and withTool() (appending a single tool) methods:
Via the AgentBuilder
TheUseTools capability integrates tools through the builder’s composition layer. This is the preferred approach when assembling agents from reusable capabilities:
UseTools merges the provided tools into any tools already registered on the builder, so you can combine multiple UseTools capabilities without overwriting earlier registrations.
Tool Contracts
The tool system is built on a small set of interfaces. Understanding them helps when you need to go beyond the basics and build custom tool implementations.ToolInterface
Every tool implementsToolInterface, which defines the three things the framework needs from a tool:
use() method receives the arguments that the LLM provided and returns a Result object wrapping either a success value or a failure. The toToolSchema() method returns a ToolDefinition value object describing the tool’s name, description, and parameters. The descriptor() method returns the tool’s identity and documentation.
CanDescribeTool
The descriptor interface provides identity and documentation at two levels of detail:metadata() returns a compact summary suitable for listing tools. The default implementation includes name and summary keys, with an optional namespace key for namespaced tool names (e.g., file.read yields namespace file).
instructions() returns the complete specification including parameter definitions and return type. This two-level design supports tool registries where an agent can browse available tools before loading their full documentation.
CanAccessAgentState
Tools that need to read the current agent execution state implementCanAccessAgentState. The framework calls withAgentState() before each invocation, passing in the current AgentState. The method returns a new (cloned) instance with the state injected:
withAgentState() method clones the tool and injects the state, ensuring that tool instances remain safe to reuse across invocations. Modifications to agent state should be handled by the agent’s state processors, not by tools directly.
CanAccessToolCall
Tools that need access to their invocation context (the rawToolCall object with its ID and arguments) implement CanAccessToolCall. This is useful for correlation, tracing, logging, and subagent tools that emit events:
CanAccessAgentState, this method clones the tool and injects the ToolCall, preserving immutability.
CanManageTools
TheCanManageTools interface defines the contract for mutable tool registries that support lazy instantiation through factories:
ToolRegistry class implements this interface and is used internally by the ToolsTool capability for dynamic tool discovery. The registerFactory() method accepts a callable(): ToolInterface that is only invoked when the tool is first requested, enabling lazy loading of expensive tools.
CanExecuteToolCalls
TheCanExecuteToolCalls interface defines the contract for executing a batch of tool calls against a given agent state:
ToolExecutor class is the default implementation, and the AgentLoop accepts a custom executor via withToolExecutor().
The Tool Class Hierarchy
The framework provides a layered set of abstract base classes. Each layer adds a specific concern, so you can extend at the level of abstraction that fits your use case:| Class | What it adds | When to use |
|---|---|---|
SimpleTool | Descriptor + result wrapper + $this->arg() helper | Full manual control over everything |
ReflectiveSchemaTool | Auto-generates toToolSchema() via reflection | When you want schema from __invoke signature |
FunctionTool | Wraps a callable with cached reflective schema | Typed callable tools (most common) |
StateAwareTool | withAgentState() / $this->agentState | When you need to read execution state |
BaseTool | State + reflective schema + default metadata/instructions | State-aware class-based tools |
ContextAwareTool | State + withToolCall() / $this->toolCall | When you need raw tool call context |
FunctionTool or BaseTool is all you need. See Building Tools for practical guidance, and Building Tools: Advanced Patterns for lower-level patterns.
How Tool Execution Works
TheToolExecutor manages the full lifecycle of a tool call. Understanding this flow helps when debugging or customizing tool behavior.
1. Schema Delivery
TheTools collection serializes all tool schemas via toToolSchema() and sends them to the LLM as part of the inference request. Each schema follows the OpenAI function-calling format:
2. Tool Call Parsing
When the LLM responds with one or more tool calls, the framework parses them intoToolCall objects containing the tool name, call ID, and arguments.
3. Hook Interception (Before)
Before executing each tool call, theToolExecutor runs the beforeToolUse lifecycle hook via the interceptor. Hooks can:
- Modify the tool call (e.g., rewrite arguments).
- Modify the agent state (e.g., inject context).
- Block execution entirely by marking the hook context as blocked. When blocked, a
ToolExecution::blocked()result is returned without invoking the tool.
stopOnToolBlock option is enabled on the ToolExecutor, the entire batch stops after the first blocked tool call.
4. Tool Preparation
The executor looks up the tool by name from theTools collection. If the tool implements CanAccessAgentState, a clone with the current AgentState injected is created. If it implements CanAccessToolCall, the raw ToolCall is injected the same way. This ensures tools are stateless and safe for concurrent use.
5. Argument Validation
Required parameters declared in the tool’s schema are checked against the provided arguments. Missing required parameters produce aResult::failure() with an InvalidToolArgumentsException without invoking the tool. The LLM sees the error message and can retry with corrected arguments.
6. Execution
The tool’suse() method is called with the LLM-provided arguments. For tools extending SimpleTool, this delegates to __invoke(), and the return value is automatically wrapped in Result::success(). Any exception (except AgentStopException) is caught and wrapped in Result::failure().
7. Event Emission
The executor dispatchesToolCallStarted and ToolCallCompleted events around each execution. These events carry timing information and success/failure status, making them useful for logging, metrics, and observability.
8. Hook Interception (After)
TheafterToolUse lifecycle hook runs, allowing inspection or modification of the execution result. Hooks can replace the ToolExecution entirely (e.g., to sanitize output or add metadata).
9. Result Formatting
Tool execution results are formatted as messages and appended to the conversation. The LLM sees these results on its next turn.10. Loop Continuation
The cycle repeats until the LLM responds without requesting any tool calls, at which point the agent produces its final response.The ToolExecution Object
Each tool invocation produces aToolExecution value object that captures the complete execution record:
ToolExecutions collection aggregates multiple executions from a single step and provides batch-level queries:
Error Handling
Tool failures are handled gracefully by default. If a tool throws an exception, the framework catches it, wraps it in aResult::failure() with a ToolExecutionException, and reports the error back to the LLM as a tool result. This lets the LLM retry with different arguments or fall back to an alternative approach.
The AgentStopException is the one exception that is never caught. Throwing it from within a tool immediately stops the agent loop with the provided StopSignal. This is the canonical way for a tool to halt execution programmatically.
Strict Failure Mode
You can change the default behavior with thethrowOnToolFailure option on the ToolExecutor. When enabled, tool exceptions propagate and halt the agent loop instead of being fed back to the LLM:
Stopping on Blocked Tools
ThestopOnToolBlock option causes the executor to stop processing remaining tool calls in a batch when a hook blocks the first one:
The Tool Registry
For scenarios where tools are numerous or expensive to instantiate, theToolRegistry provides a mutable, lazy-loading container that implements CanManageTools:
ToolRegistry is used internally by the ToolsTool capability, which exposes a meta-tool that lets the LLM browse, search, and inspect available tools at runtime.
FakeTool for Testing
When testing agent behavior, useFakeTool to create tools with predetermined responses. This avoids external dependencies and makes tests deterministic.
Static Responses
The simplest form returns the same value regardless of arguments:Dynamic Responses
Pass a callable handler for responses that depend on the arguments:Full Customization
FakeTool also accepts optional schema, metadata, and fullSpec arrays for complete control over how the fake tool presents itself:
FakeTool generates a minimal schema with an empty properties object, which is sufficient for most testing scenarios.
Next Steps
- Building Tools — practical guide to creating tools with
FunctionToolandBaseTool - Building Tools: Advanced Patterns —
ContextAwareTool,SimpleTool, descriptors, and schema strategies - Hooks — intercepting tool calls with
beforeToolUseandafterToolUsehooks