DSH / Atlas
2026-08-11implementedbug-fix

Owned-run finish reason reporting

自有运行的结束原因报告

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.

English

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 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[].

中文

问题

Python SDK 消费方需要简洁地判断自有活动区间如何进入 idle。要求每个消费方扫描原始 turn/end 事件会重复协议知识,而通用的成功状态会丢失 token 上限与模型错误之间的区别。

决策

RunResult.finish_reason 是从已提交消息进入持久 inbox 的回执开始、到整个 agent 下一次进入 idle 为止所收集的根会话最后一个 turn/end 的字符串 kind。如果该区间没有 turn/end,字段为 None。缺少字符串 data.reason.kindturn/end 会抛出 SdkProtocolError,而不会报告为区间内没有轮次结束。该字段描述自有运行区间;它不会把这个结束原因归属于已提交的提示词。自有运行边界决策仍禁止提示词级结果归因。

该字段只公开 kind,因为调用方需要稳定的分类,完整的结构化原因仍可从 RunResult.events 取得。传输丢失、超时和协议故障仍会抛出异常,而不会生成结束原因。

考虑过的替代方案

恢复 status 由部署映射的 okerror 状态会混淆不同的持久结束情况,而且看起来像传输成功状态,因此无法回答区间为何结束。

公开模型 FinishReason 一次运行可能包含多个模型步骤,中间的 tool-calls 结束并不代表运行结束。agent 最后一个 turn/end 才是相关的运行级观测。

将字段命名为 stop_reason ACP 和 subagent seam 会把轮次结束原因映射到各自的 stopReason 取值集合。Python 字段保留原始的 agent 原因 kind,因此沿用它们的名称会让人误以为该接口也执行了这种映射。

公开完整的结构化轮次原因。 原始事件流已经保留错误与取消的详细信息。在 RunResult 上复制这个对象会产生两种需要 Python 调用方协调的表示。

验证

Python SDK 测试覆盖选择最后一个轮次结束、区间内没有轮次结束,以及拒绝畸形轮次结束原因。SDK README 记录字段取值、None 情况、失败行为和运行级范围。

后果

调用方无需解析事件列表,即可按 completedmax-tokenserror 和未来的原因 kind 分支。该字段可能描述区间内加入的 steering、注入上下文或排队工作,因此不能将其表述为初始提示词的因果结果。仓库内的 TypeScript SDK 只通过类型化事件提供结束原因观测;其调用方可以直接从 SessionEvent[] 读取该观测。