mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-17 04:35:50 +00:00
* feat: support plugin-contributed custom agents * fix: await plugin loading before agent catalog * fix: refresh plugin agents on v1 reload * test(agent-core-v2): add enabledSystemPrompts to the plugin service stub
1558 lines
59 KiB
TypeScript
1558 lines
59 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, normalize } from 'pathe';
|
|
|
|
import type { Kaos } from '@moonshot-ai/kaos';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
FLAG_DEFINITIONS,
|
|
MASTER_ENV,
|
|
createRPC,
|
|
ErrorCodes,
|
|
KimiCore,
|
|
KimiError,
|
|
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';
|
|
import { testKaos } from '../fixtures/test-kaos';
|
|
|
|
function requiredFlagEnv(id: string): string {
|
|
// Micro compaction was the only registered flag and has been removed, so the
|
|
// env var name is derived directly; the (skipped) tests still type-check.
|
|
return `KIMI_CODE_EXPERIMENTAL_${id.toUpperCase()}`;
|
|
}
|
|
|
|
function clearExperimentalEnv(): void {
|
|
vi.stubEnv(MASTER_ENV, '0');
|
|
// No experimental flags are currently registered, so there are no per-flag
|
|
// env vars to clear.
|
|
}
|
|
|
|
function experimentalFeatureEnabled(core: KimiCore, id: string): boolean | undefined {
|
|
return core.getExperimentalFeatures().find((feature) => feature.id === id)?.enabled;
|
|
}
|
|
|
|
function setCoreKaos(core: KimiCore, kaos: Promise<Kaos>): void {
|
|
(core as unknown as { kaos?: Promise<Kaos> }).kaos = kaos;
|
|
}
|
|
|
|
function rejectedKaos(error: Error): Promise<Kaos> {
|
|
const promise = Promise.reject(error) as Promise<Kaos>;
|
|
promise.catch(() => undefined);
|
|
return promise;
|
|
}
|
|
|
|
// Builds a Kaos that behaves like the ACP reverse-RPC bridge during
|
|
// `session/new`: reading a `local.toml` rejects with a non-ENOENT error because
|
|
// the client does not know the session yet (issue #988). Everything else
|
|
// delegates to the underlying kaos, so once the system-file read is routed
|
|
// through a working (local) kaos, session bootstrap can still proceed.
|
|
function createLocalTomlFailingKaos(base: Kaos): Kaos {
|
|
return new Proxy(base, {
|
|
get(target, prop, receiver) {
|
|
if (prop === 'readText') {
|
|
return (
|
|
path: string,
|
|
options?: { encoding?: BufferEncoding; errors?: 'strict' | 'replace' | 'ignore' },
|
|
) => {
|
|
if (String(path).endsWith('local.toml')) {
|
|
return Promise.reject(
|
|
new Error(`acp: readTextFile failed for ${path}: unknown session (issue #988)`),
|
|
);
|
|
}
|
|
return target.readText(path, options);
|
|
};
|
|
}
|
|
if (prop === 'withCwd') {
|
|
return (cwd: string) => createLocalTomlFailingKaos(target.withCwd(cwd));
|
|
}
|
|
const value = Reflect.get(target, prop, receiver);
|
|
return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(target) : value;
|
|
},
|
|
});
|
|
}
|
|
|
|
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();
|
|
});
|
|
|
|
// Micro compaction was the only experimental flag and has been removed; this
|
|
// test is skipped because there is no flag to enable.
|
|
it.skip('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');
|
|
// No experimental flags are currently registered, so there is nothing to clear.
|
|
// for (const def of FLAG_DEFINITIONS) {
|
|
// vi.stubEnv(def.env, '0');
|
|
// }
|
|
vi.stubEnv(requiredFlagEnv('micro_compaction'), '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('micro_compaction');
|
|
expect(text.match(/experimental flags enabled/g)).toHaveLength(1);
|
|
});
|
|
|
|
// Micro compaction was the only experimental flag and has been removed; this
|
|
// test is skipped because there is no flag to resolve.
|
|
it.skip('resolves experimental flags from each core config independently', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const firstHome = join(tmp, 'first-home');
|
|
const secondHome = join(tmp, 'second-home');
|
|
await mkdir(firstHome, { recursive: true });
|
|
await mkdir(secondHome, { recursive: true });
|
|
await writeFile(
|
|
join(firstHome, 'config.toml'),
|
|
`
|
|
[experimental]
|
|
micro_compaction = true
|
|
`,
|
|
);
|
|
await writeFile(
|
|
join(secondHome, 'config.toml'),
|
|
`
|
|
[experimental]
|
|
micro_compaction = false
|
|
`,
|
|
);
|
|
clearExperimentalEnv();
|
|
|
|
const first = new KimiCore(async () => ({}) as never, { homeDir: firstHome });
|
|
const second = new KimiCore(async () => ({}) as never, { homeDir: secondHome });
|
|
|
|
expect(experimentalFeatureEnabled(first, 'micro_compaction')).toBe(true);
|
|
expect(experimentalFeatureEnabled(second, 'micro_compaction')).toBe(false);
|
|
});
|
|
|
|
// Micro compaction was the only experimental flag and has been removed; this
|
|
// test is skipped because there is no flag to update.
|
|
it.skip('updates the scoped experimental resolver after setKimiConfig', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`
|
|
[experimental]
|
|
micro_compaction = false
|
|
`,
|
|
);
|
|
clearExperimentalEnv();
|
|
|
|
const core = new KimiCore(async () => ({}) as never, { homeDir });
|
|
expect(experimentalFeatureEnabled(core, 'micro_compaction')).toBe(false);
|
|
|
|
await core.setKimiConfig({
|
|
experimental: {
|
|
'micro_compaction': true,
|
|
},
|
|
});
|
|
|
|
expect(experimentalFeatureEnabled(core, 'micro_compaction')).toBe(true);
|
|
});
|
|
|
|
// Micro compaction was the only experimental flag and has been removed; this
|
|
// test is skipped because there is no flag to update.
|
|
it.skip('updates the shared experimental resolver while goal tools stay available', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`${baseModelConfig()}
|
|
[experimental]
|
|
micro_compaction = false
|
|
`,
|
|
);
|
|
clearExperimentalEnv();
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_experimental_refresh',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
// expect(session?.experimentalFlags.enabled('micro_compaction')).toBe(false);
|
|
// expect(mainAgent?.experimentalFlags.enabled('micro_compaction')).toBe(false);
|
|
expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true);
|
|
|
|
await core.setKimiConfig({
|
|
experimental: {
|
|
'micro_compaction': true,
|
|
},
|
|
});
|
|
|
|
// expect(session?.experimentalFlags.enabled('micro_compaction')).toBe(true);
|
|
// expect(mainAgent?.experimentalFlags.enabled('micro_compaction')).toBe(true);
|
|
expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true);
|
|
|
|
await rpc.reloadSession({ sessionId: created.id });
|
|
const reloadedMainAgent = core.sessions.get(created.id)?.getReadyAgent('main');
|
|
expect(reloadedMainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true);
|
|
});
|
|
|
|
it('live-applies the complete persisted secondary recipe', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL', '1');
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_secondary_refresh',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
await rpc.setKimiConfig({
|
|
secondaryModel: {
|
|
model: 'default-mock',
|
|
maxContextSize: 65_536,
|
|
},
|
|
});
|
|
await rpc.applyPersistedSecondaryModel({ sessionId: created.id });
|
|
|
|
const config = core.sessions.get(created.id)?.getReadyAgent('main')?.kimiConfig;
|
|
expect(config?.secondaryModel).toEqual({
|
|
model: 'default-mock',
|
|
maxContextSize: 65_536,
|
|
});
|
|
expect(config?.models?.['__secondary__']?.overrides?.maxContextSize).toBe(65_536);
|
|
});
|
|
|
|
// Regression for https://github.com/MoonshotAI/kimi-code/issues/988: during
|
|
// ACP `session/new` the tool kaos is the reverse-RPC bridge and the client
|
|
// does not know the session yet, so reading `.kimi-code/local.toml` through
|
|
// it rejects. The workspace local config is a local system file and must be
|
|
// read through the persistence (local) kaos instead.
|
|
it('reads workspace local.toml through persistenceKaos during createSession', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const sharedDir = join(tmp, 'shared');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(join(workDir, '.git'), { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await mkdir(sharedDir, { recursive: true });
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["../shared"]\n`,
|
|
);
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await core.createSessionWithOverrides(
|
|
{ id: 'ses_runtime_local_toml_bootstrap', workDir, model: 'default-mock' },
|
|
{ kaos: createLocalTomlFailingKaos(testKaos), persistenceKaos: testKaos },
|
|
);
|
|
|
|
const session = core.sessions.get(created.id);
|
|
expect(session).toBeDefined();
|
|
expect(session?.getAdditionalDirs()).toContain(normalize(sharedDir));
|
|
});
|
|
|
|
it('uses the shared OAuth resolver for Moonshot service tokens', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`
|
|
[services.moonshot_search]
|
|
base_url = "https://search.example/v1"
|
|
oauth = { storage = "file", key = "oauth/custom-kimi-code" }
|
|
custom_headers = { "X-Test" = "1" }
|
|
`,
|
|
);
|
|
|
|
const getAccessToken = vi.fn().mockResolvedValue('service-token');
|
|
const resolveOAuthTokenProvider = vi.fn<OAuthTokenProviderResolver>(() => ({
|
|
getAccessToken,
|
|
}));
|
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ search_results: [] }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchImpl);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, {
|
|
homeDir,
|
|
kimiRequestHeaders: {
|
|
'User-Agent': 'kimi-code-cli/0.0.0-test',
|
|
'X-Msh-Version': '0.0.0-test',
|
|
},
|
|
resolveOAuthTokenProvider,
|
|
});
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({ id: 'ses_runtime_service_oauth', workDir });
|
|
const session = core.sessions.get(created.id);
|
|
|
|
expect(resolveOAuthTokenProvider).toHaveBeenCalledWith('managed:kimi-code', {
|
|
storage: 'file',
|
|
key: 'oauth/custom-kimi-code',
|
|
});
|
|
expect(session?.options.toolServices?.webSearcher).toBeDefined();
|
|
|
|
await session!.options.toolServices?.webSearcher!.search('kimi');
|
|
|
|
expect(getAccessToken).toHaveBeenCalledWith();
|
|
const init = fetchImpl.mock.calls[0]?.[1] as RequestInit;
|
|
expect(init.headers).toMatchObject({
|
|
Authorization: 'Bearer service-token',
|
|
'User-Agent': 'kimi-code-cli/0.0.0-test',
|
|
'X-Msh-Version': '0.0.0-test',
|
|
'X-Test': '1',
|
|
});
|
|
});
|
|
|
|
it('enables Moonshot web services from KIMI_WEB_* env vars without a services config section', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), '');
|
|
vi.stubEnv('KIMI_WEB_SEARCH_BASE_URL', 'https://search-env.example/v1');
|
|
vi.stubEnv('KIMI_WEB_SEARCH_API_KEY', 'env-search-key');
|
|
vi.stubEnv('KIMI_WEB_FETCH_BASE_URL', 'https://fetch-env.example/v1');
|
|
vi.stubEnv('KIMI_WEB_FETCH_API_KEY', 'env-fetch-key');
|
|
|
|
const fetchImpl = vi.fn().mockImplementation(async (url: string | URL) =>
|
|
String(url).includes('search')
|
|
? new Response(JSON.stringify({ search_results: [] }), { status: 200 })
|
|
: new Response('page body', { status: 200 }),
|
|
);
|
|
vi.stubGlobal('fetch', fetchImpl);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({ id: 'ses_runtime_service_env', workDir });
|
|
const session = core.sessions.get(created.id);
|
|
|
|
const webSearcher = session?.options.toolServices?.webSearcher;
|
|
const urlFetcher = session?.options.toolServices?.urlFetcher;
|
|
expect(webSearcher).toBeDefined();
|
|
expect(urlFetcher).toBeDefined();
|
|
|
|
await webSearcher!.search('kimi');
|
|
const [searchUrl, searchInit] = fetchImpl.mock.calls[0] as [string, RequestInit];
|
|
expect(searchUrl).toBe('https://search-env.example/v1');
|
|
expect((searchInit.headers as Record<string, string>)['Authorization']).toBe(
|
|
'Bearer env-search-key',
|
|
);
|
|
|
|
await urlFetcher!.fetch('https://example.com/page', {});
|
|
const [fetchUrl, fetchInit] = fetchImpl.mock.calls[1] as [string, RequestInit];
|
|
expect(fetchUrl).toBe('https://fetch-env.example/v1');
|
|
expect((fetchInit.headers as Record<string, string>)['Authorization']).toBe(
|
|
'Bearer env-fetch-key',
|
|
);
|
|
});
|
|
|
|
it('keeps persisted credentials off an env-selected Moonshot search endpoint', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`
|
|
[services.moonshot_search]
|
|
base_url = "https://search-file.example/v1"
|
|
api_key = "file-search-key"
|
|
oauth = { storage = "file", key = "oauth/custom-kimi-code" }
|
|
custom_headers = { "X-Config-Secret" = "secret-value" }
|
|
`,
|
|
);
|
|
vi.stubEnv('KIMI_WEB_SEARCH_BASE_URL', 'https://search-env.example/v1');
|
|
vi.stubEnv('KIMI_WEB_SEARCH_API_KEY', 'env-search-key');
|
|
|
|
const getAccessToken = vi.fn().mockResolvedValue('oauth-token');
|
|
const resolveOAuthTokenProvider = vi.fn<OAuthTokenProviderResolver>(() => ({
|
|
getAccessToken,
|
|
}));
|
|
const fetchImpl = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ search_results: [] }), { status: 200 }),
|
|
);
|
|
vi.stubGlobal('fetch', fetchImpl);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir, resolveOAuthTokenProvider });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({ id: 'ses_runtime_service_env_precedence', workDir });
|
|
const session = core.sessions.get(created.id);
|
|
|
|
await session?.options.toolServices?.webSearcher?.search('kimi');
|
|
|
|
const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
|
|
expect(url).toBe('https://search-env.example/v1');
|
|
expect(init.headers).toMatchObject({ Authorization: 'Bearer env-search-key' });
|
|
expect(init.headers).not.toHaveProperty('X-Config-Secret');
|
|
expect(resolveOAuthTokenProvider).not.toHaveBeenCalled();
|
|
expect(getAccessToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('falls back to defaultModel when createSession receives no model option', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`default_model = "default-mock"
|
|
|
|
[providers.test]
|
|
type = "kimi"
|
|
api_key = "test-key"
|
|
|
|
[models."default-mock"]
|
|
provider = "test"
|
|
model = "default-mock"
|
|
max_context_size = 100000
|
|
`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({ id: 'ses_runtime_default_model', workDir });
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
expect(mainAgent?.config.modelAlias).toBe('default-mock');
|
|
});
|
|
|
|
it('loads project local additional dirs into the session and main agent', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["extra"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
expect(created.additionalDirs).toEqual([extraDir]);
|
|
expect(session?.getAdditionalDirs()).toEqual([extraDir]);
|
|
expect(mainAgent?.getAdditionalDirs()).toEqual([extraDir]);
|
|
expect(mainAgent?.config.systemPrompt).toContain('## Additional Directories');
|
|
expect(mainAgent?.config.systemPrompt).toContain(extraDir);
|
|
});
|
|
|
|
it('returns additionalDirs when resuming an active session', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["extra"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_active_resume',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const resumed = await rpc.resumeSession({ sessionId: created.id });
|
|
|
|
expect(resumed.additionalDirs).toEqual([extraDir]);
|
|
expect(core.sessions.get(created.id)?.getAdditionalDirs()).toEqual([extraDir]);
|
|
});
|
|
|
|
it('returns additionalDirs when resuming a closed session', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["extra"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_closed_resume',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
await rpc.closeSession({ sessionId: created.id });
|
|
|
|
const resumed = await rpc.resumeSession({ sessionId: created.id });
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
expect(resumed.additionalDirs).toEqual([extraDir]);
|
|
expect(session?.getAdditionalDirs()).toEqual([extraDir]);
|
|
expect(mainAgent?.getAdditionalDirs()).toEqual([extraDir]);
|
|
});
|
|
|
|
it('merges caller additionalDirs when resuming a closed session', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const localDir = join(workDir, 'local');
|
|
const callerDir = join(workDir, 'caller');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(localDir, { recursive: true });
|
|
await mkdir(callerDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["local"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_resume_caller',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
await rpc.closeSession({ sessionId: created.id });
|
|
|
|
const resumed = await rpc.resumeSession({
|
|
sessionId: created.id,
|
|
additionalDirs: ['caller'],
|
|
});
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
expect(resumed.additionalDirs).toEqual([localDir, callerDir]);
|
|
expect(session?.getAdditionalDirs()).toEqual([localDir, callerDir]);
|
|
expect(mainAgent?.getAdditionalDirs()).toEqual([localDir, callerDir]);
|
|
});
|
|
|
|
it('deduplicates project local and caller relative additionalDirs after resolving them', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const sharedDir = join(workDir, 'shared');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(sharedDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["shared"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_dedupe',
|
|
workDir,
|
|
model: 'default-mock',
|
|
additionalDirs: ['shared'],
|
|
});
|
|
|
|
expect(created.additionalDirs).toEqual([sharedDir]);
|
|
expect(core.sessions.get(created.id)?.getAdditionalDirs()).toEqual([sharedDir]);
|
|
});
|
|
|
|
it('supports multiple project local and caller additionalDirs', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const localDir = join(workDir, 'shared');
|
|
const callerDir = join(workDir, 'other');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(localDir, { recursive: true });
|
|
await mkdir(callerDir, { recursive: true });
|
|
await mkdir(join(workDir, '.kimi-code'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(workDir, '.kimi-code', 'local.toml'),
|
|
`[workspace]\nadditional_dir = ["shared"]\n`,
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
void new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_multiple',
|
|
workDir,
|
|
model: 'default-mock',
|
|
additionalDirs: ['other'],
|
|
});
|
|
|
|
expect(created.additionalDirs).toEqual([localDir, callerDir]);
|
|
});
|
|
|
|
it('resolves caller relative additionalDirs against workDir rather than projectRoot', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const projectRoot = join(tmp, 'repo');
|
|
const workDir = join(projectRoot, 'apps', 'foo');
|
|
const sharedDir = join(workDir, 'shared');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(join(projectRoot, '.git'), { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(sharedDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_additional_dirs_workdir_relative',
|
|
workDir,
|
|
model: 'default-mock',
|
|
additionalDirs: ['shared'],
|
|
});
|
|
|
|
expect(created.additionalDirs).toEqual([sharedDir]);
|
|
expect(core.sessions.get(created.id)?.getAdditionalDirs()).toEqual([sharedDir]);
|
|
});
|
|
|
|
it('records a local-command-stdout message when adding a remembered dir', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_add_additional_dir_record',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
await rpc.addAdditionalDir({
|
|
sessionId: created.id,
|
|
path: 'extra',
|
|
persist: true,
|
|
});
|
|
await core.sessions.get(created.id)?.getReadyAgent('main')?.records.flush();
|
|
|
|
const records = await readMainWire(created.sessionDir);
|
|
expect(records).toContainEqual(
|
|
expect.objectContaining({
|
|
type: 'context.append_message',
|
|
message: expect.objectContaining({
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `<local-command-stdout>\nAdded workspace directory:\n extra\n Saved to:\n ${join(workDir, '.kimi-code', 'local.toml')}\n</local-command-stdout>`,
|
|
},
|
|
],
|
|
origin: { kind: 'injection', variant: 'local-command-stdout' },
|
|
}),
|
|
}),
|
|
);
|
|
expect(core.sessions.get(created.id)?.getReadyAgent('main')?.getAdditionalDirs()).toEqual([
|
|
extraDir,
|
|
]);
|
|
});
|
|
|
|
it('adds an additional dir through the session RPC', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_add_additional_dir',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
const result = await rpc.addAdditionalDir({
|
|
sessionId: created.id,
|
|
path: 'extra',
|
|
persist: true,
|
|
});
|
|
const localToml = await readFile(join(workDir, '.kimi-code', 'local.toml'), 'utf-8');
|
|
const session = core.sessions.get(created.id);
|
|
const mainAgent = session?.getReadyAgent('main');
|
|
|
|
expect(result).toMatchObject({
|
|
additionalDirs: [extraDir],
|
|
projectRoot: workDir,
|
|
configPath: join(workDir, '.kimi-code', 'local.toml'),
|
|
persisted: true,
|
|
});
|
|
expect(localToml).toContain('additional_dir = [');
|
|
expect(session?.getAdditionalDirs()).toEqual([extraDir]);
|
|
expect(mainAgent?.getAdditionalDirs()).toEqual([extraDir]);
|
|
});
|
|
|
|
it('adds a session-only additional dir without writing local.toml', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const extraDir = join(workDir, 'extra');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(extraDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_add_session_only_dir',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
const result = await rpc.addAdditionalDir({
|
|
sessionId: created.id,
|
|
path: 'extra',
|
|
persist: false,
|
|
});
|
|
await core.sessions.get(created.id)?.getReadyAgent('main')?.records.flush();
|
|
const records = await readMainWire(created.sessionDir);
|
|
|
|
expect(result).toMatchObject({
|
|
additionalDirs: [extraDir],
|
|
projectRoot: workDir,
|
|
configPath: join(workDir, '.kimi-code', 'local.toml'),
|
|
persisted: false,
|
|
});
|
|
expect(core.sessions.get(created.id)?.getAdditionalDirs()).toEqual([extraDir]);
|
|
expect(records).toContainEqual(
|
|
expect.objectContaining({
|
|
type: 'context.append_message',
|
|
message: expect.objectContaining({
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: '<local-command-stdout>\nAdded workspace directory:\n extra\n For this session only\n</local-command-stdout>',
|
|
},
|
|
],
|
|
origin: { kind: 'injection', variant: 'local-command-stdout' },
|
|
}),
|
|
}),
|
|
);
|
|
await expect(readFile(join(workDir, '.kimi-code', 'local.toml'), 'utf-8')).rejects.toThrow();
|
|
});
|
|
|
|
it('rejects createSession when shell runtime initialization fails', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
setCoreKaos(
|
|
core,
|
|
rejectedKaos(
|
|
new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, 'Git Bash missing'),
|
|
),
|
|
);
|
|
|
|
await expect(
|
|
rpc.createSession({
|
|
id: 'ses_runtime_shell_missing_create',
|
|
workDir,
|
|
model: 'default-mock',
|
|
}),
|
|
).rejects.toMatchObject({ code: ErrorCodes.SHELL_GIT_BASH_NOT_FOUND });
|
|
expect(core.sessions.has('ses_runtime_shell_missing_create')).toBe(false);
|
|
});
|
|
|
|
it('rejects resumeSession when shell runtime initialization fails', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
setCoreKaos(core, Promise.resolve(testKaos));
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_shell_missing_resume',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
await rpc.closeSession({ sessionId: created.id });
|
|
setCoreKaos(
|
|
core,
|
|
rejectedKaos(
|
|
new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, 'Git Bash missing'),
|
|
),
|
|
);
|
|
|
|
await expect(rpc.resumeSession({ sessionId: created.id })).rejects.toMatchObject({
|
|
code: ErrorCodes.SHELL_GIT_BASH_NOT_FOUND,
|
|
});
|
|
expect(core.sessions.has(created.id)).toBe(false);
|
|
});
|
|
|
|
it('reloads an active session with fresh runtime services from config.toml', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const configPath = join(homeDir, 'config.toml');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(configPath, baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const before = core.sessions.get(created.id);
|
|
expect(before?.options.toolServices?.webSearcher).toBeUndefined();
|
|
|
|
await writeFile(
|
|
configPath,
|
|
`${baseModelConfig()}
|
|
[services.moonshot_search]
|
|
base_url = "https://search.example.test/v1"
|
|
`,
|
|
);
|
|
|
|
const reloaded = await rpc.reloadSession({ sessionId: created.id });
|
|
const after = core.sessions.get(created.id);
|
|
|
|
expect(after).toBeDefined();
|
|
expect(after).not.toBe(before);
|
|
expect(after?.options.toolServices?.webSearcher).toBeDefined();
|
|
expect(reloaded.agents['main']).toBeDefined();
|
|
});
|
|
|
|
it('rejects reloadSession while the active session has a running turn', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_busy',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const active = core.sessions.get(created.id);
|
|
const main = active?.getReadyAgent('main');
|
|
vi.spyOn(main!.turn, 'hasActiveTurn', 'get').mockReturnValue(true);
|
|
|
|
await expect(rpc.reloadSession({ sessionId: created.id })).rejects.toMatchObject({
|
|
code: ErrorCodes.TURN_AGENT_BUSY,
|
|
});
|
|
expect(core.sessions.get(created.id)).toBe(active);
|
|
});
|
|
|
|
it('appends a fresh plugin_session_start reminder on forced reload', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeSessionStartPlugin(pluginRoot, 'OLD BODY');
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_reminder',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
// Before any forced reload the model has not been told about the plugin yet
|
|
// (no turn has run, so the turn-loop injector has not fired).
|
|
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
|
|
|
|
// Update the skill content on disk so the reload must pick up the new body.
|
|
// Preserve the SKILL.md frontmatter — the parser requires it to register the skill.
|
|
await writeFile(
|
|
managedSkillPath(homeDir),
|
|
`---\nname: greeter\ndescription: A greeter skill\n---\nNEW BODY\n`,
|
|
);
|
|
|
|
const reloaded = await rpc.reloadSession({
|
|
sessionId: created.id,
|
|
forcePluginSessionStartReminder: true,
|
|
});
|
|
|
|
const reminders = pluginSessionStartReminders(core, created.id);
|
|
expect(reminders).toHaveLength(1);
|
|
expect(reminders[0]).toContain('<plugin_session_start plugin="demo" skill="greeter">');
|
|
expect(reminders[0]).toContain('NEW BODY');
|
|
expect(reminders[0]).not.toContain('OLD BODY');
|
|
expect(reminders[0]).toContain('supersedes any earlier plugin_session_start');
|
|
|
|
// The returned ResumeSessionResult must already include the fresh reminder
|
|
// (otherwise SDK callers reading getResumeState() see stale plugin context).
|
|
const resultReminders = remindersFromHistory(
|
|
reloaded.agents['main']?.context.history ?? [],
|
|
);
|
|
expect(resultReminders).toHaveLength(1);
|
|
expect(resultReminders[0]).toContain('NEW BODY');
|
|
});
|
|
|
|
it('neutralizes a stale plugin_session_start reminder when the plugin is removed', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeSessionStartPlugin(pluginRoot, 'BODY');
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_neutralize',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
// First forced reload appends an active reminder, establishing a prior
|
|
// plugin_session_start in history.
|
|
await rpc.reloadSession({
|
|
sessionId: created.id,
|
|
forcePluginSessionStartReminder: true,
|
|
});
|
|
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(1);
|
|
|
|
// Removing the plugin means no sessionStart is resolvable on the next reload;
|
|
// the stale reminder must be neutralized rather than left in place.
|
|
await core.removePlugin({ id: 'demo' });
|
|
await rpc.reloadSession({
|
|
sessionId: created.id,
|
|
forcePluginSessionStartReminder: true,
|
|
});
|
|
|
|
const reminders = pluginSessionStartReminders(core, created.id);
|
|
expect(reminders).toHaveLength(2);
|
|
expect(reminders.at(-1)).toContain('no active plugin session starts');
|
|
expect(reminders.at(-1)).toContain('supersedes any earlier plugin_session_start');
|
|
});
|
|
|
|
it('replaces only the plugin agent layer when a plugin is disabled before reload', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(join(homeDir, 'agents'), { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(join(pluginRoot, 'agents'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(homeDir, 'agents', 'local-reviewer.md'),
|
|
'---\nname: local-reviewer\ndescription: Local reviewer\n---\n\nLocal prompt.\n',
|
|
);
|
|
await writeFile(
|
|
join(pluginRoot, 'kimi.plugin.json'),
|
|
JSON.stringify({ name: 'demo', agents: ['./agents'] }),
|
|
);
|
|
await writeFile(
|
|
join(pluginRoot, 'agents', 'plugin-reviewer.md'),
|
|
'---\nname: plugin-reviewer\ndescription: Plugin reviewer\n---\n\nPlugin prompt.\n',
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_plugin_agents',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
expect(core.sessions.get(created.id)?.agentCatalog.get('plugin-reviewer')).toBeDefined();
|
|
expect(core.sessions.get(created.id)?.agentCatalog.get('local-reviewer')).toBeDefined();
|
|
|
|
await core.setPluginEnabled({ id: 'demo', enabled: false });
|
|
await rpc.reloadSession({ sessionId: created.id });
|
|
|
|
const reloadedCatalog = core.sessions.get(created.id)?.agentCatalog;
|
|
expect(reloadedCatalog?.get('plugin-reviewer')).toBeUndefined();
|
|
expect(reloadedCatalog?.get('local-reviewer')?.description).toBe('Local reviewer');
|
|
});
|
|
|
|
it('adds a newly installed plugin agent on reload and preserves it on resume', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await mkdir(join(pluginRoot, 'agents'), { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeFile(
|
|
join(pluginRoot, 'kimi.plugin.json'),
|
|
JSON.stringify({ name: 'demo', agents: ['./agents'] }),
|
|
);
|
|
await writeFile(
|
|
join(pluginRoot, 'agents', 'plugin-reviewer.md'),
|
|
'---\nname: plugin-reviewer\ndescription: Plugin reviewer\n---\n\nPlugin prompt.\n',
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_new_plugin_agent',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
expect(core.sessions.get(created.id)?.agentCatalog.get('plugin-reviewer')).toBeUndefined();
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
await rpc.reloadSession({ sessionId: created.id });
|
|
expect(core.sessions.get(created.id)?.agentCatalog.get('plugin-reviewer')).toBeDefined();
|
|
|
|
await rpc.closeSession({ sessionId: created.id });
|
|
await rpc.resumeSession({ sessionId: created.id });
|
|
expect(core.sessions.get(created.id)?.agentCatalog.get('plugin-reviewer')).toBeDefined();
|
|
});
|
|
|
|
it('does not append a plugin_session_start reminder on reload without the force flag', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await writeSessionStartPlugin(pluginRoot, 'BODY');
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_no_force',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
await rpc.reloadSession({ sessionId: created.id });
|
|
|
|
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
|
|
});
|
|
|
|
it('appends nothing on forced reload when no plugin declares a sessionStart', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
const pluginRoot = join(tmp, 'plugin');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
await mkdir(pluginRoot, { recursive: true });
|
|
await writeFile(
|
|
join(pluginRoot, 'kimi.plugin.json'),
|
|
JSON.stringify({ name: 'demo', version: '1.0.0' }),
|
|
);
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
await core.installPlugin({ source: pluginRoot });
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_no_sessionstart',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
await rpc.reloadSession({
|
|
sessionId: created.id,
|
|
forcePluginSessionStartReminder: true,
|
|
});
|
|
|
|
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
|
|
});
|
|
|
|
it('neutralizes stale plugin guidance after compaction when no sessionStart is active', async () => {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
|
|
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_runtime_reload_compacted',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
const session = core.sessions.get(created.id);
|
|
const main = session?.getReadyAgent('main');
|
|
|
|
// Simulate a compaction that folded earlier messages (and any plugin guidance)
|
|
// into a single summary, leaving no discrete plugin_session_start behind.
|
|
main?.context.appendMessage({
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'summary of earlier conversation with plugin guidance' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'compaction_summary' },
|
|
});
|
|
|
|
await session?.appendPluginSessionStartReminder();
|
|
|
|
const reminders = pluginSessionStartReminders(core, created.id);
|
|
expect(reminders).toHaveLength(1);
|
|
expect(reminders[0]).toContain('no active plugin session starts');
|
|
});
|
|
});
|
|
|
|
describe('KimiCore print-mode defaults', () => {
|
|
let tmp: string;
|
|
|
|
afterEach(async () => {
|
|
if (tmp !== undefined) {
|
|
await rm(tmp, { recursive: true, force: true });
|
|
}
|
|
await __resetRootLoggerForTest();
|
|
vi.unstubAllEnvs();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
async function createCorePair(homeDir: string, uiMode?: string) {
|
|
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
|
|
const core = new KimiCore(coreRpc, { homeDir, uiMode });
|
|
const rpc = await sdkRpc({
|
|
emitEvent: vi.fn(),
|
|
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
|
|
requestQuestion: vi.fn(async () => null),
|
|
toolCall: vi.fn(async () => ({ output: '' })),
|
|
});
|
|
return { core, rpc };
|
|
}
|
|
|
|
async function setupDirs(configToml: string): Promise<{ homeDir: string; workDir: string }> {
|
|
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-print-defaults-'));
|
|
const homeDir = join(tmp, 'home');
|
|
const workDir = join(tmp, 'work');
|
|
await mkdir(homeDir, { recursive: true });
|
|
await mkdir(workDir, { recursive: true });
|
|
await writeFile(join(homeDir, 'config.toml'), configToml);
|
|
return { homeDir, workDir };
|
|
}
|
|
|
|
it('applies print-mode subagent/loop defaults to sessions when uiMode is print', async () => {
|
|
const { homeDir, workDir } = await setupDirs(baseModelConfig());
|
|
const { core, rpc } = await createCorePair(homeDir, 'print');
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_print_defaults',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
const main = core.sessions.get(created.id)?.getReadyAgent('main');
|
|
expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(0);
|
|
expect(main?.kimiConfig?.background?.bashTaskTimeoutS).toBe(0);
|
|
expect(main?.kimiConfig?.loopControl?.maxStepsPerTurn).toBe(0);
|
|
|
|
// The raw user config is left untouched so config reads/writes still
|
|
// round-trip the user's file values.
|
|
const raw = await core.getKimiConfig();
|
|
expect(raw.subagent).toBeUndefined();
|
|
expect(raw.loopControl).toBeUndefined();
|
|
});
|
|
|
|
it('keeps explicit user config over the print-mode defaults', async () => {
|
|
const { homeDir, workDir } = await setupDirs(`${baseModelConfig()}
|
|
[loop_control]
|
|
max_steps_per_turn = 7
|
|
|
|
[subagent]
|
|
timeout_ms = 5000
|
|
`);
|
|
const { core, rpc } = await createCorePair(homeDir, 'print');
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_print_defaults_user_config',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
const main = core.sessions.get(created.id)?.getReadyAgent('main');
|
|
expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(5000);
|
|
expect(main?.kimiConfig?.loopControl?.maxStepsPerTurn).toBe(7);
|
|
});
|
|
|
|
it('does not apply print-mode defaults outside print mode', async () => {
|
|
const { homeDir, workDir } = await setupDirs(baseModelConfig());
|
|
const { core, rpc } = await createCorePair(homeDir);
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_print_defaults_off',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
|
|
const main = core.sessions.get(created.id)?.getReadyAgent('main');
|
|
expect(main?.kimiConfig?.subagent).toBeUndefined();
|
|
expect(main?.kimiConfig?.loopControl).toBeUndefined();
|
|
});
|
|
|
|
it('applies print-mode defaults when a session is reloaded', async () => {
|
|
const { homeDir, workDir } = await setupDirs(baseModelConfig());
|
|
const { core, rpc } = await createCorePair(homeDir, 'print');
|
|
|
|
const created = await rpc.createSession({
|
|
id: 'ses_print_defaults_reload',
|
|
workDir,
|
|
model: 'default-mock',
|
|
});
|
|
await rpc.reloadSession({ sessionId: created.id });
|
|
|
|
// The reload path rebuilds the session through resumeSessionWithOverrides;
|
|
// the agent it constructs must carry the same print-mode defaults.
|
|
const main = core.sessions.get(created.id)?.getReadyAgent('main');
|
|
expect(main?.kimiConfig?.subagent?.timeoutMs).toBe(0);
|
|
expect(main?.kimiConfig?.background?.bashTaskTimeoutS).toBe(0);
|
|
expect(main?.kimiConfig?.loopControl?.maxStepsPerTurn).toBe(0);
|
|
});
|
|
});
|
|
|
|
async function writeSessionStartPlugin(root: string, skillBody: string): Promise<void> {
|
|
await mkdir(join(root, 'skills', 'greeter'), { recursive: true });
|
|
await writeFile(
|
|
join(root, 'kimi.plugin.json'),
|
|
JSON.stringify({
|
|
name: 'demo',
|
|
version: '1.0.0',
|
|
skills: ['./skills'],
|
|
sessionStart: { skill: 'greeter' },
|
|
}),
|
|
);
|
|
await writeFile(
|
|
join(root, 'skills', 'greeter', 'SKILL.md'),
|
|
`---\nname: greeter\ndescription: A greeter skill\n---\n${skillBody}\n`,
|
|
);
|
|
}
|
|
|
|
function managedSkillPath(homeDir: string): string {
|
|
return join(homeDir, 'plugins', 'managed', 'demo', 'skills', 'greeter', 'SKILL.md');
|
|
}
|
|
|
|
function pluginSessionStartReminders(core: KimiCore, sessionId: string): string[] {
|
|
const agent = core.sessions.get(sessionId)?.getReadyAgent('main');
|
|
if (agent === undefined) return [];
|
|
return remindersFromHistory(agent.context.history);
|
|
}
|
|
|
|
function remindersFromHistory(
|
|
history: ReadonlyArray<{
|
|
role: string;
|
|
origin?: { kind: string; variant?: string };
|
|
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
}>,
|
|
): string[] {
|
|
return history
|
|
.filter(
|
|
(message) =>
|
|
message.role === 'user' &&
|
|
message.origin?.kind === 'injection' &&
|
|
message.origin.variant === 'plugin_session_start',
|
|
)
|
|
.map((message) => message.content.map((part) => part.text ?? '').join(''));
|
|
}
|
|
|
|
async function readMainWire(sessionDir: string): Promise<readonly Record<string, unknown>[]> {
|
|
const wire = await readFile(join(sessionDir, 'agents', 'main', 'wire.jsonl'), 'utf-8');
|
|
return wire
|
|
.trim()
|
|
.split('\n')
|
|
.filter((line) => line.length > 0)
|
|
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
}
|
|
|
|
function baseModelConfig(): string {
|
|
return `default_model = "default-mock"
|
|
|
|
[providers.test]
|
|
type = "kimi"
|
|
api_key = "test-key"
|
|
|
|
[models."default-mock"]
|
|
provider = "test"
|
|
model = "default-mock"
|
|
max_context_size = 100000
|
|
`;
|
|
}
|