fix(sdk): keep query internal and return session identity

This commit is contained in:
LogosVeritas committed 2026-10-28 17:15:21 +08:00
1 parent 9976a87e97
commit 1950259ce1
25 files changed
+229 -111

No files matched your search

+2 -2
View File
@@ -12,8 +12,8 @@
en: ac793c588b13ce87
zh: 502015cda46de3ac
/deepseek-harness-python-sdk/run-text-and-commands:
en: 4a70013e5ba565a9
zh: 00bb9b06d38f91ba
en: 477bb559cbf13f84
zh: c5825be4ae2f1508
/deepseek-harness-python-sdk/client-approvals:
en: fc69be1beb8abff1
zh: 91b56781931a6f68
+6 -6
View File
@@ -61,21 +61,21 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
<a id="run-text-and-commands"></a>
## Run text and commands
`session.run(input, on_notification=..., delivery="queue")` is the high-level input entry for strings and content-block arrays. Both use the runtime's [supported command forms and Desktop aliases](../../packages/sdk/server/README.md#command-input); unrecognized input remains an ordinary prompt with its original block order. `delivery="steer"` targets the next available step for ordinary input, including images. Command admission and timing remain owned by the native handler.
`session.run(input, on_notification=..., delivery="queue")` is the public prompt-or-command input entry for strings and content-block arrays. Both use the runtime's [supported command forms and Desktop aliases](../../packages/sdk/server/README.md#command-input); unrecognized input remains an ordinary prompt with its original block order. `delivery="steer"` targets the next available step for ordinary input, including images. Command admission and timing remain owned by the native handler.
The call returns `RunResult` for an ordinary prompt's inbox-receipt-to-idle interval, or `CommandExecution` after a command settles. Use `isinstance(result, RunResult)` before reading `final_response`; otherwise inspect the command's `result.kind`. A command retains its `command_id` and optional `source_event_seq`, rather than selecting an earlier assistant reply.
The call returns `RunResult` for an ordinary prompt's inbox-receipt-to-idle interval, or `RunCommandResult` after a command settles. Both results include `session_id`, including when `harness.run()` creates a fresh Session. `RunCommandResult` extends the native `CommandExecution` with that Session identity. Use `isinstance(result, RunResult)` before reading `final_response`; otherwise inspect the command's `result.kind`. A command retains its `command_id` and optional `source_event_seq`, rather than selecting an earlier assistant reply.
Content-block arrays use the same command routing as strings. Both `session.run([{"type": "text", "text": "/compact"}])` and `session.run("/compact")` invoke native compaction and return `CommandExecution`. To bypass native command routing, call the low-level `client.session_prompt(session_id, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies.
Content-block arrays use the same command routing as strings. Both `session.run([{"type": "text", "text": "/compact"}])` and `session.run("/compact")` invoke native compaction and return `RunCommandResult`. To bypass native command routing, call the low-level `client.session_prompt(session_id, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies.
Select a model that supports image input. Submit a command and image in the existing content-block array, for example `session.run([{"type": "text", "text": "/plan Analyze this image."}, {"type": "image", "data": image_base64, "mimeType": "image/png"}])`; `harness.run()` accepts the same input. Plan input and goal creation or editing accept images; `/plan off`, goal controls, compact, and permission commands reject them through the [native attachment rules](../../packages/sdk/server/README.md#command-input). Native command input supports encoded raster images, not general file uploads or client-supplied attachment references.
Each call subscribes before sending and releases its subscription before returning or raising. `on_notification` receives the ordinary activity interval in wire order; command callbacks replay Session-tree notifications from subscription through their own `command/done` after the command response, so that interval can include overlapping work. Calls may overlap without consuming each other's results. Handler refusals return `CommandExecution.result.kind="error"`; routing, protocol, transport and observer failures raise. Malformed native-command responses from `run()` or `session_command()` raise `SdkProtocolError`, with the model validation error retained as the cause. A timeout or observer failure does not roll back runtime work or authorize an automatic retry. `request_timeout_seconds` bounds the protocol response wait, not later ordinary prompt activity.
Each call subscribes before sending and releases its subscription before returning or raising. `on_notification` receives the ordinary activity interval in wire order; command callbacks replay Session-tree notifications from subscription through their own `command/done` after the command response, so that interval can include overlapping work. Calls may overlap without consuming each other's results. Handler refusals return `RunCommandResult.result.kind="error"`; routing, protocol, transport and observer failures raise. Malformed native-command responses from `run()` or `session_command()` raise `SdkProtocolError`, with the model validation error retained as the cause. A timeout or observer failure does not roll back runtime work or authorize an automatic retry. `request_timeout_seconds` bounds the protocol response wait, not later ordinary prompt activity.
The default full `sdk` profile provides the supported native commands; `sdk-minimal` keeps unsupported input on its ordinary prompt path. `/compact` can make an auxiliary model request and completes without a normal turn or idle notification; empty history can succeed without a summary. The lower-level `HarnessClient.session_command(session_id, line)` accepts ASCII registered names or canonical English aliases of supported builtin definitions, without prompt fallback or attachments. Use `run()` for Chinese aliases. See the [compaction example](examples/README.md#compact-a-full-sdk-session).
The default full `sdk` profile provides the supported native commands; `sdk-minimal` keeps unsupported input on its ordinary prompt path. `/compact` can make an auxiliary model request and completes without a normal turn or idle notification; empty history can succeed without a summary. The lower-level `HarnessClient.session_command(session_id, line)` accepts ASCII registered names or canonical English aliases of supported builtin definitions, without prompt fallback or attachments, and returns the native `CommandExecution`. Use `run()` for Chinese aliases. See the [compaction example](examples/README.md#compact-a-full-sdk-session).
`/permission <preset>` changes only the receiving Session. An unknown preset returns a command error without changing policy. Later input and restoration retain successful changes; other Sessions and the caller's `DeepSeekHarnessConfig` keep their own values. A running approval-policy change can inject a notice and cause another model step. See the [permission example](examples/README.md#change-session-permissions).
`/plan` enters plan mode, `/plan off` leaves, and `/plan <message>` enters then submits the trimmed message through native steer. Its `CommandExecution` confirms command completion, not completion of planning or review. Enter with `/plan` then run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and use `session/wait` while keeping the question handler alive, as in the [plan example](examples/README.md#review-a-plan). Plan guidance and `exit_plan_mode` follow their native lifecycle without changing permission settings.
`/plan` enters plan mode, `/plan off` leaves, and `/plan <message>` enters then submits the trimmed message through native steer. Its `RunCommandResult` confirms command completion, not completion of planning or review. Enter with `/plan` then run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and use `session/wait` while keeping the question handler alive, as in the [plan example](examples/README.md#review-a-plan). Plan guidance and `exit_plan_mode` follow their native lifecycle without changing permission settings.
`/goal` shows the current goal; `/goal <objective>`, `edit <objective>`, `pause`, `resume`, and `clear` use the native handler. Creation and resume can start automatic rounds after `run()` returns. Subscribe before sending, observe terminal `goal/change` operations, then call `session/wait` for any closing model reply. Idle between rounds is not goal completion. A restored active goal stays disarmed until explicit resume. See the [goal example](examples/README.md#run-a-goal).
+6 -6
View File
@@ -61,21 +61,21 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
<a id="run-text-and-commands"></a>
## 运行文本与命令
`session.run(input, on_notification=..., delivery="queue")` 是字符串和内容块数组共用的高层输入入口。两者都使用运行时的[命令形式与 Desktop 别名](../../packages/sdk/server/README.zh.md#command-input)识别规则;未识别的输入保持普通提示词及原始内容块顺序。对普通输入,包括图片,`delivery="steer"` 指向下一个可用步骤。命令的接纳规则与执行时序仍由原生处理器负责。
`session.run(input, on_notification=..., delivery="queue")` 是字符串和内容块数组共用的公开提示词/命令输入入口。两者都使用运行时的[命令形式与 Desktop 别名](../../packages/sdk/server/README.zh.md#command-input)识别规则;未识别的输入保持普通提示词及原始内容块顺序。对普通输入,包括图片,`delivery="steer"` 指向下一个可用步骤。命令的接纳规则与执行时序仍由原生处理器负责。
普通提示词返回其入队回执到下一次 idle 区间的 `RunResult`,命令则在执行结束后返回 `CommandExecution`。读取 `final_response` 前使用 `isinstance(result, RunResult)` 检查类型,否则检查命令的 `result.kind`。命令保留自己的 `command_id` 和可选 `source_event_seq`,不会选取更早的助手回复作为结果。
普通提示词返回其入队回执到下一次 idle 区间的 `RunResult`,命令则在执行结束后返回 `RunCommandResult`。两种结果都包含 `session_id`,包括 `harness.run()` 创建新 Session 的情况。`RunCommandResult` 在原生 `CommandExecution` 上增加该 Session 标识。读取 `final_response` 前使用 `isinstance(result, RunResult)` 检查类型,否则检查命令的 `result.kind`。命令保留自己的 `command_id` 和可选 `source_event_seq`,不会选取更早的助手回复作为结果。
内容块数组与字符串使用相同的命令路由。`session.run([{"type": "text", "text": "/compact"}])` 和 `session.run("/compact")` 都调用原生压缩并返回 `CommandExecution`。要绕过原生命令路由,调用低层 `client.session_prompt(session_id, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。
内容块数组与字符串使用相同的命令路由。`session.run([{"type": "text", "text": "/compact"}])` 和 `session.run("/compact")` 都调用原生压缩并返回 `RunCommandResult`。要绕过原生命令路由,调用低层 `client.session_prompt(session_id, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。
先选择支持图片输入的模型。通过现有内容块数组同时提交命令和图片,例如 `session.run([{"type": "text", "text": "/plan Analyze this image."}, {"type": "image", "data": image_base64, "mimeType": "image/png"}])`;`harness.run()` 接受相同输入。计划输入和目标创建、编辑接受图片;`/plan off`、目标控制、压缩和权限命令按[原生附件规则](../../packages/sdk/server/README.zh.md#command-input)拒绝图片。原生命令输入支持编码栅格图片,不支持一般文件上传或客户端提供的附件引用。
每次调用在发送前订阅,并在返回或抛出异常前释放自己的订阅。`on_notification` 按线上的顺序接收普通活动区间通知;命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知,因此该区间可能包含重叠工作的通知。调用可以重叠,不会消耗彼此的结果。处理器拒绝返回 `CommandExecution.result.kind="error"`,路由、协议、传输和观察回调失败则抛出异常。`run()` 或 `session_command()` 收到格式错误的原生命令响应时抛出 `SdkProtocolError`,并保留模型校验错误作为 cause。超时或观察回调失败不回滚运行时工作,也不授权自动重试。`request_timeout_seconds` 限制协议响应等待,不限制之后的普通提示词活动。
每次调用在发送前订阅,并在返回或抛出异常前释放自己的订阅。`on_notification` 按线上的顺序接收普通活动区间通知;命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知,因此该区间可能包含重叠工作的通知。调用可以重叠,不会消耗彼此的结果。处理器拒绝返回 `RunCommandResult.result.kind="error"`,路由、协议、传输和观察回调失败则抛出异常。`run()` 或 `session_command()` 收到格式错误的原生命令响应时抛出 `SdkProtocolError`,并保留模型校验错误作为 cause。超时或观察回调失败不回滚运行时工作,也不授权自动重试。`request_timeout_seconds` 限制协议响应等待,不限制之后的普通提示词活动。
默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件。中文别名使用 `run()`。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。
默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件,返回原生 `CommandExecution`。中文别名使用 `run()`。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。
`/permission <preset>` 只修改接收命令的 Session。未知 preset 返回命令错误,不改变策略。后续输入与恢复保留成功的修改,其他 Session 和调用方的 `DeepSeekHarnessConfig` 保留自己的值。运行中的审批策略变化可以注入通知并触发下一模型步骤。见[权限示例](examples/README.zh.md#change-session-permissions)。
`/plan` 进入计划模式,`/plan off` 退出,`/plan <message>` 进入后通过原生 steer 提交去除首尾空白的正文。其 `CommandExecution` 确认命令执行结束,不代表计划生成或审阅结束。需要 `RunResult` 时,先用 `/plan` 进入,再运行普通文本。组合输入应在 `run()` 前订阅,并保持问答处理器存活,通过 `session/wait` 观察后续工作,见[计划示例](examples/README.zh.md#review-a-plan)。计划指导和 `exit_plan_mode` 遵循原生生命周期,不修改权限设置。
`/plan` 进入计划模式,`/plan off` 退出,`/plan <message>` 进入后通过原生 steer 提交去除首尾空白的正文。其 `RunCommandResult` 确认命令执行结束,不代表计划生成或审阅结束。需要 `RunResult` 时,先用 `/plan` 进入,再运行普通文本。组合输入应在 `run()` 前订阅,并保持问答处理器存活,通过 `session/wait` 观察后续工作,见[计划示例](examples/README.zh.md#review-a-plan)。计划指导和 `exit_plan_mode` 遵循原生生命周期,不修改权限设置。
`/goal` 查看当前目标;`/goal <objective>`、`edit <objective>`、`pause`、`resume` 和 `clear` 使用原生处理器。创建和恢复可以在 `run()` 返回后启动自动轮次。发送前订阅,观察终态 `goal/change` 操作后,再调用 `session/wait` 等待可能的模型收尾。轮次之间的 idle 不代表目标完成。恢复的活动目标在显式 resume 前保持 disarmed。见[目标示例](examples/README.zh.md#run-a-goal)。
+3 -3
View File
@@ -7,12 +7,12 @@ import argparse
import os
from pathlib import Path
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult
from deepseek_harness import RunCommandResult, DeepSeekHarness, RunResult
def display(result: RunResult | CommandExecution) -> None:
def display(result: RunResult | RunCommandResult) -> None:
"""Print the input's result and stop on a reported failure."""
if isinstance(result, CommandExecution):
if isinstance(result, RunCommandResult):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
+2 -2
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from pydantic import BaseModel
from deepseek_harness import CommandExecution, DeepSeekHarness, Notification
from deepseek_harness import RunCommandResult, DeepSeekHarness, Notification
from deepseek_harness.api import final_response
@@ -68,7 +68,7 @@ def main() -> None:
return event
execution = session.run(f"/goal {args.objective}")
if not isinstance(execution, CommandExecution):
if not isinstance(execution, RunCommandResult):
raise RuntimeError("The SDK profile did not execute the requested command")
print(execution.result.text, flush=True)
if execution.result.kind == "error":
+2 -2
View File
@@ -7,7 +7,7 @@ import argparse
import os
from pathlib import Path
from deepseek_harness import CommandExecution, DeepSeekHarness
from deepseek_harness import RunCommandResult, DeepSeekHarness
def main() -> None:
@@ -41,7 +41,7 @@ def main() -> None:
profile=args.profile,
) as harness:
result = harness.run(args.prompt, session_id=args.session_id)
if isinstance(result, CommandExecution):
if isinstance(result, RunCommandResult):
if result.result.kind == "error":
raise RuntimeError(result.result.text)
print(result.result.text or "")
+3 -3
View File
@@ -7,12 +7,12 @@ import argparse
import os
from pathlib import Path
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult
from deepseek_harness import RunCommandResult, DeepSeekHarness, RunResult
def display(result: RunResult | CommandExecution) -> None:
def display(result: RunResult | RunCommandResult) -> None:
"""Print the input's result and stop on a reported failure."""
if isinstance(result, CommandExecution):
if isinstance(result, RunCommandResult):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
+2 -2
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from pydantic import BaseModel
from deepseek_harness import CommandExecution, DeepSeekHarness, Notification, UserQuestionRequest
from deepseek_harness import RunCommandResult, DeepSeekHarness, Notification, UserQuestionRequest
from deepseek_harness.api import final_response
@@ -173,7 +173,7 @@ def main() -> None:
session = harness.start_session()
with closing(harness.client.subscribe_session_notifications(session.id)) as notifications:
execution = session.run(f"/plan {args.task}")
if not isinstance(execution, CommandExecution):
if not isinstance(execution, RunCommandResult):
raise RuntimeError("The SDK profile did not execute the requested command")
print(execution.result.text, flush=True)
if execution.result.kind == "error":
+2 -1
View File
@@ -1,4 +1,4 @@
from .api import DeepSeekHarness, DeepSeekHarnessConfig, RunResult, Session
from .api import RunCommandResult, DeepSeekHarness, DeepSeekHarnessConfig, RunResult, Session
from .client import HarnessClient, HarnessConfig, UserQuestionHandler
from .errors import SdkProtocolError
from .models import (
@@ -12,6 +12,7 @@ __all__ = [
"DeepSeekHarnessConfig",
"Session",
"RunResult",
"RunCommandResult",
"CommandExecution",
"CommandResult",
"CommandSuccessResult",
+12 -5
View File
@@ -50,6 +50,12 @@ class RunResult:
notifications: list[Notification]
class RunCommandResult(CommandExecution):
"""Settled native command result with the Session id for continued interaction."""
session_id: str
class _PromptActivity:
"""Collect an ordinary prompt's notifications from its inbox receipt through idle."""
@@ -174,8 +180,8 @@ class DeepSeekHarness:
session_id: str | None = None,
on_notification: Callable[[Notification], None] | None = None,
delivery: Literal["queue", "steer"] = "queue",
) -> RunResult | CommandExecution:
"""Run text or content blocks through native input routing on a fresh or named Session."""
) -> RunResult | RunCommandResult:
"""Run input on a fresh or named Session, returning its id with either result kind."""
return self.start_session(session_id).run(input, on_notification=on_notification, delivery=delivery)
@@ -200,11 +206,12 @@ class Session:
*,
on_notification: Callable[[Notification], None] | None = None,
delivery: Literal["queue", "steer"] = "queue",
) -> RunResult | CommandExecution:
) -> RunResult | RunCommandResult:
"""Run text or content blocks through native input routing.
Input returns the prompt's receipt-to-idle RunResult or a settled
CommandExecution; command completion does not wait for later model work.
RunCommandResult. Both include this Session's id. Command completion does
not wait for later model work.
The first text block selects native commands. Matched commands require
one text block with only encoded images beside it; native handlers decide
which commands accept those images. Ordinary prompts retain block order
@@ -232,7 +239,7 @@ class Session:
on_notification(notification)
if _is_command_done(notification, self.id, execution.command_id):
break
return execution
return RunCommandResult(session_id=self.id, commandId=execution.command_id, result=execution.result)
activity = _PromptActivity(self.id, response)
while True:
notification = subscription.next()
+31 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from deepseek_harness import CommandExecution, RunResult, Session, UserQuestionRequest
from deepseek_harness import CommandExecution, RunCommandResult, RunResult, Session, UserQuestionRequest
from test_commands_runtime import (
_Completion, _Runtime, _SESSION_ID, _WAIT_SECONDS, _assert_request_permission,
_command_pair, _of_type, _permission_facts, _read_events,
@@ -214,6 +214,36 @@ def test_plan_state_survives_resume_without_changing_permissions(tmp_path: Path)
assert [e["data"] for e in _of_type(events, "plan/mode")] == [{"active": True}]
def test_top_level_plan_result_identifies_the_session_for_wait_continue_and_resume(tmp_path: Path) -> None:
first = _Completion("PLAN_READY", blocked=True)
with _runtime(tmp_path, [first, _Completion("CONTINUED_PLAN")], initial_permission="workspace-write") as runtime:
execution = runtime.call(runtime.harness.run, "/plan Design this change without tools.")
assert isinstance(execution, RunCommandResult)
assert execution.result.kind == "success"
assert first.entered.wait(_WAIT_SECONDS), runtime.model.failures
assert not first.release.is_set()
session = runtime.harness.start_session(execution.session_id)
first.release.set()
_wait_activity(runtime, session)
continued = runtime.call(runtime.harness.run, "Continue this plan without tools.", session_id=execution.session_id)
assert isinstance(continued, RunResult)
assert continued.session_id == execution.session_id
assert continued.final_response == "CONTINUED_PLAN"
assert "Design this change without tools." in _request_texts(runtime.model.captured()[1])
with _runtime(tmp_path, [_Completion("RESTORED_PLAN")], initial_permission="workspace-write") as resumed:
session = resumed.call(resumed.harness.resume_session, execution.session_id)
result = resumed.call(session.run, "Finish reviewing this plan without tools.")
assert isinstance(result, RunResult) and result.session_id == execution.session_id
assert result.final_response == "RESTORED_PLAN"
[request] = resumed.model.captured()
assert _GUIDANCE in _effective_system(request)
assert "Design this change without tools." in _request_texts(request)
assert "Continue this plan without tools." in _request_texts(request)
events = _read_events(resumed.logs, session_id=execution.session_id)
_command_pair(events, execution, "plan")
assert len(_of_type(events, "turn/start")) == 3
def test_plan_state_is_local_and_literal_prompts_do_not_toggle_it(tmp_path: Path) -> None:
with _runtime(tmp_path, [_Completion("OTHER"), _Completion("PLANNING"), _Completion("RAW_INPUT")],
initial_permission="workspace-write") as runtime:
+46 -1
View File
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor
import pytest
from pydantic import BaseModel, ValidationError
from deepseek_harness import CommandExecution, DeepSeekHarness, Notification, RunResult, SdkProtocolError
from deepseek_harness import CommandExecution, RunCommandResult, DeepSeekHarness, Notification, RunResult, SdkProtocolError
from deepseek_harness.errors import JsonRpcError
@@ -146,11 +146,56 @@ def test_run_returns_command_without_waiting_for_idle(sdk: DeepSeekHarness, entr
result = (sdk.run("/compact", session_id="main", on_notification=notifications.append)
if entry == "harness" else sdk.start_session("main").run("/compact", on_notification=notifications.append))
assert isinstance(result, CommandExecution)
assert isinstance(result, RunCommandResult)
assert result.session_id == "main"
assert result.result.kind == "success"
assert [notice.payload["event"]["type"] for notice in notifications] == ["command/run", "command/done"]
assert_released(sdk)
def test_top_level_command_results_identify_distinct_sessions_and_allow_continuing_one(
sdk: DeepSeekHarness, tmp_path: Path,
) -> None:
first = sdk.run("/compact")
second = sdk.run("/compact")
assert isinstance(first, RunCommandResult) and isinstance(second, RunCommandResult)
assert first.session_id and second.session_id and first.session_id != second.session_id
continued = sdk.run("continue the first session", session_id=first.session_id)
assert isinstance(continued, RunResult)
assert continued.session_id == first.session_id
sent = [json.loads(line) for line in (tmp_path / "requests.jsonl").read_text(encoding="utf-8").splitlines()]
queries = [frame["params"] for frame in sent if frame["method"] == "session/query"]
assert [query["sessionId"] for query in queries] == [first.session_id, second.session_id, first.session_id]
assert_released(sdk)
@pytest.mark.parametrize("result", [
{"kind": "success"},
{"kind": "success", "text": "Compacted.", "sourceEventSeq": 0},
{"kind": "success", "sourceEventSeq": 2**53 - 1},
{"kind": "error", "text": "The agent is busy."},
])
def test_run_adds_session_identity_without_changing_command_results(peer: Path, tmp_path: Path, result: dict) -> None:
execution = {"commandId": "custom-command", "result": result}
with DeepSeekHarness(
cwd=str(tmp_path), dsh_home=str(tmp_path / "home"), request_timeout_seconds=5,
_launch_args=(sys.executable, str(peer)),
env={"REQUEST_LOG": str(tmp_path / "requests.jsonl"), "QUERY_RESPONSE": json.dumps({
"kind": "command", "execution": execution,
})},
) as sdk:
returned = sdk.run("custom-response")
assert isinstance(returned, RunCommandResult)
assert returned.session_id
assert returned.model_dump(by_alias=True, exclude_none=True) == {
**execution, "session_id": returned.session_id,
}
low_level = sdk.client._query(returned.session_id, "custom-response", "queue")
assert isinstance(low_level, CommandExecution)
assert low_level.model_dump(by_alias=True, exclude_none=True) == execution
assert_released(sdk)
@pytest.mark.parametrize("text", ["ordinary", "fast-prompt", "/model", " /compact extra "])
def test_run_preserves_ordinary_text_and_result(sdk: DeepSeekHarness, text: str) -> None:
notifications = []