Skip to content

document: 增加 dsh 设计文档 - #373

Open
weimch wants to merge 1 commit into
mainfrom
feature/dsh-agent
Open

weimch wants to merge 1 commit into
mainfrom
feature/dsh-agent

Conversation

@weimch

@weimch weimch commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

本次变更仅新增 docs/design/03-dsh-intergate-design.md(2690 行,0 增删其余文件),是 DSHAgent(dsh 生态扩展 Agent)的设计文档,无代码实现。评审范围:对照同一仓库中实际已存在的 tRPC-Agent SDK 源码(trpc_agent_sdk/)逐条验证文档中的框架侧 API 声称(Runner.run_async、InvocationContext.get_cancel_event/raise_if_cancelled、ctx.actions.state_delta 的持久化路径、InMemorySessionService、OpenAIModel、取消语义等),并检查文档内部跨语言协议的连贯性。

计划符合性:文档结构与同目录既有设计文档(01/02)风格一致,背景、架构、DSHAgent/DSHProcess、插件、调研章节齐全,绝大多数框架 API 声称与真实源码吻合(run_async 签名、custom_data、cancel_run_async、RunCancelledException、update_session_state 等方法均存在),调研部分无法在仓库内验证但不矛盾。

主要风险:发现两处高置信设计缺陷——① 文档核心的会话 JSONL 持久化机制(_run_async_impl 的 finally 中向 ctx.actions.state_delta 写入 DSH_SESSION_STATE_KEY)与框架实际持久化路径不兼容,该写入在任何路径下都不会被 Runner/BaseAgent 持久化到 SessionService.state,按此实现将导致每轮会话状态(JSONL)全部丢失;② Python 侧 DSHModelSpec 使用 snake_case(reasoning_effort/max_tokens),而 JS 侧线协议 TrpcPromptRequest.model 使用 camelCase(reasoningEffort/maxTokens),文档未说明任何序列化映射,按字面实现会导致每轮模型参数(推理力度/最大 token)被静默丢弃或拒绝。

测试影响:本变更无测试(纯文档),上述设计问题无法由测试捕获;文档中提供的 Python 示例代码(DSHAgent、Runner、InMemorySessionService)经比对与现有 SDK 签名一致,可直接使用。

门禁结论:FAILED。两处问题均锚定在变更引入的设计契约上,属于高置信、可操作的设计缺陷,建议合入前修正文档中的持久化写回方式(改为在 yield 的最终事件上携带 state_delta 或在 finally 中显式调用 update_session_state)以及补齐跨语言字段命名映射。

发现的问题

严重

docs/design/03-dsh-intergate-design.md:415-417

问题: _run_async_impl 的 finally 块(404-419 行)把本轮 JSONL 只写入 ctx.actions.state_delta[DSH_SESSION_STATE_KEY],但框架中 ctx.actions.state_delta(即 ctx.event_actions.state_delta)只在事件被 session_service.append_event() 追加时才会随事件持久化。已核对仓库源码:runner.py 仅对 async for event in current_agent.run_async(...) 产出的非 partial Event 调用 append_event(runners.py:539),base_session_service.py:94-106 只在该事件被追加时把 event.actions.state_delta 并入 session.state;而 _run_async_impl 生成器结束后没有任何代码再读取 ctx.actions.state_delta(BaseAgent.run_async 的 finally 只做 trace;Runner 仅在 RunCancelledException 分支调 update_session,且不携带 ctx.actions)。DSH_SESSION_STATE_KEY 的写入发生在最后一个事件已 yield 之后,因此既不随事件落库,也无单独持久化调用。

触发条件: 任何一次 runner.run_async() 调用 DSHAgent 时都会发生:成功路径上最后一轮 session.status idle 不被 to_trpc_event 转成 Event(不产生任何后续追加),取消路径上 raise_if_cancelled() 抛 RunCancelledException 后由 Runner 兜底清理,同样不读取 ctx.actions.state_delta。

实际影响: 按文档实现后,每轮生成的 { jsonl, version }(即整个 dsh 会话历史快照)永远不会写入 SessionService.state,下一轮 _state_store.load(ctx.session) 读到的始终是空状态;DSHAgent 的跨轮会话记忆、取消时的 partial 输出保留以及 session/trpc-prompt 回传 JSONL 的能力全部失效,这是该设计第 2/3 章的核心机制。

修正方向: 将写回动作改为可被框架持久化的形式:在 _run_async_impl yield 的最后一个非 partial Event(或新增一个收尾 Event)的 event.actions.state_delta[DSH_SESSION_STATE_KEY] 上附带该值;或在生成器结束前显式调用 await ctx.session_service.update_session_state(ctx.session, {DSH_SESSION_STATE_KEY: ...})(该接口已存在于 abc/_session_service.py:128,默认实现桥接 update_session)。文档应写明选定的路径并删除对 ctx.actions.state_delta 直接写入的依赖。

中等

docs/design/03-dsh-intergate-design.md:563-568

问题: 文档定义的两个对端协议结构字段命名不一致且未声明任何映射:Python 侧 DSHModelSpec(563-568 行)使用 snake_case reasoning_effort / max_tokens,而 JS 侧线协议 TrpcPromptRequest.model(756 行)声明的是 camelCase reasoningEffort? / maxTokens?。同一文档既是 Python 实现契约又是 JS trpc-jsonrpc-server 契约,JSON-RPC 参数按序列化结果原样传递,文档未说明 DSHModelSpec → JSON 的字段转换规则。

