import { APIConnectionError, APIEmptyResponseError, APIProviderQuotaExhaustedError, APIStatusError, APITimeoutError, ChatProviderError, } from '@moonshot-ai/kosong'; export const ErrorCodes = { CONFIG_INVALID: 'config.invalid', SESSION_NOT_FOUND: 'session.not_found', SESSION_ALREADY_EXISTS: 'session.already_exists', SESSION_ID_INVALID: 'session.id_invalid', SESSION_ID_REQUIRED: 'session.id_required', SESSION_ID_EMPTY: 'session.id_empty', SESSION_TITLE_EMPTY: 'session.title_empty', SESSION_STATE_NOT_FOUND: 'session.state_not_found', SESSION_STATE_INVALID: 'session.state_invalid', SESSION_FORK_ACTIVE_TURN: 'session.fork_active_turn', SESSION_EXPORT_NOT_FOUND: 'session.export_not_found', SESSION_EXPORT_MISSING_VERSION: 'session.export_missing_version', SESSION_CLOSED: 'session.closed', SESSION_PERMISSION_MODE_INVALID: 'session.permission_mode_invalid', SESSION_THINKING_EMPTY: 'session.thinking_empty', SESSION_MODEL_EMPTY: 'session.model_empty', SESSION_PLAN_MODE_INVALID: 'session.plan_mode_invalid', SESSION_APPROVAL_HANDLER_ERROR: 'session.approval_handler_error', SESSION_QUESTION_HANDLER_ERROR: 'session.question_handler_error', SESSION_INIT_FAILED: 'session.init_failed', AGENT_NOT_FOUND: 'agent.not_found', TURN_AGENT_BUSY: 'turn.agent_busy', GOAL_ALREADY_EXISTS: 'goal.already_exists', GOAL_NOT_FOUND: 'goal.not_found', GOAL_OBJECTIVE_EMPTY: 'goal.objective_empty', GOAL_OBJECTIVE_TOO_LONG: 'goal.objective_too_long', GOAL_STATUS_INVALID: 'goal.status_invalid', GOAL_METADATA_RESERVED: 'goal.metadata_reserved', GOAL_NOT_RESUMABLE: 'goal.not_resumable', MODEL_NOT_CONFIGURED: 'model.not_configured', MODEL_CONFIG_INVALID: 'model.config_invalid', AUTH_LOGIN_REQUIRED: 'auth.login_required', CONTEXT_OVERFLOW: 'context.overflow', LOOP_MAX_STEPS_EXCEEDED: 'loop.max_steps_exceeded', PROVIDER_API_ERROR: 'provider.api_error', PROVIDER_FILTERED: 'provider.filtered', PROVIDER_RATE_LIMIT: 'provider.rate_limit', PROVIDER_AUTH_ERROR: 'provider.auth_error', PROVIDER_CONNECTION_ERROR: 'provider.connection_error', SKILL_NOT_FOUND: 'skill.not_found', SKILL_TYPE_UNSUPPORTED: 'skill.type_unsupported', SKILL_NAME_EMPTY: 'skill.name_empty', RECORDS_WRITE_FAILED: 'records.write_failed', COMPACTION_FAILED: 'compaction.failed', COMPACTION_UNABLE: 'compaction.unable', BACKGROUND_TASK_ID_EMPTY: 'task.task_id_empty', MCP_SERVER_NOT_FOUND: 'mcp.server_not_found', MCP_SERVER_DISABLED: 'mcp.server_disabled', MCP_STARTUP_FAILED: 'mcp.startup_failed', MCP_TOOL_NAME_COLLISION: 'mcp.tool_name_collision', MCP_OAUTH_FAILED: 'mcp.oauth_failed', PLUGIN_NOT_FOUND: 'plugin.not_found', PLUGIN_LOAD_FAILED: 'plugin.load_failed', REQUEST_INVALID: 'request.invalid', REQUEST_WORK_DIR_REQUIRED: 'request.work_dir_required', REQUEST_PROMPT_INPUT_EMPTY: 'request.prompt_input_empty', SHELL_GIT_BASH_NOT_FOUND: 'shell.git_bash_not_found', NOT_IMPLEMENTED: 'not_implemented', INTERNAL: 'internal', } as const; export type KimiErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]; export interface KimiErrorInfo { readonly title: string; readonly retryable: boolean; readonly public: boolean; readonly action?: string; } export const KIMI_ERROR_INFO = { 'config.invalid': { title: 'Invalid configuration', retryable: false, public: true, action: 'Check config.toml and provider/model settings.', }, 'session.not_found': { title: 'Session not found', retryable: false, public: true, action: 'Check the session id or list available sessions.', }, 'session.already_exists': { title: 'Session already exists', retryable: false, public: true, action: 'Use a different session id or remove the existing session first.', }, 'session.id_invalid': { title: 'Invalid session id', retryable: false, public: true, action: 'Use a session id without path-traversal characters.', }, 'session.id_required': { title: 'Session id required', retryable: false, public: true, action: 'Provide a session id when calling this method.', }, 'session.id_empty': { title: 'Session id is empty', retryable: false, public: true, action: 'Provide a non-empty session id.', }, 'session.title_empty': { title: 'Session title is empty', retryable: false, public: true, action: 'Provide a non-empty session title.', }, 'session.state_not_found': { title: 'Session state missing', retryable: false, public: true, action: 'The session directory is corrupted or missing state.json.', }, 'session.state_invalid': { title: 'Session state invalid', retryable: false, public: true, action: 'The session state.json is corrupted; remove the session or repair the file.', }, 'session.fork_active_turn': { title: 'Cannot fork session during active turn', retryable: true, public: true, action: 'Wait for the active turn to complete before forking.', }, 'session.export_not_found': { title: 'Session export directory missing', retryable: false, public: true, action: 'The session has not been persisted to disk yet.', }, 'session.export_missing_version': { title: 'Export version is missing', retryable: false, public: true, action: 'Provide a version when exporting the session.', }, 'session.closed': { title: 'Session is closed', retryable: false, public: true, action: 'Create a new session.', }, 'session.permission_mode_invalid': { title: 'Invalid permission mode', retryable: false, public: true, action: 'Use one of: yolo / manual / auto.', }, 'session.thinking_empty': { title: 'Thinking value is empty', retryable: false, public: true, action: 'Provide a non-empty thinking option.', }, 'session.model_empty': { title: 'Model is empty', retryable: false, public: true, action: 'Provide a non-empty model identifier.', }, 'session.plan_mode_invalid': { title: 'Invalid plan mode', retryable: false, public: true, action: 'Provide a boolean plan mode.', }, 'session.approval_handler_error': { title: 'Approval handler threw', retryable: false, public: true, action: 'Inspect the SDK approval handler for an unhandled exception.', }, 'session.question_handler_error': { title: 'Question handler threw', retryable: false, public: true, action: 'Inspect the SDK question handler for an unhandled exception.', }, 'session.init_failed': { title: 'Session init failed', retryable: false, public: false, action: 'Review the init failure details and try again.', }, 'agent.not_found': { title: 'Agent not found', retryable: false, public: true, action: 'Check the agent id or list available agents.', }, 'turn.agent_busy': { title: 'Agent is busy', retryable: true, public: true, action: 'Wait for the current turn to finish or steer it.', }, 'goal.already_exists': { title: 'A goal is already active', retryable: false, public: true, action: 'Use `/goal replace ` to replace the current goal.', }, 'goal.not_found': { title: 'No goal found', retryable: false, public: true, action: 'Start a goal with `/goal ` first.', }, 'goal.objective_empty': { title: 'Goal objective is empty', retryable: false, public: true, action: 'Provide a non-empty objective.', }, 'goal.objective_too_long': { title: 'Goal objective is too long', retryable: false, public: true, action: 'Keep the objective under 4000 characters; reference long details by file path.', }, 'goal.status_invalid': { title: 'Invalid goal status transition', retryable: false, public: true, action: 'Use a status allowed for this actor (complete, blocked, or impossible).', }, 'goal.metadata_reserved': { title: 'Goal metadata is reserved', retryable: false, public: true, action: 'Do not write metadata.custom.goal directly; use the goal lifecycle methods.', }, 'goal.not_resumable': { title: 'Goal is not resumable', retryable: false, public: true, action: 'Only paused goals can be resumed.', }, 'model.not_configured': { title: 'No model configured', retryable: false, public: true, action: 'Set a default model in config.toml or via setModel.', }, 'model.config_invalid': { title: 'Invalid model configuration', retryable: false, public: true, action: 'Check the model and provider entries in config.toml.', }, 'auth.login_required': { title: 'Login required', retryable: false, public: true, action: 'Run the login flow for the provider before retrying.', }, 'context.overflow': { title: 'Context window overflow', retryable: true, public: true, action: 'Compact the conversation or start a new session.', }, 'loop.max_steps_exceeded': { title: 'Turn exceeded max steps', retryable: false, public: true, action: 'Increase loop_control.max_steps_per_turn in config.toml or split the task.', }, 'provider.api_error': { title: 'Provider API error', retryable: false, public: true, action: 'Inspect details.statusCode / details.requestId; check provider status.', }, 'provider.filtered': { title: 'Provider filtered response', retryable: false, public: true, action: 'Revise the prompt or model configuration to avoid provider safety filtering.', }, 'provider.rate_limit': { title: 'Provider rate limit', retryable: true, public: true, action: 'Retry after a delay or reduce request frequency.', }, 'provider.auth_error': { title: 'Provider authentication error', retryable: false, public: true, action: 'Re-authenticate with the provider.', }, 'provider.connection_error': { title: 'Provider connection error', retryable: true, public: true, action: 'Check network connectivity and retry.', }, 'skill.not_found': { title: 'Skill not found', retryable: false, public: true, action: 'List available skills via the skill registry.', }, 'skill.type_unsupported': { title: 'Skill type not supported', retryable: false, public: true, action: 'Only inline skills can be activated by the user.', }, 'skill.name_empty': { title: 'Skill name is empty', retryable: false, public: true, action: 'Provide a non-empty skill name.', }, 'records.write_failed': { title: 'Failed to write records', retryable: true, public: true, action: 'Check disk space and permissions on the session directory.', }, 'compaction.failed': { title: 'Compaction failed', retryable: false, public: true, action: 'Inspect logs and consider increasing compaction limits.', }, 'compaction.unable': { title: 'Unable to compact', retryable: false, public: true, action: 'The current history has no prefix that can be compacted (e.g. only a pending user message). Start a new turn or session instead.', }, 'task.task_id_empty': { title: 'Background task id is empty', retryable: false, public: true, action: 'Provide a non-empty task id.', }, 'mcp.server_not_found': { title: 'MCP server not found', retryable: false, public: true, action: 'List configured MCP servers and check the requested name.', }, 'mcp.server_disabled': { title: 'MCP server is disabled', retryable: false, public: true, action: 'Enable the MCP server entry in config before reconnecting.', }, 'mcp.startup_failed': { title: 'MCP server startup failed', retryable: true, public: true, action: 'Inspect the MCP server log or call reconnect once the server is healthy.', }, 'mcp.tool_name_collision': { title: 'MCP tool name collision', retryable: false, public: true, action: 'Rename one of the colliding MCP tools or servers so their qualified names are unique.', }, 'mcp.oauth_failed': { title: 'MCP OAuth authorization failed', retryable: true, public: true, action: 'Begin the authorization flow again; inspect the error details if it keeps failing.', }, 'plugin.not_found': { title: 'Plugin not found', retryable: false, public: true, action: 'List installed plugins via /plugins and check the requested id.', }, 'plugin.load_failed': { title: 'Plugin state failed to load', retryable: true, public: true, action: 'Fix the installed.json file under $KIMI_CODE_HOME/plugins/ and run /plugins reload.', }, 'request.invalid': { title: 'Invalid request', retryable: false, public: true, action: 'Check the input shape matches the API contract.', }, 'request.work_dir_required': { title: 'workDir is required', retryable: false, public: true, action: 'Provide workDir in the request payload.', }, 'request.prompt_input_empty': { title: 'Prompt input is empty', retryable: false, public: true, action: 'Provide non-empty prompt input.', }, 'shell.git_bash_not_found': { title: 'Git Bash not found', retryable: false, public: true, action: 'Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe.', }, not_implemented: { title: 'Not implemented', retryable: false, public: true, action: 'This feature is not implemented yet.', }, internal: { title: 'Internal error', retryable: false, public: true, action: 'Inspect logs or report the issue with diagnostics.', }, } as const satisfies Record; export function isKimiErrorCode(code: unknown): code is KimiErrorCode { return typeof code === 'string' && Object.hasOwn(KIMI_ERROR_INFO, code); } export interface KimiErrorOptions { readonly details?: Record; readonly cause?: unknown; } export class KimiError extends Error { readonly code: KimiErrorCode; readonly details?: Record; override readonly cause?: unknown; constructor(code: KimiErrorCode, message: string, options: KimiErrorOptions = {}) { super(message); this.name = 'KimiError'; this.code = code; this.details = options.details; this.cause = options.cause; } } export interface KimiErrorPayload { readonly code: KimiErrorCode; readonly message: string; readonly name?: string; readonly details?: Record; readonly retryable: boolean; } export function isKimiError(error: unknown): error is KimiError { return error instanceof KimiError; } export function makeErrorPayload( code: KimiErrorCode, message: string, options?: { readonly details?: Record; readonly name?: string }, ): KimiErrorPayload { return { code, message, name: options?.name, details: options?.details, retryable: KIMI_ERROR_INFO[code].retryable, }; } export function toKimiErrorPayload(error: unknown): KimiErrorPayload { if (isKimiError(error)) { return { code: error.code, message: error.message, name: error.name, details: error.details, retryable: KIMI_ERROR_INFO[error.code].retryable, }; } if (error instanceof APIStatusError) { const code: KimiErrorCode = error instanceof APIProviderQuotaExhaustedError ? ErrorCodes.PROVIDER_API_ERROR : error.statusCode === 429 ? ErrorCodes.PROVIDER_RATE_LIMIT : error.statusCode === 401 ? ErrorCodes.PROVIDER_AUTH_ERROR : ErrorCodes.PROVIDER_API_ERROR; return { code, message: sanitizeStatusErrorMessage(error.message), name: error.name, details: { statusCode: error.statusCode, requestId: error.requestId, }, retryable: KIMI_ERROR_INFO[code].retryable, }; } if (error instanceof APIConnectionError || error instanceof APITimeoutError) { return { code: ErrorCodes.PROVIDER_CONNECTION_ERROR, message: error.message, name: error.name, retryable: KIMI_ERROR_INFO[ErrorCodes.PROVIDER_CONNECTION_ERROR].retryable, }; } if (error instanceof APIEmptyResponseError) { const code = error.finishReason === 'filtered' ? ErrorCodes.PROVIDER_FILTERED : ErrorCodes.PROVIDER_API_ERROR; return { code, message: error.message, name: error.name, details: { finishReason: error.finishReason, rawFinishReason: error.rawFinishReason, }, retryable: KIMI_ERROR_INFO[code].retryable, }; } if (error instanceof ChatProviderError) { return { code: ErrorCodes.PROVIDER_API_ERROR, message: error.message, name: error.name, retryable: KIMI_ERROR_INFO[ErrorCodes.PROVIDER_API_ERROR].retryable, }; } if (error instanceof Error) { return { code: ErrorCodes.INTERNAL, message: error.message, name: error.name, retryable: KIMI_ERROR_INFO[ErrorCodes.INTERNAL].retryable, }; } return { code: ErrorCodes.INTERNAL, message: String(error), retryable: KIMI_ERROR_INFO[ErrorCodes.INTERNAL].retryable, }; } function sanitizeStatusErrorMessage(message: string): string { const titleMatch = /]*>([\s\S]*?)<\/title>/i.exec(message); const extracted = titleMatch?.[1]?.trim(); const normalized = extracted !== undefined && extracted.length > 0 ? extracted : message; return normalized.replaceAll('\r', ''); } export function fromKimiErrorPayload(payload: KimiErrorPayload): KimiError { return new KimiError(payload.code, payload.message, { details: payload.details, }); }