# Tools, LSP & DAP — What the Agent Can Touch

> The 32 built-in tools (read, write, bash, search, eval, fetch, gh…), the 13 LSP operations wired through workspace/willRenameFiles, and the 27 DAP operations that attach real debuggers. State ownership, caching boundaries, and what differs between tool categories.

- Repository: can1357/oh-my-pi
- GitHub: https://github.com/can1357/oh-my-pi
- Human wiki: https://grok-wiki.com/public/wiki/can1357-oh-my-pi-64b0ce1ccc45
- Complete Markdown: https://grok-wiki.com/public/wiki/can1357-oh-my-pi-64b0ce1ccc45/llms-full.txt

## Source Files

- `packages/coding-agent/src/tools/index.ts`
- `packages/coding-agent/src/tools/bash.ts`
- `packages/coding-agent/src/tools/read.ts`
- `packages/coding-agent/src/lsp/index.ts`
- `packages/coding-agent/src/lsp/client.ts`
- `packages/coding-agent/src/dap`
- `packages/coding-agent/src/tools/eval.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/coding-agent/src/tools/index.ts](packages/coding-agent/src/tools/index.ts)
- [packages/coding-agent/src/tools/bash.ts](packages/coding-agent/src/tools/bash.ts)
- [packages/coding-agent/src/tools/read.ts](packages/coding-agent/src/tools/read.ts)
- [packages/coding-agent/src/tools/eval.ts](packages/coding-agent/src/tools/eval.ts)
- [packages/coding-agent/src/lsp/index.ts](packages/coding-agent/src/lsp/index.ts)
- [packages/coding-agent/src/lsp/client.ts](packages/coding-agent/src/lsp/client.ts)
- [packages/coding-agent/src/dap/session.ts](packages/coding-agent/src/dap/session.ts)
- [packages/coding-agent/src/dap/types.ts](packages/coding-agent/src/dap/types.ts)
</details>

# Tools, LSP & DAP — What the Agent Can Touch

The coding agent operates through three distinct categories of capability: **built-in tools** that compose its core action vocabulary, **LSP operations** that wire live language server intelligence into file editing and navigation, and **DAP operations** that attach real debuggers to running processes. Understanding what falls into each category, who owns the state, and what settings gate each surface is essential for reasoning about what the agent can and cannot do in any given session.

This page covers the full inventory of built-in tools registered in `BUILTIN_TOOLS` and `HIDDEN_TOOLS`, the LSP action set surfaced through the `lsp` tool (including the `workspace/willRenameFiles` integration), and the complete DAP session API that `DapSessionManager` exposes. It also explains caching, concurrency, and settings-gate invariants that differ between the three categories.

---

## Built-in Tools

### Registry and Load Modes

All built-in tools are declared as factory functions in two maps in `packages/coding-agent/src/tools/index.ts`:

- **`BUILTIN_TOOLS`** — the primary, settings-gated map of 33 tool factories that are materialized for each session.
- **`HIDDEN_TOOLS`** — five additional tools (`yield`, `report_finding`, `report_tool_issue`, `resolve`, `goal`) that are injected conditionally and are never directly selectable by name in the tool list.

Sources: [packages/coding-agent/src/tools/index.ts:283-322]()

Every tool carries a `loadMode`: either `"essential"` or `"discoverable"`. The default essential set is `["read", "bash", "edit"]`, configurable via `tools.essentialOverride`. Discoverable tools such as `lsp` (`loadMode = "discoverable"`) are hidden from the model until either selected explicitly or surfaced through `search_tool_bm25`.

Sources: [packages/coding-agent/src/tools/index.ts:260-277](), [packages/coding-agent/src/lsp/index.ts:1178-1179]()

### Full Tool Inventory

| Name | Class / Factory | Concurrency | Settings Gate |
|------|----------------|-------------|---------------|
| `read` | `ReadTool` | — | always on |
| `bash` | `BashTool` | `exclusive` | always on |
| `edit` | `EditTool` | — | always on |
| `ast_grep` | `AstGrepTool` | — | `astGrep.enabled` |
| `ast_edit` | `AstEditTool` | — | `astEdit.enabled` |
| `render_mermaid` | `RenderMermaidTool` | — | `renderMermaid.enabled` |
| `ask` | `AskTool.createIf` | — | conditional |
| `debug` | `DebugTool.createIf` | — | `debug.enabled` |
| `eval` | `EvalTool` | — | either `eval.py` or `eval.js` |
| `calc` | `CalculatorTool` | — | `calc.enabled` |
| `ssh` | `loadSshTool` | — | conditional |
| `github` | `GithubTool.createIf` | — | `github.enabled` |
| `find` | `FindTool` | — | `find.enabled` |
| `search` | `SearchTool` | — | `search.enabled` |
| `lsp` | `LspTool.createIf` | — | `lsp.enabled` + `enableLsp` |
| `inspect_image` | `InspectImageTool` | — | `inspect_image.enabled` |
| `browser` | `BrowserTool` | — | `browser.enabled` |
| `checkpoint` | `CheckpointTool.createIf` | — | `checkpoint.enabled` |
| `rewind` | `RewindTool.createIf` | — | `checkpoint.enabled` |
| `task` | `TaskTool.create` | — | depth < `task.maxRecursionDepth` |
| `job` | `JobTool.createIf` | — | conditional |
| `recipe` | `RecipeTool.createIf` | — | `recipe.enabled` |
| `irc` | `IrcTool.createIf` | — | `irc.enabled` + async mode |
| `todo_write` | `TodoWriteTool` | — | `todo.enabled`, disabled when `yield` present |
| `web_search` | `WebSearchTool` | — | `web_search.enabled` |
| `search_tool_bm25` | `SearchToolBm25Tool.createIf` | — | `tools.discoveryMode` ≠ `off` |
| `write` | `WriteTool` | — | always on |
| `retain` | `HindsightRetainTool.createIf` | — | `memory.backend === "hindsight"` |
| `recall` | `HindsightRecallTool.createIf` | — | `memory.backend === "hindsight"` |
| `reflect` | `HindsightReflectTool.createIf` | — | `memory.backend === "hindsight"` |
| `yield` *(hidden)* | `YieldTool` | — | `requireYieldTool` flag |
| `resolve` *(hidden)* | `ResolveTool` | — | always injected |
| `goal` *(hidden)* | `GoalTool` | — | `goal.enabled` + active goal mode |
| `report_finding` *(hidden)* | `reportFindingTool` | — | always |
| `report_tool_issue` *(hidden)* | `createReportToolIssueTool` | — | autoQA enabled |

### The `bash` Tool — Concurrency and State

`BashTool` declares `concurrency = "exclusive"`, which means no other tool invocation can run in parallel with bash. This is the only built-in tool with this constraint.

When `async.enabled` is true, the schema gains an `async` field. With `bash.autoBackground.enabled`, jobs that exceed `bash.autoBackground.thresholdMs` (default 60 s) are automatically backgrounded and tracked by `AsyncJobManager`. The tool resolves `skill://`, `agent://`, and `local://` internal URLs in command strings before spawning the process.

