Skip to content

API stability

This page defines which parts of AI Context are public (safe to depend on) and which are internal (implementation details that may change without a deprecation period). It exists so contrib developers and site builders can answer one question without reading selector internals: what am I allowed to depend on?

The default rule

If an API is not listed as public on this page, treat it as internal.

  • Public APIs follow Drupal's deprecation policy: behavior changes are announced and a deprecation path is provided across minor releases.
  • Internal APIs may change, move, or be removed in any release without notice. Depending on them couples your code to implementation details that are expected to evolve.

While the module is in beta, public APIs may still change between releases without a full deprecation cycle — check release notes when upgrading. Once the module reaches a stable release, the boundaries on this page describe the supported surface. See also Upgrade and compatibility policy.

Deprecated APIs

CCC 1.0.x does not currently ship @deprecated public APIs. When a supported entry point is deprecated, it will be listed here with its replacement and the release that removes it.

If you depend on classes or services marked @internal in code, migrate to the public surfaces on this page — those internals are not deprecated; they were never part of the supported API.

Beta upgrade: entity scope terminology

The entity scope APIs were renamed before the 1.0.0 stable release. There are no backward-compatibility wrappers for the old names. Deploy the new code and run drush updatedb in the same release window, then update custom code and configuration that uses the old identifiers.

Old New
Scope plugin entity_bundle / AiContextScopeEntityBundle entity_type / AiContextScopeEntityType
Scope plugin target_entity / AiContextScopeTargetEntity entity_item / AiContextScopeEntityItem
Cache context ai_context.target_entity ai_context.entity_item
Base field target_entities entity_items
Usage fields target_entity_type / target_entity_id entity_item_type / entity_item_id
Usage field agent_id consumer_id plus derived consumer_type
Views field ai_context_agent_link ai_context_consumer_link
Views field ai_context_target_entity_link ai_context_entity_item_link
Service ai_context.entity_target_resolver / AiContextEntityTargetResolver ai_context.current_entity_resolver / AiContextCurrentEntityResolver
Service ai_context.current_entity_resolver / AiContextCurrentEntityResolver ai_context.request_info_resolver / AiContextRequestInfoResolver
Request factory parameter targetEntity requestEntity

The AiContextItem methods were renamed or replaced as follows:

Old New
getTargetEntities() getEntityItems()
hasTargetEntities() hasEntityItems()
getTargetEntityData() Removed; use getEntityItems()
buildTargetEntityRenderArray() Removed; use getEntityItems()
getTargetEntityMarkup() Removed; use getEntityItems()
matchesTargetEntity() matchesEntityItem()
getTagIds() / getContextTagIds() Removed; use getScopeValues('tag')

The AiContextUsage getters and setters (internal entity) now use EntityItem instead of TargetEntity, for example getEntityItemType() and setEntityItemId(). getAgentId() / setAgentId() are replaced by getConsumerId() and getConsumerType(). getRunnerId() is nullable. Do not depend on this entity from outside the module.

Updates 10015 through 10019 migrate stored scope values and revisions, scope configuration, usage storage and Views configuration, and the optional Dynamic Entity Reference field storage. Update hooks cannot rewrite custom PHP or render arrays, so custom cache-context tokens, plugin IDs, class names, and method calls must be changed during the deployment.

Beta upgrade: max_items removal

The Max context items to inject limit was removed before the 1.0.0 stable release. The token budget (max_tokens) is now the only user-facing limit on overall injected context; max_global_items still caps the Global group. There are no backward-compatibility wrappers.

Removed Replacement
max_items in ai_context.settings and per-agent overrides None; use max_tokens
maxItems parameter on AiContextSelection and AiContextSelectionParams None
AiContextSelectionItemsSelectedEvent::getMaxItems() (constructor no longer takes a max-items argument) None
max_items parameter on the get_relevant_ai_context_items function call None
AiContextLimitsConstraint and its validator None; the cross-limit validation is obsolete

Update 10020 removes the obsolete max_items keys from active configuration, and legacy configuration exports are sanitized during import. Custom code that passes maxItems or calls getMaxItems() must be updated in the same release window.

Update 10023 installs the token_count base field on ai_context_item and backfills stored estimates. See Configuration: Upgrading.

Beta upgrade: selection modes

The Match all selection mode (match_all) was removed before the 1.0.0 stable release. Context selection is now a nested ladder of three public modes. There is no runtime alias for match_all.

Removed Replacement
match_all stored agent selection_mode broad
Default getRenderedContext() / getResult() mode of match_all relevant

Post-update ai_context_post_update_0003_rewrite_match_all_selection_mode() rewrites stored agent selection_mode: match_all to broad, and ai_context_post_update_0004_add_site_default_selection_mode() backfills the new site-wide default key ai_context.settings:selection_mode with relevant. Relevant is the new-install site default. Beta4 agents implicitly used Minimal; update 10028 and the Beta4 config-import transform write explicit minimal when a leftover agent row omits selection_mode or stores an unrecognized mode. Deploy the new code and run drush updatedb in the same release window. fromArray() drops an unrecognized mode (including leftover match_all) to NULL so the factory inherits the consumer or site-wide default. Constructing AiContextSelectionParams with a non-NULL invalid string and passing it to fromParameters(), getResult(), or getRenderedContext() still throws \InvalidArgumentException. Unrecognized values read from stored consumer config are logged and resolve to the site-wide default.

See Selection modes below and Services for the full field list.

Beta upgrade: stale scope values

Post-update ai_context_post_update_0006_scrub_stale_scope_values() removes leftover values that objectively no longer exist: deleted roles, taxonomy terms, languages (when Language is available), entity types, and bundles. Live settings-save cleanup is unchanged and still strips options removed from scope settings, as decided in #3586416.

