# How the DI Kernel Manages the Scope Tree in agent-core-v2

> Discover how the DI kernel manages the scope tree in agent-core-v2. Learn about hierarchical scope creation, topology enforcement, and automatic service materialization and disposal.

- Repository: [Moonshot AI/kimi-code](https://github.com/MoonshotAI/kimi-code)
- Tags: internals
- Published: 2026-08-13

---

**The DI kernel builds a hierarchical scope tree (App → Workspace → Session → Agent) using `Scope.createApp` and `Scope.createChild`, enforcing topology constraints while automatically materializing scoped services via the ScopeUnits fold and Ledger-based disposal.**

The Dependency Injection (DI) kernel in `packages/agent-core-v2` governs Kimi Code's lifecycle architecture through a strictly hierarchical scope tree. By mapping runtime tiers to container scopes, the kernel ensures that services are instantiated, shared, and destroyed according to their declared lifecycle boundaries.

## Declaring the Scope Topology

The kernel first learns the business-level hierarchy through `setScopeTopology` in [`src/app/scopes.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/app/scopes.ts). This module defines the four-tier lifecycle and installs it as the single source of truth:

```typescript
// src/app/scopes.ts
import { setScopeTopology } from '#/_base/di/scope';

export enum LifecycleScope {
  App = 'app',
  Workspace = 'workspace',
  Session = 'session',
  Agent = 'agent',
}

export const SCOPE_TOPOLOGY: readonly LifecycleScope[] = [
  LifecycleScope.App,
  LifecycleScope.Workspace,
  LifecycleScope.Session,
  LifecycleScope.Agent,
];

// Install the topology once the module loads
setScopeTopology(SCOPE_TOPOLOGY);

```

The `setScopeTopology` function stores this ordered array in a module-private `_scopeTopology` variable. Subsequent attempts to declare a different topology trigger a `BugIndicatingError`, guaranteeing immutability.

## Creating the Root App Scope

The tree originates via `Scope.createApp` in [`src/_base/di/scope.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/scope.ts). This static factory performs four critical steps:

1. **Builds a service collection** from optional seeds.
2. **Instantiates an `InstantiationService`** (the underlying DI container) with strict mode enabled.
3. **Attaches the ScopeUnits fold** via `watchScopeUnits`, enabling automatic materialization of scoped feature recipes.
4. **Provides registered scoped services** for the `app` kind through `provideScopeServices`.

```typescript
// src/_base/di/scope.ts – createApp()
static createApp(options: ScopeOptions = {}): Scope {
  const kind: ScopeKind = 'app';
  const collection = buildCollection(options.seeds);
  const instantiation = new InstantiationService(collection, true);
  instantiation.debugLabel = options.id ?? 'app';
  try {
    watchScopeUnits(instantiation, kind);
    options.configureContainer?.(instantiation);
    provideScopeServices(instantiation, kind, collection);
  } catch (error) {
    instantiation.dispose();
    throw error;
  }
  return new Scope(options.id ?? 'app', kind, instantiation);
}

```

The resulting `Scope` instance maintains an `id`, `kind`, a `ServicesAccessor` (the public API for service retrieval), and a `Ledger` for deterministic teardown. It also tracks child scopes in an internal `children` map.

## Spawning Child Scopes

Child creation occurs through `Scope.createChild`, which strictly enforces the declared topology:

```typescript
// src/_base/di/scope.ts – createChild()
if (_scopeTopology !== undefined) {
  const parentIndex = _scopeTopology.indexOf(this.kind);
  const childIndex = _scopeTopology.indexOf(kind);
  if (parentIndex === -1 || childIndex === -1 || childIndex <= parentIndex) {
    throw new Error(
      `child scope kind '${kind}' must be greater than parent kind '${this.kind}' in the declared scope topology`,
    );
  }
}

```

If the relationship is valid, the kernel:

- Creates a **new service collection** from the child's seeds.
- Instantiates a **child `InstantiationService`** linked to the parent's container.
- Runs `watchScopeUnits` and `provideScopeServices` for the child kind.
- Registers the new scope in the parent's `children` map.
- Adds a **ledger entry** on the parent to ensure the child disposes automatically when the parent shuts down.

## Registering and Providing Scoped Services

Services are bound to specific scope levels via `registerScopedService`:

```typescript
// src/_base/di/scope.ts – registerScopedService()
export function registerScopedService<T>(
  scope: ScopeKind,
  id: ServiceIdentifier<T>,
  ctor: new (...args: any[]) => T,
  activation: ScopeActivation = ScopeActivation.OnScopeCreated,
  domain: string = 'unknown',
): void {
  const descriptor = new SyncDescriptor<T>(ctor);
  _scopedRegistry.push({
    scope,
    id: id as ServiceIdentifier<unknown>,
    descriptor: descriptor as SyncDescriptor<unknown>,
    domain,
    activation,
  });
}

```

Registrations are stored in `_scopedRegistry`. When a scope instantiates, `provideScopeServices` filters this registry by the current `ScopeKind` and pushes matching entries to the container. The **`activation`** parameter controls instantiation timing:

- **`OnScopeCreated`**: Eager instantiation when the scope spawns.
- **`OnDemand`**: Lazy instantiation upon first `accessor.get()` call.

## Materializing Services with the ScopeUnits Fold

The kernel automatically propagates feature contributions downstream via `watchScopeUnits` in [`src/_base/di/scopeUnits.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/scopeUnits.ts). This function attaches a **fold** to the DI container that tracks the `ScopeUnits(kind)` token—a collection of service recipes.

When a new scope appears:

1. The fold **subscribes** to changes on the token's collection view.
2. For each `StoredRecord`, it **materializes** a unit:
   - **Class recipes**: Constructed and registered on the scope's ledger.
   - **Function recipes**: Wrapped in a `FiberRuntime` with teardown registration.
3. **Reconciles** the view on each change, retracting removed records and materializing new ones.

This mechanism ensures that a recipe contributed at the *App* scope automatically spawns live units in every downstream *Workspace*, *Session*, and *Agent* scope without manual replication.

## Accessing Services Across the Scope Tree

Every `Scope` exposes an `accessor` (type `ServicesAccessor`) that proxies to the underlying `InstantiationService`:

```typescript
// src/_base/di/scope.ts – accessor factory
accessor = {
  get: <T>(serviceId: ServiceIdentifier<T>): T =>
    instantiation.invokeFunction((a) => a.get(serviceId)),
};

```

Consumers receive an `IScopeHandle` containing this accessor. Service resolution follows the DI parent chain implicitly, allowing an *Agent* scope to access services registered at *Session*, *Workspace*, or *App* levels.

## Disposing the Scope Tree

Disposal is deterministic through **`Ledger`** objects:

- Each `Scope` owns a ledger recording the container, internal collections, and child disposals.
- Calling `dispose()` marks the scope disposed, releases its parent's ledger entry, and triggers `ledger.teardown('scope-close')`.
- The teardown cascade runs all registered disposals, clears the child map, and removes the scope from the parent's tracking.

Destroying the root *App* scope therefore cascades cleanly through the entire hierarchy, ensuring every scoped unit and service is released exactly once.

## Code Examples

### Creating the Full Scope Tree

```typescript
import { createAppScope } from '#/_base/di/scope';
import { LifecycleScope } from '#/app/scopes';

// 1. Create the root App scope
const appScope = createAppScope({ id: 'myApp' });

// 2. Create a Workspace child
const workspace = appScope.createChild('workspace', 'myWorkspace');

// 3. Create a Session child inside the workspace
const session = workspace.createChild('session', 'mySession');

// 4. Create an Agent child inside the session
const agent = session.createChild('agent', 'myAgent');

// Access a service from the agent scope (resolves up the tree)
const myService = agent.accessor.get(MyServiceIdentifier);

```

### Registering a Scoped Service

```typescript
import { registerScopedService, ScopeActivation } from '#/_base/di/scope';
import { MyService } from './my-service';

// Register eager instantiation for Session scope only
registerScopedService(
  'session',                     // ScopeKind
  MyService,                     // ServiceIdentifier
  MyService,                     // Constructor
  ScopeActivation.OnScopeCreated,
  'my-domain'
);

```

### Contributing Feature Recipes via ScopeUnits

```typescript
import { ScopeUnits } from '#/_base/di/fiber';

export const MyFeature = {
  provide: ScopeUnits('app').provide('myFeatureToken', (host) => {
    // Materialized automatically in every Workspace, Session, and Agent
    host.registerService(MyFeatureToken, new MyFeatureImpl());
  })
};

```

## Summary

- The DI kernel enforces a strict **App → Workspace → Session → Agent** hierarchy via immutable topology declarations in [`src/app/scopes.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/app/scopes.ts).
- **Scope creation** (`createApp`, `createChild`) validates parent-child relationships against the topology and links containers hierarchically.
- **Scoped services** are registered via `registerScopedService` and provisioned through `provideScopeServices` with eager or on-demand activation.
- The **ScopeUnits fold** ([`src/_base/di/scopeUnits.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/scopeUnits.ts)) automatically materializes feature contributions into every descendant scope.
- **Ledger-based disposal** ensures deterministic teardown when parent scopes are destroyed, preventing memory leaks or dangling references.

## Frequently Asked Questions

### How does the DI kernel prevent invalid scope relationships?

The kernel validates child creation against the `_scopeTopology` array in `Scope.createChild`. If the child's index is not greater than the parent's index in the topology, or if either kind is missing from the declared hierarchy, the kernel throws an error: `child scope kind '${kind}' must be greater than parent kind`.

### What is the difference between ScopeActivation.OnScopeCreated and OnDemand?

**`OnScopeCreated`** triggers immediate instantiation when the scope spawns, ensuring the service is ready before any business logic runs. **`OnDemand`** defers construction until the first call to `accessor.get(ServiceIdentifier)`, reducing startup overhead for rarely-used services.

### How do ScopeUnits enable feature composition across the tree?

`watchScopeUnits` attaches a reactive fold that monitors the `ScopeUnits(kind)` token. When a feature contributes a recipe at the *App* scope, the fold automatically materializes that recipe into every downstream *Workspace*, *Session*, and *Agent* scope. This allows features to define behavior once while the kernel handles propagation and lifecycle management.

### Where is the service collection builder defined and what does it do?

The `buildCollection` function (invoked in `createApp` and `createChild`) aggregates service seeds into a `ServiceCollection` that the `InstantiationService` consumes. It combines static registrations, scoped services, and any seeds passed via `ScopeOptions`, preparing the dependency graph before the container activates.