Files
archived-ai/UPGRADE.md
Christopher Hertel 4d9639c760 bug #1794 [Agent] Fix dispatch of multiple tool instances of the same class (chr-hertel)
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
2026-03-22 23:44:17 +01:00

12 KiB

UPGRADE FROM 0.6 to 0.7

Agent

  • The Symfony\AI\Agent\Toolbox\ToolFactory\AbstractToolFactory class has been removed. If you extended it to create a custom tool factory, implement ToolFactoryInterface yourself and use Symfony\AI\Platform\Contract\JsonSchema\Factory directly if needed.

  • The ToolFactoryInterface::getTool() method signature has changed to accept object|string instead of string:

    -public function getTool(string $reference): iterable;
    +public function getTool(object|string $reference): iterable;
    

AI Bundle

  • The api_catalog option for Ollama has been removed as the catalog is now automatically fetched from the Ollama server
  • The api_key option for Ollama is now null by default to allow the usage of a ScopingHttpClient
  • The endpoint option for Ollama is now null by default to allow the usage of a ScopingHttpClient

Platform

  • ModelCatalog in Ollama has been replaced by OllamaApiCatalog
  • OllamaApiCatalog in Ollama has been replaced to ModelCatalog
  • Ollama model is now final

Store

  • The Retriever constructor has a new ?EventDispatcherInterface $eventDispatcher parameter inserted as 3rd argument before LoggerInterface $logger. If you pass $logger positionally, 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_key option for ElevenLabs is not required anymore if a ScopedHttpClient is used in http_client option

Platform

  • The PlatformFactory is now in charge of creating ElevenLabsApiCatalog if apiCatalog is provided as true
  • Symfony\AI\Platform\TokenUsage\TokenUsageInterface has two new methods: getCacheCreationTokens() and getCacheReadTokens()

UPGRADE FROM 0.4 to 0.5

Platform

  • The hostUrl parameter for ElevenLabsClient has been removed
  • The host parameter for ElevenLabsApiCatalog has been removed
  • The hostUrl parameter for PlatformFactory::create() in ElevenLabs has been renamed to endpoint

UPGRADE FROM 0.3 to 0.4

Agent

  • The keepToolMessages parameter of AgentProcessor has been removed and replaced with excludeToolMessages. Tool messages (AssistantMessage with tool call invocations and ToolCallMessage with tool call results) are now preserved in the MessageBag by 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\Agent class has been renamed to Symfony\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. to ai.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 a Symfony\AI\Store\Indexer\ConfiguredSourceIndexer decorator. This is transparent - the configured source is still used by default, but can be overridden by passing a source to index().

  • The host_url parameter for Ollama platform has been renamed endpoint.

Platform

  • The hostUrl parameter for OllamaClient has been removed
  • The host parameter for OllamaApiCatalog has been removed
  • The hostUrl parameter for PlatformFactory::create() in Ollama has been renamed to endpoint

Store

  • The Symfony\AI\Store\Indexer class has been replaced with two specialized implementations:

    • Symfony\AI\Store\Indexer\SourceIndexer: For indexing from sources (file paths, URLs, etc.) using a LoaderInterface
    • Symfony\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\ConfiguredIndexer class has been renamed to Symfony\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 $source parameter of index() instead:

    -$indexer->withSource('/new/source')->index();
    +$indexer->index('/new/source');
    
  • The Symfony\AI\Store\Document\TextDocument and Symfony\AI\Store\Document\VectorDocument classes now only accept string or int types for the $id property. The Uuid type has been removed and auto-casting to uuid in 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 return string or int types now.

  • Properties of Symfony\AI\Store\Document\VectorDocument and Symfony\AI\Store\Document\Loader\Rss\RssItem have 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 a QueryInterface instead of a Vector:

    -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 old Vector parameter)
    • 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\Retriever constructor 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\StreamResult class has been removed in favor of a StreamListener. Checks should now target Symfony\AI\Platform\Result\StreamResult instead.
  • The Symfony\AI\Agent\Toolbox\Source\SourceMap class has been renamed to SourceCollection. Its methods have also been renamed:
    • getSources() is now all()
    • addSource() is now add()
  • The third argument of the Symfony\AI\Agent\Toolbox\ToolResult::__construct() method now expects a SourceCollection instead of an array<int, Source>

Platform

  • The TokenUsageAggregation::__construct() method signature has changed from variadic to accept an array of TokenUsageInterface

    -$aggregation = new TokenUsageAggregation($usage1, $usage2);
    +$aggregation = new TokenUsageAggregation([$usage1, $usage2]);
    
  • The Symfony\AI\Platform\CachedPlatform has been renamed Symfony\AI\Platform\Bridge\Cache\CachePlatform

    • To use it, consider the following steps:
      • Run composer require symfony/ai-cache-platform
      • Change Symfony\AI\Platform\CachedPlatform namespace usages to Symfony\AI\Platform\Bridge\Cache\CachePlatform
      • The ttl option can be used in the configuration
  • Adopt usage of class Symfony\AI\Platform\Serializer\StructuredOutputSerializer to Symfony\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 MemoryInputProcessor now 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 of ResultInterface

    use 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 interface

    public 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 array

    Before:

    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);