diff --git a/changelog.md b/changelog.md index 670f555..15903ae 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-01-20 - 1.8.0 - feat(tools) +add ToolRegistry, ToolSearchTool and ExpertTool to support on-demand tool visibility, discovery, activation, and expert/subagent tooling; extend DualAgentOrchestrator API and interfaces to manage tool lifecycle + +- Introduce ToolRegistry to manage tool registration, visibility (initial vs on-demand), activation, initialization, cleanup, and search +- Add ToolSearchTool providing search/list/activate/details actions for discovering and enabling on-demand tools +- Add ExpertTool to wrap a DualAgentOrchestrator as a sub-agent (expert) tool and the IExpertConfig interface +- Extend DualAgentOrchestrator API: registerTool(tool, options?), registerExpert(config), enableToolSearch(), getRegistry(); registerStandardTools/start now initialize visible tools via registry +- Add IToolRegistrationOptions and IToolMetadata/IExpertConfig types to smartagent.interfaces and export ToolRegistry/ToolSearchTool/ExpertTool in public entry points +- Documentation updates (readme) describing tool visibility, tool search, and expert/subagent system + ## 2026-01-20 - 1.7.0 - feat(docs) document native tool calling support and update README to clarify standard and additional tools diff --git a/readme.hints.md b/readme.hints.md index 0828b57..b11f84b 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -7,6 +7,7 @@ - **DualAgentOrchestrator**: Main entry point, coordinates Driver and Guardian agents - **DriverAgent**: Reasons about tasks, plans steps, proposes tool calls (supports both XML and native tool calling) - **GuardianAgent**: Evaluates tool calls against configured policies +- **ToolRegistry**: Manages tool lifecycle, visibility, and discovery - **BaseToolWrapper**: Base class for creating custom tools - **plugins.ts**: Imports and re-exports smartai and other dependencies @@ -19,6 +20,49 @@ ## Additional Tools (must register manually) 6. **JsonValidatorTool** - JSON validation and formatting (NOT in registerStandardTools) +7. **ToolSearchTool** - AI-facing interface for tool discovery and activation +8. **ExpertTool** - Wraps a DualAgentOrchestrator as a specialized expert tool + +## Tool Visibility System +Tools can be registered with visibility modes: +- **initial**: Always visible to Driver, included in system prompt (default) +- **on-demand**: Only discoverable via search, must be activated before use + +```typescript +// Register with visibility options +orchestrator.registerTool(myTool, { + visibility: 'on-demand', + tags: ['database', 'sql'], + category: 'data' +}); +``` + +## Expert/SubAgent System +Experts are specialized agents wrapped as tools, enabling hierarchical agent architectures: + +```typescript +orchestrator.registerExpert({ + name: 'code_reviewer', + description: 'Reviews code for quality and best practices', + systemMessage: 'You are a code review expert...', + guardianPolicy: 'Allow read-only file access', + tools: [new FilesystemTool()], + visibility: 'on-demand', + tags: ['code', 'review'] +}); +``` + +## Tool Search +Enable tool discovery for the Driver: + +```typescript +orchestrator.enableToolSearch(); +// Driver can now use: +// - tools.search({"query": "database"}) +// - tools.list({}) +// - tools.activate({"name": "database_expert"}) +// - tools.details({"name": "filesystem"}) +``` ## Key Features - Token streaming support (`onToken` callback) @@ -28,6 +72,9 @@ - Result truncation with configurable limits - History windowing to manage token usage - **Native tool calling mode** (`useNativeToolCalling: true`) for providers like Ollama +- **Tool visibility system** (initial vs on-demand) +- **Expert/SubAgent system** for hierarchical agents +- **Tool search and discovery** via ToolSearchTool ## Native Tool Calling When `useNativeToolCalling` is enabled: diff --git a/readme.md b/readme.md index d051160..c25532b 100644 --- a/readme.md +++ b/readme.md @@ -37,19 +37,19 @@ flowchart TB end subgraph Orchestrator["DualAgentOrchestrator"] + Registry["ToolRegistry
Visibility & Lifecycle"] Driver["Driver Agent
Reason + Plan"] Guardian["Guardian Agent
Evaluate against policy"] Driver -->|"tool call proposal"| Guardian Guardian -->|"approve / reject + feedback"| Driver + Registry -->|"visible tools"| Driver end - subgraph Tools["Standard Tools"] - FS["Filesystem"] - HTTP["HTTP"] - Shell["Shell"] - Browser["Browser"] - Deno["Deno"] + subgraph Tools["Tools"] + Initial["Initial Tools
Always visible"] + OnDemand["On-Demand Tools
Discoverable via search"] + Experts["Expert SubAgents
Specialized agents as tools"] end Task --> Orchestrator @@ -266,6 +266,157 @@ orchestrator.registerTool(new JsonValidatorTool()); ``` +### 🔍 ToolSearchTool + +Enable the Driver to discover and activate on-demand tools at runtime. + +**Actions**: `search`, `list`, `activate`, `details` + +```typescript +// Enable tool search (adds the 'tools' tool) +orchestrator.enableToolSearch(); +``` + +```typescript +// Search for tools by capability + + tools + search + {"query": "database"} + + +// List all available tools + + tools + list + {} + + +// Activate an on-demand tool + + tools + activate + {"name": "database_expert"} + + +// Get detailed information about a tool + + tools + details + {"name": "filesystem"} + +``` + +### 🧠 ExpertTool (SubAgents) + +Create specialized sub-agents that can be invoked as tools. Experts are complete `DualAgentOrchestrator` instances wrapped as tools, enabling hierarchical agent architectures. + +**Actions**: `consult` + +```typescript +// Register an expert for code review +orchestrator.registerExpert({ + name: 'code_reviewer', + description: 'Reviews code for quality, bugs, and best practices', + systemMessage: `You are an expert code reviewer. Analyze code for: + - Bugs and potential issues + - Code style and best practices + - Performance concerns + - Security vulnerabilities`, + guardianPolicy: 'Allow read-only file access within the workspace', + tools: [new FilesystemTool()], + visibility: 'on-demand', // Only available via tool search + tags: ['code', 'review', 'quality'], + category: 'expert', +}); +``` + +```typescript +// Consult an expert + + code_reviewer + consult + { + "task": "Review this function for potential issues", + "context": "This is a user authentication handler" + } + +``` + +## 🎯 Tool Visibility System + +SmartAgent supports **tool visibility modes** for scalable agent architectures: + +- **`initial`** (default): Tool is visible to the Driver from the start, included in the system prompt +- **`on-demand`**: Tool is hidden until explicitly activated via `tools.activate()` + +This enables you to have many specialized tools/experts without overwhelming the Driver's context. + +```typescript +// Register a tool with on-demand visibility +orchestrator.registerTool(new MySpecializedTool(), { + visibility: 'on-demand', + tags: ['specialized', 'database'], + category: 'data', +}); + +// Enable tool search so Driver can discover and activate on-demand tools +orchestrator.enableToolSearch(); + +// The Driver can now: +// 1. tools.search({"query": "database"}) -> finds MySpecializedTool +// 2. tools.activate({"name": "myspecialized"}) -> enables it +// 3. myspecialized.action({...}) -> use the tool +``` + +### Expert SubAgent Example + +```typescript +const orchestrator = new DualAgentOrchestrator({ + openaiToken: 'sk-...', + defaultProvider: 'openai', + guardianPolicyPrompt: 'Allow safe operations...', +}); + +orchestrator.registerStandardTools(); +orchestrator.enableToolSearch(); + +// Initial expert (always visible) +orchestrator.registerExpert({ + name: 'code_assistant', + description: 'Helps with coding tasks and code generation', + systemMessage: 'You are a helpful coding assistant...', + guardianPolicy: 'Allow read-only file access', + tools: [new FilesystemTool()], +}); + +// On-demand experts (discoverable via search) +orchestrator.registerExpert({ + name: 'database_expert', + description: 'Database design, optimization, and query analysis', + systemMessage: 'You are a database expert...', + guardianPolicy: 'Allow read-only operations', + visibility: 'on-demand', + tags: ['database', 'sql', 'optimization'], +}); + +orchestrator.registerExpert({ + name: 'security_auditor', + description: 'Security vulnerability assessment and best practices', + systemMessage: 'You are a security expert...', + guardianPolicy: 'Allow read-only file access', + visibility: 'on-demand', + tags: ['security', 'audit', 'vulnerabilities'], +}); + +await orchestrator.start(); + +// Now the Driver can: +// - Use code_assistant directly +// - Search for "database" and activate database_expert when needed +// - Search for "security" and activate security_auditor when needed +``` + ## 🎥 Streaming Support SmartAgent supports token-by-token streaming for real-time output during LLM generation: @@ -608,12 +759,15 @@ const orchestrator = new DualAgentOrchestrator({ | `stop()` | Cleanup all tools and resources | | `run(task, options?)` | Execute a task with optional images for vision | | `continueTask(input)` | Continue a task with user input | -| `registerTool(tool)` | Register a custom tool | +| `registerTool(tool, options?)` | Register a custom tool with optional visibility settings | | `registerStandardTools()` | Register all built-in tools (Filesystem, HTTP, Shell, Browser, Deno) | | `registerScopedFilesystemTool(basePath, excludePatterns?)` | Register filesystem tool with path restriction | +| `registerExpert(config)` | Register a specialized sub-agent as a tool | +| `enableToolSearch()` | Enable tool discovery and activation for the Driver | | `setGuardianPolicy(policy)` | Update Guardian policy at runtime | | `getHistory()` | Get conversation history | | `getToolNames()` | Get list of registered tool names | +| `getRegistry()` | Get the ToolRegistry for advanced operations | | `isActive()` | Check if orchestrator is running | ### Exports @@ -624,6 +778,9 @@ export { DualAgentOrchestrator } from '@push.rocks/smartagent'; export { DriverAgent } from '@push.rocks/smartagent'; export { GuardianAgent } from '@push.rocks/smartagent'; +// Tool Registry +export { ToolRegistry } from '@push.rocks/smartagent'; + // Tools export { BaseToolWrapper } from '@push.rocks/smartagent'; export { FilesystemTool, type IFilesystemToolOptions } from '@push.rocks/smartagent'; @@ -632,9 +789,11 @@ export { ShellTool } from '@push.rocks/smartagent'; export { BrowserTool } from '@push.rocks/smartagent'; export { DenoTool, type TDenoPermission } from '@push.rocks/smartagent'; export { JsonValidatorTool } from '@push.rocks/smartagent'; +export { ToolSearchTool } from '@push.rocks/smartagent'; +export { ExpertTool } from '@push.rocks/smartagent'; // Types and interfaces -export * from '@push.rocks/smartagent'; // All interfaces +export * from '@push.rocks/smartagent'; // All interfaces (IExpertConfig, IToolMetadata, etc.) // Re-exported from @push.rocks/smartai export { type ISmartAiOptions, type TProvider, type ChatMessage, type ChatOptions, type ChatResponse }; diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index fe2841e..0ae1061 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartagent', - version: '1.7.0', + version: '1.8.0', description: 'an agentic framework built on top of @push.rocks/smartai' } diff --git a/ts/index.ts b/ts/index.ts index 1bf53a7..ff94c9e 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -7,6 +7,9 @@ export { DualAgentOrchestrator } from './smartagent.classes.dualagent.js'; export { DriverAgent } from './smartagent.classes.driveragent.js'; export { GuardianAgent } from './smartagent.classes.guardianagent.js'; +// Export tool registry and related classes +export { ToolRegistry } from './smartagent.classes.toolregistry.js'; + // Export base tool class for custom tool creation export { BaseToolWrapper } from './smartagent.tools.base.js'; @@ -18,6 +21,10 @@ export { BrowserTool } from './smartagent.tools.browser.js'; export { DenoTool, type TDenoPermission } from './smartagent.tools.deno.js'; export { JsonValidatorTool } from './smartagent.tools.json.js'; +// Export tool search and expert tools +export { ToolSearchTool } from './smartagent.tools.search.js'; +export { ExpertTool } from './smartagent.tools.expert.js'; + // Export all interfaces export * from './smartagent.interfaces.js'; diff --git a/ts/smartagent.classes.dualagent.ts b/ts/smartagent.classes.dualagent.ts index 65321f5..1e7651f 100644 --- a/ts/smartagent.classes.dualagent.ts +++ b/ts/smartagent.classes.dualagent.ts @@ -8,6 +8,9 @@ import { HttpTool } from './smartagent.tools.http.js'; import { ShellTool } from './smartagent.tools.shell.js'; import { BrowserTool } from './smartagent.tools.browser.js'; import { DenoTool } from './smartagent.tools.deno.js'; +import { ToolRegistry } from './smartagent.classes.toolregistry.js'; +import { ToolSearchTool } from './smartagent.tools.search.js'; +import { ExpertTool } from './smartagent.tools.expert.js'; /** * DualAgentOrchestrator - Coordinates Driver and Guardian agents @@ -20,7 +23,7 @@ export class DualAgentOrchestrator { private guardianProvider: plugins.smartai.MultiModalModel; private driver: DriverAgent; private guardian: GuardianAgent; - private tools: Map = new Map(); + private registry: ToolRegistry = new ToolRegistry(); private isRunning = false; private conversationHistory: interfaces.IAgentMessage[] = []; private ownsSmartAi = true; // true if we created the SmartAi instance, false if it was provided @@ -125,19 +128,55 @@ export class DualAgentOrchestrator { } /** - * Register a custom tool + * Register a custom tool with optional visibility settings */ - public registerTool(tool: BaseToolWrapper): void { - this.tools.set(tool.name, tool); - // Register with agents if they exist (they're created in start()) - if (this.driver) { - this.driver.registerTool(tool); - } - if (this.guardian) { - this.guardian.registerTool(tool); + public registerTool( + tool: BaseToolWrapper, + options?: interfaces.IToolRegistrationOptions + ): void { + this.registry.register(tool, options); + + // If initial visibility and agents exist, register with them + const visibility = options?.visibility ?? 'initial'; + if (visibility === 'initial') { + if (this.driver) { + this.driver.registerTool(tool); + } + if (this.guardian) { + this.guardian.registerTool(tool); + } } } + /** + * Register an expert (subagent) as a tool + */ + public registerExpert(config: interfaces.IExpertConfig): void { + const expert = new ExpertTool(config, this.smartai); + this.registerTool(expert, { + visibility: config.visibility, + tags: config.tags, + category: config.category ?? 'expert', + }); + } + + /** + * Enable tool search functionality + * This adds a 'tools' tool that allows the Driver to discover and activate on-demand tools + */ + public enableToolSearch(): void { + const searchTool = new ToolSearchTool(this.registry, (tool) => { + // Callback when an on-demand tool is activated + if (this.driver) { + this.driver.registerTool(tool); + } + if (this.guardian) { + this.guardian.registerTool(tool); + } + }); + this.registerTool(searchTool); // Always initial visibility + } + /** * Register all standard tools */ @@ -193,19 +232,14 @@ export class DualAgentOrchestrator { }); this.guardian = new GuardianAgent(this.guardianProvider, this.options.guardianPolicyPrompt); - // Register any tools that were added before start() with the agents - for (const tool of this.tools.values()) { + // Register visible tools with agents + for (const tool of this.registry.getVisibleTools()) { this.driver.registerTool(tool); this.guardian.registerTool(tool); } - // Initialize all tools - const initPromises: Promise[] = []; - for (const tool of this.tools.values()) { - initPromises.push(tool.initialize()); - } - - await Promise.all(initPromises); + // Initialize visible tools + await this.registry.initializeVisibleTools(); this.isRunning = true; } @@ -213,13 +247,7 @@ export class DualAgentOrchestrator { * Cleanup all tools */ public async stop(): Promise { - const cleanupPromises: Promise[] = []; - - for (const tool of this.tools.values()) { - cleanupPromises.push(tool.cleanup()); - } - - await Promise.all(cleanupPromises); + await this.registry.cleanup(); // Only stop smartai if we created it (don't stop external instances) if (this.ownsSmartAi) { @@ -432,7 +460,7 @@ Please output the exact XML format above.` }); // Execute the tool - const tool = this.tools.get(proposal.toolName); + const tool = this.registry.getTool(proposal.toolName); if (!tool) { const errorMessage = `Tool "${proposal.toolName}" not found.`; driverResponse = await this.driver.continueWithMessage( @@ -652,6 +680,13 @@ Please output the exact XML format above.` * Get registered tool names */ public getToolNames(): string[] { - return Array.from(this.tools.keys()); + return this.registry.getAllMetadata().map((m) => m.name); + } + + /** + * Get the tool registry for advanced operations + */ + public getRegistry(): ToolRegistry { + return this.registry; } } diff --git a/ts/smartagent.classes.toolregistry.ts b/ts/smartagent.classes.toolregistry.ts new file mode 100644 index 0000000..bc67582 --- /dev/null +++ b/ts/smartagent.classes.toolregistry.ts @@ -0,0 +1,188 @@ +import * as interfaces from './smartagent.interfaces.js'; +import { BaseToolWrapper } from './smartagent.tools.base.js'; + +/** + * ToolRegistry - Manages tool registration, visibility, and lifecycle + * + * Responsibilities: + * - Track all registered tools with their metadata + * - Manage visibility (initial vs on-demand) + * - Handle activation of on-demand tools + * - Provide search functionality + */ +export class ToolRegistry { + private tools: Map = new Map(); + private metadata: Map = new Map(); + private activated: Set = new Set(); + + /** + * Register a tool with optional visibility settings + */ + register(tool: BaseToolWrapper, options: interfaces.IToolRegistrationOptions = {}): void { + const visibility = options.visibility ?? 'initial'; + + this.tools.set(tool.name, tool); + this.metadata.set(tool.name, { + name: tool.name, + description: tool.description, + actions: tool.actions, + visibility, + isActivated: visibility === 'initial', + isInitialized: false, + tags: options.tags, + category: options.category, + }); + + if (visibility === 'initial') { + this.activated.add(tool.name); + } + } + + /** + * Get tools visible to the Driver (initial + activated on-demand) + */ + getVisibleTools(): BaseToolWrapper[] { + return Array.from(this.tools.entries()) + .filter(([name]) => this.activated.has(name)) + .map(([, tool]) => tool); + } + + /** + * Get all tools (for search results) + */ + getAllTools(): BaseToolWrapper[] { + return Array.from(this.tools.values()); + } + + /** + * Get a specific tool by name + */ + getTool(name: string): BaseToolWrapper | undefined { + return this.tools.get(name); + } + + /** + * Get metadata for a tool + */ + getMetadata(name: string): interfaces.IToolMetadata | undefined { + return this.metadata.get(name); + } + + /** + * Get all metadata + */ + getAllMetadata(): interfaces.IToolMetadata[] { + return Array.from(this.metadata.values()); + } + + /** + * Search tools by query (matches name, description, tags, action names) + */ + search(query: string): interfaces.IToolMetadata[] { + const q = query.toLowerCase(); + return this.getAllMetadata().filter((meta) => { + if (meta.name.toLowerCase().includes(q)) return true; + if (meta.description.toLowerCase().includes(q)) return true; + if (meta.tags?.some((t) => t.toLowerCase().includes(q))) return true; + if (meta.category?.toLowerCase().includes(q)) return true; + if ( + meta.actions.some( + (a) => a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) + ) + ) + return true; + return false; + }); + } + + /** + * Activate an on-demand tool + */ + async activate(name: string): Promise<{ success: boolean; error?: string }> { + const tool = this.tools.get(name); + const meta = this.metadata.get(name); + + if (!tool || !meta) { + return { success: false, error: `Tool "${name}" not found` }; + } + + if (this.activated.has(name)) { + return { success: true }; // Already activated + } + + // Initialize if not already initialized + if (!meta.isInitialized) { + await tool.initialize(); + meta.isInitialized = true; + } + + this.activated.add(name); + meta.isActivated = true; + + return { success: true }; + } + + /** + * Check if a tool is activated + */ + isActivated(name: string): boolean { + return this.activated.has(name); + } + + /** + * Initialize all initial (visible) tools + */ + async initializeVisibleTools(): Promise { + const promises: Promise[] = []; + + for (const [name, tool] of this.tools) { + const meta = this.metadata.get(name); + if (meta && this.activated.has(name) && !meta.isInitialized) { + promises.push( + tool.initialize().then(() => { + meta.isInitialized = true; + }) + ); + } + } + + await Promise.all(promises); + } + + /** + * Cleanup all initialized tools + */ + async cleanup(): Promise { + const promises: Promise[] = []; + + for (const [name, tool] of this.tools) { + const meta = this.metadata.get(name); + if (meta?.isInitialized) { + promises.push(tool.cleanup()); + } + } + + await Promise.all(promises); + } + + /** + * Check if a tool exists in the registry + */ + has(name: string): boolean { + return this.tools.has(name); + } + + /** + * Get the number of registered tools + */ + get size(): number { + return this.tools.size; + } + + /** + * Get the number of activated tools + */ + get activatedCount(): number { + return this.activated.size; + } +} diff --git a/ts/smartagent.interfaces.ts b/ts/smartagent.interfaces.ts index 3d3363e..a193e73 100644 --- a/ts/smartagent.interfaces.ts +++ b/ts/smartagent.interfaces.ts @@ -1,5 +1,65 @@ import * as plugins from './plugins.js'; +// ================================ +// Tool Visibility & Registry Types +// ================================ + +/** + * Tool visibility mode + * - 'initial': Conveyed to model in system prompt AND discoverable via search + * - 'on-demand': Only discoverable via search, must be activated before use + */ +export type TToolVisibility = 'initial' | 'on-demand'; + +/** + * Tool metadata for discovery and management + */ +export interface IToolMetadata { + name: string; + description: string; + actions: IToolAction[]; + visibility: TToolVisibility; + isActivated: boolean; + isInitialized: boolean; + tags?: string[]; + category?: string; +} + +/** + * Options when registering a tool + */ +export interface IToolRegistrationOptions { + visibility?: TToolVisibility; + tags?: string[]; + category?: string; +} + +/** + * Configuration for creating an Expert (SubAgent) + */ +export interface IExpertConfig { + /** Unique name for the expert */ + name: string; + /** Description of the expert's capabilities */ + description: string; + /** System message defining expert behavior */ + systemMessage: string; + /** Guardian policy for the expert's inner agent */ + guardianPolicy: string; + /** AI provider (defaults to parent's provider) */ + provider?: plugins.smartai.TProvider; + /** Tools available to this expert */ + tools?: IAgentToolWrapper[]; + /** Max iterations for expert tasks (default: 10) */ + maxIterations?: number; + /** Visibility mode (default: 'initial') */ + visibility?: TToolVisibility; + /** Searchable tags */ + tags?: string[]; + /** Category for grouping */ + category?: string; +} + // ================================ // Task Run Options // ================================ diff --git a/ts/smartagent.tools.expert.ts b/ts/smartagent.tools.expert.ts new file mode 100644 index 0000000..a8be7ae --- /dev/null +++ b/ts/smartagent.tools.expert.ts @@ -0,0 +1,144 @@ +import * as plugins from './plugins.js'; +import * as interfaces from './smartagent.interfaces.js'; +import { BaseToolWrapper } from './smartagent.tools.base.js'; + +// Forward declaration to avoid circular import at module load time +// The actual import happens lazily in initialize() +let DualAgentOrchestrator: typeof import('./smartagent.classes.dualagent.js').DualAgentOrchestrator; + +/** + * ExpertTool - A specialized agent wrapped as a tool + * + * Enables hierarchical agent architectures where the Driver can delegate + * complex tasks to specialized experts with their own tools and policies. + */ +export class ExpertTool extends BaseToolWrapper { + public name: string; + public description: string; + public actions: interfaces.IToolAction[] = [ + { + name: 'consult', + description: 'Delegate a task or question to this expert', + parameters: { + type: 'object', + properties: { + task: { type: 'string', description: 'The task or question for the expert' }, + context: { type: 'string', description: 'Additional context to help the expert' }, + }, + required: ['task'], + }, + }, + ]; + + private config: interfaces.IExpertConfig; + private smartAi: plugins.smartai.SmartAi; + private inner?: InstanceType; + + constructor(config: interfaces.IExpertConfig, smartAi: plugins.smartai.SmartAi) { + super(); + this.config = config; + this.smartAi = smartAi; + this.name = config.name; + this.description = config.description; + } + + async initialize(): Promise { + // Lazy import to avoid circular dependency + if (!DualAgentOrchestrator) { + const module = await import('./smartagent.classes.dualagent.js'); + DualAgentOrchestrator = module.DualAgentOrchestrator; + } + + this.inner = new DualAgentOrchestrator({ + smartAiInstance: this.smartAi, // Share SmartAi instance + defaultProvider: this.config.provider, + driverSystemMessage: this.config.systemMessage, + guardianPolicyPrompt: this.config.guardianPolicy, + maxIterations: this.config.maxIterations ?? 10, + }); + + // Register expert's tools + if (this.config.tools) { + for (const tool of this.config.tools) { + // Tools in the config are IAgentToolWrapper, but we need BaseToolWrapper + // Since all our tools extend BaseToolWrapper, this cast is safe + this.inner.registerTool(tool as BaseToolWrapper); + } + } + + await this.inner.start(); + this.isInitialized = true; + } + + async cleanup(): Promise { + if (this.inner) { + await this.inner.stop(); + this.inner = undefined; + } + this.isInitialized = false; + } + + async execute( + action: string, + params: Record + ): Promise { + this.validateAction(action); + this.ensureInitialized(); + + const task = params.task as string; + const context = params.context as string | undefined; + + const fullTask = context ? `Context: ${context}\n\nTask: ${task}` : task; + + try { + const result = await this.inner!.run(fullTask); + + return { + success: result.success, + result: { + response: result.result, + iterations: result.iterations, + status: result.status, + }, + summary: result.success + ? `Expert "${this.name}" completed (${result.iterations} iterations)` + : `Expert "${this.name}" failed: ${result.status}`, + }; + } catch (error) { + return { + success: false, + error: `Expert error: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } + + getCallSummary(action: string, params: Record): string { + const task = params.task as string; + const preview = task.length > 60 ? task.substring(0, 60) + '...' : task; + return `Consult ${this.name}: "${preview}"`; + } + + getToolExplanation(): string { + return `## Expert: ${this.name} +${this.description} + +### Usage: +Delegate tasks to this expert when you need specialized help. + +\`\`\` + + ${this.name} + consult + {"task": "Your question or task", "context": "Optional background"} + +\`\`\` +`; + } + + /** + * Get the expert's configuration + */ + getConfig(): interfaces.IExpertConfig { + return this.config; + } +} diff --git a/ts/smartagent.tools.search.ts b/ts/smartagent.tools.search.ts new file mode 100644 index 0000000..72e18a1 --- /dev/null +++ b/ts/smartagent.tools.search.ts @@ -0,0 +1,237 @@ +import * as interfaces from './smartagent.interfaces.js'; +import { BaseToolWrapper } from './smartagent.tools.base.js'; +import { ToolRegistry } from './smartagent.classes.toolregistry.js'; + +/** + * ToolSearchTool - AI-facing interface for discovering and activating tools + * + * This tool enables the Driver to: + * - Search for tools by capability + * - List all available tools + * - Activate on-demand tools + * - Get detailed information about specific tools + */ +export class ToolSearchTool extends BaseToolWrapper { + public name = 'tools'; + public description = + 'Search for and activate available tools and experts. Use this to discover specialized capabilities.'; + + public actions: interfaces.IToolAction[] = [ + { + name: 'search', + description: 'Search for tools by name, description, tags, or capabilities', + parameters: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + }, + required: ['query'], + }, + }, + { + name: 'list', + description: 'List all available tools grouped by visibility', + parameters: { type: 'object', properties: {} }, + }, + { + name: 'activate', + description: 'Activate an on-demand tool to make it available for use', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name of the tool to activate' }, + }, + required: ['name'], + }, + }, + { + name: 'details', + description: 'Get detailed information about a specific tool', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name of the tool' }, + }, + required: ['name'], + }, + }, + ]; + + private registry: ToolRegistry; + private onToolActivated?: (tool: BaseToolWrapper) => void; + + constructor(registry: ToolRegistry, onToolActivated?: (tool: BaseToolWrapper) => void) { + super(); + this.registry = registry; + this.onToolActivated = onToolActivated; + } + + async initialize(): Promise { + this.isInitialized = true; + } + + async cleanup(): Promise { + this.isInitialized = false; + } + + async execute( + action: string, + params: Record + ): Promise { + this.validateAction(action); + + switch (action) { + case 'search': + return this.handleSearch(params.query as string); + case 'list': + return this.handleList(); + case 'activate': + return this.handleActivate(params.name as string); + case 'details': + return this.handleDetails(params.name as string); + default: + return { success: false, error: `Unknown action: ${action}` }; + } + } + + private handleSearch(query: string): interfaces.IToolExecutionResult { + const results = this.registry.search(query); + return { + success: true, + result: results.map((m) => ({ + name: m.name, + description: m.description, + visibility: m.visibility, + isActivated: m.isActivated, + category: m.category, + tags: m.tags, + actionCount: m.actions.length, + })), + summary: `Found ${results.length} tools matching "${query}"`, + }; + } + + private handleList(): interfaces.IToolExecutionResult { + const all = this.registry.getAllMetadata(); + const initial = all.filter((m) => m.visibility === 'initial'); + const onDemand = all.filter((m) => m.visibility === 'on-demand'); + + return { + success: true, + result: { + initial: initial.map((m) => ({ + name: m.name, + description: m.description, + category: m.category, + })), + onDemand: onDemand.map((m) => ({ + name: m.name, + description: m.description, + category: m.category, + isActivated: m.isActivated, + })), + summary: `${initial.length} initial, ${onDemand.length} on-demand`, + }, + }; + } + + private async handleActivate(name: string): Promise { + const result = await this.registry.activate(name); + + if (result.success && this.onToolActivated) { + const tool = this.registry.getTool(name); + if (tool) { + this.onToolActivated(tool); + } + } + + return { + success: result.success, + result: result.success ? { name, message: `Tool "${name}" is now available` } : undefined, + error: result.error, + summary: result.success ? `Activated: ${name}` : result.error, + }; + } + + private handleDetails(name: string): interfaces.IToolExecutionResult { + const tool = this.registry.getTool(name); + const meta = this.registry.getMetadata(name); + + if (!tool || !meta) { + return { success: false, error: `Tool "${name}" not found` }; + } + + return { + success: true, + result: { + name: meta.name, + description: meta.description, + visibility: meta.visibility, + isActivated: meta.isActivated, + category: meta.category, + tags: meta.tags, + actions: meta.actions, + fullExplanation: tool.getToolExplanation(), + }, + }; + } + + getCallSummary(action: string, params: Record): string { + switch (action) { + case 'search': + return `Search tools: "${params.query}"`; + case 'list': + return 'List all tools'; + case 'activate': + return `Activate tool: ${params.name}`; + case 'details': + return `Get details: ${params.name}`; + default: + return `tools.${action}`; + } + } + + getToolExplanation(): string { + return `## Tool: tools +Search for and manage available tools and experts. + +### Actions: + +**search** - Find tools by capability +\`\`\` + + tools + search + {"query": "database"} + +\`\`\` + +**list** - List all tools grouped by visibility +\`\`\` + + tools + list + {} + +\`\`\` + +**activate** - Activate an on-demand tool +\`\`\` + + tools + activate + {"name": "database_expert"} + +\`\`\` + +**details** - Get full information about a tool +\`\`\` + + tools + details + {"name": "filesystem"} + +\`\`\` +`; + } +}