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 or maxItems, 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 AiContextRequest / request 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 requests via
AiContextRequestFactoryorAiContextRequestdirectly. - 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_items and 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 AiContextResult cache
metadata. Whatever you consulted to make the decision should be represented in
the result's cache tags, contexts, or max-age. AiContextResult 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: getRequest(), getSelectedItems(), setSelectedItems(),
getMaxItems(), 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-agent rules, such as limiting items from a category.
Use
getMaxItems()to inspect the effective item cap.
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
AiContextResult — 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 AiContextResult 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 AiContextResult stays consistent. During the event,
getTokensUsed() still reflects the renderer estimate; the value on
AiContextResult 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_items or max_tokens, the selector
logs a warning but does not reject the result.
Event API: getRequest(), 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.
AiContextSystemPromptSubscriber
Service ID: ai_context.system_prompt_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextSystemPromptSubscriber
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:
- Builds an
AiContextRequestviaAiContextRequestFactory::fromAgent() - Runs the request through
AiContextSelectorInterface::select()to get anAiContextResult - If relevant context is found, appends it to the system prompt with a configurable prefix
- Records usage via
AiContextUsageTrackerif a runner ID is available - Attempts to extract entity information from the route context or event tokens
The context is appended in a fenced section:
[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).
Loop-aware context injection
Before building the request, the subscriber checks the agent's loop_aware
flag (configured on the per-agent context form). 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).
See Agent configuration.
AiContextAgentToolSubscriber
Service ID: ai_context.agent_tool_subscriber
Class: Drupal\ai_context\EventSubscriber\AiContextAgentToolSubscriber
Listens for tool execution events to track what tools agents use and which entities they modify.
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.