1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Structured error types for CodeLens MCP tools.
//! Maps to JSON-RPC error codes for protocol-level error reporting.
#[derive(Debug, thiserror::Error)]
pub enum CodeLensError {
// ── Protocol errors (JSON-RPC level) ──────────────────────────────
/// Missing or invalid tool parameter (JSON-RPC -32602).
#[error("Missing required parameter: {0}")]
MissingParam(String),
/// Unknown tool name (JSON-RPC -32601).
#[error("Unknown tool: {0}")]
ToolNotFound(String),
// ── User errors ───────────────────────────────────────────────────
/// Resource (file, memory, symbol) not found.
#[error("Not found: {0}")]
NotFound(String),
/// Validation error — invalid range, path traversal, etc.
#[error("Validation error: {0}")]
Validation(String),
// ── Capability errors ─────────────────────────────────────────────
/// Feature not available (e.g., semantic search without embeddings).
#[cfg(feature = "semantic")]
#[error("Feature unavailable: {0}")]
FeatureUnavailable(String),
/// Language not supported for the requested operation.
#[error("Language '{language}' does not support '{feature}'")]
#[allow(dead_code)]
LanguageUnsupported { language: String, feature: String },
/// LSP server not attached or not configured for this project.
#[error("LSP not attached: {0}")]
LspNotAttached(String),
/// Symbol index not ready (initial indexing still in progress).
#[error("Index not ready: {0}")]
#[allow(dead_code)]
IndexNotReady(String),
// ── System errors ─────────────────────────────────────────────────
/// LSP server unavailable or error.
#[error("LSP error: {0}")]
LspError(String),
/// Operation timed out.
#[error("Timeout: {operation} after {elapsed_ms}ms")]
Timeout { operation: String, elapsed_ms: u64 },
/// Session expired or invalid.
#[error("Stale session: {0}")]
#[allow(dead_code)]
StaleSession(String),
/// Resource limit exceeded (e.g., too many concurrent LSP sessions).
#[error("Resource exhausted: {0}")]
#[allow(dead_code)]
ResourceExhausted(String),
/// I/O error.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// Internal/unexpected error.
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
impl CodeLensError {
/// Map to a JSON-RPC error code. Used by dispatch_tool for protocol-level errors.
pub fn jsonrpc_code(&self) -> i64 {
match self {
// Protocol errors
Self::MissingParam(_) => -32602,
Self::ToolNotFound(_) => -32601,
// User errors
Self::NotFound(_) => -32000,
Self::Validation(_) => -32003,
// Capability errors
#[cfg(feature = "semantic")]
Self::FeatureUnavailable(_) => -32002,
Self::LanguageUnsupported { .. } => -32002,
Self::LspNotAttached(_) => -32001,
Self::IndexNotReady(_) => -32004,
// System errors
Self::LspError(_) => -32001,
Self::Timeout { .. } => -32005,
Self::StaleSession(_) => -32006,
Self::ResourceExhausted(_) => -32007,
Self::Io(_) => -32603,
Self::Internal(_) => -32603,
}
}
/// Whether this is a protocol-level error (should be returned as JSON-RPC error).
pub fn is_protocol_error(&self) -> bool {
matches!(self, Self::ToolNotFound(_) | Self::MissingParam(_))
}
/// Structured recovery hint derived from the error variant.
///
/// Agents can parse this field to select a fallback action without
/// string-matching the error message. Returns `None` when no specific
/// recovery path is known.
pub fn recovery_hint(&self) -> Option<RecoveryHint> {
match self {
Self::MissingParam(field) => Some(RecoveryHint::RequireField {
field: field.clone(),
}),
Self::ToolNotFound(_) => Some(RecoveryHint::FallbackTool {
tool: "get_capabilities".to_owned(),
reason: "list currently available tools and features".to_owned(),
}),
#[cfg(feature = "semantic")]
Self::FeatureUnavailable(_) => Some(RecoveryHint::RequireFeature {
feature: "semantic".to_owned(),
install: "rebuild with `--features semantic` and call index_embeddings".to_owned(),
}),
Self::LspNotAttached(_) => Some(RecoveryHint::FallbackTool {
tool: "find_symbol".to_owned(),
reason: "tree-sitter index satisfies most symbol lookups without LSP".to_owned(),
}),
Self::IndexNotReady(_) => Some(RecoveryHint::RetryAfterSeconds { seconds: 5 }),
Self::Timeout { .. } => Some(RecoveryHint::FallbackTool {
tool: "start_analysis_job".to_owned(),
reason: "move heavy work to the durable job queue".to_owned(),
}),
Self::ResourceExhausted(_) => Some(RecoveryHint::RetryAfterSeconds { seconds: 10 }),
_ => None,
}
}
}
/// Structured recovery hint — lets agents pick a fallback action without
/// parsing error strings. Emitted in the error response when the variant
/// has a clear recovery path.
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RecoveryHint {
/// Call this tool instead; it satisfies the same intent by another route.
FallbackTool { tool: String, reason: String },
/// Feature must be enabled via build flag or data setup before the call succeeds.
#[cfg(feature = "semantic")]
RequireFeature { feature: String, install: String },
/// A required input field is missing — name it explicitly so the agent can supply it.
RequireField { field: String },
/// The operation can succeed if retried after a short wait.
RetryAfterSeconds { seconds: u64 },
}