refactor(stage-ui,stage-tamagotchi): have plugin-tools, mcp-tools to register directly to llm.ts
This commit is contained in:
@@ -9,7 +9,6 @@ import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/charac
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { usePluginHostInspectorStore } from '@proj-airi/stage-ui/stores/devtools/plugin-host-debug'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { clearMcpToolBridge, setMcpToolBridge } from '@proj-airi/stage-ui/stores/mcp-tool-bridge'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
@@ -27,12 +26,11 @@ import ResizeHandler from './components/ResizeHandler.vue'
|
||||
|
||||
import {
|
||||
electronGetServerChannelConfig,
|
||||
electronMcpCallTool,
|
||||
electronMcpListTools,
|
||||
electronPluginInspect,
|
||||
electronPluginList,
|
||||
electronPluginLoad,
|
||||
electronPluginLoadEnabled,
|
||||
electronPluginSetAutoReload,
|
||||
electronPluginSetEnabled,
|
||||
electronPluginUnload,
|
||||
electronPluginUpdateCapability,
|
||||
@@ -44,6 +42,8 @@ import {
|
||||
} from '../shared/eventa'
|
||||
import { initializeElectronAuthCallbackBridge } from './bridges/electron-auth-callback'
|
||||
import { initializeStageThreeRuntimeTraceBridge } from './bridges/stage-three-runtime-trace'
|
||||
import { useTamagotchiMcpToolsStore } from './stores/mcp-tools'
|
||||
import { useTamagotchiPluginToolsStore } from './stores/plugin-tools'
|
||||
import { useServerChannelSettingsStore } from './stores/settings/server-channel'
|
||||
import { useStageWindowLifecycleStore } from './stores/stage-window-lifecycle'
|
||||
|
||||
@@ -63,6 +63,8 @@ const characterOrchestratorStore = useCharacterOrchestratorStore()
|
||||
const analyticsStore = useSharedAnalyticsStore()
|
||||
const inferencePreload = useInferencePreload()
|
||||
const pluginHostInspectorStore = usePluginHostInspectorStore()
|
||||
const mcpToolsStore = useTamagotchiMcpToolsStore()
|
||||
const pluginToolsStore = useTamagotchiPluginToolsStore()
|
||||
const stageWindowLifecycleStore = useStageWindowLifecycleStore()
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const context = useElectronEventaContext()
|
||||
@@ -73,33 +75,63 @@ void stageWindowLifecycleStore.initializeWindowLifecycleBridge()
|
||||
const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig)
|
||||
const listPlugins = useElectronEventaInvoke(electronPluginList)
|
||||
const setPluginEnabled = useElectronEventaInvoke(electronPluginSetEnabled)
|
||||
const setPluginAutoReload = useElectronEventaInvoke(electronPluginSetAutoReload)
|
||||
const loadEnabledPlugins = useElectronEventaInvoke(electronPluginLoadEnabled)
|
||||
const loadPlugin = useElectronEventaInvoke(electronPluginLoad)
|
||||
const unloadPlugin = useElectronEventaInvoke(electronPluginUnload)
|
||||
const inspectPluginHost = useElectronEventaInvoke(electronPluginInspect)
|
||||
const startTrackingCursorPoint = useElectronEventaInvoke(electronStartTrackMousePosition)
|
||||
const reportPluginCapability = useElectronEventaInvoke(electronPluginUpdateCapability)
|
||||
const listMcpTools = useElectronEventaInvoke(electronMcpListTools)
|
||||
const callMcpTool = useElectronEventaInvoke(electronMcpCallTool)
|
||||
const setLocale = useElectronEventaInvoke(i18nSetLocale)
|
||||
const isChatWindowRoute = () => route.path === '/chat'
|
||||
const isWidgetsWindowRoute = () => route.path === '/widgets'
|
||||
|
||||
async function refreshPluginRuntimeTools() {
|
||||
try {
|
||||
await pluginToolsStore.refresh()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('[App] Failed to refresh plugin runtime tools:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.path, () => {
|
||||
contextBridgeStore.setSparkNotifyHostRole(isWidgetsWindowRoute() ? 'client' : 'main')
|
||||
}, { immediate: true })
|
||||
|
||||
// NOTICE: register plugin host bridge during setup to avoid race with pages using it in immediate watchers.
|
||||
pluginHostInspectorStore.setBridge({
|
||||
list: () => listPlugins(),
|
||||
setEnabled: payload => setPluginEnabled(payload),
|
||||
loadEnabled: () => loadEnabledPlugins(),
|
||||
load: payload => loadPlugin(payload),
|
||||
unload: payload => unloadPlugin(payload),
|
||||
setEnabled: async (payload) => {
|
||||
const result = await setPluginEnabled(payload)
|
||||
await refreshPluginRuntimeTools()
|
||||
return result
|
||||
},
|
||||
setAutoReload: payload => setPluginAutoReload(payload),
|
||||
loadEnabled: async () => {
|
||||
const result = await loadEnabledPlugins()
|
||||
await refreshPluginRuntimeTools()
|
||||
return result
|
||||
},
|
||||
load: async (payload) => {
|
||||
const result = await loadPlugin(payload)
|
||||
await refreshPluginRuntimeTools()
|
||||
return result
|
||||
},
|
||||
unload: async (payload) => {
|
||||
const result = await unloadPlugin(payload)
|
||||
await refreshPluginRuntimeTools()
|
||||
return result
|
||||
},
|
||||
inspect: () => inspectPluginHost(),
|
||||
})
|
||||
|
||||
// NOTICE: MCP tools are declared from stage-ui and executed during model streaming.
|
||||
// Register runtime bridge during setup to avoid missing bridge in early tool invocations.
|
||||
setMcpToolBridge({
|
||||
listTools: () => listMcpTools(),
|
||||
callTool: payload => callMcpTool(payload),
|
||||
// NOTICE: Runtime tool stores must register during setup so renderer consumers can see them
|
||||
// before `onMounted()` finishes the rest of the startup flow.
|
||||
void mcpToolsStore.refresh().catch((error) => {
|
||||
console.warn('[App] Failed to refresh MCP runtime tools:', error)
|
||||
})
|
||||
void refreshPluginRuntimeTools()
|
||||
|
||||
watch(language, () => {
|
||||
i18n.locale.value = language.value
|
||||
@@ -143,8 +175,10 @@ onMounted(async () => {
|
||||
}).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
|
||||
if (!isChatWindowRoute()) {
|
||||
contextBridgeStore.initialize()
|
||||
characterOrchestratorStore.initialize()
|
||||
await startTrackingCursorPoint()
|
||||
if (!isWidgetsWindowRoute()) {
|
||||
characterOrchestratorStore.initialize()
|
||||
await startTrackingCursorPoint()
|
||||
}
|
||||
}
|
||||
|
||||
// Expose stage provider definitions to plugin host APIs.
|
||||
@@ -176,7 +210,8 @@ onUnmounted(() => {
|
||||
if (!isChatWindowRoute()) {
|
||||
contextBridgeStore.dispose()
|
||||
}
|
||||
clearMcpToolBridge()
|
||||
mcpToolsStore.dispose()
|
||||
pluginToolsStore.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const invokeMocks = vi.hoisted(() => ({
|
||||
callMcpTool: vi.fn(async () => ({
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
isError: false,
|
||||
})),
|
||||
listMcpTools: vi.fn(async () => [{
|
||||
serverName: 'filesystem',
|
||||
name: 'filesystem::search',
|
||||
toolName: 'search',
|
||||
description: 'Search files.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
}]),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/electron-vueuse', () => ({
|
||||
useElectronEventaInvoke: (event: { receiveEvent?: { id?: string } }) => {
|
||||
if (event?.receiveEvent?.id === 'eventa:invoke:electron:mcp:list-tools-receive')
|
||||
return invokeMocks.listMcpTools
|
||||
if (event?.receiveEvent?.id === 'eventa:invoke:electron:mcp:call-tool-receive')
|
||||
return invokeMocks.callMcpTool
|
||||
|
||||
throw new Error(`Unexpected eventa invoke: ${JSON.stringify(event)}`)
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useTamagotchiMcpToolsStore', async () => {
|
||||
const { useTamagotchiMcpToolsStore } = await import('./mcp-tools')
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
invokeMocks.listMcpTools.mockClear()
|
||||
invokeMocks.callMcpTool.mockClear()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* await store.refresh()
|
||||
* expect(llmToolsStore.toolsByProvider.mcp).toHaveLength(2)
|
||||
*/
|
||||
it('loads MCP tools, proxies execution, and clears them from the shared llm-tools store', async () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const store = useTamagotchiMcpToolsStore()
|
||||
const toolOptions = {} as Parameters<Tool['execute']>[1]
|
||||
|
||||
await store.refresh()
|
||||
|
||||
const mcpTools = llmToolsStore.toolsByProvider.mcp
|
||||
const listTools = mcpTools?.find(tool => tool.function.name === 'builtIn_mcpListTools')
|
||||
const callTool = mcpTools?.find(tool => tool.function.name === 'builtIn_mcpCallTool')
|
||||
|
||||
expect(mcpTools).toEqual([
|
||||
expect.objectContaining({ function: expect.objectContaining({ name: 'builtIn_mcpListTools' }) }),
|
||||
expect.objectContaining({ function: expect.objectContaining({ name: 'builtIn_mcpCallTool' }) }),
|
||||
])
|
||||
|
||||
const listResult = await listTools?.execute({}, toolOptions)
|
||||
const callResult = await callTool?.execute({
|
||||
name: 'filesystem::search',
|
||||
arguments: JSON.stringify({ query: 'hello', limit: 10 }),
|
||||
}, toolOptions)
|
||||
|
||||
expect(invokeMocks.listMcpTools).toHaveBeenCalledTimes(1)
|
||||
expect(invokeMocks.callMcpTool).toHaveBeenCalledWith({
|
||||
name: 'filesystem::search',
|
||||
arguments: { query: 'hello', limit: 10 },
|
||||
})
|
||||
expect(listResult).toEqual([{
|
||||
serverName: 'filesystem',
|
||||
name: 'filesystem::search',
|
||||
toolName: 'search',
|
||||
description: 'Search files.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
}])
|
||||
expect(callResult).toEqual({
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
isError: false,
|
||||
})
|
||||
|
||||
store.dispose()
|
||||
|
||||
expect(llmToolsStore.toolsByProvider.mcp).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { createMcpTools } from '@proj-airi/stage-ui/tools/mcp'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { electronMcpCallTool, electronMcpListTools } from '../../shared/eventa'
|
||||
|
||||
/**
|
||||
* Registers Electron-backed MCP tools into the shared LLM tools store.
|
||||
*
|
||||
* Use when:
|
||||
* - The Tamagotchi renderer needs live MCP tools during chat streaming
|
||||
*
|
||||
* Expects:
|
||||
* - Electron Eventa handlers for MCP listing and invocation are available
|
||||
*
|
||||
* Returns:
|
||||
* - Store actions for refreshing and disposing MCP runtime tools
|
||||
*/
|
||||
export const useTamagotchiMcpToolsStore = defineStore('tamagotchi-mcp-tools', () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const listMcpTools = useElectronEventaInvoke(electronMcpListTools)
|
||||
const callMcpTool = useElectronEventaInvoke(electronMcpCallTool)
|
||||
|
||||
async function refresh() {
|
||||
return llmToolsStore.registerTools('mcp', Promise.all(createMcpTools({
|
||||
listTools: () => listMcpTools(),
|
||||
callTool: payload => callMcpTool(payload),
|
||||
})))
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
llmToolsStore.clearTools('mcp')
|
||||
}
|
||||
|
||||
return {
|
||||
dispose,
|
||||
refresh,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const invokeMocks = vi.hoisted(() => ({
|
||||
invokePluginTool: vi.fn(async (payload: unknown) => payload),
|
||||
listPluginXsaiTools: vi.fn(async () => [
|
||||
{
|
||||
ownerPluginId: 'plugin-chess',
|
||||
name: 'play_chess',
|
||||
description: 'Play a chess move.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
]),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/electron-vueuse', () => ({
|
||||
useElectronEventaInvoke: (event: { receiveEvent?: { id?: string } }) => {
|
||||
if (event?.receiveEvent?.id === 'eventa:invoke:electron:plugins:tools:list-xsai-receive')
|
||||
return invokeMocks.listPluginXsaiTools
|
||||
if (event?.receiveEvent?.id === 'eventa:invoke:electron:plugins:tools:invoke-receive')
|
||||
return invokeMocks.invokePluginTool
|
||||
|
||||
throw new Error(`Unexpected eventa invoke: ${JSON.stringify(event)}`)
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useTamagotchiPluginToolsStore', async () => {
|
||||
const { useTamagotchiPluginToolsStore } = await import('./plugin-tools')
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
invokeMocks.listPluginXsaiTools.mockClear()
|
||||
invokeMocks.invokePluginTool.mockClear()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* await store.refresh()
|
||||
* expect(llmToolsStore.toolsByProvider['plugin-tools']).toHaveLength(1)
|
||||
*/
|
||||
it('loads plugin xsai tools, proxies execution, and clears them from the shared llm-tools store', async () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const store = useTamagotchiPluginToolsStore()
|
||||
const toolOptions = {} as Parameters<Tool['execute']>[1]
|
||||
|
||||
await store.refresh()
|
||||
|
||||
const pluginTools = llmToolsStore.toolsByProvider['plugin-tools']
|
||||
const playChessTool = pluginTools?.find(tool => tool.function.name === 'play_chess')
|
||||
|
||||
expect(pluginTools).toEqual([
|
||||
expect.objectContaining({ function: expect.objectContaining({ name: 'play_chess' }) }),
|
||||
])
|
||||
|
||||
const executionResult = await playChessTool?.execute({
|
||||
move: 'e2e4',
|
||||
}, toolOptions)
|
||||
|
||||
expect(invokeMocks.invokePluginTool).toHaveBeenCalledWith({
|
||||
ownerPluginId: 'plugin-chess',
|
||||
name: 'play_chess',
|
||||
input: {
|
||||
move: 'e2e4',
|
||||
},
|
||||
})
|
||||
expect(executionResult).toEqual({
|
||||
ownerPluginId: 'plugin-chess',
|
||||
name: 'play_chess',
|
||||
input: {
|
||||
move: 'e2e4',
|
||||
},
|
||||
})
|
||||
|
||||
store.dispose()
|
||||
|
||||
expect(llmToolsStore.toolsByProvider['plugin-tools']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { rawTool } from '@xsai/tool'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { electronPluginInvokeTool, electronPluginListXsaiTools } from '../../shared/eventa'
|
||||
|
||||
/**
|
||||
* Registers Electron-backed plugin xsai tools into the shared LLM tools store.
|
||||
*
|
||||
* Use when:
|
||||
* - The Tamagotchi renderer needs plugin-provided xsai tools during chat streaming
|
||||
*
|
||||
* Expects:
|
||||
* - Electron Eventa handlers for listing and invoking plugin tools are available
|
||||
*
|
||||
* Returns:
|
||||
* - Store actions for refreshing and disposing plugin runtime tools
|
||||
*/
|
||||
export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tools', () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const listPluginXsaiToolDefinitions = useElectronEventaInvoke(electronPluginListXsaiTools)
|
||||
const invokePluginTool = useElectronEventaInvoke(electronPluginInvokeTool)
|
||||
|
||||
async function refresh() {
|
||||
return llmToolsStore.registerTools('plugin-tools', listPluginXsaiToolDefinitions().then(definitions =>
|
||||
definitions.map(definition =>
|
||||
rawTool({
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
parameters: definition.parameters,
|
||||
execute: async input => invokePluginTool({
|
||||
ownerPluginId: definition.ownerPluginId,
|
||||
name: definition.name,
|
||||
input,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
llmToolsStore.clearTools('plugin-tools')
|
||||
}
|
||||
|
||||
return {
|
||||
dispose,
|
||||
refresh,
|
||||
}
|
||||
})
|
||||
@@ -112,6 +112,7 @@ words:
|
||||
- formkit
|
||||
- frontmatter
|
||||
- gamelet
|
||||
- gamelets
|
||||
- Genshin
|
||||
- giteeai
|
||||
- gltf
|
||||
|
||||
@@ -105,7 +105,9 @@ export async function streamFrom({
|
||||
headers: options?.headers,
|
||||
stopWhen: stepCountAtLeast(10),
|
||||
tools,
|
||||
captureToolErrors: true,
|
||||
// NOTICE: Some OpenAI-compatible gateways reject the wire-level
|
||||
// `capture_tool_errors` parameter with 400 unknown_parameter.
|
||||
// Keep this unset here so the request remains provider-compatible.
|
||||
onEvent,
|
||||
})
|
||||
|
||||
|
||||
@@ -29,11 +29,13 @@
|
||||
"./libs/inference": "./src/libs/inference/index.ts",
|
||||
"./libs/*": "./src/libs/*.ts",
|
||||
"./libs": "./src/libs/index.ts",
|
||||
"./tools/mcp": "./src/tools/mcp.ts",
|
||||
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
|
||||
"./stores/analytics": "./src/stores/analytics/index.ts",
|
||||
"./stores/analytics/posthog": "./src/stores/analytics/posthog.ts",
|
||||
"./stores/analytics/privacy-policy": "./src/stores/analytics/privacy-policy.ts",
|
||||
"./stores/character": "./src/stores/character/index.ts",
|
||||
"./stores/character/orchestrator/spark-notify-agent": "./src/stores/character/orchestrator/spark-notify-agent.ts",
|
||||
"./stores/settings/analytics": "./src/stores/settings/analytics.ts",
|
||||
"./stores/settings": "./src/stores/settings/index.ts",
|
||||
"./stores/modules/vision": "./src/stores/modules/vision/index.ts",
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useLlmToolsStore } from './llm-tools'
|
||||
|
||||
describe('useLlmToolsStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('registers and merges tools by provider', () => {
|
||||
const store = useLlmToolsStore()
|
||||
const mcpTool = { function: { name: 'builtIn_mcpListTools' } } as Tool
|
||||
const pluginTool = { function: { name: 'play_chess' } } as Tool
|
||||
|
||||
store.registerTools('mcp', [mcpTool])
|
||||
store.registerTools('plugin-tools', [pluginTool])
|
||||
|
||||
expect(store.toolsByProvider).toEqual({
|
||||
'mcp': [mcpTool],
|
||||
'plugin-tools': [pluginTool],
|
||||
})
|
||||
expect(store.activeTools).toEqual([mcpTool, pluginTool])
|
||||
})
|
||||
|
||||
it('replaces tools for the same provider instead of appending forever', () => {
|
||||
const store = useLlmToolsStore()
|
||||
const first = { function: { name: 'first' } } as Tool
|
||||
const second = { function: { name: 'second' } } as Tool
|
||||
|
||||
store.registerTools('plugin-tools', [first])
|
||||
store.registerTools('plugin-tools', [second])
|
||||
|
||||
expect(store.toolsByProvider).toEqual({
|
||||
'plugin-tools': [second],
|
||||
})
|
||||
expect(store.activeTools).toEqual([second])
|
||||
})
|
||||
|
||||
it('clears one provider without touching the others', () => {
|
||||
const store = useLlmToolsStore()
|
||||
const mcpTool = { function: { name: 'builtIn_mcpListTools' } } as Tool
|
||||
const pluginTool = { function: { name: 'play_chess' } } as Tool
|
||||
|
||||
store.registerTools('mcp', [mcpTool])
|
||||
store.registerTools('plugin-tools', [pluginTool])
|
||||
store.clearTools('plugin-tools')
|
||||
|
||||
expect(store.toolsByProvider).toEqual({
|
||||
mcp: [mcpTool],
|
||||
})
|
||||
expect(store.activeTools).toEqual([mcpTool])
|
||||
})
|
||||
|
||||
it('does not change store state when the caller mutates the registered array later', () => {
|
||||
const store = useLlmToolsStore()
|
||||
const first = { function: { name: 'first' } } as Tool
|
||||
const second = { function: { name: 'second' } } as Tool
|
||||
const tools = [first]
|
||||
|
||||
store.registerTools('plugin-tools', tools)
|
||||
tools.push(second)
|
||||
|
||||
expect(store.toolsByProvider).toEqual({
|
||||
'plugin-tools': [first],
|
||||
})
|
||||
expect(store.activeTools).toEqual([first])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* store.registerTools('plugin-tools', Promise.resolve([pluginTool]))
|
||||
* await store.awaitPendingRegistrations()
|
||||
*/
|
||||
it('waits for async tool registrations before exposing them as active tools', async () => {
|
||||
const store = useLlmToolsStore()
|
||||
const pluginTool = { function: { name: 'play_chess' } } as Tool
|
||||
let resolveTools: ((tools: Tool[]) => void) | undefined
|
||||
const pendingTools = new Promise<Tool[]>((resolve) => {
|
||||
resolveTools = resolve
|
||||
})
|
||||
const onSettled = vi.fn()
|
||||
|
||||
store.registerTools('plugin-tools', pendingTools)
|
||||
const pendingWait = store.awaitPendingRegistrations().then(() => {
|
||||
onSettled()
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
|
||||
expect(store.toolsByProvider['plugin-tools']).toBeUndefined()
|
||||
expect(store.activeTools).toEqual([])
|
||||
expect(onSettled).not.toHaveBeenCalled()
|
||||
|
||||
resolveTools?.([pluginTool])
|
||||
await pendingWait
|
||||
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
expect(store.toolsByProvider['plugin-tools']).toEqual([pluginTool])
|
||||
expect(store.activeTools).toEqual([pluginTool])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* store.registerTools('plugin-tools', slowTools)
|
||||
* store.registerTools('plugin-tools', [latestTool])
|
||||
*/
|
||||
it('ignores stale async registrations after newer tools replace the same provider', async () => {
|
||||
const store = useLlmToolsStore()
|
||||
const staleTool = { function: { name: 'stale' } } as Tool
|
||||
const latestTool = { function: { name: 'latest' } } as Tool
|
||||
let resolveTools: ((tools: Tool[]) => void) | undefined
|
||||
const pendingTools = new Promise<Tool[]>((resolve) => {
|
||||
resolveTools = resolve
|
||||
})
|
||||
|
||||
store.registerTools('plugin-tools', pendingTools)
|
||||
store.registerTools('plugin-tools', [latestTool])
|
||||
resolveTools?.([staleTool])
|
||||
|
||||
await store.awaitPendingRegistrations()
|
||||
|
||||
expect(store.toolsByProvider['plugin-tools']).toEqual([latestTool])
|
||||
expect(store.activeTools).toEqual([latestTool])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
type ToolRegistration = Promise<Tool[]> | Tool[]
|
||||
|
||||
/**
|
||||
* Stores runtime-registered xsai tools keyed by provider.
|
||||
*
|
||||
* Use when:
|
||||
* - App runtimes need to publish additional LLM tools into shared stage-ui logic
|
||||
*
|
||||
* Expects:
|
||||
* - Provider names are stable identifiers such as `mcp` or `plugin-tools`
|
||||
*
|
||||
* Returns:
|
||||
* - A merged reactive list of all currently registered tools
|
||||
*/
|
||||
export const useLlmToolsStore = defineStore('llm-tools', () => {
|
||||
const toolsByProvider = ref<Record<string, Tool[]>>({})
|
||||
const providerRegistrationTokens = new Map<string, symbol>()
|
||||
const pendingRegistrations = new Map<string, Promise<void>>()
|
||||
|
||||
function assignTools(provider: string, tools: Tool[]) {
|
||||
toolsByProvider.value = {
|
||||
...toolsByProvider.value,
|
||||
[provider]: [...tools],
|
||||
}
|
||||
}
|
||||
|
||||
function registerTools(provider: string, tools: ToolRegistration) {
|
||||
const registrationToken = Symbol(provider)
|
||||
providerRegistrationTokens.set(provider, registrationToken)
|
||||
|
||||
if (Array.isArray(tools)) {
|
||||
pendingRegistrations.delete(provider)
|
||||
assignTools(provider, tools)
|
||||
return Promise.resolve([...tools])
|
||||
}
|
||||
|
||||
const registration = Promise.resolve(tools)
|
||||
.then((resolvedTools) => {
|
||||
if (providerRegistrationTokens.get(provider) !== registrationToken)
|
||||
return resolvedTools
|
||||
|
||||
assignTools(provider, resolvedTools)
|
||||
return resolvedTools
|
||||
})
|
||||
.finally(() => {
|
||||
if (providerRegistrationTokens.get(provider) === registrationToken)
|
||||
pendingRegistrations.delete(provider)
|
||||
})
|
||||
|
||||
pendingRegistrations.set(provider, registration.then(() => undefined, () => undefined))
|
||||
return registration
|
||||
}
|
||||
|
||||
function clearTools(provider: string) {
|
||||
providerRegistrationTokens.set(provider, Symbol(provider))
|
||||
pendingRegistrations.delete(provider)
|
||||
const { [provider]: _removed, ...remaining } = toolsByProvider.value
|
||||
toolsByProvider.value = remaining
|
||||
}
|
||||
|
||||
async function awaitPendingRegistrations() {
|
||||
while (pendingRegistrations.size > 0)
|
||||
await Promise.all(pendingRegistrations.values())
|
||||
}
|
||||
|
||||
const activeTools = computed(() => Object.values(toolsByProvider.value).flat())
|
||||
|
||||
// TODO: Track provider support/loading/error state if runtime diagnostics need it later.
|
||||
return {
|
||||
activeTools,
|
||||
awaitPendingRegistrations,
|
||||
clearTools,
|
||||
registerTools,
|
||||
toolsByProvider,
|
||||
}
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
import type { Message } from '@xsai/shared-chat'
|
||||
import type { Message, Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isToolRelatedError, useLLM } from './llm'
|
||||
import { useLlmToolsStore } from './llm-tools'
|
||||
|
||||
const {
|
||||
streamTextMock,
|
||||
@@ -13,9 +14,9 @@ const {
|
||||
createSparkCommandToolMock,
|
||||
} = vi.hoisted(() => ({
|
||||
streamTextMock: vi.fn(),
|
||||
mcpMock: vi.fn(async () => []),
|
||||
debugMock: vi.fn(async () => []),
|
||||
createSparkCommandToolMock: vi.fn(async () => ({
|
||||
mcpMock: vi.fn(async (): Promise<Tool[]> => []),
|
||||
debugMock: vi.fn(async (): Promise<Tool[]> => []),
|
||||
createSparkCommandToolMock: vi.fn(async (): Promise<unknown> => ({
|
||||
name: 'spark',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -56,6 +57,20 @@ function createMockStreamResult() {
|
||||
}
|
||||
}
|
||||
|
||||
function toolNameFrom(tool: unknown) {
|
||||
if (typeof tool !== 'object' || tool === null)
|
||||
return undefined
|
||||
|
||||
const candidate = tool as {
|
||||
name?: string
|
||||
function?: {
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
|
||||
return candidate.function?.name ?? candidate.name
|
||||
}
|
||||
|
||||
describe('isToolRelatedError', () => {
|
||||
beforeEach(() => {
|
||||
streamTextMock.mockReset()
|
||||
@@ -158,7 +173,18 @@ describe('isToolRelatedError', () => {
|
||||
|
||||
it('keeps builtin tools and auto-disables tools after tool-related errors', async () => {
|
||||
const store = useLLM()
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const customTool = { name: 'custom-tool' } as any
|
||||
const runtimeTool = {
|
||||
function: {
|
||||
name: 'runtime_play_chess_match',
|
||||
description: 'Start a runtime chess match.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
}
|
||||
|
||||
llmToolsStore.registerTools('plugin-tools', [runtimeTool as any])
|
||||
|
||||
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
|
||||
queueMicrotask(async () => {
|
||||
@@ -176,6 +202,7 @@ describe('isToolRelatedError', () => {
|
||||
expect(mcpMock).toHaveBeenCalledTimes(1)
|
||||
expect(debugMock).toHaveBeenCalledTimes(1)
|
||||
expect(firstCallTools).toContain(customTool)
|
||||
expect(firstCallTools?.map(toolNameFrom)).toContain('runtime_play_chess_match')
|
||||
|
||||
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
|
||||
queueMicrotask(async () => {
|
||||
@@ -191,4 +218,126 @@ describe('isToolRelatedError', () => {
|
||||
const secondCallTools = streamTextMock.mock.calls[1]?.[0]?.tools
|
||||
expect(secondCallTools).toBeUndefined()
|
||||
})
|
||||
|
||||
it('merges runtime-registered tools from the llm-tools store into the builtin tool resolver', async () => {
|
||||
const store = useLLM()
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const playChessTool = {
|
||||
function: {
|
||||
name: 'runtime_open_chess_board',
|
||||
description: 'Open the runtime chess board.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
}
|
||||
const runtimeMcpStatusTool = {
|
||||
function: {
|
||||
name: 'runtime_sync_mcp_status',
|
||||
description: 'Sync runtime MCP status.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
}
|
||||
|
||||
llmToolsStore.registerTools('mcp', [runtimeMcpStatusTool as any])
|
||||
llmToolsStore.registerTools('plugin-tools', [playChessTool as any])
|
||||
|
||||
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
|
||||
queueMicrotask(async () => {
|
||||
await options.onEvent({ type: 'finish', finishReason: 'stop' })
|
||||
})
|
||||
return createMockStreamResult()
|
||||
})
|
||||
|
||||
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
|
||||
|
||||
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools
|
||||
expect(mergedTools).toEqual(expect.arrayContaining([runtimeMcpStatusTool, playChessTool]))
|
||||
})
|
||||
|
||||
it('prefers runtime-registered tools when duplicate tool names collide with builtin tools', async () => {
|
||||
const store = useLLM()
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const builtinTool = {
|
||||
function: {
|
||||
name: 'duplicate_runtime_tool',
|
||||
description: 'Builtin version.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
} as unknown as Tool
|
||||
const runtimeTool = {
|
||||
function: {
|
||||
name: 'duplicate_runtime_tool',
|
||||
description: 'Runtime version.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
}
|
||||
|
||||
mcpMock.mockResolvedValueOnce([builtinTool] as Tool[])
|
||||
llmToolsStore.registerTools('plugin-tools', [runtimeTool as any])
|
||||
|
||||
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
|
||||
queueMicrotask(async () => {
|
||||
await options.onEvent({ type: 'finish', finishReason: 'stop' })
|
||||
})
|
||||
return createMockStreamResult()
|
||||
})
|
||||
|
||||
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
|
||||
|
||||
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools as Array<{ function?: { name?: string } }>
|
||||
const duplicateNameTools = mergedTools.filter(tool => tool.function?.name === 'duplicate_runtime_tool')
|
||||
|
||||
expect(duplicateNameTools).toHaveLength(1)
|
||||
expect(duplicateNameTools[0]).toMatchObject({
|
||||
function: {
|
||||
name: 'duplicate_runtime_tool',
|
||||
description: 'Runtime version.',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* llmToolsStore.registerTools('plugin-tools', pendingRuntimeTools)
|
||||
* await store.stream('model-a', provider, messages)
|
||||
*/
|
||||
it('waits for pending runtime tool registrations before building stream tools', async () => {
|
||||
const store = useLLM()
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const runtimeTool = {
|
||||
function: {
|
||||
name: 'runtime_pending_tool',
|
||||
description: 'Pending runtime tool.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
execute: vi.fn(),
|
||||
}
|
||||
let resolveTools: ((tools: unknown[]) => void) | undefined
|
||||
const pendingTools = new Promise<unknown[]>((resolve) => {
|
||||
resolveTools = resolve
|
||||
})
|
||||
|
||||
llmToolsStore.registerTools('plugin-tools', pendingTools as Promise<any[]>)
|
||||
|
||||
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
|
||||
queueMicrotask(async () => {
|
||||
await options.onEvent({ type: 'finish', finishReason: 'stop' })
|
||||
})
|
||||
return createMockStreamResult()
|
||||
})
|
||||
|
||||
const pendingStream = store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
|
||||
await Promise.resolve()
|
||||
|
||||
expect(streamTextMock).not.toHaveBeenCalled()
|
||||
|
||||
resolveTools?.([runtimeTool])
|
||||
await pendingStream
|
||||
|
||||
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools
|
||||
expect(mergedTools?.map(toolNameFrom)).toContain('runtime_pending_tool')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import type { StreamOptions } from '@proj-airi/core-agent'
|
||||
import type { WebSocketEvents } from '@proj-airi/server-sdk'
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
import type { Message } from '@xsai/shared-chat'
|
||||
import type { Message, Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { streamFrom as coreStreamFrom, isToolRelatedError, modelKey } from '@proj-airi/core-agent'
|
||||
import { listModels } from '@xsai/model'
|
||||
import { uniqBy } from 'es-toolkit'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { createSparkCommandTool, debug, mcp } from '../tools'
|
||||
import { useLlmToolsStore } from './llm-tools'
|
||||
import { useModsServerChannelStore } from './mods/api/channel-server'
|
||||
|
||||
export type { StreamEvent, StreamOptions } from '@proj-airi/core-agent'
|
||||
export { isToolRelatedError } from '@proj-airi/core-agent'
|
||||
|
||||
function toolNameFrom(tool: Tool) {
|
||||
const candidate = tool as Tool & {
|
||||
name?: string
|
||||
function?: {
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
|
||||
return candidate.function?.name ?? candidate.name
|
||||
}
|
||||
|
||||
export const useLLM = defineStore('llm', () => {
|
||||
const toolsCompatibility = ref<Map<string, boolean>>(new Map())
|
||||
const modsServerChannelStore = useModsServerChannelStore()
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
|
||||
async function stream(model: string, chatProvider: ChatProvider, messages: Message[], options?: StreamOptions) {
|
||||
const key = modelKey(model, chatProvider)
|
||||
@@ -42,11 +56,19 @@ export const useLLM = defineStore('llm', () => {
|
||||
chatProvider,
|
||||
messages,
|
||||
options: { ...options, toolsCompatibility: toolsCompatibility.value },
|
||||
builtinToolsResolver: async () => [
|
||||
...await mcp(),
|
||||
...await debug(),
|
||||
await createSparkCommandTool({ sendSparkCommand }),
|
||||
],
|
||||
builtinToolsResolver: async () => {
|
||||
await llmToolsStore.awaitPendingRegistrations()
|
||||
|
||||
// Reverse twice so later runtime registrations win while original tool order stays stable.
|
||||
return uniqBy(
|
||||
[
|
||||
...await mcp(),
|
||||
...await debug(),
|
||||
...await createSparkCommandTool({ sendSparkCommand }),
|
||||
].toReversed(),
|
||||
tool => toolNameFrom(tool) ?? tool,
|
||||
).toReversed()
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
export interface McpToolDescriptor {
|
||||
serverName: string
|
||||
name: string
|
||||
toolName: string
|
||||
description?: string
|
||||
inputSchema: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface McpCallToolPayload {
|
||||
name: string
|
||||
arguments?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface McpCallToolResult {
|
||||
content?: Array<Record<string, unknown>>
|
||||
structuredContent?: Record<string, unknown>
|
||||
toolResult?: unknown
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
interface McpToolBridge {
|
||||
listTools: () => Promise<McpToolDescriptor[]>
|
||||
callTool: (payload: McpCallToolPayload) => Promise<McpCallToolResult>
|
||||
}
|
||||
|
||||
let bridge: McpToolBridge | undefined
|
||||
|
||||
export function setMcpToolBridge(nextBridge: McpToolBridge) {
|
||||
bridge = nextBridge
|
||||
}
|
||||
|
||||
export function clearMcpToolBridge() {
|
||||
bridge = undefined
|
||||
}
|
||||
|
||||
export function getMcpToolBridge(): McpToolBridge {
|
||||
if (!bridge) {
|
||||
throw new Error('MCP tool bridge is not available in this runtime.')
|
||||
}
|
||||
|
||||
return bridge
|
||||
}
|
||||
@@ -88,20 +88,20 @@ describe('tools/character/orchestrator/spark-command', () => {
|
||||
})
|
||||
|
||||
it('emits a strict parameter schema', async () => {
|
||||
const tool = await createSparkCommandTool({
|
||||
const tools = await createSparkCommandTool({
|
||||
sendSparkCommand: () => undefined,
|
||||
})
|
||||
|
||||
expect(tool.function.name).toBe('builtIn_emitSparkCommand')
|
||||
expect(tool.function.parameters.additionalProperties).toBe(false)
|
||||
expect(tools[0].function.name).toBe('builtIn_emitSparkCommand')
|
||||
expect(tools[0].function.parameters.additionalProperties).toBe(false)
|
||||
})
|
||||
|
||||
it('avoids propertyNames in provider-facing schema', async () => {
|
||||
const tool = await createSparkCommandTool({
|
||||
const tools = await createSparkCommandTool({
|
||||
sendSparkCommand: () => undefined,
|
||||
})
|
||||
|
||||
const schema = tool.function.parameters as JsonSchema
|
||||
const schema = tools[0].function.parameters as JsonSchema
|
||||
const guidance = getObjectSchema(schema.properties?.guidance as JsonSchema)
|
||||
const guidancePersona = guidance?.properties?.persona as JsonSchema
|
||||
const contexts = getArraySchema(schema.properties?.contexts as JsonSchema)
|
||||
@@ -113,11 +113,11 @@ describe('tools/character/orchestrator/spark-command', () => {
|
||||
})
|
||||
|
||||
it('uses explicit required keys for nested strict option objects', async () => {
|
||||
const tool = await createSparkCommandTool({
|
||||
const tools = await createSparkCommandTool({
|
||||
sendSparkCommand: () => undefined,
|
||||
})
|
||||
|
||||
const schema = tool.function.parameters as JsonSchema
|
||||
const schema = tools[0].function.parameters as JsonSchema
|
||||
expect(schema.required).toEqual([
|
||||
'destinations',
|
||||
'interrupt',
|
||||
@@ -170,11 +170,11 @@ describe('tools/character/orchestrator/spark-command', () => {
|
||||
|
||||
it('builds and dispatches spark commands with generated ids', async () => {
|
||||
const sendSparkCommand = vi.fn()
|
||||
const tool = await createSparkCommandTool({
|
||||
const tools = await createSparkCommandTool({
|
||||
sendSparkCommand,
|
||||
})
|
||||
|
||||
const result = await tool.execute({
|
||||
const result = await tools[0].execute({
|
||||
destinations: ['minecraft'],
|
||||
interrupt: 'soft',
|
||||
priority: 'high',
|
||||
|
||||
@@ -26,45 +26,47 @@ export async function createSparkCommandTool(options: CreateSparkCommandToolOpti
|
||||
// like Azure that reject some `anyOf` nullable forms and strict-object optional-field shapes.
|
||||
const parameters = normalizeNullableAnyOf(await toJsonSchema(sparkCommandToolSchema) as any)
|
||||
|
||||
return rawTool({
|
||||
name: 'builtIn_emitSparkCommand',
|
||||
description: 'Send a spark:command to one or more frontend-connected modules or sub-agents.',
|
||||
parameters,
|
||||
execute: async (rawPayload) => {
|
||||
const payload = rawPayload as z.infer<typeof sparkCommandToolSchema>
|
||||
const command = {
|
||||
id: nanoid(),
|
||||
eventId: nanoid(),
|
||||
parentEventId: payload.parentEventId ?? undefined,
|
||||
commandId: nanoid(),
|
||||
interrupt: payload.interrupt ?? false,
|
||||
priority: payload.priority ?? 'normal',
|
||||
intent: payload.intent ?? 'action',
|
||||
ack: payload.ack ?? undefined,
|
||||
guidance: payload.guidance
|
||||
? {
|
||||
type: payload.guidance.type,
|
||||
persona: normalizeSparkCommandPersona(payload.guidance.persona ?? undefined),
|
||||
options: normalizeSparkCommandGuidanceOptions(payload.guidance.options),
|
||||
}
|
||||
: undefined,
|
||||
contexts: payload.contexts?.map(context => ({
|
||||
return [
|
||||
rawTool({
|
||||
name: 'builtIn_emitSparkCommand',
|
||||
description: 'Send a spark:command to one or more frontend-connected modules or sub-agents.',
|
||||
parameters,
|
||||
execute: async (rawPayload) => {
|
||||
const payload = rawPayload as z.infer<typeof sparkCommandToolSchema>
|
||||
const command = {
|
||||
id: nanoid(),
|
||||
contextId: nanoid(),
|
||||
lane: normalizeSparkCommandStringValue(context.lane),
|
||||
ideas: normalizeSparkCommandStringList(context.ideas),
|
||||
hints: normalizeSparkCommandStringList(context.hints),
|
||||
strategy: context.strategy,
|
||||
text: context.text,
|
||||
destinations: normalizeSparkCommandDestinations(context.destinations),
|
||||
metadata: normalizeSparkCommandMetadata(context.metadata ?? undefined),
|
||||
})),
|
||||
destinations: payload.destinations,
|
||||
} satisfies WebSocketEvents['spark:command']
|
||||
eventId: nanoid(),
|
||||
parentEventId: payload.parentEventId ?? undefined,
|
||||
commandId: nanoid(),
|
||||
interrupt: payload.interrupt ?? false,
|
||||
priority: payload.priority ?? 'normal',
|
||||
intent: payload.intent ?? 'action',
|
||||
ack: payload.ack ?? undefined,
|
||||
guidance: payload.guidance
|
||||
? {
|
||||
type: payload.guidance.type,
|
||||
persona: normalizeSparkCommandPersona(payload.guidance.persona ?? undefined),
|
||||
options: normalizeSparkCommandGuidanceOptions(payload.guidance.options),
|
||||
}
|
||||
: undefined,
|
||||
contexts: payload.contexts?.map(context => ({
|
||||
id: nanoid(),
|
||||
contextId: nanoid(),
|
||||
lane: normalizeSparkCommandStringValue(context.lane),
|
||||
ideas: normalizeSparkCommandStringList(context.ideas),
|
||||
hints: normalizeSparkCommandStringList(context.hints),
|
||||
strategy: context.strategy,
|
||||
text: context.text,
|
||||
destinations: normalizeSparkCommandDestinations(context.destinations),
|
||||
metadata: normalizeSparkCommandMetadata(context.metadata ?? undefined),
|
||||
})),
|
||||
destinations: payload.destinations,
|
||||
} satisfies WebSocketEvents['spark:command']
|
||||
|
||||
options.sendSparkCommand(command)
|
||||
options.sendSparkCommand(command)
|
||||
|
||||
return `spark:command sent (${command.commandId}) to ${command.destinations.join(', ')}`
|
||||
},
|
||||
})
|
||||
return `spark:command sent (${command.commandId}) to ${command.destinations.join(', ')}`
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"chess.js": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"stockfish": "catalog:",
|
||||
"valibot": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -32,6 +33,7 @@
|
||||
"@xsai/stream-text": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"unocss": "^66.6.7",
|
||||
"vieval": "catalog:"
|
||||
"vieval": "catalog:",
|
||||
"xsschema": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+16
-10
@@ -1011,7 +1011,7 @@ importers:
|
||||
version: 1.2.45
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^11.0.7
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@2.80.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@proj-airi/cap-vite':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/cap-vite
|
||||
@@ -1077,16 +1077,16 @@ importers:
|
||||
version: 4.6.4
|
||||
unplugin-info:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@2.80.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@2.80.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@2.80.0)
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -1101,7 +1101,7 @@ importers:
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@2.80.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
@@ -1889,7 +1889,7 @@ importers:
|
||||
version: 1.2.45
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^11.0.7
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@2.80.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@proj-airi/iconify-meteocons':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.5
|
||||
@@ -1952,16 +1952,16 @@ importers:
|
||||
version: 4.6.4
|
||||
unplugin-info:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@2.80.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@2.80.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@2.80.0)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -1976,7 +1976,7 @@ importers:
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@2.80.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
@@ -3861,6 +3861,9 @@ importers:
|
||||
stockfish:
|
||||
specifier: 'catalog:'
|
||||
version: 18.0.7
|
||||
valibot:
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.0(typescript@5.9.3)
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.32(typescript@5.9.3)
|
||||
@@ -3904,6 +3907,9 @@ importers:
|
||||
vieval:
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.1(@types/node@25.6.0)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
xsschema:
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
|
||||
|
||||
plugins/airi-plugin-homeassistant:
|
||||
dependencies:
|
||||
|
||||
@@ -153,6 +153,7 @@ catalogs:
|
||||
unspeech: ^0.1.13
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@ax-llm/ax'
|
||||
- '@prisma/client'
|
||||
- better-sqlite3
|
||||
- simple-git-hooks # [workaround] postinstall script bundled in simple-git-hooks fails to execute with `enableGlobalVirtualStore: true`. Using local postinstall script to run `npx simple-git-hooks` instead.
|
||||
|
||||
Reference in New Issue
Block a user