diff --git a/apps/kimi-code/test/utils/plugin-marketplace.test.ts b/apps/kimi-code/test/utils/plugin-marketplace.test.ts index 24bedb251..0df71a811 100644 --- a/apps/kimi-code/test/utils/plugin-marketplace.test.ts +++ b/apps/kimi-code/test/utils/plugin-marketplace.test.ts @@ -112,7 +112,11 @@ describe('loadPluginMarketplace', () => { }), })) as unknown as typeof fetch; - const marketplace = await loadPluginMarketplace({ workDir: '/tmp/work', fetchImpl }); + const marketplace = await loadPluginMarketplace({ + workDir: '/tmp/work', + source: KIMI_CODE_PLUGIN_MARKETPLACE_URL, + fetchImpl, + }); expect(fetchImpl).toHaveBeenCalledWith(KIMI_CODE_PLUGIN_MARKETPLACE_URL); expect(marketplace.plugins[0]).toEqual( diff --git a/packages/agent-core/src/agent/config/index.ts b/packages/agent-core/src/agent/config/index.ts index 3e22f39ce..653c99fe7 100644 --- a/packages/agent-core/src/agent/config/index.ts +++ b/packages/agent-core/src/agent/config/index.ts @@ -7,9 +7,10 @@ import { } from '@moonshot-ai/kosong'; import type { Agent } from '..'; -import type { ResolvedRuntimeProvider } from '../../providers/runtime-provider'; +import { ErrorCodes, KimiError } from '../../errors'; import type { AgentConfigData, AgentConfigUpdateData } from './types'; import { resolveThinkingEffort, type ThinkingEffort } from './thinking'; +import type { ResolvedRuntimeProvider } from '../../session/provider-manager'; export * from './types'; export { resolveThinkingEffort, type ThinkingEffort } from './thinking'; @@ -25,16 +26,9 @@ export class ConfigState { this._cwd = agent.runtime.kaos.getcwd(); } - update(input: AgentConfigUpdateData): void { - const changed = { ...input }; + update(changed: AgentConfigUpdateData): void { if (Object.keys(changed).length === 0) return; - if (changed.thinkingLevel !== undefined) { - changed.thinkingLevel = resolveThinkingEffort( - changed.thinkingLevel, - this.agent.providerManager?.config.thinking, - ); - } this.agent.records.logRecord({ type: 'config.update', ...changed, @@ -47,14 +41,22 @@ export class ConfigState { this._cwd = changed.cwd; void this.agent.runtime.kaos.chdir(changed.cwd); } - if (Object.hasOwn(changed, 'modelAlias')) { - this._modelAlias = changed.modelAlias ?? undefined; + if (changed.modelAlias) { + this._modelAlias = changed.modelAlias; } - if (Object.hasOwn(changed, 'profileName')) this._profileName = changed.profileName; - if (changed.thinkingLevel !== undefined) - this._thinkingLevel = changed.thinkingLevel as ThinkingEffort; - if (changed.systemPrompt !== undefined) this._systemPrompt = changed.systemPrompt; - if (this.hasProvider && (changed.cwd !== undefined || Object.hasOwn(changed, 'modelAlias'))) { + if (changed.profileName) { + this._profileName = changed.profileName; + } + if (changed.thinkingLevel !== undefined) { + this._thinkingLevel = resolveThinkingEffort( + changed.thinkingLevel, + this.agent.providerManager?.config.thinking, + ); + } + if (changed.systemPrompt !== undefined) { + this._systemPrompt = changed.systemPrompt; + } + if (this.hasProvider && (changed.cwd !== undefined || changed.modelAlias)) { this.agent.tools.initializeBuiltinTools(); } this.agent.emitStatusUpdated(); @@ -88,7 +90,7 @@ export class ConfigState { get providerConfig(): ProviderConfig { const provider = this.resolvedProviderConfig?.provider; if (provider === undefined) { - throw new Error('Provider not set'); + throw new KimiError(ErrorCodes.MODEL_NOT_CONFIGURED, 'Provider not set'); } return provider; } @@ -99,7 +101,7 @@ export class ConfigState { get model(): string { if (this._modelAlias === undefined) { - throw new Error('Model not set'); + throw new KimiError(ErrorCodes.MODEL_NOT_CONFIGURED, 'Model not set'); } return this._modelAlias; } @@ -126,7 +128,7 @@ export class ConfigState { private get resolvedProviderConfig(): ResolvedRuntimeProvider | undefined { if (this._modelAlias === undefined) return undefined; - return this.agent.providerManager?.resolveProviderConfigForModel(this._modelAlias); + return this.agent.providerManager?.resolveProviderConfig(this._modelAlias); } private tryResolvedProviderConfig(): ResolvedRuntimeProvider | undefined { diff --git a/packages/agent-core/src/agent/config/thinking.ts b/packages/agent-core/src/agent/config/thinking.ts index f6a7d924a..1e206cd19 100644 --- a/packages/agent-core/src/agent/config/thinking.ts +++ b/packages/agent-core/src/agent/config/thinking.ts @@ -8,6 +8,25 @@ const DEFAULT_THINKING_EFFORT: ThinkingEffort = 'high'; const THINKING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']); +export interface ResolveThinkingLevelOptions { + readonly defaultThinking?: boolean | undefined; + readonly thinking?: ThinkingConfig | undefined; +} + +export function resolveThinkingLevel( + requestedThinking: string | undefined, + options: ResolveThinkingLevelOptions, +): ThinkingEffort { + const resolvedRequest = + requestedThinking !== undefined && requestedThinking.trim().length > 0 + ? requestedThinking + : options.defaultThinking === false + ? 'off' + : undefined; + + return resolveThinkingEffort(resolvedRequest, options.thinking); +} + export function resolveThinkingEffort( requested: string | undefined, defaults: ThinkingConfig | undefined, diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 662c13fae..0c01d7c83 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -16,8 +16,7 @@ import type { EnabledPluginSessionStart } from '#/plugin'; import type { McpConnectionManager } from '../mcp'; import type { PreparedSystemPromptContext, ResolvedAgentProfile } from '../profile'; -import type { ProviderManager } from '../providers/provider-manager'; -import { withProviderRequestAuth } from '../providers/request-auth'; +import type { ProviderManager } from '../session/provider-manager'; import type { RuntimeConfig } from '../runtime-types'; import type { SessionSubagentHost } from '../session/subagent-host'; import type { SkillRegistry } from '../skill'; @@ -64,20 +63,18 @@ export interface AgentConfig { readonly runtime: RuntimeConfig; readonly homedir?: string; readonly skills?: SkillRegistry; - readonly rpc: SDKAgentRPC; + readonly rpc?: SDKAgentRPC; readonly persistence?: AgentRecordPersistence; readonly type?: AgentType; readonly generate?: typeof generate; readonly compactionStrategy?: CompactionStrategy; readonly providerManager?: ProviderManager | undefined; - readonly sessionId?: string; readonly subagentHost?: SessionSubagentHost | undefined; readonly mcp?: McpConnectionManager; readonly hookEngine?: HookEngine; readonly backgroundMaxRunningTasks?: number; readonly backgroundSessionDir?: string; readonly permission?: PermissionManagerOptions | undefined; - /** Parent logger; the agent appends its own ctx (agentId already bound by session). */ readonly log?: Logger; readonly telemetry?: TelemetryClient | undefined; readonly pluginSessionStarts?: readonly EnabledPluginSessionStart[]; @@ -89,7 +86,7 @@ export class Agent { readonly skills?: SkillManager; readonly pluginSessionStarts: readonly EnabledPluginSessionStart[]; readonly rawGenerate: typeof generate; - readonly rpc: SDKAgentRPC; + readonly rpc?: SDKAgentRPC; readonly telemetry: TelemetryClient; readonly providerManager: ProviderManager | undefined; readonly subagentHost: SessionSubagentHost | undefined; @@ -123,10 +120,7 @@ export class Agent { } this.pluginSessionStarts = config.pluginSessionStarts ?? []; this.rawGenerate = config.generate ?? generate; - this.providerManager = - config.sessionId === undefined - ? config.providerManager - : config.providerManager?.withPromptCacheKey(config.sessionId); + this.providerManager = config.providerManager; this.subagentHost = config.subagentHost; this.mcp = config.mcp; this.hooks = config.hookEngine; @@ -173,14 +167,16 @@ export class Agent { return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, options); } const modelAlias = this.config.modelAlias; - const resolveAuth = + const withAuth = modelAlias === undefined ? undefined - : this.providerManager?.createAuthResolverForModel(modelAlias, { - log: this.log, - }); - return withProviderRequestAuth(resolveAuth, (auth) => { - const requestOptions = auth === undefined ? options : { ...options, auth }; + : this.providerManager?.createAuthResolverForModel(modelAlias, { log: this.log }); + if (withAuth === undefined) { + this.logLlmRequest(provider, systemPrompt, tools, history, options); + return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, options); + } + return withAuth((auth) => { + const requestOptions = { ...options, auth }; this.logLlmRequest(provider, systemPrompt, tools, history, requestOptions); return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, requestOptions); }); @@ -298,21 +294,18 @@ export class Agent { this.telemetry.track('afk_toggle', { enabled: afkEnabled }); } }, - setModel: async (payload) => { - const previous = this.config.modelAlias; - const resolved = await this.providerManager?.resolveProviderForModel(payload.model); - if (resolved === undefined) { - throw new Error('Runtime provider model cannot be empty'); - } - this.config.update({ - modelAlias: resolved.modelName, - }); - if (previous !== resolved.modelName) { - this.telemetry.track('model_switch', { model: resolved.modelName }); + setModel: (payload) => { + // 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); + if (this.config.modelAlias !== payload.model) { + this.config.update({ modelAlias: payload.model }); + this.telemetry.track('model_switch', { model: payload.model }); } return { - model: resolved.modelName, - providerName: resolved.providerName, + model: payload.model, + providerName: resolved?.providerName, }; }, getModel: () => { @@ -369,7 +362,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 9450aaf77..6acfa450a 100644 --- a/packages/agent-core/src/agent/permission/index.ts +++ b/packages/agent-core/src/agent/permission/index.ts @@ -131,32 +131,38 @@ export class PermissionManager { const startedAt = Date.now(); let response: ApprovalResponse; - try { - response = await this.agent.rpc.requestApproval( - { - turnId: Number(context.turnId), - toolCallId: id, - toolName: name, - action, - display, - }, - { signal }, - ); - } catch (error) { - this.agent.telemetry.track('permission_approval_result', { - policy_name: policyName ?? null, - tool_name: name, - permission_mode: this.mode, - result: 'error', - approval_surface: display.kind, - duration_ms: Date.now() - startedAt, - session_cache_written: false, - has_feedback: false, - }); - const resolved = result.resolveError?.(error); - return resolved === undefined - ? Promise.reject(error) - : this.permissionPolicyResolutionToPrepare(resolved, context, policyName); + if (this.agent.rpc === undefined) { + response = { + decision: 'approved', + }; + } else { + try { + response = await this.agent.rpc.requestApproval( + { + turnId: Number(context.turnId), + toolCallId: id, + toolName: name, + action, + display, + }, + { signal }, + ); + } catch (error) { + this.agent.telemetry.track('permission_approval_result', { + policy_name: policyName ?? null, + tool_name: name, + permission_mode: this.mode, + result: 'error', + approval_surface: display.kind, + duration_ms: Date.now() - startedAt, + session_cache_written: false, + has_feedback: false, + }); + const resolved = result.resolveError?.(error); + return resolved === undefined + ? Promise.reject(error) + : this.permissionPolicyResolutionToPrepare(resolved, context, policyName); + } } const sessionApprovalRule = diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index fc8343be6..13ecdadbb 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -11,7 +11,6 @@ import { mcpResultToExecutableOutput } from '../../mcp/output'; import { isMcpToolName, qualifyMcpToolName } from '../../mcp/tool-naming'; import type { MCPClient } from '../../mcp/types'; import { DEFAULT_AGENT_PROFILES } from '../../profile'; -import { withProviderRequestAuth } from '../../providers/request-auth'; import { extendWorkspaceWithSkillRoots } from '../../skill'; import * as b from '../../tools/builtin'; import type { ToolStore, ToolStoreData, ToolStoreKey } from '../../tools/store'; @@ -94,7 +93,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, @@ -370,7 +369,7 @@ export class ToolManager { new b.ReadMediaFileTool(kaos, workspace, modelCapabilities, videoUploader), new b.EnterPlanModeTool(this.agent), new b.ExitPlanModeTool(this.agent), - new b.AskUserQuestionTool(this.agent), + this.agent.rpc && new b.AskUserQuestionTool(this.agent), new b.TodoListTool(this.toolStore), new b.TaskListTool(background), new b.TaskOutputTool(background), @@ -400,14 +399,12 @@ export class ToolManager { const uploadVideo = provider.uploadVideo?.bind(provider); if (uploadVideo === undefined) return undefined; - const modelAlias = this.agent.config.modelAlias; - const resolveAuth = - modelAlias === undefined - ? undefined - : this.agent.providerManager?.createAuthResolverForModel(modelAlias, { - log: this.agent.log, - }); - return (input) => withProviderRequestAuth(resolveAuth, (auth) => uploadVideo(input, { auth })); + const modelAlias = this.agent.config.modelAlias!; + const withAuth = this.agent.providerManager?.createAuthResolverForModel(modelAlias, { + log: this.agent.log, + }); + if (withAuth === undefined) return (input) => uploadVideo(input); + return (input) => withAuth((auth) => uploadVideo(input, { auth })); } get loopTools(): readonly ExecutableTool[] { diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index ebf88a7b6..05dbaab48 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -701,8 +701,8 @@ function summarizeTurnError(error: unknown, turnId: number): KimiErrorPayload { const details = { ...payload.details, turnId }; // Substitute a friendlier TUI-aware message for model-not-configured. - // Raw kosong text ("Model not set" / "Provider not set") is not - // actionable; this string points the user at the login flow. + // The raw "Model not set" / "Provider not set" text is not actionable; + // this string points the user at the login flow. if (payload.code === 'model.not_configured') { return { ...payload, message: LLM_NOT_SET_MESSAGE, details }; } diff --git a/packages/agent-core/src/errors/serialize.ts b/packages/agent-core/src/errors/serialize.ts index efbbbb453..14f594753 100644 --- a/packages/agent-core/src/errors/serialize.ts +++ b/packages/agent-core/src/errors/serialize.ts @@ -59,7 +59,6 @@ export function makeErrorPayload( * - `APIStatusError`: 429 -> rate_limit, 401 -> auth_error, otherwise -> api_error. * - `APIConnectionError` / `APITimeoutError`: connection_error. * - `ChatProviderError`: api_error. - * - Heuristic "Model not set" / "Provider not set" messages: model.not_configured. * * Anything else collapses to `internal`. We never echo `cause` or stack on * the wire. @@ -113,15 +112,6 @@ export function toKimiErrorPayload(error: unknown): KimiErrorPayload { } if (error instanceof Error) { - if (error.message === 'Model not set' || error.message === 'Provider not set') { - return { - code: ErrorCodes.MODEL_NOT_CONFIGURED, - message: error.message, - name: error.name, - retryable: KIMI_ERROR_INFO[ErrorCodes.MODEL_NOT_CONFIGURED].retryable, - }; - } - return { code: ErrorCodes.INTERNAL, message: error.message, diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 2045c9872..cba2de508 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -43,7 +43,7 @@ export type { RuntimeConfig } from './runtime-types'; export type { BearerTokenProvider, OAuthTokenProviderResolver, -} from './providers/runtime-provider'; +} from './session/provider-manager'; // ─── Wire records (for in-monorepo consumers like apps/vis) ──────────────── export type { diff --git a/packages/agent-core/src/providers/index.ts b/packages/agent-core/src/providers/index.ts deleted file mode 100644 index 85cca4b1c..000000000 --- a/packages/agent-core/src/providers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './provider-manager'; -export * from './runtime-provider'; diff --git a/packages/agent-core/src/providers/provider-manager.ts b/packages/agent-core/src/providers/provider-manager.ts deleted file mode 100644 index bebd3f3e7..000000000 --- a/packages/agent-core/src/providers/provider-manager.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { KimiConfig } from '../config'; -import { ErrorCodes, KimiError } from '#/errors'; -import type { Logger } from '#/logging/types'; -import { resolveThinkingEffort, type ThinkingEffort } from '../agent/config/thinking'; -import { - createRuntimeProviderAuthResolver, - resolveRuntimeProvider, - resolveRuntimeProviderWithOAuth, - type OAuthTokenProviderResolver, - type ProviderRequestAuthResolver, - type ResolvedRuntimeProvider, -} from './runtime-provider'; - -export interface ProviderManagerOptions { - readonly config: KimiConfig; - readonly kimiRequestHeaders?: Record | undefined; - readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; - readonly promptCacheKey?: string; -} - -export class ProviderManager { - private readonly state: ProviderManagerState; - - constructor( - private readonly options: ProviderManagerOptions, - state?: ProviderManagerState, - ) { - this.state = state ?? { config: options.config }; - } - - get config(): KimiConfig { - return this.state.config; - } - - get providers(): KimiConfig['providers'] { - return this.state.config.providers; - } - - get models(): NonNullable { - return this.state.config.models ?? {}; - } - - updateConfig(config: KimiConfig): void { - this.state.config = config; - } - - withPromptCacheKey(promptCacheKey?: string): ProviderManager { - return new ProviderManager( - { - ...this.options, - config: this.state.config, - promptCacheKey, - }, - this.state, - ); - } - - resolveProviderConfigForModel( - model: string | undefined, - options?: { readonly promptCacheKey?: string }, - ): ResolvedRuntimeProvider | undefined { - const selectedModel = this.resolveSelectedModel(model); - if (selectedModel === undefined) return undefined; - - return resolveRuntimeProvider({ - config: this.state.config, - model: selectedModel, - kimiRequestHeaders: this.options.kimiRequestHeaders, - promptCacheKey: options?.promptCacheKey ?? this.options.promptCacheKey, - validateCredentials: false, - }); - } - - async resolveProviderForModel( - model: string | undefined, - ): Promise { - const selectedModel = this.resolveSelectedModel(model); - if (selectedModel === undefined) return undefined; - - return resolveRuntimeProviderWithOAuth({ - config: this.state.config, - model: selectedModel, - kimiRequestHeaders: this.options.kimiRequestHeaders, - promptCacheKey: this.options.promptCacheKey, - resolveOAuthTokenProvider: this.options.resolveOAuthTokenProvider, - }); - } - - createAuthResolverForModel( - model: string | undefined, - options?: { readonly log?: Logger }, - ): ProviderRequestAuthResolver | undefined { - const selectedModel = this.resolveSelectedModel(model); - if (selectedModel === undefined) return undefined; - - const resolved = resolveRuntimeProvider({ - config: this.state.config, - model: selectedModel, - kimiRequestHeaders: this.options.kimiRequestHeaders, - promptCacheKey: this.options.promptCacheKey, - }); - return createRuntimeProviderAuthResolver( - { - config: this.state.config, - model: selectedModel, - kimiRequestHeaders: this.options.kimiRequestHeaders, - promptCacheKey: this.options.promptCacheKey, - resolveOAuthTokenProvider: this.options.resolveOAuthTokenProvider, - log: options?.log, - }, - resolved, - ); - } - - resolveThinkingLevel(requestedThinking?: string): ThinkingEffort { - return resolveThinkingEffort( - resolveRuntimeThinkingRequest(requestedThinking, this.state.config.defaultThinking), - this.state.config.thinking, - ); - } - - resolveSelectedModel(requestedModel: string | undefined): string | undefined { - if (requestedModel !== undefined) { - const normalized = normalizeString(requestedModel); - if (normalized === undefined) { - throw new KimiError(ErrorCodes.MODEL_CONFIG_INVALID, 'Runtime provider model cannot be empty'); - } - return normalized; - } - return normalizeString(this.state.config.defaultModel); - } -} - -interface ProviderManagerState { - config: KimiConfig; -} - -function normalizeString(value: string | undefined): string | undefined { - if (value === undefined) return undefined; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - -function resolveRuntimeThinkingRequest( - requestedThinking: string | undefined, - defaultThinking: boolean | undefined, -): string | undefined { - const normalized = normalizeString(requestedThinking); - if (normalized !== undefined) return normalized; - return defaultThinking === false ? 'off' : undefined; -} diff --git a/packages/agent-core/src/providers/request-auth.ts b/packages/agent-core/src/providers/request-auth.ts deleted file mode 100644 index 31a032177..000000000 --- a/packages/agent-core/src/providers/request-auth.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { APIStatusError, type ProviderRequestAuth } from '@moonshot-ai/kosong'; - -import { ErrorCodes, KimiError } from '#/errors'; - -export interface ProviderRequestAuthOptions { - readonly forceRefresh?: boolean; -} - -export type ProviderRequestAuthResolver = ( - options?: ProviderRequestAuthOptions, -) => Promise; - -export async function withProviderRequestAuth( - resolveAuth: ProviderRequestAuthResolver | undefined, - request: (auth: ProviderRequestAuth | undefined) => Promise, -): Promise { - let auth = await resolveAuth?.(); - for (let refreshed = false; ; refreshed = true) { - try { - return await request(auth); - } catch (error) { - if ( - auth === undefined || - !(error instanceof APIStatusError) || - error.statusCode !== 401 - ) { - throw error; - } - if (!refreshed && resolveAuth !== undefined) { - auth = await resolveAuth({ forceRefresh: true }); - continue; - } - throw new KimiError( - ErrorCodes.AUTH_LOGIN_REQUIRED, - 'OAuth provider credentials were rejected. Send /login to login.', - { - cause: error, - details: { - statusCode: error.statusCode, - requestId: error.requestId, - }, - }, - ); - } - } -} diff --git a/packages/agent-core/src/providers/runtime-provider.ts b/packages/agent-core/src/providers/runtime-provider.ts deleted file mode 100644 index 2ac4a7b99..000000000 --- a/packages/agent-core/src/providers/runtime-provider.ts +++ /dev/null @@ -1,412 +0,0 @@ -import type { KimiConfig, ModelAlias, OAuthRef, ProviderConfig } from '#/config'; -import { ErrorCodes, KimiError, isKimiError } from '#/errors'; -import { log as defaultLog } from '#/logging/logger'; -import type { Logger } from '#/logging/types'; -import { - createProvider, - UNKNOWN_CAPABILITY, - type ModelCapability, - type ProviderConfig as KosongProviderConfig, -} from '@moonshot-ai/kosong'; - -import type { ProviderRequestAuthResolver } from './request-auth'; - -export type { ProviderRequestAuthResolver }; - -export interface ResolveRuntimeProviderInput { - readonly config: KimiConfig; - readonly model?: string | undefined; - readonly kimiRequestHeaders?: Record | undefined; - readonly promptCacheKey?: string; - readonly validateCredentials?: boolean; -} - -export interface BearerTokenProvider { - getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; -} - -export type OAuthTokenProviderResolver = ( - providerName: string, - oauthRef?: OAuthRef | undefined, -) => BearerTokenProvider | undefined; - -export interface ResolveRuntimeProviderWithOAuthInput extends ResolveRuntimeProviderInput { - readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; - /** - * Caller-scoped logger (typically `agent.log`). Used to report OAuth token - * fetch failures so they land in the session log alongside the surrounding - * turn / tool call. Defaults to the global `log` when absent. - */ - readonly log?: Logger; -} - -export interface ResolvedRuntimeProvider { - readonly modelName: string; - readonly providerName?: string | undefined; - readonly provider: KosongProviderConfig; - readonly modelCapabilities: ModelCapability; - readonly resolveAuth?: ProviderRequestAuthResolver; -} - -export function resolveRuntimeProvider( - input: ResolveRuntimeProviderInput, -): ResolvedRuntimeProvider { - const modelName = input.model ?? input.config.defaultModel; - if (modelName === undefined) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - 'No model is selected. Set default_model in config.toml or pass a configured model alias.', - ); - } - - const alias = input.config.models?.[modelName]; - if (alias === undefined) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Model "${modelName}" is not configured in config.toml. Add a [models."${modelName}"] entry with max_context_size.`, - ); - } - - const resolvedModel = alias.model; - const providerName = alias.provider ?? input.config.defaultProvider; - const providerConfig = - providerName === undefined ? undefined : input.config.providers[providerName]; - - if (providerName === undefined) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Model "${modelName}" must define a provider in config.toml.`, - ); - } - - if (providerConfig === undefined) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Provider "${providerName}" for model "${modelName}" is not configured.`, - ); - } - - if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Model "${modelName}" must define a positive max_context_size in config.toml.`, - ); - } - - // Fail-fast when an operation explicitly selects/uses a runtime provider. - // Session creation only records the selected model name and does not need - // credential validation. - if ( - input.validateCredentials !== false && - providerConfig.type !== 'vertexai' && - providerConfig.oauth === undefined && - providerApiKey(providerConfig) === undefined - ) { - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Provider "${providerName}" has no credentials configured. Set apiKey, oauth, or a provider env API key in config.toml.`, - ); - } - - const provider = toKosongProviderConfig( - providerConfig, - resolvedModel, - input.kimiRequestHeaders, - alias.maxOutputSize, - alias.reasoningKey, - input.promptCacheKey, - ); - const modelCapabilities = resolveModelCapabilities(alias, provider); - - return { - modelName, - providerName, - modelCapabilities, - provider, - }; -} - -export async function resolveRuntimeProviderWithOAuth( - input: ResolveRuntimeProviderWithOAuthInput, -): Promise { - const resolved = resolveRuntimeProvider(input); - const resolveAuth = createRuntimeProviderAuthResolver(input, resolved); - if (resolveAuth === undefined) return resolved; - - // Validate login eagerly, preserving existing setModel - // behavior, but do not store the short-lived token in provider config. - await resolveAuth(); - return { - ...resolved, - resolveAuth, - }; -} - -export function createRuntimeProviderAuthResolver( - input: ResolveRuntimeProviderWithOAuthInput, - resolved: ResolvedRuntimeProvider = resolveRuntimeProvider(input), -): ProviderRequestAuthResolver | undefined { - const providerName = resolved.providerName; - if (providerName === undefined) return undefined; - - const providerConfig = input.config.providers[providerName]; - if (providerConfig?.oauth === undefined) return undefined; - if (providerApiKey(providerConfig) !== undefined) { - // oauth + apiKey on the same provider makes request auth ambiguous: - // provider construction would prefer apiKey while runtime auth resolves - // OAuth. Reject it so misconfiguration surfaces at model resolution. - throw new KimiError( - ErrorCodes.CONFIG_INVALID, - `Provider "${providerName}" has both apiKey and oauth set in config.toml — they are mutually exclusive. Remove one.`, - ); - } - - const tokenProvider = input.resolveOAuthTokenProvider?.(providerName, providerConfig.oauth); - if (tokenProvider === undefined) { - return async () => { - throw new KimiError( - ErrorCodes.AUTH_LOGIN_REQUIRED, - `OAuth provider "${providerName}" requires login before it can be used.`, - ); - }; - } - - return async (options) => { - let apiKey: string; - try { - apiKey = await tokenProvider.getAccessToken( - options?.forceRefresh === true ? { force: true } : undefined, - ); - } catch (error) { - if (!isAuthLoginRequired(error)) { - (input.log ?? defaultLog).warn('oauth token fetch failed', { providerName, error }); - } - throw new KimiError( - ErrorCodes.AUTH_LOGIN_REQUIRED, - `OAuth provider "${providerName}" requires login before it can be used.`, - { - cause: error, - }, - ); - } - if (apiKey.trim().length === 0) { - throw new KimiError( - ErrorCodes.AUTH_LOGIN_REQUIRED, - `OAuth provider "${providerName}" requires login before it can be used.`, - ); - } - return { apiKey }; - }; -} - -function isAuthLoginRequired(error: unknown): boolean { - return isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED; -} - -function resolveModelCapabilities( - alias: ModelAlias & { maxContextSize: number }, - provider: KosongProviderConfig, -): ModelCapability { - const capabilities = new Set( - (alias.capabilities ?? []).map((capability) => capability.trim().toLowerCase()), - ); - const has = (capability: string): boolean => capabilities.has(capability); - const capabilityProvider = createProvider(providerForCapabilityProbe(provider)); - const providerCapability = - capabilityProvider.getCapability?.(provider.model) ?? UNKNOWN_CAPABILITY; - - return { - image_in: has('image_in') || providerCapability.image_in, - video_in: has('video_in') || providerCapability.video_in, - audio_in: has('audio_in') || providerCapability.audio_in, - thinking: has('thinking') || has('always_thinking') || providerCapability.thinking, - tool_use: has('tool_use') || providerCapability.tool_use, - max_context_tokens: alias.maxContextSize, - }; -} - -function toKosongProviderConfig( - provider: ProviderConfig, - model: string, - kimiRequestHeaders?: Record | undefined, - maxOutputSize?: number | undefined, - reasoningKey?: string | undefined, - promptCacheKey?: string, -): KosongProviderConfig { - switch (provider.type) { - case 'anthropic': - return { - type: 'anthropic', - model, - baseUrl: providerValue(provider.baseUrl, provider.env, 'ANTHROPIC_BASE_URL'), - apiKey: providerApiKey(provider), - ...(maxOutputSize !== undefined ? { defaultMaxTokens: maxOutputSize } : {}), - ...defaultHeadersField(provider.customHeaders), - }; - case 'openai': - return { - type: 'openai', - model, - baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), - apiKey: providerApiKey(provider), - reasoningKey, - ...defaultHeadersField(provider.customHeaders), - }; - case 'kimi': { - const defaultHeaders = { - ...kimiRequestHeaders, - ...provider.customHeaders, - }; - if (Object.keys(defaultHeaders).length === 0) { - return { - type: 'kimi', - model, - baseUrl: providerValue(provider.baseUrl, provider.env, 'KIMI_BASE_URL'), - generationKwargs: { - prompt_cache_key: promptCacheKey, - }, - apiKey: providerApiKey(provider), - }; - } - return { - type: 'kimi', - model, - baseUrl: providerValue(provider.baseUrl, provider.env, 'KIMI_BASE_URL'), - generationKwargs: { - prompt_cache_key: promptCacheKey, - }, - defaultHeaders, - apiKey: providerApiKey(provider), - }; - } - case 'google-genai': - return { - type: 'google-genai', - model, - apiKey: providerApiKey(provider), - }; - case 'openai_responses': - return { - type: 'openai_responses', - model, - baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), - apiKey: providerApiKey(provider), - ...defaultHeadersField(provider.customHeaders), - }; - case 'vertexai': - return { - type: 'vertexai', - model, - vertexai: hasVertexAIServiceEnv(provider), - apiKey: hasVertexAIServiceEnv(provider) ? undefined : providerApiKey(provider), - project: vertexAIProject(provider), - location: vertexAILocation(provider), - }; - default: { - const exhaustive: never = provider.type; - throw new KimiError( - ErrorCodes.MODEL_CONFIG_INVALID, - `Unsupported provider type: ${String(exhaustive)}`, - ); - } - } -} - -// Spread-ready `defaultHeaders` field for a kosong provider config. Returns a -// fresh copy so resolved provider instances never share a header object, and -// omits the key entirely when there are no headers (callers and tests rely on -// `'defaultHeaders' in provider`). -function defaultHeadersField( - headers: Record | undefined, -): { defaultHeaders?: Record } { - if (headers === undefined || Object.keys(headers).length === 0) return {}; - return { defaultHeaders: { ...headers } }; -} - -function providerForCapabilityProbe(provider: KosongProviderConfig): KosongProviderConfig { - if (provider.type === 'vertexai') { - return { - ...provider, - vertexai: false, - project: undefined, - location: undefined, - apiKey: - provider.apiKey === undefined || provider.apiKey.length === 0 - ? 'capability-probe' - : provider.apiKey, - }; - } - if (provider.apiKey !== undefined && provider.apiKey.length > 0) return provider; - return { ...provider, apiKey: 'capability-probe' }; -} - -function providerApiKey(provider: ProviderConfig): string | undefined { - switch (provider.type) { - case 'anthropic': - return providerValue(provider.apiKey, provider.env, 'ANTHROPIC_API_KEY'); - case 'openai': - case 'openai_responses': - return providerValue(provider.apiKey, provider.env, 'OPENAI_API_KEY'); - case 'kimi': - return providerValue(provider.apiKey, provider.env, 'KIMI_API_KEY'); - case 'google-genai': - return providerValue(provider.apiKey, provider.env, 'GOOGLE_API_KEY'); - case 'vertexai': - return ( - nonEmptyString(provider.apiKey) ?? - envValue(provider.env, 'VERTEXAI_API_KEY') ?? - envValue(provider.env, 'GOOGLE_API_KEY') - ); - default: { - const exhaustive: never = provider.type; - throw new KimiError( - ErrorCodes.MODEL_CONFIG_INVALID, - `Unsupported provider type: ${String(exhaustive)}`, - ); - } - } -} - -function hasVertexAIServiceEnv(provider: ProviderConfig): boolean { - return vertexAIProject(provider) !== undefined && vertexAILocation(provider) !== undefined; -} - -function vertexAIProject(provider: ProviderConfig): string | undefined { - return envValue(provider.env, 'GOOGLE_CLOUD_PROJECT'); -} - -function vertexAILocation(provider: ProviderConfig): string | undefined { - return ( - envValue(provider.env, 'GOOGLE_CLOUD_LOCATION') ?? - locationFromVertexAIBaseUrl(provider.baseUrl) - ); -} - -function providerValue( - configured: string | undefined, - env: Record | undefined, - envKey: string, -): string | undefined { - return nonEmptyString(configured) ?? envValue(env, envKey); -} - -function envValue(env: Record | undefined, key: string): string | undefined { - return nonEmptyString(env?.[key]); -} - -function nonEmptyString(value: string | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed; -} - -function locationFromVertexAIBaseUrl(baseUrl: string | undefined): string | undefined { - const url = nonEmptyString(baseUrl); - if (url === undefined) return undefined; - try { - const host = new URL(url).hostname; - const suffix = '-aiplatform.googleapis.com'; - return host.endsWith(suffix) ? nonEmptyString(host.slice(0, -suffix.length)) : undefined; - } catch { - return undefined; - } -} diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 042cb4aa5..218bf6667 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -249,6 +249,10 @@ export interface UpdateSessionMetadataPayload { readonly metadata: SessionMetadataPatch; } +export interface GetKimiConfigPayload { + readonly reload?: boolean; +} + export type SetKimiConfigPayload = KimiConfigPatch; export interface RemoveKimiProviderPayload { @@ -302,7 +306,7 @@ type SessionAPIWithId = WithSessionId; export interface CoreAPI extends SessionAPIWithId { getCoreInfo: (payload: EmptyPayload) => CoreInfo; - getKimiConfig: (payload: EmptyPayload) => KimiConfig; + getKimiConfig: (payload: GetKimiConfigPayload) => KimiConfig; setKimiConfig: (payload: SetKimiConfigPayload) => KimiConfig; removeKimiProvider: (payload: RemoveKimiProviderPayload) => KimiConfig; createSession: (payload: CreateSessionPayload) => SessionSummary; diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 39ae24603..989e72801 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -1,13 +1,39 @@ import { randomUUID } from 'node:crypto'; import { homedir } from 'node:os'; -import { KaosShellNotFoundError, LocalKaos } from '@moonshot-ai/kaos'; import { ErrorCodes, KimiError } from '#/errors'; import { getRootLogger, log } from '#/logging/logger'; +import { PluginManager } from '#/plugin'; import { LocalFetchURLProvider } from '#/tools/providers/local-fetch-url'; import { MoonshotFetchURLProvider } from '#/tools/providers/moonshot-fetch-url'; import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search'; +import type { PromisableMethods } from '#/utils/types'; import { getCoreVersion } from '#/version'; +import { KaosShellNotFoundError, LocalKaos } from '@moonshot-ai/kaos'; +import { resolveThinkingLevel } from '../agent/config/thinking'; +import { + ensureKimiHome, + mergeConfigPatch, + readConfigFile, + resolveConfigPath, + resolveKimiHome, + writeConfigFile, + type KimiConfig, + type MoonshotServiceConfig, +} from '../config'; +import type { Logger } from '../logging/types'; +import { resolveSessionMcpConfig, type SessionMcpConfig } from '../mcp'; +import type { RuntimeConfig } from '../runtime-types'; +import { Session, type SessionMeta, type SessionSkillConfig } from '../session'; +import { exportSessionDirectory } from '../session/export'; +import { + ProviderManager, type BearerTokenProvider, + type OAuthTokenProviderResolver +} from '../session/provider-manager'; +import { SessionAPIImpl } from '../session/rpc'; +import { normalizeWorkDir, SessionStore } from '../session/store'; +import { noopTelemetryClient, withTelemetryContext, type TelemetryClient } from '../telemetry'; +import type { CoreRPCClient } from './client'; import type { ActivateSkillPayload, BeginCompactionPayload, @@ -24,6 +50,7 @@ import type { GetBackgroundOutputPathPayload, GetBackgroundOutputPayload, GetBackgroundPayload, + GetKimiConfigPayload, GetPluginInfoPayload, InstallPluginPayload, ListSessionsPayload, @@ -33,14 +60,15 @@ import type { PluginSummary, PromptPayload, ReconnectMcpServerPayload, + RegisterToolPayload, ReloadPluginsResult, RemoveKimiProviderPayload, RemovePluginPayload, RenameSessionPayload, ResumeSessionPayload, - RegisterToolPayload, - SetKimiConfigPayload, + SessionSummary, SetActiveToolsPayload, + SetKimiConfigPayload, SetModelPayload, SetModelResult, SetPermissionPayload, @@ -50,41 +78,12 @@ import type { SkillSummary, SteerPayload, StopBackgroundPayload, - SessionSummary, UnregisterToolPayload, UpdateSessionMetadataPayload, } from './core-api'; -import type { CoreRPCClient } from './client'; import type { ResumedAgentState, ResumeSessionResult } from './resumed'; import type { SDKRPC } from './sdk-api'; import { proxyWithExtraPayload } from './types'; -import type { PromisableMethods } from '#/utils/types'; - -import { PluginManager } from '#/plugin'; - -import { resolveSessionMcpConfig, type SessionMcpConfig } from '../mcp'; -import { Session, type SessionMeta, type SessionSkillConfig } from '../session'; -import { SessionAPIImpl } from '../session/rpc'; -import { - ensureKimiHome, - mergeConfigPatch, - readConfigFile, - resolveConfigPath, - resolveKimiHome, - writeConfigFile, - type KimiConfig, - type MoonshotServiceConfig, -} from '../config'; -import { exportSessionDirectory } from '../session/export'; -import { ProviderManager } from '../providers/provider-manager'; -import { - type BearerTokenProvider, - type OAuthTokenProviderResolver, -} from '../providers/runtime-provider'; -import type { Logger } from '../logging/types'; -import type { RuntimeConfig } from '../runtime-types'; -import { normalizeWorkDir, SessionStore } from '../session/store'; -import { noopTelemetryClient, withTelemetryContext, type TelemetryClient } from '../telemetry'; const KIMI_CODE_PROVIDER_NAME = 'managed:kimi-code'; @@ -112,11 +111,11 @@ export class KimiCore implements PromisableMethods { readonly telemetry: TelemetryClient; private runtime: RuntimeConfig | undefined; + private config: KimiConfig; private readonly userHomeDir: string; private readonly kimiRequestHeaders: Record | undefined; private readonly resolveOAuthTokenProvider: OAuthTokenProviderResolver | undefined; private readonly skillDirs: readonly string[]; - private readonly providerManager: ProviderManager; private readonly sessionStore: SessionStore; readonly plugins: PluginManager; private pluginsReady: Promise; @@ -138,11 +137,7 @@ export class KimiCore implements PromisableMethods { this.skillDirs = options.skillDirs ?? []; this.telemetry = options.telemetry ?? noopTelemetryClient; ensureKimiHome(this.homeDir); - this.providerManager = new ProviderManager({ - config: readConfigFile(this.configPath), - kimiRequestHeaders: this.kimiRequestHeaders, - resolveOAuthTokenProvider: this.resolveOAuthTokenProvider, - }); + this.config = readConfigFile(this.configPath); this.sessionStore = new SessionStore(this.homeDir); this.plugins = new PluginManager({ kimiHomeDir: this.homeDir }); // Capture the error rather than swallow it: mutators and explicit /plugins @@ -161,8 +156,7 @@ export class KimiCore implements PromisableMethods { const workDir = requiredWorkDir('createSession', options.workDir); const config = this.reloadProviderManager(); const id = options.id ?? createSessionId(); - const modelName = this.providerManager.resolveSelectedModel(options.model); - const thinkingLevel = this.providerManager.resolveThinkingLevel(options.thinking); + const thinkingLevel = resolveThinkingLevel(options.thinking, config); const permissionMode = options.permission ?? config.defaultPermissionMode; const baseMcpConfig = await resolveSessionMcpConfig({ cwd: workDir, @@ -190,7 +184,7 @@ export class KimiCore implements PromisableMethods { homedir: summary.sessionDir, kimiHomeDir: this.homeDir, rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }), - providerManager: this.providerManager, + providerManager: this.resolveProviderManager(summary.id), background: config.background, hooks: config.hooks, permissionRules: config.permission?.rules, @@ -214,7 +208,7 @@ export class KimiCore implements PromisableMethods { }; const mainAgent = await session.createMain(); mainAgent.config.update({ - modelAlias: modelName, + modelAlias: options.model ?? config.defaultModel, thinkingLevel, }); if (permissionMode !== undefined) { @@ -269,7 +263,7 @@ export class KimiCore implements PromisableMethods { homedir: summary.sessionDir, kimiHomeDir: this.homeDir, rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }), - providerManager: this.providerManager, + providerManager: this.resolveProviderManager(summary.id), background: config.background, hooks: config.hooks, permissionRules: config.permission?.rules, @@ -360,17 +354,17 @@ export class KimiCore implements PromisableMethods { return result; } - async getKimiConfig(input: EmptyPayload = {}): Promise { - void input; - return readConfigFile(this.configPath); + async getKimiConfig(input?: GetKimiConfigPayload): Promise { + if (input?.reload) { + this.config = readConfigFile(this.configPath); + } + return this.config; } async setKimiConfig(input: SetKimiConfigPayload): Promise { const config = mergeConfigPatch(readConfigFile(this.configPath), input); await writeConfigFile(this.configPath, config); - const updated = readConfigFile(this.configPath); - this.providerManager.updateConfig(updated); - return updated; + return this.config = readConfigFile(this.configPath); } async removeKimiProvider(input: RemoveKimiProviderPayload): Promise { @@ -401,9 +395,7 @@ export class KimiCore implements PromisableMethods { } await writeConfigFile(this.configPath, config); - const updated = readConfigFile(this.configPath); - this.providerManager.updateConfig(updated); - return updated; + return this.config = readConfigFile(this.configPath); } prompt({ sessionId, ...payload }: SessionAgentPayload) { @@ -660,6 +652,15 @@ export class KimiCore implements PromisableMethods { }; } + private resolveProviderManager(sessionId: string): ProviderManager { + return new ProviderManager({ + config: () => this.config, + kimiRequestHeaders: this.kimiRequestHeaders, + resolveOAuthTokenProvider: this.resolveOAuthTokenProvider, + promptCacheKey: sessionId, + }); + } + private mergePluginMcpConfig(base: SessionMcpConfig | undefined): SessionMcpConfig | undefined { const pluginServers = this.plugins.enabledMcpServers(); if (Object.keys(pluginServers).length === 0) return base; @@ -682,9 +683,7 @@ export class KimiCore implements PromisableMethods { } private reloadProviderManager(): KimiConfig { - const config = readConfigFile(this.configPath); - this.providerManager.updateConfig(config); - return config; + return this.config = readConfigFile(this.configPath); } private async refreshSessionRuntimeConfig( @@ -721,14 +720,6 @@ export class KimiCore implements PromisableMethods { throw error; } } - // No candidate resolved (the replayed alias and the configured default are - // both invalid/unset). Clear the stale alias so the session is honestly - // model-less — the TUI then prompts for a model instead of showing a - // selection whose next prompt fails with a config error. Not persisted: - // `refreshSessionRuntimeConfig` re-derives this on every resume. - if (requested.length > 0) { - session.agents.get('main')?.config.update({ modelAlias: undefined }); - } } } diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index b6fdd3ee3..379e83d15 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -26,7 +26,7 @@ import { prepareSystemPromptContext, type ResolvedAgentProfile, } from '../profile'; -import type { ProviderManager } from '../providers/provider-manager'; +import type { ProviderManager } from './provider-manager'; import type { RuntimeConfig } from '../runtime-types'; import { registerBuiltinSkills, @@ -409,7 +409,6 @@ export class Session { skills: this.skills, rpc: proxyWithExtraPayload(this.rpc, { agentId: id }), providerManager: this.config.providerManager, - sessionId: this.config.id, 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 new file mode 100644 index 000000000..402d9ed43 --- /dev/null +++ b/packages/agent-core/src/session/provider-manager.ts @@ -0,0 +1,338 @@ +import type { Logger } from '#/logging/types'; +import type { ProviderConfig as KosongProviderConfig, ModelCapability, ProviderRequestAuth } from '@moonshot-ai/kosong'; +import { APIStatusError, createProvider, UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong'; +import type { KimiConfig, ModelAlias, OAuthRef, ProviderConfig } from '../config'; +import { ErrorCodes, isKimiError, KimiError } from '../errors'; + +export interface BearerTokenProvider { + getAccessToken(options?: { readonly force?: boolean }): Promise; +} + +export type OAuthTokenProviderResolver = ( + providerName: string, + oauthRef?: OAuthRef, +) => BearerTokenProvider | undefined; + +export interface ResolvedRuntimeProvider { + readonly modelName: string; + readonly providerName: string; + readonly provider: KosongProviderConfig; + readonly modelCapabilities: ModelCapability; +} + +interface ProviderManagerOptions { + readonly config: KimiConfig | (() => KimiConfig); + readonly kimiRequestHeaders?: Record; + readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver; + readonly promptCacheKey?: string; +} + +type AuthorizedRequest = ( + request: (auth: ProviderRequestAuth) => Promise, +) => Promise; + +export class ProviderManager { + constructor(private readonly options: ProviderManagerOptions) {} + + get config(): KimiConfig { + const { config } = this.options; + return typeof config === 'function' ? config() : config; + } + + resolveProviderConfig(model: string): ResolvedRuntimeProvider { + const alias = this.config.models?.[model]; + if (alias === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${model}" is not configured in config.toml. Add a [models."${model}"] entry with max_context_size.`, + ); + } + + const providerName = alias.provider ?? this.config.defaultProvider; + if (providerName === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${model}" must define a provider in config.toml.`, + ); + } + + const providerConfig = this.config.providers[providerName]; + if (providerConfig === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Provider "${providerName}" for model "${model}" is not configured.`, + ); + } + + if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${model}" must define a positive max_context_size in config.toml.`, + ); + } + + const provider = toKosongProviderConfig( + providerConfig, + alias.model, + this.options.kimiRequestHeaders, + alias.maxOutputSize, + alias.reasoningKey, + this.options.promptCacheKey, + ); + + return { + modelName: model, + providerName, + provider, + modelCapabilities: resolveModelCapabilities(alias, provider), + }; + } + + createAuthResolverForModel( + model: string, + options?: { readonly log?: Logger }, + ): AuthorizedRequest | undefined { + const { providerName } = this.resolveProviderConfig(model); + const providerConfig = this.config.providers[providerName]; + if (providerConfig?.oauth === undefined) return undefined; + + if (providerApiKey(providerConfig) !== undefined) { + // oauth + apiKey on the same provider makes request auth ambiguous: + // provider construction would prefer apiKey while runtime auth resolves + // OAuth. Reject it so misconfiguration surfaces at model resolution. + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Provider "${providerName}" has both apiKey and oauth set in config.toml — they are mutually exclusive. Remove one.`, + ); + } + + const loginRequired = (cause?: unknown): KimiError => + new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + `OAuth provider "${providerName}" requires login before it can be used.`, + cause === undefined ? undefined : { cause }, + ); + + const tokenProvider = this.options.resolveOAuthTokenProvider?.(providerName, providerConfig.oauth); + if (tokenProvider === undefined) { + return async () => { + throw loginRequired(); + }; + } + + const log = options?.log; + const fetchAuth = async (force: boolean): Promise => { + let apiKey: string; + try { + apiKey = await tokenProvider.getAccessToken(force ? { force: true } : undefined); + } catch (error) { + if (!isKimiError(error) || error.code !== ErrorCodes.AUTH_LOGIN_REQUIRED) { + log?.warn('oauth token fetch failed', { providerName, error }); + } + throw loginRequired(error); + } + if (apiKey.trim().length === 0) throw loginRequired(); + return { apiKey }; + }; + + return async (request) => { + let auth = await fetchAuth(false); + for (let refreshed = false; ; refreshed = true) { + try { + return await request(auth); + } catch (error) { + if (!(error instanceof APIStatusError) || error.statusCode !== 401) throw error; + if (refreshed) { + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + 'OAuth provider credentials were rejected. Send /login to login.', + { + cause: error, + details: { statusCode: error.statusCode, requestId: error.requestId }, + }, + ); + } + auth = await fetchAuth(true); + } + } + }; + } +} + +function resolveModelCapabilities( + alias: ModelAlias, + provider: KosongProviderConfig, +): ModelCapability { + const declared = new Set((alias.capabilities ?? []).map((c) => c.trim().toLowerCase())); + const probe = createProvider(providerForCapabilityProbe(provider)); + const detected = probe.getCapability?.(provider.model) ?? UNKNOWN_CAPABILITY; + + return { + image_in: declared.has('image_in') || detected.image_in, + video_in: declared.has('video_in') || detected.video_in, + audio_in: declared.has('audio_in') || detected.audio_in, + thinking: declared.has('thinking') || declared.has('always_thinking') || detected.thinking, + tool_use: declared.has('tool_use') || detected.tool_use, + max_context_tokens: alias.maxContextSize, + }; +} + +function toKosongProviderConfig( + provider: ProviderConfig, + model: string, + kimiRequestHeaders: Record | undefined, + maxOutputSize: number | undefined, + reasoningKey: string | undefined, + promptCacheKey: string | undefined, +): KosongProviderConfig { + switch (provider.type) { + case 'anthropic': + return { + type: 'anthropic', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'ANTHROPIC_BASE_URL'), + apiKey: providerApiKey(provider), + ...(maxOutputSize !== undefined ? { defaultMaxTokens: maxOutputSize } : {}), + ...defaultHeadersField(provider.customHeaders), + }; + case 'openai': + return { + type: 'openai', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), + apiKey: providerApiKey(provider), + reasoningKey, + ...defaultHeadersField(provider.customHeaders), + }; + case 'kimi': + return { + type: 'kimi', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'KIMI_BASE_URL'), + apiKey: providerApiKey(provider), + generationKwargs: { prompt_cache_key: promptCacheKey }, + ...defaultHeadersField({ ...kimiRequestHeaders, ...provider.customHeaders }), + }; + case 'google-genai': + return { + type: 'google-genai', + model, + apiKey: providerApiKey(provider), + }; + case 'openai_responses': + return { + type: 'openai_responses', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), + apiKey: providerApiKey(provider), + ...defaultHeadersField(provider.customHeaders), + }; + case 'vertexai': { + const useServiceAccount = hasVertexAIServiceEnv(provider); + return { + type: 'vertexai', + model, + vertexai: useServiceAccount, + apiKey: useServiceAccount ? undefined : providerApiKey(provider), + project: vertexAIProject(provider), + location: vertexAILocation(provider), + }; + } + default: { + const exhaustive: never = provider.type; + throw new KimiError( + ErrorCodes.MODEL_CONFIG_INVALID, + `Unsupported provider type: ${String(exhaustive)}`, + ); + } + } +} + +// Returns a fresh `defaultHeaders` field for a kosong provider config so +// resolved instances never share a header object. Omits the key entirely when +// there are no headers — callers and tests rely on `'defaultHeaders' in provider`. +function defaultHeadersField( + headers: Record | undefined, +): { defaultHeaders?: Record } { + if (headers === undefined || Object.keys(headers).length === 0) return {}; + return { defaultHeaders: { ...headers } }; +} + +function providerForCapabilityProbe(provider: KosongProviderConfig): KosongProviderConfig { + const apiKey = provider.apiKey && provider.apiKey.length > 0 ? provider.apiKey : 'capability-probe'; + if (provider.type === 'vertexai') { + return { ...provider, vertexai: false, project: undefined, location: undefined, apiKey }; + } + return { ...provider, apiKey }; +} + +function providerApiKey(provider: ProviderConfig): string | undefined { + switch (provider.type) { + case 'anthropic': + return providerValue(provider.apiKey, provider.env, 'ANTHROPIC_API_KEY'); + case 'openai': + case 'openai_responses': + return providerValue(provider.apiKey, provider.env, 'OPENAI_API_KEY'); + case 'kimi': + return providerValue(provider.apiKey, provider.env, 'KIMI_API_KEY'); + case 'google-genai': + return providerValue(provider.apiKey, provider.env, 'GOOGLE_API_KEY'); + case 'vertexai': + return ( + nonEmptyString(provider.apiKey) ?? + envValue(provider.env, 'VERTEXAI_API_KEY') ?? + envValue(provider.env, 'GOOGLE_API_KEY') + ); + default: { + const exhaustive: never = provider.type; + throw new KimiError( + ErrorCodes.MODEL_CONFIG_INVALID, + `Unsupported provider type: ${String(exhaustive)}`, + ); + } + } +} + +function hasVertexAIServiceEnv(provider: ProviderConfig): boolean { + return vertexAIProject(provider) !== undefined && vertexAILocation(provider) !== undefined; +} + +function vertexAIProject(provider: ProviderConfig): string | undefined { + return envValue(provider.env, 'GOOGLE_CLOUD_PROJECT'); +} + +function vertexAILocation(provider: ProviderConfig): string | undefined { + return ( + envValue(provider.env, 'GOOGLE_CLOUD_LOCATION') ?? + locationFromVertexAIBaseUrl(provider.baseUrl) + ); +} + +function providerValue( + configured: string | undefined, + env: Record | undefined, + envKey: string, +): string | undefined { + return nonEmptyString(configured) ?? envValue(env, envKey); +} + +function envValue(env: Record | undefined, key: string): string | undefined { + return nonEmptyString(env?.[key]); +} + +function nonEmptyString(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed; +} + +function locationFromVertexAIBaseUrl(baseUrl: string | undefined): string | undefined { + const url = nonEmptyString(baseUrl); + if (url === undefined) return undefined; + try { + const host = new URL(url).hostname; + const suffix = '-aiplatform.googleapis.com'; + return host.endsWith(suffix) ? nonEmptyString(host.slice(0, -suffix.length)) : undefined; + } catch { + return 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 b06bf0379..1c71d7ce3 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/test/agent/compaction.test.ts b/packages/agent-core/test/agent/compaction.test.ts index 020c58bb4..a974bacf4 100644 --- a/packages/agent-core/test/agent/compaction.test.ts +++ b/packages/agent-core/test/agent/compaction.test.ts @@ -15,10 +15,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { AgentConfig } from '../../src/agent'; import { DefaultCompactionStrategy, type CompactionStrategy } from '../../src/agent/compaction'; import { HookEngine, type HookEngineTriggerArgs } from '../../src/agent/hooks'; -import type { KimiConfig } from '../../src/config'; -import { ProviderManager } from '../../src/providers/provider-manager'; import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry'; -import type { TestAgentContext } from './harness/agent'; +import type { TestAgentContext, TestAgentOptions } from './harness/agent'; import { testAgent } from './harness/agent'; type GenerateFn = NonNullable; @@ -294,7 +292,7 @@ describe('Agent compaction', () => { it('force-refreshes OAuth credentials on compaction 401 and falls back to login_required when replay 401', async () => { const tokenCalls: Array = []; const authKeys: string[] = []; - const providerManager = createOAuthProviderManager(async (options) => { + const oauthOptions = oauthTestAgentOptions(async (options) => { tokenCalls.push(options?.force); return options?.force === true ? 'forced-refresh-token' : 'fresh-token'; }); @@ -312,7 +310,7 @@ describe('Agent compaction', () => { } return textResult('Recovered compacted summary.'); }; - const ctx = testAgent({ providerManager, generate }); + const ctx = testAgent({ ...oauthOptions, generate }); ctx.configure(); await ctx.rpc.setModel({ model: 'kimi-code' }); ctx.newEvents(); @@ -336,7 +334,7 @@ describe('Agent compaction', () => { }), ); expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token']); - expect(tokenCalls).toEqual([undefined, undefined, true]); + expect(tokenCalls).toEqual([undefined, true]); expect(ctx.compactHistory()).toEqual([ { role: 'user', text: 'old user one' }, { role: 'assistant', text: 'old assistant one' }, @@ -350,7 +348,7 @@ describe('Agent compaction', () => { expect(await retryOutcome).toBe('context.apply_compaction'); expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token', 'fresh-token']); - expect(tokenCalls).toEqual([undefined, undefined, true, undefined]); + expect(tokenCalls).toEqual([undefined, true, undefined]); expect(ctx.compactHistory()).toEqual([ { role: 'assistant', text: 'Recovered compacted summary.' }, ]); @@ -1103,15 +1101,12 @@ describe('Agent compaction', () => { }); it('does not auto compact small contexts when reserved size exceeds the model window', async () => { - const providerManager = new ProviderManager({ - config: { + const ctx = testAgent({ + initialConfig: { providers: {}, - loopControl: { - reservedContextSize: 50_000, - }, + loopControl: { reservedContextSize: 50_000 }, }, }); - const ctx = testAgent({ providerManager }); ctx.configure({ provider: CATALOGUED_PROVIDER, modelCapabilities: { @@ -1133,15 +1128,12 @@ describe('Agent compaction', () => { }); it('triggers auto compaction when pending tokens cross the reserved threshold', async () => { - const providerManager = new ProviderManager({ - config: { + const ctx = testAgent({ + initialConfig: { providers: {}, - loopControl: { - reservedContextSize: 500, - }, + loopControl: { reservedContextSize: 500 }, }, }); - const ctx = testAgent({ providerManager }); ctx.configure({ provider: CATALOGUED_PROVIDER, modelCapabilities: { @@ -1319,13 +1311,11 @@ describe('Agent compaction', () => { }); const providerManager = ctx.agent.providerManager; if (providerManager === undefined) throw new Error('Expected provider manager'); - const resolveProviderConfig = providerManager.resolveProviderConfigForModel.bind(providerManager); - providerManager.resolveProviderConfigForModel = (model) => { - const resolved = resolveProviderConfig(model); - return resolved === undefined - ? undefined - : { ...resolved, modelCapabilities: UNKNOWN_CAPABILITY }; - }; + const resolveProviderConfig = providerManager.resolveProviderConfig.bind(providerManager); + providerManager.resolveProviderConfig = (model) => ({ + ...resolveProviderConfig(model), + modelCapabilities: UNKNOWN_CAPABILITY, + }); expect(ctx.agent.config.modelCapabilities.max_context_tokens).toBe(0); ctx.appendExchange(1, 'old user one', 'old assistant one', 20); ctx.newEvents(); @@ -1499,30 +1489,31 @@ function eventIndex(events: ReturnType, type: str }); } -function createOAuthProviderManager( +function oauthTestAgentOptions( getAccessToken: (options?: { readonly force?: boolean }) => Promise, -): ProviderManager { - const oauthConfig: KimiConfig = { - defaultModel: 'kimi-code', - providers: { - 'managed:kimi-code': { - type: 'vertexai', - baseUrl: 'https://api.example/v1', - oauth: { storage: 'file', key: 'oauth/kimi-code' }, +): Pick { + return { + initialConfig: { + defaultModel: 'kimi-code', + providers: { + 'managed:kimi-code': { + type: 'vertexai', + baseUrl: 'https://api.example/v1', + oauth: { storage: 'file', key: 'oauth/kimi-code' }, + }, + }, + models: { + 'kimi-code': { + provider: 'managed:kimi-code', + model: 'kimi-for-coding', + maxContextSize: 1_000_000, + }, }, }, - models: { - 'kimi-code': { - provider: 'managed:kimi-code', - model: 'kimi-for-coding', - maxContextSize: 1_000_000, - }, + providerManagerOverrides: { + resolveOAuthTokenProvider: () => ({ getAccessToken }), }, }; - return new ProviderManager({ - config: oauthConfig, - resolveOAuthTokenProvider: () => ({ getAccessToken }), - }); } function textResult(text: string): Awaited> { diff --git a/packages/agent-core/test/agent/config-state.test.ts b/packages/agent-core/test/agent/config-state.test.ts index fd17f0869..f130f57cf 100644 --- a/packages/agent-core/test/agent/config-state.test.ts +++ b/packages/agent-core/test/agent/config-state.test.ts @@ -1,7 +1,6 @@ -import { UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong'; import { describe, expect, it } from 'vitest'; -import { ProviderManager } from '../../src/providers/provider-manager'; +import { ProviderManager } from '../../src/session/provider-manager'; import { testAgent } from './harness'; describe('ConfigState model capabilities', () => { @@ -74,22 +73,10 @@ describe('ConfigState model capabilities', () => { }); }); - it('clears the selected model when modelAlias is cleared', () => { - const ctx = testAgent(); - ctx.configure(); - const config = ctx.agent.config; - - config.update({ modelAlias: undefined }); - - expect(() => config.model).toThrow('Model not set'); - expect(config.data().provider).toBeUndefined(); - expect(config.modelCapabilities).toEqual(UNKNOWN_CAPABILITY); - }); - - it('uses session id as a provider prompt cache hint without storing it on Agent', () => { +it('uses session id as a provider prompt cache hint without storing it on Agent', () => { const ctx = testAgent({ - sessionId: 'session-test', providerManager: new ProviderManager({ + promptCacheKey: 'session-test', config: { providers: { kimi: { diff --git a/packages/agent-core/test/agent/harness/agent.ts b/packages/agent-core/test/agent/harness/agent.ts index 49becada4..42cc77b55 100644 --- a/packages/agent-core/test/agent/harness/agent.ts +++ b/packages/agent-core/test/agent/harness/agent.ts @@ -21,7 +21,7 @@ import { import type { KimiConfig } from '../../../src/config'; import type { ExecutableToolResult } from '../../../src/loop'; import type { Logger } from '../../../src/logging'; -import { ProviderManager } from '../../../src/providers/provider-manager'; +import { ProviderManager } from '../../../src/session/provider-manager'; import type { QuestionResult, RPCCallOptions, SDKAgentRPC } from '../../../src/rpc'; import type { AgentAPI } from '../../../src/rpc/core-api'; import type { RuntimeConfig } from '../../../src/runtime-types'; @@ -88,7 +88,7 @@ interface ResumeStateSnapshot { readonly usage: ReturnType; } -interface TestAgentOptions { +export interface TestAgentOptions { readonly kaos?: Kaos | undefined; readonly runtime?: RuntimeConfig | undefined; readonly compactionStrategy?: CompactionStrategy | undefined; @@ -97,6 +97,8 @@ interface TestAgentOptions { readonly type?: AgentConfig['type']; readonly permission?: AgentConfig['permission']; readonly providerManager?: ProviderManager; + readonly initialConfig?: KimiConfig; + readonly providerManagerOverrides?: Omit[0], 'config'>; readonly sessionId?: string; readonly subagentHost?: AgentConfig['subagentHost']; readonly onEvent?: ((event: AgentRecord) => AgentRecord | undefined) | undefined; @@ -155,10 +157,17 @@ export class AgentTestContext { readonly mockNextResponse = this.scriptedGenerate.mockNextResponse; readonly mockNextProviderResponse = this.scriptedGenerate.mockNextProviderResponse; + private kimiConfig: KimiConfig; + constructor(options: TestAgentOptions = {}) { this.options = options; this.emitter.on('error', () => {}); - const providerManager = options.providerManager ?? new ProviderManager({ config: emptyConfig() }); + this.kimiConfig = options.initialConfig ?? emptyConfig(); + const providerManager = options.providerManager ?? new ProviderManager({ + config: () => this.kimiConfig, + ...(options.sessionId !== undefined ? { promptCacheKey: options.sessionId } : {}), + ...options.providerManagerOverrides, + }); const runtime = options.runtime ?? { kaos: options.kaos ?? testKaos, @@ -173,7 +182,6 @@ export class AgentTestContext { generate: options.generate ?? this.scriptedGenerate.generate, compactionStrategy: options.compactionStrategy, providerManager, - sessionId: options.sessionId, subagentHost: options.subagentHost, type: options.type, permission: options.permission, @@ -208,9 +216,9 @@ export class AgentTestContext { provider: ProviderConfig, modelCapabilities?: ModelCapability | undefined, ): void { - this.agent.providerManager?.updateConfig( - configWithProvider(this.agent.providerManager.config, provider, modelCapabilities), - ); + if (this.options.providerManager === undefined) { + this.kimiConfig = configWithProvider(this.kimiConfig, provider, modelCapabilities); + } this.agent.config.update({ modelAlias: provider.model }); } @@ -712,7 +720,9 @@ export class AgentTestContext { urlFetcher: this.agent.runtime.urlFetcher, webSearcher: this.agent.runtime.webSearcher, }, - providerManager: this.agent.providerManager, + providerManager: this.options.providerManager, + initialConfig: this.kimiConfig, + providerManagerOverrides: this.options.providerManagerOverrides, generate: failOnResumeGenerate, compactionStrategy: this.options.compactionStrategy, persistence: new InMemoryAgentRecordPersistence( diff --git a/packages/agent-core/test/agent/question.test.ts b/packages/agent-core/test/agent/question.test.ts index 8ad46776d..aef540c08 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 e032f44f6..06eba5b81 100644 --- a/packages/agent-core/test/agent/skill-tool-manager.test.ts +++ b/packages/agent-core/test/agent/skill-tool-manager.test.ts @@ -8,7 +8,7 @@ import { Agent, type AgentRecord } from '../../src/agent'; import { testKaos } from '../fixtures/test-kaos'; import { InMemoryAgentRecordPersistence } from '../../src/agent/records'; import type { AgentRecordPersistence } from '../../src/agent/records'; -import { ProviderManager } from '../../src/providers/provider-manager'; +import { ProviderManager } from '../../src/session/provider-manager'; import type { ApprovalResponse, SDKAgentRPC, SDKSessionRPC } from '../../src/rpc'; import { Session } from '../../src/session'; import { SkillRegistry, type SkillDefinition } from '../../src/skill'; diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index 42d3cad6a..c10869041 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -17,9 +17,7 @@ import { describe, expect, it, vi } from 'vitest'; import { HookEngine } from '../../src/agent/hooks'; import type { AgentConfig } from '../../src/agent'; -import type { KimiConfig } from '../../src/config'; import type { Logger, LogPayload } from '../../src/logging'; -import { ProviderManager } from '../../src/providers/provider-manager'; import { estimateTokens, estimateTokensForMessages, @@ -27,7 +25,7 @@ import { } from '../../src/utils/tokens'; import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry'; import { createFakeKaos } from '../tools/fixtures/fake-kaos'; -import { createCommandKaos, testAgent } from './harness/agent'; +import { createCommandKaos, testAgent, type TestAgentOptions } from './harness/agent'; import { executeTool } from '../tools/fixtures/execute-tool'; type GenerateFn = NonNullable; @@ -259,10 +257,10 @@ describe('Agent turn flow', () => { [wire] turn.prompt { "input": [ { "type": "text", "text": "Hello without login" } ], "origin": { "kind": "user" }, "time": "