# Sprint 8a Implementation Plan — Token Optimization for All Tools

## Background

GodotIQ is an MCP (Model Context Protocol) server for AI-assisted Godot 4.x game development. It exposes 32 tools via FastMCP that let Claude analyze scenes, scripts, assets, and runtime state. The tools return structured dict results serialized as JSON.

**Problem:** Several tools generate massive output on real projects. `asset_registry` on a 670-model project produces 140k characters. `scene_map` on an 876-node scene produces similar volumes. Since Claude Code's context window is ~200k tokens, a single tool call can consume most of it, making GodotIQ unusable on real projects.

**Solution:** Enforce output size limits across all 32 tools via a three-tier `detail` parameter system (brief/normal/full) with automatic truncation safety nets.

## Current State

### Existing Infrastructure (Already Built)

GodotIQ already has partial token optimization infrastructure:

**`output_limit.py`** — Contains `apply_output_limit(result: dict, detail: str, list_keys)` which:
- Takes a result dict and truncates list values to fit character limits
- Uses binary search to find optimal truncation points
- Adds `output_truncated`, `truncation_notice`, and `{key}_total` metadata
- Character limits: brief=2,000, normal=8,000, full=unlimited (None)

**Config system** — `config.py` has `default_detail: "normal"` and `config.get_default_detail()`.

**17 of 32 tools** already accept `detail` parameter and use `apply_output_limit()`.

### What Needs to Change

1. **15 tools** need `detail` parameter added (see complete inventory below)
2. **1 tool** (`impact_check`) has `detail` misused as a change description — needs parameter renaming
3. **`output_limit.py`** needs a string-based `truncate_output()` function added (future-proofing safety net)
4. **`apply_output_limit()`** needs input validation for unknown detail values
5. All existing tools need verification that they respect the char limits
6. Tests need to be added for all modified tools

### Complete Tool Inventory (32 tools)

#### Already have `detail` (17 tools) — verify limits only:
| # | Tool | Category | File |
|---|------|----------|------|
| 1 | animation_info | Animation | `tools/animation/__init__.py` |
| 2 | animation_audit | Animation | `tools/animation/__init__.py` |
| 3 | asset_registry | Assets | `tools/assets/__init__.py` |
| 4 | dependency_graph | Code | `tools/code/__init__.py` |
| 5 | signal_map | Code | `tools/code/__init__.py` |
| 6 | validate | Code | `tools/code/__init__.py` |
| 7 | trace_flow | Flow | `tools/flow/__init__.py` |
| 8 | project_summary | Memory | `tools/memory/__init__.py` |
| 9 | file_context | Memory | `tools/memory/__init__.py` |
| 10 | scene_map | Spatial | `tools/spatial/__init__.py` |
| 11 | spatial_audit | Spatial | `tools/spatial/__init__.py` |
| 12 | perf_snapshot | Bridge | `tools/bridge/__init__.py` |
| 13 | scene_tree | Bridge | `tools/bridge/__init__.py` |
| 14 | exec | Bridge | `tools/bridge/__init__.py` |
| 15 | state_inspect | Bridge | `tools/bridge/__init__.py` |
| 16 | nav_query | Bridge | `tools/bridge/__init__.py` |
| 17 | ui_map | Bridge | `tools/bridge/__init__.py` |

#### Need `detail` added (14 tools):
| # | Tool | Category | File | Output Risk |
|---|------|----------|------|-------------|
| 18 | suggest_scale | Assets | `tools/assets/__init__.py` | SMALL (no-op) |
| 19 | impact_check | Code | `tools/code/__init__.py` | MEDIUM (rename + add) |
| 20 | placement | Spatial | `tools/spatial/__init__.py` | SMALL (no-op) |
| 21 | screenshot | Bridge | `tools/bridge/__init__.py` | N/A (no-op, binary) |
| 22 | run | Bridge | `tools/bridge/__init__.py` | SMALL (no-op) |
| 23 | editor_context | Bridge | `tools/bridge/__init__.py` | MEDIUM |
| 24 | node_ops | Bridge | `tools/bridge/__init__.py` | MEDIUM |
| 25 | script_ops | Bridge | `tools/bridge/__init__.py` | SMALL (no-op) |
| 26 | file_ops | Bridge | `tools/bridge/__init__.py` | HIGH |
| 27 | input | Bridge | `tools/bridge/__init__.py` | MEDIUM |
| 28 | watch | Bridge | `tools/bridge/__init__.py` | MEDIUM |
| 29 | undo_history | Bridge | `tools/bridge/__init__.py` | MEDIUM |
| 30 | save_scene | Bridge | `tools/bridge/__init__.py` | SMALL (no-op) |
| 31 | camera | Bridge | `tools/bridge/__init__.py` | SMALL (no-op) |

