Multi-Tool Orchestration in PHP - One Agent, Many Tools, One Answer
A single tool is easy to reason about. The interesting problem is an agent with several specialized tools that has to figure out which to call, in what order, and how to combine the results into one answer.
This example is a personal concierge with three tools - location, weather, events - and the user's request needs all three chained together: "Check where I am, get the weather there, and see if there are any jazz events tonight."
Three focused tools
The first tool takes no input at all - it just reports where the (simulated) user is:
use NanoAgent\Agent;
use NanoAgent\Tools\FunctionTool;
$locationTool = new FunctionTool(
name: 'detect_user_location',
description: 'Determines the user\'s current city and country.',
parameters: ['type' => 'object', 'properties' => new stdClass()], // no params
callable: fn() => ['city' => 'Paris', 'country' => 'France']
);
The second and third tools both take a city - the value the location tool just produced is what will get passed into these two:
$weatherTool = new FunctionTool(
name: 'get_current_weather',
description: 'Fetches the live weather forecast for a given city.',
parameters: [
'type' => 'object',
'properties' => ['city' => ['type' => 'string', 'description' => 'The city name']],
'required' => ['city']
],
callable: function (array $args) {
$obs = ['Paris' => 'Cloudy, 14°C', 'London' => 'Rainy, 11°C', 'New York' => 'Sunny, 22°C'];
return $obs[$args['city']] ?? 'Weather data unavailable.';
}
);
$eventTool = new FunctionTool(
name: 'search_local_events',
description: 'Searches for upcoming cultural events or concerts in a specific city.',
parameters: [
'type' => 'object',
'properties' => ['city' => ['type' => 'string', 'description' => 'The city name']],
'required' => ['city']
],
callable: function (array $args) {
if ($args['city'] === 'Paris') {
return [
['name' => 'Jazz Festival at Le Caveau', 'time' => '20:00'],
['name' => 'Monet at Grand Palais', 'time' => '10:00-18:00']
];
}
return "No specific events found for {$args['city']} today.";
}
);
The agent decides the order
$agent = new Agent(
llm: $llmConfig,
systemPrompt: "You are a personal concierge with location, weather, and event tools. "
. "When answering, first describe the location and weather, then list any "
. "relevant events.",
tools: [$locationTool, $weatherTool, $eventTool]
);
$agent->enableActivityLogging();
$response = $agent->chat(
"Check where I am, get the weather there, and see if there are any jazz events tonight."
);
echo $response;
You did not script the sequence. The model:
detectuserlocation (no args) → Paris.getcurrentweather with city: "Paris" → Cloudy, 14°C.searchlocalevents with city: "Paris" → the jazz festival.The dependency is implicit. The model reads the location result, then passes that city into the next two tools. You gave it the pieces and the goal; it built the pipeline.
The two things that make this work
description is what the model reads to choose a tool. "Determines the user's current city and country" tells it this is the first step. Vague descriptions = wrong tool picks.Tool with no parameters - the `stdClass()` trick
detectuserlocation takes no input. In JSON Schema an object with no properties is {'type':'object','properties':{}} - in PHP you write new stdClass() for that empty object. Small gotcha, easy to miss: a [] (empty array) encodes as a JSON array, not an object.
When to use this vs. an agent chain
Rule of thumb: if the path is fixed, use a chain; if the path depends on the input, give one agent many tools and let it decide.
Ready to ship an agent?
composer require hammrouni/nanoagent
MIT, zero dependencies. Explore the full series - each article teaches one pattern.
Get each new article by email - one per publish day + the 12-part cheatsheet when it drops.