Skip to content

Commit 67e3d39

Browse files
committed
fix(stage-ui): tool definition missing strict & additionalProperties check
1 parent 99159f0 commit 67e3d39

File tree

4 files changed

+94
-13
lines changed

4 files changed

+94
-13
lines changed

packages/stage-ui/src/stores/character-orchestrator.test.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import type { Mock } from 'vitest'
77
import type { UnwrapRef } from 'vue'
88
import type z from 'zod'
99

10-
import type { sparkCommandSchema } from './character-orchestrator'
1110
import type { StreamEvent } from './llm'
1211
import type { AiriCard } from './modules'
1312

1413
import { createTestingPinia } from '@pinia/testing'
14+
import { tool } from '@xsai/tool'
1515
import { nanoid } from 'nanoid'
1616
import { setActivePinia } from 'pinia'
1717
import { beforeEach, describe, expect, it, vi } from 'vitest'
1818

1919
import { useCharacterStore } from './character'
20-
import { useCharacterOrchestratorStore } from './character-orchestrator'
20+
import { sparkCommandSchema, useCharacterOrchestratorStore } from './character-orchestrator'
2121
import { useLLM } from './llm'
2222
import { useAiriCardStore, useConsciousnessStore } from './modules'
2323
import { useProvidersStore } from './providers'
@@ -53,6 +53,54 @@ function mockedStore<TStoreDef extends () => unknown>(
5353
return useStore() as any
5454
}
5555

56+
function getObjectSchema(schema?: Record<string, any>) {
57+
if (!schema)
58+
return undefined
59+
60+
if (schema.type === 'object')
61+
return schema
62+
63+
const candidates = [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]
64+
return candidates.find((candidate: Record<string, any>) => candidate?.type === 'object')
65+
}
66+
67+
function getArraySchema(schema?: Record<string, any>) {
68+
if (!schema)
69+
return undefined
70+
71+
if (schema.type === 'array')
72+
return schema
73+
74+
const candidates = [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]
75+
return candidates.find((candidate: Record<string, any>) => candidate?.type === 'array')
76+
}
77+
78+
describe('sparkCommandSchema', () => {
79+
it('emits strict objects in the json schema', async () => {
80+
const sparkTool = await tool({
81+
name: 'builtIn_sparkCommand',
82+
description: 'test',
83+
parameters: sparkCommandSchema,
84+
execute: async () => undefined,
85+
})
86+
87+
const schema = sparkTool.function.parameters as Record<string, any>
88+
const commandsSchema = getArraySchema(schema.properties?.commands)
89+
const commandItemSchema = getObjectSchema(commandsSchema?.items)
90+
const guidanceSchema = getObjectSchema(commandItemSchema?.properties?.guidance)
91+
const personaSchema = getArraySchema(guidanceSchema?.properties?.persona)
92+
const personaItemSchema = getObjectSchema(personaSchema?.items)
93+
const optionsSchema = getArraySchema(guidanceSchema?.properties?.options)
94+
const optionsItemSchema = getObjectSchema(optionsSchema?.items)
95+
96+
expect(schema.additionalProperties).toBe(false)
97+
expect(commandItemSchema?.additionalProperties).toBe(false)
98+
expect(guidanceSchema?.additionalProperties).toBe(false)
99+
expect(personaItemSchema?.additionalProperties).toBe(false)
100+
expect(optionsItemSchema?.additionalProperties).toBe(false)
101+
})
102+
})
103+
56104
describe('store character-orchestrator', () => {
57105
beforeEach(() => {
58106
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })

packages/stage-ui/src/stores/character-orchestrator.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const sparkCommandSchema = z.object({
5555
persona: z.array(z.object({
5656
strength: z.enum(['very-high', 'high', 'medium', 'low', 'very-low']),
5757
traits: z.string().describe('Trait name to adjust behavior. For example, "bravery", "cautiousness", "friendliness".'),
58-
})).nullable().describe('Personas can be used to adjust the behavior of sub-agents. For example, when using as NPC in games, or player in Minecraft, the persona can help define the character\'s traits and decision-making style.'),
58+
}).strict()).nullable().describe('Personas can be used to adjust the behavior of sub-agents. For example, when using as NPC in games, or player in Minecraft, the persona can help define the character\'s traits and decision-making style.'),
5959
options: z.array(z.object({
6060
label: z.string().describe('Short and brief label for this option, used for identification, should be within a sentence.'),
6161
steps: z.array(z.string()).describe('Step-by-step instructions for the sub-agent to follow, useful when providing detailed guidance.'),
@@ -65,10 +65,10 @@ export const sparkCommandSchema = z.object({
6565
fallback: z.array(z.string()).nullable().describe('Fallback steps if the main steps cannot be completed.'),
6666
// TODO: consider to remove or enrich how triggers should work later
6767
triggers: z.array(z.string()).nullable().describe('Conditions or events that would trigger this option.'),
68-
})),
69-
}).nullable().describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'),
70-
})).describe('List of commands to issue to sub-agents, you may produce multiple commands in response to multiple sub-agents by specifying their IDs in destination field. Empty array can be used for zero commands.'),
71-
})
68+
}).strict()),
69+
}).strict().nullable().describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'),
70+
}).strict()).describe('List of commands to issue to sub-agents, you may produce multiple commands in response to multiple sub-agents by specifying their IDs in destination field. Empty array can be used for zero commands.'),
71+
}).strict()
7272

