feat(sdk): route native commands through run

This commit is contained in:
LogosVeritas committed 2026-10-26 00:53:20 +08:00
1 parent b367e53a6c
commit e122cfb627
47 files changed
+880 -1369

No files matched your search

+7 -7
View File
@@ -6,17 +6,17 @@
en: c97ffc5ce0f7db05
zh: bdd91202075498a3
/python-sdk-example/run-the-minimal-agent:
en: 4b79fd6576e2759d
en: ac09bb6bc3ab76a0
zh: 6258540acaf4bc37
/python-sdk-example/compact-a-full-sdk-session:
en: ad4d67225570ed01
zh: 0d59f6dac40129d6
en: 7749773f97211ac1
zh: 594348ae66a2ef6c
/python-sdk-example/change-session-permissions:
en: 3471642d8444d0e9
zh: 7831f28295a6e68a
en: 1fe5e06c6842a53b
zh: ff0836d8cffaeef7
/python-sdk-example/review-a-plan:
en: 46758c022608af0f
zh: 09b5a59176c11176
en: 2596f68bb55082c8
zh: 1e40ca75e1a0f9b9
/python-sdk-example/run-a-goal:
en: ae73e9fd9d402047
zh: f9694bc6a6e12929
+6 -6
View File
@@ -25,14 +25,14 @@ The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../packages/bundle/sdk
- `read_image` for PNG/JPEG/WebP/GIF files, with an image-capable model
- `working_directory` to inspect or change the Session directory
The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, text file tools, local instruction discovery, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. [Automatic context compaction](../../../packages/bundle/sdk-minimal/README.md#model-experience) summarizes older history through an additional model request while preserving the complete log. SDK prompt text does not execute slash commands.
The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, text file tools, local instruction discovery, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. [Automatic context compaction](../../../packages/bundle/sdk-minimal/README.md#model-experience) summarizes older history through an additional model request while preserving the complete log. This minimal composition leaves unsupported slash input on the ordinary prompt path.
The persistent PTY can modify any path available to the runtime process, so use a disposable checkout or container.
<a id="compact-a-full-sdk-session"></a>
## Compact a full SDK Session
The [compaction example](compact.py) uses the full `sdk` profile. It reads `README.md` from the selected workspace into a `Session.query()` prompt, sends `/compact` through the same method, then asks a follow-up using the retained context. Each query is paired with `Session.receive_response()`. It uses the same model credentials and `DSH_MODEL` default as the minimal example, and requires an explicit home:
The [compaction example](compact.py) uses the full `sdk` profile. It reads `README.md` from the selected workspace into a `Session.run()` prompt, executes `/compact` through the same method, then asks a follow-up using the retained context. It uses the same model credentials and `DSH_MODEL` default as the minimal example, and requires an explicit home:
```sh
python python/sdk/examples/compact.py \
@@ -40,12 +40,12 @@ python python/sdk/examples/compact.py \
--workspace /absolute/path/to/disposable-workspace
```
The script consumes notifications and prints the command id and outcome separately from the model's responses. A failed turn or command stops the example. See [Query and response](../README.md#query-and-response) for completion and error behavior; `minimal.py` retains its `run()` entry and does not enable command execution.
The script prints command ids and outcomes separately from model responses. A failed turn or command stops the example. See [Run text and commands](../README.md#run-text-and-commands) for completion and error behavior; `minimal.py` uses the same `run()` entry, with capabilities supplied by its selected profile.
<a id="change-session-permissions"></a>
## Change Session permissions
The [permission example](permission.py) starts the full `sdk` profile with a `workspace-write` default, sends `/permission <preset>`, then submits an ordinary query on the same Session. Supply the preset explicitly:
The [permission example](permission.py) starts the full `sdk` profile with a `workspace-write` default, sends `/permission <preset>`, then runs ordinary text on the same Session. Supply the preset explicitly:
```sh
python python/sdk/examples/permission.py \
@@ -54,12 +54,12 @@ python python/sdk/examples/permission.py \
--preset read-only
```
The command changes the Session's policy while the printed launch setting remains `workspace-write`. The later query uses the Session's current policy. Bare `/permission` is ordinary text, and unknown presets produce a command error. See [Query and response](../README.md#query-and-response) for result and state semantics.
The command changes the Session's policy while the printed launch setting remains `workspace-write`. The later input uses the Session's current policy. Bare `/permission` is ordinary text, and unknown presets produce a command error. See [Run text and commands](../README.md#run-text-and-commands) for result and state semantics.
<a id="review-a-plan"></a>
## Review a plan
The [plan example](plan.py) sends `/plan <task>` through query and keeps the Session notification subscription and question handler alive after command completion. Enter `a` to approve, `k` to give feedback, or `d` to dismiss; EOF dismisses without approving. Approval allows implementation from a later model step, so choose an isolated workspace.
The [plan example](plan.py) sends `/plan <task>` through `run()` and keeps the Session notification subscription and question handler alive after command completion. Enter `a` to approve, `k` to give feedback, or `d` to dismiss; EOF dismisses without approving. Approval allows implementation from a later model step, so choose an isolated workspace.
```sh
python python/sdk/examples/plan.py \
+5 -5
View File
@@ -32,7 +32,7 @@ python python/sdk/examples/minimal.py \
<a id="compact-a-full-sdk-session"></a>
## 压缩完整 SDK Session
[压缩示例](compact.py)使用完整 `sdk` profile。它将所选工作区的 `README.md` 读入 `Session.query()` 输入,通过同一方法发送 `/compact`,随后使用保留的上下文继续提问。每次 query 都配对调用 `Session.receive_response()`。模型凭据和 `DSH_MODEL` 默认值与极简示例一致,并且要求显式指定 home:
[压缩示例](compact.py)使用完整 `sdk` profile。它将所选工作区的 `README.md` 读入 `Session.run()` 输入,通过同一方法执行 `/compact`,随后使用保留的上下文继续提问。模型凭据和 `DSH_MODEL` 默认值与极简示例一致,并且要求显式指定 home:
```sh
python python/sdk/examples/compact.py \
@@ -40,12 +40,12 @@ python python/sdk/examples/compact.py \
--workspace /absolute/path/to/disposable-workspace
```
脚本消费通知,并将命令 id 和结果与模型回复分开打印。轮次或命令失败时,示例会停止。完成和错误行为见[查询与接收](../README.zh.md#query-and-response);`minimal.py` 保留其 `run()` 入口,不启用命令执行。
脚本将命令 id 和结果与模型回复分开打印。轮次或命令失败时,示例会停止。完成和错误行为见[运行文本与命令](../README.zh.md#run-text-and-commands);`minimal.py` 使用同一个 `run()` 入口,能力由所选 profile 提供。
<a id="change-session-permissions"></a>
## 修改 Session 权限
[权限示例](permission.py)以 `workspace-write` 为默认值启动完整 `sdk` profile,发送 `/permission <preset>`,随后在同一个 Session 中提交普通 query。请显式指定 preset:
[权限示例](permission.py)以 `workspace-write` 为默认值启动完整 `sdk` profile,发送 `/permission <preset>`,随后在同一个 Session 中运行普通文本。请显式指定 preset:
```sh
python python/sdk/examples/permission.py \
@@ -54,12 +54,12 @@ python python/sdk/examples/permission.py \
--preset read-only
```
命令会修改 Session 的策略,而打印出的启动设置仍为 `workspace-write`。后续 query 使用 Session 的当前策略。裸 `/permission` 是普通文本,未知 preset 返回命令错误。结果和状态语义见[查询与接收](../README.zh.md#query-and-response)。
命令会修改 Session 的策略,而打印出的启动设置仍为 `workspace-write`。后续输入使用 Session 的当前策略。裸 `/permission` 是普通文本,未知 preset 返回命令错误。结果和状态语义见[运行文本与命令](../README.zh.md#run-text-and-commands)。
<a id="review-a-plan"></a>
## 审查计划
[计划示例](plan.py) 通过 query 发送 `/plan <task>`,并在命令完成后保持 Session 通知订阅和问答处理器运行。输入 `a` 批准、`k` 提供反馈、`d` 关闭;EOF 也会关闭而不批准。批准后模型可以从后续步骤开始实施,因此应选择隔离的工作区。
[计划示例](plan.py) 通过 `run()` 发送 `/plan <task>`,并在命令完成后保持 Session 通知订阅和问答处理器运行。输入 `a` 批准、`k` 提供反馈、`d` 关闭;EOF 也会关闭而不批准。批准后模型可以从后续步骤开始实施,因此应选择隔离的工作区。
```sh
python python/sdk/examples/plan.py \
+17 -21
View File
@@ -7,22 +7,21 @@ import argparse
import os
from pathlib import Path
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult, Session
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult
def receive(session: Session) -> None:
"""Consume notifications and print the query's command or activity result."""
for result in session.receive_response():
if isinstance(result, RunResult):
if result.finish_reason != "completed":
raise RuntimeError(f"Agent turn ended with {result.finish_reason}: {result.final_response}")
print(result.final_response)
elif isinstance(result, CommandExecution):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
if result.result.text is not None:
print(result.result.text)
def display(result: RunResult | CommandExecution) -> None:
"""Print the input's result and stop on a reported failure."""
if isinstance(result, CommandExecution):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
if result.result.text is not None:
print(result.result.text)
else:
if result.finish_reason != "completed":
raise RuntimeError(f"Agent turn ended with {result.finish_reason}: {result.final_response}")
print(result.final_response)
def main() -> None:
@@ -50,15 +49,12 @@ def main() -> None:
max_tokens=args.max_tokens,
) as harness:
session = harness.start_session(args.session_id)
session.query(
display(session.run(
"Read this project README and summarize its purpose and development commands without using tools.\n\n"
+ readme.read_text(encoding="utf-8")
)
receive(session)
session.query("/compact")
receive(session)
session.query("What are the project's purpose and development commands? Use the earlier README without tools.")
receive(session)
))
display(session.run("/compact"))
display(session.run("What are the project's purpose and development commands? Use the earlier README without tools."))
if __name__ == "__main__":
+6 -6
View File
@@ -52,12 +52,12 @@ def main() -> None:
session = harness.start_session()
with harness.client.subscribe_notifications(lambda notice: notice.payload.get("sessionId") == session.id) as notifications:
deadline = time.monotonic() + args.timeout_seconds
session.query(f"/goal {args.objective}")
for message in session.receive_response():
if isinstance(message, CommandExecution):
print(message.result.text, flush=True)
if message.result.kind == "error":
raise RuntimeError(message.result.text)
execution = session.run(f"/goal {args.objective}")
if not isinstance(execution, CommandExecution):
raise RuntimeError("The SDK profile did not execute the requested command")
print(execution.result.text, flush=True)
if execution.result.kind == "error":
raise RuntimeError(execution.result.text)
while True:
event = display(notifications.next(timeout_seconds=max(0, deadline - time.monotonic())))
if event is not None and event["type"] == "goal/change" and event["data"]["operation"] in {
+7 -2
View File
@@ -7,7 +7,7 @@ import argparse
import os
from pathlib import Path
from deepseek_harness import DeepSeekHarness
from deepseek_harness import CommandExecution, DeepSeekHarness
def main() -> None:
@@ -41,7 +41,12 @@ def main() -> None:
profile=args.profile,
) as harness:
result = harness.run(args.prompt, session_id=args.session_id)
print(result.final_response)
if isinstance(result, CommandExecution):
if result.result.kind == "error":
raise RuntimeError(result.result.text)
print(result.result.text or "")
else:
print(result.final_response)
if __name__ == "__main__":
+17 -23
View File
@@ -5,30 +5,27 @@ from __future__ import annotations
import argparse
import os
from contextlib import closing
from pathlib import Path
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult, Session
from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult
def receive(session: Session) -> None:
"""Print a command or activity result and stop on a reported failure."""
with closing(session.receive_response()) as responses:
for result in responses:
if isinstance(result, CommandExecution):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
if result.result.text is not None:
print(result.result.text)
elif isinstance(result, RunResult):
if result.finish_reason != "completed":
raise RuntimeError(f"Agent turn ended with {result.finish_reason}")
print(result.final_response)
def display(result: RunResult | CommandExecution) -> None:
"""Print the input's result and stop on a reported failure."""
if isinstance(result, CommandExecution):
print(f"Command {result.command_id}: {result.result.kind}")
if result.result.kind == "error":
raise RuntimeError(result.result.text)
if result.result.text is not None:
print(result.result.text)
else:
if result.finish_reason != "completed":
raise RuntimeError(f"Agent turn ended with {result.finish_reason}: {result.final_response}")
print(result.final_response)
def main() -> None:
"""Send an explicit permission command, then query the same Session."""
"""Send an explicit permission command, then run another prompt in the same Session."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dsh-home", required=True, type=Path)
parser.add_argument("--workspace", type=Path, default=Path.cwd())
@@ -45,13 +42,10 @@ def main() -> None:
env={"DSH_PERMISSION_MODE": "workspace-write"},
) as harness:
session = harness.start_session()
session.query("Summarize the current file and approval policy in one sentence. Do not use tools.")
receive(session)
session.query(f"/permission {args.preset}")
receive(session)
display(session.run("Summarize the current file and approval policy in one sentence. Do not use tools."))
display(session.run(f"/permission {args.preset}"))
print(f"Launch setting: {harness.config.env['DSH_PERMISSION_MODE']}")
session.query("Summarize the current file and approval policy again. Do not use tools.")
receive(session)
display(session.run("Summarize the current file and approval policy again. Do not use tools."))
if __name__ == "__main__":
+6 -6
View File
@@ -72,12 +72,12 @@ def main() -> None:
) as harness:
session = harness.start_session()
with closing(harness.client.subscribe_session_notifications(session.id)) as notifications:
session.query(f"/plan {args.task}")
for message in session.receive_response():
if isinstance(message, CommandExecution):
print(message.result.text)
if message.result.kind == "error":
raise RuntimeError(message.result.text)
execution = session.run(f"/plan {args.task}")
if not isinstance(execution, CommandExecution):
raise RuntimeError("The SDK profile did not execute the requested command")
print(execution.result.text, flush=True)
if execution.result.kind == "error":
raise RuntimeError(execution.result.text)
harness.client.request(
"session/wait", {"sessionId": session.id}, response_model=WaitResult,
on_notification=lambda notice: display(notice, session.id), notification_subscription=notifications,