How the DI Kernel Manages the Scope Tree in agent-core-v2
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. This module defines the four-tier lifecycle and installs it as the single source of truth:
// 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. This static factory performs four critical steps:
- Builds a service collection from optional seeds.
- Instantiates an
InstantiationService(the underlying DI container) with strict mode enabled. - Attaches the ScopeUnits fold via
watchScopeUnits, enabling automatic materialization of scoped feature recipes. - Provides registered scoped services for the
appkind throughprovideScopeServices.
// 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:
// 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
InstantiationServicelinked to the parent's container. - Runs
watchScopeUnitsandprovideScopeServicesfor the child kind. - Registers the new scope in the parent's
childrenmap. - 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:
// 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 firstaccessor.get()call.
Materializing Services with the ScopeUnits Fold
The kernel automatically propagates feature contributions downstream via watchScopeUnits in 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:
- The fold subscribes to changes on the token's collection view.
- For each
StoredRecord, it materializes a unit:- Class recipes: Constructed and registered on the scope's ledger.
- Function recipes: Wrapped in a
FiberRuntimewith teardown registration.
- 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:
// 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
Scopeowns a ledger recording the container, internal collections, and child disposals. - Calling
dispose()marks the scope disposed, releases its parent's ledger entry, and triggersledger.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
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
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
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. - Scope creation (
createApp,createChild) validates parent-child relationships against the topology and links containers hierarchically. - Scoped services are registered via
registerScopedServiceand provisioned throughprovideScopeServiceswith eager or on-demand activation. - The ScopeUnits fold (
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.
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 →