触发条件: DSHAgent 把 model=OpenAIModel(...) 转成 DSHModelSpec 后随每轮 session/trpc-prompt 发送时(243、653 行描述的行为);若 Python 侧按 dataclass 字段名直接序列化,reasoning_effort/max_tokens 将以 snake_case 到达 JS 侧,而 TrpcPromptRequest/解析侧按 camelCase 读取。

实际影响: 每轮的 reasoningEffort/maxTokens 模型参数会被 JS 侧静默忽略(读不到对应 key),或按未知字段剔除/报错;而 provider/model 因两处同名单字段不受影响,问题不易被察觉,最终表现为 dsh 侧推理力度与最大 token 上限失效。

修正方向: 在文档中明确定义 DSHModelSpec 的序列化规则:要么在 Python 侧序列化时显式转换字段名为 reasoningEffort/maxTokens(与 JS 接口一致),要么统一两侧命名并在 TrpcPromptRequest 注释中声明 alias。建议以 JS 线协议(camelCase)为基准,并在 DSHModelSpec 后补充一行说明。

Comment on lines +415 to +417
finally:
# 正常结束和被取消都写回;取消时包含 partial 输出
ctx.actions.state_delta[DSH_SESSION_STATE_KEY] = self._state_store.state_delta(state)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _run_async_impl 的 finally 块(404-419 行)把本轮 JSONL 只写入 ctx.actions.state_delta[DSH_SESSION_STATE_KEY],但框架中 ctx.actions.state_delta(即 ctx.event_actions.state_delta)只在事件被 session_service.append_event() 追加时才会随事件持久化。已核对仓库源码:runner.py 仅对 async for event in current_agent.run_async(...) 产出的非 partial Event 调用 append_event(runners.py:539),base_session_service.py:94-106 只在该事件被追加时把 event.actions.state_delta 并入 session.state;而 _run_async_impl 生成器结束后没有任何代码再读取 ctx.actions.state_delta(BaseAgent.run_async 的 finally 只做 trace;Runner 仅在 RunCancelledException 分支调 update_session,且不携带 ctx.actions)。DSH_SESSION_STATE_KEY 的写入发生在最后一个事件已 yield 之后,因此既不随事件落库,也无单独持久化调用。

触发条件: 任何一次 runner.run_async() 调用 DSHAgent 时都会发生:成功路径上最后一轮 session.status idle 不被 to_trpc_event 转成 Event(不产生任何后续追加),取消路径上 raise_if_cancelled() 抛 RunCancelledException 后由 Runner 兜底清理,同样不读取 ctx.actions.state_delta。

实际影响: 按文档实现后,每轮生成的 { jsonl, version }(即整个 dsh 会话历史快照)永远不会写入 SessionService.state,下一轮 _state_store.load(ctx.session) 读到的始终是空状态;DSHAgent 的跨轮会话记忆、取消时的 partial 输出保留以及 session/trpc-prompt 回传 JSONL 的能力全部失效,这是该设计第 2/3 章的核心机制。

修正方向: 将写回动作改为可被框架持久化的形式:在 _run_async_impl yield 的最后一个非 partial Event(或新增一个收尾 Event)的 event.actions.state_delta[DSH_SESSION_STATE_KEY] 上附带该值;或在生成器结束前显式调用 await ctx.session_service.update_session_state(ctx.session, {DSH_SESSION_STATE_KEY: ...})(该接口已存在于 abc/_session_service.py:128,默认实现桥接 update_session)。文档应写明选定的路径并删除对 ctx.actions.state_delta 直接写入的依赖。

Comment on lines +563 to +568
@dataclass(frozen=True)
class DSHModelSpec:
provider: str
model: str
reasoning_effort: str | None = None
max_tokens: int | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 文档定义的两个对端协议结构字段命名不一致且未声明任何映射:Python 侧 DSHModelSpec(563-568 行)使用 snake_case reasoning_effort / max_tokens,而 JS 侧线协议 TrpcPromptRequest.model(756 行)声明的是 camelCase reasoningEffort? / maxTokens?。同一文档既是 Python 实现契约又是 JS trpc-jsonrpc-server 契约,JSON-RPC 参数按序列化结果原样传递,文档未说明 DSHModelSpec → JSON 的字段转换规则。

触发条件: DSHAgent 把 model=OpenAIModel(...) 转成 DSHModelSpec 后随每轮 session/trpc-prompt 发送时(243、653 行描述的行为);若 Python 侧按 dataclass 字段名直接序列化,reasoning_effort/max_tokens 将以 snake_case 到达 JS 侧,而 TrpcPromptRequest/解析侧按 camelCase 读取。

实际影响: 每轮的 reasoningEffort/maxTokens 模型参数会被 JS 侧静默忽略(读不到对应 key),或按未知字段剔除/报错;而 provider/model 因两处同名单字段不受影响,问题不易被察觉,最终表现为 dsh 侧推理力度与最大 token 上限失效。

修正方向: 在文档中明确定义 DSHModelSpec 的序列化规则:要么在 Python 侧序列化时显式转换字段名为 reasoningEffort/maxTokens(与 JS 接口一致),要么统一两侧命名并在 TrpcPromptRequest 注释中声明 alias。建议以 JS 线协议(camelCase)为基准,并在 DSHModelSpec 后补充一行说明。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants