diff --git a/apps/studio/mocks/node-polyfills.ts b/apps/studio/mocks/node-polyfills.ts index 3df043e89..c02500a4a 100644 --- a/apps/studio/mocks/node-polyfills.ts +++ b/apps/studio/mocks/node-polyfills.ts @@ -27,6 +27,7 @@ export const promises = { stat: async () => ({ isDirectory: () => false, isFile: () => false }), mkdir: async () => {}, rm: async () => {}, + access: async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }, }; // os polyfills @@ -81,8 +82,8 @@ realpathSync.native = () => ''; export const constants = {}; export const lstat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); export const stat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); -export const access = () => {}; -export const accessSync = () => {}; +export const access = async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }; +export const accessSync = () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }; export const mkdir = () => {}; export const mkdirSync = () => {}; export const rmdir = () => {}; diff --git a/apps/studio/src/components/CodeExporter.tsx b/apps/studio/src/components/CodeExporter.tsx index b219bf176..c2a7edd36 100644 --- a/apps/studio/src/components/CodeExporter.tsx +++ b/apps/studio/src/components/CodeExporter.tsx @@ -5,11 +5,11 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Code2, Copy, Check, Database, Layout, Workflow, Bot, AppWindow } from 'lucide-react'; +import { Code2, Copy, Check, Database, Layout, Workflow, Bot, AppWindow, Wrench } from 'lucide-react'; // ─── Types ────────────────────────────────────────────────────────── -type ExportType = 'object' | 'view' | 'flow' | 'agent' | 'app'; +type ExportType = 'object' | 'view' | 'flow' | 'agent' | 'tool' | 'app'; export interface CodeExporterProps { type: ExportType; @@ -24,6 +24,7 @@ const TYPE_LABELS: Record, name?: string): string { return lines.join('\n'); } +function generateToolCode(def: Record, name?: string): string { + const toolName = name || (def.name as string) || 'my_tool'; + const lines = [ + "import { defineStack } from '@objectstack/spec';", + '', + 'export default defineStack({', + ' tools: {', + ` ${toolName}: ${formatValue(def, 2)},`, + ' },', + '});', + ]; + return lines.join('\n'); +} + const CODE_GENERATORS: Record, name?: string) => string> = { object: generateObjectCode, view: generateViewCode, flow: generateFlowCode, agent: generateAgentCode, + tool: generateToolCode, app: generateAppCode, }; diff --git a/apps/studio/src/components/MetadataInspector.tsx b/apps/studio/src/components/MetadataInspector.tsx index 27705b403..8a3bbd1d8 100644 --- a/apps/studio/src/components/MetadataInspector.tsx +++ b/apps/studio/src/components/MetadataInspector.tsx @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, Copy, Check, ChevronRight, ChevronDown, - Zap, BarChart3, FileText, Workflow, Bot, Globe, BookOpen, Shield, + Zap, BarChart3, FileText, Workflow, Bot, Globe, BookOpen, Shield, Wrench, type LucideIcon, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; @@ -34,8 +34,12 @@ const TYPE_ICONS: Record = { dashboards: BarChart3, reports: FileText, flows: Workflow, + agent: Bot, agents: Bot, + tool: Wrench, + tools: Wrench, apis: Globe, + ragPipeline: BookOpen, ragPipelines: BookOpen, profiles: Shield, sharingRules: Shield, @@ -46,8 +50,12 @@ const TYPE_LABELS: Record = { dashboards: 'Dashboard', reports: 'Report', flows: 'Flow', + agent: 'Agent', agents: 'Agent', + tool: 'Tool', + tools: 'Tool', apis: 'API', + ragPipeline: 'RAG Pipeline', ragPipelines: 'RAG Pipeline', profiles: 'Profile', sharingRules: 'Sharing Rule', diff --git a/apps/studio/src/components/app-sidebar.tsx b/apps/studio/src/components/app-sidebar.tsx index 8ee4bb593..126a36f9a 100644 --- a/apps/studio/src/components/app-sidebar.tsx +++ b/apps/studio/src/components/app-sidebar.tsx @@ -32,6 +32,7 @@ import { UserCog, ChevronRight, Settings, + Wrench, type LucideIcon, } from "lucide-react" import { useState, useEffect, useCallback, useMemo } from "react" @@ -94,7 +95,11 @@ const META_TYPE_HINTS: Record = { profiles: { label: 'Profiles', icon: Shield }, sharingRules: { label: 'Sharing Rules', icon: Shield }, policies: { label: 'Policies', icon: Shield }, + agent: { label: 'Agents', icon: Bot }, agents: { label: 'Agents', icon: Bot }, + tool: { label: 'Tools', icon: Wrench }, + tools: { label: 'Tools', icon: Wrench }, + ragPipeline: { label: 'RAG Pipelines', icon: BookOpen }, ragPipelines: { label: 'RAG Pipelines', icon: BookOpen }, apis: { label: 'APIs', icon: Globe }, connectors: { label: 'Connectors', icon: Link2 }, @@ -123,7 +128,7 @@ const PROTOCOL_GROUPS: ProtocolGroup[] = [ { key: 'ui', label: 'UI', icon: AppWindow, types: ['app', 'apps', 'actions', 'views', 'pages', 'dashboards', 'reports', 'themes'] }, { key: 'automation', label: 'Automation', icon: Workflow, types: ['flows', 'workflows', 'approvals', 'webhooks'] }, { key: 'security', label: 'Security', icon: Shield, types: ['roles', 'permissions', 'profiles', 'sharingRules', 'policies'] }, - { key: 'ai', label: 'AI', icon: Bot, types: ['agents', 'ragPipelines'] }, + { key: 'ai', label: 'AI', icon: Bot, types: ['agent', 'agents', 'tool', 'tools', 'ragPipeline', 'ragPipelines'] }, { key: 'api', label: 'API', icon: Globe, types: ['apis', 'connectors'] }, ]; @@ -211,12 +216,32 @@ export function AppSidebar({ } else if (Array.isArray(typesResult)) { types = typesResult as any; } - setMetaTypes(types); + + // Normalize types: prefer singular form (agent, tool) over plural (agents, tools) + // when both exist in PROTOCOL_GROUPS, since the singular REST endpoint merges + // SchemaRegistry items with MetadataService runtime items. + const groupSingulars = new Set(PROTOCOL_GROUPS.flatMap(g => g.types).filter(t => !t.endsWith('s'))); + const normalized = types.map(t => { + if (t.endsWith('s') && groupSingulars.has(t.slice(0, -1))) { + return t.slice(0, -1); // agents → agent, tools → tool + } + return t; + }); + // Also add group types that aren't covered at all by the server types + const groupTypes = PROTOCOL_GROUPS.flatMap(g => g.types); + const coveredSet = new Set(normalized); + const extraTypes = groupTypes.filter(t => { + if (coveredSet.has(t)) return false; + const variant = t.endsWith('s') ? t.slice(0, -1) : t + 's'; + return !coveredSet.has(variant); + }); + const allTypes = Array.from(new Set([...normalized, ...extraTypes])); + setMetaTypes(allTypes); const packageId = selectedPackage?.manifest?.id; const entries = await Promise.all( - types + allTypes .filter(t => !HIDDEN_TYPES.has(t)) .map(async (type) => { try { diff --git a/apps/studio/src/lib/create-broker-shim.ts b/apps/studio/src/lib/create-broker-shim.ts index fe05c72a8..23d5be9b9 100644 --- a/apps/studio/src/lib/create-broker-shim.ts +++ b/apps/studio/src/lib/create-broker-shim.ts @@ -178,8 +178,22 @@ export function createBrokerShim(kernel: any): BrokerShim { } if (service === 'metadata') { + // Get MetadataService for runtime-registered metadata (agents, tools, etc.) + const metadataService = kernel.context?.getService('metadata'); + if (method === 'types') { - return { types: SchemaRegistry.getRegisteredTypes() }; + // Combine types from both SchemaRegistry (static) and MetadataService (runtime) + const schemaTypes = SchemaRegistry.getRegisteredTypes(); + + // MetadataService exposes types through getRegisteredTypes() method + let runtimeTypes: string[] = []; + if (metadataService && typeof metadataService.getRegisteredTypes === 'function') { + runtimeTypes = await metadataService.getRegisteredTypes(); + } + + // Merge and deduplicate + const allTypes = Array.from(new Set([...schemaTypes, ...runtimeTypes])); + return { types: allTypes }; } if (method === 'objects') { const packageId = params.packageId; @@ -206,9 +220,33 @@ export function createBrokerShim(kernel: any): BrokerShim { } return def || null; } - // Generic metadata type: metadata. → SchemaRegistry.listItems(type, packageId?) + // Generic metadata type: metadata. → check both SchemaRegistry and MetadataService const packageId = params.packageId; - const items = SchemaRegistry.listItems(method, packageId); + + // Try SchemaRegistry first (static metadata from packages) + let items = SchemaRegistry.listItems(method, packageId); + + // Also check MetadataService for runtime-registered metadata (agents, tools, etc.) + if (metadataService && typeof metadataService.list === 'function') { + try { + const runtimeItems = await metadataService.list(method); + if (runtimeItems && runtimeItems.length > 0) { + // Merge items, avoiding duplicates by name + const itemMap = new Map(); + items.forEach((item: any) => itemMap.set(item.name, item)); + runtimeItems.forEach((item: any) => { + if (item && typeof item === 'object' && 'name' in item) { + itemMap.set(item.name, item); + } + }); + items = Array.from(itemMap.values()); + } + } catch (err) { + // MetadataService.list might fail for unknown types, that's OK + console.debug(`[BrokerShim] MetadataService.list('${method}') failed:`, err); + } + } + if (items && items.length > 0) { return { type: method, items }; } diff --git a/apps/studio/src/mocks/simulateBrowser.ts b/apps/studio/src/mocks/simulateBrowser.ts index 15b8cfa7e..739d4cc8e 100644 --- a/apps/studio/src/mocks/simulateBrowser.ts +++ b/apps/studio/src/mocks/simulateBrowser.ts @@ -142,6 +142,17 @@ export async function simulateBrowser() { } }), + // Metadata - Get all types (base route returns types) + http.get('http://localhost:3000/api/v1/meta', async () => { + console.log('[VirtualNetwork] GET /meta (types)'); + try { + const result = await (kernel as any).broker.call('metadata.types', {}); + return HttpResponse.json({ success: true, data: result }); + } catch (err: any) { + return HttpResponse.json({ error: err.message }, { status: 500 }); + } + }), + // Metadata - Objects List (Singular & Plural support) http.get('http://localhost:3000/api/v1/meta/object', async () => { console.log('[VirtualNetwork] GET /meta/object'); @@ -194,6 +205,22 @@ export async function simulateBrowser() { } catch (err: any) { return HttpResponse.json({ error: err.message }, { status: 500 }); } + }), + + // Metadata - Generic type list (for agents, tools, etc.) + // This must come AFTER specific routes like /meta/object to avoid conflicts + http.get('http://localhost:3000/api/v1/meta/:type', async ({ params }) => { + // Skip if it's a specific route we already handled + if (params.type === 'object' || params.type === 'objects') { + return; + } + console.log(`[VirtualNetwork] GET /meta/${params.type}`); + try { + const result = await (kernel as any).broker.call(`metadata.${params.type}`, {}); + return HttpResponse.json({ success: true, data: result }); + } catch (err: any) { + return HttpResponse.json({ error: err.message }, { status: 500 }); + } }) ]; diff --git a/apps/studio/src/plugins/built-in/ai-plugin.tsx b/apps/studio/src/plugins/built-in/ai-plugin.tsx index 571c31113..85ff34967 100644 --- a/apps/studio/src/plugins/built-in/ai-plugin.tsx +++ b/apps/studio/src/plugins/built-in/ai-plugin.tsx @@ -9,7 +9,7 @@ import { defineStudioPlugin } from '@objectstack/spec/studio'; import type { StudioPlugin } from '../types'; -import { Bot, BookOpen } from 'lucide-react'; +import { Bot, BookOpen, Wrench } from 'lucide-react'; export const aiProtocolPlugin: StudioPlugin = { manifest: defineStudioPlugin({ @@ -23,19 +23,21 @@ export const aiProtocolPlugin: StudioPlugin = { key: 'ai', label: 'AI', icon: 'bot', - metadataTypes: ['agents', 'ragPipelines'], + metadataTypes: ['agent', 'tool', 'ragPipeline'], order: 50, }, ], metadataIcons: [ - { metadataType: 'agents', label: 'Agents', icon: 'bot' }, - { metadataType: 'ragPipelines', label: 'RAG Pipelines', icon: 'book-open' }, + { metadataType: 'agent', label: 'Agents', icon: 'bot' }, + { metadataType: 'tool', label: 'Tools', icon: 'wrench' }, + { metadataType: 'ragPipeline', label: 'RAG Pipelines', icon: 'book-open' }, ], }, }), activate(api) { - api.registerMetadataIcon('agents', Bot, 'Agents'); - api.registerMetadataIcon('ragPipelines', BookOpen, 'RAG Pipelines'); + api.registerMetadataIcon('agent', Bot, 'Agents'); + api.registerMetadataIcon('tool', Wrench, 'Tools'); + api.registerMetadataIcon('ragPipeline', BookOpen, 'RAG Pipelines'); }, }; diff --git a/apps/studio/src/plugins/built-in/default-plugin.tsx b/apps/studio/src/plugins/built-in/default-plugin.tsx index 4870ce6ec..57877bad2 100644 --- a/apps/studio/src/plugins/built-in/default-plugin.tsx +++ b/apps/studio/src/plugins/built-in/default-plugin.tsx @@ -30,6 +30,8 @@ const METADATA_TO_EXPORT_TYPE: Record = { flows: 'flow', agent: 'agent', agents: 'agent', + tool: 'tool', + tools: 'tool', app: 'app', apps: 'app', }; diff --git a/apps/studio/test/verify-metadata.test.ts b/apps/studio/test/verify-metadata.test.ts index 0b2499984..b1db9e0bd 100644 --- a/apps/studio/test/verify-metadata.test.ts +++ b/apps/studio/test/verify-metadata.test.ts @@ -18,13 +18,13 @@ describe('Metadata Service Integration', () => { const response: any = await client.meta.getItems('object'); // Response after unwrap: { type: 'object', items: [...] } const objects = response.items || response.data || response; - + console.log('Fetched Objects:', objects.map((o: any) => o.name)); - + expect(objects).toBeDefined(); expect(Array.isArray(objects)).toBe(true); expect(objects.length).toBeGreaterThan(0); - + // Object name without namespace prefix (studio config has no namespace) const task = objects.find((o: any) => o.name === 'task'); expect(task).toBeDefined(); @@ -35,15 +35,70 @@ describe('Metadata Service Integration', () => { // Use short name 'task' which resolves via registry fallback const response: any = await client.meta.getItem('object', 'task'); const def = response.data || response; - + expect(def).toBeDefined(); expect(def.name).toBe('task'); expect(def.fields).toBeDefined(); - + // Check if fields are parsed correctly (client might return Map or Object depending on version) // Adjust expectation based on ObjectStackClient behavior // Assuming it matches the raw JSON or a wrapper console.log('Fields:', def.fields); // expect(Object.keys(def.fields).length).toBeGreaterThan(0); }); + + it('should include "agent" in metadata types', async () => { + const { client } = env; + const response: any = await client.meta.getTypes(); + const types = response.types || response.data || response; + + expect(types).toBeDefined(); + expect(Array.isArray(types)).toBe(true); + expect(types).toContain('agent'); + }); + + it('should include "tool" in metadata types', async () => { + const { client } = env; + const response: any = await client.meta.getTypes(); + const types = response.types || response.data || response; + + expect(types).toBeDefined(); + expect(Array.isArray(types)).toBe(true); + expect(types).toContain('tool'); + }); + + it('should list registered agents via client.meta.getItems("agent")', async () => { + const { client } = env; + const response: any = await client.meta.getItems('agent'); + const agents = response.items || response.data || response; + + console.log('Fetched Agents:', agents?.map?.((a: any) => a.name) || agents); + + expect(agents).toBeDefined(); + expect(Array.isArray(agents)).toBe(true); + expect(agents.length).toBeGreaterThan(0); + + // Check for built-in agents + const agentNames = agents.map((a: any) => a.name); + expect(agentNames).toContain('data_chat'); + expect(agentNames).toContain('metadata_assistant'); + }); + + it('should list registered tools via client.meta.getItems("tool")', async () => { + const { client } = env; + const response: any = await client.meta.getItems('tool'); + const tools = response.items || response.data || response; + + console.log('Fetched Tools:', tools?.map?.((t: any) => t.name) || tools); + + expect(tools).toBeDefined(); + expect(Array.isArray(tools)).toBe(true); + expect(tools.length).toBeGreaterThan(0); + + // Check for some built-in tools + const toolNames = tools.map((t: any) => t.name); + expect(toolNames).toContain('create_object'); + expect(toolNames).toContain('list_objects'); + expect(toolNames).toContain('query_records'); + }); }); diff --git a/packages/metadata/src/plugin.ts b/packages/metadata/src/plugin.ts index afd627abf..802f0dd5d 100644 --- a/packages/metadata/src/plugin.ts +++ b/packages/metadata/src/plugin.ts @@ -46,6 +46,7 @@ export class MetadataPlugin implements Plugin { // Register Metadata Manager as the primary metadata service provider. ctx.registerService('metadata', this.manager); + console.log('[MetadataPlugin] Registered metadata service, has getRegisteredTypes:', typeof this.manager.getRegisteredTypes); // Register metadata system objects via the manifest service (if available). // MetadataPlugin may init before ObjectQLPlugin, so wrap in try/catch. diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index 76407929b..161591bb7 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -180,9 +180,22 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } async getMetaTypes() { - return { - types: SchemaRegistry.getRegisteredTypes() - }; + const schemaTypes = SchemaRegistry.getRegisteredTypes(); + + // Also include types from MetadataService (runtime-registered: agent, tool, etc.) + let runtimeTypes: string[] = []; + try { + const services = this.getServicesRegistry?.(); + const metadataService = services?.get('metadata'); + if (metadataService && typeof metadataService.getRegisteredTypes === 'function') { + runtimeTypes = await metadataService.getRegisteredTypes(); + } + } catch { + // MetadataService not available + } + + const allTypes = Array.from(new Set([...schemaTypes, ...runtimeTypes])); + return { types: allTypes }; } async getMetaItems(request: { type: string; packageId?: string }) { @@ -232,6 +245,34 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } } + // Merge with MetadataService (runtime-registered items: agents, tools, etc.) + try { + const services = this.getServicesRegistry?.(); + const metadataService = services?.get('metadata'); + if (metadataService && typeof metadataService.list === 'function') { + const runtimeItems = await metadataService.list(request.type); + if (runtimeItems && runtimeItems.length > 0) { + // Merge, avoiding duplicates by name + const itemMap = new Map(); + for (const item of items) { + const entry = item as any; + if (entry && typeof entry === 'object' && 'name' in entry) { + itemMap.set(entry.name, entry); + } + } + for (const item of runtimeItems) { + const entry = item as any; + if (entry && typeof entry === 'object' && 'name' in entry) { + itemMap.set(entry.name, entry); + } + } + items = Array.from(itemMap.values()); + } + } + } catch { + // MetadataService not available or doesn't support this type + } + return { type: request.type, items @@ -277,6 +318,19 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } } + // Fallback to MetadataService for runtime-registered items (agents, tools, etc.) + if (item === undefined) { + try { + const services = this.getServicesRegistry?.(); + const metadataService = services?.get('metadata'); + if (metadataService && typeof metadataService.get === 'function') { + item = await metadataService.get(request.type, request.name); + } + } catch { + // MetadataService not available + } + } + return { type: request.type, name: request.name, diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index b410057c2..f36eb5e20 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -341,22 +341,82 @@ export class HttpDispatcher { // GET /metadata/types if (parts[0] === 'types') { - // Try protocol service for dynamic types + // PRIORITY 1: Try MetadataService directly (includes both typeRegistry with agent/tool AND runtime-registered types) + console.log('[HttpDispatcher] Attempting to resolve MetadataService...'); + console.log('[HttpDispatcher] Available kernel methods:', { + hasGetServiceAsync: typeof this.kernel.getServiceAsync === 'function', + hasGetService: typeof this.kernel.getService === 'function', + hasContext: !!this.kernel.context, + hasContextGetService: typeof this.kernel.context?.getService === 'function', + }); + + // Try all service resolution paths with detailed logging + let metadataService: any = null; + + // Path 1: kernel.getServiceAsync + if (typeof this.kernel.getServiceAsync === 'function') { + try { + metadataService = await this.kernel.getServiceAsync('metadata'); + console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") returned:', !!metadataService); + } catch (e: any) { + console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") failed:', e.message); + } + } + + // Path 2: kernel.getService (if not found via async) + if (!metadataService && typeof this.kernel.getService === 'function') { + try { + metadataService = await this.kernel.getService('metadata'); + console.log('[HttpDispatcher] kernel.getService("metadata") returned:', !!metadataService); + } catch (e: any) { + console.log('[HttpDispatcher] kernel.getService("metadata") failed:', e.message); + } + } + + // Path 3: kernel.context.getService (if not found) + if (!metadataService && this.kernel.context?.getService) { + try { + metadataService = await this.kernel.context.getService('metadata'); + console.log('[HttpDispatcher] kernel.context.getService("metadata") returned:', !!metadataService); + } catch (e: any) { + console.log('[HttpDispatcher] kernel.context.getService("metadata") failed:', e.message); + } + } + + console.log('[HttpDispatcher] Final metadataService:', !!metadataService, 'has getRegisteredTypes:', typeof (metadataService as any)?.getRegisteredTypes); + + if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') { + try { + const types = await (metadataService as any).getRegisteredTypes(); + console.log('[HttpDispatcher] MetadataService.getRegisteredTypes() returned:', types); + return { handled: true, response: this.success({ types }) }; + } catch (e: any) { + // Log error but continue to fallbacks + console.warn('[HttpDispatcher] MetadataService.getRegisteredTypes() failed:', e.message, e.stack); + } + } else { + console.log('[HttpDispatcher] MetadataService not available or missing getRegisteredTypes, falling back to protocol service'); + } + // PRIORITY 2: Try protocol service (returns SchemaRegistry types only - missing agent/tool) const protocol = await this.resolveService('protocol'); if (protocol && typeof protocol.getMetaTypes === 'function') { const result = await protocol.getMetaTypes({}); + console.log('[HttpDispatcher] Protocol service returned types:', result); return { handled: true, response: this.success(result) }; } - // Fallback: ask broker for registered types + // PRIORITY 3: ask broker for registered types if (broker) { try { const data = await broker.call('metadata.types', {}, { request: context.request }); + console.log('[HttpDispatcher] Broker returned types:', data); return { handled: true, response: this.success(data) }; - } catch { + } catch (e) { + console.log('[HttpDispatcher] Broker call failed:', e); // fall through to hardcoded defaults } } // Last resort: hardcoded defaults + console.warn('[HttpDispatcher] Falling back to hardcoded defaults for metadata types'); return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) }; } @@ -464,7 +524,7 @@ export class HttpDispatcher { const typeOrName = parts[0]; // Extract optional package filter from query string const packageId = query?.package || undefined; - + // Try protocol service first for any type const protocol = await this.resolveService('protocol'); if (protocol && typeof protocol.getMetaItems === 'function') { @@ -479,6 +539,22 @@ export class HttpDispatcher { } } + // Try MetadataService directly for runtime-registered metadata (agents, tools, etc.) + const metadataService = await this.getService(CoreServiceName.enum.metadata); + if (metadataService && typeof (metadataService as any).list === 'function') { + try { + const items = await (metadataService as any).list(typeOrName); + if (items && items.length > 0) { + return { handled: true, response: this.success({ type: typeOrName, items }) }; + } + } catch (e: any) { + // MetadataService doesn't know this type or failed, continue to other fallbacks + // Sanitize typeOrName to prevent log injection (CodeQL warning) + const sanitizedType = String(typeOrName).replace(/[\r\n\t]/g, ''); + console.debug(`[HttpDispatcher] MetadataService.list() failed for type:`, sanitizedType, 'error:', e.message); + } + } + // Try broker for the type if (broker) { try { diff --git a/packages/services/service-ai/src/plugin.ts b/packages/services/service-ai/src/plugin.ts index 7cfcf309b..cf079bee3 100644 --- a/packages/services/service-ai/src/plugin.ts +++ b/packages/services/service-ai/src/plugin.ts @@ -250,7 +250,9 @@ export class AIServicePlugin implements Plugin { let metadataService: IMetadataService | undefined; try { metadataService = ctx.getService('metadata'); - } catch { + console.log('[AI Plugin] Retrieved metadata service:', !!metadataService, 'has getRegisteredTypes:', typeof (metadataService as any)?.getRegisteredTypes); + } catch (e: any) { + console.log('[AI] Metadata service not available:', e.message); ctx.logger.debug('[AI] Metadata service not available'); } @@ -261,18 +263,40 @@ export class AIServicePlugin implements Plugin { registerDataTools(this.service.toolRegistry, { dataEngine }); ctx.logger.info('[AI] Built-in data tools registered'); - // Register the built-in data_chat agent (requires metadata service) + // Register data tools as metadata (for Studio visibility) if (metadataService) { - const agentExists = - typeof metadataService.exists === 'function' - ? await metadataService.exists('agent', DATA_CHAT_AGENT.name) - : false; + const { DATA_TOOL_DEFINITIONS } = await import('./tools/data-tools.js'); + for (const toolDef of DATA_TOOL_DEFINITIONS) { + const toolExists = + typeof metadataService.exists === 'function' + ? await metadataService.exists('tool', toolDef.name) + : false; + + if (!toolExists) { + await metadataService.register('tool', toolDef.name, toolDef); + } + } + ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS.length} data tools registered as metadata`); + } - if (!agentExists) { - await metadataService.register('agent', DATA_CHAT_AGENT.name, DATA_CHAT_AGENT); - ctx.logger.info('[AI] data_chat agent registered'); - } else { - ctx.logger.debug('[AI] data_chat agent already exists, skipping auto-registration'); + // Register the built-in data_chat agent (requires metadata service) + if (metadataService) { + try { + const agentExists = + typeof metadataService.exists === 'function' + ? await metadataService.exists('agent', DATA_CHAT_AGENT.name) + : false; + + if (!agentExists) { + await metadataService.register('agent', DATA_CHAT_AGENT.name, DATA_CHAT_AGENT); + console.log('[AI] Registered data_chat agent to metadataService'); + ctx.logger.info('[AI] data_chat agent registered'); + } else { + console.log('[AI] data_chat agent already exists, skipping'); + ctx.logger.debug('[AI] data_chat agent already exists, skipping auto-registration'); + } + } catch (err) { + ctx.logger.warn('[AI] Failed to register data_chat agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) }); } } } @@ -286,17 +310,37 @@ export class AIServicePlugin implements Plugin { registerMetadataTools(this.service.toolRegistry, { metadataService }); ctx.logger.info('[AI] Built-in metadata tools registered'); + // Register metadata tools as metadata (for Studio visibility) + const { METADATA_TOOL_DEFINITIONS } = await import('./tools/metadata-tools.js'); + for (const toolDef of METADATA_TOOL_DEFINITIONS) { + const toolExists = + typeof metadataService.exists === 'function' + ? await metadataService.exists('tool', toolDef.name) + : false; + + if (!toolExists) { + await metadataService.register('tool', toolDef.name, toolDef); + } + } + ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS.length} metadata tools registered as metadata`); + // Register the built-in metadata_assistant agent - const agentExists = - typeof metadataService.exists === 'function' - ? await metadataService.exists('agent', METADATA_ASSISTANT_AGENT.name) - : false; - - if (!agentExists) { - await metadataService.register('agent', METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT); - ctx.logger.info('[AI] metadata_assistant agent registered'); - } else { - ctx.logger.debug('[AI] metadata_assistant agent already exists, skipping auto-registration'); + try { + const agentExists = + typeof metadataService.exists === 'function' + ? await metadataService.exists('agent', METADATA_ASSISTANT_AGENT.name) + : false; + + if (!agentExists) { + await metadataService.register('agent', METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT); + console.log('[AI] Registered metadata_assistant agent to metadataService'); + ctx.logger.info('[AI] metadata_assistant agent registered'); + } else { + console.log('[AI] metadata_assistant agent already exists, skipping'); + ctx.logger.debug('[AI] metadata_assistant agent already exists, skipping auto-registration'); + } + } catch (err) { + ctx.logger.warn('[AI] Failed to register metadata_assistant agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) }); } } catch (err) { ctx.logger.debug('[AI] Failed to register metadata tools', err instanceof Error ? err : undefined);