diff --git a/packages/agent-core-v2/docs/errors.md b/packages/agent-core-v2/docs/errors.md index e488d07ffe8..c4355465d0c 100644 --- a/packages/agent-core-v2/docs/errors.md +++ b/packages/agent-core-v2/docs/errors.md @@ -22,6 +22,7 @@ unified `ErrorCodes` const. ## Conventions (hard rules) - **Throw a coded error, not a bare string.** `throw new Error2(ErrorCodes.X, …)`. `throw new Error('x')` only for unreachable guards; `BugIndicatingError` when the throw site indicates a caller bug (e.g. reading a service before its `ready`); `NotImplementedError('feature')` for stubs. +- **Every domain codes ALL of its failure modes.** This includes errors raised on tool-execution paths whose message is fed back to the model (tool-input validation is a domain failure mode too) — whether a given scope (App / Workspace / Session / Agent) or the model ever sees an error is decided by event-filtered subscriptions, never by the error's type. The uncoded errors left are: `_base` infrastructure errors (DI, event, lifecycle, text, execEnv — deliberately left as plain guards / classes for now), control-flow sentinels that never leave their domain (`UserCancellationError`, `TaskCancelledError`, `TransientCloudError`, `GrepAbortedError`, `ProcessExitError`, `CompactionTruncatedError`), `CyclicDependencyError` (a documented DI wiring protection), and `PathSecurityError` (tool-path validation with its own `PathSecurityCode` taxonomy). The `ChatProviderError` L0 taxonomy is born-coded: every class extends `Error2` and computes its wire code at construction (`kosong/contract/errors.ts`), so `translateProviderError` is only the abort guard plus the foreign-error fallback. - **Define codes in the owning domain.** A domain's codes live in `/errors.ts` next to its interfaces, exported as an `XxxErrors` descriptor — never in `_base/errors`. - **One `code` per failure mode.** Codes read `domain.reason` (e.g. `tool.unknown_tool`). The set of valid code strings is fixed by the protocol (`KimiErrorCode`); adding a brand-new code means updating the protocol first. Renaming/removing a code is a major (breaks SDK clients). - **Import from the facade.** Throw sites and cross-domain consumers do `import { ErrorCodes, Error2 } from '#/errors'`. A domain's own `errors.ts` references its own descriptor (`LoopErrors.codes.X`) and imports only from `#/_base/errors` (never from `#/errors`, to avoid cycles). @@ -70,7 +71,7 @@ The os / persistence / wire domains show the standard shapes: - **`os.fs` (`HostFsError`, `os/interface/hostFsErrors.ts`)** — every `IHostFileSystem` backend translates raw errnos at its boundary via the pure `toHostFsError(err, { path, op })`: `ENOENT→os.fs.not_found`, `EISDIR→os.fs.is_directory`, `ENOTDIR→os.fs.not_directory`, `EEXIST→os.fs.already_exists`, `EACCES/EPERM→os.fs.permission_denied`, `ENOTEMPTY→os.fs.not_empty`, everything else `os.fs.unknown`. `details` carries `{ path, op, errno?, syscall? }`. Documented boolean semantics (e.g. `createExclusive` returning `false` on `EEXIST`) stay booleans, not errors. - **`os.process` (`HostProcessError`, `os/interface/hostProcess.ts`)** — `os.process.spawn_failed` (details `{ command, args?, cwd?, errno? }`) and `os.process.kill_failed`; both carry the raw error as `cause`. Kill keeps its deliberate tolerances: `ESRCH` is a silent no-op, `EPERM` degrades to `child.kill()`. -- **`storage` (`StorageError`, `persistence/interface/storage.ts`)** — `storage.not_found` / `decode_failed` / `corrupted` / `io_failed` / `locked`. ENOENT keeps its established absence semantics (`read → undefined`, `list → []`) and is *not* an error; other I/O failures become `storage.io_failed` (`retryable`). Codec parse failures become `storage.decode_failed` with `{ scope, key, format }`; append-log corruption is `AppendLogCorruptedError` (`storage.corrupted`). `storage.locked` is reserved for a store exclusively held by another process — consumers (e.g. `FileSessionIndex`) catch it explicitly and fall back to their non-read-model path with a one-time warning; there is no silent no-op degradation. (The minidb query-store backend is a multi-process `ClusterDb` and no longer throws it: peers share the store, and per-shard lock contention surfaces as a transient `LockError` instead.) +- **`storage` (`StorageError`, `persistence/interface/storage.ts`)** — `storage.not_found` / `decode_failed` / `corrupted` / `io_failed` / `locked` / `permission_denied` / `disk_full`. ENOENT keeps its established absence semantics (`read → undefined`, `list → []`) and is *not* an error; other I/O failures are mapped by errno at the backend boundary via `toStorageIoError`: `EACCES/EPERM→storage.permission_denied`, `ENOSPC→storage.disk_full`, an unexpected `ENOENT→storage.not_found`, everything else `storage.io_failed` (the only retryable one besides `storage.locked`). Codec parse failures become `storage.decode_failed` with `{ scope, key, format }`; append-log corruption is `AppendLogCorruptedError` (`storage.corrupted`). `storage.locked` is reserved for a store exclusively held by another process — consumers (e.g. `FileSessionIndex`) catch it explicitly and fall back to their non-read-model path with a one-time warning; there is no silent no-op degradation. (The minidb query-store backend is a multi-process `ClusterDb` and no longer throws it: peers share the store, and per-shard lock contention surfaces as a transient `LockError` instead.) - **`wire` (`WireError`, `wire/errors.ts`)** — `DuplicateOpError` (`wire.duplicate_op`, a build-time bug), `CycleError` (`wire.cycle`, details carry the drain depth and a capped op-type sample), and `wire.unknown_record`: replay skips records whose Op type is absent from `OP_REGISTRY` (compatibility), reports each skip through `onUnexpectedError`, and returns `{ unknownRecords }` so the caller knows the restore was lossy. ## Serialization & boundary translation diff --git a/packages/agent-core-v2/src/agent/contextMemory/contextOps.ts b/packages/agent-core-v2/src/agent/contextMemory/contextOps.ts index f2a1adab15f..1fc5cae6028 100644 --- a/packages/agent-core-v2/src/agent/contextMemory/contextOps.ts +++ b/packages/agent-core-v2/src/agent/contextMemory/contextOps.ts @@ -39,6 +39,7 @@ import { z } from 'zod'; +import { ErrorCodes, Error2 } from '#/errors'; import type { ContentPart } from '#/kosong/contract/message'; import { defineModel, type PartsTransformer } from '#/wire/model'; import type { WireRecord } from '#/wire/record'; @@ -238,7 +239,17 @@ export function readContextCompactedCount(record: ContextCompactionRecord): numb if (typeof compactedCount === 'number') return compactedCount; const legacyCount = fields['count']; if (typeof legacyCount === 'number') return legacyCount; - throw new Error('Invalid context.apply_compaction record: missing compactedCount'); + throw new Error2( + ErrorCodes.STORAGE_DECODE_FAILED, + 'Invalid context.apply_compaction record: missing compactedCount', + { + details: { + recordKeys: Object.keys(record), + compactedCountType: typeof compactedCount, + countType: typeof legacyCount, + }, + }, + ); } export function readContextCompactionSummary(record: ContextCompactionRecord): ContextMessage { @@ -248,7 +259,17 @@ export function readContextCompactionSummary(record: ContextCompactionRecord): C const summary = fields['summary']; if (typeof summary === 'string') return createCompactionSummaryMessage(summary); if (isContextMessage(summary)) return summary; - throw new Error('Invalid context.apply_compaction record: missing summary'); + throw new Error2( + ErrorCodes.STORAGE_DECODE_FAILED, + 'Invalid context.apply_compaction record: missing summary', + { + details: { + recordKeys: Object.keys(record), + summaryType: typeof summary, + contextSummaryType: typeof contextSummary, + }, + }, + ); } function readContextCompactionRawSummary(record: UnknownRecord): string { @@ -259,7 +280,17 @@ function readContextCompactionRawSummary(record: UnknownRecord): string { if (isContextMessage(summary)) { return textOf(summary); } - throw new Error('Invalid context.apply_compaction record: missing summary'); + throw new Error2( + ErrorCodes.STORAGE_DECODE_FAILED, + 'Invalid context.apply_compaction record: missing summary', + { + details: { + recordKeys: Object.keys(record), + summaryType: typeof summary, + contextSummaryType: typeof contextSummary, + }, + }, + ); } function readLegacySummaryMessage(record: UnknownRecord): ContextMessage | undefined { diff --git a/packages/agent-core-v2/src/agent/contextMemory/conversationUndoParticipants.ts b/packages/agent-core-v2/src/agent/contextMemory/conversationUndoParticipants.ts index bf3ef9f3d10..937a5839db6 100644 --- a/packages/agent-core-v2/src/agent/contextMemory/conversationUndoParticipants.ts +++ b/packages/agent-core-v2/src/agent/contextMemory/conversationUndoParticipants.ts @@ -8,6 +8,7 @@ import { createDecorator } from '#/_base/di/instantiation'; import { Disposable, toDisposable, type IDisposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { BugIndicatingError } from '#/errors'; export interface AgentConversationUndoParticipant { readonly id: string; @@ -36,7 +37,7 @@ class AgentConversationUndoParticipantRegistry register(participant: AgentConversationUndoParticipant): IDisposable { if (this.participants.has(participant.id)) { - throw new Error( + throw new BugIndicatingError( `Conversation undo participant "${participant.id}" is already registered`, ); } diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts index aec58a5d4c4..4b46d1bc5e7 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts @@ -96,7 +96,7 @@ import { type LlmRequestToolSchema, } from './llmRequestOps'; import { isAbortError } from '#/_base/utils/abort'; -import { unwrapErrorCause } from '#/errors'; +import { ErrorCodes, Error2, unwrapErrorCause } from '#/errors'; import { retryErrorFields } from '#/_base/utils/retry'; const EMPTY_TOOL_PARAMETERS: Record = { @@ -416,7 +416,10 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService { } if (message === undefined || finish === undefined) { - throw new Error('LLM request stream ended without a finish event.'); + throw new Error2( + ErrorCodes.PROVIDER_API_ERROR, + 'LLM request stream ended without a finish event.', + ); } this.usage.record(request.modelAlias, usage, request.source); diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index df36717a2e0..68d2e9cd844 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -579,7 +579,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { options: LoopErrorHandlerRegistrationOptions = {}, ): IDisposable { if (options.before !== undefined && options.after !== undefined) { - throw new Error('Loop error handler registration cannot specify both before and after'); + throw new BugIndicatingError('Loop error handler registration cannot specify both before and after'); } this.deleteErrorHandler(handler.id); const target = options.before ?? options.after; @@ -588,7 +588,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { } else { const targetIndex = this.errorHandlers.findIndex((entry) => entry.id === target); if (targetIndex < 0) { - throw new Error(`Loop error handler target "${target}" is not registered`); + throw new BugIndicatingError(`Loop error handler target "${target}" is not registered`); } const insertAt = options.before !== undefined ? targetIndex : targetIndex + 1; this.errorHandlers.splice(insertAt, 0, handler); diff --git a/packages/agent-core-v2/src/agent/mcp/tools/mcp.ts b/packages/agent-core-v2/src/agent/mcp/tools/mcp.ts index 9b9f7d648fb..5cda40b2f45 100644 --- a/packages/agent-core-v2/src/agent/mcp/tools/mcp.ts +++ b/packages/agent-core-v2/src/agent/mcp/tools/mcp.ts @@ -25,7 +25,7 @@ import type { Tool as KosongTool } from '#/kosong/contract/tool'; import type { ITelemetryService } from '#/app/telemetry/telemetry'; -import { toErrorMessage } from '#/errors'; +import { Error2, ErrorCodes, toErrorMessage } from '#/errors'; import { isAbortError } from '#/_base/utils/abort'; import type { ExecutableTool, ExecutableToolContext, ExecutableToolResult } from '#/tool/toolContract'; @@ -118,7 +118,8 @@ async function retryAfterReconnect( if (context.signal.aborted || isAbortError(reconnectError)) { throw reconnectError; } - throw new Error( + throw new Error2( + ErrorCodes.MCP_STARTUP_FAILED, `${toErrorMessage(failure)} (reconnecting the MCP server also failed: ${toErrorMessage(reconnectError)})`, { cause: reconnectError }, ); diff --git a/packages/agent-core-v2/src/agent/permissionRules/matchesRule.ts b/packages/agent-core-v2/src/agent/permissionRules/matchesRule.ts index f5614cbb9a7..d67ca9d409a 100644 --- a/packages/agent-core-v2/src/agent/permissionRules/matchesRule.ts +++ b/packages/agent-core-v2/src/agent/permissionRules/matchesRule.ts @@ -1,5 +1,6 @@ import picomatch from 'picomatch'; +import { Error2, ErrorCodes } from '#/errors'; import type { RunnableToolExecution } from '#/tool/toolContract'; import type { PermissionRule } from './permissionRules'; @@ -31,7 +32,7 @@ export interface PermissionRuleMatchInput { export function parsePattern(pattern: string): ParsedPattern { const trimmed = pattern.trim(); if (trimmed.length === 0) { - throw new Error('permission pattern: empty string'); + throw new Error2(ErrorCodes.VALIDATION_FAILED, 'permission pattern: empty string'); } const openIdx = trimmed.indexOf('('); @@ -40,13 +41,13 @@ export function parsePattern(pattern: string): ParsedPattern { } if (!trimmed.endsWith(')')) { - throw new Error(`permission pattern: missing closing paren in "${pattern}"`); + throw new Error2(ErrorCodes.VALIDATION_FAILED, `permission pattern: missing closing paren in "${pattern}"`); } const toolName = trimmed.slice(0, openIdx); const argPattern = trimmed.slice(openIdx + 1, -1); if (toolName.length === 0) { - throw new Error(`permission pattern: empty tool name in "${pattern}"`); + throw new Error2(ErrorCodes.VALIDATION_FAILED, `permission pattern: empty tool name in "${pattern}"`); } if (argPattern.length === 0) { return { toolName }; diff --git a/packages/agent-core-v2/src/agent/plan/planService.ts b/packages/agent-core-v2/src/agent/plan/planService.ts index 255a0c50584..5ca8ddb29d7 100644 --- a/packages/agent-core-v2/src/agent/plan/planService.ts +++ b/packages/agent-core-v2/src/agent/plan/planService.ts @@ -27,6 +27,7 @@ import { dirname, join } from 'pathe'; import { Disposable, type IDisposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { unwrapErrorCause } from '#/_base/errors/errors'; +import { Error2, ErrorCodes } from '#/errors'; import { generateHeroSlug } from '#/_base/utils/hero-slug'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector'; @@ -181,7 +182,7 @@ export class AgentPlanService extends Disposable implements IAgentPlanService { async enter(id = this.createPlanId(), createFile = false): Promise { if (this.isActive) { - throw new Error('Already in plan mode'); + throw new Error2(ErrorCodes.SESSION_PLAN_MODE_INVALID, 'Already in plan mode'); } const planFilePath = this.planFilePathFor(id); diff --git a/packages/agent-core-v2/src/agent/shellCommand/shellCommandService.ts b/packages/agent-core-v2/src/agent/shellCommand/shellCommandService.ts index 1c8afce678d..3512eb8b3d0 100644 --- a/packages/agent-core-v2/src/agent/shellCommand/shellCommandService.ts +++ b/packages/agent-core-v2/src/agent/shellCommand/shellCommandService.ts @@ -31,6 +31,7 @@ import { IAgentStateService } from '#/agent/state/agentState'; import type { ToolUpdate } from '#/tool/toolContract'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { IEventBus } from '#/app/event/eventBus'; +import { Error2, ErrorCodes } from '#/errors'; import { IAgentShellCommandService, @@ -200,7 +201,7 @@ export class AgentShellCommandService implements IAgentShellCommandService { private ensureBashTool() { const bash = this.toolRegistry.resolve('Bash'); if (bash === undefined) { - throw new Error('Bash tool is not registered.'); + throw new Error2(ErrorCodes.INTERNAL, 'Bash tool is not registered.'); } return bash; } diff --git a/packages/agent-core-v2/src/agent/task/errors.ts b/packages/agent-core-v2/src/agent/task/errors.ts index 7be6f5b054a..f2e43ace268 100644 --- a/packages/agent-core-v2/src/agent/task/errors.ts +++ b/packages/agent-core-v2/src/agent/task/errors.ts @@ -7,7 +7,9 @@ import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; export const TaskErrors = { codes: { TASK_ID_EMPTY: 'task.task_id_empty', + TASK_LIMIT_EXCEEDED: 'task.limit_exceeded', }, + retryable: ['task.limit_exceeded'], } as const satisfies ErrorDomain; registerErrorDomain(TaskErrors); diff --git a/packages/agent-core-v2/src/agent/task/persist.ts b/packages/agent-core-v2/src/agent/task/persist.ts index 6f57d700cdc..9a58e131e7e 100644 --- a/packages/agent-core-v2/src/agent/task/persist.ts +++ b/packages/agent-core-v2/src/agent/task/persist.ts @@ -19,6 +19,7 @@ import { join } from 'pathe'; +import { BugIndicatingError } from '#/errors'; import type { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore'; import type { IFileSystemStorageService } from '#/persistence/interface/storage'; @@ -62,7 +63,7 @@ interface TaskOutputData { function validateTaskId(taskId: string): void { if (!VALID_TASK_ID.test(taskId)) { - throw new Error(`Invalid task id: "${taskId}"`); + throw new BugIndicatingError(`Invalid task id: "${taskId}"`); } } diff --git a/packages/agent-core-v2/src/agent/task/taskService.ts b/packages/agent-core-v2/src/agent/task/taskService.ts index 04a9920d4e3..d5078a767ac 100644 --- a/packages/agent-core-v2/src/agent/task/taskService.ts +++ b/packages/agent-core-v2/src/agent/task/taskService.ts @@ -54,6 +54,7 @@ import { } from '#/_base/utils/abort'; import { escapeXml, escapeXmlAttr } from '#/_base/utils/xml-escape'; import { IEventBus } from '#/app/event/eventBus'; +import { Error2, ErrorCodes } from '#/errors'; import { defineCheckpointedModel } from '#/agent/contextMemory/conversationTime'; import { IAgentConversationUndoParticipantRegistry } from '#/agent/contextMemory/conversationUndoParticipants'; import type { ContextMessage, TaskOrigin } from '#/agent/contextMemory/types'; @@ -918,7 +919,9 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { if (maxRunningTasks === undefined) return; if (!detached) return; if (this.activeTaskCount() < maxRunningTasks) return; - throw new Error('Too many background tasks are already running.'); + throw new Error2(ErrorCodes.TASK_LIMIT_EXCEEDED, 'Too many background tasks are already running.', { + details: { running: this.activeTaskCount(), max: maxRunningTasks }, + }); } private activeTaskCount(): number { diff --git a/packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts b/packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts index 099e2ef62e0..7f2f2780712 100644 --- a/packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts +++ b/packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts @@ -23,6 +23,7 @@ */ import { Emitter } from '#/_base/event'; +import { BugIndicatingError } from '#/errors'; import type { ToolCall } from '#/kosong/contract/message'; import type { LLMRequestTrace } from '#/kosong/contract/requestTrace'; import type { @@ -112,7 +113,7 @@ export class BeforeToolExecuteEventImpl implements BeforeToolExecuteEvent { private assertOpen(statement: string): void { if (!this._open) { - throw new Error(`${statement} can NOT be called asynchronously`); + throw new BugIndicatingError(`${statement} can NOT be called asynchronously`); } } } diff --git a/packages/agent-core-v2/src/agent/tools/agent-swarm/agentSwarmTool.ts b/packages/agent-core-v2/src/agent/tools/agent-swarm/agentSwarmTool.ts index ddae2a75e0b..87e48f573ac 100644 --- a/packages/agent-core-v2/src/agent/tools/agent-swarm/agentSwarmTool.ts +++ b/packages/agent-core-v2/src/agent/tools/agent-swarm/agentSwarmTool.ts @@ -26,6 +26,7 @@ import { type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; +import { Error2, ErrorCodes } from '#/errors'; import { registerAgentToolService } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IConfigService } from '#/app/config/config'; @@ -171,11 +172,17 @@ export class AgentSwarmTool implements IAgentSwarmTool { const own = this.profile.data(); const allowlist = subagentAllowlistFor(this.catalog, own); if (allowlist !== undefined && !allowlist.includes(profileName)) { - throw new Error(subagentTypeNotAllowedMessage(profileName, allowlist)); + throw new Error2( + ErrorCodes.AGENT_TYPE_NOT_ALLOWED, + subagentTypeNotAllowedMessage(profileName, allowlist), + { details: { profileName, allowlist } }, + ); } const targetProfile = this.catalog.get(profileName); if (targetProfile === undefined) { - throw new Error(`Unknown agent type: "${profileName}"`); + throw new Error2(ErrorCodes.PROFILE_UNKNOWN, `Unknown agent type: "${profileName}"`, { + details: { profileName }, + }); } if (own.modelAlias !== undefined) { binding = resolveSubagentBinding( @@ -242,18 +249,30 @@ async function createAgentSwarmSpecs( const resumeCount = resumeEntries.length; const totalCount = resumeCount + itemCount; if (!hasMinimumAgentSwarmInputs(itemCount, resumeCount)) { - throw new Error('AgentSwarm requires at least 2 items unless resume_agent_ids is provided.'); + throw new Error2( + ErrorCodes.VALIDATION_FAILED, + 'AgentSwarm requires at least 2 items unless resume_agent_ids is provided.', + ); } if (totalCount > MAX_AGENT_SWARM_SUBAGENTS) { - throw new Error(`AgentSwarm supports at most ${String(MAX_AGENT_SWARM_SUBAGENTS)} subagents.`); + throw new Error2( + ErrorCodes.VALIDATION_FAILED, + `AgentSwarm supports at most ${String(MAX_AGENT_SWARM_SUBAGENTS)} subagents.`, + { details: { total: totalCount, max: MAX_AGENT_SWARM_SUBAGENTS } }, + ); } const promptTemplate = normalizeOptionalString(args.prompt_template); if (items.length > 0 && promptTemplate === undefined) { - throw new Error('prompt_template is required when items are provided.'); + throw new Error2( + ErrorCodes.VALIDATION_FAILED, + 'prompt_template is required when items are provided.', + ); } if (promptTemplate !== undefined && !promptTemplate.includes(PROMPT_TEMPLATE_PLACEHOLDER)) { - throw new Error( + throw new Error2( + ErrorCodes.VALIDATION_FAILED, `prompt_template must include the ${PROMPT_TEMPLATE_PLACEHOLDER} placeholder.`, + { details: { placeholder: PROMPT_TEMPLATE_PLACEHOLDER } }, ); } @@ -274,8 +293,10 @@ async function createAgentSwarmSpecs( const prompt = itemPromptTemplate.split(PROMPT_TEMPLATE_PLACEHOLDER).join(item); const previousIndex = seenPrompts.get(prompt); if (previousIndex !== undefined) { - throw new Error( + throw new Error2( + ErrorCodes.VALIDATION_FAILED, `Duplicate subagent prompts from items ${String(previousIndex)} and ${String(index + 1)}. AgentSwarm requires distinct subagents.`, + { details: { previousIndex, index: index + 1 } }, ); } seenPrompts.set(prompt, index + 1); diff --git a/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts b/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts index 1795eeda2fe..64d8aa9dde1 100644 --- a/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts +++ b/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts @@ -32,6 +32,7 @@ import { isUserCancellation, userCancellationReason, } from '#/_base/utils/abort'; +import { Error2, ErrorCodes, isError2 } from '#/errors'; import { toInputJsonSchema } from '#/tool/input-schema'; import { matchesGlobRuleSubject } from '#/tool/rule-match'; import { @@ -240,7 +241,11 @@ export class SubagentTool implements ISubagentTool { ): Promise { const requester = this.lifecycle.get(this.callerAgentId); if (requester === undefined) { - throw new Error(`Caller agent "${this.callerAgentId}" does not exist`); + throw new Error2( + ErrorCodes.AGENT_NOT_FOUND, + `Caller agent "${this.callerAgentId}" does not exist`, + { details: { agentId: this.callerAgentId } }, + ); } const resumeAgentId = args.resume?.trim(); @@ -252,7 +257,9 @@ export class SubagentTool implements ISubagentTool { if (isResume) { const target = this.lifecycle.get(resumeAgentId); if (target === undefined) { - throw new Error(`Agent instance "${resumeAgentId}" does not exist`); + throw new Error2(ErrorCodes.AGENT_NOT_FOUND, `Agent instance "${resumeAgentId}" does not exist`, { + details: { agentId: resumeAgentId }, + }); } await this.ensureOwnedIdleSubagent(resumeAgentId, target); agentId = target.id; @@ -266,14 +273,22 @@ export class SubagentTool implements ISubagentTool { const own = this.profile.data(); const allowlist = subagentAllowlistFor(this.catalog, own); if (allowlist !== undefined && !allowlist.includes(requestedProfileName)) { - throw new Error(subagentTypeNotAllowedMessage(requestedProfileName, allowlist)); + throw new Error2( + ErrorCodes.AGENT_TYPE_NOT_ALLOWED, + subagentTypeNotAllowedMessage(requestedProfileName, allowlist), + { details: { profileName: requestedProfileName, allowlist } }, + ); } const profile = this.catalog.get(requestedProfileName); if (profile === undefined) { - throw new Error(`Unknown agent type: "${requestedProfileName}"`); + throw new Error2(ErrorCodes.PROFILE_UNKNOWN, `Unknown agent type: "${requestedProfileName}"`, { + details: { profileName: requestedProfileName }, + }); } if (own.modelAlias === undefined) { - throw new Error('Caller agent has no model bound'); + throw new Error2(ErrorCodes.MODEL_NOT_CONFIGURED, 'Caller agent has no model bound', { + details: { agentId: this.callerAgentId }, + }); } const binding = resolveSubagentBinding( this.config, @@ -342,13 +357,23 @@ export class SubagentTool implements ISubagentTool { ): Promise { const meta = (await this.sessionMetadata.read()).agents?.[agentId]; if (!isSubagentMeta(meta)) { - throw new Error(`Agent instance "${agentId}" is not a subagent`); + throw new Error2(ErrorCodes.AGENT_NOT_A_SUBAGENT, `Agent instance "${agentId}" is not a subagent`, { + details: { agentId }, + }); } if (subagentParentAgentId(meta) !== this.callerAgentId) { - throw new Error(`Agent instance "${agentId}" does not belong to this parent agent`); + throw new Error2( + ErrorCodes.AGENT_NOT_OWNED, + `Agent instance "${agentId}" does not belong to this parent agent`, + { details: { agentId, callerAgentId: this.callerAgentId } }, + ); } if (target.accessor.get(IAgentLoopService).status().state === 'running') { - throw new Error(`Agent instance "${agentId}" is already running and cannot run concurrently`); + throw new Error2( + ErrorCodes.AGENT_ALREADY_RUNNING, + `Agent instance "${agentId}" is already running and cannot run concurrently`, + { details: { agentId } }, + ); } } @@ -422,7 +447,7 @@ export class SubagentTool implements ISubagentTool { const message = error instanceof Error ? error.message : String(error); return { output: - message === 'Too many detached tasks are already running.' + isError2(error) && error.code === ErrorCodes.TASK_LIMIT_EXCEEDED ? 'Too many background tasks are already running.' : message, isError: true, diff --git a/packages/agent-core-v2/src/agent/tools/skill/skill.ts b/packages/agent-core-v2/src/agent/tools/skill/skill.ts index eca7445a129..6dfaa2bd68a 100644 --- a/packages/agent-core-v2/src/agent/tools/skill/skill.ts +++ b/packages/agent-core-v2/src/agent/tools/skill/skill.ts @@ -13,20 +13,22 @@ import { z } from 'zod'; import { createDecorator } from '#/_base/di/instantiation'; +import { Error2, ErrorCodes } from '#/errors'; import { type AgentTool } from '#/tool/toolContract'; export const MAX_SKILL_QUERY_DEPTH = 3; -export class NestedSkillTooDeepError extends Error { +export class NestedSkillTooDeepError extends Error2 { readonly skillName?: string; readonly depth: number; constructor(depth: number, skillName?: string) { const label = skillName !== undefined ? ` "${skillName}"` : ''; super( + ErrorCodes.SKILL_NESTED_TOO_DEEP, `Nested skill invocation${label} exceeded the maximum depth of ${String(depth)} — refusing to recurse further.`, + { name: 'NestedSkillTooDeepError', details: { depth, skillName } }, ); - this.name = 'NestedSkillTooDeepError'; this.depth = depth; if (skillName !== undefined) this.skillName = skillName; } diff --git a/packages/agent-core-v2/src/agent/tools/web-search/webSearchTool.ts b/packages/agent-core-v2/src/agent/tools/web-search/webSearchTool.ts index 38e86ca8209..c0e9e0d9c26 100644 --- a/packages/agent-core-v2/src/agent/tools/web-search/webSearchTool.ts +++ b/packages/agent-core-v2/src/agent/tools/web-search/webSearchTool.ts @@ -23,6 +23,7 @@ import { import { ToolResultBuilder } from '#/tool/result-builder'; import { registerAgentToolService } from '#/agent/toolRegistry/toolContribution'; import { IWebSearchProviderService } from '#/app/auth/webSearch/webSearch'; +import { Error2, ErrorCodes } from '#/errors'; import { IWebSearchTool, @@ -46,7 +47,7 @@ export class WebSearchTool implements IWebSearchTool { ) { const provider = providerService.getWebSearchProvider(); if (provider === undefined) { - throw new Error('WebSearchProviderService returned no provider during tool activation.'); + throw new Error2(ErrorCodes.INTERNAL, 'WebSearchProviderService returned no provider during tool activation.'); } this.provider = provider; } diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/builtinAgentProfileLoaderService.ts b/packages/agent-core-v2/src/app/agentProfileCatalog/builtinAgentProfileLoaderService.ts index 042a4bf780a..06ad5bd894c 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/builtinAgentProfileLoaderService.ts +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/builtinAgentProfileLoaderService.ts @@ -6,13 +6,14 @@ * `IAgentProfileRegistry`. Register-after-construction is not supported: like * `IAgentToolRegistryService`, contributions are expected to accumulate at * import time before the container resolves the service. `getDefault()` - * throws a plain `Error` when the builtin default profile is missing — a + * throws a `BugIndicatingError` when the builtin default profile is missing — a * programming-time invariant violation, not a request failure. Bound at App * scope. */ import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { BugIndicatingError } from '#/errors'; import type { AgentProfile } from './agentProfileCatalog'; import { DEFAULT_AGENT_PROFILE_NAME } from './agentProfileCatalog'; @@ -54,7 +55,7 @@ export class BuiltinAgentProfileLoaderService getDefault(): AgentProfile { const profile = this.byName.get(DEFAULT_AGENT_PROFILE_NAME); if (profile === undefined) { - throw new Error( + throw new BugIndicatingError( `Default agent profile "${DEFAULT_AGENT_PROFILE_NAME}" is not registered`, ); } diff --git a/packages/agent-core-v2/src/app/auth/authService.ts b/packages/agent-core-v2/src/app/auth/authService.ts index a147eff8018..9b581c45345 100644 --- a/packages/agent-core-v2/src/app/auth/authService.ts +++ b/packages/agent-core-v2/src/app/auth/authService.ts @@ -45,6 +45,7 @@ import type { import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { Error2, ErrorCodes } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigService } from '#/app/config/config'; import { IEventService } from '#/app/event/event'; @@ -317,7 +318,9 @@ export class OAuthService extends Disposable implements IOAuthService { }); const tokenProvider = this.resolveTokenProvider(KIMI_CODE_PROVIDER_NAME, auth.oauthRef); if (tokenProvider === undefined) { - throw new Error('OAuth token provider is not configured.'); + throw new Error2(ErrorCodes.AUTH_TOKEN_MISSING, 'OAuth token provider is not configured.', { + details: { provider_id: KIMI_CODE_PROVIDER_NAME }, + }); } const token = await tokenProvider.getAccessToken(); const models = await fetchManagedKimiCodeModels({ diff --git a/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts index de85211e966..d75812f9363 100644 --- a/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts @@ -1,4 +1,5 @@ import type { WebSearchProvider, WebSearchResult } from '#/agent/tools/web-search/web-search'; +import { Error2, ErrorCodes } from '#/errors'; export interface BearerTokenProvider { getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; @@ -60,15 +61,19 @@ export class MoonshotWebSearchProvider implements WebSearchProvider { if (response.status === 401) { const detail = await safeReadText(response); - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, `Moonshot search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim(), + { details: { status: response.status } }, ); } if (response.status !== 200) { const detail = await safeReadText(response); - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, `Moonshot search request failed: HTTP ${String(response.status)}. ${detail}`.trim(), + { details: { status: response.status } }, ); } @@ -119,7 +124,8 @@ export class MoonshotWebSearchProvider implements WebSearchProvider { } } if (this.apiKey !== undefined && this.apiKey.length > 0) return this.apiKey; - throw new Error( + throw new Error2( + ErrorCodes.AUTH_TOKEN_MISSING, 'Moonshot search service is not configured: missing API key or token provider.', ); } diff --git a/packages/agent-core-v2/src/app/config/configService.ts b/packages/agent-core-v2/src/app/config/configService.ts index 05ba4b0c0ee..8546c80c9bd 100644 --- a/packages/agent-core-v2/src/app/config/configService.ts +++ b/packages/agent-core-v2/src/app/config/configService.ts @@ -25,6 +25,7 @@ import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Emitter, type Event } from '#/_base/event'; +import { BugIndicatingError } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { ILogService } from '#/_base/log/log'; import { @@ -170,7 +171,7 @@ export class ConfigRegistry implements IConfigRegistry { ) { return; } - throw new Error(`ConfigRegistry: section '${domain}' is already registered`); + throw new BugIndicatingError(`ConfigRegistry: section '${domain}' is already registered`); } this.sections.set(domain, { domain, diff --git a/packages/agent-core-v2/src/app/cron/cron-expr.ts b/packages/agent-core-v2/src/app/cron/cron-expr.ts index ea5548f6a14..bf50e875423 100644 --- a/packages/agent-core-v2/src/app/cron/cron-expr.ts +++ b/packages/agent-core-v2/src/app/cron/cron-expr.ts @@ -17,6 +17,8 @@ * that. */ +import { Error2, ErrorCodes } from '#/errors'; + /** A parsed cron expression. Opaque to callers — pass it back into {@link computeNextCronRun}. */ export interface ParsedCronExpression { readonly raw: string; @@ -39,16 +41,20 @@ const MS_PER_MINUTE = 60_000; export function parseCronExpression(expr: string): ParsedCronExpression { if (typeof expr !== 'string') { - throw new TypeError('cron expression must be a string'); + throw new Error2(ErrorCodes.CRON_EXPRESSION_INVALID, 'cron expression must be a string', { + details: { received: typeof expr }, + }); } const trimmed = expr.trim(); if (trimmed === '') { - throw new Error('cron expression is empty'); + throw new Error2(ErrorCodes.CRON_EXPRESSION_INVALID, 'cron expression is empty'); } const fields = trimmed.split(/\s+/); if (fields.length !== 5) { - throw new Error( + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, `cron expression must have exactly 5 fields (minute hour day-of-month month day-of-week); got ${fields.length}`, + { details: { fieldCount: fields.length } }, ); } const [minField, hourField, domField, monthField, dowField] = fields as [ @@ -85,18 +91,26 @@ function isWildcard(field: string): boolean { function parseField(field: string, min: number, max: number, name: string): Set { if (field === '') { - throw new Error(`cron ${name} field is empty`); + throw new Error2(ErrorCodes.CRON_EXPRESSION_INVALID, `cron ${name} field is empty`, { + details: { field: name }, + }); } const out = new Set(); const terms = field.split(','); for (const term of terms) { if (term === '') { - throw new Error(`cron ${name} field has empty term in list`); + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, + `cron ${name} field has empty term in list`, + { details: { field: name } }, + ); } addTerm(out, term, min, max, name); } if (out.size === 0) { - throw new Error(`cron ${name} field matches no values`); + throw new Error2(ErrorCodes.CRON_EXPRESSION_INVALID, `cron ${name} field matches no values`, { + details: { field: name }, + }); } return out; } @@ -105,8 +119,10 @@ const DIGIT_ONLY = /^\d+$/; function parseCronInt(raw: string, name: string, role: string): number { if (!DIGIT_ONLY.test(raw)) { - throw new Error( + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, `cron ${name} ${role} must be a non-negative integer with digits only (got ${JSON.stringify(raw)})`, + { details: { field: name, role, value: raw } }, ); } return Number.parseInt(raw, 10); @@ -120,15 +136,25 @@ function addTerm(out: Set, term: string, min: number, max: number, name: rangePart = term.slice(0, slash); const stepStr = term.slice(slash + 1); if (stepStr === '') { - throw new Error(`cron ${name} step is empty in "${term}"`); + throw new Error2(ErrorCodes.CRON_EXPRESSION_INVALID, `cron ${name} step is empty in "${term}"`, { + details: { field: name, term }, + }); } const parsedStep = parseCronInt(stepStr, name, 'step'); if (parsedStep <= 0) { - throw new Error(`cron ${name} step must be a positive integer (got "${stepStr}")`); + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, + `cron ${name} step must be a positive integer (got "${stepStr}")`, + { details: { field: name, term, step: stepStr } }, + ); } step = parsedStep; if (rangePart === '') { - throw new Error(`cron ${name} step needs a range or "*" before "/" in "${term}"`); + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, + `cron ${name} step needs a range or "*" before "/" in "${term}"`, + { details: { field: name, term } }, + ); } } @@ -142,7 +168,11 @@ function addTerm(out: Set, term: string, min: number, max: number, name: if (dash === -1) { const single = parseCronInt(rangePart, name, 'value'); if (single < min || single > max) { - throw new Error(`cron ${name} value ${single} out of range ${min}..${max}`); + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, + `cron ${name} value ${single} out of range ${min}..${max}`, + { details: { field: name, value: single, min, max } }, + ); } if (slash !== -1) { lo = single; @@ -157,8 +187,10 @@ function addTerm(out: Set, term: string, min: number, max: number, name: lo = parseCronInt(loStr, name, 'range lower bound'); hi = parseCronInt(hiStr, name, 'range upper bound'); if (lo < min || hi > max || lo > hi) { - throw new Error( + throw new Error2( + ErrorCodes.CRON_EXPRESSION_INVALID, `cron ${name} range ${lo}-${hi} out of bounds (must be ${min}..${max}, ascending)`, + { details: { field: name, lo, hi, min, max } }, ); } } diff --git a/packages/agent-core-v2/src/app/cron/errors.ts b/packages/agent-core-v2/src/app/cron/errors.ts new file mode 100644 index 00000000000..02730b44601 --- /dev/null +++ b/packages/agent-core-v2/src/app/cron/errors.ts @@ -0,0 +1,13 @@ +/** + * `cron` domain error codes. + */ + +import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; + +export const CronErrors = { + codes: { + CRON_EXPRESSION_INVALID: 'cron.expression_invalid', + }, +} as const satisfies ErrorDomain; + +registerErrorDomain(CronErrors); diff --git a/packages/agent-core-v2/src/app/flag/flagRegistryService.ts b/packages/agent-core-v2/src/app/flag/flagRegistryService.ts index 610580f07dd..ae1dfdd3e90 100644 --- a/packages/agent-core-v2/src/app/flag/flagRegistryService.ts +++ b/packages/agent-core-v2/src/app/flag/flagRegistryService.ts @@ -8,6 +8,7 @@ import { Disposable, type IDisposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { BugIndicatingError } from '#/errors'; import { type FlagDefinitionInput, @@ -46,7 +47,7 @@ export class FlagRegistryService extends Disposable implements IFlagRegistry { private add(definition: FlagDefinitionInput): void { if (this.byId.has(definition.id)) { - throw new Error(`Flag '${definition.id}' is already registered`); + throw new BugIndicatingError(`Flag '${definition.id}' is already registered`); } this.byId.set(definition.id, definition); } diff --git a/packages/agent-core-v2/src/app/gateway/gatewayService.ts b/packages/agent-core-v2/src/app/gateway/gatewayService.ts index 8dc8f2b1ae0..dba32d345ed 100644 --- a/packages/agent-core-v2/src/app/gateway/gatewayService.ts +++ b/packages/agent-core-v2/src/app/gateway/gatewayService.ts @@ -16,6 +16,7 @@ import { registerScopedService, } from '#/_base/di/scope'; import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; +import { Error2, ErrorCodes } from '#/errors'; import { ILogService } from '#/_base/log/log'; import { IWorkspaceLifecycleService } from '#/app/workspaceLifecycle/workspaceLifecycle'; import { ISessionLifecycleService } from '#/workspace/sessionLifecycle/sessionLifecycle'; @@ -34,10 +35,18 @@ export class RestGateway implements IRestGateway { private agent(sessionId: string, agentId: string): IAgentScopeHandle { const session = this.liveSession(sessionId); - if (session === undefined) throw new Error(`unknown session '${sessionId}'`); + if (session === undefined) { + throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `unknown session '${sessionId}'`, { + details: { sessionId }, + }); + } const agents = session.accessor.get(IAgentLifecycleService); const agent = agents.get(agentId); - if (agent === undefined) throw new Error(`unknown agent '${agentId}'`); + if (agent === undefined) { + throw new Error2(ErrorCodes.AGENT_NOT_FOUND, `unknown agent '${agentId}'`, { + details: { agentId, sessionId }, + }); + } return agent; } diff --git a/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts b/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts index dcb715227a7..6db7396fda6 100644 --- a/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts +++ b/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts @@ -13,6 +13,9 @@ import { z } from 'zod'; import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; +import { CoreErrors } from '#/_base/errors/codes'; +import { Error2 } from '#/_base/errors/errors'; +import { FsErrors } from '#/workspace/workspaceFs/internal/errors'; export const fsBrowseQuerySchema = z.object({ path: z.string().min(1).optional(), @@ -39,28 +42,30 @@ export const fsHomeResponseSchema = z.object({ }); export type FsHomeResponse = z.infer; -export class HostFolderNotAbsoluteError extends Error { +export class HostFolderNotAbsoluteError extends Error2 { readonly path: string; constructor(path: string) { - super(`path must be absolute: ${path}`); + super(CoreErrors.codes.VALIDATION_FAILED, `path must be absolute: ${path}`, { + details: { path }, + }); this.name = 'HostFolderNotAbsoluteError'; this.path = path; } } -export class HostFolderNotFoundError extends Error { +export class HostFolderNotFoundError extends Error2 { readonly path: string; constructor(path: string) { - super(`path not found: ${path}`); + super(FsErrors.codes.FS_PATH_NOT_FOUND, `path not found: ${path}`, { details: { path } }); this.name = 'HostFolderNotFoundError'; this.path = path; } } -export class HostFolderPermissionError extends Error { +export class HostFolderPermissionError extends Error2 { readonly path: string; constructor(path: string) { - super(`permission denied: ${path}`); + super(FsErrors.codes.FS_PERMISSION_DENIED, `permission denied: ${path}`, { details: { path } }); this.name = 'HostFolderPermissionError'; this.path = path; } diff --git a/packages/agent-core-v2/src/app/kosongConfig/discoveryService.ts b/packages/agent-core-v2/src/app/kosongConfig/discoveryService.ts index 6b0110d93f1..fc7a77a08f3 100644 --- a/packages/agent-core-v2/src/app/kosongConfig/discoveryService.ts +++ b/packages/agent-core-v2/src/app/kosongConfig/discoveryService.ts @@ -46,6 +46,7 @@ import { import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Error2 } from '#/_base/errors/errors'; import { IOAuthService } from '#/app/auth/auth'; +import { AuthErrors } from '#/app/auth/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigService } from '#/app/config/config'; import { IEventService } from '#/app/event/event'; @@ -282,7 +283,9 @@ export class ProviderDiscoveryService implements IProviderDiscoveryService { oauthRef as unknown as OAuthRef | undefined, ); if (tokenProvider === undefined) { - throw new Error('OAuth token provider is not configured.'); + throw new Error2(AuthErrors.codes.AUTH_TOKEN_MISSING, 'OAuth token provider is not configured.', { + details: { provider_id: providerName }, + }); } return tokenProvider.getAccessToken(); } diff --git a/packages/agent-core-v2/src/app/kosongConfig/modelsDevUpstream.ts b/packages/agent-core-v2/src/app/kosongConfig/modelsDevUpstream.ts index 4a112b62c4b..9ec40db4c36 100644 --- a/packages/agent-core-v2/src/app/kosongConfig/modelsDevUpstream.ts +++ b/packages/agent-core-v2/src/app/kosongConfig/modelsDevUpstream.ts @@ -4,7 +4,8 @@ * item mapping behind the import service's browse methods. */ -import { Error2 } from '#/_base/errors/errors'; +import { CoreErrors } from '#/_base/errors/codes'; +import { BugIndicatingError, Error2 } from '#/_base/errors/errors'; import type { ModelCapability } from '#/kosong/contract/capability'; import type { ModelRecord } from '#/kosong/model/model'; @@ -79,10 +80,14 @@ async function fetchAndCache(): Promise { headers: { Accept: 'application/json', 'User-Agent': 'kimi-code-kap-server' }, signal: AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS), }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + if (!res.ok) { + throw new Error2(CoreErrors.codes.INTERNAL, `HTTP ${res.status}`, { + details: { status: res.status }, + }); + } const payload: unknown = await res.json(); if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) { - throw new Error('unexpected catalog payload shape'); + throw new Error2(CoreErrors.codes.INTERNAL, 'unexpected catalog payload shape'); } cache = { catalog: payload as ModelsDevCatalog, fetchedAt: now }; return cache.catalog; @@ -176,7 +181,9 @@ export function toModelsDevProviderItem( reject_reason: resolution.reason, }; } - throw new Error(`unhandled models.dev import resolution: ${JSON.stringify(resolution)}`); + throw new BugIndicatingError( + `unhandled models.dev import resolution: ${JSON.stringify(resolution)}`, + ); } diff --git a/packages/agent-core-v2/src/app/plugin/archive.ts b/packages/agent-core-v2/src/app/plugin/archive.ts index eae97f1a502..781bdc6ea3f 100644 --- a/packages/agent-core-v2/src/app/plugin/archive.ts +++ b/packages/agent-core-v2/src/app/plugin/archive.ts @@ -5,6 +5,8 @@ import { pipeline } from 'node:stream/promises'; import { type Entry, fromBuffer as yauzlFromBuffer } from 'yauzl'; +import { Error2, ErrorCodes } from '#/errors'; + export async function downloadZip(url: string, signal?: AbortSignal): Promise { const controller = new AbortController(); const timeoutHandle = setTimeout(() => { @@ -13,7 +15,11 @@ export async function downloadZip(url: string, signal?: AbortSignal): Promise((resolve, reject) => { yauzlFromBuffer(buffer, { lazyEntries: true }, (openErr, zipfile) => { if (openErr !== null || zipfile === undefined) { - reject(new Error(`Failed to open zip: ${openErr?.message ?? 'unknown error'}`)); + reject( + new Error2( + ErrorCodes.PLUGIN_LOAD_FAILED, + `Failed to open zip: ${openErr?.message ?? 'unknown error'}`, + { cause: openErr ?? undefined }, + ), + ); return; } @@ -40,7 +52,13 @@ export async function extractZip(buffer: Buffer, destDir: string): Promise\` to bypass release lookup.`, + { details: { owner, repo, status: resp.status, url } }, ); } diff --git a/packages/agent-core-v2/src/app/plugin/manager.ts b/packages/agent-core-v2/src/app/plugin/manager.ts index f0e13c3473c..2b6fae8549f 100644 --- a/packages/agent-core-v2/src/app/plugin/manager.ts +++ b/packages/agent-core-v2/src/app/plugin/manager.ts @@ -9,7 +9,7 @@ import { cp, mkdir, mkdtemp, realpath, rename, rm, stat } from 'node:fs/promises import { tmpdir } from 'node:os'; import path from 'node:path'; -import { Error2, PluginErrors } from '#/errors'; +import { BugIndicatingError, Error2, ErrorCodes, PluginErrors } from '#/errors'; import type { HookDef } from '#/agent/externalHooks/types'; import type { McpServerConfig } from '#/mcpCore/config-schema'; import type { PluginAgentRoot } from './types'; @@ -125,10 +125,12 @@ export class PluginManager { const parsed = await parseManifest(sourceRoot); if (parsed.manifest === undefined) { const msg = parsed.diagnostics.find((d) => d.severity === 'error')?.message ?? 'no manifest'; - throw new Error( + throw new Error2( + ErrorCodes.PLUGIN_LOAD_FAILED, sourceType === 'local-path' ? `Cannot install plugin at ${sourceRoot}: ${msg}` : `Cannot install plugin from ${originalSource}: ${msg}`, + { details: { sourceType } }, ); } @@ -165,10 +167,16 @@ export class PluginManager { try { await rollbackManagedPluginCopy(managedCopy); } catch (rollbackError) { - throw new AggregateError( - [error, rollbackError], + throw new Error2( + ErrorCodes.PLUGIN_LOAD_FAILED, 'Plugin installation failed and the previous managed copy could not be restored', - { cause: error }, + { + cause: new AggregateError( + [error, rollbackError], + 'Plugin installation failed and the previous managed copy could not be restored', + { cause: error }, + ), + }, ); } } @@ -196,7 +204,11 @@ export class PluginManager { const current = this.records.get(key); if (current === undefined) throw pluginNotFound(id); if (current.manifest?.mcpServers?.[server] === undefined) { - throw new Error(`Plugin "${id}" does not declare MCP server "${server}"`); + throw new Error2( + ErrorCodes.MCP_SERVER_NOT_FOUND, + `Plugin "${id}" does not declare MCP server "${server}"`, + { details: { id, server } }, + ); } const currentMcpServers = current.capabilities?.mcpServers ?? {}; const nextCapabilities: PluginCapabilityState = { @@ -416,7 +428,8 @@ async function installedGithubSha( async function checkGithubUpdate(record: PluginRecord): Promise { const github = record.github; - if (github === undefined) throw new Error(`Plugin "${record.id}" has no GitHub metadata`); + if (github === undefined) + throw new BugIndicatingError(`Plugin "${record.id}" has no GitHub metadata`); const current = github.ref; const pinned = explicitGithubRef(record); @@ -487,16 +500,25 @@ function pluginNotFound(id: string): Error2 { async function normalizeInstallRoot(rootPath: string): Promise { const trimmed = rootPath.trim(); if (!path.isAbsolute(trimmed)) { - throw new Error(`Plugin root must be an absolute path (got "${rootPath}")`); + throw new Error2( + ErrorCodes.VALIDATION_FAILED, + `Plugin root must be an absolute path (got "${rootPath}")`, + { details: { path: rootPath } }, + ); } let resolved: string; try { resolved = await realpath(trimmed); } catch (error) { - throw new Error(`Plugin root does not exist: ${trimmed}`, { cause: error }); + throw new Error2(ErrorCodes.FS_PATH_NOT_FOUND, `Plugin root does not exist: ${trimmed}`, { + cause: error, + details: { path: trimmed }, + }); } if (!(await stat(resolved)).isDirectory()) { - throw new Error(`Plugin root is not a directory: ${trimmed}`); + throw new Error2(ErrorCodes.VALIDATION_FAILED, `Plugin root is not a directory: ${trimmed}`, { + details: { path: trimmed }, + }); } return resolved; } diff --git a/packages/agent-core-v2/src/app/plugin/pluginService.ts b/packages/agent-core-v2/src/app/plugin/pluginService.ts index a215a5b0ae9..84226957bb2 100644 --- a/packages/agent-core-v2/src/app/plugin/pluginService.ts +++ b/packages/agent-core-v2/src/app/plugin/pluginService.ts @@ -15,7 +15,7 @@ import { KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; import { Disposable } from '#/_base/di/lifecycle'; import { Emitter, type Event } from '#/_base/event'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; -import { Error2, PluginErrors } from '#/errors'; +import { BugIndicatingError, Error2, PluginErrors } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IProviderService } from '#/kosong/provider/provider'; import { ISkillDiscovery } from '#/app/skillCatalog/skillDiscovery'; @@ -86,7 +86,8 @@ export class PluginService extends Disposable implements IPluginService { return this.runSerializedOperation(async () => { const record = await this.manager.install(input.source); const info = this.manager.info(record.id); - if (info === undefined) throw new Error(`Plugin "${record.id}" missing right after install`); + if (info === undefined) + throw new BugIndicatingError(`Plugin "${record.id}" missing right after install`); return info; }); } diff --git a/packages/agent-core-v2/src/app/plugin/source.ts b/packages/agent-core-v2/src/app/plugin/source.ts index a2092c087e8..7909e81a98c 100644 --- a/packages/agent-core-v2/src/app/plugin/source.ts +++ b/packages/agent-core-v2/src/app/plugin/source.ts @@ -1,5 +1,7 @@ import path from 'node:path'; +import { Error2, ErrorCodes } from '#/errors'; + export interface GithubRef { readonly kind: 'branch' | 'tag' | 'sha'; readonly value: string; @@ -24,7 +26,11 @@ export function resolveInstallSource(source: string): ResolvedSource { return { kind: 'zip-url', path: trimmed }; } if (!path.isAbsolute(trimmed)) { - throw new Error(`Plugin root must be an absolute path (got "${source}")`); + throw new Error2( + ErrorCodes.VALIDATION_FAILED, + `Plugin root must be an absolute path (got "${source}")`, + { details: { source } }, + ); } return { kind: 'local-path', path: trimmed }; } diff --git a/packages/agent-core-v2/src/app/plugin/store.ts b/packages/agent-core-v2/src/app/plugin/store.ts index 6439ae235cb..e367c7cdee4 100644 --- a/packages/agent-core-v2/src/app/plugin/store.ts +++ b/packages/agent-core-v2/src/app/plugin/store.ts @@ -1,6 +1,8 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import path from 'node:path'; +import { Error2, ErrorCodes } from '#/errors'; + import type { PluginCapabilityState, PluginGithubMetadata, PluginSource } from './types'; const INSTALLED_REL = path.join('plugins', 'installed.json'); @@ -33,15 +35,24 @@ export async function readInstalled(kimiHomeDir: string): Promise if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY; throw error; } + let parsed: InstalledFile; try { - const parsed = JSON.parse(text) as InstalledFile; - if (typeof parsed !== 'object' || parsed === null || !Array.isArray(parsed.plugins)) { - throw new Error('installed.json is not a valid InstalledFile object'); - } - return parsed; + parsed = JSON.parse(text) as InstalledFile; } catch (error) { - throw new Error(`Failed to parse ${filePath}: ${(error as Error).message}`, { cause: error }); + throw new Error2( + ErrorCodes.PLUGIN_LOAD_FAILED, + `Failed to parse ${filePath}: ${(error as Error).message}`, + { cause: error, details: { path: filePath } }, + ); + } + if (typeof parsed !== 'object' || parsed === null || !Array.isArray(parsed.plugins)) { + throw new Error2( + ErrorCodes.PLUGIN_LOAD_FAILED, + `Failed to parse ${filePath}: installed.json is not a valid InstalledFile object`, + { details: { path: filePath } }, + ); } + return parsed; } export async function writeInstalled(kimiHomeDir: string, data: InstalledFile): Promise { diff --git a/packages/agent-core-v2/src/app/sessionExport/file-source.ts b/packages/agent-core-v2/src/app/sessionExport/file-source.ts index a2b4fc9fc31..49494d46072 100644 --- a/packages/agent-core-v2/src/app/sessionExport/file-source.ts +++ b/packages/agent-core-v2/src/app/sessionExport/file-source.ts @@ -10,6 +10,8 @@ import { Readable } from 'node:stream'; import { finished } from 'node:stream/promises'; import { resolve } from 'pathe'; +import { Error2, ErrorCodes } from '#/errors'; + export interface ZipSource { readonly stream: Readable; readonly size: number; @@ -31,9 +33,17 @@ export async function openZipSource(source: string, signal?: AbortSignal): Promi try { signal?.throwIfAborted(); const file = await handle.stat({ bigint: true }); - if (!file.isFile()) throw new Error(`not a file: ${source}`); + if (!file.isFile()) { + throw new Error2(ErrorCodes.FS_IS_DIRECTORY, `not a file: ${source}`, { + details: { path: source }, + }); + } const size = Number(file.size); - if (!Number.isSafeInteger(size)) throw new Error(`file is too large to export: ${source}`); + if (!Number.isSafeInteger(size)) { + throw new Error2(ErrorCodes.SESSION_EXPORT_TOO_LARGE, `file is too large to export: ${source}`, { + details: { path: source }, + }); + } signal?.throwIfAborted(); stream = size === 0 diff --git a/packages/agent-core-v2/src/app/skillCatalog/errors.ts b/packages/agent-core-v2/src/app/skillCatalog/errors.ts index 601cf2177cb..fda774c4c50 100644 --- a/packages/agent-core-v2/src/app/skillCatalog/errors.ts +++ b/packages/agent-core-v2/src/app/skillCatalog/errors.ts @@ -9,6 +9,8 @@ export const SkillErrors = { SKILL_NOT_FOUND: 'skill.not_found', SKILL_TYPE_UNSUPPORTED: 'skill.type_unsupported', SKILL_NAME_EMPTY: 'skill.name_empty', + SKILL_PARSE_FAILED: 'skill.parse_failed', + SKILL_NESTED_TOO_DEEP: 'skill.nested_too_deep', }, } as const satisfies ErrorDomain; diff --git a/packages/agent-core-v2/src/app/skillCatalog/parser.ts b/packages/agent-core-v2/src/app/skillCatalog/parser.ts index 317ac100a8b..3a99a4a525b 100644 --- a/packages/agent-core-v2/src/app/skillCatalog/parser.ts +++ b/packages/agent-core-v2/src/app/skillCatalog/parser.ts @@ -8,27 +8,31 @@ import path from 'pathe'; +import { Error2 } from '#/_base/errors/errors'; import { FrontmatterError, parseFrontmatter } from '#/_base/text/frontmatter'; +import { SkillErrors } from './errors'; import type { SkillDefinition, SkillMetadata, SkillSource } from './types'; import { isSupportedSkillType } from './types'; -export class SkillParseError extends Error { +export class SkillParseError extends Error2 { readonly reason?: unknown; constructor(message: string, cause?: unknown) { - super(message); + super(SkillErrors.codes.SKILL_PARSE_FAILED, message, { cause }); this.name = 'SkillParseError'; if (cause !== undefined) this.reason = cause; } } -export class UnsupportedSkillTypeError extends Error { +export class UnsupportedSkillTypeError extends Error2 { readonly skillType: string; constructor(skillType: string) { super( + SkillErrors.codes.SKILL_TYPE_UNSUPPORTED, `Skill type "${skillType}" is not supported; only "prompt", "inline", and "flow" are supported.`, + { details: { skillType } }, ); this.name = 'UnsupportedSkillTypeError'; this.skillType = skillType; diff --git a/packages/agent-core-v2/src/app/web/errors.ts b/packages/agent-core-v2/src/app/web/errors.ts new file mode 100644 index 00000000000..ad66e25aaf2 --- /dev/null +++ b/packages/agent-core-v2/src/app/web/errors.ts @@ -0,0 +1,16 @@ +/** + * `web` domain error codes — URL fetching and SSRF guard failures. + */ + +import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; + +export const WebErrors = { + codes: { + WEB_INVALID_URL: 'web.invalid_url', + WEB_PRIVATE_ADDRESS: 'web.private_address', + WEB_FETCH_FAILED: 'web.fetch_failed', + }, + retryable: ['web.fetch_failed'], +} as const satisfies ErrorDomain; + +registerErrorDomain(WebErrors); diff --git a/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts b/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts index 8c01fce0e18..403f2efe049 100644 --- a/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts @@ -21,6 +21,7 @@ import { parseHTML as rawParseHTML } from 'linkedom'; import { Agent, type Dispatcher } from 'undici'; import { isProxyConfigured, makeNoProxyMatcher, resolveNoProxy } from '#/_base/utils/proxy'; +import { Error2, ErrorCodes } from '#/errors'; import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../tools/fetch-url-types'; @@ -103,8 +104,10 @@ export class LocalFetchURLProvider implements UrlFetcher { if (Number.isFinite(cl) && cl > this.maxBytes) { await response.body?.cancel().catch(() => { }); - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, `Response body too large: ${String(cl)} bytes exceeds maxBytes (${String(this.maxBytes)}).`, + { details: { bytes: cl, maxBytes: this.maxBytes } }, ); } } @@ -113,8 +116,10 @@ export class LocalFetchURLProvider implements UrlFetcher { const actualBytes = Buffer.byteLength(body, 'utf8'); if (actualBytes > this.maxBytes) { - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, `Response body too large: ${String(actualBytes)} bytes exceeds maxBytes (${String(this.maxBytes)}).`, + { details: { bytes: actualBytes, maxBytes: this.maxBytes } }, ); } @@ -148,8 +153,10 @@ export class LocalFetchURLProvider implements UrlFetcher { await response.body?.cancel().catch(() => { }); if (redirects >= MAX_REDIRECT_HOPS) { - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, `Too many redirects while fetching "${url}" (limit ${String(MAX_REDIRECT_HOPS)}).`, + { details: { url, limit: MAX_REDIRECT_HOPS } }, ); } redirects += 1; @@ -201,7 +208,8 @@ export class LocalFetchURLProvider implements UrlFetcher { const fallbackText = (container?.textContent ?? '').trim(); if (fallbackText.length === 0) { - throw new Error( + throw new Error2( + ErrorCodes.WEB_FETCH_FAILED, 'Failed to extract meaningful content from the page. The page may require JavaScript to render.', ); } @@ -243,10 +251,14 @@ async function resolveSafeFetchTarget(url: string, allowPrivate: boolean): Promi try { parsed = new URL(url); } catch { - throw new Error(`Invalid URL: "${url}"`); + throw new Error2(ErrorCodes.WEB_INVALID_URL, `Invalid URL: "${url}"`, { details: { url } }); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - throw new Error(`Unsupported URL scheme "${parsed.protocol}" — only http(s) allowed.`); + throw new Error2( + ErrorCodes.WEB_INVALID_URL, + `Unsupported URL scheme "${parsed.protocol}" — only http(s) allowed.`, + { details: { url, protocol: parsed.protocol } }, + ); } const hostRaw = parsed.hostname.toLowerCase(); const host = hostRaw.startsWith('[') && hostRaw.endsWith(']') ? hostRaw.slice(1, -1) : hostRaw; @@ -254,25 +266,35 @@ async function resolveSafeFetchTarget(url: string, allowPrivate: boolean): Promi if (allowPrivate) return { host, port }; if (isIP(host) !== 0) { if (isBlockedAddress(host)) { - throw new Error(`Refusing to fetch private address: "${host}"`); + throw new Error2(ErrorCodes.WEB_PRIVATE_ADDRESS, `Refusing to fetch private address: "${host}"`, { + details: { host }, + }); } return { host, port }; } if (host === 'localhost' || host.endsWith('.localhost')) { - throw new Error(`Refusing to fetch private host: "${host}"`); + throw new Error2(ErrorCodes.WEB_PRIVATE_ADDRESS, `Refusing to fetch private host: "${host}"`, { + details: { host }, + }); } let addresses: LookupAddress[]; try { addresses = await lookup(host, { all: true }); } catch (error) { const detail = error instanceof Error ? error.message : String(error); - throw new Error(`Cannot resolve host "${host}" for the fetch safety check: ${detail}`, { - cause: error, - }); + throw new Error2( + ErrorCodes.WEB_PRIVATE_ADDRESS, + `Cannot resolve host "${host}" for the fetch safety check: ${detail}`, + { cause: error, details: { host } }, + ); } for (const { address } of addresses) { if (isBlockedAddress(address)) { - throw new Error(`Refusing to fetch host "${host}": resolves to private address "${address}".`); + throw new Error2( + ErrorCodes.WEB_PRIVATE_ADDRESS, + `Refusing to fetch host "${host}": resolves to private address "${address}".`, + { details: { host, address } }, + ); } } return { host, port, addresses }; diff --git a/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts index 5f69d2fc3e3..f992c267a7f 100644 --- a/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts @@ -1,3 +1,5 @@ +import { Error2, ErrorCodes } from '#/errors'; + import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../tools/fetch-url-types'; interface BearerTokenProvider { @@ -103,6 +105,9 @@ export class MoonshotFetchURLProvider implements UrlFetcher { } } if (this.apiKey !== undefined && this.apiKey.length > 0) return this.apiKey; - throw new Error('Moonshot fetch service is not configured: missing API key or token provider.'); + throw new Error2( + ErrorCodes.AUTH_TOKEN_MISSING, + 'Moonshot fetch service is not configured: missing API key or token provider.', + ); } } diff --git a/packages/agent-core-v2/src/app/web/tools/fetch-url-types.ts b/packages/agent-core-v2/src/app/web/tools/fetch-url-types.ts index 1d1f2eb80b9..3750c03eac8 100644 --- a/packages/agent-core-v2/src/app/web/tools/fetch-url-types.ts +++ b/packages/agent-core-v2/src/app/web/tools/fetch-url-types.ts @@ -2,6 +2,10 @@ * `web` domain — host-injected `UrlFetcher` contract. */ +import { Error2 } from '#/_base/errors/errors'; + +import { WebErrors } from '../errors'; + /** * How the returned content relates to the original response body. * @@ -24,11 +28,11 @@ export interface UrlFetcher { ): Promise; } -export class HttpFetchError extends Error { +export class HttpFetchError extends Error2 { override readonly name = 'HttpFetchError'; readonly status: number; constructor(status: number, message: string) { - super(message); + super(WebErrors.codes.WEB_FETCH_FAILED, message, { details: { status } }); this.status = status; } } diff --git a/packages/agent-core-v2/src/errors.ts b/packages/agent-core-v2/src/errors.ts index 0f002c69e11..5ecfd33a549 100644 --- a/packages/agent-core-v2/src/errors.ts +++ b/packages/agent-core-v2/src/errors.ts @@ -10,6 +10,7 @@ import { AuthErrors } from '#/app/auth/errors'; import { TaskErrors } from '#/agent/task/errors'; import { ProtocolErrors } from '#/kosong/protocol/errors'; import { ConfigErrors } from '#/app/config/errors'; +import { CronErrors } from '#/app/cron/errors'; import { FileErrors } from '#/app/file/fileService'; import { FsErrors } from '#/workspace/workspaceFs/internal/errors'; import { FullCompactionErrors } from '#/agent/fullCompaction/errors'; @@ -30,6 +31,7 @@ import { SkillErrors } from '#/app/skillCatalog/errors'; import { StorageErrors } from '#/persistence/interface/storage'; import { TerminalErrors } from '#/os/interface/terminalErrors'; import { UsageErrors } from '#/agent/usage/errors'; +import { WebErrors } from '#/app/web/errors'; import { WireErrors } from '#/wire/errors'; import { WorkspaceErrors } from '#/app/workspace/errors'; @@ -43,6 +45,7 @@ export { AuthErrors } from '#/app/auth/errors'; export { TaskErrors } from '#/agent/task/errors'; export { ProtocolErrors } from '#/kosong/protocol/errors'; export { ConfigErrors } from '#/app/config/errors'; +export { CronErrors } from '#/app/cron/errors'; export { FileErrors } from '#/app/file/fileService'; export { FsErrors } from '#/workspace/workspaceFs/internal/errors'; export { FullCompactionErrors } from '#/agent/fullCompaction/errors'; @@ -63,6 +66,7 @@ export { SkillErrors } from '#/app/skillCatalog/errors'; export { StorageErrors } from '#/persistence/interface/storage'; export { TerminalErrors } from '#/os/interface/terminalErrors'; export { UsageErrors } from '#/agent/usage/errors'; +export { WebErrors } from '#/app/web/errors'; export { WireErrors } from '#/wire/errors'; export { WorkspaceErrors } from '#/app/workspace/errors'; @@ -73,6 +77,7 @@ export const ErrorCodes = { ...TaskErrors.codes, ...ProtocolErrors.codes, ...ConfigErrors.codes, + ...CronErrors.codes, ...FileErrors.codes, ...FsErrors.codes, ...FullCompactionErrors.codes, @@ -93,6 +98,7 @@ export const ErrorCodes = { ...StorageErrors.codes, ...TerminalErrors.codes, ...UsageErrors.codes, + ...WebErrors.codes, ...WireErrors.codes, ...WorkspaceErrors.codes, } as const; diff --git a/packages/agent-core-v2/src/hooks.ts b/packages/agent-core-v2/src/hooks.ts index 24b024940a9..dd97aede16d 100644 --- a/packages/agent-core-v2/src/hooks.ts +++ b/packages/agent-core-v2/src/hooks.ts @@ -5,6 +5,7 @@ * forks. Bound as utility infrastructure, not a scoped Service. */ import { toDisposable, type IDisposable } from "#/_base/di/lifecycle"; +import { BugIndicatingError } from "#/errors"; export type Hooks> = { readonly [K in keyof TEvents]: HookSlot; @@ -46,7 +47,7 @@ export class OrderedHookSlot implements HookSlot { options: HookRegisterOptions = {}, ): IDisposable { if (options.before !== undefined && options.after !== undefined) { - throw new Error('Hook registration cannot specify both before and after'); + throw new BugIndicatingError('Hook registration cannot specify both before and after'); } this.delete(id); @@ -59,7 +60,7 @@ export class OrderedHookSlot implements HookSlot { const targetIndex = this.entries.findIndex((item) => item.id === target); if (targetIndex < 0) { - throw new Error(`Hook target "${target}" is not registered`); + throw new BugIndicatingError(`Hook target "${target}" is not registered`); } const insertAt = options.before !== undefined ? targetIndex : targetIndex + 1; diff --git a/packages/agent-core-v2/src/kosong/contract/errors.ts b/packages/agent-core-v2/src/kosong/contract/errors.ts index d00b1efde88..bea23797a8f 100644 --- a/packages/agent-core-v2/src/kosong/contract/errors.ts +++ b/packages/agent-core-v2/src/kosong/contract/errors.ts @@ -9,6 +9,13 @@ * by-design capability gap (provider has no video upload hook) so callers * can tell it apart from an upload that failed at runtime. * + * The family is born-coded: every class extends `Error2` and computes its + * wire code (`provider.*` / `context.overflow`) at construction from the + * status code / finish reason, so no boundary translation is needed — the + * code string constants live here (the L0 wire contract) and are registered + * by `kosong/protocol/errors.ts` (`ProtocolErrors`). `translateProviderError` + * only remains as the abort guard and the foreign-error fallback. + * * Abort has exactly one standard shape here: the DOMException built by * `createAbortError`. Provider error converters must run the `throwIfAbortError` * guard FIRST in their classification chain — a user cancellation is thrown @@ -16,20 +23,55 @@ * a retryable provider error. */ +import { Error2, type Error2Options } from '#/_base/errors/errors'; import type { FinishReason } from './provider'; export const CONFIG_INVALID_ERROR_CODE = 'config.invalid'; -export class ChatProviderError extends Error { - constructor(message: string) { - super(message); - this.name = 'ChatProviderError'; +export const PROVIDER_API_ERROR_CODE = 'provider.api_error'; +export const PROVIDER_FILTERED_ERROR_CODE = 'provider.filtered'; +export const PROVIDER_RATE_LIMIT_ERROR_CODE = 'provider.rate_limit'; +export const PROVIDER_AUTH_ERROR_CODE = 'provider.auth_error'; +export const PROVIDER_CONNECTION_ERROR_CODE = 'provider.connection_error'; +export const PROVIDER_OVERLOADED_ERROR_CODE = 'provider.overloaded'; +export const CONTEXT_OVERFLOW_ERROR_CODE = 'context.overflow'; + +export type ProviderErrorCode = + | typeof PROVIDER_API_ERROR_CODE + | typeof PROVIDER_FILTERED_ERROR_CODE + | typeof PROVIDER_RATE_LIMIT_ERROR_CODE + | typeof PROVIDER_AUTH_ERROR_CODE + | typeof PROVIDER_CONNECTION_ERROR_CODE + | typeof PROVIDER_OVERLOADED_ERROR_CODE + | typeof CONTEXT_OVERFLOW_ERROR_CODE; + +export 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', ''); +} + +function codeForStatusError(statusCode: number): ProviderErrorCode { + if (statusCode === 429) return PROVIDER_RATE_LIMIT_ERROR_CODE; + if (statusCode === 401 || statusCode === 403) return PROVIDER_AUTH_ERROR_CODE; + if (statusCode === 529) return PROVIDER_OVERLOADED_ERROR_CODE; + return PROVIDER_API_ERROR_CODE; +} + +export class ChatProviderError extends Error2 { + constructor( + message: string, + code: ProviderErrorCode = PROVIDER_API_ERROR_CODE, + options?: Error2Options, + ) { + super(code, message, { ...options, name: 'ChatProviderError' }); } } export class APIConnectionError extends ChatProviderError { constructor(message: string) { - super(message); + super(message, PROVIDER_CONNECTION_ERROR_CODE); this.name = 'APIConnectionError'; } } @@ -43,7 +85,7 @@ export class VideoUploadUnsupportedError extends ChatProviderError { export class APITimeoutError extends ChatProviderError { constructor(message: string) { - super(message); + super(message, PROVIDER_CONNECTION_ERROR_CODE); this.name = 'APITimeoutError'; } } @@ -60,8 +102,11 @@ export class APIStatusError extends ChatProviderError { requestId?: string | null, retryAfterMs?: number | null, traceId?: string | null, + code: ProviderErrorCode = codeForStatusError(statusCode), ) { - super(message); + super(sanitizeStatusErrorMessage(message), code, { + details: { statusCode, requestId: requestId ?? null, traceId: traceId ?? null }, + }); this.name = 'APIStatusError'; this.statusCode = statusCode; this.requestId = requestId ?? null; @@ -78,7 +123,7 @@ export class APIContextOverflowError extends APIStatusError { retryAfterMs?: number | null, traceId?: string | null, ) { - super(statusCode, message, requestId, retryAfterMs, traceId); + super(statusCode, message, requestId, retryAfterMs, traceId, CONTEXT_OVERFLOW_ERROR_CODE); this.name = 'APIContextOverflowError'; } } @@ -115,7 +160,7 @@ export class APIProviderQuotaExhaustedError extends APIStatusError { retryAfterMs?: number | null, traceId?: string | null, ) { - super(429, message, requestId, retryAfterMs, traceId); + super(429, message, requestId, retryAfterMs, traceId, PROVIDER_API_ERROR_CODE); this.name = 'APIProviderQuotaExhaustedError'; } } @@ -128,7 +173,7 @@ export class APIProviderOverloadedError extends APIStatusError { retryAfterMs?: number | null, traceId?: string | null, ) { - super(statusCode, message, requestId, retryAfterMs, traceId); + super(statusCode, message, requestId, retryAfterMs, traceId, PROVIDER_OVERLOADED_ERROR_CODE); this.name = 'APIProviderOverloadedError'; } } @@ -144,10 +189,16 @@ export class APIEmptyResponseError extends ChatProviderError { readonly rawFinishReason?: string | null; } = {}, ) { - super(message); + const finishReason = options.finishReason ?? null; + const rawFinishReason = options.rawFinishReason ?? null; + super( + message, + finishReason === 'filtered' ? PROVIDER_FILTERED_ERROR_CODE : PROVIDER_API_ERROR_CODE, + { details: { finishReason, rawFinishReason } }, + ); this.name = 'APIEmptyResponseError'; - this.finishReason = options.finishReason ?? null; - this.rawFinishReason = options.rawFinishReason ?? null; + this.finishReason = finishReason; + this.rawFinishReason = rawFinishReason; } } diff --git a/packages/agent-core-v2/src/kosong/model/inspection.ts b/packages/agent-core-v2/src/kosong/model/inspection.ts index fd3b0da9644..bf94aae2279 100644 --- a/packages/agent-core-v2/src/kosong/model/inspection.ts +++ b/packages/agent-core-v2/src/kosong/model/inspection.ts @@ -19,6 +19,8 @@ import { parseKimiCodeCustomHeaders } from '@moonshot-ai/kimi-code-oauth'; +import { BugIndicatingError } from '#/_base/errors/errors'; + import type { ModelCapability } from '#/kosong/contract/capability'; import type { InspectionSource, ResolutionTrace } from '#/kosong/contract/inspection'; import type { Protocol, ProtocolProviderOptions } from '#/kosong/protocol/protocol'; @@ -501,7 +503,7 @@ function attributeHeaders( function required(trace: ResolutionTraceCollector, key: string, what: string): T { const value = trace.captured(key); if (value === undefined) { - throw new Error(`resolution trace is missing the ${what} capture ('${key}')`); + throw new BugIndicatingError(`resolution trace is missing the ${what} capture ('${key}')`); } return value; } diff --git a/packages/agent-core-v2/src/kosong/protocol/errors.ts b/packages/agent-core-v2/src/kosong/protocol/errors.ts index e237def8932..5e3c2c0e1cd 100644 --- a/packages/agent-core-v2/src/kosong/protocol/errors.ts +++ b/packages/agent-core-v2/src/kosong/protocol/errors.ts @@ -2,11 +2,16 @@ * `kosong/protocol` domain — wire API failure codes and the boundary * translation from raw contract errors to coded `Error2`s. * - * `translateProviderError` converts the L0 `API*Error` family into coded - * errors callers can branch on across the wire. Its FIRST guard is the - * contract's `throwIfAbortError`: a user cancellation is thrown as the - * standard abort DOMException and can never be misclassified as a retryable - * provider failure. The guard throws rather than returns, by design. + * The `ChatProviderError` family is born-coded (see `kosong/contract/errors`): + * every instance already carries its wire code, so `translateProviderError`'s + * `isError2` guard passes it through untouched. What remains here is the + * abort guard and the fallback for errors foreign to the family (plain + * `Error` / unknown thrown values → `internal`). + * + * `translateProviderError`'s FIRST guard is the contract's + * `throwIfAbortError`: a user cancellation is thrown as the standard abort + * DOMException and can never be misclassified as a retryable provider + * failure. The guard throws rather than returns, by design. * * Side-effect module: importing registers the error domain. */ @@ -14,26 +19,27 @@ import { CoreErrors, registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; import { Error2, isError2 } from '#/_base/errors/errors'; import { - APIConnectionError, - APIContextOverflowError, - APIEmptyResponseError, - APIProviderOverloadedError, - APIProviderQuotaExhaustedError, - APIStatusError, - APITimeoutError, - ChatProviderError, + CONTEXT_OVERFLOW_ERROR_CODE, + PROVIDER_API_ERROR_CODE, + PROVIDER_AUTH_ERROR_CODE, + PROVIDER_CONNECTION_ERROR_CODE, + PROVIDER_FILTERED_ERROR_CODE, + PROVIDER_OVERLOADED_ERROR_CODE, + PROVIDER_RATE_LIMIT_ERROR_CODE, throwIfAbortError, } from '#/kosong/contract/errors'; +export { sanitizeStatusErrorMessage } from '#/kosong/contract/errors'; + export const ProtocolErrors = { codes: { - 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', - PROVIDER_OVERLOADED: 'provider.overloaded', - CONTEXT_OVERFLOW: 'context.overflow', + PROVIDER_API_ERROR: PROVIDER_API_ERROR_CODE, + PROVIDER_FILTERED: PROVIDER_FILTERED_ERROR_CODE, + PROVIDER_RATE_LIMIT: PROVIDER_RATE_LIMIT_ERROR_CODE, + PROVIDER_AUTH_ERROR: PROVIDER_AUTH_ERROR_CODE, + PROVIDER_CONNECTION_ERROR: PROVIDER_CONNECTION_ERROR_CODE, + PROVIDER_OVERLOADED: PROVIDER_OVERLOADED_ERROR_CODE, + CONTEXT_OVERFLOW: CONTEXT_OVERFLOW_ERROR_CODE, }, retryable: [ 'provider.rate_limit', @@ -82,55 +88,6 @@ export function translateProviderError(error: unknown): Error2 { if (isError2(error)) { return error; } - if (error instanceof APIStatusError) { - const code = - error instanceof APIContextOverflowError - ? ProtocolErrors.codes.CONTEXT_OVERFLOW - : error instanceof APIProviderOverloadedError || error.statusCode === 529 - ? ProtocolErrors.codes.PROVIDER_OVERLOADED - : error instanceof APIProviderQuotaExhaustedError - ? ProtocolErrors.codes.PROVIDER_API_ERROR - : error.statusCode === 429 - ? ProtocolErrors.codes.PROVIDER_RATE_LIMIT - : error.statusCode === 401 || error.statusCode === 403 - ? ProtocolErrors.codes.PROVIDER_AUTH_ERROR - : ProtocolErrors.codes.PROVIDER_API_ERROR; - return new Error2(code, sanitizeStatusErrorMessage(error.message), { - name: error.name, - cause: error, - details: { - statusCode: error.statusCode, - requestId: error.requestId, - traceId: error.traceId, - }, - }); - } - if (error instanceof APIConnectionError || error instanceof APITimeoutError) { - return new Error2(ProtocolErrors.codes.PROVIDER_CONNECTION_ERROR, error.message, { - name: error.name, - cause: error, - }); - } - if (error instanceof APIEmptyResponseError) { - const code = - error.finishReason === 'filtered' - ? ProtocolErrors.codes.PROVIDER_FILTERED - : ProtocolErrors.codes.PROVIDER_API_ERROR; - return new Error2(code, error.message, { - name: error.name, - cause: error, - details: { - finishReason: error.finishReason, - rawFinishReason: error.rawFinishReason, - }, - }); - } - if (error instanceof ChatProviderError) { - return new Error2(ProtocolErrors.codes.PROVIDER_API_ERROR, error.message, { - name: error.name, - cause: error, - }); - } if (error instanceof Error) { return new Error2(CoreErrors.codes.INTERNAL, error.message, { name: error.name, @@ -139,10 +96,3 @@ export function translateProviderError(error: unknown): Error2 { } return new Error2(CoreErrors.codes.INTERNAL, String(error), { cause: error }); } - -export 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', ''); -} diff --git a/packages/agent-core-v2/src/kosong/protocol/protocolBase.ts b/packages/agent-core-v2/src/kosong/protocol/protocolBase.ts index 06aea9af72e..72146fb0539 100644 --- a/packages/agent-core-v2/src/kosong/protocol/protocolBase.ts +++ b/packages/agent-core-v2/src/kosong/protocol/protocolBase.ts @@ -11,6 +11,7 @@ * deliberately registers nothing on its own. */ +import { BugIndicatingError } from '#/_base/errors/errors'; import type { ModelCapability } from '#/kosong/contract/capability'; import type { ChatProvider } from '#/kosong/contract/provider'; @@ -39,7 +40,7 @@ const protocolBases = new Map(); export function registerProtocolBase(definition: ProtocolBaseDefinition): void { if (protocolBases.has(definition.id)) { - throw new Error(`protocol base '${definition.id}' is already registered`); + throw new BugIndicatingError(`protocol base '${definition.id}' is already registered`); } protocolBases.set(definition.id, definition); } diff --git a/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-common.ts b/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-common.ts index f712d6c5e5a..ac3ddeea1f1 100644 --- a/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-common.ts +++ b/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-common.ts @@ -25,6 +25,7 @@ import { OpenAIError, } from 'openai'; +import { BugIndicatingError } from '#/_base/errors/errors'; import { APIConnectionError, APIProviderQuotaExhaustedError, @@ -81,7 +82,7 @@ export function convertContentPart(part: ContentPart): OpenAIContentPart | null : { url: part.videoUrl.url, id: part.videoUrl.id }, }; default: - throw new Error(`Unknown content part type: ${(part as ContentPart).type}`); + throw new BugIndicatingError(`Unknown content part type: ${(part as ContentPart).type}`); } } diff --git a/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-responses.ts b/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-responses.ts index 0018ad2179e..89e3219de83 100644 --- a/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-responses.ts +++ b/packages/agent-core-v2/src/kosong/provider/bases/openai/openai-responses.ts @@ -15,6 +15,7 @@ import OpenAI from 'openai'; +import { Error2 } from '#/_base/errors/errors'; import { APIContextOverflowError, APIProviderQuotaExhaustedError, @@ -41,6 +42,7 @@ import type { } from '#/kosong/contract/provider'; import type { Tool } from '#/kosong/contract/tool'; import type { TokenUsage } from '#/kosong/contract/usage'; +import { ProtocolErrors } from '#/kosong/protocol/errors'; import { convertOpenAIError, @@ -1170,7 +1172,8 @@ export class OpenAIResponsesChatProvider implements ChatProvider { !('responses' in client) || typeof (client as { responses?: { create?: unknown } }).responses?.create !== 'function' ) { - throw new Error( + throw new Error2( + ProtocolErrors.codes.PROVIDER_API_ERROR, 'OpenAI SDK version does not support Responses API. Upgrade to >=4.x with responses support.', ); } diff --git a/packages/agent-core-v2/src/kosong/provider/bases/tool-call-id.ts b/packages/agent-core-v2/src/kosong/provider/bases/tool-call-id.ts index ca16143da6b..5a059a544bf 100644 --- a/packages/agent-core-v2/src/kosong/provider/bases/tool-call-id.ts +++ b/packages/agent-core-v2/src/kosong/provider/bases/tool-call-id.ts @@ -6,6 +6,7 @@ * `toolCallId` pair consistently and keeps rewritten ids unique. */ +import { BugIndicatingError } from '#/_base/errors/errors'; import type { Message, ToolCall } from '#/kosong/contract/message'; import type { ToolCallIdPolicy } from '#/kosong/contract/provider'; @@ -123,7 +124,9 @@ function truncateToolCallId(base: string, maxLength: number | undefined, suffix: if (maxLength === undefined) return `${base}${suffix}`; const baseLength = maxLength - suffix.length; if (baseLength <= 0) { - throw new Error(`Tool call id maxLength ${maxLength} is too small for suffix ${suffix}.`); + throw new BugIndicatingError( + `Tool call id maxLength ${maxLength} is too small for suffix ${suffix}.`, + ); } return `${base.slice(0, baseLength)}${suffix}`; } diff --git a/packages/agent-core-v2/src/kosong/provider/providerDefinition.ts b/packages/agent-core-v2/src/kosong/provider/providerDefinition.ts index 55e4ae28b2d..2e9f99ccaeb 100644 --- a/packages/agent-core-v2/src/kosong/provider/providerDefinition.ts +++ b/packages/agent-core-v2/src/kosong/provider/providerDefinition.ts @@ -17,6 +17,7 @@ * bag (defaulting to `process.env`). */ +import { BugIndicatingError } from '#/_base/errors/errors'; import type { Protocol, ProtocolAdapterConfig } from '#/kosong/protocol/protocol'; import type { ProtocolEndpoint, @@ -44,7 +45,7 @@ export function registerProviderDefinition(definition: ProviderDefinition): void providerDefinitions.set(definition.id, byProtocol); } if (byProtocol.has(definition.baseProtocol)) { - throw new Error( + throw new BugIndicatingError( `provider definition '${definition.id}' is already registered for protocol '${definition.baseProtocol}'`, ); } diff --git a/packages/agent-core-v2/src/kosong/provider/providers/kimi/kimi-schema.ts b/packages/agent-core-v2/src/kosong/provider/providers/kimi/kimi-schema.ts index 5cd85c91cdf..acc7645befd 100644 --- a/packages/agent-core-v2/src/kosong/provider/providers/kimi/kimi-schema.ts +++ b/packages/agent-core-v2/src/kosong/provider/providers/kimi/kimi-schema.ts @@ -10,6 +10,9 @@ * remaining local `$ref` pointers stay resolvable to a JSON Schema validator. */ +import { Error2 } from '#/_base/errors/errors'; +import { ProtocolErrors } from '#/kosong/protocol/errors'; + export function derefJsonSchema(schema: Record): Record { const visited = new Set(); const result = resolveNode(schema, schema, visited) as Record; @@ -111,7 +114,10 @@ export function normalizeKimiToolSchema(schema: Record): Record function ensureKimiPropertyTypes(schema: Record): Record { const normalized = cloneJsonValue(schema); if (!isRecord(normalized)) { - throw new Error('JSON Schema root must normalize to an object.'); + throw new Error2( + ProtocolErrors.codes.PROVIDER_API_ERROR, + 'JSON Schema root must normalize to an object.', + ); } recurseSchema(normalized); return normalized; @@ -345,7 +351,10 @@ function inferTypeFromValues(values: unknown[]): JsonSchemaType { for (const value of values) { const valueType = inferValueType(value); if (valueType === undefined) { - throw new Error('Cannot infer JSON Schema type from non-JSON enum or const value.'); + throw new Error2( + ProtocolErrors.codes.PROVIDER_API_ERROR, + 'Cannot infer JSON Schema type from non-JSON enum or const value.', + ); } inferred.add(valueType); } @@ -353,11 +362,17 @@ function inferTypeFromValues(values: unknown[]): JsonSchemaType { if (types.length === 1) { const onlyType = types[0]; if (onlyType === undefined) { - throw new Error('Cannot infer JSON Schema type from an empty enum.'); + throw new Error2( + ProtocolErrors.codes.PROVIDER_API_ERROR, + 'Cannot infer JSON Schema type from an empty enum.', + ); } return onlyType; } - throw new Error('Mixed JSON Schema enum or const types are not supported by Kimi tool schemas.'); + throw new Error2( + ProtocolErrors.codes.PROVIDER_API_ERROR, + 'Mixed JSON Schema enum or const types are not supported by Kimi tool schemas.', + ); } function inferValueType(value: unknown): JsonSchemaType | undefined { diff --git a/packages/agent-core-v2/src/mcpCore/client-http.ts b/packages/agent-core-v2/src/mcpCore/client-http.ts index 04b42d499e8..91971685e48 100644 --- a/packages/agent-core-v2/src/mcpCore/client-http.ts +++ b/packages/agent-core-v2/src/mcpCore/client-http.ts @@ -2,6 +2,7 @@ * `mcpCore` domain — Streamable HTTP transport MCP client. */ +import { ErrorCodes, Error2 } from '#/errors'; import type { McpServerHttpConfig } from './config-schema'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; @@ -63,7 +64,7 @@ export class HttpMcpClient implements MCPClient { async connect(): Promise { if (this.closed) { - throw new Error('MCP HTTP client is closed'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP HTTP client is closed'); } if (this.started) return; this.started = true; @@ -79,7 +80,7 @@ export class HttpMcpClient implements MCPClient { } if (this.closed) { await this.closeStartedClient(); - throw new Error('MCP HTTP client was closed during startup'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP HTTP client was closed during startup'); } this.ready = true; } diff --git a/packages/agent-core-v2/src/mcpCore/client-sse.ts b/packages/agent-core-v2/src/mcpCore/client-sse.ts index 3da3bab6f37..0084e4b31f3 100644 --- a/packages/agent-core-v2/src/mcpCore/client-sse.ts +++ b/packages/agent-core-v2/src/mcpCore/client-sse.ts @@ -2,6 +2,7 @@ * `mcpCore` domain — SSE transport MCP client. */ +import { ErrorCodes, Error2 } from '#/errors'; import type { McpServerSseConfig } from './config-schema'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; @@ -63,7 +64,7 @@ export class SseMcpClient implements MCPClient { async connect(): Promise { if (this.closed) { - throw new Error('MCP SSE client is closed'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP SSE client is closed'); } if (this.started) return; this.started = true; @@ -79,7 +80,7 @@ export class SseMcpClient implements MCPClient { } if (this.closed) { await this.closeStartedClient(); - throw new Error('MCP SSE client was closed during startup'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP SSE client was closed during startup'); } this.ready = true; } diff --git a/packages/agent-core-v2/src/mcpCore/client-stdio.ts b/packages/agent-core-v2/src/mcpCore/client-stdio.ts index 489c365df33..7f81f396458 100644 --- a/packages/agent-core-v2/src/mcpCore/client-stdio.ts +++ b/packages/agent-core-v2/src/mcpCore/client-stdio.ts @@ -71,7 +71,7 @@ export class StdioMcpClient implements MCPClient { async connect(): Promise { if (this.closed) { - throw new Error('MCP stdio client is closed'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP stdio client is closed'); } if (this.started) return; this.started = true; @@ -87,7 +87,7 @@ export class StdioMcpClient implements MCPClient { } if (this.closed) { await this.closeStartedClient(); - throw new Error('MCP stdio client was closed during startup'); + throw new Error2(ErrorCodes.MCP_STARTUP_FAILED, 'MCP stdio client was closed during startup'); } this.ready = true; } diff --git a/packages/agent-core-v2/src/mcpCore/connection-manager.ts b/packages/agent-core-v2/src/mcpCore/connection-manager.ts index 78940ac5430..e483b1cc8a0 100644 --- a/packages/agent-core-v2/src/mcpCore/connection-manager.ts +++ b/packages/agent-core-v2/src/mcpCore/connection-manager.ts @@ -511,7 +511,7 @@ async function withTimeout( return await new Promise((resolve, reject) => { timer = setTimeout(() => { onTimeout?.(); - reject(new Error(`Timed out after ${timeoutMs}ms`)); + reject(new Error2(ErrorCodes.MCP_STARTUP_FAILED, `Timed out after ${timeoutMs}ms`)); }, timeoutMs); promise.then(resolve, reject); }); diff --git a/packages/agent-core-v2/src/mcpCore/errors.ts b/packages/agent-core-v2/src/mcpCore/errors.ts index 0511869d832..d8d32bd2d00 100644 --- a/packages/agent-core-v2/src/mcpCore/errors.ts +++ b/packages/agent-core-v2/src/mcpCore/errors.ts @@ -10,6 +10,7 @@ export const McpErrors = { 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', }, } as const satisfies ErrorDomain; diff --git a/packages/agent-core-v2/src/mcpCore/oauth/provider.ts b/packages/agent-core-v2/src/mcpCore/oauth/provider.ts index 206d704ed87..545b7dfa580 100644 --- a/packages/agent-core-v2/src/mcpCore/oauth/provider.ts +++ b/packages/agent-core-v2/src/mcpCore/oauth/provider.ts @@ -17,6 +17,8 @@ import { randomBytes } from 'node:crypto'; +import { BugIndicatingError } from '#/errors'; + import type { OAuthClientProvider, OAuthDiscoveryState, @@ -146,7 +148,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider { codeVerifier(): string { if (this._codeVerifier === undefined) { - throw new Error('McpOAuthClientProvider: PKCE code verifier not initialized'); + throw new BugIndicatingError('McpOAuthClientProvider: PKCE code verifier not initialized'); } return this._codeVerifier; } diff --git a/packages/agent-core-v2/src/mcpCore/oauth/service.ts b/packages/agent-core-v2/src/mcpCore/oauth/service.ts index 3b8c0f41f43..b4a25d9d573 100644 --- a/packages/agent-core-v2/src/mcpCore/oauth/service.ts +++ b/packages/agent-core-v2/src/mcpCore/oauth/service.ts @@ -24,6 +24,8 @@ import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; +import { ErrorCodes, Error2, isError2 } from '#/errors'; + import { startCallbackServer, type CallbackServer } from './callback-server'; import { McpOAuthClientProvider } from './provider'; import { mcpOAuthStoreKey, type McpOAuthStore } from './store'; @@ -110,7 +112,10 @@ export class McpOAuthService { } authorizationUrl = provider.takeAuthorizationUrl(); if (authorizationUrl === undefined) { - throw new Error('OAuth provider did not capture an authorization URL'); + throw new Error2( + ErrorCodes.MCP_OAUTH_FAILED, + 'OAuth provider did not capture an authorization URL', + ); } } catch (error) { await callbackServer.close().catch(() => undefined); @@ -129,7 +134,7 @@ export class McpOAuthService { const complete: BeginAuthorizationResult['complete'] = async (opts = {}) => { if (settled) { - throw new Error('OAuth flow already completed or cancelled'); + throw new Error2(ErrorCodes.MCP_OAUTH_FAILED, 'OAuth flow already completed or cancelled'); } try { const { code, state } = await callbackServer.waitForCode({ @@ -138,14 +143,21 @@ export class McpOAuthService { }); const expectedState = provider.expectedState(); if (expectedState !== undefined && state !== expectedState) { - throw new Error('OAuth state mismatch — possible CSRF; refusing token exchange'); + throw new Error2( + ErrorCodes.MCP_OAUTH_FAILED, + 'OAuth state mismatch — possible CSRF; refusing token exchange', + ); } const finalResult = await auth(provider as OAuthClientProvider, { serverUrl, authorizationCode: code, }); if (finalResult !== 'AUTHORIZED') { - throw new Error(`OAuth code exchange returned "${finalResult}" instead of AUTHORIZED`); + throw new Error2( + ErrorCodes.MCP_OAUTH_FAILED, + `OAuth code exchange returned "${finalResult}" instead of AUTHORIZED`, + { details: { result: finalResult } }, + ); } } catch (error) { await cancel(); @@ -168,18 +180,24 @@ export class McpOAuthService { } } -export class AlreadyAuthorizedError extends Error { +export class AlreadyAuthorizedError extends Error2 { constructor(serverName: string) { - super(`"${serverName}" is already authorized; no browser flow needed`); + super( + ErrorCodes.MCP_OAUTH_FAILED, + `"${serverName}" is already authorized; no browser flow needed`, + ); this.name = 'AlreadyAuthorizedError'; } } -function wrapAuthError(prefix: string, error: unknown): Error { +function wrapAuthError(prefix: string, error: unknown): Error2 { + if (isError2(error)) { + return error; + } if (error instanceof Error) { - const wrapped = new Error(`${prefix}: ${error.message}`); - wrapped.cause = error; - return wrapped; + return new Error2(ErrorCodes.MCP_OAUTH_FAILED, `${prefix}: ${error.message}`, { + cause: error, + }); } - return new Error(`${prefix}: ${String(error)}`); + return new Error2(ErrorCodes.MCP_OAUTH_FAILED, `${prefix}: ${String(error)}`, { cause: error }); } diff --git a/packages/agent-core-v2/src/mcpCore/oauth/store.ts b/packages/agent-core-v2/src/mcpCore/oauth/store.ts index bf7083a57bb..00aee8cfc8e 100644 --- a/packages/agent-core-v2/src/mcpCore/oauth/store.ts +++ b/packages/agent-core-v2/src/mcpCore/oauth/store.ts @@ -12,10 +12,12 @@ import { createHash } from 'node:crypto'; import { basename } from 'pathe'; +import { ErrorCodes, Error2 } from '#/errors'; + export function sanitizeStoreKey(name: string): string { const safe = basename(name).replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_'); if (safe.length === 0 || safe.startsWith('.')) { - throw new Error(`Invalid MCP OAuth store key: "${name}"`); + throw new Error2(ErrorCodes.CONFIG_INVALID, `Invalid MCP OAuth store key: "${name}"`); } return safe; } diff --git a/packages/agent-core-v2/src/mcpCore/types.ts b/packages/agent-core-v2/src/mcpCore/types.ts index 2eecfe22fc1..09e9d8c712b 100644 --- a/packages/agent-core-v2/src/mcpCore/types.ts +++ b/packages/agent-core-v2/src/mcpCore/types.ts @@ -6,6 +6,8 @@ * fake transport without pulling in the MCP SDK type graph. */ +import { ErrorCodes, Error2 } from '#/errors'; + /** * Inline resource contents nested under an EmbeddedResource block. * Exactly one of `text` or `blob` is populated, per the MCP schema's @@ -57,5 +59,8 @@ export function assertMcpInputSchema( if (typeof inputSchema === 'object' && inputSchema !== null && !Array.isArray(inputSchema)) { return inputSchema as Record; } - throw new Error(`Invalid inputSchema for MCP tool "${toolName}": schema must be a JSON object`); + throw new Error2( + ErrorCodes.MCP_STARTUP_FAILED, + `Invalid inputSchema for MCP tool "${toolName}": schema must be a JSON object`, + ); } diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts index 5eb1562b9b8..6edac6f0919 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts @@ -20,6 +20,7 @@ import { type Entry, fromBuffer as yauzlFromBuffer } from 'yauzl'; import { basename, join } from 'pathe'; import { abortable } from '#/_base/utils/abort'; +import { ErrorCodes, Error2 } from '#/errors'; const RG_VERSION = '15.0.0'; const RG_BASE_URL = 'https://code.kimi.com/kimi-code/rg'; @@ -101,7 +102,7 @@ async function resolveRgPath( if (options.allowCachedFallback === true) { return downloadRgWithLock(probe, shareDir); } - throw new Error('ripgrep (rg) is not available on PATH'); + throw new Error2(ErrorCodes.OS_FS_UNAVAILABLE, 'ripgrep (rg) is not available on PATH'); } export async function findExistingRg( @@ -181,8 +182,10 @@ export function detectTarget(): string | undefined { async function downloadAndInstallRg(shareDir: string): Promise { const target = detectTarget(); if (target === undefined) { - throw new Error( + throw new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, `Unsupported platform/arch for ripgrep download: ${process.platform}/${process.arch}`, + { details: { platform: process.platform, arch: process.arch } }, ); } @@ -191,7 +194,11 @@ async function downloadAndInstallRg(shareDir: string): Promise { const archiveName = `ripgrep-${RG_VERSION}-${target}.${archiveExt}`; const expectedSha256 = RG_ARCHIVE_SHA256[archiveName]; if (expectedSha256 === undefined) { - throw new Error(`No pinned SHA-256 is configured for ripgrep archive ${archiveName}`); + throw new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, + `No pinned SHA-256 is configured for ripgrep archive ${archiveName}`, + { details: { archiveName } }, + ); } const url = `${RG_BASE_URL}/${archiveName}`; @@ -214,7 +221,11 @@ async function downloadAndInstallRg(shareDir: string): Promise { clearTimeout(timeoutHandle); } if (!resp.ok || resp.body === null) { - throw new Error(`Failed to download ripgrep: HTTP ${String(resp.status)} ${resp.statusText}`); + throw new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, + `Failed to download ripgrep: HTTP ${String(resp.status)} ${resp.statusText}`, + { details: { url, status: resp.status, statusText: resp.statusText } }, + ); } const write = createWriteStream(archivePath); await pipeline(Readable.fromWeb(resp.body as never), write); @@ -233,9 +244,11 @@ async function downloadAndInstallRg(shareDir: string): Promise { }); const extracted = join(extractDir, `ripgrep-${RG_VERSION}-${target}`, rgBinaryName()); if (!existsSync(extracted)) { - throw new Error( + throw new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, `Ripgrep archive did not contain expected binary at ${extracted}. ` + 'CDN content may have changed.', + { details: { path: extracted } }, ); } const installDir = await mkdtemp(join(binDir, '.rg-install-')); @@ -263,9 +276,11 @@ export async function verifyArchiveChecksum( .update(await readFile(archivePath)) .digest('hex'); if (actualSha256 !== expectedSha256) { - throw new Error( + throw new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, `Ripgrep archive checksum mismatch for ${archiveName}: expected ${expectedSha256}, ` + `got ${actualSha256}. CDN content may have changed.`, + { details: { archiveName, expectedSha256, actualSha256 } }, ); } } @@ -276,7 +291,13 @@ export async function extractRgFromZip(archivePath: string, destination: string) await new Promise((resolve, reject) => { yauzlFromBuffer(buf, { lazyEntries: true }, (openErr, zipfile) => { if (openErr !== null || zipfile === undefined) { - reject(new Error(`Failed to open ripgrep archive: ${openErr?.message ?? 'unknown error'}`)); + reject( + new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, + `Failed to open ripgrep archive: ${openErr?.message ?? 'unknown error'}`, + { cause: openErr ?? undefined }, + ), + ); return; } let found = false; @@ -289,7 +310,11 @@ export async function extractRgFromZip(archivePath: string, destination: string) zipfile.openReadStream(entry, (streamErr, stream) => { if (streamErr !== null) { reject( - new Error(`Failed to read ${entry.fileName} from archive: ${streamErr.message}`), + new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, + `Failed to read ${entry.fileName} from archive: ${streamErr.message}`, + { cause: streamErr }, + ), ); zipfile.close(); return; @@ -302,7 +327,13 @@ export async function extractRgFromZip(archivePath: string, destination: string) resolve(); } catch (error) { zipfile.close(); - reject(error instanceof Error ? error : new Error(String(error))); + reject( + new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, + error instanceof Error ? error.message : String(error), + { cause: error }, + ), + ); } })(); }); @@ -311,9 +342,11 @@ export async function extractRgFromZip(archivePath: string, destination: string) zipfile.on('end', () => { if (!found) { reject( - new Error( + new Error2( + ErrorCodes.OS_FS_UNAVAILABLE, `Ripgrep archive did not contain expected binary '${binName}'. ` + 'CDN content may have changed.', + { details: { binary: binName } }, ), ); } diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/runRg.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/runRg.ts index 3775ec3b17a..6b2b43d1ff3 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/runRg.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/runRg.ts @@ -9,6 +9,7 @@ import type { Readable } from 'node:stream'; +import { BugIndicatingError } from '#/errors'; import type { IHostProcess, IHostProcessService } from '#/os/interface/hostProcess'; export const DEFAULT_TIMEOUT_MS = 20_000; @@ -46,7 +47,7 @@ export async function runRgOnce( const [command, ...args] = rgArgs; if (command === undefined) { - throw new Error('runRgOnce: rgArgs must not be empty'); + throw new BugIndicatingError('runRgOnce: rgArgs must not be empty'); } const proc: IHostProcess = await processService.spawn(command, args, { cwd: options?.cwd }); diff --git a/packages/agent-core-v2/src/os/interface/hostProcess.ts b/packages/agent-core-v2/src/os/interface/hostProcess.ts index 390290ed5c8..20242ab82fd 100644 --- a/packages/agent-core-v2/src/os/interface/hostProcess.ts +++ b/packages/agent-core-v2/src/os/interface/hostProcess.ts @@ -54,6 +54,7 @@ export const OsProcessErrors = { codes: { OS_PROCESS_SPAWN_FAILED: 'os.process.spawn_failed', OS_PROCESS_KILL_FAILED: 'os.process.kill_failed', + SHELL_GIT_BASH_NOT_FOUND: 'shell.git_bash_not_found', }, info: { 'os.process.spawn_failed': { @@ -67,6 +68,12 @@ export const OsProcessErrors = { retryable: false, public: true, }, + 'shell.git_bash_not_found': { + title: 'Git Bash not found', + retryable: false, + public: true, + action: 'Install Git for Windows so shell commands can run under Git Bash.', + }, }, } as const satisfies ErrorDomain; diff --git a/packages/agent-core-v2/src/persistence/interface/storage.ts b/packages/agent-core-v2/src/persistence/interface/storage.ts index 0e050f5e1b8..1d55183cb12 100644 --- a/packages/agent-core-v2/src/persistence/interface/storage.ts +++ b/packages/agent-core-v2/src/persistence/interface/storage.ts @@ -41,6 +41,8 @@ export const StorageErrors = { STORAGE_CORRUPTED: 'storage.corrupted', STORAGE_IO_FAILED: 'storage.io_failed', STORAGE_LOCKED: 'storage.locked', + STORAGE_PERMISSION_DENIED: 'storage.permission_denied', + STORAGE_DISK_FULL: 'storage.disk_full', }, retryable: ['storage.io_failed', 'storage.locked'], info: { @@ -72,6 +74,18 @@ export const StorageErrors = { public: true, action: 'Another process holds the store; close it or retry later.', }, + 'storage.permission_denied': { + title: 'Storage permission denied', + retryable: false, + public: true, + action: 'Check the permissions of the storage directory.', + }, + 'storage.disk_full': { + title: 'Storage disk full', + retryable: false, + public: true, + action: 'Free up disk space and retry.', + }, }, } as const satisfies ErrorDomain; @@ -96,16 +110,41 @@ function readErrno(error: unknown): string | undefined { return typeof code === 'string' ? code : undefined; } +type StorageIoErrorCode = + | typeof StorageErrors.codes.STORAGE_NOT_FOUND + | typeof StorageErrors.codes.STORAGE_IO_FAILED + | typeof StorageErrors.codes.STORAGE_PERMISSION_DENIED + | typeof StorageErrors.codes.STORAGE_DISK_FULL; + +const REASONS: Record = { + 'storage.not_found': 'path does not exist', + 'storage.io_failed': 'unrecognized I/O error', + 'storage.permission_denied': 'permission denied', + 'storage.disk_full': 'no space left on device', +}; + +function mapErrno(errno: string | undefined): StorageIoErrorCode { + switch (errno) { + case 'ENOENT': + return StorageErrors.codes.STORAGE_NOT_FOUND; + case 'EACCES': + case 'EPERM': + return StorageErrors.codes.STORAGE_PERMISSION_DENIED; + case 'ENOSPC': + return StorageErrors.codes.STORAGE_DISK_FULL; + default: + return StorageErrors.codes.STORAGE_IO_FAILED; + } +} + export function toStorageIoError(error: unknown, ctx: { path: string; op: string }): StorageError { if (error instanceof StorageError) return error; - return new StorageError( - StorageErrors.codes.STORAGE_IO_FAILED, - `storage ${ctx.op} failed`, - { - details: { path: ctx.path, op: ctx.op, errno: readErrno(error) }, - cause: error, - }, - ); + const errno = readErrno(error); + const code = mapErrno(errno); + return new StorageError(code, `storage ${ctx.op} failed: ${REASONS[code]}`, { + details: { path: ctx.path, op: ctx.op, errno }, + cause: error, + }); } export interface StorageWriteOptions { diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts index 6b180032bf5..b83c91b9c4d 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts @@ -22,6 +22,7 @@ import { IInstantiationService } from '#/_base/di/instantiation'; import { Disposable, type IDisposable } from '#/_base/di/lifecycle'; import { Emitter } from '#/_base/event'; +import { Error2, ErrorCodes } from '#/errors'; import { join } from 'pathe'; import { createScopedChildHandle, @@ -202,9 +203,15 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle async fork(sourceAgentId: string, opts?: ForkAgentOptions): Promise { const source = this.handles.get(sourceAgentId); - if (source === undefined) throw new Error(`Source agent "${sourceAgentId}" does not exist`); + if (source === undefined) { + throw new Error2(ErrorCodes.AGENT_NOT_FOUND, `Source agent "${sourceAgentId}" does not exist`, { + details: { agentId: sourceAgentId }, + }); + } if (opts?.agentId !== undefined && this.handles.has(opts.agentId)) { - throw new Error(`Agent "${opts.agentId}" already exists`); + throw new Error2(ErrorCodes.AGENT_ALREADY_EXISTS, `Agent "${opts.agentId}" already exists`, { + details: { agentId: opts.agentId }, + }); } const child = await this.create({ agentId: opts?.agentId, forkedFrom: source.id }); diff --git a/packages/agent-core-v2/src/session/agentLifecycle/errors.ts b/packages/agent-core-v2/src/session/agentLifecycle/errors.ts index cfe09345a99..1432ad72bbc 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/errors.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/errors.ts @@ -7,6 +7,12 @@ import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; export const AgentLifecycleErrors = { codes: { AGENT_NOT_FOUND: 'agent.not_found', + AGENT_ALREADY_EXISTS: 'agent.already_exists', + AGENT_ALREADY_RUNNING: 'agent.already_running', + AGENT_NOT_A_SUBAGENT: 'agent.not_a_subagent', + AGENT_NOT_OWNED: 'agent.not_owned', + AGENT_TYPE_NOT_ALLOWED: 'agent.type_not_allowed', + AGENT_MAX_TOKENS_EXCEEDED: 'agent.max_tokens_exceeded', }, } as const satisfies ErrorDomain; diff --git a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts index a51a66d4b8d..94ee5566e70 100644 --- a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts +++ b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts @@ -47,6 +47,7 @@ import { IWireService } from '#/wire/wire'; import { type DomainEvent, IEventBus } from '#/app/event/eventBus'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { IAgentLoopService, type Turn } from '#/agent/loop/loop'; +import { BugIndicatingError } from '#/errors'; import { ICronCreateTool } from '#/agent/tools/cron/cron-create/cron-create'; import { ICronListTool } from '#/agent/tools/cron/cron-list/cron-list'; @@ -662,7 +663,7 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe if (!CRON_ID_REGEX.test(candidate)) continue; if (!this.tasks.has(candidate)) return candidate; } - throw new Error( + throw new BugIndicatingError( `SessionCronService: failed to generate a unique ULID after ${MAX_ID_ATTEMPTS} attempts`, ); } diff --git a/packages/agent-core-v2/src/session/errors.ts b/packages/agent-core-v2/src/session/errors.ts index a642ef06a24..b38a25f229c 100644 --- a/packages/agent-core-v2/src/session/errors.ts +++ b/packages/agent-core-v2/src/session/errors.ts @@ -13,6 +13,7 @@ export const SessionErrors = { SESSION_FORK_ACTIVE_TURN: 'session.fork_active_turn', SESSION_UNDO_UNAVAILABLE: 'session.undo_unavailable', SESSION_INIT_FAILED: 'session.init_failed', + SESSION_PLAN_MODE_INVALID: 'session.plan_mode_invalid', }, retryable: ['session.fork_active_turn'], } as const satisfies ErrorDomain; diff --git a/packages/agent-core-v2/src/session/process/processRunnerService.ts b/packages/agent-core-v2/src/session/process/processRunnerService.ts index 81c39c1b5b3..2f4ecb07d56 100644 --- a/packages/agent-core-v2/src/session/process/processRunnerService.ts +++ b/packages/agent-core-v2/src/session/process/processRunnerService.ts @@ -16,6 +16,7 @@ */ import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { BugIndicatingError } from '#/errors'; import { IHostProcessService } from '#/os/interface/hostProcess'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; @@ -32,7 +33,7 @@ export class SessionProcessRunner implements ISessionProcessRunner { async exec(args: readonly string[], options?: ProcessExecOptions): Promise { const command = args[0]; if (command === undefined) { - throw new Error( + throw new BugIndicatingError( 'SessionProcessRunner.exec(): at least one argument (the command to run) is required.', ); } diff --git a/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts index 9b2085d6ff0..b60579f16b6 100644 --- a/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts +++ b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts @@ -21,6 +21,7 @@ import { Disposable } from '#/_base/di/lifecycle'; import { Emitter, type Event } from '#/_base/event'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { ILogService } from '#/_base/log/log'; +import { BugIndicatingError } from '#/errors'; import type { AgentProfile } from '#/app/agentProfileCatalog/agentProfileCatalog'; import { DEFAULT_AGENT_PROFILE_NAME } from '#/app/agentProfileCatalog/agentProfileCatalog'; import { @@ -82,7 +83,7 @@ export class SessionAgentProfileCatalogService getDefault(): AgentProfile { const profile = this.get(DEFAULT_AGENT_PROFILE_NAME); if (profile === undefined) { - throw new Error( + throw new BugIndicatingError( `Default agent profile "${DEFAULT_AGENT_PROFILE_NAME}" is not registered`, ); } diff --git a/packages/agent-core-v2/src/session/subagent/runAgentTurn.ts b/packages/agent-core-v2/src/session/subagent/runAgentTurn.ts index 044abefd7ee..0c8c839ca3f 100644 --- a/packages/agent-core-v2/src/session/subagent/runAgentTurn.ts +++ b/packages/agent-core-v2/src/session/subagent/runAgentTurn.ts @@ -20,7 +20,7 @@ import { linkAbortSignal, userCancellationReason } from '#/_base/utils/abort'; import type { IAgentScopeHandle } from '#/_base/di/scope'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import type { ContextMessage, PromptOrigin } from '#/agent/contextMemory/types'; -import { ErrorCodes, toKimiErrorPayload, type KimiErrorPayload } from '#/errors'; +import { Error2, ErrorCodes, toKimiErrorPayload, type KimiErrorPayload } from '#/errors'; import { IAgentPromptService } from '#/agent/prompt/prompt'; import { IAgentLoopService, type Turn, type TurnResult } from '#/agent/loop/loop'; import { IAgentUsageService } from '#/agent/usage/usage'; @@ -58,7 +58,7 @@ export async function runAgentTurn( origin: AGENT_RUN_PROMPT_ORIGIN, } })).launched : await promptService.retry(); - if (turn === undefined) throw new Error('Agent turn could not be started'); + if (turn === undefined) throw new Error2(ErrorCodes.INTERNAL, 'Agent turn could not be started'); if (options.onReady !== undefined) { void turn.ready.then(() => options.onReady?.()).catch(() => {}); @@ -162,7 +162,7 @@ function classifyTurnResult(result: TurnResult): void { switch (result.type) { case 'completed': if (result.truncated) { - throw new Error(SUBAGENT_MAX_TOKENS_ERROR); + throw new Error2(ErrorCodes.AGENT_MAX_TOKENS_EXCEEDED, SUBAGENT_MAX_TOKENS_ERROR); } return; case 'failed': { diff --git a/packages/agent-core-v2/src/session/subagent/subagentService.ts b/packages/agent-core-v2/src/session/subagent/subagentService.ts index 27a66ceb9e5..a77a1599b47 100644 --- a/packages/agent-core-v2/src/session/subagent/subagentService.ts +++ b/packages/agent-core-v2/src/session/subagent/subagentService.ts @@ -10,6 +10,7 @@ */ import { Disposable } from '#/_base/di/lifecycle'; +import { Error2, ErrorCodes } from '#/errors'; import { type IAgentScopeHandle, LifecycleScope, @@ -54,7 +55,11 @@ export class SessionSubagentService extends Disposable implements ISessionSubage run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): Promise { const handle = this.agentLifecycle.get(agentId); - if (handle === undefined) throw new Error(`Agent "${agentId}" does not exist`); + if (handle === undefined) { + throw new Error2(ErrorCodes.AGENT_NOT_FOUND, `Agent "${agentId}" does not exist`, { + details: { agentId }, + }); + } return runAgentTurn(handle, request, { summaryPolicy: opts.summaryPolicy ?? this.summaryPolicyFor(handle), signal: opts.signal, diff --git a/packages/agent-core-v2/src/session/swarm/agentRunBatch.ts b/packages/agent-core-v2/src/session/swarm/agentRunBatch.ts index 4357d4f66b7..1518c1f8d9e 100644 --- a/packages/agent-core-v2/src/session/swarm/agentRunBatch.ts +++ b/packages/agent-core-v2/src/session/swarm/agentRunBatch.ts @@ -12,6 +12,7 @@ import { type TokenUsage } from '#/kosong/contract/usage'; import * as retry from 'retry'; import { isUserCancellation } from '#/_base/utils/abort'; +import { BugIndicatingError, Error2, ErrorCodes } from '#/errors'; import type { SessionSwarmRunResult, SessionSwarmTask } from './sessionSwarm'; @@ -155,7 +156,7 @@ export class AgentRunBatch { run(): Promise>> { if (this.started) { - throw new Error('AgentRunBatch.run() can only be called once.'); + throw new BugIndicatingError('AgentRunBatch.run() can only be called once.'); } this.started = true; @@ -643,8 +644,10 @@ export function resolveSwarmMaxConcurrency( if (raw === undefined || raw.trim() === '') return undefined; const value = Number(raw); if (!Number.isInteger(value) || value <= 0) { - throw new Error( + throw new Error2( + ErrorCodes.VALIDATION_FAILED, `${AGENT_SWARM_MAX_CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`, + { details: { value: raw } }, ); } return value; diff --git a/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts b/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts index 275baf6c36a..3f41f07a4f2 100644 --- a/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts +++ b/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts @@ -22,6 +22,7 @@ import type { TokenUsage } from '#/kosong/contract/usage'; import { IModelCatalog } from '#/kosong/model/catalog'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { Error2, ErrorCodes } from '#/errors'; import { linkAbortSignal } from '#/_base/utils/abort'; import type { IAgentScopeHandle } from '#/_base/di/scope'; import { IAgentProfileService } from '#/agent/profile/profile'; @@ -145,11 +146,15 @@ export class SessionSwarmService implements ISessionSwarmService { await this.catalog.ready; const profile = this.catalog.get(options.profileName); if (profile === undefined) { - throw new Error(`Unknown agent type: "${options.profileName}"`); + throw new Error2(ErrorCodes.PROFILE_UNKNOWN, `Unknown agent type: "${options.profileName}"`, { + details: { profileName: options.profileName }, + }); } const callerData = caller.accessor.get(IAgentProfileService).data(); if (callerData.modelAlias === undefined) { - throw new Error('Caller agent has no model bound'); + throw new Error2(ErrorCodes.MODEL_NOT_CONFIGURED, 'Caller agent has no model bound', { + details: { agentId: callerAgentId }, + }); } const binding = options.binding ?? { model: callerData.modelAlias, @@ -249,23 +254,37 @@ export class SessionSwarmService implements ISessionSwarmService { private requireHandle(agentId: string, label: string): IAgentScopeHandle { const handle = this.lifecycle.get(agentId); - if (handle === undefined) throw new Error(`${label} "${agentId}" does not exist`); + if (handle === undefined) { + throw new Error2(ErrorCodes.AGENT_NOT_FOUND, `${label} "${agentId}" does not exist`, { + details: { agentId }, + }); + } return handle; } private requireIdleSubagent(agentId: string, child: IAgentScopeHandle): void { if (child.accessor.get(IAgentLoopService).status().state === 'running') { - throw new Error(`Agent instance "${agentId}" is already running and cannot run concurrently`); + throw new Error2( + ErrorCodes.AGENT_ALREADY_RUNNING, + `Agent instance "${agentId}" is already running and cannot run concurrently`, + { details: { agentId } }, + ); } } private async requireOwnedSubagent(callerAgentId: string, agentId: string): Promise { const meta = await this.agentMeta(agentId); if (!isSubagentMeta(meta)) { - throw new Error(`Agent instance "${agentId}" is not a subagent`); + throw new Error2(ErrorCodes.AGENT_NOT_A_SUBAGENT, `Agent instance "${agentId}" is not a subagent`, { + details: { agentId }, + }); } if (subagentParentAgentId(meta) !== callerAgentId) { - throw new Error(`Agent instance "${agentId}" does not belong to this parent agent`); + throw new Error2( + ErrorCodes.AGENT_NOT_OWNED, + `Agent instance "${agentId}" does not belong to this parent agent`, + { details: { agentId, callerAgentId } }, + ); } } diff --git a/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts b/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts index b4b4fd7a432..4af45b48fcb 100644 --- a/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts +++ b/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts @@ -15,6 +15,7 @@ import { isAbsolute, relative, resolve } from 'node:path'; import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { defineState } from '#/_base/state/stateRegistry'; +import { ErrorCodes, Error2 } from '#/errors'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; import { ISessionStateService } from '#/session/state/sessionState'; import { ISessionWorkspaceInfo } from '#/session/workspaceInfo/workspaceInfo'; @@ -85,7 +86,9 @@ export class SessionWorkspaceContextService extends Disposable implements ISessi assertAllowed(absPath: string, op: PathAccessOperation): string { const target = this.resolve(absPath); if (!this.isWithin(target)) { - throw new Error(`Path outside workspace (${op}): ${target}`); + throw new Error2(ErrorCodes.FS_PATH_ESCAPES, `Path outside workspace (${op}): ${target}`, { + details: { op, path: target }, + }); } return target; } diff --git a/packages/agent-core-v2/src/tool/result-builder.ts b/packages/agent-core-v2/src/tool/result-builder.ts index 4ac834f7274..9debc664788 100644 --- a/packages/agent-core-v2/src/tool/result-builder.ts +++ b/packages/agent-core-v2/src/tool/result-builder.ts @@ -6,6 +6,8 @@ * service. */ +import { BugIndicatingError } from '#/errors'; + import type { ExecutableToolErrorResult, ExecutableToolSuccessResult } from './toolContract'; const DEFAULT_MAX_CHARS = 50_000; @@ -41,7 +43,7 @@ export class ToolResultBuilder { options.maxLineLength === undefined ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength; if (this.maxLineLength !== null && this.maxLineLength <= TRUNCATION_MARKER.length) { - throw new Error('maxLineLength must be greater than the truncation marker length.'); + throw new BugIndicatingError('maxLineLength must be greater than the truncation marker length.'); } } diff --git a/packages/agent-core-v2/src/wire/errors.ts b/packages/agent-core-v2/src/wire/errors.ts index 8e4c34234fb..b40a0c73719 100644 --- a/packages/agent-core-v2/src/wire/errors.ts +++ b/packages/agent-core-v2/src/wire/errors.ts @@ -16,6 +16,7 @@ export const WireErrors = { WIRE_DUPLICATE_OP: 'wire.duplicate_op', WIRE_CYCLE: 'wire.cycle', WIRE_UNKNOWN_RECORD: 'wire.unknown_record', + WIRE_MIGRATION_MISSING: 'wire.migration_missing', RECORDS_WRITE_FAILED: 'records.write_failed', }, info: { @@ -37,6 +38,12 @@ export const WireErrors = { public: true, action: 'The record was written by a newer version; upgrade or drop it.', }, + 'wire.migration_missing': { + title: 'Wire migration missing', + retryable: false, + public: true, + action: 'The wire file predates the supported migration chain; start a new session.', + }, 'records.write_failed': { title: 'Wire journal write failed', retryable: false, diff --git a/packages/agent-core-v2/src/wire/migration/migration.ts b/packages/agent-core-v2/src/wire/migration/migration.ts index f572b886c82..b9397c3eaec 100644 --- a/packages/agent-core-v2/src/wire/migration/migration.ts +++ b/packages/agent-core-v2/src/wire/migration/migration.ts @@ -1,5 +1,7 @@ import type { WireRecord } from '#/wire/record'; +import { WireError, WireErrors } from '../errors'; + import { migrateV1_0ToV1_1 } from './v1.1'; import { migrateV1_1ToV1_2 } from './v1.2'; import { migrateV1_2ToV1_3 } from './v1.3'; @@ -46,7 +48,11 @@ export function resolveWireMigrations(readVersion: string): readonly WireMigrati while (compareWireVersions(version, WIRE_PROTOCOL_VERSION) < 0) { const migration = findMigration(version); if (migration === undefined) { - throw new Error(`Missing wire migration for version ${version}`); + throw new WireError( + WireErrors.codes.WIRE_MIGRATION_MISSING, + `Missing wire migration for version ${version}`, + { details: { version } }, + ); } migrations.push(migration); version = migration.targetVersion; diff --git a/packages/agent-core-v2/src/workspace/workspaceAgentProfileLoader/internal/agentFile.ts b/packages/agent-core-v2/src/workspace/workspaceAgentProfileLoader/internal/agentFile.ts index 90b1c17b8b8..6d5eaba3a0d 100644 --- a/packages/agent-core-v2/src/workspace/workspaceAgentProfileLoader/internal/agentFile.ts +++ b/packages/agent-core-v2/src/workspace/workspaceAgentProfileLoader/internal/agentFile.ts @@ -11,16 +11,20 @@ * (Claude Code). */ +import { CoreErrors } from '#/_base/errors/codes'; +import { Error2 } from '#/_base/errors/errors'; import { FrontmatterError, parseFrontmatter } from '#/_base/text/frontmatter'; import type { AgentFileDefinition, AgentFileSource } from './types'; -export class AgentFileParseError extends Error { +export class AgentFileParseError extends Error2 { readonly reason?: unknown; constructor(message: string, cause?: unknown) { - super(message); - this.name = 'AgentFileParseError'; + super(CoreErrors.codes.VALIDATION_FAILED, message, { + cause, + name: 'AgentFileParseError', + }); if (cause !== undefined) this.reason = cause; } } diff --git a/packages/agent-core-v2/src/workspace/workspaceFs/internal/rgLocator.ts b/packages/agent-core-v2/src/workspace/workspaceFs/internal/rgLocator.ts index 9492e34204c..057742c7aa1 100644 --- a/packages/agent-core-v2/src/workspace/workspaceFs/internal/rgLocator.ts +++ b/packages/agent-core-v2/src/workspace/workspaceFs/internal/rgLocator.ts @@ -19,6 +19,8 @@ import { homedir } from 'node:os'; import { join } from 'node:path'; +import { ErrorCodes, Error2 } from '#/errors'; + export type RgResolutionSource = 'system-path' | 'share-bin-cached'; export interface RgResolution { @@ -75,7 +77,7 @@ export async function ensureRgPath( } } - throw new Error('ripgrep (rg) is not available on PATH'); + throw new Error2(ErrorCodes.OS_FS_UNAVAILABLE, 'ripgrep (rg) is not available on PATH'); } export function rgUnavailableMessage(cause: unknown): string { diff --git a/packages/agent-core-v2/src/workspace/workspaceProcess/workspaceProcessRunnerService.ts b/packages/agent-core-v2/src/workspace/workspaceProcess/workspaceProcessRunnerService.ts index 69b9abf8946..753ff0995b9 100644 --- a/packages/agent-core-v2/src/workspace/workspaceProcess/workspaceProcessRunnerService.ts +++ b/packages/agent-core-v2/src/workspace/workspaceProcess/workspaceProcessRunnerService.ts @@ -14,6 +14,7 @@ */ import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { BugIndicatingError } from '#/errors'; import { IHostProcessService } from '#/os/interface/hostProcess'; import { type IProcess, ISessionProcessRunner, type ProcessExecOptions } from '#/session/process/processRunner'; import { IWorkspaceContext } from '#/workspace/workspaceContext/workspaceContext'; @@ -29,7 +30,7 @@ export class WorkspaceProcessRunnerService implements ISessionProcessRunner { async exec(args: readonly string[], options?: ProcessExecOptions): Promise { const command = args[0]; if (command === undefined) { - throw new Error( + throw new BugIndicatingError( 'WorkspaceProcessRunnerService.exec(): at least one argument (the command to run) is required.', ); } diff --git a/packages/agent-core-v2/test/app/protocol/errors.test.ts b/packages/agent-core-v2/test/app/protocol/errors.test.ts index fb5cb266301..9f9d3130100 100644 --- a/packages/agent-core-v2/test/app/protocol/errors.test.ts +++ b/packages/agent-core-v2/test/app/protocol/errors.test.ts @@ -24,12 +24,11 @@ describe('translateProviderError', () => { expect(translateProviderError(coded)).toBe(coded); }); - it('maps 429 to provider.rate_limit, keeping the raw error as cause and status in details', () => { + it('maps 429 to provider.rate_limit at birth, passing through with status in details', () => { const raw = new APIStatusError(429, 'Too Many Requests', 'req-1'); const error = translateProviderError(raw); - expect(error).toBeInstanceOf(Error2); + expect(error).toBe(raw); expect(error.code).toBe('provider.rate_limit'); - expect(error.cause).toBe(raw); expect(error.name).toBe('APIStatusError'); expect(error.details).toMatchObject({ statusCode: 429, requestId: 'req-1' }); }); @@ -52,11 +51,11 @@ describe('translateProviderError', () => { expect(error.code).toBe('context.overflow'); }); - it('maps provider-overload errors to provider.overloaded, keeping HTTP details', () => { + it('maps provider-overload errors to provider.overloaded at birth, keeping HTTP details', () => { const raw = new APIProviderOverloadedError(529, 'Overloaded', 'req-overload'); const error = translateProviderError(raw); + expect(error).toBe(raw); expect(error.code).toBe('provider.overloaded'); - expect(error.cause).toBe(raw); expect(error.details).toMatchObject({ statusCode: 529, requestId: 'req-overload' }); }); diff --git a/packages/agent-core-v2/test/kosong/protocol/errors.test.ts b/packages/agent-core-v2/test/kosong/protocol/errors.test.ts index b30008dc6ab..e6c4afc351b 100644 --- a/packages/agent-core-v2/test/kosong/protocol/errors.test.ts +++ b/packages/agent-core-v2/test/kosong/protocol/errors.test.ts @@ -80,7 +80,7 @@ describe('translateProviderError — classification', () => { expect(translateProviderError(original)).toBe(original); }); - it('maps status errors to their codes and preserves wire details', () => { + it('maps status errors to their codes at birth and preserves wire details', () => { const cases: ReadonlyArray<[APIStatusError, string]> = [ [new APIStatusError(429, 'too many requests'), 'provider.rate_limit'], [new APIStatusError(529, 'overloaded'), 'provider.overloaded'], @@ -92,8 +92,8 @@ describe('translateProviderError — classification', () => { ]; for (const [error, code] of cases) { const translated = translateProviderError(error); + expect(translated).toBe(error); expect(translated.code).toBe(code); - expect(translated.cause).toBe(error); expect(translated.details?.['statusCode']).toBe(error.statusCode); } }); diff --git a/packages/kap-server/src/protocol/events-zod.ts b/packages/kap-server/src/protocol/events-zod.ts index 7ce003fe61c..add4c2b6179 100644 --- a/packages/kap-server/src/protocol/events-zod.ts +++ b/packages/kap-server/src/protocol/events-zod.ts @@ -307,6 +307,12 @@ export const kimiErrorCodeSchema = z.enum([ 'session.question_handler_error', 'session.init_failed', 'agent.not_found', + 'agent.already_exists', + 'agent.already_running', + 'agent.not_a_subagent', + 'agent.not_owned', + 'agent.type_not_allowed', + 'agent.max_tokens_exceeded', 'activity.agent_busy', 'activity.cancelling', 'activity.disposing', @@ -343,15 +349,19 @@ export const kimiErrorCodeSchema = z.enum([ 'skill.not_found', 'skill.type_unsupported', 'skill.name_empty', + 'skill.parse_failed', + 'skill.nested_too_deep', 'records.write_failed', 'compaction.failed', 'compaction.unable', 'task.task_id_empty', + 'task.limit_exceeded', 'usage.turn_id_conflict', 'mcp.server_not_found', 'mcp.server_disabled', 'mcp.startup_failed', 'mcp.tool_name_collision', + 'mcp.oauth_failed', 'message.not_found', 'plugin.not_found', 'plugin.load_failed', @@ -376,6 +386,13 @@ export const kimiErrorCodeSchema = z.enum([ 'fs.too_many_results', 'fs.grep_timeout', 'fs.git_unavailable', + 'wire.migration_missing', + 'storage.permission_denied', + 'storage.disk_full', + 'cron.expression_invalid', + 'web.invalid_url', + 'web.private_address', + 'web.fetch_failed', 'validation.failed', 'not_implemented', 'internal', diff --git a/packages/protocol/src/events.ts b/packages/protocol/src/events.ts index 6f9178d1454..82680cf57ed 100644 --- a/packages/protocol/src/events.ts +++ b/packages/protocol/src/events.ts @@ -236,6 +236,12 @@ export type KimiErrorCode = | 'session.question_handler_error' | 'session.init_failed' | 'agent.not_found' + | 'agent.already_exists' + | 'agent.already_running' + | 'agent.not_a_subagent' + | 'agent.not_owned' + | 'agent.type_not_allowed' + | 'agent.max_tokens_exceeded' | 'turn.agent_busy' | 'goal.already_exists' | 'goal.not_found' @@ -269,15 +275,19 @@ export type KimiErrorCode = | 'skill.not_found' | 'skill.type_unsupported' | 'skill.name_empty' + | 'skill.parse_failed' + | 'skill.nested_too_deep' | 'records.write_failed' | 'compaction.failed' | 'compaction.unable' | 'task.task_id_empty' + | 'task.limit_exceeded' | 'usage.turn_id_conflict' | 'mcp.server_not_found' | 'mcp.server_disabled' | 'mcp.startup_failed' | 'mcp.tool_name_collision' + | 'mcp.oauth_failed' | 'message.not_found' | 'plugin.not_found' | 'plugin.load_failed' @@ -317,9 +327,16 @@ export type KimiErrorCode = | 'storage.corrupted' | 'storage.io_failed' | 'storage.locked' + | 'storage.permission_denied' + | 'storage.disk_full' | 'wire.duplicate_op' | 'wire.cycle' | 'wire.unknown_record' + | 'wire.migration_missing' + | 'cron.expression_invalid' + | 'web.invalid_url' + | 'web.private_address' + | 'web.fetch_failed' | 'validation.failed' | 'not_implemented' | 'internal'; @@ -1170,6 +1187,12 @@ export const kimiErrorCodeSchema = z.enum([ 'session.question_handler_error', 'session.init_failed', 'agent.not_found', + 'agent.already_exists', + 'agent.already_running', + 'agent.not_a_subagent', + 'agent.not_owned', + 'agent.type_not_allowed', + 'agent.max_tokens_exceeded', 'turn.agent_busy', 'goal.already_exists', 'goal.not_found', @@ -1203,15 +1226,19 @@ export const kimiErrorCodeSchema = z.enum([ 'skill.not_found', 'skill.type_unsupported', 'skill.name_empty', + 'skill.parse_failed', + 'skill.nested_too_deep', 'records.write_failed', 'compaction.failed', 'compaction.unable', 'task.task_id_empty', + 'task.limit_exceeded', 'usage.turn_id_conflict', 'mcp.server_not_found', 'mcp.server_disabled', 'mcp.startup_failed', 'mcp.tool_name_collision', + 'mcp.oauth_failed', 'message.not_found', 'plugin.not_found', 'plugin.load_failed', @@ -1236,6 +1263,31 @@ export const kimiErrorCodeSchema = z.enum([ 'fs.too_many_results', 'fs.grep_timeout', 'fs.git_unavailable', + 'os.fs.not_found', + 'os.fs.is_directory', + 'os.fs.not_directory', + 'os.fs.already_exists', + 'os.fs.permission_denied', + 'os.fs.not_empty', + 'os.fs.unavailable', + 'os.fs.unknown', + 'os.process.spawn_failed', + 'os.process.kill_failed', + 'storage.not_found', + 'storage.decode_failed', + 'storage.corrupted', + 'storage.io_failed', + 'storage.locked', + 'storage.permission_denied', + 'storage.disk_full', + 'wire.duplicate_op', + 'wire.cycle', + 'wire.unknown_record', + 'wire.migration_missing', + 'cron.expression_invalid', + 'web.invalid_url', + 'web.private_address', + 'web.fetch_failed', 'validation.failed', 'not_implemented', 'internal',