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 state
  • start() – Called when the session becomes active
  • stop() – 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 Feature from src/features/feature.ts to 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; FeatureAssemblyService discovers it automatically via directory scanning
  • Use createTestScope() from test utilities to write isolated unit tests against the DI container
  • Reference PlanFeature and SessionInitFeature in 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:

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 →