The upgrade scrub is conservative because updatedb usually runs before config import:

  • Disabled or unavailable scopes are skipped, so stored Language values survive on a single-language site.
  • Entity Types values are removed only when the type or bundle is gone from Drupal, not merely missing from pre-import settings.
  • Site Section values are left in place; incoming section config can restore them.
  • Custom and other settings-derived scopes are left in place.
  • Use Case values are not scrubbed.

Re-export configuration after drush updatedb. Values backed by deleted entities are still removed and are not restored by import.

Beta upgrade: scope field JSON:API shape

The context item scope field was replaced before the 1.0.0 stable release: a single serialized map became a multi-value scope_id + value field. This changes the field's shape everywhere the Field API's default normalization is used, including JSON:API. There is no backward-compatible wrapper.

// Before: one object, scope_id as key
"scope": {"use_case": ["working_with_text"], "language": ["en"]}

// After: a list of {scope_id, value} pairs, one per delta
"scope": [
  {"scope_id": "use_case", "value": "working_with_text"},
  {"scope_id": "language", "value": "en"}
]

getScope() / setScope() still return and accept the grouped scope_id => [value, ...] shape in PHP; only the field's own Field API and JSON:API representation changed. See Storing scope values for the two shapes and which callers use which one.

Update 10026 migrates stored scope values and revisions from the old map storage; see Configuration: Upgrading.

Beta upgrade: scope availability

Scope plugins gained isAvailable() on AiContextScopeInterface before the 1.0.0 stable release. AiContextScopeBase defaults it to TRUE. Direct implementations of the interface (without the base class) must add the method. Override it when scope usability depends on site configuration that may be missing; Language returns FALSE when the site has fewer than two languages.

AiContextScopeCacheableMetadataInterface gained getAvailabilityCacheableMetadata(). Plugins that implement that companion interface directly (without AiContextScopeBase) must add the method. The base class returns empty metadata; Language adds config:configurable_language_list.

AiContextScopeSubscriptionFormBuilderInterface::extractValues() gained an optional $previous argument. Direct implementations of the public form-builder interface must accept it so a save can keep subscriptions for disabled, unavailable, or missing scopes. Existing callers can omit it; omitted previous values are not preserved.

Disabled and unavailable scopes are hidden from forms and ignored at runtime, but stored item values and consumer subscriptions are kept on save unless the item is Global or a subcontext that inherits parent scope. Those two cases still strip non-applicable stored scope in preSave(). See Scopes and Scope API.

Beta upgrade: scope usable gate

AiContextScopeInterface gained isEnabledAndAvailable(). AiContextScopeBase returns isEnabled() && isAvailable(). Direct implementations of the interface (without the base class) must add the method in the same release window.

Call $plugin->isEnabledAndAvailable() from contrib. Do not inject plugin.manager.ai_context_scope to use the manager's same-named helper; that class remains @internal.

Beta upgrade: consumer type availability cacheability

AiContextConsumerTypeInterface gained getAvailabilityCacheableMetadata(). AiContextConsumerTypeBase returns empty metadata. Direct implementations of the interface (without the base class) must add the method in the same release window. Override it when isAvailable() depends on modules or other site state. Automator adds config:core.extension. Callers that cache enumerations or listings must merge the returned metadata; the router and admin listings already do.

See Consumer type API.

Beta upgrade: consumer type settings NULL clears the key

submitTypeSettingsForm() values are merged into ai_context.consumer_type_settings.{type} by key. A NULL value now clears that key, matching scope settings. Keys omitted from the returned array still stay in config. Custom types that returned NULL expecting a stored null must return a real value or omit the key instead.

See Consumer type API.

Beta upgrade: buildValueForm and buildWidgets drop &$form

AiContextScopeInterface::buildValueForm() and AiContextScopeSubscriptionFormBuilderInterface::buildWidgets() now take array $form instead of array &$form. Nothing wrote the parent form. Custom scopes or form-builder implementations that still declare array &$form will fatal; PHP requires the reference to match. Drop the &. Callers that passed $form without a reference are unchanged.

Method Old first argument New first argument
buildValueForm() array &$form array $form
buildWidgets() array &$form array $form

See Scope API and Services.

Beta upgrade: fromArray drops unrecognized selection_mode

AiContextSelectionParams::fromArray() drops an unrecognized selection_mode to NULL, matching a non-canonical consumer_id. Unrecognized non-empty strings are logged at debug. The DTO uses \Drupal::logger() because fromArray() has no injection; the call is guarded by hasContainer(). Non-string selection_mode, consumer_id, and entity_type values (arrays, objects, integers) are dropped without logging so casting cannot warn or throw. entity_id keeps a non-empty string or int, matching the constructor. Other entity_id types are dropped without logging. The factory then inherits the consumer mode or the site-wide default. Direct fromArray() callers no longer throw on junk selection_mode, including leftover match_all. Get Relevant still has a Choice constraint, so AI Agents can reject those values before fromArray() runs. Constructing params with a non-NULL invalid string and passing them to fromParameters(), getResult(), or getRenderedContext() still throws \InvalidArgumentException.

See Selection modes.

Beta upgrade: Get Relevant accepts string entity_id

GetRelevantAiContextItems used to return Invalid entity_id: must be numeric. for a non-numeric value. It now passes entity_id to fromArray(), the same as List. Non-empty strings and integers are kept. Other types are dropped. Config-entity machine names can reach scope matching.

See Function calls.

Beta upgrade: invalid constructor selection_mode throws

AiContextSelection and AiContextSelectionResult throw \InvalidArgumentException when constructed with an unrecognized selection_mode. This used to be assert(), which is a no-op in production. fromArray() still drops junk to NULL. The factory still throws for an explicit invalid mode on fromParameters(), getResult(), and getRenderedContext().

See Selection modes.

Beta upgrade: selection result cache metadata is a clone

AiContextSelectionResult::getCacheableMetadata() returns a clone, matching the selection events. Mutating the returned object does not change the result. Read tags, contexts, and max-age from the result itself.

See ai_context.selection_factory.

Beta upgrade: usage limit Range

usage_max_records and usage_max_age now have schema Range min: 0. There is no max, so existing high values still import. 0 remains valid (pruning treats it as unlimited on that axis). Negative values fail config import.

Beta upgrade: consumer getSettings()

AiContextConsumerStoredConfig and AiContextConsumerResolvedConfig now expose getSettings(). Stored returns the saved map or empty. Resolved merges stored keys over getInstanceDefaults()['settings']. Agent isLoopAware() reads the resolved bag instead of walking ai_context.consumers rows.

Beta upgrade: getResultFromSelection()

AiContextSelectionFactoryInterface::getResultFromSelection() is the supported execute path for a selection built with fromConsumer() or fromParameters(). Do not inject ai_context.selector. getResult() and getRenderedContext() still build a limited selection internally, then call this method.

Beta upgrade: getTruncatedItems() is getTruncatedItemIds()

AiContextSelectionResult and AiContextSelectionTextRenderedEvent renamed getTruncatedItems() to getTruncatedItemIds(). The internal renderer result key truncated_items is now truncated_item_ids. There are no backward-compatibility wrappers.

Beta upgrade: null push_enabled inherits the type default

push_enabled: null on a consumers row used to cast to FALSE and turn push off. It now matches an omitted key. Stored isPushEnabled() is NULL. Resolve and isPushAllowed() use the type default. Config schema allows a null flag.

See Push gates.

Beta upgrade: leftover public API removals

These leftovers were removed or demoted before the 1.0.0 stable release. There are no backward-compatibility wrappers.

Old New
Scope getId() getPluginId()
AiContextScopeInterface::isDynamic() Removed. Use getAlteredValues(), cache-metadata companions, and isAvailable()
AiContextConsumerInstanceCollection::getCacheableMetadata() Removed. Use CacheableDependencyInterface (getCacheTags(), getCacheContexts(), getCacheMaxAge())
AiContextConsumerId::SEPARATOR Private. Build IDs with fromParts() / fromString()
AiContextConsumerId::isValidType() @internal. Validate full IDs with isValid() / fromString()
AiContextConsumerRouter::resolveConsumerId() Removed. Use routeRequest(). Type plugins still implement resolveConsumerId()

See Scope API and Consumer type API.

Beta upgrade: resolveConsumerId does not check excluded tags

This is the type-plugin method. The router's thin resolveConsumerId() wrapper was removed; router callers use routeRequest(). Type plugins still implement resolveConsumerId().

The router already drops ai_agents and ai_assistant_api before calling the plugin method. Plugins must not call the @internal hasExcludedRequestTag() helper. Returning NULL still declines a request for type-specific reasons.

See Consumer type API.

Beta upgrade: request-context terminology

The scope plugin API and request-factory named parameter were renamed before the 1.0.0 stable release so they describe the request context, which a caller can supply, rather than implying the HTTP current route. There are no backward-compatibility wrappers. Deploy the new code and update custom scope plugins and named-argument callers in the same release window.

Old New
AiContextScopeInterface::matchesCurrentContext() matchesRequestContext()
AiContextScopeInterface::getCurrentValue() getDetectedValue()
AiContextScopeBase::doGetCurrentValue() doGetDetectedValue()
Request factory parameter currentEntity requestEntity
AiContextScopeSiteSection::matchesCurrentPath() matchesRequestPath() (now protected; use matchesRequestContext())

requestEntity is a named parameter. Callers that used currentEntity: $node must switch to requestEntity: $node. Positional callers are unaffected.

Direct implementations of AiContextScopeInterface (without AiContextScopeBase) must rename matchesCurrentContext() and getCurrentValue(). Plugins that extend the base class must rename doGetCurrentValue() if they override it, and any override of matchesCurrentContext().

Internal resolver and manager methods (filterByRequestContext(), getRequestContextValues()) and protected Taxonomy helpers (getRequestEntityTermUuids()) were renamed in the same sweep and need no upgrade note.

AiContextScopeBase::isCurrentManageRoute() and $currentUser are unchanged: those still mean the current admin route and the acting session user.

Beta upgrade: request-context matching uses getDetectedValue()

matchesRequestContext() on AiContextScopeBase, Entity Type, and Entity Item now reads the request value through getDetectedValue(). doGetDetectedValue() stays the in-class hook that the public method calls. Custom scopes that override getDetectedValue() to wrap or replace the hook now affect matching. Overrides of matchesRequestContext() that still call doGetDetectedValue() directly keep the old bypass.

Method Override behavior
getDetectedValue() Used by matching and by the scope manager
doGetDetectedValue() Protected hook only; matching does not call it

See Scope API.

Beta upgrade: selection API terminology

The public selector-job value objects and factory were renamed before the 1.0.0 stable release from Request to Selection. This keeps the selector job distinct from request-context matching APIs and provider-call request snapshots. There are no backward-compatibility wrappers. Deploy the new code and update custom callers in the same release window.

Old New
AiContextRequest AiContextSelection
AiContextRequestParamsData AiContextSelectionParams
AiContextResult AiContextSelectionResult
AiContextRequestFactory AiContextSelectionFactory (class now @internal; type-hint AiContextSelectionFactoryInterface)
AiContextRequestFactoryInterface AiContextSelectionFactoryInterface
Service ID ai_context.request_factory ai_context.selection_factory
AiContextSelectionItemsSelectedEvent::getRequest() getSelection()
AiContextSelectionTextRenderedEvent::getRequest() getSelection()

The SELECTION_MODE_* constants now live on AiContextSelection. Method names that already described selection behavior are unchanged, including fromParameters(), fromConsumer(), getResult(), AiContextSelector::select(), and the config keys consumer_id, selection_mode, and push_enabled.

