DeepSeek-Reasonix ACP Protocol: Implementation and Communication Guide
The Agent Client Protocol (ACP) is a lightweight JSON-RPC 2.0 protocol that enables bidirectional communication between DeepSeek-Reasonix and host applications via NDJSON over standard input/output, supporting session management, tool calling, and real-time streaming.
The DeepSeek-Reasonix repository implements the Agent Client Protocol (ACP) to provide a language-agnostic interface for LLM-powered agents. This protocol allows editors, IDEs, and automation tools to spawn the Reasonix agent as a subprocess and control it through structured JSON messages. All protocol definitions and communication handlers reside in the internal/acp package according to the source code.
What Is the ACP Protocol?
ACP stands for Agent Client Protocol, a specification that defines how a host process interacts with an LLM-powered agent over standard input/output streams. The protocol is defined in internal/acp/protocol.go and represents a faithful port of the original TypeScript implementation used by the v1 agent.
Core Design and Transport
The protocol uses NDJSON (Newline-Delimited JSON) over stdio, where each line represents a single JSON-RPC 2.0 message. This design makes it easy to pipe communication through subprocess boundaries and language runtimes.
Key characteristics include:
- Transport: Stdio JSON-RPC with line framing via
io.Reader/io.Writer - Message Format: One JSON object per line (NDJSON)
- Size Limits: 32 MiB maximum message size enforced by the
Connimplementation - Capabilities: Load persisted sessions, list/resume/close/delete sessions, embed inline text resources, and stream MCP (Model-Control-Protocol) over HTTP
Protocol Version and Error Handling
The current implementation uses ProtocolVersion = 1, defined as a constant in internal/acp/protocol.go at line 23. Error handling follows the JSON-RPC 2.0 specification with a specific subset of error codes:
-32700(Parse error)-32600(Invalid Request)-32601(Method not found)-32602(Invalid params)-32603(Internal error)
These codes are defined in internal/acp/protocol.go lines 26-33.
How DeepSeek-Reasonix Communicates with ACP
The agent-side implementation lives in the internal/acp package and is orchestrated by service.Serve. The communication flow involves three architectural layers: the transport Conn, the RPC service, and the event updateSink.
The Connection Layer (Conn)
The Conn type in internal/acp/server.go handles low-level NDJSON framing, request-response correlation, and thread-safe writes. It provides the foundational methods for all ACP communication:
conn := acp.NewConn(os.Stdin, os.Stdout)
result, err := conn.Request(ctx, "session/list", acp.SessionListParams{})
The Conn implementation guarantees correct JSON-RPC framing and uses a mutex-protected write method to ensure thread-safe operations when multiple goroutines access the transport.
RPC Handlers and Service Layer
The service struct in internal/acp/service.go registers RPC method handlers during initialization. The Serve method (lines 94-101) sets up the following endpoints:
initialize– Handshake and capability negotiationauthenticate– Authentication flowssession/new– Create new sessionssession/prompt– Send prompts and receive responsessession/update– Notification channel for streaming events
The initialization handshake occurs when the client sends InitializeParams and the agent replies with InitializeResult containing AgentCapabilities and supported auth methods. This implementation is found in svc.initialize at lines 40-61 of service.go.
Session Lifecycle Management
Sessions are created via the session/new RPC method, implemented in svc.sessionNew (lines 84-111 of service.go). The process follows this sequence:
- Generate a UUID using
newSessionID - Create an
updateSink(event-to-ACP adapter) - Instantiate a per-session
control.Controllervia the injectedFactory - Return
SessionNewResultcontaining the session ID, model list, and config options
Configuration changes are handled by session/set_config_option or the legacy session/set_model, which rebuild the controller on-the-fly and persist metadata to an ACP sidecar JSON file (*.acp.json).
Event Streaming and Notifications
When the agent generates events (thoughts, tool calls, or completions), the updateSink in internal/acp/dispatch.go bridges internal events to ACP notifications. The sink emits session/update notifications for:
- Tool calls: When the agent invokes a tool, the sink emits a
tool_callnotification - Permission requests: For gated tools,
updateSink.requestPermission(lines 21-46 ofdispatch.go) forwards asession/request_permissionrequest and awaits aPermissionRequestResult - Session state: Real-time updates on turn completion, errors, or cancellation
The prompting flow in svc.sessionPrompt (lines 14-70 of service.go) flattens prompts using FlattenPrompt, resolves slash-commands, runs the turn on the controller, and streams events back via the updateSink before returning a SessionPromptResult indicating the stop reason (StopEndTurn, StopCancelled, or StopError).
Implementation Walkthrough
Below is a minimal Go client that demonstrates communicating with a running Reasonix ACP process. This example shows the handshake, session creation, prompting, and graceful shutdown:
package main
import (
"context"
"encoding/json"
"log"
"os"
"os/exec"
"time"
"reasonix/internal/acp"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Start the agent as a subprocess
cmd := exec.Command("reasonix", "acp")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatalf("start agent: %v", err)
}
defer cmd.Process.Kill()
// Wire the stdio JSON-RPC connection
conn := acp.NewConn(os.Stdin, os.Stdout)
// 1. Initialize
var initRes acp.InitializeResult
if raw, err := conn.Request(ctx, "initialize", acp.InitializeParams{
ProtocolVersion: acp.ProtocolVersion,
}); err != nil {
log.Fatalf("init: %v", err)
} else if err := json.Unmarshal(raw, &initRes); err != nil {
log.Fatalf("decode init: %v", err)
}
log.Printf("Agent: %s %s", initRes.AgentInfo.Name, initRes.AgentInfo.Version)
// 2. Open a new session
var newRes acp.SessionNewResult
if raw, err := conn.Request(ctx, "session/new", acp.SessionNewParams{}); err != nil {
log.Fatalf("session/new: %v", err)
} else if err := json.Unmarshal(raw, &newRes); err != nil {
log.Fatalf("decode new: %v", err)
}
sid := newRes.SessionID
log.Printf("Created session %s", sid)
// 3. Send a prompt
prompt := []acp.ContentBlock{{
Type: "text",
Text: "Explain the difference between recursion and iteration.",
}}
if raw, err := conn.Request(ctx, "session/prompt", acp.SessionPromptParams{
SessionID: sid,
Prompt: prompt,
}); err != nil {
log.Fatalf("prompt: %v", err)
} else {
var resp acp.SessionPromptResult
_ = json.Unmarshal(raw, &resp)
log.Printf("Turn finished: %s", resp.StopReason)
}
// 4. Close the session
if _, err := conn.Request(ctx, "session/close",
acp.SessionCloseParams{SessionID: sid}); err != nil {
log.Fatalf("close: %v", err)
}
}
This snippet uses only public types from protocol.go, while the underlying connection logic resides in server.go.
Key Source Files
internal/acp/protocol.go: Defines every wire type includingInitializeResult,ContentBlock,SessionUpdateParams, and error codes.internal/acp/server.go: Implements the NDJSON JSON-RPC connection (Conn,Request,Notify, and framing logic).internal/acp/service.go: High-level RPC handlers includingsessionNew,sessionPrompt, and configuration management.internal/acp/dispatch.go: Bridges the agent's internal event stream to ACP notifications viaupdateSink.internal/acp/live_test.go: End-to-end test suite demonstrating full ACP session lifecycle.
Summary
- ACP is a JSON-RPC 2.0 protocol using NDJSON over stdio, designed for low-latency, language-agnostic interaction with the Reasonix agent.
- The communication stack consists of three layers:
Connfor transport,servicefor RPC handlers, andupdateSinkfor event streaming. - Session management uses UUIDs with persisted state stored in
*.acp.jsonsidecar files, supporting resume, close, and delete operations. - Tool permission flows use bidirectional requests where the client must approve gated operations before execution continues.
- All message handling preserves thread safety through mutex-protected writes and supports messages up to 32 MiB in size.
Frequently Asked Questions
What transport mechanism does the ACP protocol use?
The ACP protocol uses standard input/output (stdio) with NDJSON (Newline-Delimited JSON) formatting. The Conn type in internal/acp/server.go wraps io.Reader and io.Writer interfaces to handle line framing, enabling the agent to run as a subprocess that communicates via pipes.
How does DeepSeek-Reasonix handle session persistence?
Sessions persist state to the filesystem using sidecar JSON files (*.acp.json). When configuration changes occur via session/set_config_option, the service rebuilds the controller and writes metadata to these sidecar files. Sessions can be listed, resumed, or deleted using the session/list, session/resume, and session/delete RPC methods.
What is the maximum message size supported by ACP?
The protocol enforces a 32 MiB size limit per message. The Conn implementation validates message size during framing to prevent memory exhaustion when handling large text resources or MCP streams.
How does the protocol handle tool permission requests?
When the agent encounters a gated tool, the updateSink.requestPermission method in internal/acp/dispatch.go sends a session/request_permission request to the client and blocks until receiving a PermissionRequestResult. This synchronous round-trip ensures the host application can prompt users for approval before sensitive operations execute.
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 →