Skip to content

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_selected and text_rendered provide useful extension points while keeping candidate filtering and scoring internals private.
  • A fuller pipeline with start and candidates_gathered would 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_selected and ai_context.selection.text_rendered (see below).
  • Build requests via AiContextRequestFactory or AiContextRequest directly.
  • 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.selector in services.yml.
  • Depending on AiContextSelector internals 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:

  1. Builds an AiContextRequest via AiContextRequestFactory::fromAgent()
  2. Runs the request through AiContextSelectorInterface::select() to get an AiContextResult
  3. If relevant context is found, appends it to the system prompt with a configurable prefix
  4. Records usage via AiContextUsageTracker if a runner ID is available
  5. 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:

  1. Records the tool plugin ID via AiContextUsageTracker::recordToolUsed()
  2. Attempts to extract entity information from the tool's output (e.g., "Entity of type node created with id: 123")
  3. 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.