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 @@ -67,4 +67,4 @@ Invariants that hold across every stage. Each is expanded in the stage file note
9. Throw coded errors; register codes centrally; branch on `code` across the wire, never `instanceof`. (errors.md)
10. Gate unreleased behavior behind a flag contributed via `registerFlagDefinition` and resolved through `IFlagService.enabled(id)`; no ad-hoc env toggles. (flags.md)
11. Tests resolve the SUT by interface; shared stubs live under `test/`, never `src/`. (test.md)
12. Config is the preference registry: only preferences that are persistable, schema'd, and user/operator-facing go in `IConfigService`. Domain-specific config (including env-only operational toggles) goes through `registerSection` + `envOverlay`. Facts → `IBootstrapService`, and host invocation arguments (CLI flags, host identity headers, prompt identity) → `BootstrapInput.args` / `IBootstrapService.args` — never new per-domain runtime-options services; domain runtime state (cron/flags/model) never goes onto `IBootstrapService`; session state → Session scope; constants → code. Business domains never call `IBootstrapService.getEnv()` directly. (config.md)
12. Config is the preference registry: only preferences that are persistable, schema'd, and user/operator-facing go in `IConfigService`. Domain-specific config (including env-only operational toggles) goes through `registerConfigSection` + `envOverlay`. Facts → `IBootstrapService`, and host invocation arguments (CLI flags, host identity headers, prompt identity) → `BootstrapInput.args` / `IBootstrapService.args` — never new per-domain runtime-options services; domain runtime state (cron/flags/model) never goes onto `IBootstrapService`; session state → Session scope; constants → code. Business domains never call `IBootstrapService.getEnv()` directly. (config.md)
3 changes: 2 additions & 1 deletion .agents/skills/agent-core-dev/align.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ import { InstantiationType, registerSingleton } from '../../di';
registerSingleton(IXxxService, XxxService, InstantiationType.Delayed);

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

Expand Down
67 changes: 40 additions & 27 deletions .agents/skills/agent-core-dev/config.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .agents/skills/agent-core-dev/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ The three mechanisms above are also where a domain accepts new behavior without
| Step into an operation in order / veto | a **hook** (`onWill`/`onDid`, `OrderedHookSlot`) | the owning scope |
| Swap a backend (File ↔ DB ↔ S3) | a **Store / Storage token** at the byte layer (see persistence.md) | `App` (composition root) |

The standard shape of a "registry / catalog the domain queries" row is an L3 contribution point: the target domain owns a `collection<T>` token, contributors call `this.provide(token, record)` from a unit, and a fold service in the target domain injects the `CollectionView` (incremental `onDidChange`; provider death withdraws the record). The four in-repo seams are `ConfigSectionContribution` → `ConfigRegistry`, `AgentToolContribution` → `AgentToolActivationService`, `AgentProfileContribution` → `IAgentProfileRegistry`, and `WireModelContribution` → `WireService` (file-level pointers: `packages/agent-core-v2/AGENTS.md` §Units and contribution points).

Closed-for-modification means: the domain's own file is not where new scenarios branch. If a new scenario forces an edit here, an extension point is missing or misplaced.

## 5. Dependency direction
Expand Down
16 changes: 8 additions & 8 deletions .agents/skills/agent-core-dev/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito

## Layout

- `src/flag/flagRegistry.ts` — `IFlagRegistry` token + `FlagDefinitionInput` / `FlagId` / `FlagSurface` types + `registerFlagDefinition` / `getContributedFlags` (import-time contribution queue).
- `src/flag/flagRegistryService.ts` — `FlagRegistryService` impl; in-memory catalog seeded from import-time contributions; App scope.
- `src/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `ExperimentalConfigSchema` / `ExperimentalConfig` (zod).
- `src/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`) + `EXPERIMENTAL_SECTION` (`experimental`); reads definitions from `IFlagRegistry`; self-registers at App scope.
- `src/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './flag/flagService'`).
- `src/app/flag/flagRegistry.ts` — `IFlagRegistry` token + `FlagDefinitionInput` / `FlagId` / `FlagSurface` types + `registerFlagDefinition` / `getContributedFlags` (import-time contribution queue).
- `src/app/flag/flagRegistryService.ts` — `FlagRegistryService` impl; in-memory catalog seeded from import-time contributions; App scope.
- `src/app/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `EXPERIMENTAL_SECTION` (`experimental`) / `ExperimentalConfigSchema` (zod) + the module-level `registerConfigSection(EXPERIMENTAL_SECTION, …)` call that owns the section.
- `src/app/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`); reads definitions from `IFlagRegistry` and overrides from `IConfigService`; self-registers at App scope.
- `src/app/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './app/flag/flagService'`).
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.

## Public surface
Expand All @@ -33,9 +33,9 @@ Highest wins; env is read live on every call (nothing cached):

## Config integration

- `FlagService` registers the `[experimental]` section into `IConfigRegistry` at construction (`registerSection('experimental', ExperimentalConfigSchema)`) and reads overrides from `IConfigService`.
- The flag domain owns the `[experimental]` section: `src/app/flag/flag.ts` registers it at module load via `registerConfigSection(EXPERIMENTAL_SECTION, ExperimentalConfigSchema, { fromToml, toToml })` (import = register, drained by `ConfigRegistry` at construction); `FlagService` reads overrides from `IConfigService`.
- It subscribes `IConfigService.onDidChange` and refreshes overrides whenever the `experimental` domain changes, so config edits apply live.
- `IConfigRegistry.registerSection` throws if a domain is registered twice — `experimental` is owned exclusively by `FlagService`.
- `ConfigRegistry.registerSection` throws if a domain is registered twice — `experimental` is owned exclusively by the flag domain.
- `setConfigOverrides(overrides)` is an imperative escape hatch for tests and hosts without an `IConfigService`; hosts on `IConfigService` should set the `[experimental]` section instead.

Config shape:
Expand All @@ -54,7 +54,7 @@ Declare the definition in the owning domain's `flag.ts` and call `registerFlagDe
`src/<domain>/flag.ts`:

```ts
import { type FlagDefinitionInput, registerFlagDefinition } from '#/flag';
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';

export const myFeatureFlag: FlagDefinitionInput = {
id: 'my_feature',
Expand Down
16 changes: 11 additions & 5 deletions .agents/skills/agent-core-dev/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('

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

export class Greeter implements IGreeter {
Expand Down Expand Up @@ -127,7 +128,8 @@ export class WSBroadcastService extends Disposable implements IWSBroadcastServic

- Extend `Disposable`, collect any `IDisposable` with `this._register(d)` (event subscriptions, `toDisposable(fn)`, etc.).
- 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.
- Disposal order is deterministic (orient.md): child scopes first; within a scope the Ledger (`src/_base/lifecycle/`) tears entries down in strict reverse registration order, serially — `Disposable` / `DisposableStore` delegate to it.
- Extend `Service` (from `#/_base/di/service`) instead when the unit needs capability calls on `this` (`provide` / `effect` / `on` / `get` / `ref`) — e.g. contributing a record to a `collection` token. `Service` extends `Disposable` (so `_register` is unchanged) and adds the two-phase construction protocol: `provide` / `on` / `effect` calls inside the constructor are buffered and flushed by the kernel after `Reflect.construct`; `get` / `ref` throw inside the constructor — dependencies stay constructor parameters. A manually `new`ed `Service` has no capabilities: every capability call throws.

## §5 Scope activation

Expand Down Expand Up @@ -160,14 +162,16 @@ registerScopedService(
);
```

`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.OnScopeCreated` is the default fourth argument. Scope creation activates every registration using this mode, after constructing its dependencies. An eager constructor failure no longer fails scope creation: the unit lands in sticky `Failed` — scope creation succeeds, resolving the unit rethrows its error, and an explicit `update()` reloads it (see the bootstrap note below). 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.

Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`.

The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth.

**Bootstrap shares the dynamic provide path.** Scope creation (`Scope.createApp` / `Scope.createChild` / `createScopedChildHandle` in `src/_base/di/scope.ts`) submits the scope kind's entire `registerScopedService` batch as ONE cascade transaction via `provideAll`: every token registers before the activation wave runs, so **registration order never matters**, and untracked transitive `createInstance` resolutions succeed inside the batch. A seed occupying a token (the `extra` tuple in `ScopeOptions`) overrides the static registration for that token. `activateScopeServices` is gone — there is no separate static activation path.

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

When you do not want a new class and just need a service once, or when you expose a `ServicesAccessor` to the outside:
Expand Down Expand Up @@ -198,7 +202,7 @@ class TurnRunner {
const runner = instantiation.createInstance(TurnRunner, 'hello', 1);
```

Static params come first (you pass them), service params follow (the container fills them), then `Reflect.construct` builds the instance. This object is **not** placed in any scope's singleton cache — every call is a fresh instance.
Static params come first (you pass them), service params follow (the container fills them), then `Reflect.construct` builds the instance. This object is **not** placed in any scope's singleton cache — every call is a fresh instance — and it is not tracked as a cascade unit either: `createInstance` products are cascade-exempt leaves that no cascade tears down or rebuilds; their owner disposes them.

> This is why service params must follow static params **for `createInstance`**: the container sorts by the parameter positions recorded via `@IX`. `_serviceBrand` lets the compiler tell the two kinds apart. Scoped services built by `registerScopedService` follow a different convention (`@IX` params first, optional static params after) — see service-authoring.md §constructor-conventions.

Expand Down Expand Up @@ -234,7 +238,7 @@ Key points:
- `instantiation.createChild(collection)` builds a child container whose parent pointer is the current container — so the child resolves upward to `App` services (the visibility rule).
- Expose the child to the outside by wrapping it in a `ServicesAccessor` via `invokeFunction` (§6).

> Higher-level code usually calls `Scope.createChild(kind, id)` (it does the "filter descriptors + build child" for you). Drop to the manual `ServiceCollection` form only when you need explicit control.
> Higher-level code usually calls `Scope.createChild(kind, id)` (it does the "filter descriptors + build child" for you, then submits the whole batch through `provideAll` as one cascade transaction — see §5). Drop to the manual `ServiceCollection` form only when you need explicit control; to change bindings on an already-created container, prefer `provide` / `unprovide` / `update` over rebuilding a collection. Before the static batch lands, the scope-creation point runs the kernel's `ScopeUnits` fold (`_base/di/scopeUnits.ts` — materializes the recipes contributed to `ScopeUnits(kind)` as per-scope units) and then the `ScopeOptions.assemble` hook — the session domain uses the hook to construct its seed-adapter units (`session/sessionSeed/sessionSeedAdapters.ts`) so their provided tokens exist before the session services activate.

## §9 Cyclic dependencies (forbidden — refactor)

Expand Down Expand Up @@ -276,6 +280,8 @@ Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct t
| `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 |
| `Service` (`_base/di/service`) | §4 | unit base class — `this.provide/effect/on/get/ref` capabilities, two-phase construction |
| `collection<T>(name)` / `CollectionView<T>` (`_base/di/collection`) | §4 | contribution-point token + the fold's live view (provider death withdraws the record) |
| `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 Down
32 changes: 21 additions & 11 deletions .agents/skills/agent-core-dev/orient.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,26 @@ Classes talk only to interfaces and never care how an implementation is construc
Lifetimes form a tree, from longest to shortest:

```text
App (0) process-wide, single global instance
└── Workspace (1) one workspace handler (a materialized workspace root)
└── Session (2) one session
└── Agent (3) one agent
App process-wide, single global instance
└── Workspace one workspace handler (a materialized workspace root)
└── Session one session
└── Agent one agent
```

```ts
// src/app/scopes.ts — the business layer declares the tiers and their order;
// the DI kernel only knows opaque string kinds plus the declared topology.
export enum LifecycleScope {
App = 0,
Workspace = 1,
Session = 2,
Agent = 3,
App = 'app',
Workspace = 'workspace',
Session = 'session',
Agent = 'agent',
}
```

- A larger number = shorter life = closer to a leaf.
- Later in the topology = shorter life = closer to a leaf.
- "Singleton" means **one per scope**: `ILogService` is global once; each `Session` scope has its own `ISessionMetadata`.
- `kind` strictly increases along the parent→child direction.
- `kind` must advance along the declared topology in the parent→child direction.

### Visibility rule

Expand All @@ -47,7 +49,15 @@ A child scope sees its ancestors; a parent never sees its children. Resolution w

### Disposal order

Deterministic: **child scopes die first; within one scope, instances dispose in reverse construction order** (last constructed, first disposed). Business code declares which tier it lives in and never disposes by hand.
Deterministic: **child scopes die first; within one scope, teardown runs in strict reverse registration order, one entry at a time.** The mechanism is the Ledger (`src/_base/lifecycle/`): ordered effect bookkeeping, dual-track (sync + async disposers), serial reverse-order teardown (never parallel), with the teardown reason (`'scope-close' | 'cascade' | 'unload'`) passed through to every disposer. `Disposable` / `DisposableStore` (`src/_base/di/lifecycle.ts`) delegate to it — "reverse construction order" is a Ledger property, not a container convention. Business code declares which tier it lives in and never disposes by hand.

## Dynamic DI: units and cascades

Registration is not the end of the story. Every unit a container tracks — static registrations and runtime `provide`s alike — lives in a small state machine owned by the scope's cascade engine (`src/_base/di/cascadeEngine.ts`, one per scope container, orchestrating tree-wide). Vocabulary you will meet in errors, tests, and the debug surface:

- **Unit states** — `Pending → Activating → Active`, plus `Unloading` during teardown and a sticky `Failed`. A construction failure parks the unit in `Failed` with no auto-retry: resolving it rethrows its error; an explicit `update()` reloads it.
- **Waiting area** — a unit whose declared dependencies are missing sits `Pending` and auto-activates when they arrive, including cross-scope wake-up when an ancestor gains the token. An `ondemand` unit counts as available: consumers pull it transitively at materialization.
- **Cascade transaction** — every `provide` / `unprovide` / `update` runs as one tree-wide transaction: contagion set from the persistent dependency graph (instance edges, child→parent across scopes) → abort hook → global reverse-topo teardown → apply the change → waiting-area recheck fixpoint → history ring. Static bootstrap shares this path: scope creation submits the kind's whole registration batch as one `provideAll`, so registration order never matters.

## Import boundaries

Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/agent-core-dev/server-align.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +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';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';

constructor(@IAgentPromptService private readonly prompt: IAgentPromptService /*, ... */) {}
// submit() builds v2-native input, calls the native Service, projects the result
Expand Down
Loading
Loading