[Tracker]
Update Summary: [One-line status update for stakeholders]
Short Description: New submodule that exposes AI request/response/stream/failure events to ECA as first-class Events, Conditions, and Actions.
Check-in Date: MM/DD/YYYY
[/Tracker]

Problem/Motivation

The AI module dispatches a rich set of events around provider calls (PreGenerateResponseEvent, PostGenerateResponseEvent, PostStreamingResponseEvent) 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.

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:

  • Listen for AI request/response/stream/failure events.
  • Filter by operation type, provider, or model.
  • Read input text, output text, token usage, rate limits, moderation state, or exception metadata via tokens.
  • Mutate the request in-flight (system prompt, messages, config, tags, provider/model reroute) or the response (replace/force output, block, count tokens).

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.

Proposed resolution

Add a new submodule ai_eca_interceptor under the AI module that exposes the AI event pipeline to ECA as first-class Events, Conditions, and Actions.

Events (ECA event plugins, derived)

  • ai_request - before the provider call (PreGenerateResponseEvent).
  • ai_response - after the provider call (PostGenerateResponseEvent).
  • ai_stream_finished - after a streamed response completes (PostStreamingResponseEvent).
  • ai_response_failed - new AiResponseFailedEvent dispatched by a decorating provider proxy whenever the underlying provider throws.

Each event supports per-instance filtering by operation type, provider id, and model id via a wildcard (op:provider:model).

A single event 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 input_text), output (normalized + raw + metadata + output_text + output_is_streamed), 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 (is_rate_limit, is_quota, is_unsafe_prompt, is_missing_feature, is_bad_request, is_response_error, is_request_error).

Conditions

  • Request/response shape: OperationTypeCondition, ProviderIdCondition, ModelIdCondition, HasTagCondition, InputTextCondition, OutputTextCondition, ChatHasSchemaCondition, ChatHasToolsCondition, ChatStreamedCondition, TokenUsageCondition, ModerationFlaggedCondition.
  • Failure triage: AiResponseFailedCondition plus one condition per typed exception (IsRateLimitExceptionCondition, IsQuotaExceptionCondition, IsUnsafePromptExceptionCondition, IsMissingFeatureExceptionCondition, IsBadRequestExceptionCondition, IsResponseErrorExceptionCondition, IsRequestErrorExceptionCondition).

Actions

  • Request-side: BlockRequestAction, ChatAppendMessageAction, ChatSetSystemPromptAction, ChatReplaceMessageTextAction, ChatSetStreamedAction, SetConfigValueAction / UnsetConfigValueAction, SetMetadataValueAction, SetTagAction, RerouteAction (switch provider/model for this call).
  • Response-side: BlockResponseAction, ForceChatOutputAction, ReplaceChatOutputAction, SetResponseTextAction, CountTokensAction.

Blocking is implemented via RequestBlockedException / ResponseBlockedException so call sites fail loudly rather than receive silently empty results.

Services

  • ai_eca_interceptor.token_builder - EventTokenBuilder that flattens any AI event (pre/post/stream/failed) into the token tree above.
  • ai_eca_interceptor.provider - FailureDispatchingProviderPluginManager decorating @ai.provider and returning FailureDispatchingProviderProxy instances that catch provider exceptions and dispatch AiResponseFailedEvent before rethrowing, enabling the ai_response_failed ECA event.
  • ai_eca_interceptor.reroute_subscriber - ProviderRerouteSubscriber that honors RerouteAction directives written to event metadata during ai_request, so an ECA model can switch provider/model mid-flight.

Remaining tasks

  • Maintainer code review.
  • Usage examples / recipe in module README (e.g. "reroute on rate limit", "block prompts over N tokens", "strip PII from input text").
  • Decide final placement: ship as a submodule of ai or as a companion module under the AI ecosystem.

User interface changes
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.

API changes
Additive only. Introduces:

  • Drupal\ai_eca_interceptor\Event\AiResponseFailedEvent (new event, dispatched by the decorating provider proxy).
  • Drupal\ai_eca_interceptor\Exception\RequestBlockedException and ResponseBlockedException (thrown by the block actions).
  • A decorator around @ai.provider. Consumers that type-hint against the provider manager interface are unaffected; the proxy implements the same contract.

No changes to existing AI module public APIs.

Data model changes
None. No schema, no config entities, no state writes. All behavior is driven by ECA models and per-request event metadata.

Test coverage
Kernel tests under tests/src/Kernel/ exercise the end-to-end flow against a spoof provider (EcaSpoofProvider):

  • ChatBlockRequestTest, ChatBlockResponseTest
  • ChatOverrideInputTextTest, ChatReplaceOutputTextTest
  • ChatProviderSwitchingTest, ChatModelSwitchingTest
  • ChatConfigSwitchingMaxTokensTest
  • ChatFailedResponseTest
  • CountTokensTest

Shared setup lives in InterceptorKernelTestBase.

Dependencies

  • ai:ai (core AI module, ^1.1)
  • eca:eca

Core: ^10.3 || ^11.

AI usage (if applicable)

[x] AI Assisted Issue
This issue was generated with AI assistance, but was reviewed and refined by the creator.

[ ] AI Assisted Code
[x] AI Generated Code
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.

[ ] Vibe Coded

- This issue was created with the help of AI

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

marcus_johansson created an issue. See original summary.

marcus_johansson’s picture

Project: AI (Artificial Intelligence) » AI Integration - ECA
Component: Documentation » Code
marcus_johansson’s picture

Full transperancy - I did code the solution first, and let AI create the issue based on it afterwards.

marcus_johansson’s picture

This stays in draft/active until I have had time to go through all lines of code. Its complete AI Generated, but using skills, examples and full testing, so it should be somewhere close to the final product.

marcus_johansson’s picture

We will remove the decoration of provider proxy and add an event into the AI module instead for failovers, so we don't have to do this work around.

rakhimandhania’s picture