mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -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 100–199 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user