Merge remote-tracking branch 'origin/master' into worktree/remove-sdk-project-toolchain
This commit is contained in:
+2
-2
@@ -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 .agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md
|
||||
2026-07-30-followup-enqueue-and-owned-runs.md: e0561f8cfc975dd60e69307b160070c292efff8e
|
||||
2026-07-30-followup-enqueue-and-owned-runs.zh.md: a807b3ebe47399cae95c38bc2232e20e00e839f5
|
||||
2026-07-30-followup-enqueue-and-owned-runs.md: 9978b3a8ab8678fe98e000476505cee9dcaa1bc6
|
||||
2026-07-30-followup-enqueue-and-owned-runs.zh.md: 6501266d32b5ae884a478bfae9e4f1e3d5d85575
|
||||
+3
-3
@@ -16,7 +16,7 @@ Keep `Agent.followup(message): void` as an enqueue-only operation. `Agent.whenId
|
||||
|
||||
The low-level SDK protocol answers `session/prompt` as soon as enqueue succeeds with `{ messageId }`. It streams durable facts through `session.event`, publishes whole-agent transitions through `session.status`, and has no `session.finished`. A low-level client may observe that receipt and later idleness, but receives no prompt result.
|
||||
|
||||
High-level automation APIs return a `RunResult` only when they explicitly own an activity interval. The TypeScript and Python SDK `run()` methods collect from the submitted message's durable inbox receipt through the next whole-agent `idle`; their `finalResponse` is the last committed assistant message in that interval, not a response causally attributed to the submitted prompt. The one-shot CLI owns the analogous idle-to-idle interval. An isolated child-agent run may report a result because its caller owns the complete child lifecycle and any steering belongs to that run.
|
||||
High-level automation APIs return a `RunResult` only when they explicitly own an activity interval. The TypeScript and Python SDK `run()` methods collect from the submitted message's durable inbox receipt through the next whole-agent `idle`; their final response is the last committed assistant message in that interval, not a response causally attributed to the submitted prompt. The Python SDK also reports the last root turn's reason kind as the run-level [`finish_reason`](../bug-fix/2026-08-11-owned-run-finish-reason.md), without attributing it to the submitted prompt. The one-shot CLI owns the analogous idle-to-idle interval. An isolated child-agent run may report a result because its caller owns the complete child lifecycle and any steering belongs to that run.
|
||||
|
||||
ACP must return a protocol `stopReason`. Its bridge serializes one in-flight prompt per ACP session, waits for whole-agent idle, and otherwise reports the generic `end_turn`. Token-limit endings are not attributed to the prompt: they settle as `end_turn`. A model error on the prompt's correlated turn does reject the prompt immediately (the error is attributed by its owning turn), and a turnless slot (admission discarded the prompt) settles as `cancelled` at idle alongside explicit ACP cancellation or disposal.
|
||||
|
||||
@@ -33,10 +33,10 @@ Goal continuation retains `MessageId` only to recognize its durable queued and a
|
||||
## Verification
|
||||
|
||||
- Agent and inbox tests pin enqueue-only follow-up, durable admission or cancellation, and whole-agent idle observation.
|
||||
- SDK protocol, TypeScript SDK, and Python SDK tests pin the `{ messageId }` receipt, `session.status`, the absence of `session.finished`, and receipt-to-idle `RunResult` collection without prompt-level `status` or `reason`.
|
||||
- SDK protocol, TypeScript SDK, and Python SDK tests pin the `{ messageId }` receipt, `session.status`, the absence of `session.finished`, and receipt-to-idle `RunResult` collection without prompt-level `status` or `reason`; Python SDK tests separately pin its run-level `finish_reason` observation.
|
||||
- ACP, one-shot CLI, goal continuation, and subagent tests pin the distinct activity ownership each integration possesses.
|
||||
- Consumer tests pin that no production integration derives a follow-up result by correlating `MessageId` with `turn/end`.
|
||||
|
||||
## Consequences
|
||||
|
||||
An owned activity interval can include steering, injected context, or other work submitted before idleness, so its final response and events are deliberately broader than the initiating message. Prompt-level model error and token-limit classifications disappear from SDK and ACP results; callers that need those facts must inspect the durable event stream without claiming causal attribution. Concurrent automation on one session requires an explicit serialization or ownership policy rather than an implicit per-prompt result.
|
||||
An owned activity interval can include steering, injected context, or other work submitted before idleness, so its final response, finish reason, and events are deliberately broader than the initiating message. Prompt-level model error and token-limit classifications remain absent from SDK and ACP results; callers may inspect run-level or durable event facts without claiming causal attribution. Concurrent automation on one session requires an explicit serialization or ownership policy rather than an implicit per-prompt result.
|
||||
+3
-3
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
底层 SDK 协议在入队成功后立即以 `{ messageId }` 响应 `session/prompt`。它通过 `session.event` 流式传输持久事实,通过 `session.status` 发布整个 agent 的状态转换,且不包含 `session.finished`。底层客户端可以观察该回执和之后的 idle,但不会收到提示词结果。
|
||||
|
||||
只有明确拥有一个活动区间时,高层自动化 API 才返回 `RunResult`。TypeScript 和 Python SDK 的 `run()` 方法从已提交消息的持久 inbox 回执开始收集,直至整个 agent 下一次进入 `idle`;其 `finalResponse` 是该区间内最后一条已提交的 assistant 消息,而不是按因果关系归属于已提交提示词的响应。单次 CLI(命令行界面)拥有相应的 idle 到 idle 区间。隔离的子 agent 运行可以报告结果,因为调用方拥有完整的子级生命周期,任何 steering 都属于该运行。
|
||||
只有明确拥有一个活动区间时,高层自动化 API 才返回 `RunResult`。TypeScript 和 Python SDK 的 `run()` 方法从已提交消息的持久 inbox 回执开始收集,直至整个 agent 下一次进入 `idle`;其最终响应是该区间内最后一条已提交的 assistant 消息,而不是按因果关系归属于已提交提示词的响应。Python SDK 还把根会话最后一个轮次的结束原因 kind 作为运行级 [`finish_reason`](../bug-fix/2026-08-11-owned-run-finish-reason.md) 返回,但不会将其归因于已提交的提示词。单次 CLI(命令行界面)拥有相应的 idle 到 idle 区间。隔离的子 agent 运行可以报告结果,因为调用方拥有完整的子级生命周期,任何 steering 都属于该运行。
|
||||
|
||||
ACP(Agent Client Protocol)必须返回协议规定的 `stopReason`。其桥接层串行处理每个 ACP 会话中唯一一个正在处理的提示词,等待整个 agent 进入 idle,其他情况均报告通用的 `end_turn`。token 上限的轮次结束不归因于提示词:它们以 `end_turn` 结算。与该提示词关联的轮次上的模型错误会立即以该错误拒绝提示词(错误按其所属轮次归因),而 turnless 槽位(准入已丢弃提示词)会在 idle 时以 `cancelled` 结算,与显式 ACP 取消或 dispose(资源释放)并列。
|
||||
|
||||
@@ -33,10 +33,10 @@ Goal 续行只保留 `MessageId`,用于识别持久排队和已准入的 goal
|
||||
## 验证
|
||||
|
||||
- Agent 与 inbox 测试固定 follow-up 仅入队、持久准入或取消以及整个 agent 的 idle 观测。
|
||||
- SDK 协议、TypeScript SDK 和 Python SDK 测试固定 `{ messageId }` 回执、`session.status`、不存在 `session.finished`,以及不含提示词级 `status` 或 `reason` 的回执到 idle `RunResult` 收集。
|
||||
- SDK 协议、TypeScript SDK 和 Python SDK 测试固定 `{ messageId }` 回执、`session.status`、不存在 `session.finished`,以及不含提示词级 `status` 或 `reason` 的回执到 idle `RunResult` 收集;Python SDK 测试另行固定其运行级 `finish_reason` 观测。
|
||||
- ACP、单次 CLI、goal 续行和 subagent 测试固定各集成实际拥有的不同活动边界。
|
||||
- 消费方测试固定生产集成都不会通过关联 `MessageId` 与 `turn/end` 来推导 follow-up 结果。
|
||||
|
||||
## 后果
|
||||
|
||||
自有活动区间可以包含进入 idle 前提交的 steering、注入上下文或其他工作,因此其最终响应和事件有意比初始消息涵盖更广。SDK 和 ACP 结果不再包含提示词级模型错误和 token 上限分类;需要这些事实的调用方必须检查持久事件流,但不能声称这些事实具有因果归属。在同一会话上并发执行自动化操作时,必须采用显式串行或所有权策略,不能依赖隐式的按提示词结果。
|
||||
自有活动区间可以包含进入 idle 前提交的 steering、注入上下文或其他工作,因此其最终响应、结束原因和事件有意比初始消息涵盖更广。SDK 和 ACP 结果仍不包含提示词级模型错误和 token 上限分类;调用方可以检查运行级或持久事件事实,但不能声称这些事实具有因果归属。在同一会话上并发执行自动化操作时,必须采用显式串行或所有权策略,不能依赖隐式的按提示词结果。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 .agents/notes/implemented/bug-fix/2026-08-11-owned-run-finish-reason.md
|
||||
2026-08-11-owned-run-finish-reason.md: 87a158c7fedc8c6447306c489ba9f969d95dc412
|
||||
2026-08-11-owned-run-finish-reason.zh.md: d75b7d4f0c932ec2d3665b8911835e3b081af129
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Owned-run finish reason reporting
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-owned-run-finish-reason.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Python SDK consumers need a concise classification of how an owned activity interval reached idle. Requiring each consumer to scan raw `turn/end` events duplicates protocol knowledge, while a generic success status loses token-limit and model-error distinctions.
|
||||
|
||||
## Decision
|
||||
|
||||
`RunResult.finish_reason` is the string `kind` from the last root-session `turn/end` collected between the submitted message's durable inbox receipt and the next whole-agent idle. It is `None` when the interval contains no `turn/end`. A `turn/end` without a string `data.reason.kind` raises `SdkProtocolError` instead of being reported as an interval without a turn ending. The field describes the owned run interval; it does not assign that ending to the submitted prompt. The [owned-run boundary decision](../architecture/2026-07-30-followup-enqueue-and-owned-runs.md) continues to prohibit prompt-level result attribution.
|
||||
|
||||
The field exposes only the kind because callers need a stable classification and the complete structured reason remains available in `RunResult.events`. Transport loss, timeout, and protocol failures still raise instead of producing a finish reason.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Restore `status`.** A deployment-mapped `ok` or `error` status conflates distinct durable endings and resembles transport success, so it does not answer why the interval finished.
|
||||
|
||||
**Expose a model `FinishReason`.** A run may contain multiple model steps, and intermediate `tool-calls` endings do not finish the run. The agent's last `turn/end` is the relevant run-level observation.
|
||||
|
||||
**Call the field `stop_reason`.** ACP and subagent seams map turn-ending reasons into their own `stopReason` value sets. The Python field preserves the raw agent reason kind, so sharing their name would imply a mapping this interface does not perform.
|
||||
|
||||
**Expose the complete structured turn reason.** The raw event stream already preserves error and cancellation details. Duplicating that object on `RunResult` would create two representations that Python callers must reconcile.
|
||||
|
||||
## Verification
|
||||
|
||||
Python SDK tests cover selection of the last turn ending, an interval without a turn ending, and rejection of a malformed turn-ending reason. The SDK README documents the field's values, `None` case, failure behavior, and run-level scope.
|
||||
|
||||
## Consequences
|
||||
|
||||
Callers can branch on `completed`, `max-tokens`, `error`, and future reason kinds without parsing the event list. The field may describe steering, injected context, or queued work that joined the interval, so it must not be presented as the initiating prompt's causal outcome. The in-repo TypeScript SDK exposes the finish-reason observation only through its typed events; its callers can read it directly from `SessionEvent[]`.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 自有运行的结束原因报告
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-owned-run-finish-reason.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Python SDK 消费方需要简洁地判断自有活动区间如何进入 idle。要求每个消费方扫描原始 `turn/end` 事件会重复协议知识,而通用的成功状态会丢失 token 上限与模型错误之间的区别。
|
||||
|
||||
## 决策
|
||||
|
||||
`RunResult.finish_reason` 是从已提交消息进入持久 inbox 的回执开始、到整个 agent 下一次进入 idle 为止所收集的根会话最后一个 `turn/end` 的字符串 `kind`。如果该区间没有 `turn/end`,字段为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 会抛出 `SdkProtocolError`,而不会报告为区间内没有轮次结束。该字段描述自有运行区间;它不会把这个结束原因归属于已提交的提示词。[自有运行边界决策](../architecture/2026-07-30-followup-enqueue-and-owned-runs.md)仍禁止提示词级结果归因。
|
||||
|
||||
该字段只公开 kind,因为调用方需要稳定的分类,完整的结构化原因仍可从 `RunResult.events` 取得。传输丢失、超时和协议故障仍会抛出异常,而不会生成结束原因。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**恢复 `status`。** 由部署映射的 `ok` 或 `error` 状态会混淆不同的持久结束情况,而且看起来像传输成功状态,因此无法回答区间为何结束。
|
||||
|
||||
**公开模型 `FinishReason`。** 一次运行可能包含多个模型步骤,中间的 `tool-calls` 结束并不代表运行结束。agent 最后一个 `turn/end` 才是相关的运行级观测。
|
||||
|
||||
**将字段命名为 `stop_reason`。** ACP 和 subagent seam 会把轮次结束原因映射到各自的 `stopReason` 取值集合。Python 字段保留原始的 agent 原因 kind,因此沿用它们的名称会让人误以为该接口也执行了这种映射。
|
||||
|
||||
**公开完整的结构化轮次原因。** 原始事件流已经保留错误与取消的详细信息。在 `RunResult` 上复制这个对象会产生两种需要 Python 调用方协调的表示。
|
||||
|
||||
## 验证
|
||||
|
||||
Python SDK 测试覆盖选择最后一个轮次结束、区间内没有轮次结束,以及拒绝畸形轮次结束原因。SDK README 记录字段取值、`None` 情况、失败行为和运行级范围。
|
||||
|
||||
## 后果
|
||||
|
||||
调用方无需解析事件列表,即可按 `completed`、`max-tokens`、`error` 和未来的原因 kind 分支。该字段可能描述区间内加入的 steering、注入上下文或排队工作,因此不能将其表述为初始提示词的因果结果。仓库内的 TypeScript SDK 只通过类型化事件提供结束原因观测;其调用方可以直接从 `SessionEvent[]` 读取该观测。
|
||||
@@ -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