Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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-experimental-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Gate Tower multi-agent orchestration behind an experimental flag: it is off by default, opt in with `KIMI_CODE_EXPERIMENTAL_TOWER=1` or by setting `tower = true` in the `[experimental]` config section.
16 changes: 16 additions & 0 deletions packages/agent-core-v2/src/features/tower/flag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';

import { TOWER_FLAG_ID } from './tower';

export const TOWER_FLAG_ENV = 'KIMI_CODE_EXPERIMENTAL_TOWER';

export const towerFlag: FlagDefinitionInput = {
id: TOWER_FLAG_ID,
title: 'Tower multi-agent orchestration',
description: 'Coordinate parallel worker agents through Tower tools and the tower mode.',
env: TOWER_FLAG_ENV,
default: false,
surface: 'core',
};

registerFlagDefinition(towerFlag);
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ import '#/agent/goal/goalDeadlineSchedulerService';
export * from '#/agent/goal/goal';
export * from '#/agent/goal/goalService';
export * from '#/agent/goal/types';
export * from '#/features/tower/flag';
export * from '#/features/tower/tower';
export * from '#/features/tower/towerService';
export * from '#/features/tower/towerRateLimit';
Expand Down
105 changes: 105 additions & 0 deletions packages/agent-core-v2/test/app/flag/flagWiring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';

import { describe, expect, it } from 'vitest';

const sourceRoot = join(import.meta.dirname, '../../../src');
const packageEntry = join(sourceRoot, 'index.ts');
const flagRegistryModule = join(sourceRoot, 'app/flag/flagRegistry.ts');

