<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Drupal AI</title>
    <description>Stay informed about developments in Drupal AI.</description>
    <link>https://www.drupal.org/project/drupal-ai</link>
    <item>
      <title>#3578417: AI Automators uninstall leaves ai_automator_status field and related configuration behind</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3578417.md</link>
      <guid isPermaLink="false">019d8709-bf3e-7213-b98f-7ed03f3348fa</guid>
      <pubDate>Mon, 13 Apr 2026 09:44:11 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/7976436675da257cf0cf5047b404f1466300f104"&gt;7976436&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3578417"&gt;#3578417&lt;/a&gt; · 7 contributors · 33 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 10&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When the AI Automators module was uninstalled, the &lt;code&gt;ai_automator_status&lt;/code&gt; field it had created -- along with its field storage config, field instance config, and references in entity form and view displays -- was left behind on the site. This meant the &amp;quot;AI Automator Status&amp;quot; field kept appearing in Manage Fields for any bundle it had been attached to, and leftover configuration polluted config exports. The fix ensures that all &lt;code&gt;ai_automator_status&lt;/code&gt; configuration is fully removed when the module is uninstalled.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The root cause was that the &lt;code&gt;ai_automator_status&lt;/code&gt; &lt;code&gt;FieldStorageConfig&lt;/code&gt; and &lt;code&gt;FieldConfig&lt;/code&gt; entities created by &lt;code&gt;AiAutomatorStatusField::modifyStatusField()&lt;/code&gt; were not declaring an enforced dependency on the &lt;code&gt;ai_automators&lt;/code&gt; module. Drupal's config system uses enforced dependencies to automatically remove config entities when their owning module is uninstalled; without them, the field configs survived uninstall.&lt;/p&gt;
&lt;p&gt;The fix operates on three levels:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Enforced dependencies on new field creation (&lt;code&gt;AiAutomatorStatusField.php&lt;/code&gt;).&lt;/strong&gt; When &lt;code&gt;modifyStatusField()&lt;/code&gt; creates a new &lt;code&gt;FieldStorageConfig&lt;/code&gt; it now includes &lt;code&gt;dependencies.enforced.module: [ai_automators]&lt;/code&gt;. If the storage already exists, the new &lt;code&gt;ensureModuleDependency(ConfigEntityInterface $configEntity)&lt;/code&gt; helper method checks for and adds the enforced dependency. The same enforced dependency is set on the &lt;code&gt;FieldConfig&lt;/code&gt; at creation time. A new &lt;code&gt;MODULE_DEPENDENCY&lt;/code&gt; constant holds the &lt;code&gt;'ai_automators'&lt;/code&gt; string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Selective &lt;code&gt;hook_uninstall()&lt;/code&gt; for &lt;code&gt;field_config&lt;/code&gt; (&lt;code&gt;ai_automators.install&lt;/code&gt;).&lt;/strong&gt; Drupal's config system automatically removes &lt;code&gt;field_storage_config&lt;/code&gt; entities that carry an enforced dependency during uninstall, but does NOT do the same for &lt;code&gt;field_config&lt;/code&gt; entities (a subtlety discovered during implementation). &lt;code&gt;hook_uninstall()&lt;/code&gt; therefore retains an explicit deletion loop -- now scoped to only &lt;code&gt;field_config&lt;/code&gt; and using the correct &lt;code&gt;field_name&lt;/code&gt; query condition instead of the previous incorrect &lt;code&gt;label&lt;/code&gt; condition. Entity form and view display references are cleaned up implicitly by Drupal's config dependency cascade when the field config is removed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Post-update hooks to backfill existing installations (&lt;code&gt;ai_automators.post_update.php&lt;/code&gt;).&lt;/strong&gt; Two new post-update hooks using &lt;code&gt;ConfigEntityUpdater&lt;/code&gt; handle sites that already have &lt;code&gt;ai_automator_status&lt;/code&gt; fields in place: &lt;code&gt;ai_automators_post_update_13001&lt;/code&gt; iterates over all &lt;code&gt;field_storage_config&lt;/code&gt; entities and adds the enforced dependency where it is missing; &lt;code&gt;ai_automators_post_update_13002&lt;/code&gt; does the same for &lt;code&gt;field_config&lt;/code&gt; entities. Post-update hooks were chosen over regular update hooks on the recommendation of project maintainer fago, because only post-update hooks guarantee the system is in a fully reliable state for entity API operations.&lt;/p&gt;
&lt;p&gt;A new kernel test, &lt;code&gt;AiAutomatorsUninstallCleanupTest&lt;/code&gt;, covers the full lifecycle: create the media type and automator config, confirm &lt;code&gt;ai_automator_status&lt;/code&gt; field storage and field config exist with display components, uninstall the module, and assert that all field config and display references are gone.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Sites running AI Automators that already have an &lt;code&gt;ai_automator_status&lt;/code&gt; field need to run database updates so that the backfill post-update hooks add the enforced module dependency to existing field config entities. Without this, the field will still be left behind on the next uninstall.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;drush updb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or visit &lt;strong&gt;Administration &amp;gt; Reports &amp;gt; Available updates&lt;/strong&gt; and run updates from the UI. The two post-update hooks that run are &lt;code&gt;ai_automators_post_update_13001&lt;/code&gt; (field storage configs) and &lt;code&gt;ai_automators_post_update_13002&lt;/code&gt; (field instance configs).&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 10, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (1 month and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;cadence96 (SeeD EM)&lt;/li&gt;
&lt;li&gt;annmarysruthy (QED42)&lt;/li&gt;
&lt;li&gt;fago (drunomics)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Two contributions during review meaningfully shaped the final design. First, the issue reporter cadence96 tested an early patch and flagged that while field cleanup worked, the uninstall confirmation page showed nothing -- because without enforced dependencies Drupal had no way to surface what would be removed. That observation drove the enforced-dependency approach. Second, maintainer fago reviewed the implementation and insisted on converting a regular update hook to post-update hooks, explaining that only post-update hooks guarantee the system is in a reliable state for entity operations. During that conversion, a.dmitriiev discovered that Drupal's config system auto-removes &lt;code&gt;field_storage_config&lt;/code&gt; via enforced dependencies on uninstall but does NOT do the same for &lt;code&gt;field_config&lt;/code&gt; -- which is why &lt;code&gt;hook_uninstall()&lt;/code&gt; had to be kept for the latter, while the post-update hooks handle backfilling both types for existing sites.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582743: Set temp directory in tokenizer</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3582743.md</link>
      <guid isPermaLink="false">019d870d-5d28-75a3-aafb-77a6b40cbfef</guid>
      <pubDate>Mon, 13 Apr 2026 09:36:31 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Major bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/dcc1f9eb68fa86e1767c2ee204fae6116d716452"&gt;dcc1f9e&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582743"&gt;#3582743&lt;/a&gt; · 4 contributors · 17 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 5&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The AI module's tokenizer was writing vocabulary cache files to the hard-coded &lt;code&gt;/tmp&lt;/code&gt; directory instead of the server's configured PHP temporary directory. On hosting environments where the temporary directory is set to a non-standard path, those cache files would land in the wrong place or fail silently. The fix ensures the tokenizer now respects the site's configured temp directory, so vocabulary caches go where they are expected on all environments.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;Tokenizer&lt;/code&gt; utility (&lt;code&gt;src/Utility/Tokenizer.php&lt;/code&gt;) wraps &lt;code&gt;Yethee\Tiktoken\EncoderProvider&lt;/code&gt;, a library that downloads and caches large vocabulary files needed for tokenization. When no cache path is given to &lt;code&gt;EncoderProvider&lt;/code&gt;, it defaults to &lt;code&gt;/tmp&lt;/code&gt; regardless of PHP's &lt;code&gt;sys_get_temp_dir()&lt;/code&gt; or Drupal's file system settings.&lt;/p&gt;
&lt;p&gt;The fix calls &lt;code&gt;EncoderProvider::setVocabCache()&lt;/code&gt; in &lt;code&gt;Tokenizer::__construct()&lt;/code&gt;, passing the result of &lt;code&gt;FileSystemInterface::getTempDirectory()&lt;/code&gt;. Rather than adding &lt;code&gt;FileSystemInterface&lt;/code&gt; as a new constructor argument (which would have broken the existing API), a new &lt;code&gt;FileSystemTrait&lt;/code&gt; was introduced at &lt;code&gt;src/Traits/File/FileSystemTrait.php&lt;/code&gt;. The trait provides a &lt;code&gt;getFileSystem()&lt;/code&gt; method that resolves the &lt;code&gt;file_system&lt;/code&gt; service via &lt;code&gt;\Drupal::service('file_system')&lt;/code&gt;. &lt;code&gt;Tokenizer&lt;/code&gt; now uses this trait, and its constructor calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;$temp_directory = $this-&amp;gt;getFileSystem()-&amp;gt;getTempDirectory();
$this-&amp;gt;encoderProvider-&amp;gt;setVocabCache($temp_directory);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new kernel test class &lt;code&gt;TokenizerTest&lt;/code&gt; was added at &lt;code&gt;tests/src/Kernel/Utility/TokenizerTest.php&lt;/code&gt;, covering tokenization consistency, different-input behavior, &lt;code&gt;countTokens&lt;/code&gt;, empty-string handling, a missing-model error path, the fallback encoder for unrecognized model names, and a specific test (&lt;code&gt;testVocabCacheStoredInTempDirectory&lt;/code&gt;) that verifies vocabulary files are written under the PHP temp directory after construction.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 13, 2026 (11 days and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;jan kellermann (werk21)&lt;/li&gt;
&lt;li&gt;avinash.jha&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;avinash.jha suggested constructor injection of &lt;code&gt;FileSystemInterface&lt;/code&gt; as the idiomatic Drupal approach. marcus_johansson chose a trait instead, explicitly noting it was a &amp;quot;non-breaking change&amp;quot;, then added the accompanying kernel tests for the tokenizer service. jan kellermann (the original reporter) confirmed the patch worked on their environment.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3573429: Processing automators does not catch all errors</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3573429.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-9474a4c42ff7</guid>
      <category>Module maintainers</category>
      <pubDate>Thu, 09 Apr 2026 16:38:43 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/e970c92cd8e4cc927a7ab704e634b964cc0a18ac"&gt;e970c92&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3573429"&gt;#3573429&lt;/a&gt; · 8 contributors · 28 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 9&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The OpenAI provider in the AI module could not catch PHP errors (like &lt;code&gt;\TypeError&lt;/code&gt;) thrown by the OpenAI PHP client when it received bad API responses. These errors bypassed the existing &lt;code&gt;catch (\Exception)&lt;/code&gt; blocks because &lt;code&gt;\Error&lt;/code&gt; does not extend &lt;code&gt;\Exception&lt;/code&gt; in PHP. The provider layer now catches &lt;code&gt;\Throwable&lt;/code&gt; and wraps &lt;code&gt;\Error&lt;/code&gt; instances in &lt;code&gt;AiResponseErrorException&lt;/code&gt;, so all consumers (automators, chatbot, API explorer, etc.) receive a proper exception they can handle.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; &lt;code&gt;OpenAiBasedProviderClientBase::handleApiException()&lt;/code&gt; parameter type changed from &lt;code&gt;\Exception&lt;/code&gt; to &lt;code&gt;\Throwable&lt;/code&gt;; PHP &lt;code&gt;\Error&lt;/code&gt; instances are now wrapped in &lt;code&gt;AiResponseErrorException&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenAiBasedProviderClientBase&lt;/code&gt; is the shared base class for OpenAI-compatible AI providers. Its API methods (&lt;code&gt;chat()&lt;/code&gt;, &lt;code&gt;moderation()&lt;/code&gt;, &lt;code&gt;textToImage()&lt;/code&gt;, &lt;code&gt;textToSpeech()&lt;/code&gt;, &lt;code&gt;speechToText()&lt;/code&gt;, &lt;code&gt;embeddings()&lt;/code&gt;) each wrap SDK calls in try/catch blocks and route errors through a central &lt;code&gt;handleApiException()&lt;/code&gt; method. Both the catch blocks and &lt;code&gt;handleApiException()&lt;/code&gt; used &lt;code&gt;\Exception&lt;/code&gt; as the type, so PHP &lt;code&gt;\Error&lt;/code&gt; subclasses (like &lt;code&gt;\TypeError&lt;/code&gt; thrown by &lt;code&gt;CreateResponse::from()&lt;/code&gt; when the API returns a string instead of an array) were uncaught and caused fatal errors.&lt;/p&gt;
&lt;p&gt;The fix has two parts. First, all seven catch blocks in the provider methods changed from &lt;code&gt;catch (\Exception)&lt;/code&gt; to &lt;code&gt;catch (\Throwable)&lt;/code&gt;, and &lt;code&gt;handleApiException()&lt;/code&gt; now accepts &lt;code&gt;\Throwable&lt;/code&gt;. When the throwable is an &lt;code&gt;\Error&lt;/code&gt; instance, it is wrapped in &lt;code&gt;AiResponseErrorException&lt;/code&gt; with the original error preserved as the previous exception. Regular exceptions and the existing rate-limit/quota detection pass through unchanged.&lt;/p&gt;
&lt;p&gt;Second, the try block scope was narrowed in &lt;code&gt;moderation()&lt;/code&gt;, &lt;code&gt;textToSpeech()&lt;/code&gt;, &lt;code&gt;speechToText()&lt;/code&gt;, and &lt;code&gt;embeddings()&lt;/code&gt;. Response object construction (e.g. &lt;code&gt;new ModerationOutput(...)&lt;/code&gt;, &lt;code&gt;new SpeechToTextOutput(...)&lt;/code&gt;) was moved outside the try block so that only the SDK call is covered. A bug in that local code now surfaces as what it is rather than being caught and rethrown as a provider API error.&lt;/p&gt;
&lt;p&gt;The initial proposal was to catch &lt;code&gt;\Throwable&lt;/code&gt; in the automator processors (&lt;code&gt;DirectSaveProcessing&lt;/code&gt;, &lt;code&gt;FieldWidgetProcessing&lt;/code&gt;). A reviewer pointed out that fixing it at the provider layer protects all consumers in one place. An intermediate version kept &lt;code&gt;\Throwable&lt;/code&gt; at both levels as defense in depth, but the automator-level &lt;code&gt;\Throwable&lt;/code&gt; was reverted back to &lt;code&gt;\Exception&lt;/code&gt; because the provider now wraps &lt;code&gt;\Error&lt;/code&gt; in &lt;code&gt;AiResponseErrorException&lt;/code&gt; (which extends &lt;code&gt;\Exception&lt;/code&gt;), making the broader catch unnecessary and potentially harmful for debugging local bugs in the automator call chain.&lt;/p&gt;
&lt;p&gt;New tests: a unit test for &lt;code&gt;handleApiException()&lt;/code&gt; covering &lt;code&gt;\TypeError&lt;/code&gt; wrapping, &lt;code&gt;\ValueError&lt;/code&gt; wrapping, original error preservation, regular exception rethrow, and rate-limit/quota detection; kernel tests for both &lt;code&gt;DirectSaveProcessing&lt;/code&gt; and &lt;code&gt;FieldWidgetProcessing&lt;/code&gt; verifying they handle &lt;code&gt;AiResponseErrorException&lt;/code&gt; and regular &lt;code&gt;\Exception&lt;/code&gt; gracefully (return &lt;code&gt;FALSE&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Subclasses of &lt;code&gt;OpenAiBasedProviderClientBase&lt;/code&gt; that override &lt;code&gt;handleApiException()&lt;/code&gt; must update the parameter type:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Before:
protected function handleApiException(\Exception $e): void {

// After:
protected function handleApiException(\Throwable $e): void {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code that catches exceptions from the provider should already work, since &lt;code&gt;AiResponseErrorException&lt;/code&gt; extends &lt;code&gt;\Exception&lt;/code&gt;. No changes needed for callers that catch &lt;code&gt;\Exception&lt;/code&gt; or &lt;code&gt;AiResponseErrorException&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: February 13, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 month and 6 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;fago (drunomics)&lt;/li&gt;
&lt;li&gt;petar_basic (drunomics)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;li&gt;harivansh (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;abhisekmazumdar (Dropsolid)&lt;/li&gt;
&lt;li&gt;jibran (Morpht)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The initial fix by fago caught &lt;code&gt;\Throwable&lt;/code&gt; at the automator processor level. petar_basic suggested fixing it at the provider layer instead, so all consumers benefit in one place. ajv009 implemented both levels, but abhisekmazumdar reverted the automator-level &lt;code&gt;\Throwable&lt;/code&gt; back to &lt;code&gt;\Exception&lt;/code&gt; and narrowed the try blocks to SDK calls only, arguing the broader catch was no longer needed and would make debugging local bugs harder.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3579079: Restrict Topic guardrail silently bypassed due to case-sensitive topic matching</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3579079.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94e27346aeb7</guid>
      <pubDate>Thu, 09 Apr 2026 16:35:27 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/5089c7753034501a8955a246b732ec5e7d641af5"&gt;5089c77&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3579079"&gt;#3579079&lt;/a&gt; · 7 contributors · 23 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 8&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;restrict_to_topic&lt;/code&gt; AI guardrail could silently pass when it should block, because topic matching between configured topics and LLM-classified topics was case-sensitive. Since LLM responses are non-deterministic in casing (e.g. &amp;quot;Legal Advice&amp;quot; vs &amp;quot;legal advice&amp;quot;), a mismatch would leave both &lt;code&gt;$invalid_topics_found&lt;/code&gt; and &lt;code&gt;$valid_topics_found&lt;/code&gt; empty, and the guardrail would pass. Both configured topics and LLM-returned topics are now normalized to lowercase before comparison, so the guardrail works regardless of what casing the LLM returns.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;RestrictToTopic&lt;/code&gt; guardrail plugin classifies user input by sending the prompt text and a list of configured topics to an LLM, which returns a JSON list of &lt;code&gt;topics_present&lt;/code&gt;. The plugin then uses &lt;code&gt;in_array()&lt;/code&gt; to check each returned topic against the configured &lt;code&gt;$valid_topics&lt;/code&gt; and &lt;code&gt;$invalid_topics&lt;/code&gt; arrays. Because &lt;code&gt;in_array()&lt;/code&gt; is case-sensitive by default, any casing difference between the LLM response and the stored config caused neither array to be populated, silently passing the guardrail.&lt;/p&gt;
&lt;p&gt;The fix adds &lt;code&gt;mb_strtolower&lt;/code&gt; via &lt;code&gt;array_map&lt;/code&gt; to three arrays in &lt;code&gt;RestrictToTopic::processInput()&lt;/code&gt;: &lt;code&gt;$valid_topics&lt;/code&gt;, &lt;code&gt;$invalid_topics&lt;/code&gt;, and &lt;code&gt;$topics_present&lt;/code&gt;. The &lt;code&gt;in_array()&lt;/code&gt; comparison logic is unchanged. &lt;code&gt;mb_strtolower&lt;/code&gt; was chosen over &lt;code&gt;strtolower&lt;/code&gt; because topic names are user-configured strings that can contain non-ASCII characters (e.g. Ä, É, ñ), and &lt;code&gt;strtolower&lt;/code&gt; only operates on ASCII. Because normalization happens before &lt;code&gt;$all_topics&lt;/code&gt; is built, the topic list sent to the LLM in the classification prompt is also lowercase, which nudges the model toward returning lowercase output.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 13, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (27 days and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;abhisekmazumdar (Dropsolid)&lt;/li&gt;
&lt;li&gt;annmarysruthy (QED42)&lt;/li&gt;
&lt;li&gt;harivansh (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;breidert (1xINTERNET)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;An initial more complex approach by harivansh introduced a &lt;code&gt;matchConfiguredTopics()&lt;/code&gt; method that preferred exact-case matches first and fell back to case-insensitive matching. After review feedback from breidert that the code was hard to read, ajv009 simplified it back to a straightforward normalize-then-compare approach. harivansh raised a concern about merging topics that differ only by case (e.g. &amp;quot;Apple&amp;quot; valid and &amp;quot;apple&amp;quot; invalid), but ajv009 and the issue author abhisekmazumdar agreed this scenario is not realistic in practice and the simpler approach was correct. abhisekmazumdar then swapped &lt;code&gt;strtolower&lt;/code&gt; for &lt;code&gt;mb_strtolower&lt;/code&gt; to handle multibyte characters, as originally proposed in the issue.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582074: AssertionError: Cannot load the "key" entity with NULL ID</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3582074.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-9515918f572c</guid>
      <pubDate>Thu, 09 Apr 2026 16:17:27 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/2d8811e178039b7180c0d2cf445b315ca4436229"&gt;2d8811e&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582074"&gt;#3582074&lt;/a&gt; · 3 contributors · 17 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Fixed a fatal error (&lt;code&gt;AssertionError: Cannot load the &amp;quot;key&amp;quot; entity with NULL ID&lt;/code&gt;) that occurred when setting up a new Search API server with an AI provider whose API key is not yet configured. The form would crash instead of handling the missing key gracefully. After this fix, a missing API key is logged as an error and the provider continues without authentication where possible.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;When a provider's &lt;code&gt;api_key&lt;/code&gt; config value is not yet set (e.g. during initial Search API server setup), &lt;code&gt;$this-&amp;gt;getConfig()-&amp;gt;get('api_key')&lt;/code&gt; returns NULL. The old &lt;code&gt;loadApiKey()&lt;/code&gt; in &lt;code&gt;AiProviderClientBase&lt;/code&gt; passed this NULL directly to &lt;code&gt;$this-&amp;gt;keyRepository-&amp;gt;getKey(NULL)&lt;/code&gt;, which calls &lt;code&gt;EntityStorageBase::load(NULL)&lt;/code&gt;. Core's &lt;code&gt;load()&lt;/code&gt; method has an &lt;code&gt;assert()&lt;/code&gt; on line 266 that rejects NULL IDs, producing the fatal &lt;code&gt;AssertionError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix has two parts:&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;AiProviderClientBase::loadApiKey()&lt;/code&gt;, the NULL is coalesced to an empty string (&lt;code&gt;?? ''&lt;/code&gt;) before being passed to &lt;code&gt;getKey()&lt;/code&gt;, and the nullsafe operator (&lt;code&gt;?-&amp;gt;&lt;/code&gt;) is used to chain &lt;code&gt;getKeyValue()&lt;/code&gt;. An empty or missing key still throws &lt;code&gt;AiSetupFailureException&lt;/code&gt; as before.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;OpenAiBasedProviderClientBase::createClient()&lt;/code&gt;, the call to &lt;code&gt;loadApiKey()&lt;/code&gt; is now wrapped in a try/catch for &lt;code&gt;AiSetupFailureException&lt;/code&gt;. If the key cannot be loaded, the error is logged via &lt;code&gt;$this-&amp;gt;loggerFactory&lt;/code&gt; and the client factory proceeds without calling &lt;code&gt;withApiKey()&lt;/code&gt;. Previously, the code unconditionally called &lt;code&gt;$clientFactory-&amp;gt;withApiKey($this-&amp;gt;apiKey)&lt;/code&gt; even when authentication setup failed.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 29, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (11 days and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;dany.almeida.kairouz&lt;/li&gt;
&lt;li&gt;b_sharpe (ImageX)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3574611: Forms are broken when automators or FWA are not configurable</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3574611.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-948b78261ade</guid>
      <pubDate>Thu, 09 Apr 2026 16:11:45 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/14c542829576ff8d55a59022b3c3b99c20135592"&gt;14c5428&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3574611"&gt;#3574611&lt;/a&gt; · 3 contributors · 14 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When no AI provider was configured for a given operation type (e.g., text-to-image), the automator form displayed an empty form without any notice and could still be saved. Now, &lt;code&gt;RuleBase::extraAdvancedFormFields()&lt;/code&gt; checks for available providers and shows an inline warning message with a link to the provider configuration docs instead of rendering an empty form.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;ai_automators&lt;/code&gt; module lets site builders attach AI-driven automation rules to entity fields. Each rule type has an associated LLM operation type (chat, text-to-image, etc.) and needs a configured provider to function. The configuration form for these rules is built in the &lt;code&gt;RuleBase&lt;/code&gt; base class, which all automator plugins extend.&lt;/p&gt;
&lt;p&gt;Previously, &lt;code&gt;RuleBase::extraAdvancedFormFields()&lt;/code&gt; called &lt;code&gt;$this-&amp;gt;formHelper-&amp;gt;getAiProvidersOptions($this-&amp;gt;llmType)&lt;/code&gt; and continued building the form regardless of whether any providers were returned. With no providers, the advanced settings section rendered empty, giving the user no feedback. The form could also be saved in this broken state.&lt;/p&gt;
&lt;p&gt;The fix adds an early return when &lt;code&gt;$providers&lt;/code&gt; is empty. It looks up the human-readable label for the operation type via &lt;code&gt;$this-&amp;gt;aiPluginManager-&amp;gt;getOperationType()&lt;/code&gt; and renders a &lt;code&gt;messages--warning&lt;/code&gt; div telling the user to configure a provider. Because the fix is in the base class, it applies to all automator types, not just the text-to-image case where it was first noticed.&lt;/p&gt;
&lt;p&gt;Whether to also block saving the form via validation was raised in comment #7 but deferred to follow-up &lt;a href="https://www.drupal.org/node/3583972"&gt;#3583972&lt;/a&gt; for the 2.x branch.&lt;/p&gt;
&lt;p&gt;A new FunctionalJavascript test (&lt;code&gt;AutomatorStringLongProviderWarningTest&lt;/code&gt;) covers both paths: one test method verifies the warning appears and the provider select is absent when no provider is installed, and another installs the &lt;code&gt;ai_test&lt;/code&gt; module's EchoAI provider and verifies the provider select appears instead.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: February 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 month and 3 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;fago (drunomics)&lt;/li&gt;
&lt;li&gt;annmarysruthy (QED42)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3574519: Fix the tag-release to work on linux</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3574519.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-948a5a8755d9</guid>
      <pubDate>Thu, 09 Apr 2026 16:10:39 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/fe4de554f600d564b7fca9759b8e5be003233035"&gt;fe4de55&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3574519"&gt;#3574519&lt;/a&gt; · 5 contributors · 24 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is an internal tooling change. The &lt;code&gt;scripts/tag-release.sh&lt;/code&gt; script in the Drupal AI module used bash/zsh-specific constructs (&lt;code&gt;pushd&lt;/code&gt;/&lt;code&gt;popd&lt;/code&gt;, &lt;code&gt;[[ ]]&lt;/code&gt;, &lt;code&gt;read -p&lt;/code&gt;) despite its &lt;code&gt;#!/bin/sh&lt;/code&gt; shebang. On Linux, where &lt;code&gt;/bin/sh&lt;/code&gt; is typically a minimal POSIX shell like dash, this caused errors such as &amp;quot;pushd: not found&amp;quot;. The script was rewritten to use only POSIX-compliant shell constructs so it works on both Linux and macOS. The required Node.js version was also updated from v20 to v24+.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;tag-release.sh&lt;/code&gt; script automates creating tagged releases of the Drupal AI module. It clones the repo, builds JS UIs in three subdirectories (&lt;code&gt;modules/ai_ckeditor&lt;/code&gt;, &lt;code&gt;ui/mdxeditor&lt;/code&gt;, &lt;code&gt;ui/json-schema-editor&lt;/code&gt;), commits the build artifacts, creates a git tag, removes the artifacts, and pushes.&lt;/p&gt;
&lt;p&gt;The original script declared &lt;code&gt;#!/bin/sh&lt;/code&gt; but relied on bash/zsh features. On Linux where &lt;code&gt;/bin/sh&lt;/code&gt; is typically dash, &lt;code&gt;pushd&lt;/code&gt;/&lt;code&gt;popd&lt;/code&gt; are unavailable, causing the npm commands to run in the wrong directory and fail with &amp;quot;ENOENT: no such file or directory&amp;quot; errors.&lt;/p&gt;
&lt;p&gt;The rewrite makes the script POSIX-compliant. &lt;code&gt;pushd&lt;/code&gt;/&lt;code&gt;popd&lt;/code&gt; for directory changes during builds are replaced with &lt;code&gt;npm --prefix &amp;quot;$DIR&amp;quot;&lt;/code&gt; in the new &lt;code&gt;build_dir()&lt;/code&gt; helper function, which avoids directory changes entirely. The &lt;code&gt;reset_version()&lt;/code&gt; function uses &lt;code&gt;cd&lt;/code&gt;/&lt;code&gt;cd -&lt;/code&gt; instead. Bash-specific &lt;code&gt;[[ ]]&lt;/code&gt; conditionals become &lt;code&gt;[ ]&lt;/code&gt;, &lt;code&gt;read -p&lt;/code&gt; becomes &lt;code&gt;printf&lt;/code&gt; followed by &lt;code&gt;read&lt;/code&gt;, and &lt;code&gt;echo&lt;/code&gt; with escape sequences becomes &lt;code&gt;printf&lt;/code&gt;. Color variables are assigned via &lt;code&gt;$(printf '\033[...')&lt;/code&gt; subshells instead of bare escape-sequence strings.&lt;/p&gt;
&lt;p&gt;Other improvements bundled in: a &lt;code&gt;check_dependency()&lt;/code&gt; function validates node and npm versions up front using POSIX &lt;code&gt;case&lt;/code&gt; pattern matching. The minimum Node.js version was raised from v20 to v24, and an npm &amp;gt;= v11 check was added. The macOS-only &lt;code&gt;open&lt;/code&gt; command now falls back to &lt;code&gt;xdg-open&lt;/code&gt; on Linux. &lt;code&gt;git tag &amp;quot;$TAG&amp;quot; HEAD&lt;/code&gt; was changed to &lt;code&gt;git tag -a &amp;quot;$TAG&amp;quot;&lt;/code&gt; to create annotated tags that open an editor for the tag message. Error handling was added with &lt;code&gt;|| exit 1&lt;/code&gt; throughout the script.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ui/json-schema-editor/package.json&lt;/code&gt; dependency keys were reordered alphabetically. The &lt;code&gt;package-lock.json&lt;/code&gt; was regenerated, adding &lt;code&gt;resolved&lt;/code&gt;/&lt;code&gt;integrity&lt;/code&gt; fields and picking up minor dependency version bumps (rollup 4.57.1 to 4.59.0, etc.).&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: February 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 month and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;marcus_johansson identified the problem, rewrote the script with full POSIX support, and used it to produce the RC2 release on Ubuntu 22. abhisekmazumdar tested the script end-to-end and successfully created a tag (1.3.1) with it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3579953: Add bulk operations for media overview page</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3579953.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94f5dd51a66f</guid>
      <category>Site owners</category>
      <pubDate>Thu, 09 Apr 2026 15:10:18 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai_recipe_image_classification/-/commit/d4798cbd952094b1ac5e82621c9cf6ac66327c57"&gt;d4798cb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3579953"&gt;#3579953&lt;/a&gt; · 6 contributors · 21 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The AI image classification recipe now adds Views Bulk Operations (VBO) to the media overview page at &lt;code&gt;/admin/content/media&lt;/code&gt;. Editors can select multiple media items and run bulk actions including delete, publish, unpublish, save, and two AI actions: &amp;quot;AI: Image Description&amp;quot; and &amp;quot;AI: Image Tags&amp;quot;. This lets editors classify many images at once instead of one at a time.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Recipe now requires &lt;code&gt;drupal/ai: ^1.4&lt;/code&gt; (was &lt;code&gt;^1.2&lt;/code&gt;) and new dependency &lt;code&gt;drupal/views_bulk_operations: ^4.4&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The core &lt;code&gt;views.view.media&lt;/code&gt; view already has a &lt;code&gt;media_bulk_form&lt;/code&gt; field using the core &lt;code&gt;bulk_form&lt;/code&gt; plugin. Since VBO and core bulk operations fields cannot be used in the same view display together (comment #14), the recipe replaces the existing &lt;code&gt;media_bulk_form&lt;/code&gt; field with the &lt;code&gt;views_bulk_operations_bulk_form&lt;/code&gt; plugin via the &lt;code&gt;setProperties&lt;/code&gt; config action. This modifies the existing view config without overriding the whole thing.&lt;/p&gt;
&lt;p&gt;Two new AI automator config entities are added with &lt;code&gt;worker_type: action&lt;/code&gt;, which makes them available as VBO actions. They are field-scoped, so two separate automators are needed (comment #4). The image description automator uses the &lt;code&gt;llm_simple_string_long&lt;/code&gt; rule with the &lt;code&gt;default_vision&lt;/code&gt; AI provider to analyze images and write to &lt;code&gt;field_image_description&lt;/code&gt;. The image tags automator uses the &lt;code&gt;llm_taxonomy&lt;/code&gt; rule with &lt;code&gt;default_json&lt;/code&gt;, taking &lt;code&gt;field_image_description&lt;/code&gt; as its base field input to choose or create up to 5 taxonomy terms in the &amp;quot;Image Classification&amp;quot; vocabulary.&lt;/p&gt;
&lt;p&gt;The VBO field is configured with six &lt;code&gt;selected_actions&lt;/code&gt;: delete, publish, save, unpublish, and the two AI automator actions. An earlier revision used ECA-based actions (&lt;code&gt;eca_preconfigured_action:media_publish_action&lt;/code&gt;) for publish/save/unpublish, but review in comment #11 identified that ECA was not a recipe dependency. The actions were switched to core equivalents (&lt;code&gt;entity:publish_action:media&lt;/code&gt;, etc.) so they work without ECA installed.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;drupal/ai&lt;/code&gt; version requirement was raised from &lt;code&gt;^1.2&lt;/code&gt; to &lt;code&gt;^1.4&lt;/code&gt; because the AI automator VBO action feature depends on &lt;a href="https://www.drupal.org/node/3456973"&gt;#3456973&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 18, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (22 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;lbesenyei (Brainsum)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;li&gt;nodles (1xINTERNET)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Review by ajv009 (comment #11) caught that the original MR used ECA-based action plugins for publish/save/unpublish, but ECA was not a recipe dependency, so those actions silently disappeared from the dropdown. The fix switched to core entity action equivalents. The maintainer noted the merge would proceed but the release would wait for AI Core 1.4.0 to reach at least release candidate.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3577970: Dispatch JS event when DeepChat completes agent calls</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3577970.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94d5abc36d65</guid>
      <category>Module maintainers</category>
      <pubDate>Thu, 09 Apr 2026 14:01:49 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.2.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/9c85dc8ca8565df24385557d6a8b4dfc0c3e1702"&gt;9c85dc8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3577970"&gt;#3577970&lt;/a&gt; · 2 contributors · 16 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;External scripts can now detect when the DeepChat chatbot finishes running agent calls. In verbose mode, the chatbot updates the same &amp;quot;Contacting Agents...&amp;quot; message rather than creating new messages, so DeepChat's native &lt;code&gt;new-message&lt;/code&gt; event never fires on completion. A new &lt;code&gt;agent-call-completed&lt;/code&gt; custom event is now dispatched on the &lt;code&gt;deep-chat&lt;/code&gt; element when agent execution finishes.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;agent-call-completed&lt;/code&gt; JavaScript CustomEvent dispatched on the &lt;code&gt;deep-chat&lt;/code&gt; element when agent execution finishes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;When the AI chatbot runs in verbose mode and loops through agents, it repeatedly updates the initial message in place rather than emitting new messages. Because of this, DeepChat's built-in &lt;code&gt;onMessage&lt;/code&gt; / &lt;code&gt;new-message&lt;/code&gt; event is never triggered when agent calls complete, leaving no way for external code to detect completion.&lt;/p&gt;
&lt;p&gt;The fix adds a &lt;code&gt;CustomEvent(&amp;quot;agent-call-completed&amp;quot;)&lt;/code&gt; dispatch on the &lt;code&gt;deepchatElement&lt;/code&gt; in &lt;code&gt;deepchat-init.js&lt;/code&gt;. The event fires at the end of the agent loop, right after &lt;code&gt;stepMessages&lt;/code&gt; is cleared and &lt;code&gt;shouldContinue&lt;/code&gt; is reset to &lt;code&gt;false&lt;/code&gt;. The event's &lt;code&gt;detail.message&lt;/code&gt; carries the final AI response with &lt;code&gt;role: 'ai'&lt;/code&gt; and the rendered &lt;code&gt;html&lt;/code&gt; from &lt;code&gt;data.html&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Listen for the new event on the &lt;code&gt;deep-chat&lt;/code&gt; element:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;document.querySelector('deep-chat').addEventListener('agent-call-completed', (e) =&amp;gt; {
  console.log(e.detail.message.html);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 9, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 month and 6 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;bbruno (1xINTERNET)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582856: Add Input Length Limit guardrail plugin against DoW attacks</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3582856.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867765b2b1a4</guid>
      <category>Module maintainers</category>
      <category>Site owners</category>
      <pubDate>Thu, 09 Apr 2026 13:47:33 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.4.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/9af5bb0e63429b979c2e0e53a2f8fb5cb5dc9c62"&gt;9af5bb0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582856"&gt;#3582856&lt;/a&gt; · 2 contributors · 17 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;New &lt;code&gt;input_length_limit&lt;/code&gt; guardrail plugin blocks chat requests that exceed a configurable character or token count before they reach the AI provider API. This provides a safety net against denial-of-wallet attacks (consuming expensive API tokens), context stuffing attacks, and overwhelming the model's context window.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers and site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config change:&lt;/strong&gt; New &lt;code&gt;ai.guardrail.settings.input_length_limit&lt;/code&gt; config schema with &lt;code&gt;max_length&lt;/code&gt;, &lt;code&gt;use_tokens&lt;/code&gt;, &lt;code&gt;tokenizer_model&lt;/code&gt;, &lt;code&gt;check_all_messages&lt;/code&gt;, and &lt;code&gt;violation_message&lt;/code&gt; keys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;input_length_limit&lt;/code&gt; guardrail plugin blocks chat input exceeding a configurable character or token count.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The AI module's guardrail system had no built-in way to limit the length of user input sent to AI providers. The new &lt;code&gt;InputLengthLimit&lt;/code&gt; plugin (&lt;code&gt;src/Plugin/AiGuardrail/InputLengthLimit.php&lt;/code&gt;) extends &lt;code&gt;AiGuardrailPluginBase&lt;/code&gt; and implements &lt;code&gt;processInput()&lt;/code&gt; as a pre-generate guardrail.&lt;/p&gt;
&lt;p&gt;The plugin only applies to &lt;code&gt;ChatInput&lt;/code&gt;. Non-chat input types (e.g. &lt;code&gt;TextToImageInput&lt;/code&gt;) receive a &lt;code&gt;PassResult&lt;/code&gt; and are skipped. By default, it measures the last user message only. When &lt;code&gt;check_all_messages&lt;/code&gt; is enabled, it concatenates all &lt;code&gt;ChatMessage&lt;/code&gt; texts (joined with newlines) and measures the total.&lt;/p&gt;
&lt;p&gt;Length measurement supports two modes: character-based counting via &lt;code&gt;mb_strlen()&lt;/code&gt; (the default), and token-based counting via the &lt;code&gt;ai.tokenizer&lt;/code&gt; service with a configurable &lt;code&gt;tokenizer_model&lt;/code&gt; (defaults to &lt;code&gt;gpt-4&lt;/code&gt;). When the measured length exceeds &lt;code&gt;max_length&lt;/code&gt;, the plugin returns a &lt;code&gt;StopResult&lt;/code&gt; with a configurable violation message supporting &lt;code&gt;@count&lt;/code&gt;, &lt;code&gt;@max&lt;/code&gt;, and &lt;code&gt;@unit&lt;/code&gt; placeholders. The &lt;code&gt;processOutput()&lt;/code&gt; method always returns &lt;code&gt;PassResult&lt;/code&gt; since this is an input-only guardrail.&lt;/p&gt;
&lt;p&gt;The configuration form uses Drupal &lt;code&gt;#states&lt;/code&gt; to show the &lt;code&gt;tokenizer_model&lt;/code&gt; field only when &lt;code&gt;use_tokens&lt;/code&gt; is checked. A bug in the initial implementation used the wrong form element name selector for this visibility toggle, which was fixed during review.&lt;/p&gt;
&lt;p&gt;Config schema was added at &lt;code&gt;ai.guardrail.settings.input_length_limit&lt;/code&gt; with five properties: &lt;code&gt;max_length&lt;/code&gt; (integer), &lt;code&gt;use_tokens&lt;/code&gt; (boolean), &lt;code&gt;tokenizer_model&lt;/code&gt; (string), &lt;code&gt;check_all_messages&lt;/code&gt; (boolean), and &lt;code&gt;violation_message&lt;/code&gt; (string). Unit tests cover character limits, token limits, all-messages mode, last-message-only mode, custom violation messages, multibyte characters, and non-chat input passthrough. Kernel tests verify the full guardrail pipeline using &lt;code&gt;AiGuardrail&lt;/code&gt; and &lt;code&gt;AiGuardrailSet&lt;/code&gt; config entities with the EchoAI test provider.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (6 days and 4 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;ahmad khader (Vardot)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;During review, ahmad khader found a bug where the &lt;code&gt;tokenizer_model&lt;/code&gt; field visibility state used the wrong form element name (&lt;code&gt;use_tokens&lt;/code&gt; instead of &lt;code&gt;guardrail_settings[use_tokens]&lt;/code&gt;), fixed it, and moved the issue to RTBC. marcus_johansson split a broader guardrail config schema bug discovered during this work into a separate issue &lt;a href="https://www.drupal.org/node/3583785"&gt;#3583785&lt;/a&gt; for backporting.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3553458: Agents failing to determine solvability forever stuck in "started" state</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3553458.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0f1cf299faeb</guid>
      <pubDate>Thu, 09 Apr 2026 10:02:07 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Major bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.2.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai_agents/-/commit/5ff1ec6986abc80d57fa854cc6621da9e7092fbe"&gt;5ff1ec6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3553458"&gt;#3553458&lt;/a&gt; · 7 contributors · 16 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 8&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When an AI agent failed to determine solvability -- either because it had hit the maximum loop count or because the AI provider chat call threw an exception -- the agent was left permanently in a &amp;quot;started&amp;quot; state with no corresponding &amp;quot;finished&amp;quot; event. This happened because &lt;code&gt;AgentStartedExecutionEvent&lt;/code&gt; was dispatched before the conditions that cause early returns were checked. The fix moves the max-loops check before the started event dispatch, and wraps the &lt;code&gt;chat()&lt;/code&gt; call in a try/catch that dispatches &lt;code&gt;AgentFinishedExecutionEvent&lt;/code&gt; on failure.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;AiAgentEntityWrapper::determineSolvability()&lt;/code&gt;, the execution tracking relies on a pair of events: &lt;code&gt;AgentStartedExecutionEvent&lt;/code&gt; and &lt;code&gt;AgentFinishedExecutionEvent&lt;/code&gt;. Before this fix, the method dispatched the started event, then incremented &lt;code&gt;$this-&amp;gt;looped&lt;/code&gt;, then checked whether the loop count exceeded &lt;code&gt;max_loops&lt;/code&gt;. If it did, the method returned &lt;code&gt;JOB_NOT_SOLVABLE&lt;/code&gt; immediately without ever dispatching the finished event. Similarly, if &lt;code&gt;$this-&amp;gt;aiProvider-&amp;gt;chat()&lt;/code&gt; threw an exception (e.g. due to an exhausted API quota), the exception propagated up uncaught, again with no finished event dispatched.&lt;/p&gt;
&lt;p&gt;Two changes fix this in &lt;code&gt;AiAgentEntityWrapper::determineSolvability()&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;$this-&amp;gt;looped++&lt;/code&gt; increment and max-loops check are moved to before the &lt;code&gt;AgentStartedExecutionEvent&lt;/code&gt; dispatch. If the loop limit is already exceeded, the method returns &lt;code&gt;JOB_NOT_SOLVABLE&lt;/code&gt; without creating any tracking state. Because &lt;code&gt;looped&lt;/code&gt; is now incremented before the event is constructed, the loop count passed to the event constructor changes from &lt;code&gt;$this-&amp;gt;looped&lt;/code&gt; to &lt;code&gt;$this-&amp;gt;looped - 1&lt;/code&gt; to preserve the original value semantics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;$this-&amp;gt;aiProvider-&amp;gt;chat()&lt;/code&gt; call is wrapped in a try/catch for &lt;code&gt;\Exception&lt;/code&gt;. On failure, the catch block sets &lt;code&gt;$this-&amp;gt;finished = TRUE&lt;/code&gt;, constructs a dummy &lt;code&gt;ChatOutput&lt;/code&gt; with an empty assistant message (the event subscriber does not use the response body), dispatches &lt;code&gt;AgentFinishedExecutionEvent&lt;/code&gt;, and returns &lt;code&gt;JOB_NOT_SOLVABLE&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;New kernel tests in &lt;code&gt;AiAgentEntityWrapperDetermineSolvabilityTest&lt;/code&gt; cover all three scenarios: max loops exceeded (expects 0 started and 0 finished events), chat exception (expects 1 started and 1 finished event), and normal execution (expects 1 started and 1 finished event).&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: October 20, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (5 months and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mglaman (Acquia)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;ajv009, a first-time contributor to the issue, reproduced both failure scenarios described by mglaman, wrote a detailed analysis of the root cause and proposed fix approach, and supplied the patch including kernel tests. marcus_johansson (FreelyGive) made a minor compatibility adjustment to make the tests work with Drupal 10 before merging.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3583705: AiProviderConfiguration form element doesn't work in nested forms with subform states</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3583705.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867bf5e5539b</guid>
      <pubDate>Wed, 08 Apr 2026 15:55:52 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/ef8f787b9d021302c342273725cb173850bd4e6c"&gt;ef8f787&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583705"&gt;#3583705&lt;/a&gt; · 2 contributors · 13 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;ai_provider_configuration&lt;/code&gt; form element returned a raw string instead of the expected &lt;code&gt;[provider, model, config]&lt;/code&gt; array when placed inside a nested form (a container with &lt;code&gt;#tree = TRUE&lt;/code&gt;, or a &lt;code&gt;SubformState&lt;/code&gt;). This affected any module that embedded the element inside another form's subform. The bug is now fixed and covered by a functional JavaScript test that exercises four nesting configurations.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;AiProviderConfiguration&lt;/code&gt; form element has a value callback that reconstructs a structured array from the raw POST data. To locate the selected provider and config values it builds &lt;code&gt;$select_parents&lt;/code&gt; and &lt;code&gt;$config_parents&lt;/code&gt;, which are absolute paths from the form root (e.g. &lt;code&gt;['root_tree', 'provider', 'provider_model']&lt;/code&gt;). The bug was that the callback then called &lt;code&gt;NestedArray::getValue($input, $select_parents)&lt;/code&gt;, where &lt;code&gt;$input&lt;/code&gt; is the element's own local value — a shallow array like &lt;code&gt;['provider_model' =&amp;gt; 'echoai__gpt-test', 'config' =&amp;gt; [...]]&lt;/code&gt;. When the element sits at the form root the local value and the full user-input tree happen to share the same keys, so the lookup accidentally worked. Inside a nested container or &lt;code&gt;SubformState&lt;/code&gt; the paths diverge and the lookup returns &lt;code&gt;NULL&lt;/code&gt;, leaving the value as the raw &lt;code&gt;provider_model&lt;/code&gt; string.&lt;/p&gt;
&lt;p&gt;The fix replaces both lookups with &lt;code&gt;NestedArray::getValue($form_state-&amp;gt;getUserInput(), ...)&lt;/code&gt;. &lt;code&gt;getUserInput()&lt;/code&gt; always returns the complete raw POST array regardless of nesting level, so the absolute &lt;code&gt;$select_parents&lt;/code&gt; and &lt;code&gt;$config_parents&lt;/code&gt; paths resolve correctly in every context.&lt;/p&gt;
&lt;p&gt;A new &lt;code&gt;FunctionalJavascript&lt;/code&gt; test (&lt;code&gt;AiProviderConfigurationElementNestingTest&lt;/code&gt;) exercises all four relevant configurations in one test method: root element without a wrapping container, root container with &lt;code&gt;#tree = TRUE&lt;/code&gt;, root container with &lt;code&gt;#tree = FALSE&lt;/code&gt;, and an actual &lt;code&gt;SubformState&lt;/code&gt;. Each scenario is backed by a dedicated section of &lt;code&gt;AiProviderConfigurationTestForm&lt;/code&gt; which renders the captured submitted values into individually addressable &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; blocks so the test can assert &lt;code&gt;[provider]&lt;/code&gt;, &lt;code&gt;[model]&lt;/code&gt;, and &lt;code&gt;[config]&lt;/code&gt; independently per scenario.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 8, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;marcus_johansson turned around a fix the same day the issue was opened, providing a video recording of the passing functional JavaScript test as proof across all four nesting scenarios.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3578846: AiGuardrailSet does not declare config dependencies on its referenced guardrails</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3578846.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94e06aeb872c</guid>
      <category>Site owners</category>
      <pubDate>Wed, 08 Apr 2026 09:09:54 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/aa301520b44f87265b8d77d3976a3e280fbaa43b"&gt;aa30152&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3578846"&gt;#3578846&lt;/a&gt; · 6 contributors · 21 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AiGuardrailSet&lt;/code&gt; config entities now correctly declare config dependencies on the &lt;code&gt;AiGuardrail&lt;/code&gt; entities they reference. Before this fix, exported config always showed empty &lt;code&gt;dependencies: {}&lt;/code&gt;. This meant Drupal could try to import a guardrail set before its guardrails existed, and it would not warn or prevent deletion of a guardrail that a set still referenced (which would cause a fatal error at runtime). A post-update hook resaves all existing guardrail sets so their stored config gets the correct dependencies.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config change:&lt;/strong&gt; &lt;code&gt;ai.ai_guardrail_set.*&lt;/code&gt; config now stores dependencies on referenced &lt;code&gt;ai.ai_guardrail.*&lt;/code&gt; entities; run database updates to populate dependencies on existing guardrail sets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AiGuardrailSet&lt;/code&gt; extends &lt;code&gt;ConfigEntityBase&lt;/code&gt; but never overrode &lt;code&gt;calculateDependencies()&lt;/code&gt;. The entity stores references to &lt;code&gt;AiGuardrail&lt;/code&gt; config entities by ID in &lt;code&gt;pre_generate_guardrails['plugin_id']&lt;/code&gt; and &lt;code&gt;post_generate_guardrails['plugin_id']&lt;/code&gt;. Without declared dependencies, Drupal's config system had no knowledge of these relationships.&lt;/p&gt;
&lt;p&gt;The fix adds &lt;code&gt;calculateDependencies()&lt;/code&gt; to &lt;code&gt;AiGuardrailSet&lt;/code&gt;. It collects IDs from both arrays, deduplicates them with &lt;code&gt;array_unique()&lt;/code&gt;, loads the actual entities with &lt;code&gt;AiGuardrail::loadMultiple()&lt;/code&gt;, and calls &lt;code&gt;addDependency('config', $guardrail-&amp;gt;getConfigDependencyName())&lt;/code&gt; for each. Using &lt;code&gt;loadMultiple()&lt;/code&gt; and &lt;code&gt;getConfigDependencyName()&lt;/code&gt; rather than hardcoding the config name (e.g. &lt;code&gt;'ai.ai_guardrail.' . $guardrail_id&lt;/code&gt;) was a.dmitriiev's deliberate choice to avoid fragile string construction.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ai_post_update_13001&lt;/code&gt; uses &lt;code&gt;ConfigEntityUpdater&lt;/code&gt; to resave all existing &lt;code&gt;ai_guardrail_set&lt;/code&gt; entities, populating the missing dependencies in already-exported config.&lt;/p&gt;
&lt;p&gt;The schema in &lt;code&gt;ai.schema.yml&lt;/code&gt; had related bugs found during this work: &lt;code&gt;post_generate_guardrails.plugin_id&lt;/code&gt; was typed as a &lt;code&gt;mapping&lt;/code&gt; instead of a &lt;code&gt;sequence&lt;/code&gt; (not matching &lt;code&gt;pre_generate_guardrails&lt;/code&gt;), and &lt;code&gt;stop_threshold&lt;/code&gt; (type &lt;code&gt;float&lt;/code&gt;) and &lt;code&gt;guardrail_settings&lt;/code&gt; (type &lt;code&gt;ignore&lt;/code&gt;) were missing entirely from their respective schema entries.&lt;/p&gt;
&lt;p&gt;A kernel test &lt;code&gt;AiGuardrailSetDependencyTest&lt;/code&gt; covers the new behavior, verifying that both &lt;code&gt;getDependencies()&lt;/code&gt; and the raw stored config contain the correct dependency names. The test also demonstrates that referenced &lt;code&gt;AiGuardrail&lt;/code&gt; entities must be saved before the &lt;code&gt;AiGuardrailSet&lt;/code&gt; that references them.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;drush updatedb&lt;/code&gt; (or equivalent) to trigger &lt;code&gt;ai_post_update_13001&lt;/code&gt;, which resaves all &lt;code&gt;ai_guardrail_set&lt;/code&gt; config entities so their dependencies are recalculated and written to stored config.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 12, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (26 days and 5 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;abhisekmazumdar (Dropsolid)&lt;/li&gt;
&lt;li&gt;annmarysruthy (QED42)&lt;/li&gt;
&lt;li&gt;nikro (Dropsolid)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;nikro marked the initial patch RTBC but suggested (as optional) adding an update hook to recalculate dependencies on existing entities. a.dmitriiev then added &lt;code&gt;ai_post_update_13001&lt;/code&gt;, changed the dependency calculation to use &lt;code&gt;AiGuardrail::loadMultiple()&lt;/code&gt; and &lt;code&gt;getConfigDependencyName()&lt;/code&gt; instead of hardcoding the config name, and fixed the kernel test to create guardrail entities before the set (the test was broken because it assumed guardrails could be referenced without being saved first).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3577813: AI Chatbot block crashes entire site when placed without configured AI Assistant entity</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3577813.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94d344831579</guid>
      <pubDate>Wed, 08 Apr 2026 09:00:34 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Major bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/6a814d75f73a62debb6720a9c5182a22ca432771"&gt;6a814d7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3577813"&gt;#3577813&lt;/a&gt; · 9 contributors · 39 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 12&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Placing the AI Chatbot block (&lt;code&gt;ai_deepchat_block&lt;/code&gt;) without selecting an AI Assistant entity caused a fatal &lt;code&gt;AssertionError: Cannot load the &amp;quot;ai_assistant&amp;quot; entity with NULL ID&lt;/code&gt; that crashed the entire site, not just the page rendering the block. The block now silently hides itself when no assistant is configured or when the configured assistant has been deleted or disabled. Sites that placed the block before creating any AI Assistant entities, or that deleted a referenced assistant, were affected. The workaround was to delete the block config via Drush.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DeepChatFormBlock&lt;/code&gt; in the &lt;code&gt;ai_chatbot&lt;/code&gt; module did not guard against a NULL or empty &lt;code&gt;ai_assistant&lt;/code&gt; configuration value before calling &lt;code&gt;entityTypeManager-&amp;gt;getStorage('ai_assistant')-&amp;gt;load()&lt;/code&gt;. Passing NULL to that method triggers a fatal &lt;code&gt;AssertionError&lt;/code&gt; that aborts the entire page render.&lt;/p&gt;
&lt;p&gt;The fix adds early-exit guards in both &lt;code&gt;blockAccess()&lt;/code&gt; and &lt;code&gt;build()&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blockAccess()&lt;/code&gt; now returns &lt;code&gt;AccessResult::forbidden()&lt;/code&gt; immediately when &lt;code&gt;$this-&amp;gt;configuration['ai_assistant']&lt;/code&gt; is empty, before any entity load is attempted.&lt;/li&gt;
&lt;li&gt;When a non-empty ID is set but the entity cannot be loaded (stale reference after deletion), it returns &lt;code&gt;AccessResult::forbidden()-&amp;gt;addCacheTags(['ai_assistant_list'])&lt;/code&gt; so the result is invalidated if a new assistant is later created.&lt;/li&gt;
&lt;li&gt;When the entity loads but is disabled, it returns &lt;code&gt;AccessResult::forbidden()-&amp;gt;addCacheableDependency($assistant)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;userHasAccess()&lt;/code&gt; forbidden and allowed results now include &lt;code&gt;-&amp;gt;addCacheableDependency($assistant)-&amp;gt;cachePerUser()&lt;/code&gt;, since access depends on both the entity state and the current user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build()&lt;/code&gt; mirrors this with an empty array return for the empty-config and missing/disabled-entity cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Previously, the combined &lt;code&gt;!$assistant || !$assistant-&amp;gt;status()&lt;/code&gt; check returned a bare &lt;code&gt;AccessResult::forbidden()&lt;/code&gt; with no cache metadata in all cases, which also meant that calling &lt;code&gt;addCacheableDependency($assistant)&lt;/code&gt; on a NULL value was a silent no-op and cache metadata was lost. The split into separate branches, contributed by ajv009 in a follow-up commit, corrects both issues.&lt;/p&gt;
&lt;p&gt;A new &lt;code&gt;DeepChatBlockNoAssistantTest&lt;/code&gt; functional JavaScript test verifies that the front page returns a 200 and the block is not rendered when placed with no assistant configured. The base test class &lt;code&gt;BaseClassFunctionalJavascriptTests&lt;/code&gt; also had its screenshot and video output paths corrected from &lt;code&gt;sites/simpletest/&lt;/code&gt; to &lt;code&gt;sites/default/files/simpletest/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 8, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (1 month and 6 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;harivansh (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;fago (drunomics)&lt;/li&gt;
&lt;li&gt;jatingupta40&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Multiple contributors iterated toward the final fix. jatingupta40 posted an initial patch; ajv009 then pushed a follow-up that split the single &lt;code&gt;instanceof&lt;/code&gt; check into two branches, explaining that calling &lt;code&gt;addCacheableDependency($assistant)&lt;/code&gt; on NULL was a silent no-op causing cache metadata to be silently lost, and added &lt;code&gt;cachePerUser()&lt;/code&gt; based on a suggestion from jatingupta40. marcus_johansson added the functional JS test and set the issue RTBC. a.dmitriiev rebased the MR to resolve a PHPStan pipeline failure introduced by an unrelated fix already merged into 1.3.x.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3555856: AI Chatbot blocks fail to render: Uninitialized $userMessage property in AiAssistantApiRunner</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3555856.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0f2805f1fa22</guid>
      <pubDate>Wed, 08 Apr 2026 08:43:43 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.2.2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/a90bcfe1ea762ffcac9998f6667349c2c8546826"&gt;a90bcfe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3555856"&gt;#3555856&lt;/a&gt; · 7 contributors · 22 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 8&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;AI chatbot blocks (&lt;code&gt;ai_chatbot_block&lt;/code&gt; and &lt;code&gt;ai_deepchat_block&lt;/code&gt;) failed to render on any page due to a fatal PHP error. The &lt;code&gt;getMessageHistory()&lt;/code&gt; method in &lt;code&gt;AiAssistantApiRunner&lt;/code&gt; tried to call &lt;code&gt;$this-&amp;gt;userMessage-&amp;gt;getMessage()&lt;/code&gt; before any user message existed, which PHP 8.x treats as a fatal error for uninitialized typed properties. This happened on every page load where a chatbot block was placed, making the blocks completely unusable.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The class &lt;code&gt;AiAssistantApiRunner&lt;/code&gt; declares &lt;code&gt;protected UserMessage|NULL $userMessage;&lt;/code&gt; as a typed property with no default value. When session storage is disabled (the &lt;code&gt;!$this-&amp;gt;shouldStoreSession()&lt;/code&gt; branch of &lt;code&gt;getMessageHistory()&lt;/code&gt;), the method returned early with &lt;code&gt;$this-&amp;gt;userMessage-&amp;gt;getMessage()&lt;/code&gt;. Because the property is typed and uninitialized rather than explicitly set to &lt;code&gt;null&lt;/code&gt;, PHP 8.x throws &lt;code&gt;Error: Typed property must not be accessed before initialization&lt;/code&gt; rather than returning &lt;code&gt;null&lt;/code&gt;. A commenter confirmed the call path: &lt;code&gt;getMessageHistory()&lt;/code&gt; is triggered during block rendering when the &amp;quot;allow history&amp;quot; setting is &lt;code&gt;session_one_thread&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix wraps the access in &lt;code&gt;isset($this-&amp;gt;userMessage) &amp;amp;&amp;amp; $this-&amp;gt;userMessage instanceof UserMessage&lt;/code&gt;. If the check fails (no message has been set yet, as is the case on initial render before any user interaction), the method now returns an empty array instead.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: November 3, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (5 months and 7 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;harivansh (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;li&gt;joshua1234511 (Axelerant)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582575: Add agent skill and drush generator for Guardrail plugins</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3582575.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-951c6cf19283</guid>
      <category>Module maintainers</category>
      <pubDate>Wed, 08 Apr 2026 07:30:09 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.4.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/0b36db60d3d633ac63ff773611dbab0d8598e52d"&gt;0b36db6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582575"&gt;#3582575&lt;/a&gt; · 3 contributors · 21 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 5&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Drupal AI module now ships a Drush code generator and an AI agent skill to help developers create custom guardrail plugins. Before this change, creating a guardrail required knowing the full plugin API: the &lt;code&gt;#[AiGuardrail]&lt;/code&gt; attribute, base class hierarchy, four result types, streaming interfaces, and config schema requirements. The new &lt;code&gt;drush generate plugin:ai:guardrail&lt;/code&gt; command (alias: &lt;code&gt;ai-guardrail&lt;/code&gt;) asks five questions and writes a ready-to-edit plugin stub. The agent skill at &lt;code&gt;.agents/skills/create-guardrail-plugin/SKILL.md&lt;/code&gt; gives AI coding assistants (such as Claude or GitHub Copilot) a structured guide to scaffold guardrails from a natural-language description.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;drush generate plugin:ai:guardrail&lt;/code&gt; command (alias &lt;code&gt;ai-guardrail&lt;/code&gt;) scaffolds a deterministic or non-deterministic &lt;code&gt;AiGuardrail&lt;/code&gt; plugin stub in any module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Guardrail plugins (&lt;code&gt;#[AiGuardrail]&lt;/code&gt;) run before and after AI generation and return one of four result types from &lt;code&gt;Drupal\ai\Guardrail\Result\&lt;/code&gt;: &lt;code&gt;PassResult&lt;/code&gt;, &lt;code&gt;StopResult&lt;/code&gt;, &lt;code&gt;RewriteInputResult&lt;/code&gt;, or &lt;code&gt;RewriteOutputResult&lt;/code&gt;. Two distinct plugin patterns exist: deterministic (fixed rules, extends &lt;code&gt;AiGuardrailPluginBase&lt;/code&gt;, implements &lt;code&gt;ConfigurableInterface&lt;/code&gt; + &lt;code&gt;PluginFormInterface&lt;/code&gt;) and non-deterministic (calls an AI provider internally, additionally implements &lt;code&gt;NonDeterministicGuardrailInterface&lt;/code&gt;, &lt;code&gt;NonStreamableGuardrailInterface&lt;/code&gt;, and &lt;code&gt;ContainerFactoryPluginInterface&lt;/code&gt;, uses &lt;code&gt;NeedsAiPluginManagerTrait&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drush generator&lt;/strong&gt; (&lt;code&gt;src/Drush/Generators/GuardrailGenerator.php&lt;/code&gt;) extends &lt;code&gt;DrupalCodeGenerator\Command\BaseGenerator&lt;/code&gt; and is registered via the &lt;code&gt;#[Generator]&lt;/code&gt; attribute with name &lt;code&gt;plugin:ai:guardrail&lt;/code&gt; and alias &lt;code&gt;ai-guardrail&lt;/code&gt;. The &lt;code&gt;generate()&lt;/code&gt; method interviews for module machine name, label, plugin ID (auto-derived as snake_case from the label), description, and deterministic/non-deterministic choice, then writes &lt;code&gt;src/Plugin/AiGuardrail/{ClassName}.php&lt;/code&gt; from one of two Twig templates at &lt;code&gt;templates/Plugin/_ai-guardrail/&lt;/code&gt;. The deterministic template produces &lt;code&gt;processInput()&lt;/code&gt; with a &lt;code&gt;ChatInput&lt;/code&gt; type check, last-message extraction via &lt;code&gt;end($messages)&lt;/code&gt;, and &lt;code&gt;@todo&lt;/code&gt; markers for custom validation logic, plus full &lt;code&gt;ConfigurableInterface&lt;/code&gt; boilerplate. The non-deterministic template adds the &lt;code&gt;NeedsAiPluginManagerTrait&lt;/code&gt;, a &lt;code&gt;create()&lt;/code&gt; method injecting &lt;code&gt;AiProviderFormHelper&lt;/code&gt; via &lt;code&gt;ai.form_helper&lt;/code&gt;, provider/model configuration in &lt;code&gt;buildConfigurationForm()&lt;/code&gt; using &lt;code&gt;AiProviderFormHelper::generateAiProvidersForm()&lt;/code&gt;, and &lt;code&gt;submitConfigurationForm()&lt;/code&gt; that extracts and type-casts LLM config keys using &lt;code&gt;CastUtility::typeCast()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent skill&lt;/strong&gt; (&lt;code&gt;.agents/skills/create-guardrail-plugin/SKILL.md&lt;/code&gt;) is a structured prompt document consumed by AI coding assistants via the &lt;code&gt;/create-guardrail-plugin&lt;/code&gt; slash command. It walks through four steps: determine guardrail type, gather requirements, run &lt;code&gt;drush generate plugin:ai:guardrail&lt;/code&gt;, add config schema to &lt;code&gt;config/schema/ai.schema.yml&lt;/code&gt; under the key &lt;code&gt;ai.guardrail.settings.{plugin_id}&lt;/code&gt;, and optionally generate a kernel test using the EchoAI provider. Config schema guidance was added during review (comment #8) after a reviewer found it missing when testing the skill against the &lt;code&gt;InputLengthLimit&lt;/code&gt; guardrail from &lt;a href="https://www.drupal.org/node/3582856"&gt;#3582856&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;#[Generator]&lt;/code&gt; attribute sets &lt;code&gt;templatePath&lt;/code&gt; to &lt;code&gt;__DIR__ . '/../../../templates/Plugin/_ai-guardrail'&lt;/code&gt;, which is the actual location of the Twig files. The SKILL.md reference section lists &lt;code&gt;templates/guardrail/deterministic.twig&lt;/code&gt; (the older path discussed in review), but the authoritative path used by the generator is &lt;code&gt;templates/Plugin/_ai-guardrail/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 1, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (7 days and 4 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;ahmad khader (Vardot)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;During review, ahmad khader tested the agent skill by running the exact one-shot prompt from comment #5 against the &lt;code&gt;InputLengthLimit&lt;/code&gt; guardrail from &lt;a href="https://www.drupal.org/node/3582856"&gt;#3582856&lt;/a&gt;. The generated code passed &lt;code&gt;php -l&lt;/code&gt; and &lt;code&gt;phpstan --level=1&lt;/code&gt; with zero errors, but the reviewer found that config schema instructions were missing from the skill. This gap was documented in detail in comment #8 and was then incorporated into the SKILL.md before the issue was fixed. The reviewer also raised a naming convention concern about the Twig template directory (&lt;code&gt;templates/guardrail/&lt;/code&gt; vs &lt;code&gt;templates/Plugin/_ai-guardrail/&lt;/code&gt;), and the generator was updated to use the latter path.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3579424: The tool category should fixed on Tool Library modal</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3579424.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94e8cb950c60</guid>
      <pubDate>Tue, 07 Apr 2026 15:43:51 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/5cf71f8d35a44ed034c12a1c37f9eafc49e73eab"&gt;5cf71f8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3579424"&gt;#3579424&lt;/a&gt; · 5 contributors · 14 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In the AI Tools Library modal (&amp;quot;Select Tool&amp;quot; dialog), the left-side category menu scrolled with the content, so it disappeared when a user scrolled down through a long tool list. The menu is now sticky so it stays visible at all times. The content area also gains &lt;code&gt;overflow-y: auto&lt;/code&gt; to scroll independently.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The fix is a CSS-only change to &lt;code&gt;assets/css/ai_tools_library.css&lt;/code&gt;. The &lt;code&gt;.ai-tools-library-menu&lt;/code&gt; selector had &lt;code&gt;position: relative&lt;/code&gt;, which meant it participated in normal document flow and scrolled away with the rest of the modal. Changing it to &lt;code&gt;position: sticky; top: 0; align-self: flex-start;&lt;/code&gt; pins the menu to the top of its scroll container while the content area scrolls. The &lt;code&gt;align-self: flex-start&lt;/code&gt; is required for sticky positioning to work inside a flex container, because a flex item that stretches to full height cannot scroll relative to itself. The content panel (the rule around line 149 with &lt;code&gt;padding: 1em; outline: none;&lt;/code&gt;) gains &lt;code&gt;overflow-y: auto&lt;/code&gt; so it becomes the scrolling region, enabling the sticky behavior to take effect.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 16, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (22 days and 4 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;shubham.prakash (OpenSense Labs)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3579967: StreamedChatMessageIterator buffer corrupts HTML when consuming streamed responses server-side (relative URLs split mid-attribute)</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3579967.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94f6ffb6fac2</guid>
      <category>Module maintainers</category>
      <pubDate>Tue, 07 Apr 2026 15:38:03 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Major bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai/-/commit/e37b6f727e9c771a641b2d916d64e082d99e0edb"&gt;e37b6f7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3579967"&gt;#3579967&lt;/a&gt; · 5 contributors · 21 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When using the AI module's Chat operation type to process HTML content server-side (for example via &lt;code&gt;ai_integration_eca&lt;/code&gt; or any Fiber-based batch consumer), HTML attributes containing relative URLs like &lt;code&gt;src=&amp;quot;/sites/default/files/...&amp;quot;&lt;/code&gt; were silently corrupted. The &lt;code&gt;shouldFlush()&lt;/code&gt; method in &lt;code&gt;StreamedChatMessageIterator&lt;/code&gt; held the buffer when it detected an absolute URL being assembled, but not a relative URL starting with &lt;code&gt;/&lt;/code&gt;. When the 100-character buffer limit was reached mid-attribute, the buffer flushed and split the URL value across two chunks, breaking the HTML. This fix makes the buffer hold until relative URLs are fully assembled, matching the existing protection for absolute URLs.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;getMaxBufferSize()&lt;/code&gt; and &lt;code&gt;setMaxBufferSize()&lt;/code&gt; public methods on &lt;code&gt;StreamedChatMessageIterator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;StreamedChatMessageIterator&lt;/code&gt; accumulates streamed chunks in a &lt;code&gt;$buffer&lt;/code&gt; string and flushes it to callers when a paragraph boundary or the 100-character &lt;code&gt;$maxBufferSize&lt;/code&gt; limit is reached. Before flushing, &lt;code&gt;shouldFlush()&lt;/code&gt; runs two &lt;code&gt;preg_match&lt;/code&gt; calls to detect whether a URL is still being built inside a quoted HTML attribute (&lt;code&gt;&amp;quot;...&amp;quot;&lt;/code&gt;) or a Markdown link (&lt;code&gt;(...)&lt;/code&gt;). The original patterns were &lt;code&gt;/&amp;quot;(?:http|\/\/:)[^&amp;quot;]*$/&lt;/code&gt; and &lt;code&gt;/\((?:http|\/\/:)[^)]*$/&lt;/code&gt;, which matched &lt;code&gt;http&lt;/code&gt; and the protocol-relative prefix &lt;code&gt;//:&lt;/code&gt;  but not plain relative URLs starting with &lt;code&gt;/&lt;/code&gt;. A relative &lt;code&gt;src&lt;/code&gt; like &lt;code&gt;/sites/default/files/image.png&lt;/code&gt; split across a buffer boundary would trigger a flush at &lt;code&gt;src=&amp;quot;/sites/default&amp;quot;&lt;/code&gt;, leaving &lt;code&gt;/files/image.png&amp;quot; width=&amp;quot;2048&amp;quot;&lt;/code&gt; as orphaned text after the closing quote.&lt;/p&gt;
&lt;p&gt;The fix adds &lt;code&gt;\/&lt;/code&gt; as a third alternative in both patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;// Before
preg_match('/&amp;quot;(?:http|\/\/:)[^&amp;quot;]*$/', $this-&amp;gt;buffer) || preg_match('/\((?:http|\/\/:)[^)]*$/', $this-&amp;gt;buffer)

// After
preg_match('/&amp;quot;(?:http|\/\/:|\/)[^&amp;quot;]*$/', $this-&amp;gt;buffer) || preg_match('/\((?:http|\/\/:|\/)[^)]*$/', $this-&amp;gt;buffer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test helper &lt;code&gt;EchoProvider&lt;/code&gt; was also changed to split its output into 10-character chunks using &lt;code&gt;str_split($string, 10)&lt;/code&gt; instead of emitting a single string, so existing and new kernel tests actually exercise the buffer logic. Two new tests were added to &lt;code&gt;ChatInterfaceTest&lt;/code&gt; for long relative links in Markdown (&lt;code&gt;[link](/path...)&lt;/code&gt;) and HTML (&lt;code&gt;&amp;lt;a href=&amp;quot;/path...&amp;quot;&amp;gt;&lt;/code&gt;), and a new &lt;code&gt;HostnameFilterTest&lt;/code&gt; kernel test class exercises the iterator directly with a realistic HTML stream containing a relative &lt;code&gt;src&lt;/code&gt; attribute split across chunk boundaries. The new class is annotated &lt;code&gt;@coversDefaultClass \Drupal\ai\Service\HostnameFilter&lt;/code&gt; and verifies that relative URLs survive intact while absolute external URLs are filtered out by the hostname filter service.&lt;/p&gt;
&lt;p&gt;Two new public methods, &lt;code&gt;getMaxBufferSize(): int&lt;/code&gt; and &lt;code&gt;setMaxBufferSize(int $size): void&lt;/code&gt;, were added to &lt;code&gt;StreamedChatMessageIterator&lt;/code&gt; to expose the buffer size limit.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 18, 2026&lt;/li&gt;
&lt;li&gt;First commit: March 26, 2026 (8 days later)&lt;/li&gt;
&lt;li&gt;Last commit: April 7, 2026 (11 days and 6 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;increweb21&lt;/li&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;abhisekmazumdar (Dropsolid)&lt;/li&gt;
&lt;li&gt;a.dmitriiev (1xINTERNET)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;marcus_johansson noted after applying the fix and writing kernel tests that he could not actually reproduce the original HTML corruption himself, even with the buffer splitting mid-attribute. He applied the regex fix and added tests based on the reporter's Xdebug evidence regardless.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3559183: Create first improvement of agent form</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3559183.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0f32f7a5e4dc</guid>
      <category>Module maintainers</category>
      <category>Site owners</category>
      <pubDate>Tue, 07 Apr 2026 08:51:42 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.3.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai_agents/-/commit/652d1941c56df141353dd8ca3c7477f84bc3eaf3"&gt;652d194&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3559183"&gt;#3559183&lt;/a&gt; · 10 contributors · 42 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 11&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The add/edit form for AI Agents is significantly redesigned to reduce visual complexity. Instead of expanding inline details sections for each selected tool, a &amp;quot;Configure&amp;quot; button on each tool card opens a MicroModal overlay containing the tool's settings. The system prompt field now uses the MDX Editor with token typeahead. The structured output schema field uses the &lt;code&gt;ai_json_schema&lt;/code&gt; element type instead of a plain textarea. Secondary fields (max loops, orchestration/triage classification, default information tools) are grouped inside a collapsed &amp;quot;Advanced settings&amp;quot; section. The default &lt;code&gt;max_loops&lt;/code&gt; value changed from 3 to 10. Property restriction fields are now presented in a table layout per tool rather than individual fieldsets.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers and site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Minimum &lt;code&gt;drupal/ai&lt;/code&gt; version raised from &lt;code&gt;^1.2.0&lt;/code&gt; to &lt;code&gt;^1.3.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; &lt;code&gt;AiAgentForm&lt;/code&gt; constructor gains a required &lt;code&gt;Token&lt;/code&gt; &lt;code&gt;$token&lt;/code&gt; parameter; subclasses must inject &lt;code&gt;token&lt;/code&gt; service. Modal CSS classes renamed from &lt;code&gt;.modal__*&lt;/code&gt; to &lt;code&gt;.ai-modal__*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;AiAgentForm::afterBuildToolsLibrary()&lt;/code&gt; public static after-build callback. New &lt;code&gt;hook_preprocess_ai_tools_library_item()&lt;/code&gt; implementation adds a Configure button to tool cards on agent add/edit routes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The form in &lt;code&gt;AiAgentForm.php&lt;/code&gt; was restructured in several ways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool configuration modal.&lt;/strong&gt; &lt;code&gt;hook_preprocess_ai_tools_library_item()&lt;/code&gt; in &lt;code&gt;ai_agents.module&lt;/code&gt; injects a &amp;quot;Configure&amp;quot; button into each tool card, but only when the current route is &lt;code&gt;entity.ai_agent.edit_form&lt;/code&gt; or &lt;code&gt;entity.ai_agent.add_form&lt;/code&gt;. The button carries a &lt;code&gt;data-tool&lt;/code&gt; attribute (tool ID with colons replaced by underscores). &lt;code&gt;createToolUsageForm()&lt;/code&gt; now wraps each tool's settings in MicroModal HTML via &lt;code&gt;#prefix&lt;/code&gt;/&lt;code&gt;#suffix&lt;/code&gt; on the tool's container element, using the same derived ID. &lt;code&gt;MicroModal.show(id)&lt;/code&gt; is called from the &lt;code&gt;openToolsModal&lt;/code&gt; Drupal behavior in &lt;code&gt;agents_form.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MicroModal bundling.&lt;/strong&gt; MicroModal v0.4.10 is managed via npm (&lt;code&gt;package.json&lt;/code&gt;) and the built &lt;code&gt;micromodal.min.js&lt;/code&gt; is committed to the repository under &lt;code&gt;js/&lt;/code&gt;. The &lt;code&gt;build&lt;/code&gt; script copies it from &lt;code&gt;node_modules&lt;/code&gt;. License and source metadata are available via &lt;code&gt;package.json&lt;/code&gt; without requiring asset packagist or a CDN. The library is declared in &lt;code&gt;ai_agents.libraries.yml&lt;/code&gt; alongside a new &lt;code&gt;css/micromodal.css&lt;/code&gt; with CSS classes prefixed &lt;code&gt;ai-modal__&lt;/code&gt; to avoid collisions with themes and other modules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript rewrite.&lt;/strong&gt; &lt;code&gt;agents_form.js&lt;/code&gt; was rewritten from jQuery to vanilla JS using Drupal's &lt;code&gt;once()&lt;/code&gt; API (comment #34 from jibran, implemented by ajv009 in comment #36). This also resolved ESLint errors. The jQuery UI dialog close event (unavailable without jQuery) is replaced by a &lt;code&gt;MutationObserver&lt;/code&gt; on &lt;code&gt;document.body&lt;/code&gt; that watches for the removal of &lt;code&gt;.ui-dialog&lt;/code&gt; elements, then fires a &lt;code&gt;mousedown&lt;/code&gt; on &lt;code&gt;[data-ai-tools-library-form-element-update]&lt;/code&gt; to refresh the tool card widget after a tool is selected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Form structure changes.&lt;/strong&gt; &lt;code&gt;max_loops&lt;/code&gt;, &lt;code&gt;orchestration_agent&lt;/code&gt;, &lt;code&gt;triage_agent&lt;/code&gt;, &lt;code&gt;default_information_tools&lt;/code&gt;, and structured output fields were moved under &lt;code&gt;prompt_detail &amp;gt; advanced&lt;/code&gt; (a collapsed details element). Tool settings fields (&lt;code&gt;return_directly&lt;/code&gt;, &lt;code&gt;require_usage&lt;/code&gt;, &lt;code&gt;description_enabled&lt;/code&gt;, &lt;code&gt;description_override&lt;/code&gt;, &lt;code&gt;use_artifacts&lt;/code&gt;, &lt;code&gt;progress_message&lt;/code&gt;) were grouped under a &lt;code&gt;tool_settings&lt;/code&gt; sub-key within each tool's container. Property restrictions were restructured from individual fieldsets into a &lt;code&gt;#type =&amp;gt; 'table'&lt;/code&gt; with three columns (Property, Description override, Restrictions), and the form state paths updated accordingly (e.g., &lt;code&gt;tool_usage[tool_id][property_restrictions][table][property_name][restrictions][action]&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Submit handler.&lt;/strong&gt; The &lt;code&gt;save()&lt;/code&gt; method was updated to read &lt;code&gt;tool_settings.*&lt;/code&gt; and &lt;code&gt;property_restrictions.table.*&lt;/code&gt; from their new nested paths. A bug found during review (comment #23) where settings like &lt;code&gt;progress_message&lt;/code&gt; and &lt;code&gt;use_artifacts&lt;/code&gt; were silently dropped was fixed by correcting these paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AJAX stability.&lt;/strong&gt; &lt;code&gt;afterBuildToolsLibrary()&lt;/code&gt; is registered as &lt;code&gt;#after_build&lt;/code&gt; on the &lt;code&gt;ai_tools_library&lt;/code&gt; element. It sets &lt;code&gt;#limit_validation_errors = []&lt;/code&gt; on the &lt;code&gt;update_widget&lt;/code&gt; AJAX button and also updates the triggering element copy stored in &lt;code&gt;$form_state&lt;/code&gt;, so the form rebuilds correctly after tool selection without requiring all fields to be valid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Other fixes.&lt;/strong&gt; &lt;code&gt;orchestration_agent&lt;/code&gt; and &lt;code&gt;triage_agent&lt;/code&gt; on &lt;code&gt;AiAgent&lt;/code&gt; entity changed from non-nullable &lt;code&gt;bool&lt;/code&gt; to &lt;code&gt;?bool = NULL&lt;/code&gt; to prevent PHP errors on new entities. A &lt;code&gt;Token&lt;/code&gt; service is now injected into &lt;code&gt;AiAgentForm&lt;/code&gt; to build token typeahead configuration for the MDX Editor. Gin dark mode support added via CSS custom properties (&lt;code&gt;--gin-bg-layer-rgb&lt;/code&gt;, &lt;code&gt;--gin-bg-app-rgb&lt;/code&gt;). Z-index values for the modal overlay (1300) and header (1310) were set to sit above Drupal core toolbar and Gin theme layers. A hardcoded element ID selector in &lt;code&gt;agents_form.css&lt;/code&gt; was replaced with the generic class &lt;code&gt;.ai-agent-property-restrictions-table&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Update &lt;code&gt;drupal/ai&lt;/code&gt; to &lt;code&gt;^1.3.0&lt;/code&gt;&lt;/strong&gt; in your project's &lt;code&gt;composer.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;&amp;quot;drupal/ai&amp;quot;: &amp;quot;^1.3.0&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Subclasses of &lt;code&gt;AiAgentForm&lt;/code&gt;&lt;/strong&gt; must add the &lt;code&gt;Token&lt;/code&gt; service to their constructor and pass it to &lt;code&gt;parent::__construct()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-php"&gt;public function __construct(
  FunctionCallPluginManager $functionCallPluginManager,
  FunctionGroupPluginManager $functionGroupPluginManager,
  Token $token,
) {
  parent::__construct($functionCallPluginManager, $functionGroupPluginManager, $token);
}

public static function create(ContainerInterface $container): static {
  return new static(
    $container-&amp;gt;get('plugin.manager.ai.function_calls'),
    $container-&amp;gt;get('plugin.manager.ai.function_groups'),
    $container-&amp;gt;get('token'),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Custom CSS or JavaScript&lt;/strong&gt; targeting &lt;code&gt;.modal__overlay&lt;/code&gt;, &lt;code&gt;.modal__container&lt;/code&gt;, &lt;code&gt;.modal__header&lt;/code&gt;, &lt;code&gt;.modal__title&lt;/code&gt;, &lt;code&gt;.modal__close&lt;/code&gt;, or &lt;code&gt;.modal__content&lt;/code&gt; must be updated to the prefixed equivalents:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;/* Before */
.modal__overlay { … }
.modal__container { … }

/* After */
.ai-modal__overlay { … }
.ai-modal__container { … }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: November 21, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (4 months later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;marcus_johansson (FreelyGive)&lt;/li&gt;
&lt;li&gt;emma horrell (The University of Edinburgh)&lt;/li&gt;
&lt;li&gt;ferran_bosch (1xINTERNET)&lt;/li&gt;
&lt;li&gt;bbruno (1xINTERNET)&lt;/li&gt;
&lt;li&gt;angela saldaña (1xINTERNET)&lt;/li&gt;
&lt;li&gt;jibran&lt;/li&gt;
&lt;li&gt;abhisekmazumdar (Dropsolid)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Review ran two full cycles. abhisekmazumdar found two bugs during the first review pass: the Configure button crashed on the Add Agent form because the AJAX tool picker refreshed only the tool cards widget while the modal HTML lived in a separate DOM section that was not refreshed; and tool settings (&lt;code&gt;progress_message&lt;/code&gt;, &lt;code&gt;use_artifacts&lt;/code&gt;, &lt;code&gt;description_override&lt;/code&gt;) were silently dropped on save because the submit handler still read from the old flat paths after the form nesting was changed. Both were fixed before the second review. jibran then noted that &lt;code&gt;agents_form.js&lt;/code&gt; was close to a full rewrite and suggested removing the jQuery dependency. ajv009 rewrote the file to vanilla JS using Drupal's &lt;code&gt;once()&lt;/code&gt; API, which also cleared the blocking ESLint errors. abhisekmazumdar followed up with a CSS commit that replaced a hardcoded element ID selector with a generic class and prefixed all &lt;code&gt;.modal__*&lt;/code&gt; classes to &lt;code&gt;.ai-modal__*&lt;/code&gt;. Gin dark mode issues visible during testing were filed as separate follow-up issues against the AI module (&lt;a href="https://www.drupal.org/node/3582473"&gt;#3582473&lt;/a&gt;, &lt;a href="https://www.drupal.org/node/3582474"&gt;#3582474&lt;/a&gt;) rather than blocking this issue.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3579632: Add ai:logs Drush command for inspecting AI LLM calls from the CLI</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-ai/3579632.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94eb8ee91afd</guid>
      <category>Module maintainers</category>
      <pubDate>Fri, 03 Apr 2026 03:12:41 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 2.0.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/ai_logging/-/commit/a3ad1c964ab5a5db308a635617e8a06c24cf2820"&gt;a3ad1c9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3579632"&gt;#3579632&lt;/a&gt; · 5 contributors · 22 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The ai_logging module now provides a &lt;code&gt;drush ai:logs&lt;/code&gt; command (aliases &lt;code&gt;ail&lt;/code&gt;, &lt;code&gt;ai-logs&lt;/code&gt;) to inspect AI LLM call logs from the terminal. The command queries &lt;code&gt;ai_log&lt;/code&gt; entities and displays prompts, responses, provider/model metadata, and tags. It supports filtering by tag (&lt;code&gt;--tag=ai_agents&lt;/code&gt;) or provider (&lt;code&gt;--provider=anthropic&lt;/code&gt;), limiting result count (&lt;code&gt;--count=20&lt;/code&gt;), and viewing single entries with full text (&lt;code&gt;--id=42&lt;/code&gt;). This is useful for debugging AI agents, chatbots, or other AI-powered features without opening the Drupal admin UI. The ai_logging module must be enabled and logging must be turned on at /admin/config/ai/logging/settings.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;drush ai:logs&lt;/code&gt; command (aliases &lt;code&gt;ail&lt;/code&gt;, &lt;code&gt;ai-logs&lt;/code&gt;) for inspecting AI LLM call logs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;AiLogCommands&lt;/code&gt; Drush command class uses &lt;code&gt;AutowireTrait&lt;/code&gt; for dependency injection and injects &lt;code&gt;EntityTypeManagerInterface&lt;/code&gt; to query the &lt;code&gt;ai_log&lt;/code&gt; entity storage. The command has two modes: list mode (default) returns multiple log entries sorted by &lt;code&gt;created&lt;/code&gt; DESC then &lt;code&gt;id&lt;/code&gt; DESC, and single-entry mode (&lt;code&gt;--id=N&lt;/code&gt;) returns one full entry. List mode truncates &lt;code&gt;prompt&lt;/code&gt; and &lt;code&gt;output_text&lt;/code&gt; fields to 80 characters using core's &lt;code&gt;Unicode::truncate()&lt;/code&gt;. Filtering is implemented via entity query conditions on &lt;code&gt;tags&lt;/code&gt; or &lt;code&gt;provider&lt;/code&gt; fields. The command returns &lt;code&gt;RowsOfFields&lt;/code&gt;, so Drush provides &lt;code&gt;--format=json|yaml|csv|table&lt;/code&gt;, &lt;code&gt;--fields&lt;/code&gt;, and &lt;code&gt;--filter&lt;/code&gt; options automatically. The &lt;code&gt;ValidateModulesEnabled&lt;/code&gt; attribute ensures ai_logging is enabled before execution. Ten kernel tests cover list mode, filtering, single-entry lookup, truncation, empty results, and row structure. The initial implementation was rewritten after maintainer feedback to use Drush's built-in formatters instead of custom output logic, reducing the command from approximately 500 lines to 130 lines and dropping custom token estimation and formatting utilities.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 17, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 3, 2026 (16 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ahmad khader (Vardot)&lt;/li&gt;
&lt;li&gt;ajv009 (FreelyGive)&lt;/li&gt;
&lt;li&gt;scott_euser (Soapbox)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;scott_euser (comment #16) reviewed the initial implementation and noted it was overcomplicated with custom formatters, token counters, and truncation logic, and questioned whether to accept it given the project is minimally maintained. ajv009 (comment #18) rewrote the entire command based on that feedback, replacing all custom output formatting with Drush's built-in &lt;code&gt;RowsOfFields&lt;/code&gt; formatter, replacing custom utilities with core functions like &lt;code&gt;Unicode::truncate()&lt;/code&gt;, and adding kernel test coverage. The rewrite reduced the code from approximately 500 lines to 130 lines and made the command composable with Drush's standard options. The simplified version was accepted.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
  </channel>
</rss>