Understanding the Agent-Core Dependency Injection System in Kimi Code

Kimi Code's core uses a custom dependency injection container in packages/agent-core/src/di built around ServiceCollection, SyncDescriptor, and InstantiationService to manage lazy instantiation, cycle detection, and scoped child containers.

Kimi Code implements a VS Code-inspired dependency injection (DI) system that powers its agent-core architecture. Located under packages/agent-core/src/di, this container handles service registration, deferred instantiation, and dependency resolution while actively guarding against cyclic dependencies through graph-based detection.

Core DI Components

The system centers on three primary abstractions defined in the agent-core package:

ServiceCollection: The Service Registry

The ServiceCollection class maintains a private _entries Map that associates each ServiceIdentifier<T> with either an instantiated object or a SyncDescriptor. As implemented in src/di/serviceCollection.ts, the collection distinguishes between immediately available instances and lazy factories using instanceof SyncDescriptor checks (lines 4-5).

When registering services, developers populate the collection with tuples of identifiers and their corresponding implementations:

import { ServiceCollection } from '#/agent-core/src/di/serviceCollection';
import { SyncDescriptor } from '#/agent-core/src/di/descriptors';

const collection = new ServiceCollection(
  [LoggerService, new LoggerService()],                 // Concrete instance
  [DatabaseService, new SyncDescriptor(DatabaseService)] // Lazy descriptor
);

SyncDescriptor: Controlling Instantiation Timing

The SyncDescriptor<T> class defined in src/di/descriptors.ts encapsulates a constructor, static arguments, and an optional delayed-instantiation flag. When the supportsDelayedInstantiation parameter is set to true, the container creates a proxy via GlobalIdleValue that delays actual object creation until a property is accessed.

This mechanism enables expensive services—such as event emitters or database connections—to remain uninitialized until explicitly needed, reducing startup overhead for Kimi Code's agent sessions.

Service Resolution Workflow

When code invokes instantiationService.invokeFunction or instantiationService.createInstance, the container executes a multi-phase resolution process defined in src/di/instantiationService.ts.

Phase 1: Trace Initialization and Lookup

The container optionally starts a diagnostic trace via Trace.traceInvocation or Trace.traceCreation (lines 40-52). It then attempts to retrieve the service through _getOrCreateServiceInstance, which checks the ServiceCollection for existing instances or descriptors.

If the entry is a concrete instance, the container returns it immediately (line 99). For SyncDescriptor entries, the system proceeds to instantiation.

Phase 2: Dependency Extraction and Recursive Resolution

For services requiring construction, the container extracts constructor dependencies using _util.getServiceDependencies (lines 51-53). Each dependency triggers a recursive call to _getOrCreateServiceInstance, building a complete dependency chain before invocation.

The actual instantiation occurs through Reflect.construct (line 80), with the gathered dependencies injected into the constructor. After creation, the container caches the result by replacing the SyncDescriptor with the actual instance in _setCreatedServiceInstance (lines 39-44).

Handling Delayed Instantiation

For services registered with supportsDelayedInstantiation: true, the container performs a special proxy construction (lines 52-85). The GlobalIdleValue utility creates a placeholder object that instantiates the real service only when a property is first accessed, enabling truly lazy initialization for event-heavy services.

Dependency Graph and Cycle Detection

The DI container constructs a directed graph (Graph<Triple>) during the resolution process to track dependencies and prevent circular references (lines 45-66).

Graph Construction and Root Extraction

Each node in the graph represents a SyncDescriptor awaiting instantiation. Edges represent "depends-on" relationships between services. The algorithm repeatedly extracts roots—nodes with no outgoing edges—which are safe to instantiate because all their dependencies are already resolved.

If the algorithm encounters a non-empty graph with no available roots, it throws a CyclicDependencyError (lines 70-74). For debugging purposes, src/di/graph.ts provides a findCycleSlow method that traces the specific path causing the cycle.

Child Containers and Service Scoping

The InstantiationService supports hierarchical containers through the createChild method. A child container inherits services from its parent via fallback lookup in _getServiceInstanceOrDescriptor (lines 55-58), while maintaining its own ServiceCollection for local overrides.

This pattern enables Kimi Code to isolate per-session or per-plugin services without polluting the global container:

const child = parent.createChild(
  new ServiceCollection([
    LoggerService, 
    new SyncDescriptor(MockLogger, [], true)
  ])
);

Child containers resolve dependencies by checking their local registry first, then traversing up to parent containers, enabling sophisticated scoping for testing and plugin isolation.

Tracing and Performance Monitoring

