mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(agent-core): ModelProvider interface and SingleModelProvider (#167)
This commit is contained in:
parent
eb93fdfe4a
commit
b5981a523b
17 changed files with 91 additions and 54 deletions
6
.changeset/constructable-agent-model-provider.md
Normal file
6
.changeset/constructable-agent-model-provider.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Introduce `ModelProvider` interface and `SingleModelProvider` to decouple `Agent` from `ProviderManager`.
|
||||
|
|
@ -61,7 +61,7 @@ export class FullCompaction {
|
|||
{
|
||||
...DEFAULT_COMPACTION_CONFIG,
|
||||
reservedContextSize:
|
||||
agent.providerManager?.config.loopControl?.reservedContextSize ??
|
||||
agent.kimiConfig?.loopControl?.reservedContextSize ??
|
||||
DEFAULT_COMPACTION_CONFIG.reservedContextSize,
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class ConfigState {
|
|||
|
||||
constructor(protected readonly agent: Agent) {
|
||||
this._cwd = agent.runtime.kaos.getcwd();
|
||||
this._modelAlias = agent.modelProvider?.defaultModel;
|
||||
}
|
||||
|
||||
update(changed: AgentConfigUpdateData): void {
|
||||
|
|
@ -50,7 +51,7 @@ export class ConfigState {
|
|||
if (changed.thinkingLevel !== undefined) {
|
||||
this._thinkingLevel = resolveThinkingEffort(
|
||||
changed.thinkingLevel,
|
||||
this.agent.providerManager?.config.thinking,
|
||||
this.agent.kimiConfig?.thinking,
|
||||
);
|
||||
}
|
||||
if (changed.systemPrompt !== undefined) {
|
||||
|
|
@ -128,7 +129,7 @@ export class ConfigState {
|
|||
|
||||
private get resolvedProviderConfig(): ResolvedRuntimeProvider | undefined {
|
||||
if (this._modelAlias === undefined) return undefined;
|
||||
return this.agent.providerManager?.resolveProviderConfig(this._modelAlias);
|
||||
return this.agent.modelProvider?.resolveProviderConfig(this._modelAlias);
|
||||
}
|
||||
|
||||
private tryResolvedProviderConfig(): ResolvedRuntimeProvider | undefined {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type { EnabledPluginSessionStart } from '#/plugin';
|
|||
|
||||
import type { McpConnectionManager } from '../mcp';
|
||||
import type { PreparedSystemPromptContext, ResolvedAgentProfile } from '../profile';
|
||||
import type { ProviderManager } from '../session/provider-manager';
|
||||
import type { ModelProvider } from '../session/provider-manager';
|
||||
import type { RuntimeConfig } from '../runtime-types';
|
||||
import type { SessionSubagentHost } from '../session/subagent-host';
|
||||
import type { SkillRegistry } from '../skill';
|
||||
|
|
@ -64,17 +64,16 @@ export interface AgentOptions {
|
|||
readonly runtime: RuntimeConfig;
|
||||
readonly config?: KimiConfig;
|
||||
readonly homedir?: string;
|
||||
readonly rpc?: SDKAgentRPC;
|
||||
readonly rpc?: Partial<SDKAgentRPC>;
|
||||
readonly persistence?: AgentRecordPersistence;
|
||||
readonly type?: AgentType;
|
||||
readonly generate?: typeof generate;
|
||||
readonly compactionStrategy?: CompactionStrategy;
|
||||
readonly providerManager?: ProviderManager | undefined;
|
||||
readonly modelProvider?: ModelProvider | undefined;
|
||||
readonly subagentHost?: SessionSubagentHost | undefined;
|
||||
readonly skills?: SkillRegistry;
|
||||
readonly mcp?: McpConnectionManager;
|
||||
readonly hookEngine?: HookEngine;
|
||||
readonly cronSessionDir?: string;
|
||||
readonly permission?: PermissionManagerOptions | undefined;
|
||||
readonly log?: Logger;
|
||||
readonly telemetry?: TelemetryClient | undefined;
|
||||
|
|
@ -82,20 +81,20 @@ export interface AgentOptions {
|
|||
}
|
||||
|
||||
export class Agent {
|
||||
readonly type: AgentType;
|
||||
readonly runtime: RuntimeConfig;
|
||||
readonly kimiConfig?: KimiConfig;
|
||||
readonly homedir?: string;
|
||||
readonly skills?: SkillManager;
|
||||
readonly rpc?: Partial<SDKAgentRPC>;
|
||||
readonly pluginSessionStarts: readonly EnabledPluginSessionStart[];
|
||||
readonly rawGenerate: typeof generate;
|
||||
readonly rpc?: SDKAgentRPC;
|
||||
readonly modelProvider?: ModelProvider;
|
||||
readonly subagentHost?: SessionSubagentHost;
|
||||
readonly mcp?: McpConnectionManager;
|
||||
readonly hooks?: HookEngine;
|
||||
readonly log: Logger;
|
||||
readonly telemetry: TelemetryClient;
|
||||
readonly providerManager: ProviderManager | undefined;
|
||||
readonly subagentHost: SessionSubagentHost | undefined;
|
||||
readonly mcp: McpConnectionManager | undefined;
|
||||
readonly hooks: HookEngine | undefined;
|
||||
|
||||
readonly type: AgentType;
|
||||
readonly blobStore: BlobStore | undefined;
|
||||
readonly records: AgentRecords;
|
||||
readonly fullCompaction: FullCompaction;
|
||||
|
|
@ -106,31 +105,29 @@ export class Agent {
|
|||
readonly permission: PermissionManager;
|
||||
readonly planMode: PlanMode;
|
||||
readonly usage: UsageRecorder;
|
||||
readonly skills: SkillManager | null;
|
||||
readonly tools: ToolManager;
|
||||
readonly background: BackgroundManager;
|
||||
readonly cron: CronManager | null;
|
||||
readonly replayBuilder: ReplayBuilder;
|
||||
readonly log: Logger;
|
||||
|
||||
private lastLlmConfigLogSignature?: string;
|
||||
|
||||
constructor(options: AgentOptions) {
|
||||
this.log = options.log ?? log;
|
||||
this.kimiConfig = options.config;
|
||||
this.type = options.type ?? 'main';
|
||||
this.runtime = options.runtime;
|
||||
this.kimiConfig = options.config;
|
||||
this.homedir = options.homedir;
|
||||
if (options.skills !== undefined) {
|
||||
this.skills = new SkillManager(this, options.skills);
|
||||
}
|
||||
this.rpc = options.rpc;
|
||||
this.pluginSessionStarts = options.pluginSessionStarts ?? [];
|
||||
this.rawGenerate = options.generate ?? generate;
|
||||
this.providerManager = options.providerManager;
|
||||
this.modelProvider = options.modelProvider;
|
||||
this.subagentHost = options.subagentHost;
|
||||
this.mcp = options.mcp;
|
||||
this.hooks = options.hookEngine;
|
||||
this.type = options.type ?? 'main';
|
||||
this.rpc = options.rpc;
|
||||
this.log = options.log ?? log;
|
||||
this.telemetry = options.telemetry ?? noopTelemetryClient;
|
||||
|
||||
this.blobStore = options.homedir
|
||||
? new BlobStore({ blobsDir: join(options.homedir, 'blobs') })
|
||||
: undefined;
|
||||
|
|
@ -154,6 +151,7 @@ export class Agent {
|
|||
this.permission = new PermissionManager(this, options.permission);
|
||||
this.planMode = new PlanMode(this);
|
||||
this.usage = new UsageRecorder(this);
|
||||
this.skills = options.skills ? new SkillManager(this, options.skills) : null;
|
||||
this.tools = new ToolManager(this);
|
||||
this.background = new BackgroundManager(this);
|
||||
this.cron = this.type === 'sub' ? null : new CronManager(this);
|
||||
|
|
@ -170,7 +168,7 @@ export class Agent {
|
|||
const withAuth =
|
||||
modelAlias === undefined
|
||||
? undefined
|
||||
: this.providerManager?.createAuthResolverForModel(modelAlias, { log: this.log });
|
||||
: this.modelProvider?.resolveAuth?.(modelAlias, { log: this.log });
|
||||
if (withAuth === undefined) {
|
||||
this.logLlmRequest(provider, systemPrompt, tools, history, options);
|
||||
return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, options);
|
||||
|
|
@ -186,7 +184,7 @@ export class Agent {
|
|||
get llm(): KosongLLM {
|
||||
const model = this.config.model;
|
||||
const provider = this.config.provider.withThinking(this.config.thinkingLevel);
|
||||
const loopControl = this.providerManager?.config.loopControl;
|
||||
const loopControl = this.kimiConfig?.loopControl;
|
||||
const completionBudgetConfig = resolveCompletionBudget({
|
||||
reservedContextSize: loopControl?.reservedContextSize,
|
||||
});
|
||||
|
|
@ -299,7 +297,7 @@ export class Agent {
|
|||
// Validate the alias resolves before recording it so resume / runtime
|
||||
// callers fail fast on missing aliases instead of deferring to the
|
||||
// next prompt.
|
||||
const resolved = this.providerManager?.resolveProviderConfig(payload.model);
|
||||
const resolved = this.modelProvider?.resolveProviderConfig(payload.model);
|
||||
if (this.config.modelAlias !== payload.model) {
|
||||
this.config.update({ modelAlias: payload.model });
|
||||
this.telemetry.track('model_switch', { model: payload.model });
|
||||
|
|
@ -344,7 +342,7 @@ export class Agent {
|
|||
this.context.clear();
|
||||
},
|
||||
activateSkill: (payload) => {
|
||||
if (this.skills === undefined) {
|
||||
if (this.skills === null) {
|
||||
throw new KimiError(ErrorCodes.SKILL_NOT_FOUND, `Skill "${payload.name}" was not found`);
|
||||
}
|
||||
this.skills.activate(payload);
|
||||
|
|
@ -363,7 +361,7 @@ export class Agent {
|
|||
|
||||
emitEvent(event: AgentEvent): void {
|
||||
if (this.records.restoring) return;
|
||||
void this.rpc?.emitEvent(event);
|
||||
void this.rpc?.emitEvent?.(event);
|
||||
}
|
||||
|
||||
emitStatusUpdated(): void {
|
||||
|
|
|
|||
|
|
@ -131,11 +131,7 @@ export class PermissionManager {
|
|||
const startedAt = Date.now();
|
||||
|
||||
let response: ApprovalResponse;
|
||||
if (this.agent.rpc === undefined) {
|
||||
response = {
|
||||
decision: 'approved',
|
||||
};
|
||||
} else {
|
||||
if (this.agent.rpc?.requestApproval) {
|
||||
try {
|
||||
response = await this.agent.rpc.requestApproval(
|
||||
{
|
||||
|
|
@ -163,6 +159,10 @@ export class PermissionManager {
|
|||
? Promise.reject(error)
|
||||
: this.permissionPolicyResolutionToPrepare(resolved, context, policyName);
|
||||
}
|
||||
} else {
|
||||
response = {
|
||||
decision: 'approved',
|
||||
};
|
||||
}
|
||||
|
||||
const sessionApprovalRule =
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export class ToolManager {
|
|||
|
||||
constructor(protected readonly agent: Agent) {
|
||||
this.attachMcpTools();
|
||||
if (agent.config.hasProvider) {
|
||||
this.initializeBuiltinTools();
|
||||
}
|
||||
}
|
||||
|
||||
protected get toolStore(): ToolStore {
|
||||
|
|
@ -93,7 +96,7 @@ export class ToolManager {
|
|||
return {
|
||||
approvalRule: name,
|
||||
execute: async (context) => {
|
||||
return this.agent.rpc!.toolCall(
|
||||
return this.agent.rpc!.toolCall!(
|
||||
{
|
||||
turnId: Number(context.turnId),
|
||||
toolCallId: context.toolCallId,
|
||||
|
|
@ -369,7 +372,7 @@ export class ToolManager {
|
|||
new b.ReadMediaFileTool(kaos, workspace, modelCapabilities, videoUploader),
|
||||
new b.EnterPlanModeTool(this.agent),
|
||||
new b.ExitPlanModeTool(this.agent),
|
||||
this.agent.rpc && new b.AskUserQuestionTool(this.agent),
|
||||
this.agent.rpc?.requestQuestion && new b.AskUserQuestionTool(this.agent),
|
||||
new b.TodoListTool(this.toolStore),
|
||||
new b.TaskListTool(background),
|
||||
new b.TaskOutputTool(background),
|
||||
|
|
@ -377,8 +380,7 @@ export class ToolManager {
|
|||
this.agent.cron && new b.CronCreateTool(this.agent.cron),
|
||||
this.agent.cron && new b.CronListTool(this.agent.cron),
|
||||
this.agent.cron && new b.CronDeleteTool(this.agent.cron),
|
||||
this.agent.skills !== undefined &&
|
||||
this.agent.skills.registry.listInvocableSkills().length > 0 &&
|
||||
this.agent.skills?.registry.listInvocableSkills().length &&
|
||||
new b.SkillTool(this.agent),
|
||||
this.agent.subagentHost &&
|
||||
new b.AgentTool(
|
||||
|
|
@ -403,7 +405,7 @@ export class ToolManager {
|
|||
if (uploadVideo === undefined) return undefined;
|
||||
|
||||
const modelAlias = this.agent.config.modelAlias!;
|
||||
const withAuth = this.agent.providerManager?.createAuthResolverForModel(modelAlias, {
|
||||
const withAuth = this.agent.modelProvider?.resolveAuth?.(modelAlias, {
|
||||
log: this.agent.log,
|
||||
});
|
||||
if (withAuth === undefined) return (input) => uploadVideo(input);
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ export class TurnFlow {
|
|||
while (true) {
|
||||
signal.throwIfAborted();
|
||||
const model = this.agent.config.model;
|
||||
const loopControl = this.agent.providerManager?.config.loopControl;
|
||||
const loopControl = this.agent.kimiConfig?.loopControl;
|
||||
try {
|
||||
const result = await runTurn({
|
||||
turnId: String(turnId),
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ export type {
|
|||
BackgroundTaskStatus,
|
||||
} from './tools/background/manager';
|
||||
export type { RuntimeConfig } from './runtime-types';
|
||||
export { SingleModelProvider } from './session/provider-manager';
|
||||
export type {
|
||||
BearerTokenProvider,
|
||||
ModelProvider,
|
||||
OAuthTokenProviderResolver,
|
||||
} from './session/provider-manager';
|
||||
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ export class Session {
|
|||
homedir,
|
||||
skills: this.skills,
|
||||
rpc: proxyWithExtraPayload(this.rpc, { agentId: id }),
|
||||
providerManager: this.options.providerManager,
|
||||
modelProvider: this.options.providerManager,
|
||||
hookEngine: config.hookEngine ?? this.hookEngine,
|
||||
subagentHost:
|
||||
config.subagentHost ?? new SessionSubagentHost(this, id, this.backgroundTaskTimeoutMs()),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export type OAuthTokenProviderResolver = (
|
|||
) => BearerTokenProvider | undefined;
|
||||
|
||||
export interface ResolvedRuntimeProvider {
|
||||
readonly modelName: string;
|
||||
readonly providerName: string;
|
||||
readonly provider: KosongProviderConfig;
|
||||
readonly modelCapabilities: ModelCapability;
|
||||
|
|
@ -31,10 +30,41 @@ type AuthorizedRequest = <T>(
|
|||
request: (auth: ProviderRequestAuth) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
|
||||
export class ProviderManager {
|
||||
export interface ModelProvider {
|
||||
readonly defaultModel?: string;
|
||||
resolveProviderConfig(model: string): ResolvedRuntimeProvider;
|
||||
resolveAuth?(model: string, options?: { readonly log?: Logger }): AuthorizedRequest | undefined;
|
||||
}
|
||||
|
||||
export class SingleModelProvider implements ModelProvider {
|
||||
constructor(
|
||||
private readonly providerConfig: KosongProviderConfig,
|
||||
private readonly modelCapabilities: ModelCapability = UNKNOWN_CAPABILITY,
|
||||
) {}
|
||||
|
||||
get defaultModel(): string {
|
||||
return this.providerConfig.model;
|
||||
}
|
||||
|
||||
resolveProviderConfig(model: string): ResolvedRuntimeProvider {
|
||||
if (model !== this.providerConfig.model) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.CONFIG_INVALID,
|
||||
`Model "${model}" is not supported by SingleModelProvider.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
modelCapabilities: this.modelCapabilities,
|
||||
providerName: 'single-model-provider',
|
||||
provider: this.providerConfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProviderManager implements ModelProvider {
|
||||
constructor(private readonly options: ProviderManagerOptions) {}
|
||||
|
||||
get config(): KimiConfig {
|
||||
private get config(): KimiConfig {
|
||||
const { config } = this.options;
|
||||
return typeof config === 'function' ? config() : config;
|
||||
}
|
||||
|
|
@ -81,14 +111,13 @@ export class ProviderManager {
|
|||
);
|
||||
|
||||
return {
|
||||
modelName: model,
|
||||
providerName,
|
||||
provider,
|
||||
modelCapabilities: resolveModelCapabilities(alias, provider),
|
||||
};
|
||||
}
|
||||
|
||||
createAuthResolverForModel(
|
||||
resolveAuth(
|
||||
model: string,
|
||||
options?: { readonly log?: Logger },
|
||||
): AuthorizedRequest | undefined {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export class AskUserQuestionTool implements BuiltinTool<AskUserQuestionInput> {
|
|||
}: ExecutableToolContext,
|
||||
): Promise<ExecutableToolResult> {
|
||||
try {
|
||||
const result = await this.agent.rpc!.requestQuestion(
|
||||
const result = await this.agent.rpc!.requestQuestion!(
|
||||
{
|
||||
turnId: numericTurnId(turnId),
|
||||
toolCallId,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export class SkillTool implements BuiltinTool<SkillToolInput> {
|
|||
}
|
||||
|
||||
const skills = this.agent.skills;
|
||||
if (skills === undefined) {
|
||||
if (skills === null) {
|
||||
return errorResult(`Skill "${args.skill}" not found in the current skill listing.`);
|
||||
}
|
||||
const skill = skills.registry.getSkill(args.skill);
|
||||
|
|
|
|||
|
|
@ -1309,7 +1309,7 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
const providerManager = ctx.agent.providerManager;
|
||||
const providerManager = ctx.agent.modelProvider;
|
||||
if (providerManager === undefined) throw new Error('Expected provider manager');
|
||||
const resolveProviderConfig = providerManager.resolveProviderConfig.bind(providerManager);
|
||||
providerManager.resolveProviderConfig = (model) => ({
|
||||
|
|
|
|||
|
|
@ -177,11 +177,12 @@ export class AgentTestContext {
|
|||
);
|
||||
this.agent = new Agent({
|
||||
runtime,
|
||||
config: this.kimiConfig,
|
||||
rpc: this.createRpcProxy(),
|
||||
persistence,
|
||||
generate: options.generate ?? this.scriptedGenerate.generate,
|
||||
compactionStrategy: options.compactionStrategy,
|
||||
providerManager,
|
||||
modelProvider: providerManager,
|
||||
subagentHost: options.subagentHost,
|
||||
type: options.type,
|
||||
permission: options.permission,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ describe('Agent question', () => {
|
|||
it('roundtrips a question request through wire rpc', async () => {
|
||||
const ctx = testAgent();
|
||||
|
||||
const resultPromise = ctx.agent.rpc!.requestQuestion(
|
||||
const resultPromise = ctx.agent.rpc!.requestQuestion!(
|
||||
{
|
||||
questions: [
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@ describe('Agent question', () => {
|
|||
it('sends multiple questions in one request', async () => {
|
||||
const ctx = testAgent();
|
||||
|
||||
const resultPromise = ctx.agent.rpc!.requestQuestion(
|
||||
const resultPromise = ctx.agent.rpc!.requestQuestion!(
|
||||
{
|
||||
questions: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function makeAgent(
|
|||
rpc,
|
||||
skills,
|
||||
persistence,
|
||||
providerManager: testProviderManager(),
|
||||
modelProvider: testProviderManager(),
|
||||
});
|
||||
agent.config.update({
|
||||
cwd: process.cwd(),
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ describe('resolveRuntimeProvider model metadata', () => {
|
|||
tool_use: true,
|
||||
max_context_tokens: 1_000_000,
|
||||
});
|
||||
expect(resolved.modelName).toBe('kimi-code/kimi-for-coding');
|
||||
expect(resolved.provider.model).toBe('kimi-for-coding');
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +96,6 @@ describe('resolveRuntimeProvider model metadata', () => {
|
|||
});
|
||||
|
||||
expect(resolved.providerName).toBe('openai');
|
||||
expect(resolved.modelName).toBe('gpt-alias');
|
||||
expect(resolved.provider).toMatchObject({
|
||||
type: 'openai',
|
||||
model: 'gpt-runtime',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue