mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Prepare Python SDK public PyPI publication
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write python/sdk/README.md
|
||||
README.md: 8cf366c27c8a604391ea85e298ba725e9987d428
|
||||
README.zh.md: a9258ce9aee9bce973107b49114d4ed6e81441e4
|
||||
README.md: 649dbed7afa0f11177f87984033aa58ac7d8ac2e
|
||||
README.zh.md: 75e6ba91152af527395fa53903e53fd0d056bdb5
|
||||
|
||||
@@ -40,12 +40,12 @@ with DeepSeekHarness(
|
||||
|
||||
`provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog.
|
||||
|
||||
The [Python SDK tutorial](../../docs/user/guide/python-sdk.md) uses a complete standalone Cordis file to demonstrate installation, direct SDK usage, and runs without the Web UI.
|
||||
The [Python SDK tutorial](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md) uses a complete standalone Cordis file to demonstrate installation, direct SDK usage, and runs without the Web UI.
|
||||
|
||||
`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, session_root)`. `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` in the interval, 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 runtime protocol and raises `SdkProtocolError`. Both result fields describe the owned interval rather than an output or ending causally assigned to the prompt. Steering, injected context, and other queued work may contribute before idle.
|
||||
|
||||
`HarnessClient` retains discovered subagent ancestry for the lifetime of the runtime process. During each `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and all known descendant notifications in wire order, including nested subagent lifecycle and session events. `RunResult.events` contains root-session events only, so descendant messages cannot replace the root response. The low-level `session_prompt()` returns the queued `MessageId` immediately; callers that bypass `Session.run()` own any later activity boundary themselves.
|
||||
|
||||
The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. The injection lives in `HarnessClient.start()`, so the low-level client's default launch gets it too: when the launch resolves to the bundled runtime and neither `cordis` nor a non-empty `DSH_CORDIS_CONFIG` is set (the runtime treats an empty value as absent, and so does the injection check), the bundled default configuration is used; an explicit `runtime_bin`, `bridge_bin`, or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](../sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them.
|
||||
The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. The injection lives in `HarnessClient.start()`, so the low-level client's default launch gets it too: when the launch resolves to the bundled runtime and neither `cordis` nor a non-empty `DSH_CORDIS_CONFIG` is set (the runtime treats an empty value as absent, and so does the injection check), the bundled default configuration is used; an explicit `runtime_bin`, `bridge_bin`, or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them.
|
||||
|
||||
`cwd` and `runtime_cwd` are resolved to absolute paths before subprocess launch, environment injection, and the wire handshake. The public API exposes only applied options: deployment persona and persistence belong in `cordis.yml`, while `session_root` remains the high-level convenience that sets `DSH_SESSION_ROOT`.
|
||||
|
||||
@@ -37,12 +37,12 @@ with DeepSeekHarness(
|
||||
|
||||
`provider` 用于选择当前 Cordis 组合已注册的提供方路由;`model` 是该适配器解析的模型 ID。`max_tokens` 是可选的正整数,用于限制根 agent(智能体)及其进程内后代每次请求的输出 token;省略时由提供方默认值控制。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方的凭据与端点,再选择 pi-ai 已安装目录中的任意提供方/模型组合。
|
||||
|
||||
[Python SDK 教程](../../docs/user/guide/python-sdk.md)使用完整的独立 Cordis 文件演示安装方式、直接调用 SDK,以及在不使用 Web UI 的情况下运行 agent。
|
||||
[Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)使用完整的独立 Cordis 文件演示安装方式、直接调用 SDK,以及在不使用 Web UI 的情况下运行 agent。
|
||||
|
||||
`Session.run()` 拥有一个从提示词进入持久 inbox 时开始、到整个 agent 下一次进入空闲状态为止的活动区间,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是该区间内根会话最后一个 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。两个结果字段描述的都是自有活动区间,而不是因果上归属于该提示词的输出或结束原因。steering(中途引导)、注入的上下文和其他排队工作都可能在进入空闲状态前参与其中。
|
||||
|
||||
`HarnessClient` 会在运行时进程的生命周期内保留已发现的 subagent(子 agent)祖先关系。每次执行 `Session.run()` 时,`RunResult.notifications` 与 `on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。
|
||||
|
||||
同样的行为也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程选定。注入逻辑位于 `HarnessClient.start()`,因此底层客户端的默认启动也具有此行为:当启动解析到内置运行时,且 `cordis` 与非空的 `DSH_CORDIS_CONFIG` 均未设置时(运行时把空值视为缺省,注入检查与之一致),使用内置的默认配置;显式给出 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](../sdk-runtime/README.md)。
|
||||
同样的行为也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程选定。注入逻辑位于 `HarnessClient.start()`,因此底层客户端的默认启动也具有此行为:当启动解析到内置运行时,且 `cordis` 与非空的 `DSH_CORDIS_CONFIG` 均未设置时(运行时把空值视为缺省,注入检查与之一致),使用内置的默认配置;显式给出 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md)。
|
||||
|
||||
`cwd` 与 `runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露真正生效的选项:部署的角色设定与持久化配置归 `cordis.yml` 管理,而 `session_root` 继续作为设置 `DSH_SESSION_ROOT` 的高层便捷选项。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.30.1"]
|
||||
requires = ["hatchling==1.30.1"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
@@ -8,12 +8,19 @@ version = "0.0.0.dev0"
|
||||
description = "Python SDK for DeepSeek Harness"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "BSD-3-Clause" }
|
||||
license = "BSD-3-Clause"
|
||||
authors = [{ name = "DeepSeek" }]
|
||||
dependencies = [
|
||||
"pydantic>=2.12",
|
||||
"pydantic>=2.12,<3",
|
||||
"deepseek-harness-runtime-bin==0.0.0.dev0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/deepseek-ai/deepseek-harness"
|
||||
Documentation = "https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md"
|
||||
Issues = "https://github.com/deepseek-ai/deepseek-harness/issues"
|
||||
Source = "https://github.com/deepseek-ai/deepseek-harness"
|
||||
|
||||
[dependency-groups]
|
||||
test = ["pytest>=8.0"]
|
||||
|
||||
|
||||
@@ -269,7 +269,11 @@ class HarnessClient:
|
||||
if remaining <= 0:
|
||||
with self._lock:
|
||||
self._responses.pop(request_id, None)
|
||||
raise TimeoutError(f"{method} timed out waiting for DeepSeek Harness runtime")
|
||||
diagnostics = self._runtime_diagnostics()
|
||||
suffix = f"\n{diagnostics}" if diagnostics else ""
|
||||
raise TimeoutError(
|
||||
f"{method} timed out waiting for DeepSeek Harness runtime{suffix}"
|
||||
)
|
||||
wait_timeout = remaining if wait_timeout is None else min(wait_timeout, remaining)
|
||||
try:
|
||||
item = waiter.get(timeout=wait_timeout)
|
||||
@@ -393,6 +397,11 @@ class HarnessClient:
|
||||
self._requests.put(exc)
|
||||
|
||||
def _runtime_closed_error(self, reason: str) -> TransportClosedError:
|
||||
diagnostics = self._runtime_diagnostics()
|
||||
return TransportClosedError(f"{reason}\n{diagnostics}" if diagnostics else reason)
|
||||
|
||||
def _runtime_diagnostics(self) -> str:
|
||||
"""Return available subprocess state for transport failures and timeouts."""
|
||||
proc = self._proc
|
||||
if (
|
||||
proc is not None
|
||||
@@ -403,14 +412,14 @@ class HarnessClient:
|
||||
):
|
||||
self._stderr_thread.join(timeout=0.1)
|
||||
|
||||
parts = [reason]
|
||||
parts: list[str] = []
|
||||
if proc is not None:
|
||||
exit_code = proc.poll()
|
||||
if exit_code is not None:
|
||||
parts.append(f"exit code: {exit_code}")
|
||||
if self._stderr_lines:
|
||||
parts.append("stderr tail:\n" + "\n".join(self._stderr_lines))
|
||||
return TransportClosedError("\n".join(parts))
|
||||
return "\n".join(parts)
|
||||
|
||||
def _default_launch_args(self) -> tuple[str, ...]:
|
||||
if self.config.runtime_bin is not None:
|
||||
|
||||
@@ -721,8 +721,10 @@ def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -
|
||||
script = tmp_path / "fake_bridge.py"
|
||||
script.write_text(
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
|
||||
print("bridge is still starting", file=sys.stderr, flush=True)
|
||||
time.sleep(60)
|
||||
""".strip()
|
||||
)
|
||||
@@ -736,8 +738,9 @@ time.sleep(60)
|
||||
start = time.monotonic()
|
||||
try:
|
||||
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
||||
except TimeoutError:
|
||||
except TimeoutError as exc:
|
||||
assert time.monotonic() - start < 2
|
||||
assert "bridge is still starting" in str(exc)
|
||||
else:
|
||||
raise AssertionError("initialize should time out")
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ def test_pep440_version_spells_a_prerelease_the_python_way() -> None:
|
||||
build_python_release.pep440_version("1.2.3-nightly")
|
||||
|
||||
|
||||
def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None:
|
||||
assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64"
|
||||
|
||||
|
||||
def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: Path) -> None:
|
||||
destination = tmp_path / "staging"
|
||||
|
||||
@@ -66,6 +70,8 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path:
|
||||
assert 'name = "deepseek-harness-sdk"' in pyproject
|
||||
assert 'version = "1.2.3"' in pyproject
|
||||
assert '"deepseek-harness-runtime-bin==1.2.3"' in pyproject
|
||||
assert 'license-files = ["LICENSE"]' in pyproject
|
||||
assert (destination / "LICENSE").read_bytes() == (ROOT / "LICENSE").read_bytes()
|
||||
assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@@ -88,3 +94,9 @@ def test_stage_runtime_copies_platform_payload(
|
||||
|
||||
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
||||
assert {path.name: path.read_bytes() for path in runtime_dir.glob("dsh-jsonrpc-agent-pkg-*")} == expected
|
||||
pyproject = (destination / "pyproject.toml").read_text()
|
||||
assert 'license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]' in pyproject
|
||||
assert (destination / "LICENSE").read_bytes() == (ROOT / "LICENSE").read_bytes()
|
||||
assert (destination / "THIRD_PARTY_NOTICES.md").read_bytes() == (
|
||||
ROOT / "THIRD_PARTY_NOTICES.md"
|
||||
).read_bytes()
|
||||
|
||||
32
python/sdk/tests/test_smoke_model.py
Normal file
32
python/sdk/tests/test_smoke_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SMOKE = runpy.run_path(ROOT / "scripts" / "smoke-python-runtime.py")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("prompt_name", "expected"),
|
||||
[
|
||||
("SNAPSHOT_DIRECT_CHILD_PROMPT", "DIRECT_CHILD_OK"),
|
||||
("SNAPSHOT_WORKFLOW_CHILD_PROMPT", "WORKFLOW_CHILD_OK"),
|
||||
],
|
||||
)
|
||||
def test_child_prompt_precedes_runtime_context(prompt_name: str, expected: str) -> None:
|
||||
chunks = SMOKE["completion_chunks"]({
|
||||
"messages": [
|
||||
{"role": "user", "content": SMOKE[prompt_name]},
|
||||
{"role": "user", "content": "Current runtime context"},
|
||||
],
|
||||
})
|
||||
|
||||
assert any(
|
||||
choice.get("delta", {}).get("content") == expected
|
||||
for chunk in chunks
|
||||
for choice in chunk.get("choices", [])
|
||||
)
|
||||
2
python/sdk/uv.lock
generated
2
python/sdk/uv.lock
generated
@@ -42,7 +42,7 @@ test = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "deepseek-harness-runtime-bin", editable = "../sdk-runtime" },
|
||||
{ name = "pydantic", specifier = ">=2.12" },
|
||||
{ name = "pydantic", specifier = ">=2.12,<3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
Reference in New Issue
Block a user