@push.rocks/smartagent
A dual-agent agentic framework with Driver and Guardian agents for safe, policy-controlled AI task execution. 🤖🛡️
Install
npm install @push.rocks/smartagent
# or
pnpm install @push.rocks/smartagent
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Overview
SmartAgent implements a dual-agent architecture where AI safety isn't just an afterthought—it's baked into the core design:
- 🎯 Driver Agent: The executor. Reasons about goals, plans steps, and proposes tool calls
- 🛡️ Guardian Agent: The gatekeeper. Evaluates every tool call against your policy, approving or rejecting with feedback
This design ensures safe tool use through AI-based policy evaluation rather than rigid programmatic rules. The Guardian can understand context, nuance, and intent—catching dangerous operations that simple regex or allowlists would miss.
Why Dual-Agent?
Traditional AI agents have a fundamental problem: they're given tools and expected to use them responsibly. SmartAgent adds a second AI specifically trained to evaluate whether each action is safe and appropriate. Think of it as separation of concerns, but for AI safety.
Architecture
flowchart TB
subgraph Input
Task["User Task"]
Policy["Guardian Policy Prompt"]
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["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
Policy --> Guardian
Driver -->|"execute<br/>(if approved)"| Tools
Tools -->|"result"| Driver
Quick Start
import { DualAgentOrchestrator } from '@push.rocks/smartagent';
// Create orchestrator with Guardian policy
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
defaultProvider: 'openai',
guardianPolicyPrompt: `
FILE SYSTEM POLICY:
- ONLY allow reading/writing within /tmp or the current working directory
- REJECT operations on system directories or sensitive files
SHELL POLICY:
- Allow read-only commands (ls, cat, grep, echo)
- REJECT destructive commands (rm, mv, chmod) without explicit justification
FLAG any attempt to expose secrets or credentials.
`,
});
// Register standard tools
orchestrator.registerStandardTools();
// Start the orchestrator (initializes all tools)
await orchestrator.start();
// Run a task
const result = await orchestrator.run('List all TypeScript files in the current directory');
console.log('Success:', result.success);
console.log('Result:', result.result);
console.log('Iterations:', result.iterations);
// Cleanup
await orchestrator.stop();
Standard Tools
SmartAgent comes with five battle-tested tools out of the box via registerStandardTools():
🗂️ FilesystemTool
File and directory operations powered by @push.rocks/smartfs.
Actions: read, write, append, list, delete, exists, stat, copy, move, mkdir
// Example tool call by Driver
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/tmp/config.json"}</params>
<reasoning>Need to read the configuration file to understand the settings</reasoning>
</tool_call>
Scoped Filesystem: Lock file operations to a specific directory with optional exclusion patterns:
// Only allow access within a specific directory
orchestrator.registerScopedFilesystemTool('/home/user/workspace');
// With exclusion patterns (glob syntax)
orchestrator.registerScopedFilesystemTool('/home/user/workspace', [
'.nogit/**',
'node_modules/**',
'*.secret',
]);
Line-range Reading: Read specific portions of large files:
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/var/log/app.log", "startLine": 100, "endLine": 150}</params>
<reasoning>Reading only the relevant log section to avoid token overload</reasoning>
</tool_call>
🌐 HttpTool
HTTP requests using @push.rocks/smartrequest.
Actions: get, post, put, patch, delete
<tool_call>
<tool>http</tool>
<action>get</action>
<params>{"url": "https://api.example.com/data", "headers": {"Authorization": "Bearer token"}}</params>
<reasoning>Fetching data from the API endpoint</reasoning>
</tool_call>
💻 ShellTool
Secure shell command execution using @push.rocks/smartshell with execSpawn (no shell injection possible).
Actions: execute, which
<tool_call>
<tool>shell</tool>
<action>execute</action>
<params>{"command": "ls", "args": ["-la", "/tmp"]}</params>
<reasoning>Listing directory contents to find relevant files</reasoning>
</tool_call>
🔒 Security Note: The shell tool uses
execSpawnwithshell: false, meaning command and arguments are passed separately. This makes shell injection attacks impossible.
🌍 BrowserTool
Web page interaction using @push.rocks/smartbrowser (Puppeteer-based).
Actions: screenshot, pdf, evaluate, getPageContent
<tool_call>
<tool>browser</tool>
<action>getPageContent</action>
<params>{"url": "https://example.com"}</params>
<reasoning>Extracting text content from the webpage</reasoning>
</tool_call>
🦕 DenoTool
Execute TypeScript/JavaScript code in a sandboxed Deno environment with fine-grained permission control.
Actions: execute, executeWithResult
Permissions: all, env, ffi, hrtime, net, read, run, sys, write
By default, code runs fully sandboxed with no permissions. Permissions must be explicitly requested and are subject to Guardian approval.
// Simple code execution (sandboxed, no permissions)
<tool_call>
<tool>deno</tool>
<action>execute</action>
<params>{"code": "console.log('Hello from Deno!')"}</params>
<reasoning>Running a simple script to verify the environment</reasoning>
</tool_call>
// Code with network permission
<tool_call>
<tool>deno</tool>
<action>execute</action>
<params>{
"code": "const resp = await fetch('https://api.example.com/data'); console.log(await resp.json());",
"permissions": ["net"]
}</params>
<reasoning>Fetching data from API using Deno's fetch</reasoning>
</tool_call>
// Execute and parse JSON result
<tool_call>
<tool>deno</tool>
<action>executeWithResult</action>
<params>{
"code": "const result = { sum: 2 + 2, date: new Date().toISOString() }; console.log(JSON.stringify(result));"
}</params>
<reasoning>Computing values and returning structured data</reasoning>
</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
import { JsonValidatorTool } from '@push.rocks/smartagent';
// Register the JSON validator tool (not included in registerStandardTools)
orchestrator.registerTool(new JsonValidatorTool());
// Validate JSON with required field checking
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{
"jsonString": "{\"name\": \"test\", \"version\": \"1.0.0\"}",
"requiredFields": ["name", "version", "description"]
}</params>
<reasoning>Ensuring the config has all required fields before saving</reasoning>
</tool_call>
// Pretty-print JSON
<tool_call>
<tool>json</tool>
<action>format</action>
<params>{"jsonString": "{\"compact\":true,\"data\":[1,2,3]}"}</params>
<reasoning>Formatting JSON for readable output</reasoning>
</tool_call>
🔍 ToolSearchTool
Enable the Driver to discover and activate on-demand tools at runtime.
Actions: search, list, activate, details
// Enable tool search (adds the 'tools' tool)
orchestrator.enableToolSearch();
// 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
// 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',
});
// 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 prompton-demand: Tool is hidden until explicitly activated viatools.activate()
This enables you to have many specialized tools/experts without overwhelming the Driver's context.
// 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
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:
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
defaultProvider: 'openai',
guardianPolicyPrompt: '...',
// Token streaming callback
onToken: (token, source) => {
// source is 'driver' or 'guardian'
process.stdout.write(token);
},
});
This is perfect for CLI applications or UIs that need to show progress as the agent thinks.
🖼️ Vision Support
Pass images to vision-capable models for multimodal tasks:
import { readFileSync } from 'fs';
// Load image as base64
const imageBase64 = readFileSync('screenshot.png').toString('base64');
// Run task with images
const result = await orchestrator.run(
'Analyze this UI screenshot and describe any usability issues',
{ images: [imageBase64] }
);
📊 Progress Events
Get real-time feedback on task execution with the onProgress callback:
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
guardianPolicyPrompt: '...',
logPrefix: '[MyAgent]', // Optional prefix for log messages
onProgress: (event) => {
// Pre-formatted log message ready for output
console.log(event.logMessage);
// Or handle specific event types
switch (event.type) {
case 'tool_proposed':
console.log(`Proposing: ${event.toolName}.${event.action}`);
break;
case 'tool_approved':
console.log(`✓ Approved`);
break;
case 'tool_rejected':
console.log(`✗ Rejected: ${event.reason}`);
break;
case 'task_completed':
console.log(`Done in ${event.iteration} iterations`);
break;
}
},
});
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:
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:
🔐 Strict Security Policy
const securityPolicy = `
SECURITY POLICY:
1. REJECT any file operations outside /home/user/workspace
2. REJECT any shell commands that could modify system state
3. REJECT any HTTP requests to internal/private IP ranges
4. REJECT any attempts to read environment variables or credentials
5. FLAG and REJECT obfuscated code execution
When rejecting, always explain:
- What policy was violated
- What would be a safer alternative
`;
🛠️ Development Environment Policy
const devPolicy = `
DEVELOPMENT POLICY:
- Allow file operations only within the project directory
- Allow npm/pnpm commands for package management
- Allow git commands for version control
- Allow HTTP requests to public APIs only
- REJECT direct database modifications
- REJECT commands that could affect other users
Always verify:
- File paths are relative or within project bounds
- Commands don't have dangerous flags (--force, -rf)
`;
🦕 Deno Code Execution Policy
const denoPolicy = `
DENO CODE EXECUTION POLICY:
- ONLY allow 'read' permission for files within the workspace
- REJECT 'all' permission unless explicitly justified for the task
- REJECT 'run' permission (subprocess execution) without specific justification
- REJECT code that attempts to:
- Access credentials or environment secrets (even with 'env' permission)
- Make network requests to internal/private IP ranges
- Write to system directories
- FLAG obfuscated or encoded code (base64, eval with dynamic strings)
- Prefer sandboxed execution (no permissions) when possible
When evaluating code:
- Review the actual code content, not just permissions
- Consider what data the code could exfiltrate
- Verify network endpoints are legitimate public APIs
`;
Configuration Options
interface IDualAgentOptions {
// Provider tokens (from @push.rocks/smartai)
openaiToken?: string;
anthropicToken?: string;
perplexityToken?: string;
groqToken?: string;
xaiToken?: string;
ollamaToken?: string; // URL for Ollama endpoint
// Use existing SmartAi instance (optional - avoids duplicate providers)
smartAiInstance?: SmartAi;
// Provider selection
defaultProvider?: TProvider; // For both Driver and Guardian
guardianProvider?: TProvider; // Optional: separate provider for Guardian
// Agent configuration
driverSystemMessage?: string; // Custom system message for Driver
guardianPolicyPrompt: string; // REQUIRED: Policy for Guardian to enforce
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)
maxResultChars?: number; // Max chars for tool results before truncation (default: 15000)
maxHistoryMessages?: number; // Max history messages for API (default: 20)
// Callbacks
onProgress?: (event: IProgressEvent) => void; // Progress event callback
onToken?: (token: string, source: 'driver' | 'guardian') => void; // Streaming callback
logPrefix?: string; // Prefix for log messages
}
Result Interface
interface IDualAgentRunResult {
success: boolean; // Whether task completed successfully
completed: boolean; // Task completion status
result: string; // Final result or response
iterations: number; // Number of iterations taken
history: IAgentMessage[]; // Full conversation history
status: TDualAgentRunStatus; // 'completed' | 'max_iterations_reached' | etc.
toolCallCount?: number; // Number of tool calls made
rejectionCount?: number; // Number of Guardian rejections
toolLog?: IToolExecutionLog[]; // Detailed tool execution log
error?: string; // Error message if status is 'error'
}
type TDualAgentRunStatus =
| 'completed'
| 'in_progress'
| 'max_iterations_reached'
| 'max_rejections_reached'
| 'clarification_needed'
| 'error';
Custom Tools
Create custom tools by extending BaseToolWrapper:
import { BaseToolWrapper, IToolAction, IToolExecutionResult } from '@push.rocks/smartagent';
class MyCustomTool extends BaseToolWrapper {
public name = 'custom';
public description = 'My custom tool for specific operations';
public actions: IToolAction[] = [
{
name: 'myAction',
description: 'Performs a custom action',
parameters: {
type: 'object',
properties: {
input: { type: 'string', description: 'Input for the action' },
},
required: ['input'],
},
},
];
public async initialize(): Promise<void> {
// Setup your tool (called when orchestrator.start() runs)
this.isInitialized = true;
}
public async cleanup(): Promise<void> {
// Cleanup resources (called when orchestrator.stop() runs)
this.isInitialized = false;
}
public async execute(action: string, params: Record<string, unknown>): Promise<IToolExecutionResult> {
this.validateAction(action);
this.ensureInitialized();
if (action === 'myAction') {
return {
success: true,
result: { processed: params.input },
summary: `Processed input: ${params.input}`, // Optional human-readable summary
};
}
return { success: false, error: 'Unknown action' };
}
// Human-readable summary for Guardian evaluation
public getCallSummary(action: string, params: Record<string, unknown>): string {
return `Custom action "${action}" with input "${params.input}"`;
}
}
// Register custom tool
orchestrator.registerTool(new MyCustomTool());
Reusing SmartAi Instances
If you already have a @push.rocks/smartai instance, you can share it:
import { SmartAi } from '@push.rocks/smartai';
import { DualAgentOrchestrator } from '@push.rocks/smartagent';
const smartai = new SmartAi({ openaiToken: 'sk-...' });
await smartai.start();
const orchestrator = new DualAgentOrchestrator({
smartAiInstance: smartai, // Reuse existing instance
guardianPolicyPrompt: '...',
});
await orchestrator.start();
// ... use orchestrator ...
await orchestrator.stop();
// SmartAi instance lifecycle is managed separately
await smartai.stop();
Supported Providers
SmartAgent supports all providers from @push.rocks/smartai:
| Provider | Driver | Guardian |
|---|---|---|
| OpenAI | ✅ | ✅ |
| Anthropic | ✅ | ✅ |
| Perplexity | ✅ | ✅ |
| Groq | ✅ | ✅ |
| Ollama | ✅ | ✅ |
| XAI | ✅ | ✅ |
| Exo | ✅ | ✅ |
💡 Pro tip: Use a faster/cheaper model for Guardian (like Groq) and a more capable model for Driver:
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
groqToken: 'gsk-...',
defaultProvider: 'openai', // Driver uses OpenAI
guardianProvider: 'groq', // Guardian uses Groq (faster, cheaper)
guardianPolicyPrompt: '...',
});
API Reference
DualAgentOrchestrator
| Method | Description |
|---|---|
start() |
Initialize all tools and AI providers |
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, 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
// Main classes
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';
export { HttpTool } from '@push.rocks/smartagent';
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 (IExpertConfig, IToolMetadata, etc.)
// Re-exported from @push.rocks/smartai
export { type ISmartAiOptions, type TProvider, type ChatMessage, type ChatOptions, type ChatResponse };
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the LICENSE file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.