From e14bcfb08aae6a7115b46b9b5a02e0eb1cbf7ee9 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 5 Aug 2026 23:25:42 +0800 Subject: [PATCH] refactor(tools): pin the two language tables to one union, and name python at the seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SDK_RENDERERS` and `RUN_CODE_FLAVORS` had to stay in step by review alone: the `Object.hasOwn` guards catch drift only once a runtime reporting the half-added language exists, which is the one case that cannot arise. Both tables are now `satisfies`-checked against a shared `CodeSdkLanguage` union, so a missing or extra entry fails `typecheck`. The declared `Record` type stays, since `CodeRuntime.language` is an unconstrained `string`. The code-runtime seam's own README row and `CodeRuntime.language` JSDoc still named `'typescript'` as the sole well-known value; both now name `'python'` too and say only `'typescript'` has a published backend. --- ...26-07-31-code-mode-language-dispatch.i18n.yaml | 4 ++-- .../2026-07-31-code-mode-language-dispatch.md | 2 +- .../2026-07-31-code-mode-language-dispatch.zh.md | 2 +- .../code-runtime/code-runtime/README.i18n.yaml | 4 ++-- packages/code-runtime/code-runtime/README.md | 2 +- packages/code-runtime/code-runtime/README.zh.md | 2 +- packages/code-runtime/code-runtime/src/index.ts | 3 ++- packages/core/tools/src/code-mode.ts | 15 +++++++++++++-- packages/core/tools/src/index.ts | 8 ++++++-- 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml index 211b854cf5..1611e6737a 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml @@ -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/feature/2026-07-31-code-mode-language-dispatch.md -2026-07-31-code-mode-language-dispatch.md: 1fbe7ed46885d10e0420004284a40b606cafd521 -2026-07-31-code-mode-language-dispatch.zh.md: c9e0b6f84715db5fd9a0568b4c9a368dd564e315 +2026-07-31-code-mode-language-dispatch.md: 292fb104b12fc326261f3716a191c360d69a37d8 +2026-07-31-code-mode-language-dispatch.zh.md: 16be72f9c619bee35295e3e6eec9189fcbc6bb04 diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md index 1fbe7ed468..292fb104b1 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md @@ -37,7 +37,7 @@ The standard that cap serves is grammatical validity, and the boundary is delibe ## Consequences -Adding a backend language is two table entries — an `SDK_RENDERERS` entry and a `RUN_CODE_FLAVORS` entry — plus the renderer function the former points at, with no change to `agent-loop` or the registry structure. The two tables (`SDK_RENDERERS`, `RUN_CODE_FLAVORS`) must stay in step: a language present in one but not the other is a latent inconsistency the `Object.hasOwn` guards turn into a loud failure rather than a wrong-language prompt. Which of the two failures surfaces depends on the entry point, for a language absent from both tables: assembly reports the missing renderer, because `wireSchemas` calls `requireCodeRuntime` before projecting, while the public `schemas()` reaches `run_code`'s language-aware getters first and reports the missing flavor. The tool layer stays free of any concrete backend dependency, so it lands and is testable on master ahead of the Python protocol and backend. +Adding a backend language is two table entries — an `SDK_RENDERERS` entry and a `RUN_CODE_FLAVORS` entry — plus the renderer function the former points at, with no change to `agent-loop` or the registry structure. The two tables (`SDK_RENDERERS`, `RUN_CODE_FLAVORS`) must stay in step, and that invariant is checked statically rather than left to review: both are `satisfies`-checked against one `CodeSdkLanguage` union, so a language added to one and not the other fails `typecheck`. This is the mechanical form the drift risk deserves — the runtime `Object.hasOwn` guards would catch it too, but only once a backend reporting that language exists, which for the half-added language is precisely the case that cannot arise. The tables keep their `Record` declared type because `CodeRuntime.language` is an unconstrained `string`; the union pins what the harness ships, the guards reject what a runtime reports. A unit test pinning the two key sets equal was rejected in favor of this: it would buy the same check at the cost of a test-only export of two private tables, and would run later than the compiler does. Which of the two runtime failures surfaces depends on the entry point, for a language absent from both tables: assembly reports the missing renderer, because `wireSchemas` calls `requireCodeRuntime` before projecting, while the public `schemas()` reaches `run_code`'s language-aware getters first and reports the missing flavor. The tool layer stays free of any concrete backend dependency, so it lands and is testable on master ahead of the Python protocol and backend. The cost is that the Python branch of both tables is unreachable on this base: `CodeRuntime.language` is set by the loaded backend, the only published backend is `dsh-code-runtime-worker` (`'typescript'`), and the registry reads the loaded runtime rather than a config field, so no assembled application can select `renderToolsSdkPy` or `PYTHON_FLAVOR`. The model-visible surface is therefore unchanged by this note's work until a backend reporting `'python'` is published, and this PR's coverage is unit-level — the renderer output plus the dispatch and rejection paths. The keyless snapshot for the Python model interface belongs to the PR that publishes that backend, because only there does a real `cordis.yml` over published plugins produce a Python assembly; a snapshot example that mounted a fixture runtime here would assert against a test double, which [docs/testing.md](../../../../docs/testing.md) rejects as a substitute for the assembled application transcript. diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md index c9e0b6f847..16be72f9c6 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md @@ -37,7 +37,7 @@ Code Mode 只生成一种 SDK 形态:TypeScript。`ToolRegistry` 为 `tools:sd ## Consequences -新增一门后端语言就是两条表项——一个 `SDK_RENDERERS` 表项加一个 `RUN_CODE_FLAVORS` 表项——再加前者所指向的渲染器函数,不动 `agent-loop`,也不动注册表结构。两张表(`SDK_RENDERERS`、`RUN_CODE_FLAVORS`)必须同步:某语言只在其一而不在另一是潜在的不一致,`Object.hasOwn` 守卫会把它变成一次 loud failure,而不是错误语言的 prompt。对两张表都缺席的语言,报出哪一条随入口而异:组装路径报缺渲染器,因为 `wireSchemas` 在投影前先调 `requireCodeRuntime`;而公共 `schemas()` 先经过 `run_code` 的语言感知 getter,报的是缺 flavor 表项。工具层不依赖任何具体后端,因此它能先于 Python 协议和后端在 master 上落地并可测。 +新增一门后端语言就是两条表项——一个 `SDK_RENDERERS` 表项加一个 `RUN_CODE_FLAVORS` 表项——再加前者所指向的渲染器函数,不动 `agent-loop`,也不动注册表结构。两张表(`SDK_RENDERERS`、`RUN_CODE_FLAVORS`)必须同步,且这条不变式由静态检查把关,而非交给 review:两张表都以 `satisfies` 对同一个 `CodeSdkLanguage` union 校验,因此只加其一而漏掉另一会在 `typecheck` 处失败。这正是该漂移风险应有的机械形式——运行期的 `Object.hasOwn` 守卫同样能捕获,但要等到有后端报告该语言之后,而对那门只加了一半的语言来说,这恰恰是不可能出现的情形。两张表的声明类型仍是 `Record`,因为 `CodeRuntime.language` 是不受约束的 `string`:union 钉住本仓库交付了什么,守卫拒绝运行时报告了什么。用一个断言两张表键集相等的 unit test 的方案被否决:它买到的是同一条检查,代价却是把两张私有表做测试专用导出,且运行时机晚于编译器。对两张表都缺席的语言,报出哪一条随入口而异:组装路径报缺渲染器,因为 `wireSchemas` 在投影前先调 `requireCodeRuntime`;而公共 `schemas()` 先经过 `run_code` 的语言感知 getter,报的是缺 flavor 表项。工具层不依赖任何具体后端,因此它能先于 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](../../../../docs/testing.md) 明确拒绝以此替代组装好的应用 transcript。 diff --git a/packages/code-runtime/code-runtime/README.i18n.yaml b/packages/code-runtime/code-runtime/README.i18n.yaml index 8e45c6265b..c0e47dc710 100644 --- a/packages/code-runtime/code-runtime/README.i18n.yaml +++ b/packages/code-runtime/code-runtime/README.i18n.yaml @@ -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 packages/code-runtime/code-runtime/README.md -README.md: c7a2d519e47d160f5ab123bfc887e7e9f24ec602 -README.zh.md: 22d0b120d7cea50b578a184b3e40d77707ebc489 +README.md: ec962d7def4bc751151d417fd5a7026038814f33 +README.zh.md: a94ea0feed18f2c7dd99816f072645eebe197e97 diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index c7a2d519e4..ec962d7def 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -11,7 +11,7 @@ This package is the interface third of the capability (the bash trio is the temp | Member | Semantics | |---|---| | `run(request)` | Execute one program against the request's bindings. **Resolves with an error FIELD for every program outcome** — parse/transform failure, thrown exception, invalid completion, output overflow, budget expiry, abort, or substrate death (`CodeRunFailure`'s orthogonal `kind` taxonomy); it rejects only for caller misuse of the seam itself (e.g. a run submitted after disposal). The program runs as the body of an async function: top-level `await`/`return` work, and a lossless JSON completion becomes `result.value`. | -| `language` | Readonly descriptor: the source language `run` expects (`'typescript'` is the well-known value). Informational, not gating — a consumer that generates language-specific presentation switches on it and fails loud on a language it cannot present. | +| `language` | Readonly descriptor: the source language `run` expects. `'typescript'` and `'python'` are the well-known values — the two `dsh-tools` presents; only `'typescript'` has a published backend. Informational, not gating — a consumer that generates language-specific presentation switches on it and fails loud on a language it cannot present. | | `isolation` | Readonly descriptor: the execution substrate (`'worker-thread'`, `'process'`, `'container'`). A label for deployments and diagnostics, **not a security claim**. | Semantics every implementation must honor (contract details in the class JSDoc): binding calls bridge complete lossless-JSON arguments and resolutions with no seam-level byte cap; the program is treated as a hostile peer (arbitrary binding names are own properties, malformed traffic never crashes the host); no state survives between runs; disposal terminates in-flight runs AND awaits their exit before completing. diff --git a/packages/code-runtime/code-runtime/README.zh.md b/packages/code-runtime/code-runtime/README.zh.md index 22d0b120d7..a94ea0feed 100644 --- a/packages/code-runtime/code-runtime/README.zh.md +++ b/packages/code-runtime/code-runtime/README.zh.md @@ -11,7 +11,7 @@ | 成员 | 语义 | |---|---| | `run(request)` | 针对请求的绑定执行一段程序。**所有程序失败结果都通过 resolve 结果中的 error 字段报告**:包括解析/转换失败、抛出异常、无效完成值、输出溢出、预算到期、中止或执行基底终止(由 `CodeRunFailure` 的正交 `kind` 分类表示);只有调用方误用 seam 本身时才 reject(例如 dispose(资源释放)后仍提交运行)。程序作为异步函数的函数体运行,因此顶层 `await`/`return` 可用,无损 JSON 完成值会成为 `result.value`。 | -| `language` | 只读描述符:`run` 期望的源语言(已知值为 `'typescript'`)。仅供参考,不作门禁;生成语言专用呈现的消费方会根据该值选择分支,遇到无法呈现的语言时明确失败。 | +| `language` | 只读描述符:`run` 期望的源语言。已知值为 `'typescript'` 与 `'python'`——`dsh-tools` 能呈现的两种;其中只有 `'typescript'` 有已发布的后端。仅供参考,不作门禁;生成语言专用呈现的消费方会根据该值选择分支,遇到无法呈现的语言时明确失败。 | | `isolation` | 只读描述符:执行基底(`'worker-thread'`、`'process'`、`'container'`)。供部署与诊断使用,**不构成安全声明**。 | 每个实现都必须遵守以下语义(完整契约见类 JSDoc):绑定调用会桥接完整的无损 JSON 参数与 resolve 值,seam 层不设字节上限;程序被视为敌对对等方(任意绑定名称都会成为自有属性,格式错误的通信绝不能使宿主崩溃);不同运行之间不保留任何状态;dispose 会终止进行中的运行,并且在完成前等待其退出。 diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index bd52b9ed29..83c302d13f 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -36,7 +36,8 @@ export abstract class CodeRuntime extends Service { * lowercase identifier. Informational, not gating — a consumer that * generates language-specific presentation (typed SDK stubs, usage * instructions) switches on it and fails loud on a language it cannot - * present. Well-known value: `'typescript'`. + * present. Well-known values: `'typescript'` and `'python'`, the two + * `dsh-tools` presents; only `'typescript'` has a published backend. */ abstract readonly language: string diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 7132ca3646..5ed2c7b4e2 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -100,11 +100,22 @@ const PYTHON_FLAVOR: RunCodeFlavor = { codeDescription: 'The program: the body of an async Python function.', } -/** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per `SDK_RENDERERS` language. */ +/** + * The languages Code Mode ships a presentation for. Both per-language tables — + * {@link RUN_CODE_FLAVORS} here and `SDK_RENDERERS` in {@link ./index.ts} — are + * checked against this union with `satisfies`, so a language added to one and + * not the other fails `typecheck` instead of waiting for a runtime that reports + * it. The tables stay declared `Record` because `CodeRuntime.language` + * is an unconstrained `string`: this union pins what the harness ships, while the + * `Object.hasOwn` guards reject what a mounted runtime may report. + */ +export type CodeSdkLanguage = 'typescript' | 'python' + +/** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link CodeSdkLanguage}. */ const RUN_CODE_FLAVORS: Record = { typescript: TYPESCRIPT_FLAVOR, python: PYTHON_FLAVOR, -} +} satisfies Record /** * The `description` parameter's model-facing description: language-independent diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 820390228e..23cdc4e085 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -22,6 +22,7 @@ import type { ToolCallView, ToolResultView } from './presentation.ts' import { assertSupportedJsonSchema, validateJsonSchemaValue } from './json-schema.ts' import type { JsonSchemaNode } from './json-schema.ts' import { createRunCodeTool, RUN_CODE_NAME, SDK_SECTION_ORDER } from './code-mode.ts' +import type { CodeSdkLanguage } from './code-mode.ts' import { renderToolsSdk } from './ts-types.ts' import type { ToolSdkSchema } from './ts-types.ts' import { renderToolsSdkPy } from './py-types.ts' @@ -33,12 +34,15 @@ import { renderToolsSdkPy } from './py-types.ts' * fails the assembly loudly (same idiom as `toolOrder` violations). Adding a * new backend language is two table entries — an entry here and a * `RUN_CODE_FLAVORS` entry in `code-mode.ts` for its `run_code` schema strings - * — plus the renderer function this table points at. + * — plus the renderer function this table points at. The `satisfies` clause + * pins this table's key set to {@link CodeSdkLanguage}, the same union the + * flavor table is checked against, so adding one entry without the other is a + * typecheck failure. */ const SDK_RENDERERS: Record string> = { typescript: renderToolsSdk, python: renderToolsSdkPy, -} +} satisfies Record string> export { defineTool,