AI Assistant runner fatals when an ai_agent config entity shares its ID with a code-defined AiAgent plugin
Project: AI\
Component: AI Assistant API\
Version: 1.4.x\
Issue type: Bug
**Problem/Motivation**
Drupal\\ai_assistant_api\\Service\\AgentRunner::runAsAgent() resolves the assistant's underlying agent through the plugin.manager.ai_agents plugin manager:
// AgentRunner.php:74\\
$agent = this-\>aiAgentPluginManager-\>createInstance(assistant_id);
The plugin manager surfaces two kinds of definitions under a single ID namespace:
1. Code-defined AiAgent plugins, which extend AiAgentBase.
2. Config-entity agents, which are wrapped at discovery time in AiAgentEntityWrapper.
When a code plugin and a config entity share the same ID, createInstance() returns the code-plugin instance, silently shadowing the config entity. This was verified locally:
get_class(\\Drupal::service('plugin.manager.ai_agents')-\>createInstance('taxonomy_agent'));\
// Drupal\\ai_agents\\Plugin\\AiAgent\\TaxonomyAgent (extends AiAgentBase)
get_class(\\Drupal::service('plugin.manager.ai_agents')-\>createInstance('taxonomy_agent_config'));\
// Drupal\\ai_agents\\PluginBase\\AiAgentEntityWrapper
The runner's $agent parameter is documented as ConfigAiAgentInterface (see the property docblock on AgentRunner.php:21) and the method body calls into that interface throughout: setProgressThreadId(), setLooped(), fromArray(), toArray(), isFinished(), getChatHistory(). None of those exist on AiAgentBase. As a result, any assistant whose ai_agent value resolves to a code plugin fatals on the first such call. With the setProgressThreadId() call added in #3582030, that's now the very first method invoked after createInstance():
Error: Call to undefined method Drupal\\ai_agents\\Plugin\\AiAgent\\TaxonomyAgent::setProgressThreadId() in Drupal\\ai_assistant_api\\Service\\AgentRunner-\>runAsAgent()\
(line 80 of …/ai_assistant_api/src/Service/AgentRunner.php)
If that call is guarded, the next bare-property access surfaces:
Error: Typed property Drupal\\ai_agents\\PluginBase\\AiAgentBase::$task\
must not be accessed before initialization in AiAgentBase-\>getTask()
The module's own shipped default config sidesteps this by suffixing the config entity it ships (config/install/ai_agents.ai_agent.taxonomy_agent_config.yml) with \_config to avoid colliding with the taxonomy_agent code-plugin\
ID. But nothing in the module enforces this — an admin can create a config entity with any ID via /admin/config/ai/ai_agents, including one that collides with a code-plugin ID, and the failure mode is opaque (the assistant\
form still works, the dropdown still shows the entity, the chatbot just fatals at runtime).
**Steps to reproduce**
1. Install ai, ai_assistant_api, ai_agents, and a provider (e.g. ai_provider_openai).
2. At /admin/config/ai/ai_agents, create a new agent with ID taxonomy_agent (same as the shipped code plugin). Save.
3. At /admin/config/ai/ai_assistants, create an assistant and select that agent.
4. Open the chatbot and send any message.
**Expected**
The assistant runner always operates on a ConfigAiAgentInterface instance. Either:
- the entity form rejects IDs that collide with existing code-plugin IDs at save time, or
- the runner loads the agent through ai_agent entity storage and wraps it explicitly, rather than going through the plugin manager.
**Actual**
AgentRunner::runAsAgent() receives the code-plugin instance and fatals on the first ConfigAiAgentInterface-only method it touches.
**Proposed resolution**
Two complementary changes:
1. In AgentRunner::runAsAgent(), resolve the agent through entity storage instead of the plugin manager, then construct the wrapper directly. This eliminates the ambiguity at the call site that actually requires the\
config-entity contract.
2. In the AiAgent entity form (ai_agents module), validate that the chosen machine name does not collide with an existing code-plugin ID at save time, with a clear error message pointing the admin at a non-colliding name (the same pattern the module uses internally with the \_config suffix).
The first change alone fixes the runtime fatal. The second prevents new instances of the same misconfiguration from being created in the future.
**Workaround**
Point any affected assistant at a non-colliding config-entity ID (e.g. the module's shipped taxonomy_agent_config):
drush config:set ai_assistant_api.ai_assistant. ai_agent \<non_colliding_id\>\
drush cr
If a colliding config entity exists, delete it as well:
drush config:delete ai_agents.ai_agent.\<colliding_id\>
**Related**
- #3582030 introduced the setProgressThreadId() call that now surfaces this bug on the very first method invocation. Before that commit the collision was equally broken in principle —\
fromArray()/toArray()/isFinished()/getChatHistory() were also missing on AiAgentBase — but the runner happened to reach a different uninitialized-property fatal (AiAgentBase::$task) rather than an undefined-method fatal, making the misconfiguration harder to recognize.
- https://git.drupalcode.org/project/ai_agents/-/work_items/3586026
issue