From 45ee5711fa8d6c098a08eef73ee17830776025a7 Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Mon, 23 Feb 2026 10:31:12 +0100 Subject: [PATCH 1/3] Added NES RFD --- docs/rfds/next-edit-suggestions.mdx | 490 ++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 docs/rfds/next-edit-suggestions.mdx diff --git a/docs/rfds/next-edit-suggestions.mdx b/docs/rfds/next-edit-suggestions.mdx new file mode 100644 index 00000000..8f2d6bbf --- /dev/null +++ b/docs/rfds/next-edit-suggestions.mdx @@ -0,0 +1,490 @@ +--- +title: "Next Edit Suggestions" +--- + +Author(s): [@anna239](https://github.com/anna239) + +## Elevator pitch + +> What are you proposing to change? + +Add a **Next Edit Suggestion (NES)** capability to ACP, allowing agents to provide predictive code edits. The protocol is designed around **capability negotiation**: agents declare what events and context they can consume, and clients provide only what was requested. + +## Status quo + +> How do things work today and what problems does this cause? Why would we change things? + +ACP currently has no mechanism for agents to provide inline edit predictions. Each client–agent pair implements NES through proprietary protocols. + +## What we propose to do about it + +> What are you proposing to improve the situation? + +Introduce a `nes` capability that agents advertise during initialization. The capability declares: + +- **Events** the agent wants to receive (file lifecycle notifications). +- **Context** the agent wants attached to each suggestion request. + +The client inspects these declarations and provides only what was requested, minimizing overhead for simple agents while allowing rich context for advanced ones. + +### Capability advertisement + +During `initialize`, the agent includes a `nes` field in its capabilities: + +```json +{ + "capabilities": { + "nes": { + "events": { + "didOpen": {}, + "didChange": { + "syncKind": "incremental" + }, + "didClose": {}, + "didSave": {}, + "didFocus": {} + }, + "context": { + "recentFiles": { + "maxCount": 10 + }, + "relatedSnippets": {}, + "editHistory": { + "maxCount": 6 + }, + "userActions": { + "maxCount": 16 + }, + "openFiles": {}, + "diagnostics": {} + } + } + } +} +``` + +All fields under `events` and `context` are optional — an agent advertises only what it can use. + +### Session lifecycle + +If the `nes` capability is present, the client may call `nes/start` to begin an NES session. The agent can also use the existing `configOptions` mechanism to expose NES-related settings (model selection, debounce preferences, enabled/disabled state, etc.). + +## Implementation details and plan + +> Tell me more about your implementation. What is your detailed implementation plan? + +### Events + +Events are fire-and-forget notifications from client to agent. The client sends them only if the corresponding key is present in `nes.events`. + +#### `nes/didOpen` + +Sent when a file is opened in the editor. + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didOpen", + "params": { + "uri": "file:///path/to/file.rs", + "languageId": "rust", + "version": 1, + "text": "fn main() {\n println!(\"hello\");\n}\n" + } +} +``` + +#### `nes/didChange` + +Sent when a file is edited. Supports two sync modes declared by the agent: + +- `"full"` — client sends entire file content each time. +- `"incremental"` — client sends only the changed ranges. + +**Incremental:** + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didChange", + "params": { + "uri": "file:///path/to/file.rs", + "version": 2, + "contentChanges": [ + { + "range": { + "start": { "line": 1, "character": 4 }, + "end": { "line": 1, "character": 4 } + }, + "text": "let x = 42;\n " + } + ] + } +} +``` + +**Full:** + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didChange", + "params": { + "uri": "file:///path/to/file.rs", + "version": 2, + "contentChanges": [ + { + "text": "fn main() {\n let x = 42;\n println!(\"hello\");\n}\n" + } + ] + } +} +``` + +#### `nes/didClose` + +Sent when a file is closed. + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didClose", + "params": { + "uri": "file:///path/to/file.rs" + } +} +``` + +#### `nes/didSave` + +Sent when a file is saved. + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didSave", + "params": { + "uri": "file:///path/to/file.rs" + } +} +``` + +#### `nes/didFocus` + +Sent when a file becomes the active editor tab. Unlike `nes/didOpen` (which fires once when a file is first opened), `nes/didFocus` fires every time the user switches to a file, including files that are already open. This is the primary trigger for agents that need to refresh context on tab switch (e.g. re-indexing relevant code snippets). + +```json +{ + "jsonrpc": "2.0", + "method": "nes/didFocus", + "params": { + "uri": "file:///path/to/file.rs", + "version": 2, + "position": { "line": 5, "character": 12 }, + "visibleRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 45, "character": 0 } + } + } +} +``` + +The `position` is the current cursor position. The `visibleRange` is the portion of the file currently visible in the editor viewport. + +### Suggestion request + +The client requests a suggestion by calling `nes/suggest`. Context fields are included only if the agent declared interest in the corresponding `nes.context` key. + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "method": "nes/suggest", + "params": { + "uri": "file:///path/to/file.rs", + "version": 2, + "position": { "line": 5, "character": 12 }, + "selection": { + "start": { "line": 5, "character": 4 }, + "end": { "line": 5, "character": 12 } + }, + "triggerKind": "automatic", + "context": { + "recentFiles": [ + { + "uri": "file:///path/to/utils.rs", + "languageId": "rust", + "text": "pub fn helper() -> i32 { 42 }\n" + } + ], + "relatedSnippets": [ + { + "uri": "file:///path/to/types.rs", + "excerpts": [ + { + "startLine": 10, + "endLine": 25, + "text": "pub struct Config {\n pub name: String,\n ...\n}" + } + ] + } + ], + "editHistory": [ + { + "uri": "file:///path/to/file.rs", + "diff": "--- a/file.rs\n+++ b/file.rs\n@@ -3,0 +3,1 @@\n+ let x = 42;" + } + ], + "userActions": [ + { + "action": "insertChar", + "uri": "file:///path/to/file.rs", + "line": 5, + "offset": 12, + "timestampMs": 1719400000000 + }, + { + "action": "cursorMovement", + "uri": "file:///path/to/file.rs", + "line": 10, + "offset": 0, + "timestampMs": 1719400001200 + } + ], + "openFiles": [ + { + "uri": "file:///path/to/utils.rs", + "languageId": "rust", + "visibleRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 30, "character": 0 } + }, + "lastFocusedMs": 1719399998000 + }, + { + "uri": "file:///path/to/types.rs", + "languageId": "rust", + "visibleRange": null, + "lastFocusedMs": 1719399990000 + } + ], + "diagnostics": [ + { + "uri": "file:///path/to/file.rs", + "range": { + "start": { "line": 5, "character": 0 }, + "end": { "line": 5, "character": 10 } + }, + "severity": "error", + "message": "cannot find value `foo` in this scope" + } + ] + } + } +} +``` + +`selection` is the current text selection range, if any. When the selection is empty (cursor is a point), this field may be omitted or have `start` equal to `end`. Agents can use selection state to predict replacements or transformations of the selected text. + +`triggerKind` is one of: +- `"automatic"` — triggered by user typing or cursor movement +- `"diagnostic"` — triggered by a diagnostic (error/warning) appearing at or near the cursor position. The client includes the relevant diagnostics in the `diagnostics` context field so the agent can target a fix. +- `"manual"` — triggered by explicit user action (keyboard shortcut) + +### Suggestion response + +A suggestion is one of two kinds: an **edit** (text changes) or a **jump** (navigate to a different file). + +**Edit suggestion:** + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": { + "suggestions": [ + { + "id": "sugg_001", + "kind": "edit", + "uri": "file:///path/to/other_file.rs", + "edits": [ + { + "range": { + "start": { "line": 5, "character": 0 }, + "end": { "line": 5, "character": 10 } + }, + "newText": "let result = helper();" + } + ], + "cursorPosition": { "line": 5, "character": 22 } + } + ] + } +} +``` + +**Jump suggestion:** + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": { + "suggestions": [ + { + "id": "sugg_002", + "kind": "jump", + "uri": "file:///path/to/other_file.rs", + "position": { "line": 15, "character": 4 } + } + ] + } +} +``` + +A response may contain a mix of edit and jump suggestions. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). + +Each suggestion contains: +- `id` — unique identifier for accept/reject tracking. +- `kind` — `"edit"` or `"jump"`. + +Edit suggestions additionally contain: +- `edits` — one or more text edits to apply. +- `cursorPosition` — optional suggested cursor position after applying edits. + +Jump suggestions additionally contain: +- `uri` — the file to navigate to. +- `position` — the target position within that file. + +### Accept / Reject + +```json +{ + "jsonrpc": "2.0", + "method": "nes/accept", + "params": { + "id": "sugg_001" + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "method": "nes/reject", + "params": { + "id": "sugg_001", + "reason": "rejected" + } +} +``` + +`reason` is one of: +- `"rejected"` — the user explicitly dismissed the suggestion (e.g. pressed Escape or typed something incompatible). +- `"ignored"` — the suggestion was shown but the user continued editing without interacting with it, and the context changed enough to invalidate it. +- `"replaced"` — the suggestion was superseded by a newer suggestion before the user could act on it. +- `"cancelled"` — the request was cancelled before the agent returned a response (e.g. the user typed quickly and the previous request became stale). + +The `reason` field is optional. If omitted, the agent should treat it as `"rejected"`. Providing granular reasons allows agents to improve their models — for example, a `"replaced"` suggestion carries different training signal than an explicit `"rejected"`. + +### NES session start + +The client provides workspace metadata when starting a session. This information is static for the lifetime of the session. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "nes/start", + "params": { + "workspaceUri": "file:///Users/alice/projects/my-app", + "workspaceFolders": [ + { + "uri": "file:///Users/alice/projects/my-app", + "name": "my-app" + } + ], + "repository": { + "name": "my-app", + "owner": "alice", + "remoteUrl": "https://github.com/alice/my-app.git" + } + } +} +``` + +All fields in `params` are optional. The `repository` field is omitted if the workspace is not a git repository or the info is unavailable. + +Response: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "sessionId": "nes_abc123" + } +} +``` + +### Config options + +The agent can use the existing `configOptions` mechanism from ACP to expose NES-related settings. For example, an agent might return config options like: + +```json +{ + "configOptions": [ + { + "id": "nes_model", + "name": "Prediction Model", + "category": "model", + "type": "enum", + "currentValue": "fast", + "options": [ + { "value": "fast", "label": "Fast" }, + { "value": "accurate", "label": "Accurate" } + ] + } + ] +} +``` + +## Frequently asked questions + +> What questions have arisen over the course of authoring this document or during subsequent discussions? + +### Why separate events from context? + +Events and context serve different purposes and have different delivery models: + +- **Events** are pushed as they happen — they allow the agent to maintain internal state (like an LSP server tracking open documents). This is the model Copilot uses. +- **Context** is attached to each request — it allows stateless agents to receive everything they need in one call. This is the model Zed Predict and Supermaven use. + +A note about Cursor: Cursor has a separate context-collection phase (`RefreshTabContext`) that involves vector DB lookup and is triggered on file open, tab switch, and significant edits. The event-based approach supports this flow: an NES agent can listen for `nes/didOpen`, `nes/didFocus`, and accumulated `nes/didChange` events to self-trigger its own context refresh. The `nes/didFocus` event (with cursor position and visible range) and workspace metadata from `nes/start` provide all the inputs Cursor's `RefreshTabContext` needs. + +An agent may want both (events for incremental file tracking + context for edit history), or just one. The capability split lets each agent pick the model that fits its architecture. + +### Why not reuse LSP's `textDocument/didOpen` etc. directly? + +LSP's document sync notifications carry the same information, but: +1. ACP is not LSP — reusing method names could cause confusion in implementations that bridge both. +2. We may want to evolve the event payloads independently (e.g. adding metadata fields). +3. Keeping them namespaced under `nes/` makes capability negotiation cleaner. + +### How does this relate to PR #325? + +This RFD covers the session lifecycle and also suggests a protocol that would cover a variety of different nes providers + +### Why provide workspace info in `nes/start`? + +Agents that perform server-side indexing (embedding-based retrieval, semantic search) need to know which repository they're working with. This metadata — workspace root, repo name/owner, remote URL — is static for the session lifetime, so it belongs in the session start rather than being repeated on every request or requiring a separate query. + +### What alternative approaches did you consider? + +1. **Context-only** — Pass all file content, edit history, and metadata as context fields on each `nes/suggest` request, with no event notifications. This is simpler for stateless agents but forces the client to assemble and transmit potentially large payloads on every request, even when nothing changed. It also prevents agents from maintaining their own incremental state (e.g. an internal file mirror or semantic index). +2. **Events-only** — Rely entirely on event notifications (`didOpen`, `didChange`, etc.) and have the agent maintain all state internally, with `nes/suggest` sending only the cursor position. This is efficient on the wire but requires every agent to implement stateful document tracking, which is a high barrier for simple agents that just want the code around the cursor. +3. **Events + context (chosen)** — Allow agents to declare both. An agent that wants to track files incrementally can request events; an agent that prefers stateless request-response can request context fields; a sophisticated agent can use both (events for file sync, context for edit history and definition excerpts). This gives each agent the flexibility to pick the model that fits its architecture without imposing unnecessary complexity. + +## Revision history + +- 2026-02-22: Initial draft From 76cf879ad896c755cb0a8146cb4c4552dccc5c19 Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Mon, 23 Feb 2026 12:00:18 +0100 Subject: [PATCH 2/3] Added ide actions for nes --- docs/rfds/next-edit-suggestions.mdx | 77 +++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/rfds/next-edit-suggestions.mdx b/docs/rfds/next-edit-suggestions.mdx index 8f2d6bbf..6e889b01 100644 --- a/docs/rfds/next-edit-suggestions.mdx +++ b/docs/rfds/next-edit-suggestions.mdx @@ -65,6 +65,25 @@ During `initialize`, the agent includes a `nes` field in its capabilities: All fields under `events` and `context` are optional — an agent advertises only what it can use. +#### Client capabilities + +The **client** advertises its own NES-related capabilities in the `initialize` request. Currently, the client can declare which well-known IDE actions it supports by listing their IDs. The agent reads these and may later include `"action"` kind suggestions that reference them. + +```json +{ + "capabilities": { + "nes": { + "ideActions": { + "rename": {}, + "searchAndReplace": {} + } + } + } +} +``` + +Each entry in `ideActions` is the ID of a well-known action (see [Well-known IDE actions](#well-known-ide-actions) below). Agents should only suggest actions that the client has advertised. + ### Session lifecycle If the `nes` capability is present, the client may call `nes/start` to begin an NES session. The agent can also use the existing `configOptions` mechanism to expose NES-related settings (model selection, debounce preferences, enabled/disabled state, etc.). @@ -293,7 +312,7 @@ The client requests a suggestion by calling `nes/suggest`. Context fields are in ### Suggestion response -A suggestion is one of two kinds: an **edit** (text changes) or a **jump** (navigate to a different file). +A suggestion is one of three kinds: an **edit** (text changes), a **jump** (navigate to a different file), or an **action** (trigger an IDE action). **Edit suggestion:** @@ -342,11 +361,38 @@ A suggestion is one of two kinds: an **edit** (text changes) or a **jump** (navi } ``` -A response may contain a mix of edit and jump suggestions. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). +**Action suggestion:** + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": { + "suggestions": [ + { + "id": "sugg_003", + "kind": "action", + "actionId": "rename", + "arguments": { + "uri": "file:///path/to/file.rs", + "position": { "line": 5, "character": 10 }, + "newName": "calculateTotal" + } + } + ] + } +} +``` + +Action suggestions reference an IDE action that the client previously advertised in its capabilities: +- `actionId` — matches an `id` from the client's advertised `ideActions`. +- `arguments` — matches the parameter schema declared by the client for that action. + +A response may contain a mix of edit, jump, and action suggestions. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). Each suggestion contains: - `id` — unique identifier for accept/reject tracking. -- `kind` — `"edit"` or `"jump"`. +- `kind` — `"edit"`, `"jump"`, or `"action"`. Edit suggestions additionally contain: - `edits` — one or more text edits to apply. @@ -356,6 +402,10 @@ Jump suggestions additionally contain: - `uri` — the file to navigate to. - `position` — the target position within that file. +Action suggestions additionally contain: +- `actionId` — the IDE action to perform (must match a client-advertised action `id`). +- `arguments` — action parameters matching the schema from the client's capability. + ### Accept / Reject ```json @@ -427,6 +477,27 @@ Response: } ``` +### Well-known IDE actions + +The following actions are well-known and have standardized parameter schemas. Clients that support these actions should use the IDs and parameter shapes defined here. + +**`rename`** — Rename a symbol across the workspace. + +Parameters: +- `uri` (`string`) — the file URI containing the symbol. +- `position` (`Position`) — the position of the symbol to rename. +- `newName` (`string`) — the new name for the symbol. + +**`searchAndReplace`** — Search and replace text within a file. + +Parameters: +- `uri` (`string`) — the file URI to search within. +- `search` (`string`) — the text or pattern to find. +- `replace` (`string`) — the replacement text. +- `isRegex` (`boolean`, optional) — whether `search` is a regular expression. Defaults to `false`. + +Additional well-known actions may be added to the protocol in the future. Agents should only suggest actions whose `id` matches an entry the client has advertised. + ### Config options The agent can use the existing `configOptions` mechanism from ACP to expose NES-related settings. For example, an agent might return config options like: From af67747c5aa052136c73ac729e2bc91587e042a6 Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Tue, 24 Feb 2026 11:01:22 +0100 Subject: [PATCH 3/3] fix formatting --- docs/rfds/next-edit-suggestions.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/rfds/next-edit-suggestions.mdx b/docs/rfds/next-edit-suggestions.mdx index 6e889b01..c5fcff38 100644 --- a/docs/rfds/next-edit-suggestions.mdx +++ b/docs/rfds/next-edit-suggestions.mdx @@ -306,6 +306,7 @@ The client requests a suggestion by calling `nes/suggest`. Context fields are in `selection` is the current text selection range, if any. When the selection is empty (cursor is a point), this field may be omitted or have `start` equal to `end`. Agents can use selection state to predict replacements or transformations of the selected text. `triggerKind` is one of: + - `"automatic"` — triggered by user typing or cursor movement - `"diagnostic"` — triggered by a diagnostic (error/warning) appearing at or near the cursor position. The client includes the relevant diagnostics in the `diagnostics` context field so the agent can target a fix. - `"manual"` — triggered by explicit user action (keyboard shortcut) @@ -385,24 +386,29 @@ A suggestion is one of three kinds: an **edit** (text changes), a **jump** (navi ``` Action suggestions reference an IDE action that the client previously advertised in its capabilities: + - `actionId` — matches an `id` from the client's advertised `ideActions`. - `arguments` — matches the parameter schema declared by the client for that action. A response may contain a mix of edit, jump, and action suggestions. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). Each suggestion contains: + - `id` — unique identifier for accept/reject tracking. - `kind` — `"edit"`, `"jump"`, or `"action"`. Edit suggestions additionally contain: + - `edits` — one or more text edits to apply. - `cursorPosition` — optional suggested cursor position after applying edits. Jump suggestions additionally contain: + - `uri` — the file to navigate to. - `position` — the target position within that file. Action suggestions additionally contain: + - `actionId` — the IDE action to perform (must match a client-advertised action `id`). - `arguments` — action parameters matching the schema from the client's capability. @@ -430,6 +436,7 @@ Action suggestions additionally contain: ``` `reason` is one of: + - `"rejected"` — the user explicitly dismissed the suggestion (e.g. pressed Escape or typed something incompatible). - `"ignored"` — the suggestion was shown but the user continued editing without interacting with it, and the context changed enough to invalidate it. - `"replaced"` — the suggestion was superseded by a newer suggestion before the user could act on it. @@ -484,6 +491,7 @@ The following actions are well-known and have standardized parameter schemas. Cl **`rename`** — Rename a symbol across the workspace. Parameters: + - `uri` (`string`) — the file URI containing the symbol. - `position` (`Position`) — the position of the symbol to rename. - `newName` (`string`) — the new name for the symbol. @@ -491,6 +499,7 @@ Parameters: **`searchAndReplace`** — Search and replace text within a file. Parameters: + - `uri` (`string`) — the file URI to search within. - `search` (`string`) — the text or pattern to find. - `replace` (`string`) — the replacement text. @@ -538,6 +547,7 @@ An agent may want both (events for incremental file tracking + context for edit ### Why not reuse LSP's `textDocument/didOpen` etc. directly? LSP's document sync notifications carry the same information, but: + 1. ACP is not LSP — reusing method names could cause confusion in implementations that bridge both. 2. We may want to evolve the event payloads independently (e.g. adding metadata fields). 3. Keeping them namespaced under `nes/` makes capability negotiation cleaner.