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 |
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 forfromParameters(). Build instances withfromArray()when passing raw input from forms, plugins, or other loosely typed sources.Drupal\ai_context\Model\AiContextConsumerStoredConfig— storedai_context.consumersconfig fromloadConsumerConfig().getSettings()is the saved type-specific map or empty.getSelectionMode()returns the raw stored value, which can beinheritas well as a ladder mode; resolve it before building anAiContextSelection, or useAiContextConsumerResolvedConfig.Drupal\ai_context\Model\AiContextConsumerResolvedConfig— stored config over plugin defaults fromresolveConsumerConfig().getSettings()is stored settings overgetInstanceDefaults()['settings'].Drupal\ai_context\Model\AiContextConsumerInstance— one enumerated consumer (canonical ID, label, optional description) fromgetInstances().Drupal\ai_context\Model\AiContextConsumerInstanceCollection— thegetInstances()return: the list plus cacheability (IteratorAggregate,Countable,CacheableDependencyInterface). There is nogetCacheableMetadata(); 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()returnslabelandpurpose.getSelectedItems(TRUE)is the selector full payload:label,purpose,is_global,parent_id,subcontext_type,scope,has_entity_items, andcontent_snippet. Context Tags is thetagscope; tag UUIDs live underscope['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 defaultai_context.settings:selection_mode, which applies whenfromConsumer(),fromParameters(),getRenderedContext(),getResult(), or the function-callselection_modereceive no mode, and to unrecognized stored consumer config orfromArray()values): Minimal, plus items that Languages, Site Sections, Entity Types, or Taxonomy Terms positively match (matchesRequestContext()returnsTRUE) 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_modeand theget_relevant_ai_context_itemsfunction-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
AiContextScopeplugins are a public extension point. Create your own scope plugins as described in Custom scopes and the Scope API. OptionalAiContextScopeValueMatchingInterfaceis the supported way for a custom scope to define wildcard or hierarchical matching. OptionalAiContextScopeStoredValueInterfaceis 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.
AiContextScopeCacheableMetadataInterfaceis an optional companion for dynamic label cache dependencies. It is intentionally separate fromAiContextScopeInterface, 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
AiContextItemare public for reading scope state. Resolve effective scope throughgetScopeItem()first; use stored accessors (getScope(),getScopeValues(),isStoredGlobal(),getStoredEntityItemScopeValues()) only when you need values saved on this item. Context Tags aregetScopeValues('tag'); call it ongetScopeItem()for inherited Context Tags.getEntityItems()andhasEntityItems()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 callgetDescriptionSummary(); 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 filtersai_context.scope_index— denormalized scope index used for prefilteringai_context.subcontext_resolver— child item resolutionai_context.renderer,ai_context.token_estimator, andai_context.subscription_budget— rendering, token budgeting, and consumer settings budget summary internalsai_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
- Public API classes and interfaces
- Events — Supported extension model
- Services — per-service public/internal status labels
- Access boundaries — permissions versus entity access
- Upgrade and compatibility policy
- Scope API and Custom scopes
- Consumer type API
- Hooks