Sources: [packages/coding-agent/src/tools/bash.ts:225-257]()

### The `eval` Tool — Dual-Backend and Kernel State

`EvalTool` dispatches across two backends: `"py"` (an IPython/Jupyter kernel) and `"js"` (a persistent V8 VM). State within each language persists across cells and across successive tool calls in the same session. The `reset: true` flag on a cell wipes only that cell's language kernel. The tool is enabled if *either* backend is reachable; Python availability is checked at startup only when JS is disabled.

Sources: [packages/coding-agent/src/tools/eval.ts:34-51](), [packages/coding-agent/src/tools/index.ts:383-402]()

### The `read` Tool — File Read Cache

`ReadTool` writes every file it reads into a per-session `FileReadCache` stored on `ToolSession.fileReadCache`. This cache is consumed by the `edit` tool's hashline anchor-stale recovery path to reconstruct the file version the model authored its edit anchors against, in case the file changed out-of-band between `read` and `edit`.

Sources: [packages/coding-agent/src/tools/index.ts:239-250]()

---

## LSP Operations

### Architecture

The `lsp` tool is a single `AgentTool` with an `action` discriminant field. It connects lazily to language servers defined in the project's LSP config, reusing long-lived `LspClient` processes across tool calls. A module-level `Map<string, LspClient>` in `lsp/client.ts` acts as the client registry; locks prevent concurrent initialization of the same server. An idle-check interval (default 60 s cadence) shuts down clients that have exceeded the configured idle timeout.

