fix(stage-tamagotchi): plugin tools should have faster fail
This commit is contained in:
@@ -2,7 +2,7 @@ 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'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const invokeMocks = vi.hoisted(() => ({
|
||||
invokePluginTool: vi.fn(async (payload: unknown) => payload),
|
||||
@@ -39,6 +39,11 @@ describe('useTamagotchiPluginToolsStore', async () => {
|
||||
invokeMocks.invokePluginTool.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* await store.refresh()
|
||||
@@ -81,4 +86,54 @@ describe('useTamagotchiPluginToolsStore', async () => {
|
||||
|
||||
expect(llmToolsStore.toolsByProvider['plugin-tools']).toBeUndefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* await store.refresh()
|
||||
* await vi.advanceTimersByTimeAsync(5_000)
|
||||
* await llmToolsStore.awaitPendingRegistrations()
|
||||
*/
|
||||
it('falls back to empty plugin tools when listing xsai tools never resolves during cold start', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
invokeMocks.listPluginXsaiTools.mockImplementationOnce((_req?: undefined, options?: { signal?: AbortSignal }) => new Promise((_, reject) => {
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
reject(options.signal?.reason)
|
||||
}, { once: true })
|
||||
}))
|
||||
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const store = useTamagotchiPluginToolsStore()
|
||||
const onSettled = vi.fn()
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// If the renderer asks the main process for plugin xsai tools before the
|
||||
// Eventa handler is ready, the invoke promise can remain pending forever.
|
||||
// The shared LLM store then waits in awaitPendingRegistrations() before
|
||||
// building chat tools, so no model HTTP request is sent and chat sync
|
||||
// eventually times out.
|
||||
//
|
||||
// Before the fix, this wait never settled.
|
||||
//
|
||||
// We fixed this by letting optional plugin tool listing time out and
|
||||
// complete registration with an empty tool list.
|
||||
store.refresh()
|
||||
const pendingRegistrations = llmToolsStore.awaitPendingRegistrations().then(() => {
|
||||
onSettled()
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
|
||||
expect(onSettled).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5_000)
|
||||
await pendingRegistrations
|
||||
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
expect(llmToolsStore.toolsByProvider['plugin-tools']).toEqual([])
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[plugin-tools] Failed to list plugin xsai tools'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { rawTool } from '@xsai/tool'
|
||||
@@ -23,20 +24,34 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
))
|
||||
const abortController = new AbortController()
|
||||
const timeout = setTimeout(() => abortController.abort(new Error(`Timed out after ${5_000}ms`)), 5_000)
|
||||
|
||||
return llmToolsStore.registerTools(
|
||||
'plugin-tools',
|
||||
listPluginXsaiToolDefinitions(undefined, { signal: abortController.signal })
|
||||
.catch((error) => {
|
||||
console.warn(`[plugin-tools] Failed to list plugin xsai tools: ${errorMessageFrom(error) ?? 'Unknown error'}`)
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
.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() {
|
||||
|
||||
Reference in New Issue
Block a user