Consumer type API
Consumer types are Drupal attribute plugins. They describe how a kind of
thing receives context. AI Context ships the public contract, the
agent type, and the automator type. Other types are implemented
and owned by the consuming project.
This page is the public plugin API. The consumer type manager, route subscriber, and listing forms are internal.
Ownership
Implement the plugin in the consuming project when that project can own the type:
- Preferred: a small integration submodule. Its
info.ymlshould constrain the AI Context version.config/installshould shipai_context.consumer_type_settings.{type}. - Lighter: put the plugin in the main module. There is no version enforcement and no install-time settings default.
The real test is what internals the plugin touches. A type that reads config entities through entity storage and matches on emitted request tags can live in AI Context. A type that reaches into another module's final classes must live with those classes.
Do not add a CKEditor type to AI Context. That stays in the AI CKEditor project.
AI Context ships automator because Automators stay in AI core and
AI core does not take a dependency on CCC. That type is gated on
moduleExists('ai_automators'), enumerates ai_automator config
entities, and matches only ai_automator:id:{id}.
Do not add an assistant type. The generic path excludes
ai_agents and ai_assistant_api so those calls stay on
the agent path.
Registering a type
Extend AiContextConsumerTypeBase and use the
AiContextConsumerType attribute. Plugin IDs must not contain a
colon. The colon is reserved for canonical instance IDs
{type}:{instance}.
#[AiContextConsumerType(
id: 'example',
label: new TranslatableMarkup('Example'),
description: new TranslatableMarkup('Example consumers.'),
)]
final class AiContextConsumerTypeExample extends AiContextConsumerTypeBase {
}
Instances
getInstances() must be built from the data that defines the
instances (plugin definitions, entities, config). Never hardcode a
list.
Each instance has:
getInstanceId()— the instance ID (content_editor)getConsumerId()— the canonicalagent:content_editorvalue object, fromAiContextConsumerId::fromParts($type, $instanceId)- A label
- An optional description
getLabelRoute($instanceId) may return a route to the owning object's
admin page (the listing name link). Return NULL if there is no page.
That is not the CCC consumer editor.
Matching provider requests
The consumer router (AiContextConsumerRouter) is an AI Context
service. It is not Drupal's routing system. The router itself is
internal.
Types advertise routing tags with getRoutingRequestTags()
(for example ai_ckeditor). Those are AI request tags, not taxonomy
or cache tags. The router then calls resolveConsumerId() on
matching types:
- Return an
AiContextConsumerIdto claim the request - Return
NULLto decline, including sub-calls of a larger flow
Unmatched tags (no type advertised them) load no consumer config and no context items. Two types resolving different IDs fail closed as ambiguous.
The Automator type advertises ai_automator and claims a request
only when tags include exactly one ai_automator:id:{id} and that
ai_automator config entity exists. It does not reconstruct the
instance from entity type, bundle, or field name. Those tags are
ambiguous when a field has more than one automator.
Until AI core emits the ID tag from RuleBase::getTags(), an
internal subscriber (AiContextAutomatorIdTagSubscriber) adds it
when the field properties uniquely identify one automator, or
when a field widget action click maps to one automator on that
field. AiContextAutomatorWidgetOnlySubscriber uses the same
click to skip the other automators on that field when the
clicked automator is allowed to push. Remove both
subscribers when core ships the tag and widget clicks run only
the clicked instance.
Type settings vs instance overrides
| Store | What it holds |
|---|---|
ai_context.consumer_type_settings.{type} |
Type-wide settings. enabled is the push kill switch. |
ai_context.consumers |
Per-instance overrides (push_enabled, subscriptions, limits, settings) |
Missing type settings mean the type is enabled. Instance
defaults come from getInstanceDefaults(). Scopes still use
defaultConfiguration() for ai_context.scope_settings.{id}.
Leftover type settings after a plugin is gone show as stale rows on
the Consumer Types listing. Removing them deletes that config object
only, not instance rows in ai_context.consumers.
Schema pattern:
- Shared base type for
ai_context.consumer_type_settings.* - A wildcard fallback for unknown types
- Per-type entries that extend the base
- Per-instance
settingsis dynamically typed (ai_context.consumer_settings.{type}, with a*fallback)
The * fallbacks are bare mappings that permit no keys. A type that
stores anything under type settings beyond enabled, or anything at
all under per-instance settings, must ship its own
ai_context.consumer_type_settings.{type} or
ai_context.consumer_settings.{type} schema entry. Otherwise config
validation rejects those keys as unsupported.
The shared consumer editor never edits type-specific instance
settings; it preserves whatever the row already stores. Types that
need a UI for those values own that form.
The Agent type ships settings.loop_aware and
isLoopAware($instanceId). Loop-aware and Debug / Explore stay
on that type. They are not generic consumer API.
Push gates
Automatic push runs only when all of these are true:
- The instance
push_enabledflag (or the type default) - The type
isEnabled()kill switch - The type
isAvailable()environment gate (for example, an optional module the type integrates with is missing)
Override isAvailable() when the type cannot work on this site.
The base returns TRUE. The Agent type does not override it
because ai_agents is a hard dependency. The Automator type
returns FALSE when ai_automators is not installed.
isPushAllowed() is TRUE only when all three pass. The generic
subscriber reports unavailable types as type_disabled.
Pull-based tools ignore those three gates.
Invocation result
After the generic subscriber handles a chat request, it writes an
ai_context key on event metadata and on the caller's chat input
debug data (ChatInput::getDebugData(), not request metadata). An
absent key means the request was not handled. The debug-data copy
also marks the input as processed: dispatching the same ChatInput
again (a tool-call inner request) re-publishes the stored result to
the new event instead of appending a second context block.
Compare statuses to AiContextInvocationResult constants
(PUSHED, NO_ITEMS, PUSH_DISABLED,
TYPE_DISABLED, STALE, DECLINED, AMBIGUOUS). The payload
never includes prompt or context content.
NO_ITEMS under an anonymous session means the entity query ran with
accessCheck(TRUE) and every item failed the view access check, not that
routing failed. View access to context items is permission-based, not
per-item: anonymous sessions can only view published items, and only when
the anonymous role has the Use Published AI Context in AI Features
(access published ai context) permission. Granting it exposes every
published context item to anonymous selection — there is no per-item
grant — so review the published catalog before enabling it for a public
chatbot. Scopes narrow what a consumer selects; they are not an access
control.
See API stability.
Naming notes
These names stay as they are:
- Router means
AiContextConsumerRouter(tag → consumer ID), not Drupal routes. Type settings routes are added byAiContextConsumerTypeRouteSubscriber. - Type plugins use inherited
getPluginId()(agent). InstancegetInstanceId()is the instance ID (content_editor).getConsumerId()is the canonicalagent:{id}. fromConsumer()accepts a string ID.AiContextConsumerIdis the value object for validation and parts.getRenderedContext()/getResult()/fromParameters():consumerIdmerges canonical config. Pulls without a consumer log asnone.- There are two “request context” ideas: scope matching
(
matchesRequestContext(), page/entity) and the provider-call snapshot (AiContextProviderRequestContext). - The invocation-result metadata key is
ai_context.
Convenience APIs
getRenderedContext() and getResult() stay as caller-opt-in
helpers. Their trailing $selectionMode is public and nullable:
NULL uses the site default (relevant). broad is available by
choice. Only minimal uses the scope index for prefiltering; relevant
and broad walk the full catalog. Automatic injection never widens a
consumer's stored mode; these helpers may.
See Services.