initial
This commit is contained in:
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.nogit/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.serena/
|
||||
|
||||
#------# custom
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npmci": {
|
||||
"type": "object",
|
||||
"description": "settings for npmci"
|
||||
},
|
||||
"gitzone": {
|
||||
"type": "object",
|
||||
"description": "settings for gitzone",
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
npmextra.json
Normal file
18
npmextra.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartagent",
|
||||
"description": "an agentic framework built on top of @push.rocks/smartai",
|
||||
"npmPackagename": "@push.rocks/smartagent",
|
||||
"license": "MIT",
|
||||
"projectDomain": "push.rocks"
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
}
|
||||
}
|
||||
54
package.json
Normal file
54
package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@push.rocks/smartagent",
|
||||
"version": "1.0.1",
|
||||
"private": false,
|
||||
"description": "an agentic framework built on top of @push.rocks/smartai",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "(tsdoc)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsbundle": "^2.6.2",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^24.10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartai": "^0.8.0",
|
||||
"@push.rocks/smartbrowser": "^2.0.8",
|
||||
"@push.rocks/smartfs": "^1.2.0",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartshell": "^3.3.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartagent.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/push.rocks/smartagent/issues"
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/smartagent#readme",
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
8967
pnpm-lock.yaml
generated
Normal file
8967
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
readme.hints.md
Normal file
16
readme.hints.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
|
||||
## Key Dependencies
|
||||
- `@push.rocks/smartai`: Provides the underlying multi-modal AI provider interface
|
||||
|
||||
## Test Structure
|
||||
- Tests use `@git.zone/tstest/tapbundle`
|
||||
- Tests must end with `export default tap.start();`
|
||||
299
readme.md
Normal file
299
readme.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# @push.rocks/smartagent
|
||||
A dual-agent agentic framework with Driver and Guardian agents for safe, policy-controlled AI task execution.
|
||||
|
||||
## Install
|
||||
```bash
|
||||
npm install @push.rocks/smartagent
|
||||
# or
|
||||
pnpm install @push.rocks/smartagent
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
SmartAgent implements a dual-agent architecture:
|
||||
|
||||
- **Driver Agent**: Executes tasks, reasons about goals, and proposes tool calls
|
||||
- **Guardian Agent**: Evaluates tool call proposals against a policy prompt, approving or rejecting with feedback
|
||||
|
||||
This design ensures safe tool use through AI-based policy evaluation rather than rigid programmatic rules.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Task + Guardian Policy Prompt
|
||||
|
|
||||
+---------------------------------------+
|
||||
| DualAgentOrchestrator |
|
||||
| |
|
||||
| +--------+ +------------+ |
|
||||
| | Driver |-------> | Guardian | |
|
||||
| | Agent | tool | Agent | |
|
||||
| | | call | | |
|
||||
| | Reason |<--------| Evaluate | |
|
||||
| | + Plan | approve | against | |
|
||||
| +--------+ /reject | policy | |
|
||||
| | +feedback+-----------+ |
|
||||
| v (if approved) |
|
||||
| +-----------------------------------+|
|
||||
| | Standard Tools ||
|
||||
| | Filesystem | HTTP | Shell | Browser|
|
||||
| +-----------------------------------+|
|
||||
+---------------------------------------+
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
### FilesystemTool
|
||||
File and directory operations using `@push.rocks/smartfs`.
|
||||
|
||||
**Actions**: `read`, `write`, `append`, `list`, `delete`, `exists`, `stat`, `copy`, `move`, `mkdir`
|
||||
|
||||
```typescript
|
||||
// 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>
|
||||
```
|
||||
|
||||
### HttpTool
|
||||
HTTP requests using `@push.rocks/smartrequest`.
|
||||
|
||||
**Actions**: `get`, `post`, `put`, `patch`, `delete`
|
||||
|
||||
```typescript
|
||||
<tool_call>
|
||||
<tool>http</tool>
|
||||
<action>get</action>
|
||||
<params>{"url": "https://api.example.com/data"}</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).
|
||||
|
||||
**Actions**: `execute`, `which`
|
||||
|
||||
```typescript
|
||||
<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>
|
||||
```
|
||||
|
||||
### BrowserTool
|
||||
Web page interaction using `@push.rocks/smartbrowser` (Puppeteer-based).
|
||||
|
||||
**Actions**: `screenshot`, `pdf`, `evaluate`, `getPageContent`
|
||||
|
||||
```typescript
|
||||
<tool_call>
|
||||
<tool>browser</tool>
|
||||
<action>getPageContent</action>
|
||||
<params>{"url": "https://example.com"}</params>
|
||||
<reasoning>Extracting text content from the webpage</reasoning>
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
## Guardian Policy Examples
|
||||
|
||||
### Strict Security Policy
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
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)
|
||||
`;
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```typescript
|
||||
interface IDualAgentOptions {
|
||||
// Provider tokens (from @push.rocks/smartai)
|
||||
openaiToken?: string;
|
||||
anthropicToken?: string;
|
||||
perplexityToken?: string;
|
||||
groqToken?: string;
|
||||
xaiToken?: string;
|
||||
|
||||
// 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
|
||||
|
||||
// Limits
|
||||
maxIterations?: number; // Max task iterations (default: 20)
|
||||
maxConsecutiveRejections?: number; // Abort after N rejections (default: 3)
|
||||
}
|
||||
```
|
||||
|
||||
## Result Interface
|
||||
|
||||
```typescript
|
||||
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.
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Tools
|
||||
|
||||
Create custom tools by extending `BaseToolWrapper`:
|
||||
|
||||
```typescript
|
||||
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> {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
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 },
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, error: 'Unknown action' };
|
||||
}
|
||||
|
||||
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||||
return `Custom action "${action}" with input "${params.input}"`;
|
||||
}
|
||||
}
|
||||
|
||||
// Register custom tool
|
||||
orchestrator.registerTool(new MyCustomTool());
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
|
||||
| Provider | Driver | Guardian |
|
||||
|----------|:------:|:--------:|
|
||||
| OpenAI | Yes | Yes |
|
||||
| Anthropic | Yes | Yes |
|
||||
| Perplexity | Yes | Yes |
|
||||
| Groq | Yes | Yes |
|
||||
| Ollama | Yes | Yes |
|
||||
| XAI | Yes | Yes |
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**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 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, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require 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.
|
||||
115
test/test.ts
Normal file
115
test/test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartagent from '../ts/index.js';
|
||||
|
||||
// Test exports
|
||||
tap.test('should export DualAgentOrchestrator class', async () => {
|
||||
expect(smartagent.DualAgentOrchestrator).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export DriverAgent class', async () => {
|
||||
expect(smartagent.DriverAgent).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export GuardianAgent class', async () => {
|
||||
expect(smartagent.GuardianAgent).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export BaseToolWrapper class', async () => {
|
||||
expect(smartagent.BaseToolWrapper).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
// Test standard tools exports
|
||||
tap.test('should export FilesystemTool class', async () => {
|
||||
expect(smartagent.FilesystemTool).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export HttpTool class', async () => {
|
||||
expect(smartagent.HttpTool).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export ShellTool class', async () => {
|
||||
expect(smartagent.ShellTool).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
tap.test('should export BrowserTool class', async () => {
|
||||
expect(smartagent.BrowserTool).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
// Test tool instantiation
|
||||
tap.test('should be able to instantiate FilesystemTool', async () => {
|
||||
const fsTool = new smartagent.FilesystemTool();
|
||||
expect(fsTool.name).toEqual('filesystem');
|
||||
expect(fsTool.actions).toBeTypeOf('object');
|
||||
expect(fsTool.actions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should be able to instantiate HttpTool', async () => {
|
||||
const httpTool = new smartagent.HttpTool();
|
||||
expect(httpTool.name).toEqual('http');
|
||||
expect(httpTool.actions).toBeTypeOf('object');
|
||||
});
|
||||
|
||||
tap.test('should be able to instantiate ShellTool', async () => {
|
||||
const shellTool = new smartagent.ShellTool();
|
||||
expect(shellTool.name).toEqual('shell');
|
||||
expect(shellTool.actions).toBeTypeOf('object');
|
||||
});
|
||||
|
||||
tap.test('should be able to instantiate BrowserTool', async () => {
|
||||
const browserTool = new smartagent.BrowserTool();
|
||||
expect(browserTool.name).toEqual('browser');
|
||||
expect(browserTool.actions).toBeTypeOf('object');
|
||||
});
|
||||
|
||||
// Test tool descriptions
|
||||
tap.test('FilesystemTool should have required actions', async () => {
|
||||
const fsTool = new smartagent.FilesystemTool();
|
||||
const actionNames = fsTool.actions.map((a) => a.name);
|
||||
expect(actionNames).toContain('read');
|
||||
expect(actionNames).toContain('write');
|
||||
expect(actionNames).toContain('list');
|
||||
expect(actionNames).toContain('delete');
|
||||
expect(actionNames).toContain('exists');
|
||||
});
|
||||
|
||||
tap.test('HttpTool should have required actions', async () => {
|
||||
const httpTool = new smartagent.HttpTool();
|
||||
const actionNames = httpTool.actions.map((a) => a.name);
|
||||
expect(actionNames).toContain('get');
|
||||
expect(actionNames).toContain('post');
|
||||
expect(actionNames).toContain('put');
|
||||
expect(actionNames).toContain('delete');
|
||||
});
|
||||
|
||||
tap.test('ShellTool should have required actions', async () => {
|
||||
const shellTool = new smartagent.ShellTool();
|
||||
const actionNames = shellTool.actions.map((a) => a.name);
|
||||
expect(actionNames).toContain('execute');
|
||||
expect(actionNames).toContain('which');
|
||||
});
|
||||
|
||||
tap.test('BrowserTool should have required actions', async () => {
|
||||
const browserTool = new smartagent.BrowserTool();
|
||||
const actionNames = browserTool.actions.map((a) => a.name);
|
||||
expect(actionNames).toContain('screenshot');
|
||||
expect(actionNames).toContain('pdf');
|
||||
expect(actionNames).toContain('evaluate');
|
||||
expect(actionNames).toContain('getPageContent');
|
||||
});
|
||||
|
||||
// Test getCallSummary
|
||||
tap.test('FilesystemTool should generate call summaries', async () => {
|
||||
const fsTool = new smartagent.FilesystemTool();
|
||||
const summary = fsTool.getCallSummary('read', { path: '/tmp/test.txt' });
|
||||
expect(summary).toBeTypeOf('string');
|
||||
expect(summary).toInclude('/tmp/test.txt');
|
||||
});
|
||||
|
||||
tap.test('HttpTool should generate call summaries', async () => {
|
||||
const httpTool = new smartagent.HttpTool();
|
||||
const summary = httpTool.getCallSummary('get', { url: 'https://example.com' });
|
||||
expect(summary).toBeTypeOf('string');
|
||||
expect(summary).toInclude('example.com');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
29
ts/index.ts
Normal file
29
ts/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// Export the dual-agent orchestrator (main entry point)
|
||||
export { DualAgentOrchestrator } from './smartagent.classes.dualagent.js';
|
||||
|
||||
// Export individual agents
|
||||
export { DriverAgent } from './smartagent.classes.driveragent.js';
|
||||
export { GuardianAgent } from './smartagent.classes.guardianagent.js';
|
||||
|
||||
// Export base tool class for custom tool creation
|
||||
export { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
// Export standard tools
|
||||
export { FilesystemTool } from './smartagent.tools.filesystem.js';
|
||||
export { HttpTool } from './smartagent.tools.http.js';
|
||||
export { ShellTool } from './smartagent.tools.shell.js';
|
||||
export { BrowserTool } from './smartagent.tools.browser.js';
|
||||
|
||||
// Export all interfaces
|
||||
export * from './smartagent.interfaces.js';
|
||||
|
||||
// Re-export useful types from smartai
|
||||
export {
|
||||
type ISmartAiOptions,
|
||||
type TProvider,
|
||||
type ChatMessage,
|
||||
type ChatOptions,
|
||||
type ChatResponse,
|
||||
} from '@push.rocks/smartai';
|
||||
14
ts/plugins.ts
Normal file
14
ts/plugins.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// @push.rocks scope
|
||||
import * as smartai from '@push.rocks/smartai';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartbrowser from '@push.rocks/smartbrowser';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
|
||||
export {
|
||||
smartai,
|
||||
smartfs,
|
||||
smartrequest,
|
||||
smartbrowser,
|
||||
smartshell,
|
||||
};
|
||||
321
ts/smartagent.classes.driveragent.ts
Normal file
321
ts/smartagent.classes.driveragent.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import type { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* DriverAgent - Executes tasks by reasoning and proposing tool calls
|
||||
* Works in conjunction with GuardianAgent for approval
|
||||
*/
|
||||
export class DriverAgent {
|
||||
private provider: plugins.smartai.MultiModalModel;
|
||||
private systemMessage: string;
|
||||
private messageHistory: plugins.smartai.ChatMessage[] = [];
|
||||
private tools: Map<string, BaseToolWrapper> = new Map();
|
||||
|
||||
constructor(
|
||||
provider: plugins.smartai.MultiModalModel,
|
||||
systemMessage?: string
|
||||
) {
|
||||
this.provider = provider;
|
||||
this.systemMessage = systemMessage || this.getDefaultSystemMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tool for use by the driver
|
||||
*/
|
||||
public registerTool(tool: BaseToolWrapper): void {
|
||||
this.tools.set(tool.name, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools
|
||||
*/
|
||||
public getTools(): Map<string, BaseToolWrapper> {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new conversation for a task
|
||||
*/
|
||||
public async startTask(task: string): Promise<interfaces.IAgentMessage> {
|
||||
// Reset message history
|
||||
this.messageHistory = [];
|
||||
|
||||
// Build the user message
|
||||
const userMessage = `TASK: ${task}\n\nAnalyze this task and determine what actions are needed. If you need to use a tool, provide a tool call proposal.`;
|
||||
|
||||
// Add to history
|
||||
this.messageHistory.push({
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
});
|
||||
|
||||
// Build tool descriptions for the system message
|
||||
const toolDescriptions = this.buildToolDescriptions();
|
||||
const fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
|
||||
|
||||
// Get response from provider
|
||||
const response = await this.provider.chat({
|
||||
systemMessage: fullSystemMessage,
|
||||
userMessage: userMessage,
|
||||
messageHistory: [],
|
||||
});
|
||||
|
||||
// Add assistant response to history
|
||||
this.messageHistory.push({
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
});
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue the conversation with feedback or results
|
||||
*/
|
||||
public async continueWithMessage(message: string): Promise<interfaces.IAgentMessage> {
|
||||
// Add the new message to history
|
||||
this.messageHistory.push({
|
||||
role: 'user',
|
||||
content: message,
|
||||
});
|
||||
|
||||
// Build tool descriptions for the system message
|
||||
const toolDescriptions = this.buildToolDescriptions();
|
||||
const fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
|
||||
|
||||
// Get response from provider (pass all but last user message as history)
|
||||
const historyForChat = this.messageHistory.slice(0, -1);
|
||||
|
||||
const response = await this.provider.chat({
|
||||
systemMessage: fullSystemMessage,
|
||||
userMessage: message,
|
||||
messageHistory: historyForChat,
|
||||
});
|
||||
|
||||
// Add assistant response to history
|
||||
this.messageHistory.push({
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
});
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tool call proposals from assistant response
|
||||
*/
|
||||
public parseToolCallProposals(response: string): interfaces.IToolCallProposal[] {
|
||||
const proposals: interfaces.IToolCallProposal[] = [];
|
||||
|
||||
// Match <tool_call>...</tool_call> blocks
|
||||
const toolCallRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
|
||||
let match;
|
||||
|
||||
while ((match = toolCallRegex.exec(response)) !== null) {
|
||||
const content = match[1];
|
||||
|
||||
try {
|
||||
const proposal = this.parseToolCallContent(content);
|
||||
if (proposal) {
|
||||
proposals.push(proposal);
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip malformed tool calls
|
||||
console.warn('Failed to parse tool call:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return proposals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the content inside a tool_call block
|
||||
*/
|
||||
private parseToolCallContent(content: string): interfaces.IToolCallProposal | null {
|
||||
// Extract tool name
|
||||
const toolMatch = content.match(/<tool>(.*?)<\/tool>/s);
|
||||
if (!toolMatch) return null;
|
||||
const toolName = toolMatch[1].trim();
|
||||
|
||||
// Extract action
|
||||
const actionMatch = content.match(/<action>(.*?)<\/action>/s);
|
||||
if (!actionMatch) return null;
|
||||
const action = actionMatch[1].trim();
|
||||
|
||||
// Extract params (JSON)
|
||||
const paramsMatch = content.match(/<params>([\s\S]*?)<\/params>/);
|
||||
let params: Record<string, unknown> = {};
|
||||
if (paramsMatch) {
|
||||
try {
|
||||
params = JSON.parse(paramsMatch[1].trim());
|
||||
} catch {
|
||||
// Try to extract individual parameters if JSON fails
|
||||
params = this.extractParamsFromXml(paramsMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract reasoning (optional)
|
||||
const reasoningMatch = content.match(/<reasoning>([\s\S]*?)<\/reasoning>/);
|
||||
const reasoning = reasoningMatch ? reasoningMatch[1].trim() : undefined;
|
||||
|
||||
return {
|
||||
proposalId: this.generateProposalId(),
|
||||
toolName,
|
||||
action,
|
||||
params,
|
||||
reasoning,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameters from XML-like format when JSON parsing fails
|
||||
*/
|
||||
private extractParamsFromXml(content: string): Record<string, unknown> {
|
||||
const params: Record<string, unknown> = {};
|
||||
const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
||||
let match;
|
||||
|
||||
while ((match = paramRegex.exec(content)) !== null) {
|
||||
const key = match[1];
|
||||
let value: unknown = match[2].trim();
|
||||
|
||||
// Try to parse as JSON for arrays/objects
|
||||
try {
|
||||
value = JSON.parse(value as string);
|
||||
} catch {
|
||||
// Keep as string if not valid JSON
|
||||
}
|
||||
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the response indicates task completion
|
||||
*/
|
||||
public isTaskComplete(response: string): boolean {
|
||||
// Check for explicit completion markers
|
||||
const completionMarkers = [
|
||||
'<task_complete>',
|
||||
'<task_completed>',
|
||||
'TASK COMPLETE',
|
||||
'Task completed successfully',
|
||||
];
|
||||
|
||||
const lowerResponse = response.toLowerCase();
|
||||
return completionMarkers.some(marker =>
|
||||
lowerResponse.includes(marker.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the response needs clarification or user input
|
||||
*/
|
||||
public needsClarification(response: string): boolean {
|
||||
const clarificationMarkers = [
|
||||
'<needs_clarification>',
|
||||
'<question>',
|
||||
'please clarify',
|
||||
'could you specify',
|
||||
'what do you mean by',
|
||||
];
|
||||
|
||||
const lowerResponse = response.toLowerCase();
|
||||
return clarificationMarkers.some(marker =>
|
||||
lowerResponse.includes(marker.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the final result from a completed task
|
||||
*/
|
||||
public extractTaskResult(response: string): string | null {
|
||||
// Try to extract from result tags
|
||||
const resultMatch = response.match(/<task_result>([\s\S]*?)<\/task_result>/);
|
||||
if (resultMatch) {
|
||||
return resultMatch[1].trim();
|
||||
}
|
||||
|
||||
const completeMatch = response.match(/<task_complete>([\s\S]*?)<\/task_complete>/);
|
||||
if (completeMatch) {
|
||||
return completeMatch[1].trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tool descriptions for the system message
|
||||
*/
|
||||
private buildToolDescriptions(): string {
|
||||
const descriptions: string[] = [];
|
||||
|
||||
for (const tool of this.tools.values()) {
|
||||
descriptions.push(tool.getFullDescription());
|
||||
}
|
||||
|
||||
return descriptions.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique proposal ID
|
||||
*/
|
||||
private generateProposalId(): string {
|
||||
return `prop_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default system message for the driver
|
||||
*/
|
||||
private getDefaultSystemMessage(): 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.
|
||||
|
||||
## Tool Usage Format
|
||||
When you need to use a tool, output a tool call proposal in this format:
|
||||
|
||||
<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_call>
|
||||
|
||||
## 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:
|
||||
|
||||
<task_complete>
|
||||
Brief summary of what was accomplished
|
||||
</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
|
||||
- If you need clarification, ask using <needs_clarification>your question</needs_clarification>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the conversation state
|
||||
*/
|
||||
public reset(): void {
|
||||
this.messageHistory = [];
|
||||
}
|
||||
}
|
||||
350
ts/smartagent.classes.dualagent.ts
Normal file
350
ts/smartagent.classes.dualagent.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
import { DriverAgent } from './smartagent.classes.driveragent.js';
|
||||
import { GuardianAgent } from './smartagent.classes.guardianagent.js';
|
||||
import { FilesystemTool } from './smartagent.tools.filesystem.js';
|
||||
import { HttpTool } from './smartagent.tools.http.js';
|
||||
import { ShellTool } from './smartagent.tools.shell.js';
|
||||
import { BrowserTool } from './smartagent.tools.browser.js';
|
||||
|
||||
/**
|
||||
* DualAgentOrchestrator - Coordinates Driver and Guardian agents
|
||||
* Manages the complete lifecycle of task execution with tool approval
|
||||
*/
|
||||
export class DualAgentOrchestrator {
|
||||
private options: interfaces.IDualAgentOptions;
|
||||
private smartai: plugins.smartai.SmartAi;
|
||||
private driverProvider: plugins.smartai.MultiModalModel;
|
||||
private guardianProvider: plugins.smartai.MultiModalModel;
|
||||
private driver: DriverAgent;
|
||||
private guardian: GuardianAgent;
|
||||
private tools: Map<string, BaseToolWrapper> = new Map();
|
||||
private isRunning = false;
|
||||
private conversationHistory: interfaces.IAgentMessage[] = [];
|
||||
|
||||
constructor(options: interfaces.IDualAgentOptions) {
|
||||
this.options = {
|
||||
maxIterations: 20,
|
||||
maxConsecutiveRejections: 3,
|
||||
defaultProvider: 'openai',
|
||||
...options,
|
||||
};
|
||||
|
||||
// Create SmartAi instance
|
||||
this.smartai = new plugins.smartai.SmartAi(options);
|
||||
|
||||
// Get providers
|
||||
this.driverProvider = this.getProviderByName(this.options.defaultProvider!);
|
||||
this.guardianProvider = this.options.guardianProvider
|
||||
? this.getProviderByName(this.options.guardianProvider)
|
||||
: this.driverProvider;
|
||||
|
||||
// Create agents
|
||||
this.driver = new DriverAgent(this.driverProvider, options.driverSystemMessage);
|
||||
this.guardian = new GuardianAgent(this.guardianProvider, options.guardianPolicyPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider by name
|
||||
*/
|
||||
private getProviderByName(providerName: plugins.smartai.TProvider): plugins.smartai.MultiModalModel {
|
||||
switch (providerName) {
|
||||
case 'openai':
|
||||
return this.smartai.openaiProvider;
|
||||
case 'anthropic':
|
||||
return this.smartai.anthropicProvider;
|
||||
case 'perplexity':
|
||||
return this.smartai.perplexityProvider;
|
||||
case 'ollama':
|
||||
return this.smartai.ollamaProvider;
|
||||
case 'groq':
|
||||
return this.smartai.groqProvider;
|
||||
case 'xai':
|
||||
return this.smartai.xaiProvider;
|
||||
case 'exo':
|
||||
return this.smartai.exoProvider;
|
||||
default:
|
||||
return this.smartai.openaiProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom tool
|
||||
*/
|
||||
public registerTool(tool: BaseToolWrapper): void {
|
||||
this.tools.set(tool.name, tool);
|
||||
this.driver.registerTool(tool);
|
||||
this.guardian.registerTool(tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all standard tools
|
||||
*/
|
||||
public registerStandardTools(): void {
|
||||
const standardTools = [
|
||||
new FilesystemTool(),
|
||||
new HttpTool(),
|
||||
new ShellTool(),
|
||||
new BrowserTool(),
|
||||
];
|
||||
|
||||
for (const tool of standardTools) {
|
||||
this.registerTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all tools (eager loading)
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Start smartai
|
||||
await this.smartai.start();
|
||||
|
||||
// Initialize all tools
|
||||
const initPromises: Promise<void>[] = [];
|
||||
for (const tool of this.tools.values()) {
|
||||
initPromises.push(tool.initialize());
|
||||
}
|
||||
|
||||
await Promise.all(initPromises);
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.smartai.stop();
|
||||
this.isRunning = false;
|
||||
this.driver.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task through the dual-agent system
|
||||
*/
|
||||
public async run(task: string): Promise<interfaces.IDualAgentRunResult> {
|
||||
if (!this.isRunning) {
|
||||
throw new Error('Orchestrator not started. Call start() first.');
|
||||
}
|
||||
|
||||
this.conversationHistory = [];
|
||||
let iterations = 0;
|
||||
let consecutiveRejections = 0;
|
||||
let completed = false;
|
||||
let finalResult: string | null = null;
|
||||
|
||||
// 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);
|
||||
this.conversationHistory.push(driverResponse);
|
||||
|
||||
while (
|
||||
iterations < this.options.maxIterations! &&
|
||||
consecutiveRejections < this.options.maxConsecutiveRejections! &&
|
||||
!completed
|
||||
) {
|
||||
iterations++;
|
||||
|
||||
// Check if task is complete
|
||||
if (this.driver.isTaskComplete(driverResponse.content)) {
|
||||
completed = true;
|
||||
finalResult = this.driver.extractTaskResult(driverResponse.content) || driverResponse.content;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if driver needs clarification
|
||||
if (this.driver.needsClarification(driverResponse.content)) {
|
||||
// Return with clarification needed status
|
||||
return {
|
||||
success: false,
|
||||
completed: false,
|
||||
result: driverResponse.content,
|
||||
iterations,
|
||||
history: this.conversationHistory,
|
||||
status: 'clarification_needed',
|
||||
};
|
||||
}
|
||||
|
||||
// Parse tool call proposals
|
||||
const proposals = this.driver.parseToolCallProposals(driverResponse.content);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Process the first proposal (one at a time)
|
||||
const proposal = proposals[0];
|
||||
|
||||
// Quick validation first
|
||||
const quickDecision = this.guardian.quickValidate(proposal);
|
||||
let decision: interfaces.IGuardianDecision;
|
||||
|
||||
if (quickDecision) {
|
||||
decision = quickDecision;
|
||||
} else {
|
||||
// Full AI evaluation
|
||||
decision = await this.guardian.evaluate(proposal, task);
|
||||
}
|
||||
|
||||
if (decision.decision === 'approve') {
|
||||
consecutiveRejections = 0;
|
||||
|
||||
// Execute the tool
|
||||
const tool = this.tools.get(proposal.toolName);
|
||||
if (!tool) {
|
||||
const errorMessage = `Tool "${proposal.toolName}" not found.`;
|
||||
driverResponse = await this.driver.continueWithMessage(
|
||||
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
|
||||
);
|
||||
this.conversationHistory.push(driverResponse);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await tool.execute(proposal.action, proposal.params);
|
||||
|
||||
// Send result to driver
|
||||
const resultMessage = result.success
|
||||
? `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${JSON.stringify(result.result, null, 2)}`
|
||||
: `TOOL ERROR (${proposal.toolName}.${proposal.action}):\n${result.error}`;
|
||||
|
||||
this.conversationHistory.push({
|
||||
role: 'system',
|
||||
content: resultMessage,
|
||||
toolCall: proposal,
|
||||
toolResult: result,
|
||||
});
|
||||
|
||||
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.`
|
||||
);
|
||||
this.conversationHistory.push(driverResponse);
|
||||
}
|
||||
} else {
|
||||
// Rejected
|
||||
consecutiveRejections++;
|
||||
|
||||
// Build rejection feedback
|
||||
let feedback = `TOOL CALL REJECTED by Guardian:\n`;
|
||||
feedback += `- Reason: ${decision.reason}\n`;
|
||||
|
||||
if (decision.concerns && decision.concerns.length > 0) {
|
||||
feedback += `- Concerns:\n${decision.concerns.map(c => ` - ${c}`).join('\n')}\n`;
|
||||
}
|
||||
|
||||
if (decision.suggestions) {
|
||||
feedback += `- Suggestions: ${decision.suggestions}\n`;
|
||||
}
|
||||
|
||||
feedback += `\nPlease adapt your approach based on this feedback.`;
|
||||
|
||||
this.conversationHistory.push({
|
||||
role: 'system',
|
||||
content: feedback,
|
||||
toolCall: proposal,
|
||||
guardianDecision: decision,
|
||||
});
|
||||
|
||||
driverResponse = await this.driver.continueWithMessage(feedback);
|
||||
this.conversationHistory.push(driverResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine final status
|
||||
let status: interfaces.TDualAgentRunStatus = 'completed';
|
||||
if (!completed) {
|
||||
if (iterations >= this.options.maxIterations!) {
|
||||
status = 'max_iterations_reached';
|
||||
} else if (consecutiveRejections >= this.options.maxConsecutiveRejections!) {
|
||||
status = 'max_rejections_reached';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: completed,
|
||||
completed,
|
||||
result: finalResult || driverResponse.content,
|
||||
iterations,
|
||||
history: this.conversationHistory,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue an existing task with user input
|
||||
*/
|
||||
public async continueTask(userInput: string): Promise<interfaces.IDualAgentRunResult> {
|
||||
if (!this.isRunning) {
|
||||
throw new Error('Orchestrator not started. Call start() first.');
|
||||
}
|
||||
|
||||
this.conversationHistory.push({
|
||||
role: 'user',
|
||||
content: userInput,
|
||||
});
|
||||
|
||||
const driverResponse = await this.driver.continueWithMessage(userInput);
|
||||
this.conversationHistory.push(driverResponse);
|
||||
|
||||
// Continue the run loop
|
||||
// For simplicity, we return the current state - full continuation would need refactoring
|
||||
return {
|
||||
success: false,
|
||||
completed: false,
|
||||
result: driverResponse.content,
|
||||
iterations: 1,
|
||||
history: this.conversationHistory,
|
||||
status: 'in_progress',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the conversation history
|
||||
*/
|
||||
public getHistory(): interfaces.IAgentMessage[] {
|
||||
return [...this.conversationHistory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the guardian policy
|
||||
*/
|
||||
public setGuardianPolicy(policyPrompt: string): void {
|
||||
this.guardian.setPolicy(policyPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if orchestrator is running
|
||||
*/
|
||||
public isActive(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered tool names
|
||||
*/
|
||||
public getToolNames(): string[] {
|
||||
return Array.from(this.tools.keys());
|
||||
}
|
||||
}
|
||||
241
ts/smartagent.classes.guardianagent.ts
Normal file
241
ts/smartagent.classes.guardianagent.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import type { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* GuardianAgent - Evaluates tool call proposals against a policy
|
||||
* Uses AI reasoning to approve or reject tool calls
|
||||
*/
|
||||
export class GuardianAgent {
|
||||
private provider: plugins.smartai.MultiModalModel;
|
||||
private policyPrompt: string;
|
||||
private tools: Map<string, BaseToolWrapper> = new Map();
|
||||
|
||||
constructor(
|
||||
provider: plugins.smartai.MultiModalModel,
|
||||
policyPrompt: string
|
||||
) {
|
||||
this.provider = provider;
|
||||
this.policyPrompt = policyPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tool for reference during evaluation
|
||||
*/
|
||||
public registerTool(tool: BaseToolWrapper): void {
|
||||
this.tools.set(tool.name, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a tool call proposal against the policy
|
||||
*/
|
||||
public async evaluate(
|
||||
proposal: interfaces.IToolCallProposal,
|
||||
taskContext: string
|
||||
): Promise<interfaces.IGuardianDecision> {
|
||||
// Get the tool to generate a human-readable summary
|
||||
const tool = this.tools.get(proposal.toolName);
|
||||
let callSummary = `${proposal.toolName}.${proposal.action}(${JSON.stringify(proposal.params)})`;
|
||||
|
||||
if (tool) {
|
||||
try {
|
||||
callSummary = tool.getCallSummary(proposal.action, proposal.params);
|
||||
} catch {
|
||||
// Fallback to basic summary
|
||||
}
|
||||
}
|
||||
|
||||
// Build the evaluation prompt
|
||||
const evaluationPrompt = this.buildEvaluationPrompt(
|
||||
proposal,
|
||||
callSummary,
|
||||
taskContext
|
||||
);
|
||||
|
||||
// Get response from provider
|
||||
const response = await this.provider.chat({
|
||||
systemMessage: this.buildGuardianSystemMessage(),
|
||||
userMessage: evaluationPrompt,
|
||||
messageHistory: [],
|
||||
});
|
||||
|
||||
// Parse the decision from the response
|
||||
return this.parseDecision(response.message, proposal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system message for the Guardian
|
||||
*/
|
||||
private buildGuardianSystemMessage(): string {
|
||||
return `You are a Guardian AI responsible for evaluating tool call proposals.
|
||||
|
||||
## Your Role
|
||||
You evaluate whether proposed tool calls are safe and aligned with the policy.
|
||||
|
||||
## Policy to Enforce
|
||||
${this.policyPrompt}
|
||||
|
||||
## Response Format
|
||||
For EVERY evaluation, respond with a decision in this exact format:
|
||||
|
||||
<guardian_decision>
|
||||
<decision>approve OR reject</decision>
|
||||
<reason>Your detailed explanation</reason>
|
||||
<concerns>List any concerns, even if approving</concerns>
|
||||
<suggestions>Alternative approaches if rejecting</suggestions>
|
||||
</guardian_decision>
|
||||
|
||||
## Guidelines
|
||||
1. Carefully analyze what the tool call will do
|
||||
2. Consider security implications
|
||||
3. Check against the policy requirements
|
||||
4. If uncertain, err on the side of caution (reject)
|
||||
5. Provide actionable feedback when rejecting`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the evaluation prompt for a specific proposal
|
||||
*/
|
||||
private buildEvaluationPrompt(
|
||||
proposal: interfaces.IToolCallProposal,
|
||||
callSummary: string,
|
||||
taskContext: string
|
||||
): string {
|
||||
const toolInfo = this.tools.get(proposal.toolName);
|
||||
const toolDescription = toolInfo ? toolInfo.getFullDescription() : 'Unknown tool';
|
||||
|
||||
return `## Task Context
|
||||
${taskContext}
|
||||
|
||||
## Tool Being Used
|
||||
${toolDescription}
|
||||
|
||||
## Proposed Tool Call
|
||||
- **Tool**: ${proposal.toolName}
|
||||
- **Action**: ${proposal.action}
|
||||
- **Parameters**: ${JSON.stringify(proposal.params, null, 2)}
|
||||
|
||||
## Human-Readable Summary
|
||||
${callSummary}
|
||||
|
||||
## Driver's Reasoning
|
||||
${proposal.reasoning || 'No reasoning provided'}
|
||||
|
||||
---
|
||||
|
||||
Evaluate this tool call against the policy. Should it be approved or rejected?`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the guardian decision from the response
|
||||
*/
|
||||
private parseDecision(
|
||||
response: string,
|
||||
proposal: interfaces.IToolCallProposal
|
||||
): interfaces.IGuardianDecision {
|
||||
// Try to extract from XML tags
|
||||
const decisionMatch = response.match(/<decision>(.*?)<\/decision>/s);
|
||||
const reasonMatch = response.match(/<reason>([\s\S]*?)<\/reason>/);
|
||||
const concernsMatch = response.match(/<concerns>([\s\S]*?)<\/concerns>/);
|
||||
const suggestionsMatch = response.match(/<suggestions>([\s\S]*?)<\/suggestions>/);
|
||||
|
||||
// Determine decision
|
||||
let decision: 'approve' | 'reject' = 'reject';
|
||||
if (decisionMatch) {
|
||||
const decisionText = decisionMatch[1].trim().toLowerCase();
|
||||
decision = decisionText.includes('approve') ? 'approve' : 'reject';
|
||||
} else {
|
||||
// Fallback: look for approval keywords in the response
|
||||
const lowerResponse = response.toLowerCase();
|
||||
if (
|
||||
lowerResponse.includes('approved') ||
|
||||
lowerResponse.includes('i approve') ||
|
||||
lowerResponse.includes('looks safe')
|
||||
) {
|
||||
decision = 'approve';
|
||||
}
|
||||
}
|
||||
|
||||
// Extract reason
|
||||
let reason = reasonMatch ? reasonMatch[1].trim() : '';
|
||||
if (!reason) {
|
||||
// Use the full response as reason if no tag found
|
||||
reason = response.substring(0, 500);
|
||||
}
|
||||
|
||||
// Extract concerns
|
||||
const concerns: string[] = [];
|
||||
if (concernsMatch) {
|
||||
const concernsText = concernsMatch[1].trim();
|
||||
if (concernsText && concernsText.toLowerCase() !== 'none') {
|
||||
// Split by newlines or bullet points
|
||||
const concernLines = concernsText.split(/[\n\r]+/).map(l => l.trim()).filter(l => l);
|
||||
concerns.push(...concernLines);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract suggestions
|
||||
const suggestions = suggestionsMatch ? suggestionsMatch[1].trim() : undefined;
|
||||
|
||||
return {
|
||||
decision,
|
||||
reason,
|
||||
concerns: concerns.length > 0 ? concerns : undefined,
|
||||
suggestions: suggestions && suggestions.toLowerCase() !== 'none' ? suggestions : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick validation without AI (for obviously safe/unsafe operations)
|
||||
* Returns null if AI evaluation is needed
|
||||
*/
|
||||
public quickValidate(proposal: interfaces.IToolCallProposal): interfaces.IGuardianDecision | null {
|
||||
// Check if tool exists
|
||||
if (!this.tools.has(proposal.toolName)) {
|
||||
return {
|
||||
decision: 'reject',
|
||||
reason: `Unknown tool: ${proposal.toolName}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if action exists
|
||||
const tool = this.tools.get(proposal.toolName)!;
|
||||
const validAction = tool.actions.find(a => a.name === proposal.action);
|
||||
if (!validAction) {
|
||||
return {
|
||||
decision: 'reject',
|
||||
reason: `Unknown action "${proposal.action}" for tool "${proposal.toolName}". Available actions: ${tool.actions.map(a => a.name).join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
const schema = validAction.parameters;
|
||||
if (schema && schema.required && Array.isArray(schema.required)) {
|
||||
for (const requiredParam of schema.required as string[]) {
|
||||
if (!(requiredParam in proposal.params)) {
|
||||
return {
|
||||
decision: 'reject',
|
||||
reason: `Missing required parameter: ${requiredParam}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needs full AI evaluation
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the policy prompt
|
||||
*/
|
||||
public setPolicy(policyPrompt: string): void {
|
||||
this.policyPrompt = policyPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current policy
|
||||
*/
|
||||
public getPolicy(): string {
|
||||
return this.policyPrompt;
|
||||
}
|
||||
}
|
||||
210
ts/smartagent.interfaces.ts
Normal file
210
ts/smartagent.interfaces.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// ================================
|
||||
// Agent Configuration Interfaces
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Configuration options for the DualAgentOrchestrator
|
||||
*/
|
||||
export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
|
||||
/** Name of the agent system */
|
||||
name?: string;
|
||||
/** Default AI provider for both Driver and Guardian */
|
||||
defaultProvider?: plugins.smartai.TProvider;
|
||||
/** Optional separate provider for Guardian (for cost optimization) */
|
||||
guardianProvider?: plugins.smartai.TProvider;
|
||||
/** System message for the Driver agent */
|
||||
driverSystemMessage?: string;
|
||||
/** Policy prompt for the Guardian agent - REQUIRED */
|
||||
guardianPolicyPrompt: string;
|
||||
/** Maximum iterations for task completion (default: 20) */
|
||||
maxIterations?: number;
|
||||
/** Maximum consecutive rejections before aborting (default: 3) */
|
||||
maxConsecutiveRejections?: number;
|
||||
/** Enable verbose logging */
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Message Interfaces
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Represents a message in the agent's conversation history
|
||||
*/
|
||||
export interface IAgentMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool' | 'guardian';
|
||||
content: string;
|
||||
toolName?: string;
|
||||
toolResult?: unknown;
|
||||
toolCall?: IToolCallProposal;
|
||||
guardianDecision?: IGuardianDecision;
|
||||
timestamp?: Date;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Tool Interfaces
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Represents an action that a tool can perform
|
||||
*/
|
||||
export interface IToolAction {
|
||||
/** Action name (e.g., 'read', 'write', 'delete') */
|
||||
name: string;
|
||||
/** Description of what this action does */
|
||||
description: string;
|
||||
/** JSON schema for action parameters */
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proposed tool call from the Driver
|
||||
*/
|
||||
export interface IToolCallProposal {
|
||||
/** Unique ID for this proposal */
|
||||
proposalId: string;
|
||||
/** Name of the tool */
|
||||
toolName: string;
|
||||
/** Specific action to perform */
|
||||
action: string;
|
||||
/** Parameters for the action */
|
||||
params: Record<string, unknown>;
|
||||
/** Driver's reasoning for this call */
|
||||
reasoning?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of tool execution
|
||||
*/
|
||||
export interface IToolExecutionResult {
|
||||
success: boolean;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for wrapped tools
|
||||
*/
|
||||
export interface IAgentToolWrapper {
|
||||
/** Tool name */
|
||||
name: string;
|
||||
/** Tool description */
|
||||
description: string;
|
||||
/** Available actions */
|
||||
actions: IToolAction[];
|
||||
/** Initialize the tool */
|
||||
initialize(): Promise<void>;
|
||||
/** Cleanup resources */
|
||||
cleanup(): Promise<void>;
|
||||
/** Execute an action */
|
||||
execute(action: string, params: Record<string, unknown>): Promise<IToolExecutionResult>;
|
||||
/** Get a summary for Guardian review */
|
||||
getCallSummary(action: string, params: Record<string, unknown>): string;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Guardian Interfaces
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Request for Guardian evaluation
|
||||
*/
|
||||
export interface IGuardianEvaluationRequest {
|
||||
/** The proposed tool call */
|
||||
proposal: IToolCallProposal;
|
||||
/** Current task context */
|
||||
taskContext: string;
|
||||
/** Recent conversation history (last N messages) */
|
||||
recentHistory: IAgentMessage[];
|
||||
/** Summary of what the tool call will do */
|
||||
callSummary: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardian's decision
|
||||
*/
|
||||
export interface IGuardianDecision {
|
||||
/** Approve or reject */
|
||||
decision: 'approve' | 'reject';
|
||||
/** Explanation of the decision */
|
||||
reason: string;
|
||||
/** Specific concerns if rejected */
|
||||
concerns?: string[];
|
||||
/** Suggestions for the Driver if rejected */
|
||||
suggestions?: string;
|
||||
/** Confidence level (0-1) */
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Result Interfaces
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Log entry for tool executions
|
||||
*/
|
||||
export interface IToolExecutionLog {
|
||||
timestamp: Date;
|
||||
toolName: string;
|
||||
action: string;
|
||||
params: Record<string, unknown>;
|
||||
guardianDecision: 'approved' | 'rejected';
|
||||
guardianReason: string;
|
||||
executionResult?: unknown;
|
||||
executionError?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a dual-agent run
|
||||
*/
|
||||
export type TDualAgentRunStatus =
|
||||
| 'completed'
|
||||
| 'in_progress'
|
||||
| 'max_iterations_reached'
|
||||
| 'max_rejections_reached'
|
||||
| 'clarification_needed'
|
||||
| 'error';
|
||||
|
||||
/**
|
||||
* Result of a dual-agent run
|
||||
*/
|
||||
export interface IDualAgentRunResult {
|
||||
/** Whether the task was successful */
|
||||
success: boolean;
|
||||
/** Whether the task is completed */
|
||||
completed: boolean;
|
||||
/** Final result or response */
|
||||
result: string;
|
||||
/** Total iterations taken */
|
||||
iterations: number;
|
||||
/** Full conversation history */
|
||||
history: IAgentMessage[];
|
||||
/** Current status */
|
||||
status: TDualAgentRunStatus;
|
||||
/** Number of tool calls made */
|
||||
toolCallCount?: number;
|
||||
/** Number of Guardian rejections */
|
||||
rejectionCount?: number;
|
||||
/** Tool execution log */
|
||||
toolLog?: IToolExecutionLog[];
|
||||
/** Error message if status is 'error' */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Utility Types
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Available tool names
|
||||
*/
|
||||
export type TToolName = 'filesystem' | 'http' | 'browser' | 'shell';
|
||||
|
||||
/**
|
||||
* Generate a unique proposal ID
|
||||
*/
|
||||
export function generateProposalId(): string {
|
||||
return `proposal_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
80
ts/smartagent.tools.base.ts
Normal file
80
ts/smartagent.tools.base.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
|
||||
/**
|
||||
* Abstract base class for tool wrappers
|
||||
* All tool implementations should extend this class
|
||||
*/
|
||||
export abstract class BaseToolWrapper implements interfaces.IAgentToolWrapper {
|
||||
abstract name: string;
|
||||
abstract description: string;
|
||||
abstract actions: interfaces.IToolAction[];
|
||||
|
||||
protected isInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the tool and any required resources
|
||||
*/
|
||||
abstract initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Cleanup any resources used by the tool
|
||||
*/
|
||||
abstract cleanup(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Execute an action with the given parameters
|
||||
*/
|
||||
abstract execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult>;
|
||||
|
||||
/**
|
||||
* Generate a human-readable summary of what the action will do
|
||||
* This is used by the Guardian to understand the proposed action
|
||||
*/
|
||||
abstract getCallSummary(action: string, params: Record<string, unknown>): string;
|
||||
|
||||
/**
|
||||
* Validate that an action exists for this tool
|
||||
* @throws Error if the action is not valid
|
||||
*/
|
||||
protected validateAction(action: string): void {
|
||||
const validAction = this.actions.find((a) => a.name === action);
|
||||
if (!validAction) {
|
||||
const availableActions = this.actions.map((a) => a.name).join(', ');
|
||||
throw new Error(
|
||||
`Unknown action "${action}" for tool "${this.name}". Available actions: ${availableActions}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tool is initialized
|
||||
*/
|
||||
protected ensureInitialized(): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(`Tool "${this.name}" is not initialized. Call initialize() first.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full tool description including all actions
|
||||
* Used for Driver's tool awareness
|
||||
*/
|
||||
public getFullDescription(): string {
|
||||
const actionDescriptions = this.actions
|
||||
.map((a) => ` - ${a.name}: ${a.description}`)
|
||||
.join('\n');
|
||||
|
||||
return `${this.name}: ${this.description}\nActions:\n${actionDescriptions}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON schema for a specific action
|
||||
*/
|
||||
public getActionSchema(action: string): Record<string, unknown> | undefined {
|
||||
const actionDef = this.actions.find((a) => a.name === action);
|
||||
return actionDef?.parameters;
|
||||
}
|
||||
}
|
||||
200
ts/smartagent.tools.browser.ts
Normal file
200
ts/smartagent.tools.browser.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* Browser tool for web page interaction
|
||||
* Wraps @push.rocks/smartbrowser (Puppeteer-based)
|
||||
*/
|
||||
export class BrowserTool extends BaseToolWrapper {
|
||||
public name = 'browser';
|
||||
public description =
|
||||
'Interact with web pages - take screenshots, generate PDFs, and execute JavaScript on pages';
|
||||
|
||||
public actions: interfaces.IToolAction[] = [
|
||||
{
|
||||
name: 'screenshot',
|
||||
description: 'Take a screenshot of a webpage',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL of the page to screenshot' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pdf',
|
||||
description: 'Generate a PDF from a webpage',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL of the page to convert to PDF' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate',
|
||||
description:
|
||||
'Execute JavaScript code on a webpage and return the result. The script runs in the browser context.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL of the page to run the script on' },
|
||||
script: {
|
||||
type: 'string',
|
||||
description:
|
||||
'JavaScript code to execute. Must be a valid expression or statements that return a value.',
|
||||
},
|
||||
},
|
||||
required: ['url', 'script'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'getPageContent',
|
||||
description: 'Get the text content and title of a webpage',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL of the page to get content from' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private smartbrowser!: plugins.smartbrowser.SmartBrowser;
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.smartbrowser = new plugins.smartbrowser.SmartBrowser();
|
||||
await this.smartbrowser.start();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
if (this.smartbrowser) {
|
||||
await this.smartbrowser.stop();
|
||||
}
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
this.validateAction(action);
|
||||
this.ensureInitialized();
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'screenshot': {
|
||||
const result = await this.smartbrowser.screenshotFromPage(params.url as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
url: params.url,
|
||||
name: result.name,
|
||||
id: result.id,
|
||||
bufferBase64: Buffer.from(result.buffer).toString('base64'),
|
||||
bufferLength: result.buffer.length,
|
||||
type: 'screenshot',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'pdf': {
|
||||
const result = await this.smartbrowser.pdfFromPage(params.url as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
url: params.url,
|
||||
name: result.name,
|
||||
id: result.id,
|
||||
bufferBase64: Buffer.from(result.buffer).toString('base64'),
|
||||
bufferLength: result.buffer.length,
|
||||
type: 'pdf',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'evaluate': {
|
||||
const script = params.script as string;
|
||||
// Create an async function from the script
|
||||
// The script should be valid JavaScript that returns a value
|
||||
const result = await this.smartbrowser.evaluateOnPage(params.url as string, async () => {
|
||||
// This runs in the browser context
|
||||
// We need to evaluate the script string dynamically
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(script);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
url: params.url,
|
||||
script: script.substring(0, 200) + (script.length > 200 ? '...' : ''),
|
||||
evaluationResult: result,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'getPageContent': {
|
||||
const result = await this.smartbrowser.evaluateOnPage(params.url as string, async () => {
|
||||
return {
|
||||
title: document.title,
|
||||
textContent: document.body?.innerText || '',
|
||||
url: window.location.href,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
url: params.url,
|
||||
title: result.title,
|
||||
textContent:
|
||||
result.textContent.length > 10000
|
||||
? result.textContent.substring(0, 10000) + '... [truncated]'
|
||||
: result.textContent,
|
||||
actualUrl: result.url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${action}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||||
switch (action) {
|
||||
case 'screenshot':
|
||||
return `Take screenshot of "${params.url}"`;
|
||||
|
||||
case 'pdf':
|
||||
return `Generate PDF from "${params.url}"`;
|
||||
|
||||
case 'evaluate': {
|
||||
const script = params.script as string;
|
||||
const preview = script.length > 100 ? script.substring(0, 100) + '...' : script;
|
||||
return `Execute JavaScript on "${params.url}": "${preview}"`;
|
||||
}
|
||||
|
||||
case 'getPageContent':
|
||||
return `Get text content and title from "${params.url}"`;
|
||||
|
||||
default:
|
||||
return `Unknown action: ${action}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
379
ts/smartagent.tools.filesystem.ts
Normal file
379
ts/smartagent.tools.filesystem.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* Filesystem tool for file and directory operations
|
||||
* Wraps @push.rocks/smartfs
|
||||
*/
|
||||
export class FilesystemTool extends BaseToolWrapper {
|
||||
public name = 'filesystem';
|
||||
public description = 'Read, write, list, and delete files and directories';
|
||||
|
||||
public actions: interfaces.IToolAction[] = [
|
||||
{
|
||||
name: 'read',
|
||||
description: 'Read the contents of a file',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute path to the file' },
|
||||
encoding: {
|
||||
type: 'string',
|
||||
enum: ['utf8', 'binary', 'base64'],
|
||||
default: 'utf8',
|
||||
description: 'File encoding',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'write',
|
||||
description: 'Write content to a file (creates or overwrites)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute path to the file' },
|
||||
content: { type: 'string', description: 'Content to write' },
|
||||
encoding: {
|
||||
type: 'string',
|
||||
enum: ['utf8', 'binary', 'base64'],
|
||||
default: 'utf8',
|
||||
description: 'File encoding',
|
||||
},
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'append',
|
||||
description: 'Append content to a file',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute path to the file' },
|
||||
content: { type: 'string', description: 'Content to append' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List files and directories in a path',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Directory path to list' },
|
||||
recursive: { type: 'boolean', default: false, description: 'List recursively' },
|
||||
filter: { type: 'string', description: 'Glob pattern to filter results (e.g., "*.ts")' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
description: 'Delete a file or directory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Path to delete' },
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'For directories, delete recursively',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'exists',
|
||||
description: 'Check if a file or directory exists',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Path to check' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stat',
|
||||
description: 'Get file or directory statistics (size, dates, etc.)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Path to get stats for' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copy',
|
||||
description: 'Copy a file to a new location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: { type: 'string', description: 'Source file path' },
|
||||
destination: { type: 'string', description: 'Destination file path' },
|
||||
},
|
||||
required: ['source', 'destination'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'move',
|
||||
description: 'Move a file to a new location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: { type: 'string', description: 'Source file path' },
|
||||
destination: { type: 'string', description: 'Destination file path' },
|
||||
},
|
||||
required: ['source', 'destination'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mkdir',
|
||||
description: 'Create a directory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Directory path to create' },
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Create parent directories if needed',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private smartfs!: plugins.smartfs.SmartFs;
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
this.validateAction(action);
|
||||
this.ensureInitialized();
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'read': {
|
||||
const encoding = (params.encoding as string) || 'utf8';
|
||||
const content = await this.smartfs
|
||||
.file(params.path as string)
|
||||
.encoding(encoding as 'utf8' | 'binary' | 'base64')
|
||||
.read();
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
content: content.toString(),
|
||||
encoding,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'write': {
|
||||
const encoding = (params.encoding as string) || 'utf8';
|
||||
await this.smartfs
|
||||
.file(params.path as string)
|
||||
.encoding(encoding as 'utf8' | 'binary' | 'base64')
|
||||
.write(params.content as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
written: true,
|
||||
bytesWritten: (params.content as string).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'append': {
|
||||
await this.smartfs.file(params.path as string).append(params.content as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
appended: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
let dir = this.smartfs.directory(params.path as string);
|
||||
if (params.recursive) {
|
||||
dir = dir.recursive();
|
||||
}
|
||||
if (params.filter) {
|
||||
dir = dir.filter(params.filter as string);
|
||||
}
|
||||
const entries = await dir.list();
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
entries,
|
||||
count: entries.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const path = params.path as string;
|
||||
// Check if it's a directory or file
|
||||
const exists = await this.smartfs.file(path).exists();
|
||||
if (exists) {
|
||||
// Try to get stats to check if it's a directory
|
||||
try {
|
||||
const stats = await this.smartfs.file(path).stat();
|
||||
if (stats.isDirectory && params.recursive) {
|
||||
await this.smartfs.directory(path).recursive().delete();
|
||||
} else {
|
||||
await this.smartfs.file(path).delete();
|
||||
}
|
||||
} catch {
|
||||
await this.smartfs.file(path).delete();
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path,
|
||||
deleted: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'exists': {
|
||||
const exists = await this.smartfs.file(params.path as string).exists();
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
exists,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'stat': {
|
||||
const stats = await this.smartfs.file(params.path as string).stat();
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
stats,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'copy': {
|
||||
await this.smartfs.file(params.source as string).copy(params.destination as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
source: params.source,
|
||||
destination: params.destination,
|
||||
copied: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'move': {
|
||||
await this.smartfs.file(params.source as string).move(params.destination as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
source: params.source,
|
||||
destination: params.destination,
|
||||
moved: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'mkdir': {
|
||||
let dir = this.smartfs.directory(params.path as string);
|
||||
if (params.recursive !== false) {
|
||||
dir = dir.recursive();
|
||||
}
|
||||
await dir.create();
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
path: params.path,
|
||||
created: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${action}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||||
switch (action) {
|
||||
case 'read':
|
||||
return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
|
||||
|
||||
case 'write': {
|
||||
const content = params.content as string;
|
||||
const preview = content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||
return `Write ${content.length} bytes to "${params.path}". Content preview: "${preview}"`;
|
||||
}
|
||||
|
||||
case 'append': {
|
||||
const content = params.content as string;
|
||||
const preview = content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||
return `Append ${content.length} bytes to "${params.path}". Content preview: "${preview}"`;
|
||||
}
|
||||
|
||||
case 'list':
|
||||
return `List directory "${params.path}"${params.recursive ? ' recursively' : ''}${params.filter ? ` with filter "${params.filter}"` : ''}`;
|
||||
|
||||
case 'delete':
|
||||
return `Delete "${params.path}"${params.recursive ? ' recursively' : ''}`;
|
||||
|
||||
case 'exists':
|
||||
return `Check if "${params.path}" exists`;
|
||||
|
||||
case 'stat':
|
||||
return `Get statistics for "${params.path}"`;
|
||||
|
||||
case 'copy':
|
||||
return `Copy "${params.source}" to "${params.destination}"`;
|
||||
|
||||
case 'move':
|
||||
return `Move "${params.source}" to "${params.destination}"`;
|
||||
|
||||
case 'mkdir':
|
||||
return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
|
||||
|
||||
default:
|
||||
return `Unknown action: ${action}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
205
ts/smartagent.tools.http.ts
Normal file
205
ts/smartagent.tools.http.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* HTTP tool for making web requests
|
||||
* Wraps @push.rocks/smartrequest
|
||||
*/
|
||||
export class HttpTool extends BaseToolWrapper {
|
||||
public name = 'http';
|
||||
public description = 'Make HTTP requests to web APIs and services';
|
||||
|
||||
public actions: interfaces.IToolAction[] = [
|
||||
{
|
||||
name: 'get',
|
||||
description: 'Make a GET request',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL to request' },
|
||||
headers: { type: 'object', description: 'Request headers (key-value pairs)' },
|
||||
query: { type: 'object', description: 'Query parameters (key-value pairs)' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
description: 'Make a POST request with JSON body',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL to request' },
|
||||
body: { type: 'object', description: 'JSON body to send' },
|
||||
headers: { type: 'object', description: 'Request headers (key-value pairs)' },
|
||||
query: { type: 'object', description: 'Query parameters (key-value pairs)' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'put',
|
||||
description: 'Make a PUT request with JSON body',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL to request' },
|
||||
body: { type: 'object', description: 'JSON body to send' },
|
||||
headers: { type: 'object', description: 'Request headers (key-value pairs)' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
},
|
||||
required: ['url', 'body'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
description: 'Make a PATCH request with JSON body',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL to request' },
|
||||
body: { type: 'object', description: 'JSON body to send' },
|
||||
headers: { type: 'object', description: 'Request headers (key-value pairs)' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
},
|
||||
required: ['url', 'body'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
description: 'Make a DELETE request',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'URL to request' },
|
||||
headers: { type: 'object', description: 'Request headers (key-value pairs)' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
// SmartRequest is stateless, no initialization needed
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
this.validateAction(action);
|
||||
this.ensureInitialized();
|
||||
|
||||
try {
|
||||
let request = plugins.smartrequest.SmartRequest.create().url(params.url as string);
|
||||
|
||||
// Add headers
|
||||
if (params.headers && typeof params.headers === 'object') {
|
||||
for (const [key, value] of Object.entries(params.headers as Record<string, string>)) {
|
||||
request = request.header(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add query parameters
|
||||
if (params.query && typeof params.query === 'object') {
|
||||
request = request.query(params.query as Record<string, string>);
|
||||
}
|
||||
|
||||
// Add timeout
|
||||
if (params.timeout) {
|
||||
request = request.timeout(params.timeout as number);
|
||||
}
|
||||
|
||||
// Add JSON body for POST, PUT, PATCH
|
||||
if (params.body && ['post', 'put', 'patch'].includes(action)) {
|
||||
request = request.json(params.body);
|
||||
}
|
||||
|
||||
// Execute the request
|
||||
let response;
|
||||
switch (action) {
|
||||
case 'get':
|
||||
response = await request.get();
|
||||
break;
|
||||
case 'post':
|
||||
response = await request.post();
|
||||
break;
|
||||
case 'put':
|
||||
response = await request.put();
|
||||
break;
|
||||
case 'patch':
|
||||
response = await request.patch();
|
||||
break;
|
||||
case 'delete':
|
||||
response = await request.delete();
|
||||
break;
|
||||
default:
|
||||
return { success: false, error: `Unknown action: ${action}` };
|
||||
}
|
||||
|
||||
// Parse response body
|
||||
let body: unknown;
|
||||
const contentType = response.headers?.['content-type'] || '';
|
||||
|
||||
try {
|
||||
if (contentType.includes('application/json')) {
|
||||
body = await response.json();
|
||||
} else {
|
||||
body = await response.text();
|
||||
}
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
result: {
|
||||
url: params.url,
|
||||
method: action.toUpperCase(),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
headers: response.headers,
|
||||
body,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||||
const method = action.toUpperCase();
|
||||
let summary = `${method} request to "${params.url}"`;
|
||||
|
||||
if (params.query && Object.keys(params.query as object).length > 0) {
|
||||
const queryStr = JSON.stringify(params.query);
|
||||
summary += ` with query: ${queryStr.length > 50 ? queryStr.substring(0, 50) + '...' : queryStr}`;
|
||||
}
|
||||
|
||||
if (params.body) {
|
||||
const bodyStr = JSON.stringify(params.body);
|
||||
const preview = bodyStr.length > 100 ? bodyStr.substring(0, 100) + '...' : bodyStr;
|
||||
summary += ` with body: ${preview}`;
|
||||
}
|
||||
|
||||
if (params.headers && Object.keys(params.headers as object).length > 0) {
|
||||
const headerKeys = Object.keys(params.headers as object).join(', ');
|
||||
summary += ` with headers: [${headerKeys}]`;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
182
ts/smartagent.tools.shell.ts
Normal file
182
ts/smartagent.tools.shell.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './smartagent.interfaces.js';
|
||||
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||||
|
||||
/**
|
||||
* Shell tool for executing commands securely
|
||||
* Wraps @push.rocks/smartshell with execSpawn for safety (no shell injection)
|
||||
*/
|
||||
export class ShellTool extends BaseToolWrapper {
|
||||
public name = 'shell';
|
||||
public description =
|
||||
'Execute shell commands securely. Uses execSpawn (shell:false) to prevent command injection.';
|
||||
|
||||
public actions: interfaces.IToolAction[] = [
|
||||
{
|
||||
name: 'execute',
|
||||
description:
|
||||
'Execute a command with arguments (secure, no shell injection possible). Command and args are passed separately.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: {
|
||||
type: 'string',
|
||||
description: 'The command to execute (e.g., "ls", "cat", "grep", "node")',
|
||||
},
|
||||
args: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of arguments (each argument is properly escaped)',
|
||||
},
|
||||
cwd: { type: 'string', description: 'Working directory for the command' },
|
||||
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||||
env: {
|
||||
type: 'object',
|
||||
description: 'Additional environment variables (key-value pairs)',
|
||||
},
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'which',
|
||||
description: 'Check if a command exists and get its path',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Command name to look up (e.g., "node", "git")' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private smartshell!: plugins.smartshell.Smartshell;
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.smartshell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
this.validateAction(action);
|
||||
this.ensureInitialized();
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'execute': {
|
||||
const command = params.command as string;
|
||||
const args = (params.args as string[]) || [];
|
||||
|
||||
// Build options
|
||||
const options: {
|
||||
timeout?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cwd?: string;
|
||||
} = {};
|
||||
|
||||
if (params.timeout) {
|
||||
options.timeout = params.timeout as number;
|
||||
}
|
||||
|
||||
if (params.env) {
|
||||
options.env = {
|
||||
...process.env,
|
||||
...(params.env as NodeJS.ProcessEnv),
|
||||
};
|
||||
}
|
||||
|
||||
// Use execSpawn for security - no shell injection possible
|
||||
const result = await this.smartshell.execSpawn(command, args, options);
|
||||
|
||||
return {
|
||||
success: result.exitCode === 0,
|
||||
result: {
|
||||
command,
|
||||
args,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
signal: result.signal,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'which': {
|
||||
try {
|
||||
const commandPath = await plugins.smartshell.which(params.command as string);
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
command: params.command,
|
||||
path: commandPath,
|
||||
exists: true,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
command: params.command,
|
||||
path: null,
|
||||
exists: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${action}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||||
switch (action) {
|
||||
case 'execute': {
|
||||
const command = params.command as string;
|
||||
const args = (params.args as string[]) || [];
|
||||
const fullCommand = [command, ...args].join(' ');
|
||||
let summary = `Execute: ${fullCommand}`;
|
||||
|
||||
if (params.cwd) {
|
||||
summary += ` (in ${params.cwd})`;
|
||||
}
|
||||
|
||||
if (params.timeout) {
|
||||
summary += ` [timeout: ${params.timeout}ms]`;
|
||||
}
|
||||
|
||||
if (params.env && Object.keys(params.env as object).length > 0) {
|
||||
const envKeys = Object.keys(params.env as object).join(', ');
|
||||
summary += ` [env: ${envKeys}]`;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
case 'which':
|
||||
return `Check if command "${params.command}" exists and get its path`;
|
||||
|
||||
default:
|
||||
return `Unknown action: ${action}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": ["dist_*/**/*.d.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user