# Service Registry & Capabilities

This page is the curated reference for the registry surface: typed handles, the registry itself, the plugin-facing scoped wrapper, the three registration wrappers, resolution methods, the `PluginService` hierarchy, and capabilities. Reach for it when you want to know exactly which arguments are positional, which are named, and what a method actually returns.

The model is covered in [Service Registry](https://plugin-kit.saad-ardati.dev/concepts/service-registry/). This page is the API.

## Typed handles

The registry keys most types as plain strings under the hood. The public API wraps those strings in zero-cost extension types so the dot-separated `namespace.id` shape stays explicit.

### `PluginId`

```dart
extractRegion(apiReferenceSource, 'api-reference-typed-handles')
```

Wraps a plugin identifier. The extension type `implements String`, so a `PluginId` flows directly into any `String`-typed parameter, interpolation, or `Map<String, T>` lookup without unwrapping. Equality and `toString()` delegate to the wrapped string.

### `Namespace`

```dart
extractRegion(apiReferenceSource, 'api-reference-typed-handles')
```

Wraps a namespace name. The `service(...)` helper builds a namespaced `ServiceId`, and `call(...)` makes the namespace itself callable so `agent('model')` reads as a tiny constructor. `child(...)` produces a sub-namespace by appending a segment, so `Namespace('a').child('b')` is `Namespace('a.b')`. `has(ServiceId)` is the typed predicate for "does this id live under this namespace at any depth"; it returns true for direct children and nested descendants alike. Like the other handles, `Namespace` `implements String`.

### `ServiceId`

```dart
extractRegion(apiReferenceSource, 'api-reference-typed-handles')
```

Wraps the registry key (`namespace.id` or just `id`). The extension type `implements String`, so a `ServiceId` is usable anywhere a `String` is expected. `==` against another `ServiceId` (or a raw `String`) compares the wrapped values directly.

The three derived getters cover the two ways code typically wants to slice a dotted id:

- `.namespace` returns the full prefix. For `ServiceId('a.b.c')` it returns `Namespace('a.b')`.
- `.id` returns everything after the last dot. For `ServiceId('a.b.c')` it returns `'c'`.
- `.topNamespace` returns just the first segment. For `ServiceId('a.b.c')` it returns `Namespace('a')`. Use this when UI grouping should flatten nested ids under their root.

```dart
extractRegion(apiReferenceSource, 'service-id-getters')
```

To produce a namespaced id, build it from a `Namespace`:

```dart
extractRegion(apiReferenceSource, 'namespace-build-service-id')
```

`ScopedServiceRegistry` forwards the `ServiceId` straight through to the raw registry. Because the handle `implements String`, the dot separator stays an implementation detail at the call site.

## `ServiceRegistry`

The raw, non-scoped registry. Every plugin gets a [`ScopedServiceRegistry`](#scopedserviceregistry) view in its `register` hook, but the raw registry is what sits on `PluginContext.registry` and what tests construct directly.

```dart
class ServiceRegistry {
  static const int defaultPriority = Priority.normal; // 500

  ServiceRegistry({List<LocalPluginOverride> overrides = const []});
  ServiceRegistry.empty();

  // ... register*, resolve*, listing, mutation
}
```

`defaultPriority` is `Priority.normal` (500). Higher wins. `EventBus` uses the same polarity and default.

### Registration

Three pairs, each with a flat and a namespaced variant. All registration methods take named arguments at this level.

| Method | Wrapper kind | When the instance is built |
|---|---|---|
| `registerSingleton` | `SingletonWrapper` | At registration time, by the registry via `create()` |
| `registerLazySingleton` | `LazySingletonWrapper` | On first resolve |
| `registerFactory` | `FactoryWrapper` | On every resolve |

```dart
extractRegion(serviceRegistrySource, 'service-registry-register-singleton')
```

There are no `*Namespace` variants on the registry. Build a namespaced `ServiceId` via `Namespace.call(...)` (the call-shorthand `agent('model')`) or `Namespace.service(...)`, then pass the resulting `ServiceId` to the regular `register*` method. That keeps the registry surface narrow: the registry stores `ServiceId` keys; the `Namespace` helper only composes them.

If the same `pluginId` registers the same `serviceId` twice, the second call replaces the first unless the existing registration is an attached `StatefulPluginService`, in which case registration throws `ArgumentError`. After insertion the candidate list is re-sorted by priority descending.
**Stateful services reject factory registration:** `registerFactory` checks at runtime that `T` is not a subtype of `StatefulPluginService` and throws `ArgumentError` if it is. Stateful services need lifecycle management; the runtime cannot manage instances it never sees again.

### Resolution

The registry stores registrations as a sorted list per `serviceId`. Resolution picks the first enabled wrapper in the list. Disabled wrappers (skipped by an active `LocalPluginOverride`) are passed over.

| Method | Returns | Throws on miss |
|---|---|---|
| `resolve<T>(serviceId)` | The instance for the winner | Yes (`StateError`) |
| `maybeResolve<T>(serviceId)` | The instance, or `null` | No |
| `resolveAfter<T>({pluginId, serviceId})` | The next enabled instance after `pluginId` in the chain | Yes |
| `resolveRaw<T>(serviceId)` | The winning `RegistrationWrapper<T>`, no instantiation | Yes |
| `maybeResolveRaw<T>(serviceId)` | The winning wrapper, or `null` | No |

There are no `*Namespace` resolution helpers. Build a namespaced `ServiceId` from a `Namespace` (`agent('model')` or `agent.service('model')`) and pass it to `resolve`, `maybeResolve`, or `resolveRaw`.

`resolveAfter` is the chain-of-responsibility hook. The signature is named-only:

```dart
extractRegion(serviceRegistrySource, 'service-registry-resolve-after')
```

It locates the registration owned by `pluginId`, walks past it, and returns the next enabled wrapper's instance. The caller's own enabled state is not consulted, which is what makes it useful for a winning plugin to defer to whatever it overrode. If no later registration exists, or every later one is disabled, the error message distinguishes the two cases.

### Listing and inspection

These methods walk the registry without mutating it. Most return empty collections or `null` when nothing matches. `listAllServices()` calls `resolve()` for each id, so it can throw `StateError` when a slot has only disabled registrations.

| Method | Returns |
|---|---|
| `listAllServiceIds([PluginId? pluginId])` | Every registered `ServiceId`, optionally filtered to one plugin's registrations. |
| `listAllServices()` | `Map<ServiceId, Object>` of every slot to its resolved winning instance. Calls `resolve` for each id, so factories run, and throws `StateError` if a slot has only disabled registrations. |
| `resolveRaw<T>(ServiceId)` / `maybeResolveRaw<T>(ServiceId)` | The winning [`RegistrationWrapper`](#registration-wrappers) for one slot, no instantiation. Iterate `listAllServiceIds()` if you need every winner. |
| `listCapabilitiesOfNamespace(Namespace)` | Union of every `Capability` on every wrapper in `namespace.*`. |
| `getRegistrations(ServiceId)` | All wrappers for `serviceId` in priority order, or `null` if none registered. |
| `getRegistrationsOfType<T>(ServiceId)` | Same, filtered to wrappers whose payload is `T`. |
| `getPluginServices(PluginId, {bool skipFactories = false})` | Every resolved instance owned by the plugin (used by the lifecycle to drive `attach` / `detach` on `StatefulPluginService`). |
| `didPluginRegisterServices(PluginId)` | Whether the plugin currently has any registration in any slot. |

`listCapabilitiesOfNamespace` is the discoverability backbone for things like the dialog Services tab: it aggregates the metadata of every slot in a namespace without building a single instance.

For "is this plugin enabled," reach for [`PluginRuntime.isPluginEnabled`](https://plugin-kit.saad-ardati.dev/reference/plugins-and-lifecycle/) (settings-aware) or [`PluginRuntime.isPluginAttached`](https://plugin-kit.saad-ardati.dev/reference/plugins-and-lifecycle/) (runtime-effective). The registry tracks registrations, not enablement; `didPluginRegisterServices` is a registration-presence check, not an enablement check.

### Mutation

```dart
extractRegion(serviceRegistrySource, 'service-registry-unregister')
```

`unregister` removes a single plugin's registration. If the slot becomes empty, the key disappears from the registry. The runtime calls this during settings reconciliation when a plugin is disabled. To target a namespaced slot, build the `ServiceId` via `Namespace.call(...)` first.

`updateSettings` replaces the override list, restamps each existing wrapper's effective priority from the new overrides, and re-sorts every registration list. Both enabled/disabled state changes and priority changes (plugin-specific *and* wildcard `*`) take effect on the next resolve, with no re-registration required. `RegistrationWrapper.basePriority` exposes the unmodified registration-time value separately for tests and tooling that need to distinguish "what the registrant asked for" from "what the active settings dictate."

`copy()` produces an isolated snapshot: each wrapper is cloned (independent mutable `priority`, shared underlying instance / factory / capabilities) and overrides are copied by value. Subsequent `updateSettings` calls on the live registry do NOT mutate the snapshot. This is what makes `oldContext` (passed to `Plugin.onPluginSettingsChanged`) compare reliably against `newContext`.

### Per-plugin scope

```dart
extractRegion(serviceRegistrySnippets, 'service-registry-scoped-for')
```

Returns a [`ScopedServiceRegistry`](#scopedserviceregistry) that fills in `pluginId` on every registration call. Plugins always receive one of these in `register`, so they never type their own `pluginId`.

## `ScopedServiceRegistry`

The everyday surface. Plugins receive one of these in their `register(ScopedServiceRegistry registry)` override and never deal with the raw registry unless they have a reason to.

```dart
class ScopedServiceRegistry {
  final ServiceRegistry raw;
  final PluginId pluginId;
  final int? defaultPriority;

  const ScopedServiceRegistry(
    this.raw,
    this.pluginId, {
    this.defaultPriority,
  });

  ScopedServiceRegistry withPriority(int priority);

  // register* methods below
}
```

The two big differences from the raw registry are positional arguments for the plain `register*` methods, and automatic `pluginId` scoping.

### Positional registration

```dart
extractRegion(serviceRegistrySource, 'service-registry-register-singleton-2')
```

The intent is dense plugin `register` overrides:

```dart
extractRegion(serviceRegistrySnippets, 'service-registry-register-all-three')
```

`priority` is nullable. When omitted it falls back to `defaultPriority` (set by `withPriority`), and ultimately to `ServiceRegistry.defaultPriority`. The per-call value, when supplied, always wins.

### Namespaced registration

There are no `*Namespace` register methods. The `ServiceId` you pass already carries any namespace prefix, so the registry only needs the one method per registration mode.

```dart
extractRegion(serviceRegistrySnippets, 'service-registry-naming-namespace')
```

### `withPriority`

```dart
ScopedServiceRegistry withPriority(int priority);
```

Returns a new scope whose positional `register*` methods default to `priority` instead of `ServiceRegistry.defaultPriority` (`Priority.normal`, 500). Per-call `priority` still overrides. Convenient for cascades:

```dart
extractRegion(serviceRegistrySnippets, 'service-registry-settings-injection')
```

### `raw`

```dart
extractRegion(serviceRegistrySource, 'service-registry-final')
```

The escape hatch. Use it when you need to register on behalf of another plugin, inspect existing registrations, or call any of the listing methods that the scoped view does not re-export.

## Registration wrappers

Every entry in the registry is a `RegistrationWrapper<T>`. The base class is sealed; the three concrete subtypes pick how the instance gets built.

```dart
sealed class RegistrationWrapper<T extends Object> {
  final PluginId pluginId;
  final int basePriority;
  int get priority;          // effective; mutated by wildcard overrides
  final CapabilitySet capabilities;

  T provide();
}
```

Equality and `hashCode` are based on `(pluginId, basePriority)`. The
effective `priority` getter starts equal to `basePriority` but the runtime
restamps it whenever a wildcard override applies, so two wrappers can have
the same identity yet different effective priorities at different points
in time. `provide()` is the single hook that turns a wrapper into an
instance.

| Wrapper | Built when | Built how often | Reach for it when |
|---|---|---|---|
| `SingletonWrapper<T>` | At registration | Once, by the caller | The instance already exists, or eager construction is desired |
| `LazySingletonWrapper<T>` | On first `provide()` | Once, by the registry | Setup is expensive but shared state is fine |
| `FactoryWrapper<T>` | On every `provide()` | Per resolve | Service is cheap and stateless, or callers need fresh instances |

You normally do not construct wrappers directly; the registry does it for you. They show up in your code through `resolveRaw`, `getRegistrations`, and friends, where the `pluginId`, `priority`, and `capabilities` fields are what you actually want.

## `PluginService`

Base class for services that participate in settings injection.

```dart
abstract class PluginService {
  late PluginId pluginId;
  late ServiceId serviceId;

  Map<String, dynamic> get settings;
  String get settingsHash;
  ConfigNode config;

  @nonVirtual
  void injectSettings(Map<String, dynamic> settings, {String? hash});

  void onSettingsInjected() {}
}
```

### Stamped identity

`pluginId` and `serviceId` are `late` and stamped by the registry on every resolve. They are only authoritative after resolution; reading either one inside a constructor or before the first resolve throws `LateInitializationError`.

The stamp happens in `_provideAndInject` inside `ServiceRegistry`, which is the single point of truth. Subclasses do not pass identity fields to `super()`.

### Settings access

`settings` is the raw injected map. Most code reads through `config` instead, which is a [`ConfigNode`](https://plugin-kit.saad-ardati.dev/concepts/configuration/) wrapping the same data with typed accessors:

```dart
extractRegion(pluginServicesSource, 'plugin-service-settings-inject')
```

`settingsHash` is a stable hash of the current settings. The registry uses it to skip redundant `injectSettings` calls on singleton and lazy-singleton wrappers when nothing changed.

### Settings injection

```dart
@nonVirtual
void injectSettings(Map<String, dynamic> settings, {String? hash});

void onSettingsInjected() {}
```

`injectSettings` is `@nonVirtual`. The registry calls it on resolve when an override applies; it updates `settings` / `settingsHash` / `config`, then fires `onSettingsInjected()`. Override `onSettingsInjected` to react. Singleton and lazy-singleton wrappers gate on `settingsHash` to skip redundant injections; factory wrappers run injection on every resolve.

## `StatefulPluginService`

`PluginService` plus plugin lifecycle hooks and automatic subscription cleanup.

```dart
extractRegion(pluginServicesSource, 'stateful-plugin-service-basic')
```

The type parameter is the [`PluginContext`](https://plugin-kit.saad-ardati.dev/concepts/sessions/) subclass the service expects, so domain-specific context fields stay typed.

### `attach` and `detach`

`attach()` and `detach()` are pure user hooks. The framework binds `context` before calling `attach()`, then calls `detach()` and unbinds the context, cancelling every subscription opened via the tracked helpers below; you do not call `super.attach()` or `super.detach()`. Both base implementations are no-ops.

Inside `attach()`, read the bound context via `this.context` (or just `context`). The helpers `on`, `onRequest`, `bind`, `emit`, `resolve`, and `maybeResolve` read `this.context` implicitly, so no parameter is needed.

Reading `context` outside the `attach`/`detach` window throws `StateError`. Use `hasContext` to guard if the timing might overlap.

### Tracked subscription helpers

The `StatefulPluginServiceHelper` extension adds bus subscription helpers that auto-track. Subscriptions opened through these helpers land in `activeSubscriptions` and get cancelled by the framework when the service detaches.

```dart
EventSubscription on<E>(EventHandler<E> handler, {int priority = Priority.normal, String? identifier});

EventSubscription onRequest<Request, Response>(
  RequestHandler<Request, Response> handler, {
  int priority = Priority.normal,
  String? identifier,
});

EventSubscription onRequestSync<Request, Response>(
  SyncRequestHandler<Request, Response> handler, {
  int priority = Priority.normal,
  String? identifier,
});

void Function() bind(EventBindingCallback callback);

Future<EventEnvelope<T>> emit<T>(T event, {String? identifier});
```

All five helpers return what callers might need to cancel early: a `EventSubscription` for `on` / `onRequest` / `onRequestSync`, a cancel callback for `bind`, and the resulting envelope for `emit`. All call sites assume `attach` has run; `emit` throws `StateError` when called outside the window.

The same helper also exposes `resolve`, `maybeResolve`, and `resolveAfter` so a stateful service can reach into the registry without typing `context.registry` first. To resolve a namespaced slot, build the `ServiceId` via `Namespace.call(...)` and pass it to `resolve(...)` or `maybeResolve(...)`. See [Event Bus & Events](https://plugin-kit.saad-ardati.dev/reference/event-bus-and-events/) for the underlying bus contract.

```dart
extractRegion(pluginServicesSource, 'stateful-plugin-service-resolve-emit')
```

## `Capability` and `CapabilityLookup`

Capabilities are metadata tags on the registration wrapper. They are read without instantiating the service.

### `Capability`

```dart
abstract class Capability {
  const Capability();
}
```

An empty extensible base. The dialog ships `UiConfigurableCapability` (in `plugin_kit`) for opting services into the dialog's Services tab; host apps and plugins define their own beyond that. The convention is `const` constructors so capability sets stay literal:

```dart
extractRegion(capabilitiesSource, 'capability-register-multiple')
```

### `CapabilitySet`

```dart
extractRegion(serviceRegistrySource, 'service-registry-capability-set')
```

A plain `Set<Capability>` aliased for readability. Always immutable in practice; `register*` methods accept `const {}` as the default.

### `CapabilityLookup`

Two extension methods on `Set<Capability>`:

```dart
extension CapabilityLookup on Set<Capability> {
  T? getOfType<T extends Capability>();
  bool hasType<T extends Capability>();
}
```

`hasType<T>()` returns whether the set contains at least one capability of type `T`. `getOfType<T>()` returns the first matching capability, or `null`. Together they cover the two questions you ask of a capability set:

```dart
extractRegion(capabilitiesSource, 'capability-resolve-raw-wrapper')
```

The dialog uses exactly this pattern to decide whether a service deserves a config card and which icon to draw next to it. See [Plugin Kit Dialog](https://plugin-kit.saad-ardati.dev/guides/plugin-kit-dialog/) for the end-to-end story.

## Related reading

[Service Registry](https://plugin-kit.saad-ardati.dev/concepts/service-registry/)
  [Plugin Services](https://plugin-kit.saad-ardati.dev/concepts/plugin-services/)
  [Capabilities](https://plugin-kit.saad-ardati.dev/concepts/capabilities/)
  [Settings & Configuration](https://plugin-kit.saad-ardati.dev/reference/settings-and-configuration/)