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
src/features/feature.ts– Defines the abstractFeaturebase class and contribution helper methods.src/features/featureAssembly.ts– Implements the globalFeatureRegistryand theregisterFeatureentry point.src/app/feature/featureManagerService.ts– Orchestrates Feature instantiation and callsprovideon each registered Feature.src/app/feature/featureManager.ts– Provides the DI-aware interface used by other system components to request Feature services.test/features/sessionInit/sessionInitFeature.test.ts– Contains examples of registry cleanup and Feature registration in unit tests.
Summary
- Features in agent-core-v2 extend an abstract base class to declare services and configuration contributions.
- The
registerFeaturefunction insrc/features/featureAssembly.tsadds Features to a global singleton registry, enabling self-registration. FeatureManagerService.provideUnitinstantiates registered Features and binds them to specific scopes during application initialization.- The registry can be cleared using
_clearFeatureRecipesForTeststo 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →