From 1745ea074a841700b9d76171a3f8efaee73be118 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 1 Jul 2026 11:18:46 +0800 Subject: [PATCH] docs(agent-core-dev): rename Core scope to App and drop Turn tier - replace `Core` with `App` across the skill docs and dep-graph.mjs - drop the `Turn` scope, collapsing the four-tier tree to three (App/Session/Agent) - update examples, anti-patterns, banned entity-service names, and createCoreScope -> createAppScope --- .agents/skills/agent-core-dev/SKILL.md | 4 +-- .agents/skills/agent-core-dev/align.md | 26 +++++++++---------- .agents/skills/agent-core-dev/config.md | 6 ++--- .agents/skills/agent-core-dev/design.md | 12 +++------ .../agent-core-dev/domain-boundaries.md | 4 +-- .agents/skills/agent-core-dev/flags.md | 8 +++--- .agents/skills/agent-core-dev/implement.md | 4 +-- .agents/skills/agent-core-dev/orient.md | 6 ++--- .agents/skills/agent-core-dev/permission.md | 2 +- .agents/skills/agent-core-dev/server-align.md | 2 +- .../agent-core-dev/service-authoring.md | 6 ++--- .agents/skills/agent-core-dev/telemetry.md | 8 +++--- .agents/skills/agent-core-dev/verify.md | 2 +- packages/agent-core-v2/scripts/dep-graph.mjs | 2 +- 14 files changed, 42 insertions(+), 50 deletions(-) diff --git a/.agents/skills/agent-core-dev/SKILL.md b/.agents/skills/agent-core-dev/SKILL.md index 182f93677..452d44301 100644 --- a/.agents/skills/agent-core-dev/SKILL.md +++ b/.agents/skills/agent-core-dev/SKILL.md @@ -39,7 +39,7 @@ End-to-end procedures that span the stages. Reach for these before reading the s - Topic: [Edge exposure — `resource:action` + WS events](edge-exposure.md) — which Services are exposed over `/api/v2` (per-scope action map) and which events stream over WS; what to wrap in a facade. - [Stage 3 — Implement](implement.md): the standard Service recipe and the DI building blocks — interface + identity, constructor injection, scoped registration, `Disposable`, eager vs delayed, `invokeFunction`, `createInstance`, child scopes, and the cycle-refactor playbook. - Topic: [Service authoring](service-authoring.md) — file layout, naming, contract vs impl contents, interface style, constructor/field conventions, events, multi-Service domains, comment rules. - - Topic: [Config](config.md) — the section-registry model, Core vs Session split, owning a config section, the TOML format, and the env overlay. + - Topic: [Config](config.md) — the section-registry model, App vs Session split, owning a config section, the TOML format, and the env overlay. - Topic: [Errors](errors.md) — co-located `XxxError`, the central code registry, wire serialization, boundary translation. - Topic: [Flags](flags.md) — `FLAG_DEFINITIONS`, `IFlagService.enabled(id)`, the `[experimental]` config section, resolution precedence. - Topic: [Permission](permission.md) — composable chain-of-responsibility kernel, policy registry + composer, `modes`/`agentTypes` metadata, `resolveExecution`/`accesses`. @@ -61,7 +61,7 @@ Invariants that hold across every stage. Each is expanded in the stage file note 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) 6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md) -7. Scope follows state identity — no `Map` at `Core` to fake per-session state. (design.md) +7. Scope follows state identity — no `Map` 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) 9. Throw coded errors; register codes centrally; branch on `code` across the wire, never `instanceof`. (errors.md) 10. Gate unreleased behavior behind a `FLAG_DEFINITIONS` flag; no ad-hoc env toggles. (flags.md) diff --git a/.agents/skills/agent-core-dev/align.md b/.agents/skills/agent-core-dev/align.md index e4ea6ade7..9c356884e 100644 --- a/.agents/skills/agent-core-dev/align.md +++ b/.agents/skills/agent-core-dev/align.md @@ -6,7 +6,7 @@ Use this when the task is "move feature X from v1 to v2", "port `IXxxService` to ## The one-paragraph mental model -v1 is a **VSCode-style singleton container**: services self-register with `registerSingleton`, resolve as singleton-per-container, and have no explicit lifetime tier — so a single `ISessionService` / `IToolService` tends to accumulate global, per-session, and per-agent state in one class. v2 is a **DI × Scope tree**: every service binds to one of `Core` / `Session` / `Agent` / `Turn`, and a domain with state at several lifetimes is split into several Services. Porting is therefore **not** a file copy — it is "find each lifetime of state hiding in the v1 class, give each its own v2 Service at the right scope, then re-wire the dependencies". +v1 is a **VSCode-style singleton container**: services self-register with `registerSingleton`, resolve as singleton-per-container, and have no explicit lifetime tier — so a single `ISessionService` / `IToolService` tends to accumulate global, per-session, and per-agent state in one class. v2 is a **DI × Scope tree**: every service binds to one of `App` / `Session` / `Agent`, and a domain with state at several lifetimes is split into several Services. Porting is therefore **not** a file copy — it is "find each lifetime of state hiding in the v1 class, give each its own v2 Service at the right scope, then re-wire the dependencies". ## v1 → v2 at a glance @@ -14,7 +14,7 @@ v1 is a **VSCode-style singleton container**: services self-register with `regis |---|---|---| | 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'` | -| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (Core/Session/Agent/Turn) — see orient.md | +| 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'` | | Resolve SUT in tests | `ix.createInstance(Impl)` (common) | `ix.get(IX)` by interface — see test.md | @@ -40,7 +40,7 @@ Actions: - Locate the v1 entry: contract (`/.ts`) + impl (`/Service.ts`), plus any helpers under the same folder. - Inventory three things from the impl: - - **State** — every field / `Map` / cache the class holds. For each, note its *identity* (global? keyed by `sessionId`? by `agentId`? by `turnId`?). + - **State** — every field / `Map` / cache the class holds. For each, note its *identity* (global? keyed by `sessionId`? by `agentId`?). - **Behavior** — every public method; group them by which state they touch. - **Dependencies** — every `@IFoo` constructor injection and every cross-domain relative import (`from '..//...'`). - Note the v1 registration line (`registerSingleton(...)`) and any `services.set(IX, ...)` overrides at bootstrap (these reveal runtime static args or prebuilt instances the port must preserve). @@ -53,13 +53,13 @@ Do not start splitting yet — an accurate inventory prevents the common mistake Method — for each piece of state from the inventory, ask: -1. **What is it keyed by?** nothing → a global unit; `sessionId` → a per-session unit; `agentId` → a per-agent unit; `turnId` → a per-turn unit. -2. **When should it die?** with the process / the session / the agent / the turn. State that must outlive its neighbors is a different unit. +1. **What is it keyed by?** nothing → a global unit; `sessionId` → a per-session unit; `agentId` → a per-agent unit. +2. **When should it die?** with the process / the session / the agent. State that must outlive its neighbors is a different unit. 3. **Which methods touch only this state?** they travel with the unit. Worked example — v1 `ISessionService` (one class, ~600 lines) holds: -- a global index of all sessions → **global** unit → v2 `sessionStore` (`ISessionStore`, Core); +- a global index of all sessions → **global** unit → v2 `sessionStore` (`ISessionStore`, App); - this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session); - this session's activity / status → **per-session** unit → v2 `session-activity`; - this session's context projection → **per-session** unit → v2 `session-context`; @@ -118,8 +118,8 @@ When the table says "verify", or when v1 and v2 have diverged, **read the v2 `sr For each semantic unit, fix its `LifecycleScope` from the identity you found in step 2. Follow design.md §2 verbatim: -- global → `Core`; per `sessionId` → `Session`; per `agentId` → `Agent`; per `turnId` → `Turn`. -- Stateless unit → default to `Core`, pulled down only by a shorter-lived dependency. +- global → `App`; per `sessionId` → `Session`; per `agentId` → `Agent`. +- Stateless unit → default to `App`, pulled down only by a shorter-lived dependency. - Self-check: "when this scope is disposed, should this state disappear with it?" This is the decision v1 never had to make — get it right before writing any v2 code, because the scope is fixed at registration and changing it later ripples through every consumer. @@ -129,17 +129,17 @@ This is the decision v1 never had to make — get it right before writing any v2 Decide the Service shape per unit, following design.md §3: - A unit that owns **one instance's** state → a single per-instance Service (`ISessionXxx` / `IAgentXxx`). -- A unit that owns a **global view plus per-instance** state → split into a `Core` registry/factory (`XxxStore` / `XxxRegistry` / `XxxCatalog`) **and** a per-instance Service. The `Core` half creates or locates the per-instance half. +- A unit that owns a **global view plus per-instance** state → split into an `App` registry/factory (`XxxStore` / `XxxRegistry` / `XxxCatalog`) **and** a per-instance Service. The `App` half creates or locates the per-instance half. - Do not pre-split a unit that has state at only one lifetime. -Most consumers inject the per-instance Service; inject the `Core` factory only for genuine cross-instance management. +Most consumers inject the per-instance Service; inject the `App` factory only for genuine cross-instance management. ### 6. Direct dependencies Re-wire the dependencies you inventoried in step 1, now across the new v2 Services. Follow design.md §4–§5: - **Calling style** — need a result / I orchestrate → direct call (`@IX` injection); stating a fact → event; ordered participation that may veto → hook. -- **Scope direction** — a Service may inject only its own scope or an ancestor. If a `Core` Service needs something from a `Session` Service, the dependency is backwards: re-scope or invert into an event. +- **Scope direction** — a Service may inject only its own scope or an ancestor. If an `App` Service needs something from a `Session` Service, the dependency is backwards: re-scope or invert into an event. - **Domain direction** — foundational layers must not know upstream ones. A cycle means a v1 relative import is now pointing the wrong way; extract a third Service or invert the notification into an event. - **Durable facts** — state changes that must be recorded / replayed / projected across agents go on the wire (`wireRecord`), not a direct call alone. @@ -188,7 +188,7 @@ import { KimiError, type ErrorCode } from '#/_base/errors'; Red lines: -- Do not copy a v1 file and "fix imports". Re-split first (steps 2–6); a straight copy carries v1's implicit-singleton assumptions into v2 and creates the `Map`-at-`Core` anti-pattern. +- Do not copy a v1 file and "fix imports". Re-split first (steps 2–6); a straight copy carries v1's implicit-singleton assumptions into v2 and creates the `Map`-at-`App` anti-pattern. - Do not leave v1 relative imports (`from '../x/...'`) in v2 — use the `#/...` alias and respect the domain layers. - Do not preserve a v1 behavior just because it exists; if the split reveals it was a workaround for the missing scope tree, drop it. @@ -218,7 +218,7 @@ const svc = ix.get(IXxxService); Before submitting a port: -- [ ] Every piece of v1 state landed in a v2 Service whose scope matches its identity (no `Map` at `Core`). +- [ ] Every piece of v1 state landed in a v2 Service whose scope matches its identity (no `Map` at `App`). - [ ] Each v1 dependency now points in the right scope and domain direction; `lint:domain` passes. - [ ] Registrations use `registerScopedService` with an explicit scope and domain name; no `registerSingleton` remains. - [ ] Imports use the `#/...` alias; no v1 relative (`../../di`, `../../errors`) imports remain. diff --git a/.agents/skills/agent-core-dev/config.md b/.agents/skills/agent-core-dev/config.md index d96f8c72b..4e0f4ee5d 100644 --- a/.agents/skills/agent-core-dev/config.md +++ b/.agents/skills/agent-core-dev/config.md @@ -1,6 +1,6 @@ # Topic — Config -How the `config` domain works and how a domain owns its configuration section. Covers the section-registry model, the Core vs Session split, the TOML on-disk format, and the recipe for adding or migrating a config section. +How the `config` domain works and how a domain owns its configuration section. Covers the section-registry model, the App vs Session split, the TOML on-disk format, and the recipe for adding or migrating a config section. The `config` domain is a thin registry + loader: it does **not** know the shape of any individual section. Each domain owns the schema (and, where needed, the TOML transform) for the config it consumes, registers the section into `IConfigRegistry`, and reads it through `IConfigService`. There is no whole-config object passed around. @@ -96,7 +96,7 @@ A domain that owns a section keeps the schema in its own `configSection.ts` (e.g ## Scope -- `IConfigRegistry` / `IConfigService` — **Core** scope, process-global. One registry of sections; one loader reading `~/.kimi-code/config.toml` (path from `IBootstrapService.configPath`). +- `IConfigRegistry` / `IConfigService` — **App** scope, process-global. One registry of sections; one loader reading `~/.kimi-code/config.toml` (path from `IBootstrapService.configPath`). All config reads go through `IConfigService` (global config). Per-session runtime state (active model, thinking level, etc.) lives in the owning Session-scoped service (e.g. `IProfileService`), not in `config`. @@ -253,7 +253,7 @@ When `KIMI_MODEL_NAME` is set, the `provider` domain's `kimiModelEnvOverlay` (`s - `config` is **L2**. Domains that own sections import `config` (for `IConfigRegistry` / `IConfigService`) and must be at L2 or higher; lower layers need an entry in `ALLOWED_EXCEPTIONS` (e.g. `kosong>config`, `kosong>provider`). - Cross-domain type sharing for a config type may need an exception too (e.g. `plugin>mcp` for `McpServerConfig`). Prefer importing the type from the owning domain over re-declaring it. -- `IConfigRegistry` / `IConfigService` are **Core**. Agent / Turn scope services may inject Core services via ancestor lookup. +- `IConfigRegistry` / `IConfigService` are **App**. Agent scope services may inject App services via ancestor lookup. - `config` never imports a higher domain and holds no section schemas of its own; if a section needs a type from another domain, that schema lives in that domain. ## Red lines (this topic) diff --git a/.agents/skills/agent-core-dev/design.md b/.agents/skills/agent-core-dev/design.md index c85f56380..9cd5589ea 100644 --- a/.agents/skills/agent-core-dev/design.md +++ b/.agents/skills/agent-core-dev/design.md @@ -22,7 +22,6 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim | `App` | none (single global instance) | the process | | `Session` | `sessionId` | one session | | `Agent` | `agentId` | one agent | -| `Turn` | `turnId` | one turn | ### Decision tree @@ -36,7 +35,6 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim - one global instance → **`App`** - one per session → **`Session`** - one per agent → **`Agent`** -- one per turn → **`Turn`** - a mix (a global registry *and* per-instance state) → **split it** (see §3). **Q3 (stateless). What is the shortest-lived dependency it must inject?** @@ -45,7 +43,7 @@ A stateless Service is pulled *down* by its shortest-lived dependency: if it inj ### The core anti-pattern (a litmus test) -> **Do not store per-session state in a `Map` inside a `App` Service.** +> **Do not store per-session state in a `Map` inside an `App` Service.** This is the tell-tale sign of "should have been `Session`-scoped but was parked at `App`". Consequences: nobody cleans the entry up when the session ends (leak); every consumer threads `sessionId` around (loss of type safety); it cannot inject `Session`/`Agent`-scoped collaborators. @@ -187,8 +185,7 @@ domain: `` (owning scope: ) ├─ exposes (interfaces I provide, by scope) │ ├─ App : │ ├─ Session : -│ ├─ Agent : -│ └─ Turn : — — (none) +│ └─ Agent : └─ depends (what I inject) tag = calling style └─ @ direct/event/hook — ``` @@ -235,8 +232,7 @@ domain: `session` (owning scope: Session) ├─ exposes (interfaces I provide, by scope) │ ├─ App : — — (no global session state here) │ ├─ Session : ISessionService — this session's operations + child-agent set -│ ├─ Agent : — — (per-agent state lives in agent-lifecycle) -│ └─ Turn : — — (no per-turn session state) +│ └─ Agent : — — (per-agent state lives in agent-lifecycle) └─ depends (what I inject) ├─ session-context @Session direct — reads its own identity ├─ agent-lifecycle @Session direct — drives child-agent lifecycle @@ -270,7 +266,7 @@ For a multi-scope split, the `exposes` block fills more than one scope — see t ## Red lines (this stage) - Scope is not a domain; ownership follows write authority and invariants, not read consumption. -- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`, `ITurnEntityService`) that re-merge multiple domains. +- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`) that re-merge multiple domains. - No `Map` at `App` to fake per-session state. - Scope follows state identity; stateless Services are pulled down by their shortest-lived dependency, otherwise default to `App`. - Do not pre-split a domain that has state at only one lifetime. diff --git a/.agents/skills/agent-core-dev/domain-boundaries.md b/.agents/skills/agent-core-dev/domain-boundaries.md index 24b8ed9a1..f97f0bddf 100644 --- a/.agents/skills/agent-core-dev/domain-boundaries.md +++ b/.agents/skills/agent-core-dev/domain-boundaries.md @@ -12,10 +12,8 @@ A Service registered at `LifecycleScope.Session` or `LifecycleScope.Agent` is ** | Term | Meaning | |---|---| -| **Scope** | Lifetime / visibility tier. Current code registers Services at `Core`, `Session`, or `Agent`. | +| **Scope** | Lifetime / visibility tier. Current code registers Services at `App`, `Session`, or `Agent`. | | **Domain** | A cohesive business responsibility with its own model, invariants, and write authority. | - -> If a future branch adds a shorter-lived scope such as `Turn`, treat it as another lifetime tier, not as a data owner. The ownership rules below do not change. | **Entity** | Data with identity and lifecycle, usually suitable for `get/list/create/update/delete` semantics. | | **Aggregate** | A consistency boundary: the owner that enforces invariants over a cluster of data. | | **Read model / projection** | Derived data built for queries; it may be shaped like a domain, but it is not the write authority. | diff --git a/.agents/skills/agent-core-dev/flags.md b/.agents/skills/agent-core-dev/flags.md index 5f57c3074..1bc24b3ef 100644 --- a/.agents/skills/agent-core-dev/flags.md +++ b/.agents/skills/agent-core-dev/flags.md @@ -1,6 +1,6 @@ # Topic — Flags -Experimental feature-flag gating for agent-core-v2 — a App-scope `IFlagService` resolver plus a writable `IFlagRegistry` catalog that domains contribute their flags to, backed by the `[experimental]` config section. +Experimental feature-flag gating for agent-core-v2 — an App-scope `IFlagService` resolver plus a writable `IFlagRegistry` catalog that domains contribute their flags to, backed by the `[experimental]` config section. Gate not-yet-public features behind `IFlagService.enabled(id)`, per the repository hard rule that unreleased behavior must be flag-gated. v1 was a process-global `FlagResolver` singleton over a central `FLAG_DEFINITIONS` array; v2 is a scoped DI service whose flag definitions are registered **decentrally** by each owning domain — there is no central catalog to edit. @@ -85,7 +85,7 @@ export * from './flag'; ## Consume a flag -Inject `IFlagService` and gate on it. It is resolvable from any scope (Core ancestor): +Inject `IFlagService` and gate on it. It is resolvable from any scope (App ancestor): ```ts constructor(@IFlagService private readonly flags: IFlagService) {} @@ -97,7 +97,7 @@ if (!this.flags.enabled('micro_compaction')) return; - Domain `flag` is registered at **L3**. It imports only `config` (L2) downward. - It cannot live in `_base` (L0): registering/reading the config section requires importing `config`, and L0 must not import L2. -- Scope: `IFlagRegistry` and `IFlagService` are both `Core`. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at **import time** (top-level `registerFlagDefinition` calls), so they are queued before any scope is created and drained when `FlagRegistryService` is first instantiated — before `IFlagService` is first resolved. +- Scope: `IFlagRegistry` and `IFlagService` are both `App`. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at **import time** (top-level `registerFlagDefinition` calls), so they are queued before any scope is created and drained when `FlagRegistryService` is first instantiated — before `IFlagService` is first resolved. - Tests build `FlagService` + `FlagRegistryService` directly with a real `ConfigRegistry`/`ConfigService` and an injected env map, then `register` the flags they exercise. ## Red lines (this topic) @@ -106,4 +106,4 @@ if (!this.flags.enabled('micro_compaction')) return; - Contribute each flag from the **owning domain's** `flag.ts` (`src//flag.ts`) via a top-level `registerFlagDefinition` call; there is no central catalog to edit. The directory names the domain, so the file is just `flag.ts`. - `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`; `id` must not be `flag`. - `FlagId` is `string` (decentralized registration) — do not reintroduce a central `FLAG_DEFINITIONS` array or a derived literal union. -- `flag` lives at L3 and `Core` scope — never in `_base`, never per-session. +- `flag` lives at L3 and `App` scope — never in `_base`, never per-session. diff --git a/.agents/skills/agent-core-dev/implement.md b/.agents/skills/agent-core-dev/implement.md index 1462bd939..708518c0c 100644 --- a/.agents/skills/agent-core-dev/implement.md +++ b/.agents/skills/agent-core-dev/implement.md @@ -203,7 +203,7 @@ export class ScopeRegistry implements IScopeRegistry { Key points: - `getScopedServiceDescriptors(scope)` returns every descriptor registered at that tier; load them into a `ServiceCollection`. -- `instantiation.createChild(collection)` builds a child container whose parent pointer is the current container — so the child resolves upward to `Core` services (the visibility rule). +- `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. @@ -218,7 +218,7 @@ If A needs B while being created and B needs A while being created, the containe ### Why cycles are disallowed -- Scope layering makes normal dependencies a DAG (Turn → Agent → Session → Core, resolving upward); a cycle is almost always a design smell. +- Scope layering makes normal dependencies a DAG (Agent → Session → App, resolving upward); a cycle is almost always a design smell. - "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug. v2's stance: **the dependency graph must be acyclic.** diff --git a/.agents/skills/agent-core-dev/orient.md b/.agents/skills/agent-core-dev/orient.md index d4c15c0e3..e3b3c8e2b 100644 --- a/.agents/skills/agent-core-dev/orient.md +++ b/.agents/skills/agent-core-dev/orient.md @@ -12,7 +12,7 @@ When writing business code you declare three things; the container handles the r Classes talk only to interfaces and never care how an implementation is constructed. -## The four `LifecycleScope` tiers +## The three `LifecycleScope` tiers Lifetimes form a tree, from longest to shortest: @@ -20,7 +20,6 @@ Lifetimes form a tree, from longest to shortest: App (0) process-wide, single global instance └── Session (1) one session └── Agent (2) one agent - └── Turn (3) one turn of conversation ``` ```ts @@ -28,7 +27,6 @@ export enum LifecycleScope { App = 0, Session = 1, Agent = 2, - Turn = 3, } ``` @@ -40,7 +38,7 @@ export enum LifecycleScope { A child scope sees its ancestors; a parent never sees its children. Resolution walks *up* the tree: -- ✅ A `Turn` service injects a `Session` or `App` service (found upward). +- ✅ An `Agent` service injects a `Session` or `App` service (found upward). - ❌ An `App` service injects a `Session` service (the parent does not look down, and the child may not exist yet). > **Short-lived may inject long-lived; never the reverse.** The tree structure enforces this — it is not a matter of discipline. diff --git a/.agents/skills/agent-core-dev/permission.md b/.agents/skills/agent-core-dev/permission.md index 6d7abb725..7db146b9c 100644 --- a/.agents/skills/agent-core-dev/permission.md +++ b/.agents/skills/agent-core-dev/permission.md @@ -94,7 +94,7 @@ this.policies = registry.list() Key points: - `modes` / `agentTypes` are **declarations** — they lift the `if (mode !== 'yolo') return` out of `YoloModeApprove` into metadata. -- `factory`, not `instance`: a node may depend on agent-scoped services (mode, rules) and must be instantiated in the Agent scope — symmetric to `IToolDefinitionRegistry` (Core) storing factories and `IToolService` (Agent) instantiating tools. +- `factory`, not `instance`: a node may depend on agent-scoped services (mode, rules) and must be instantiated in the Agent scope — symmetric to `IToolDefinitionRegistry` (App) storing factories and `IToolService` (Agent) instantiating tools. - **Different `(agent, mode)` produce differently-shaped chains** — under yolo the ask/fallback phases are physically filtered out. ### 5.3 Two contribution paths diff --git a/.agents/skills/agent-core-dev/server-align.md b/.agents/skills/agent-core-dev/server-align.md index 2e4b1dc32..f3d22ab60 100644 --- a/.agents/skills/agent-core-dev/server-align.md +++ b/.agents/skills/agent-core-dev/server-align.md @@ -74,7 +74,7 @@ Reach for a LegacyService when **any** hold: - The v1 endpoint carries state the v2 domain deliberately dropped (e.g. a FIFO queue, a `prompt_id`, idempotent `abort`/`steer`, auto-start-next). - The v1 method returns a handle/stream that v2 wraps differently, and the v1 clients expect the old envelope shape. -- Matching v1 would force a `Map`-at-`Core` anti-pattern or a scope/domain-direction violation into the native Service (see [align.md](align.md) red lines). +- Matching v1 would force a `Map`-at-`App` anti-pattern or a scope/domain-direction violation into the native Service (see [align.md](align.md) red lines). - The native Service's error set / return type would have to grow v1-only branches. Do **not** put v1 quirks into the native v2 Service "to keep the route simple". That is the conflict this rule exists to prevent: the native Service serves the v2 architecture; the LegacyService serves the wire contract. diff --git a/.agents/skills/agent-core-dev/service-authoring.md b/.agents/skills/agent-core-dev/service-authoring.md index 8451e6036..dcf41de4d 100644 --- a/.agents/skills/agent-core-dev/service-authoring.md +++ b/.agents/skills/agent-core-dev/service-authoring.md @@ -36,7 +36,7 @@ The package entry `src/index.ts` re-exports each domain barrel so that importing The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Session and Agent services always carry `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names. -> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, `ISessionEntityService`, and `ITurnEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md). +> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md). ### File names @@ -256,7 +256,7 @@ Inject `@IEventService` and `publish(...)`; `subscribe(...)` returns an `IDispos A domain may define several Services. How to organize them: - **Same scope, tightly coupled** → one contract file, possibly one impl file with several classes and several `registerScopedService(...)` calls (e.g. `logService.ts` registers both `ILogWriterService` and `ILogService`). -- **Different scopes** → separate impl files named after the Service (`logService.ts` for Core `ILogService`, `sessionLogService.ts` for Session `ISessionLogService`); one shared contract file (`log.ts`). +- **Different scopes** → separate impl files named after the Service (`logService.ts` for App `ILogService`, `sessionLogService.ts` for Session `ISessionLogService`); one shared contract file (`log.ts`). - **Split by responsibility** — even within one scope, prefer a separate impl file when a class is large or independently testable. The contract file still holds **all** of the domain's interfaces and decorators in one place so consumers import the domain's surface from `./`. @@ -332,7 +332,7 @@ export * from './greet/index'; - One folder per domain, kebab-case; contract `.ts`, impl `Service.ts`, barrel `index.ts`. - `IXxxService` / `XxxService` naming; decorator string is lowerCamelCase, globally unique, and stable. -- Name Services by owning domain, never by scope (`IAgentEntityService`, `ISessionEntityService`, `ITurnEntityService`). +- Name Services by owning domain, never by scope (`IAgentEntityService`, `ISessionEntityService`). - `_serviceBrand` only on interfaces used as a DI token — never on base interfaces or plain models. - Sync methods return concrete types, async return `Promise`; do not `Promise`-wrap sync work. - `createInstance` objects put static parameters before service parameters; scoped services put `@IX` parameters first (static params need defaults). diff --git a/.agents/skills/agent-core-dev/telemetry.md b/.agents/skills/agent-core-dev/telemetry.md index 71c579a2c..122cd20cb 100644 --- a/.agents/skills/agent-core-dev/telemetry.md +++ b/.agents/skills/agent-core-dev/telemetry.md @@ -2,7 +2,7 @@ Telemetry infrastructure for agent-core-v2: how business services emit events, how context propagates, and how events reach a destination through appenders. -Telemetry is a **layer-1 root** domain (alongside `log`): pure `Core` scope, stateless, no business-domain dependencies. It is a thin facade — enrichment, batching, and transport belong to the appenders, not to this layer. +Telemetry is a **layer-1 root** domain (alongside `log`): pure `App` scope, stateless, no business-domain dependencies. It is a thin facade — enrichment, batching, and transport belong to the appenders, not to this layer. ## Where things live @@ -62,8 +62,8 @@ Built-in appenders: Appenders are added after the App scope exists, by resolving the service and calling `addAppender`: ```ts -const core = createCoreScope(); -const telemetry = core.accessor.get(ITelemetryService); +const app = createAppScope(); +const telemetry = app.accessor.get(ITelemetryService); telemetry.addAppender(new ConsoleAppender({ prefix: '[dev]' })); // dev echo telemetry.addAppender(new CloudAppender({ // production @@ -85,7 +85,7 @@ telemetry.addAppender(new CloudAppender({ // production ## Red lines (this topic) - Business services depend only on `ITelemetryService` — never import an appender class. -- Telemetry is layer-1 root: do not inject any business-domain service into it, and do not move it off `Core`. +- Telemetry is layer-1 root: do not inject any business-domain service into it, and do not move it off `App`. - Appenders are plain `ITelemetryAppender` objects, not DI Services — register them with `addAppender`, never via `registerScopedService`. - `track` is fire-and-forget and must not throw; appender `track` must be synchronous — buffer and send asynchronously via `flush` / `shutdown`. - Await `telemetry.shutdown()` before process exit when a buffering appender is registered. diff --git a/.agents/skills/agent-core-dev/verify.md b/.agents/skills/agent-core-dev/verify.md index 3cd01cf76..93dae3e3e 100644 --- a/.agents/skills/agent-core-dev/verify.md +++ b/.agents/skills/agent-core-dev/verify.md @@ -25,7 +25,7 @@ If the change is user-facing and ships through the CLI, generate a changeset wit Walk the stages you touched and confirm: -- **Design** — scope follows state identity; no `Map` at `Core`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around. +- **Design** — scope follows state identity; no `Map` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around. - **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior. - **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`. - **Files** — header comments describe role + scope only; registration runs from the impl file's top level; the new domain is exported from `src/index.ts`. diff --git a/packages/agent-core-v2/scripts/dep-graph.mjs b/packages/agent-core-v2/scripts/dep-graph.mjs index 927acffea..5a163f4d8 100644 --- a/packages/agent-core-v2/scripts/dep-graph.mjs +++ b/packages/agent-core-v2/scripts/dep-graph.mjs @@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SRC_ROOT = join(__dirname, '..', 'src'); -const SCOPE_OF = ['Core', 'Session', 'Agent', 'Turn']; +const SCOPE_OF = ['App', 'Session', 'Agent']; function walk(dir) { const out = [];