fix(python): report run finish reason

This commit is contained in:
_Kerman
2026-08-11 16:25:59 +08:00
parent 5d591e55c1
commit 6896fd1545
11 changed files with 108 additions and 13 deletions

View File

@@ -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: 366480f9a05cfeb0071146f9d5bcdcbb6f55f14c
README.zh.md: 3c5d96e27c47a85350cb6afdab4b941381d0faf1

View File

@@ -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. Both 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.

View File

@@ -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`。两个字段描述的都是自有活动区间,而不是因果上归属于该提示词的输出或结束原因。steering中途引导、注入的上下文和其他排队工作都可能在进入空闲状态前参与其中。
`HarnessClient` 会在运行时进程的生命周期内保留已发现的 subagent子 agent祖先关系。每次执行 `Session.run()` 时,`RunResult.notifications``on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。

View File

@@ -38,6 +38,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 +175,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 +219,15 @@ 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 root turn's reason kind in an owned run interval."""
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
return kind if isinstance(kind, str) else None
return None

View File

@@ -58,6 +58,17 @@ 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.status",
@@ -86,7 +97,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 == "completed"
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 +149,7 @@ for line in sys.stdin:
)
assert seen == ["session.event", "session.status", "subagent.started", "session.status"]
assert result.finish_reason is None
def test_relative_cwd_is_absolute_in_process_environment_and_wire(