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 Conn implementation
  • 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 negotiation
  • authenticate – Authentication flows
  • session/new – Create new sessions
  • session/prompt – Send prompts and receive responses
  • session/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:

  1. Generate a UUID using newSessionID
  2. Create an updateSink (event-to-ACP adapter)
  3. Instantiate a per-session control.Controller via the injected Factory
  4. Return SessionNewResult containing 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_call notification
  • Permission requests: For gated tools, updateSink.requestPermission (lines 21-46 of dispatch.go) forwards a session/request_permission request and awaits a PermissionRequestResult
  • 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

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: Conn for transport, service for RPC handlers, and updateSink for event streaming.
  • Session management uses UUIDs with persisted state stored in *.acp.json sidecar 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →