## Token Savings: LSP vs Grep/Read How much context does an AI agent consume when navigating code with grep/read versus structured LSP calls? This experiment measures the bytes flowing through each approach on real codebases across three languages and four projects. ### Key findings | Codebase | Language | Lines | Overall | `/lsp-rename` | Precision | Tokens saved | |----------|----------|------:|--------:|--------------:|----------:|-------------:| | agent-lsp | Go | 15K | **5x** | **96x** | 92% noise | ~1.3M | | Hono | TypeScript | 24K | **13x** | **1,441x** | 93% noise | ~1.2M | | FastAPI | Python | 33K | **2x** | **116x** | 97% noise | ~693K | | Next.js | TypeScript | 196K | **5x** | **52x** | 98% noise | ~1.1M | | HashiCorp Consul | Go | 319K | **34x** | **97x** | 99% noise | ~13.1M | - **`/lsp-rename` saves 92-1,441x consistently.** The grep agent must read every file containing the symbol to safely rename it; LSP does it atomically in 3 calls. - **92-99% of grep results are false positives.** Grep matches strings; LSP returns only actual symbol references. On consul (319K lines), grep for `Close` returned 1,156 matches; only 12 are real references. - **Savings scale with codebase size.** 5x at 15K lines, 21x at 319K lines. - **Speculative execution (`/lsp-safe-edit`): 60x.** Preview an edit without touching disk. The grep agent must edit, build (1.3s), revert, re-build. LSP answers in 2ms. - **Code Map (`/lsp-understand`): 6x, 936 grep calls reduced to 162.** Full file analysis with hover, references, and call hierarchy for every exported symbol. - **Edit safety: 23-656x.** Structured diagnostics vs raw compiler output. - **Interface implementations: 1,002-1,813x** (Go). Grep cannot determine which types satisfy an interface without reading every file. - **Multi-hop call chains: 9-12x, 87-311x faster** (Go). **Methodology:** - **Grep/Read** = total bytes of grep output + file content read + build/test output. - **LSP** = JSON response bytes from each LSP call (normalized to compact relative paths, matching what agent-lsp returns to clients). - Token estimate: 1 token per 4 bytes (standard approximation for code). - LSP startup cost excluded (amortizes over a session). - Go: 13 tasks covering 7 agent skills. Python/TypeScript: 11 tasks. - Skill workflows test the full multi-step sequence (e.g. `/lsp-refactor` = blast-radius + rename + verify), not individual tool calls. The grep side models the equivalent multi-step agent workflow for the same task. - Generated by `go run ./experiments/token-savings`. --- ### agent-lsp (15,850 lines, 82 files) **Simple tasks** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Find callers of `Shutdown` | 3,115 | 1,133 | **3x** | 1 vs 1 | 79ms vs 2ms | | Type signature of `Shutdown` | 4,872 | 393 | **12x** | 1 vs 1 | 54ms vs 0ms | | Edit safety check (break build, measure output) | 75,740 | 1,171 | **65x** | 3 vs 3 | 0ms vs 0ms | **Skill workflows and advanced tasks (10 tasks)** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Skill: `/lsp-refactor` rename `Shutdown` | 28,304 | 30,733 | **1x** | 17 vs 5 | 0ms vs 0ms | | Skill: `/lsp-impact` on `client.go` (52 exports) | 2,082,537 | 419,978 | **5x** | 795 vs 109 | 0ms vs 0ms | | Skill: `/lsp-rename` `Shutdown` (14 files) | 220,455 | 2,285 | **96x** | 16 vs 3 | 0ms vs 0ms | | Skill: `/lsp-dead-code` on `client.go` (52 exports, 5 dead) | 679,099 | 239,761 | **3x** | 57 vs 57 | 0ms vs 0ms | | Skill: `/lsp-understand` Code Map of `client.go` (52 exports) | 3,250,695 | 554,820 | **6x** | 936 vs 162 | 924ms vs 30ms | | Skill: `/lsp-safe-edit` (speculative edit + verify + revert) | 75,740 | 1,269 | **60x** | 4 vs 3 | 1241ms vs 2ms | | Skill: `/lsp-verify` (diagnostics + build + tests) | 75,804 | 27,406 | **3x** | 3 vs 3 | 5157ms vs 0ms | | Precision: `Close` (61 grep vs 5 LSP refs, 56 false+) | 4,958 | 517 | **10x** | 1 vs 1 | 59ms vs 22ms | | Multi-hop: callers of callers of `Shutdown` | 41,680 | 4,637 | **9x** | 25 vs 2 | 1097ms vs 2ms | | Interface: implementations of `logSender` (1 found) | 98,221 | 98 | **1002x** | 4 vs 1 | 171ms vs 27ms | **Total: 6,641,220 grep/read vs 1,284,201 LSP = 5x savings (~1,339,254 tokens saved)** ### Hono (24,178 lines TypeScript, 185 files) **Simple tasks** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Find callers of `Variables` | 7,205 | 84 | **86x** | 1 vs 1 | 59ms vs 1ms | | Type signature of `Variables` | 0 | 168 | **0x** | 1 vs 1 | 37ms vs 1ms | | Edit safety check (break build, measure output) | 79,359 | 121 | **656x** | 3 vs 3 | 0ms vs 0ms | **Skill workflows and advanced tasks (8 tasks)** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Skill: `/lsp-refactor` rename `Variables` | 49,578 | 447 | **111x** | 27 vs 5 | 0ms vs 0ms | | Skill: `/lsp-impact` on `types.ts` (41 exports) | 1,625,162 | 129,096 | **13x** | 610 vs 86 | 0ms vs 0ms | | Skill: `/lsp-rename` `Variables` (24 files) | 492,954 | 342 | **1441x** | 26 vs 3 | 0ms vs 0ms | | Skill: `/lsp-dead-code` on `types.ts` (41 exports, 8 dead) | 694,757 | 129,014 | **5x** | 45 vs 45 | 0ms vs 0ms | | Skill: `/lsp-understand` Code Map of `types.ts` (41 exports) | 1,922,818 | 142,249 | **14x** | 748 vs 128 | 475ms vs 14ms | | Skill: `/lsp-safe-edit` (speculative edit + verify + revert) | 79,359 | 218 | **364x** | 4 vs 3 | 201ms vs 1ms | | Skill: `/lsp-verify` (diagnostics + build + tests) | 86,227 | 68 | **1268x** | 3 vs 3 | 930ms vs 0ms | | Precision: `Close` (15 grep vs 1 LSP refs, 14 false+) | 1,051 | 91 | **12x** | 1 vs 1 | 33ms vs 6ms | **Total: 5,038,470 grep/read vs 401,898 LSP = 13x savings (~1,159,143 tokens saved)** ### fastapi-bench (32,564 lines, 621 files) **Simple tasks** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Find callers of `middleware` | 7,928 | 572 | **14x** | 1 vs 1 | 91ms vs 53ms | | Type signature of `middleware` | 0 | 156 | **0x** | 1 vs 1 | 91ms vs 0ms | | Edit safety check (break build, measure output) | 181,470 | 4,200 | **43x** | 3 vs 3 | 0ms vs 0ms | **Skill workflows and advanced tasks (8 tasks)** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Skill: `/lsp-refactor` rename `middleware` | 24,665 | 4,202 | **6x** | 25 vs 5 | 0ms vs 0ms | | Skill: `/lsp-impact` on `routing.py` (38 exports) | 1,460,499 | 802,062 | **2x** | 418 vs 79 | 0ms vs 0ms | | Skill: `/lsp-rename` `middleware` (22 files) | 416,042 | 3,601 | **116x** | 24 vs 3 | 0ms vs 0ms | | Skill: `/lsp-dead-code` on `routing.py` (38 exports, 22 dead) | 904,821 | 626,555 | **1x** | 41 vs 41 | 0ms vs 0ms | | Skill: `/lsp-understand` Code Map of `routing.py` (38 exports) | 1,482,337 | 825,217 | **2x** | 346 vs 118 | 1454ms vs 146ms | | Skill: `/lsp-safe-edit` (speculative edit + verify + revert) | 181,470 | 4,297 | **42x** | 4 vs 3 | 0ms vs 3ms | | Skill: `/lsp-verify` (diagnostics + build + tests) | 379,102 | 3,659 | **104x** | 4 vs 3 | 0ms vs 0ms | | Precision: `validate` (64 grep vs 2 LSP refs, 62 false+) | 6,465 | 204 | **32x** | 1 vs 1 | 86ms vs 24ms | **Total: 5,044,799 grep/read vs 2,274,725 LSP = 2x savings (~692,518 tokens saved)** ### consul-bench (319,072 lines, 1468 files) **Simple tasks** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Find callers of `GetNode` | 10,815 | 5,956 | **2x** | 1 vs 1 | 315ms vs 27ms | | Type signature of `GetNode` | 20,817 | 464 | **45x** | 1 vs 1 | 381ms vs 0ms | | Edit safety check (break build, measure output) | 173,498 | 7,477 | **23x** | 3 vs 3 | 0ms vs 0ms | **Skill workflows and advanced tasks (10 tasks)** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Skill: `/lsp-refactor` rename `GetNode` | 46,874 | 14,055 | **3x** | 21 vs 5 | 0ms vs 0ms | | Skill: `/lsp-impact` on `catalog.go` (57 exports) | 17,745,627 | 841,371 | **21x** | 5534 vs 119 | 0ms vs 0ms | | Skill: `/lsp-rename` `GetNode` (18 files) | 802,815 | 8,286 | **97x** | 20 vs 3 | 0ms vs 0ms | | Skill: `/lsp-dead-code` on `catalog.go` (57 exports, 22 dead) | 7,133,669 | 348,452 | **20x** | 62 vs 62 | 0ms vs 0ms | | Skill: `/lsp-understand` Code Map of `catalog.go` (57 exports) | 27,288,671 | 315,737 | **86x** | 6661 vs 178 | 5424ms vs 473ms | | Skill: `/lsp-safe-edit` (speculative edit + verify + revert) | 173,498 | 7,575 | **23x** | 4 vs 3 | 25829ms vs 28ms | | Skill: `/lsp-verify` (diagnostics + build + tests) | 173,557 | 1,551 | **112x** | 3 vs 3 | 17149ms vs 0ms | | Precision: `Close` (1156 grep vs 12 LSP refs, 1144 false+) | 83,702 | 1,337 | **63x** | 1 vs 1 | 281ms vs 223ms | | Multi-hop: callers of callers of `GetNode` | 134,480 | 15,901 | **8x** | 29 vs 2 | 3687ms vs 33ms | | Interface: implementations of `ExportFetcher` (1 found) | 177,668 | 98 | **1813x** | 4 vs 1 | 312ms vs 134ms | **Total: 53,965,691 grep/read vs 1,568,260 LSP = 34x savings (~13,099,357 tokens saved)** --- ### Reproduce ```bash # Run on any Go project (13 tasks, 7 skills) go run ./experiments/token-savings --root /path/to/go/project # Run on any Python project (11 tasks, 7 skills) go run ./experiments/token-savings --root /path/to/python/project --language python # Run on any TypeScript project (11 tasks, 7 skills) go run ./experiments/token-savings --root /path/to/ts/project/src --language typescript # Append results to the doc go run ./experiments/token-savings --root /path/to/project --output docs/token-savings.md ``` Prerequisites: language server on PATH (`gopls`, `pyright-langserver`, or `typescript-language-server`). Source: [`experiments/token-savings/main.go`](../experiments/token-savings/main.go) ### src (196,523 lines, 1201 files) **Simple tasks** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Find callers of `RouteCacheEntry` | 9,014 | 352 | **26x** | 1 vs 1 | 138ms vs 1ms | | Type signature of `RouteCacheEntry` | 7,896 | 310 | **25x** | 1 vs 1 | 114ms vs 1ms | | Edit safety check (break build, measure output) | 110,830 | 6,386 | **17x** | 3 vs 3 | 0ms vs 0ms | **Skill workflows and advanced tasks (8 tasks)** | Task | Grep/Read | LSP | Ratio | Round trips | Time | |------|----------:|----:|------:|------------:|-----:| | Skill: `/lsp-refactor` rename `RouteCacheEntry` | 22,660 | 7,000 | **3x** | 9 vs 5 | 0ms vs 0ms | | Skill: `/lsp-impact` on `cache.ts` (43 exports) | 1,493,510 | 649,755 | **2x** | 477 vs 90 | 0ms vs 0ms | | Skill: `/lsp-rename` `RouteCacheEntry` (6 files) | 345,596 | 6,635 | **52x** | 8 vs 3 | 0ms vs 0ms | | Skill: `/lsp-dead-code` on `cache.ts` (43 exports, 0 dead) | 698,926 | 641,104 | **1x** | 47 vs 47 | 0ms vs 0ms | | Skill: `/lsp-understand` Code Map of `cache.ts` (43 exports) | 1,958,789 | 803,213 | **2x** | 612 vs 134 | 1871ms vs 55ms | | Skill: `/lsp-safe-edit` (speculative edit + verify + revert) | 111,343 | 6,483 | **17x** | 4 vs 3 | 1778ms vs 1ms | | Skill: `/lsp-verify` (diagnostics + build + tests) | 111,963 | 6,066 | **18x** | 3 vs 3 | 1847ms vs 0ms | | Precision: `Close` (158 grep vs 3 LSP refs, 155 false+) | 13,279 | 357 | **37x** | 1 vs 1 | 103ms vs 93ms | **Total: 4,883,806 grep/read vs 2,127,661 LSP = 2x savings (~689,036 tokens saved)**