feat(plugin-*): prompt for toolset
This commit is contained in:
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user