Events
Selector pipeline events
AI Context exposes two stable selector events for contrib modules that need to
inspect or alter selected context without coupling to AiContextSelector
internals. Event name constants live on
Drupal\ai_context\Event\AiContextSelectionEvents. The
ai_context.selection.* event name prefix is reserved for selector pipeline
events.
The module intentionally exposes only the later selector stages:
items_selectedandtext_renderedprovide useful extension points while keeping candidate filtering and scoring internals private.- A fuller pipeline with
startandcandidates_gatheredwould offer more flexibility, but it would also make intermediate selector stages part of the public API before there are concrete contrib requirements. - Observer-only events would be safer for refactors, but would not support the ticket goal of allowing supported selection alteration.
- Plugin-based filters or boosters may be useful later, but would add more architecture than this stabilization issue needs.
Potential future start use cases include adding consumer-specific scope
subscriptions, lowering maxTokens, adding always/never
include IDs from route, role, feature flag, or workflow state, or skipping
selection entirely (return an empty result without loading, scoring, or
rendering — for example feature flags, maintenance mode, or route/agent
policy).
Potential future candidates_gathered use cases include removing candidates
based on external policy, taxonomy, workspace, tenant, or site-section rules;
moving items between internal candidate groups; or inspecting how many items
survived filtering. These use cases are deferred because candidate groups are
more likely to change during selector refactors.
The selector pipeline names form one lifecycle family. The two current events ship now; the earlier-stage names are reserved so the namespace stays consistent when they are added:
| Stage | Class | Constant | Event name |
|---|---|---|---|
| Start (reserved) | AiContextSelectionStartEvent |
START |
ai_context.selection.start |
| Candidates gathered (reserved) | AiContextSelectionCandidatesGatheredEvent |
CANDIDATES_GATHERED |
ai_context.selection.candidates_gathered |
| Items selected | AiContextSelectionItemsSelectedEvent |
ITEMS_SELECTED |
ai_context.selection.items_selected |
| Text rendered | AiContextSelectionTextRenderedEvent |
TEXT_RENDERED |
ai_context.selection.text_rendered |
Supported extension model
Contrib and site-specific integrations should extend selection through the
documented selector pipeline events and AiContextSelection / selection factory
APIs — not by decorating or replacing ai_context.selector.
Supported:
- Subscribe to
ai_context.selection.items_selectedandai_context.selection.text_rendered(see below). - Build selections via
AiContextSelectionFactoryorAiContextSelectiondirectly. - Skip calling
select()entirely when your integration controls the call site (for example loop-aware agent injection).
Not supported:
- Decorating, extending, or replacing
ai_context.selectorinservices.yml. - Depending on
AiContextSelectorinternals such as candidate loading, scoring, merge order, or subcontext resolution.
The selector pipeline may change without deprecation. Events define the stable public contract; the selector implementation is internal. For the module-wide public/internal boundary, see API stability.
When the selector finds no candidates, ITEMS_SELECTED still fires so
subscribers can inject items — but that is not a skip: the full pipeline has
already run. Skipping work before any database queries requires a future
start event (see above).
Subscriber constraints
Configured max_tokens limits are enforced by the selector and renderer
before events fire. Event subscribers may override those outcomes (for
example by injecting additional items or replacing rendered text). When
subscribers alter selection, they are responsible for staying within any budget
or policy their integration requires.
Cache metadata: Subscribers that alter the selected item list or rendered
text based on runtime state (route, role, workspace, config, feature flags,
and so on) must ensure that state is reflected in the AiContextSelectionResult cache
metadata. Whatever you consulted to make the decision should be represented in
the result's cache tags, contexts, or max-age. AiContextSelectionResult is immutable;
during selection, merge dependencies via addCacheableDependency() on the
event where you make the decision — ITEMS_SELECTED when you alter the item list,
TEXT_RENDERED when you alter rendered text. Both flow into the result the
selector builds. Entity cache tags for selected items are added automatically
during normal result building.
Items added or replaced via setSelectedItems() are not re-run through scope
matching, scoring, or request-level neverInclude filtering. They must still
pass a view access check, but contrib should not assume injected items went
through the normal candidate pipeline.
When the selector finds no candidates, it still dispatches ITEMS_SELECTED with
an empty item list so subscribers can inject items before rendering. Injected
items on that path skip resolveSubcontextItems(), so parent subcontexts will
not expand automatically.
Subscribers on the same event run in Symfony registration order unless a
priority is set on the service tag or getSubscribedEvents() return value.
ITEMS_SELECTED always runs before TEXT_RENDERED.
Items selected
Event name: ai_context.selection.items_selected
Class: Drupal\ai_context\Event\AiContextSelectionItemsSelectedEvent
Fires after priority merge and subcontext resolution, before selected items are
rendered. Subscribers may filter or reorder the final selected item array via
setSelectedItems(). Injected items bypass scope matching and scoring; only
view access is validated.
setSelectedItems() throws \InvalidArgumentException when any value is not
an AiContextItem, and \Drupal\Core\Access\AccessException when any item
fails a view access check (the whole batch is rejected).
Event API: getSelection(), getSelectedItems(), setSelectedItems(),
getCacheableMetadata() (clone), addCacheableDependency()
Supported use cases:
- Filter, remove, or reorder the final selected item list before rendering.
- Remove selected items that conflict with other selected items.
- Add a mandatory context or disclaimer item after normal selection.
- Enforce per-consumer rules, such as limiting items from a category.
Cache metadata: Injected or retained items receive entity cache tags when
the selector builds the result. When your items-selected changes depend on runtime
context beyond those entities (for example route or workspace), call
addCacheableDependency() on this event so that state is represented on
AiContextSelectionResult — you no longer need to also subscribe to TEXT_RENDERED for
cache metadata. getCacheableMetadata() returns a clone; mutating it does not
affect the result. See Subscriber constraints.
Text rendered
Event name: ai_context.selection.text_rendered
Class: Drupal\ai_context\Event\AiContextSelectionTextRenderedEvent
Fires after selected items are rendered and before the AiContextSelectionResult is
constructed. Subscribers may alter the rendered text and merge cache metadata
into the result via addCacheableDependency(). The selected items and the
IDs of items truncated during rendering are read-only — exposed for observation
and telemetry, not modification.
After subscribers run, the selector recalculates tokensUsed from the final
rendered text so AiContextSelectionResult stays consistent. During the event,
getTokensUsed() still reflects the renderer estimate; the value on
AiContextSelectionResult reflects post-event text. Do not pass untrusted input into
setRenderedText(); subscriber output is injected into agent prompts without
further sanitization.
getCacheableMetadata() returns a clone. Mutating it does not affect the
result; use addCacheableDependency() to merge tags, contexts, or max-age.
When a subscriber exceeds configured max_tokens, the selector logs a warning
during rendering but does not reject the result.
Event API: getSelection(), getSelectedItems() (read-only),
getRenderedText(), setRenderedText(), getTokensUsed(), getMaxTokens(),
getTruncatedItems() (read-only), getCacheableMetadata() (clone),
addCacheableDependency()
Supported use cases:
- Observe final rendered text for debugging, telemetry, audit trails, or developer tools.
- Add a wrapper, delimiter, provenance note, or prompt-cache hint.
- Redact sensitive patterns from final rendered text.
- Add cache metadata when subscriber output depends on route, user role, workspace, or other context.
Subscribers must preserve access-checked entities, avoid logging sensitive
context content unless it is explicitly safe, add cache metadata whenever
their changes depend on additional runtime state, and treat setRenderedText()
as trusted output only.
Example subscriber:
<?php
declare(strict_types=1);
namespace Drupal\example\EventSubscriber;
use Drupal\ai_context\Event\AiContextSelectionTextRenderedEvent;
use Drupal\ai_context\Event\AiContextSelectionItemsSelectedEvent;
use Drupal\ai_context\Event\AiContextSelectionEvents;
use Drupal\Core\Cache\CacheableMetadata;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class ExampleAiContextSelectionSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
AiContextSelectionEvents::ITEMS_SELECTED => 'onItemsSelected',
AiContextSelectionEvents::TEXT_RENDERED => 'onTextRendered',
];
}
public function onItemsSelected(AiContextSelectionItemsSelectedEvent $event): void {
$items = $event->getSelectedItems();
$event->setSelectedItems(array_slice($items, 0, 3, TRUE));
}
public function onTextRendered(AiContextSelectionTextRenderedEvent $event): void {
$event->setRenderedText($event->getRenderedText() . "\n<!-- checked -->\n");
// Cache metadata can only be merged, never replaced.
$metadata = new CacheableMetadata();
$metadata->addCacheContexts(['user.roles']);
$event->addCacheableDependency($metadata);
}
}
AI Agents integration events
The AI Context module uses two event subscribers to integrate with the AI Agents module.
AiContextBuildSystemPromptSubscriber
Service ID: ai_context.build_system_prompt_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextBuildSystemPromptSubscriber
This is the primary integration point between AI Context and AI Agents. It listens for two events:
AgentStartedExecutionEvent (priority 100)
Captures the runner ID when an agent starts execution. The runner ID is stored temporarily and used later to associate usage records with the correct agent run.
BuildSystemPromptEvent
Fires when the agent's system prompt is being assembled. The subscriber:
- Checks the agent's
push_enabledflag viaAiContextSelectionFactory::isPushAllowed(); if disabled, returns immediately and none of the following steps run - Passes
BuildSystemPromptEventpayload values toAiContextRequestInfoResolver::applyRequestInfo()so the resolver can resolve the entity and page path - Builds an
AiContextSelectionviaAiContextSelectionFactory::fromConsumer() - Runs the selection through
AiContextSelectorInterface::select()to get anAiContextSelectionResult - If relevant context is found, appends it to the system prompt with a configurable prefix
- Records usage via
AiContextUsageTrackerif a runner ID is available - Reads the resolved entity and path from the resolver for usage tracking. The subscriber always has the agent ID from this event; the tool path does not. See Page and entity context.
The context is appended in a context block:
[configurable prefix text]
-----------------------------------------------
[rendered context items]
-----------------------------------------------
The prefix text is configurable at /admin/config/ai/context/settings/general.
Default: "The following site-specific context applies to this task. Use it
strictly when relevant; do not override user intent."
The prefix is translatable via Configuration translation
(/admin/config/regional/config-translation → AI Context settings).
Allow context injection
Before anything else, the subscriber checks the agent's
push_enabled flag (configured on the consumer editor, default
enabled) and the Agent type kill switch. When either is disabled, the
subscriber returns immediately: no selection is built or run, and no
usage is recorded for that call. This fully suppresses push-based
injection -- global items, always-include overrides, and
scope-subscribed matches alike -- since none of the pipeline that
would produce them executes.
This does not affect pull-based context retrieval: scope_subscriptions,
always_include, and never_include are dual-duty settings that
AiContextSelectionFactory::fromParameters() still merges into tool-call
selections that carry the agent's ID, regardless of this flag.
GetRelevantAiContextItems and ListAiContextItems inherit the
running agent ID when agent_id is omitted, so those defaults apply
without a prompt instruction naming the agent. Disable injection for agents that should
retrieve context on demand via tool plugins
(GetRelevantAiContextItems, ListAiContextItems) instead of receiving
it automatically.
Loop-aware context injection
Before building the selection, the subscriber checks
settings.loop_aware (configured on the consumer editor for
the Agent type). When loop-aware is enabled and the current
loop count is greater than zero, context injection and usage recording
for that iteration are skipped entirely.
Use loop-aware injection when first-iteration context is sufficient (for example tone or style guidance). Leave it disabled when agents need context re-injected on every loop (for example factual references or output format rules).
AiContextPreGenerateResponseSubscriber
Service ID: ai_context.pre_generate_response_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextPreGenerateResponseSubscriber
Pushes context into generic chat calls on PreGenerateResponseEvent.
Agents keep using BuildSystemPromptEvent; this subscriber is the
non-agent path.
The subscriber runs at priority -100 so
GlobalGuardrailsEventSubscriber (priority 100) and the
guardrail/moderation subscribers (priority 0) run first. Requests they
reject never receive context, and guardrails never evaluate injected
site context as if it were user input.
AiContextAutomatorIdTagSubscriber is a temporary internal bridge
at priority -50. AI Automators do not yet emit
ai_automator:id:{id}. When the request has ai_automator plus
exactly one entity type, bundle, and field tag, and exactly one
ai_automator config entity matches those properties, this
subscriber adds the ID tag so the injector can claim the request.
It does not guess .default. When a field has more than one
automator, it uses the clicked field-widget-action UUID from the
current POST if that action maps to one matching automator.
Direct, cron, or other non-widget runs on a shared field still
leave the tags unchanged. An ID tag already present is left
alone. Remove this subscriber when AI core adds the ID in
RuleBase::getTags().
AiContextAutomatorWidgetOnlySubscriber is a second temporary
bridge. A field-widget click still asks AI Automators to process
the whole field, so every automator on that field would run and
the last write would win. When the POST identifies one widget
action and that automator is allowed to push, this subscriber
force-skips the other automators. Direct or cron runs, and
clicks whose automator cannot push, are left alone.
Remove it when AI Automators run only the clicked instance.
PreGenerateResponseEvent
The subscriber:
- Exits unless the operation type is
chatand the input is aChatInput - Re-publishes the stored result and exits when the
ChatInputalready carries anai_contextdebug-data key (a tool-call inner request reusing the outer input); no second context block is appended - Exits without writing metadata when request tags include
ai_agentsorai_assistant_api, or when no consumer type routes for the tags - Asks routing types for a consumer ID or a decline
- Skips push-disabled, type-disabled (including unavailable), stale, or ambiguous consumers
- Builds a selection from saved subscriptions, using the latest user
message as the task and
entity_contextmetadata as the entity (never the route) - Appends the same context block used on the agent path
- Records usage with a NULL runner and the
entity_contextentity when present - Writes an
ai_contextinvocation result to event metadata and the chat input debug data. When debug logging is on, also writes one privacy-safe watchdog line with status, consumer ID, item IDs, and request tags. See Debugging.
An absent ai_context key means the request was not handled
(non-chat, unrouted tags, excluded agent/assistant tags, or a
higher-priority subscriber stopped the event). Assistants that are
not backed by an agent receive no push in 1.x: the generic path
excludes ai_agents and ai_assistant_api, and no assistant
consumer type exists.
Agent-backed assistants use the agent path. Distinguished skip
reasons use the AiContextInvocationResult status constants. The
payload contains consumer_id, status,
item_count, and item_ids only — never prompt text or rendered
context.
Both channels are best-effort. A subscriber that returns a forced
output, stops propagation, or replaces the event input can leave the
caller without the result. See the AI core ProviderProxy behavior
for those edge cases.
AI Context tests this path with the test module's consumer types and
the shipped automator type. AI CKEditor plugins are owned by that
project. Automator chat calls are claimed only when they carry
ai_automator:id:{id} and that automator exists.
Field, bundle, and entity tags are not a plugin fallback. A
temporary subscriber may add the ID tag when those properties
uniquely identify one automator, or when a widget-action click
identifies one of several automators on the same field. Attach
entity_context on the chat input so Entity Types, Taxonomy, and
Specific Entities can match. Image, speech, and other non-chat
operations never hit this subscriber. Multi-value fields generate
one chat call per delta, so context is selected and usage-tracked
N times; per-instance max_tokens is the budget control.
AiContextAgentToolSubscriber
Service ID: ai_context.agent_tool_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextAgentToolSubscriber
Listens for tool execution events to inherit the running agent ID on context tools and to track what tools agents use.
AgentToolPreExecuteEvent
Fires immediately before $tool->execute(). Tools opt in by
implementing AiContextInheritsAgentIdInterface and declaring an
optional agent_id context definition. When agent_id is omitted, the
subscriber sets it from $event->getAgentId(). An explicit agent_id
argument is left unchanged. Non-agent callers never dispatch this
event.
GetRelevantAiContextItems and ListAiContextItems implement the
interface. LoadAiContextItemById does not, because it has no
agent_id parameter. Custom or third-party tools may opt in the same
way.
If the resolved ID has no saved CCC configuration, the existing
"no context config found for agent" warning from
AiContextSelectionFactory::fromParameters() still applies.
AgentToolFinishedExecutionEvent
Fires when an agent tool completes execution. The subscriber:
- Records the tool plugin ID via
AiContextUsageTracker::recordToolUsed() - Attempts to extract entity information from the tool's output (e.g., "Entity of type node created with id: 123")
- If entity info is found, attaches it to the usage record via
AiContextUsageTracker::attachEntity()
This provides visibility into what agents actually do with the context they receive.
Tool IDs are buffered in memory during the run and are not written to the
database on each tool call. When an entity is attached via
attachEntity(), any buffered tools are merged onto the in-memory usage
records before they are saved.
AiContextAgentFinishedSubscriber
Service ID: ai_context.agent_finished_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextAgentFinishedSubscriber
Listens for agent completion so buffered tool usage can be flushed once per run instead of on every tool event.
AgentFinishedExecutionEvent
Fires when an agent run completes. The subscriber calls
AiContextUsageTracker::flushToolsUsed() with the runner ID so any tool IDs
buffered by recordToolUsed() are persisted in a single load-and-save cycle.