feat(acp): add stdio adapter for editor agents (#782)
This commit is contained in:
@@ -227,12 +227,35 @@ deepseek resume --last # resume the most recent sessio
|
||||
deepseek resume <SESSION_ID> # resume a specific session by UUID
|
||||
deepseek fork <SESSION_ID> # fork a session at a chosen turn
|
||||
deepseek serve --http # HTTP/SSE API server
|
||||
deepseek serve --acp # ACP stdio adapter for Zed/custom agents
|
||||
deepseek pr <N> # fetch PR and pre-seed review prompt
|
||||
deepseek mcp list # list configured MCP servers
|
||||
deepseek mcp validate # validate MCP config/connectivity
|
||||
deepseek mcp-server # run dispatcher MCP stdio server
|
||||
```
|
||||
|
||||
### Zed / ACP
|
||||
|
||||
DeepSeek can run as a custom Agent Client Protocol server for editors that
|
||||
spawn local ACP agents over stdio. In Zed, add a custom agent server:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"DeepSeek": {
|
||||
"type": "custom",
|
||||
"command": "deepseek",
|
||||
"args": ["serve", "--acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first ACP slice supports new sessions and prompt responses through your
|
||||
existing DeepSeek config/API key. Tool-backed editing and checkpoint replay are
|
||||
not exposed through ACP yet.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//! Minimal Agent Client Protocol stdio adapter.
|
||||
//!
|
||||
//! This intentionally starts with the ACP baseline: initialize, new session,
|
||||
//! prompt, and cancel. It keeps stdout protocol-clean for editor clients and
|
||||
//! routes prompts through the same configured DeepSeek client as one-shot CLI
|
||||
//! mode.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
|
||||
use crate::client::DeepSeekClient;
|
||||
use crate::config::Config;
|
||||
use crate::llm_client::LlmClient;
|
||||
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt};
|
||||
|
||||
const ACP_PROTOCOL_VERSION: u64 = 1;
|
||||
|
||||
pub async fn run_acp_server(config: Config, model: String, default_cwd: PathBuf) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
let mut reader = BufReader::new(stdin).lines();
|
||||
let mut writer = tokio::io::BufWriter::new(stdout);
|
||||
let mut server = AcpServer::new(config, model, default_cwd);
|
||||
|
||||
while let Some(line) = reader.next_line().await? {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let message: Value = match serde_json::from_str(&line) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
write_jsonrpc_error(&mut writer, None, -32700, format!("invalid json: {err}"))
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if message.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
|
||||
write_jsonrpc_error(
|
||||
&mut writer,
|
||||
message.get("id").cloned(),
|
||||
-32600,
|
||||
"jsonrpc version must be 2.0",
|
||||
)
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let id = message.get("id").cloned();
|
||||
let method = match message.get("method").and_then(Value::as_str) {
|
||||
Some(method) => method,
|
||||
None => {
|
||||
write_jsonrpc_error(&mut writer, id, -32600, "missing method").await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let params = message.get("params").cloned().unwrap_or_else(|| json!({}));
|
||||
|
||||
match server.handle_request(method, params, &mut writer).await {
|
||||
Ok(AcpDispatch::Response(result)) => {
|
||||
if let Some(id) = id {
|
||||
write_jsonrpc_result(&mut writer, id, result).await?;
|
||||
}
|
||||
}
|
||||
Ok(AcpDispatch::Shutdown) => {
|
||||
if let Some(id) = id {
|
||||
write_jsonrpc_result(&mut writer, id, json!(null)).await?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
write_jsonrpc_error(&mut writer, id, err.code, err.message).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AcpServer {
|
||||
config: Config,
|
||||
model: String,
|
||||
default_cwd: PathBuf,
|
||||
sessions: HashMap<String, AcpSession>,
|
||||
}
|
||||
|
||||
struct AcpSession {
|
||||
cwd: PathBuf,
|
||||
}
|
||||
|
||||
enum AcpDispatch {
|
||||
Response(Value),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
struct AcpError {
|
||||
code: i32,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl AcpServer {
|
||||
fn new(config: Config, model: String, default_cwd: PathBuf) -> Self {
|
||||
Self {
|
||||
config,
|
||||
model,
|
||||
default_cwd,
|
||||
sessions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request<W>(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
writer: &mut W,
|
||||
) -> std::result::Result<AcpDispatch, AcpError>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
match method {
|
||||
"initialize" => Ok(AcpDispatch::Response(initialize_result(
|
||||
params.get("protocolVersion").and_then(Value::as_u64),
|
||||
))),
|
||||
"session/new" => Ok(AcpDispatch::Response(self.new_session(params)?)),
|
||||
"session/prompt" => {
|
||||
self.prompt(params, writer).await?;
|
||||
Ok(AcpDispatch::Response(json!({ "stopReason": "end_turn" })))
|
||||
}
|
||||
"session/cancel" => Ok(AcpDispatch::Response(json!(null))),
|
||||
"shutdown" => Ok(AcpDispatch::Shutdown),
|
||||
_ => Err(AcpError::method_not_found(method)),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_session(&mut self, params: Value) -> std::result::Result<Value, AcpError> {
|
||||
let cwd = params
|
||||
.get("cwd")
|
||||
.and_then(Value::as_str)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| self.default_cwd.clone());
|
||||
let session_id = format!("deepseek-{}", uuid::Uuid::new_v4());
|
||||
self.sessions.insert(session_id.clone(), AcpSession { cwd });
|
||||
Ok(json!({ "sessionId": session_id }))
|
||||
}
|
||||
|
||||
async fn prompt<W>(&self, params: Value, writer: &mut W) -> std::result::Result<(), AcpError>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let session_id = params
|
||||
.get("sessionId")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| AcpError::invalid_params("sessionId is required"))?;
|
||||
let session = self
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.ok_or_else(|| AcpError::invalid_params("unknown sessionId"))?;
|
||||
let prompt = extract_prompt_text(params.get("prompt"))
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.ok_or_else(|| AcpError::invalid_params("prompt must include text content"))?;
|
||||
|
||||
let output = self
|
||||
.run_prompt(&prompt, &session.cwd)
|
||||
.await
|
||||
.map_err(|err| AcpError::internal(err.to_string()))?;
|
||||
|
||||
if !output.is_empty() {
|
||||
write_session_update(writer, session_id, output)
|
||||
.await
|
||||
.map_err(|err| AcpError::internal(err.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_prompt(&self, prompt: &str, cwd: &PathBuf) -> Result<String> {
|
||||
let _cwd_guard = ScopedCurrentDir::new(cwd)?;
|
||||
let client = DeepSeekClient::new(&self.config)?;
|
||||
let route = crate::resolve_cli_auto_route(&self.config, &self.model, prompt).await;
|
||||
let reasoning_effort = route
|
||||
.reasoning_effort
|
||||
.map(|effort| effort.as_setting().to_string());
|
||||
|
||||
let request = MessageRequest {
|
||||
model: route.model,
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentBlock::Text {
|
||||
text: prompt.to_string(),
|
||||
cache_control: None,
|
||||
}],
|
||||
}],
|
||||
max_tokens: 4096,
|
||||
system: Some(SystemPrompt::Text(
|
||||
"You are a coding assistant inside an ACP-compatible editor. Give concise, actionable responses.".to_string(),
|
||||
)),
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
metadata: None,
|
||||
thinking: None,
|
||||
reasoning_effort,
|
||||
stream: Some(false),
|
||||
temperature: Some(0.2),
|
||||
top_p: Some(0.9),
|
||||
};
|
||||
|
||||
let response = client.create_message(request).await?;
|
||||
let mut output = String::new();
|
||||
for block in response.content {
|
||||
if let ContentBlock::Text { text, .. } = block {
|
||||
output.push_str(&text);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
struct ScopedCurrentDir {
|
||||
prior: PathBuf,
|
||||
}
|
||||
|
||||
impl ScopedCurrentDir {
|
||||
fn new(cwd: &PathBuf) -> Result<Self> {
|
||||
let prior = std::env::current_dir()?;
|
||||
if cwd.as_os_str().is_empty() {
|
||||
return Ok(Self { prior });
|
||||
}
|
||||
std::env::set_current_dir(cwd)
|
||||
.map_err(|err| anyhow!("failed to enter ACP session cwd {}: {err}", cwd.display()))?;
|
||||
Ok(Self { prior })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScopedCurrentDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::env::set_current_dir(&self.prior);
|
||||
}
|
||||
}
|
||||
|
||||
impl AcpError {
|
||||
fn invalid_params(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code: -32602,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn method_not_found(method: &str) -> Self {
|
||||
Self {
|
||||
code: -32601,
|
||||
message: format!("method not found: {method}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn internal(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code: -32603,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_result(client_protocol_version: Option<u64>) -> Value {
|
||||
json!({
|
||||
"protocolVersion": client_protocol_version
|
||||
.map(|version| version.min(ACP_PROTOCOL_VERSION))
|
||||
.unwrap_or(ACP_PROTOCOL_VERSION),
|
||||
"agentCapabilities": {
|
||||
"loadSession": false,
|
||||
"promptCapabilities": {
|
||||
"image": false,
|
||||
"audio": false,
|
||||
"embeddedContext": true
|
||||
},
|
||||
"mcpCapabilities": {
|
||||
"http": false,
|
||||
"sse": false
|
||||
},
|
||||
"sessionCapabilities": {}
|
||||
},
|
||||
"agentInfo": {
|
||||
"name": "deepseek",
|
||||
"title": "DeepSeek TUI",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
},
|
||||
"authMethods": []
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_prompt_text(prompt: Option<&Value>) -> Option<String> {
|
||||
match prompt? {
|
||||
Value::String(text) => Some(text.clone()),
|
||||
Value::Array(blocks) => {
|
||||
let parts = blocks
|
||||
.iter()
|
||||
.filter_map(content_block_text)
|
||||
.collect::<Vec<_>>();
|
||||
(!parts.is_empty()).then(|| parts.join("\n\n"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_block_text(block: &Value) -> Option<String> {
|
||||
match block.get("type").and_then(Value::as_str)? {
|
||||
"text" => block
|
||||
.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
"resource" => resource_text(block),
|
||||
"resource_link" | "resourceLink" => resource_link_text(block),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_text(block: &Value) -> Option<String> {
|
||||
let resource = block.get("resource").unwrap_or(block);
|
||||
if let Some(text) = resource.get("text").and_then(Value::as_str) {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
resource_link_text(resource)
|
||||
}
|
||||
|
||||
fn resource_link_text(block: &Value) -> Option<String> {
|
||||
let uri = block
|
||||
.get("uri")
|
||||
.or_else(|| block.pointer("/resource/uri"))
|
||||
.and_then(Value::as_str)?;
|
||||
Some(format!("@{uri}"))
|
||||
}
|
||||
|
||||
async fn write_session_update<W>(writer: &mut W, session_id: &str, text: String) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let notification = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session/update",
|
||||
"params": {
|
||||
"sessionId": session_id,
|
||||
"update": {
|
||||
"sessionUpdate": "agent_message_chunk",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": text
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
write_json_line(writer, notification).await
|
||||
}
|
||||
|
||||
async fn write_jsonrpc_result<W>(writer: &mut W, id: Value, result: Value) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
write_json_line(
|
||||
writer,
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": result
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_jsonrpc_error<W>(
|
||||
writer: &mut W,
|
||||
id: Option<Value>,
|
||||
code: i32,
|
||||
message: impl Into<String>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
write_json_line(
|
||||
writer,
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message.into()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_json_line<W>(writer: &mut W, value: Value) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
writer.write_all(value.to_string().as_bytes()).await?;
|
||||
writer.write_all(b"\n").await?;
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn initialize_advertises_baseline_acp_agent() {
|
||||
let result = initialize_result(Some(1));
|
||||
|
||||
assert_eq!(result["protocolVersion"], 1);
|
||||
assert_eq!(result["agentInfo"]["name"], "deepseek");
|
||||
assert_eq!(result["agentCapabilities"]["loadSession"], false);
|
||||
assert_eq!(
|
||||
result["agentCapabilities"]["promptCapabilities"]["embeddedContext"],
|
||||
true
|
||||
);
|
||||
assert_eq!(result["authMethods"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_prompt_text_accepts_text_and_resource_blocks() {
|
||||
let prompt = json!([
|
||||
{ "type": "text", "text": "Review this file" },
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "file:///tmp/app.rs",
|
||||
"mimeType": "text/rust",
|
||||
"text": "fn main() {}"
|
||||
}
|
||||
},
|
||||
{ "type": "resource_link", "uri": "file:///tmp/lib.rs" }
|
||||
]);
|
||||
|
||||
let text = extract_prompt_text(Some(&prompt)).expect("prompt text");
|
||||
|
||||
assert!(text.contains("Review this file"));
|
||||
assert!(text.contains("fn main() {}"));
|
||||
assert!(text.contains("@file:///tmp/lib.rs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_update_is_protocol_clean_single_line_json() {
|
||||
let mut out = Vec::new();
|
||||
|
||||
write_session_update(&mut out, "sess_1", "hello\nworld".to_string())
|
||||
.await
|
||||
.expect("write update");
|
||||
|
||||
let line = String::from_utf8(out).expect("utf8");
|
||||
assert_eq!(line.lines().count(), 1);
|
||||
let value: Value = serde_json::from_str(line.trim()).expect("json");
|
||||
assert_eq!(value["method"], "session/update");
|
||||
assert_eq!(value["params"]["sessionId"], "sess_1");
|
||||
assert_eq!(value["params"]["update"]["content"]["text"], "hello\nworld");
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -12,6 +12,7 @@ use dotenvy::dotenv;
|
||||
use tempfile::NamedTempFile;
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
mod acp_server;
|
||||
mod audit;
|
||||
mod auto_reasoning;
|
||||
mod automation_manager;
|
||||
@@ -387,6 +388,9 @@ struct ServeArgs {
|
||||
/// Start runtime HTTP/SSE API server
|
||||
#[arg(long)]
|
||||
http: bool,
|
||||
/// Start ACP server over stdio for editor clients such as Zed
|
||||
#[arg(long)]
|
||||
acp: bool,
|
||||
/// Bind host for HTTP server (default localhost)
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
@@ -689,8 +693,12 @@ async fn main() -> Result<()> {
|
||||
let workspace = cli.workspace.clone().unwrap_or_else(|| {
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
});
|
||||
if args.mcp && args.http {
|
||||
bail!("Choose exactly one server mode: --mcp or --http");
|
||||
let selected_modes = [args.mcp, args.http, args.acp]
|
||||
.into_iter()
|
||||
.filter(|selected| *selected)
|
||||
.count();
|
||||
if selected_modes != 1 {
|
||||
bail!("Choose exactly one server mode: --mcp, --http, or --acp");
|
||||
}
|
||||
if args.mcp {
|
||||
mcp_server::run_mcp_server(workspace)
|
||||
@@ -708,8 +716,12 @@ async fn main() -> Result<()> {
|
||||
},
|
||||
)
|
||||
.await
|
||||
} else if args.acp {
|
||||
let config = load_config_from_cli(&cli)?;
|
||||
let model = config.default_model();
|
||||
acp_server::run_acp_server(config, model, workspace).await
|
||||
} else {
|
||||
bail!("No server mode specified. Use --mcp or --http.")
|
||||
unreachable!("server mode count checked above")
|
||||
}
|
||||
}
|
||||
Commands::Resume { session_id, last } => {
|
||||
|
||||
@@ -139,9 +139,7 @@ pub fn strip_ansi_into(s: &str, out: &mut String) {
|
||||
/// to `1` for continuation bytes / invalid leads so callers always make
|
||||
/// forward progress.
|
||||
fn utf8_seq_len(lead: u8) -> usize {
|
||||
if lead < 0x80 {
|
||||
1
|
||||
} else if lead < 0xc0 {
|
||||
if lead < 0xc0 {
|
||||
1
|
||||
} else if lead < 0xe0 {
|
||||
2
|
||||
|
||||
+10
-8
@@ -176,16 +176,18 @@ Tools from a self-hosted DeepSeek server follow the standard naming convention:
|
||||
|
||||
For example, the `shell` tool becomes `mcp_deepseek_shell`.
|
||||
|
||||
### MCP Server vs HTTP/SSE API
|
||||
### MCP Server vs HTTP/SSE API vs ACP
|
||||
|
||||
| | `deepseek-tui serve --mcp` | `deepseek-tui serve --http` |
|
||||
|---|---|---|
|
||||
| **Protocol** | MCP stdio | HTTP/SSE JSON-RPC |
|
||||
| **Use case** | Tool server for MCP clients | Runtime API for apps |
|
||||
| **Config** | `~/.deepseek/mcp.json` entry | Direct URL connection |
|
||||
| **Lifecycle** | Spawned per client session | Long-running daemon |
|
||||
| | `deepseek-tui serve --mcp` | `deepseek-tui serve --http` | `deepseek-tui serve --acp` |
|
||||
|---|---|---|---|
|
||||
| **Protocol** | MCP stdio | HTTP/SSE JSON-RPC | ACP stdio |
|
||||
| **Use case** | Tool server for MCP clients | Runtime API for apps | Editor agent for Zed/custom ACP clients |
|
||||
| **Config** | `~/.deepseek/mcp.json` entry | Direct URL connection | Editor `agent_servers` custom command |
|
||||
| **Lifecycle** | Spawned per client session | Long-running daemon | Spawned per editor agent session |
|
||||
|
||||
Use `mcp add-self` when you want DeepSeek tools available to other MCP clients. Use `serve --http` when building applications that consume the API directly.
|
||||
Use `mcp add-self` when you want DeepSeek tools available to other MCP clients.
|
||||
Use `serve --http` when building applications that consume the API directly.
|
||||
Use `serve --acp` when an editor wants to talk to DeepSeek as an ACP agent.
|
||||
|
||||
### Verification
|
||||
|
||||
|
||||
+25
-4
@@ -1,10 +1,11 @@
|
||||
# Runtime API & Integration Contract
|
||||
|
||||
DeepSeek TUI exposes a local runtime API through `deepseek serve --http` and
|
||||
machine-readable health via `deepseek doctor --json`. This document is the
|
||||
stable integration contract for native macOS workbench applications (and other
|
||||
local supervisors) that embed the DeepSeek engine without screen-scraping
|
||||
terminal output.
|
||||
machine-readable health via `deepseek doctor --json`. It also exposes
|
||||
`deepseek serve --acp` for editor clients that speak the Agent Client Protocol
|
||||
over stdio. This document is the stable integration contract for native macOS
|
||||
workbench applications (and other local supervisors) that embed the DeepSeek
|
||||
engine without screen-scraping terminal output.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -13,6 +14,7 @@ macOS workbench (or any local supervisor)
|
||||
│
|
||||
├─ deepseek doctor --json → machine-readable health & capability
|
||||
├─ deepseek serve --http → HTTP/SSE runtime API
|
||||
├─ deepseek serve --acp → ACP stdio agent for editors such as Zed
|
||||
├─ deepseek serve --mcp → MCP stdio server
|
||||
└─ deepseek [args] → interactive TUI session
|
||||
```
|
||||
@@ -20,6 +22,25 @@ macOS workbench (or any local supervisor)
|
||||
The engine runs as a local-only process. All APIs bind to `localhost` by
|
||||
default. No hosted relay, no provider-token custody, no secret leakage.
|
||||
|
||||
## ACP stdio adapter: `deepseek serve --acp`
|
||||
|
||||
`deepseek serve --acp` speaks JSON-RPC 2.0 over newline-delimited stdio for
|
||||
ACP-compatible editor clients. The initial adapter implements the ACP baseline:
|
||||
|
||||
- `initialize`
|
||||
- `session/new`
|
||||
- `session/prompt`
|
||||
- `session/cancel`
|
||||
|
||||
Prompt requests are routed through the configured DeepSeek client and current
|
||||
default model. Responses are emitted as `session/update` agent message chunks
|
||||
followed by a `session/prompt` response with `stopReason: "end_turn"`.
|
||||
|
||||
The adapter is intentionally conservative: it does not yet expose shell tools,
|
||||
file-write tools, checkpoint replay, or session loading through ACP. Use
|
||||
`deepseek serve --http` for the full local runtime API and `deepseek serve --mcp`
|
||||
when another client needs DeepSeek's tools as MCP tools.
|
||||
|
||||
## Capability endpoint: `deepseek doctor --json`
|
||||
|
||||
Returns a JSON object describing the current installation's readiness state.
|
||||
|
||||
Reference in New Issue
Block a user