How to Create Custom Features Using the `Feature` Base Class in agent-core-v2
Extend the abstract Feature class and implement setup() to register lifecycle-aware capabilities that are automatically discovered and managed by FeatureManagerService.
The agent-core-v2 package in the MoonshotAI/kimi-code repository provides a modular Feature system for extending Kimi Code sessions. The Feature base class—located at src/features/feature.ts—extends Service to provide dependency injection support and standardized lifecycle hooks. This architecture lets you add custom capabilities without modifying core engine code.
Understanding the Feature Architecture
The Feature Class Hierarchy
In agent-core-v2, every capability is modeled as a Feature. The inheritance chain is:
Service (DI container base)
└── Feature (abstract)
└── YourCustomFeature
The Feature class in src/features/feature.ts defines three async lifecycle methods:
setup()– Called once during session assembly; use this to register commands, subscribe to events, or initialize statestart()– Called when the session becomes activestop()– Called during session shutdown for cleanup
These methods are invoked by FeatureManagerService (src/app/feature/featureManagerService.ts), which orchestrates all features for a session.
Automatic Discovery via FeatureAssemblyService
You do not manually register features. Instead, FeatureAssemblyService (src/features/featureAssemblyService.ts) scans the src/features/** directory, discovers all concrete Feature subclasses, and registers them with FeatureManagerService.
The registration flow:
FeatureAssemblyService ──► discovers MyFeature
│
▼
FeatureManagerService ───► calls MyFeature.setup()
│
▼
MyFeature.start() / stop() during session lifecycle
Creating a Custom Feature: Step-by-Step
Step 1: Extend Feature and Implement setup()
Create a file under src/features/<your-feature>/ that extends the base class. Inject required services via the constructor using the @inject decorator.
// src/features/myFeature/myFeature.ts
import { Feature } from '#/features/feature';
import { CommandRegistry } from '#/services/commandRegistry';
import { inject } from '#/di';
export class MyFeature extends Feature {
constructor(
@inject('CommandRegistry') private readonly commands: CommandRegistry
) {
super();
}
async setup(): Promise<void> {
// Register commands, event handlers, or initial state
this.commands.register('my:greet', async () => {
console.log('Greeting from MyFeature!');
});
}
async start(): Promise<void> {
console.log('MyFeature activated');
}
async stop(): Promise<void> {
console.log('MyFeature shutting down');
}
}
Step 2: Export for Discovery
Export your feature from an index.ts so the assembly scanner can locate it:
// src/features/myFeature/index.ts
export * from './myFeature';
No additional registration code is required—the glob pattern in FeatureAssemblyService picks up all feature directories automatically.
Built-in Feature Examples
The agent-core-v2 source contains several reference implementations demonstrating patterns for Feature subclassing.
PlanFeature – Complex Setup Implementation
src/features/plan/planFeature.ts shows a feature that registers multiple commands and integrates with planning services:
// From src/features/plan/planFeature.ts (lines 1-45)
import { Feature } from '#/features/feature';
// ... other imports
export class PlanFeature extends Feature {
async setup(): Promise<void> {
// Registers plan-related commands and subscriptions
// Implementation details in source...
}
}
This example demonstrates injecting multiple services and coordinating between them during setup().
SessionInitFeature – Initialization-Order Pattern
src/features/sessionInit/sessionInitFeature.ts illustrates a feature that must run early in the session lifecycle. It uses setup() to configure initial session state before other features start:
// From src/features/sessionInit/sessionInitFeature.ts
export class SessionInitFeature extends Feature {
async setup(): Promise<void> {
// Configure session defaults, load persisted state, etc.
}
}
Reference the corresponding test at test/features/sessionInit/sessionInitFeature.test.ts for the testing pattern.
Testing Your Custom Feature
The agent-core-v2 test utilities provide createTestScope() for isolated feature testing. This pattern mirrors the actual DI container used in production.
// test/features/myFeature/myFeature.test.ts
import { createTestScope } from '#/test/util';
import { MyFeature } from '#/features/myFeature';
test('MyFeature registers greet command', async () => {
const { scope, commandRegistry } = await createTestScope();
await scope.register(MyFeature);
await scope.start();
expect(commandRegistry.has('my:greet')).toBe(true);
});
The createTestScope() helper instantiates a minimal DI container with mocked services, letting you verify feature behavior without a full session.
Key Source Files for Custom Feature Development
| File | Purpose | GitHub Link |
|---|---|---|
src/features/feature.ts |
Abstract Feature base class with lifecycle definitions |
View source |
src/app/feature/featureManagerService.ts |
Orchestrates feature lifecycle per session | View source |
src/features/featureAssemblyService.ts |
Discovers and registers Feature subclasses |
View source |
src/features/plan/planFeature.ts |
Reference: multi-service feature implementation | View source |
src/features/sessionInit/sessionInitFeature.ts |
Reference: initialization-order pattern | View source |
test/features/sessionInit/sessionInitFeature.test.ts |
Reference: feature unit testing pattern | View source |
Summary
- Extend
Featurefromsrc/features/feature.tsto create custom capabilities with automatic DI and lifecycle management - Implement
setup()to register commands, handlers, and initial state—this is the required hook for feature activation - Export your class from a feature directory;
FeatureAssemblyServicediscovers it automatically via directory scanning - Use
createTestScope()from test utilities to write isolated unit tests against the DI container - Reference
PlanFeatureandSessionInitFeaturein the source for production patterns of varying complexity
Frequently Asked Questions
What methods must I implement when extending Feature?
Only setup() is abstract and must be implemented. The start() and stop() methods are optional lifecycle hooks provided by the base class—override them if your feature needs activation or cleanup behavior.
How does dependency injection work with custom features?
The Feature class extends Service, making it DI-aware. Decorate constructor parameters with @inject('ServiceName') to receive injected instances. The FeatureManagerService resolves all dependencies when instantiating your feature during session assembly.
Can I control the order in which features initialize?
Feature discovery order depends on filesystem glob results, but you can implement dependency-like ordering by having features check for or wait on shared services during setup(). For strict ordering requirements, examine SessionInitFeature as a pattern for early-initialization features.
Where should I place my custom feature files?
Create a directory under packages/agent-core-v2/src/features/<your-feature-name>/ containing your implementation and an index.ts export. The FeatureAssemblyService glob pattern src/features/** automatically includes any TypeScript files in this structure.
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 →