mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
feat(sdk): add explicit persisted session resume
This commit is contained in:
1 parent
56bdf15b4f
commit
0bec9479ae
26 files changed
+495
-25
No files matched your search
@@ -12,8 +12,8 @@
|
||||
en: 9a069c1a7cbb0779
|
||||
zh: 7f625a802cd1e9ea
|
||||
/deepseek-harness-python-sdk/customize-plugins:
|
||||
en: d32ee602f121e6af
|
||||
zh: fdea4487688e5774
|
||||
en: a6f2fcfa7ba8a050
|
||||
zh: 60fdca6dc0594c79
|
||||
/deepseek-harness-python-sdk/results-and-notifications:
|
||||
en: 173d2dc1ce63fe2d
|
||||
zh: 5354e18c59f20878
|
||||
@@ -79,6 +79,14 @@ with DeepSeekHarness(
|
||||
|
||||
The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides only a platform-selected persistent shell, local execution, and JSONL sessions; filesystem tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles.
|
||||
|
||||
`harness.resume_session(saved_session_id)` restores persisted history and returns a `Session` ready for `run()`. Save the original session id and reuse its `dsh_home` and workspace. Restoration preserves the stored directory, repairs an interrupted final turn, and sends no new input. Missing, unreadable, incompatible, or occupied sessions raise without creating a replacement. `start_session(id)` only opens a lazy handle; it does not request persisted restoration. The low-level client also exposes `resume_session(id)`, returning the restored identity. This API does not restore shell, browser, or other external processes.
|
||||
|
||||
```py
|
||||
with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
|
||||
session = harness.resume_session(saved_session_id)
|
||||
result = session.run("Continue the task.")
|
||||
```
|
||||
|
||||
## Results and notifications
|
||||
|
||||
`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end`, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the protocol and raises `SdkProtocolError`.
|
||||
|
||||
@@ -79,6 +79,14 @@ with DeepSeekHarness(
|
||||
|
||||
随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 ID。它只提供按平台选择的持久 shell、本地执行与 JSONL 会话;文件系统工具、设置、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。
|
||||
|
||||
`harness.resume_session(saved_session_id)` 恢复持久化历史,并返回可调用 `run()` 的 `Session`。请保存原会话 id,并沿用其 `dsh_home` 和工作区。恢复会保留存储的目录、修复最后一个被中断的轮次,且不发送新输入。会话缺失、不可读、不兼容或被占用时会抛出异常,不创建替代会话。`start_session(id)` 只打开惰性句柄,不会请求持久化恢复。底层客户端也提供 `resume_session(id)`,返回恢复后的标识。此 API 不恢复 shell、浏览器或其他外部进程。
|
||||
|
||||
```py
|
||||
with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
|
||||
session = harness.resume_session(saved_session_id)
|
||||
result = session.run("Continue the task.")
|
||||
```
|
||||
|
||||
## 结果与通知
|
||||
|
||||
`Session.run()` 的活动区间从提示词被持久 inbox 接收时开始,到整个 agent 下一次进入空闲状态时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是最后一个根会话 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反协议,并会抛出 `SdkProtocolError`。
|
||||
|
||||
@@ -121,6 +121,15 @@ class DeepSeekHarness:
|
||||
self.start()
|
||||
return Session(self, session_id or f"session-{uuid.uuid4().hex}")
|
||||
|
||||
def resume_session(self, session_id: str) -> "Session":
|
||||
"""Restore persisted history from the configured Harness home without sending input.
|
||||
|
||||
Missing, unreadable, or occupied sessions raise instead of creating a new session.
|
||||
"""
|
||||
self.start()
|
||||
restored_id = self._client.resume_session(session_id)
|
||||
return Session(self, restored_id)
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: str | list[JsonObject],
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import Callable, TypeAlias, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .errors import JsonRpcError, TransportClosedError
|
||||
from .errors import JsonRpcError, SdkProtocolError, TransportClosedError
|
||||
from .models import IncomingRequest, InitializeResponse, JsonObject, JsonValue, Notification
|
||||
|
||||
ModelT = TypeVar("ModelT", bound=BaseModel)
|
||||
@@ -188,6 +188,17 @@ class HarnessClient:
|
||||
)
|
||||
return response.messageId
|
||||
|
||||
def resume_session(self, session_id: str) -> str:
|
||||
"""Restore a persisted session without sending a prompt; propagate restoration failures."""
|
||||
response = self.request(
|
||||
"session/resume",
|
||||
{"sessionId": session_id},
|
||||
response_model=_SessionResumeResponse,
|
||||
)
|
||||
if response.sessionId != session_id:
|
||||
raise SdkProtocolError("session/resume returned an unexpected session id")
|
||||
return response.sessionId
|
||||
|
||||
def get_working_directory(self, session_id: str) -> str:
|
||||
"""Read the effective directory, recovering a missing directory to the Session origin."""
|
||||
response = self.request(
|
||||
@@ -599,6 +610,10 @@ class _SessionPromptResponse(BaseModel):
|
||||
messageId: str
|
||||
|
||||
|
||||
class _SessionResumeResponse(BaseModel):
|
||||
sessionId: str
|
||||
|
||||
|
||||
class _WorkingDirectoryResponse(BaseModel):
|
||||
cwd: str
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from deepseek_harness import DeepSeekHarness, SdkProtocolError
|
||||
from deepseek_harness.errors import JsonRpcError
|
||||
|
||||
|
||||
@pytest.mark.parametrize("outcome", ["restored", "missing", "wrong-id"])
|
||||
def test_resume_checks_identity_and_propagates_failure(tmp_path: Path, outcome: str) -> None:
|
||||
script = tmp_path / "runtime.py"
|
||||
requests = tmp_path / "requests.jsonl"
|
||||
script.write_text(
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
frame = json.loads(line)
|
||||
with open(os.environ["REQUESTS"], "a") as log:
|
||||
log.write(line)
|
||||
method = frame["method"]
|
||||
response = {"jsonrpc": "2.0", "id": frame["id"]}
|
||||
if method == "initialize":
|
||||
response["result"] = {"serverInfo": {"name": "fake-runtime"}}
|
||||
elif method == "session/resume":
|
||||
outcome = os.environ["OUTCOME"]
|
||||
if outcome == "missing":
|
||||
response["error"] = {"code": -32603, "message": "session not found"}
|
||||
else:
|
||||
response["result"] = {"sessionId": "saved" if outcome == "restored" else "wrong"}
|
||||
else:
|
||||
response["result"] = {}
|
||||
print(json.dumps(response), flush=True)
|
||||
if method == "shutdown":
|
||||
break
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with DeepSeekHarness(
|
||||
_launch_args=(sys.executable, str(script)),
|
||||
env={"OUTCOME": outcome, "REQUESTS": str(requests)},
|
||||
) as harness:
|
||||
if outcome == "restored":
|
||||
session = harness.resume_session("saved")
|
||||
assert session.id == "saved"
|
||||
assert session.harness is harness
|
||||
elif outcome == "missing":
|
||||
with pytest.raises(JsonRpcError, match="session not found"):
|
||||
harness.resume_session("saved")
|
||||
else:
|
||||
with pytest.raises(SdkProtocolError, match="unexpected session id"):
|
||||
harness.resume_session("saved")
|
||||
frames = [json.loads(line) for line in requests.read_text(encoding="utf-8").splitlines()]
|
||||
assert [frame["method"] for frame in frames] == ["initialize", "session/resume", "shutdown"]
|
||||
assert frames[1]["params"] == {"sessionId": "saved"}
|
||||
Reference in new issue
Block a user