Merge remote-tracking branch 'origin/master' into codex/rfc-subagent-background-tasks

# Conflicts:
#	docs/event-producer-consumer.md
#	packages/bash/tool-bash/tests/tools.spec.ts
This commit is contained in:
Yichen Jiang
2026-07-14 09:59:21 +08:00
127 changed files with 11195 additions and 543 deletions
+6
View File
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
README.md: 35ed645ce0e4d5e4c88c8aae2fe33d94aea79b3e
README.zh.md: 214c5cd1900e52f9fe479247f9940a97bb22766b
+76
View File
@@ -0,0 +1,76 @@
# DeepSeek Harness Python SDK
English | [中文](README.zh.md)
Python packages for driving DeepSeek Harness as a subprocess: a client SDK that spawns the `dsh-jsonrpc-agent` binary and talks newline-delimited JSON-RPC over stdio. The runtime carrier is the single-file executable produced by this repo; design, build, and acceptance details live in [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
## Packages
| Directory | Dist / module | Role |
|---|---|---|
| [sdk](sdk/) | `deepseek-harness` / `deepseek_harness` | Client SDK: the `DeepSeekHarness` high-level turns API and the lower-level `HarnessClient` JSON-RPC client |
| [sdk-runtime](sdk-runtime/) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Runtime carrier: locates the bundled runtime binaries and ships the default agent configuration |
## Building the runtime executable
The platform executables are build artifacts, not checked into git. From the repo root:
```sh
pnpm install
pnpm exec tsx scripts/build-exe-for-python-sdk.ts # host platform, ~2 min
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifacts already built
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64
```
Products land in `dist-exe/` and are synced into this package at `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`) — after a local build the SDK finds the executable with no further setup. The `build-exe-for-python-sdk` CI workflow (manual dispatch, or the `build-exe` PR label) exercises the same binaries but retains only the four release wheels. Which plugins the exe bundles and how the carriers are organized: [sdk-runtime README](sdk-runtime/README.md); the build also refreshes the dev-only node carrier (see "against the Node source" below).
## Validating the SDK against the executable
```sh
export UV_PROJECT_ENVIRONMENT="$PWD/tmp/py-sdk-venv" # keep the venv out of python/
uv sync --project python/sdk --group test
uv run --project python/sdk pytest python/sdk/tests/test_bundled_runtime.py # boots the real carriers
uv run --project python/sdk pytest # full suite; keyless tests included
```
For an interactive check (needs `DEEPSEEK_API_KEY` in the environment or the repo-root `.env`):
```python
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness() as harness:
print(harness.run("say hi").final_response) # auto-resolution picks the bundled exe
```
## Running the SDK against the Node source (no executable)
Two flavors, both for repo members:
- **Built node carrier** — set `DSH_RUNTIME_MODE=node` and the SDK runs `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` on the system Node (>= 22.19). The tree is refreshed on every build-script run and is the same dependency closure the exe snapshots, so plugin semantics are identical. Never auto-selected, never distributed.
- **Unbuilt source (tsx)** — point the client straight at the bin's TypeScript source for edit-run loops and debugging: `launch_args_override=("./node_modules/.bin/tsx", "packages/ui/jsonrpc-agent/src/bin.ts")` with `cwd` at the repo root, plus a config via `cordis=...` (or rely on the default-config injection). [sdk/tests/manual_sdk_agent_smoke.py](sdk/tests/manual_sdk_agent_smoke.py) is the worked example.
## Distributing the Python packages
The root [`package.json`](../package.json) version is authoritative for both Python distributions. The common staging script reads that version, injects it into both wheels, and pins the SDK metadata to the same `deepseek-harness-runtime-bin==X.Y.Z`; an optional `python-vX.Y.Z` release tag is accepted only when it matches the repository version. Build the pure SDK wheel once and one runtime wheel on each native platform:
```sh
version="$(node -p "require('./package.json').version")"
python scripts/build-python-release.py --package sdk --output-dir dist-python
python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
pip install --find-links dist-python deepseek-harness=="$version"
```
The runtime distribution is wheel-only and rejects sdist builds, missing executables, and mixed-platform payloads. Its three wheel tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the SDK remains `py3-none-any`. A matching `python-vX.Y.Z` tag pipeline builds these four non-conflicting files and publishes them together, so a normal `pip install deepseek-harness==X.Y.Z` selects the matching runtime wheel and `import deepseek_harness` needs no `runtime_bin`.
## Zero-config semantics
The runtime binary itself always requires an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as the first argv argument), has no built-in fallback, and boots only what the config lists. Zero-config is SDK wrapper behavior: when the caller uses no explicit channel, the client injects the runtime package's checked-in default configuration ([runtime/cordis.yml](sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml)) via `DSH_CORDIS_CONFIG`; any explicit channel wins and disables the injection. The full injection conditions live in the [sdk README](sdk/README.md); the default config's contents and the hard semantic in the [sdk-runtime README](sdk-runtime/README.md).
The executable is also a supported direct interface; keep stdin open for the NDJSON JSON-RPC exchange and supply a config explicitly:
```sh
DSH_CORDIS_CONFIG=/absolute/path/cordis.yml ./dsh-jsonrpc-agent-pkg-macos-arm64
```
## Test layout
`test_client.py` is fully keyless (a Python fake runtime is the peer). `test_bundled_runtime.py` boots each bundled carrier and skips per carrier when its artifact is missing. `test_runtime_resolution.py` covers the carrier-resolution rules without spawning anything.
+76
View File
@@ -0,0 +1,76 @@
# DeepSeek Harness Python SDK
[English](README.md) | 中文
以子进程方式驱动 DeepSeek Harness 的 Python 包:客户端 SDK spawn `dsh-jsonrpc-agent` 二进制,并通过 stdio 上按行分隔的 JSON-RPC 与之通信。运行时载体是本仓库产出的单文件可执行文件;设计、构建与验收细节见 [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)。
## 包
| 目录 | 分发名 / 模块 | 职责 |
|---|---|---|
| [sdk](sdk/) | `deepseek-harness` / `deepseek_harness` | 客户端 SDK:高层回合 API `DeepSeekHarness` 与低层 JSON-RPC 客户端 `HarnessClient` |
| [sdk-runtime](sdk-runtime/) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 运行时载体:定位内置的运行时二进制,并携带默认的 agent(智能体)配置 |
## 构建运行时可执行文件
各平台可执行文件是构建产物,不检入 git。在仓库根目录执行:
```sh
pnpm install
pnpm exec tsx scripts/build-exe-for-python-sdk.ts # host platform, ~2 min
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build # lib/ artifacts already built
pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64
```
产物落入 `dist-exe/`,并同步进本包的 `sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-<platform>-<arch>`platform`linux`/`macos`arch`x64`/`arm64`),本地构建完成后 SDK 不需要额外设置就能找到可执行文件。`build-exe-for-python-sdk` CI 工作流(手动触发,或给 PR 打 `build-exe` 标签)会测试同样的二进制,但只保留 4 个发布用 wheel 包。exe 内置哪些插件、载体如何组织,见 [sdk-runtime README](sdk-runtime/README.md);构建还会顺带刷新仅供开发使用的 `node` 载体(见下文「对着 Node 源码运行」)。
## 用可执行文件验证 SDK
```sh
export UV_PROJECT_ENVIRONMENT="$PWD/tmp/py-sdk-venv" # keep the venv out of python/
uv sync --project python/sdk --group test
uv run --project python/sdk pytest python/sdk/tests/test_bundled_runtime.py # boots the real carriers
uv run --project python/sdk pytest # full suite; keyless tests included
```
交互式验证(需要环境变量或仓库根 `.env` 中的 `DEEPSEEK_API_KEY`):
```python
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness() as harness:
print(harness.run("say hi").final_response) # auto-resolution picks the bundled exe
```
## 对着 Node 源码运行 SDK(不用可执行文件)
两种方式,均面向仓库成员:
- **已构建的 `node` 载体**——设置 `DSH_RUNTIME_MODE=node`SDK 会用系统 Node>= 22.19)运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`。这棵树每次运行构建脚本都会刷新,与 exe 打入 pkg 虚拟文件系统(VFS)的是同一份依赖闭包,因此插件语义一致。它不会被自动选中,也不进入分发物。
- **未构建的源码(tsx)**——把客户端直接指向 `bin` 的 TypeScript 源码,用于编辑、运行和调试:`launch_args_override=("./node_modules/.bin/tsx", "packages/ui/jsonrpc-agent/src/bin.ts")``cwd` 设为仓库根,再通过 `cordis=...` 传入配置(或使用默认配置注入)。[sdk/tests/manual_sdk_agent_smoke.py](sdk/tests/manual_sdk_agent_smoke.py) 是现成范例。
## 分发 Python 包
根目录 [`package.json`](../package.json) 的版本是两个 Python 分发物的权威版本。统一暂存脚本读取这个版本并注入两个 wheel 包,同时在 SDK 元数据中钉死相同版本的 `deepseek-harness-runtime-bin==X.Y.Z`;可选的 `python-vX.Y.Z` 发布标签只有与仓库版本匹配时才会被接受。纯 SDK wheel 包只构建一次,运行时 wheel 包则在每个原生平台各构建一个:
```sh
version="$(node -p "require('./package.json').version")"
python scripts/build-python-release.py --package sdk --output-dir dist-python
python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
pip install --find-links dist-python deepseek-harness=="$version"
```
运行时分发物只提供 wheel 包,并拒绝 sdist 构建、缺失可执行文件以及混合平台载荷。三个 wheel 包标签分别是 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64`SDK 保持 `py3-none-any`。匹配的 `python-vX.Y.Z` 标签流水线统一构建并发布这 4 个互不冲突的文件,因此常规的 `pip install deepseek-harness==X.Y.Z` 会选中匹配平台的运行时 wheel 包,`import deepseek_harness` 不需要 `runtime_bin`
## 零配置语义
运行时二进制本身始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为首个 argv 参数的配置路径),没有内置兜底,也只启动配置里列出的内容。零配置是 SDK 包装层的行为:调用方没有使用任何显式通道时,客户端把运行时包检入的默认配置([runtime/cordis.yml](sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml))注入 `DSH_CORDIS_CONFIG`;任一显式通道存在即优先采用,并禁用注入。注入条件的完整定义见 [sdk README](sdk/README.md),默认配置的内容与硬语义见 [sdk-runtime README](sdk-runtime/README.md)。
可执行文件也支持直接调用;在 NDJSON JSON-RPC 交互期间保持 stdin 打开,并显式提供配置:
```sh
DSH_CORDIS_CONFIG=/absolute/path/cordis.yml ./dsh-jsonrpc-agent-pkg-macos-arm64
```
## 测试布局
`test_client.py` 完全无需密钥(对端是 Python 假运行时)。`test_bundled_runtime.py` 逐个启动内置载体,某个载体产物缺失时跳过对应用例。`test_runtime_resolution.py` 覆盖载体解析规则,不 spawn 任何进程。
+6
View File
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
README.md: 4beac57526761bb150e90b60f0030ed311c4034d
README.zh.md: c0cc0eef6a9b569105408d2f2e6321795493036a
+29
View File
@@ -0,0 +1,29 @@
# DeepSeek Harness Runtime Wheel
English | [中文](README.zh.md)
Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, module `deepseek_harness_runtime`): it locates the bundled runtime binaries the `deepseek-harness` client spawns, and ships the default configuration behind zero-config runs.
## Runtime carriers
Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored:
- **exe (production)** — single-file executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`). No Node installation 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-agent/lib/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.
Missing carriers raise `FileNotFoundError` naming the 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. 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 executable. 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 executables, 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.
## Resolution API
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` — the argv tuple that launches the bundled runtime: `(exe_path,)` in exe mode, `(node_path, bin_js_path)` in node mode. Mode selection: explicit argument > `DSH_RUNTIME_MODE` env var (`exe` | `node`) > automatic. Automatic resolution finds the production exe ONLY — the dev-only node carrier must be opted into explicitly so a production deployment can never silently ride on a source build.
- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only; the node carrier has no single-path equivalent and launches via the argv tuple above).
- `bundled_default_config_path() -> Path` — the checked-in default config (see below).
- `bundled_package_dir() -> Path` — the installed package data root.
## 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 surface (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` (the JSON-RPC serving entry, agent core, preloaded DeepSeek adapter, JSONL session persistence, local bash, each parameterized by the `DSH_*` env vars the SDK sets); 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.
+29
View File
@@ -0,0 +1,29 @@
# DeepSeek Harness 运行时 wheel 包
[English](README.md) | 中文
Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,模块名 `deepseek_harness_runtime`):它定位 `deepseek-harness` 客户端要 spawn 的内置运行时二进制,并附带支撑零配置运行的默认配置。
## 运行时载体
两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略:
- **exe(生产)**——单文件可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>`platform`linux`/`macos`arch`x64`/`arm64`)。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
- **`node`(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。
每个 wheel 包只包含一个可执行文件。固定标签为 `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` 发布标签必须与其匹配。
## 解析 API
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]`——启动内置运行时的 argv 元组:exe 模式下为 `(exe_path,)``node` 模式下为 `(node_path, bin_js_path)`。模式选择:显式参数 > `DSH_RUNTIME_MODE` 环境变量(`exe` | `node`)> 自动。自动解析只找生产 exe——仅限开发的 `node` 载体必须显式选用,从而生产部署绝不会悄悄跑在源码构建上。
- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体;`node` 载体没有单一路径的等价物,经由上面的 argv 元组启动)。
- `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。
- `bundled_package_dir() -> Path`——已安装包的数据根目录。
## 零配置设计
运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径),缺了就报错退出——这一硬语义是运行时设计的一部分,本包不软化它。`bin``dsh-jsonrpc-agent`)只启动配置里列出的插件;对外服务接口(stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-jsonrpc`),缺了它,启动出的 agent 就没有对外通道。本包检入 `runtime/cordis.yml`JSON-RPC 服务条目、`agent-core`、预载的 DeepSeek 适配器、JSONL 会话持久化、本地 bash,各项由 SDK 设置的 `DSH_*` 环境变量参数化);调用方未使用任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](../sdk/README.md))。因此,零配置是包装层中一次显式、可见的参数传递,而不是运行时中的隐藏回退。
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
import os
import platform
import stat
from pathlib import Path
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"),
}
def _host_platform_tag() -> str:
machine = platform.machine().lower()
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
system = platform.system().lower()
key = f"macos-{arch}" if system == "darwin" else f"linux-{arch}" if system == "linux" else system
try:
return _PLATFORMS[key][0]
except KeyError as exc:
raise RuntimeError(f"unsupported deepseek-harness-runtime-bin build platform: {key}") from exc
class RuntimeBuildHook(BuildHookInterface):
"""Assign the native wheel tag and reject incomplete or mixed-platform payloads."""
def initialize(self, version: str, build_data: dict[str, object]) -> None:
if version == "editable":
return
if self.target_name == "sdist":
raise RuntimeError(
"deepseek-harness-runtime-bin is wheel-only; build and publish platform wheels only."
)
platform_tag = os.environ.get("DSH_RUNTIME_PLATFORM_TAG") or _host_platform_tag()
matches = [value for value in _PLATFORMS.values() if value[0] == platform_tag]
if len(matches) != 1:
supported = ", ".join(value[0] for value in _PLATFORMS.values())
raise RuntimeError(
f"unsupported DSH_RUNTIME_PLATFORM_TAG {platform_tag!r}; expected one of {supported}"
)
expected_executable = matches[0][1]
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
executables = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
if [path.name for path in executables] != [expected_executable]:
found = ", ".join(path.name for path in executables) or "none"
raise RuntimeError(
f"runtime wheel {platform_tag} must contain only {expected_executable}; found {found}"
)
if executables[0].stat().st_mode & stat.S_IXUSR == 0:
raise RuntimeError(f"runtime executable is not executable: {executables[0]}")
build_data["pure_python"] = False
build_data["infer_tag"] = False
build_data["tag"] = f"py3-none-{platform_tag}"
+76
View File
@@ -0,0 +1,76 @@
{
"name": "dsh-jsonrpc-agent-pkg",
"description": "Deploy root of the single-exe pipeline and the single source of truth unifying 'which plugins the exe bundles' and 'what the Python runtime distributes': the dependency list below IS the exe closure. Pure manifest — no code; a deploy materializes only this package.json plus node_modules.",
"version": "0.0.1",
"private": true,
"type": "module",
"dependencies": {
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@cordisjs/plugin-timer": "workspace:^",
"@deepseek-ai/dsh-acp": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-core": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-compact-basic": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-fs-policy": "workspace:^",
"@deepseek-ai/dsh-hook-protocol": "workspace:^",
"@deepseek-ai/dsh-hooks-claude": "workspace:^",
"@deepseek-ai/dsh-hooks-codex": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-jsonrpc": "workspace:^",
"@deepseek-ai/dsh-jsonrpc-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-subprocess": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
"@deepseek-ai/dsh-tool-fs": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
"@deepseek-ai/dsh-tool-web": "workspace:^",
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-web": "workspace:^",
"@deepseek-ai/dsh-web-fetch-local": "workspace:^",
"@deepseek-ai/dsh-web-search-deepseek": "workspace:^",
"@deepseek-ai/dsh-web-search-exa": "workspace:^",
"@deepseek-ai/dsh-web-search-perplexity": "workspace:^",
"@deepseek-ai/dsh-workflow": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"cordis": "workspace:^"
}
}
+25
View File
@@ -0,0 +1,25 @@
[build-system]
requires = ["hatchling>=1.24.0"]
build-backend = "hatchling.build"
[project]
name = "deepseek-harness-runtime-bin"
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" }
# Distributions carry the platform executables (build-injected, VCS-ignored —
# hence `artifacts`) and the checked-in runtime/cordis.yml; the dev-only node
# closure under runtime/node/ is explicitly excluded from wheel and sdist.
[tool.hatch.build]
artifacts = ["src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*"]
exclude = ["src/deepseek_harness_runtime/runtime/node"]
[tool.hatch.build.targets.wheel]
packages = ["src/deepseek_harness_runtime"]
[tool.hatch.build.targets.wheel.hooks.custom]
[tool.hatch.build.targets.sdist.hooks.custom]
@@ -0,0 +1,151 @@
"""Locate the bundled DeepSeek Harness SDK runtime shipped with this package.
Two runtime carriers coexist under ``runtime/``, both injected by the repo's
``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
- **exe (production)**: single-file executables named
``dsh-jsonrpc-agent-pkg-<platform>-<arch>`` (platform in {linux, macos}, arch in
{x64, arm64}); the target machine needs no Node installation.
- **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-agent/lib/bin.js`` on a
system Node >= 22.19. It is the current checkout's source build, never
selected automatically, and excluded from wheel/sdist distributions.
``runtime/cordis.yml`` IS checked in: it is the default agent configuration
the client SDK injects via ``$DSH_CORDIS_CONFIG`` for zero-config runs — the
runtime itself always requires an explicit config and has no built-in
fallback.
"""
from __future__ import annotations
import os
import platform
import shutil
import sys
from pathlib import Path
PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos"}
_ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
_EXE_ACQUISITION_HINT = (
"Two ways to get the executable: run `scripts/build-exe-for-python-sdk.ts` (via tsx) in a "
"deepseek-harness checkout, or install the matching `deepseek-harness-runtime-bin` platform "
"wheel retained by the `build-exe-for-python-sdk` CI workflow. For local development "
"against a repo source build, explicitly select the dev-only node carrier with "
f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))."
)
def bundled_package_dir() -> Path:
"""Root directory of the installed runtime package data (the directory of this module)."""
root = Path(__file__).resolve().parent
metadata = root / PACKAGE_METADATA_FILENAME
if not metadata.is_file():
raise FileNotFoundError(f"deepseek-harness-runtime-bin is missing {metadata}")
return root
def bundled_default_config_path() -> Path:
"""Path of the checked-in default runtime configuration (``runtime/cordis.yml``).
The client SDK injects this path via ``$DSH_CORDIS_CONFIG`` when the caller
supplies no config and the launch resolves to the bundled runtime — the
runtime binary itself always demands an explicit config.
"""
path = bundled_package_dir() / "runtime" / "cordis.yml"
if not path.is_file():
raise FileNotFoundError(
f"deepseek-harness-runtime-bin is missing the default runtime config at {path}"
)
return path
def bundled_runtime_path() -> Path:
"""Absolute path of the bundled single-file runtime executable for the current platform.
Raises FileNotFoundError when the platform is unsupported or the executable
has not been placed into this package; the message names the acquisition
routes (acquisition strategy is deliberately separate from this lookup
interface, so an on-demand download can replace it without touching
callers).
"""
tag = _current_platform_tag()
path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}"
if not path.is_file():
raise FileNotFoundError(
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
+ _EXE_ACQUISITION_HINT
)
return path
def resolve_bundled_launch_args(mode: str | None = None) -> tuple[str, ...]:
"""The argv tuple that launches the bundled runtime.
Mode selection: the explicit ``mode`` argument wins, then the
``DSH_RUNTIME_MODE`` environment variable (``exe`` | ``node``), then
automatic resolution. Automatic resolution finds the production exe ONLY —
the dev-only node carrier must be selected explicitly so a production
deployment can never silently ride on a source build. Returns
``(exe_path,)`` in exe mode and ``(node_path, bin_js_path)`` in node mode;
raises FileNotFoundError when the selected carrier is unavailable and
ValueError for an unknown mode value.
"""
selected = mode if mode is not None else os.environ.get(RUNTIME_MODE_ENV_VAR)
if selected is None or selected == "exe":
return (str(bundled_runtime_path()),)
if selected == "node":
return _node_launch_args()
raise ValueError(
f"unsupported DeepSeek Harness runtime mode {selected!r}: expected 'exe' or 'node' "
f"(explicit argument or ${RUNTIME_MODE_ENV_VAR})"
)
def _current_platform_tag() -> str:
plat = _PLATFORM_TAGS.get(sys.platform)
arch = _ARCH_TAGS.get(platform.machine().lower())
if plat is None or arch is None:
raise FileNotFoundError(
"no bundled dsh-jsonrpc-agent executable exists for this platform "
f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
"linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT
)
return f"{plat}-{arch}"
def _node_launch_args() -> tuple[str, str]:
node_root = bundled_package_dir() / "runtime" / "node"
bin_js = (
node_root / "node_modules" / "@deepseek-ai" / "dsh-jsonrpc-agent" / "lib" / "bin.js"
)
if not bin_js.is_file():
raise FileNotFoundError(
f"the dev-only node runtime closure is missing at {node_root} "
f"(no {bin_js}); run `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness "
"checkout, which builds and copies the deploy closure here. The node carrier "
"is for repo-local development only — production uses the single-file exe."
)
node = shutil.which("node")
if node is None:
raise FileNotFoundError(
"the node runtime mode needs a system `node` (>=22.19) on PATH; "
"install Node.js or use the exe mode"
)
return (node, str(bin_js))
__all__ = [
"PACKAGE_METADATA_FILENAME",
"RUNTIME_MODE_ENV_VAR",
"bundled_default_config_path",
"bundled_package_dir",
"bundled_runtime_path",
"resolve_bundled_launch_args",
]
@@ -0,0 +1 @@
{"name":"deepseek-harness-runtime-bin","version":"0.0.0-dev"}
@@ -0,0 +1,50 @@
# Default runtime configuration for the bundled dsh-jsonrpc-agent. The runtime
# binary has NO built-in fallback — it always requires an explicit config via
# `$DSH_CORDIS_CONFIG` (wins) or an argv positional path. The Python client SDK
# injects THIS file's path via `$DSH_CORDIS_CONFIG` when the caller supplies
# no config and the launch resolves to the bundled runtime; that explicit
# injection is what restores the zero-config experience. The runtime bin only
# boots this config; the serving surface (the stdio JSON-RPC server) comes
# from the @deepseek-ai/dsh-jsonrpc entry below.
#
# $DSH_SESSION_ROOT and $DSH_CWD are set by the SDK per launch; the `!!js`
# fallbacks keep this file usable when the runtime is driven manually.
# The serving surface: HarnessSdkServer + line-delimited JSON-RPC transport on
# stdio. Without this entry the runtime boots an agent nobody can talk to.
- id: jsonrpc
name: '@deepseek-ai/dsh-jsonrpc'
# The agent spine bundle: session store, system prompt, tool registry, agent
# registry, and the agent loop. No pre-created agents — the SDK server creates
# one per session/prompt sessionId.
- id: agent-core
name: '@deepseek-ai/dsh-agent-core'
# The DeepSeek adapter, preloaded for the stock models. The adapter fails loud
# at load without an API key, so keyless boots must still export a dummy
# DEEPSEEK_API_KEY (initialize/shutdown never call the model). baseURL falls
# back to the public endpoint when unset.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
models:
- deepseek-v4-flash
- deepseek-v4-pro
# JSONL session persistence. $DSH_SESSION_ROOT (set by the SDK whenever
# `session_root` is configured) wins; otherwise ./.sessions relative to the
# runtime process cwd.
- id: sessions
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
# Local bash executor behind the spine's `bash` tool. $DSH_CWD (always set by
# the SDK) wins; otherwise the runtime process cwd.
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
cwd: !!js process.env.DSH_CWD ?? process.cwd()
+6
View File
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
README.md: 60540376c5fd85b0852e204bc8bad3f01c849de5
README.zh.md: 241c06057889f1aa4add6fc54024fba92bd19429
+40
View File
@@ -0,0 +1,40 @@
# DeepSeek Harness Python SDK
English | [中文](README.zh.md)
Python subprocess SDK for driving DeepSeek Harness over JSON-RPC stdio. The
runtime inherits normal DeepSeek Harness environment variables such as
`DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY`, so callers can use real model
endpoints directly or point those variables at a local proxy during
benchmark runs.
Installing `deepseek-harness` installs the exact same-version `deepseek-harness-runtime-bin` platform wheel. The normal entry point therefore needs no executable argument:
```py
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness() as harness:
result = harness.run("Say hi.")
```
`DeepSeekHarness` keeps its lazily started runtime subprocess for reuse across calls. Use it as a context manager, as above, or call `close()` explicitly when finished.
By default, the SDK launches the bundled single-file `dsh-jsonrpc-agent` executable from the `deepseek-harness-runtime-bin` package and injects that package's default configuration (the stdio JSON-RPC server, agent core, preloaded DeepSeek adapter, JSONL session persistence, local bash) via `DSH_CORDIS_CONFIG`. To run a plugin composition of your own, keep the `@deepseek-ai/dsh-jsonrpc` entry in the config and pass the Cordis config path.
```py
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness(
model="deepseek-v4-flash",
cordis="examples/dsbench-coding-agent/cordis.yml",
) as harness:
result = harness.run("Make the requested code change.")
```
`TurnResult.final_response` is the text content from the last
`assistant/message` event in the turn. Use `TurnResult.events` for the complete
event stream, including intermediate assistant messages and tool activity.
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` 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.
`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`.
+34
View File
@@ -0,0 +1,34 @@
# DeepSeek Harness Python SDK
[English](README.md) | 中文
通过 JSON-RPC stdio 驱动 DeepSeek Harness 的 Python 子进程 SDK。运行时继承常规的 DeepSeek Harness 环境变量(如 `DEEPSEEK_BASE_URL``DEEPSEEK_API_KEY`),调用方可以直接用真实模型端点,也可以在跑基准测试时把它们指向本地代理。
安装 `deepseek-harness` 会同时安装版本完全相同的 `deepseek-harness-runtime-bin` 平台 wheel 包。因此常规入口不需要传可执行文件参数:
```py
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness() as harness:
result = harness.run("Say hi.")
```
`DeepSeekHarness` 会保留延迟启动的运行时子进程,以供多次调用复用。请像上例一样将其用作上下文管理器,或在用完后显式调用 `close()`
默认情况下,SDK 启动 `deepseek-harness-runtime-bin` 包内置的单文件 `dsh-jsonrpc-agent` 可执行程序,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置(stdio JSON-RPC 服务器、`agent-core`、预载的 DeepSeek 适配器、JSONL 会话持久化、本地 bash)。要运行自己的插件组合,请在配置里保留 `@deepseek-ai/dsh-jsonrpc` 条目,并传入 Cordis 配置路径。
```py
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness(
model="deepseek-v4-flash",
cordis="examples/dsbench-coding-agent/cordis.yml",
) as harness:
result = harness.run("Make the requested code change.")
```
`TurnResult.final_response` 是本轮次最后一个 `assistant/message` 事件的文本内容。完整的事件流(包括中间的助手消息与工具活动)用 `TurnResult.events` 获取。
同样的行为也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程选定。注入逻辑位于 `HarnessClient.start()`,因此底层客户端的默认启动也具有此行为:当启动解析到内置运行时,且 `cordis` 与非空的 `DSH_CORDIS_CONFIG` 均未设置时(运行时把空值视为缺省,注入检查与之一致),使用内置的默认配置;显式给出 `runtime_bin``launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](../sdk-runtime/README.md)。
`cwd``runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露真正生效的选项:部署的角色设定与持久化配置归 `cordis.yml` 管理,而 `session_root` 继续作为设置 `DSH_SESSION_ROOT` 的高层便捷选项。
+31
View File
@@ -0,0 +1,31 @@
[build-system]
requires = ["hatchling>=1.24.0"]
build-backend = "hatchling.build"
[project]
name = "deepseek-harness"
version = "0.0.0.dev0"
description = "Python SDK for DeepSeek Harness"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "BSD-3-Clause" }
dependencies = [
"pydantic>=2.12",
"deepseek-harness-runtime-bin==0.0.0.dev0",
]
[dependency-groups]
test = ["pytest>=8.0"]
[tool.pytest.ini_options]
addopts = "-q"
testpaths = ["tests"]
[tool.hatch.build.targets.wheel]
packages = ["src/deepseek_harness"]
# Editable: the runtime package's executables are injected into its source
# tree AFTER install (by scripts/build-exe-for-python-sdk.ts or a manual copy); an
# editable install sees them immediately instead of freezing a wheel snapshot.
[tool.uv.sources]
deepseek-harness-runtime-bin = { path = "../sdk-runtime", editable = true }
@@ -0,0 +1,17 @@
from .api import DeepSeekHarness, DeepSeekHarnessConfig, Session, TurnResult
from .client import HarnessClient, HarnessConfig
from .models import IncomingRequest, InitializeResponse, JsonObject, Notification, ServerInfo
__all__ = [
"DeepSeekHarness",
"DeepSeekHarnessConfig",
"Session",
"TurnResult",
"HarnessClient",
"HarnessConfig",
"IncomingRequest",
"InitializeResponse",
"JsonObject",
"Notification",
"ServerInfo",
]
+195
View File
@@ -0,0 +1,195 @@
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from .client import HarnessClient, HarnessConfig
from .models import JsonObject, Notification
@dataclass(slots=True)
class DeepSeekHarnessConfig:
"""Configuration for launching the local DeepSeek Harness SDK runtime.
The runtime inherits the caller's environment by default, so existing
DEEPSEEK_API_KEY and DEEPSEEK_BASE_URL settings keep working. Use ``env`` to
intentionally override or inject variables for a subprocess.
"""
model: str = "deepseek-v4-flash"
cwd: str | None = None
runtime_cwd: str | None = None
session_root: str | None = None
cordis: 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
base_url: str | None = None
api_key: str | None = None
@dataclass(slots=True)
class TurnResult:
session_id: str
status: str
final_response: str
events: list[JsonObject]
notifications: list[Notification]
session_root: str | None = None
class DeepSeekHarness:
"""Reusable synchronous SDK for running DeepSeek Harness agent turns.
The runtime subprocess starts lazily and remains owned by this instance
across calls to :meth:`run`. Use the instance as a context manager, or call
:meth:`close` explicitly when finished, so the subprocess is always reaped.
"""
def __init__(self, config: DeepSeekHarnessConfig | None = None, **kwargs: object) -> None:
if config is not None and kwargs:
raise TypeError("pass either DeepSeekHarnessConfig or keyword options, not both")
self.config = config or DeepSeekHarnessConfig(**kwargs)
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
if self.config.cordis is not None:
env["DSH_CORDIS_CONFIG"] = self.config.cordis
env["DSH_CWD"] = cwd
if self.config.base_url is not None:
env["DEEPSEEK_BASE_URL"] = self.config.base_url
if self.config.api_key is not None:
env["DEEPSEEK_API_KEY"] = self.config.api_key
self._client = HarnessClient(
HarnessConfig(
runtime_bin=self.config.runtime_bin,
launch_args_override=self.config.launch_args_override,
cwd=runtime_cwd,
env=env,
request_timeout_seconds=self.config.request_timeout_seconds,
shutdown_timeout_seconds=self.config.shutdown_timeout_seconds,
)
)
self._initialized = False
def __enter__(self) -> "DeepSeekHarness":
self.start()
return self
def __exit__(self, _exc_type, _exc, _tb) -> None:
self.close()
@property
def client(self) -> HarnessClient:
return self._client
def start(self) -> None:
if self._initialized:
return
self._client.start()
self._client.initialize(
cwd=self._cwd,
model=self.config.model,
)
self._initialized = True
def close(self) -> None:
self._client.close()
self._initialized = False
def start_session(self, session_id: str | None = None) -> "Session":
self.start()
return Session(self, session_id or f"session-{uuid.uuid4().hex}")
def run(
self,
input: str | list[JsonObject],
*,
session_id: str | None = None,
on_notification: Callable[[Notification], None] | None = None,
) -> TurnResult:
return self.start_session(session_id).run(input, on_notification=on_notification)
class Session:
def __init__(self, harness: DeepSeekHarness, session_id: str) -> None:
self.harness = harness
self.id = session_id
def run(
self,
input: str | list[JsonObject],
*,
on_notification: Callable[[Notification], None] | None = None,
) -> TurnResult:
content_blocks = normalize_input(input)
notifications: list[Notification] = []
events: list[JsonObject] = []
status = "error"
finished = False
def collect(notification: Notification) -> None:
nonlocal finished, status
notifications.append(notification)
if on_notification is not None:
on_notification(notification)
if notification.method == "session.event":
event = notification.payload.get("event")
if isinstance(event, dict):
events.append(event)
if notification.method == "session.finished" and notification.payload.get("sessionId") == self.id:
status = str(notification.payload.get("status") or "ok")
finished = True
with self.harness.client.subscribe_session_notifications(self.id) as subscription:
self.harness.client.session_prompt(
self.id,
content_blocks,
on_notification=collect,
notification_subscription=subscription,
)
while not finished:
notification = subscription.next()
collect(notification)
return TurnResult(
session_id=self.id,
status=status,
final_response=final_response(events),
events=events,
notifications=notifications,
session_root=self.harness.config.session_root,
)
def normalize_input(input: str | list[JsonObject]) -> list[JsonObject]:
if isinstance(input, str):
return [{"type": "text", "text": input}]
return input
def final_response(events: list[JsonObject]) -> str:
for event in reversed(events):
if event.get("type") != "assistant/message":
continue
data = event.get("data")
if not isinstance(data, dict):
continue
content = data.get("content")
if not isinstance(content, list):
continue
parts: list[str] = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
parts.append(str(block.get("text") or ""))
return "".join(parts)
return ""
+511
View File
@@ -0,0 +1,511 @@
from __future__ import annotations
import json
import os
import queue
import subprocess
import threading
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
from .errors import JsonRpcError, TransportClosedError
from .models import IncomingRequest, InitializeResponse, JsonObject, JsonValue, Notification
ModelT = TypeVar("ModelT", bound=BaseModel)
NotificationFilter: TypeAlias = Callable[[Notification], bool]
@dataclass(slots=True)
class HarnessConfig:
"""Configuration for launching the local DeepSeek Harness SDK runtime."""
runtime_bin: str | None = None
bridge_bin: str | None = None
launch_args_override: tuple[str, ...] | None = None
cwd: str | None = None
env: dict[str, str] | None = None
request_timeout_seconds: float | None = None
shutdown_timeout_seconds: float | None = 1.0
class HarnessClient:
"""Synchronous JSON-RPC client for the DeepSeek Harness SDK runtime over stdio."""
def __init__(self, config: HarnessConfig | None = None) -> None:
self.config = config or HarnessConfig()
self._proc: subprocess.Popen[str] | None = None
self._lock = threading.Lock()
self._write_lock = threading.Lock()
self._responses: dict[str, queue.Queue[JsonValue | BaseException]] = {}
self._notifications: queue.Queue[Notification | BaseException] = queue.Queue()
self._notification_subscribers: dict[
str, tuple[queue.Queue[Notification | BaseException], NotificationFilter | None]
] = {}
self._requests: queue.Queue[IncomingRequest | BaseException] = queue.Queue()
self._stderr_lines: deque[str] = deque(maxlen=400)
self._reader_thread: threading.Thread | None = None
self._stderr_thread: threading.Thread | None = None
def __enter__(self) -> "HarnessClient":
self.start()
return self
def __exit__(self, _exc_type, _exc, _tb) -> None:
self.close()
def start(self) -> None:
if self._proc is not None:
return
args = list(self.config.launch_args_override or self._default_launch_args())
env = os.environ.copy()
if self.config.env:
env.update(self.config.env)
self._inject_bundled_default_config(env)
self._proc = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
cwd=None if self.config.cwd is None else str(Path(self.config.cwd).resolve()),
env=env,
bufsize=1,
)
self._start_reader_thread()
self._start_stderr_thread()
def close(self) -> None:
proc = self._proc
if proc is None:
return
try:
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}")
if proc.stdin:
try:
proc.stdin.close()
except Exception as exc:
self._stderr_lines.append(f"stdin close failed: {exc}")
if proc.poll() is None:
try:
proc.terminate()
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)
if self._stderr_thread and self._stderr_thread.is_alive():
self._stderr_thread.join(timeout=0.5)
def initialize(
self,
*,
cwd: str,
model: str,
) -> InitializeResponse:
payload: JsonObject = {
"cwd": str(Path(cwd).resolve()),
"model": model,
}
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],
*,
on_notification: Callable[[Notification], None] | None = None,
notification_subscription: "NotificationSubscription | None" = None,
) -> None:
payload: JsonObject = {"sessionId": session_id, "contentBlocks": content_blocks}
self.request(
"session/prompt",
payload,
response_model=_SessionPromptResponse,
on_notification=on_notification,
notification_filter=_notification_belongs_to_session(session_id),
notification_subscription=notification_subscription,
)
def request(
self,
method: str,
params: JsonObject | None,
*,
response_model: type[ModelT],
timeout_seconds: float | None = None,
on_notification: Callable[[Notification], None] | None = None,
notification_filter: NotificationFilter | None = None,
notification_subscription: "NotificationSubscription | None" = None,
) -> ModelT:
result = self._request_raw(
method,
params,
timeout_seconds=timeout_seconds,
on_notification=on_notification,
notification_filter=notification_filter,
notification_subscription=notification_subscription,
)
if not isinstance(result, dict):
raise TypeError(f"{method} response must be a JSON object")
return response_model.model_validate(result)
def notify(self, method: str, params: JsonObject | None = None) -> None:
message: JsonObject = {"jsonrpc": "2.0", "method": method}
if params is not None:
message["params"] = params
self._write_message(message)
def next_notification(self) -> Notification:
item = self._notifications.get()
if isinstance(item, BaseException):
raise item
return item
def subscribe_notifications(
self,
notification_filter: NotificationFilter | None = None,
) -> "NotificationSubscription":
subscription_id = str(uuid.uuid4())
notifications: queue.Queue[Notification | BaseException] = queue.Queue()
with self._lock:
self._notification_subscribers[subscription_id] = (notifications, notification_filter)
return NotificationSubscription(self, subscription_id, notifications)
def subscribe_session_notifications(self, session_id: str) -> "NotificationSubscription":
return self.subscribe_notifications(_notification_belongs_to_session(session_id))
def next_request(self) -> IncomingRequest:
item = self._requests.get()
if isinstance(item, BaseException):
raise item
return item
def respond(self, request_id: str | int, result: JsonValue) -> None:
self._write_message({"jsonrpc": "2.0", "id": request_id, "result": result})
def respond_error(
self,
request_id: str | int,
*,
code: int,
message: str,
data: JsonValue | None = None,
) -> None:
error: JsonObject = {"code": code, "message": message}
if data is not None:
error["data"] = data
self._write_message({"jsonrpc": "2.0", "id": request_id, "error": error})
def _request_raw(
self,
method: str,
params: JsonObject | None = None,
*,
timeout_seconds: float | None = None,
on_notification: Callable[[Notification], None] | None = None,
notification_filter: NotificationFilter | None = None,
notification_subscription: "NotificationSubscription | None" = None,
) -> JsonValue:
request_id = str(uuid.uuid4())
waiter: queue.Queue[JsonValue | BaseException] = queue.Queue(maxsize=1)
temp_subscription: NotificationSubscription | None = None
subscription = notification_subscription
with self._lock:
self._responses[request_id] = waiter
if on_notification is not None and subscription is None:
temp_subscription = self.subscribe_notifications(notification_filter)
subscription = temp_subscription
try:
message: JsonObject = {"jsonrpc": "2.0", "id": request_id, "method": method}
if params is not None:
message["params"] = params
self._write_message(message)
except BaseException:
with self._lock:
self._responses.pop(request_id, None)
if temp_subscription is not None:
temp_subscription.close()
raise
timeout = self.config.request_timeout_seconds if timeout_seconds is None else timeout_seconds
deadline = None if timeout is None else time.monotonic() + timeout
try:
while True:
if on_notification is not None and subscription is not None:
subscription.drain(on_notification)
wait_timeout = None
if on_notification is not None:
wait_timeout = 0.05
if deadline is not None:
remaining = deadline - time.monotonic()
if remaining <= 0:
with self._lock:
self._responses.pop(request_id, None)
raise TimeoutError(f"{method} timed out waiting for DeepSeek Harness runtime")
wait_timeout = remaining if wait_timeout is None else min(wait_timeout, remaining)
try:
item = waiter.get(timeout=wait_timeout)
if on_notification is not None and subscription is not None:
subscription.drain(on_notification)
break
except queue.Empty:
continue
except BaseException:
with self._lock:
self._responses.pop(request_id, None)
if temp_subscription is not None:
temp_subscription.close()
raise
finally:
if temp_subscription is not None:
temp_subscription.close()
if isinstance(item, BaseException):
raise item
return item
def _write_message(self, message: JsonObject) -> None:
proc = self._proc
if proc is None or proc.stdin is None:
raise TransportClosedError("DeepSeek Harness runtime is not running")
try:
payload = json.dumps(message, separators=(",", ":")) + "\n"
with self._write_lock:
proc.stdin.write(payload)
proc.stdin.flush()
except Exception as exc:
raise self._runtime_closed_error("Failed to write to DeepSeek Harness runtime") from exc
def _start_reader_thread(self) -> None:
self._reader_thread = threading.Thread(target=self._reader_loop, name="dsh-runtime-reader", daemon=True)
self._reader_thread.start()
def _start_stderr_thread(self) -> None:
self._stderr_thread = threading.Thread(target=self._stderr_loop, name="dsh-runtime-stderr", daemon=True)
self._stderr_thread.start()
def _reader_loop(self) -> None:
proc = self._proc
if proc is None or proc.stdout is None:
return
try:
for line in proc.stdout:
if not line.strip():
continue
try:
message = json.loads(line)
except json.JSONDecodeError:
continue
self._handle_message(message)
except BaseException as exc:
self._fail_waiters(exc)
finally:
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime stdout closed"))
def _stderr_loop(self) -> None:
proc = self._proc
if proc is None or proc.stderr is None:
return
for line in proc.stderr:
self._stderr_lines.append(line.rstrip())
def _handle_message(self, message: object) -> None:
if not isinstance(message, dict):
return
msg_id = message.get("id")
method = message.get("method")
if isinstance(msg_id, (str, int)) and isinstance(method, str):
params = message.get("params")
self._requests.put(IncomingRequest(id=msg_id, method=method, payload=params if isinstance(params, dict) else {}))
return
if isinstance(msg_id, (str, int)):
with self._lock:
waiter = self._responses.pop(str(msg_id), None)
if waiter is None:
return
if isinstance(message.get("error"), dict):
err = message["error"]
waiter.put(JsonRpcError(_int_or_none(err.get("code")), str(err.get("message", "JSON-RPC error")), err.get("data")))
else:
waiter.put(message.get("result"))
return
if isinstance(method, str):
params = message.get("params")
notification = Notification(method=method, payload=params if isinstance(params, dict) else {})
with self._lock:
subscribers = list(self._notification_subscribers.items())
delivered = False
for subscription_id, (subscriber, predicate) in subscribers:
try:
matches = predicate is None or predicate(notification)
except BaseException as exc:
with self._lock:
current = self._notification_subscribers.get(subscription_id)
if current is not None and current[0] is subscriber:
self._notification_subscribers.pop(subscription_id, None)
subscriber.put(exc)
continue
if matches:
subscriber.put(notification)
delivered = True
if not delivered:
self._notifications.put(notification)
def _fail_waiters(self, exc: BaseException) -> None:
with self._lock:
waiters = list(self._responses.values())
self._responses.clear()
subscribers = list(self._notification_subscribers.values())
self._notification_subscribers.clear()
for waiter in waiters:
waiter.put(exc)
for subscriber, _predicate in subscribers:
subscriber.put(exc)
self._notifications.put(exc)
self._requests.put(exc)
def _runtime_closed_error(self, reason: str) -> TransportClosedError:
proc = self._proc
if (
proc is not None
and proc.poll() is not None
and self._stderr_thread is not None
and self._stderr_thread.is_alive()
and threading.current_thread() is not self._stderr_thread
):
self._stderr_thread.join(timeout=0.1)
parts = [reason]
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))
def _default_launch_args(self) -> tuple[str, ...]:
if self.config.runtime_bin is not None:
return (self.config.runtime_bin,)
if self.config.bridge_bin is not None:
return (self.config.bridge_bin,)
try:
from deepseek_harness_runtime import resolve_bundled_launch_args
except ImportError as exc:
raise FileNotFoundError(
"Unable to locate the bundled DeepSeek Harness SDK runtime. "
"Install deepseek-harness-runtime-bin or set HarnessConfig.runtime_bin."
) from exc
return resolve_bundled_launch_args()
def _inject_bundled_default_config(self, env: dict[str, str]) -> None:
"""Restore the zero-config experience over the config-mandatory bundled runtime.
The bundled runtime (single-file exe or the dev-only node closure)
always demands an explicit config. When the launch resolves to the
bundled runtime (no ``runtime_bin`` / ``bridge_bin`` /
``launch_args_override``) and the merged subprocess environment has no
non-empty ``DSH_CORDIS_CONFIG`` — the runtime bin treats an empty
value as absent, so this does too — inject the runtime package's
checked-in default cordis.yml. With an explicit runtime or config
channel the client stays out of the way.
"""
uses_bundled_runtime = (
self.config.launch_args_override is None
and self.config.runtime_bin is None
and self.config.bridge_bin is None
)
if not uses_bundled_runtime or env.get("DSH_CORDIS_CONFIG"):
return
# Cannot fail: _default_launch_args() already imported the runtime
# package on this (bundled) path, raising the actionable install
# error when it is absent.
from deepseek_harness_runtime import bundled_default_config_path
env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path())
def _unsubscribe_notifications(self, subscription_id: str) -> None:
with self._lock:
self._notification_subscribers.pop(subscription_id, None)
class NotificationSubscription:
def __init__(
self,
client: HarnessClient,
subscription_id: str,
notifications: queue.Queue[Notification | BaseException],
) -> None:
self._client = client
self._subscription_id = subscription_id
self._notifications = notifications
self._closed = False
def __enter__(self) -> "NotificationSubscription":
return self
def __exit__(self, _exc_type, _exc, _tb) -> None:
self.close()
def close(self) -> None:
if self._closed:
return
self._closed = True
self._client._unsubscribe_notifications(self._subscription_id)
def next(self) -> Notification:
item = self._notifications.get()
if isinstance(item, BaseException):
raise item
return item
def drain(self, on_notification: Callable[[Notification], None]) -> None:
while True:
try:
item = self._notifications.get_nowait()
except queue.Empty:
return
if isinstance(item, BaseException):
raise item
on_notification(item)
class _SessionPromptResponse(BaseModel):
accepted: Literal[True]
class _ShutdownResponse(BaseModel):
pass
def _int_or_none(value: object) -> int | None:
return value if isinstance(value, int) else None
def _notification_belongs_to_session(session_id: str) -> NotificationFilter:
def belongs(notification: Notification) -> bool:
payload = notification.payload
return (
payload.get("sessionId") == session_id
or payload.get("parentSessionId") == session_id
or payload.get("childSessionId") == session_id
)
return belongs
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
class HarnessError(Exception):
"""Base exception for SDK and runtime failures."""
class TransportClosedError(HarnessError):
"""Raised when the runtime subprocess exits or closes stdout."""
class JsonRpcError(HarnessError):
"""Raised when the runtime returns a JSON-RPC error response."""
def __init__(self, code: int | None, message: str, data: object | None = None) -> None:
super().__init__(message)
self.code = code
self.message = message
self.data = data
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TypeAlias
from pydantic import BaseModel
JsonScalar: TypeAlias = str | int | float | bool | None
JsonValue: TypeAlias = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"]
JsonObject: TypeAlias = dict[str, JsonValue]
@dataclass(slots=True)
class Notification:
method: str
payload: JsonObject
@dataclass(slots=True)
class IncomingRequest:
id: str | int
method: str
payload: JsonObject
class ServerInfo(BaseModel):
name: str | None = None
version: str | None = None
class InitializeResponse(BaseModel):
serverInfo: ServerInfo | None = None
+125
View File
@@ -0,0 +1,125 @@
"""Manual keyless smoke: drive the repo-source jsonrpc-agent bin (node + tsx).
Runs the SDK against `packages/ui/jsonrpc-agent/src/bin.ts` executed from the
repo checkout (requires `pnpm install`; no build, no API key — the model
endpoint is a local mock SSE server). The bin only boots the supplied
cordis.yml — the stdio JSON-RPC server itself comes from the config's
`@deepseek-ai/dsh-jsonrpc` entry — so the runtime package's default cordis.yml
is passed explicitly. Not collected by pytest; run it directly:
`python tests/manual_sdk_agent_smoke.py`.
"""
from __future__ import annotations
import argparse
import json
import shutil
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from deepseek_harness import DeepSeekHarness
from deepseek_harness_runtime import bundled_default_config_path
class MockCompletionHandler(BaseHTTPRequestHandler):
requests: list[dict[str, Any]] = []
def do_POST(self) -> None:
length = int(self.headers.get("content-length", "0"))
body = self.rfile.read(length).decode("utf-8")
self.requests.append({
"path": self.path,
"authorization": self.headers.get("authorization"),
"body": json.loads(body),
})
self.send_response(200)
self.send_header("content-type", "text/event-stream")
self.end_headers()
self.wfile.write(b'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}\n\n')
self.wfile.write(b'data: {"choices":[{"delta":{"content":"SDK runtime reached the configured HTTP model endpoint."}}]}\n\n')
self.wfile.write(b'data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":9}}\n\n')
self.wfile.write(b"data: [DONE]\n\n")
def log_message(self, _format: str, *_args: object) -> None:
return
def run_smoke(repo_root: Path, keep_sessions: bool) -> None:
session_root = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-sessions-"))
runtime_entry = repo_root / "packages/ui/jsonrpc-agent/src/bin.ts"
server = ThreadingHTTPServer(("127.0.0.1", 0), MockCompletionHandler)
thread = threading.Thread(target=server.serve_forever, name="mock-openai-compatible-server", daemon=True)
thread.start()
base_url = f"http://127.0.0.1:{server.server_address[1]}"
print(f"repo_root={repo_root}")
print(f"session_root={session_root}")
print(f"mock_base_url={base_url}")
try:
with DeepSeekHarness(
model="sdk-smoke-model",
cwd=str(repo_root / "python/sdk"),
runtime_cwd=str(repo_root),
session_root=str(session_root),
cordis=str(bundled_default_config_path()),
launch_args_override=("node", "--import", "tsx", str(runtime_entry)),
env={
"DEEPSEEK_BASE_URL": base_url,
"DEEPSEEK_API_KEY": "sdk-smoke-key",
},
request_timeout_seconds=20,
shutdown_timeout_seconds=2,
) as harness:
result = harness.run(
"Please reply with a short confirmation and do not call tools.",
session_id="sdk-smoke-main",
)
print(f"turn_status={result.status}")
print(f"final_response={result.final_response}")
assert result.status == "ok", result
assert "configured HTTP model endpoint" in result.final_response
assert len(MockCompletionHandler.requests) == 1
request = MockCompletionHandler.requests[0]
print(json.dumps(request, ensure_ascii=False, indent=2)[:4000])
assert request["authorization"] == "Bearer sdk-smoke-key"
assert request["body"]["model"] == "sdk-smoke-model"
jsonl_files = sorted(session_root.rglob("*.jsonl"))
assert jsonl_files, f"no jsonl sessions were written under {session_root}"
print("session_jsonl_files:")
for path in jsonl_files:
print(f" {path} bytes={path.stat().st_size}")
with path.open("r", encoding="utf-8") as handle:
first_line = handle.readline().strip()
if first_line:
print(f" first_line={first_line[:500]}")
finally:
server.shutdown()
server.server_close()
if keep_sessions:
print(f"kept_session_root={session_root}")
else:
shutil.rmtree(session_root)
print("removed temporary session root")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--repo-root",
type=Path,
default=Path(__file__).resolve().parents[3],
help="Path to the deepseek-harness checkout.",
)
parser.add_argument("--keep-sessions", action="store_true")
args = parser.parse_args()
run_smoke(args.repo_root.resolve(), args.keep_sessions)
if __name__ == "__main__":
main()
+125
View File
@@ -0,0 +1,125 @@
"""Smoke tests against the bundled dsh-jsonrpc-agent artifacts.
These boot the runtime the way an installed SDK does, once per bundled
carrier: the platform single-file exe (production) and the dev-only node
closure under ``runtime/node`` driven by system ``node``. Each carrier skips
independently when its artifact is absent on this machine — build or fetch it
per the FileNotFoundError guidance quoted in the skip reason. Keyless: the
dummy DEEPSEEK_API_KEY only satisfies the adapter's load-time check;
initialize/shutdown never call a model.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
from deepseek_harness.errors import TransportClosedError
from deepseek_harness_runtime import resolve_bundled_launch_args
_MODES = ("exe", "node")
# The serving surface is itself a plugin: without the dsh-jsonrpc entry the
# runtime boots an agent nobody can talk to and exits 0 on stdin EOF.
_CORDIS_YML = """\
- id: jsonrpc
name: '@deepseek-ai/dsh-jsonrpc'
- id: agent-core
name: '@deepseek-ai/dsh-agent-core'
- id: sessions
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './sessions'
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
cwd: '.'
- id: todo
name: '@deepseek-ai/dsh-tool-todo'
"""
def _launch_args(mode: str) -> tuple[str, ...]:
try:
return resolve_bundled_launch_args(mode)
except FileNotFoundError as exc:
pytest.skip(f"bundled {mode}-mode runtime unavailable on this machine: {exc}")
def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient:
return HarnessClient(
HarnessConfig(
launch_args_override=launch_args,
cwd=str(tmp_path),
env={
"DSH_CORDIS_CONFIG": "./cordis.yml",
"DSH_SESSION_ROOT": str(tmp_path / "sessions"),
"DSH_CWD": str(tmp_path),
# initialize() lazily mounts the llm-deepseek adapter for the
# requested model; a dummy key keeps the keyless boot green
# (initialize/shutdown never call the model).
"DEEPSEEK_API_KEY": "sk-dummy-for-boot",
"DEEPSEEK_BASE_URL": "http://127.0.0.1:9",
},
request_timeout_seconds=120,
)
)
@pytest.mark.parametrize("mode", _MODES)
def test_bundled_runtime_boots_a_cordis_config(tmp_path: Path, mode: str) -> None:
launch_args = _launch_args(mode)
(tmp_path / "cordis.yml").write_text(_CORDIS_YML)
with _client(tmp_path, launch_args) as client:
init = client.initialize(cwd=str(tmp_path), model="deepseek-v4-pro")
assert init.serverInfo is not None
assert init.serverInfo.name == "deepseek-harness-sdk-runtime"
@pytest.mark.parametrize("mode", _MODES)
def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode: str) -> None:
launch_args = _launch_args(mode)
(tmp_path / "cordis.yml").write_text(
"- id: missing\n name: '@deepseek-ai/dsh-does-not-exist'\n"
)
client = _client(tmp_path, launch_args)
client.start()
try:
with pytest.raises((TransportClosedError, TimeoutError)) as excinfo:
client.initialize(cwd=str(tmp_path), model="deepseek-v4-pro")
finally:
client.close()
assert "@deepseek-ai/dsh-does-not-exist" in str(excinfo.value)
@pytest.mark.parametrize("mode", _MODES)
@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
def test_zero_config_run_injects_bundled_default_cordis_config(
tmp_path: Path, mode: str, ambient_config: str | None, monkeypatch: pytest.MonkeyPatch
) -> None:
_launch_args(mode) # skip early when this carrier is unavailable
monkeypatch.setenv("DSH_RUNTIME_MODE", mode)
if ambient_config is None:
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
else:
monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
harness = DeepSeekHarness(
model="deepseek-v4-pro",
cwd=str(tmp_path),
session_root=str(tmp_path / "sessions"),
api_key="sk-dummy-for-boot",
base_url="http://127.0.0.1:9",
request_timeout_seconds=120,
)
with harness:
# __enter__ boots the runtime, which exits with a usage error unless
# HarnessClient.start() injected the bundled default config over the
# unset/empty DSH_CORDIS_CONFIG; __exit__ shuts it down.
pass
+767
View File
@@ -0,0 +1,767 @@
from __future__ import annotations
import json
import inspect
import sys
import threading
import time
from pathlib import Path
import pytest
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
env_dump = tmp_path / "env.json"
script.write_text(
"""
import json
import os
import sys
env_dump = os.environ["ENV_DUMP"]
json.dump({
"DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
"DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
"DSH_CWD": os.environ.get("DSH_CWD"),
"DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
}, open(env_dump, "w"))
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
params = msg.get("params") or {}
print(json.dumps({
"jsonrpc": "2.0",
"method": "session.event",
"params": {
"sessionId": params["sessionId"],
"event": {
"type": "assistant/message",
"data": {"content": [{"type": "text", "text": "hello from runtime"}]},
},
},
}), flush=True)
print(json.dumps({
"jsonrpc": "2.0",
"method": "session.finished",
"params": {"sessionId": params["sessionId"], "status": "ok"},
}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with DeepSeekHarness(
model="deepseek-v4-flash",
cwd=str(tmp_path),
cordis=str(tmp_path / "cordis.yml"),
session_root=str(tmp_path / "sessions"),
launch_args_override=(sys.executable, str(script)),
env={
"ENV_DUMP": str(env_dump),
"DEEPSEEK_API_KEY": "env-key",
"DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
},
) as harness:
result = harness.run("say hello", session_id="main")
assert result.status == "ok"
assert result.final_response == "hello from runtime"
assert result.events[0]["type"] == "assistant/message"
dumped_env = json.loads(env_dump.read_text())
assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
assert dumped_env["DSH_CWD"] == str(tmp_path)
assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions")
assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml")
def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
seen: list[str] = []
with DeepSeekHarness(
launch_args_override=(sys.executable, str(script)),
cwd=str(tmp_path),
) as harness:
session = harness.start_session("main")
result = session.run(
"spawn a helper",
on_notification=lambda notification: seen.append(notification.method),
)
assert result.status == "ok"
assert seen == ["subagent.started", "session.finished"]
def test_relative_cwd_is_absolute_in_process_environment_and_wire(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
script = tmp_path / "capture_cwd.py"
capture = tmp_path / "cwd.json"
script.write_text(
"""
import json
import os
import sys
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
json.dump({"process": os.getcwd(), "environment": os.environ.get("DSH_CWD"), "wire": msg["params"]["cwd"]}, open(os.environ["CAPTURE"], "w"))
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
monkeypatch.chdir(tmp_path)
with DeepSeekHarness(
cwd=".",
runtime_cwd=".",
launch_args_override=(sys.executable, str(script)),
env={"CAPTURE": str(capture)},
):
pass
expected = str(tmp_path.resolve())
assert json.loads(capture.read_text()) == {
"process": expected,
"environment": expected,
"wire": expected,
}
def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with DeepSeekHarness(
launch_args_override=(sys.executable, str(script)),
cwd=str(tmp_path),
) as harness:
result = harness.run("spawn a helper", session_id="main")
assert result.status == "ok"
assert [notification.method for notification in result.notifications] == [
"subagent.started",
"subagent.finished",
"session.finished",
]
def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
params = msg.get("params") or {}
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "other", "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "wrong session"}]}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "other", "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "right session"}]}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with DeepSeekHarness(
launch_args_override=(sys.executable, str(script)),
cwd=str(tmp_path),
) as harness:
result = harness.run("stay in your lane", session_id="main")
assert result.status == "ok"
assert result.final_response == "right session"
assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main", "main"]
def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
params = msg.get("params") or {}
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "ok"}]}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
result = harness.run("one turn", session_id="main")
assert result.status == "ok"
assert harness.client._notifications.qsize() == 0
def test_session_run_waits_for_late_finished_without_replaying_stale_notifications(tmp_path: Path) -> None:
script = tmp_path / "fake_runtime.py"
script.write_text(
"""
import json
import sys
import time
turn = 0
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
elif method == "session/prompt":
turn += 1
params = msg.get("params") or {}
session_id = params["sessionId"]
if turn == 1:
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "first"}]}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
else:
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
time.sleep(0.05)
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "second"}]}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
first = harness.run("first turn", session_id="main")
second = harness.run("second turn", session_id="main")
assert first.final_response == "first"
assert second.final_response == "second"
assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main", "main"]
def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif method == "session/prompt":
params = msg.get("params") or {}
print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with HarnessClient(
HarnessConfig(launch_args_override=(sys.executable, str(script)))
) as client:
init = client.initialize(cwd="/workspace", model="dsagent")
assert init.serverInfo.name == "fake-dsh"
client.session_prompt("main", [{"type": "text", "text": "fix it"}])
notification = client.next_notification()
assert notification.method == "llm/request"
assert notification.payload["requestId"] == "req-1"
assert notification.payload["sessionId"] == "main"
def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
client = HarnessClient()
with client.subscribe_session_notifications("main"):
client._handle_message({
"jsonrpc": "2.0",
"method": "session.event",
"params": {"sessionId": "other", "event": {"type": "assistant/message"}},
})
assert client._notifications.qsize() == 1
notification = client._notifications.get_nowait()
assert not isinstance(notification, BaseException)
assert notification.method == "session.event"
assert notification.payload["sessionId"] == "other"
def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif method in {"emit-first", "emit-second"}:
print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
elif method == "session/prompt":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
def broken_filter(_notification: object) -> bool:
raise RuntimeError("bad notification filter")
with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
client.initialize(cwd="/workspace", model="dsagent")
with (
client.subscribe_notifications(broken_filter) as broken,
client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
):
client.notify("emit-first")
with pytest.raises(RuntimeError, match="bad notification filter"):
broken.next()
assert healthy.next().payload == {"source": "emit-first"}
assert client._notifications.qsize() == 0
client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
client.notify("emit-second")
assert healthy.next().payload == {"source": "emit-second"}
def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif method == "session/prompt":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
client.initialize(cwd="/workspace", model="dsagent")
with pytest.raises(ValueError):
client.session_prompt("main", [{"type": "text", "text": "fix it"}])
def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
method = msg.get("method")
if method == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
elif "id" in msg and "method" not in msg:
print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
elif method == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with HarnessClient(
HarnessConfig(launch_args_override=(sys.executable, str(script)))
) as client:
client.initialize(cwd="/workspace", model="dsagent")
request = client.next_request()
assert request.id == "bridge-req-1"
assert request.method == "llm.request"
assert request.payload["requestId"] == "req-1"
client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
notification = client.next_notification()
assert notification.method == "response/seen"
assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
print("node warning: experimental loader", flush=True)
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with HarnessClient(
HarnessConfig(launch_args_override=(sys.executable, str(script)))
) as client:
init = client.initialize(cwd="/workspace", model="dsagent")
assert init.serverInfo.name == "fake-dsh"
def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import time
time.sleep(60)
""".strip()
)
with HarnessClient(
HarnessConfig(
launch_args_override=(sys.executable, str(script)),
request_timeout_seconds=0.1,
)
) as client:
start = time.monotonic()
try:
client.initialize(cwd="/workspace", model="dsagent")
except TimeoutError:
assert time.monotonic() - start < 2
else:
raise AssertionError("initialize should time out")
def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import signal
import sys
import time
signal.signal(signal.SIGTERM, signal.SIG_IGN)
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif msg.get("method") == "shutdown":
time.sleep(60)
""".strip()
)
client = HarnessClient(
HarnessConfig(
launch_args_override=(sys.executable, str(script)),
shutdown_timeout_seconds=0.1,
)
)
client.start()
proc = client._proc
assert proc is not None
client.initialize(cwd="/workspace", model="dsagent")
start = time.monotonic()
client.close()
assert time.monotonic() - start < 2
assert proc.poll() is not None
assert client._proc is None
def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
script = tmp_path / "rejecting_runtime.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
elif msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
client.start()
proc = client._proc
assert proc is not None
with pytest.raises(Exception, match="bad initialize"):
client.initialize(cwd=".", model="dsagent")
assert proc.wait(timeout=1) is not None
assert client._proc is None
def test_public_signatures_omit_unsupported_wire_parameters() -> None:
from deepseek_harness import DeepSeekHarnessConfig, Session
assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
assert "profile" not in inspect.signature(Session.run).parameters
assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
assert "client_name" not in HarnessConfig.__dataclass_fields__
assert "client_version" not in HarnessConfig.__dataclass_fields__
def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
HarnessClient().close()
script = tmp_path / "fake_bridge.py"
script.write_text(
"""
import json
import sys
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
client.start()
client.initialize(cwd="/workspace", model="dsagent")
client.close()
client.close()
def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
script = tmp_path / "crashing_runtime.py"
script.write_text(
"""
import sys
print("fatal bridge exploded", file=sys.stderr, flush=True)
sys.exit(42)
""".strip()
)
with HarnessClient(
HarnessConfig(
launch_args_override=(sys.executable, str(script)),
request_timeout_seconds=2,
)
) as client:
with pytest.raises(Exception, match="fatal bridge exploded"):
client.initialize(cwd="/workspace", model="dsagent")
def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
script = tmp_path / "fake_bridge.py"
output = tmp_path / "seen.jsonl"
script.write_text(
"""
import json
import os
import sys
with open(os.environ["SEEN"], "w") as seen:
for line in sys.stdin:
seen.write(line)
seen.flush()
msg = json.loads(line)
if "id" in msg and msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
elif "id" in msg and msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
with HarnessClient(
HarnessConfig(
launch_args_override=(sys.executable, str(script)),
env={"SEEN": str(output)},
)
) as client:
client.initialize(cwd="/workspace", model="dsagent")
threads = [
threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
for index in range(50)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
for line in output.read_text().splitlines():
json.loads(line)
def _install_fake_bundled_runtime(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> Path:
"""Fake the deepseek-harness-runtime-bin package on sys.path.
A stub exe that dumps DSH_CORDIS_CONFIG to $ENV_DUMP before serving
initialize/shutdown, plus a module exposing the resolution surface the
client consumes. Returns the fake bundled default config path.
"""
runtime = tmp_path / "dsh-jsonrpc-agent"
runtime.write_text(
"""#!/usr/bin/env python3
import json
import os
import sys
json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
for line in sys.stdin:
msg = json.loads(line)
if msg.get("method") == "initialize":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
elif msg.get("method") == "shutdown":
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
break
""".strip()
)
runtime.chmod(0o755)
default_config = tmp_path / "default-cordis.yml"
module_dir = tmp_path / "deepseek_harness_runtime"
module_dir.mkdir()
(module_dir / "__init__.py").write_text(
f"""
def resolve_bundled_launch_args(mode=None):
return ({str(runtime)!r},)
def bundled_default_config_path():
return {str(default_config)!r}
""".strip()
)
monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
return default_config
@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
) -> None:
env_dump = tmp_path / "env.json"
default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
if ambient_config is None:
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
else:
monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
init = client.initialize(cwd="/workspace", model="deepseek-v4-pro")
assert init.serverInfo.name == "bundled-runtime"
assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
def test_client_respects_explicit_config_over_bundled_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
env_dump = tmp_path / "env.json"
_install_fake_bundled_runtime(tmp_path, monkeypatch)
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
with HarnessClient(
HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
) as client:
client.initialize(cwd="/workspace", model="deepseek-v4-pro")
assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
monkeypatch.setattr(sys, "path", [])
with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
HarnessClient().start()
+39
View File
@@ -0,0 +1,39 @@
"""Tests for repository-owned Python release versions."""
from __future__ import annotations
import json
import runpy
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "scripts" / "build-python-release.py"
build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
def test_repository_version_matches_root_package_json() -> None:
expected = json.loads((ROOT / "package.json").read_text())["version"]
assert build_python_release.repository_version() == expected
def test_release_tag_is_optional_for_non_release_builds() -> None:
build_python_release.validate_release_tag(None, "1.2.3")
def test_release_tag_must_match_repository_version() -> None:
build_python_release.validate_release_tag("python-v1.2.3", "1.2.3")
with pytest.raises(ValueError, match="expected 'python-v1.2.3'"):
build_python_release.validate_release_tag("python-v1.2.4", "1.2.3")
def test_repository_version_rejects_non_stable_versions(tmp_path: Path) -> None:
(tmp_path / "package.json").write_text('{"version":"1.2.3-dev"}\n')
with pytest.raises(ValueError, match="must be stable X.Y.Z"):
build_python_release.repository_version(tmp_path)
@@ -0,0 +1,43 @@
"""Keyless tests for the deepseek_harness_runtime resolution API.
These never launch a runtime, so they run everywhere regardless of which
bundled artifacts are present; the launch-and-boot coverage lives in
``test_bundled_runtime.py``.
"""
from __future__ import annotations
import pytest
from deepseek_harness_runtime import (
RUNTIME_MODE_ENV_VAR,
bundled_default_config_path,
bundled_package_dir,
resolve_bundled_launch_args,
)
def test_default_config_is_shipped_with_the_package() -> None:
path = bundled_default_config_path()
assert path == bundled_package_dir() / "runtime" / "cordis.yml"
assert "@deepseek-ai/dsh-agent-core" in path.read_text()
def test_unknown_explicit_mode_fails_loud() -> None:
with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
resolve_bundled_launch_args("bogus")
def test_unknown_env_mode_fails_loud(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
resolve_bundled_launch_args()
def test_explicit_mode_wins_over_env_mode(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
try:
args = resolve_bundled_launch_args("exe")
except FileNotFoundError:
return # explicit 'exe' was honored; only the artifact is missing
assert args[0].endswith(("-x64", "-arm64"))
+321
View File
@@ -0,0 +1,321 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "deepseek-harness"
version = "0.0.0.dev0"
source = { editable = "." }
dependencies = [
{ name = "deepseek-harness-runtime-bin" },
{ name = "pydantic" },
]
[package.dev-dependencies]
test = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "deepseek-harness-runtime-bin", editable = "../sdk-runtime" },
{ name = "pydantic", specifier = ">=2.12" },
]
[package.metadata.requires-dev]
test = [{ name = "pytest", specifier = ">=8.0" }]
[[package]]
name = "deepseek-harness-runtime-bin"
version = "0.0.0.dev0"
source = { editable = "../sdk-runtime" }
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]