// Value imports/re-exports only: type-only statements are erased at runtime and
// cannot pull in a module's side effects, so they never register a flag.
Comment thread
bj456736 marked this conversation as resolved.
Outdated
const STATIC_SPECIFIER =
/(?:^|\n)\s*(?:import\s+(?!type[\s{])[^'"]*?from\s+|import\s+|export\s+(?!type[\s{])[^'"]*?from\s+)['"]([^'"]+)['"]/g;
Comment thread
bj456736 marked this conversation as resolved.

function listTypeScriptFiles(root: string): string[] {
const result: string[] = [];
const visit = (directory: string): void => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const absolute = join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.name.endsWith('.ts')) result.push(absolute);
}
};
visit(root);
return result;
}

function resolveSpecifier(fromFile: string, specifier: string): string | undefined {
let base: string;
if (specifier.startsWith('#/')) base = join(sourceRoot, specifier.slice(2));
else if (specifier.startsWith('.')) base = resolve(dirname(fromFile), specifier);
else return undefined;
const candidates = specifier.endsWith('.js')
? [base.replace(/\.js$/, '.ts'), base.replace(/\.js$/, '.tsx')]
: [base, `${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')];
for (const candidate of candidates) {
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
}
return undefined;
}

function collectModulesReachableFromEntry(): Set<string> {
const reachable = new Set<string>();
const queue = [packageEntry];
while (queue.length > 0) {
const file = queue.pop()!;
if (reachable.has(file)) continue;
reachable.add(file);
const content = readFileSync(file, 'utf8').replaceAll(/\/\*[\s\S]*?\*\//g, ' ');
for (const match of content.matchAll(STATIC_SPECIFIER)) {
const resolved = resolveSpecifier(file, match[1]!);
if (resolved && !reachable.has(resolved)) queue.push(resolved);
}
}
return reachable;
}

const REGISTRATION_CALL = /\bregisterFlagDefinition\s*\(/;
const FLAG_ID_CONSTANT = /export\s+const\s+([A-Z0-9_]+_FLAG_ID)\s*=\s*['"]([^'"]+)['"]/g;

function findRegistrationModules(): string[] {
return listTypeScriptFiles(sourceRoot)
.filter((file) => file !== flagRegistryModule)
.filter((file) => REGISTRATION_CALL.test(readFileSync(file, 'utf8')));
}

describe('experimental flag wiring', () => {
it('loads every module that registers a flag definition from the package entry', () => {
const reachable = collectModulesReachableFromEntry();
const unwired = findRegistrationModules()
.filter((file) => !reachable.has(file))
.map((file) => relative(sourceRoot, file));
expect(
unwired,
'these modules call registerFlagDefinition but are not reachable from src/index.ts, ' +
'so their flags never get registered and every gate on them silently stays off. ' +
"Import each module for its side effects from the package entry (see how src/index.ts imports '#/…/flag').",
).toEqual([]);
});

it('registers every exported *_FLAG_ID constant', () => {
const registrationModules = findRegistrationModules();
const registrationSource = registrationModules
.map((file) => readFileSync(file, 'utf8'))
.join('\n');
const unregistered: string[] = [];
for (const file of listTypeScriptFiles(sourceRoot)) {
if (registrationModules.includes(file)) continue;
const content = readFileSync(file, 'utf8');
for (const match of content.matchAll(FLAG_ID_CONSTANT)) {
const name = match[1]!;
const id = match[2]!;
if (!new RegExp(`\\b${name}\\b`).test(registrationSource)) {
unregistered.push(`${name} ('${id}') in ${relative(sourceRoot, file)}`);
}
}
}
expect(
unregistered,
'these exported flag id constants are never referenced by a module calling registerFlagDefinition, ' +
'so gates evaluating them silently stay off. ' +
'Register each flag in a flag module wired into the package entry (see src/features/tower/flag.ts).',
).toEqual([]);
});
});
59 changes: 47 additions & 12 deletions packages/agent-core-v2/test/features/tower/towerFeature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
_clearFeatureRecipesForTests,
registerFeature,
} from '#/features/featureRegistry';
import { TOWER_FLAG_ENV } from '#/features/tower/flag';
import { TOWER_FLAG_ID } from '#/features/tower/tower';
import { ITowerRateLimitService } from '#/features/tower/towerRateLimit';
import { TowerFeature } from '#/features/tower/towerFeature';
Expand Down Expand Up @@ -116,7 +117,7 @@ describe('TowerFeature — experimental flag gating', () => {
});
});

describe('tower flag — hard-disabled (no declaration registered)', () => {
describe('tower flag — resolution', () => {
let disposables: DisposableStore;
let homeDir: string;

Expand All @@ -139,19 +140,53 @@ describe('tower flag — hard-disabled (no declaration registered)', () => {
return { config: ix.get(IConfigService), flags: ix.get(IFlagService) };
}

it('cannot be enabled by the dedicated or master env while no tower flag is registered', () => {
const { flags } = makeFlags({
KIMI_CODE_EXPERIMENTAL_TOWER: 'true',
[MASTER_ENV]: 'true',
it('is registered and disabled by default', () => {
const { flags } = makeFlags();
expect(flags.explain(TOWER_FLAG_ID)).toMatchObject({
id: TOWER_FLAG_ID,
env: TOWER_FLAG_ENV,
defaultEnabled: false,
enabled: false,
source: 'default',
});
});

it('is enabled by the dedicated env', () => {
const { flags } = makeFlags({ [TOWER_FLAG_ENV]: '1' });
expect(flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: true,
source: 'env',
});
});

it('uses config unless the dedicated env overrides it', async () => {
const configured = makeFlags();
await configured.config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: true });
expect(configured.flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: true,
source: 'config',
configValue: true,
});

const overridden = makeFlags({ [TOWER_FLAG_ENV]: 'false' });
await overridden.config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: true });
expect(overridden.flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: false,
source: 'env',
configValue: true,
});
expect(flags.explain(TOWER_FLAG_ID)).toBeUndefined();
expect(flags.enabled(TOWER_FLAG_ID)).toBe(false);
});

it('cannot be enabled through the [experimental] config section', async () => {
const { config, flags } = makeFlags();
await config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: true });
expect(flags.explain(TOWER_FLAG_ID)).toBeUndefined();
expect(flags.enabled(TOWER_FLAG_ID)).toBe(false);
it('lets the master env override the dedicated env and config', async () => {
const { config, flags } = makeFlags({
[TOWER_FLAG_ENV]: 'false',
[MASTER_ENV]: 'true',
});
await config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: false });
expect(flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: true,
source: 'master-env',
configValue: false,
});
});
});
Loading