Merge remote-tracking branch 'origin/master' into worktree/minimal-profiles-bare-runtime

This commit is contained in:
Yichen Jiang
2026-08-11 16:59:22 +08:00
31 changed files with 510 additions and 29 deletions
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/sdk/README.md
README.md: 9640c7e8dfd011b94acdc781ae0e4fdc8ad87378
README.zh.md: 47ac04f9083ef41e23fda8ec527c1da160fe4769
README.md: 8cf366c27c8a604391ea85e298ba725e9987d428
README.zh.md: a9258ce9aee9bce973107b49114d4ed6e81441e4
+1 -1
View File
@@ -42,7 +42,7 @@ with DeepSeekHarness(
The [Python SDK tutorial](../../docs/user/guide/python-sdk.md) uses a complete standalone Cordis file to demonstrate installation, direct SDK usage, and runs without the Web UI.
`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, events, notifications, session_root)`. The result has no prompt-level status or turn reason: `final_response` is the last committed root-session assistant text in the interval, not an output causally assigned to the prompt. Steering, injected context, and other queued work may contribute before idle.
`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end` in the interval, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the runtime protocol and raises `SdkProtocolError`. Both result fields describe the owned interval rather than an output or ending causally assigned to the prompt. Steering, injected context, and other queued work may contribute before idle.
`HarnessClient` retains discovered subagent ancestry for the lifetime of the runtime process. During each `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and all known descendant notifications in wire order, including nested subagent lifecycle and session events. `RunResult.events` contains root-session events only, so descendant messages cannot replace the root response. The low-level `session_prompt()` returns the queued `MessageId` immediately; callers that bypass `Session.run()` own any later activity boundary themselves.
+1 -1
View File
@@ -39,7 +39,7 @@ with DeepSeekHarness(
[Python SDK 教程](../../docs/user/guide/python-sdk.md)使用完整的独立 Cordis 文件演示安装方式、直接调用 SDK,以及在不使用 Web UI 的情况下运行 agent。
`Session.run()` 拥有一个从提示词进入持久 inbox 时开始、到整个 agent 下一次进入空闲状态为止的活动区间,并返回 `RunResult(session_id, final_response, events, notifications, session_root)`结果不携带提示词级状态或轮次原因:`final_response` 是该区间内根会话最后提交的助手文本,并非因果上归属于该提示词的输出。steering(中途引导)、注入的上下文和其他排队工作都可能在进入空闲状态前参与其中。
`Session.run()` 拥有一个从提示词进入持久 inbox 时开始、到整个 agent 下一次进入空闲状态为止的活动区间,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)``final_response` 是该区间内根会话最后提交的助手文本`finish_reason` 是该区间内根会话最后一个 `turn/end``kind`,例如 `completed``max-tokens``error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind``turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。两个结果字段描述的都是自有活动区间,而不是因果上归属于该提示词的输出或结束原因。steering(中途引导)、注入的上下文和其他排队工作都可能在进入空闲状态前参与其中。
`HarnessClient` 会在运行时进程的生命周期内保留已发现的 subagent(子 agent)祖先关系。每次执行 `Session.run()` 时,`RunResult.notifications``on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。
@@ -1,5 +1,6 @@
from .api import DeepSeekHarness, DeepSeekHarnessConfig, RunResult, Session
from .client import HarnessClient, HarnessConfig
from .errors import SdkProtocolError
from .models import IncomingRequest, InitializeResponse, JsonObject, Notification, ServerInfo
__all__ = [
@@ -9,6 +10,7 @@ __all__ = [
"RunResult",
"HarnessClient",
"HarnessConfig",
"SdkProtocolError",
"IncomingRequest",
"InitializeResponse",
"JsonObject",
+23
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from typing import Callable
from .client import HarnessClient, HarnessConfig
from .errors import SdkProtocolError
from .models import JsonObject, Notification
@@ -38,6 +39,7 @@ class DeepSeekHarnessConfig:
class RunResult:
session_id: str
final_response: str
finish_reason: str | None
events: list[JsonObject]
notifications: list[Notification]
session_root: str | None = None
@@ -174,6 +176,7 @@ class Session:
return RunResult(
session_id=self.id,
final_response=final_response(events),
finish_reason=finish_reason(events),
events=events,
notifications=notifications,
session_root=self.harness.config.session_root,
@@ -217,3 +220,23 @@ def final_response(events: list[JsonObject]) -> str:
parts.append(str(block.get("text") or ""))
return "".join(parts)
return ""
def finish_reason(events: list[JsonObject]) -> str | None:
"""Return the last turn-ending kind.
The input must contain root-session events from one owned run interval.
Raises:
SdkProtocolError: The last ``turn/end`` has no string reason kind.
"""
for event in reversed(events):
if event.get("type") != "turn/end":
continue
data = event.get("data")
reason = data.get("reason") if isinstance(data, dict) else None
kind = reason.get("kind") if isinstance(reason, dict) else None
if not isinstance(kind, str):
raise SdkProtocolError("turn/end event requires a string data.reason.kind")
return kind
return None
@@ -9,6 +9,10 @@ class TransportClosedError(HarnessError):
"""Raised when the runtime subprocess exits or closes stdout."""
class SdkProtocolError(HarnessError):
"""Raised when the runtime sends data outside the SDK protocol."""
class JsonRpcError(HarnessError):
"""Raised when the runtime returns a JSON-RPC error response."""
+61 -2
View File
@@ -9,7 +9,7 @@ from pathlib import Path
import pytest
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, SdkProtocolError
def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
@@ -58,6 +58,28 @@ for line in sys.stdin:
},
},
}), flush=True)
print(json.dumps({
"jsonrpc": "2.0",
"method": "session.event",
"params": {
"sessionId": params["sessionId"],
"event": {
"type": "turn/end",
"data": {"turn": 1, "reason": {"kind": "completed"}},
},
},
}), flush=True)
print(json.dumps({
"jsonrpc": "2.0",
"method": "session.event",
"params": {
"sessionId": params["sessionId"],
"event": {
"type": "turn/end",
"data": {"turn": 2, "reason": {"kind": "max-tokens"}},
},
},
}), flush=True)
print(json.dumps({
"jsonrpc": "2.0",
"method": "session.status",
@@ -86,7 +108,8 @@ for line in sys.stdin:
result = harness.run("say hello", session_id="main")
assert result.final_response == "hello from runtime"
assert result.events[-1]["type"] == "assistant/message"
assert result.finish_reason == "max-tokens"
assert result.events[-1]["type"] == "turn/end"
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"
@@ -137,6 +160,42 @@ for line in sys.stdin:
)
assert seen == ["session.event", "session.status", "subagent.started", "session.status"]
assert result.finish_reason is None
def test_high_level_sdk_rejects_turn_end_without_reason_kind(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": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), 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": "turn/end", "data": {"turn": 1, "reason": {}}}}}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), 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:
with pytest.raises(
SdkProtocolError,
match=r"turn/end event requires a string data\.reason\.kind",
):
harness.run("reject malformed turn ending", session_id="main")
def test_relative_cwd_is_absolute_in_process_environment_and_wire(