docs(tools): align Code Mode docs with multi-language dispatch; py-types notes
Address ds-review-bot v5/v6 review round 3: - Config.mode JSDoc and the regenerated config-catalog no longer claim Code Mode requires a TypeScript runtime; both now say a language with a registered SDK renderer. - The active 2026-06-15-code-mode base note (both languages) follows shipped reality: the SDK renders the loaded runtime's language, dsh-tools accepts any language with a renderer and run_code flavor, and it cross-links the language-dispatch note. - The language-dispatch note distinguishes the two Object.hasOwn guards' reachability and documents the peekRuntime no-runtime degrade vs the rejected silent fallback. - SDK_RENDERERS comment: adding a language is two table entries, not one. - py-types: document the deliberate PEP 586 deviation for float Literals; add oneOf-object-branch tests (named union classes and context-free degrade), keeping py-types.ts at 100% per-file coverage.
This commit is contained in:
@@ -31,7 +31,9 @@ import { renderToolsSdkPy } from './py-types.ts'
|
||||
* `ctx.codeRuntime.language` in this table when assembling the `tools:sdk`
|
||||
* section under a non-native mode; a runtime whose language is not a key
|
||||
* fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
|
||||
* new backend language is a table entry plus its renderer, nothing else.
|
||||
* new backend language is two table entries — a renderer here and a
|
||||
* {@link RUN_CODE_FLAVORS} entry for its `run_code` schema strings — plus the
|
||||
* renderer itself.
|
||||
*/
|
||||
const SDK_RENDERERS: Record<string, (schemas: ToolSdkSchema[]) => string> = {
|
||||
typescript: renderToolsSdk,
|
||||
@@ -604,8 +606,9 @@ export interface Config {
|
||||
/**
|
||||
* Model presentation. `native` (default) sends every visible schema; `code`
|
||||
* sends only `run_code` plus a generated SDK prompt; `both` sends both forms.
|
||||
* Code modes require a TypeScript runtime and fail prompt assembly when it is
|
||||
* absent or mismatched. Under `code`, native names in `toolOrder` are invalid.
|
||||
* Code modes require a `ctx.codeRuntime` whose `language` has a registered
|
||||
* SDK renderer (TypeScript or Python) and fail prompt assembly when it is
|
||||
* absent or has no renderer. Under `code`, native names in `toolOrder` are invalid.
|
||||
*/
|
||||
mode?: ToolPresentationMode
|
||||
/**
|
||||
|
||||
@@ -154,7 +154,14 @@ function pyScalar(value: JsonSchemaScalar): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
/** Render a validated scalar `const`/`enum` as `Literal[...]`, falling back to the broad type. */
|
||||
/**
|
||||
* Render a validated scalar `const`/`enum` as `Literal[...]`, falling back to
|
||||
* the broad type. Deliberately deviates from PEP 586, which restricts `Literal`
|
||||
* parameters to int/bool/str/bytes/enum/None: a number `const`/`enum` emits a
|
||||
* float literal (`Literal[1.5]`) a strict checker would reject. Harmless here —
|
||||
* the stub is advisory prompt text, only required to parse — and keeping the
|
||||
* exact value communicates the constraint to the model.
|
||||
*/
|
||||
function renderConstrainedScalar(node: Record<string, unknown>, broad: string, state: RenderState): string {
|
||||
if (Object.hasOwn(node, 'const')) {
|
||||
state.typing.add('Literal')
|
||||
|
||||
@@ -217,6 +217,40 @@ describe('renderToolsSdkPy', () => {
|
||||
expect(text.indexOf('class WorkflowArgs(TypedDict):')).toBeLessThan(text.indexOf('class Tools(Protocol):'))
|
||||
})
|
||||
|
||||
it('renders a oneOf of object branches as a union of named TypedDicts declared before the parent', () => {
|
||||
const tool: ToolSdkSchema = {
|
||||
name: 'act',
|
||||
description: 'Union output.',
|
||||
parameters: { type: 'object', additionalProperties: false, properties: {} },
|
||||
output: {
|
||||
oneOf: [
|
||||
{ type: 'object', additionalProperties: false, properties: { ok: { type: 'boolean' } }, required: ['ok'] },
|
||||
{ type: 'object', additionalProperties: false, properties: { err: { type: 'string' } }, required: ['err'] },
|
||||
],
|
||||
},
|
||||
}
|
||||
const text = renderToolsSdkPy([tool])
|
||||
// Each object branch becomes its own named class (`${base}Output1/2`),
|
||||
// declared before the protocol references the union.
|
||||
expect(text).toContain('class ActOutput1(TypedDict):')
|
||||
expect(text).toContain('class ActOutput2(TypedDict):')
|
||||
expect(text).toContain('-> ActOutput1 | ActOutput2')
|
||||
expect(text.indexOf('class ActOutput1(TypedDict):')).toBeLessThan(text.indexOf('class Tools(Protocol):'))
|
||||
expect(text.indexOf('class ActOutput2(TypedDict):')).toBeLessThan(text.indexOf('class Tools(Protocol):'))
|
||||
})
|
||||
|
||||
it('degrades a context-free oneOf of object branches to a union of dict[str, Any]', () => {
|
||||
// jsonSchemaToPy has no naming context, so each object branch degrades
|
||||
// rather than declaring a class.
|
||||
const type = jsonSchemaToPy({
|
||||
oneOf: [
|
||||
{ type: 'object', additionalProperties: false, properties: { ok: { type: 'boolean' } }, required: ['ok'] },
|
||||
{ type: 'string' },
|
||||
],
|
||||
})
|
||||
expect(type).toBe('dict[str, Any] | str')
|
||||
})
|
||||
|
||||
it('suffixes a counter when two tools CamelCase to the same class base', () => {
|
||||
const a: ToolSdkSchema = {
|
||||
name: 'my-tool',
|
||||
|
||||
Reference in New Issue
Block a user