fix(plugin-sdk-tamagotchi): allow nullable enum tool parameters (#2314)

This commit is contained in:
Neko
2026-08-18 20:28:55 +08:00
committed by GitHub
parent d832137fde
commit 677329427f
2 changed files with 38 additions and 2 deletions
@@ -10,7 +10,7 @@ import type { HostDataRecord } from '@proj-airi/plugin-sdk/plugin-host'
import type { ToolKitRuntime } from './tools'
import { DisposableStore } from '@proj-airi/plugin-sdk'
import { object, optional, string } from 'valibot'
import { object, optional, picklist, string } from 'valibot'
import { describe, expect, it, vi } from 'vitest'
import {
@@ -731,6 +731,38 @@ describe('plugin-sdk-tamagotchi', () => {
expect(parameters.properties.opening.type).toEqual(['string', 'null'])
})
// ROOT CAUSE:
//
// The normalizer added null to an optional enum but kept type: "string".
// OpenAI rejected the schema because null does not satisfy the string type.
// The fix must allow null in both type and enum.
it('serializes optional enum tool fields as nullable enum properties', async () => {
const registerTool = vi.fn()
const tools = toolKit.createClient(createToolRuntime({
extensionId: 'airi-extension-chess',
sessionId: 'session-1',
moduleId: 'chess',
register: registerTool,
registerToolsetPrompt: vi.fn(),
}))
await tools.registerTool({
id: 'play_chess',
title: 'placeholder-title',
description: 'placeholder-description',
inputSchema: object({
airiSide: optional(picklist(['white', 'black'])),
}),
execute: async () => ({ ok: true }),
})
const parameters = registerTool.mock.calls[0]?.[0].tool.parameters
expect(parameters.required).toEqual(['airiSide'])
expect(parameters.properties.airiSide.type).toEqual(['string', 'null'])
expect(parameters.properties.airiSide.enum).toEqual(['white', 'black', null])
})
/**
* @example
* expect(registerBinding).toHaveBeenCalledWith(expect.objectContaining({ moduleId: 'chess:board' }))
@@ -171,7 +171,6 @@ function withNullableValue(schema: JsonSchema): JsonSchema {
if (Array.isArray(next.enum)) {
next.enum = next.enum.includes(null) ? next.enum : [...next.enum, null]
return next
}
if (Array.isArray(next.type)) {
@@ -184,6 +183,11 @@ function withNullableValue(schema: JsonSchema): JsonSchema {
return next
}
// An enum without a type already accepts every enum value, including null.
if (Array.isArray(next.enum)) {
return next
}
next.anyOf = [...(next.anyOf ?? []), { type: 'null' }]
return next
}