From 96bbc471c4aca9526e4dcfe00e6bad2b653bbe66 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 29 May 2026 19:55:10 +0800 Subject: [PATCH] feat: add experimental feature-flag system (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a central, env-driven flag registry in agent-core. Each flag is declared once with an id, full env var name, default, and surface. Within agent-core, flags are consulted through a process-global 'flags' constant that reads live process.env. Resolution precedence: master switch KIMI_CODE_EXPERIMENTAL_FLAG > per-feature KIMI_CODE_EXPERIMENTAL_ > registry default, with lenient boolean parsing via parseBooleanEnv. FlagId is a literal union derived from the registry for compile-time autocomplete and typo-checking. SDK boundary: KimiHarness.getExperimentalFlags() returns the resolved values over RPC, and the SDK re-exports only the flag *types* — no runtime value crosses the boundary. The TUI caches that snapshot once at startup and reads it synchronously for command gating. Gate the plugin system behind the 'plugins' flag, off by default: PluginManager.load() consults flags.enabled('plugins'), so when off no installed plugins are loaded or activated, and the TUI /plugins command is hidden from the palette and resolves as an unknown command. Tests cover the resolver precedence matrix, registry invariants, the FlagId type guard, the live-env singleton, the plugin-load gate, the getExperimentalFlags RPC, and the TUI command gating. --- .changeset/experimental-flags.md | 5 + AGENTS.md | 4 + .../src/tui/commands/experimental-flags.ts | 15 +++ apps/kimi-code/src/tui/commands/index.ts | 1 + apps/kimi-code/src/tui/commands/resolve.ts | 13 ++- apps/kimi-code/src/tui/commands/types.ts | 3 + apps/kimi-code/src/tui/kimi-tui.ts | 8 +- .../test/tui/commands/resolve.test.ts | 1 + .../test/tui/kimi-tui-message-flow.test.ts | 1 + .../test/tui/kimi-tui-startup.test.ts | 1 + packages/agent-core/src/flags/index.ts | 3 + packages/agent-core/src/flags/registry.ts | 16 +++ packages/agent-core/src/flags/resolver.ts | 65 +++++++++++ packages/agent-core/src/flags/types.ts | 19 +++ packages/agent-core/src/index.ts | 1 + packages/agent-core/src/rpc/core-api.ts | 2 + packages/agent-core/src/rpc/core-impl.ts | 12 ++ .../agent-core/test/flags/resolver.test.ts | 108 ++++++++++++++++++ packages/node-sdk/src/index.ts | 10 ++ packages/node-sdk/src/kimi-harness.ts | 6 + packages/node-sdk/src/rpc.ts | 6 + 21 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 .changeset/experimental-flags.md create mode 100644 apps/kimi-code/src/tui/commands/experimental-flags.ts create mode 100644 packages/agent-core/src/flags/index.ts create mode 100644 packages/agent-core/src/flags/registry.ts create mode 100644 packages/agent-core/src/flags/resolver.ts create mode 100644 packages/agent-core/src/flags/types.ts create mode 100644 packages/agent-core/test/flags/resolver.test.ts diff --git a/.changeset/experimental-flags.md b/.changeset/experimental-flags.md new file mode 100644 index 000000000..96de96dc6 --- /dev/null +++ b/.changeset/experimental-flags.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/agent-core": minor +--- + +Add an experimental feature-flag system: a central registry (`flags/registry.ts`) plus an env-driven resolver. Gate a feature with `flags.enabled('id')`, toggled via `KIMI_CODE_EXPERIMENTAL_` or the `KIMI_CODE_EXPERIMENTAL_FLAG` master switch. No flags are defined yet. diff --git a/AGENTS.md b/AGENTS.md index 3c344585c..994f95125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,10 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo - When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug. - Do not sacrifice code quality for external compatibility unless the user explicitly asks for it. Breaking changes go through changesets and a `major` bump, gated by the rule below. +## Experimental Features + +- Gate a not-yet-public feature behind an experimental flag. Add the flag to the registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`. + ## Where to Update Instructions - Hard rules that affect almost every task: update the root `AGENTS.md`. diff --git a/apps/kimi-code/src/tui/commands/experimental-flags.ts b/apps/kimi-code/src/tui/commands/experimental-flags.ts new file mode 100644 index 000000000..e1231742c --- /dev/null +++ b/apps/kimi-code/src/tui/commands/experimental-flags.ts @@ -0,0 +1,15 @@ +import type { ExperimentalFlagMap } from '@moonshot-ai/kimi-code-sdk'; + +// Resolved experimental flags, fetched once from the core over RPC at startup and then read +// synchronously by the command palette and dispatch. App-local cache, not a source of truth. +let snapshot: ExperimentalFlagMap = {}; + +/** Replace the cached flag snapshot. Call once after fetching via `harness.getExperimentalFlags()`. */ +export function setExperimentalFlags(flags: ExperimentalFlagMap): void { + snapshot = flags; +} + +/** An `undefined` flag means "not gated" → always enabled, so callers can pass an optional flag id. */ +export function isExperimentalFlagEnabled(flag: string | undefined): boolean { + return flag === undefined || snapshot[flag] === true; +} diff --git a/apps/kimi-code/src/tui/commands/index.ts b/apps/kimi-code/src/tui/commands/index.ts index 2ee577850..60178b265 100644 --- a/apps/kimi-code/src/tui/commands/index.ts +++ b/apps/kimi-code/src/tui/commands/index.ts @@ -1,3 +1,4 @@ +export * from './experimental-flags'; export * from './parse'; export * from './registry'; export * from './resolve'; diff --git a/apps/kimi-code/src/tui/commands/resolve.ts b/apps/kimi-code/src/tui/commands/resolve.ts index 2d0807bf4..a47f11409 100644 --- a/apps/kimi-code/src/tui/commands/resolve.ts +++ b/apps/kimi-code/src/tui/commands/resolve.ts @@ -4,8 +4,13 @@ import { type BuiltinSlashCommand, type BuiltinSlashCommandName, } from './registry'; +import { isExperimentalFlagEnabled } from './experimental-flags'; import { parseSlashInput } from './parse'; -import type { SlashCommandBusyReason, SlashCommandInvalidReason } from './types'; +import type { + KimiSlashCommand, + SlashCommandBusyReason, + SlashCommandInvalidReason, +} from './types'; export type SlashCommandIntent = | { readonly kind: 'not-command' } @@ -45,7 +50,11 @@ export function resolveSlashCommandInput(options: ResolveSlashCommandInput): Sla if (parsed === null) return { kind: 'not-command' }; const command = findBuiltInSlashCommand(parsed.name); - if (command !== undefined) { + // `command` is a literal union where only some members carry `experimentalFlag`; widen to read it. + if ( + command !== undefined && + isExperimentalFlagEnabled((command as KimiSlashCommand).experimentalFlag) + ) { const busyReason = slashCommandBusyReason(options); if ( busyReason !== undefined && diff --git a/apps/kimi-code/src/tui/commands/types.ts b/apps/kimi-code/src/tui/commands/types.ts index cb784f84d..532a301ea 100644 --- a/apps/kimi-code/src/tui/commands/types.ts +++ b/apps/kimi-code/src/tui/commands/types.ts @@ -1,4 +1,5 @@ import type { SlashCommand } from '@earendil-works/pi-tui'; +import type { FlagId } from '@moonshot-ai/kimi-code-sdk'; export type SlashCommandAvailability = 'always' | 'idle-only'; @@ -8,6 +9,8 @@ export interface KimiSlashCommand extends SlashCom readonly description: string; readonly priority?: number; readonly availability?: SlashCommandAvailability | ((args: string) => SlashCommandAvailability); + /** When set, the command is hidden from the palette and blocked unless this flag is enabled. */ + readonly experimentalFlag?: FlagId; } export interface ParsedSlashInput { diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 79622dfd5..db5bf7b8a 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -34,6 +34,8 @@ import { detectFdPath } from '#/utils/process/fd-detect'; import { BUILTIN_SLASH_COMMANDS, buildSkillSlashCommands, + isExperimentalFlagEnabled, + setExperimentalFlags, sortSlashCommands, type KimiSlashCommand, type SkillListSession, @@ -287,7 +289,10 @@ export class KimiTUI { // ========================================================================= private getSlashCommands(): readonly KimiSlashCommand[] { - return [...sortSlashCommands(BUILTIN_SLASH_COMMANDS), ...this.skillCommands]; + const builtins = sortSlashCommands(BUILTIN_SLASH_COMMANDS).filter((command) => + isExperimentalFlagEnabled(command.experimentalFlag), + ); + return [...builtins, ...this.skillCommands]; } private setupAutocomplete(): void { @@ -380,6 +385,7 @@ export class KimiTUI { // Mount only after init() succeeds; see mountFooter(). this.mountFooter(); this.renderWelcome(); + setExperimentalFlags(await this.harness.getExperimentalFlags()); this.setupAutocomplete(); void this.loadPersistedInputHistory(); this.state.editorContainer.clear(); diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index d151bad80..07381c0bd 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -131,6 +131,7 @@ describe('resolveSlashCommandInput', () => { input: '/does-not-exist arg', }); }); + }); describe('slash command busy helpers', () => { diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 40ab4e651..ce198f283 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -200,6 +200,7 @@ function makeHarness(session = makeSession(), overrides: Record track: vi.fn(), setTelemetryContext: vi.fn(), interactiveAgentId: 'main', + getExperimentalFlags: vi.fn(async () => ({})), auth: { status: vi.fn(), login: vi.fn(), diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index 570f7efe4..de33c6abd 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -132,6 +132,7 @@ function makeHarness(session = makeSession(), overrides: Record close: vi.fn(async () => {}), track: vi.fn(), setTelemetryContext: vi.fn(), + getExperimentalFlags: vi.fn(async () => ({})), auth: { status: vi.fn(async () => ({ providers: [] })), login: vi.fn(async () => {}), diff --git a/packages/agent-core/src/flags/index.ts b/packages/agent-core/src/flags/index.ts new file mode 100644 index 000000000..e37a8178e --- /dev/null +++ b/packages/agent-core/src/flags/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './registry'; +export * from './resolver'; diff --git a/packages/agent-core/src/flags/registry.ts b/packages/agent-core/src/flags/registry.ts new file mode 100644 index 000000000..1e9f57b87 --- /dev/null +++ b/packages/agent-core/src/flags/registry.ts @@ -0,0 +1,16 @@ +import type { FlagDefinitionInput } from './types'; + +/** + * Experimental feature flags. Empty by default — there are no experimental features yet. + * + * To add one, append an entry and gate the feature with `flags.enabled('my-feature')`: + * { id: 'my-feature', env: 'KIMI_CODE_EXPERIMENTAL_MY_FEATURE', default: false, surface: 'both' } + * + * Keep the `as const satisfies` — it derives the literal `FlagId` union that gives `enabled()` + * autocomplete and typo-checking. `env` must start with 'KIMI_CODE_EXPERIMENTAL_', be unique, and + * not equal the master switch 'KIMI_CODE_EXPERIMENTAL_FLAG'; `id` must not be 'flag'. + */ +export const FLAG_DEFINITIONS = [] as const satisfies readonly FlagDefinitionInput[]; + +/** Literal union of registered flag ids (currently none → `never`). */ +export type FlagId = (typeof FLAG_DEFINITIONS)[number]['id']; diff --git a/packages/agent-core/src/flags/resolver.ts b/packages/agent-core/src/flags/resolver.ts new file mode 100644 index 000000000..7685cd52f --- /dev/null +++ b/packages/agent-core/src/flags/resolver.ts @@ -0,0 +1,65 @@ +import { parseBooleanEnv } from '#/config/resolve'; + +import { FLAG_DEFINITIONS, type FlagId } from './registry'; +import type { FlagDefinitionInput } from './types'; + +/** Master switch: when truthy, forces every flag on (highest priority). */ +export const MASTER_ENV = 'KIMI_CODE_EXPERIMENTAL_FLAG'; + +/** Shared prefix for per-feature variables. */ +export const EXPERIMENTAL_PREFIX = 'KIMI_CODE_EXPERIMENTAL_'; + +/** + * Conventional env-name generator: flag id → recommended env variable name. Only used when + * authoring a new flag (to fill `env`) and for an optional consistency test; NOT used during + * resolution. + */ +export function flagEnvKey(id: string): string { + return `${EXPERIMENTAL_PREFIX}${id.toUpperCase().replaceAll('-', '_')}`; +} + +/** + * Pure, synchronous flag resolver. State comes entirely from (env, registry) and nothing is + * cached: env is read live on every call, so a single shared instance always reflects the current + * process env. Defaults to process.env + FLAG_DEFINITIONS; tests can inject a custom env / defs. + * + * Precedence (highest wins): + * L1 master switch KIMI_CODE_EXPERIMENTAL_FLAG → every flag is on + * L2 per-feature def.env (parseBooleanEnv, may force on or off) + * L3 registry default + */ +export class FlagResolver { + private readonly env: Readonly>; + private readonly byId: ReadonlyMap; + + constructor( + env: Readonly> = process.env, + definitions: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS, + ) { + this.env = env; + this.byId = new Map(definitions.map((def) => [def.id, def])); + } + + enabled(id: FlagId): boolean { + const def = this.byId.get(id); + if (def === undefined) return false; + if (parseBooleanEnv(this.env[MASTER_ENV]) === true) return true; // L1 master switch + const override = parseBooleanEnv(this.env[def.env]); // L2 per-feature + if (override !== undefined) return override; + return def.default; // L3 default + } +} + +export function createFlagResolver( + env?: Readonly>, + definitions?: readonly FlagDefinitionInput[], +): FlagResolver { + return new FlagResolver(env, definitions); +} + +/** + * Process-global flag accessor. Flags are env-driven and process-global, so a single shared + * instance (reading live process.env) is the canonical way to consult them — import this directly + * rather than constructing or injecting a resolver. + */ +export const flags = new FlagResolver(); diff --git a/packages/agent-core/src/flags/types.ts b/packages/agent-core/src/flags/types.ts new file mode 100644 index 000000000..3c6668036 --- /dev/null +++ b/packages/agent-core/src/flags/types.ts @@ -0,0 +1,19 @@ +import type { FlagId } from './registry'; + +/** Which layer consumes a flag — documentation/grouping only; not used in resolution. */ +export type FlagSurface = 'core' | 'tui' | 'both'; + +/** Shape of a registry entry (id is a loose string so `as const satisfies` can validate it). */ +export interface FlagDefinitionInput { + readonly id: string; + /** Full environment variable name, e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`. Read directly by the resolver. */ + readonly env: string; + readonly default: boolean; + readonly surface: FlagSurface; +} + +/** FlagId-typed view so consumers can fetch a definition by its literal id. */ +export type FlagDefinition = FlagDefinitionInput & { readonly id: FlagId }; + +/** Resolved enabled-state of every experimental flag (flag id → enabled); used for the SDK snapshot. */ +export type ExperimentalFlagMap = Record; diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 670def093..c874d2ca2 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -2,6 +2,7 @@ export * from './agent'; export * from './session'; export * from './rpc'; export * from './config'; +export * from './flags'; export * from './session/export'; export * from './telemetry'; export * from './errors'; diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 99e244d45..504e9a309 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -4,6 +4,7 @@ import type { PermissionData, PermissionMode } from '#/agent/permission'; import type { PlanData } from '#/agent/plan'; import type { ToolInfo } from '#/agent/tool'; import type { KimiConfig, KimiConfigPatch } from '#/config'; +import type { ExperimentalFlagMap } from '#/flags'; import type { ResumeSessionResult } from '#/rpc/resumed'; import type { SessionMeta } from '#/session'; import type { BackgroundTaskInfo } from '#/tools/builtin'; @@ -307,6 +308,7 @@ type SessionAPIWithId = WithSessionId; export interface CoreAPI extends SessionAPIWithId { getCoreInfo: (payload: EmptyPayload) => CoreInfo; + getExperimentalFlags: (payload: EmptyPayload) => ExperimentalFlagMap; getKimiConfig: (payload: GetKimiConfigPayload) => KimiConfig; setKimiConfig: (payload: SetKimiConfigPayload) => KimiConfig; removeKimiProvider: (payload: RemoveKimiProviderPayload) => KimiConfig; diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index a20899e34..7fa4a6bd3 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -20,6 +20,13 @@ import { type KimiConfig, 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'; import { Session, type SessionMeta, type SessionSkillConfig } from '../session'; @@ -242,6 +249,11 @@ export class KimiCore implements PromisableMethods { return { version: getCoreVersion() }; } + getExperimentalFlags(): ExperimentalFlagMap { + const defs: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS; + return Object.fromEntries(defs.map((def) => [def.id, flags.enabled(def.id as FlagId)])); + } + async closeSession({ sessionId }: CloseSessionPayload): Promise { const session = this.sessions.get(sessionId); if (session) { diff --git a/packages/agent-core/test/flags/resolver.test.ts b/packages/agent-core/test/flags/resolver.test.ts new file mode 100644 index 000000000..15c300483 --- /dev/null +++ b/packages/agent-core/test/flags/resolver.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; + +import { + EXPERIMENTAL_PREFIX, + FLAG_DEFINITIONS, + MASTER_ENV, + createFlagResolver, + flagEnvKey, + type FlagDefinitionInput, + type FlagId, +} from '../../src/flags'; + +// Controlled fake definitions to assert the precedence matrix precisely (independent of the +// real registry contents). +const DEFS = [ + { + id: 'a-on-default', + env: 'KIMI_CODE_EXPERIMENTAL_A', + default: true, + surface: 'core', + }, + { + id: 'b-off-default', + env: 'KIMI_CODE_EXPERIMENTAL_B', + default: false, + surface: 'tui', + }, +] as const satisfies readonly FlagDefinitionInput[]; + +type Env = Record; + +function make(env: Env) { + const resolver = createFlagResolver(env, DEFS); + // The fake ids are not part of the real FlagId union, so cast to FlagId when calling. + return (id: string) => resolver.enabled(id as FlagId); +} + +describe('FlagResolver', () => { + it('L3 default: returns the registry default when env is empty', () => { + const enabled = make({}); + expect(enabled('a-on-default')).toBe(true); + expect(enabled('b-off-default')).toBe(false); + }); + + it('L2 per-feature on (lenient truthy values)', () => { + for (const v of ['1', 'true', 'yes', 'on', 'TRUE', ' On ']) { + expect(make({ KIMI_CODE_EXPERIMENTAL_B: v })('b-off-default')).toBe(true); + } + }); + + it('L2 per-feature off (lenient falsy values) overrides default=true', () => { + for (const v of ['0', 'false', 'no', 'off']) { + expect(make({ KIMI_CODE_EXPERIMENTAL_A: v })('a-on-default')).toBe(false); + } + }); + + it('L2 unparseable value falls back to default', () => { + expect(make({ KIMI_CODE_EXPERIMENTAL_B: 'maybe' })('b-off-default')).toBe(false); + expect(make({ KIMI_CODE_EXPERIMENTAL_A: 'maybe' })('a-on-default')).toBe(true); + }); + + it('L1 master switch: every flag is on when enabled (including default=false)', () => { + const enabled = make({ [MASTER_ENV]: '1' }); + expect(enabled('a-on-default')).toBe(true); + expect(enabled('b-off-default')).toBe(true); + }); + + it('L1 master switch beats an L2 per-feature off (D2)', () => { + const enabled = make({ [MASTER_ENV]: '1', KIMI_CODE_EXPERIMENTAL_A: '0' }); + expect(enabled('a-on-default')).toBe(true); + }); + + it('master switch is inactive for lenient falsy values', () => { + const enabled = make({ [MASTER_ENV]: '0' }); + expect(enabled('b-off-default')).toBe(false); + }); + + 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). + expect(make({ KIMI_CODE_EXPERIMENTAL_B_OFF_DEFAULT: '1' })('b-off-default')).toBe(false); + }); + + it('unknown id resolves to false (defensive)', () => { + expect(make({})('not-a-real-flag')).toBe(false); + }); + + it('flagEnvKey convention: kebab -> prefix + upper snake', () => { + expect(flagEnvKey('my-feature')).toBe('KIMI_CODE_EXPERIMENTAL_MY_FEATURE'); + }); +}); + +describe('FLAG_DEFINITIONS invariants', () => { + it('every env satisfies: prefix / unique / not the master switch', () => { + const seenEnv = new Set(); + const seenId = new Set(); + const defs: readonly FlagDefinitionInput[] = FLAG_DEFINITIONS; + for (const def of defs) { + expect(def.env.startsWith(EXPERIMENTAL_PREFIX)).toBe(true); + expect(def.env).not.toBe(MASTER_ENV); + expect(def.id).not.toBe('flag'); // reserved: would collide with the master switch + expect(seenEnv.has(def.env)).toBe(false); + expect(seenId.has(def.id)).toBe(false); + seenEnv.add(def.env); + seenId.add(def.id); + } + }); +}); diff --git a/packages/node-sdk/src/index.ts b/packages/node-sdk/src/index.ts index a3136fb48..ae3d677d2 100644 --- a/packages/node-sdk/src/index.ts +++ b/packages/node-sdk/src/index.ts @@ -44,6 +44,16 @@ export { } from '@moonshot-ai/agent-core'; export type { LogContext, LogLevel, LogPayload, Logger } from '@moonshot-ai/agent-core'; +// Experimental feature flags — types only. Resolved values come from +// `KimiHarness.getExperimentalFlags()` over RPC, not from a re-exported runtime value. +export type { + ExperimentalFlagMap, + FlagDefinition, + FlagDefinitionInput, + FlagId, + FlagSurface, +} from '@moonshot-ai/agent-core'; + export type { KimiAuthLoginResult, KimiAuthLogoutResult, diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index e9b51831d..769cd3a3d 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -8,6 +8,7 @@ import { resolveKimiHome, resolveLoggingConfig, withTelemetryContext, + type ExperimentalFlagMap, type TelemetryClient, type TelemetryContextPatch, type TelemetryProperties, @@ -191,6 +192,11 @@ export class KimiHarness { return this.rpc.getConfig(options); } + /** Resolved enabled-state of every experimental flag (flag id → enabled). */ + async getExperimentalFlags(): Promise { + return this.rpc.getExperimentalFlags(); + } + async ensureConfigFile(): Promise { await ensureConfigFile(this.configPath); } diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index a65b3e716..7346e5a5f 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -9,6 +9,7 @@ import { type ApprovalResponse, type CoreAPI, type Event, + type ExperimentalFlagMap, type OAuthTokenProviderResolver, type QuestionRequest, type QuestionResult, @@ -197,6 +198,11 @@ export class SDKRpcClient { return rpc.getKimiConfig(input ?? {}); } + async getExperimentalFlags(): Promise { + const rpc = await this.getRpc(); + return rpc.getExperimentalFlags({}); + } + async setConfig(input: KimiConfigPatch): Promise { const rpc = await this.getRpc(); return rpc.setKimiConfig(input);