Sources: [packages/coding-agent/src/lsp/client.ts:20-54]()

Config is cached per `cwd` in a `Map<string, LspConfig>` inside `lsp/index.ts` to avoid repeated file I/O on every tool call. The cache is populated on first use and reused for the session lifetime.

Sources: [packages/coding-agent/src/lsp/index.ts:226-236]()

### The 14 LSP Actions

| Action | LSP Method(s) | Scope | Notes |
|--------|--------------|-------|-------|
| `status` | — | workspace | Reports configured servers and lspmux state |
| `diagnostics` | `textDocument/publishDiagnostics`, linter clients | file or `*` | `*` runs workspace-level compile tool (cargo/tsc/go build/pyright) |
| `definition` | `textDocument/definition` | file + position | Normalizes `Location` and `LocationLink` |
| `type_definition` | `textDocument/typeDefinition` | file + position | Same normalization |
| `implementation` | `textDocument/implementation` | file + position | Same normalization |
| `references` | `textDocument/references` | file + position | Retries up to 2× with delay for project-aware servers |
| `hover` | `textDocument/hover` | file + position | Returns markdown or plaintext |
| `code_actions` | `textDocument/codeAction`, `codeAction/resolve`, `workspace/executeCommand` | file + position | `apply=true` applies the selected action |
| `symbols` | `textDocument/documentSymbol` / `workspace/symbol` | file or `*` + query | Workspace symbols deduped and limited to 200 |
| `rename` | `textDocument/rename` | file + position | `apply=false` returns a preview |
| `rename_file` | `workspace/willRenameFiles` + `workspace/didRenameFiles` | path pair | See below |
| `reload` | `rust-analyzer/reloadWorkspace` or `workspace/didChangeConfiguration`, then kill | server | Tries reload methods in order |
| `capabilities` | — | server | Returns raw `serverCapabilities` JSON |
| `request` | any method via `query` | file or server | Escape hatch for server-specific methods |

Sources: [packages/coding-agent/src/lsp/index.ts:1196-2281]()

### `rename_file` and `workspace/willRenameFiles`

This action is the most complex LSP operation. It:

1. Enumerates `{oldUri, newUri}` pairs (up to `MAX_RENAME_PAIRS = 1000`) by walking directory contents recursively if the source is a directory.
2. Sends `workspace/willRenameFiles` to every configured LSP server in parallel to collect `WorkspaceEdit` responses — the server returns the import-path updates it would make.
3. Coalesces per-URI text edits across servers: project-aware servers take priority; overlapping edits from lower-priority servers are discarded with a warning.
4. Applies the coalesced text edits atomically from a single pre-rename snapshot (not re-reading after each server), then performs the filesystem rename.
5. Sends `workspace/didRenameFiles` and `textDocument/didClose` notifications to all servers.

The `apply=false` flag returns a preview of what edits would be made without touching the filesystem.

Sources: [packages/coding-agent/src/lsp/index.ts:1380-1646]()

### LSP Writethrough: How `write`/`edit` Hook Into LSP

When `createLspWritethrough` is used (called by `WriteTool` and `EditTool`), every file write goes through a pipeline:

```text
write(content)
  → syncFileContent()      # didOpen or didChange to all LSP servers
  → formatContent()        # textDocument/formatting (in-memory)
  → write to disk
  → notifyFileSaved()      # didSave
  → waitForDiagnostics()   # poll up to 3s for fresh publishDiagnostics
```

If the entire pipeline exceeds 5 seconds, it times out, writes the file anyway, and optionally schedules a deferred diagnostics fetch (up to 25 s) that fires a callback when results arrive.

Sources: [packages/coding-agent/src/lsp/index.ts:984-1101]()

---

## DAP Operations

### Session Model

`DapSessionManager` maintains a `Map<string, DapSession>` where each session holds a live `DapClient` (a subprocess running a debug adapter), breakpoint state, output buffer, and a `DapSessionStatus` state machine. There is at most one *active* session at a time, selected by `#activeSessionId`. Sessions that exceed `IDLE_TIMEOUT_MS` (10 minutes) are garbage-collected by a cleanup loop running every 30 seconds.

Sources: [packages/coding-agent/src/dap/session.ts:66-100](), [packages/coding-agent/src/dap/session.ts:193-211]()

```text
DapSession state machine:

  launching → configuring → stopped ⇄ running
                                 ↓
                            terminated
```

The `configuring` state exists because DAP adapters often delay their `launch` response until after `configurationDone` is sent. The manager subscribes to `stopped` events *before* sending `launch` or `attach` to avoid missing `stopOnEntry` events.

Sources: [packages/coding-agent/src/dap/session.ts:238-279]()

### The 27 DAP Operations

#### Session Lifecycle
| Method | DAP Command(s) | Notes |
|--------|---------------|-------|
| `launch` | `initialize`, `launch`, `configurationDone` | 30 s default timeout; captures initial stop |
| `attach` | `initialize`, `attach`, `configurationDone` | Accepts `pid`, `port`, `host` |
| `terminate` | `terminate` + `disconnect` | Graceful then forced; cleans up session |

#### Breakpoints
| Method | DAP Command | State Owned |
|--------|------------|-------------|
| `setBreakpoint` | `setBreakpoints` | `session.breakpoints: Map<path, DapBreakpointRecord[]>` |
| `removeBreakpoint` | `setBreakpoints` (updated list) | same |
| `setFunctionBreakpoint` | `setFunctionBreakpoints` | `session.functionBreakpoints` |
| `removeFunctionBreakpoint` | `setFunctionBreakpoints` | same |
| `setInstructionBreakpoint` | `setInstructionBreakpoints` | `session.instructionBreakpoints` |
| `removeInstructionBreakpoint` | `setInstructionBreakpoints` | same |
| `dataBreakpointInfo` | `dataBreakpointInfo` | read-only query |
| `setDataBreakpoint` | `setDataBreakpoints` | `session.dataBreakpoints` |
| `removeDataBreakpoint` | `setDataBreakpoints` | same |

Breakpoint mutations always re-send the *complete* list for the affected file or category (not a delta), which is the correct DAP contract.

Sources: [packages/coding-agent/src/dap/session.ts:337-575]()

#### Execution Control
| Method | DAP Command | Notes |
|--------|------------|-------|
| `continue` | `continue` | Resets `session.stop` and `lastStackFrames` before sending |
| `pause` | `pause` | Waits for `stopped` event |
| `stepIn` | `stepIn` | |
| `stepOut` | `stepOut` | |
| `stepOver` | `next` | |

Sources: [packages/coding-agent/src/dap/session.ts:704-754]()

#### Inspection
| Method | DAP Command | Caches Result |
|--------|------------|--------------|
| `threads` | `threads` | Updates `session.threads` |
| `stackTrace` | `stackTrace` | Updates `session.lastStackFrames` and top-frame stop location |
| `scopes` | `scopes` | Falls back to `session.stop.frameId` if no `frameId` passed |
| `variables` | `variables` | No session cache |
| `evaluate` | `evaluate` | Defaults to top stopped frame |
| `getOutput` | — | Returns in-memory `session.output` buffer (up to 128 KB) |

Sources: [packages/coding-agent/src/dap/session.ts:756-863]()

