# How Services Are Materialized and Disposed in agent-core-v2

> Discover how agent-core-v2 materializes services on-demand and disposes them efficiently. Learn about the lightweight dependency-injection system for object creation and cleanup in Workspaces, Sessions, and Agents.

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

---

**agent-core-v2 implements a lightweight dependency-injection system that separates service registration from instance creation, materializing concrete objects on-demand via `ServiceCollection.materialize()` and cleaning them up through `unmaterialize()` when hierarchical scopes like Workspaces, Sessions, or Agents are disposed.**

The `agent-core-v2` engine in the **MoonshotAI/kimi-code** repository manages sophisticated service lifecycles across nested architectural layers. Understanding exactly how lazy descriptors transform into concrete instances—and how the framework guarantees deterministic cleanup—is critical for building stable agent applications that handle resource teardown correctly.

## Service Registration and the SyncDescriptor Pattern

All services in `agent-core-v2` are stored in a **ServiceCollection** that acts as the central registry for dependency injection. Each entry in this collection holds either a **SyncDescriptor** (the recipe for creating the service) or a ready-made instance.

When a provider registers a service, the entry contains the descriptor rather than the instance itself. This design enables lazy initialization. The descriptor remains dormant until the runtime explicitly requests the service, at which point the system can materialize the object on-demand.

In [`src/_base/di/serviceCollection.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/serviceCollection.ts), the `materialize(id, instance)` method handles the transition from recipe to concrete object. According to the source code at lines 62-73, this method replaces the descriptor with the actual instance while preserving the entry's generation (`uid`) and maintaining a copy of the original descriptor in a `recipe` property for potential future reference.

## Materializing Concrete Instances

When the runtime requires an actual object—such as when an agent first accesses its **IEventBus**—the **InstantiationService** orchestrates the materialization process. The service calls `this._services.materialize(id, instance)`, which copies the newly created instance into the collection, making it available for all subsequent lookups within that scope.

The surrounding scope—whether a Workspace, Session, or Agent—retains a reference to the **LedgerEntry** so that the lifecycle of each materialized instance can be tracked independently. This ledger-based tracking ensures that the system knows exactly which services were materialized within each hierarchical boundary.

In [`src/_base/di/instantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/instantiationService.ts) (lines 530-539), the `materializedInstance` logic triggers the actual storage of the concrete instance, bridging the gap between instantiation and persistent availability in the container.

## Disposal and Un-Materialization

When an owning scope tears down—during workspace shutdown, session end, or provider withdrawal—the DI system must release resources and revert entries to their original state. This cleanup is handled through **unmaterialization**.

The `ServiceCollection.unmaterialize(id)` method, implemented at lines 75-84 in [`src/_base/di/serviceCollection.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/serviceCollection.ts), restores the original **SyncDescriptor** to the entry, effectively "forgetting" the concrete instance. This operation is triggered by the **InstantiationService** at line 449 when a scope begins its disposal sequence.

This un-materialization is crucial for memory management, as it allows the JavaScript garbage collector to reclaim instance memory while keeping the service registration intact for potential future materialization in a new scope.

## Scope-Aware Lifecycle Management

The DI container tracks disposables via **LedgerEntry** objects associated with each materialized service. When a scope's `dispose()` method is invoked—defined at lines 673-686 in [`src/_base/di/instantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/instantiationService.ts)—the system iterates over its ledger and calls each registered disposer.

This guarantees that all materialized services are cleaned up in the correct order, respecting the hierarchical lifecycle of **App → Workspace → Session → Agent**. The [`src/_base/di/scopeUnits.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/scopeUnits.ts) file manages these per-scope views, ensuring that materialized units are torn down precisely when their containing scope dies, without affecting parent or sibling scopes.

## Code Example: Complete Materialization Flow

```typescript
import { SyncDescriptor, ServiceCollection } from './di/serviceCollection';
import { InstantiationService } from './di/instantiationService';

// 1️⃣ Register a service recipe (lazy registration)
const services = new ServiceCollection();
services.set(MyServiceId, new SyncDescriptor(MyServiceClass));

// 2️⃣ Materialize the service the first time it is required
const container = new InstantiationService(services);
const instance = await container.createInstance(MyServiceId); // triggers instantiation
services.materialize(MyServiceId, instance); // now stored as concrete instance

// 3️⃣ Use the materialized instance elsewhere
const myService = services.get(MyServiceId) as MyService; // returns the instance

// 4️⃣ Dispose the owning scope (e.g., a session ends)
await sessionScope.dispose(); // triggers unmaterialize()
// After disposal the entry reverts to the original descriptor

```

## Summary

- **ServiceCollection** stores either **SyncDescriptor** recipes or concrete instances, enabling lazy initialization tracked via unique entry IDs.
- **Materialization** occurs through `ServiceCollection.materialize(id, instance)` (lines 62-73), which swaps descriptors for instances while preserving generation metadata.
- **Un-materialization** via `ServiceCollection.unmaterialize(id)` (lines 75-84) restores original descriptors during scope teardown, allowing garbage collection.
- **InstantiationService** (lines 530-539 and 673-686) orchestrates both creation and cleanup, iterating over ledger entries to dispose resources hierarchically.
- The system supports nested lifecycles (App → Workspace → Session → Agent) through scope-specific **LedgerEntry** tracking in [`src/_base/di/scopeUnits.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/src/_base/di/scopeUnits.ts).

## Frequently Asked Questions

### What happens to materialized services when a scope is disposed?

When a scope dispose() runs, the **InstantiationService** iterates over the scope's ledger and calls `ServiceCollection.unmaterialize(id)` for each materialized entry. This restores the original **SyncDescriptor** and triggers any registered disposers, ensuring complete cleanup while preserving the service registration for future scopes.

### How does materialize differ from instantiate in agent-core-v2?

**Instantiate** creates a new instance using the **SyncDescriptor** recipe, while **materialize** stores that instance in the **ServiceCollection** registry for reuse. Instantiation happens once per service ID within a scope, but materialization commits the instance to the collection, making it available to all subsequent `get()` calls without reconstruction.

### Can a service be re-materialized after unmaterialization?

Yes. After `unmaterialize(id)` restores the **SyncDescriptor**, the service entry returns to its lazy state. The next request for that service ID triggers a fresh instantiation and materialization cycle. This pattern enables clean resource recycling when scopes restart without requiring re-registration of service recipes.

### What is the role of SyncDescriptor in service materialization?

The **SyncDescriptor** acts as the immutable recipe or factory for service creation. It defines how to construct the service but defers actual instantiation until the runtime calls for materialization. During `materialize()`, the descriptor is archived to the `recipe` property and replaced by the concrete instance; during `unmaterialize()`, it is restored to active duty.