How the Cascade Engine Handles Service Updates in agent-core-v2
The cascade engine processes service updates as deterministic, tree-wide transactions that mark services as pending, tear down affected dependents, and reactivate them in topological order once dependencies are satisfied.
The cascade engine serves as the core dependency injection (DI) system in MoonshotAI/kimi-code's agent-core-v2. When you trigger a service update, the engine orchestrates an atomic, scope-wide re-evaluation that guarantees consistency across the entire service tree without immediately recreating instances.
Submitting an Update Request
Service updates begin by calling engine.update(token, reason) in packages/agent-core-v2/src/_base/di/cascadeEngine.ts. This method creates a CascadeChange object with action: 'update' and queues it internally via the submit() method (lines 38-45).
The update request does not execute immediately. Instead, it enters a batch queue where the engine aggregates multiple changes before processing them as a single transaction. This batching strategy prevents partial states and ensures that cascading effects are computed holistically.
Transaction Processing and the Contagion Set
Once queued, the request is pulled by the private _pump() method and passed to _transact(). Here, the engine merges the batch and computes the contagion set—the complete set of service tokens potentially affected by the change (lines 71-76).
The engine determines this set using the global DependencyGraph defined in packages/agent-core-v2/src/_base/di/dependencyGraph.ts. By analyzing dependency relationships, the engine identifies not just the updated service, but all dependents that might become invalid due to the change.
Before proceeding with teardown, the engine checks for an optional onWillCascade hook. If provided, the transaction waits up to abortWaitMs milliseconds for the hook to complete, allowing external systems to prepare for the incoming changes (lines 48-57).
Teardown and Pending State Management
For every token in the contagion set, the cascade engine calls _teardownForCascade(). Critically, because an update action does not unprovide the token, the service is parked as pending rather than fully destroyed (lines 24-34).
This distinction is fundamental to the update semantics: the service instance remains in a Pending state while the engine evaluates whether its dependencies (or dependents) have changed. The _applyChangeForCascade() method handles this transition by explicitly marking the token as Pending or maintaining its pending status (lines 64-66). No new instance is created at this stage.
Reactivation and Topological Ordering
After teardown completes, _recheckPending() repeatedly scans the pending index to determine which services can be safely reactivated. The engine builds a topological order of services whose dependencies are now satisfied and activates them via _activate() (lines 70-77, 87-96).
During this phase:
- Satisfied dependencies trigger
host.materialize, moving the service to theActivestate - Failures during activation are captured, resulting in a
Failedstate for that service - The process respects the dependency graph ordering to ensure that upstream services activate before their dependents
Transaction Completion and History Logging
Once the transaction finishes, the engine pushes a CascadeHistoryEntry to its internal history buffer. This entry captures the reason for the cascade, the affected tokens, and the final states of torn-down, rebuilt, and failed services (lines 133-141).
You can observe state changes in real time by subscribing to engine.onDidChangeUnitState(), which emits events containing the token and its new state (Pending, Active, or Failed).
Practical Implementation
import { CascadeEngine, CascadeHost, CascadeScopeHandle } from '#/packages/agent-core-v2/src/_base/di';
// Assume we have a host implementation that knows about `MyService`
const host: CascadeHost = …;
const scopeHandle: CascadeScopeHandle = …;
const tree = new CascadeTree(/* DependencyGraph instance */);
const engine = new CascadeEngine(host, scopeHandle, tree);
// Trigger an update of a registered service
await engine.update(MyService, 'Refresh configuration after user change');
The call above enqueues an update, runs the full transaction described above, and returns only when the service and its dependents have settled into their final states.
To monitor state transitions during updates:
// Listening for state changes (e.g., to update UI)
engine.onDidChangeUnitState(event => {
console.log(`Service ${event.token} is now ${event.state}`);
});
Key Source Files
packages/agent-core-v2/src/_base/di/cascadeEngine.ts— Core implementation of submission, transaction processing, teardown, and activationpackages/agent-core-v2/src/_base/di/dependencyGraph.ts— Provides graph analysis for contagion sets and topological sortingpackages/agent-core-v2/src/_base/di/errors.ts— DefinesCascadeConflictErrorand other error types encountered during updatespackages/agent-core-v2/test/_base/di/cascade.test.ts— Test suite verifying update scenarios and cascade behavior
Summary
- Update requests are queued as
CascadeChangeobjects withaction: 'update'and processed viaengine.update() - The engine computes a contagion set using the
DependencyGraphto identify all potentially affected services across the scope tree - Updated services are marked as Pending rather than immediately recreated, preserving instance lifecycle boundaries
- Dependent services are torn down and rebuilt in topological order once dependencies are satisfied via
_recheckPending()and_activate() - Complete transaction history is preserved via
CascadeHistoryEntryfor debugging and audit purposes
Frequently Asked Questions
What is the contagion set in agent-core-v2 service updates?
The contagion set is the complete collection of service tokens that might be affected by an update, computed by the _transact() method using the global DependencyGraph. It includes the directly updated service and all transitive dependents, ensuring the cascade engine evaluates the full impact tree before making any state changes.
Why doesn't the cascade engine immediately recreate service instances during an update?
The engine treats updates as dependency re-evaluations rather than destruction-recreation cycles. By marking services as Pending via _applyChangeForCascade(), the engine can first tear down dependents that might become invalid, then selectively rebuild only those services whose dependencies are actually satisfied, preventing unnecessary instantiation and maintaining atomic consistency.
How does the cascade engine handle failures during service reactivation?
During the _recheckPending() phase, if _activate() encounters an error while materializing a service via host.materialize, the engine captures the exception and transitions the service to the Failed state. This failure is recorded in the CascadeHistoryEntry, allowing the transaction to complete while preserving error information for subsequent handling.
What is the purpose of the onWillCascade hook?
The optional onWillCascade hook provides a grace period before teardown begins. When provided, the engine waits up to abortWaitMs milliseconds for this hook to resolve, allowing external systems (such as UI components or network clients) to prepare for service destruction. If the hook fails or times out, the transaction proceeds normally, but the delay allows for coordinated shutdown sequences.
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 →