#### Low-Level / Advanced
| Method | DAP Command | Notes |
|--------|------------|-------|
| `disassemble` | `disassemble` | Returns `DapDisassembledInstruction[]` |
| `readMemory` | `readMemory` | Returns base64 data |
| `writeMemory` | `writeMemory` | Accepts base64 data |
| `modules` | `modules` | Paginated via `startModule`/`moduleCount` |
| `loadedSources` | `loadedSources` | Returns all currently loaded source files |
| `customRequest` | any command | Escape hatch for adapter-specific methods |

Sources: [packages/coding-agent/src/dap/session.ts:577-702]()

### Output Buffer Ownership

Program stdout/stderr captured during debugging is appended to `session.output` (capped at `MAX_OUTPUT_BYTES = 128 KB`). When the buffer overflows, oldest bytes are sliced from the front and `session.outputTruncated` is set. `getOutput(limitBytes?)` retrieves a suffix of the buffer without clearing it — output is *not* consumed on read.

Sources: [packages/coding-agent/src/dap/session.ts:97-100](), [packages/coding-agent/src/dap/session.ts:147-155](), [packages/coding-agent/src/dap/session.ts:851-863]()

---

## State Ownership and Caching Boundaries

```text
┌─────────────────────────────────────────────────────────────┐
│ ToolSession (per agent session)                             │
│                                                             │
│  fileReadCache ← populated by ReadTool                      │
│  conflictHistory ← populated by ReadTool (conflict:// URIs) │
│  skills, settings, cwd                                      │
└───────────────┬────────────────────────────────────────────┘
                │ passed to all tool factories
        ┌───────┼────────────────────────┐
        ▼                ▼               ▼
 ┌──────────┐   ┌──────────────┐  ┌──────────────────┐
 │ Bash     │   │ LSP subsystem│  │ DAP subsystem    │
 │ (no      │   │              │  │                  │
 │ session  │   │ module-level │  │ DapSessionManager│
 │ state)   │   │ Map<cwd,     │  │ (singleton per   │
 │          │   │ LspClient>   │  │ process)         │
 └──────────┘   │ Map<cwd,     │  │ Map<id, DapSess> │
                │ LspConfig>   │  │ active session   │
                └──────────────┘  └──────────────────┘
```

- **Bash** is stateless between calls: no in-process state survives a tool call boundary (env vars set with `env:` do not persist to the next invocation unless the session itself carries them).
- **LSP** state (open files, diagnostics, server capabilities) lives in module-level singleton maps, shared across all sessions in the same process. File content synced via `syncContent` updates the server's in-memory view; the on-disk write happens separately.
- **DAP** state is owned by `DapSessionManager`, also a process-level singleton. Breakpoints, output, and stop location are maintained per session object and survive across multiple tool calls until the session is terminated or idle-collected.
- **Eval** kernel state (Python variables, JS globals) is per-session within the kernel subprocess and persists across eval tool calls until `reset: true` is passed or the session is torn down.

---

## What Differs Between Tool Categories

| Dimension | Built-in Tools | LSP Tool | DAP (via debug tool) |
|-----------|---------------|----------|---------------------|
| State lifetime | Mostly per-call (except `eval`, `read` cache) | Process-level (shared across sessions) | Session until idle timeout (10 min) |
| Settings gate | Per-tool booleans | `lsp.enabled` + `enableLsp` | `debug.enabled` |
| Concurrency | `bash` is `exclusive`; rest parallel | Parallel per file, locked per server init | Single active session |
| Caching | `fileReadCache` on `ToolSession` | `LspConfig` per `cwd`, `LspClient` per server | Output buffer in `DapSession` |
| Timeout | Per-tool (bash: 1–3600 s, lsp: clamped) | Per-operation, 5 s writethrough wall clock | Per-request (default 30 s) |
| Mutation scope | Filesystem | Filesystem + LSP server in-memory state | Debuggee process memory and execution |

The `resolve` hidden tool is always injected regardless of the requested tool list, functioning as the control surface for plan-mode and queue-directed invocations. It is the only tool whose presence is enforced unconditionally.

Sources: [packages/coding-agent/src/tools/index.ts:505-511]()