#### Trivial tool (1):
| # | Tool | Category | File | Notes |
|---|------|----------|------|-------|
| 32 | ping | Server | `server.py` | No-op detail for API consistency |

## Architecture

### Detail Level System

Every tool accepts `detail: str = "normal"` with three levels:

| Level | Max Chars | Use Case |
|-------|-----------|----------|
| `brief` | 2,000 | Quick overview, dashboards, multi-tool workflows |
| `normal` | 8,000 | Standard single-tool investigation |
| `full` | Unlimited | Deep dive when user explicitly needs everything |

### Input Validation

`apply_output_limit()` must validate the `detail` parameter. Unknown values (e.g., `"verbose"`, `"minimal"`, typos) normalize to `"normal"` to prevent silent bypass of limits. Currently, unknown values fall through to `None` (no limit), which is a security hole.

### Output Limiting Strategy

Two complementary functions in `output_limit.py`:

1. **`apply_output_limit(result, detail, list_keys)`** — Existing. Handles dict results by truncating list values. Used by most tools. **Important limitation:** only truncates Python `list` values — large string values in dicts are not truncated. For tools that may return large strings (e.g., `file_ops` with `op="read"`), the string must be truncated before calling `apply_output_limit()`.

2. **`truncate_output(text, detail, tool_name)`** — New. Safety net for raw string output. Uses same limits as `apply_output_limit()` (brief=2000, normal=8000, full=unlimited/None). No current callers (all tools return dicts), but added for future-proofing and as a general safety net per spec requirement.

### Bridge Tool Filtering Location

For bridge tools that proxy to Godot runtime: `apply_output_limit()` is called in the `__init__.py` wrapper function **after** receiving the response from the sub-module. Sub-modules are not modified — they continue to return full data. This minimizes file changes and centralizes truncation logic.

### Docstring Convention

Each tool's docstring gets a one-line mention:
```
detail: Output verbosity — "brief" (summary), "normal" (default), or "full" (everything).
```

## Implementation Plan

### Section 1: Enhance output_limit.py

**File:** `src/godotiq/tools/output_limit.py`

**Changes:**

1. Add `truncate_output(text: str, detail: str, tool_name: str = "") -> str` function:
   - Check text length against same limits as `apply_output_limit()`: brief=2000, normal=8000, full=None (unlimited)
   - If within limit, return unchanged
   - If over limit: truncate to `max_chars - 200`, count remaining lines, append truncation notice
   - Notice format: `"\n\n... truncated ({N} more lines). Use detail='full' for complete output."`

2. Add input validation to `apply_output_limit()`:
   - If `detail` is not one of `{"brief", "normal", "full"}`, normalize to `"normal"`
   - Log a warning when normalization occurs

### Section 2: Add detail to Assets Tools

**File:** `src/godotiq/tools/assets/__init__.py`

**`suggest_scale`** — Add `detail: str = "normal"` parameter. This tool returns a single scale recommendation dict (always small). The parameter exists for API consistency. For `brief`, return only the recommended scale value and confidence. For `normal`/`full`, return current behavior. Apply `apply_output_limit()` at return.

### Section 3: Rename impact_check Parameter + Add detail to Code Tools

**File:** `src/godotiq/tools/code/__init__.py`

**`impact_check`** — This tool currently has `detail: str = ""` used as a free-text description of the intended change (not output verbosity).

**Breaking change:** The MCP tool schema changes. LLM agents read schemas each session so this is safe, but other MCP clients that cache schemas will need to update.

Changes needed:

1. Rename `detail` parameter to `change_description` in the function signature
2. Update all internal references to the renamed parameter, including:
   - The returned result dict field `"detail"` → `"change_description"`
   - String interpolation in `safe_alternative` message that uses the old `detail` variable
