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:
@@ -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 () => {
|
||||
|
||||
@@ -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."""')
|
||||
|
||||
Reference in New Issue
Block a user