- Delink dsh-code-runtime-python README references (that package ships in a later PR of the split; keep the package name unlinked meanwhile). - Add the mandatory `## Alternatives considered` and `## Consequences` sections to the language-dispatch Agent Note. - Regenerate config/cordis catalogs and the event graph for the shifted index.ts source lines; re-record the README and note i18n pairings.
4.6 KiB
Agent Note: Code Mode language dispatch and the Python SDK renderer
Status: implemented
English | 中文
Problem
Code Mode generated one SDK flavor: TypeScript. ToolRegistry hard-coded renderToolsSdk for the tools:sdk section and requireCodeRuntime rejected any ctx.codeRuntime.language !== 'typescript'. Adding a CPython backend means a program's source language is no longer fixed: the same visible tool registry must project a Python SDK when a Python runtime is loaded, and the model-facing run_code schema strings ("Execute a Python program …") must match the SDK section's language so the model never sees a TypeScript instruction over a Python runtime.
This is the tool-facing half of the multi-language Code Mode split; the code-runtime seam already carries CodeRuntime.language. This note owns only how dsh-tools dispatches on that field. The backend that implements language: 'python' is owned by its own note, delivered separately.
Decision
Language selection is a lookup on ctx.codeRuntime.language, resolved lazily at prompt assembly, against two parallel tables in dsh-tools:
SDK_RENDERERS(index.ts) maps a language to itstools:sdkrenderer —typescript → renderToolsSdk,python → renderToolsSdkPy. Thetools:sdksection reads the loaded runtime's language and picks the renderer;requireCodeRuntimerejects amode: code/bothruntime whose language is absent from the table, naming the known languages.RUN_CODE_FLAVORS(code-mode.ts) maps a language to its two model-facingrun_codestrings (tooldescriptionand thecodeparameter description), so a language's SDK section and its transport schema always agree.
Both tables are read with Object.hasOwn before use so a language named toString/constructor cannot resolve an inherited Object.prototype member as a renderer; a language present on neither table but reaching the read fails loud (defense-in-depth against a caller bypassing the guard). Adding a backend language is two table entries plus its renderer — no agent-loop or registry-structure change.
code-mode.ts depends only on the runtime seam (@deepseek-ai/dsh-code-runtime), never on a concrete backend; dispatch is by runtime.language at run time. The tool layer therefore lands independently of the protocol and backend PRs — it needs only the seam's language field, which is already on master.
The Python SDK renderer
py-types.ts renders the same unified tool-schema vocabulary jsonSchemaToTs covers, targeting Python: jsonSchemaToPy emits a type expression per JSON-schema node, and renderToolsSdkPy assembles named TypedDicts for each visible tool's arguments and canonical output plus a tools object with usage instructions equivalent to the TypeScript flavor. Unsupported raw constructs degrade rather than throwing during assembly, matching the TypeScript renderer's contract. The output is deterministic — lexicographic tool order, byte-identical text for an unchanged tool set — so the prompt stays prefix-cache-friendly.
Alternatives considered
- A
languageconfig field onToolRegistry. Deployment would then have two places to name the language (the loaded runtime and the tools config) that can disagree; the loaded runtime is the single source of truth, so the registry reads it rather than duplicating it. - Importing the Python backend into
code-mode.tsto detect it. That would couple the tool layer to a concrete backend and force the protocol/backend PRs to land first. Runtime dispatch onlanguagekeeps the layer backend-agnostic and independently shippable. - A default renderer for an unknown language. A silent fallback would emit a TypeScript SDK over, e.g., a Ruby runtime — the model would see instructions in the wrong language. Failing loud at assembly is the repository's misconfiguration stance.
Consequences
Adding a backend language is a table entry plus its renderer, 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. 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 a python runtime cannot actually be exercised end to end until that backend ships, so this PR's coverage is unit-level (the renderer output and the dispatch/rejection paths) rather than a real Python run.