fix(sdk): harden command examples and run wheel regressions
This commit is contained in:
1 parent
e122cfb627
commit
5771a93f2e
24 files changed
+687
-82
No files matched your search
@@ -7,7 +7,7 @@
|
||||
zh: bdd91202075498a3
|
||||
/python-sdk-example/run-the-minimal-agent:
|
||||
en: ac09bb6bc3ab76a0
|
||||
zh: 6258540acaf4bc37
|
||||
zh: e69d1e2bd796f3bf
|
||||
/python-sdk-example/compact-a-full-sdk-session:
|
||||
en: 7749773f97211ac1
|
||||
zh: 594348ae66a2ef6c
|
||||
@@ -15,11 +15,11 @@
|
||||
en: 1fe5e06c6842a53b
|
||||
zh: ff0836d8cffaeef7
|
||||
/python-sdk-example/review-a-plan:
|
||||
en: 2596f68bb55082c8
|
||||
zh: 1e40ca75e1a0f9b9
|
||||
en: e303821f2432111b
|
||||
zh: f418c686498743d0
|
||||
/python-sdk-example/run-a-goal:
|
||||
en: ae73e9fd9d402047
|
||||
zh: f9694bc6a6e12929
|
||||
en: f4748b8e074f3497
|
||||
zh: 271aa49d20d83a73
|
||||
/python-sdk-example/add-plugins:
|
||||
en: 5e1e30dac13f6c9e
|
||||
zh: 0af98ebb75c88f43
|
||||
@@ -59,7 +59,7 @@ The command changes the Session's policy while the printed launch setting remain
|
||||
<a id="review-a-plan"></a>
|
||||
## Review a plan
|
||||
|
||||
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.
|
||||
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; cancellation or EOF before an answer dismisses the review. Approval allows implementation from a later model step, so choose an isolated workspace.
|
||||
|
||||
```sh
|
||||
python python/sdk/examples/plan.py \
|
||||
@@ -68,7 +68,7 @@ python python/sdk/examples/plan.py \
|
||||
"Plan the requested change and present it for review."
|
||||
```
|
||||
|
||||
The native plan is carried in `exit_plan_mode`, not written to a workspace document. Permissions remain workspace-write. Dismissal leaves plan mode active and ends the current turn. The example waits through `session/wait` and rejects truncated or failed root turns; its question callback is independent of the protocol reader.
|
||||
The native plan is carried in `exit_plan_mode`, not written to a workspace document. Permissions remain workspace-write. Dismissal leaves plan mode active and ends the current turn. The example waits through `session/wait` and rejects truncated or failed root turns; its question callback is independent of the protocol reader. Ctrl+C stops the example even while it is waiting for input.
|
||||
|
||||
<a id="run-a-goal"></a>
|
||||
## Run a goal
|
||||
@@ -82,7 +82,7 @@ python python/sdk/examples/goal.py \
|
||||
"Calculate 17 * 19, verify the result, and complete the goal without changing files."
|
||||
```
|
||||
|
||||
`--timeout-seconds` bounds observation (300 seconds by default). A timeout reports failure and closes the runtime; it does not mark the goal complete. The persisted goal can be inspected after restoration and explicitly resumed. `session/wait` alone is insufficient because automatic rounds have idle gaps.
|
||||
`--timeout-seconds` requires a finite positive value and bounds the initial command wait and subsequent observation (300 seconds by default). A timeout reports failure and closes the runtime; it does not mark the goal complete. The persisted goal can be inspected after restoration and explicitly resumed. `session/wait` alone is insufficient because automatic rounds have idle gaps.
|
||||
|
||||
## Add plugins
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ python python/sdk/examples/minimal.py \
|
||||
- 配合支持图片输入的模型读取 PNG/JPEG/WebP/GIF 文件的 `read_image`
|
||||
- 查询或修改 Session 目录的 `working_directory`
|
||||
|
||||
该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、文本文件工具、本地指令发现、设置、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。[自动上下文压缩](../../../packages/bundle/sdk-minimal/README.zh.md#model-experience)通过额外模型请求生成较早历史的摘要,同时保留完整日志。SDK 提示词文本不会执行斜杠命令。
|
||||
该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、文本文件工具、本地指令发现、设置、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。[自动上下文压缩](../../../packages/bundle/sdk-minimal/README.zh.md#model-experience)通过额外模型请求生成较早历史的摘要,同时保留完整日志。这个最小组合将不支持的斜杠输入保留在普通提示词路径上。
|
||||
|
||||
持久 PTY 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。
|
||||
|
||||
@@ -59,7 +59,7 @@ python python/sdk/examples/permission.py \
|
||||
<a id="review-a-plan"></a>
|
||||
## 审查计划
|
||||
|
||||
[计划示例](plan.py) 通过 `run()` 发送 `/plan <task>`,并在命令完成后保持 Session 通知订阅和问答处理器运行。输入 `a` 批准、`k` 提供反馈、`d` 关闭;EOF 也会关闭而不批准。批准后模型可以从后续步骤开始实施,因此应选择隔离的工作区。
|
||||
[计划示例](plan.py) 通过 `run()` 发送 `/plan <task>`,并在命令完成后保持 Session 通知订阅和问答处理器运行。输入 `a` 批准、`k` 提供反馈、`d` 关闭;取消或作答前遇到 EOF 会关闭审查。批准后模型可以从后续步骤开始实施,因此应选择隔离的工作区。
|
||||
|
||||
```sh
|
||||
python python/sdk/examples/plan.py \
|
||||
@@ -68,7 +68,7 @@ python python/sdk/examples/plan.py \
|
||||
"Plan the requested change and present it for review."
|
||||
```
|
||||
|
||||
原生计划通过 `exit_plan_mode` 携带,而不是写入工作区文档。权限仍为 workspace-write。关闭审查会保留计划模式并结束当前轮次。示例通过 `session/wait` 等待,并报告根轮次截断或失败;问答回调独立于协议读取线程。
|
||||
原生计划通过 `exit_plan_mode` 携带,而不是写入工作区文档。权限仍为 workspace-write。关闭审查会保留计划模式并结束当前轮次。示例通过 `session/wait` 等待,并报告根轮次截断或失败;问答回调独立于协议读取线程。等待输入时也可以通过 Ctrl+C 结束示例。
|
||||
|
||||
<a id="run-a-goal"></a>
|
||||
## 运行目标
|
||||
@@ -82,7 +82,7 @@ python python/sdk/examples/goal.py \
|
||||
"Calculate 17 * 19, verify the result, and complete the goal without changing files."
|
||||
```
|
||||
|
||||
`--timeout-seconds` 限制观察时间,默认为 300 秒。超时会报告失败并关闭运行时,不会将目标标记为完成。恢复会话后可查看持久化目标,再显式恢复执行。自动轮次之间存在 idle 间隙,因此仅调用 `session/wait` 不足以等待目标结束。
|
||||
`--timeout-seconds` 必须是有限正数,用于限制首次命令等待及之后的观察时间,默认为 300 秒。超时会报告失败并关闭运行时,不会将目标标记为完成。恢复会话后可查看持久化目标,再显式恢复执行。自动轮次之间存在 idle 间隙,因此仅调用 `session/wait` 不足以等待目标结束。
|
||||
|
||||
## 添加插件
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -43,11 +44,12 @@ def main() -> None:
|
||||
parser.add_argument("--timeout-seconds", type=float, default=300)
|
||||
parser.add_argument("objective", help="Objective for the native /goal command")
|
||||
args = parser.parse_args()
|
||||
if not args.objective.strip() or args.timeout_seconds <= 0:
|
||||
parser.error("objective must be non-blank and timeout-seconds must be positive")
|
||||
if not args.objective.strip() or not math.isfinite(args.timeout_seconds) or args.timeout_seconds <= 0:
|
||||
parser.error("objective must be non-blank and timeout-seconds must be finite and positive")
|
||||
with DeepSeekHarness(
|
||||
dsh_home=str(args.dsh_home.resolve()), cwd=str(args.workspace.resolve()),
|
||||
profile="sdk", model=args.model, env={"DSH_PERMISSION_MODE": "workspace-write"},
|
||||
request_timeout_seconds=args.timeout_seconds,
|
||||
) as harness:
|
||||
session = harness.start_session()
|
||||
with harness.client.subscribe_notifications(lambda notice: notice.payload.get("sessionId") == session.id) as notifications:
|
||||
|
||||
+117
-19
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
@@ -19,25 +21,121 @@ class WaitResult(BaseModel):
|
||||
"""The empty session/wait result."""
|
||||
|
||||
|
||||
def review(request: UserQuestionRequest, cancelled: threading.Event) -> dict:
|
||||
"""Let the user approve, give feedback, or dismiss the native plan review."""
|
||||
if cancelled.is_set():
|
||||
return {"cancelled": True}
|
||||
if len(request.questions) != 1 or request.questions[0].get("intent", {}).get("kind") != "plan-review":
|
||||
raise RuntimeError("This example handles plan reviews only")
|
||||
question = request.questions[0]
|
||||
print(question["detail"])
|
||||
class _PlanReviews:
|
||||
"""Own cancellable stdin readers and join review callbacks before interpreter exit."""
|
||||
|
||||
# A buffered reader can consume later answers from a pipe before it exits.
|
||||
_READ_LINE = """import os, sys
|
||||
if sys.stdin.isatty():
|
||||
try:
|
||||
while True:
|
||||
choice = input("[a] Approve, [k] Keep planning, [d] Dismiss: ").strip().lower()
|
||||
if choice == "d":
|
||||
return {"cancelled": True}
|
||||
if choice == "a":
|
||||
return {"answers": [{"id": question["id"], "selected": ["Approve"]}]}
|
||||
if choice == "k":
|
||||
return {"answers": [{"id": question["id"], "selected": ["Keep planning"], "custom": input("Feedback: ")}]}
|
||||
text = input() + '\\n'
|
||||
except EOFError:
|
||||
return {"cancelled": True}
|
||||
text = ''
|
||||
else:
|
||||
line = bytearray()
|
||||
while True:
|
||||
value = os.read(0, 1)
|
||||
if not value:
|
||||
break
|
||||
line.extend(value)
|
||||
if value == b'\\n':
|
||||
break
|
||||
text = line.decode(sys.stdin.encoding or 'utf-8')
|
||||
sys.stdout.buffer.write(text.encode('utf-8'))
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._closed = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
self._question_lock = threading.Lock()
|
||||
self._readers: list[subprocess.Popen[bytes]] = []
|
||||
self._callbacks: set[threading.Thread] = set()
|
||||
|
||||
def __enter__(self) -> _PlanReviews:
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
||||
with self._lock:
|
||||
self._closed.set()
|
||||
readers = tuple(self._readers)
|
||||
callbacks = tuple(self._callbacks)
|
||||
for reader in readers:
|
||||
self._terminate(reader)
|
||||
for reader in readers:
|
||||
reader.wait()
|
||||
for callback in callbacks:
|
||||
if callback is not threading.current_thread():
|
||||
callback.join()
|
||||
|
||||
@staticmethod
|
||||
def _terminate(reader: subprocess.Popen[bytes]) -> None:
|
||||
if reader.poll() is None:
|
||||
try:
|
||||
reader.kill()
|
||||
except ProcessLookupError as error:
|
||||
# The reader exited between poll and kill.
|
||||
pass
|
||||
|
||||
def _read_line(self, prompt: str, cancelled: threading.Event) -> str | None:
|
||||
with self._lock:
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
return None
|
||||
reader = subprocess.Popen(
|
||||
[sys.executable, "-c", self._READ_LINE], stdin=sys.stdin,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
self._readers.append(reader)
|
||||
try:
|
||||
print(prompt, end="", flush=True)
|
||||
while True:
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
self._terminate(reader)
|
||||
try:
|
||||
output, errors = reader.communicate(timeout=0.05)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
return None
|
||||
if reader.returncode != 0:
|
||||
raise RuntimeError(f"Plan review input reader failed: {errors.decode(errors='replace').strip()}")
|
||||
return output.decode("utf-8").removesuffix("\n").removesuffix("\r") if output else None
|
||||
finally:
|
||||
self._terminate(reader)
|
||||
reader.communicate()
|
||||
with self._lock:
|
||||
self._readers.remove(reader)
|
||||
|
||||
def answer(self, request: UserQuestionRequest, cancelled: threading.Event) -> dict:
|
||||
"""Serialize plan reviews; cancellation or empty stdin dismisses the current question."""
|
||||
with self._lock:
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
return {"cancelled": True}
|
||||
self._callbacks = {callback for callback in self._callbacks if callback.is_alive()}
|
||||
self._callbacks.add(threading.current_thread())
|
||||
while not self._question_lock.acquire(timeout=0.05):
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
return {"cancelled": True}
|
||||
try:
|
||||
if self._closed.is_set() or cancelled.is_set():
|
||||
return {"cancelled": True}
|
||||
if len(request.questions) != 1 or request.questions[0].get("intent", {}).get("kind") != "plan-review":
|
||||
raise RuntimeError("This example handles plan reviews only")
|
||||
question = request.questions[0]
|
||||
print(question["detail"], flush=True)
|
||||
while True:
|
||||
choice = self._read_line("[a] Approve, [k] Keep planning, [d] Dismiss: ", cancelled)
|
||||
if choice is None or choice.strip().lower() == "d":
|
||||
return {"cancelled": True}
|
||||
if choice.strip().lower() == "a":
|
||||
return {"answers": [{"id": question["id"], "selected": ["Approve"]}]}
|
||||
if choice.strip().lower() == "k":
|
||||
feedback = self._read_line("Feedback: ", cancelled)
|
||||
if feedback is None:
|
||||
return {"cancelled": True}
|
||||
return {"answers": [{"id": question["id"], "selected": ["Keep planning"], "custom": feedback}]}
|
||||
finally:
|
||||
self._question_lock.release()
|
||||
|
||||
|
||||
def display(notification: Notification, session_id: str) -> None:
|
||||
@@ -65,9 +163,9 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
if not args.task.strip():
|
||||
parser.error("task must be non-blank")
|
||||
with DeepSeekHarness(
|
||||
with _PlanReviews() as reviews, DeepSeekHarness(
|
||||
dsh_home=str(args.dsh_home.resolve()), cwd=str(args.workspace.resolve()),
|
||||
profile="sdk", model=args.model, on_user_question=review,
|
||||
profile="sdk", model=args.model, on_user_question=reviews.answer,
|
||||
env={"DSH_PERMISSION_MODE": "workspace-write"},
|
||||
) as harness:
|
||||
session = harness.start_session()
|
||||
|
||||
Reference in new issue
Block a user