Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md
Chinesezjc cc6e4d59fc docs(tools): scope the Python SDK validity standard to the grammar
The list-nesting cap guards against a tokenizer SyntaxError, which makes the
text not Python. A long `A | B | …` union is valid at any length and only
defeats CPython's compile-time C recursion (measured: 1,000 branches compile,
5,000 raise RecursionError); nothing compiles this block, and capping would
retire the deep-chain tests pinning the walk's linear time. Records that
boundary at the `oneOf` arm and in the Agent Note (both languages).

Also documents that the context-free degrade marker reads the call's
className rather than the frame's — frames propagate a derived name, so a
per-frame read would declare classes the caller cannot receive — and pins
that path with oneOf-of-objects and array-of-oneOf assertions.
2026-08-05 15:59:15 +08:00

8.5 KiB
Raw Blame History

Agent Note: Code Mode 语言分发与 Python SDK 渲染器

Status: implemented

English | 中文

问题

Code Mode 只生成一种 SDK 形态:TypeScript。ToolRegistry 为 tools:sdk 段硬编码了 renderToolsSdk,且 requireCodeRuntime 会拒绝任何 ctx.codeRuntime.language !== 'typescript'。引入 CPython 后端后,程序的源语言不再固定:同一个可见工具注册表在加载 Python 运行时时必须投射出 Python SDK,而面向模型的 run_code schema 字符串("Execute a Python program …")也必须与 SDK 段的语言一致,模型才不会在 Python 运行时下看到 TypeScript 指令。

这是多语言 Code Mode 拆分中面向工具的那一半;代码运行时 seam 已经携带 CodeRuntime.language。本 Note 只负责 dsh-tools 如何在该字段上分发。实现 language: 'python' 的后端由它自己的 Note 负责,单独交付。

决策

语言选择就是对 ctx.codeRuntime.language 的查表,在 prompt 装配时惰性解析,查 dsh-tools 里两张平行的表:

  • SDK_RENDERERS(index.ts)把语言映射到它的 tools:sdk 渲染器——typescript → renderToolsSdk、python → renderToolsSdkPy。tools:sdk 段读取所加载运行时的语言并选出渲染器;requireCodeRuntime 拒绝其语言不在表中的 mode: code/both 运行时,并列出已知语言。
  • RUN_CODE_FLAVORS(code-mode.ts)把语言映射到它那两条面向模型的 run_code 字符串(工具 description 与 code 参数描述),使一种语言的 SDK 段与它的传输 schema 始终一致。

两张表在使用前都以 Object.hasOwn 读取,这样名为 toString/constructor 的语言不会把继承自 Object.prototype 的成员解析成渲染器。两个守卫的可达性不同:SDK_RENDERERS 的段内守卫不可达,因为 requireCodeRuntime 已在同一回调更早处校验过同一张 const 表(它带 /* v8 ignore */);而 RUN_CODE_FLAVORS 的守卫是主要的、可公开到达的拒绝路径——在语言有渲染器却无 flavor 表项的运行时下读 ctx.tools.schemas() 即到达,且有测试覆盖。schema 发射通过 peekRuntime() 而非 requireRuntime() 读取运行时:undefined(无运行时,即永不喂给模型的 doc-catalog schema 采集)降级到 TypeScript flavor,而挂载了未知语言则 fail loud——这不是下方被否决的静默回退,那指的是为真实运行时发出错误语言的 SDK。新增一门后端语言就是两条表项加它的渲染器——不动 agent-loop,也不动注册表结构。

code-mode.ts 只依赖运行时 seam(@deepseek-ai/dsh-code-runtime),绝不依赖具体后端;分发在运行时按 runtime.language 进行。因此工具层独立于协议和后端 PR 落地——它只需要 seam 的 language 字段,而该字段已在 master 上。

Python SDK 渲染器

py-types.ts 渲染 jsonSchemaToTs 所覆盖的同一套统一工具 schema 词汇,目标为 Python:jsonSchemaToPy 为每个 JSON-schema 节点发出一个类型表达式,renderToolsSdkPy 为每个可见工具的参数与规范输出装配具名 TypedDict,再加一个带用法说明的 tools 对象,与 TypeScript 形态等价。不支持的原始构造在装配时降级而非抛错,与 TypeScript 渲染器的契约一致。输出是确定性的——工具按字典序排列,工具集不变时文本逐字节相同——因此 prompt 保持 prefix-cache 友好。字典序意味着单一有序的成员流:名字不是合法属性的工具以 tools[name] 注释出现在它排序后的位置上,而不是被分拣到末尾,与 TypeScript 形态就地为异常键加引号的做法一致。这个成员流直接决定了一件事:注释行不是语句,所以一个不发出任何方法的工具集仍需显式 pass。另有两条规则并非源自排序,而是 Python 特有。其一,描述会成为方法的 docstring,且必须作为方法体的第一条语句发出:放在 async def 之上,第一条会变成 Tools 的类文档、其余都是无效果表达式,导致每个方法都没有文档。其二,list[…] 链超过 MAX_LIST_NESTING 后降级为 Any,因为 CPython 的 tokenizer 拒绝一行中超过 200 个同时未闭合的括号,而这个块必须是可解析的 Python——与 docLines 转义引号和反斜杠是同一个理由。ts-types 两者都不需要:TypeScript 会把前置的 /** … */ 附着到其后的成员上,其语法也不对嵌套设限。

该上限服务的标准是语法合法性,这条边界是有意划定的:长的 A | B | … union 在任何长度下都是合法 Python,故不设上限——尽管 CPython 的 compile() 在沿左嵌套 BinOp 脊柱下降时会耗尽 C 递归(在 3.9 上实测:1,000 个分支可编译,5,000 个抛 RecursionError)。没有任何东西会编译这个块——它是提示词文本——所以那条限制在这里没有代价;而给 union 长度封顶会作废那几个钉住 walk 线性时间与类名传播上限的深链测试。将来若有渲染器确实需要可编译的输出,应当把 union 拍平,而不是截断。

renderType 先用 assertSupportedJsonSchema 整树校验一次、随后信任它,用单个 try/catch 把整个遍历兜住并降级为 Any——与姊妹渲染器 ts-types 在这个 typed 同进程 seam 上采取的「校验后信任」姿态一致(Trust TypeScript at typed same-process seams)。它有意不设任何针对「访问器在多次读取间变值」的防御(校验后成环、const/enum 的 TOCTOU、自引用函数):输入是第一方注册(defineTool 字面量或 raw 注册)或从 wire 桥接而来的纯 JSON——前者按 AGENTS.md 受信任,后者是 JSON.parse 产物、物理上不可能携带访问器,且每次调用 renderType 都会整树重新校验——这类输入不可达,而在此加逐形态守卫会为静态接口所禁止的值破坏与 ts-types(没有这类守卫)的对称。jsonSchemaToPy(schema: unknown) 接受 unknown 并对畸形 schema 返回 Any——TypeScript 形态 unknown 的对应物——但它的契约是「降级不支持的 schema」,而非「扛住对抗性的可变 schema」。

Alternatives considered

  • 在 ToolRegistry 上加一个 language 配置字段。 那样部署方就会有两处命名语言(所加载的运行时与 tools 配置)且可能相互矛盾;所加载的运行时是唯一真相来源,故注册表读取它而不复制它。
  • 把 Python 后端 import 进 code-mode.ts 来检测它。 那会把工具层耦合到具体后端,并迫使协议/后端 PR 先落地。按 language 运行时分发使该层保持后端无关、可独立发布。
  • 为未知语言提供默认渲染器。 静默回退会在比如 Ruby 运行时上发出 TypeScript SDK——模型会看到错误语言的指令。在装配处 fail loud 是本仓库对错误配置的立场。

Consequences

新增一门后端语言就是两条表项——一个 SDK_RENDERERS 表项加一个 RUN_CODE_FLAVORS 表项——再加前者所指向的渲染器函数,不动 agent-loop,也不动注册表结构。两张表(SDK_RENDERERS、RUN_CODE_FLAVORS)必须同步:某语言只在其一而不在另一是潜在的不一致,Object.hasOwn 守卫会把它变成一次 loud failure,而不是错误语言的 prompt。工具层不依赖任何具体后端,因此它能先于 Python 协议和后端在 master 上落地并可测。

代价是两张表的 Python 分支在当前 base 上不可达:CodeRuntime.language 由所加载的后端设定,已发布的后端只有 dsh-code-runtime-worker('typescript'),而注册表读取的是所加载的运行时而非某个配置字段,因此没有任何一份组装好的应用能选中 renderToolsSdkPy 或 PYTHON_FLAVOR。也就是说,在报告 'python' 的后端发布之前,本 note 的工作不改变模型可见表面,本 PR 的覆盖因此是 unit 级——渲染器输出加分发与拒绝路径。Python 模型界面的 keyless snapshot 归属于发布该后端的那个 PR,因为只有在那里,一份基于已发布插件的真实 cordis.yml 才会产出 Python 组装;在此处挂载 fixture 运行时的快照示例断言的是测试替身,而 docs/testing.md 明确拒绝以此替代组装好的应用 transcript。