Skip to content

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.yml should constrain the AI Context version. config/install should ship ai_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 canonical agent:content_editor value object, from AiContextConsumerId::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 AiContextConsumerId to claim the request
  • Return NULL to 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 settings is 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:

  1. The instance push_enabled flag (or the type default)
  2. The type isEnabled() kill switch
  3. 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 by AiContextConsumerTypeRouteSubscriber.
  • Type plugins use inherited getPluginId() (agent). Instance getInstanceId() is the instance ID (content_editor). getConsumerId() is the canonical agent:{id}.
  • fromConsumer() accepts a string ID. AiContextConsumerId is the value object for validation and parts.
  • getRenderedContext() / getResult() / fromParameters(): consumerId merges canonical config. Pulls without a consumer log as none.
  • 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.