feat(plugin-*): prompt for toolset
This commit is contained in:
@@ -848,10 +848,13 @@ describe('setupPluginHost', () => {
|
||||
expect.objectContaining({ id: 'play_chess' }),
|
||||
expect.objectContaining({ id: 'end_play_chess' }),
|
||||
])
|
||||
await expect(invokeListXsaiTools()).resolves.toEqual([
|
||||
expect.objectContaining({ name: 'play_chess' }),
|
||||
expect.objectContaining({ name: 'end_play_chess' }),
|
||||
])
|
||||
await expect(invokeListXsaiTools()).resolves.toEqual({
|
||||
prompts: [],
|
||||
tools: [
|
||||
expect.objectContaining({ name: 'play_chess' }),
|
||||
expect.objectContaining({ name: 'end_play_chess' }),
|
||||
],
|
||||
})
|
||||
await expect(invokePluginTool({
|
||||
ownerPluginId: session.identity.plugin.id,
|
||||
name: 'play_chess',
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { useLlmToolsetPromptsStore } from '@proj-airi/stage-ui/stores/llm-toolset-prompts'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, 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: {},
|
||||
listPluginXsaiTools: vi.fn(async () => ({
|
||||
tools: [
|
||||
{
|
||||
ownerPluginId: 'plugin-chess',
|
||||
name: 'play_chess',
|
||||
description: 'Play a chess move.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
prompts: [
|
||||
{
|
||||
ownerPluginId: 'plugin-chess',
|
||||
id: 'chess-tools',
|
||||
prompt: {
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/electron-vueuse', () => ({
|
||||
@@ -51,6 +65,7 @@ describe('useTamagotchiPluginToolsStore', async () => {
|
||||
*/
|
||||
it('loads plugin xsai tools, proxies execution, and clears them from the shared llm-tools store', async () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const llmToolsetPromptsStore = useLlmToolsetPromptsStore()
|
||||
const store = useTamagotchiPluginToolsStore()
|
||||
const toolOptions = {} as Parameters<Tool['execute']>[1]
|
||||
|
||||
@@ -62,6 +77,7 @@ describe('useTamagotchiPluginToolsStore', async () => {
|
||||
expect(pluginTools).toEqual([
|
||||
expect.objectContaining({ function: expect.objectContaining({ name: 'play_chess' }) }),
|
||||
])
|
||||
expect(llmToolsetPromptsStore.activeToolsetPrompt).toContain('Do not pass fen or pgn when mode is "new".')
|
||||
|
||||
const executionResult = await playChessTool?.execute({
|
||||
move: 'e2e4',
|
||||
@@ -85,6 +101,7 @@ describe('useTamagotchiPluginToolsStore', async () => {
|
||||
store.dispose()
|
||||
|
||||
expect(llmToolsStore.toolsByProvider['plugin-tools']).toBeUndefined()
|
||||
expect(llmToolsetPromptsStore.promptsByProvider['plugin-tools']).toBeUndefined()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
|
||||
import { useLlmToolsetPromptsStore } from '@proj-airi/stage-ui/stores/llm-toolset-prompts'
|
||||
import { rawTool } from '@xsai/tool'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
@@ -20,6 +21,7 @@ import { electronPluginInvokeTool, electronPluginListXsaiTools } from '../../sha
|
||||
*/
|
||||
export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tools', () => {
|
||||
const llmToolsStore = useLlmToolsStore()
|
||||
const llmToolsetPromptsStore = useLlmToolsetPromptsStore()
|
||||
const listPluginXsaiToolDefinitions = useElectronEventaInvoke(electronPluginListXsaiTools)
|
||||
const invokePluginTool = useElectronEventaInvoke(electronPluginInvokeTool)
|
||||
|
||||
@@ -32,13 +34,22 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
|
||||
listPluginXsaiToolDefinitions(undefined, { signal: abortController.signal })
|
||||
.catch((error) => {
|
||||
console.warn(`[plugin-tools] Failed to list plugin xsai tools: ${errorMessageFrom(error) ?? 'Unknown error'}`)
|
||||
return []
|
||||
return { prompts: [], tools: [] }
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
.then(definitions =>
|
||||
definitions.map(definition =>
|
||||
.then((definitions) => {
|
||||
llmToolsetPromptsStore.registerToolsetPrompts(
|
||||
'plugin-tools',
|
||||
definitions.prompts.map(definition => ({
|
||||
id: `${definition.ownerPluginId}:${definition.id}`,
|
||||
title: definition.prompt.title,
|
||||
content: definition.prompt.content,
|
||||
})),
|
||||
)
|
||||
|
||||
return definitions.tools.map(definition =>
|
||||
rawTool({
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
@@ -49,13 +60,14 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
|
||||
input,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
llmToolsStore.clearTools('plugin-tools')
|
||||
llmToolsetPromptsStore.clearToolsetPrompts('plugin-tools')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,8 +41,47 @@ export interface ElectronPluginXsaiToolDefinition {
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialized toolset prompt exposed by the plugin host.
|
||||
*
|
||||
* Use when:
|
||||
* - Registering plugin-backed prompt guidance in the renderer
|
||||
*
|
||||
* Expects:
|
||||
* - `content` is already model-facing prompt text
|
||||
*
|
||||
* Returns:
|
||||
* - N/A
|
||||
*/
|
||||
export interface ElectronPluginToolsetPromptDefinition {
|
||||
ownerPluginId: string
|
||||
id: string
|
||||
prompt: {
|
||||
id: string
|
||||
title?: string
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialized plugin xsai tools and shared prompt guidance.
|
||||
*
|
||||
* Use when:
|
||||
* - Refreshing renderer LLM tool registrations from the Electron plugin host
|
||||
*
|
||||
* Expects:
|
||||
* - The host filtered out inactive plugin sessions
|
||||
*
|
||||
* Returns:
|
||||
* - N/A
|
||||
*/
|
||||
export interface ElectronPluginXsaiToolsetDefinition {
|
||||
tools: ElectronPluginXsaiToolDefinition[]
|
||||
prompts: ElectronPluginToolsetPromptDefinition[]
|
||||
}
|
||||
|
||||
export const electronPluginListAgentTools = defineInvokeEventa<ElectronPluginToolDescriptor[]>('eventa:invoke:electron:plugins:tools:list')
|
||||
export const electronPluginListXsaiTools = defineInvokeEventa<ElectronPluginXsaiToolDefinition[]>('eventa:invoke:electron:plugins:tools:list-xsai')
|
||||
export const electronPluginListXsaiTools = defineInvokeEventa<ElectronPluginXsaiToolsetDefinition>('eventa:invoke:electron:plugins:tools:list-xsai')
|
||||
export const electronPluginInvokeTool = defineInvokeEventa<unknown, {
|
||||
ownerPluginId: string
|
||||
name: string
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('plugin-sdk-tamagotchi', () => {
|
||||
},
|
||||
tools: {
|
||||
register: registerTool,
|
||||
registerToolsetPrompt: vi.fn(),
|
||||
},
|
||||
kits: {
|
||||
list: async () => [
|
||||
@@ -75,6 +76,12 @@ describe('plugin-sdk-tamagotchi', () => {
|
||||
})
|
||||
|
||||
await defineToolset(ctx, {
|
||||
id: 'chess-tools',
|
||||
prompt: {
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
id: 'play_chess',
|
||||
@@ -88,6 +95,14 @@ describe('plugin-sdk-tamagotchi', () => {
|
||||
],
|
||||
})
|
||||
|
||||
expect(ctx.apis.tools.registerToolsetPrompt).toHaveBeenCalledWith({
|
||||
id: 'chess-tools',
|
||||
prompt: {
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
})
|
||||
expect(gamelet).toBeDefined()
|
||||
expect(registerBinding).toHaveBeenCalledWith({
|
||||
moduleId: 'chess',
|
||||
@@ -167,6 +182,7 @@ describe('plugin-sdk-tamagotchi', () => {
|
||||
},
|
||||
tools: {
|
||||
register: registerTool,
|
||||
registerToolsetPrompt: vi.fn(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -223,6 +239,7 @@ describe('plugin-sdk-tamagotchi', () => {
|
||||
},
|
||||
tools: {
|
||||
register: registerTool,
|
||||
registerToolsetPrompt: vi.fn(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ContextInit } from '@proj-airi/plugin-sdk'
|
||||
import type { HostDataRecord } from '@proj-airi/plugin-sdk/plugin-host'
|
||||
import type { HostDataRecord, ToolsetPromptManifest } from '@proj-airi/plugin-sdk/plugin-host'
|
||||
import type { JsonSchema, Schema as StandardSchemaV1 } from 'xsschema'
|
||||
|
||||
import { hostDataRecordSchema } from '@proj-airi/plugin-sdk/plugin-host'
|
||||
@@ -118,6 +118,8 @@ export interface PluginToolDefinition<TInputSchema = unknown> {
|
||||
* - Resolves once every tool has been registered with the host
|
||||
*/
|
||||
export interface DefineToolsetOptions<TInputSchema = unknown> {
|
||||
id?: string
|
||||
prompt?: ToolsetPromptManifest
|
||||
tools: Array<PluginToolDefinition<TInputSchema>>
|
||||
}
|
||||
|
||||
@@ -331,6 +333,13 @@ export async function defineToolset(
|
||||
): Promise<void> {
|
||||
const executionContext = createToolExecutionContext(ctx)
|
||||
|
||||
if (options.prompt) {
|
||||
await ctx.apis.tools.registerToolsetPrompt({
|
||||
id: options.id ?? options.prompt.id,
|
||||
prompt: options.prompt,
|
||||
})
|
||||
}
|
||||
|
||||
for (const definition of options.tools) {
|
||||
const isAvailable = definition.isAvailable
|
||||
|
||||
|
||||
@@ -477,6 +477,14 @@ describe('for PluginHost', () => {
|
||||
availability: () => false,
|
||||
execute: async () => ({ ok: true, ended: true }),
|
||||
})
|
||||
await session.apis.tools.registerToolsetPrompt({
|
||||
id: 'chess-tools',
|
||||
prompt: {
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
})
|
||||
|
||||
await expect(host.listAvailableToolDescriptors()).resolves.toEqual([
|
||||
{
|
||||
@@ -489,21 +497,34 @@ describe('for PluginHost', () => {
|
||||
},
|
||||
},
|
||||
])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual([
|
||||
{
|
||||
ownerPluginId: session.identity.plugin.id,
|
||||
name: 'play_chess',
|
||||
description: 'Open chess.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
opening: {
|
||||
type: 'string',
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual({
|
||||
prompts: [
|
||||
{
|
||||
ownerPluginId: session.identity.plugin.id,
|
||||
id: 'chess-tools',
|
||||
prompt: {
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
ownerPluginId: session.identity.plugin.id,
|
||||
name: 'play_chess',
|
||||
description: 'Open chess.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
opening: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
],
|
||||
})
|
||||
await expect(host.invokeTool(session.identity.plugin.id, 'play_chess', { opening: 'sicilian' })).resolves.toEqual({
|
||||
ok: true,
|
||||
input: { opening: 'sicilian' },
|
||||
@@ -550,7 +571,7 @@ describe('for PluginHost', () => {
|
||||
host.stop(session.id)
|
||||
|
||||
await expect(host.listAvailableToolDescriptors()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual({ prompts: [], tools: [] })
|
||||
await expect(host.invokeTool(session.identity.plugin.id, 'play_chess', {})).rejects.toThrow(
|
||||
`Plugin tool not found: ${session.identity.plugin.id}:play_chess`,
|
||||
)
|
||||
@@ -612,7 +633,7 @@ describe('for PluginHost', () => {
|
||||
expect(host.getSession(session.id)).toBeUndefined()
|
||||
expect(host.getBinding('module-ready-hook-failure')).toBeUndefined()
|
||||
await expect(host.listAvailableToolDescriptors()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual({ prompts: [], tools: [] })
|
||||
await expect(host.invokeTool(session.identity.plugin.id, 'ready_hook_tool', {})).rejects.toThrow(
|
||||
`Plugin tool not found: ${session.identity.plugin.id}:ready_hook_tool`,
|
||||
)
|
||||
@@ -668,7 +689,7 @@ describe('for PluginHost', () => {
|
||||
expect(host.getSession(session.id)).toBeUndefined()
|
||||
expect(host.getBinding('module-stopped-hook-failure')).toBeUndefined()
|
||||
await expect(host.listAvailableToolDescriptors()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual([])
|
||||
await expect(host.listSerializedXsaiTools()).resolves.toEqual({ prompts: [], tools: [] })
|
||||
await expect(host.invokeTool(session.identity.plugin.id, 'stopped_hook_tool', {})).rejects.toThrow(
|
||||
`Plugin tool not found: ${session.identity.plugin.id}:stopped_hook_tool`,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActorRefFrom } from 'xstate'
|
||||
|
||||
import type { createApis } from '../plugin/apis/client'
|
||||
import type { AnnounceBindingInput, UpdateBindingInput } from '../plugin/apis/client/bindings'
|
||||
import type { RegisterToolInput } from '../plugin/apis/client/tools'
|
||||
import type { RegisterToolInput, RegisterToolsetPromptInput } from '../plugin/apis/client/tools'
|
||||
import type { Plugin } from '../plugin/shared'
|
||||
import type { BindingRecord, KitCapabilityDescriptor, KitDescriptor } from './shared'
|
||||
import type {
|
||||
@@ -873,6 +873,7 @@ export class PluginHost {
|
||||
},
|
||||
tools: {
|
||||
register: input => this.registerTool(session.id, input),
|
||||
registerToolsetPrompt: input => this.registerToolsetPrompt(session.id, input),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1177,6 +1178,28 @@ export class PluginHost {
|
||||
})
|
||||
}
|
||||
|
||||
registerToolsetPrompt(sessionId: string, input: RegisterToolsetPromptInput) {
|
||||
const session = this.getSessionOrThrow(sessionId)
|
||||
|
||||
this.assertPermission(session, {
|
||||
area: 'apis',
|
||||
action: 'invoke',
|
||||
key: pluginToolApiRegisterEventName,
|
||||
})
|
||||
this.assertPermission(session, {
|
||||
area: 'resources',
|
||||
action: 'write',
|
||||
key: pluginToolRegistryResourceKey,
|
||||
})
|
||||
|
||||
this.tools.registerToolsetPrompt({
|
||||
ownerSessionId: session.id,
|
||||
ownerPluginId: session.identity.plugin.id,
|
||||
toolset: structuredClone(input),
|
||||
availability: () => Boolean(this.getSession(session.id)),
|
||||
})
|
||||
}
|
||||
|
||||
async load(manifest: ManifestV1, options: PluginLoadOptions = {}): Promise<PluginHostSession> {
|
||||
// Step 0 (channel gateway preparation): resolve runtime and transport for this plugin.
|
||||
const runtime = options.runtime ?? this.runtime
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
PluginToolDefinitionRecord,
|
||||
PluginToolsetPromptDefinitionRecord,
|
||||
RegisteredPluginToolDescriptor,
|
||||
SerializedToolsetPromptDefinition,
|
||||
SerializedXsaiToolDefinition,
|
||||
SerializedXsaiToolsetDefinition,
|
||||
} from '../../../shared'
|
||||
|
||||
/**
|
||||
@@ -24,6 +27,25 @@ export interface ToolRegistryRecord {
|
||||
execute: (input: unknown) => Promise<unknown> | unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores one plugin toolset prompt registration inside the in-memory host runtime.
|
||||
*
|
||||
* Use when:
|
||||
* - Tracking prompt ownership and lifecycle for a plugin-owned toolset
|
||||
*
|
||||
* Expects:
|
||||
* - `ownerPluginId` and `toolset.id` together are unique
|
||||
*
|
||||
* Returns:
|
||||
* - A host-managed record used for prompt serialization
|
||||
*/
|
||||
export interface ToolsetPromptRegistryRecord {
|
||||
ownerSessionId: string
|
||||
ownerPluginId: string
|
||||
toolset: PluginToolsetPromptDefinitionRecord
|
||||
availability?: () => Promise<boolean> | boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory registry for plugin-contributed tools.
|
||||
*
|
||||
@@ -39,6 +61,7 @@ export interface ToolRegistryRecord {
|
||||
*/
|
||||
export class ToolRegistryService {
|
||||
private readonly tools = new Map<string, ToolRegistryRecord>()
|
||||
private readonly toolsetPrompts = new Map<string, ToolsetPromptRegistryRecord>()
|
||||
|
||||
register(record: ToolRegistryRecord) {
|
||||
const key = `${record.ownerPluginId}:${record.tool.id}`
|
||||
@@ -46,6 +69,12 @@ export class ToolRegistryService {
|
||||
return record
|
||||
}
|
||||
|
||||
registerToolsetPrompt(record: ToolsetPromptRegistryRecord) {
|
||||
const key = `${record.ownerPluginId}:${record.toolset.id}`
|
||||
this.toolsetPrompts.set(key, record)
|
||||
return record
|
||||
}
|
||||
|
||||
async listAvailableDescriptors() {
|
||||
const items: RegisteredPluginToolDescriptor[] = []
|
||||
|
||||
@@ -68,7 +97,25 @@ export class ToolRegistryService {
|
||||
return items
|
||||
}
|
||||
|
||||
async listSerializedXsaiTools() {
|
||||
async listToolsetPrompts() {
|
||||
const prompts: SerializedToolsetPromptDefinition[] = []
|
||||
|
||||
for (const record of this.toolsetPrompts.values()) {
|
||||
if (await record.availability?.() === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
prompts.push({
|
||||
ownerPluginId: record.ownerPluginId,
|
||||
id: record.toolset.id,
|
||||
prompt: structuredClone(record.toolset.prompt),
|
||||
})
|
||||
}
|
||||
|
||||
return prompts
|
||||
}
|
||||
|
||||
async listSerializedXsaiTools(): Promise<SerializedXsaiToolsetDefinition> {
|
||||
const items: SerializedXsaiToolDefinition[] = []
|
||||
|
||||
for (const record of this.tools.values()) {
|
||||
@@ -84,7 +131,10 @@ export class ToolRegistryService {
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
return {
|
||||
prompts: await this.listToolsetPrompts(),
|
||||
tools: items,
|
||||
}
|
||||
}
|
||||
|
||||
async invoke(ownerPluginId: string, toolId: string, input: unknown) {
|
||||
|
||||
@@ -43,6 +43,59 @@ export interface SerializedXsaiToolDefinition {
|
||||
parameters: HostDataRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes model-facing guidance shared by every tool in one plugin toolset.
|
||||
*
|
||||
* Use when:
|
||||
* - A toolset needs shared usage policy without duplicating prompt text on each tool
|
||||
*
|
||||
* Expects:
|
||||
* - `content` is ready to append into a runtime system prompt
|
||||
*
|
||||
* Returns:
|
||||
* - A serializable manifest that the host can pass to renderer prompt stores
|
||||
*/
|
||||
export interface ToolsetPromptManifest {
|
||||
id: string
|
||||
title?: string
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures one registered toolset prompt with plugin ownership metadata.
|
||||
*
|
||||
* Use when:
|
||||
* - Serializing plugin-contributed toolset guidance across host boundaries
|
||||
*
|
||||
* Expects:
|
||||
* - `id` is stable within the owning plugin session
|
||||
*
|
||||
* Returns:
|
||||
* - A prompt contribution suitable for renderer LLM prompt injection
|
||||
*/
|
||||
export interface SerializedToolsetPromptDefinition {
|
||||
ownerPluginId: string
|
||||
id: string
|
||||
prompt: ToolsetPromptManifest
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundles plugin xsai tools with their shared toolset prompt contributions.
|
||||
*
|
||||
* Use when:
|
||||
* - The renderer refreshes plugin-backed tools and model prompt guidance together
|
||||
*
|
||||
* Expects:
|
||||
* - Tools and prompts have already been filtered for active sessions
|
||||
*
|
||||
* Returns:
|
||||
* - A serializable snapshot for renderer tool and prompt stores
|
||||
*/
|
||||
export interface SerializedXsaiToolsetDefinition {
|
||||
tools: SerializedXsaiToolDefinition[]
|
||||
prompts: SerializedToolsetPromptDefinition[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the single source-of-truth definition submitted by a plugin.
|
||||
*
|
||||
@@ -65,3 +118,20 @@ export interface PluginToolDefinitionRecord {
|
||||
}
|
||||
parameters: HostDataRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a plugin-owned prompt shared by a toolset.
|
||||
*
|
||||
* Use when:
|
||||
* - A plugin registers model-facing guidance for a group of related tools
|
||||
*
|
||||
* Expects:
|
||||
* - `prompt` content is validated by the authoring helper or caller
|
||||
*
|
||||
* Returns:
|
||||
* - A host-owned record that can be filtered by session lifecycle
|
||||
*/
|
||||
export interface PluginToolsetPromptDefinitionRecord {
|
||||
id: string
|
||||
prompt: ToolsetPromptManifest
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EventContext } from '@moeru/eventa'
|
||||
|
||||
import type { PluginToolDefinitionRecord } from '../../../../plugin-host/shared'
|
||||
import type { PluginToolDefinitionRecord, PluginToolsetPromptDefinitionRecord } from '../../../../plugin-host/shared'
|
||||
|
||||
/**
|
||||
* Identifies the bound API call used to register plugin tools.
|
||||
@@ -48,6 +48,20 @@ export interface RegisterToolInput {
|
||||
execute: (input: unknown) => Promise<unknown> | unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Carries one low-level toolset prompt registration request into the host.
|
||||
*
|
||||
* Use when:
|
||||
* - A plugin wants shared model-facing guidance for a group of tools
|
||||
*
|
||||
* Expects:
|
||||
* - `id` is stable within the owning plugin
|
||||
*
|
||||
* Returns:
|
||||
* - A registration payload consumed by the bound host implementation
|
||||
*/
|
||||
export type RegisterToolsetPromptInput = PluginToolsetPromptDefinitionRecord
|
||||
|
||||
/**
|
||||
* Defines the host-side callbacks needed by the low-level plugin tool client.
|
||||
*
|
||||
@@ -62,6 +76,7 @@ export interface RegisterToolInput {
|
||||
*/
|
||||
export interface ToolClientBindings {
|
||||
register: (input: RegisterToolInput) => Promise<void> | void
|
||||
registerToolsetPrompt: (input: RegisterToolsetPromptInput) => Promise<void> | void
|
||||
}
|
||||
|
||||
function createMissingBindingError(method: string) {
|
||||
@@ -93,6 +108,9 @@ export function createTools(_ctx: EventContext<any, any>, bindings?: ToolClientB
|
||||
async register(input: RegisterToolInput) {
|
||||
return await requireBinding(bindings, 'tools.register').register(input)
|
||||
},
|
||||
async registerToolsetPrompt(input: RegisterToolsetPromptInput) {
|
||||
return await requireBinding(bindings, 'tools.registerToolsetPrompt').registerToolsetPrompt(input)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useLlmToolsetPromptsStore } from './llm-toolset-prompts'
|
||||
|
||||
describe('useLlmToolsetPromptsStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* store.registerToolsetPrompts('plugin-tools', [{ id: 'chess', content: 'Use chess correctly.' }])
|
||||
* expect(store.activeToolsetPrompt).toContain('Use chess correctly.')
|
||||
*/
|
||||
it('renders active toolset prompts grouped by provider and clears them by provider', () => {
|
||||
const store = useLlmToolsetPromptsStore()
|
||||
|
||||
store.registerToolsetPrompts('plugin-tools', [
|
||||
{
|
||||
id: 'airi-plugin-game-chess.prompt',
|
||||
title: 'Chess Plugin Guidance',
|
||||
content: 'Do not pass fen or pgn when mode is "new".',
|
||||
},
|
||||
])
|
||||
|
||||
expect(store.activeToolsetPrompt).toContain('Runtime Toolset Guidance')
|
||||
expect(store.activeToolsetPrompt).toContain('Chess Plugin Guidance')
|
||||
expect(store.activeToolsetPrompt).toContain('Do not pass fen or pgn when mode is "new".')
|
||||
|
||||
store.clearToolsetPrompts('plugin-tools')
|
||||
|
||||
expect(store.activeToolsetPrompt).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface LlmToolsetPromptContribution {
|
||||
id: string
|
||||
title?: string
|
||||
content: string
|
||||
}
|
||||
|
||||
function renderToolsetPrompts(prompts: LlmToolsetPromptContribution[]) {
|
||||
const activePrompts = prompts.filter(prompt => prompt.content.trim().length > 0)
|
||||
if (activePrompts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const lines = ['## Toolset', '']
|
||||
|
||||
for (const prompt of activePrompts) {
|
||||
if (prompt.title) {
|
||||
lines.push(`### ${prompt.title}`, '')
|
||||
}
|
||||
|
||||
lines.push(prompt.content.trim())
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n').trim()
|
||||
}
|
||||
|
||||
export const useLlmToolsetPromptsStore = defineStore('llm-toolset-prompts', () => {
|
||||
const promptsByProvider = ref<Record<string, LlmToolsetPromptContribution[]>>({})
|
||||
|
||||
function registerToolsetPrompts(provider: string, prompts: LlmToolsetPromptContribution[]) {
|
||||
promptsByProvider.value = {
|
||||
...promptsByProvider.value,
|
||||
[provider]: structuredClone(prompts),
|
||||
}
|
||||
}
|
||||
|
||||
function clearToolsetPrompts(provider: string) {
|
||||
const { [provider]: _removed, ...remaining } = promptsByProvider.value
|
||||
promptsByProvider.value = remaining
|
||||
}
|
||||
|
||||
const activeToolsetPrompt = computed(() => renderToolsetPrompts(Object.values(promptsByProvider.value).flat()))
|
||||
|
||||
return {
|
||||
activeToolsetPrompt,
|
||||
clearToolsetPrompts,
|
||||
promptsByProvider,
|
||||
registerToolsetPrompts,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user