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
6 changes: 6 additions & 0 deletions .changeset/log-enabled-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
---

Log enabled experimental flags at startup.
18 changes: 14 additions & 4 deletions packages/agent-core/src/flags/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ export const MASTER_ENV = 'KIMI_CODE_EXPERIMENTAL_FLAG';
* L3 registry default
*/
export class FlagResolver {
private readonly env: Readonly<Record<string, string | undefined>>;
private readonly byId: ReadonlyMap<string, FlagDefinitionInput>;

constructor(
env: Readonly<Record<string, string | undefined>> = process.env,
definitions: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS,
private readonly env: Readonly<Record<string, string | undefined>> = process.env,
private readonly definitions: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS,
) {
this.env = env;
this.byId = new Map(definitions.map((def) => [def.id, def]));
}

Expand All @@ -36,6 +34,18 @@ export class FlagResolver {
if (override !== undefined) return override;
return def.default; // L3 default
}

snapshot(): Record<string, boolean> {
return Object.fromEntries(
this.definitions.map((def) => [def.id, this.enabled(def.id as FlagId)]),
);
}

enabledIds(): readonly FlagId[] {
return this.definitions
.filter((def) => this.enabled(def.id as FlagId))
.map((def) => def.id as FlagId);
}
}

/**
Expand Down
7 changes: 2 additions & 5 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ import {
type MoonshotServiceConfig,
} from '../config';
import {
FLAG_DEFINITIONS,
flags,
type ExperimentalFlagMap,
type FlagDefinitionInput,
type FlagId,
} from '../flags';
import type { Logger } from '../logging/types';
import { resolveSessionMcpConfig, type SessionMcpConfig } from '../mcp';
Expand Down Expand Up @@ -169,6 +166,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
this.pluginsReady = this.plugins.load().catch((error: unknown) => {
this.pluginsLoadError = error instanceof Error ? error : new Error(String(error));
});
log.info('experimental flags enabled', { flags: flags.enabledIds() });

this.sdk = rpcClient(this);
}
Expand Down Expand Up @@ -259,8 +257,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
}

getExperimentalFlags(): ExperimentalFlagMap {
const defs: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS;
return Object.fromEntries(defs.map((def) => [def.id, flags.enabled(def.id as FlagId)]));
return flags.snapshot();
}

async closeSession({ sessionId }: CloseSessionPayload): Promise<void> {
Expand Down
11 changes: 10 additions & 1 deletion packages/agent-core/test/flags/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('FlagResolver', () => {
expect(enabled('b-off-default')).toBe(true);
});

it('L1 master switch beats an L2 per-feature off (D2)', () => {
it('L1 master switch beats an L2 per-feature off', () => {
const enabled = make({ [MASTER_ENV]: '1', KIMI_CODE_EXPERIMENTAL_A: '0' });
expect(enabled('a-on-default')).toBe(true);
});
Expand All @@ -73,6 +73,15 @@ describe('FlagResolver', () => {
expect(enabled('b-off-default')).toBe(false);
});

it('returns a full snapshot and enabled ids in registry order', () => {
const resolver = new FlagResolver({ KIMI_CODE_EXPERIMENTAL_B: '1' }, DEFS);
expect(resolver.snapshot()).toEqual({
'a-on-default': true,
'b-off-default': true,
});
expect(resolver.enabledIds()).toEqual(['a-on-default', 'b-off-default']);
});

it('reads the env name declared in the registry (the declared name works, others do not)', () => {
expect(make({ KIMI_CODE_EXPERIMENTAL_B: '1' })('b-off-default')).toBe(true);
// The name mechanically derived from the id must not take effect (env is explicitly ..._B).
Expand Down
41 changes: 40 additions & 1 deletion packages/agent-core/test/harness/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'pathe';

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

import {
FLAG_DEFINITIONS,
MASTER_ENV,
createRPC,
KimiCore,
type ApprovalResponse,
type CoreAPI,
type SDKAPI,
} from '../../src';
import {
__resetRootLoggerForTest,
getRootLogger,
resolveGlobalLogPath,
} from '../../src/logging/logger';
import { resolveLoggingConfig } from '../../src/logging/resolve-config';
import type { OAuthTokenProviderResolver } from '../../src/session/provider-manager';

function requiredFlagEnv(id: string): string {
const def = FLAG_DEFINITIONS.find((item) => item.id === id);
if (def === undefined) throw new Error(`Missing flag definition: ${id}`);
return def.env;
}

describe('KimiCore runtime config', () => {
let tmp: string;

afterEach(async () => {
if (tmp !== undefined) {
await rm(tmp, { recursive: true, force: true });
}
await __resetRootLoggerForTest();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});

it('logs all enabled experimental flags once on core startup', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
await mkdir(homeDir, { recursive: true });
await getRootLogger().configure(resolveLoggingConfig({ homeDir }));

vi.stubEnv(MASTER_ENV, '0');
for (const def of FLAG_DEFINITIONS) {
vi.stubEnv(def.env, '0');
}
vi.stubEnv(requiredFlagEnv('goal-command'), '1');
vi.stubEnv(requiredFlagEnv('background-ask'), '1');

void new KimiCore(async () => ({}) as never, { homeDir });
await getRootLogger().flushGlobal();

const text = await readFile(resolveGlobalLogPath(homeDir), 'utf-8');
expect(text).toContain('experimental flags enabled');
expect(text).toContain('goal-command');
expect(text).toContain('background-ask');
expect(text.match(/experimental flags enabled/g)).toHaveLength(1);
});

it('uses the shared OAuth resolver for Moonshot service tokens', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
Expand Down
Loading