- remove defaultServicesModule() and services/src/module.ts; consume getSingletonServiceDescriptors() directly\n- update daemon service registrations and bootstrap to use registry descriptors\n- add DisposableMap, DisposableSet, disposable tracking, and disposeOnReturn to agent-core DI\n- update AGENTS.md with new registration patterns
8.8 KiB
@moonshot-ai/services
In-process service container for the kimi-code daemon. Every public member follows the VSCode platform-service convention so DI wiring, docstrings, and call-site ergonomics stay uniform.
Naming convention (normative)
Every injectable thing in this package uses the Service suffix.
No Bus, no Broker, no Bridge, no Registry, no Manager.
- Decorator:
export const IXxxService = createDecorator<IXxxService>('xxxService') - Interface:
export interface IXxxService { readonly _serviceBrand: undefined; ... } - Class:
export class XxxService implements IXxxService { ... } - Decorator string (3rd arg of
createDecorator): lowerCamelCase of the interface name minus the leadingI—xxxService. This string surfaces inCyclicDependencyError.pathandNo service registered for identifier ...messages, so it must be unique and stable.
The role (business facade / one-shot reverse-RPC broker / pub-sub bus / cross-process RPC adapter) is communicated through the docstring and the interface shape, not the suffix. Patterns:
| Role | Interface signature | Example |
|---|---|---|
| Business facade | mostly Promise<T> returns |
IPromptService.submit(...) |
| One-shot broker | request(req): Promise<resp> + resolve(id, resp) |
IApprovalService |
| Pub-sub bus | publish(e) + readonly onDidXxx: Event<T> |
IEventService |
| Cross-process adapter | readonly rpc: ... + ready(): Promise<void> |
ICoreProcessService |
File / folder convention (normative)
- One folder per domain, camelCase, no kebab:
coreProcess/,authSummary/, NOTcore-process/, NOTauth-summary/. - Contracts file =
<domain>.ts(camelCase, noServicesuffix). Holds the interface, decorator, sentinel errors, adapter helpers, and protocol↔in-process shape translations. - Impl file =
<domain>Service.ts(camelCase, withServicesuffix). Holds the concrete class. Imports the decorator + interface from the sibling contracts file.
Example domain layout:
coreProcess/
coreProcess.ts ← ICoreProcessService, CoreProcessServiceOptions
coreProcessService.ts ← CoreProcessService implements ICoreProcessService
This mirrors vscode/src/vs/platform/<domain>/common/<domain>.ts +
<domain>Service.ts.
Out of scope (intentionally deferred)
The following are recognised as VSCode-aligned improvements but NOT covered by this convention today; they would be follow-up refactors:
- Split
ICoreProcessService.rpc(currentCoreRPCmega-proxy) into per-domain typed slices, so aSessionServiceonly seesPick<CoreRPC, 'createSession' | 'listSessions' | ...>and not the entireCoreAPI. Pure soft slicing (noagent-corechanges) gives us boundary discipline + test ergonomics without the IPC-channel cost. - Dissolve
IEventServiceinto per-service typedEvent<T>properties wired off a single core stream. The first step is done:IEventServiceis now a transport-agnostic pure pub-sub bus (publish+onDidPublish) and the daemon's WS-specific concerns (per-session seq, ring buffer, WS fan-out, replay) live on a separate daemon-onlyIWSBroadcastServicethat subscribes to the bus. The remaining step is folding the central stream into per-domain typed emitters on eachIXxxServiceso consumers can subscribe to a narrowEvent<T>rather than the full firehose. - Real channel registry (
getChannel(name) / registerChannel(...)onICoreProcessService) mirroring VSCode'sIMainProcessService. Requiresagent-coreRPC layer changes.
When taking on (1) or (2), the new types still follow the rules above — no new suffixes get reintroduced.
Per-domain layout (current)
| Folder | Contracts | Impl | Decorator |
|---|---|---|---|
coreProcess/ |
coreProcess.ts |
coreProcessService.ts |
ICoreProcessService |
event/ |
event.ts |
eventService.ts |
IEventService |
approval/ |
approval.ts |
(impl lives in daemon) | IApprovalService |
question/ |
question.ts |
(impl lives in daemon) | IQuestionService |
environment/ |
environment.ts |
(impl lives in daemon) | IEnvironmentService |
session/ |
session.ts |
sessionService.ts |
ISessionService |
message/ |
message.ts |
messageService.ts |
IMessageService |
prompt/ |
prompt.ts |
promptService.ts |
IPromptService |
tool/ |
tool.ts |
toolService.ts |
IToolService |
mcp/ |
mcp.ts |
mcpService.ts |
IMcpService |
task/ |
task.ts |
taskService.ts |
ITaskService |
oauth/ |
oauth.ts |
oauthService.ts |
IOAuthService |
authSummary/ |
authSummary.ts |
authSummaryService.ts |
IAuthSummaryService |
Adding a new service: create the folder + contracts + impl pair, add a
bottom-of-file registerSingleton(IXxxService, XxxService, InstantiationType.Delayed) in the impl, then re-export the contracts and impl
from index.ts so importing @moonshot-ai/services runs the registration
side effect. Daemon bootstrap consumes getSingletonServiceDescriptors() for
descriptor-only services; only override the registry entry (via
services.set(I, prebuiltInstance) or services.set(I, new SyncDescriptor(C, [runtimeArgs], false))) when the service needs an external handle or runtime
static args that the registry can't supply.
Service registration (normative)
@moonshot-ai/services uses the registry-based wiring pattern modelled on
vscode/src/vs/platform/extensions/common/extensions.ts.
-
Each
<X>Service.tsimpl file self-registers at the bottom:import { registerSingleton, InstantiationType } from '@moonshot-ai/agent-core'; // …class body… registerSingleton(IXxxService, XxxService, InstantiationType.Delayed);- Prefer
InstantiationType.Delayed(the default). The container returns aProxythat defers real construction until the first method call, which avoids paying ctor cost for services that are registered but never used in a given session. - Use
InstantiationType.Eageronly when the service must exist before any consumer touches it (e.g.ILogServiceso early errors are captured). - When the ctor takes a leading data-bag prefix (e.g.
CoreProcessService'soptions), fall back to the descriptor overload:registerSingleton(IXxxService, new SyncDescriptor(XxxService, [optionsBag])).
- Prefer
-
Consumers seed from
getSingletonServiceDescriptors()directly. Importing@moonshot-ai/servicesloads the package barrel, whose impl re-exports run theregisterSingleton(...)side effects. -
Daemon-side
services.set(...)may override the registry-derived entry for services that need runtime static args (e.g.services.set(ICoreProcessService, new SyncDescriptor(CoreProcessService, [opts.coreProcessOptions ?? {}], false))instart.ts) or for prebuilt instances carrying external closures (PinoLogger,FastifyRestGateway). The duplicate-registration throw was removed fromregisterSingleton(plan §158); the later registration wins at every layer.
The legacy "hand-built array" and defaultServicesModule() wrapper patterns
are gone. Do NOT reintroduce them — the registry is the source of truth, and
bootstrap code should read it with getSingletonServiceDescriptors().
Comments (normative)
Default to no comments. Well-named identifiers and types already say WHAT the code does; a comment that restates that just decays as the code changes around it.
Write a comment only when the WHY is non-obvious to a reader who has the diff in front of them: a hidden constraint, a subtle invariant, a workaround for a specific upstream bug, behavior that would surprise someone reading the call. One short line max.
Do not write:
- Block / paragraph docstrings on internal helpers.
- Comments that narrate the diff itself ("now we call resumeSession first so cold sessions auto-load") — that belongs in the commit message and PR description, not in the source. The next reader has no diff context; they just see prose that drifts as the surrounding code evolves.
- Comments that re-explain types already visible at the call site
("returns
Promise<Session>", "throwsSessionNotFoundError"). - Comments pointing at other files by line number (
core-impl.ts:286-289). Line numbers move; the pointer rots within a release. - "Regression guard for …" / "fixes the bug where …" preambles on tests. The test name and assertions are the contract; the bug history belongs in git.
Existing files in this package over-comment by historical accident. Do not propagate that style to new code. When touching an existing file, prefer leaving the surrounding comments alone — large comment deletions belong in their own dedicated cleanup pass, not bundled into behavior changes.