29 Commits

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
4e4d3c0e08 v1.5.3
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:10:53 +00:00
79efe8f6b8 fix(driveragent): prefix thinking tokens with [THINKING] when forwarding streaming chunks to onToken 2026-01-20 03:10:53 +00:00
8bcf3257e2 v1.5.2
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:54:58 +00:00
6753553394 fix(): no changes in this diff; nothing to release 2026-01-20 02:54:58 +00:00
a46dbd0da6 fix(driveragent): enable streaming for native tool calling methods 2026-01-20 02:54:45 +00:00
7379daf4c5 v1.5.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:45:41 +00:00
83422b4b0e fix(smartagent): bump patch version to 1.5.1 (no changes in diff) 2026-01-20 02:45:41 +00:00
4310c8086b feat(native-tools): add native tool calling support for Ollama models
- Add INativeToolCall interface for native tool call format
- Add useNativeToolCalling option to IDualAgentOptions
- Add getToolsAsJsonSchema() to convert tools to Ollama JSON Schema format
- Add parseNativeToolCalls() to convert native tool calls to proposals
- Add startTaskWithNativeTools() and continueWithNativeTools() to DriverAgent
- Update DualAgentOrchestrator to support both XML parsing and native tool calling modes

Native tool calling is more efficient for models like GPT-OSS that use Harmony format,
as it activates Ollama's built-in tool parser instead of requiring XML generation.
2026-01-20 02:44:54 +00:00
472a8ed7f8 v1.5.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:05:12 +00:00
44137a8710 feat(driveragent): preserve assistant reasoning in message history and update @push.rocks/smartai dependency to ^0.13.0 2026-01-20 02:05:12 +00:00
c12a6a7be9 v1.4.2
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:41:18 +00:00
49dcc7a1a1 fix(repo): no changes detected in diff 2026-01-20 01:41:18 +00:00
e649e9caab fix(driver): make tool call format instructions explicit about literal XML output
The system message now clearly states that the <tool_call> XML tags MUST
be literally written in the response, not just described. Includes examples
of CORRECT vs WRONG usage to help smaller models understand.
2026-01-20 01:40:57 +00:00
c39e7e76b8 v1.4.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:36:30 +00:00
c24a4306d9 fix(): no changes detected (empty diff) 2026-01-20 01:36:30 +00:00
9718048dff fix(dualagent): improve no-tool-call feedback with explicit XML format reminder
When the LLM fails to emit a tool_call XML block, the feedback now includes
the exact XML format expected with a concrete example for json.validate.
This helps smaller models understand the exact output format required.
2026-01-20 01:36:03 +00:00
13 changed files with 1548 additions and 87 deletions

View File

@@ -1,5 +1,93 @@
# 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
- Wraps chunk.thinking with '[THINKING] ' before calling onToken to mark thinking tokens
- Forwards chunk.content unchanged
- Change applied in ts/smartagent.classes.driveragent.ts for both initial and subsequent assistant streaming responses
- No API signature changes; only the token payloads sent to onToken are altered
## 2026-01-20 - 1.5.2 - fix()
no changes in this diff; nothing to release
- No files changed; no release required
- No code or dependency changes detected
## 2026-01-20 - 1.5.1 - fix(smartagent)
bump patch version to 1.5.1 (no changes in diff)
- No code changes detected in the provided diff
- Current package.json version is 1.5.0
- Recommended semantic version bump: patch -> 1.5.1
## 2026-01-20 - 1.5.0 - feat(driveragent)
preserve assistant reasoning in message history and update @push.rocks/smartai dependency to ^0.13.0
- Store response.reasoning in messageHistory for assistant responses (two places in driveragent)
- Bump dependency @push.rocks/smartai from ^0.12.0 to ^0.13.0
## 2026-01-20 - 1.4.2 - fix(repo)
no changes detected in diff
- No files changed in diff; no code or metadata updates were made.
- No version bump required.
## 2026-01-20 - 1.4.1 - fix()
no changes detected (empty diff)
- No files changed in this commit
- No release required
## 2026-01-20 - 1.4.0 - feat(docs)
document Dual-Agent Driver/Guardian architecture, new standard tools, streaming/vision support, progress events, and updated API/export docs

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartagent",
"version": "1.4.0",
"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.12.0",
"@push.rocks/smartai": "^0.13.3",
"@push.rocks/smartbrowser": "^2.0.8",
"@push.rocks/smartdeno": "^1.2.0",
"@push.rocks/smartfs": "^1.2.0",

28
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@push.rocks/smartai':
specifier: ^0.12.0
version: 0.12.0(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)
@@ -243,6 +243,10 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
@@ -267,6 +271,9 @@ packages:
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -837,8 +844,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartai@0.12.0':
resolution: {integrity: sha512-T4HRaSSxO6TQGGXlQeswX2eYkB+gMu0FbKF9qCUri6FdRlYzmPDn19jgPrPJxyg5m3oj6TzflvfYwcBCFlWo/A==}
'@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==}
@@ -4331,6 +4338,8 @@ snapshots:
'@babel/runtime@7.28.4': {}
'@babel/runtime@7.28.6': {}
'@borewit/text-codec@0.1.1': {}
'@cloudflare/workers-types@4.20251202.0': {}
@@ -4395,6 +4404,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
@@ -4682,7 +4696,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.7.1
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -5158,7 +5172,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartai@0.12.0(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
@@ -7588,7 +7602,7 @@ snapshots:
json-schema-to-ts@3.1.1:
dependencies:
'@babel/runtime': 7.28.4
'@babel/runtime': 7.28.6
ts-algebra: 2.0.0
jsonfile@6.2.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.4.0',
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,
@@ -121,10 +122,11 @@ export class DriverAgent {
});
}
// Add assistant response to history (store images if provided)
// Add assistant response to history (store images if provided, preserve reasoning for GPT-OSS)
const historyMessage: plugins.smartai.ChatMessage = {
role: 'assistant',
content: response.message,
reasoning: response.reasoning,
};
this.messageHistory.push(historyMessage);
@@ -189,10 +191,11 @@ export class DriverAgent {
});
}
// Add assistant response to history
// Add assistant response to history (preserve reasoning for GPT-OSS)
this.messageHistory.push({
role: 'assistant',
content: response.message,
reasoning: response.reasoning,
});
return {
@@ -375,33 +378,33 @@ export class DriverAgent {
## Your Role
You analyze tasks, break them down into steps, and use tools to accomplish goals.
## Tool Usage Format
When you need to use a tool, output a tool call proposal in this format:
## CRITICAL: Tool Usage Format
To use a tool, you MUST literally write out the XML tags in your response. The system parses your output looking for these exact tags. Do NOT just describe or mention the tool call - you must OUTPUT the actual XML.
CORRECT (the XML is in the output):
<tool_call>
<tool>tool_name</tool>
<action>action_name</action>
<params>
{"param1": "value1", "param2": "value2"}
</params>
<reasoning>Brief explanation of why this action is needed</reasoning>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "{\\"key\\":\\"value\\"}"}</params>
</tool_call>
WRONG (just describing, no actual XML):
"I will call json.validate now" or "Let me use the tool"
## Guidelines
1. Think step by step about what needs to be done
2. Use only the tools that are available to you
3. Provide clear reasoning for each tool call
4. If a tool call is rejected, adapt your approach based on the feedback
5. When the task is complete, indicate this clearly:
2. When you need a tool, OUTPUT the <tool_call> XML tags - do not just mention them
3. Only propose ONE tool call at a time
4. Wait for the result before proposing the next action
5. When the task is complete, OUTPUT:
<task_complete>
Brief summary of what was accomplished
Your final result here
</task_complete>
## Important
- Only propose ONE tool call at a time
- Wait for the result before proposing the next action
- If you encounter an error, analyze it and try an alternative approach
- The <tool_call> and <task_complete> tags MUST appear literally in your response
- If you just say "I'll call the tool" without the actual XML, it will NOT work
- If you need clarification, ask using <needs_clarification>your question</needs_clarification>`;
}
@@ -440,4 +443,333 @@ Your complete output here
public reset(): void {
this.messageHistory = [];
}
// ================================
// Native Tool Calling Support
// ================================
/**
* Start a task with native tool calling support
* Uses Ollama's native tool calling API instead of XML parsing
* @param task The task description
* @param images Optional base64-encoded images for vision tasks
* @returns Response with content, reasoning, and any tool calls
*/
public async startTaskWithNativeTools(
task: string,
images?: string[]
): Promise<{ message: interfaces.IAgentMessage; toolCalls?: interfaces.INativeToolCall[] }> {
// Reset message history
this.messageHistory = [];
// Build simple user message (no XML instructions needed for native tool calling)
const userMessage = `TASK: ${task}\n\nComplete this task using the available tools. When done, provide your final output.`;
// Add to history
this.messageHistory.push({
role: 'user',
content: userMessage,
});
// Build system message for native tool calling
const fullSystemMessage = this.getNativeToolsSystemMessage();
// Get tools in JSON schema format
const tools = this.getToolsAsJsonSchema();
// Check if provider supports native tool calling (Ollama)
const provider = this.provider as any;
if (typeof provider.collectStreamResponse !== 'function') {
throw new Error('Provider does not support native tool calling. Use startTask() instead.');
}
// Use collectStreamResponse for streaming support with tools
const response = await provider.collectStreamResponse(
{
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
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) {
// 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: 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;
if (response.toolCalls && response.toolCalls.length > 0) {
toolCalls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
index: tc.function.index,
},
}));
}
return {
message: {
role: 'assistant',
content: response.message || '',
},
toolCalls,
};
}
/**
* 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,
toolName?: string
): Promise<{ message: interfaces.IAgentMessage; toolCalls?: interfaces.INativeToolCall[] }> {
// Add the new message to history
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();
// Get tools in JSON schema format
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 = 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 = [
fullHistory[0],
...fullHistory.slice(-(this.maxHistoryMessages - 1)),
];
} else {
historyForChat = fullHistory;
}
// Check if provider supports native tool calling
const provider = this.provider as any;
if (typeof provider.collectStreamResponse !== 'function') {
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: 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) {
// 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: 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;
if (response.toolCalls && response.toolCalls.length > 0) {
toolCalls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
index: tc.function.index,
},
}));
}
return {
message: {
role: 'assistant',
content: response.message || '',
},
toolCalls,
};
}
/**
* Get system message for native tool calling mode
* Simplified prompt that lets the model use tools naturally
*/
private getNativeToolsSystemMessage(): string {
return `You are an AI assistant that executes tasks by using available tools.
## Your Role
You analyze tasks, break them down into steps, and use tools to accomplish goals.
## Guidelines
1. Think step by step about what needs to be done
2. Use the available tools to complete the task
3. Process tool results and continue until the task is complete
4. When the task is complete, provide a final summary
## Important
- Use tools when needed to gather information or perform actions
- If you need clarification, ask the user
- Always verify your work before marking the task complete`;
}
/**
* Convert registered tools to Ollama JSON Schema format for native tool calling
* Each tool action becomes a separate function with name format: "toolName_actionName"
* @returns Array of IOllamaTool compatible tool definitions
*/
public getToolsAsJsonSchema(): plugins.smartai.IOllamaTool[] {
const tools: plugins.smartai.IOllamaTool[] = [];
for (const tool of this.tools.values()) {
for (const action of tool.actions) {
// Build the tool definition in Ollama format
const toolDef: plugins.smartai.IOllamaTool = {
type: 'function',
function: {
name: `${tool.name}_${action.name}`, // e.g., "json_validate"
description: `[${tool.name}] ${action.description}`,
parameters: action.parameters as plugins.smartai.IOllamaTool['function']['parameters'],
},
};
tools.push(toolDef);
}
}
return tools;
}
/**
* Parse native tool calls from provider response into IToolCallProposal format
* @param toolCalls Array of native tool calls from the provider
* @returns Array of IToolCallProposal ready for execution
*/
public parseNativeToolCalls(
toolCalls: interfaces.INativeToolCall[]
): interfaces.IToolCallProposal[] {
return toolCalls.map(tc => {
// Split "json_validate" -> toolName="json", action="validate"
const fullName = tc.function.name;
const underscoreIndex = fullName.indexOf('_');
let toolName: string;
let action: string;
if (underscoreIndex > 0) {
toolName = fullName.substring(0, underscoreIndex);
action = fullName.substring(underscoreIndex + 1);
} else {
// Fallback: treat entire name as tool name with empty action
toolName = fullName;
action = '';
}
return {
proposalId: this.generateProposalId(),
toolName,
action,
params: tc.function.arguments,
};
});
}
}

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) {
@@ -242,12 +270,18 @@ export class DualAgentOrchestrator {
throw new Error('Orchestrator not started. Call start() first.');
}
// Use native tool calling if enabled
const useNativeTools = this.options.useNativeToolCalling === true;
this.conversationHistory = [];
let iterations = 0;
let consecutiveRejections = 0;
let completed = false;
let finalResult: string | null = null;
// Track pending native tool calls
let pendingNativeToolCalls: interfaces.INativeToolCall[] | undefined;
// Extract images from options
const images = options?.images;
@@ -258,7 +292,17 @@ export class DualAgentOrchestrator {
});
// Start the driver with the task and optional images
let driverResponse = await this.driver.startTask(task, images);
let driverResponse: interfaces.IAgentMessage;
if (useNativeTools) {
// Native tool calling mode
const result = await this.driver.startTaskWithNativeTools(task, images);
driverResponse = result.message;
pendingNativeToolCalls = result.toolCalls;
} else {
// XML parsing mode
driverResponse = await this.driver.startTask(task, images);
}
this.conversationHistory.push(driverResponse);
// Emit task started event
@@ -281,10 +325,16 @@ export class DualAgentOrchestrator {
maxIterations: this.options.maxIterations,
});
// Check if task is complete
if (this.driver.isTaskComplete(driverResponse.content)) {
// Check if task is complete (for native mode, no pending tool calls and has content)
const isComplete = useNativeTools
? (!pendingNativeToolCalls || pendingNativeToolCalls.length === 0) && driverResponse.content.length > 0
: this.driver.isTaskComplete(driverResponse.content);
if (isComplete) {
completed = true;
finalResult = this.driver.extractTaskResult(driverResponse.content) || driverResponse.content;
finalResult = useNativeTools
? driverResponse.content
: (this.driver.extractTaskResult(driverResponse.content) || driverResponse.content);
// Emit task completed event
this.emitProgress({
@@ -315,16 +365,56 @@ export class DualAgentOrchestrator {
};
}
// Parse tool call proposals
const proposals = this.driver.parseToolCallProposals(driverResponse.content);
// Parse tool call proposals - native mode uses pendingNativeToolCalls, XML mode parses content
let proposals: interfaces.IToolCallProposal[];
if (useNativeTools && pendingNativeToolCalls && pendingNativeToolCalls.length > 0) {
// Native tool calling mode - convert native tool calls to proposals
proposals = this.driver.parseNativeToolCalls(pendingNativeToolCalls);
pendingNativeToolCalls = undefined; // Clear after processing
} else if (!useNativeTools) {
// XML parsing mode
proposals = this.driver.parseToolCallProposals(driverResponse.content);
} else {
proposals = [];
}
if (proposals.length === 0) {
// No tool calls, continue the conversation
driverResponse = await this.driver.continueWithMessage(
'Please either use a tool to make progress on the task, or indicate that the task is complete with <task_complete>summary</task_complete>.'
);
this.conversationHistory.push(driverResponse);
continue;
if (useNativeTools) {
// Native mode: no tool calls and no content means we should continue
const result = await this.driver.continueWithNativeTools(
'Please continue with the task. Use the available tools or provide your final output.'
);
driverResponse = result.message;
pendingNativeToolCalls = result.toolCalls;
this.conversationHistory.push(driverResponse);
continue;
} else {
// XML mode: remind the model of the exact XML format
driverResponse = await this.driver.continueWithMessage(
`No valid tool call was found in your response. To use a tool, you MUST output the exact XML format:
<tool_call>
<tool>tool_name</tool>
<action>action_name</action>
<params>{"param1": "value1"}</params>
</tool_call>
For example, to validate JSON:
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "{\\"key\\":\\"value\\"}", "requiredFields": ["key"]}</params>
</tool_call>
Or to complete the task:
<task_complete>your final JSON output here</task_complete>
Please output the exact XML format above.`
);
this.conversationHistory.push(driverResponse);
continue;
}
}
// Process the first proposal (one at a time)
@@ -370,7 +460,7 @@ export class DualAgentOrchestrator {
});
// 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(
@@ -431,13 +521,31 @@ export class DualAgentOrchestrator {
toolResult: result,
});
driverResponse = await this.driver.continueWithMessage(resultMessage);
// Continue with appropriate method based on mode
if (useNativeTools) {
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(resultMessage, toolNameForHistory);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(resultMessage);
}
this.conversationHistory.push(driverResponse);
} catch (error) {
const errorMessage = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
driverResponse = await this.driver.continueWithMessage(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
);
if (useNativeTools) {
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`,
toolNameForHistory
);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
);
}
this.conversationHistory.push(driverResponse);
}
} else {
@@ -474,7 +582,14 @@ export class DualAgentOrchestrator {
guardianDecision: decision,
});
driverResponse = await this.driver.continueWithMessage(feedback);
// Continue with appropriate method based on mode
if (useNativeTools) {
const continueResult = await this.driver.continueWithNativeTools(feedback);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(feedback);
}
this.conversationHistory.push(driverResponse);
}
}
@@ -565,6 +680,13 @@ export class DualAgentOrchestrator {
* 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
// ================================
@@ -48,6 +108,12 @@ export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
logPrefix?: string;
/** Callback fired for each token during LLM generation (streaming mode) */
onToken?: (token: string, source: 'driver' | 'guardian') => void;
/**
* Enable native tool calling mode (default: false)
* When enabled, uses Ollama's native tool calling API instead of XML parsing
* This is more efficient for models that support it (e.g., GPT-OSS with Harmony format)
*/
useNativeToolCalling?: boolean;
}
// ================================
@@ -83,6 +149,18 @@ export interface IToolAction {
parameters: Record<string, unknown>;
}
/**
* Native tool call from provider (matches Ollama's tool calling format)
* Format: function name is "toolName_actionName" (e.g., "json_validate")
*/
export interface INativeToolCall {
function: {
name: string; // Format: "toolName_actionName"
arguments: Record<string, unknown>;
index?: number;
};
}
/**
* Proposed tool call from the Driver
*/

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