kimi-code / packages /agent-core-v2 /src /app /plugin /pluginService.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw
History Blame Contribute Delete
12.6 kB
import { KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { Service } from '#/_base/di/service';
import { AsyncEmitter, Emitter, type Event } from '#/_base/event';
import type { HookDef } from '#/features/externalHooks/internal/types';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { LifecycleScope } from '#/app/scopes';
import { ISkillDiscovery } from '#/features/skill/catalog/skillDiscovery';
import type { SkillRoot } from '#/features/skill/catalog/types';
import { BugIndicatingError, Error2, PluginErrors } from '#/errors';
import { IProviderService } from '#/llm-adapter/provider/provider';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import { PluginManager } from './manager';
import {
type GetPluginInfoInput,
type InstallPluginInput,
IPluginService,
type RemovePluginInput,
type SetPluginEnabledInput,
type SetPluginMcpServerEnabledInput,
} from './plugin';
import type {
EnabledPluginSessionStart,
EnabledPluginSystemPrompt,
PluginCommandDef,
PluginInfo,
PluginAgentRoot,
PluginMcpServerEntry,
PluginMutation,
PluginMutationSummary,
PluginReloadEvent,
PluginSummary,
PluginUpdateStatus,
ReloadSummary,
} from './types';
const KIMI_CODE_BASE_URL_ENV = 'KIMI_CODE_BASE_URL';
const KIMI_CODE_OAUTH_HOST_ENV = 'KIMI_CODE_OAUTH_HOST';
const KIMI_OAUTH_HOST_ENV = 'KIMI_OAUTH_HOST';
const NO_ABORT = new AbortController().signal;
interface PluginReloadNotification {
readonly summary: ReloadSummary;
readonly delivery: Promise<void>;
}
interface PluginMutationOutcome<T> {
readonly result: T;
readonly notification: PluginReloadNotification;
}
export class PluginService extends Service implements IPluginService {
declare readonly _serviceBrand: undefined;
private readonly homeDir: string;
private readonly envBaseUrl: string | undefined;
private readonly envOAuthHost: string | undefined;
private readonly manager: PluginManager;
private initialLoadPromise: Promise<void> | undefined;
private snapshotLoaded = false;
private loadError: Error | undefined;
private mutationQueue: Promise<void> = Promise.resolve();
private readonly onDidReloadEmitter = this._register(new AsyncEmitter<PluginReloadEvent>());
private readonly onDidMutateEmitter = this._register(new Emitter<PluginMutationSummary>());
readonly onDidReload: Event<PluginReloadEvent> = this.onDidReloadEmitter.event;
readonly onDidMutate: Event<PluginMutationSummary> = this.onDidMutateEmitter.event;
constructor(
@IBootstrapService bootstrap: IBootstrapService,
@ISkillDiscovery discovery: ISkillDiscovery,
@IProviderService private readonly providers: IProviderService,
) {
super();
this.homeDir = bootstrap.homeDir;
this.envBaseUrl = bootstrap.getEnv(KIMI_CODE_BASE_URL_ENV);
this.envOAuthHost =
bootstrap.getEnv(KIMI_CODE_OAUTH_HOST_ENV) ?? bootstrap.getEnv(KIMI_OAUTH_HOST_ENV);
this.manager = new PluginManager({
kimiHomeDir: this.homeDir,
discoverSkills: (roots) => discovery.discover(roots),
});
}
listPlugins(): Promise<readonly PluginSummary[]> {
return this.runManagementRead(async () => this.manager.summaries());
}
installPlugin(input: InstallPluginInput): Promise<PluginSummary> {
return this.runNotifiedMutation(async () => {
const record = await this.manager.install(input.source);
const info = this.manager.info(record.id);
if (info === undefined)
throw new BugIndicatingError(`Plugin "${record.id}" missing right after install`);
const notification = await this.reloadAndNotify({
mutation: { kind: 'install', id: record.id },
});
return { result: info, notification };
});
}
setPluginEnabled(input: SetPluginEnabledInput): Promise<void> {
return this.runNotifiedMutation(async () => {
await this.manager.setEnabled(input.id, input.enabled);
const notification = await this.reloadAndNotify({
mutation: { kind: input.enabled ? 'enable' : 'disable', id: input.id },
});
return { result: undefined, notification };
});
}
setPluginMcpServerEnabled(input: SetPluginMcpServerEnabledInput): Promise<void> {
return this.runNotifiedMutation(async () => {
await this.manager.setMcpServerEnabled(input.id, input.server, input.enabled);
const notification = await this.reloadAndNotify({
mutation: { kind: 'mcp-server', id: input.id },
});
return { result: undefined, notification };
});
}
removePlugin(input: RemovePluginInput): Promise<void> {
return this.runNotifiedMutation(async () => {
await this.manager.remove(input.id);
const notification = await this.reloadAndNotify({
mutation: { kind: 'remove', id: input.id },
});
return { result: undefined, notification };
});
}
reloadPlugins(): Promise<ReloadSummary> {
const reload = this.awaitReloadDelivery(
this.enqueueMutation(async () => {
try {
const notification = await this.reloadAndNotify();
return { result: notification.summary, notification };
} catch (error) {
this.loadError = error instanceof Error ? error : new Error(String(error));
throw new Error2(
PluginErrors.codes.PLUGIN_LOAD_FAILED,
`Failed to reload plugins: ${this.loadError.message}`,
{ cause: this.loadError, details: { kimiHomeDir: this.homeDir } },
);
}
}),
);
this.initialLoadPromise ??= reload.then(
() => undefined,
() => undefined,
);
return reload;
}
private async reloadAndNotify(options?: {
readonly mutation: PluginMutation;
}): Promise<PluginReloadNotification> {
const summary = await this.manager.reload();
this.snapshotLoaded = true;
this.loadError = undefined;
const delivery = this.onDidReloadEmitter.fireAsyncConcurrent(summary, NO_ABORT);
if (options?.mutation !== undefined)
this.onDidMutateEmitter.fire({ ...summary, mutation: options.mutation });
return { summary, delivery };
}
private runNotifiedMutation<T>(operation: () => Promise<PluginMutationOutcome<T>>): Promise<T> {
return this.awaitReloadDelivery(this.runSerializedOperation(operation));
}
private async awaitReloadDelivery<T>(operation: Promise<PluginMutationOutcome<T>>): Promise<T> {
const { result, notification } = await operation;
await notification.delivery;
return result;
}
getPluginInfo(input: GetPluginInfoInput): Promise<PluginInfo> {
return this.runManagementRead(async () => {
const info = this.manager.info(input.id);
if (info === undefined) {
throw new Error2(
PluginErrors.codes.PLUGIN_NOT_FOUND,
`Plugin "${input.id}" is not installed`,
{ details: { id: input.id } },
);
}
return info;
});
}
listPluginCommands(): Promise<readonly PluginCommandDef[]> {
return this.runSerializedOperation(async () => this.manager.enabledCommands());
}
checkUpdates(): Promise<readonly PluginUpdateStatus[]> {
return this.runManagementRead(async () => this.manager.checkUpdates());
}
pluginSkillRoots(): Promise<readonly SkillRoot[]> {
return this.runConsumptionRead([], async () => this.manager.pluginSkillRoots());
}
pluginAgentRoots(): Promise<readonly PluginAgentRoot[]> {
return this.runConsumptionRead([], async () => this.manager.pluginAgentRoots());
}
enabledSessionStarts(): Promise<readonly EnabledPluginSessionStart[]> {
return this.runConsumptionRead([], async () => this.manager.enabledSessionStarts());
}
enabledSystemPrompts(): Promise<readonly EnabledPluginSystemPrompt[]> {
return this.runConsumptionRead([], async () => this.manager.enabledSystemPrompts());
}
enabledMcpServers(): Promise<Record<string, McpServerConfig>> {
return this.runConsumptionRead({}, async () => {
const pluginServers = this.manager.enabledMcpServers();
if (!Object.values(pluginServers).some((server) => server.transport === 'stdio')) {
return pluginServers;
}
const managedEnv = await this.managedKimiCodeEnvForPlugins();
return withManagedKimiPluginEnv(pluginServers, managedEnv);
});
}
mcpServerEntries(): Promise<readonly PluginMcpServerEntry[]> {
return this.runManagementRead(async () => {
const entries = this.manager.mcpServerEntries();
if (!entries.some((entry) => entry.config.transport === 'stdio')) {
return entries;
}
const managedEnv = await this.managedKimiCodeEnvForPlugins();
return withManagedKimiPluginEnvOnEntries(entries, managedEnv);
});
}
enabledHooks(): Promise<readonly HookDef[]> {
return this.runConsumptionRead([], async () => this.manager.enabledHooks());
}
hasLoadedSnapshot(): boolean {
return this.snapshotLoaded;
}
private runSerializedOperation<T>(operation: () => Promise<T>): Promise<T> {
void this.startInitialLoad();
return this.enqueueMutation(async () => {
this.assertLoaded();
return operation();
});
}
private async runManagementRead<T>(operation: () => Promise<T>): Promise<T> {
await this.waitForPendingMutations();
this.assertLoaded();
return operation();
}
private async runConsumptionRead<T>(fallback: T, operation: () => Promise<T>): Promise<T> {
await this.waitForPendingMutations();
if (!this.snapshotLoaded) return fallback;
return operation();
}
private async waitForPendingMutations(): Promise<void> {
void this.startInitialLoad();
await this.mutationQueue;
}
private startInitialLoad(): Promise<void> {
this.initialLoadPromise ??= this.enqueueMutation(async () => {
await this.loadOnce();
});
return this.initialLoadPromise;
}
private async loadOnce(): Promise<void> {
try {
await this.manager.load();
this.snapshotLoaded = true;
this.loadError = undefined;
} catch (error) {
this.loadError = error instanceof Error ? error : new Error(String(error));
}
}
private enqueueMutation<T>(operation: () => Promise<T>): Promise<T> {
const result = this.mutationQueue.then(operation);
this.mutationQueue = result.then(
() => undefined,
() => undefined,
);
return result;
}
private assertLoaded(): void {
if (this.loadError === undefined) return;
throw new Error2(
PluginErrors.codes.PLUGIN_LOAD_FAILED,
`Plugin state failed to load: ${this.loadError.message}. ` +
`Fix the file at ${this.homeDir}/plugins/installed.json and run /plugins reload.`,
{ cause: this.loadError, details: { kimiHomeDir: this.homeDir } },
);
}
private async managedKimiCodeEnvForPlugins(): Promise<Record<string, string>> {
await this.providers.ready;
const provider = this.providers.get(KIMI_CODE_PROVIDER_NAME);
const envBaseUrl = this.envBaseUrl;
const envOAuthHost = this.envOAuthHost;
const hasEnvOverride = envBaseUrl !== undefined || envOAuthHost !== undefined;
const baseUrl = envBaseUrl !== undefined ? envBaseUrl.replace(/\/+$/, '') : provider?.baseUrl;
const oauthHost = hasEnvOverride ? envOAuthHost : provider?.oauth?.oauthHost;
const env: Record<string, string> = {};
if (baseUrl !== undefined) env[KIMI_CODE_BASE_URL_ENV] = baseUrl;
if (oauthHost !== undefined) env[KIMI_CODE_OAUTH_HOST_ENV] = oauthHost;
return env;
}
}
function withManagedKimiPluginEnv(
pluginServers: Record<string, McpServerConfig>,
managedEnv: Record<string, string>,
): Record<string, McpServerConfig> {
if (Object.keys(managedEnv).length === 0) return pluginServers;
const out: Record<string, McpServerConfig> = {};
for (const [name, server] of Object.entries(pluginServers)) {
out[name] =
server.transport === 'stdio' ? { ...server, env: { ...server.env, ...managedEnv } } : server;
}
return out;
}
function withManagedKimiPluginEnvOnEntries(
entries: readonly PluginMcpServerEntry[],
managedEnv: Record<string, string>,
): readonly PluginMcpServerEntry[] {
if (Object.keys(managedEnv).length === 0) return entries;
return entries.map((entry) =>
entry.config.transport === 'stdio'
? { ...entry, config: { ...entry.config, env: { ...entry.config.env, ...managedEnv } } }
: entry,
);
}
registerScopedService(
LifecycleScope.App,
IPluginService,
PluginService,
ScopeActivation.OnScopeCreated,
'plugin',
);