feat(tools): render a Python SDK and dispatch Code Mode by runtime language

Code Mode generated only a TypeScript SDK and rejected any runtime whose
language was not "typescript". Add py-types.ts (jsonSchemaToPy /
renderToolsSdkPy) and select the SDK-section renderer and the run_code
schema flavor by ctx.codeRuntime.language through two parallel tables
(SDK_RENDERERS, RUN_CODE_FLAVORS), read with Object.hasOwn and failing
loud on a language with no renderer. The tool layer depends only on the
code-runtime seam's language field, so it lands independently of the
Python protocol and backend.
This commit is contained in:
Chinesezjc
2026-07-31 18:16:15 +08:00
parent d6853a667e
commit 4fdfa89d51
11 changed files with 1176 additions and 29 deletions

View File

@@ -11,7 +11,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { defineTool } from './schema.ts'
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
@@ -56,6 +56,95 @@ export const RUN_CODE_NAME = 'run_code'
/** The `tools:sdk` section order: inside the 100199 tool-guidance band, after per-tool guidance sections. */
export const SDK_SECTION_ORDER = 150
/**
* The language-specific `run_code` schema text: the tool `description` and its
* `code` parameter description, kept together so a language's two model-facing
* strings share one source of truth. Keyed by `CodeRuntime.language`, mirroring
* `SDK_RENDERERS` in {@link ./index.ts}. The emitted flavor MUST match the
* semantics the same language's SDK instructions promise, so the model never
* receives a TypeScript-shaped schema beside a Python SDK (or vice versa).
*/
interface RunCodeFlavor {
/** The tool `description` the model sees for this language. */
readonly description: string
/** The `code` parameter's description for this language. */
readonly codeDescription: string
}
/**
* The TypeScript flavor: the historical default, and the fallback the schema
* harvest degrades to when no runtime is mounted (the doc-catalog generator
* reads `schemas()` without one). A real assembly always resolves a runtime
* first, so the model never sees this fallback outside its own language.
*/
const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
description:
'Execute a TypeScript program against the available tools. Write the BODY of an '
+ 'async function (erasable syntax only; top-level `await` and `return` work) and '
+ 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
+ 'Only what you print or return comes back — curate it.',
codeDescription: 'The program: the body of an async TypeScript function.',
}
/**
* The Python flavor: the body of an async function, top-level `await` and
* `return`, answer via `print` and/or the returned value, matching
* {@link ./py-types.ts}'s SDK instructions.
*/
const PYTHON_FLAVOR: RunCodeFlavor = {
description:
'Execute a Python program against the available tools. Write the BODY of an '
+ 'async function (top-level `await` and `return` work) and call tools as '
+ '`await tools.name(args)` per the declarations in the system prompt. Answer '
+ 'with `print(...)` and/or `return <value>` — only that comes back, so curate it.',
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. */
const RUN_CODE_FLAVORS: Record<string, RunCodeFlavor> = {
typescript: TYPESCRIPT_FLAVOR,
python: PYTHON_FLAVOR,
}
/**
* The `description` parameter's model-facing description: language-independent
* (the UI label contract is the same for every runtime), shared between the
* static spec and the language-aware `parameters` getter so the two emissions
* can never drift.
*/
const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
= 'Clear, concise description of what this program does in active voice, '
+ '5-10 words (shown in the UI). Examples: "Count TODO markers across packages"; '
+ '"Read failing test and its fixture"; "Rename config key in every cordis.yml".'
/**
* Resolve the {@link RunCodeFlavor} for the loaded runtime's language, read at
* schema-emission time so the model-visible `run_code` schema always matches
* the SDK section's language. When no runtime is mounted the schema harvest
* degrades to {@link TYPESCRIPT_FLAVOR} (a doc-only path — an assembly always
* has one). A mounted runtime whose language has no flavor entry fails loud,
* keeping this table coupled to `SDK_RENDERERS`.
*/
function resolveFlavor(requireRuntime: () => CodeRuntime): RunCodeFlavor {
let runtime: CodeRuntime
try {
runtime = requireRuntime()
} catch {
// No runtime mounted: the only reader here is the static schema harvest
// (doc catalog), which never reaches a model — degrade to the TS default.
return TYPESCRIPT_FLAVOR
}
// Own-property read: a language like `toString`/`constructor` would otherwise
// resolve an inherited Object.prototype member as a flavor.
const flavor = RUN_CODE_FLAVORS[runtime.language]
/* v8 ignore next 3 -- requireRuntime rejects a language absent from SDK_RENDERERS, whose keys
mirror RUN_CODE_FLAVORS; the guard is defense-in-depth against the two tables drifting. */
if (!Object.hasOwn(RUN_CODE_FLAVORS, runtime.language) || flavor === undefined) {
throw new Error(`dsh-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)}`)
}
return flavor
}
/**
* Thrown by `run_code` when the program run itself failed — a program
* exception, a budget expiry, an abort, or substrate death. Extends
@@ -213,21 +302,21 @@ export interface RunCodeBridgeOptions {
*/
export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridgeOptions): ToolDefinition {
const { requireRuntime, maxParallel, shapeDispatchLog } = options
return defineTool({
const definition = defineTool({
name: RUN_CODE_NAME,
description:
'Execute a TypeScript program against the available tools. Write the BODY of an '
+ 'async function (erasable syntax only; top-level `await` and `return` work) and '
+ 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
+ 'Only what you print or return comes back — curate it.',
// The description and `code` parameter description are placeholders here:
// the language-aware getters installed below replace both, resolving the
// loaded runtime's flavor at schema-emission time so the schema the MODEL
// sees matches the SDK section's language. Argument VALIDATION still keys
// off this static spec (defineTool closes over it), which is language-
// independent (one required string `code`).
description: TYPESCRIPT_FLAVOR.description,
parameters: {
code: { type: 'string', required: true, description: 'The program: the body of an async TypeScript function.' },
code: { type: 'string', required: true, description: TYPESCRIPT_FLAVOR.codeDescription },
description: {
type: 'string',
required: true,
description: 'Clear, concise description of what this program does in active voice, '
+ '5-10 words (shown in the UI). Examples: "Count TODO markers across packages"; '
+ '"Read failing test and its fixture"; "Rename config key in every cordis.yml".',
description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION,
},
},
output: {
@@ -569,4 +658,22 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
// title and reads durable result content without duplicating a large raw
// result into the host view payload.
})
// Resolve the language flavor lazily, at the moment the registry projects the
// schema (`schemaOf` destructures `description`/`parameters`). The definition
// is minted once at registration, before a runtime is known; deferring here
// is the least invasive point that still emits the loaded runtime's language.
Object.defineProperty(definition, 'description', {
enumerable: true,
get: () => resolveFlavor(requireRuntime).description,
})
Object.defineProperty(definition, 'parameters', {
enumerable: true,
// Recompile through the same spec→schema projection defineTool used, so
// the emitted shape can never drift from the validated one.
get: () => parameterSchemaSpecToJsonSchema({
code: { type: 'string', required: true, description: resolveFlavor(requireRuntime).codeDescription },
description: { type: 'string', required: true, description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION },
}) as unknown as Record<string, unknown>,
})
return definition
}