feat: #3586638 Add AI request tag registry with static and dynamic discovery

Description

Adds a tag registry service (ai.tag_registry) so modules can declare, up front, which AI request tags they may emit — closing the gap described in #3586638, where tags are free-form strings nobody declares until a request has already run.

Two discovery paths feed the registry:

  • Static — a new hook_ai_tags_info() (documented in the new ai.api.php, this module had none before). Any module — OOP #[Hook] class or legacy procedural .module function — can implement it. ai core registers its own known tags in src/Hook/AiTags.php (operation types pulled from AiProviderPluginManager::getOperationTypes(), plus ai and short_term_memory).
  • Dynamic — a new AiTagProvider plugin type (src/Attribute/AiTagProvider.php + src/AiTag/AiTagProviderPluginManager.php), modeled on the existing AiGuardrail plugin pattern. Each plugin watches one config entity type and derives concrete tags from live entities of that type. ai_assistant_api ships the reference implementation (AiAssistantMessageTagProvider): an AiAssistant with id foo implies the tag ai_assistant_api_assistant_message_foo, matching the literal already emitted by AiAssistantApiRunner. The registry recomputes automatically on entity CRUD via the entity's list cache tags — no custom entity-hook subscriber needed.

AiTagDefinition (src/AiTag/AiTagDefinition.php) carries a kind (AiTagKind::CallSite vs Correlation) so consumers can tell a stable, selectable tag (ai_agent_foo_request) apart from a high-cardinality correlation tag (ai_assistant_thread_{thread_id}) that should never reach a tag selector.

Scope note: per the issue's own triage, this MR is the registry core + static discovery + one dynamic example end to end. Migrating ai_external_moderation/ai_observability's free-text tag fields to registry-backed selectors, a Drush command, and an admin listing page are explicitly left as follow-ups — nothing about how tags are emitted or collected at request time (ProviderProxy, AiProviderInterface, AiProviderRequestBaseEvent) changes in this MR.

Closes #3586638

Testing instructions

This feature doesn't call any AI provider, so no API key setup is needed — the registry only reasons about which tags could be sent, not about making requests.

  1. Enable ai and ai_assistant_api.
  2. Run the automated coverage: vendor/bin/phpunit -c core modules/contrib/ai/tests/src/Kernel/AiTag/AiTagRegistryTest.php — asserts static tags are present, a per-assistant tag appears/disappears with the entity's lifecycle, and hook_ai_tags_info_alter() works.
  3. Manual (optional): create an ai_assistant config entity (any id), then inspect \Drupal::service('ai.tag_registry')->getDefinitions() — a ai_assistant_api_assistant_message_<id> entry should appear. Delete the entity and confirm it disappears.

Checklist

  • I have linked the related issue in the MR title or description
  • I have performed a self-review of my own code
  • I have added or updated tests, or explained in the description why this change is not covered by tests
  • I have updated documentation for any new or changed functionality
  • I have written testing instructions and verified them locally
  • I have noted any required post-merge steps (config imports, cache rebuilds, manual changes) — none beyond the routine cache rebuild that follows any code deploy
  • This MR contains no breaking API or hook changes, or they are explicitly documented in the description

AI Compliance

Note

Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.

  • AI Assisted Code
    Mainly written by a human; AI used for autocomplete or partial generation under full human supervision.

  • AI Generated Code
    Mainly generated by AI, reviewed and approved by a human before this MR was created.

  • Vibe Coded
    Generated by AI and only functionally reviewed before this MR was created.

Merge request reports

Loading