Prepare Python SDK public PyPI publication

This commit is contained in:
Yichen Jiang
2026-08-11 14:27:59 +08:00
parent 1d12ae62e7
commit 4445de9921
36 changed files with 874 additions and 127 deletions

View File

@@ -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/development.md
development.md: 9614c06436ab6863a5e1b2ff83fbe605552dc13b
development.zh.md: 1c646ca39735b85a5d380768fe215c92532be7e7
development.md: 31dc254b58c05c2a19c7c4cd5dc1e53207517902
development.zh.md: dbb85a0cffc5e06c7ca781b2b01f6993395204e3

View File

@@ -58,4 +58,12 @@ python scripts/build-python-release.py --package runtime --platform macos-arm64
pip install --find-links dist-python deepseek-harness-sdk=="$version"
```
The runtime distribution is wheel-only. The release pipeline publishes three platform wheels with the pure SDK wheel: Linux x64, Linux arm64, and macOS arm64. A `python-vX.Y.Z` tag is accepted only when it matches the repository version.
The runtime distribution is wheel-only. The release pipeline publishes three platform wheels with the pure SDK wheel: Linux x64, Linux arm64, and macOS 14 or newer on arm64. A `python-v<repository-version>` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata.
## Validate a release candidate
Label a pull request `python-release-dry-run`, or manually run the GitHub `Release (Python)` workflow with `publish=false`, to build all four wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. Both paths have no registry credentials; a pull request run cannot enter either publication job.
Public publication runs from the private automation repository; package metadata points to the separate read-only public source mirror, which does not run release Actions. The private repository defines the repository variable `PYPI_PUBLISHER_REPOSITORY` as its own `owner/name` and keeps `PUBLIC_PYPI_RELEASE_ENABLED=false` except during an intentional release.
Separate runtime and SDK jobs let an SDK upload failure resume without resending immutable runtime files. They accept `publish=true` only when the workflow runs from the configured publisher repository at the matching `python-v*` tag and the protected `pypi-runtime` and `pypi` environments approve the runtime and SDK jobs, respectively. PyPI Trusted Publishing still supplies short-lived OIDC credentials, but public attestations are disabled because they would disclose the private publisher identity.

View File

@@ -58,4 +58,12 @@ python scripts/build-python-release.py --package runtime --platform macos-arm64
pip install --find-links dist-python deepseek-harness-sdk=="$version"
```
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布三个平台 wheel 包Linux x64、Linux arm64 和 macOS arm64。只有与仓库版本匹配时才接受 `python-vX.Y.Z` 标签。
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布三个平台 wheel 包Linux x64、Linux arm64 和 macOS 14 或更高版本的 arm64。只有与仓库版本匹配时才接受 `python-v<repository-version>` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`
## 验证候选发行版
为拉取请求添加 `python-release-dry-run` 标签,或手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部四个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。两条路径都没有注册表凭据,拉取请求运行无法进入任何发布作业。
公开发布从私有自动化仓库运行;包元数据指向独立的只读公开源码镜像,该镜像不运行发布 Actions。私有仓库把仓库变量 `PYPI_PUBLISHER_REPOSITORY` 定义为自身的 `owner/name`,并且只在有意发布期间把 `PUBLIC_PYPI_RELEASE_ENABLED``false` 改为 `true`
独立的运行时与 SDK 作业使 SDK 上传失败后可以继续执行,而无需重新发送不可变的运行时文件。只有工作流从配置的发布仓库、匹配的 `python-v*` 标签运行,且受保护的 `pypi-runtime``pypi` 环境分别批准运行时与 SDK 作业时,才接受 `publish=true`。PyPI Trusted Publishing 仍会提供短期 OIDC 凭据,但公开 attestation 会披露私有发布仓库身份,因此将其禁用。

View File

@@ -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-runtime/README.md
README.md: fa2fc83a88212f6ff163e1a5f86246bfac37cc1f
README.zh.md: 5b82f33cfe1413e4fb6ceded04d9b6feca4c94ca
README.md: 0774ad206b366c59c40edf21c0cdb9b3993510f9
README.zh.md: bbe9f871b183bc83e1113594dfd29e1f81e06ac2

View File

@@ -11,11 +11,11 @@ Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injecte
- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`). macOS builds also ship the native `-spawn-helper` sibling that `node-pty` uses there. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists.
- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions.
Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding.
Both carriers hold the same content, defined once: the [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding.
A missing exe raises `FileNotFoundError` naming both acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. A missing dev-only node carrier names its sole route, the build script. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers.
Each wheel contains exactly one runtime executable. The macOS wheel also contains its matching native spawn helper; a missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple runtime files, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-vX.Y.Z` release tag must match it.
Each wheel contains exactly one runtime executable. The macOS wheel also contains its matching native spawn helper; a missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_14_0_arm64`; the macOS tag conservatively matches the bundled Node 24 executable's macOS 13.5 deployment target. The build hook rejects `py3-none-any`, absent or multiple runtime files, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-v<repository-version>` release tag must match it.
## Resolution API
@@ -26,4 +26,4 @@ Each wheel contains exactly one runtime executable. The macOS wheel also contain
## Zero-config design
The runtime binary always demands an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as an argv positional argument) and exits loudly without one — that hard semantic is part of the runtime's design and this package does not soften it. The bin (`dsh-jsonrpc-agent`) boots only the plugins the config lists; the serving interface (the stdio JSON-RPC server) is itself one of its entries (`@deepseek-ai/dsh-jsonrpc`), and without it the booted agent has no channel to the outside. This package checks in `runtime/cordis.yml` with the JSON-RPC serving entry, agent core, a preloaded DeepSeek adapter, JSONL persistence, the explicitly composed semantic checkpoint policy, local bash, and a local filesystem provider for bounded workspace-instruction loading. The persistence backend owns durable storage while the separate policy selects request-, tool-dispatch-, and completed-step checkpoints. The adapter reads `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL`, while persistence, bash, and the filesystem provider use `DSH_SESSION_ROOT` and `DSH_CWD` with manual-run fallbacks. When the caller uses no explicit config channel, the `deepseek_harness` client injects that file's path via `DSH_CORDIS_CONFIG` (injection conditions: [sdk README](../sdk/README.md)). Zero-config is thus an explicit, visible parameter pass in the wrapper, not a hidden fallback in the runtime.
The runtime binary always demands an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as an argv positional argument) and exits loudly without one — that hard semantic is part of the runtime's design and this package does not soften it. The bin (`dsh-jsonrpc-agent`) boots only the plugins the config lists; the serving interface (the stdio JSON-RPC server) is itself one of its entries (`@deepseek-ai/dsh-jsonrpc`), and without it the booted agent has no channel to the outside. This package checks in `runtime/cordis.yml` with the JSON-RPC serving entry, agent core, a preloaded DeepSeek adapter, JSONL persistence, the explicitly composed semantic checkpoint policy, local bash, and a local filesystem provider for bounded workspace-instruction loading. The persistence backend owns durable storage while the separate policy selects request-, tool-dispatch-, and completed-step checkpoints. The adapter reads `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL`, while persistence, bash, and the filesystem provider use `DSH_SESSION_ROOT` and `DSH_CWD` with manual-run fallbacks. When the caller uses no explicit config channel, the `deepseek_harness` client injects that file's path via `DSH_CORDIS_CONFIG` (injection conditions: [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md)). Zero-config is thus an explicit, visible parameter pass in the wrapper, not a hidden fallback in the runtime.

View File

@@ -11,11 +11,11 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`
- **exe生产**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>`platform`linux`/`macos`arch`x64`/`arm64`。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
- **node仅限开发**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。仅限开发的 node 载体缺失时只提示构建脚本这一条途径。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。
每个 wheel 包只包含一个运行时可执行文件。macOS wheel 包还包含与其匹配的原生 spawn helper缺少伴随文件意味着该安装不完整并会在启动时硬失败即使所选 Cordis 组合不使用 PTY 工具也是如此。Linux wheel 包不包含 spawn helper因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、不存在运行时文件、存在多个运行时文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-vX.Y.Z` 发布标签必须与其匹配。
每个 wheel 包只包含一个运行时可执行文件。macOS wheel 包还包含与其匹配的原生 spawn helper缺少伴随文件意味着该安装不完整并会在启动时硬失败即使所选 Cordis 组合不使用 PTY 工具也是如此。Linux wheel 包不包含 spawn helper因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_14_0_arm64`macOS 标签保守匹配内置 Node 24 可执行文件的 macOS 13.5 部署目标。构建钩子会拒绝 `py3-none-any`、不存在运行时文件、存在多个运行时文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-v<repository-version>` 发布标签必须与其匹配。
## 解析 API
@@ -26,4 +26,4 @@ exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deep
## 零配置设计
运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径缺了就报错退出——这一强制语义是运行时设计的一部分本包不会弱化它。bin`dsh-jsonrpc-agent`只启动配置里列出的插件对外服务接口stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-jsonrpc`),缺了它,启动出的 agent智能体就没有对外通道。本包检入的 `runtime/cordis.yml` 包含 JSON-RPC 服务条目、agent 核心、预载的 DeepSeek 适配器、JSONL 持久化、显式组合的语义检查点策略、本地 bash以及用于有界加载工作区指令的本地文件系统提供方。持久化后端负责持久存储独立的策略则选择请求、工具分发和已完成步骤的检查点。DeepSeek 适配器读取 `DEEPSEEK_API_KEY``DEEPSEEK_BASE_URL`持久化、bash 和文件系统提供方则使用 `DSH_SESSION_ROOT``DSH_CWD`,并为手动运行提供回退值。调用方未使用任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](../sdk/README.md))。因此,零配置是包装层中一次显式、可见的参数传递,而不是运行时中的隐藏回退。
运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径缺了就报错退出——这一强制语义是运行时设计的一部分本包不会弱化它。bin`dsh-jsonrpc-agent`只启动配置里列出的插件对外服务接口stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-jsonrpc`),缺了它,启动出的 agent智能体就没有对外通道。本包检入的 `runtime/cordis.yml` 包含 JSON-RPC 服务条目、agent 核心、预载的 DeepSeek 适配器、JSONL 持久化、显式组合的语义检查点策略、本地 bash以及用于有界加载工作区指令的本地文件系统提供方。持久化后端负责持久存储独立的策略则选择请求、工具分发和已完成步骤的检查点。DeepSeek 适配器读取 `DEEPSEEK_API_KEY``DEEPSEEK_BASE_URL`持久化、bash 和文件系统提供方则使用 `DSH_SESSION_ROOT``DSH_CWD`,并为手动运行提供回退值。调用方未使用任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md))。因此,零配置是包装层中一次显式、可见的参数传递,而不是运行时中的隐藏回退。

View File

@@ -11,7 +11,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
_PLATFORMS = {
"linux-x64": ("manylinux_2_28_x86_64", "dsh-jsonrpc-agent-pkg-linux-x64"),
"linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"),
"macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
"macos-arm64": ("macosx_14_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
}

View File

@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling>=1.30.1"]
requires = ["hatchling==1.30.1"]
build-backend = "hatchling.build"
[project]
@@ -8,7 +8,14 @@ version = "0.0.0.dev0"
description = "Pinned DeepSeek Harness runtime for the Python SDK"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "BSD-3-Clause" }
license = "BSD-3-Clause"
authors = [{ name = "DeepSeek" }]
[project.urls]
Homepage = "https://github.com/deepseek-ai/deepseek-harness"
Documentation = "https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md"
Issues = "https://github.com/deepseek-ai/deepseek-harness/issues"
Source = "https://github.com/deepseek-ai/deepseek-harness"
# Include the injected executable and default config; exclude the dev-only node
# closure from wheels and sdists.

View File

@@ -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

View File

@@ -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`.

View File

@@ -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` 的高层便捷选项。

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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")

View File

@@ -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()

View 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
View File

@@ -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]