This PR was squashed before being merged into the main branch.
Discussion
----------
[Agent] Fix dispatch of multiple tool instances of the same class
| Q | A
| ------------- | ---
| Bug fix? | yes
| New feature? | no
| Docs? | no
| Issues | Fix #1657
| License | MIT
When multiple instances of the same class are registered as separate tools (e.g. two `Subagent` instances wrapping different agents), `Toolbox` would always dispatch to the first matching instance since it resolved executables by class name.
This PR fixes the issue by:
- **`MemoryToolFactory`**: Keys tool registrations by `spl_object_id` when objects are passed, allowing per-instance lookups. Builds `Tool` objects directly with `ExecutionReference` instead of going through `AsTool`/`convertAttribute`.
- **`Toolbox`**: Tries instance-specific lookup via `spl_object_id` first, falls back to class-based lookup. Maintains an `$instanceMap` to dispatch tool calls to the correct instance.
- **Removes `AbstractToolFactory`**: Inlines its logic into `ReflectionToolFactory`, since `MemoryToolFactory` no longer needs it.
- **Updates AI Bundle DI config** to reflect the removal of the abstract factory service.
Replaces #1658 1658
Commits
-------
fe195ea1 [Agent] Fix dispatch of multiple tool instances of the same class
12 KiB
UPGRADE FROM 0.6 to 0.7
Agent
-
The
Symfony\AI\Agent\Toolbox\ToolFactory\AbstractToolFactoryclass has been removed. If you extended it to create a custom tool factory, implementToolFactoryInterfaceyourself and useSymfony\AI\Platform\Contract\JsonSchema\Factorydirectly if needed. -
The
ToolFactoryInterface::getTool()method signature has changed to acceptobject|stringinstead ofstring:-public function getTool(string $reference): iterable; +public function getTool(object|string $reference): iterable;
AI Bundle
- The
api_catalogoption forOllamahas been removed as the catalog is now automatically fetched from the Ollama server - The
api_keyoption forOllamais nownullby default to allow the usage of aScopingHttpClient - The
endpointoption forOllamais nownullby default to allow the usage of aScopingHttpClient
Platform
ModelCataloginOllamahas been replaced byOllamaApiCatalogOllamaApiCataloginOllamahas been replaced toModelCatalogOllamamodel is nowfinal
Store
-
The
Retrieverconstructor has a new?EventDispatcherInterface $eventDispatcherparameter inserted as 3rd argument beforeLoggerInterface $logger. If you pass$loggerpositionally, update to use a named argument:-$retriever = new Retriever($store, $vectorizer, $logger); +$retriever = new Retriever($store, $vectorizer, logger: $logger);
UPGRADE FROM 0.5 to 0.6
AI Bundle
- The
api_keyoption forElevenLabsis not required anymore if aScopedHttpClientis used inhttp_clientoption
Platform
- The
PlatformFactoryis now in charge of creatingElevenLabsApiCatalogifapiCatalogis provided astrue Symfony\AI\Platform\TokenUsage\TokenUsageInterfacehas two new methods:getCacheCreationTokens()andgetCacheReadTokens()
UPGRADE FROM 0.4 to 0.5
Platform
- The
hostUrlparameter forElevenLabsClienthas been removed - The
hostparameter forElevenLabsApiCataloghas been removed - The
hostUrlparameter forPlatformFactory::create()inElevenLabshas been renamed toendpoint
UPGRADE FROM 0.3 to 0.4
Agent
-
The
keepToolMessagesparameter ofAgentProcessorhas been removed and replaced withexcludeToolMessages. Tool messages (AssistantMessagewith tool call invocations andToolCallMessagewith tool call results) are now preserved in theMessageBagby default.If you were opting in to keep tool messages, just remove the parameter:
-$processor = new AgentProcessor($toolbox, keepToolMessages: true); +$processor = new AgentProcessor($toolbox);If you were explicitly discarding tool messages, use the new parameter:
-$processor = new AgentProcessor($toolbox, keepToolMessages: false); +$processor = new AgentProcessor($toolbox, excludeToolMessages: true);If you were relying on the previous default (tool messages discarded), opt in explicitly:
-$processor = new AgentProcessor($toolbox); +$processor = new AgentProcessor($toolbox, excludeToolMessages: true); -
The
Symfony\AI\Agent\Toolbox\Tool\Agentclass has been renamed toSymfony\AI\Agent\Toolbox\Tool\Subagent:-use Symfony\AI\Agent\Toolbox\Tool\Agent; +use Symfony\AI\Agent\Toolbox\Tool\Subagent; -$agentTool = new Agent($agent); +$subagent = new Subagent($agent);
AI Bundle
-
The service ID prefix for agent tools wrapping sub-agents has changed from
ai.toolbox.{agent}.agent_wrapper.toai.toolbox.{agent}.subagent.:-$container->get('ai.toolbox.my_agent.agent_wrapper.research_agent'); +$container->get('ai.toolbox.my_agent.subagent.research_agent'); -
An indexer configured with a
source, now wraps the indexer with aSymfony\AI\Store\Indexer\ConfiguredSourceIndexerdecorator. This is transparent - the configured source is still used by default, but can be overridden by passing a source toindex(). -
The
host_urlparameter forOllamaplatform has been renamedendpoint.
Platform
- The
hostUrlparameter forOllamaClienthas been removed - The
hostparameter forOllamaApiCataloghas been removed - The
hostUrlparameter forPlatformFactory::create()inOllamahas been renamed toendpoint
Store
-
The
Symfony\AI\Store\Indexerclass has been replaced with two specialized implementations:Symfony\AI\Store\Indexer\SourceIndexer: For indexing from sources (file paths, URLs, etc.) using aLoaderInterfaceSymfony\AI\Store\Indexer\DocumentIndexer: For indexing documents directly without a loader
-use Symfony\AI\Store\Indexer; +use Symfony\AI\Store\Indexer\SourceIndexer; -$indexer = new Indexer($loader, $vectorizer, $store, '/path/to/source'); -$indexer->index(); +$indexer = new SourceIndexer($loader, $vectorizer, $store); +$indexer->index('/path/to/file');For indexing documents directly:
use Symfony\AI\Store\Document\TextDocument;use Symfony\AI\Store\Indexer\DocumentIndexer; $indexer = new DocumentIndexer($processor); $indexer->index(new TextDocument($id, 'content')); $indexer->index([$document1, $document2]); -
The
Symfony\AI\Store\ConfiguredIndexerclass has been renamed toSymfony\AI\Store\Indexer\ConfiguredSourceIndexer:-use Symfony\AI\Store\ConfiguredIndexer; +use Symfony\AI\Store\Indexer\ConfiguredSourceIndexer; -$indexer = new ConfiguredIndexer($innerIndexer, 'default-source'); +$indexer = new ConfiguredSourceIndexer($sourceIndexer, 'default-source'); -
The
Symfony\AI\Store\IndexerInterface::index()method signature has changed - the input parameter is no longer nullable:-public function index(string|iterable|null $source = null, array $options = []): void; +public function index(string|iterable|object $input, array $options = []): void; -
The
Symfony\AI\Store\IndexerInterface::withSource()method has been removed. Use the$sourceparameter ofindex()instead:-$indexer->withSource('/new/source')->index(); +$indexer->index('/new/source'); -
The
Symfony\AI\Store\Document\TextDocumentandSymfony\AI\Store\Document\VectorDocumentclasses now only acceptstringorinttypes for the$idproperty. TheUuidtype has been removed and auto-casting touuidin store bridges has been removed as well:use Symfony\AI\Store\Document\TextDocument; use Symfony\AI\Store\Document\VectorDocument; use Symfony\Component\Uid\Uuid; -$textDoc = new TextDocument(Uuid::v4(), 'content'); +$textDoc = new TextDocument(Uuid::v4()->toString(), 'content'); -$vectorDoc = new VectorDocument(Uuid::v4(), [0.1, ...]); +$vectorDoc = new VectorDocument(Uuid::v4()->toString(), [0.1, ...]); -
The
Symfony\AI\Store\Document\EmbeddableDocumentInterface::getId()can only returnstringorinttypes now. -
Properties of
Symfony\AI\Store\Document\VectorDocumentandSymfony\AI\Store\Document\Loader\Rss\RssItemhave been changed to private, use getters instead of public properties:-$document->id; -$document->metadata; -$document->score; -$document->vector; +$document->getId(); +$document->getMetadata(); +$document->getScore(); +$document->getVector(); -
The
StoreInterface::query()method signature has changed to accept aQueryInterfaceinstead of aVector:-use Symfony\AI\Platform\Vector\Vector; +use Symfony\AI\Store\Query\VectorQuery; -$results = $store->query($vector, ['limit' => 10]); +$results = $store->query(new VectorQuery($vector), ['limit' => 10]);The Store component now supports multiple query types through a query abstraction system. Available query types:
VectorQuery: For vector similarity search (replaces the oldVectorparameter)TextQuery: For full-text search (when supported by the store)HybridQuery: For combined vector + text search (when supported by the store)
Check if a store supports a specific query type using the new
supports()method:if ($store->supports(VectorQuery::class)) { $results = $store->query(new VectorQuery($vector)); } -
The
Symfony\AI\Store\Retrieverconstructor signature has changed - the first two arguments have been swapped:-use Symfony\AI\Store\Retriever; - -$retriever = new Retriever($vectorizer, $store, $logger); +$retriever = new Retriever($store, $vectorizer, $logger);This change aligns the constructor with the primary dependency (the store) being first, followed by the optional vectorizer.
UPGRADE FROM 0.2 to 0.3
Agent
- The
Symfony\AI\Agent\Toolbox\StreamResultclass has been removed in favor of aStreamListener. Checks should now targetSymfony\AI\Platform\Result\StreamResultinstead. - The
Symfony\AI\Agent\Toolbox\Source\SourceMapclass has been renamed toSourceCollection. Its methods have also been renamed:getSources()is nowall()addSource()is nowadd()
- The third argument of the
Symfony\AI\Agent\Toolbox\ToolResult::__construct()method now expects aSourceCollectioninstead of anarray<int, Source>
Platform
-
The
TokenUsageAggregation::__construct()method signature has changed from variadic to accept an array ofTokenUsageInterface-$aggregation = new TokenUsageAggregation($usage1, $usage2); +$aggregation = new TokenUsageAggregation([$usage1, $usage2]); -
The
Symfony\AI\Platform\CachedPlatformhas been renamedSymfony\AI\Platform\Bridge\Cache\CachePlatform- To use it, consider the following steps:
- Run
composer require symfony/ai-cache-platform - Change
Symfony\AI\Platform\CachedPlatformnamespace usages toSymfony\AI\Platform\Bridge\Cache\CachePlatform - The
ttloption can be used in the configuration
- Run
- To use it, consider the following steps:
-
Adopt usage of class
Symfony\AI\Platform\Serializer\StructuredOutputSerializertoSymfony\AI\Platform\StructuredOutput\Serializer
UPGRADE FROM 0.1 to 0.2
AI Bundle
-
Agents are now injected using their configuration name directly, instead of appending Agent or MultiAgent
public function __construct( - private AgentInterface $blogAgent, + private AgentInterface $blog, - private AgentInterface $supportMultiAgent, + private AgentInterface $support, ) {}
Agent
-
Constructor of
MemoryInputProcessornow accepts an iterable of inputs instead of variadic arguments.use Symfony\AI\Agent\Memory\MemoryInputProcessor; // Before $processor = new MemoryInputProcessor($input1, $input2); // After $processor = new MemoryInputProcessor([$input1, $input2]);
Platform
-
The
ChoiceResult::__construct()method signature has changed from variadic to accept an array ofResultInterfaceuse Symfony\AI\Platform\Result\ChoiceResult; // Before $choiceResult = new ChoiceResult($result1, $result2); // After $choiceResult = new ChoiceResult([$result1, $result2]);
Store
-
The
StoreInterface::remove()method was added to the interfacepublic function remove(string|array $ids, array $options = []): void; // Usage $store->remove('vector-id-1'); $store->remove(['vid-1', 'vid-2']); -
The
StoreInterface::add()method signature has changed from variadic to accept a single document or an arrayBefore:
public function add(VectorDocument ...$documents): void; // Usage $store->add($document1, $document2); $store->add(...$documents);After:
public function add(VectorDocument|array $documents): void; // Usage $store->add($document); $store->add([$document1, $document2]); $store->add($documents);