Skip to content

Services

The AI Context module registers the following services in ai_context.services.yml.

Each service below is marked Status: Public or Status: Internal. Public services are safe to depend on; internal services are implementation details that may change without deprecation. Services without an explicit label are internal; see Support services. See API stability for the full policy and the public API surface.

Core services

ai_context.selection_factory

Status: Public

Interface: Drupal\ai_context\Service\AiContextSelectionFactoryInterface

Class: Drupal\ai_context\Service\AiContextSelectionFactory (@internal implementation; type-hint the interface)

Builds AiContextSelection value objects from consumer config or raw parameters, and provides convenience methods for callers that just need rendered context.

Selection-building methods:

  • fromConsumer(string $consumerId, string $task, ...) -- builds a selection from a saved consumer config entry.
  • fromParameters(AiContextSelectionParams $params) -- builds a selection from a strongly-typed DTO (used by function call plugins and advanced consumers).

Convenience methods (build + select in one call):

// One-liner: returns the rendered context string (empty if nothing matches).
public function getRenderedContext(
  array $scopes = [],
  ?int $maxTokens = NULL,
  ?EntityInterface $requestEntity = NULL,
  ?string $consumerId = NULL,
  ?string $selectionMode = NULL,
): string

// Full result with cache metadata, selected IDs, token usage.
public function getResult(
  array $scopes = [],
  ?int $maxTokens = NULL,
  ?EntityInterface $requestEntity = NULL,
  ?string $consumerId = NULL,
  ?string $selectionMode = NULL,
): AiContextSelectionResult

Usage examples:

/** @var \Drupal\ai_context\Service\AiContextSelectionFactoryInterface $factory */
$factory = \Drupal::service('ai_context.selection_factory');

// Get rendered context text (returns empty string if nothing matches).
$text = $factory->getRenderedContext(
  scopes: ['use_case' => ['working_with_text']],
  maxTokens: 2000,
  requestEntity: $node,
);

// Or get the full result object with cache metadata and selected item IDs.
$result = $factory->getResult(
  scopes: ['use_case' => ['working_with_text']],
  maxTokens: 2000,
  requestEntity: $node,
);
$text = $result->getRenderedText();
$cacheMetadata = $result->getCacheableMetadata();
$selectedIds = $result->getSelectedItemIds();

