How Features Self-Register and Are Provided in agent-core-v2: A Complete Guide

Features in agent-core-v2 automatically register themselves via a global registry and are provisioned into dependency injection scopes through a declarative three-phase lifecycle.

In the MoonshotAI/kimi-code repository, the agent-core-v2 package implements a modular plugin architecture where Features serve as first-class units that contribute services, configuration, and runtime capabilities. Understanding how these Features self-register and integrate into specific scopes—such as App, Workspace, Session, or Agent—is essential for extending the agent engine without manual wiring.

The Three-Phase Feature Lifecycle

Every Feature in agent-core-v2 follows a strict lifecycle that separates declaration from registration and provision. This design enables a fully decoupled, self-contained plugin model.

Phase 1: Declaration via the Feature Base Class

A Feature begins as a class extending the abstract base Feature in src/features/feature.ts. Within its constructor, developers declare contributions using helper methods like contributeService, contributeConfig, or contributeProvider. These declarations specify what the Feature will supply to the dependency injection container without immediately instantiating or binding the services.

Phase 2: Self-Registration with registerFeature

Self-registration occurs through the exported registerFeature function defined in src/features/featureAssembly.ts. When a module containing a concrete Feature is evaluated, calling registerFeature(MyFeature) stores the constructor in a singleton FeatureRegistry.

// src/features/featureAssembly.ts
export function registerFeature<T extends Feature>(ctor: new (...args: any[]) => T) {
    FeatureRegistry.instance.register(ctor);
}

The registry maintains an array of Feature constructors. This global registration mechanism means that merely importing a Feature module and invoking registerFeature makes the Feature discoverable throughout the system without additional configuration.

Phase 3: Provision via FeatureManagerService

When a scope (App, Workspace, Session, or Agent) initializes, the FeatureManagerService iterates over registered constructors and provisions them. The provideUnit method in src/app/feature/featureManagerService.ts instantiates each Feature and triggers its contribution to the target scope.

// src/app/feature/featureManagerService.ts
async provideUnit<T extends Feature>(FeatureCtor: new (...args: any[]) => T) {
    const feature = this.instantiationService.createInstance(FeatureCtor);
    feature.provide(this.scope);
}

The Feature's provide method receives the target scope and executes the previously declared contribute* helpers, which internally call the DI container's binding APIs.

Understanding the Global FeatureRegistry

The FeatureRegistry acts as a centralized singleton that decouples Feature discovery from Feature consumption. When registerFeature is called, the registry stores the constructor in an internal array. This pattern allows the framework to maintain a complete catalog of available capabilities without requiring explicit imports or manual lists in application startup code.

Providing Services to Scopes

Scopes in agent-core-v2 represent hierarchical lifecycle boundaries. When FeatureManagerService provisions a Feature, it passes the specific scope instance to the provide method. The Feature then binds its declared services to that scope using scope units:

this.provide(ScopeUnits(LifecycleScope.Workspace), MyService);

This mechanism ensures that services are instantiated and cached according to their intended lifecycle—Workspace-scoped services survive across sessions, while Session-scoped services are recreated for each new interaction.

Practical Implementation Example

The following example demonstrates the complete flow from Feature definition to registration:

// 1️⃣ Define a Feature that contributes a workspace-level service
import { Feature } from '#/features/feature';
import { WorkspaceService } from '#/services/workspaceService';

export class MyWorkspaceFeature extends Feature {
  constructor() {
    super();
    // Declare provision of a service bound to the Workspace scope
    this.provide(ScopeUnits(LifecycleScope.Workspace), WorkspaceService);
  }
}

// 2️⃣ Register the Feature (typically done once at application startup)
import { registerFeature } from '#/features/featureAssembly';
registerFeature(MyWorkspaceFeature);

// 3️⃣ Runtime provisioning happens automatically
// FeatureManagerService iterates the registry and calls provideUnit
// No additional wiring code required

Testing Features with the Registry

The agent-core-v2 test utilities expose _clearFeatureRecipesForTests to reset the global registry between test cases. This isolation prevents test pollution and ensures each test starts with a clean Feature set:

import { registerFeature, _clearFeatureRecipesForTests } from '#/features/featureAssembly';
import { SessionInitFeature } from '#/features/sessionInit/sessionInitFeature';

beforeEach(() => {
  _clearFeatureRecipesForTests();          // Reset global registry
  registerFeature(SessionInitFeature);      // Register only required Features
});

Key Source Files and Their Roles

Summary

  • Features in agent-core-v2 extend an abstract base class to declare services and configuration contributions.
  • The registerFeature function in src/features/featureAssembly.ts adds Features to a global singleton registry, enabling self-registration.
  • FeatureManagerService.provideUnit instantiates registered Features and binds them to specific scopes during application initialization.
  • The registry can be cleared using _clearFeatureRecipesForTests to support isolated unit testing.
  • This architecture provides a declarative, decoupled plugin model for extending agent capabilities without manual DI wiring.

Frequently Asked Questions

What is the difference between Feature declaration and registration in agent-core-v2?

Declaration occurs inside the Feature class constructor when calling contributeService or provide methods to specify what the Feature will offer. Registration is the external act of calling registerFeature(MyFeature) to add the class constructor to the global FeatureRegistry, making it discoverable by the system.

How does FeatureManagerService handle Feature instantiation?

The FeatureManagerService uses its internal instantiationService to create instances of registered Features. Specifically, provideUnit calls this.instantiationService.createInstance(FeatureCtor) to instantiate the Feature before invoking its provide method to bind services to the target scope.

Can Features be registered dynamically at runtime?

Yes, Features can be registered at any point before the scope assembly phase by calling registerFeature. However, once FeatureManagerService has iterated the registry and provisioned Features for a specific scope, new registrations will only take effect during subsequent scope initializations.

How do you reset the Feature registry during unit testing?

Import _clearFeatureRecipesForTests from src/features/featureAssembly.ts and call it in a beforeEach block. This clears the singleton FeatureRegistry instance, allowing tests to register only the specific Features required for the test case without interference from previously registered Features.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →