16 Commits

Author SHA1 Message Date
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
b1deccaa26 v1.4.0
Some checks failed
Default (tags) / security (push) Successful in 32s
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:30:26 +00:00
52d1d128c7 feat(docs): document Dual-Agent Driver/Guardian architecture, new standard tools, streaming/vision support, progress events, and updated API/export docs 2026-01-20 01:30:26 +00:00
60f8bbe1b6 feat(tools): add getToolExplanation() method with XML examples for LLM tool calling
Each tool now provides comprehensive documentation including parameter
schemas and concrete <tool_call> XML examples. This helps smaller LLMs
understand the exact format required for tool invocation.
2026-01-20 01:30:03 +00:00
b6308d2113 v1.3.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:12:03 +00:00
e7968a31b1 feat(smartagent): add JsonValidatorTool and support passing base64-encoded images with task runs (vision-capable models); bump @push.rocks/smartai to ^0.12.0 2026-01-20 01:12:03 +00:00
05e4f03061 v1.2.8 2026-01-20 00:38:41 +00:00
37d4069806 feat(streaming): add streaming support to DriverAgent and DualAgentOrchestrator
- Add onToken callback option to IDualAgentOptions interface
- Add onToken property and setOnToken method to DriverAgent
- Wire up streaming in startTask and continueWithMessage methods
- Pass source identifier ('driver'/'guardian') to onToken callback
2026-01-20 00:38:36 +00:00
fe0de36b1a v1.2.7
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 00:13:32 +00:00
e49f35e7de fix(deps(smartai)): bump @push.rocks/smartai to ^0.11.0 2026-01-20 00:13:32 +00:00
4fbffdd97d v1.2.6
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 1m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 00:05:42 +00:00
560838477f fix(deps): bump @push.rocks/smartai to ^0.10.1 2026-01-20 00:05:42 +00:00
7503fccbf2 1.2.5
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:30:13 +00:00
a76bd0d3e4 update 2025-12-15 15:29:56 +00:00
18 changed files with 1036 additions and 71 deletions

View File

