[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- newAiResponseFailedEventdispatched 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:
AiResponseFailedConditionplus 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-EventTokenBuilderthat flattens any AI event (pre/post/stream/failed) into the token tree above.ai_eca_interceptor.provider-FailureDispatchingProviderPluginManagerdecorating@ai.providerand returningFailureDispatchingProviderProxyinstances that catch provider exceptions and dispatchAiResponseFailedEventbefore rethrowing, enabling theai_response_failedECA event.ai_eca_interceptor.reroute_subscriber-ProviderRerouteSubscriberthat honorsRerouteActiondirectives written to event metadata duringai_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
aior 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\RequestBlockedExceptionandResponseBlockedException(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,ChatBlockResponseTestChatOverrideInputTextTest,ChatReplaceOutputTextTestChatProviderSwitchingTest,ChatModelSwitchingTestChatConfigSwitchingMaxTokensTestChatFailedResponseTestCountTokensTest
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
Issue fork ai_integration_eca-3584407
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
Comment #2
marcus_johansson commentedComment #3
marcus_johansson commentedFull transperancy - I did code the solution first, and let AI create the issue based on it afterwards.
Comment #5
marcus_johansson commentedThis 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.
Comment #6
marcus_johansson commentedWe 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.
Comment #7
rakhimandhania commented