Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Invariants that hold across every stage. Each is expanded in the stage file note
2. `@IX` decorates constructor parameters only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services). (service-authoring.md)
3. Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. (implement.md)
4. Parent scope never depends on child scope — short-lived may inject long-lived, never the reverse. (orient.md)
5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); do not break the cycle with `Delayed`. (design.md, implement.md)
5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); activation timing does not break dependency cycles. (design.md, implement.md)
6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md)
7. Scope follows state identity — no `Map<sessionId, …>` at `App` to fake per-session state. (design.md)
8. Foundational layers never know upstream ones; business code never depends on the edge layer (`gateway`/`rpc`). (design.md)
Expand Down
11 changes: 5 additions & 6 deletions .agents/skills/agent-core-dev/align.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ v1 is a **VSCode-style singleton container**: services self-register with `regis

| Concern | v1 (`agent-core`) | v2 (`agent-core-v2`) |
|---|---|---|
| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, InstantiationType.Delayed, 'domain')` |
| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/extensions'` / `'#/_base/di/lifecycle'` |
| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, ScopeActivation.OnDemand, 'domain')` |
| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/lifecycle'` |
| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Session/Agent) — see orient.md |
| Domain granularity | coarse (`session`, `tool`, `loop`) | fine, split by scope + responsibility |
| Test import | `from '@moonshot-ai/agent-core/di/test'` | `from '#/_base/di/test'` |
Expand Down Expand Up @@ -157,9 +157,8 @@ import { InstantiationType, registerSingleton } from '../../di';
registerSingleton(IXxxService, XxxService, InstantiationType.Delayed);

// v2
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
registerScopedService(LifecycleScope.Session, IXxxService, XxxService, InstantiationType.Delayed, 'xxx');
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
registerScopedService(LifecycleScope.Session, IXxxService, XxxService, ScopeActivation.OnDemand, 'xxx');
```

**Imports:**
Expand Down Expand Up @@ -232,4 +231,4 @@ Before submitting a port:
- Decide scope from state identity before writing v2 code; the scope is fixed at registration.
- Verify the domain mapping against current v2 `src/`; the table here is a starting point, not authority.
- One Service owns state at exactly one lifetime; split global-view + per-instance into registry + per-instance.
- A dependency cycle introduced by the port means a v1 import is now backwards — refactor, do not route around it with `Delayed`.
- A dependency cycle introduced by the port means a v1 import is now backwards — refactor it; activation timing cannot break the cycle.
75 changes: 51 additions & 24 deletions .agents/skills/agent-core-dev/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Write the contract leaf, implementation leaf (with its registration), and the pa
## Standard recipe for a new `IXxxService`

1. **Contract leaf** — `src/<domain>/<domain>.ts`: interface (with `_serviceBrand`) + `createDecorator` identity.
2. **Impl leaf** — `src/<domain>/<domain>Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, type, '<domain>')`.
2. **Impl leaf** — `src/<domain>/<domain>Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, activation, '<domain>')`. The fourth argument is activation; the fifth is the domain.
3. **Entry** — `src/index.ts`: load each leaf precisely — `export * from './<domain>/<domain>';` for the contract and `import './<domain>/<domain>Service';` for the impl (importing the impl runs the registration). **No `src/<domain>/index.ts` barrel.**
4. **Tests** — see test.md.

Expand All @@ -31,8 +31,7 @@ export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('

```ts
// greet/greetService.ts
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { LifecycleScope, registerScopedService, ScopeActivation } from '#/_base/di/scope';
import { IGreeter } from './greet';

export class Greeter implements IGreeter {
Expand All @@ -41,11 +40,11 @@ export class Greeter implements IGreeter {
}

registerScopedService(
LifecycleScope.App, // lifetime: process-wide
IGreeter, // identity
Greeter, // implementation
InstantiationType.Eager, // when to construct: immediately
'greet', // domain name (for diagnostics)
LifecycleScope.App, // lifetime: process-wide
IGreeter, // identity
Greeter, // implementation
ScopeActivation.OnScopeCreated, // construct when the App scope is created
'greet', // domain name (for diagnostics)
);
```

Expand Down Expand Up @@ -95,10 +94,16 @@ const meta = accessor.get(ISessionMetadata); // type is ISessionMetadata

## §3 Scoped registration (not global)

Swap the `scope` argument to bind to a different tier:
Swap the `scope` argument to bind to a different tier. Use `ScopeActivation.OnDemand` when the service should be constructed only on its first `get()`:

```ts
registerScopedService(LifecycleScope.Session, ISessionMetadata, SessionMetadata, InstantiationType.Delayed, 'sessionMetadata');
registerScopedService(
LifecycleScope.Session,
ISessionMetadata,
SessionMetadata,
ScopeActivation.OnDemand,
'sessionMetadata',
);
```

Remember the visibility rule from orient.md: a service may inject services from its own scope or any ancestor; never from a descendant.
Expand All @@ -124,23 +129,44 @@ export class WSBroadcastService extends Disposable implements IWSBroadcastServic
- The container calls `dispose()` automatically when the service is torn down; child resources release in turn.
- Disposal order is deterministic (orient.md): child scopes first, then reverse construction order within a scope.

## §5 Eager vs delayed instantiation
## §5 Scope activation

`ScopeActivation` is the only construction-timing choice for scoped services:

```ts
export enum ScopeActivation {
OnScopeCreated = 0,
OnDemand = 1,
}
```

```ts
// Eager (default): constructed when the scope is created — registration alone
// (via module import) is enough, nobody has to `get()` it first
registerScopedService(LifecycleScope.App, ILogService, LogService, InstantiationType.Eager, 'log');
// Default: construct the real instance while the App scope is created.
registerScopedService(
LifecycleScope.App,
ILogService,
LogService,
ScopeActivation.OnScopeCreated,
'log',
);

// Delayed: scope creation materializes only a Proxy; the real constructor
// still defers to first property access
registerScopedService(LifecycleScope.App, IScopeRegistry, ScopeRegistry, InstantiationType.Delayed, 'gateway');
// Construct the real instance on the first get(IScopeRegistry).
registerScopedService(
LifecycleScope.App,
IScopeRegistry,
ScopeRegistry,
ScopeActivation.OnDemand,
'gateway',
);
```

When a scope (App / Session / Agent) is created, the container instantiates **every** service registered for that tier: the dependency graph is statically known from the `@IX` constructor metadata, so construction automatically follows dependency order, and cycles still throw `CyclicDependencyError`. A failing constructor fails the whole scope creation.
`ScopeActivation.OnScopeCreated` is the default fourth argument. Scope creation constructs every registration using this mode, after constructing its dependencies. If any constructor fails, scope creation fails. Use it for ordinary services and for constructor side effects that must exist when the scope becomes ready.

`ScopeActivation.OnDemand` stores the descriptor without constructing the service. The first `get()` constructs and caches the real instance directly; later `get()` calls return that same instance. Use it only when construction should wait until the service is actually requested.

A `Delayed` service returns a **Proxy** that constructs the real instance on first property access. Listeners registered on its `onDid…` / `onWill…` events before construction are not lost — the container records them and replays the subscriptions once the instance exists.
Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`.

> Rule of thumb: keep the default `Eager` for everything; mark `Delayed` only when construction is genuinely expensive and you want it deferred (a legacy escape hatch — only a handful of services use it).
The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth.

## §6 Using a service inside a plain function (`invokeFunction`)

Expand Down Expand Up @@ -231,24 +257,25 @@ v2's stance: **the dependency graph must be acyclic.**
2. **Decouple with an event.** If A only needs to know about a change in B, have B emit via `IEventService` and A subscribe, rather than A holding a reference to B.
3. **Re-partition scope.** One of them may belong at a different tier — moving it makes the cycle disappear.

### Delayed as a cycle-breaker (legacy escape hatch — forbidden)
### Activation does not break cycles

A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchronous Proxy. **Do not use it to bypass cyclic dependencies** — it exists for historical compatibility, not to paper over your design. On `CyclicDependencyError`, refactor per the above.
Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct through the same synchronous dependency graph. Changing activation cannot make a cycle valid. On `CyclicDependencyError`, refactor per the above.

## Interface cheat sheet

| Interface | Section | Role |
|---|---|---|
| `createDecorator<T>(name)` → `ServiceIdentifier<T>` | §1 | identity (runtime key + compile-time type + param decorator) |
| `@IService` | §2, §7 | declare a dependency on a constructor param |
| `registerScopedService(scope, id, ctor, type, domain)` | §1, §3, §5 | bind an impl to a lifetime tier |
| `registerScopedService(scope, id, ctor, activation, domain)` | §1, §3, §5 | bind an impl to a lifetime tier and construction time |
| `ServicesAccessor.get(IX)` | §2, §6 | resolve an instance by interface |
| `IInstantiationService.invokeFunction(fn, …)` | §6, §8 | obtain a temporary accessor inside a function |
| `IInstantiationService.createInstance(ctor, …args)` | §7 | build a non-singleton object with deps injected |
| `IInstantiationService.createChild(collection)` | §8 | spawn a child container |
| `getScopedServiceDescriptors(scope)` | §8 | retrieve all descriptors registered at a tier |
| `Disposable` / `DisposableStore` / `IDisposable` | §4 | resource management and disposal |
| `Scope` / `LifecycleScope` | §3, §8 | the lifetime tree |
| `ScopeActivation` | §3, §5 | choose scope-created or first-`get()` construction |
| `SyncDescriptor` | (tests / low-level) | package a constructor + static args into a pending descriptor |

> Legacy export (not used in v2, just recognize it): `refineServiceDecorator` is a VS Code leftover DI helper. v2 src/test has zero references; always use `registerScopedService`.
Expand All @@ -259,4 +286,4 @@ A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchron
- `@IX` decorates constructor params only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services — see service-authoring.md).
- Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique.
- `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use.
- No cyclic dependencies — refactor (extract / event / re-scope); do not break the cycle with `Delayed`.
- No cyclic dependencies — refactor (extract / event / re-scope); activation does not change cycle detection.
4 changes: 3 additions & 1 deletion .agents/skills/agent-core-dev/server-align.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const IAgentPromptService: ServiceIdentifier<IAgentPromptService> =

```ts
// promptService.ts — impl delegates to the native v2 Service
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';

constructor(@IAgentPromptService private readonly prompt: IAgentPromptService /*, ... */) {}
// submit() builds v2-native input, calls the native Service, projects the result
// back into the protocol PromptSubmitResult.
Expand All @@ -117,7 +119,7 @@ registerScopedService(
LifecycleScope.Agent, // scope = the lifetime of the legacy state
IAgentPromptService,
AgentPromptLegacyService,
InstantiationType.Delayed,
ScopeActivation.OnDemand,
'prompt',
);
```
Expand Down
14 changes: 6 additions & 8 deletions .agents/skills/agent-core-dev/service-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ Holds the concrete class(es) and the top-level registration. A typical impl:
* … collaborators as roles ("logs through `log`") … Bound at App scope.
*/

import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { ILogService } from '#/log';

import { type Greeting, IGreeter } from './greet';
Expand All @@ -154,12 +153,12 @@ export class Greeter implements IGreeter {
}
}

registerScopedService(LifecycleScope.App, IGreeter, Greeter, InstantiationType.Eager, 'greet');
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```

What belongs here:

- **Imports** — `InstantiationType` from `'#/_base/di/extensions'`; `LifecycleScope` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/<domain>` alias; the contract's types + decorator via a relative `./<domain>` import.
- **Imports** — `LifecycleScope` + `ScopeActivation` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/<domain>` alias; the contract's types + decorator via a relative `./<domain>` import.
- **Class** — `XxxService implements IXxxService`, with `declare readonly _serviceBrand: undefined`.
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.
Expand Down Expand Up @@ -245,7 +244,7 @@ Conventions:

- Back the public `Event<T>` with a private `Emitter<T>`, registered with `this._register(...)` so it disposes with the Service.
- Naming: `onDid…` for "happened" (past tense, after the fact); `onWill…` for "about to happen" (may allow `waitUntil` participation / veto — see `AsyncEmitter` / `IWaitUntil` in `'#/_base/event'`).
- The Delayed-instantiation Proxy preserves early `onDid…` / `onWill…` subscriptions (implement.md §5).
- A service must be constructed before consumers can subscribe to its events. Use the default `OnScopeCreated` activation when subscriptions must be available as soon as the scope is ready.

### `IEventService` — global pub-sub bus

Expand Down Expand Up @@ -318,16 +317,15 @@ export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('

```ts
// greet/greetService.ts
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { type Greeting, IGreeter } from './greet';

export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined;
hello(): Greeting { return { message: 'hi' }; }
}

registerScopedService(LifecycleScope.App, IGreeter, Greeter, InstantiationType.Eager, 'greet');
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```

```ts
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/agent-core-dev/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ Reach for this only when *which layer a service lives in* is itself the thing be

```ts
import { beforeEach, describe, expect, it } from 'vitest';
import { InstantiationType } from '#/_base/di/extensions';
import {
LifecycleScope,
ScopeActivation,
_clearScopedRegistryForTests,
registerScopedService,
} from '#/_base/di/scope';
Expand All @@ -94,7 +94,7 @@ describe('XxxService (scoped)', () => {
LifecycleScope.Agent,
IXxxService,
XxxService,
InstantiationType.Delayed,
ScopeActivation.OnDemand,
'xxx',
);
});
Expand Down
5 changes: 2 additions & 3 deletions apps/kimi-code/src/tui/components/messages/tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,8 @@ const PLAN_SAVED_TO_RE = /\nPlan saved to: ([^\n]+)\n/;
/**
* Parses the ExitPlanMode result content string to recover the approval outcome
* and optional plan path. Core-side templates live in
* `packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts` (auto-approved
* path) and `.../permissionPolicy/policies/exit-plan-mode-review-ask.ts`
* (user-reviewed path):
* `packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts` and
* `.../agent/permission/policies/exit-plan-mode-review-ask.ts`:
* - Approved output starts with 'Exited plan mode.' and selected options
* are reported as 'Selected approach: <label>'. Older outputs may start
* with 'User approved option "<label>".' Plan-file mode may include
Expand Down
Loading
Loading