Parameters:

  • scopes: Scope subscriptions keyed by scope plugin ID. Pass an empty array to match all published context (subject to the token budget, the global item cap, and the selector's internal candidate bound).
  • maxTokens: Maximum tokens for rendered text. NULL defers to module settings.
  • requestEntity: The matching-environment entity for request-context scope matching, not the selection object. Named-argument callers must use requestEntity:; currentEntity: no longer works.
  • consumerId: Canonical {type}:{instance} ID. Merges that consumer's saved configuration. Other strings are ignored. Pulls without a consumer log as none.
  • selectionMode: Optional trailing SELECTION_MODE_* override; NULL or an omitted argument defers to the site-wide default from ai_context.settings:selection_mode (Relevant unless changed).

Under the shipped relevant site default: global, always-included, exact-match, and situational-match items are considered without requiring a subscription; pass SELECTION_MODE_BROAD to additionally fill leftover budget with any published candidate that passes context filters.

Explicit scopes only affect the scope-scored group. Relevant and Broad still scan the full published catalog. Pass SELECTION_MODE_MINIMAL to prefilter.

On catalogs larger than 50 published items, the selector logs one watchdog warning per consumer per request. That warning is expected; it does not mean selection failed. See Debugging and #3586420.

Consumer config lookup:

  • fromConsumer(string $consumerId, string $task, ...) — builds a selection from a stored ai_context.consumers row plus plugin defaults.
  • isPushAllowed(string $consumerId): bool — whether automatic push is allowed (push_enabled, type enablement, and availability).
  • loadConsumerConfig(string $consumerId) — stored consumer config (AiContextConsumerStoredConfig) or NULL. Missing keys are not filled with defaults.
  • resolveConsumerConfig(string $consumerId) — effective config (AiContextConsumerResolvedConfig): stored config over plugin defaults, plus type enablement and availability.

See AiContextSelection and selection modes and AiContextSelectionResult below.

ai_context.scope_subscription_form

Status: Public

Class: Drupal\ai_context\Service\AiContextScopeSubscriptionFormBuilder

Interface: Drupal\ai_context\Service\AiContextScopeSubscriptionFormBuilderInterface

Builds and processes scope subscription form widgets for consumers that need the same subscription UI as the consumer editor without depending on the internal scope plugin manager.

Methods:

  • hasSubscribableScopes(): bool — whether any enabled scope supports subscriptions.
  • getNonSubscribableScopeLabels(): string[] — human-readable labels for scopes that do not support subscriptions (for example Global).
  • buildWidgets(array &$form, FormStateInterface $form_state, array $defaults = [], bool $for_subscription = TRUE): array — per-scope subscription widgets keyed by scope plugin ID.
  • extractValues(array $submitted, array $previous = []): array — normalizes raw submitted values into nested scope values (scope_id => [value_ids]). Pass previously stored subscriptions as $previous so disabled, unavailable, and missing scopes are kept.
  • buildSummary(array $scope_subscriptions): array — summary render array for selected subscriptions.

Usage example (custom entity form with scope subscriptions):

/** @var \Drupal\ai_context\Service\AiContextScopeSubscriptionFormBuilderInterface $builder */
$builder = \Drupal::service('ai_context.scope_subscription_form');

if (!$builder->hasSubscribableScopes()) {
  return;
}

$form['my_module']['scope_subscriptions'] = [
  '#type' => 'container',
  '#tree' => TRUE,
];

foreach ($builder->buildWidgets($form, $form_state, $current_scopes) as $scope_id => $element) {
  $form['my_module']['scope_subscriptions'][$scope_id] = $element;
}

// In submit/entity builder:
$submitted = $form_state->getValue(['my_module', 'scope_subscriptions']) ?? [];
$scope_values = $builder->extractValues(
  is_array($submitted) ? $submitted : [],
  $current_scopes,
);

ai_context.selector

Status: Internal

Class: Drupal\ai_context\Service\AiContextSelector

Interface: Drupal\ai_context\Service\AiContextSelectorInterface

Orchestrates context selection: filtering, scope scoring, subcontext resolution, rendering, and dispatching stable selector pipeline events. It has zero knowledge of agents or consumer-specific configuration.

Items with inheritsParentScope() are excluded from the top-level published-item scan; they enter selection with their parent via subcontext resolution or through explicit always-include overrides. Subcontext items with custom scope remain full top-level candidates. See Parent scope.

Extension model: Contrib modules extend selection via documented selector pipeline events — not by decorating this service. Decorating or replacing ai_context.selector is unsupported; the selector and its internal pipeline may change without deprecation. See Supported extension model in Events.

Key method:

public function select(AiContextSelection $selection): AiContextSelectionResult

The AiContextSelectionResult contains the rendered text, selected item metadata, token usage, and CacheableMetadata. See AiContextSelectionResult for the full API. Pipeline event names and subscriber contracts are documented in Events.

ai_context.scope_index

Status: Internal

Class: Drupal\ai_context\Service\AiContextScopeIndexService

Maintains the denormalized ai_context_item_scope_index table — one row per (item_id, scope_id, value) triple mirroring each item's effective scope (via AiContextItem::getScopeItem()). This lets the selector prefilter candidates with SQL instead of loading every published item when scope subscriptions are present.

Key methods:

  • indexItem(AiContextItem $item) — rebuild index rows for one item (called on insert/update). For subcontext items with inheritsParentScope(), rows reflect the parent's scope values. When a parent is updated, those children are reindexed in the entity update hook.
  • deindexItem(int $item_id) — remove all rows for an item
  • getItemIdsByScopeValues(string $scopeId, array $values): int[] — items matching any of the given values for one scope
  • getItemIdsByScope(array $scope_subscriptions, array $candidate_ids = []): int[] — items with at least one matching value for any subscribed scope. Missing item values earn no subscription score. Callers add global and exact-match candidates separately
  • countPublishedGlobalItems(?AiContextItem $exclude_item = NULL): int — published items that store global on themselves (field query, not the index table, so inherited child rows are excluded)
  • hasPublishedGlobalItem(): bool — whether that count is greater than zero

The entity_item scope uses a separate DER field and is not indexed here.

ai_context.scope_cleanup

Status: Internal

Class: Drupal\ai_context\Service\AiContextScopeCleanupService

Removes stale scope values from context items and consumer subscriptions. This service only strips values. Plugins that implement AiContextScopeStoredValueInterface decide which stored values are stale and whether a Drupal change should trigger a scrub.

Key methods:

  • removeScopeValues(string $scopeId, array $values) — strip values from item scope values, consumer subscriptions, and the scope index
  • scrubScope(string $scopeId) — ask one opted-in plugin which of its stored values are stale and remove them
  • scrubStaleScopeValues() — ask every opted-in plugin. Use Case is skipped because it does not implement the interface.
  • scrubScopesAfterEntityDelete(EntityInterface $entity) — scrub plugins that opt in for this delete
  • scrubScopesAfterBundleDelete(string $entityTypeId, string $bundle) — scrub plugins that opt in for this bundle delete
  • scrubScopeAfterSettingsChange(string $scopeId) — scrub one plugin after its settings config is saved
  • removeDeletedConsumerConfig(AiContextConsumerId $consumerId) — drop the stored ai_context.consumers row for a deleted instance
  • removeEntityItemReference(string $entityTypeId, int|string $entityId) — clear DER target references when a referenced entity is deleted

Triggered automatically from hook_entity_delete(), hook_entity_bundle_delete(), hook_modules_uninstalled(), and AiContextScopeConfigSubscriber (see Event subscribers below). Subscription collection reads raw ai_context.consumers (no settings.php overrides) so removal writes the same storage it read. Custom scopes opt in through AiContextScopeStoredValueInterface; do not invent a second cleanup path.

ai_context.scope_resolver

Status: Internal

Class: Drupal\ai_context\Service\AiContextScopeResolver

Scores context items against scope subscriptions and applies hard context filters (e.g., language, site section). Replaces the former AiContextScopeMatcher.

ai_context.renderer

Status: Internal

Class: Drupal\ai_context\Service\AiContextRenderer

Renders selected context items into a token-limited text block for prompt injection. Truncation and assembled-output token checks use the AI module TextChunker and @ai.tokenizer, configured from the same resolved tokenizer model as ai_context.token_estimator (explicit provider_config.model, the site chat default when use_default is enabled, or gpt-4 when neither is available). Receives pre-translated entities from the selector.

ai_context.token_estimator

Status: Internal

Class: Drupal\ai_context\Service\AiContextTokenEstimator

Estimates token counts via the AI module @ai.tokenizer service and classifies budget-relative weight tiers for context text. Used by the renderer for token budgeting, by the listing UI for token badges, and to persist token_count on context items at save time. The tokenizer is a shared service that other consumers re-point at their own model, so the configured model is re-applied before every count. The selector uses this after TEXT_RENDERED to recalculate AiContextSelectionResult::getTokensUsed() from the final rendered text.

The model name is only used to select a local tokenizer encoding. Resolution matches chat provider lookup: an explicit provider_config.model, else the site-wide AI chat default when use_default is enabled, else gpt-4.

When the resolved tokenizer model changes, AiContextSettingsConfigSubscriber hands the change to ai_context.token_count_backfill_scheduler. That includes ai_context.settings:provider_config and, when CCC is using the site chat default, a change to ai.settings default_providers.chat. Config saves act immediately; configuration imports record the change in state (durable across the batch requests a UI import spans) and act at ConfigEvents::IMPORT. See Recounting after a model change.

ai_context.token_count_backfill_scheduler

Status: Internal

Class: Drupal\ai_context\Service\AiContextTokenCountBackfillScheduler

Owns the chunked token_count recount shared by update 10023, the settings batch, and the cron queue worker, so all three write counts identically.

How a recount runs depends on what the request can finish:

Context Mechanism
CLI (Drush config set/import, scripts) backfillAll(), synchronous
A request that processes a batch (settings form save) Batch, draining the queue
Any other web request (programmatic save, UI config import) Queue only, drained by cron

Non-CLI scheduling always enqueues first, because a request that never processes a batch would otherwise drop the recount silently. The batch consumes the same queue, so a form save recounts immediately and leaves cron nothing to repeat.

Chunks write only the token_count column of the exact revision each count was read from, keyed on ID, revision ID, and langcode. A concurrent editorial save creates a newer revision, so the update matches no rows and the editor's own preSave() count stands — the recount can never overwrite content changes.

Key methods:

  • scheduleIfTokenizerModelChanged(mixed $old, mixed $new): void — no-op when the resolved model is unchanged or no items exist
  • schedule(): void — queue or run a recount after the resolved model changed (including a site chat default change)
  • backfillChunk(int $last_id): array{last_id: int, processed: int} — recount one chunk; a processed of 0 means the catalog is exhausted
  • backfillAll(): int — recount everything, blocking
  • processQueuedChunk(): array / drainQueue(): int — consume queued cursors

ai_context.subscription_budget

Status: Internal

Class: Drupal\ai_context\Service\AiContextSubscriptionBudgetCalculator

Calculates subscribed context token totals for the consumer settings form budget summary. Returns AiContextSubscriptionBudgetSummary, an internal value object consumed by AiContextConsumerForm.

Totals sum each item's stored guidance content (token_count). They do not include render overhead such as item IDs, labels, purpose lines, guidance indentation, block separators, or truncation. Compare with AiContextSelectionResult::getTokensUsed() for runtime usage after selection.

Candidate narrowing mirrors AiContextSelector so the summary counts what runtime selection would actually inject:

  • Items are counted in the detected content language, matching the translation the selector injects.
  • Always-include items bypass scope handling; never-include items are dropped; inaccessible items are dropped; inheriting children are only counted through their parent.
  • Hard context filters (filterByRequestContext()) apply to everything else, on every selection mode — not only MINIMAL without subscriptions.
  • max_global_items caps globals (higher Priority first, then shorter content, then highest ID). Other groups use the selector's internal per-group candidate bound. Groups merge in this order: global, always-included, exact-match, scope-scored, situational-match (Relevant and Broad), and broad-fill.
  • Relevant (the shipped site-wide default when selection_mode is omitted) includes situational-match items from scopes declaring supportsSituationalMatch() — Languages, Site Sections, Entity Types, and Taxonomy Terms by default. Minimal does not.
  • Subscription scoring runs before situational-match so a Minimal match cannot be dropped when the situational-match group hits its internal cap. Situational-match classification remains as a fallback for candidates displaced from the final scope-scored group. Overflow situational-match items still feed broad-fill.
  • Final scope-scored duplicates are removed before the broad-fill group is pruned, so they cannot consume its candidate allowance.
  • Relevant and Broad stream published IDs and load entities in 200-item chunks. Each group is pruned and memory-cache entries introduced by the scan are released after every chunk. Broad-fill still runs when subscriptions exist.

Results are cached per consumer configuration, module settings, scope-plugin configuration, effective permissions, and request context (scope context values, target entity, content language, and request path) for one hour. Cache tags include ai_context_item_list, config:ai_context.settings, config:ai_context.consumers, and each config:ai_context.scope_settings.* object, plus metadata supplied by the user.permissions cache context. The consumer settings lazy builder varies by target entity, detected content language, permissions, and URL path.

Key methods:

  • summarize(array $consumer_config): AiContextSubscriptionBudgetSummary — within-budget, conditional-range, or over-budget totals for one consumer config entry

ai_context.subcontext_resolver

Status: Internal

Class: Drupal\ai_context\Service\AiContextSubcontextResolver

Resolves child (subcontext) items for selected parent items. When conditional AI decisions are enabled, filters conditional children via the configured provider/model; without a provider or on failure, conditional children for that parent are skipped. When conditional AI decisions are disabled, conditional children are excluded from normal subcontext resolution. Skipped entirely when subcontext_enabled is FALSE in module settings. This service assumes saved parent-child relationships already satisfy the entity integrity rules.

See Subcontext for when to use parent/child items.

ai_context.request_info_resolver

Status: Internal

Class: Drupal\ai_context\Service\AiContextRequestInfoResolver

Resolves request information used for matching: the entity (when there is one) and the effective page path. Used by Entity Type, Specific Entity, and Site Section scopes. Site builders should start with Page and entity context.

Entity sources, in order:

  1. Request attributes ai_context_entity_type and ai_context_entity_id (set by setEntity(), applyRequestInfo(), or a caller)
  2. Route parameters (any loaded entity)
  3. Request body values (entity_type / entity_id in POST or JSON)

Effective page path sources, in order:

  1. Request attribute ai_context_path (set by setPath(), applyRequestInfo(), or a caller)
  2. Request body values (path in POST or JSON)
  3. $request->getPathInfo()

applyRequestInfo() copies untrusted path and entity values onto the request. The usual source is a BuildSystemPromptEvent payload (getTokens() means those keys, not LLM token counts). A route entity wins over an array entity. A path-like path or route value is stored and preferred over getPathInfo(). Route names are ignored.

Entity and path resolution deliberately invert each other. A route entity wins over a caller-supplied entity, because the route is usually the page being viewed. A caller-supplied path wins over getPathInfo(), because on API-proxy routes getPathInfo() is the proxy endpoint, not the viewed page. Making the two rules identical would either reopen this issue (path never overrides) or let a payload replace the entity you are actually on (entity always overrides).

Callers that run on API-proxy routes (for example a chatbot endpoint) must supply the viewed page path. Site Sections matches that hint instead of the proxy path. A supplied path is an untrusted context hint used for string matching only. It is not used to load files, follow redirects, or grant access. Absolute URLs are reduced to their path.

/** @var \Drupal\ai_context\Service\AiContextRequestInfoResolver $resolver */
$resolver = \Drupal::service('ai_context.request_info_resolver');

$request = \Drupal::request();
$request->attributes->set(
  AiContextRequestInfoResolver::PATH,
  '/node/1',
);
// Or:
$resolver->setPath('/node/1');

ai_context.usage_tracker

Status: Internal

Class: Drupal\ai_context\Service\AiContextUsageTracker

Records usage data when context items are selected. Tracks which items were used, by which consumer, on which route, and which entities were involved. recordUsage() requires a canonical consumer ID. The runner ID and entity item are optional so non-agent consumers can record usage.

Performance notes:

  • recordUsage() batches the existing-record lookup with a single entity query using context_item_id IN (...).
  • attachEntity() batches duplicate checks for entity-specific records.
  • recordToolUsed() buffers tool IDs in memory; call flushToolsUsed() once per agent run (handled automatically by AiContextAgentFinishedSubscriber on AgentFinishedExecutionEvent).
  • Lookup indexes on runner_id, changed, (entity_item_type, entity_item_id), and (context_item_id, consumer_id) are managed by _ai_context_ensure_usage_indexes() in ai_context.install.

Tracking is controlled by the usage_tracking_enabled setting (disabled by default). When disabled, all tracker methods return immediately without database access.

ai_context.children

Status: Internal

Class: Drupal\ai_context\Service\AiContextChildrenService

Provides parent/child hierarchy queries for context items and supports cleanup of child references when a parent item is deleted. Revision view pages use loadChildrenForParentAtTime() so Nav panel siblings match the child list at the viewed revision timestamp.

ai_context.item_view_builder

Status: Internal

Class: Drupal\ai_context\Service\AiContextItemViewBuilder

Builds template variables for the ai_context_item theme hook. AiContextThemeHooks::preprocessAiContextItem() delegates to preprocess() on this service. Only the full view mode (canonical and revision routes) receives sidebar and header variables; other view modes get content field render arrays only.

Key methods:

  • preprocess(array &$variables) -- for full view mode, populates title, parent link, subcontext row, status badge, scope rows, author, nav items, and content field render arrays; bubbles cache metadata onto $variables (the top-level preprocess array ThemeManager reads). Non-full view modes only expose content.
  • buildStatus(AiContextItem $entity) -- published/moderation status label and CSS modifier, including "Published (Draft available)" when Content Moderation is enabled.
  • buildScopeRows(AiContextItem $entity, ?CacheableMetadata $cacheability = NULL) -- sidebar scope rows. Global items show only the global row (Enabled). When at least one non-global scope has values, every enabled scope row is included (global first as Disabled, empty scopes show None); otherwise returns an empty array. When a collector is passed, Specific Entities, Context Tags, and Taxonomy Terms labels use getSelectedValueLabelsWithCacheability() so access results and term cache tags bubble.
  • buildScopeSection(AiContextItem $entity, ?CacheableMetadata $cacheability = NULL) -- empty-state SCOPE heading and message when global is disabled and no non-global scopes are in use. Full-view preprocess computes rows and section in one pass so labels are not formatted three times. The builders themselves stay independent (no memo) for kernel tests. View-specific label formatting (custom site section paths, collapsed entity type labels) is applied in buildScopeRows().
  • addScopeViewCacheMetadata(CacheableMetadata $cacheability, AiContextItem $entity) -- adds each scope plugin's config tag (including disabled plugins, since enabled is stored in ai_context.scope_settings.*). Also adds the languages:language_interface context so translated plugin labels, "None", and language names vary by UI language. Tag term tags and Specific Entities access cacheability are collected while building scope rows, not reconstructed here.

ai_context.item_validator

Status: Internal

Class: Drupal\ai_context\Service\AiContextItemValidator

Validates persistence-level invariants for context items. For subcontexts, it prevents self-parenting, nested subcontexts, and converting an item with children into a subcontext. It also requires subcontext_type when a parent is selected. The published-global limit uses AiContextScopeIndexService::countPublishedGlobalItems().

ai_context.scheduler_defaults

Status: Internal

Class: Drupal\ai_context\Service\AiContextSchedulerDefaults

Applies default moderation states when Scheduler publish/unpublish dates are set on context items without an explicit publish_state or unpublish_state. Used during entity save so editors only need to set schedule dates when Content Moderation and Scheduler are enabled.

Key method:

  • applyScheduledModerationStates(ContentEntityInterface $entity) — sets published when publish_on is set and unpublished when unpublish_on is set, if the corresponding state field is empty.

Selection and result value objects

AiContextSelectionParams

Status: Public

Class: Drupal\ai_context\Model\AiContextSelectionParams

Immutable DTO passed to AiContextSelectionFactory::fromParameters(). Centralizes validation and normalization of raw selection parameters before an AiContextSelection is built. Use AiContextSelectionParams::fromArray() when constructing from plugin context, form values, or other untyped input. consumer_id merges canonical {type}:{instance} config. Non-canonical consumer_id values are dropped. Pulls without a consumer log as none.

AiContextConsumerStoredConfig and AiContextConsumerResolvedConfig

Status: Public

loadConsumerConfig() returns AiContextConsumerStoredConfig or NULL. Omitted overrides stay NULL or empty; they are not filled with defaults. isPushEnabled() on stored config is TRUE, FALSE, or NULL when the config inherits the type default.

resolveConsumerConfig() returns AiContextConsumerResolvedConfig. That object is stored config over plugin defaults, plus isTypeEnabled() and isTypeAvailable(). isPushEnabled() on the resolved object is always a bool. isPushAllowed() ANDs the push gates with that flag. Loop-aware is not on these objects; use AiContextConsumerTypeAgent::isLoopAware(). Limits that are still NULL inherit the site default when a selection is built.

Exported ai_context.consumers rows store always_include and never_include as context item UUIDs. The stored and resolved config objects expose those values as-is. The selection factory resolves them to local entity IDs when building an AiContextSelection. Legacy numeric IDs are still accepted at runtime.

See AiContextSelection for the fields carried into the resulting selection.

AiContextSelection and selection modes

Status: Public

Class: Drupal\ai_context\Model\AiContextSelection

Immutable value object built by AiContextSelectionFactory::fromConsumer() or fromParameters(). Callers should obtain results through ai_context.selection_factory (getResult(), getRenderedContext(), or fromParameters() followed by selection via the factory), not by injecting ai_context.selector directly.

Constructor fields:

Field Description
task Task description or user prompt (used for conditional subcontext)
scopeSubscriptions Scope values keyed by scope plugin ID
alwaysInclude / neverInclude Local item IDs to force in or out after the factory resolves stored UUIDs (legacy numeric IDs still work)
consumerId Canonical consumer ID after merge, or NULL
entityType / entityId Optional target for entity/bundle matching
maxGlobalItems / maxTokens Per-request limits; NULL falls back to module settings
selectionMode minimal, relevant, or broad

Selection modes (constant SELECTION_MODE_* on the class) are a nested ladder: each mode is a strict superset of the one before it, and hard filters apply in every mode.

  • minimal: only global items, exact-match (Specific Entities matches), always/never overrides, and strict scope subscription matches are considered. An item with no values for a subscribed scope no longer earns automatic credit for that scope.
  • relevant (default for fromConsumer(), fromParameters(), getResult(), and getRenderedContext()): Minimal, plus items that scopes declaring supportsSituationalMatch() (Languages, Site Sections, Entity Types, and Taxonomy Terms by default) positively match against the request context (matchesRequestContext() returns TRUE), even without a subscription to that scope.
  • broad: Relevant, plus any remaining published candidate that passes hard context filters, ordered by Priority, appended after the scope-scored bucket to fill leftover token budget. It supplements, never replaces, the scope-scored bucket.

There is no runtime alias for the removed match_all mode; update 10028 rewrites match_all to broad for rows it migrates out of the legacy ai_context.agents config (any other unrecognized legacy mode is dropped with a logged notice, so the row inherits the site default), and passing an unrecognized mode explicitly (e.g. via AiContextSelectionParams) throws \InvalidArgumentException. Values read back from stored consumer config are handled more defensively: an unrecognized stored value is logged and treated as relevant, so a single corrupted consumer entry cannot break every selection.

Function call plugins expose selection_mode explicitly on get_relevant_ai_context_items. See Function calls for the stable plugin IDs (ai_context:list_ai_context_items, ai_context:load_ai_context_item_by_id, ai_context:get_relevant_ai_context_items) and agent tool workflow.

AiContextSelectionResult

Status: Public

Class: Drupal\ai_context\Model\AiContextSelectionResult

Immutable result from context selection via ai_context.selection_factory. Implements CacheableDependencyInterface.

Methods:

Method Returns
getRenderedText(): string Token-limited context block for prompt injection
getSelectedItemIds(): string[] IDs of selected items
getSelectedItems(bool $fullData = FALSE): array Item metadata keyed by ID; $fullData includes scope, tags, subcontext fields, content snippet
getTokensUsed(): int Approximate tokens consumed by the final rendered text (recalculated after selector pipeline events)
getMaxTokens(): int Max token cap applied during rendering
getTruncatedItems(): string[] IDs of items omitted or trimmed because the token budget was insufficient
getCacheableMetadata(): CacheableMetadata Full cache metadata object
getCacheTags() / getCacheContexts() / getCacheMaxAge() CacheableDependencyInterface delegates

Example:

$result = \Drupal::service('ai_context.selection_factory')->getResult(
  scopes: ['language' => ['en']],
  maxTokens: 1200,
);

$text = $result->getRenderedText();
$ids = $result->getSelectedItemIds();
$metadata = $result->getCacheableMetadata();

Plugin managers

plugin.manager.ai_context_scope

Status: Internal

Class: Drupal\ai_context\AiContextScopeManager

Plugin manager for scope plugins. Handles discovery, instantiation, and ordering of AiContextScope plugins. getScopePlugins() returns all plugins in ascending display_weight order, with plugin ID used to break ties. Core and custom plugins are interleaved according to their declared display_weight. The scoring_weight attribute is separate and controls subscription scoring influence independently of display order; values below 1 are treated as 1 during scoring.

Support services

Status: Internal — All services in this section and the sections below (event subscribers, route subscribers, access checks, and other support services) are internal. They are not labeled individually; see API stability.

ai_context.markdown_renderer

Class: Drupal\ai_context\Markdown\MarkdownRenderer

Converts GitHub Flavored Markdown content to HTML for display (including tables).

ai_context.provider_request_context_factory

Class: Drupal\ai_context\Service\AiContextProviderRequestContextFactory

Builds an immutable AiContextProviderRequestContext from a PreGenerateResponseEvent. Copies tags, conversation text, and structured metadata only. Live chat and file objects are not retained. Builder in this module is reserved for UI list/view/form builders.

logger.channel.ai_context

Standard Drupal logger channel for ai_context log messages.

Event subscribers (registered as services)

ai_context.build_system_prompt_subscriber

Class: Drupal\ai_context\EventSubscriber\AiContextBuildSystemPromptSubscriber

Listens for BuildSystemPromptEvent and AgentStartedExecutionEvent. See Events.

ai_context.pre_generate_response_subscriber

Class: Drupal\ai_context\EventSubscriber\AiContextPreGenerateResponseSubscriber

Listens for PreGenerateResponseEvent at priority -100. See Events.

ai_context.agent_tool_subscriber

Class: Drupal\ai_context\EventSubscriber\AiContextAgentToolSubscriber

Listens for AgentToolFinishedExecutionEvent. See Events.

ai_context.scope_config_subscriber

Class: Drupal\ai_context\EventSubscriber\AiContextScopeConfigSubscriber

When any ai_context.scope_settings.{plugin_id} config is saved, asks that plugin whether to scrub stored values. Skips the scrub while config is being imported so plugins do not compare stored values to a half-imported environment. Bundle-delete hooks still run during import. See ai_context.scope_cleanup.

Route subscribers

ai_context.route_subscriber

Class: Drupal\ai_context\Routing\AiContextRouteSubscriber

Alters routes for the module.

ai_context.scope_route_subscriber

Class: Drupal\ai_context\Routing\AiContextScopeRouteSubscriber

Dynamically generates routes for per-scope settings forms based on discovered scope plugins.

Access checks

ai_context.access_check.overview_page

Class: Drupal\ai_context\Access\AiContextOverviewAccessCheck

Controls access to the overview page based on the show_overview_page setting.

ai_context.access_check.usage_page

Class: Drupal\ai_context\Access\AiContextUsageAccessCheck

Controls access to the usage tracking page based on whether tracking is enabled.

Other services

ai_context.uninstall_validator

Class: Drupal\ai_context\AiContextUninstallValidator

Runs during module uninstall validation with higher priority than core's content validator. Its sole responsibility is schema repair, installing missing content entity schemas provided by this module before core queries entity tables. This prevents fatal errors on sites where entity tables were never created (for example config-sync installs without entity updates).

The AI Context Tags vocabulary (ai_context_tags) is not checked here: its config declares an enforced dependency on ai_context, so Drupal will remove the vocabulary and any terms automatically on uninstall. For older sites that installed ai_context before this dependency was added, update hook ai_context_update_10012() backfills the enforced dependency on active config.

Drupal\ai_context\Hook\DiffHooks

Class: Drupal\ai_context\Hook\DiffHooks

Autowired service for Diff module integration hooks.

Drupal\ai_context\Hook\DynamicEntityReferenceHooks

Class: Drupal\ai_context\Hook\DynamicEntityReferenceHooks

Autowired service for Dynamic Entity Reference module integration hooks.