jsonrpc: harden Python SDK lifecycle and protocol

This commit is contained in:
Yichen Jiang
2026-07-13 16:34:03 +08:00
parent 7696f88d9f
commit 5d913a796a
7 changed files with 198 additions and 66 deletions

View File

@@ -23,14 +23,11 @@ class DeepSeekHarnessConfig:
runtime_cwd: str | None = None
session_root: str | None = None
cordis: str | None = None
system_prompt: str | None = None
env: dict[str, str] = field(default_factory=dict)
runtime_bin: str | None = None
launch_args_override: tuple[str, ...] | None = None
request_timeout_seconds: float | None = None
shutdown_timeout_seconds: float | None = 1.0
client_name: str = "deepseek_harness_python_sdk"
client_version: str = "0.0.0-dev"
base_url: str | None = None
api_key: str | None = None
@@ -52,8 +49,9 @@ class DeepSeekHarness:
if config is not None and kwargs:
raise TypeError("pass either DeepSeekHarnessConfig or keyword options, not both")
self.config = config or DeepSeekHarnessConfig(**kwargs)
cwd = self.config.cwd or str(Path.cwd())
runtime_cwd = self.config.runtime_cwd or cwd
cwd = str(Path(self.config.cwd or Path.cwd()).resolve())
runtime_cwd = str(Path(self.config.runtime_cwd).resolve()) if self.config.runtime_cwd is not None else cwd
self._cwd = cwd
env = dict(self.config.env)
if self.config.session_root is not None:
env["DSH_SESSION_ROOT"] = self.config.session_root
@@ -73,8 +71,6 @@ class DeepSeekHarness:
env=env,
request_timeout_seconds=self.config.request_timeout_seconds,
shutdown_timeout_seconds=self.config.shutdown_timeout_seconds,
client_name=self.config.client_name,
client_version=self.config.client_version,
)
)
self._initialized = False
@@ -95,10 +91,8 @@ class DeepSeekHarness:
return
self._client.start()
self._client.initialize(
cwd=self.config.cwd or str(Path.cwd()),
cwd=self._cwd,
model=self.config.model,
session_root=self.config.session_root,
system_prompt=self.config.system_prompt,
)
self._initialized = True
@@ -115,10 +109,9 @@ class DeepSeekHarness:
input: str | list[JsonObject],
*,
session_id: str | None = None,
profile: str | None = None,
on_notification: Callable[[Notification], None] | None = None,
) -> TurnResult:
return self.start_session(session_id).run(input, profile=profile, on_notification=on_notification)
return self.start_session(session_id).run(input, on_notification=on_notification)
class Session:
@@ -130,7 +123,6 @@ class Session:
self,
input: str | list[JsonObject],
*,
profile: str | None = None,
on_notification: Callable[[Notification], None] | None = None,
) -> TurnResult:
content_blocks = normalize_input(input)
@@ -156,7 +148,6 @@ class Session:
self.harness.client.session_prompt(
self.id,
content_blocks,
profile=profile,
on_notification=collect,
notification_subscription=subscription,
)

View File

@@ -9,6 +9,7 @@ import time
import uuid
from collections import deque
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Literal, TypeAlias, TypeVar
from pydantic import BaseModel
@@ -31,8 +32,6 @@ class HarnessConfig:
env: dict[str, str] | None = None
request_timeout_seconds: float | None = None
shutdown_timeout_seconds: float | None = 1.0
client_name: str = "deepseek_harness_python_sdk"
client_version: str = "0.0.0-dev"
class HarnessClient:
@@ -75,7 +74,7 @@ class HarnessClient:
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
cwd=self.config.cwd,
cwd=None if self.config.cwd is None else str(Path(self.config.cwd).resolve()),
env=env,
bufsize=1,
)
@@ -90,18 +89,22 @@ class HarnessClient:
self.request("shutdown", None, response_model=_ShutdownResponse, timeout_seconds=self.config.shutdown_timeout_seconds)
except Exception as exc:
self._stderr_lines.append(f"shutdown request failed: {exc}")
self._proc = None
if proc.stdin:
try:
proc.stdin.close()
except Exception as exc:
self._stderr_lines.append(f"stdin close failed: {exc}")
try:
if proc.poll() is None:
if proc.poll() is None:
try:
proc.terminate()
proc.wait(timeout=2)
except Exception:
except ProcessLookupError:
pass
try:
proc.wait(timeout=self.config.shutdown_timeout_seconds)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
self._proc = None
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime closed"))
if self._reader_thread and self._reader_thread.is_alive():
self._reader_thread.join(timeout=0.5)
@@ -113,35 +116,26 @@ class HarnessClient:
*,
cwd: str,
model: str,
session_root: str | None = None,
system_prompt: str | None = None,
) -> InitializeResponse:
payload: JsonObject = {
"clientInfo": {
"name": self.config.client_name,
"version": self.config.client_version,
},
"cwd": cwd,
"cwd": str(Path(cwd).resolve()),
"model": model,
}
if session_root is not None:
payload["sessionRoot"] = session_root
if system_prompt is not None:
payload["systemPrompt"] = system_prompt
return self.request("initialize", payload, response_model=InitializeResponse)
try:
return self.request("initialize", payload, response_model=InitializeResponse)
except BaseException:
self.close()
raise
def session_prompt(
self,
session_id: str,
content_blocks: list[JsonObject],
*,
profile: str | None = None,
on_notification: Callable[[Notification], None] | None = None,
notification_subscription: "NotificationSubscription | None" = None,
) -> None:
payload: JsonObject = {"sessionId": session_id, "contentBlocks": content_blocks}
if profile is not None:
payload["profile"] = profile
self.request(
"session/prompt",
payload,