diff --git a/.changeset/constructable-agent-model-provider.md b/.changeset/constructable-agent-model-provider.md new file mode 100644 index 000000000..e6bd10507 --- /dev/null +++ b/.changeset/constructable-agent-model-provider.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Introduce `ModelProvider` interface and `SingleModelProvider` to decouple `Agent` from `ProviderManager`. diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts index d3d008a46..34c699aa0 100644 --- a/packages/agent-core/src/agent/compaction/full.ts +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -61,7 +61,7 @@ export class FullCompaction { { ...DEFAULT_COMPACTION_CONFIG, reservedContextSize: - agent.providerManager?.config.loopControl?.reservedContextSize ?? + agent.kimiConfig?.loopControl?.reservedContextSize ?? DEFAULT_COMPACTION_CONFIG.reservedContextSize, } ); diff --git a/packages/agent-core/src/agent/config/index.ts b/packages/agent-core/src/agent/config/index.ts index 653c99fe7..83d8c20c2 100644 --- a/packages/agent-core/src/agent/config/index.ts +++ b/packages/agent-core/src/agent/config/index.ts @@ -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 { diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 8c6cacf80..f9b0d15f6 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -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; 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; 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 { diff --git a/packages/agent-core/src/agent/permission/index.ts b/packages/agent-core/src/agent/permission/index.ts index 6acfa450a..ddd51d529 100644 --- a/packages/agent-core/src/agent/permission/index.ts +++ b/packages/agent-core/src/agent/permission/index.ts @@ -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 = diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index 1d255d079..77c7dde3f 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -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); diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index e4a7b1c44..c14a4727f 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -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), diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index cba2de508..7e78d3103 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -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'; diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 5e3d77ea4..222872f27 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -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()), diff --git a/packages/agent-core/src/session/provider-manager.ts b/packages/agent-core/src/session/provider-manager.ts index 402d9ed43..c0e391790 100644 --- a/packages/agent-core/src/session/provider-manager.ts +++ b/packages/agent-core/src/session/provider-manager.ts @@ -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 = ( request: (auth: ProviderRequestAuth) => Promise, ) => Promise; -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 { diff --git a/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts index 1c71d7ce3..9ee7dc778 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts @@ -104,7 +104,7 @@ export class AskUserQuestionTool implements BuiltinTool { }: ExecutableToolContext, ): Promise { try { - const result = await this.agent.rpc!.requestQuestion( + const result = await this.agent.rpc!.requestQuestion!( { turnId: numericTurnId(turnId), toolCallId, diff --git a/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts index b7350e3c2..db40cdfd2 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts @@ -105,7 +105,7 @@ export class SkillTool implements BuiltinTool { } 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); diff --git a/packages/agent-core/test/agent/compaction.test.ts b/packages/agent-core/test/agent/compaction.test.ts index d82acee4f..87e955ae3 100644 --- a/packages/agent-core/test/agent/compaction.test.ts +++ b/packages/agent-core/test/agent/compaction.test.ts @@ -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) => ({ diff --git a/packages/agent-core/test/agent/harness/agent.ts b/packages/agent-core/test/agent/harness/agent.ts index b38f682c1..481a2de7e 100644 --- a/packages/agent-core/test/agent/harness/agent.ts +++ b/packages/agent-core/test/agent/harness/agent.ts @@ -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, diff --git a/packages/agent-core/test/agent/question.test.ts b/packages/agent-core/test/agent/question.test.ts index aef540c08..624d25b62 100644 --- a/packages/agent-core/test/agent/question.test.ts +++ b/packages/agent-core/test/agent/question.test.ts @@ -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: [ { diff --git a/packages/agent-core/test/agent/skill-tool-manager.test.ts b/packages/agent-core/test/agent/skill-tool-manager.test.ts index 06eba5b81..4eff1dc68 100644 --- a/packages/agent-core/test/agent/skill-tool-manager.test.ts +++ b/packages/agent-core/test/agent/skill-tool-manager.test.ts @@ -51,7 +51,7 @@ function makeAgent( rpc, skills, persistence, - providerManager: testProviderManager(), + modelProvider: testProviderManager(), }); agent.config.update({ cwd: process.cwd(), diff --git a/packages/agent-core/test/harness/runtime-provider.test.ts b/packages/agent-core/test/harness/runtime-provider.test.ts index ece99af8f..a0b3e8c7c 100644 --- a/packages/agent-core/test/harness/runtime-provider.test.ts +++ b/packages/agent-core/test/harness/runtime-provider.test.ts @@ -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',