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
This commit is contained in:
10
changelog.md
10
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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
175
readme.md
175
readme.md
@@ -37,19 +37,19 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph Orchestrator["DualAgentOrchestrator"]
|
||||
Registry["ToolRegistry<br/><i>Visibility & Lifecycle</i>"]
|
||||
Driver["Driver Agent<br/><i>Reason + Plan</i>"]
|
||||
Guardian["Guardian Agent<br/><i>Evaluate against policy</i>"]
|
||||
|
||||
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<br/><i>Always visible</i>"]
|
||||
OnDemand["On-Demand Tools<br/><i>Discoverable via search</i>"]
|
||||
Experts["Expert SubAgents<br/><i>Specialized agents as tools</i>"]
|
||||
end
|
||||
|
||||
Task --> Orchestrator
|
||||
@@ -266,6 +266,157 @@ orchestrator.registerTool(new JsonValidatorTool());
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
### 🔍 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
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>search</action>
|
||||
<params>{"query": "database"}</params>
|
||||
</tool_call>
|
||||
|
||||
// List all available tools
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>list</action>
|
||||
<params>{}</params>
|
||||
</tool_call>
|
||||
|
||||
// Activate an on-demand tool
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>activate</action>
|
||||
<params>{"name": "database_expert"}</params>
|
||||
</tool_call>
|
||||
|
||||
// Get detailed information about a tool
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>details</action>
|
||||
<params>{"name": "filesystem"}</params>
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
### 🧠 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
|
||||
<tool_call>
|
||||
<tool>code_reviewer</tool>
|
||||
<action>consult</action>
|
||||
<params>{
|
||||
"task": "Review this function for potential issues",
|
||||
"context": "This is a user authentication handler"
|
||||
}</params>
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
## 🎯 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 };
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<string, BaseToolWrapper> = 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<void>[] = [];
|
||||
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<void> {
|
||||
const cleanupPromises: Promise<void>[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
188
ts/smartagent.classes.toolregistry.ts
Normal file
188
ts/smartagent.classes.toolregistry.ts
Normal file
@@ -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<string, BaseToolWrapper> = new Map();
|
||||
private metadata: Map<string, interfaces.IToolMetadata> = new Map();
|
||||
private activated: Set<string> = 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<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
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<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// ================================
|
||||
|
||||
144
ts/smartagent.tools.expert.ts
Normal file
144
ts/smartagent.tools.expert.ts
Normal file
@@ -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<typeof DualAgentOrchestrator>;
|
||||
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
if (this.inner) {
|
||||
await this.inner.stop();
|
||||
this.inner = undefined;
|
||||
}
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
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, unknown>): 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.
|
||||
|
||||
\`\`\`
|
||||
<tool_call>
|
||||
<tool>${this.name}</tool>
|
||||
<action>consult</action>
|
||||
<params>{"task": "Your question or task", "context": "Optional background"}</params>
|
||||
</tool_call>
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expert's configuration
|
||||
*/
|
||||
getConfig(): interfaces.IExpertConfig {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
237
ts/smartagent.tools.search.ts
Normal file
237
ts/smartagent.tools.search.ts
Normal file
@@ -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<void> {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
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<interfaces.IToolExecutionResult> {
|
||||
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, unknown>): 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
|
||||
\`\`\`
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>search</action>
|
||||
<params>{"query": "database"}</params>
|
||||
</tool_call>
|
||||
\`\`\`
|
||||
|
||||
**list** - List all tools grouped by visibility
|
||||
\`\`\`
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>list</action>
|
||||
<params>{}</params>
|
||||
</tool_call>
|
||||
\`\`\`
|
||||
|
||||
**activate** - Activate an on-demand tool
|
||||
\`\`\`
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>activate</action>
|
||||
<params>{"name": "database_expert"}</params>
|
||||
</tool_call>
|
||||
\`\`\`
|
||||
|
||||
**details** - Get full information about a tool
|
||||
\`\`\`
|
||||
<tool_call>
|
||||
<tool>tools</tool>
|
||||
<action>details</action>
|
||||
<params>{"name": "filesystem"}</params>
|
||||
</tool_call>
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user