Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/constructable-agent-model-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---

Introduce `ModelProvider` interface and `SingleModelProvider` to decouple `Agent` from `ProviderManager`.
2 changes: 1 addition & 1 deletion packages/agent-core/src/agent/compaction/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class FullCompaction {
{
...DEFAULT_COMPACTION_CONFIG,
reservedContextSize:
agent.providerManager?.config.loopControl?.reservedContextSize ??
agent.kimiConfig?.loopControl?.reservedContextSize ??
DEFAULT_COMPACTION_CONFIG.reservedContextSize,
}
);
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-core/src/agent/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class ConfigState {

constructor(protected readonly agent: Agent) {
this._cwd = agent.runtime.kaos.getcwd();
this._modelAlias = agent.modelProvider?.defaultModel;
Comment thread
kermanx marked this conversation as resolved.
}

update(changed: AgentConfigUpdateData): void {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 23 additions & 25 deletions packages/agent-core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -64,38 +64,37 @@ 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;
Comment thread
kermanx marked this conversation as resolved.
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;
readonly pluginSessionStarts?: readonly EnabledPluginSessionStart[];
}

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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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,
});
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions packages/agent-core/src/agent/permission/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down Expand Up @@ -163,6 +159,10 @@ export class PermissionManager {
? Promise.reject(error)
: this.permissionPolicyResolutionToPrepare(resolved, context, policyName);
}
} else {
response = {
decision: 'approved',
};
}

const sessionApprovalRule =
Expand Down
12 changes: 7 additions & 5 deletions packages/agent-core/src/agent/tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export class ToolManager {

constructor(protected readonly agent: Agent) {
this.attachMcpTools();
if (agent.config.hasProvider) {
this.initializeBuiltinTools();
Comment thread
kermanx marked this conversation as resolved.
}
}

protected get toolStore(): ToolStore {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -369,16 +372,15 @@ 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),
new b.TaskStopTool(background),
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(
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/agent/turn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
kermanx marked this conversation as resolved.
try {
const result = await runTurn({
turnId: String(turnId),
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
39 changes: 34 additions & 5 deletions packages/agent-core/src/session/provider-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export type OAuthTokenProviderResolver = (
) => BearerTokenProvider | undefined;

export interface ResolvedRuntimeProvider {
readonly modelName: string;
readonly providerName: string;
readonly provider: KosongProviderConfig;
readonly modelCapabilities: ModelCapability;
Expand All @@ -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 {
Comment thread
kermanx marked this conversation as resolved.
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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/test/agent/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
3 changes: 2 additions & 1 deletion packages/agent-core/test/agent/harness/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading