Merge master and address image offload review
This commit is contained in:
commit
60f9d9b936
4291 files changed
+65094
-23525
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
|
||||
README.md: 050ae85d9b0a38b82a3c66a84c3d8f34e137be6c
|
||||
README.zh.md: 7066d7224752294c25b58cfe8fb6a94524a2013d
|
||||
README.md: fb7478f305015e60dafd23861ab6fd91f10757f3
|
||||
README.zh.md: 7c663aba8d387fb7b1048afe28d50faee7350303
|
||||
@@ -19,7 +19,7 @@ Both carriers execute the same `dsh` grammar and shipped profiles, including the
|
||||
- `bundled_package_dir() -> Path` returns the installed module-data root and verifies its release metadata.
|
||||
- `bundled_runtime_path() -> Path` returns the current platform executable and verifies required sidecars.
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` returns the executable argv by default. Explicit `mode="node"` or `DSH_RUNTIME_MODE=node` selects the repo-only Node carrier.
|
||||
- `main()` implements the installed `dsh` console command and rejects an absent or blank `DSH_HOME` before replacing the Python process.
|
||||
- `main()` implements the installed `dsh` console command and rejects an absent or blank `DSH_HOME`. On Windows it waits for the bundled process with inherited standard streams and forwards its exit status; on POSIX it replaces the Python process.
|
||||
|
||||
Unsupported platforms and missing executables or sidecars raise `FileNotFoundError` with the build and installation routes. Unknown runtime modes raise `ValueError`.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模
|
||||
- `bundled_package_dir() -> Path` 返回已安装模块数据根目录,并校验发布元数据。
|
||||
- `bundled_runtime_path() -> Path` 返回当前平台可执行程序,并校验必需伴随文件。
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` 默认返回可执行程序 argv。显式 `mode="node"` 或 `DSH_RUNTIME_MODE=node` 会选择仅限仓库使用的 Node 载体。
|
||||
- `main()` 实现已安装的 `dsh` 控制台命令,并在替换 Python 进程前拒绝缺失或空白的 `DSH_HOME`。
|
||||
- `main()` 实现已安装的 `dsh` 控制台命令,并拒绝缺失或空白的 `DSH_HOME`。在 Windows 上,它让打包进程继承标准流,等待其结束并转发退出状态;在 POSIX 上,它替换 Python 进程。
|
||||
|
||||
不支持的平台以及缺失的可执行程序或伴随文件会抛出 `FileNotFoundError`,并指出构建与安装路径。未知运行时模式会抛出 `ValueError`。
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -156,7 +157,7 @@ def _node_launch_args() -> tuple[str, str]:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Execute the bundled dsh CLI with an explicitly selected Harness home."""
|
||||
"""Launch the CLI with explicit DSH_HOME; wait on Windows, replace the process on POSIX."""
|
||||
if not os.environ.get("DSH_HOME", "").strip():
|
||||
print(
|
||||
"dsh: the Python runtime command requires an explicit DSH_HOME; "
|
||||
@@ -165,6 +166,9 @@ def main() -> None:
|
||||
)
|
||||
raise SystemExit(2)
|
||||
argv = (*resolve_bundled_launch_args(), *sys.argv[1:])
|
||||
if sys.platform == "win32":
|
||||
# Windows CRT exec does not replace the process; wait and preserve the runtime status.
|
||||
raise SystemExit(subprocess.run(argv, env=os.environ).returncode)
|
||||
os.execvpe(argv[0], argv, os.environ)
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,17 @@ for line in sys.stdin:
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.event",
|
||||
"params": {
|
||||
"sessionId": params["sessionId"],
|
||||
"event": {
|
||||
"type": "image/offload",
|
||||
"data": {"turn": 1, "step": 1, "watermark": {"seq": 7, "path": [3]}},
|
||||
},
|
||||
},
|
||||
}), flush=True)
|
||||
print(json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.event",
|
||||
@@ -109,6 +120,10 @@ for line in sys.stdin:
|
||||
|
||||
assert result.final_response == "hello from runtime"
|
||||
assert result.finish_reason == "max-tokens"
|
||||
assert result.events[1] == {
|
||||
"type": "image/offload",
|
||||
"data": {"turn": 1, "step": 1, "watermark": {"seq": 7, "path": [3]}},
|
||||
}
|
||||
assert result.events[-1]["type"] == "turn/end"
|
||||
dumped_env = json.loads(env_dump.read_text())
|
||||
assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import deepseek_harness_runtime as runtime
|
||||
import pytest
|
||||
@@ -129,7 +133,7 @@ def test_python_dsh_command_executes_the_bundled_cli(
|
||||
called: dict[str, object] = {}
|
||||
monkeypatch.setenv("DSH_HOME", "/explicit/home")
|
||||
monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("/runtime",))
|
||||
monkeypatch.setattr(runtime.sys, "argv", ["dsh", "plugin", "--profile", "sdk", "list"])
|
||||
monkeypatch.setattr(runtime, "sys", SimpleNamespace(platform="linux", argv=["dsh", "plugin", "--profile", "sdk", "list"]))
|
||||
|
||||
def execvpe(file: str, args: tuple[str, ...], env: dict[str, str]) -> None:
|
||||
called.update(file=file, args=args, home=env.get("DSH_HOME"))
|
||||
@@ -143,3 +147,51 @@ def test_python_dsh_command_executes_the_bundled_cli(
|
||||
"args": ("/runtime", "plugin", "--profile", "sdk", "list"),
|
||||
"home": "/explicit/home",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("returncode", [0, 37, 513])
|
||||
def test_windows_console_waits_and_forwards_runtime_status(monkeypatch: pytest.MonkeyPatch, returncode: int) -> None:
|
||||
monkeypatch.setenv("DSH_HOME", "/explicit/home")
|
||||
monkeypatch.setattr(runtime, "sys", SimpleNamespace(platform="win32", argv=["dsh", "plugin", "argument with spaces", "中文"]))
|
||||
monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("runtime.exe",))
|
||||
called = []
|
||||
|
||||
def run(args: tuple[str, ...], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
||||
called.append((args, kwargs))
|
||||
return subprocess.CompletedProcess(args, returncode)
|
||||
|
||||
def forbidden_exec(*args: object) -> None:
|
||||
pytest.fail("Windows console must wait instead of entering CRT exec")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", run)
|
||||
monkeypatch.setattr(runtime.os, "execvpe", forbidden_exec)
|
||||
with pytest.raises(SystemExit) as result:
|
||||
main()
|
||||
assert result.value.code == returncode
|
||||
assert called == [(("runtime.exe", "plugin", "argument with spaces", "中文"), {"env": os.environ})]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("returncode", [0, 37, pytest.param(513, marks=pytest.mark.skipif(sys.platform != "win32", reason="POSIX truncates process exit codes to eight bits"))])
|
||||
def test_windows_console_branch_preserves_real_child_io_and_completion(tmp_path: Path, returncode: int) -> None:
|
||||
child = tmp_path / "child with spaces.py"
|
||||
sentinel = tmp_path / "finished"
|
||||
child.write_text(
|
||||
"import pathlib,sys\n"
|
||||
"assert sys.argv[1] == 'argument with spaces'\n"
|
||||
"assert sys.argv[2] == '中文'\n"
|
||||
"print('stdout-中文', flush=True)\n"
|
||||
"print('stderr-中文', file=sys.stderr, flush=True)\n"
|
||||
f"pathlib.Path({str(sentinel)!r}).write_text('done')\n"
|
||||
f"raise SystemExit({returncode})\n", encoding="utf-8",
|
||||
)
|
||||
driver = (
|
||||
"import deepseek_harness_runtime as runtime; from types import SimpleNamespace; "
|
||||
f"runtime.sys = SimpleNamespace(platform='win32', argv=['dsh', 'argument with spaces', '中文']); "
|
||||
f"runtime.resolve_bundled_launch_args = lambda: ({sys.executable!r}, {str(child)!r}); runtime.main()"
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", driver], capture_output=True, text=True, encoding="utf-8",
|
||||
env={**os.environ, "DSH_HOME": str(tmp_path), "PYTHONIOENCODING": "utf-8"}, timeout=15)
|
||||
assert result.returncode == returncode, result.stderr
|
||||
assert result.stdout == "stdout-中文\n"
|
||||
assert result.stderr == "stderr-中文\n"
|
||||
assert sentinel.read_text() == "done"
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -71,3 +72,208 @@ def test_mcp_smoke_accepts_the_external_server_result() -> None:
|
||||
for chunk in chunks
|
||||
for choice in chunk.get("choices", [])
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_comparison_normalizes_only_session_generation_provenance() -> None:
|
||||
normalize = SMOKE["normalize_session_format_comparison"]
|
||||
expected = {
|
||||
"header": {"type": "session", "version": 0, "otherVersion": 7},
|
||||
"accepted": {
|
||||
"type": "session-log-deepseek/delivery-accepted",
|
||||
"data": {"sessionId": "s", "throughSeq": 4},
|
||||
},
|
||||
"source": {
|
||||
"kind": "session-reference",
|
||||
"references": [{"sessionId": "other", "capturedThroughSeq": 8}],
|
||||
},
|
||||
}
|
||||
actual = {
|
||||
"header": {"type": "session", "version": 1, "otherVersion": 7},
|
||||
"accepted": {
|
||||
"type": "session-log-deepseek/delivery-accepted",
|
||||
"data": {"sessionId": "s", "sessionFormatVersion": 1, "throughSeq": 4},
|
||||
},
|
||||
"source": {
|
||||
"kind": "session-reference",
|
||||
"references": [{
|
||||
"sessionId": "other",
|
||||
"capturedFormatVersion": 1,
|
||||
"capturedThroughSeq": 8,
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
assert normalize(expected) == normalize(actual)
|
||||
assert normalize(expected)["header"]["otherVersion"] == 7
|
||||
|
||||
|
||||
def test_snapshot_value_normalizes_embedded_assistant_stream_timing() -> None:
|
||||
normalize = SMOKE["normalize_snapshot_value"]
|
||||
event = {
|
||||
"type": "assistant/message",
|
||||
"seq": 4,
|
||||
"time": 100,
|
||||
"data": {
|
||||
"stream": [
|
||||
{"type": "chunk", "time": 101, "chunk": {"type": "finish"}},
|
||||
{"type": "text-chunks", "time0": 102, "dt": [1, 2], "texts": ["a", "b", "c"]},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
normalized = normalize(event, [])
|
||||
|
||||
assert normalized["time"] == 0
|
||||
assert normalized["data"]["stream"] == [
|
||||
{"type": "chunk", "time": 0, "chunk": {"type": "finish"}},
|
||||
{"type": "text-chunks", "time0": 0, "dt": [0, 0], "texts": ["a", "b", "c"]},
|
||||
]
|
||||
|
||||
|
||||
def test_snapshot_comparison_expands_embedded_assistant_streams() -> None:
|
||||
normalize = SMOKE["normalize_session_format_comparison"]
|
||||
expected = [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"seq": 4,
|
||||
"time": 0,
|
||||
"data": {"turn": 1, "step": 1, "chunk": {
|
||||
"type": "text-delta", "index": 0, "text": "done",
|
||||
}},
|
||||
},
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"seq": 5,
|
||||
"time": 0,
|
||||
"data": {"turn": 1, "step": 1, "message": {"role": "assistant"}},
|
||||
"sourceEventSeqs": [4],
|
||||
"surfaceOp": "append",
|
||||
},
|
||||
]
|
||||
actual = [{
|
||||
"type": "assistant/message",
|
||||
"seq": 4,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {"role": "assistant"},
|
||||
"stream": [{
|
||||
"type": "text-chunks", "time0": 0, "index": 0, "dt": [], "texts": ["done"],
|
||||
}],
|
||||
},
|
||||
"surfaceOp": "append",
|
||||
}]
|
||||
|
||||
assert normalize(actual, 2) == normalize(expected, 1)
|
||||
|
||||
tool_result = {
|
||||
"type": "tool/result",
|
||||
"data": {"turn": 1, "step": 1},
|
||||
"sourceEventSeqs": [4],
|
||||
}
|
||||
assert normalize(tool_result, 1)["sourceEventSeqs"] == [4]
|
||||
assert normalize(tool_result, 2)["sourceEventSeqs"] == [4]
|
||||
|
||||
|
||||
def test_snapshot_stream_expands_reasoning_and_tool_call_records() -> None:
|
||||
expand = SMOKE["expand_snapshot_stream_member"]
|
||||
|
||||
assert expand({
|
||||
"type": "reasoning-chunks", "time0": 0, "index": 1,
|
||||
"dt": [], "texts": ["think"],
|
||||
}) == [{"type": "reasoning-delta", "index": 1, "text": "think"}]
|
||||
assert expand({
|
||||
"type": "tool-call-chunks", "time0": 0, "index": 2,
|
||||
"id": "call-1", "name": "read", "dt": [1], "args": ["{", "}"],
|
||||
}) == [
|
||||
{"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "{"},
|
||||
{"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "}"},
|
||||
]
|
||||
|
||||
|
||||
def test_snapshot_file_builder_order_is_checked_outside_update_mode(tmp_path: Path) -> None:
|
||||
compare = SMOKE["compare_snapshot_files"]
|
||||
|
||||
with pytest.raises(AssertionError, match="snapshot builder produced"):
|
||||
compare({}, False, tmp_path, ("result.json",))
|
||||
|
||||
|
||||
def test_snapshot_comparison_expands_sdk_wrapped_attempts() -> None:
|
||||
normalize = SMOKE["normalize_session_format_comparison"]
|
||||
actual = [{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "s",
|
||||
"event": {
|
||||
"type": "assistant/attempt",
|
||||
"seq": 7,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"stream": [{"type": "chunk", "time": 0, "chunk": {"type": "finish"}}],
|
||||
},
|
||||
},
|
||||
},
|
||||
}]
|
||||
|
||||
assert normalize(actual) == [{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "s",
|
||||
"event": {
|
||||
"type": "assistant/chunk",
|
||||
"data": {"turn": 1, "step": 1, "chunk": {"type": "finish"}},
|
||||
},
|
||||
},
|
||||
}]
|
||||
|
||||
|
||||
def test_snapshot_generation_names_select_highest_role_without_double_counting(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
render = SMOKE["snapshot_session_filename"]
|
||||
select = SMOKE["selected_snapshot_session_files"]
|
||||
assert render(0, 0) == "session.jsonl"
|
||||
assert render(0, 2) == "session.v2.jsonl"
|
||||
assert render(3, 0) == "session.3.jsonl"
|
||||
assert render(3, 2) == "session.3.v2.jsonl"
|
||||
|
||||
(tmp_path / "session.jsonl").write_text(
|
||||
'{"type":"session","version":0}\n', encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "session.v1.jsonl").write_text(
|
||||
'{"type":"session","version":1}\n', encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "session.1.jsonl").write_text(
|
||||
'{"type":"session","version":0}\n', encoding="utf-8",
|
||||
)
|
||||
|
||||
assert {index: path.name for index, path in select(tmp_path).items()} == {
|
||||
0: "session.v1.jsonl",
|
||||
1: "session.1.jsonl",
|
||||
}
|
||||
|
||||
|
||||
def test_snapshot_generation_filename_must_match_header(tmp_path: Path) -> None:
|
||||
(tmp_path / "session.v1.jsonl").write_text(
|
||||
'{"type":"session","version":0}\n', encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(AssertionError, match="filename declares Session format v1"):
|
||||
SMOKE["selected_snapshot_session_files"](tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("returncode", [1, -1073741819, 3221225477])
|
||||
def test_profile_plugin_failure_reports_native_exit_status(monkeypatch: pytest.MonkeyPatch, returncode: int) -> None:
|
||||
def failed_install(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.CompletedProcess(args=[], returncode=returncode, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", failed_install)
|
||||
with pytest.raises(AssertionError) as error:
|
||||
SMOKE["smoke_sdk_profile_plugin"]("http://127.0.0.1:1")
|
||||
message = str(error.value)
|
||||
assert f"returncode={returncode}" in message
|
||||
assert f"0x{returncode & 0xffffffff:08x}" in message
|
||||
assert "stdout='' stderr=''" in message
|
||||
Reference in new issue
Block a user