When instantiated with tracing enabled (_enableTracing: true), the container records every service creation and function invocation. The Trace class captures:

  • Operation type: Creation vs. Invocation (TraceType.Creation/Invocation)
  • Dependency tree: Hierarchical relationships via branch nodes
  • Timing data: Duration measured at stop()

If instantiation exceeds 2ms or triggers additional service creation, the trace is added to Trace.all for diagnostic review. This visibility helps developers identify eager instantiation hotspots and optimize startup performance in complex agent configurations.

Practical Implementation Examples

Basic Service Registration and Retrieval

import { InstantiationService } from '#/agent-core/src/di/instantiationService';
import { ServiceCollection } from '#/agent-core/src/di/serviceCollection';
import { SyncDescriptor } from '#/agent-core/src/di/descriptors';

class Logger {
  log(msg: string) { console.log(msg); }
}

class Greeter {
  static deps = [{ id: Logger, index: 0 }];
  constructor(private logger: Logger) {}
  greet(name: string) {
    this.logger.log(`Hello, ${name}!`);
  }
}

const services = new ServiceCollection(
  [Logger, new SyncDescriptor(Logger)],
  [Greeter, new SyncDescriptor(Greeter)]
);

const container = new InstantiationService(services);
container.invokeFunction((accessor) => {
  const greeter = accessor.get(Greeter);
  greeter.greet('Kimi'); // Outputs: Hello, Kimi!
});

Implementing Scoped Overrides with Child Containers

class MockLogger {
  log(msg: string) { /* silent */ }
}

const parent = new InstantiationService(
  new ServiceCollection([Logger, new SyncDescriptor(Logger)])
);

const child = parent.createChild(
  new ServiceCollection([Logger, new MockLogger()])
);

child.invokeFunction((accessor) => {
  const greeter = accessor.get(Greeter);
  greeter.greet('test'); // No output due to MockLogger
});

Configuring Delayed Instantiation

class EventBus {
  listeners: Function[] = [];
  onDidChange(fn: Function) { this.listeners.push(fn); }
}

class Component {
  static deps = [{ id: EventBus, index: 0 }];
  constructor(private bus: EventBus) {}
  attach() {
    this.bus.onDidChange(() => console.log('changed'));
  }
}

const services = new ServiceCollection(
  [EventBus, new SyncDescriptor(EventBus, [], true)], // delayed
  [Component, new SyncDescriptor(Component)]
);

const container = new InstantiationService(services);
// Note: EventBus is not yet instantiated

container.invokeFunction((accessor) => {
  const comp = accessor.get(Component);
  comp.attach(); // EventBus instantiated here on first access
});

Summary

  • Lazy instantiation is the default behavior when registering services as SyncDescriptor instances, reducing startup overhead until dependencies are actually accessed.
  • Cycle detection uses a directed graph (Graph<Triple>) to identify circular dependencies before instantiation, throwing CyclicDependencyError when detected.
  • Child containers enable scoped service overrides and isolation through hierarchical resolution, supporting per-session and per-plugin boundaries in src/di/instantiationService.ts.
  • Tracing capabilities in InstantiationService provide performance insights by logging construction timing and dependency chains exceeding 2ms.

Frequently Asked Questions

How does Kimi Code's DI container differ from standard JavaScript DI libraries?

Kimi Code's agent-core dependency injection system is purpose-built for the VS Code extension host environment, featuring tight integration with SyncDescriptor for synchronous lazy loading and built-in cycle detection through graph traversal. Unlike generic JS DI libraries, it specifically handles "delayed instantiation" via GlobalIdleValue proxies to support expensive service initialization without blocking the extension host.

What triggers the CyclicDependencyError in the DI system?

The container throws CyclicDependencyError when the dependency graph algorithm in src/di/instantiationService.ts (lines 70-74) cannot find a root node—meaning every remaining service depends on another service that hasn't been instantiated yet. This indicates a circular dependency chain that would cause infinite recursion during construction.

Can services be registered after the InstantiationService is created?

No, the ServiceCollection is effectively sealed once passed to the InstantiationService constructor. While child containers can extend or override registrations via createChild, the root container's service map remains immutable to ensure deterministic resolution behavior and thread-safe access patterns.

How does delayed instantiation work with constructor dependencies?

When a service is registered with supportsDelayedInstantiation: true, the container creates a proxy object using GlobalIdleValue from src/di/util/idleValue.ts. This proxy intercepts property access and invokes the real constructor only when a method or property is first used, while still maintaining proper dependency injection through the static deps metadata array.

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 →