fix(tools): correct Python SDK fidelity and language-dispatch contract

Address ds-review-bot v5/v6 review on the Python SDK renderer:
- resolveFlavor now takes a peekRuntime() reader: undefined (no runtime,
  the doc-catalog harvest) degrades to the TS flavor, but a mounted
  runtime whose language is absent from RUN_CODE_FLAVORS fails loud. This
  removes the try/catch that silently swallowed the invalid-language path
  and drops the /* v8 ignore */ that hid the flavor guard from coverage;
  wireSchemas validates the runtime before projecting schemas so the
  renderer-table rejection stays the canonical assembly error.
- py-types RESERVED drops the soft keywords match/case: they are legal as
  TypedDict fields and methods, so keeping them needlessly degraded
  common search/regex arg objects to dict[str, Any].
- py-types treats an object with omitted properties as {} like the unified
  validator and TS renderer do, so a closed empty object declares an empty
  TypedDict instead of a permissive dict[str, Any].
- README: symmetric jsonSchemaToPy->Any note; a stale zh SDK bullet and
  limitation corrected; link the service-wide-language limitation to its
  Agent Note.
This commit is contained in:
Chinesezjc
2026-07-31 18:55:07 +08:00
parent 85a831259c
commit 683c92cb2e
8 changed files with 130 additions and 57 deletions

View File

@@ -373,13 +373,27 @@ describe('mode-aware wire contribution', () => {
expect(codeParam.description).toBe('The program: the body of an async Python function.')
})
it('fails loud when the runtime language has no run_code schema flavor', async () => {
// A language with an SDK renderer registered but (hypothetically) no schema
// flavor would fail here; a language with neither fails earlier at
// requireCodeRuntime. Both guards keep the two tables coupled.
const { ctx, systemPrompt } = await setup({ mode: 'code', runtime: { language: 'ruby' } })
registerEcho(ctx)
await expect(systemPrompt.assemble()).rejects.toThrow(/no SDK renderer registered for runtime language "ruby"/)
it('resolves the run_code schema flavor lazily and fails loud on a language absent from the flavor table', async () => {
// The flavor getter reads the runtime directly (peekRuntime), so it — not
// requireCodeRuntime — owns the flavor-table guard. A language with no
// flavor entry throws when the schema is projected, keeping
// RUN_CODE_FLAVORS coupled to SDK_RENDERERS. Assembly's requireCodeRuntime
// rejects such a language earlier; this reaches the guard on its own.
const { ctx } = await setup({ mode: 'code', runtime: { language: 'ruby' } })
const definition = ctx.tools.get(RUN_CODE_NAME)
expect(() => definition?.description).toThrow(/no run_code schema flavor registered for runtime language "ruby"/)
})
it('degrades the run_code flavor to TypeScript when no runtime is mounted (doc-catalog schema harvest)', async () => {
// The tool-catalog generator boots the registry under `mode: code` and
// reads run_code's schema WITHOUT a runtime; peekRuntime returns undefined
// there, so the flavor getter degrades to the TS default rather than
// throwing (that harvest never feeds a model).
const { ctx } = await setup({ mode: 'code', runtime: false })
const definition = ctx.tools.get(RUN_CODE_NAME)
expect(definition?.description).toContain('Execute a TypeScript program')
const params = definition?.parameters as { properties: { code: { description: string } } }
expect(params.properties.code.description).toBe('The program: the body of an async TypeScript function.')
})
it("rejects the assembly when toolOrder names a native tool that mode 'code' no longer contributes", async () => {

View File

@@ -260,6 +260,59 @@ describe('renderToolsSdkPy', () => {
expect(text).not.toContain('WeirdFieldsArgs')
})
it('keeps soft-keyword field names as TypedDict fields (match/case/type are only special in statement position)', () => {
const tool: ToolSdkSchema = {
name: 'search',
description: 'Soft keywords as fields.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
match: { type: 'string' },
case: { type: 'boolean' },
type: { type: 'string' },
},
required: ['match'],
},
output: { type: 'string' },
}
const text = renderToolsSdkPy([tool])
// The object keeps its shape rather than degrading to dict[str, Any].
expect(text).toContain('class SearchArgs(TypedDict):')
expect(text).toContain('match: str')
expect(text).toContain('case: NotRequired[bool]')
expect(text).toContain('type: NotRequired[str]')
expect(text).not.toContain('dict[str, Any]')
})
it('declares a closed empty object with omitted properties as an empty TypedDict, not dict[str, Any]', () => {
// `{ type: 'object', additionalProperties: false }` with no `properties`
// is a closed empty object — no key accepted — exactly as the validator
// and the TS renderer read it. It must not degrade to a permissive dict.
const tool: ToolSdkSchema = {
name: 'closed',
description: 'Closed empty object with omitted properties.',
parameters: {
type: 'object',
additionalProperties: false,
properties: { inner: { type: 'object', additionalProperties: false } },
required: ['inner'],
},
output: { type: 'string' },
}
const text = renderToolsSdkPy([tool])
expect(text).toMatch(/class ClosedArgsInner\(TypedDict\):\n pass/)
expect(text).toContain('inner: ClosedArgsInner')
expect(text).not.toContain('dict[str, Any]')
})
it('degrades an open object with omitted properties to dict[str, Any]', () => {
// An OPEN empty object (default additionalProperties) is any dict.
const type = jsonSchemaToPy({ type: 'object', properties: {} })
expect(type).toBe('dict[str, Any]')
expect(jsonSchemaToPy({ type: 'object' })).toBe('dict[str, Any]')
})
it('renders docstrings for descriptions and orders emissions lexicographically', () => {
const text = renderToolsSdkPy([bash, exotic])
expect(text).toContain('"""Run a shell command."""')