Add AI ECA Interceptor submodule: intercept AI requests/responses through ECA
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3584407. -->
Reported by: [marcus_johansson](https://www.drupal.org/user/385947)
Related to !8
>>>
<p>[Tracker]<br>
<strong>Update Summary: </strong>[One-line status update for stakeholders]<br>
<strong>Short Description: </strong>New submodule that exposes AI request/response/stream/failure events to ECA as first-class Events, Conditions, and Actions.<br>
<strong>Check-in Date: </strong>MM/DD/YYYY<br>
[/Tracker]</p>
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
<p>The AI module dispatches a rich set of events around provider calls (<code>PreGenerateResponseEvent</code>, <code>PostGenerateResponseEvent</code>, <code>PostStreamingResponseEvent</code>) and exposes typed exceptions for rate limits, quotas, unsafe prompts, bad requests, moderation, etc. Today, site builders who want to react to those events - re-route a failing request to a backup provider, enforce a token/cost cap, block a prompt that hits moderation, append a system message, swap a model on the fly - must write custom PHP event subscribers.</p>
<p>ECA (Events, Conditions, Actions) is the standard no-code orchestration layer in the Drupal ecosystem, but it has no bridge into the AI pipeline. There is no way from ECA to:</p>
<ul>
<li>Listen for AI request/response/stream/failure events.</li>
<li>Filter by operation type, provider, or model.</li>
<li>Read input text, output text, token usage, rate limits, moderation state, or exception metadata via tokens.</li>
<li>Mutate the request in-flight (system prompt, messages, config, tags, provider/model reroute) or the response (replace/force output, block, count tokens).</li>
</ul>
<p>Without this bridge, every interception policy - governance, fallback, PII scrubbing, cost control, A/B routing - requires bespoke code, which is exactly the class of problem ECA was designed to solve.</p>
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<p>Add a new submodule <code>ai_eca_interceptor</code> under the AI module that exposes the AI event pipeline to ECA as first-class Events, Conditions, and Actions.</p>
<p><strong>Events (ECA event plugins, derived)</strong></p>
<ul>
<li><code>ai_request</code> - before the provider call (<code>PreGenerateResponseEvent</code>).</li>
<li><code>ai_response</code> - after the provider call (<code>PostGenerateResponseEvent</code>).</li>
<li><code>ai_stream_finished</code> - after a streamed response completes (<code>PostStreamingResponseEvent</code>).</li>
<li><code>ai_response_failed</code> - new <code>AiResponseFailedEvent</code> dispatched by a decorating provider proxy whenever the underlying provider throws.</li>
</ul>
<p>Each event supports per-instance filtering by operation type, provider id, and model id via a wildcard (<code>op:provider:model</code>).</p>
<p>A single <code>event</code> token tree exposes the full context to ECA: machine name, request thread/parent ids, provider/model/operation, tags, configuration, debug data, metadata, input (DTO-as-array plus a best-effort <code>input_text</code>), output (normalized + raw + metadata + <code>output_text</code> + <code>output_is_streamed</code>), token usage (input/output/total/reasoning/cached), rate limits, moderation flag/message, and on failure the exception class/message/code/file/line plus typed flags (<code>is_rate_limit</code>, <code>is_quota</code>, <code>is_unsafe_prompt</code>, <code>is_missing_feature</code>, <code>is_bad_request</code>, <code>is_response_error</code>, <code>is_request_error</code>).</p>
<p><strong>Conditions</strong></p>
<ul>
<li>Request/response shape: <code>OperationTypeCondition</code>, <code>ProviderIdCondition</code>, <code>ModelIdCondition</code>, <code>HasTagCondition</code>, <code>InputTextCondition</code>, <code>OutputTextCondition</code>, <code>ChatHasSchemaCondition</code>, <code>ChatHasToolsCondition</code>, <code>ChatStreamedCondition</code>, <code>TokenUsageCondition</code>, <code>ModerationFlaggedCondition</code>.</li>
<li>Failure triage: <code>AiResponseFailedCondition</code> plus one condition per typed exception (<code>IsRateLimitExceptionCondition</code>, <code>IsQuotaExceptionCondition</code>, <code>IsUnsafePromptExceptionCondition</code>, <code>IsMissingFeatureExceptionCondition</code>, <code>IsBadRequestExceptionCondition</code>, <code>IsResponseErrorExceptionCondition</code>, <code>IsRequestErrorExceptionCondition</code>).</li>
</ul>
<p><strong>Actions</strong></p>
<ul>
<li>Request-side: <code>BlockRequestAction</code>, <code>ChatAppendMessageAction</code>, <code>ChatSetSystemPromptAction</code>, <code>ChatReplaceMessageTextAction</code>, <code>ChatSetStreamedAction</code>, <code>SetConfigValueAction</code> / <code>UnsetConfigValueAction</code>, <code>SetMetadataValueAction</code>, <code>SetTagAction</code>, <code>RerouteAction</code> (switch provider/model for this call).</li>
<li>Response-side: <code>BlockResponseAction</code>, <code>ForceChatOutputAction</code>, <code>ReplaceChatOutputAction</code>, <code>SetResponseTextAction</code>, <code>CountTokensAction</code>.</li>
</ul>
<p>Blocking is implemented via <code>RequestBlockedException</code> / <code>ResponseBlockedException</code> so call sites fail loudly rather than receive silently empty results.</p>
<p><strong>Services</strong></p>
<ul>
<li><code>ai_eca_interceptor.token_builder</code> - <code>EventTokenBuilder</code> that flattens any AI event (pre/post/stream/failed) into the token tree above.</li>
<li><code>ai_eca_interceptor.provider</code> - <code>FailureDispatchingProviderPluginManager</code> decorating <code>@ai.provider</code> and returning <code>FailureDispatchingProviderProxy</code> instances that catch provider exceptions and dispatch <code>AiResponseFailedEvent</code> before rethrowing, enabling the <code>ai_response_failed</code> ECA event.</li>
<li><code>ai_eca_interceptor.reroute_subscriber</code> - <code>ProviderRerouteSubscriber</code> that honors <code>RerouteAction</code> directives written to event metadata during <code>ai_request</code>, so an ECA model can switch provider/model mid-flight.</li>
</ul>
<p><strong>Remaining tasks</strong></p>
<ul>
<li>Maintainer code review.</li>
<li>Usage examples / recipe in module README (e.g. "reroute on rate limit", "block prompts over N tokens", "strip PII from input text").</li>
<li>Decide final placement: ship as a submodule of <code>ai</code> or as a companion module under the AI ecosystem.</li>
</ul>
<p><strong>User interface changes</strong><br>
No admin UI of its own. All configuration happens through the existing ECA modeller: new event/condition/action plugins appear in the palette under the AI category.</p>
<p><strong>API changes</strong><br>
Additive only. Introduces:</p>
<ul>
<li><code>Drupal\ai_eca_interceptor\Event\AiResponseFailedEvent</code> (new event, dispatched by the decorating provider proxy).</li>
<li><code>Drupal\ai_eca_interceptor\Exception\RequestBlockedException</code> and <code>ResponseBlockedException</code> (thrown by the block actions).</li>
<li>A decorator around <code>@ai.provider</code>. Consumers that type-hint against the provider manager interface are unaffected; the proxy implements the same contract.</li>
</ul>
<p>No changes to existing AI module public APIs.</p>
<p><strong>Data model changes</strong><br>
None. No schema, no config entities, no state writes. All behavior is driven by ECA models and per-request event metadata.</p>
<p><strong>Test coverage</strong><br>
Kernel tests under <code>tests/src/Kernel/</code> exercise the end-to-end flow against a spoof provider (<code>EcaSpoofProvider</code>):</p>
<ul>
<li><code>ChatBlockRequestTest</code>, <code>ChatBlockResponseTest</code></li>
<li><code>ChatOverrideInputTextTest</code>, <code>ChatReplaceOutputTextTest</code></li>
<li><code>ChatProviderSwitchingTest</code>, <code>ChatModelSwitchingTest</code></li>
<li><code>ChatConfigSwitchingMaxTokensTest</code></li>
<li><code>ChatFailedResponseTest</code></li>
<li><code>CountTokensTest</code></li>
</ul>
<p>Shared setup lives in <code>InterceptorKernelTestBase</code>.</p>
<p><strong>Dependencies</strong></p>
<ul>
<li><code>ai:ai</code> (core AI module, ^1.1)</li>
<li><code>eca:eca</code></li>
</ul>
<p>Core: <code>^10.3 || ^11</code>.</p>
<h3 id="summary-ai-usage">AI usage (if applicable)</h3>
<p>[x] AI Assisted Issue<br>
This issue was generated with AI assistance, but was reviewed and refined by the creator.</p>
<p>[ ] AI Assisted Code<br>
[x] AI Generated Code<br>
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.</p>
<p>[ ] Vibe Coded</p>
<p>- <strong>This issue was created with the help of AI</strong></p>
issue