lean-lsp-mcp 0.14.1__py3-none-any.whl → 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lean_lsp_mcp/__init__.py +19 -0
- lean_lsp_mcp/instructions.py +31 -12
- lean_lsp_mcp/loogle.py +329 -0
- lean_lsp_mcp/models.py +120 -0
- lean_lsp_mcp/outline_utils.py +190 -59
- lean_lsp_mcp/server.py +666 -519
- lean_lsp_mcp/utils.py +31 -0
- {lean_lsp_mcp-0.14.1.dist-info → lean_lsp_mcp-0.16.0.dist-info}/METADATA +25 -3
- lean_lsp_mcp-0.16.0.dist-info/RECORD +17 -0
- lean_lsp_mcp-0.14.1.dist-info/RECORD +0 -15
- {lean_lsp_mcp-0.14.1.dist-info → lean_lsp_mcp-0.16.0.dist-info}/WHEEL +0 -0
- {lean_lsp_mcp-0.14.1.dist-info → lean_lsp_mcp-0.16.0.dist-info}/entry_points.txt +0 -0
- {lean_lsp_mcp-0.14.1.dist-info → lean_lsp_mcp-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {lean_lsp_mcp-0.14.1.dist-info → lean_lsp_mcp-0.16.0.dist-info}/top_level.txt +0 -0
lean_lsp_mcp/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import os
|
|
2
3
|
|
|
3
4
|
from lean_lsp_mcp.server import mcp
|
|
4
5
|
|
|
@@ -24,7 +25,25 @@ def main():
|
|
|
24
25
|
default=8000,
|
|
25
26
|
help="Host port for transport",
|
|
26
27
|
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--loogle-local",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Enable local loogle (auto-installs on first run, ~5-10 min). "
|
|
32
|
+
"Avoids rate limits and network dependencies.",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--loogle-cache-dir",
|
|
36
|
+
type=str,
|
|
37
|
+
help="Override loogle cache location (default: ~/.cache/lean-lsp-mcp/loogle)",
|
|
38
|
+
)
|
|
27
39
|
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
# Set env vars from CLI args (CLI takes precedence over env vars)
|
|
42
|
+
if args.loogle_local:
|
|
43
|
+
os.environ["LEAN_LOOGLE_LOCAL"] = "true"
|
|
44
|
+
if args.loogle_cache_dir:
|
|
45
|
+
os.environ["LEAN_LOOGLE_CACHE_DIR"] = args.loogle_cache_dir
|
|
46
|
+
|
|
28
47
|
mcp.settings.host = args.host
|
|
29
48
|
mcp.settings.port = args.port
|
|
30
49
|
mcp.run(transport=args.transport)
|
lean_lsp_mcp/instructions.py
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
INSTRUCTIONS = """## General Rules
|
|
2
2
|
- All line and column numbers are 1-indexed.
|
|
3
|
-
-
|
|
4
|
-
- This MCP does NOT make permanent file changes. Use other tools for editing.
|
|
5
|
-
- Work iteratively: Small steps, intermediate sorries, frequent checks.
|
|
3
|
+
- This MCP does NOT edit files. Use other tools for editing.
|
|
6
4
|
|
|
7
5
|
## Key Tools
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
6
|
+
- **lean_goal**: Proof state at position. Omit `column` for before/after. "no goals" = done!
|
|
7
|
+
- **lean_diagnostic_messages**: Compiler errors/warnings. "no goals to be solved" = remove tactics.
|
|
8
|
+
- **lean_hover_info**: Type signature + docs. Column at START of identifier.
|
|
9
|
+
- **lean_completions**: IDE autocomplete on incomplete code.
|
|
10
|
+
- **lean_local_search**: Fast local declaration search. Use BEFORE trying a lemma name.
|
|
11
|
+
- **lean_file_outline**: Token-efficient file skeleton (slow-ish).
|
|
12
|
+
- **lean_multi_attempt**: Test tactics without editing: `["simp", "ring", "omega"]`
|
|
13
|
+
- **lean_declaration_file**: Get declaration source. Use sparingly (large output).
|
|
14
|
+
- **lean_run_code**: Run standalone snippet. Use rarely.
|
|
15
|
+
- **lean_build**: Rebuild + restart LSP. Only if needed (new imports). SLOW!
|
|
16
|
+
|
|
17
|
+
## Search Tools (rate limited)
|
|
18
|
+
- **lean_leansearch** (3/30s): Natural language → mathlib
|
|
19
|
+
- **lean_loogle** (3/30s): Type pattern → mathlib
|
|
20
|
+
- **lean_leanfinder** (10/30s): Semantic/conceptual search
|
|
21
|
+
- **lean_state_search** (3/30s): Goal → closing lemmas
|
|
22
|
+
- **lean_hammer_premise** (3/30s): Goal → premises for simp/aesop
|
|
23
|
+
|
|
24
|
+
## Search Decision Tree
|
|
25
|
+
1. "Does X exist locally?" → lean_local_search
|
|
26
|
+
2. "I need a lemma that says X" → lean_leansearch
|
|
27
|
+
3. "Find lemma with type pattern" → lean_loogle
|
|
28
|
+
4. "What's the Lean name for concept X?" → lean_leanfinder
|
|
29
|
+
5. "What closes this goal?" → lean_state_search
|
|
30
|
+
6. "What to feed simp?" → lean_hammer_premise
|
|
31
|
+
|
|
32
|
+
After finding a name: lean_local_search to verify, lean_hover_info for signature.
|
|
33
|
+
|
|
34
|
+
## Return Formats
|
|
35
|
+
List tools return JSON arrays. Empty = `[]`.
|
|
17
36
|
"""
|
lean_lsp_mcp/loogle.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Loogle search - local subprocess and remote API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import orjson
|
|
17
|
+
|
|
18
|
+
from lean_lsp_mcp.models import LoogleResult
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_cache_dir() -> Path:
|
|
24
|
+
if d := os.environ.get("LEAN_LOOGLE_CACHE_DIR"):
|
|
25
|
+
return Path(d)
|
|
26
|
+
xdg = os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")
|
|
27
|
+
return Path(xdg) / "lean-lsp-mcp" / "loogle"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
31
|
+
"""Query the remote loogle API."""
|
|
32
|
+
try:
|
|
33
|
+
req = urllib.request.Request(
|
|
34
|
+
f"https://loogle.lean-lang.org/json?q={urllib.parse.quote(query)}",
|
|
35
|
+
headers={"User-Agent": "lean-lsp-mcp/0.1"},
|
|
36
|
+
)
|
|
37
|
+
with urllib.request.urlopen(req, timeout=20) as response:
|
|
38
|
+
results = orjson.loads(response.read())
|
|
39
|
+
if "hits" not in results:
|
|
40
|
+
return "No results found."
|
|
41
|
+
hits = results["hits"][:num_results]
|
|
42
|
+
return [
|
|
43
|
+
LoogleResult(
|
|
44
|
+
name=r.get("name", ""),
|
|
45
|
+
type=r.get("type", ""),
|
|
46
|
+
module=r.get("module", ""),
|
|
47
|
+
)
|
|
48
|
+
for r in hits
|
|
49
|
+
]
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return f"loogle error:\n{e}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LoogleManager:
|
|
55
|
+
"""Manages local loogle installation and async subprocess."""
|
|
56
|
+
|
|
57
|
+
REPO_URL = "https://github.com/nomeata/loogle.git"
|
|
58
|
+
READY_SIGNAL = "Loogle is ready."
|
|
59
|
+
|
|
60
|
+
def __init__(self, cache_dir: Path | None = None):
|
|
61
|
+
self.cache_dir = cache_dir or get_cache_dir()
|
|
62
|
+
self.repo_dir = self.cache_dir / "repo"
|
|
63
|
+
self.index_dir = self.cache_dir / "index"
|
|
64
|
+
self.process: asyncio.subprocess.Process | None = None
|
|
65
|
+
self._ready = False
|
|
66
|
+
self._lock = asyncio.Lock()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def binary_path(self) -> Path:
|
|
70
|
+
return self.repo_dir / ".lake" / "build" / "bin" / "loogle"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_installed(self) -> bool:
|
|
74
|
+
return self.binary_path.exists()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_running(self) -> bool:
|
|
78
|
+
return (
|
|
79
|
+
self._ready and self.process is not None and self.process.returncode is None
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _check_prerequisites(self) -> tuple[bool, str]:
|
|
83
|
+
if not shutil.which("git"):
|
|
84
|
+
return False, "git not found in PATH"
|
|
85
|
+
if not shutil.which("lake"):
|
|
86
|
+
return (
|
|
87
|
+
False,
|
|
88
|
+
"lake not found (install elan: https://github.com/leanprover/elan)",
|
|
89
|
+
)
|
|
90
|
+
return True, ""
|
|
91
|
+
|
|
92
|
+
def _run(
|
|
93
|
+
self, cmd: list[str], timeout: int = 300, cwd: Path | None = None
|
|
94
|
+
) -> subprocess.CompletedProcess:
|
|
95
|
+
env = os.environ.copy()
|
|
96
|
+
env["LAKE_ARTIFACT_CACHE"] = "false"
|
|
97
|
+
return subprocess.run(
|
|
98
|
+
cmd,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
cwd=cwd or self.repo_dir,
|
|
103
|
+
env=env,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _clone_repo(self) -> bool:
|
|
107
|
+
if self.repo_dir.exists():
|
|
108
|
+
return True
|
|
109
|
+
logger.info(f"Cloning loogle to {self.repo_dir}...")
|
|
110
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
try:
|
|
112
|
+
r = self._run(
|
|
113
|
+
["git", "clone", "--depth", "1", self.REPO_URL, str(self.repo_dir)],
|
|
114
|
+
cwd=self.cache_dir,
|
|
115
|
+
)
|
|
116
|
+
if r.returncode != 0:
|
|
117
|
+
logger.error(f"Clone failed: {r.stderr}")
|
|
118
|
+
return False
|
|
119
|
+
return True
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Clone error: {e}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _build_loogle(self) -> bool:
|
|
125
|
+
if self.is_installed:
|
|
126
|
+
return True
|
|
127
|
+
if not self.repo_dir.exists():
|
|
128
|
+
return False
|
|
129
|
+
logger.info("Downloading mathlib cache...")
|
|
130
|
+
try:
|
|
131
|
+
self._run(["lake", "exe", "cache", "get"], timeout=600)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.warning(f"Cache download: {e}")
|
|
134
|
+
logger.info("Building loogle...")
|
|
135
|
+
try:
|
|
136
|
+
return self._run(["lake", "build"], timeout=900).returncode == 0
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Build error: {e}")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _get_mathlib_version(self) -> str:
|
|
142
|
+
try:
|
|
143
|
+
manifest = json.loads((self.repo_dir / "lake-manifest.json").read_text())
|
|
144
|
+
for pkg in manifest.get("packages", []):
|
|
145
|
+
if pkg.get("name") == "mathlib":
|
|
146
|
+
return pkg.get("rev", "unknown")[:12]
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
return "unknown"
|
|
150
|
+
|
|
151
|
+
def _get_toolchain_version(self) -> str | None:
|
|
152
|
+
"""Get the Lean toolchain version from lean-toolchain file."""
|
|
153
|
+
try:
|
|
154
|
+
return (self.repo_dir / "lean-toolchain").read_text().strip()
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _check_toolchain_installed(self) -> tuple[bool, str]:
|
|
159
|
+
"""Check if the required Lean toolchain is installed."""
|
|
160
|
+
tc = self._get_toolchain_version()
|
|
161
|
+
if not tc:
|
|
162
|
+
return True, "" # Can't check without lean-toolchain file
|
|
163
|
+
# Convert lean-toolchain format to elan directory name
|
|
164
|
+
# e.g., "leanprover/lean4:v4.25.0-rc1" -> "leanprover--lean4---v4.25.0-rc1"
|
|
165
|
+
tc_dir_name = tc.replace("/", "--").replace(":", "---")
|
|
166
|
+
elan_home = Path(os.environ.get("ELAN_HOME", Path.home() / ".elan"))
|
|
167
|
+
tc_path = elan_home / "toolchains" / tc_dir_name
|
|
168
|
+
if not tc_path.exists():
|
|
169
|
+
return False, (
|
|
170
|
+
f"Toolchain '{tc}' not installed. "
|
|
171
|
+
f"Run: cd {self.repo_dir} && lake update"
|
|
172
|
+
)
|
|
173
|
+
return True, ""
|
|
174
|
+
|
|
175
|
+
def check_environment(self) -> tuple[bool, str]:
|
|
176
|
+
"""Check if the loogle environment is valid. Returns (ok, error_msg)."""
|
|
177
|
+
if not self.is_installed:
|
|
178
|
+
return False, "Loogle binary not found"
|
|
179
|
+
ok, err = self._check_toolchain_installed()
|
|
180
|
+
if not ok:
|
|
181
|
+
return False, err
|
|
182
|
+
return True, ""
|
|
183
|
+
|
|
184
|
+
def _get_index_path(self) -> Path:
|
|
185
|
+
return self.index_dir / f"mathlib-{self._get_mathlib_version()}.idx"
|
|
186
|
+
|
|
187
|
+
def _cleanup_old_indices(self) -> None:
|
|
188
|
+
"""Remove old index files from previous mathlib versions."""
|
|
189
|
+
if not self.index_dir.exists():
|
|
190
|
+
return
|
|
191
|
+
current = self._get_index_path()
|
|
192
|
+
for idx in self.index_dir.glob("*.idx"):
|
|
193
|
+
if idx != current:
|
|
194
|
+
try:
|
|
195
|
+
idx.unlink()
|
|
196
|
+
logger.info(f"Removed old index: {idx.name}")
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
def _build_index(self) -> Path | None:
|
|
201
|
+
index_path = self._get_index_path()
|
|
202
|
+
if index_path.exists():
|
|
203
|
+
return index_path
|
|
204
|
+
if not self.is_installed:
|
|
205
|
+
return None
|
|
206
|
+
self.index_dir.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
self._cleanup_old_indices()
|
|
208
|
+
logger.info("Building search index...")
|
|
209
|
+
try:
|
|
210
|
+
self._run(
|
|
211
|
+
[str(self.binary_path), "--write-index", str(index_path), "--json", ""],
|
|
212
|
+
timeout=600,
|
|
213
|
+
)
|
|
214
|
+
return index_path if index_path.exists() else None
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(f"Index build error: {e}")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def ensure_installed(self) -> bool:
|
|
220
|
+
ok, err = self._check_prerequisites()
|
|
221
|
+
if not ok:
|
|
222
|
+
logger.warning(f"Prerequisites: {err}")
|
|
223
|
+
return False
|
|
224
|
+
if not self._clone_repo() or not self._build_loogle():
|
|
225
|
+
return False
|
|
226
|
+
if not self._build_index():
|
|
227
|
+
logger.warning("Index build failed, loogle will build on startup")
|
|
228
|
+
return self.is_installed
|
|
229
|
+
|
|
230
|
+
async def start(self) -> bool:
|
|
231
|
+
if self.process is not None and self.process.returncode is None:
|
|
232
|
+
return self._ready
|
|
233
|
+
ok, err = self.check_environment()
|
|
234
|
+
if not ok:
|
|
235
|
+
logger.error(f"Loogle environment check failed: {err}")
|
|
236
|
+
return False
|
|
237
|
+
cmd = [str(self.binary_path), "--json", "--interactive"]
|
|
238
|
+
if (idx := self._get_index_path()).exists():
|
|
239
|
+
cmd.extend(["--read-index", str(idx)])
|
|
240
|
+
logger.info("Starting loogle subprocess...")
|
|
241
|
+
try:
|
|
242
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
243
|
+
*cmd,
|
|
244
|
+
stdin=asyncio.subprocess.PIPE,
|
|
245
|
+
stdout=asyncio.subprocess.PIPE,
|
|
246
|
+
stderr=asyncio.subprocess.PIPE,
|
|
247
|
+
cwd=self.repo_dir,
|
|
248
|
+
)
|
|
249
|
+
line = await asyncio.wait_for(self.process.stdout.readline(), timeout=120)
|
|
250
|
+
decoded = line.decode()
|
|
251
|
+
if self.READY_SIGNAL in decoded:
|
|
252
|
+
self._ready = True
|
|
253
|
+
logger.info("Loogle ready")
|
|
254
|
+
return True
|
|
255
|
+
# Check stderr for error messages
|
|
256
|
+
try:
|
|
257
|
+
stderr_data = await asyncio.wait_for(
|
|
258
|
+
self.process.stderr.read(), timeout=1
|
|
259
|
+
)
|
|
260
|
+
if stderr_data:
|
|
261
|
+
logger.error(f"Loogle stderr: {stderr_data.decode().strip()}")
|
|
262
|
+
except asyncio.TimeoutError:
|
|
263
|
+
pass
|
|
264
|
+
logger.error(f"Loogle failed to start. stdout: {decoded.strip()}")
|
|
265
|
+
return False
|
|
266
|
+
except asyncio.TimeoutError:
|
|
267
|
+
logger.error("Loogle startup timeout")
|
|
268
|
+
return False
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"Start failed: {e}")
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
async def query(self, q: str, num_results: int = 8) -> list[dict[str, Any]]:
|
|
274
|
+
async with self._lock:
|
|
275
|
+
# Try up to 2 attempts (initial + one restart)
|
|
276
|
+
for attempt in range(2):
|
|
277
|
+
if (
|
|
278
|
+
not self._ready
|
|
279
|
+
or self.process is None
|
|
280
|
+
or self.process.returncode is not None
|
|
281
|
+
):
|
|
282
|
+
if attempt > 0:
|
|
283
|
+
raise RuntimeError("Loogle subprocess not ready")
|
|
284
|
+
self._ready = False
|
|
285
|
+
if not await self.start():
|
|
286
|
+
raise RuntimeError("Failed to start loogle")
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
self.process.stdin.write(f"{q}\n".encode())
|
|
291
|
+
await self.process.stdin.drain()
|
|
292
|
+
line = await asyncio.wait_for(
|
|
293
|
+
self.process.stdout.readline(), timeout=30
|
|
294
|
+
)
|
|
295
|
+
response = json.loads(line.decode())
|
|
296
|
+
if err := response.get("error"):
|
|
297
|
+
logger.warning(f"Query error: {err}")
|
|
298
|
+
return []
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
"name": h.get("name", ""),
|
|
302
|
+
"type": h.get("type", ""),
|
|
303
|
+
"module": h.get("module", ""),
|
|
304
|
+
"doc": h.get("doc"),
|
|
305
|
+
}
|
|
306
|
+
for h in response.get("hits", [])[:num_results]
|
|
307
|
+
]
|
|
308
|
+
except asyncio.TimeoutError:
|
|
309
|
+
raise RuntimeError("Query timeout") from None
|
|
310
|
+
except json.JSONDecodeError as e:
|
|
311
|
+
raise RuntimeError(f"Invalid response: {e}") from e
|
|
312
|
+
|
|
313
|
+
raise RuntimeError("Loogle subprocess not ready")
|
|
314
|
+
|
|
315
|
+
async def stop(self) -> None:
|
|
316
|
+
if self.process:
|
|
317
|
+
try:
|
|
318
|
+
self.process.terminate()
|
|
319
|
+
await asyncio.wait_for(self.process.wait(), timeout=5)
|
|
320
|
+
except asyncio.TimeoutError:
|
|
321
|
+
self.process.kill()
|
|
322
|
+
try:
|
|
323
|
+
await asyncio.wait_for(self.process.wait(), timeout=2)
|
|
324
|
+
except asyncio.TimeoutError:
|
|
325
|
+
pass
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
self.process = None
|
|
329
|
+
self._ready = False
|
lean_lsp_mcp/models.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Pydantic models for MCP tool structured outputs."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalSearchResult(BaseModel):
|
|
8
|
+
name: str = Field(description="Declaration name")
|
|
9
|
+
kind: str = Field(description="Declaration kind (theorem, def, class, etc.)")
|
|
10
|
+
file: str = Field(description="Relative file path")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LeanSearchResult(BaseModel):
|
|
14
|
+
name: str = Field(description="Full qualified name")
|
|
15
|
+
module_name: str = Field(description="Module where declared")
|
|
16
|
+
kind: Optional[str] = Field(None, description="Declaration kind")
|
|
17
|
+
type: Optional[str] = Field(None, description="Type signature")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LoogleResult(BaseModel):
|
|
21
|
+
name: str = Field(description="Declaration name")
|
|
22
|
+
type: str = Field(description="Type signature")
|
|
23
|
+
module: str = Field(description="Module where declared")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LeanFinderResult(BaseModel):
|
|
27
|
+
full_name: str = Field(description="Full qualified name")
|
|
28
|
+
formal_statement: str = Field(description="Lean type signature")
|
|
29
|
+
informal_statement: str = Field(description="Natural language description")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StateSearchResult(BaseModel):
|
|
33
|
+
name: str = Field(description="Theorem/lemma name")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PremiseResult(BaseModel):
|
|
37
|
+
name: str = Field(description="Premise name for simp/omega/aesop")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DiagnosticMessage(BaseModel):
|
|
41
|
+
severity: str = Field(description="error, warning, info, or hint")
|
|
42
|
+
message: str = Field(description="Diagnostic message text")
|
|
43
|
+
line: int = Field(description="Line (1-indexed)")
|
|
44
|
+
column: int = Field(description="Column (1-indexed)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GoalState(BaseModel):
|
|
48
|
+
line_context: str = Field(description="Source line where goals were queried")
|
|
49
|
+
goals: str = Field(description="Goal state (before→after if column omitted)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CompletionItem(BaseModel):
|
|
53
|
+
label: str = Field(description="Completion text to insert")
|
|
54
|
+
kind: Optional[str] = Field(
|
|
55
|
+
None, description="Completion kind (function, variable, etc.)"
|
|
56
|
+
)
|
|
57
|
+
detail: Optional[str] = Field(None, description="Additional detail")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HoverInfo(BaseModel):
|
|
61
|
+
symbol: str = Field(description="The symbol being hovered")
|
|
62
|
+
info: str = Field(description="Type signature and documentation")
|
|
63
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
64
|
+
default_factory=list, description="Diagnostics at this position"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TermGoalState(BaseModel):
|
|
69
|
+
line_context: str = Field(description="Source line where term goal was queried")
|
|
70
|
+
expected_type: Optional[str] = Field(
|
|
71
|
+
None, description="Expected type at this position"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OutlineEntry(BaseModel):
|
|
76
|
+
name: str = Field(description="Declaration name")
|
|
77
|
+
kind: str = Field(description="Declaration kind (Thm, Def, Class, Struct, Ns, Ex)")
|
|
78
|
+
start_line: int = Field(description="Start line (1-indexed)")
|
|
79
|
+
end_line: int = Field(description="End line (1-indexed)")
|
|
80
|
+
type_signature: Optional[str] = Field(
|
|
81
|
+
None, description="Type signature if available"
|
|
82
|
+
)
|
|
83
|
+
children: List["OutlineEntry"] = Field(
|
|
84
|
+
default_factory=list, description="Nested declarations"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FileOutline(BaseModel):
|
|
89
|
+
imports: List[str] = Field(default_factory=list, description="Import statements")
|
|
90
|
+
declarations: List[OutlineEntry] = Field(
|
|
91
|
+
default_factory=list, description="Top-level declarations"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AttemptResult(BaseModel):
|
|
96
|
+
snippet: str = Field(description="Code snippet that was tried")
|
|
97
|
+
goal_state: Optional[str] = Field(
|
|
98
|
+
None, description="Goal state after applying snippet"
|
|
99
|
+
)
|
|
100
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
101
|
+
default_factory=list, description="Diagnostics for this attempt"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BuildResult(BaseModel):
|
|
106
|
+
success: bool = Field(description="Whether build succeeded")
|
|
107
|
+
output: str = Field(description="Build output")
|
|
108
|
+
errors: List[str] = Field(default_factory=list, description="Build errors if any")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RunResult(BaseModel):
|
|
112
|
+
success: bool = Field(description="Whether code compiled successfully")
|
|
113
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
114
|
+
default_factory=list, description="Compiler diagnostics"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DeclarationInfo(BaseModel):
|
|
119
|
+
file_path: str = Field(description="Path to declaration file")
|
|
120
|
+
content: str = Field(description="File content")
|