Merge remote-tracking branch 'origin/master' into worktree/minimal-profiles-bare-runtime
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write python/sdk/README.md
|
||||
README.md: 9640c7e8dfd011b94acdc781ae0e4fdc8ad87378
|
||||
README.zh.md: 47ac04f9083ef41e23fda8ec527c1da160fe4769
|
||||
README.md: 8cf366c27c8a604391ea85e298ba725e9987d428
|
||||
README.zh.md: a9258ce9aee9bce973107b49114d4ed6e81441e4
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user