The request-context APIs intentionally keep their names: matchesRequestContext(), requestEntity, and AiContextRequestInfoResolver. Provider-call snapshot APIs such as AiContextProviderRequestContext, resolveConsumerId(), and request tags are also unchanged.

Beta upgrade: scope capability methods

Scope plugins gained supportsExactMatch() and supportsSituationalMatch() on AiContextScopeInterface before the 1.0.0 stable release (#3586419), splitting the former dual-purpose supportsSubscriptions() flag into explicit capabilities. AiContextScopeBase defaults both new methods to FALSE — exact-match and situational-match are opt-in. Direct implementations of the interface (without the base class) must add both methods.

Removed or changed Replacement
supportsSubscriptions() gating exact-match eligibility and Minimal prefilter candidacy supportsExactMatch(); a custom scope that returned supportsSubscriptions(): FALSE and relied on exact-match must now declare supportsExactMatch(): TRUE
Hardcoded situational scope list (SITUATIONAL_SCOPE_PLUGIN_IDS on the internal AiContextScopeResolverInterface) supportsSituationalMatch(); Languages, Site Sections, Entity Types, and Taxonomy Terms declare it
Internal AiContextScopeResolverInterface::getNonSubscriptionIndexedItemIds() getExactMatchIndexedItemIds()

supportsSubscriptions() keeps its signature with a narrowed, documented contract: form visibility, subscription scoring, and save-time value preservation. Behavior is unchanged for every built-in scope (Entity Item declares supportsExactMatch(): TRUE; Languages, Site Sections, Entity Types, and Taxonomy Terms declare supportsSituationalMatch(): TRUE); the capability matrix is frozen by a kernel test. The only migration is for custom scopes that relied on the implicit non-subscription-implies-exact-match rule, per the table above.

One selection behavior changed in the same release: situational matching now requires the scope to be enabled and available, the same gate every other selection path uses (it previously required only enabled). Concretely, on a monolingual site the Languages scope is unavailable, so items still carrying language values from before a language was removed, or from imports, are no longer situationally included in Relevant mode. They still pass hard filters and remain reachable through Broad mode, Global, Always include, or another matching scope, and they resume matching when a second language makes the scope available again. The budget estimates apply the same gate, so Relevant estimates no longer count scopes that selection ignores.

Beta upgrade: situational prefilter method

Scope plugins gained getMatchingIndexedValues(array): ?array on AiContextScopeInterface before the 1.0.0 stable release (#3586420), an intentional public addition so the Relevant selection mode can prefilter candidates through the scope index instead of scanning the full published catalog. Given the distinct values indexed for the plugin, it returns the subset that can situationally match the current request, using the same predicate as matchesRequestContext().

AiContextScopeBase defaults it to NULL, the safe fallback: a situational scope that does not say how its indexed values match forces Relevant back to the full catalog scan — correct but slow, and logged by the full-scan warning. Plugins that extend the base class keep compiling and stay correct by default; direct implementations of the interface must add the method. Return NULL when matches cannot be enumerated from indexed values (for example, values in custom storage); return the matching subset — never a lossy guess — to keep the prefilter fast. Languages, Entity Types, Site Sections, and Taxonomy Terms override it; AiContextMatchingIndexedValuesContractTrait is the reusable kernel test guard for the predicate symmetry.

The method only affects scopes declaring supportsSituationalMatch(); exact-match and subscription prefiltering are unchanged.

Beta upgrade: Languages no longer support subscriptions

The Languages scope now returns supportsSubscriptions(): FALSE (#3586448). It remains a hard filter and still declares supportsSituationalMatch(): TRUE. This is the first built-in that combines no subscriptions with situational matching.

Removed or changed Replacement
Language widgets on consumer subscription forms Hidden; Languages is listed in the "do not allow subscriptions" note
Language credit in subscription scoring None. Matching the request language no longer boosts an item over an untagged peer
Minimal reach via a language subscription Language-only items are unreachable in Minimal except via Always include. Raise the selection mode to Relevant for situational inclusion

Stored scope_subscriptions.language values are preserved dormant on save (#3586412) and stay valid options, so stale-value scrubbing (#3586416) still removes them if the language itself is uninstalled. They do not affect ranking.

The capability matrix kernel test now expects language => [FALSE, FALSE, TRUE].

Minimal's subscription prefilter now drops non-participating scopes before it queries the index, so a language-only subscription is treated like no subscriptions (global, Always include, and exact-match still apply).

Beta upgrade: Specific Entities is availability-gated

Specific Entities was hidden by deleting its plugin definition when dynamic_entity_reference was absent. It now declares isAvailable(): FALSE instead (#3586448), matching how Languages behaves on a single-language site. ai_context_ai_context_scope_info_alter() is no longer implemented by this module; the alter hook itself is unchanged and still available to third parties.

Changed Effect
AiContextScopeManager::getScopePlugins() Always returns 10 built-ins, including entity_item, on every site
Scope overview and settings tab A Specific Entities row appears with status Unavailable on sites without dynamic_entity_reference
Matching, scoring, and forms Unchanged: every caller already gates on isEnabledAndAvailable()

Code that treated a missing entity_item definition as "DER is not installed" must call $plugin->isEnabledAndAvailable() (or the plugin's isAvailable()) instead. Nothing else changes: stored values are still preserved, the item form still hides the scope, and no update hook is needed.

Beta upgrade: scope weight split

The single weight attribute on AiContextScope has been replaced by two independent parameters (#3586438):

  • display_weight (ascending integer, lower = first) controls form, tab, and listing order.
  • scoring_weight (nullable integer) controls subscription scoring influence.
Changed Effect
AiContextScopeInterface::getWeight() Removed. Replace calls with getDisplayWeight() or getSubscriptionScoringWeight().
AiContextScopeManager::getScopePluginDisplayWeights() Removed. Use $plugin->getDisplayWeight() directly.
Plugin attribute weight: Renamed. Use display_weight: for display order and scoring_weight: for scoring influence.
Sort order Now ascending on display_weight (lower = first), matching Drupal #weight. Previously descending (higher = first).

Custom scope plugins must rename weight: to display_weight: in their #[AiContextScope] attribute and re-evaluate whether they need an explicit scoring_weight:. If no scoring_weight: is declared, scoring influence defaults to 1 (the floor), not the old display weight.

The direction inverted, so the key rename is not a 1:1 swap. Mechanically renaming weight: 25 to display_weight: 25 moves a custom scope from near the bottom of the form to third from the top.

hook_ai_context_scope_info_alter() implementations that still set $definitions['x']['weight'] fail silently. The definition key is now display_weight. There is no warning and no fallback.

Ranking of subscribed items changes on existing sites. There is no config migration and none is possible. Use Cases now carry twice the influence of the topical scopes (Context Tags, Site Sections, Entity Types, Taxonomy Terms), which are tied with each other. Form order is not ranking.

Site builders can override either weight per scope via the scope settings page Advanced section — no plugin code change needed. The scoring override is only offered for scopes that support subscriptions.

Beta upgrade: consumer configuration

Per-agent context configuration was replaced by consumers before the 1.0.0 stable release. Agents remain one consumer type. There are no backward-compatibility wrappers for the old APIs, config object, or admin paths. Old /settings/agents URLs are gone.

Removed Replacement
fromAgent() fromConsumer('agent:' . $id)
findAgentConfig() loadConsumerConfig() / resolveConsumerConfig()
Config object ai_context.agents ai_context.consumers
Config key allow_context_injection push_enabled
isInjectionAllowed() isPushAllowed()
Admin paths /settings/agents /settings/consumers and /settings/consumers/{consumer_id}/edit
Usage field agent_id consumer_id plus consumer_type
Views field ai_context_agent_link ai_context_consumer_link

isPushAllowed() is the factory helper for the combined push gate. It reads push_enabled, the type-wide enablement kill switch, and isAvailable(). Empty or non-canonical IDs return FALSE. A valid ID with no stored config uses plugin defaults. Callers that need push_disabled vs type_disabled should use isTypeEnabled(), isTypeAvailable(), and isPushEnabled() on resolveConsumerConfig() instead. Unavailable types use the same type_disabled invocation status.

Loop-aware injection is stored as settings.loop_aware on agent rows. getSettings() on stored config is the saved map or empty. getSettings() on resolved config merges that map over plugin instance defaults. AiContextConsumerTypeAgent::isLoopAware() reads settings.loop_aware from resolveConsumerConfig(). The agent subscriber uses the Agent type helper. It is not a factory method.

Update 10028 copies ai_context.agents rows to ai_context.consumers (canonical IDs agent:{id}, loop_aware → settings.loop_aware) and deletes the old object. Missing or unrecognized leftover selection_mode values become explicit minimal; match_all becomes broad. An interrupted 10028 that saved consumers without a mode treats that omission as Minimal on resume. A completed earlier 10028 is not rewritten: omitted consumer modes keep inheriting the site default. Updates 10029–10031 migrate usage fields and the usage View. After drush updatedb, re-export site configuration.

A complete Beta4 sync directory (ai_context.agents, no ai_context.consumers, settings without selection_mode) is transformed on import: leftover agent rows become consumer rows, shipped consumer-type settings are copied from active config (or created enabled only when active has none), active ai_context.scope_settings.roles is copied when present so a disabled Roles scope is not re-enabled, leftover views.view.ai_context_usage handlers are remapped in place (agent_id / ai_context_agent_link become consumer_id / ai_context_consumer_link) so source customizations are kept and stale handlers are not imported when active has no View, leftover ai_context_map view-display formatters and ai_context_scope_map_field_diff_builder Diff plugin IDs are remapped in place (same rules as update 10026), a leftover beta1/beta2 usage View path is remapped (same rules as update 10027), and ai_context.agents is not recreated. Those repairs also run for a sync directory that lost ai_context.agents.yml but still carries structural leftovers. Converted consumer rows and the remapped View, view-display, and Diff payloads are written in schema key order, so re-importing the same Beta4 sync directory reports no changes even though the upgraded site saved those objects in cast order. Duplicate rows for one agent collapse into the first row when they convert identically; conflicting duplicates are rejected, matching update 10028. The transform replaces the consumers object rather than merging like 10028, so consumer rows added after the upgrade and absent from a Beta4 export are removed by that import. It listens at priority -50, after Config Split's merge and before Config Ignore. An ai_context.agents object without an agents list is rejected, not imported as empty. Settings may still omit selection_mode after import (requiredKey: false is for that Beta4 window). Runtime selection and the General settings radios fall back to Relevant; saving the form persists that default. Ordinary imports that omit type settings are not rewritten. Importing leftover ai_context.agents alongside ai_context.consumers is still a validation error that asks for a re-export from an upgraded site; leftovers without a structural marker are not detected, so removing the file alone is not a fix.

See Configuration: Update 10028.

Beta upgrade: tool parameter agent_id is consumer_id

Get Relevant and List renamed the function-call parameter agent_id to consumer_id. Saved AI Agents tool_usage_limits and property_description_override values still keyed as agent_id are ignored. Post-update 0010 renames those keys on ai_agent and ai_agent_override entities. If both keys exist, consumer_id is kept. Bare force_value / only_allow values such as my_agent are upgraded to agent:my_agent. Empty values stay empty. A leftover string-shaped values entry is turned into a one-item list first. Re-save limits on consumer_id if you skip updatedb. Then re-export configuration.

See Configuration: Post-update 0010.

Beta upgrade: Context Tags and Taxonomy Terms store term UUIDs

Context Tags (tag) and Taxonomy Terms (taxonomy) scope values are taxonomy term UUIDs. They used to be serial term IDs, which Default Content and recipes cannot keep across installs. Matching is string equality, so a leftover term ID never matches a UUID.

# Before: serial term ID
scope:
  - scope_id: tag
    value: '5'

# After: term UUID
scope:
  - scope_id: tag
    value: 7a1e8432-3616-438e-a411-76c93afba733

The same change applies to scope_subscriptions.tag and scope_subscriptions.taxonomy in ai_context.consumers, to the scope index, and to the scope_subscriptions parameter of Get Relevant and List. AiContextItem::getScopeValues('tag') now returns term UUIDs; load terms with loadByProperties(['uuid' => $values]) instead of loadMultiple(). getContextTagIds() is removed; it was a wrapper around getScopeValues('tag'). The edit forms are unchanged: editors still pick terms by name.

hook_ai_context_scope_values_alter() receives the tag value list keyed by term UUID. An implementation that adds or removes Context Tags by serial term ID (for example unset($values['5'])) no longer matches anything and must key on the UUID instead. Taxonomy Terms still passes an empty list.

Post-update 0013 converts items, revisions, the scope index, and consumer subscriptions. A term ID whose term no longer exists stays in place and stays unmatched. Re-export configuration after updatedb.

See Configuration: Post-update 0013.

Public API

Selection factory service

Service ID ai_context.selection_factory (Drupal\ai_context\Service\AiContextSelectionFactoryInterface) is the supported entry point for running context selection from your own code. Type-hint the interface. Replace or decorate the service rather than depending on the @internal concrete class.

API Purpose
getRenderedContext() One-liner returning the rendered context string for non-agent consumers
getResult() Full AiContextSelectionResult with cache metadata, selected IDs, token usage, and the resolved selection mode
getResultFromSelection() Run a built AiContextSelection from fromConsumer() or fromParameters()
fromConsumer() Build an AiContextSelection from a saved consumer config entry
fromParameters() Build an AiContextSelection from typed parameters
isPushAllowed() Whether automatic push is allowed (push_enabled, type enablement, and availability)
loadConsumerConfig() Stored consumer config, or NULL
resolveConsumerConfig() Row over plugin defaults, plus type gates

The trailing $selectionMode argument on getRenderedContext() and getResult() is public; passing NULL (or omitting it) defers to the site-wide default in ai_context.settings:selection_mode, which ships as relevant. $consumerId merges canonical {type}:{instance} config. Pulls without a consumer log as none. See Selection modes.

See Services for full signatures and examples.

Scope subscription form builder

Service ID ai_context.scope_subscription_form (Drupal\ai_context\Service\AiContextScopeSubscriptionFormBuilderInterface) is the supported way to build and process scope subscription widgets on forms that are not the shared consumer editor. Type-hint the interface. Replace or decorate the service rather than depending on the @internal concrete class.

API Purpose
hasSubscribableScopes() Whether any enabled scope supports subscriptions
getNonSubscribableScopeLabels() Labels for scopes that cannot be subscribed to
buildWidgets() Per-scope subscription form elements (same UI as the consumer editor)
extractValues() Normalize submitted widgets into nested scope values; pass stored subscriptions as $previous so hidden scopes are kept
buildSummary() Render-array summary of selected subscriptions

Do not depend on plugin.manager.ai_context_scope for subscription UI; use this service instead. See Services.

Selection and result value objects

  • Drupal\ai_context\Model\AiContextSelectionParams — validated parameter DTO for fromParameters(). Build instances with fromArray() when passing raw input from forms, plugins, or other loosely typed sources.
  • Drupal\ai_context\Model\AiContextConsumerStoredConfig — stored ai_context.consumers config from loadConsumerConfig(). getSettings() is the saved type-specific map or empty. getSelectionMode() returns the raw stored value, which can be inherit as well as a ladder mode; resolve it before building an AiContextSelection, or use AiContextConsumerResolvedConfig.
  • Drupal\ai_context\Model\AiContextConsumerResolvedConfig — stored config over plugin defaults from resolveConsumerConfig(). getSettings() is stored settings over getInstanceDefaults()['settings'].
  • Drupal\ai_context\Model\AiContextConsumerInstance — one enumerated consumer (canonical ID, label, optional description) from getInstances().
  • Drupal\ai_context\Model\AiContextConsumerInstanceCollection — the getInstances() return: the list plus cacheability (IteratorAggregate, Countable, CacheableDependencyInterface). There is no getCacheableMetadata(); use the cache triad.
  • Drupal\ai_context\Model\AiContextSelection — the immutable selection passed into selection.
  • Drupal\ai_context\Model\AiContextSelectionResult — the immutable result returned from selection (rendered text, selected item IDs, token usage, the resolved selection mode, cache metadata). getCacheableMetadata() returns a clone. getSelectedItems() returns label and purpose. getSelectedItems(TRUE) is the selector full payload: label, purpose, is_global, parent_id, subcontext_type, scope, has_entity_items, and content_snippet. Context Tags is the tag scope; tag UUIDs live under scope['tag']. There is no top-level tag-ID key. New keys may be added later. Removing or renaming a key is a break.

The supported way to run a selection is through ai_context.selection_factory, which builds these objects and invokes selection for you. The params DTO getters, selection fields, and result accessors — such as getRenderedText(), getSelectedItemIds(), getSelectionMode(), and getCacheableMetadata() — are public.

Selection modes

The three selection mode identifiers and their nested meaning are public 1.0 behavior. After 1.0.0, changing a mode name, default, or what each rung includes requires a deprecation path. Callers may depend on:

Identifier Constant on AiContextSelection Role
minimal SELECTION_MODE_MINIMAL Strictest rung
relevant SELECTION_MODE_RELEVANT Shipped site-wide default
broad SELECTION_MODE_BROAD Most inclusive rung

inherit (SELECTION_MODE_INHERIT) is a plugin-default, form-radio, and stored consumer value. It is not a ladder rung. isValidSelectionMode() is FALSE. Runtime follows ai_context.settings:selection_mode.

Frozen contract:

  • The modes are a nested ladder: each rung includes everything the one above it does. Hard context filters apply in every mode.
  • An item with no values for a subscribed scope does not earn subscription credit for that scope.
  • minimal: Global items, always/never overrides, Specific Entities matches, and strict scope subscription matches.
  • relevant (the shipped value of the site-wide default ai_context.settings:selection_mode, which applies when fromConsumer(), fromParameters(), getRenderedContext(), getResult(), or the function-call selection_mode receive no mode, and to unrecognized stored consumer config or fromArray() values): Minimal, plus items that Languages, Site Sections, Entity Types, or Taxonomy Terms positively match (matchesRequestContext() returns TRUE) even without a subscription to that scope.
  • broad: Relevant, plus remaining published candidates that pass hard filters, ordered by Priority, appended after the scope-scored bucket to fill leftover token budget. Broad supplements; it does not replace scope-scored or exact-match groups.
  • Consumer config key selection_mode and the get_relevant_ai_context_items function-call parameter use these identifiers.
  • AiContextSelectionResult::getSelectionMode() returns the resolved rung that ran. It is never NULL. Subscribers that only have the result use this; they do not need the selection object.

Candidate ceilings, chunking, and the selector pipeline remain internal. Those ceilings must not drop an item from a higher rung that a lower rung selected. See Services for constructor fields and examples.

Item priority

Drupal\ai_context\AiContextItemPriority is the public allowed-values contract for the item priority field. Broad mode orders leftover candidates by these values. Changing a constant, the default, or the sort meaning after 1.0.0 needs a deprecation path.

Label Constant Value
High HIGH 10
Medium MEDIUM 5
Low LOW 0

AiContextItemPriority::default() is Medium. New and legacy items use that default.

Selector pipeline events

The ai_context.selection.* events are the supported way to inspect or alter selection without coupling to selector internals:

  • ai_context.selection.items_selected (AiContextSelectionItemsSelectedEvent)
  • ai_context.selection.text_rendered (AiContextSelectionTextRenderedEvent)

Event name constants live on Drupal\ai_context\Event\AiContextSelectionEvents.

stopPropagation() stops later subscribers on the same event (Symfony default). The selector does not check isPropagationStopped(). After items_selected it always reads getSelectedItems() and continues to render and text_rendered. After text_rendered it always builds the result. Calling stopPropagation() does not skip the next pipeline stage or cancel selection.

See the Supported extension model in Events for the full contract, subscriber constraints, and examples.

Scheduler item events

When Scheduler is installed, it dispatches six events for ai_context_item. Event name constants live on Drupal\ai_context\Event\SchedulerAiContextItemEvents. The class name follows Scheduler's Scheduler{Entity}Events pattern. Listeners receive Scheduler's SchedulerEvent (an EntityInterface).

Constant Event name
PRE_PUBLISH_IMMEDIATELY scheduler.ai_context_item_pre_publish_immediately
PUBLISH_IMMEDIATELY scheduler.ai_context_item_publish_immediately
PRE_PUBLISH scheduler.ai_context_item_pre_publish
PUBLISH scheduler.ai_context_item_publish
PRE_UNPUBLISH scheduler.ai_context_item_pre_unpublish
UNPUBLISH scheduler.ai_context_item_unpublish

The Scheduler plugin PHP class is @internal. The plugin ID ai_context_item_scheduler is public config.

See Scheduling.

Scope plugins, hooks, and alters

  • AiContextScope plugins are a public extension point. Create your own scope plugins as described in Custom scopes and the Scope API. Optional AiContextScopeValueMatchingInterface is the supported way for a custom scope to define wildcard or hierarchical matching. Optional AiContextScopeStoredValueInterface is the supported way for a custom scope to declare which stored values are stale after a delete or settings change.
  • The documented hooks and alter hooks are public. See Hooks.
  • AiContextScopeCacheableMetadataInterface is an optional companion for dynamic label cache dependencies. It is intentionally separate from AiContextScopeInterface, so existing direct implementations remain valid.
  • 1.0 custom scope storage: supported custom scopes store values in the context item scope field. Use getScope() / setScope() for the grouped array (scope[plugin_id] => string[]). create(), $entity->set('scope', …), Default Content, and recipes require Field API deltas (scope_id + value). See Storing scope values. Custom entity-field storage, cleanup, and form lifecycle integration are internal module concerns in 1.0; scope-field plugins are indexed automatically.
  • Entity scope helpers on AiContextItem are public for reading scope state. Resolve effective scope through getScopeItem() first; use stored accessors (getScope(), getScopeValues(), isStoredGlobal(), getStoredEntityItemScopeValues()) only when you need values saved on this item. Context Tags are getScopeValues('tag'); call it on getScopeItem() for inherited Context Tags. getEntityItems() and hasEntityItems() are the supported way to read referenced entities. buildDescriptionRenderArray() is the supported way to render the item description with truncation and hover text. Do not call getDescriptionSummary(); that helper is protected. See Scope API.

Do not inject plugin.manager.ai_context_scope from contrib modules; prefer the subscription form builder service for subscription widgets. The usable gate is $plugin->isEnabledAndAvailable(). The manager may gain orchestration methods in a future release without a deprecation period while marked @internal.

Consumer type plugins

AiContextConsumerType plugins are a public extension point. Implement them in the consuming project when the type must read that project's internals. Types that only enumerate public config entities and match emitted request tags may live in AI Context. The plugin attribute, interface, base class, and AiContextConsumerId are public. Build IDs with fromParts() / fromString(); SEPARATOR is private and isValidType() is @internal. getInstances() returns AiContextConsumerInstanceCollection of AiContextConsumerInstance value objects; those types are public. The Agent type's isLoopAware() is public for that type only. AI Context ships agent and automator. Types whose availability depends on site state must implement getAvailabilityCacheableMetadata(); see Beta upgrade: consumer type availability cacheability. There is no hook_ai_context_consumer_type_info_alter(); the site lever is enabled on ai_context.consumer_type_settings.{type}. The consumer type manager, router, route result, route subscriber, and listing forms are internal.

AiContextConsumerTypeInterface gained getConsumerIdForDeletedEntity() before the 1.0.0 stable release. AiContextConsumerType gained host_entity_type at the same time. AiContextConsumerTypeBase maps a 1:1 host from that attribute when the instance ID is the entity ID, and returns NULL when the key is empty. Direct implementations of the interface (without the base class) must add the method. Override it only for a different ID scheme. Deleting a matching host removes only that row from ai_context.consumers.

getInstanceDefaults() may declare an optional selection_mode key. This key was added before the 1.0.0 stable release and is part of the frozen contract. Omit the key or set inherit (AiContextSelection::SELECTION_MODE_INHERIT) for Site default; the first save writes inherit. A ladder mode locks that mode. The base sets inherit. Automator sets minimal. Direct implementations that extend the base class need no new method. An unrecognized value is treated as inherit. A stored inherit follows the site setting. A saved row that omits the key uses the type default. Do not special-case plugin IDs in the consumer form.

See Consumer type API.

Provider-request snapshot

Drupal\ai_context\Model\AiContextProviderRequestContext is the immutable snapshot passed to type-plugin resolveConsumerId(). Nested types AiContextProviderRequestMessage and AiContextProviderRequestFileDescriptor are public with it. Getters such as getModelId(), conversation history, and file descriptors are the matching contract for other consumer types; shipped types may not read every getter. The ai_context.provider_request_context_factory service and AiContextProviderRequestContextFactory class are @internal.

Function call plugins

The context_tools function call plugin IDs (ai_context:list_ai_context_items, ai_context:load_ai_context_item_by_id, ai_context:get_relevant_ai_context_items) are stable identifiers for agent configuration. The PHP classes are @internal. See Function calls.

Drupal\ai_context\Plugin\AiFunctionCall\AiContextInheritsConsumerIdInterface is the supported opt-in for tools that should inherit the running consumer ID when consumer_id is omitted. Implement it and declare a consumer_id context definition. A non-empty non-canonical value is replaced the same way and logged at debug. See Events.

Views field plugins

The Views field plugin IDs (ai_context_consumer_link, ai_context_entity_item_link, ai_context_context_item_link, ai_context_usage_record_link, ai_context_json_array) are public config identifiers. The PHP handler classes are @internal.

Field formatter plugins

The ai_context_markdown formatter ID is a public config identifier used on context-item view displays. The PHP class is @internal.

Generic-path invocation result

On PreGenerateResponseEvent, the generic subscriber writes an ai_context key to event metadata and chat-input debug data after it routes a request. Drupal\ai_context\Model\AiContextInvocationResult is the public API for that key and the status values (AiContextInvocationResult::KEY, ::PUSHED, and so on). Callers may depend on this payload shape:

Key Meaning
consumer_id Canonical consumer ID, or empty when routing declined or was ambiguous
status One of the AiContextInvocationResult status constants
item_count Number of selected item IDs
item_ids Selected context item IDs

TYPE_DISABLED covers the type kill switch and isAvailable().

A ChatInput that received a context block is single-use. The debug-data copy of the key marks the input as processed: when the same input object is dispatched again (a tool-call inner request reusing the outer request's ChatInput), the subscriber re-publishes the stored pushed payload to the new event's metadata and appends no second context block. A stored payload with any other status is not replayed: those calls appended nothing, so a later call on the same input is routed again and can push once the condition that declined it is gone. A rebuilt or fresh ChatInput gets a full pass; debug data is in-memory and lives only as long as the input object.

An absent ai_context key means the request was not handled (non-chat, unrouted tags, excluded agent/assistant tags, the module's own ai_context / ai_context_subcontext tags, or a failure inside the subscriber, which is caught and logged so the provider call continues without context). The payload never includes prompt or rendered text. See Events.

Internal implementation

The following are internal. They may change without deprecation; do not depend on them, decorate them, or replace them.

Selector and its pipeline

ai_context.selector (Drupal\ai_context\Service\AiContextSelector) and AiContextSelectorInterface — and the candidate pipeline (loading, scoring, priority merge, and subcontext resolution) — are internal. Extend selection through the selector pipeline events, not by depending on or replacing the selector.

Supporting services

These services support the selector and are not part of the public surface:

  • ai_context.scope_resolver — scope scoring and hard context filters
  • ai_context.scope_index — denormalized scope index used for prefiltering
  • ai_context.subcontext_resolver — child item resolution
  • ai_context.renderer, ai_context.token_estimator, and ai_context.subscription_budget — rendering, token budgeting, and consumer settings budget summary internals
  • ai_context.request_info_resolver, ai_context.scope_cleanup, ai_context.children, ai_context.item_validator, and the remaining services in Services

Event subscribers, route subscribers, access checks, cache contexts, and the scope plugin manager (plugin.manager.ai_context_scope) are likewise internal infrastructure. All entries under Support services in the services reference are internal — they are not labeled individually. Use the public extension points above instead.

Service decoration and replacement

Decorating, extending, or replacing internal services (for example ai_context.selector) in a services.yml file is not supported. The internal pipeline may change shape, argument order, or service graph between releases. Use the selector pipeline events and the selection factory for supported customization.

See also