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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Scopes

Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (string-valued, declared in `src/app/scopes.ts` — the DI kernel in `src/_base/di/scope.ts` only knows opaque `ScopeKind` strings plus the order installed by `setScopeTopology`). The `workspace/` domain owns the Workspace tier: the App-scope `workspaceLifecycle` holds the live handler registry (one handler per workspaceId, create-or-get + join, never closed), and each handler's `sessionLifecycle` owns the session lifecycle (create/resume/fork/close/delete) as its child scopes. Workspace-scope services (`workspaceSkillCatalog` / `workspaceAgentProfileLoader` / `workspaceInstructions` / `workspaceMcp` / `workspaceDirs` / `workspaceFs` / `workspaceFsWatch` / `workspaceProcess` / `workspaceGit` / `workspaceToolPolicy` / `workspaceTrust`) hold the handler-shared resources — loaded once at handler materialization, then refreshed by fs watch — and sessions consume them through session-domain seed contracts with change events (`session/mcp`, `session/workspaceInfo`, `session/sessionSkillCatalog` data, …), projected by five seed-adapter units (`src/session/sessionSeed/sessionSeedAdapters.ts`): each adapter `@ref`-observes its workspace upstream, live-reads through getters, re-fires `onDidChange` when the backing generation switches, and provides the seed token synchronously through the session scope's `ScopeOptions.assemble` hook before session services activate (a host without the workspace layer keeps the scope's default `extra` registration; the inline seeds stay plain `extra`). `workspaceMcp` is pure connection orchestration over the scope-agnostic `mcpCore` layer; the effective server set is owned by `workspaceMcpConfig` (mcp.json files + plugin contributions, fs-watch refreshed), and MCP persistence — the `[mcp]` config section plus OAuth credentials — lives in `app/mcpConfig`, the same wrapper shape as `kosongConfig` over kosong. `workspaceDirs` is backed by `.kimi-code/local.toml`; `workspaceToolPolicy` is the os-level tool veto. A session created with `CreateSessionOptions.mcpServers` additionally gets ephemeral per-session MCP servers: `workspaceMcp.sessionOverlay` builds a session-owned manager for them (never persisted, invisible to the handler's other sessions, not gated by `workspaceTrust`), the session's `ISessionMcpHandle` seed carries a `session/mcp` `MergedMcpConnectionView` over the shared manager and the overlay (an ephemeral name shadows a workspace server for that session), and `sessionLifecycle` shuts the overlay down when the session handle disposes (backstopped by the lifecycle service's own dispose for teardown paths that bypass the handle wrapper). Agent profiles follow the Contribution / Registry / Catalog extension point instead of a workspace catalog: the `workspaceAgentProfileLoader` domain owns agent-file discovery end to end (parse / roots / SYSTEM.md / explicit runtime files) and its Workspace-scope loaders (`workspace` / `user` / `plugin` / `extra` / `explicit`) contribute `AgentProfileContribution` records to the collection via `this.provide`, tagged with the handler's `workspaceId`; the App-scope `IAgentProfileRegistry` is a fold over that collection (same-(sourceId, workspaceKey) later records shadow earlier ones, provider death withdraws; the App-scope `builtinAgentProfileLoader` contributes the code-defined profiles through an owned helper unit), and each Session-scope `sessionAgentProfileCatalog` projects the registry into the merged read view directly (name-level dedup + the builtin-override rule in the projection) — its seed carries only the workspace key. `workspaceTrust` records the per-workspace trust marker (persisted under the home, keyed by `encodeWorkDirKey(root)`); while untrusted, `workspaceMcpConfig` skips the project-level MCP config files (`.mcp.json`, `.kimi-code/mcp.json`). The trust state flips through kap-server's `GET|POST /workspaces/{id}/trust` + `POST /workspaces/{id}/untrust` routes. The old App-level session-lifecycle facade and `ISessionMcpService` / `ISessionFsService` are gone — compose `sessionIndex` → `workspaceLifecycle.handlerFor` → the handler instead.
Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (string-valued, declared in `src/app/scopes.ts` — the DI kernel in `src/_base/di/scope.ts` only knows opaque `ScopeKind` strings plus the order installed by `setScopeTopology`). The `workspace/` domain owns the Workspace tier: the App-scope `workspaceLifecycle` holds the live handler registry (one handler per workspaceId, create-or-get + join, never closed), and each handler's `sessionLifecycle` owns the session lifecycle (create/resume/fork/close/delete) as its child scopes. Workspace-scope services (`workspaceSkillCatalog` / `workspaceAgentProfileLoader` / `workspaceInstructions` / `workspaceMcp` / `workspaceDirs` / `workspaceFs` / `workspaceFsWatch` / `workspaceProcess` / `workspaceGit` / `workspaceToolPolicy` / `workspaceTrust`) hold the handler-shared resources — loaded once at handler materialization, then refreshed by fs watch — and sessions consume them through session-domain seed contracts with change events (`session/mcp`, `session/workspaceInfo`, `session/sessionSkillCatalog` data, …), projected by five seed-adapter units (`src/session/sessionSeed/sessionSeedAdapters.ts`): each adapter `@ref`-observes its workspace upstream, live-reads through getters, re-fires `onDidChange` when the backing generation switches, and provides the seed token synchronously through the session scope's `ScopeOptions.configureContainer` hook before session services activate (a host without the workspace layer keeps the scope's default `extra` registration; the inline seeds stay plain `extra`). The same `configureContainer` window also fires `sessionLifecycle.onWillCreateSession` — a synchronous participation event whose surface speaks the session domain's own vocabulary (`readSeed` / `contributeSeed` / `onSessionDispose`), so Workspace-scope participants contribute session-scoped resources without the lifecycle depending on them or on kernel mechanics: `workspaceMcp` uses it to activate a session's ephemeral-server overlay (the configs travel as the `ISessionEphemeralMcpServers` session seed), contributing the merged `ISessionMcpHandle` over the adapter's workspace projection and attaching the overlay's shutdown to the session's teardown. `workspaceMcp` is pure connection orchestration over the scope-agnostic `mcpCore` layer; the effective server set is owned by `workspaceMcpConfig` (mcp.json files + plugin contributions, fs-watch refreshed), and MCP persistence — the `[mcp]` config section plus OAuth credentials — lives in `app/mcpConfig`, the same wrapper shape as `kosongConfig` over kosong. `workspaceDirs` is backed by `.kimi-code/local.toml`; `workspaceToolPolicy` is the os-level tool veto. A session created with `CreateSessionOptions.mcpServers` additionally gets ephemeral per-session MCP servers: `workspaceMcp.sessionOverlay` builds a session-owned manager for them (never persisted, invisible to the handler's other sessions, not gated by `workspaceTrust`), the session's `ISessionMcpHandle` seed carries a `session/mcp` `MergedMcpConnectionView` over the shared manager and the overlay (an ephemeral name shadows a workspace server for that session), and `sessionLifecycle` shuts the overlay down when the session handle disposes (backstopped by the lifecycle service's own dispose for teardown paths that bypass the handle wrapper). Agent profiles follow the Contribution / Registry / Catalog extension point instead of a workspace catalog: the `workspaceAgentProfileLoader` domain owns agent-file discovery end to end (parse / roots / SYSTEM.md / explicit runtime files) and its Workspace-scope loaders (`workspace` / `user` / `plugin` / `extra` / `explicit`) contribute `AgentProfileContribution` records to the collection via `this.provide`, tagged with the handler's `workspaceId`; the App-scope `IAgentProfileRegistry` is a fold over that collection (same-(sourceId, workspaceKey) later records shadow earlier ones, provider death withdraws; the App-scope `builtinAgentProfileLoader` contributes the code-defined profiles through an owned helper unit), and each Session-scope `sessionAgentProfileCatalog` projects the registry into the merged read view directly (name-level dedup + the builtin-override rule in the projection) — its seed carries only the workspace key. `workspaceTrust` records the per-workspace trust marker (persisted under the home, keyed by `encodeWorkDirKey(root)`); while untrusted, `workspaceMcpConfig` skips the project-level MCP config files (`.mcp.json`, `.kimi-code/mcp.json`). The trust state flips through kap-server's `GET|POST /workspaces/{id}/trust` + `POST /workspaces/{id}/untrust` routes. The old App-level session-lifecycle facade and `ISessionMcpService` / `ISessionFsService` are gone — compose `sessionIndex` → `workspaceLifecycle.handlerFor` → the handler instead.

## Units and contribution points (L3)

Expand All @@ -13,7 +13,7 @@ The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registr
- `service.ts` — `Service`: the unit base class (extends `Disposable`). Capabilities live on `this` (`provide` / `effect` / `on` / `get` / `ref`, plus `name` / `state` / `config`). Two-phase construction: inside the ctor `provide`/`on`/`effect` buffer (writes only — `get`/`ref` throw, dependencies are constructor parameters); the kernel binds the runtime after `Reflect.construct` and flushes in writing order; a manually `new`ed instance throws on every capability call. Services whose own members collide with the `Service` vocabulary keep `extends Disposable` with a NOTE comment — still full DI units (cascade/ledger do not require `Service`).
- `fiber.ts` — the `Fiber` capability interface (not a DI token), `FiberHandle` (thenable / `state` / `uid` / `update` / `dispose`), `ServiceRecipe` (class / arrow function / `{apply}`), the `FiberState` five-state machine, and `ScopeUnits(kind)` — the materialization collection token, one per scope kind.
- `collection.ts` — `collection<T>(name)` contribution tokens. Contribute with `this.provide(token, value)`; a fold declares the token as a constructor parameter and receives a `CollectionView<T>` (`items` / `records` / incremental `onDidChange`). Records are visible to the provider's ancestors and descendants (never sibling subtrees); provider death withdraws. Collection edges enter the graph for introspection but never join a cascade contagion set.
- `scopeUnits.ts` — the kernel fold: every scope-creation point (`createScopedChildHandle` / `Scope.createApp` / `Scope.createChild`) runs `watchScopeUnits(container, kind)` before eager activation, materializing each visible `ScopeUnits(kind)` record's recipe as a unit inside the new scope (disposal hangs on the record provider's book — provider death tears the materialized units down across the tree). `ScopeOptions.assemble` runs at the same point (the session seed adapters use it).
- `scopeUnits.ts` — the kernel fold: every scope-creation point (`createScopedChildHandle` / `Scope.createApp` / `Scope.createChild`) runs `watchScopeUnits(container, kind)` before eager activation, materializing each visible `ScopeUnits(kind)` record's recipe as a unit inside the new scope (disposal hangs on the record provider's book — provider death tears the materialized units down across the tree). `ScopeOptions.configureContainer` runs at the same point (the session seed adapters use it).
- `instantiation.ts` — the `@ref(IX)` decorator factory (`LiveRef<T>`: `current` live read + `onDidChange` availability event; observation creates no binding and no graph edge) and `ScopeActivation`.
- `src/app/feature/` — `IFeatureManager` (App scope): runtime unit assembly (`provideUnit` / `unprovideUnit` / `updateUnit`) and introspection (`units()` / `onDidChangeUnits`); managed units hang on the manager's own book. External package management stays with `IPluginService`. The `features` assembly (`src/features/featureAssemblyService.ts`) drains the module-level feature table through it.

Expand Down
22 changes: 11 additions & 11 deletions packages/agent-core-v2/src/_base/di/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import { BugIndicatingError } from '../errors/errors';
import { SyncDescriptor } from './descriptors';
import { ScopeActivation, type ProvideAllEntry } from './instantiation';

Check warning on line 14 in packages/agent-core-v2/src/_base/di/scope.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-import(no-duplicates)

Module './instantiation' is imported more than once in this file
import type { ServiceIdentifier, ServicesAccessor, IInstantiationService } from './instantiation';
import { InstantiationService } from './instantiationService';
import { DisposableStore, type IDisposable } from './lifecycle';
Expand Down Expand Up @@ -84,8 +84,8 @@

export interface ScopeOptions {
readonly id?: string;
readonly extra?: ScopeSeed;
readonly assemble?: (container: InstantiationService) => void;
readonly seeds?: ScopeSeed;
readonly configureContainer?: (container: InstantiationService) => void;
}

export interface IScopeHandle<K extends ScopeKind = ScopeKind> {
Expand All @@ -100,10 +100,10 @@
export type ISessionScopeHandle = IScopeHandle<'session'>;
export type IAgentScopeHandle = IScopeHandle<'agent'>;

function buildCollection(extra?: ScopeSeed): ServiceCollection {
function buildCollection(seeds?: ScopeSeed): ServiceCollection {
const collection = new ServiceCollection();
if (extra) {
for (const [id, value] of extra) {
if (seeds) {
for (const [id, value] of seeds) {
collection.set(id, value);
}
}
Expand Down Expand Up @@ -137,12 +137,12 @@
id: string,
options: ScopeOptions = {},
): IScopeHandle {
const collection = buildCollection(options.extra);
const collection = buildCollection(options.seeds);
const child = parent.createChild(collection);
(child as InstantiationService).debugLabel = id;
try {
watchScopeUnits(child as InstantiationService, kind);
options.assemble?.(child as InstantiationService);
options.configureContainer?.(child as InstantiationService);
provideScopeServices(child, kind, collection);
} catch (error) {
child.dispose();
Expand Down Expand Up @@ -189,12 +189,12 @@

static createApp(options: ScopeOptions = {}): Scope {
const kind: ScopeKind = 'app';
const collection = buildCollection(options.extra);
const collection = buildCollection(options.seeds);
const instantiation = new InstantiationService(collection, true);
instantiation.debugLabel = options.id ?? 'app';
try {
watchScopeUnits(instantiation, kind);
options.assemble?.(instantiation);
options.configureContainer?.(instantiation);
provideScopeServices(instantiation, kind, collection);
} catch (error) {
instantiation.dispose();
Expand Down Expand Up @@ -223,12 +223,12 @@
if (this.children.has(id)) {
throw new Error(`Scope '${this.id}' already has a child with id '${id}'`);
}
const collection = buildCollection(options.extra);
const collection = buildCollection(options.seeds);
const childInstantiation = this.instantiation.createChild(collection);
(childInstantiation as InstantiationService).debugLabel = id;
try {
watchScopeUnits(childInstantiation as InstantiationService, kind);
options.assemble?.(childInstantiation as InstantiationService);
options.configureContainer?.(childInstantiation as InstantiationService);
provideScopeServices(childInstantiation, kind, collection);
} catch (error) {
childInstantiation.dispose();
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/_base/di/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
ServiceRegistration,
} from './testInstantiationService';

import { type ServiceIdentifier } from './instantiation';

Check warning on line 15 in packages/agent-core-v2/src/_base/di/test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-import(first)

Import statements must come first
import { createAppScope, Scope, type ScopeKind, type ScopeSeed } from './scope';

Check warning on line 16 in packages/agent-core-v2/src/_base/di/test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-import(first)

Import statements must come first

export interface ScopedTestHost {
readonly app: Scope;
Expand All @@ -23,14 +23,14 @@
}

export function createScopedTestHost(appStubs: ScopeSeed = []): ScopedTestHost {
const app = createAppScope({ extra: appStubs });
const app = createAppScope({ seeds: appStubs });
return {
app,
child(kind, id, stubs = []) {
return app.createChild(kind, id, { extra: stubs });
return app.createChild(kind, id, { seeds: stubs });
},
childOf(parent, kind, id, stubs = []) {
return parent.createChild(kind, id, { extra: stubs });
return parent.createChild(kind, id, { seeds: stubs });
},
dispose() {
app.dispose();
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/bootstrap/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export interface BootstrapResult {
export function bootstrap(input: BootstrapInput, extraSeeds: ScopeSeed = []): BootstrapResult {
const options = resolveBootstrapOptions(input);
const app = createAppScope({
extra: [...bootstrapSeed(input), ...storageSeed(options), ...skillSeed(), ...extraSeeds],
seeds: [...bootstrapSeed(input), ...storageSeed(options), ...skillSeed(), ...extraSeeds],
});
return { app };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class WorkspaceLifecycleService extends Service implements IWorkspaceLife
this.instantiation,
LifecycleScope.Workspace,
workspaceId,
{ extra: workspaceContextSeed(ctx) },
{ seeds: workspaceContextSeed(ctx) },
) as IWorkspaceScopeHandle;
this.live.set(workspaceId, handle);
this._onDidMaterializeHandler.fire(handle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
LifecycleScope.Agent,
agentId,
{
extra: [
seeds: [
[IAgentScopeContext, makeAgentScopeContext({ agentId, agentScope })],
[ITelemetryService, this.telemetry.withContext({ agent_id: agentId })],
],
Expand Down
28 changes: 28 additions & 0 deletions packages/agent-core-v2/src/session/mcp/ephemeralMcpServers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* `mcp` domain — seeded ephemeral per-session MCP server configs.
*
* Defines `ISessionEphemeralMcpServers`, the pure-data injection contract
* carrying the session's ephemeral (caller-injected, never persisted) MCP
* server configs, copied verbatim from the session's creation options
* (`CreateSessionOptions.mcpServers` / `ResumeSessionOptions.mcpServers`).
* Always seeded into the Session scope by the session lifecycle (an empty
* record for ordinary sessions), so consumers can resolve it
* unconditionally. The contract carries no IO of its own — connecting the
* servers and projecting the resulting session handle is the
* Workspace-side MCP domain's concern, activated through the session
* lifecycle's `onWillCreateSession` event. Session-scoped.
*/

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ScopeSeed } from '#/_base/di/scope';
import type { McpServerConfig } from '#/mcpCore/config-schema';

export const ISessionEphemeralMcpServers: ServiceIdentifier<
Readonly<Record<string, McpServerConfig>>
> = createDecorator<Readonly<Record<string, McpServerConfig>>>('sessionEphemeralMcpServers');

export function sessionEphemeralMcpServersSeed(
servers: Readonly<Record<string, McpServerConfig>>,
): ScopeSeed {
return [[ISessionEphemeralMcpServers as ServiceIdentifier<unknown>, servers]];
}
Loading
Loading