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
5 changes: 5 additions & 0 deletions .changeset/tower-config-experimental-assembly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix tower mode not turning on when enabled through the [experimental] section in config.toml.
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/features/featureAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio

export interface IFeatureAssemblyService {
readonly _serviceBrand: undefined;
readonly ready: Promise<void>;
}

export const IFeatureAssemblyService: ServiceIdentifier<IFeatureAssemblyService> =
Expand Down
30 changes: 26 additions & 4 deletions packages/agent-core-v2/src/features/featureAssemblyService.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import type { ServiceClassRecipe } from '#/_base/di/fiber';
import { IConfigService } from '#/app/config/config';
import { IFeatureManager } from '#/app/feature/featureManager';
import { IFlagService } from '#/app/flag/flag';
import type { FlagId } from '#/app/flag/flagRegistry';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { Service } from '#/_base/di/service';

import { IFeatureAssemblyService } from './featureAssembly';
import { getFeatureRecipes } from './featureRegistry';
import { getFeatureRegistrations } from './featureRegistry';

export class FeatureAssemblyService extends Service implements IFeatureAssemblyService {
declare readonly _serviceBrand: undefined;
readonly ready: Promise<void>;

constructor(@IFeatureManager featureManager: IFeatureManager) {
constructor(
@IFeatureManager featureManager: IFeatureManager,
@IConfigService config: IConfigService,
@IFlagService flags: IFlagService,
) {
super();
for (const recipe of getFeatureRecipes()) {
featureManager.provideUnit(recipe);
const gated: { recipe: ServiceClassRecipe; flag: FlagId }[] = [];
for (const { recipe, flag } of getFeatureRegistrations()) {
if (flag === undefined) {
featureManager.provideUnit(recipe);
} else {
gated.push({ recipe, flag });
}
}
this.ready = config.ready.then(() => {
for (const { recipe, flag } of gated) {
if (flags.enabled(flag)) {
featureManager.provideUnit(recipe);
}
}
});
void this.ready.catch(() => {});
}
}

Expand Down
21 changes: 15 additions & 6 deletions packages/agent-core-v2/src/features/featureRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import type { ServiceClassRecipe } from '#/_base/di/fiber';
import type { FlagId } from '#/app/flag/flagRegistry';

const _featureRecipes: ServiceClassRecipe[] = [];
export interface FeatureRegistration {
readonly recipe: ServiceClassRecipe;
readonly flag?: FlagId;
}

const _featureRegistrations: FeatureRegistration[] = [];

export function registerFeature(recipe: ServiceClassRecipe): void {
_featureRecipes.push(recipe);
export function registerFeature(
recipe: ServiceClassRecipe,
options: { readonly flag?: FlagId } = {},
): void {
_featureRegistrations.push({ recipe, flag: options.flag });
}

export function getFeatureRecipes(): readonly ServiceClassRecipe[] {
return _featureRecipes;
export function getFeatureRegistrations(): readonly FeatureRegistration[] {
return _featureRegistrations;
}

export function _clearFeatureRecipesForTests(): void {
_featureRecipes.length = 0;
_featureRegistrations.length = 0;
}
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/features/tower/towerFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ export function _setTowerFeatureAssembledForTests(value: boolean | undefined): v
assembledOverrideForTests = value;
}

registerFeature(TowerFeature);
registerFeature(TowerFeature, { flag: TOWER_FLAG_ID });
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export class WorkspaceInstanceManager implements IWorkspaceInstanceManager {

private async materialize(workspace: Workspace): Promise<WorkspaceInstance> {
await this.environment.ready;
await this.config.ready;
const runtimes = new RuntimeRegistry(workspace.id);
const unitHost = this.unitHostFactory.create(this.instantiation, runtimes);
const instance = new WorkspaceInstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
registerScopedService,
} from '#/_base/di/scope';
import { createScopedTestHost } from '#/_base/di/test';
import { IConfigService } from '#/app/config/config';
import { IFeatureManager } from '#/app/feature/featureManager';
import { FeatureManagerService } from '#/app/feature/featureManagerService';
import { IFlagService } from '#/app/flag/flag';
import { LifecycleScope } from '#/app/scopes';
import { IFeatureAssemblyService } from '#/features/featureAssembly';
import { FeatureAssemblyService } from '#/features/featureAssemblyService';
Expand All @@ -18,8 +20,14 @@ import {
} from '#/features/featureRegistry';

import { IDebugEventsService } from '#/features/debugEvents/debugEvents';
import { stubFlag } from '../../app/flag/stubs';
import { DebugEventsFeature } from '#/features/debugEvents/debugEventsFeature';

const featureAssemblySeeds = [
[IConfigService, { ready: Promise.resolve() }],
[IFlagService, stubFlag(false)],
] as const;

describe('DebugEventsFeature — App-scope introspection service', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
Expand Down Expand Up @@ -48,7 +56,7 @@ describe('DebugEventsFeature — App-scope introspection service', () => {
),
).toBe(false);

const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);
expect(manager.units().map((unit) => unit.name)).toContain('debugEvents');
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IConfigService } from '#/app/config/config';
import { IFeatureManager } from '#/app/feature/featureManager';
import { FeatureManagerService } from '#/app/feature/featureManagerService';
import { IFlagService } from '#/app/flag/flag';
import { IPluginService } from '#/app/plugin/plugin';
import { LifecycleScope } from '#/app/scopes';
import { IFeatureAssemblyService } from '#/features/featureAssembly';
Expand All @@ -23,6 +24,7 @@ import { ISessionExternalHooksService } from '#/features/externalHooks/session/s
import { IHostProcessService } from '#/os/interface/hostProcess';

import { stubBootstrap } from '../../app/bootstrap/stubs';
import { stubFlag } from '../../app/flag/stubs';

function collectionViewOf<T>(scope: Scope, token: CollectionToken<T>): CollectionView<T> {
return (scope.instantiation as InstantiationService).fiberHost.collectionView(token);
Expand Down Expand Up @@ -59,6 +61,7 @@ describe('ExternalHooksFeature — assembly (src/features/externalHooks)', () =>
{ _serviceBrand: undefined, enabledHooks: async () => [], onDidReload: Event.None },
],
[IHostProcessService, { _serviceBrand: undefined }],
[IFlagService, stubFlag(false)],
]);
const manager = host.app.accessor.get(IFeatureManager);
expect(manager.units().map((unit) => unit.name)).toContain('externalHooks');
Expand Down
21 changes: 15 additions & 6 deletions packages/agent-core-v2/test/features/feature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import {
import { Service } from '#/_base/di/service';
import { createScopedTestHost } from '#/_base/di/test';
import { AgentProfileContribution } from '#/app/agentProfileCatalog/agentProfileContribution';
import { IConfigService } from '#/app/config/config';
import { ConfigSectionContribution } from '#/app/config/configSectionContributions';
import { IFeatureManager } from '#/app/feature/featureManager';
import { FeatureManagerService } from '#/app/feature/featureManagerService';
import { IFlagService } from '#/app/flag/flag';
import { LifecycleScope } from '#/app/scopes';
import { AgentToolContribution } from '#/agent/toolRegistry/toolContribution';
import { Feature } from '#/features/feature';
Expand All @@ -38,6 +40,13 @@ import {
} from '#/features/featureRegistry';
import type { AgentTool, ToolExecution } from '#/tool/toolContract';

import { stubFlag } from '../app/flag/stubs';

const featureAssemblySeeds = [
[IConfigService, { ready: Promise.resolve() }],
[IFlagService, stubFlag(false)],
] as const;

interface IGreeter {
readonly _serviceBrand: undefined;
greet(): string;
Expand Down Expand Up @@ -115,7 +124,7 @@ describe('Feature — built-in capability assembly (src/features)', () => {
}
registerFeature(TestFeature);

const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);
expect(manager.units()).toHaveLength(1);
expect(manager.units()[0]!.name).toBe('test-feature');
Expand Down Expand Up @@ -185,7 +194,7 @@ describe('Feature — built-in capability assembly (src/features)', () => {
}
registerFeature(DomainFeature);

const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);
const views = [
collectionViewOf(host.app, SessionModelContribution),
Expand Down Expand Up @@ -225,7 +234,7 @@ describe('Feature — built-in capability assembly (src/features)', () => {
}
registerFeature(FirstFeature);

const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);
const agent = host.child(LifecycleScope.Agent, 'agent-1');
const original = agent.accessor.get(IGreeter);
Expand Down Expand Up @@ -261,8 +270,8 @@ describe('Feature — built-in capability assembly (src/features)', () => {
}
registerFeature(SharedFeature);

const first = createScopedTestHost();
const second = createScopedTestHost();
const first = createScopedTestHost(featureAssemblySeeds);
const second = createScopedTestHost(featureAssemblySeeds);
const firstManager = first.app.accessor.get(IFeatureManager);
const secondManager = second.app.accessor.get(IFeatureManager);
const firstAgent = first.child(LifecycleScope.Agent, 'agent-1');
Expand Down Expand Up @@ -301,7 +310,7 @@ describe('Feature — built-in capability assembly (src/features)', () => {
}
registerFeature(SoloFeature);

const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const agent = host.child(LifecycleScope.Agent, 'agent-1');
expect(agent.accessor.get(IGreeter).greet()).toBe('hi');
host.dispose();
Expand Down
11 changes: 9 additions & 2 deletions packages/agent-core-v2/test/features/goal/goalFeature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ import { GoalFeature } from '#/features/goal/goalFeature';
import { ISessionUsageService } from '#/session/usage/sessionUsage';
import { IEventDispatcher } from '#/state/eventDispatcher';

import { stubFlag } from '../../app/flag/stubs';

const featureAssemblySeeds = [
[IConfigService, { ready: Promise.resolve() }],
[IFlagService, stubFlag(false)],
] as const;

describe('GoalFeature', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
Expand All @@ -53,14 +60,14 @@ describe('GoalFeature', () => {
});

it('assembles a named, introspectable goal unit', () => {
const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);
expect(manager.units().map((unit) => unit.name)).toContain('goal');
host.dispose();
});

it('retracts the goal runtime contribution with the Feature', async () => {
const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const manager = host.app.accessor.get(IFeatureManager);

await manager.unprovideUnit('goal');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
} from '#/_base/di/scope';
import { createScopedTestHost, stubPair } from '#/_base/di/test';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IConfigService } from '#/app/config/config';
import { IFeatureManager } from '#/app/feature/featureManager';
import { FeatureManagerService } from '#/app/feature/featureManagerService';
import { IFlagService } from '#/app/flag/flag';
import { LifecycleScope } from '#/app/scopes';
import { SessionInitFeature } from '#/features/sessionInit/sessionInitFeature';
import { ISessionInitService } from '#/features/sessionInit/sessionInit';
Expand All @@ -24,6 +26,13 @@ import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { ISessionSubagentService } from '#/session/subagent/subagent';

import { stubFlag } from '../../app/flag/stubs';

const featureAssemblySeeds = [
[IConfigService, { ready: Promise.resolve() }],
[IFlagService, stubFlag(false)],
] as const;

describe('SessionInitFeature', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
Expand All @@ -46,7 +55,7 @@ describe('SessionInitFeature', () => {
});

it('withdraws and restores the Session service with the Feature', async () => {
const host = createScopedTestHost();
const host = createScopedTestHost(featureAssemblySeeds);
const session = host.child(LifecycleScope.Session, 'session-1', [
stubPair(IAgentLifecycleService, {} as IAgentLifecycleService),
stubPair(ISessionSubagentService, {} as ISessionSubagentService),
Expand Down
Loading
Loading