# Agent-Facing Tools

pi-lens registers the following tools with the pi agent. Most are also exposed
through the MCP mirror (`clients/lens-engine.ts` is the seam both adapters
share) — current exception: `ast_grep_outline`
(module_report supersedes them for discovery), and `lens_diagnostic_mark`
(pi-lens-internal for now). `read_enclosing` gained MCP parity
(`pilens_read_enclosing`) as of #536, closing #522 item 1. The standalone
`lsp_diagnostics` tool was folded into `lens_diagnostics` (`source=lsp`,
#2860). The retired MCP name remains a one-release compatibility redirect to
`pilens_diagnostics` with `source=lsp` and `scope=paths` (`mcp/server.ts`),
logging one `lsp-diagnostics-compatibility` degradation per session. Callers
should move to `pilens_diagnostics`.


**Dynamic tooling.** Five tools stay always-active: `lens_diagnostics`,
	`module_report`, `read_symbol`, `read_enclosing`,
`symbol_search`. Five situational tools — `ast_grep_search`, `ast_grep_replace`,
`ast_grep_outline`, `lsp_navigation`, `lens_diagnostic_mark` —
are registered but
inactive by default; the model activates the ones it needs via the always-active
loader tool `pi_lens_activate_tools`, per pi's dynamic-tool-loading API
(`pi.setActiveTools`/`pi.getActiveTools`). The loader explicitly reports
"Available starting next turn"; do not retry the tool in the same turn.
Feature-detected: on hosts without that API, the five situational tools fall back
to being statically active, exactly as before (`tools/activate-tools.ts`, wired
in `index.ts`).

Tool descriptions contain the contract sentence and one example. Operational
guidance, including cache state, scan scope, safety details, and lifecycle
results, belongs in the returned result so it is paid only when the tool runs.

**Result contract.** One post-result gate per surface — `finalizeToolResult` /
`finalizeToolResultWithDelivery` in `tools/render-compact.ts`, wired into every
pi tool's `execute` wrapper in `index.ts` and into the MCP `tools/call`
dispatcher in `mcp/server.ts` — bounds every delivered result to 40 KiB
(`MAX_RESULT_BYTES`) and stamps a trailing usage footer describing what was
actually sent: `usage tokens=<n> elapsed-ms=<n> bytes=<n> truncated=<true|false>`.
A rejected call (e.g. MCP's "Unknown or disabled tool") is rendered through the
same gate rather than bypassing it.

**MCP argument handling (#3749).** The MCP `tools/call` dispatcher compares the
call's argument keys with the `inputSchema` the tool advertises in `tools/list`
(`findIgnoredArguments`, `mcp/tool-arguments.ts`), once, for every tool. An
undeclared top-level key is never silently dropped (keys inside a nested
object argument such as `callHierarchyItem`, `rule` or `flags` are not checked):

- the result's first line is ``Ignored unknown argument(s) for <tool>: `key`
  (did you mean `nearest`?). They had no effect on this call.`` (the suggestion
  appears only when a declared key is near), followed by the tool's own result;
- the result carries `structuredContent: { ignoredArguments: [...],
  ignoredArgumentCount: N }` (at most 8 keys, each cut at 64 characters; the
  count is exact);
- when an ignored key leaves the call without something it needed, the tool
  does not run and the result is an error whose first line is the same
  sentence, followed by `Not run: ...`. That is the case when a
  schema-`required` input is missing (``Not run: required argument(s) `file`
  missing.``), or when the ignored key is a declared parameter the call did not
  send, written another way: a case or punctuation variant, a plural, or a key
  whose last word is the parameter (`filePath`, `file_path` and `Path` for the
  optional `path` of `pilens_diagnostics`: ``Not run: `filePath` looks like a
  mistyped `path`, which was not sent.``). The exact predicate is named in
  [public-api-stability.md](public-api-stability.md). Anything looser (a typo,
  an abbreviation, `files` for `maxLspFiles`, or a count, flag or output key
  such as `maxFiles`, `includeFiles`, `outFile` or `cwdPath` where `cwd` is
  declared) stays a warning;
- each such call adds one count to the `mcp-ignored-arguments` degradation
  group (subject: the tool name), visible in `pilens_health`.

Unknown keys are still not rejected outright: a caller that passes an extra key
that is not a declared parameter written another way (or that comes with the
parameter it resembles) keeps getting its answer, with the report above. Hard rejection is a change to the
public MCP input contract and is owned by the stability policy
([public-api-stability.md](public-api-stability.md)). A schema declares only
what the tool reads, so `cwd` on a tool whose handler never reads it
(`pilens_health`, `pilens_latency`, `pilens_session_end`, `pilens_rebuild`;
the dispatcher uses it only to decide whether the tool is enabled) is reported
as an ignored key, a warning, because nothing it resembles is missing. The
retired `pilens_lsp_diagnostics` name is checked against `pilens_diagnostics`'s
schema; the retired `pilens_ast_grep_dump` name has no schema and is not
checked. A non-object `arguments` is a JSON-RPC `-32602` error; absent, `null`
and `[]` mean no arguments.

## Per-edit

- **`lens_diagnostics`** — Session-cache or LSP-probe diagnostic state, selected
  by `source` (`session` default, or `lsp`) and `scope` (`paths` or
  `workspace`; explicit `paths` always win over `scope`). `severity` is a
  threshold, not an exact filter: `error` shows only errors; `warning` adds
  warnings; `information` adds information; `hint`/`all` (default) show every
  tier. `source=lsp` reads a file's on-disk content and will not sync it to a
  language server past **2 MiB or 5000 lines** (`clients/runtime-config.ts`
  `pipeline.lspMaxFileBytes`/`lspMaxFileLines`, checked in
  `clients/lsp/content-limits.ts`) — not env-tunable today. A file over that
  bound gets an explicit `file too large for LSP diagnostics (<N> bytes/lines
  > <limit>)` result instead of stale or partial diagnostics. This is a
  *different* 2 MiB from the auto-fix full-content attachment cap described in
  [agent-guide.md](agent-guide.md), section 6 ("Auto-format / auto-fix
  timing") — the two caps currently share one constant
  (`RUNTIME_CONFIG.pipeline.lspMaxFileBytes`) but gate unrelated behaviors
  (LSP sync vs. tool-result content attachment) and can diverge independently
  in the future. Separately, every save-triggered edit now sends the server a
  `textDocument/didSave` when it declared `textDocumentSync.save` — with the
  file text when the server asked for it (`includeText`) and the file is
  within the same bound, otherwise the save is still sent but without `text`
  (never dropped outright); this is what makes save-triggered servers like
  Expert (Elixir) re-diagnose an edit made through pi-lens (refs #3405,
  #3408). Legacy `mode`: `delta` (current turn), `all` (resurfaces stale
  blockers dropped from turn context), `full` (project-wide scan).
- **`lens_diagnostic_mark`** — Triage a diagnostic: `false-positive` /
  `suppress` (writes an inline `pi-lens-ignore` comment) / `defer`
  (session-only) / `flagged` (persists, rendered `📌 flagged-to-fix`).
  Content-anchored so marks survive edits; every mark is logged and published
  on the bus. See [dispositions.md](dispositions.md).
- **`lsp_navigation`** — IDE-style navigation, 19 operations: `definition`,
  `typeDefinition`, `declaration`, `references`, `hover`, `signatureHelp`,
  `documentSymbol`, `findSymbol`, `workspaceSymbol`, `codeAction`, `rename`,
  `rename_file`, `implementation`, `prepareCallHierarchy`, `incomingCalls`,
  `outgoingCalls`, `executeCommand`, `workspaceDiagnostics`, and `capabilities`
  (`tools/lsp-navigation.ts` operation description). Position-based operations
  accept a `path`/`line`/`character` triple. `documentSymbol` accepts a `kinds`
  filter (e.g. `function`, `class`) and a `maxResults` cap (default 20, max 100)
  to keep large files bounded. Full per-operation parameter reference:
  [skills/pi-lens-lsp-navigation/SKILL.md](../skills/pi-lens-lsp-navigation/SKILL.md).
- **`ast_grep_search`** — AST-aware structural search across ~40 languages via
  the `sg` CLI. Supports metavariables (`$VAR`, `$$$ARGS`), `strictness`
  modes (`smart`, `relaxed`, `ast`, `cst`, `signature`, `template`), structural
  constraints (`insideKind`, `hasKind`, `hasDescendantKind`, `follows`,
  `precedes`), raw YAML `rule` passthrough, `validateOnly` for compile/shape
  checks without scanning project files, and pagination via `skip` / `maxMatches`
  (per-call cap, default 50, max 200; also sets the pagination step).
  `nodeKind` is an expert grammar-specific escape hatch: it finds every node of
  the exact kind used by the target grammar. Node kinds are not universal across
  languages; use `dump=true` with the representative snippet in `pattern` to discover the kind in the target language. It is mutually exclusive with `rule`.
  `hasKind` retains ast-grep's immediate-child semantics; use
  `hasDescendantKind` for an explicit recursive descendant search. A future
  canonical `find`/`query` facade (call/function/import/etc.) should map to
  this same synthesized YAML path, with per-language templates and a clear
  unsupported-concept error — it is intentionally not a second search engine
  in this patch.
  `groupByFile: true` renders a compact one-line-per-file distribution
  (`L<line>:<col>` locations) instead of each match body — for high-volume
  searches. `pattern` is optional when a `rule` or `nodeKind` is given.
  Results include `details.matchLocations[]` — each hit carries a ready
  `readSlice` (`path`/`offset`/`limit`) for a bounded context read; zero-match
  results include a `suggestedDump` hint pointing at `ast_grep_search` with `dump=true`.
- **`ast_grep_replace`** — AST-aware structural replace. Re-validates the pattern
  against the current file before writing and reports a clear error if the
  file changed since the preview.
- **`ast_grep_search` with `dump=true`** — Dumps the raw tree-sitter AST for a source snippet. Use
  this when an `ast_grep_search` or `ast_grep_replace` pattern returns zero
  matches and the correct node kind or field name is unknown. `includeAnonymous`
  shows punctuation/CST nodes.
- **`ast_grep_outline`** — Syntax-only code structure (symbols, imports, exports,
  members) for files or directories via `ast-grep outline`. Fast, local, no
  index/LSP/cross-file semantics. Supports `items`/`view`/`type`/`match`/
  `pubMembers`/`globs`; returns per-file `items[]` (nested `members[]`) with
  ready `read` args. Prefer `module_report` for pi-lens-aware navigation; reach
  for this when the syntax tree's own view is enough or pi-lens's extractor is
  weak for a language.

## Project intelligence

- **`symbol_search`** — Ranked identifier search over the persisted word index
  (BM25 + priors demoting tests/vendor/docs, optional graph-centrality boost).
  Answers "which files are most relevant to `<query>`" by identifier — the
  first step of the discovery funnel: `symbol_search` finds candidates,
  `module_report` explains one, `read_symbol` reads its body. Complements
  `grep` (raw substrings) and `lsp_navigation` (exact references). Returns
  `available: false` with a retry hint when the index isn't built yet — it
  self-builds in the background and never blocks the call. See
  [docs/word-index.md](word-index.md) for how the index is built and kept
  warm.
- **`module_report`** — Navigable outline of a file: every symbol's name/kind/
  startLine/endLine/signature, exported vs internal split, class/interface
  member nesting, who-uses-this, fanout/complexity risk flags, and a
  `recommendedReads` top-3 ranked by usage + complexity. No per-symbol `read`
  block (#512) — `offset`/`limit` are pure derivations of `startLine`/`endLine`
  on the report's own `path`; to read a symbol call `read`/`read_symbol` with
  `offset=startLine, limit=endLine-startLine+1` on that path. Cross-file
  entries (`blastRadius.files[].read`, `usedBy[].file`) keep their own path.
  Each entry also carries a first-line `doc` summary (whitespace-collapsed,
  ~120 chars) extracted from an attached doc comment when the language uses
  tree-sitter's conventional `comment` node — JS/TS is the primary target;
  languages sharing that node shape (confirmed: Python, and any grammar whose
  comment nodes are plain preceding siblings) get it for free with no
  per-grammar query work. `exported` stays a boolean field only — it is NOT
  also repeated inside `flags` (#512); `flags` carries only non-derivable
  signals (`async`, `high fanout`, `high complexity`, `boundary wrapper`).
  Each entry carries a `decorators[]` array — the declaration's
  decorators/attributes/annotations (`@app.get("/x")`, `#[tokio::main]`,
  `@Override`) — so the agent reads a symbol's role (route/test/fixture/
  entrypoint) without opening the body. Also emits a `callbacks[]` section for
  high-signal inline executables (event handlers, timers, promise callbacks,
  object/dict function props, assigned closures) with stable synthetic
  handles and flags. The optional `focus` string re-ranks `recommendedReads`
  without expanding scope. `callbackSupport` (`tuned`/`generic`) reports
  whether language-specific callback rules applied — the callback *node
  kinds* are language-uniform, but the semantics are per-language (JS/TS-tuned
  by default, plus Go goroutine/defer, Python scheduler/future lambda, Rust
  spawn/move-closure, Swift weak/strong-self capture, C++ by-reference-
  capture, Kotlin coroutine-builder, Java thread/executor/listener, and C#
  Task.Run/event-`+=` slices); named symbols span all tree-sitter
  `SYMBOL_QUERIES` languages. Pass `view: "summary"` for a smaller orientation
  payload (top-level entries + recommendations, heavy callback/usedBy/
  blast-radius payloads omitted); pass `view: "compact"` for a line-oriented
  TEXT rendering of the full report (cheapest option — roughly a quarter of
  the JSON cost, same underlying data). Reports section-level `provenance`
  for syntax, cached-graph, and heuristic sections. Pass `blastRadius: true`
  for cross-file transitive dependents (read-only over the cached graph).
- **`read_symbol`** — One symbol's verbatim source body, by name or by a
  `module_report` callback handle. Returned body is recorded as genuine
  read-guard coverage for that symbol/callback's line range.
- **`read_enclosing`** — Maps a `path` + `line` (from `ast_grep_search`,
  diagnostics, or LSP locations) to the verbatim body of the smallest enclosing
  symbol or callback. Tree-sitter only — no LSP or graph build. Optional `kinds`
  filter and `maxLines` cap; records read-guard coverage for the returned range.
  When the body exceeds `maxLines`, `onOversize` controls the fallback: `error`
  (default) returns metadata only, `slice` returns a bounded partial read around
  the line (size via `aroundLine`, default `maxLines` then 80), and `outline`
  returns the nested symbols/callbacks with read handles.

## Session

- **`lens_health`** — Runtime health, latency telemetry, and current LSP
  status.
- **`lens_project_scan`** — Cheap project-wide scans (knip, jscpd, duplicates).
- **`lens_diagnostics mode=full`** — full project-wide review: LSP + the cached
  heavyweight analyzers (jscpd, madge, gitleaks, govulncheck, trivy, dead-code)
  folded in via `refreshRunners`.