@@ -1,5 +1,45 @@
# Changelog
## 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
- Add DualAgentOrchestrator concept and describe Driver/Guardian agents and BaseToolWrapper
- Document six standard tools including new JsonValidatorTool and expanded descriptions for Filesystem, Http, Shell, Browser, Deno
- Add examples for scoped filesystem with exclusion patterns and line-range reads
- Add token streaming (onToken) and progress events (onProgress) examples and event types
- Document vision support for passing images as base64 and example usage
- Expose additional config options in docs: name, verbose, maxResultChars, maxHistoryMessages, onProgress, onToken, logPrefix
- Document additional result fields: toolCallCount, rejectionCount, toolLog, and error
- Update API signatures in docs: run(task, options?) and registerScopedFilesystemTool(basePath, excludePatterns?)
- Update re-exports to include IFilesystemToolOptions, TDenoPermission, JsonValidatorTool and re-export several smartai types
## 2026-01-20 - 1.3.0 - feat(smartagent)
add JsonValidatorTool and support passing base64-encoded images with task runs (vision-capable models); bump @push.rocks/smartai to ^0.12.0
- Add JsonValidatorTool (validate/format actions) implemented in ts/smartagent.tools.json.ts
- Export JsonValidatorTool from ts/index.ts
- Add ITaskRunOptions interface (images?: string[]) in smartagent.interfaces.ts
- DualAgent.run and Driver.startTask accept optional images and pass them to provider.chat/provider.chatStreaming; assistant responses added to message history
- Bump dependency @push.rocks/smartai from ^0.11.1 to ^0.12.0 in package.json
## 2026-01-20 - 1.2.7 - fix(deps(smartai))
bump @push.rocks/smartai to ^0.11.0
- package.json: @push.rocks/smartai updated from ^0.10.1 to ^0.11.0
- Recommend a patch release since this is a dependency update with no breaking API changes: 1.2.7
## 2026-01-20 - 1.2.6 - fix(deps)
bump @push.rocks/smartai to ^0.10.1
- Updated dependency @push.rocks/smartai from ^0.8.0 to ^0.10.1 in package.json
- No other code changes; dependency-only update
## 2025-12-15 - 1.1.1 - fix(ci)
Update CI/release config and bump devDependencies; enable verbose tests

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartagent",
"version": "1.2.4",
"version": "1.4.1",
"private": false,
"description": "an agentic framework built on top of @push.rocks/smartai",
"main": "dist_ts/index.js",
@@ -21,12 +21,13 @@
"@types/node": "^25.0.2"
},
"dependencies": {
"@push.rocks/smartai": "^0.8.0",
"@push.rocks/smartai": "^0.12.0",
"@push.rocks/smartbrowser": "^2.0.8",
"@push.rocks/smartdeno": "^1.2.0",
"@push.rocks/smartfs": "^1.2.0",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartshell": "^3.3.0"
"@push.rocks/smartshell": "^3.3.0",
"minimatch": "^10.1.1"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"repository": {

69
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@push.rocks/smartai':
specifier: ^0.8.0
version: 0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
specifier: ^0.12.0
version: 0.12.0(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)
@@ -26,6 +26,9 @@ importers:
'@push.rocks/smartshell':
specifier: ^3.3.0
version: 3.3.0
minimatch:
specifier: ^10.1.1
version: 10.1.1
devDependencies:
'@git.zone/tsbuild':
specifier: ^4.0.2
@@ -45,8 +48,8 @@ importers:
packages:
'@anthropic-ai/sdk@0.65.0':
resolution: {integrity: sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==}
'@anthropic-ai/sdk@0.71.2':
resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==}
hasBin: true
peerDependencies:
zod: ^3.25.0 || ^4.0.0
@@ -717,6 +720,9 @@ packages:
'@lit/reactive-element@2.1.1':
resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==}
'@mistralai/mistralai@1.12.0':
resolution: {integrity: sha512-oDr1hcS3wsIT/QupBG93TNiA5kilwBYoAIyl5BNYqMM2Ix/xsNq+wT8b++uhp/GTUMx44n+8Bn1mkATbwxe6bQ==}
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -831,8 +837,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartai@0.8.0':
resolution: {integrity: sha512-guzi28meUDc3mydC8kpoA+4pzExRQqygXYFDD4qQSWPpIRHQ7qhpeNqJzrrGezT1yOH5Gb9taPEGwT56hI+nwQ==}
'@push.rocks/smartai@0.12.0':
resolution: {integrity: sha512-T4HRaSSxO6TQGGXlQeswX2eYkB+gMu0FbKF9qCUri6FdRlYzmPDn19jgPrPJxyg5m3oj6TzflvfYwcBCFlWo/A==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -916,6 +922,9 @@ packages:
'@push.rocks/smartfs@1.2.0':
resolution: {integrity: sha512-1R47jJZwX869z7DYgKeAZKTU1SbGnM7W/ZmgsI7AkQQhiascNqY3/gF4V5kIprmuf1WhpRbCbZyum8s7J1LDdg==}
'@push.rocks/smartfs@1.3.1':
resolution: {integrity: sha512-ZSduVS8tM+/erbyCTvRRvc9gLWwbpqN5xdIIkMr+gub7fowSeJb7tR2rnGwySa63DyimU0q2KTp79VV9YqGLeg==}
'@push.rocks/smartguard@3.1.0':
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
@@ -2972,12 +2981,12 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
openai@5.23.2:
resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==}
openai@6.16.0:
resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
zod: ^3.25 || ^4.0
peerDependenciesMeta:
ws:
optional: true
@@ -3293,8 +3302,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@@ -3732,6 +3742,11 @@ packages:
resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==}
engines: {node: '>=12'}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: ^3.25 || ^4
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -3740,7 +3755,7 @@ packages:
snapshots:
'@anthropic-ai/sdk@0.65.0(zod@3.25.76)':
'@anthropic-ai/sdk@0.71.2(zod@3.25.76)':
dependencies:
json-schema-to-ts: 3.1.1
optionalDependencies:
@@ -4891,6 +4906,11 @@ snapshots:
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
'@mistralai/mistralai@1.12.0':
dependencies:
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
'@mixmark-io/domino@2.2.0': {}
'@module-federation/error-codes@0.21.6': {}
@@ -5138,17 +5158,18 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartai@0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
'@push.rocks/smartai@0.12.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
dependencies:
'@anthropic-ai/sdk': 0.65.0(zod@3.25.76)
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
'@mistralai/mistralai': 1.12.0
'@push.rocks/smartarray': 1.1.0
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartfs': 1.3.1
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpdf': 4.1.1(typescript@5.9.3)
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.4.2
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/webstream': 1.0.10
openai: 5.23.2(ws@8.18.3)(zod@3.25.76)
openai: 6.16.0(ws@8.18.3)(zod@3.25.76)
transitivePeerDependencies:
- '@nuxt/kit'
- aws-crt
@@ -5444,6 +5465,10 @@ snapshots:
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartfs@1.3.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartguard@3.1.0':
dependencies:
'@push.rocks/smartpromise': 4.2.3
@@ -8151,7 +8176,7 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
openai@5.23.2(ws@8.18.3)(zod@3.25.76):
openai@6.16.0(ws@8.18.3)(zod@3.25.76):
optionalDependencies:
ws: 8.18.3
zod: 3.25.76
@@ -8522,7 +8547,7 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.4.3: {}
sax@1.4.4: {}
semver@6.3.1: {}
@@ -8999,7 +9024,7 @@ snapshots:
xml2js@0.5.0:
dependencies:
sax: 1.4.3
sax: 1.4.4
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
@@ -9034,6 +9059,10 @@ snapshots:
buffer-crc32: 0.2.13
pend: 1.2.0
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76
zod@3.25.76: {}
zwitch@2.0.4: {}

View File

@@ -1,15 +1,39 @@
# Project Readme Hints
## Overview
`@push.rocks/smartagent` is an agentic framework built on top of `@push.rocks/smartai`. It provides autonomous AI agent capabilities including tool use, multi-step reasoning, and conversation memory.
`@push.rocks/smartagent` is a dual-agent agentic framework built on top of `@push.rocks/smartai`. It implements a Driver/Guardian architecture where the Driver proposes tool calls and the Guardian evaluates them against security policies.
## Architecture
- **SmartAgent**: Main class that wraps SmartAi and adds agentic behaviors
- **plugins.ts**: Imports and re-exports smartai
- **index.ts**: Main entry point, exports SmartAgent class and relevant types
- **DualAgentOrchestrator**: Main entry point, coordinates Driver and Guardian agents
- **DriverAgent**: Reasons about tasks, plans steps, proposes tool calls
- **GuardianAgent**: Evaluates tool calls against configured policies
- **BaseToolWrapper**: Base class for creating custom tools
- **plugins.ts**: Imports and re-exports smartai and other dependencies
## Standard Tools
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
## Key Features
- Token streaming support (`onToken` callback)
- Vision support (pass images as base64)
- Progress events (`onProgress` callback)
- Scoped filesystem with exclusion patterns
- Result truncation with configurable limits
- History windowing to manage token usage
## Key Dependencies
- `@push.rocks/smartai`: Provides the underlying multi-modal AI provider interface
- `@push.rocks/smartai`: Multi-provider AI interface
- `@push.rocks/smartfs`: Filesystem operations
- `@push.rocks/smartshell`: Shell command execution
- `@push.rocks/smartbrowser`: Browser automation
- `@push.rocks/smartdeno`: Deno code execution
- `@push.rocks/smartrequest`: HTTP requests
- `minimatch`: Glob pattern matching for exclusions
## Test Structure
- Tests use `@git.zone/tstest/tapbundle`

152
readme.md
View File

@@ -50,6 +50,7 @@ flowchart TB
Shell["Shell"]
Browser["Browser"]
Deno["Deno"]
JSON["JSON Validator"]
end
Task --> Orchestrator
@@ -99,7 +100,7 @@ await orchestrator.stop();
## Standard Tools
SmartAgent comes with five battle-tested tools out of the box:
SmartAgent comes with six battle-tested tools out of the box:
### 🗂️ FilesystemTool
@@ -117,11 +118,29 @@ File and directory operations powered by `@push.rocks/smartfs`.
</tool_call>
```
**Scoped Filesystem**: Lock file operations to a specific directory:
**Scoped Filesystem**: Lock file operations to a specific directory with optional exclusion patterns:
```typescript
// 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:
```typescript
<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
@@ -212,6 +231,105 @@ By default, code runs **fully sandboxed with no permissions**. Permissions must
</tool_call>
```
### 📋 JsonValidatorTool
Validate and format JSON data. Perfect for agents to self-check their JSON output before completing tasks.
**Actions**: `validate`, `format`
```typescript
// 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>
```
## 🎥 Streaming Support
SmartAgent supports token-by-token streaming for real-time output during LLM generation:
```typescript
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:
```typescript
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:
```typescript
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`
## Guardian Policy Examples
The Guardian's power comes from your policy. Here are battle-tested examples:
@@ -294,10 +412,19 @@ interface IDualAgentOptions {
// 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
// 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
}
```
@@ -311,6 +438,10 @@ interface IDualAgentRunResult {
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 =
@@ -365,6 +496,7 @@ class MyCustomTool extends BaseToolWrapper {
return {
success: true,
result: { processed: params.input },
summary: `Processed input: ${params.input}`, // Optional human-readable summary
};
}
@@ -439,11 +571,11 @@ const orchestrator = new DualAgentOrchestrator({
|--------|-------------|
| `start()` | Initialize all tools and AI providers |
| `stop()` | Cleanup all tools and resources |
| `run(task: string)` | Execute a task and return result |
| `continueTask(input: string)` | Continue a task with user input |
| `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 |
| `registerScopedFilesystemTool(basePath)` | Register filesystem tool with path restriction |
| `registerScopedFilesystemTool(basePath, excludePatterns?)` | Register filesystem tool with path restriction |
| `setGuardianPolicy(policy)` | Update Guardian policy at runtime |
| `getHistory()` | Get conversation history |
| `getToolNames()` | Get list of registered tool names |
@@ -459,14 +591,18 @@ export { GuardianAgent } from '@push.rocks/smartagent';
// Tools
export { BaseToolWrapper } from '@push.rocks/smartagent';
export { FilesystemTool } 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 } from '@push.rocks/smartagent';
export { DenoTool, type TDenoPermission } from '@push.rocks/smartagent';
export { JsonValidatorTool } from '@push.rocks/smartagent';
// Types and interfaces
export * from '@push.rocks/smartagent'; // All interfaces
export * from '@push.rocks/smartagent'; // All interfaces
// Re-exported from @push.rocks/smartai
export { type ISmartAiOptions, type TProvider, type ChatMessage, type ChatOptions, type ChatResponse };
```
## License and Legal Information

View File

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

View File

@@ -16,6 +16,7 @@ export { HttpTool } from './smartagent.tools.http.js';
export { ShellTool } from './smartagent.tools.shell.js';
export { BrowserTool } from './smartagent.tools.browser.js';
export { DenoTool, type TDenoPermission } from './smartagent.tools.deno.js';
export { JsonValidatorTool } from './smartagent.tools.json.js';
// Export all interfaces
export * from './smartagent.interfaces.js';

View File

@@ -3,6 +3,11 @@ import * as path from 'path';
export { path };
// third party
import { minimatch } from 'minimatch';
export { minimatch };
// @push.rocks scope
import * as smartai from '@push.rocks/smartai';
import * as smartdeno from '@push.rocks/smartdeno';

View File

@@ -10,6 +10,8 @@ export interface IDriverAgentOptions {
systemMessage?: string;
/** Maximum history messages to pass to API (default: 20). Set to 0 for unlimited. */
maxHistoryMessages?: number;
/** Callback fired for each token during LLM generation */
onToken?: (token: string) => void;
}
/**
@@ -22,6 +24,7 @@ export class DriverAgent {
private maxHistoryMessages: number;
private messageHistory: plugins.smartai.ChatMessage[] = [];
private tools: Map<string, BaseToolWrapper> = new Map();
private onToken?: (token: string) => void;
constructor(
provider: plugins.smartai.MultiModalModel,
@@ -36,9 +39,18 @@ export class DriverAgent {
} else {
this.systemMessage = options?.systemMessage || this.getDefaultSystemMessage();
this.maxHistoryMessages = options?.maxHistoryMessages ?? 20;
this.onToken = options?.onToken;
}
}
/**
* Set the token callback for streaming mode
* @param callback Function to call for each generated token
*/
public setOnToken(callback: (token: string) => void): void {
this.onToken = callback;
}
/**
* Register a tool for use by the driver
*/
@@ -55,8 +67,10 @@ export class DriverAgent {
/**
* Initialize a new conversation for a task
* @param task The task description
* @param images Optional base64-encoded images for vision tasks
*/
public async startTask(task: string): Promise<interfaces.IAgentMessage> {
public async startTask(task: string, images?: string[]): Promise<interfaces.IAgentMessage> {
// Reset message history
this.messageHistory = [];
@@ -85,18 +99,34 @@ export class DriverAgent {
fullSystemMessage = this.getNoToolsSystemMessage();
}
// Get response from provider
const response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
});
// Get response from provider - use streaming if available and callback is set
let response: plugins.smartai.ChatResponse;
// Add assistant response to history
this.messageHistory.push({
if (this.onToken && typeof (this.provider as any).chatStreaming === 'function') {
// Use streaming mode with token callback
response = await (this.provider as any).chatStreaming({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
onToken: this.onToken,
});
} else {
// Fallback to non-streaming mode
response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
});
}
// Add assistant response to history (store images if provided)
const historyMessage: plugins.smartai.ChatMessage = {
role: 'assistant',
content: response.message,
});
};
this.messageHistory.push(historyMessage);
return {
role: 'assistant',
@@ -139,11 +169,25 @@ export class DriverAgent {
historyForChat = fullHistory;
}
const response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
});
// Get response from provider - use streaming if available and callback is set
let response: plugins.smartai.ChatResponse;
if (this.onToken && typeof (this.provider as any).chatStreaming === 'function') {
// Use streaming mode with token callback
response = await (this.provider as any).chatStreaming({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
onToken: this.onToken,
});
} else {
// Fallback to non-streaming mode
response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
});
}
// Add assistant response to history
this.messageHistory.push({

View File

@@ -158,9 +158,10 @@ export class DualAgentOrchestrator {
/**
* Register a scoped filesystem tool that can only access files within the specified directory
* @param basePath The directory to scope filesystem operations to
* @param excludePatterns Optional glob patterns to exclude from listings (e.g., ['.nogit/**', 'node_modules/**'])
*/
public registerScopedFilesystemTool(basePath: string): void {
const scopedTool = new FilesystemTool({ basePath });
public registerScopedFilesystemTool(basePath: string, excludePatterns?: string[]): void {
const scopedTool = new FilesystemTool({ basePath, excludePatterns });
this.registerTool(scopedTool);
}
@@ -180,9 +181,15 @@ export class DualAgentOrchestrator {
: this.driverProvider;
// NOW create agents with initialized providers
// Set up token callback wrapper if streaming is enabled
const driverOnToken = this.options.onToken
? (token: string) => this.options.onToken!(token, 'driver')
: undefined;
this.driver = new DriverAgent(this.driverProvider, {
systemMessage: this.options.driverSystemMessage,
maxHistoryMessages: this.options.maxHistoryMessages,
onToken: driverOnToken,
});
this.guardian = new GuardianAgent(this.guardianProvider, this.options.guardianPolicyPrompt);
@@ -227,8 +234,10 @@ export class DualAgentOrchestrator {
/**
* Run a task through the dual-agent system
* @param task The task description
* @param options Optional task run options (e.g., images for vision tasks)
*/
public async run(task: string): Promise<interfaces.IDualAgentRunResult> {
public async run(task: string, options?: interfaces.ITaskRunOptions): Promise<interfaces.IDualAgentRunResult> {
if (!this.isRunning) {
throw new Error('Orchestrator not started. Call start() first.');
}
@@ -239,14 +248,17 @@ export class DualAgentOrchestrator {
let completed = false;
let finalResult: string | null = null;
// Extract images from options
const images = options?.images;
// Add initial task to history
this.conversationHistory.push({
role: 'user',
content: task,
});
// Start the driver with the task
let driverResponse = await this.driver.startTask(task);
// Start the driver with the task and optional images
let driverResponse = await this.driver.startTask(task, images);
this.conversationHistory.push(driverResponse);
// Emit task started event
@@ -307,9 +319,27 @@ export class DualAgentOrchestrator {
const proposals = this.driver.parseToolCallProposals(driverResponse.content);
if (proposals.length === 0) {
// No tool calls, continue the conversation
// No tool calls found - remind the model of the exact XML format
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>.'
`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;