7373
export type SparkCommandSchema = z.infer<typeof sparkCommandSchema>
7474

@@ -97,7 +97,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
9797
const sparkNoResponseTool = await tool({
9898
name: 'builtIn_sparkNoResponse',
9999
description: `Indicate that no response or action is needed for the current spark:notify event.`,
100-
parameters: z.object({}),
100+
parameters: z.object({}).strict(),
101101
execute: async (_payload) => {
102102
noResponse = true
103103
return 'AIRI System: Acknowledged, no response or action will be processed.'
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { JsonSchema } from 'xsschema'
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { mcp } from './mcp'
6+
7+
describe('tools mcp schema', () => {
8+
it('emits strict parameter objects', async () => {
9+
const tools = await mcp()
10+
const toolNames = [
11+
'mcp_list_tools',
12+
'mcp_connect_server',
13+
'mcp_disconnect_server',
14+
'mcp_call_tool',
15+
]
16+
17+
for (const name of toolNames) {
18+
const tool = tools.find(entry => entry.function.name === name)
19+
expect(tool, `missing tool: ${name}`).toBeDefined()
20+
expect(tool?.function.parameters.additionalProperties).toBe(false)
21+
}
22+
})
23+
24+
it('keeps mcp_call_tool parameters items strict', async () => {
25+
const tools = await mcp()
26+
const callTool = tools.find(entry => entry.function.name === 'mcp_call_tool')
27+
28+
expect(callTool).toBeDefined()
29+
const items = ((callTool?.function.parameters as JsonSchema).properties?.parameters as any)?.items
30+
expect(items).toBeDefined()
31+
expect(items?.additionalProperties).toBe(false)
32+
})
33+
})

packages/stage-ui/src/tools/mcp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const tools = [
99
execute: async (_, __) => {
1010
return await listTools()
1111
},
12-
parameters: z.object({}),
12+
parameters: z.object({}).strict(),
1313
}),
1414
tool({
1515
name: 'mcp_connect_server',
@@ -21,7 +21,7 @@ const tools = [
2121
parameters: z.object({
2222
command: z.string().describe('The command to connect to the MCP server'),
2323
args: z.array(z.string()).describe('The arguments to pass to the MCP server'),
24-
}),
24+
}).strict(),
2525
}),
2626
tool({
2727
name: 'mcp_disconnect_server',
@@ -30,7 +30,7 @@ const tools = [
3030
await disconnectServer()
3131
return 'success'
3232
},
33-
parameters: z.object({}),
33+
parameters: z.object({}).strict(),
3434
}),
3535
tool({
3636
name: 'mcp_call_tool',
@@ -50,8 +50,8 @@ const tools = [
5050
name: z.string().describe('The name of the tool to call'),
5151
parameters: z.array(z.object({
5252
name: z.string().describe('The name of the parameter'),
53-
value: z.union([z.string(), z.number(), z.boolean(), z.object({})]).describe('The value of the parameter, it can be a string, a number, a boolean, or an object'),
54-
})).describe('The parameters to pass to the tool'),
53+
value: z.union([z.string(), z.number(), z.boolean(), z.object({}).strict()]).describe('The value of the parameter, it can be a string, a number, a boolean, or an object'),
54+
}).strict()).describe('The parameters to pass to the tool'),
5555
}),
5656
}),
5757
]

0 commit comments

Comments
 (0)