3. Add new `detail: str = "normal"` parameter for output verbosity
4. Apply `apply_output_limit()` on the result dict with `list_keys=["callers", "signal_impact"]`
   - **Note:** This is a behavioral change — the result dict may now include `output_truncated`, `truncation_notice`, and `callers_total` fields that were not present before

The docstring must clearly distinguish the two parameters:
- `change_description`: Free-text description of the intended change (for context)
- `detail`: Output verbosity level (brief/normal/full)

### Section 4: Add detail to Spatial Tools

**File:** `src/godotiq/tools/spatial/__init__.py`

**`placement`** — Add `detail: str = "normal"` parameter. This tool returns placement suggestions. For `brief`: return only top suggestion (position + confidence). For `normal`: top 3 + excluded zones. For `full`: all candidates with scoring detail. Apply `apply_output_limit()` at return.

### Section 5: Add detail to Bridge Tools (Runtime Communication)

**Files:**
- `src/godotiq/tools/bridge/__init__.py` (wrapper functions — where `apply_output_limit()` is applied)
- Sub-modules are NOT modified (they return full data)

All bridge tools communicate with Godot via WebSocket. Detail filtering happens **Python-side** in the `__init__.py` wrapper after receiving the sub-module response.

#### High-output-risk bridge tools (need real filtering):

**`file_ops`** — Can return large file lists from search/list operations, and large file content from read operations. Add `detail` parameter. Apply `apply_output_limit()` on result lists. **Special case:** For `op="read"`, the result contains a `"content"` string field. If this string exceeds the char limit, truncate it to the limit before returning (since `apply_output_limit()` only truncates lists, not strings).

**`node_ops`** — Can batch many operations with results. Add `detail` parameter. Apply `apply_output_limit()` on the result — the implementer must inspect the actual Godot response structure to determine the correct `list_keys`.

**`editor_context`** — Can return full editor state with many open scenes. Add `detail`. Apply `apply_output_limit()` with auto-detected list keys.

**`input`** — Side-effect tracking can grow. Add `detail`. Apply `apply_output_limit()` on result.

**`watch`** — Variable watch state. Add `detail`. Apply `apply_output_limit()` on result.

**`undo_history`** — Deep undo stacks. Add `detail`. Apply `apply_output_limit()` on result.

#### Small/fixed-output bridge tools (no-op detail):

These tools have bounded, small output. Add `detail: str = "normal"` parameter for API consistency, but behavior doesn't change across levels:

- **`screenshot`** — Returns binary image data. Detail is irrelevant.
- **`run`** — Returns play/stop confirmation string.
- **`save_scene`** — Returns save confirmation.
- **`camera`** — Returns camera position/target.
- **`script_ops`** — Returns single file operation result.

For these, add the parameter to the signature and docstring, but don't add any filtering logic.

#### Trivial server tool:

**`ping`** in `server.py` — Add `detail: str = "normal"` parameter for API consistency. No filtering logic needed. Returns a simple dict.

### Section 6: Verify and Tune Existing Detail Implementations

Review all 17 tools that already have `detail` to ensure they respect the character limits. Key tools to verify:

**`asset_registry`** — Already has detail + path_filter + max_results. Verify brief stays under 2000 chars, normal under 8000. Current behavior: brief=counts, normal=top 20 unused, full=all. May need to reduce normal output if it exceeds 8000 on large projects.

**`scene_map`** — Already has detail with node caps (brief=10, normal=20, full=30). Verify these caps produce output within limits. **Keep the full=30 cap** — removing it would cause O(n^2) distance matrix computation to explode on large scenes. Users who need more data should use filters (focus, radius, include) for targeted queries.

**`scene_tree`** — Bridge tool, already has detail with brief=max 20 nodes. Verify limits.

**`ui_map`** — Bridge tool, has implicit detail support. Verify limits.

**`spatial_audit`** — Filters by severity per detail level. Verify output size.

**`signal_map`** — Caps at 30 signals. Verify limits.

For each: if the existing implementation can produce output exceeding the limit for its detail level, adjust the truncation parameters (max items, field selection) until it fits.

### Section 7: Tests

**New test files** (do not modify existing tests):

#### Per-tool detail tests
For each modified tool, add tests verifying:
1. `detail="brief"` produces output under 3,000 chars (buffer accounts for metadata overhead from `apply_output_limit()`)
2. `detail="normal"` produces output under 10,000 chars
3. Default call (no detail param) respects normal limits
4. `detail="full"` produces output matching previous behavior

#### Integration token budget test
A test that calls multiple tools with brief/normal and verifies total output stays under 30,000 chars. Uses existing fixtures from `tests/fixtures/`.

#### impact_check regression test
Verify that the renamed `change_description` parameter still works correctly:
- The returned dict contains `"change_description"` field (not `"detail"`)
- The `safe_alternative` message correctly uses the change description
- The new `detail` parameter controls output size

#### truncate_output unit tests
Test the new `truncate_output()` function:
- Text under limit returns unchanged
- Text over limit is truncated with notice
- Line count in notice is accurate
- All three detail levels have correct limits
- Unknown detail values normalize to "normal"

#### Input validation tests
Test that `apply_output_limit()` normalizes unknown detail values to "normal".

## File Modification Summary

| File | Changes |
|------|---------|
| `src/godotiq/tools/output_limit.py` | Add `truncate_output()`, add input validation |
| `src/godotiq/tools/assets/__init__.py` | Add detail to `suggest_scale` |
| `src/godotiq/tools/code/__init__.py` | Rename `impact_check.detail` → `change_description`, add proper `detail` |
| `src/godotiq/tools/spatial/__init__.py` | Add detail to `placement` |
| `src/godotiq/tools/bridge/__init__.py` | Add detail to 13 bridge tool wrappers, apply `apply_output_limit()` |
| `src/godotiq/server.py` | Add detail to `ping` |
| `src/godotiq/tools/animation/__init__.py` | Verify existing detail limits |
| `src/godotiq/tools/memory/__init__.py` | Verify existing detail limits |
| `src/godotiq/tools/flow/__init__.py` | Verify existing detail limits |
| `tests/test_tools/test_detail_levels.py` | New: per-tool detail tests |
| `tests/test_tools/test_truncate_output.py` | New: truncate_output unit tests |
| `tests/test_tools/test_token_budget.py` | New: integration token budget test |

**Note:** Bridge tool sub-modules (`file_ops.py`, `node_ops.py`, `editor_context.py`, etc.) are NOT modified. Filtering happens in the `__init__.py` wrapper.

## Files NOT to Touch

- `godot-addon/` — No GDScript changes (Python-side filtering only)
- `.mcp.json`, `.godotiq.json` — Configuration files
- Existing test files — Only add new test files

## Risks and Mitigations

**Risk:** Renaming `impact_check.detail` is a breaking MCP schema change.
**Mitigation:** LLM agents read tool schemas each session — no cached state. Other MCP clients are unlikely given the project's maturity stage. The rename is acknowledged as a breaking change.

**Risk:** Adding `apply_output_limit()` to `impact_check` adds new fields to output dict.
**Mitigation:** The new fields (`output_truncated`, `truncation_notice`) are informational metadata. They don't break consumers that ignore unknown fields.

**Risk:** Tightening limits on existing tools could change behavior for users relying on current output.
**Mitigation:** `detail="full"` preserves unlimited behavior. Default "normal" is already the existing default, so only extreme cases are affected.

**Risk:** Bridge tools may have varying response structures that complicate generic truncation.
**Mitigation:** `apply_output_limit()` supports auto-detected list keys. For `file_ops` read operations with large string values, explicit string truncation is applied before `apply_output_limit()`.

**Risk:** `scene_map` full mode limited to 30 nodes.
**Mitigation:** Intentional — prevents O(n^2) distance matrix explosion. Users use filters (focus, radius) for targeted queries.

## Success Criteria

1. `pytest tests/ -v` — All existing tests pass (zero regressions)
2. Every tool accepts `detail` parameter (32/32)
3. Brief output ≤ 2,000 chars for all tools
4. Normal output ≤ 8,000 chars for all tools
5. Token budget test passes (combined workflow < 30,000 chars)
6. `impact_check` parameter rename is clean (no references to old `detail` usage)
7. Unknown `detail` values are safely normalized to "normal"
