diff --git a/AGENTS.md b/AGENTS.md index 5706416..2bb33f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,11 +129,8 @@ in [BACKLOG.md](BACKLOG.md). Highest leverage first: matches at ranks 4–10. Local only; never an API call on the query path. 3. **#176, #175** — cheap, contained perf/size wins. -**Two items need the user's decision before any code:** +**One item needs the user's decision before any code:** -- **#177** (fold the four `graph-*` tools into one) renames tools referenced by - the OMI vault's `Playbook.md`, the managed `omind` skill, and other fleet - machines. Coordinated fleet change, not a local one. - **#172** (`omind consolidate`) merges notes. Propose-and-review only — a wrong merge destroys memory that exists nowhere else. diff --git a/BACKLOG.md b/BACKLOG.md index 736b1ff..450f992 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -21,11 +21,10 @@ _No open items._ ### Efficiency -- [ ] **Remove the deprecated `graph-*` MCP compatibility aliases after one release** +- [x] **Remove the deprecated `graph-*` MCP compatibility aliases after one release** ([#181](https://github.com/CryptoJones/omind/issues/181)) — _chore (tokens)_ — - `graph-path`, `graph-orphans`, `graph-dangling`, and `graph-stats` remain for - one bridge release while fleet clients refresh; remove them in the following - release. `graph-neighbors` stays. + Removed `graph-path`, `graph-orphans`, `graph-dangling`, and `graph-stats` + after the 5.0 bridge release; `graph-neighbors` stays. ## Not planned diff --git a/CHANGELOG.md b/CHANGELOG.md index 1183328..81f0990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed +- Remove the deprecated `graph-path`, `graph-orphans`, `graph-dangling`, and + `graph-stats` MCP compatibility aliases after the 5.0 bridge release. Use the + unified `graph(op=path|orphans|dangling|stats)` tool; `graph-neighbors` stays. + ### Changed +- `omind bench` now reports the exposed MCP tool count and serialized schema + token estimate so future surface changes remain measurable. - **A genuine preflight miss auto-clears the per-turn gate instead of forcing a manual consult.** Previously, when `retrieve.relevant_titles` found nothing relevant to the turn's task, the gate stayed armed and demanded a diff --git a/README.md b/README.md index 8ec9f19..a0e0715 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ questions that previously matched nothing now return ranked answers. ```bash omind search "why did release signing fail" --explain omind reindex --index-only # refresh the derived index (search does this too) -omind bench # latency and token cost on your own vault +omind bench # latency, MCP schema size, and token cost ``` The index lives in the state directory, never in the vault — it is disposable diff --git a/src/omind/bench.py b/src/omind/bench.py index 9430b08..77820e9 100644 --- a/src/omind/bench.py +++ b/src/omind/bench.py @@ -10,9 +10,9 @@ * **latency** — build the index, refresh it incrementally, and run queries both through the index and through the pre-index full-vault scan, so the speedup is a measurement rather than a claim. -* **tokens** — the size of what a session actually pays for: the SessionStart - capsule, one bounded recall, and the paged-versus-unpaged listing payload that - used to be ~87k tokens in a single MCP tool result. +* **tokens** — the size of what a session actually pays for: the MCP tool schema, + SessionStart capsule, one bounded recall, and the paged-versus-unpaged listing + payload that used to be ~87k tokens in a single MCP tool result. Read-only: it never writes a note. It does build/refresh the derived index, which is disposable by design. @@ -20,6 +20,8 @@ from __future__ import annotations +import asyncio +import json import time from dataclasses import dataclass, field from pathlib import Path @@ -105,6 +107,7 @@ def _timed(call: Any) -> tuple[float, Any]: def run(omi_dir: Path | str, *, queries: tuple[str, ...] = SAMPLE_QUERIES) -> Report: """Measure retrieval on ``omi_dir`` and return the report.""" from omind import ai_usage, embed, hooks, recall, searchindex + from omind.server import build_server from omind.store import OmiStore omi = Path(omi_dir).expanduser() @@ -119,6 +122,14 @@ def run(omi_dir: Path | str, *, queries: tuple[str, ...] = SAMPLE_QUERIES) -> Re "count", str(embed.status()["reason"] or "on"), ) + tools = asyncio.run(build_server(omi).list_tools()) + tool_schemas = json.dumps( + [tool.model_dump(mode="json", exclude_none=True) for tool in tools], + ensure_ascii=False, + separators=(",", ":"), + ) + report.add("MCP tools exposed", len(tools), "count") + report.add("MCP tool schemas", ai_usage.estimate_tokens(tool_schemas), "tokens") if not searchindex.available(): report.add("search index", 0, "count", "unavailable — search scans the vault") @@ -218,8 +229,6 @@ def run_quality( def _listing_tokens(notes: list[Any]) -> tuple[int, int]: """``(paged, unpaged)`` token estimate for the listing payload — the tool result that was ~87k tokens on a 744-note vault before it was paged.""" - import json - from omind import ai_usage from omind.server import DEFAULT_PAGE diff --git a/src/omind/server.py b/src/omind/server.py index 06ecfee..755a2f5 100644 --- a/src/omind/server.py +++ b/src/omind/server.py @@ -137,7 +137,7 @@ async def stdout_writer() -> None: #: Default and hard-cap page sizes for every list-shaped tool. Unbounded list #: tools were the single largest token leak in the memory layer: `list-notes` on #: a 744-note vault returned ~348 KB (~87k tokens) in ONE tool result, and -#: graph-orphans/graph-dangling returned hundreds of rows nobody paged through. +#: graph(op=orphans|dangling) returned hundreds of rows nobody paged through. #: An agent that needs more asks for the next page; an agent that needed one #: note no longer pays for the whole vault. DEFAULT_PAGE = 25 @@ -468,49 +468,6 @@ def graph_tool( ) -> dict[str, object]: return _graph_query(op, source, target, limit, offset) - @mcp.tool( - name="graph-path", - description=( - "Shortest [[wikilink]] path between two notes, as a list of filenames; " - "`path` is null when no path connects them. Deprecated: use graph " - "with op=path; this compatibility name will be removed next release." - ), - ) - def graph_path(source: str, target: str) -> dict[str, object]: - return _graph_query("path", source, target) - - @mcp.tool( - name="graph-orphans", - description=( - "One page of notes with no inbound or outbound [[wikilinks]]. " - "Deprecated: use graph with op=orphans; this compatibility name " - "will be removed next release." - ), - ) - def graph_orphans(limit: int = DEFAULT_PAGE, offset: int = 0) -> dict[str, object]: - return _graph_query("orphans", limit=limit, offset=offset) - - @mcp.tool( - name="graph-dangling", - description=( - "One page of [[wikilinks]] resolving to no existing note, with their " - "source. Deprecated: use graph with op=dangling; this compatibility " - "name will be removed next release." - ), - ) - def graph_dangling(limit: int = DEFAULT_PAGE, offset: int = 0) -> dict[str, object]: - return _graph_query("dangling", limit=limit, offset=offset) - - @mcp.tool( - name="graph-stats", - description=( - "Whole-graph counts. Deprecated: use graph with op=stats; this " - "compatibility name will be removed next release." - ), - ) - def graph_stats() -> dict[str, object]: - return _graph_query("stats") - return mcp diff --git a/tests/test_bench.py b/tests/test_bench.py index c5300d4..0c1e147 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -35,6 +35,8 @@ def test_report_covers_latency_and_tokens(tmp_path: Path) -> None: report = bench.run(_vault(tmp_path), queries=("nebraska",)) names = [m.name for m in report.measurements] assert "notes in vault" in names + assert "MCP tools exposed" in names + assert "MCP tool schemas" in names assert "index build (from scratch)" in names assert "index refresh (no changes)" in names assert any(name.startswith("search ") for name in names) @@ -44,6 +46,13 @@ def test_report_covers_latency_and_tokens(tmp_path: Path) -> None: assert {"ms", "tokens", "count"} <= units +def test_report_measures_the_reduced_mcp_surface(tmp_path: Path) -> None: + report = bench.run(_vault(tmp_path), queries=("nebraska",)) + by_name = {m.name: m.value for m in report.measurements} + assert by_name["MCP tools exposed"] == 13 + assert by_name["MCP tool schemas"] > 0 + + def test_paging_is_reported_as_a_token_saving(tmp_path: Path) -> None: report = bench.run(_vault(tmp_path), queries=("nebraska",)) by_name = {m.name: m.value for m in report.measurements} diff --git a/tests/test_server.py b/tests/test_server.py index a702739..cb97ab5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -39,10 +39,6 @@ "list-tags", "graph-neighbors", "graph", - "graph-path", - "graph-orphans", - "graph-dangling", - "graph-stats", } @@ -190,8 +186,6 @@ def test_every_list_tool_is_bounded(server: FastMCP) -> None: for tool, args in ( ("backlinks", {"name": "Note 00.md", "limit": 1}), ("list-tags", {"limit": 1}), - ("graph-orphans", {"limit": 1}), - ("graph-dangling", {"limit": 1}), ("graph-neighbors", {"name": "Linker", "limit": 1}), ("graph", {"op": "orphans", "limit": 1}), ("graph", {"op": "dangling", "limit": 1}), @@ -276,24 +270,15 @@ def test_graph_tools(server: FastMCP) -> None: nbrs = call(server, "graph-neighbors", {"name": "A", "depth": 2, "direction": "out"})["result"] assert [n["filename"] for n in nbrs] == ["B.md", "C.md"] - assert call(server, "graph-path", {"source": "A", "target": "C"})["path"] == [ - "A.md", - "B.md", - "C.md", - ] - assert call(server, "graph-orphans", {})["result"] == ["Lonely.md"] - dangling = call(server, "graph-dangling", {})["result"] - assert dangling == [{"source": "Lonely.md", "target": "Ghost"}] - assert call(server, "graph-stats", {})["notes"] == 4 - - # New unified surface; legacy names above remain for one compatibility release. assert call( server, "graph", {"op": "path", "source": "A", "target": "C"}, )["path"] == ["A.md", "B.md", "C.md"] assert call(server, "graph", {"op": "orphans"})["result"] == ["Lonely.md"] - assert call(server, "graph", {"op": "dangling"})["result"] == dangling + assert call(server, "graph", {"op": "dangling"})["result"] == [ + {"source": "Lonely.md", "target": "Ghost"} + ] assert call(server, "graph", {"op": "stats"})["notes"] == 4 @@ -318,12 +303,12 @@ def counting(omi: Path) -> Any: monkeypatch.setattr(graph_mod, "build_graph", counting) server = build_server(omi_dir, node_id="testnode-abc123") call(server, "create-note", {"title": "A", "connections": ["B"]}) - call(server, "graph-stats", {}) - call(server, "graph-orphans", {}) - call(server, "graph-dangling", {}) + call(server, "graph", {"op": "stats"}) + call(server, "graph", {"op": "orphans"}) + call(server, "graph", {"op": "dangling"}) assert calls["n"] == 1 # three graph queries, one build (cached) call(server, "create-note", {"title": "B"}) # a write changes the vault - call(server, "graph-stats", {}) + call(server, "graph", {"op": "stats"}) assert calls["n"] == 2 # cache busted, rebuilt once