Agent Code Protocol (ACP) in Reasonix: A Complete Guide to Editor Integration
Reasonix implements ACP v1 using JSON-RPC 2.0 over stdin/stdout with NDJSON framing, enabling any editor to launch sessions, execute prompts, and apply mid-turn steering through vendor-specific extensions.
The Agent Code Protocol (ACP) is the standardized interface that allows editors like VS Code and VSCodium to communicate with AI coding agents. In the esengine/DeepSeek-Reasonix repository, ACP is implemented as a first-class integration surface that exposes the full Reasonix engine—including session management, file operations, and real-time steering—to external clients. This article explains the complete architecture, from process launch to advanced vendor extensions.
Launching the ACP Agent in Reasonix
The entry point for ACP integration is the reasonix acp command, defined in [internal/cli/acp.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/cli/acp.go). This command starts the ACP server with stdio as its transport layer, ensuring stdout remains a pure message stream while stderr carries diagnostics【/internal/cli/acp.go#L28-L38】.
reasonix acp # default model and preset
reasonix acp --model deepseek-pro
reasonix acp --preset delivery
The CLI parses flags, validates configuration, and delegates to the server implementation in [internal/acp/server.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/acp/server.go). This server runs a JSON-RPC loop that reads NDJSON from stdin and writes responses to stdout, making it compatible with any client that can spawn a subprocess and pipe stdio.
ACP Handshake and Capability Negotiation
Before any session operations, the client must complete an initialize handshake. Reasonix responds with a capability shape that declares both core ACP features and vendor-specific extensions. The structure is documented in [docs/ACP.md](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/ACP.md#L44-L75)【/docs/ACP.md#L68-L73】.
{
"protocolVersion": 1,
"agentCapabilities": {
"loadSession": true,
"sessionCapabilities": { "list": {}, "resume": {}, "delete": {} },
"promptCapabilities": { "embeddedContext": true },
"mcpCapabilities": { "http": true },
"_meta": {
"reasonix.io": {
"sessionSteer": { "method": "_reasonix.io/session/steer" },
"sessionInbox": { "version": 1 }
}
}
}
}
Key capability categories include:
- sessionCapabilities — Session listing, resumption, and persistence
- promptCapabilities — Context embedding and streaming response formats
- mcpCapabilities — Model Context Protocol transport options
- _meta.reasonix.io — Vendor extensions for steering, inbox, and UI surfaces
If the client advertises methods like fs.readTextFile or terminal forwarding, Reasonix routes these through the editor's buffers rather than local filesystem access, keeping editor state synchronized with agent operations【/docs/ACP.md#L78-L86】.
ACP Session Lifecycle Methods
Each ACP session maintains isolated state including its controller, workspace root, model configuration, preset, and transcript history. The session API mirrors Reasonix's core controller but is scoped to the ACP surface. All methods are defined in [internal/acp/protocol.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/acp/protocol.go)【/internal/acp/protocol.go#L1-L30】.
| Method | Purpose |
|---|---|
session/new |
Create fresh session with absolute cwd path |
session/load |
Replay persisted transcript from disk |
session/resume |
Restore session without replaying history |
session/prompt |
Execute one turn with streaming updates |
session/cancel |
Abort active turn (non-blocking) |
session/list |
Enumerate active and persisted sessions |
session/close |
Gracefully terminate session |
session/delete |
Permanently remove persisted session |
The session/prompt method is the primary interaction surface. It accepts a prompt array ( multimodal messages) and streams progress notifications until reaching a stop reason. The streaming protocol uses JSON-RPC notifications with method names like $/progress and $/promptUpdate.
Mid-Turn Steering: The _reasonix.io/session/steer Extension
Mid-turn steering is Reasonix's signature ACP extension, allowing editors to inject guidance while a session/prompt is actively streaming. This capability is advertised in the _meta block and must be discovered by clients—it does not exist in the core ACP specification【/docs/ACP.md#L77-L85】.
The steering method name is _reasonix.io/session/steer, and clients should verify its presence in the initialize response before presenting steering UI to users.
Steering request format:
{
"jsonrpc": "2.0",
"id": 2,
"method": "_reasonix.io/session/steer",
"params": {
"sessionId": "abc-123",
"prompt": [{ "type": "text", "text": "use email instead of username" }]
}
}
Possible responses:
steer_accepted— Guidance applied to active turn, visible in subsequent outputqueued_followup— Guidance stored for next turn, current turn unaffected
The response structure is documented in [docs/ACP.md](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/ACP.md#L90-L108). Clients must respect these dispositions and not assume steering always succeeds.
Durable Inbox and Additional Extensions
Beyond steering, Reasonix exposes three additional vendor extensions under agentCapabilities._meta["reasonix.io"]:
| Extension | Method | Purpose |
|---|---|---|
| sessionInbox | _reasonix.io/session/inbox/* |
Queue follow-up tasks with CRUD operations and dispatcher pause/resume |
| reloadExtensions | _reasonix.io/session/reloadExtensions |
Atomic rebuild of runtime (tools, plugins, providers) between turns |
| extensionSurface | (advertised capability) | Receive structured UI payloads (cards, forms) when client supports it |
The inbox implementation resides in [internal/acp/inbox.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/acp/inbox.go), providing versioned queue semantics for complex multi-step workflows【/internal/acp/inbox.go#L1-L20】. Extension data structures are defined in [internal/acp/extension_models.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/acp/extension_models.go).
The transport layer recognizes ACP through the UIHostACP enum ("acp"), defined in [internal/extension/protocol/enums.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/extension/protocol/enums.go) and exposed in the Go SDK at [sdk/go/types_generated.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/sdk/go/types_generated.go#L193).
Complete ACP Client Implementation (Node.js)
This minimal client demonstrates the full lifecycle: launch, handshake, session creation, prompting, and steering.
const { spawn } = require('child_process');
const readline = require('readline');
// 1. Spawn Reasonix ACP with isolated stdio
const proc = spawn('reasonix', ['acp', '--model', 'deepseek-pro'], {
stdio: ['pipe', 'pipe', 'inherit'] // stdin, stdout, stderr
});
let msgId = 0;
const pending = new Map();
let sessionId = null;
let canSteer = false;
// 2. NDJSON writer
function send(method, params) {
const id = ++msgId;
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
proc.stdin.write(msg + '\n');
pending.set(id, { method, startTime: Date.now() });
return id;
}
// 3. NDJSON reader
const rl = readline.createInterface({ input: proc.stdout });
rl.on('line', line => {
const msg = JSON.parse(line);
if (msg.id !== undefined && pending.has(msg.id)) {
handleResponse(msg, pending.get(msg.id).method);
pending.delete(msg.id);
} else if (msg.method?.startsWith('$/')) {
handleNotification(msg);
} else if (msg.method) {
// Client-to-server request (rare in ACP)
handleRequest(msg);
}
});
function handleResponse(msg, method) {
console.log(`← ${method} result:`, msg.result ? 'success' : 'error');
if (method === 'initialize') {
// 4. Extract steering capability
const meta = msg.result?.agentCapabilities?._meta?.['reasonix.io'];
canSteer = !!meta?.sessionSteer?.method;
console.log(`Steering available: ${canSteer}`);
// 5. Create session with absolute path
send('session/new', { cwd: process.cwd() });
} else if (method === 'session/new') {
sessionId = msg.result?.sessionId;
console.log(`Session created: ${sessionId}`);
// 6. Execute first prompt
send('session/prompt', {
sessionId,
prompt: [{ type: 'text', text: 'Explain how ACP enables editor integration' }]
});
} else if (method === '_reasonix.io/session/steer') {
console.log(`Steer result: ${msg.result?.disposition}`);
}
}
function handleNotification(msg) {
if (msg.method === '$/promptUpdate') {
const { content, stopReason } = msg.params;
process.stdout.write(content || '');
if (stopReason) {
console.log(`\n[turn complete: ${stopReason}]`);
}
}
}
// (Optional) Mid-turn steering trigger
function injectSteer(text) {
if (!canSteer || !sessionId) return;
send('_reasonix.io/session/steer', {
sessionId,
prompt: [{ type: 'text', text }]
});
}
// Example: steer after 5 seconds if still running
setTimeout(() => injectSteer('focus on the JSON-RPC transport layer'), 5000);
Using the Native Go SDK for ACP
Reasonix includes a Go SDK that wraps the JSON-RPC protocol. This matches the implementation in [internal/cli/acp.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/cli/acp.go).
package main
import (
"context"
"fmt"
"log"
"github.com/esengine/DeepSeek-Reasonix/internal/acp"
)
func main() {
ctx := context.Background()
// 1. Create factory (matches CLI initialization)
factory := acp.NewFactory(acp.FactoryOptions{
DefaultModel: "deepseek-pro",
DefaultPreset: "delivery",
})
// 2. Initialize session
sess, err := factory.NewSession(ctx, acp.SessionParams{
Cwd: "/absolute/path/to/workspace",
Sink: acp.NewStreamingSink(func(evt acp.Event) {
fmt.Printf("[stream] %s: %s\n", evt.Kind, evt.Data)
}),
})
if err != nil {
log.Fatal(err)
}
defer sess.Close()
// 3. Execute prompt with streaming
resp, err := sess.Prompt(ctx, acp.PromptParams{
Prompt: []acp.Message{
{Type: "text", Text: "How does ACP steering work?"},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Turn completed: %s\n", resp.StopReason)
// 4. Conditional steering (check capability first)
caps := sess.AgentCapabilities()
if steerMethod, ok := caps.Extensions["reasonix.io/session/steer"]; ok {
steerResp, err := sess.CallMethod(ctx, steerMethod, map[string]any{
"prompt": []map[string]string{
{"type": "text", "text": "add a code example"},
},
})
fmt.Printf("Steer response: %+v\n", steerResp)
}
}
Key Source Files for ACP Implementation
Integration Checklist for Editor Developers
Based on the specification in [docs/ACP.md](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/ACP.md#L89-L101), clients should verify:
- Launch isolation — Spawn
reasonix acpwith dedicated stdio pipes, never sharing stdin/stdout with other processes - Capability discovery — Parse both core and
_metablocks; do not assume extensions exist - Absolute paths — Always provide absolute
cwdinsession/newto avoid resolution ambiguity - Request servicing — Handle
fs.*andterminal.*requests promptly while prompts run - Steering UI gating — Show steering controls only when
sessionSteermethod is advertised AND a prompt is active - Disposition handling — Distinguish
steer_accepted(immediate effect) fromqueued_followup(deferred) - Cleanup protocol — Use
session/closefor temporary sessions,session/deletefor permanent removal
Summary
- ACP v1 in Reasonix uses JSON-RPC 2.0 over NDJSON on stdio, with stderr reserved for diagnostics
- Capability negotiation via
initializeexposes core features and vendor extensions in_meta.reasonix.io - Session lifecycle methods in [
internal/acp/protocol.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/acp/protocol.go) provide full CRUD operations with persistence - Mid-turn steering (
_reasonix.io/session/steer) allows real-time guidance injection withsteer_acceptedorqueued_followupdispositions - Extended surface includes inbox queue, runtime reload, and structured UI payloads for capable clients
- Editor integration requires careful stdio isolation, absolute paths, and dynamic capability checking
Frequently Asked Questions
What transport does Reasonix ACP use?
Reasonix ACP uses stdio-based NDJSON transport: the client spawns reasonix acp as a subprocess, writes JSON-RPC requests as newline-delimited JSON to stdin, and reads responses from stdout. All diagnostic output goes to stderr, keeping the message stream clean. This design is defined in [internal/cli/acp.go](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/internal/cli/acp.go#L28-L38) and matches the Agent Client Protocol specification.
How do I discover if steering is available?
Inspect the _meta["reasonix.io"].sessionSteer object in the initialize response. If present, the method field contains the exact string to use (typically _reasonix.io/session/steer). Do not attempt steering if this capability is absent—there is no fallback in the core protocol. This discovery pattern is documented in [docs/ACP.md](https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/ACP.md#L68-L73).
Can I use ACP with my own editor, or only VS Code?
Any editor or tool that can spawn subprocesses and handle JSON-RPC over stdio can implement ACP. The protocol is editor-agnostic. The esengine/DeepSeek-Reasonix repository includes test clients and the Go SDK demonstrates programmatic usage without any VS Code-specific dependencies.
What happens to steering commands if the turn completes first?
The server returns queued_followup in the steer response, indicating the guidance was stored for the next turn rather than applied to the now-completed turn. Clients should surface this distinction to users so they understand whether their intervention affected the current output or will appear in the next exchange.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →