13 Commits
v1.5.3 ... main

Author SHA1 Message Date
903de44644 v1.8.0
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 14:39:34 +00:00
5aa69cc998 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 2026-01-20 14:39:34 +00:00
5ca0c80ea9 v1.7.0
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 12:01:07 +00:00
940bf3d3ef feat(docs): document native tool calling support and update README to clarify standard and additional tools 2026-01-20 12:01:07 +00:00
c1b269f301 v1.6.2
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:56:44 +00:00
7cb970f9e2 fix(release): bump version to 1.6.2 2026-01-20 03:56:44 +00:00
1fbcf8bb8b fix(driveragent): save tool_calls in message history for native tool calling
When using native tool calling, the assistant's tool_calls must be saved
in message history. Without this, the model doesn't know it already called
a tool and loops indefinitely calling the same tool.

This fix saves tool_calls in both startTaskWithNativeTools and
continueWithNativeTools methods.

Also updates @push.rocks/smartai to v0.13.3 for tool_calls forwarding support.
2026-01-20 03:56:10 +00:00
4a8789019a v1.6.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:38:07 +00:00
0da85a5dcd fix(driveragent): include full message history for tool results and use a continuation prompt when invoking provider.collectStreamResponse 2026-01-20 03:38:07 +00:00
121e216eea v1.6.0
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:28:59 +00:00
eb1058bfb5 feat(smartagent): record native tool results in message history by adding optional toolName to continueWithNativeTools and passing tool identifier from DualAgent 2026-01-20 03:28:59 +00:00
ecdc125a43 v1.5.4
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:16:02 +00:00
73657be550 fix(driveragent): prevent duplicate thinking/output markers during token streaming and mark transitions 2026-01-20 03:16:02 +00:00
13 changed files with 1113 additions and 65 deletions

View File

@@ -1,5 +1,54 @@
# 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
- Add 'Native Tool Calling' section documenting useNativeToolCalling option and behavior for providers (e.g., Ollama).
- Explain tool name mapping when native tool calling is enabled (toolName_actionName) and streaming markers ([THINKING], [OUTPUT]).
- Add example showing enabling useNativeToolCalling and note ollamaToken config option (Ollama endpoint).
- Clarify that registerStandardTools() registers five tools (Filesystem, HTTP, Shell, Browser, Deno) and that JsonValidatorTool must be registered manually as an additional tool.
- Documentation-only changes (README updates) — no code functionality changed in this diff.
## 2026-01-20 - 1.6.2 - fix(release)
bump version to 1.6.2
- No source changes detected in the diff
- Current package.json version is 1.6.1
- Recommend a patch bump to 1.6.2 for a release
## 2026-01-20 - 1.6.1 - fix(driveragent)
include full message history for tool results and use a continuation prompt when invoking provider.collectStreamResponse
- When toolName is provided, include the full messageHistory (do not slice off the last message) so tool result messages are preserved.
- Set userMessage to a continuation prompt ('Continue with the task. The tool result has been provided above.') when handling tool results to avoid repeating the tool output.
- Keeps existing maxHistoryMessages trimming and validates provider.collectStreamResponse is available before streaming.
## 2026-01-20 - 1.6.0 - feat(smartagent)
record native tool results in message history by adding optional toolName to continueWithNativeTools and passing tool identifier from DualAgent
- continueWithNativeTools(message, toolName?) now accepts an optional toolName; when provided the message is stored with role 'tool' and includes a toolName property (cast to ChatMessage)
- DualAgent constructs a toolNameForHistory as `${proposal.toolName}_${proposal.action}` and forwards it to continueWithNativeTools in both normal and error flows
- Preserves tool-origin information in the conversation history to support native tool calling and tracking
## 2026-01-20 - 1.5.4 - fix(driveragent)
prevent duplicate thinking/output markers during token streaming and mark transitions
- Add isInThinkingMode flag to track thinking vs output state
- Emit "\n[THINKING] " only when transitioning into thinking mode (avoids repeated thinking markers)
- Emit "\n[OUTPUT] " when transitioning out of thinking mode to mark content output
- Reset thinking state after response completes to ensure correct markers for subsequent responses
- Applied the same streaming marker logic to both response handling paths
## 2026-01-20 - 1.5.3 - fix(driveragent)
prefix thinking tokens with [THINKING] when forwarding streaming chunks to onToken

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartagent",
"version": "1.5.3",
"version": "1.8.0",
"private": false,
"description": "an agentic framework built on top of @push.rocks/smartai",
"main": "dist_ts/index.js",
@@ -21,7 +21,7 @@
"@types/node": "^25.0.2"
},
"dependencies": {
"@push.rocks/smartai": "^0.13.1",
"@push.rocks/smartai": "^0.13.3",
"@push.rocks/smartbrowser": "^2.0.8",
"@push.rocks/smartdeno": "^1.2.0",
"@push.rocks/smartfs": "^1.2.0",

10
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@push.rocks/smartai':
specifier: ^0.13.1
version: 0.13.1(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
specifier: ^0.13.3
version: 0.13.3(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
'@push.rocks/smartbrowser':
specifier: ^2.0.8
version: 2.0.8(typescript@5.9.3)
@@ -844,8 +844,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartai@0.13.1':
resolution: {integrity: sha512-V9J6a+rjBkFpdFnC6OBm8CbEtqCfJnEsUmNKfRUOiTa+VIVtD4OOceraZah6kGHWltUhZ1XV4eLWwFf4+YO3NA==}
'@push.rocks/smartai@0.13.3':
resolution: {integrity: sha512-VDZzHs101hpGMmUaectuLfcME4kHpuOS7o5ffuGk5lYl383foyAN71+5v441jpk/gLDNf2KhDACR/d2O4n90Ag==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -5172,7 +5172,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartai@0.13.1(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
'@push.rocks/smartai@0.13.3(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
'@mistralai/mistralai': 1.12.0

View File

@@ -5,18 +5,64 @@
## Architecture
- **DualAgentOrchestrator**: Main entry point, coordinates Driver and Guardian agents
- **DriverAgent**: Reasons about tasks, plans steps, proposes tool calls
- **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
## Standard Tools
## Standard Tools (via registerStandardTools)
1. **FilesystemTool** - File operations with scoping and exclusion patterns
2. **HttpTool** - HTTP requests
3. **ShellTool** - Secure shell commands (no injection possible)
4. **BrowserTool** - Web page interaction via Puppeteer
5. **DenoTool** - Sandboxed TypeScript/JavaScript execution
6. **JsonValidatorTool** - JSON validation and formatting
## 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)
@@ -25,6 +71,17 @@
- Scoped filesystem with exclusion patterns
- 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:
- Uses provider's built-in tool calling API instead of XML parsing
- Tool names become `toolName_actionName` (e.g., `json_validate`)
- Streaming includes `[THINKING]` and `[OUTPUT]` markers
- More efficient for models that support it
## Key Dependencies
- `@push.rocks/smartai`: Multi-provider AI interface

216
readme.md
View File

@@ -37,20 +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"]
JSON["JSON Validator"]
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
@@ -100,7 +99,7 @@ await orchestrator.stop();
## Standard Tools
SmartAgent comes with six battle-tested tools out of the box:
SmartAgent comes with five battle-tested tools out of the box via `registerStandardTools()`:
### 🗂️ FilesystemTool
@@ -231,12 +230,21 @@ By default, code runs **fully sandboxed with no permissions**. Permissions must
</tool_call>
```
## Additional Tools
### 📋 JsonValidatorTool
Validate and format JSON data. Perfect for agents to self-check their JSON output before completing tasks.
**Actions**: `validate`, `format`
```typescript
import { JsonValidatorTool } from '@push.rocks/smartagent';
// Register the JSON validator tool (not included in registerStandardTools)
orchestrator.registerTool(new JsonValidatorTool());
```
```typescript
// Validate JSON with required field checking
<tool_call>
@@ -258,6 +266,157 @@ Validate and format JSON data. Perfect for agents to self-check their JSON outpu
</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:
@@ -330,6 +489,29 @@ const orchestrator = new DualAgentOrchestrator({
**Event Types**: `task_started`, `iteration_started`, `tool_proposed`, `guardian_evaluating`, `tool_approved`, `tool_rejected`, `tool_executing`, `tool_completed`, `task_completed`, `clarification_needed`, `max_iterations`, `max_rejections`
## 🔧 Native Tool Calling
For providers that support native tool calling (like Ollama with certain models), SmartAgent can use the provider's built-in tool calling API instead of XML parsing:
```typescript
const orchestrator = new DualAgentOrchestrator({
ollamaToken: 'http://localhost:11434', // Ollama endpoint
defaultProvider: 'ollama',
guardianPolicyPrompt: '...',
// Enable native tool calling
useNativeToolCalling: true,
});
```
When `useNativeToolCalling` is enabled:
- Tools are converted to JSON schema format automatically
- The provider handles tool call parsing natively
- Streaming still works with `[THINKING]` and `[OUTPUT]` markers for supported models
- Tool calls appear as `toolName_actionName` (e.g., `json_validate`)
This is more efficient for models that support it and avoids potential XML parsing issues.
## Guardian Policy Examples
The Guardian's power comes from your policy. Here are battle-tested examples:
@@ -401,6 +583,7 @@ interface IDualAgentOptions {
perplexityToken?: string;
groqToken?: string;
xaiToken?: string;
ollamaToken?: string; // URL for Ollama endpoint
// Use existing SmartAi instance (optional - avoids duplicate providers)
smartAiInstance?: SmartAi;
@@ -415,6 +598,9 @@ interface IDualAgentOptions {
name?: string; // Agent system name
verbose?: boolean; // Enable verbose logging
// Native tool calling
useNativeToolCalling?: boolean; // Use provider's native tool calling API (default: false)
// Limits
maxIterations?: number; // Max task iterations (default: 20)
maxConsecutiveRejections?: number; // Abort after N rejections (default: 3)
@@ -573,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 |
| `registerStandardTools()` | Register all built-in tools |
| `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
@@ -589,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';
@@ -597,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 };

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartagent',
version: '1.5.3',
version: '1.8.0',
description: 'an agentic framework built on top of @push.rocks/smartai'
}

View File

@@ -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';

View File

@@ -25,6 +25,7 @@ export class DriverAgent {
private messageHistory: plugins.smartai.ChatMessage[] = [];
private tools: Map<string, BaseToolWrapper> = new Map();
private onToken?: (token: string) => void;
private isInThinkingMode = false; // Track thinking/content state for markers
constructor(
provider: plugins.smartai.MultiModalModel,
@@ -494,21 +495,46 @@ Your complete output here
// Pass onToken callback through onChunk for streaming with thinking markers
this.onToken ? (chunk: any) => {
if (chunk.thinking && this.onToken) {
this.onToken(`[THINKING] ${chunk.thinking}`);
// Add marker only when transitioning INTO thinking mode
if (!this.isInThinkingMode) {
this.onToken('\n[THINKING] ');
this.isInThinkingMode = true;
}
this.onToken(chunk.thinking);
}
if (chunk.content && this.onToken) {
// Add marker when transitioning OUT of thinking mode
if (this.isInThinkingMode) {
this.onToken('\n[OUTPUT] ');
this.isInThinkingMode = false;
}
this.onToken(chunk.content);
}
} : undefined
);
// Reset thinking state after response completes
this.isInThinkingMode = false;
// Add assistant response to history
const historyMessage: plugins.smartai.ChatMessage = {
const historyMessage: any = {
role: 'assistant',
content: response.message || '',
reasoning: response.thinking || response.reasoning,
};
this.messageHistory.push(historyMessage);
// CRITICAL: Preserve tool_calls in history for native tool calling
// Without this, the model doesn't know it already called a tool and loops forever
if (response.toolCalls && response.toolCalls.length > 0) {
historyMessage.tool_calls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
}));
}
this.messageHistory.push(historyMessage as unknown as plugins.smartai.ChatMessage);
// Convert Ollama tool calls to our format
let toolCalls: interfaces.INativeToolCall[] | undefined;
@@ -534,16 +560,29 @@ Your complete output here
/**
* Continue conversation with native tool calling support
* @param message The message to continue with (e.g., tool result)
* @param toolName Optional tool name - when provided, message is added as role: 'tool' instead of 'user'
* @returns Response with content, reasoning, and any tool calls
*/
public async continueWithNativeTools(
message: string
message: string,
toolName?: string
): Promise<{ message: interfaces.IAgentMessage; toolCalls?: interfaces.INativeToolCall[] }> {
// Add the new message to history
this.messageHistory.push({
role: 'user',
content: message,
});
if (toolName) {
// Tool result - must use role: 'tool' for native tool calling
// The 'tool' role is supported by providers but not in the ChatMessage type
this.messageHistory.push({
role: 'tool',
content: message,
toolName: toolName,
} as unknown as plugins.smartai.ChatMessage);
} else {
// Regular user message
this.messageHistory.push({
role: 'user',
content: message,
});
}
// Build system message
const fullSystemMessage = this.getNativeToolsSystemMessage();
@@ -552,8 +591,12 @@ Your complete output here
const tools = this.getToolsAsJsonSchema();
// Get response from provider with history windowing
// For tool results, include the full history (with tool message)
// For regular user messages, exclude the last message (it becomes userMessage)
let historyForChat: plugins.smartai.ChatMessage[];
const fullHistory = this.messageHistory.slice(0, -1);
const fullHistory = toolName
? this.messageHistory // Include tool result in history
: this.messageHistory.slice(0, -1); // Exclude last user message
if (this.maxHistoryMessages > 0 && fullHistory.length > this.maxHistoryMessages) {
historyForChat = [
@@ -570,31 +613,62 @@ Your complete output here
throw new Error('Provider does not support native tool calling. Use continueWithMessage() instead.');
}
// For tool results, use a continuation prompt instead of repeating the result
const userMessage = toolName
? 'Continue with the task. The tool result has been provided above.'
: message;
// Use collectStreamResponse for streaming support with tools
const response = await provider.collectStreamResponse(
{
systemMessage: fullSystemMessage,
userMessage: message,
userMessage: userMessage,
messageHistory: historyForChat,
tools: tools.length > 0 ? tools : undefined,
},
// Pass onToken callback through onChunk for streaming with thinking markers
this.onToken ? (chunk: any) => {
if (chunk.thinking && this.onToken) {
this.onToken(`[THINKING] ${chunk.thinking}`);
// Add marker only when transitioning INTO thinking mode
if (!this.isInThinkingMode) {
this.onToken('\n[THINKING] ');
this.isInThinkingMode = true;
}
this.onToken(chunk.thinking);
}
if (chunk.content && this.onToken) {
// Add marker when transitioning OUT of thinking mode
if (this.isInThinkingMode) {
this.onToken('\n[OUTPUT] ');
this.isInThinkingMode = false;
}
this.onToken(chunk.content);
}
} : undefined
);
// Reset thinking state after response completes
this.isInThinkingMode = false;
// Add assistant response to history
this.messageHistory.push({
const historyMessage: any = {
role: 'assistant',
content: response.message || '',
reasoning: response.thinking || response.reasoning,
});
};
// CRITICAL: Preserve tool_calls in history for native tool calling
// Without this, the model doesn't know it already called a tool and loops forever
if (response.toolCalls && response.toolCalls.length > 0) {
historyMessage.tool_calls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
}));
}
this.messageHistory.push(historyMessage as unknown as plugins.smartai.ChatMessage);
// Convert Ollama tool calls to our format
let toolCalls: interfaces.INativeToolCall[] | undefined;

View File

@@ -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(
@@ -495,7 +523,8 @@ Please output the exact XML format above.`
// Continue with appropriate method based on mode
if (useNativeTools) {
const continueResult = await this.driver.continueWithNativeTools(resultMessage);
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(resultMessage, toolNameForHistory);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
@@ -505,8 +534,10 @@ Please output the exact XML format above.`
} catch (error) {
const errorMessage = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
if (useNativeTools) {
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`,
toolNameForHistory
);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
@@ -649,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;
}
}

View 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;
}
}

View File

@@ -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
// ================================

View 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;
}
}

View 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>
\`\`\`
`;
}
}