View File

@@ -1,5 +1,17 @@
import * as plugins from './plugins.js';
// ================================
// Task Run Options
// ================================
/**
* Options for running a task with the DualAgentOrchestrator
*/
export interface ITaskRunOptions {
/** Base64-encoded images to include with the task (for vision-capable models) */
images?: string[];
}
// ================================
// Agent Configuration Interfaces
// ================================
@@ -34,6 +46,8 @@ export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
onProgress?: (event: IProgressEvent) => void;
/** Prefix for log messages (e.g., "[README]", "[Commit]"). Default: empty */
logPrefix?: string;
/** Callback fired for each token during LLM generation (streaming mode) */
onToken?: (token: string, source: 'driver' | 'guardian') => void;
}
// ================================

View File

@@ -35,6 +35,13 @@ export abstract class BaseToolWrapper implements interfaces.IAgentToolWrapper {
*/
abstract getCallSummary(action: string, params: Record<string, unknown>): string;
/**
* Get a comprehensive explanation of this tool for LLM consumption.
* Tools should implement this to provide detailed usage instructions with examples.
* This includes parameter schemas and concrete <tool_call> XML examples.
*/
abstract getToolExplanation(): string;
/**
* Validate that an action exists for this tool
* @throws Error if the action is not valid
@@ -60,14 +67,10 @@ export abstract class BaseToolWrapper implements interfaces.IAgentToolWrapper {
/**
* Get the full tool description including all actions
* Used for Driver's tool awareness
* Used for Driver's tool awareness - now delegates to getToolExplanation()
*/
public getFullDescription(): string {
const actionDescriptions = this.actions
.map((a) => ` - ${a.name}: ${a.description}`)
.join('\n');
return `${this.name}: ${this.description}\nActions:\n${actionDescriptions}`;
return this.getToolExplanation();
}
/**

View File

@@ -176,6 +176,59 @@ export class BrowserTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: browser
Interact with web pages - take screenshots, generate PDFs, and execute JavaScript on pages.
### Actions:
**screenshot** - Take a screenshot of a webpage
Parameters:
- url (required): URL of the page to screenshot
Example:
<tool_call>
<tool>browser</tool>
<action>screenshot</action>
<params>{"url": "https://example.com"}</params>
</tool_call>
**pdf** - Generate a PDF from a webpage
Parameters:
- url (required): URL of the page to convert to PDF
Example:
<tool_call>
<tool>browser</tool>
<action>pdf</action>
<params>{"url": "https://example.com/report"}</params>
</tool_call>
**evaluate** - Execute JavaScript code on a webpage and return the result
Parameters:
- url (required): URL of the page to run the script on
- script (required): JavaScript code to execute (must return a value)
Example:
<tool_call>
<tool>browser</tool>
<action>evaluate</action>
<params>{"url": "https://example.com", "script": "document.querySelectorAll('a').length"}</params>
</tool_call>
**getPageContent** - Get the text content and title of a webpage
Parameters:
- url (required): URL of the page to get content from
Example:
<tool_call>
<tool>browser</tool>
<action>getPageContent</action>
<params>{"url": "https://example.com"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'screenshot':

View File

@@ -164,6 +164,45 @@ export class DenoTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: deno
Execute TypeScript/JavaScript code in a sandboxed Deno environment with fine-grained permission control.
### Actions:
**execute** - Execute TypeScript/JavaScript code and return stdout/stderr
Parameters:
- code (required): TypeScript/JavaScript code to execute
- permissions (optional): Array of Deno permissions to grant. Options: "all", "env", "net", "read", "write", "run", "sys", "ffi", "hrtime". Default: none (fully sandboxed)
Example - Simple execution:
<tool_call>
<tool>deno</tool>
<action>execute</action>
<params>{"code": "console.log('Hello from Deno!');"}</params>
</tool_call>
Example - With network permission:
<tool_call>
<tool>deno</tool>
<action>execute</action>
<params>{"code": "const resp = await fetch('https://api.example.com/data');\\nconsole.log(await resp.text());", "permissions": ["net"]}</params>
</tool_call>
**executeWithResult** - Execute code that outputs JSON on the last line of stdout
Parameters:
- code (required): Code that console.logs a JSON value on the final line
- permissions (optional): Array of Deno permissions to grant
Example:
<tool_call>
<tool>deno</tool>
<action>executeWithResult</action>
<params>{"code": "const result = { sum: 1 + 2, product: 2 * 3 };\\nconsole.log(JSON.stringify(result));"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
const code = params.code as string;
const permissions = (params.permissions as string[]) || [];

View File

@@ -8,6 +8,8 @@ import { BaseToolWrapper } from './smartagent.tools.base.js';
export interface IFilesystemToolOptions {
/** Base path to scope all operations to. If set, all paths must be within this directory. */
basePath?: string;
/** Glob patterns to exclude from listings (e.g., ['.nogit/**', 'node_modules/**']) */
excludePatterns?: string[];
}
/**
@@ -20,12 +22,25 @@ export class FilesystemTool extends BaseToolWrapper {
/** Base path to scope all operations to */
private basePath?: string;
/** Glob patterns to exclude from listings */
private excludePatterns: string[];
constructor(options?: IFilesystemToolOptions) {
super();
if (options?.basePath) {
this.basePath = plugins.path.resolve(options.basePath);
}
this.excludePatterns = options?.excludePatterns || [];
}
/**
* Check if a relative path should be excluded based on exclude patterns
*/
private isExcluded(relativePath: string): boolean {
if (this.excludePatterns.length === 0) return false;
return this.excludePatterns.some(pattern =>
plugins.minimatch(relativePath, pattern, { dot: true })
);
}
/**
@@ -361,7 +376,16 @@ export class FilesystemTool extends BaseToolWrapper {
if (params.filter) {
dir = dir.filter(params.filter as string);
}
const entries = await dir.list();
let entries = await dir.list();
// Filter out excluded paths
if (this.excludePatterns.length > 0) {
entries = entries.filter(entry => {
const relativePath = plugins.path.relative(validatedPath, entry.path);
return !this.isExcluded(relativePath) && !this.isExcluded(entry.name);
});
}
return {
success: true,
result: {
@@ -499,6 +523,11 @@ export class FilesystemTool extends BaseToolWrapper {
const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
const isDir = item.isDirectory;
// Skip excluded paths
if (this.isExcluded(itemRelPath) || this.isExcluded(item.name)) {
continue;
}
const entry: ITreeEntry = {
path: itemPath,
relativePath: itemRelPath,
@@ -603,11 +632,14 @@ export class FilesystemTool extends BaseToolWrapper {
const matches = await dir.list();
// Return file paths relative to base path for readability
const files = matches.map((entry) => ({
path: entry.path,
relativePath: plugins.path.relative(basePath, entry.path),
isDirectory: entry.isDirectory,
}));
// Filter out excluded paths
const files = matches
.map((entry) => ({
path: entry.path,
relativePath: plugins.path.relative(basePath, entry.path),
isDirectory: entry.isDirectory,
}))
.filter((file) => !this.isExcluded(file.relativePath));
return {
success: true,
@@ -634,6 +666,170 @@ export class FilesystemTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: filesystem
Read, write, list, and delete files and directories.
### Actions:
**read** - Read file contents (full or specific line range)
Parameters:
- path (required): Path to the file
- encoding (optional): File encoding - "utf8" (default), "binary", or "base64"
- startLine (optional): First line to read (1-indexed, inclusive)
- endLine (optional): Last line to read (1-indexed, inclusive)
Example:
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/path/to/file.txt"}</params>
</tool_call>
Example with line range:
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/path/to/file.txt", "startLine": 10, "endLine": 20}</params>
</tool_call>
**write** - Write content to a file (creates or overwrites)
Parameters:
- path (required): Absolute path to the file
- content (required): Content to write
- encoding (optional): File encoding - "utf8" (default), "binary", or "base64"
Example:
<tool_call>
<tool>filesystem</tool>
<action>write</action>
<params>{"path": "/path/to/output.txt", "content": "Hello, World!"}</params>
</tool_call>
**list** - List files and directories in a path
Parameters:
- path (required): Directory path to list
- recursive (optional): List recursively (default: false)
- filter (optional): Glob pattern to filter results (e.g., "*.ts")
Example:
<tool_call>
<tool>filesystem</tool>
<action>list</action>
<params>{"path": "/path/to/dir", "recursive": true, "filter": "*.ts"}</params>
</tool_call>
**exists** - Check if a file or directory exists
Parameters:
- path (required): Path to check
Example:
<tool_call>
<tool>filesystem</tool>
<action>exists</action>
<params>{"path": "/path/to/check"}</params>
</tool_call>
**mkdir** - Create a directory
Parameters:
- path (required): Directory path to create
- recursive (optional): Create parent directories if needed (default: true)
Example:
<tool_call>
<tool>filesystem</tool>
<action>mkdir</action>
<params>{"path": "/path/to/new/dir"}</params>
</tool_call>
**delete** - Delete a file or directory
Parameters:
- path (required): Path to delete
- recursive (optional): For directories, delete recursively (default: false)
Example:
<tool_call>
<tool>filesystem</tool>
<action>delete</action>
<params>{"path": "/path/to/delete", "recursive": true}</params>
</tool_call>
**copy** - Copy a file to a new location
Parameters:
- source (required): Source file path
- destination (required): Destination file path
Example:
<tool_call>
<tool>filesystem</tool>
<action>copy</action>
<params>{"source": "/path/to/source.txt", "destination": "/path/to/dest.txt"}</params>
</tool_call>
**move** - Move a file to a new location
Parameters:
- source (required): Source file path
- destination (required): Destination file path
Example:
<tool_call>
<tool>filesystem</tool>
<action>move</action>
<params>{"source": "/path/to/old.txt", "destination": "/path/to/new.txt"}</params>
</tool_call>
**stat** - Get file or directory statistics (size, dates, etc.)
Parameters:
- path (required): Path to get stats for
Example:
<tool_call>
<tool>filesystem</tool>
<action>stat</action>
<params>{"path": "/path/to/file.txt"}</params>
</tool_call>
**append** - Append content to a file
Parameters:
- path (required): Absolute path to the file
- content (required): Content to append
Example:
<tool_call>
<tool>filesystem</tool>
<action>append</action>
<params>{"path": "/path/to/log.txt", "content": "New log entry\\n"}</params>
</tool_call>
**tree** - Show directory structure as a tree
Parameters:
- path (required): Root directory path
- maxDepth (optional): Maximum depth to traverse (default: 3)
- filter (optional): Glob pattern to filter files
- showSizes (optional): Include file sizes in output (default: false)
- format (optional): Output format - "string" (default) or "json"
Example:
<tool_call>
<tool>filesystem</tool>
<action>tree</action>
<params>{"path": "/path/to/dir", "maxDepth": 2}</params>
</tool_call>
**glob** - Find files matching a glob pattern
Parameters:
- pattern (required): Glob pattern (e.g., "**/*.ts", "src/**/*.js")
- path (optional): Base path to search from
Example:
<tool_call>
<tool>filesystem</tool>
<action>glob</action>
<params>{"pattern": "**/*.ts", "path": "/path/to/project"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'read': {

View File

@@ -180,6 +180,84 @@ export class HttpTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: http
Make HTTP requests to web APIs and services.
### Actions:
**get** - Make a GET request
Parameters:
- url (required): URL to request
- headers (optional): Request headers (key-value object)
- query (optional): Query parameters (key-value object)
- timeout (optional): Timeout in milliseconds
Example:
<tool_call>
<tool>http</tool>
<action>get</action>
<params>{"url": "https://api.example.com/data", "headers": {"Authorization": "Bearer token123"}}</params>
</tool_call>
**post** - Make a POST request with JSON body
Parameters:
- url (required): URL to request
- body (optional): JSON body to send
- headers (optional): Request headers (key-value object)
- query (optional): Query parameters (key-value object)
- timeout (optional): Timeout in milliseconds
Example:
<tool_call>
<tool>http</tool>
<action>post</action>
<params>{"url": "https://api.example.com/submit", "body": {"name": "test", "value": 123}}</params>
</tool_call>
**put** - Make a PUT request with JSON body
Parameters:
- url (required): URL to request
- body (required): JSON body to send
- headers (optional): Request headers (key-value object)
- timeout (optional): Timeout in milliseconds
Example:
<tool_call>
<tool>http</tool>
<action>put</action>
<params>{"url": "https://api.example.com/resource/1", "body": {"name": "updated"}}</params>
</tool_call>
**patch** - Make a PATCH request with JSON body
Parameters:
- url (required): URL to request
- body (required): JSON body to send
- headers (optional): Request headers (key-value object)
- timeout (optional): Timeout in milliseconds
Example:
<tool_call>
<tool>http</tool>
<action>patch</action>
<params>{"url": "https://api.example.com/resource/1", "body": {"status": "active"}}</params>
</tool_call>
**delete** - Make a DELETE request
Parameters:
- url (required): URL to request
- headers (optional): Request headers (key-value object)
- timeout (optional): Timeout in milliseconds
Example:
<tool_call>
<tool>http</tool>
<action>delete</action>
<params>{"url": "https://api.example.com/resource/1"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
const method = action.toUpperCase();
let summary = `${method} request to "${params.url}"`;

224
ts/smartagent.tools.json.ts Normal file
View File

@@ -0,0 +1,224 @@
import * as interfaces from './smartagent.interfaces.js';
import { BaseToolWrapper } from './smartagent.tools.base.js';
/**
* JsonValidatorTool - Validates and formats JSON data
* Useful for agents to self-validate their JSON output before completing a task
*/
export class JsonValidatorTool extends BaseToolWrapper {
public name = 'json';
public description = 'Validate and format JSON data. Use this to verify your JSON output is valid before completing a task.';
public actions: interfaces.IToolAction[] = [
{
name: 'validate',
description: 'Validate that a string is valid JSON and optionally check required fields',
parameters: {
type: 'object',
properties: {
jsonString: {
type: 'string',
description: 'The JSON string to validate',
},
requiredFields: {
type: 'array',
items: { type: 'string' },
description: 'Optional list of field names that must be present at the root level',
},
},
required: ['jsonString'],
},
},
{
name: 'format',
description: 'Parse and pretty-print JSON string',
parameters: {
type: 'object',
properties: {
jsonString: {
type: 'string',
description: 'The JSON string to format',
},
},
required: ['jsonString'],
},
},
];
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 'validate':
return this.validateJson(params);
case 'format':
return this.formatJson(params);
default:
return { success: false, error: `Unknown action: ${action}` };
}
}
/**
* Validate JSON string and optionally check for required fields
*/
private validateJson(params: Record<string, unknown>): interfaces.IToolExecutionResult {
const jsonString = params.jsonString as string;
const requiredFields = params.requiredFields as string[] | undefined;
if (!jsonString || typeof jsonString !== 'string') {
return {
success: false,
error: 'jsonString parameter is required and must be a string',
};
}
try {
const parsed = JSON.parse(jsonString);
// Check required fields if specified
if (requiredFields && Array.isArray(requiredFields)) {
const missingFields = requiredFields.filter((field) => {
if (typeof parsed !== 'object' || parsed === null) {
return true;
}
return !(field in parsed);
});
if (missingFields.length > 0) {
return {
success: false,
error: `Missing required fields: ${missingFields.join(', ')}`,
result: {
valid: false,
missingFields,
presentFields: Object.keys(parsed || {}),
},
};
}
}
return {
success: true,
result: {
valid: true,
parsed,
type: Array.isArray(parsed) ? 'array' : typeof parsed,
fieldCount: typeof parsed === 'object' && parsed !== null ? Object.keys(parsed).length : undefined,
},
summary: `JSON is valid (${Array.isArray(parsed) ? 'array' : typeof parsed})`,
};
} catch (error) {
const errorMessage = (error as Error).message;
// Extract position from error message if available
const posMatch = errorMessage.match(/position\s*(\d+)/i);
const position = posMatch ? parseInt(posMatch[1]) : undefined;
// Provide context around the error position
let context: string | undefined;
if (position !== undefined) {
const start = Math.max(0, position - 20);
const end = Math.min(jsonString.length, position + 20);
context = jsonString.substring(start, end);
}
return {
success: false,
error: `Invalid JSON: ${errorMessage}`,
result: {
valid: false,
errorPosition: position,
errorContext: context,
},
};
}
}
/**
* Format/pretty-print JSON string
*/
private formatJson(params: Record<string, unknown>): interfaces.IToolExecutionResult {
const jsonString = params.jsonString as string;
if (!jsonString || typeof jsonString !== 'string') {
return {
success: false,
error: 'jsonString parameter is required and must be a string',
};
}
try {
const parsed = JSON.parse(jsonString);
const formatted = JSON.stringify(parsed, null, 2);
return {
success: true,
result: formatted,
summary: `Formatted JSON (${formatted.length} chars)`,
};
} catch (error) {
return {
success: false,
error: `Cannot format invalid JSON: ${(error as Error).message}`,
};
}
}
public getToolExplanation(): string {
return `## Tool: json
Validate and format JSON data. Use this to verify your JSON output is valid before completing a task.
### Actions:
**validate** - Validate that a string is valid JSON and optionally check required fields
Parameters:
- jsonString (required): The JSON string to validate
- requiredFields (optional): Array of field names that must be present
Example:
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "{\\"invoice_number\\":\\"INV-001\\",\\"total\\":99.99}", "requiredFields": ["invoice_number", "total"]}</params>
</tool_call>
**format** - Parse and pretty-print JSON string
Parameters:
- jsonString (required): The JSON string to format
Example:
<tool_call>
<tool>json</tool>
<action>format</action>
<params>{"jsonString": "{\\"name\\":\\"test\\",\\"value\\":123}"}</params>
</tool_call>
`;
}
getCallSummary(action: string, params: Record<string, unknown>): string {
const jsonStr = (params.jsonString as string) || '';
const preview = jsonStr.length > 50 ? jsonStr.substring(0, 50) + '...' : jsonStr;
switch (action) {
case 'validate':
const fields = params.requiredFields as string[] | undefined;
const fieldInfo = fields ? ` (checking fields: ${fields.join(', ')})` : '';
return `Validate JSON: ${preview}${fieldInfo}`;
case 'format':
return `Format JSON: ${preview}`;
default:
return `JSON ${action}: ${preview}`;
}
}
}

View File

@@ -148,6 +148,54 @@ export class ShellTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: shell
Execute shell commands securely. Uses execSpawn (shell:false) to prevent command injection.
### Actions:
**execute** - Execute a command with arguments (secure, no shell injection possible)
Parameters:
- command (required): The command to execute (e.g., "ls", "cat", "grep", "node")
- args (optional): Array of arguments (each argument is properly escaped)
- cwd (optional): Working directory for the command
- timeout (optional): Timeout in milliseconds
- env (optional): Additional environment variables (key-value object)
Example - List files:
<tool_call>
<tool>shell</tool>
<action>execute</action>
<params>{"command": "ls", "args": ["-la", "/path/to/dir"]}</params>
</tool_call>
Example - Run Node script:
<tool_call>
<tool>shell</tool>
<action>execute</action>
<params>{"command": "node", "args": ["script.js"], "cwd": "/path/to/project"}</params>
</tool_call>
Example - Search in files:
<tool_call>
<tool>shell</tool>
<action>execute</action>
<params>{"command": "grep", "args": ["-r", "pattern", "src/"]}</params>
</tool_call>
**which** - Check if a command exists and get its path
Parameters:
- command (required): Command name to look up (e.g., "node", "git")
Example:
<tool_call>
<tool>shell</tool>
<action>which</action>
<params>{"command": "node"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'execute': {