46 Commits

Author SHA1 Message Date
c1b269f301 v1.6.2
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:56:44 +00:00
7cb970f9e2 fix(release): bump version to 1.6.2 2026-01-20 03:56:44 +00:00
1fbcf8bb8b fix(driveragent): save tool_calls in message history for native tool calling
When using native tool calling, the assistant's tool_calls must be saved
in message history. Without this, the model doesn't know it already called
a tool and loops indefinitely calling the same tool.

This fix saves tool_calls in both startTaskWithNativeTools and
continueWithNativeTools methods.

Also updates @push.rocks/smartai to v0.13.3 for tool_calls forwarding support.
2026-01-20 03:56:10 +00:00
4a8789019a v1.6.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:38:07 +00:00
0da85a5dcd fix(driveragent): include full message history for tool results and use a continuation prompt when invoking provider.collectStreamResponse 2026-01-20 03:38:07 +00:00
121e216eea v1.6.0
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:28:59 +00:00
eb1058bfb5 feat(smartagent): record native tool results in message history by adding optional toolName to continueWithNativeTools and passing tool identifier from DualAgent 2026-01-20 03:28:59 +00:00
ecdc125a43 v1.5.4
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:16:02 +00:00
73657be550 fix(driveragent): prevent duplicate thinking/output markers during token streaming and mark transitions 2026-01-20 03:16:02 +00:00
4e4d3c0e08 v1.5.3
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 03:10:53 +00:00
79efe8f6b8 fix(driveragent): prefix thinking tokens with [THINKING] when forwarding streaming chunks to onToken 2026-01-20 03:10:53 +00:00
8bcf3257e2 v1.5.2
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:54:58 +00:00
6753553394 fix(): no changes in this diff; nothing to release 2026-01-20 02:54:58 +00:00
a46dbd0da6 fix(driveragent): enable streaming for native tool calling methods 2026-01-20 02:54:45 +00:00
7379daf4c5 v1.5.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:45:41 +00:00
83422b4b0e fix(smartagent): bump patch version to 1.5.1 (no changes in diff) 2026-01-20 02:45:41 +00:00
4310c8086b feat(native-tools): add native tool calling support for Ollama models
- Add INativeToolCall interface for native tool call format
- Add useNativeToolCalling option to IDualAgentOptions
- Add getToolsAsJsonSchema() to convert tools to Ollama JSON Schema format
- Add parseNativeToolCalls() to convert native tool calls to proposals
- Add startTaskWithNativeTools() and continueWithNativeTools() to DriverAgent
- Update DualAgentOrchestrator to support both XML parsing and native tool calling modes

Native tool calling is more efficient for models like GPT-OSS that use Harmony format,
as it activates Ollama's built-in tool parser instead of requiring XML generation.
2026-01-20 02:44:54 +00:00
472a8ed7f8 v1.5.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 02:05:12 +00:00
44137a8710 feat(driveragent): preserve assistant reasoning in message history and update @push.rocks/smartai dependency to ^0.13.0 2026-01-20 02:05:12 +00:00
c12a6a7be9 v1.4.2
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:41:18 +00:00
49dcc7a1a1 fix(repo): no changes detected in diff 2026-01-20 01:41:18 +00:00
e649e9caab fix(driver): make tool call format instructions explicit about literal XML output
The system message now clearly states that the <tool_call> XML tags MUST
be literally written in the response, not just described. Includes examples
of CORRECT vs WRONG usage to help smaller models understand.
2026-01-20 01:40:57 +00:00
c39e7e76b8 v1.4.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:36:30 +00:00
c24a4306d9 fix(): no changes detected (empty diff) 2026-01-20 01:36:30 +00:00
9718048dff fix(dualagent): improve no-tool-call feedback with explicit XML format reminder
When the LLM fails to emit a tool_call XML block, the feedback now includes
the exact XML format expected with a concrete example for json.validate.
This helps smaller models understand the exact output format required.
2026-01-20 01:36:03 +00:00
b1deccaa26 v1.4.0
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:30:26 +00:00
52d1d128c7 feat(docs): document Dual-Agent Driver/Guardian architecture, new standard tools, streaming/vision support, progress events, and updated API/export docs 2026-01-20 01:30:26 +00:00
60f8bbe1b6 feat(tools): add getToolExplanation() method with XML examples for LLM tool calling
Each tool now provides comprehensive documentation including parameter
schemas and concrete <tool_call> XML examples. This helps smaller LLMs
understand the exact format required for tool invocation.
2026-01-20 01:30:03 +00:00
b6308d2113 v1.3.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 01:12:03 +00:00
e7968a31b1 feat(smartagent): add JsonValidatorTool and support passing base64-encoded images with task runs (vision-capable models); bump @push.rocks/smartai to ^0.12.0 2026-01-20 01:12:03 +00:00
05e4f03061 v1.2.8 2026-01-20 00:38:41 +00:00
37d4069806 feat(streaming): add streaming support to DriverAgent and DualAgentOrchestrator
- Add onToken callback option to IDualAgentOptions interface
- Add onToken property and setOnToken method to DriverAgent
- Wire up streaming in startTask and continueWithMessage methods
- Pass source identifier ('driver'/'guardian') to onToken callback
2026-01-20 00:38:36 +00:00
fe0de36b1a v1.2.7
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 00:13:32 +00:00
e49f35e7de fix(deps(smartai)): bump @push.rocks/smartai to ^0.11.0 2026-01-20 00:13:32 +00:00
4fbffdd97d v1.2.6
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 1m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-20 00:05:42 +00:00
560838477f fix(deps): bump @push.rocks/smartai to ^0.10.1 2026-01-20 00:05:42 +00:00
7503fccbf2 1.2.5
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:30:13 +00:00
a76bd0d3e4 update 2025-12-15 15:29:56 +00:00
1556a9d3e9 1.2.4
Some checks failed
Default (tags) / security (push) Successful in 54s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:16:08 +00:00
19ba58ca40 update 2025-12-15 15:11:22 +00:00
8662b73adb update 2025-12-15 14:49:26 +00:00
9e848045f7 1.2.3
Some checks failed
Default (tags) / security (push) Successful in 55s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 14:25:05 +00:00
8827a55768 1.2.2
Some checks failed
Default (tags) / security (push) Successful in 58s
Default (tags) / test (push) Failing after 1m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 14:24:06 +00:00
37b6e98a81 improve tools 2025-12-15 14:23:53 +00:00
35911c21de 1.2.1
Some checks failed
Default (tags) / security (push) Successful in 1m0s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 12:37:28 +00:00
7403e769d0 update 2025-12-15 12:37:19 +00:00
18 changed files with 2423 additions and 175 deletions

View File

@@ -1,5 +1,108 @@
# Changelog
## 2026-01-20 - 1.6.2 - fix(release)
bump version to 1.6.2
- No source changes detected in the diff
- Current package.json version is 1.6.1
- Recommend a patch bump to 1.6.2 for a release
## 2026-01-20 - 1.6.1 - fix(driveragent)
include full message history for tool results and use a continuation prompt when invoking provider.collectStreamResponse
- When toolName is provided, include the full messageHistory (do not slice off the last message) so tool result messages are preserved.
- Set userMessage to a continuation prompt ('Continue with the task. The tool result has been provided above.') when handling tool results to avoid repeating the tool output.
- Keeps existing maxHistoryMessages trimming and validates provider.collectStreamResponse is available before streaming.
## 2026-01-20 - 1.6.0 - feat(smartagent)
record native tool results in message history by adding optional toolName to continueWithNativeTools and passing tool identifier from DualAgent
- continueWithNativeTools(message, toolName?) now accepts an optional toolName; when provided the message is stored with role 'tool' and includes a toolName property (cast to ChatMessage)
- DualAgent constructs a toolNameForHistory as `${proposal.toolName}_${proposal.action}` and forwards it to continueWithNativeTools in both normal and error flows
- Preserves tool-origin information in the conversation history to support native tool calling and tracking
## 2026-01-20 - 1.5.4 - fix(driveragent)
prevent duplicate thinking/output markers during token streaming and mark transitions
- Add isInThinkingMode flag to track thinking vs output state
- Emit "\n[THINKING] " only when transitioning into thinking mode (avoids repeated thinking markers)
- Emit "\n[OUTPUT] " when transitioning out of thinking mode to mark content output
- Reset thinking state after response completes to ensure correct markers for subsequent responses
- Applied the same streaming marker logic to both response handling paths
## 2026-01-20 - 1.5.3 - fix(driveragent)
prefix thinking tokens with [THINKING] when forwarding streaming chunks to onToken
- Wraps chunk.thinking with '[THINKING] ' before calling onToken to mark thinking tokens
- Forwards chunk.content unchanged
- Change applied in ts/smartagent.classes.driveragent.ts for both initial and subsequent assistant streaming responses
- No API signature changes; only the token payloads sent to onToken are altered
## 2026-01-20 - 1.5.2 - fix()
no changes in this diff; nothing to release
- No files changed; no release required
- No code or dependency changes detected
## 2026-01-20 - 1.5.1 - fix(smartagent)
bump patch version to 1.5.1 (no changes in diff)
- No code changes detected in the provided diff
- Current package.json version is 1.5.0
- Recommended semantic version bump: patch -> 1.5.1
## 2026-01-20 - 1.5.0 - feat(driveragent)
preserve assistant reasoning in message history and update @push.rocks/smartai dependency to ^0.13.0
- Store response.reasoning in messageHistory for assistant responses (two places in driveragent)
- Bump dependency @push.rocks/smartai from ^0.12.0 to ^0.13.0
## 2026-01-20 - 1.4.2 - fix(repo)
no changes detected in diff
- No files changed in diff; no code or metadata updates were made.
- No version bump required.
## 2026-01-20 - 1.4.1 - fix()
no changes detected (empty diff)
- No files changed in this commit
- No release required
## 2026-01-20 - 1.4.0 - feat(docs)
document Dual-Agent Driver/Guardian architecture, new standard tools, streaming/vision support, progress events, and updated API/export docs
- Add DualAgentOrchestrator concept and describe Driver/Guardian agents and BaseToolWrapper
- Document six standard tools including new JsonValidatorTool and expanded descriptions for Filesystem, Http, Shell, Browser, Deno
- Add examples for scoped filesystem with exclusion patterns and line-range reads
- Add token streaming (onToken) and progress events (onProgress) examples and event types
- Document vision support for passing images as base64 and example usage
- Expose additional config options in docs: name, verbose, maxResultChars, maxHistoryMessages, onProgress, onToken, logPrefix
- Document additional result fields: toolCallCount, rejectionCount, toolLog, and error
- Update API signatures in docs: run(task, options?) and registerScopedFilesystemTool(basePath, excludePatterns?)
- Update re-exports to include IFilesystemToolOptions, TDenoPermission, JsonValidatorTool and re-export several smartai types
## 2026-01-20 - 1.3.0 - feat(smartagent)
add JsonValidatorTool and support passing base64-encoded images with task runs (vision-capable models); bump @push.rocks/smartai to ^0.12.0
- Add JsonValidatorTool (validate/format actions) implemented in ts/smartagent.tools.json.ts
- Export JsonValidatorTool from ts/index.ts
- Add ITaskRunOptions interface (images?: string[]) in smartagent.interfaces.ts
- DualAgent.run and Driver.startTask accept optional images and pass them to provider.chat/provider.chatStreaming; assistant responses added to message history
- Bump dependency @push.rocks/smartai from ^0.11.1 to ^0.12.0 in package.json
## 2026-01-20 - 1.2.7 - fix(deps(smartai))
bump @push.rocks/smartai to ^0.11.0
- package.json: @push.rocks/smartai updated from ^0.10.1 to ^0.11.0
- Recommend a patch release since this is a dependency update with no breaking API changes: 1.2.7
## 2026-01-20 - 1.2.6 - fix(deps)
bump @push.rocks/smartai to ^0.10.1
- Updated dependency @push.rocks/smartai from ^0.8.0 to ^0.10.1 in package.json
- No other code changes; dependency-only update
## 2025-12-15 - 1.1.1 - fix(ci)
Update CI/release config and bump devDependencies; enable verbose tests

View File

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

87
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@push.rocks/smartai':
specifier: ^0.8.0
version: 0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
specifier: ^0.13.3
version: 0.13.3(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)
'@push.rocks/smartbrowser':
specifier: ^2.0.8
version: 2.0.8(typescript@5.9.3)
@@ -26,6 +26,9 @@ importers:
'@push.rocks/smartshell':
specifier: ^3.3.0
version: 3.3.0
minimatch:
specifier: ^10.1.1
version: 10.1.1
devDependencies:
'@git.zone/tsbuild':
specifier: ^4.0.2
@@ -45,8 +48,8 @@ importers:
packages:
'@anthropic-ai/sdk@0.65.0':
resolution: {integrity: sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==}
'@anthropic-ai/sdk@0.71.2':
resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==}
hasBin: true
peerDependencies:
zod: ^3.25.0 || ^4.0.0
@@ -240,6 +243,10 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
@@ -264,6 +271,9 @@ packages:
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -717,6 +727,9 @@ packages:
'@lit/reactive-element@2.1.1':
resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==}
'@mistralai/mistralai@1.12.0':
resolution: {integrity: sha512-oDr1hcS3wsIT/QupBG93TNiA5kilwBYoAIyl5BNYqMM2Ix/xsNq+wT8b++uhp/GTUMx44n+8Bn1mkATbwxe6bQ==}
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -831,8 +844,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartai@0.8.0':
resolution: {integrity: sha512-guzi28meUDc3mydC8kpoA+4pzExRQqygXYFDD4qQSWPpIRHQ7qhpeNqJzrrGezT1yOH5Gb9taPEGwT56hI+nwQ==}
'@push.rocks/smartai@0.13.3':
resolution: {integrity: sha512-VDZzHs101hpGMmUaectuLfcME4kHpuOS7o5ffuGk5lYl383foyAN71+5v441jpk/gLDNf2KhDACR/d2O4n90Ag==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -916,6 +929,9 @@ packages:
'@push.rocks/smartfs@1.2.0':
resolution: {integrity: sha512-1R47jJZwX869z7DYgKeAZKTU1SbGnM7W/ZmgsI7AkQQhiascNqY3/gF4V5kIprmuf1WhpRbCbZyum8s7J1LDdg==}
'@push.rocks/smartfs@1.3.1':
resolution: {integrity: sha512-ZSduVS8tM+/erbyCTvRRvc9gLWwbpqN5xdIIkMr+gub7fowSeJb7tR2rnGwySa63DyimU0q2KTp79VV9YqGLeg==}
'@push.rocks/smartguard@3.1.0':
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
@@ -2972,12 +2988,12 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
openai@5.23.2:
resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==}
openai@6.16.0:
resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
zod: ^3.25 || ^4.0
peerDependenciesMeta:
ws:
optional: true
@@ -3293,8 +3309,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@@ -3732,6 +3749,11 @@ packages:
resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==}
engines: {node: '>=12'}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: ^3.25 || ^4
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -3740,7 +3762,7 @@ packages:
snapshots:
'@anthropic-ai/sdk@0.65.0(zod@3.25.76)':
'@anthropic-ai/sdk@0.71.2(zod@3.25.76)':
dependencies:
json-schema-to-ts: 3.1.1
optionalDependencies:
@@ -4316,6 +4338,8 @@ snapshots:
'@babel/runtime@7.28.4': {}
'@babel/runtime@7.28.6': {}
'@borewit/text-codec@0.1.1': {}
'@cloudflare/workers-types@4.20251202.0': {}
@@ -4380,6 +4404,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
@@ -4667,7 +4696,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.7.1
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -4891,6 +4920,11 @@ snapshots:
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
'@mistralai/mistralai@1.12.0':
dependencies:
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
'@mixmark-io/domino@2.2.0': {}
'@module-federation/error-codes@0.21.6': {}
@@ -5138,17 +5172,18 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartai@0.8.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
'@push.rocks/smartai@0.13.3(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)':
dependencies:
'@anthropic-ai/sdk': 0.65.0(zod@3.25.76)
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
'@mistralai/mistralai': 1.12.0
'@push.rocks/smartarray': 1.1.0
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartfs': 1.3.1
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpdf': 4.1.1(typescript@5.9.3)
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.4.2
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/webstream': 1.0.10
openai: 5.23.2(ws@8.18.3)(zod@3.25.76)
openai: 6.16.0(ws@8.18.3)(zod@3.25.76)
transitivePeerDependencies:
- '@nuxt/kit'
- aws-crt
@@ -5444,6 +5479,10 @@ snapshots:
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartfs@1.3.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartguard@3.1.0':
dependencies:
'@push.rocks/smartpromise': 4.2.3
@@ -7563,7 +7602,7 @@ snapshots:
json-schema-to-ts@3.1.1:
dependencies:
'@babel/runtime': 7.28.4
'@babel/runtime': 7.28.6
ts-algebra: 2.0.0
jsonfile@6.2.0:
@@ -8151,7 +8190,7 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
openai@5.23.2(ws@8.18.3)(zod@3.25.76):
openai@6.16.0(ws@8.18.3)(zod@3.25.76):
optionalDependencies:
ws: 8.18.3
zod: 3.25.76
@@ -8522,7 +8561,7 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.4.3: {}
sax@1.4.4: {}
semver@6.3.1: {}
@@ -8999,7 +9038,7 @@ snapshots:
xml2js@0.5.0:
dependencies:
sax: 1.4.3
sax: 1.4.4
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
@@ -9034,6 +9073,10 @@ snapshots:
buffer-crc32: 0.2.13
pend: 1.2.0
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76
zod@3.25.76: {}
zwitch@2.0.4: {}

View File

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

318
readme.md
View File

@@ -1,21 +1,31 @@
# @push.rocks/smartagent
A dual-agent agentic framework with Driver and Guardian agents for safe, policy-controlled AI task execution.
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
```
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview
SmartAgent implements a dual-agent architecture:
SmartAgent implements a **dual-agent architecture** where AI safety isn't just an afterthought—it's baked into the core design:
- **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
- **🎯 Driver Agent**: The executor. Reasons about goals, plans steps, and proposes tool calls
- **🛡️ Guardian Agent**: The gatekeeper. Evaluates every tool call against your policy, approving or rejecting with feedback
This design ensures safe tool use through AI-based policy evaluation rather than rigid programmatic rules.
This design ensures safe tool use through **AI-based policy evaluation** rather than rigid programmatic rules. The Guardian can understand context, nuance, and intent—catching dangerous operations that simple regex or allowlists would miss.
### Why Dual-Agent?
Traditional AI agents have a fundamental problem: they're given tools and expected to use them responsibly. SmartAgent adds a second AI specifically trained to evaluate whether each action is safe and appropriate. Think of it as separation of concerns, but for AI safety.
## Architecture
@@ -40,6 +50,7 @@ flowchart TB
Shell["Shell"]
Browser["Browser"]
Deno["Deno"]
JSON["JSON Validator"]
end
Task --> Orchestrator
@@ -89,8 +100,11 @@ await orchestrator.stop();
## Standard Tools
### FilesystemTool
File and directory operations using `@push.rocks/smartfs`.
SmartAgent comes with six battle-tested tools out of the box:
### 🗂️ FilesystemTool
File and directory operations powered by `@push.rocks/smartfs`.
**Actions**: `read`, `write`, `append`, `list`, `delete`, `exists`, `stat`, `copy`, `move`, `mkdir`
@@ -104,7 +118,33 @@ File and directory operations using `@push.rocks/smartfs`.
</tool_call>
```
### HttpTool
**Scoped Filesystem**: Lock file operations to a specific directory with optional exclusion patterns:
```typescript
// Only allow access within a specific directory
orchestrator.registerScopedFilesystemTool('/home/user/workspace');
// With exclusion patterns (glob syntax)
orchestrator.registerScopedFilesystemTool('/home/user/workspace', [
'.nogit/**',
'node_modules/**',
'*.secret',
]);
```
**Line-range Reading**: Read specific portions of large files:
```typescript
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/var/log/app.log", "startLine": 100, "endLine": 150}</params>
<reasoning>Reading only the relevant log section to avoid token overload</reasoning>
</tool_call>
```
### 🌐 HttpTool
HTTP requests using `@push.rocks/smartrequest`.
**Actions**: `get`, `post`, `put`, `patch`, `delete`
@@ -113,13 +153,14 @@ HTTP requests using `@push.rocks/smartrequest`.
<tool_call>
<tool>http</tool>
<action>get</action>
<params>{"url": "https://api.example.com/data"}</params>
<params>{"url": "https://api.example.com/data", "headers": {"Authorization": "Bearer token"}}</params>
<reasoning>Fetching data from the API endpoint</reasoning>
</tool_call>
```
### ShellTool
Secure shell command execution using `@push.rocks/smartshell` with `execSpawn` (no shell injection).
### 💻 ShellTool
Secure shell command execution using `@push.rocks/smartshell` with `execSpawn` (no shell injection possible).
**Actions**: `execute`, `which`
@@ -132,7 +173,10 @@ Secure shell command execution using `@push.rocks/smartshell` with `execSpawn` (
</tool_call>
```
### BrowserTool
> 🔒 **Security Note**: The shell tool uses `execSpawn` with `shell: false`, meaning command and arguments are passed separately. This makes shell injection attacks impossible.
### 🌍 BrowserTool
Web page interaction using `@push.rocks/smartbrowser` (Puppeteer-based).
**Actions**: `screenshot`, `pdf`, `evaluate`, `getPageContent`
@@ -146,17 +190,18 @@ Web page interaction using `@push.rocks/smartbrowser` (Puppeteer-based).
</tool_call>
```
### DenoTool
Execute TypeScript/JavaScript code in a sandboxed Deno environment using `@push.rocks/smartdeno`.
### 🦕 DenoTool
Execute TypeScript/JavaScript code in a **sandboxed Deno environment** with fine-grained permission control.
**Actions**: `execute`, `executeWithResult`
**Permissions**: `all`, `env`, `ffi`, `hrtime`, `net`, `read`, `run`, `sys`, `write`
By default, code runs fully sandboxed with no permissions. Permissions must be explicitly requested.
By default, code runs **fully sandboxed with no permissions**. Permissions must be explicitly requested and are subject to Guardian approval.
```typescript
// Simple code execution
// Simple code execution (sandboxed, no permissions)
<tool_call>
<tool>deno</tool>
<action>execute</action>
@@ -186,9 +231,111 @@ By default, code runs fully sandboxed with no permissions. Permissions must be e
</tool_call>
```
### 📋 JsonValidatorTool
Validate and format JSON data. Perfect for agents to self-check their JSON output before completing tasks.
**Actions**: `validate`, `format`
```typescript
// Validate JSON with required field checking
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{
"jsonString": "{\"name\": \"test\", \"version\": \"1.0.0\"}",
"requiredFields": ["name", "version", "description"]
}</params>
<reasoning>Ensuring the config has all required fields before saving</reasoning>
</tool_call>
// Pretty-print JSON
<tool_call>
<tool>json</tool>
<action>format</action>
<params>{"jsonString": "{\"compact\":true,\"data\":[1,2,3]}"}</params>
<reasoning>Formatting JSON for readable output</reasoning>
</tool_call>
```
## 🎥 Streaming Support
SmartAgent supports token-by-token streaming for real-time output during LLM generation:
```typescript
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
defaultProvider: 'openai',
guardianPolicyPrompt: '...',
// Token streaming callback
onToken: (token, source) => {
// source is 'driver' or 'guardian'
process.stdout.write(token);
},
});
```
This is perfect for CLI applications or UIs that need to show progress as the agent thinks.
## 🖼️ Vision Support
Pass images to vision-capable models for multimodal tasks:
```typescript
import { readFileSync } from 'fs';
// Load image as base64
const imageBase64 = readFileSync('screenshot.png').toString('base64');
// Run task with images
const result = await orchestrator.run(
'Analyze this UI screenshot and describe any usability issues',
{ images: [imageBase64] }
);
```
## 📊 Progress Events
Get real-time feedback on task execution with the `onProgress` callback:
```typescript
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
guardianPolicyPrompt: '...',
logPrefix: '[MyAgent]', // Optional prefix for log messages
onProgress: (event) => {
// Pre-formatted log message ready for output
console.log(event.logMessage);
// Or handle specific event types
switch (event.type) {
case 'tool_proposed':
console.log(`Proposing: ${event.toolName}.${event.action}`);
break;
case 'tool_approved':
console.log(`✓ Approved`);
break;
case 'tool_rejected':
console.log(`✗ Rejected: ${event.reason}`);
break;
case 'task_completed':
console.log(`Done in ${event.iteration} iterations`);
break;
}
},
});
```
**Event Types**: `task_started`, `iteration_started`, `tool_proposed`, `guardian_evaluating`, `tool_approved`, `tool_rejected`, `tool_executing`, `tool_completed`, `task_completed`, `clarification_needed`, `max_iterations`, `max_rejections`
## Guardian Policy Examples
### Strict Security Policy
The Guardian's power comes from your policy. Here are battle-tested examples:
### 🔐 Strict Security Policy
```typescript
const securityPolicy = `
SECURITY POLICY:
@@ -204,7 +351,8 @@ When rejecting, always explain:
`;
```
### Development Environment Policy
### 🛠️ Development Environment Policy
```typescript
const devPolicy = `
DEVELOPMENT POLICY:
@@ -221,7 +369,8 @@ Always verify:
`;
```
### Deno Code Execution Policy
### 🦕 Deno Code Execution Policy
```typescript
const denoPolicy = `
DENO CODE EXECUTION POLICY:
@@ -253,6 +402,9 @@ interface IDualAgentOptions {
groqToken?: string;
xaiToken?: string;
// Use existing SmartAi instance (optional - avoids duplicate providers)
smartAiInstance?: SmartAi;
// Provider selection
defaultProvider?: TProvider; // For both Driver and Guardian
guardianProvider?: TProvider; // Optional: separate provider for Guardian
@@ -260,10 +412,19 @@ interface IDualAgentOptions {
// Agent configuration
driverSystemMessage?: string; // Custom system message for Driver
guardianPolicyPrompt: string; // REQUIRED: Policy for Guardian to enforce
name?: string; // Agent system name
verbose?: boolean; // Enable verbose logging
// Limits
maxIterations?: number; // Max task iterations (default: 20)
maxConsecutiveRejections?: number; // Abort after N rejections (default: 3)
maxResultChars?: number; // Max chars for tool results before truncation (default: 15000)
maxHistoryMessages?: number; // Max history messages for API (default: 20)
// Callbacks
onProgress?: (event: IProgressEvent) => void; // Progress event callback
onToken?: (token: string, source: 'driver' | 'guardian') => void; // Streaming callback
logPrefix?: string; // Prefix for log messages
}
```
@@ -277,7 +438,19 @@ interface IDualAgentRunResult {
iterations: number; // Number of iterations taken
history: IAgentMessage[]; // Full conversation history
status: TDualAgentRunStatus; // 'completed' | 'max_iterations_reached' | etc.
toolCallCount?: number; // Number of tool calls made
rejectionCount?: number; // Number of Guardian rejections
toolLog?: IToolExecutionLog[]; // Detailed tool execution log
error?: string; // Error message if status is 'error'
}
type TDualAgentRunStatus =
| 'completed'
| 'in_progress'
| 'max_iterations_reached'
| 'max_rejections_reached'
| 'clarification_needed'
| 'error';
```
## Custom Tools
@@ -306,10 +479,12 @@ class MyCustomTool extends BaseToolWrapper {
];
public async initialize(): Promise<void> {
// Setup your tool (called when orchestrator.start() runs)
this.isInitialized = true;
}
public async cleanup(): Promise<void> {
// Cleanup resources (called when orchestrator.stop() runs)
this.isInitialized = false;
}
@@ -321,12 +496,14 @@ class MyCustomTool extends BaseToolWrapper {
return {
success: true,
result: { processed: params.input },
summary: `Processed input: ${params.input}`, // Optional human-readable summary
};
}
return { success: false, error: 'Unknown action' };
}
// Human-readable summary for Guardian evaluation
public getCallSummary(action: string, params: Record<string, unknown>): string {
return `Custom action "${action}" with input "${params.input}"`;
}
@@ -336,32 +513,115 @@ class MyCustomTool extends BaseToolWrapper {
orchestrator.registerTool(new MyCustomTool());
```
## Reusing SmartAi Instances
If you already have a `@push.rocks/smartai` instance, you can share it:
```typescript
import { SmartAi } from '@push.rocks/smartai';
import { DualAgentOrchestrator } from '@push.rocks/smartagent';
const smartai = new SmartAi({ openaiToken: 'sk-...' });
await smartai.start();
const orchestrator = new DualAgentOrchestrator({
smartAiInstance: smartai, // Reuse existing instance
guardianPolicyPrompt: '...',
});
await orchestrator.start();
// ... use orchestrator ...
await orchestrator.stop();
// SmartAi instance lifecycle is managed separately
await smartai.stop();
```
## Supported Providers
SmartAgent supports all providers from `@push.rocks/smartai`:
| Provider | Driver | Guardian |
|----------|:------:|:--------:|
| OpenAI | Yes | Yes |
| Anthropic | Yes | Yes |
| Perplexity | Yes | Yes |
| Groq | Yes | Yes |
| Ollama | Yes | Yes |
| XAI | Yes | Yes |
| OpenAI | ✅ | ✅ |
| Anthropic | ✅ | ✅ |
| Perplexity | ✅ | ✅ |
| Groq | ✅ | ✅ |
| Ollama | ✅ | ✅ |
| XAI | ✅ | ✅ |
| Exo | ✅ | ✅ |
**💡 Pro tip**: Use a faster/cheaper model for Guardian (like Groq) and a more capable model for Driver:
```typescript
const orchestrator = new DualAgentOrchestrator({
openaiToken: 'sk-...',
groqToken: 'gsk-...',
defaultProvider: 'openai', // Driver uses OpenAI
guardianProvider: 'groq', // Guardian uses Groq (faster, cheaper)
guardianPolicyPrompt: '...',
});
```
## API Reference
### DualAgentOrchestrator
| Method | Description |
|--------|-------------|
| `start()` | Initialize all tools and AI providers |
| `stop()` | Cleanup all tools and resources |
| `run(task, options?)` | Execute a task with optional images for vision |
| `continueTask(input)` | Continue a task with user input |
| `registerTool(tool)` | Register a custom tool |
| `registerStandardTools()` | Register all built-in tools |
| `registerScopedFilesystemTool(basePath, excludePatterns?)` | Register filesystem tool with path restriction |
| `setGuardianPolicy(policy)` | Update Guardian policy at runtime |
| `getHistory()` | Get conversation history |
| `getToolNames()` | Get list of registered tool names |
| `isActive()` | Check if orchestrator is running |
### Exports
```typescript
// Main classes
export { DualAgentOrchestrator } from '@push.rocks/smartagent';
export { DriverAgent } from '@push.rocks/smartagent';
export { GuardianAgent } from '@push.rocks/smartagent';
// Tools
export { BaseToolWrapper } from '@push.rocks/smartagent';
export { FilesystemTool, type IFilesystemToolOptions } from '@push.rocks/smartagent';
export { HttpTool } from '@push.rocks/smartagent';
export { ShellTool } from '@push.rocks/smartagent';
export { BrowserTool } from '@push.rocks/smartagent';
export { DenoTool, type TDenoPermission } from '@push.rocks/smartagent';
export { JsonValidatorTool } from '@push.rocks/smartagent';
// Types and interfaces
export * from '@push.rocks/smartagent'; // All interfaces
// Re-exported from @push.rocks/smartai
export { type ISmartAiOptions, type TProvider, type ChatMessage, type ChatOptions, type ChatResponse };
```
## License and Legal Information
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.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH 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.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
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.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

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

View File

@@ -11,11 +11,12 @@ export { GuardianAgent } from './smartagent.classes.guardianagent.js';
export { BaseToolWrapper } from './smartagent.tools.base.js';
// Export standard tools
export { FilesystemTool } from './smartagent.tools.filesystem.js';
export { FilesystemTool, type IFilesystemToolOptions } 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 { DenoTool, type TDenoPermission } from './smartagent.tools.deno.js';
export { JsonValidatorTool } from './smartagent.tools.json.js';
// Export all interfaces
export * from './smartagent.interfaces.js';

View File

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

View File

@@ -2,6 +2,18 @@ import * as plugins from './plugins.js';
import * as interfaces from './smartagent.interfaces.js';
import type { BaseToolWrapper } from './smartagent.tools.base.js';
/**
* Options for configuring the DriverAgent
*/
export interface IDriverAgentOptions {
/** Custom system message for the driver */
systemMessage?: string;
/** Maximum history messages to pass to API (default: 20). Set to 0 for unlimited. */
maxHistoryMessages?: number;
/** Callback fired for each token during LLM generation */
onToken?: (token: string) => void;
}
/**
* DriverAgent - Executes tasks by reasoning and proposing tool calls
* Works in conjunction with GuardianAgent for approval
@@ -9,15 +21,35 @@ import type { BaseToolWrapper } from './smartagent.tools.base.js';
export class DriverAgent {
private provider: plugins.smartai.MultiModalModel;
private systemMessage: string;
private maxHistoryMessages: number;
private messageHistory: plugins.smartai.ChatMessage[] = [];
private tools: Map<string, BaseToolWrapper> = new Map();
private onToken?: (token: string) => void;
private isInThinkingMode = false; // Track thinking/content state for markers
constructor(
provider: plugins.smartai.MultiModalModel,
systemMessage?: string
options?: IDriverAgentOptions | string
) {
this.provider = provider;
this.systemMessage = systemMessage || this.getDefaultSystemMessage();
// Support both legacy string systemMessage and new options object
if (typeof options === 'string') {
this.systemMessage = options || this.getDefaultSystemMessage();
this.maxHistoryMessages = 20;
} else {
this.systemMessage = options?.systemMessage || this.getDefaultSystemMessage();
this.maxHistoryMessages = options?.maxHistoryMessages ?? 20;
this.onToken = options?.onToken;
}
}
/**
* Set the token callback for streaming mode
* @param callback Function to call for each generated token
*/
public setOnToken(callback: (token: string) => void): void {
this.onToken = callback;
}
/**
@@ -36,13 +68,21 @@ export class DriverAgent {
/**
* Initialize a new conversation for a task
* @param task The task description
* @param images Optional base64-encoded images for vision tasks
*/
public async startTask(task: string): Promise<interfaces.IAgentMessage> {
public async startTask(task: string, images?: string[]): Promise<interfaces.IAgentMessage> {
// Reset message history
this.messageHistory = [];
// 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.`;
// Build the user message based on available tools
const hasTools = this.tools.size > 0;
let userMessage: string;
if (hasTools) {
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.`;
} else {
userMessage = `TASK: ${task}\n\nComplete this task directly. When done, wrap your final output in <task_complete>your output here</task_complete> tags.`;
}
// Add to history
this.messageHistory.push({
@@ -50,22 +90,45 @@ export class DriverAgent {
content: userMessage,
});
// Build tool descriptions for the system message
const toolDescriptions = this.buildToolDescriptions();
const fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
// Build the system message - adapt based on available tools
let fullSystemMessage: string;
if (hasTools) {
const toolDescriptions = this.buildToolDescriptions();
fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
} else {
// Use a simpler system message when no tools are available
fullSystemMessage = this.getNoToolsSystemMessage();
}
// Get response from provider
const response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
});
// Get response from provider - use streaming if available and callback is set
let response: plugins.smartai.ChatResponse;
// Add assistant response to history
this.messageHistory.push({
if (this.onToken && typeof (this.provider as any).chatStreaming === 'function') {
// Use streaming mode with token callback
response = await (this.provider as any).chatStreaming({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
onToken: this.onToken,
});
} else {
// Fallback to non-streaming mode
response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
});
}
// Add assistant response to history (store images if provided, preserve reasoning for GPT-OSS)
const historyMessage: plugins.smartai.ChatMessage = {
role: 'assistant',
content: response.message,
});
reasoning: response.reasoning,
};
this.messageHistory.push(historyMessage);
return {
role: 'assistant',
@@ -83,23 +146,56 @@ export class DriverAgent {
content: message,
});
// Build tool descriptions for the system message
const toolDescriptions = this.buildToolDescriptions();
const fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
// Build the system message - adapt based on available tools
const hasTools = this.tools.size > 0;
let fullSystemMessage: string;
if (hasTools) {
const toolDescriptions = this.buildToolDescriptions();
fullSystemMessage = `${this.systemMessage}\n\n## Available Tools\n${toolDescriptions}`;
} else {
fullSystemMessage = this.getNoToolsSystemMessage();
}
// Get response from provider (pass all but last user message as history)
const historyForChat = this.messageHistory.slice(0, -1);
// Get response from provider with history windowing
// Keep original task and most recent messages to avoid token explosion
let historyForChat: plugins.smartai.ChatMessage[];
const fullHistory = this.messageHistory.slice(0, -1); // Exclude the just-added message
const response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
});
if (this.maxHistoryMessages > 0 && fullHistory.length > this.maxHistoryMessages) {
// Keep the original task (first message) and most recent messages
historyForChat = [
fullHistory[0], // Original task
...fullHistory.slice(-(this.maxHistoryMessages - 1)), // Recent messages
];
} else {
historyForChat = fullHistory;
}
// Add assistant response to history
// Get response from provider - use streaming if available and callback is set
let response: plugins.smartai.ChatResponse;
if (this.onToken && typeof (this.provider as any).chatStreaming === 'function') {
// Use streaming mode with token callback
response = await (this.provider as any).chatStreaming({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
onToken: this.onToken,
});
} else {
// Fallback to non-streaming mode
response = await this.provider.chat({
systemMessage: fullSystemMessage,
userMessage: message,
messageHistory: historyForChat,
});
}
// Add assistant response to history (preserve reasoning for GPT-OSS)
this.messageHistory.push({
role: 'assistant',
content: response.message,
reasoning: response.reasoning,
});
return {
@@ -282,40 +378,398 @@ export class DriverAgent {
## Your Role
You analyze tasks, break them down into steps, and use tools to accomplish goals.
## Tool Usage Format
When you need to use a tool, output a tool call proposal in this format:
## CRITICAL: Tool Usage Format
To use a tool, you MUST literally write out the XML tags in your response. The system parses your output looking for these exact tags. Do NOT just describe or mention the tool call - you must OUTPUT the actual XML.
CORRECT (the XML is in the output):
<tool_call>
<tool>tool_name</tool>
<action>action_name</action>
<params>
{"param1": "value1", "param2": "value2"}
</params>
<reasoning>Brief explanation of why this action is needed</reasoning>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "{\\"key\\":\\"value\\"}"}</params>
</tool_call>
WRONG (just describing, no actual XML):
"I will call json.validate now" or "Let me use the tool"
## Guidelines
1. Think step by step about what needs to be done
2. Use only the tools that are available to you
3. Provide clear reasoning for each tool call
4. If a tool call is rejected, adapt your approach based on the feedback
5. When the task is complete, indicate this clearly:
2. When you need a tool, OUTPUT the <tool_call> XML tags - do not just mention them
3. Only propose ONE tool call at a time
4. Wait for the result before proposing the next action
5. When the task is complete, OUTPUT:
<task_complete>
Brief summary of what was accomplished
Your final result here
</task_complete>
## Important
- Only propose ONE tool call at a time
- Wait for the result before proposing the next action
- If you encounter an error, analyze it and try an alternative approach
- The <tool_call> and <task_complete> tags MUST appear literally in your response
- If you just say "I'll call the tool" without the actual XML, it will NOT work
- If you need clarification, ask using <needs_clarification>your question</needs_clarification>`;
}
/**
* Get the system message when no tools are available
* Used for direct task completion without tool usage
*/
private getNoToolsSystemMessage(): string {
// Use custom system message if provided, otherwise use a simple default
if (this.systemMessage && this.systemMessage !== this.getDefaultSystemMessage()) {
return this.systemMessage;
}
return `You are an AI assistant that completes tasks directly.
## Your Role
You analyze tasks and provide complete, high-quality outputs.
## Output Format
When you have completed the task, wrap your final output in task_complete tags:
<task_complete>
Your complete output here
</task_complete>
## Guidelines
1. Analyze the task requirements carefully
2. Provide a complete and accurate response
3. Always wrap your final output in <task_complete></task_complete> tags
4. If you need clarification, ask using <needs_clarification>your question</needs_clarification>`;
}
/**
* Reset the conversation state
*/
public reset(): void {
this.messageHistory = [];
}
// ================================
// Native Tool Calling Support
// ================================
/**
* Start a task with native tool calling support
* Uses Ollama's native tool calling API instead of XML parsing
* @param task The task description
* @param images Optional base64-encoded images for vision tasks
* @returns Response with content, reasoning, and any tool calls
*/
public async startTaskWithNativeTools(
task: string,
images?: string[]
): Promise<{ message: interfaces.IAgentMessage; toolCalls?: interfaces.INativeToolCall[] }> {
// Reset message history
this.messageHistory = [];
// Build simple user message (no XML instructions needed for native tool calling)
const userMessage = `TASK: ${task}\n\nComplete this task using the available tools. When done, provide your final output.`;
// Add to history
this.messageHistory.push({
role: 'user',
content: userMessage,
});
// Build system message for native tool calling
const fullSystemMessage = this.getNativeToolsSystemMessage();
// Get tools in JSON schema format
const tools = this.getToolsAsJsonSchema();
// Check if provider supports native tool calling (Ollama)
const provider = this.provider as any;
if (typeof provider.collectStreamResponse !== 'function') {
throw new Error('Provider does not support native tool calling. Use startTask() instead.');
}
// Use collectStreamResponse for streaming support with tools
const response = await provider.collectStreamResponse(
{
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: [],
images: images,
tools: tools.length > 0 ? tools : undefined,
},
// Pass onToken callback through onChunk for streaming with thinking markers
this.onToken ? (chunk: any) => {
if (chunk.thinking && this.onToken) {
// Add marker only when transitioning INTO thinking mode
if (!this.isInThinkingMode) {
this.onToken('\n[THINKING] ');
this.isInThinkingMode = true;
}
this.onToken(chunk.thinking);
}
if (chunk.content && this.onToken) {
// Add marker when transitioning OUT of thinking mode
if (this.isInThinkingMode) {
this.onToken('\n[OUTPUT] ');
this.isInThinkingMode = false;
}
this.onToken(chunk.content);
}
} : undefined
);
// Reset thinking state after response completes
this.isInThinkingMode = false;
// Add assistant response to history
const historyMessage: any = {
role: 'assistant',
content: response.message || '',
reasoning: response.thinking || response.reasoning,
};
// CRITICAL: Preserve tool_calls in history for native tool calling
// Without this, the model doesn't know it already called a tool and loops forever
if (response.toolCalls && response.toolCalls.length > 0) {
historyMessage.tool_calls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
}));
}
this.messageHistory.push(historyMessage as unknown as plugins.smartai.ChatMessage);
// Convert Ollama tool calls to our format
let toolCalls: interfaces.INativeToolCall[] | undefined;
if (response.toolCalls && response.toolCalls.length > 0) {
toolCalls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
index: tc.function.index,
},
}));
}
return {
message: {
role: 'assistant',
content: response.message || '',
},
toolCalls,
};
}
/**
* Continue conversation with native tool calling support
* @param message The message to continue with (e.g., tool result)
* @param toolName Optional tool name - when provided, message is added as role: 'tool' instead of 'user'
* @returns Response with content, reasoning, and any tool calls
*/
public async continueWithNativeTools(
message: string,
toolName?: string
): Promise<{ message: interfaces.IAgentMessage; toolCalls?: interfaces.INativeToolCall[] }> {
// Add the new message to history
if (toolName) {
// Tool result - must use role: 'tool' for native tool calling
// The 'tool' role is supported by providers but not in the ChatMessage type
this.messageHistory.push({
role: 'tool',
content: message,
toolName: toolName,
} as unknown as plugins.smartai.ChatMessage);
} else {
// Regular user message
this.messageHistory.push({
role: 'user',
content: message,
});
}
// Build system message
const fullSystemMessage = this.getNativeToolsSystemMessage();
// Get tools in JSON schema format
const tools = this.getToolsAsJsonSchema();
// Get response from provider with history windowing
// For tool results, include the full history (with tool message)
// For regular user messages, exclude the last message (it becomes userMessage)
let historyForChat: plugins.smartai.ChatMessage[];
const fullHistory = toolName
? this.messageHistory // Include tool result in history
: this.messageHistory.slice(0, -1); // Exclude last user message
if (this.maxHistoryMessages > 0 && fullHistory.length > this.maxHistoryMessages) {
historyForChat = [
fullHistory[0],
...fullHistory.slice(-(this.maxHistoryMessages - 1)),
];
} else {
historyForChat = fullHistory;
}
// Check if provider supports native tool calling
const provider = this.provider as any;
if (typeof provider.collectStreamResponse !== 'function') {
throw new Error('Provider does not support native tool calling. Use continueWithMessage() instead.');
}
// For tool results, use a continuation prompt instead of repeating the result
const userMessage = toolName
? 'Continue with the task. The tool result has been provided above.'
: message;
// Use collectStreamResponse for streaming support with tools
const response = await provider.collectStreamResponse(
{
systemMessage: fullSystemMessage,
userMessage: userMessage,
messageHistory: historyForChat,
tools: tools.length > 0 ? tools : undefined,
},
// Pass onToken callback through onChunk for streaming with thinking markers
this.onToken ? (chunk: any) => {
if (chunk.thinking && this.onToken) {
// Add marker only when transitioning INTO thinking mode
if (!this.isInThinkingMode) {
this.onToken('\n[THINKING] ');
this.isInThinkingMode = true;
}
this.onToken(chunk.thinking);
}
if (chunk.content && this.onToken) {
// Add marker when transitioning OUT of thinking mode
if (this.isInThinkingMode) {
this.onToken('\n[OUTPUT] ');
this.isInThinkingMode = false;
}
this.onToken(chunk.content);
}
} : undefined
);
// Reset thinking state after response completes
this.isInThinkingMode = false;
// Add assistant response to history
const historyMessage: any = {
role: 'assistant',
content: response.message || '',
reasoning: response.thinking || response.reasoning,
};
// CRITICAL: Preserve tool_calls in history for native tool calling
// Without this, the model doesn't know it already called a tool and loops forever
if (response.toolCalls && response.toolCalls.length > 0) {
historyMessage.tool_calls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
}));
}
this.messageHistory.push(historyMessage as unknown as plugins.smartai.ChatMessage);
// Convert Ollama tool calls to our format
let toolCalls: interfaces.INativeToolCall[] | undefined;
if (response.toolCalls && response.toolCalls.length > 0) {
toolCalls = response.toolCalls.map((tc: any) => ({
function: {
name: tc.function.name,
arguments: tc.function.arguments,
index: tc.function.index,
},
}));
}
return {
message: {
role: 'assistant',
content: response.message || '',
},
toolCalls,
};
}
/**
* Get system message for native tool calling mode
* Simplified prompt that lets the model use tools naturally
*/
private getNativeToolsSystemMessage(): string {
return `You are an AI assistant that executes tasks by using available tools.
## Your Role
You analyze tasks, break them down into steps, and use tools to accomplish goals.
## Guidelines
1. Think step by step about what needs to be done
2. Use the available tools to complete the task
3. Process tool results and continue until the task is complete
4. When the task is complete, provide a final summary
## Important
- Use tools when needed to gather information or perform actions
- If you need clarification, ask the user
- Always verify your work before marking the task complete`;
}
/**
* Convert registered tools to Ollama JSON Schema format for native tool calling
* Each tool action becomes a separate function with name format: "toolName_actionName"
* @returns Array of IOllamaTool compatible tool definitions
*/
public getToolsAsJsonSchema(): plugins.smartai.IOllamaTool[] {
const tools: plugins.smartai.IOllamaTool[] = [];
for (const tool of this.tools.values()) {
for (const action of tool.actions) {
// Build the tool definition in Ollama format
const toolDef: plugins.smartai.IOllamaTool = {
type: 'function',
function: {
name: `${tool.name}_${action.name}`, // e.g., "json_validate"
description: `[${tool.name}] ${action.description}`,
parameters: action.parameters as plugins.smartai.IOllamaTool['function']['parameters'],
},
};
tools.push(toolDef);
}
}
return tools;
}
/**
* Parse native tool calls from provider response into IToolCallProposal format
* @param toolCalls Array of native tool calls from the provider
* @returns Array of IToolCallProposal ready for execution
*/
public parseNativeToolCalls(
toolCalls: interfaces.INativeToolCall[]
): interfaces.IToolCallProposal[] {
return toolCalls.map(tc => {
// Split "json_validate" -> toolName="json", action="validate"
const fullName = tc.function.name;
const underscoreIndex = fullName.indexOf('_');
let toolName: string;
let action: string;
if (underscoreIndex > 0) {
toolName = fullName.substring(0, underscoreIndex);
action = fullName.substring(underscoreIndex + 1);
} else {
// Fallback: treat entire name as tool name with empty action
toolName = fullName;
action = '';
}
return {
proposalId: this.generateProposalId(),
toolName,
action,
params: tc.function.arguments,
};
});
}
}

View File

@@ -23,27 +23,27 @@ export class DualAgentOrchestrator {
private tools: Map<string, BaseToolWrapper> = new Map();
private isRunning = false;
private conversationHistory: interfaces.IAgentMessage[] = [];
private ownsSmartAi = true; // true if we created the SmartAi instance, false if it was provided
constructor(options: interfaces.IDualAgentOptions) {
this.options = {
maxIterations: 20,
maxConsecutiveRejections: 3,
defaultProvider: 'openai',
maxResultChars: 15000,
maxHistoryMessages: 20,
...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);
// Use existing SmartAi instance if provided, otherwise create a new one
if (options.smartAiInstance) {
this.smartai = options.smartAiInstance;
this.ownsSmartAi = false; // Don't manage lifecycle of provided instance
} else {
this.smartai = new plugins.smartai.SmartAi(options);
this.ownsSmartAi = true;
}
// Note: Don't access providers here - they don't exist until start() is called
}
/**
@@ -70,13 +70,72 @@ export class DualAgentOrchestrator {
}
}
/**
* Emit a progress event if callback is configured
*/
private emitProgress(event: Omit<interfaces.IProgressEvent, 'timestamp' | 'logLevel' | 'logMessage'>): void {
if (this.options.onProgress) {
const prefix = this.options.logPrefix ? `${this.options.logPrefix} ` : '';
const { logLevel, logMessage } = this.formatProgressEvent(event, prefix);
this.options.onProgress({
...event,
timestamp: new Date(),
logLevel,
logMessage,
});
}
}
/**
* Format a progress event into a log level and message
*/
private formatProgressEvent(
event: Omit<interfaces.IProgressEvent, 'timestamp' | 'logLevel' | 'logMessage'>,
prefix: string
): { logLevel: interfaces.TLogLevel; logMessage: string } {
switch (event.type) {
case 'task_started':
return { logLevel: 'info', logMessage: `${prefix}Task started` };
case 'iteration_started':
return { logLevel: 'info', logMessage: `${prefix}Iteration ${event.iteration}/${event.maxIterations}` };
case 'tool_proposed':
return { logLevel: 'info', logMessage: `${prefix} → Proposing: ${event.toolName}.${event.action}` };
case 'guardian_evaluating':
return { logLevel: 'info', logMessage: `${prefix} ⏳ Guardian evaluating...` };
case 'tool_approved':
return { logLevel: 'info', logMessage: `${prefix} ✓ Approved: ${event.toolName}.${event.action}` };
case 'tool_rejected':
return { logLevel: 'warn', logMessage: `${prefix} ✗ Rejected: ${event.toolName}.${event.action} - ${event.reason}` };
case 'tool_executing':
return { logLevel: 'info', logMessage: `${prefix} ⚡ Executing: ${event.toolName}.${event.action}...` };
case 'tool_completed':
return { logLevel: 'info', logMessage: `${prefix} ✓ Completed: ${event.message}` };
case 'task_completed':
return { logLevel: 'success', logMessage: `${prefix}Task completed in ${event.iteration} iterations` };
case 'clarification_needed':
return { logLevel: 'warn', logMessage: `${prefix}Clarification needed from user` };
case 'max_iterations':
return { logLevel: 'error', logMessage: `${prefix}${event.message}` };
case 'max_rejections':
return { logLevel: 'error', logMessage: `${prefix}${event.message}` };
default:
return { logLevel: 'info', logMessage: `${prefix}${event.type}` };
}
}
/**
* Register a custom tool
*/
public registerTool(tool: BaseToolWrapper): void {
this.tools.set(tool.name, tool);
this.driver.registerTool(tool);
this.guardian.registerTool(tool);
// Register with agents if they exist (they're created in start())
if (this.driver) {
this.driver.registerTool(tool);
}
if (this.guardian) {
this.guardian.registerTool(tool);
}
}
/**
@@ -96,12 +155,49 @@ export class DualAgentOrchestrator {
}
}
/**
* Register a scoped filesystem tool that can only access files within the specified directory
* @param basePath The directory to scope filesystem operations to
* @param excludePatterns Optional glob patterns to exclude from listings (e.g., ['.nogit/**', 'node_modules/**'])
*/
public registerScopedFilesystemTool(basePath: string, excludePatterns?: string[]): void {
const scopedTool = new FilesystemTool({ basePath, excludePatterns });
this.registerTool(scopedTool);
}
/**
* Initialize all tools (eager loading)
*/
public async start(): Promise<void> {
// Start smartai
await this.smartai.start();
// Start smartai only if we created it (external instances should already be started)
if (this.ownsSmartAi) {
await this.smartai.start();
}
// NOW get providers (after they've been initialized by smartai.start())
this.driverProvider = this.getProviderByName(this.options.defaultProvider!);
this.guardianProvider = this.options.guardianProvider
? this.getProviderByName(this.options.guardianProvider)
: this.driverProvider;
// NOW create agents with initialized providers
// Set up token callback wrapper if streaming is enabled
const driverOnToken = this.options.onToken
? (token: string) => this.options.onToken!(token, 'driver')
: undefined;
this.driver = new DriverAgent(this.driverProvider, {
systemMessage: this.options.driverSystemMessage,
maxHistoryMessages: this.options.maxHistoryMessages,
onToken: driverOnToken,
});
this.guardian = new GuardianAgent(this.guardianProvider, this.options.guardianPolicyPrompt);
// Register any tools that were added before start() with the agents
for (const tool of this.tools.values()) {
this.driver.registerTool(tool);
this.guardian.registerTool(tool);
}
// Initialize all tools
const initPromises: Promise<void>[] = [];
@@ -124,35 +220,69 @@ export class DualAgentOrchestrator {
}
await Promise.all(cleanupPromises);
await this.smartai.stop();
// Only stop smartai if we created it (don't stop external instances)
if (this.ownsSmartAi) {
await this.smartai.stop();
}
this.isRunning = false;
this.driver.reset();
if (this.driver) {
this.driver.reset();
}
}
/**
* Run a task through the dual-agent system
* @param task The task description
* @param options Optional task run options (e.g., images for vision tasks)
*/
public async run(task: string): Promise<interfaces.IDualAgentRunResult> {
public async run(task: string, options?: interfaces.ITaskRunOptions): Promise<interfaces.IDualAgentRunResult> {
if (!this.isRunning) {
throw new Error('Orchestrator not started. Call start() first.');
}
// Use native tool calling if enabled
const useNativeTools = this.options.useNativeToolCalling === true;
this.conversationHistory = [];
let iterations = 0;
let consecutiveRejections = 0;
let completed = false;
let finalResult: string | null = null;
// Track pending native tool calls
let pendingNativeToolCalls: interfaces.INativeToolCall[] | undefined;
// Extract images from options
const images = options?.images;
// Add initial task to history
this.conversationHistory.push({
role: 'user',
content: task,
});
// Start the driver with the task
let driverResponse = await this.driver.startTask(task);
// Start the driver with the task and optional images
let driverResponse: interfaces.IAgentMessage;
if (useNativeTools) {
// Native tool calling mode
const result = await this.driver.startTaskWithNativeTools(task, images);
driverResponse = result.message;
pendingNativeToolCalls = result.toolCalls;
} else {
// XML parsing mode
driverResponse = await this.driver.startTask(task, images);
}
this.conversationHistory.push(driverResponse);
// Emit task started event
this.emitProgress({
type: 'task_started',
message: task.length > 100 ? task.substring(0, 100) + '...' : task,
});
while (
iterations < this.options.maxIterations! &&
consecutiveRejections < this.options.maxConsecutiveRejections! &&
@@ -160,15 +290,42 @@ export class DualAgentOrchestrator {
) {
iterations++;
// Check if task is complete
if (this.driver.isTaskComplete(driverResponse.content)) {
// Emit iteration started event
this.emitProgress({
type: 'iteration_started',
iteration: iterations,
maxIterations: this.options.maxIterations,
});
// Check if task is complete (for native mode, no pending tool calls and has content)
const isComplete = useNativeTools
? (!pendingNativeToolCalls || pendingNativeToolCalls.length === 0) && driverResponse.content.length > 0
: this.driver.isTaskComplete(driverResponse.content);
if (isComplete) {
completed = true;
finalResult = this.driver.extractTaskResult(driverResponse.content) || driverResponse.content;
finalResult = useNativeTools
? driverResponse.content
: (this.driver.extractTaskResult(driverResponse.content) || driverResponse.content);
// Emit task completed event
this.emitProgress({
type: 'task_completed',
iteration: iterations,
message: 'Task completed successfully',
});
break;
}
// Check if driver needs clarification
if (this.driver.needsClarification(driverResponse.content)) {
// Emit clarification needed event
this.emitProgress({
type: 'clarification_needed',
iteration: iterations,
message: 'Driver needs clarification from user',
});
// Return with clarification needed status
return {
success: false,
@@ -180,21 +337,70 @@ export class DualAgentOrchestrator {
};
}
// Parse tool call proposals
const proposals = this.driver.parseToolCallProposals(driverResponse.content);
// Parse tool call proposals - native mode uses pendingNativeToolCalls, XML mode parses content
let proposals: interfaces.IToolCallProposal[];
if (useNativeTools && pendingNativeToolCalls && pendingNativeToolCalls.length > 0) {
// Native tool calling mode - convert native tool calls to proposals
proposals = this.driver.parseNativeToolCalls(pendingNativeToolCalls);
pendingNativeToolCalls = undefined; // Clear after processing
} else if (!useNativeTools) {
// XML parsing mode
proposals = this.driver.parseToolCallProposals(driverResponse.content);
} else {
proposals = [];
}
if (proposals.length === 0) {
// No tool calls, continue the conversation
driverResponse = await this.driver.continueWithMessage(
'Please either use a tool to make progress on the task, or indicate that the task is complete with <task_complete>summary</task_complete>.'
);
this.conversationHistory.push(driverResponse);
continue;
if (useNativeTools) {
// Native mode: no tool calls and no content means we should continue
const result = await this.driver.continueWithNativeTools(
'Please continue with the task. Use the available tools or provide your final output.'
);
driverResponse = result.message;
pendingNativeToolCalls = result.toolCalls;
this.conversationHistory.push(driverResponse);
continue;
} else {
// XML mode: remind the model of the exact XML format
driverResponse = await this.driver.continueWithMessage(
`No valid tool call was found in your response. To use a tool, you MUST output the exact XML format:
<tool_call>
<tool>tool_name</tool>
<action>action_name</action>
<params>{"param1": "value1"}</params>
</tool_call>
For example, to validate JSON:
<tool_call>
<tool>json</tool>
<action>validate</action>
<params>{"jsonString": "{\\"key\\":\\"value\\"}", "requiredFields": ["key"]}</params>
</tool_call>
Or to complete the task:
<task_complete>your final JSON output here</task_complete>
Please output the exact XML format above.`
);
this.conversationHistory.push(driverResponse);
continue;
}
}
// Process the first proposal (one at a time)
const proposal = proposals[0];
// Emit tool proposed event
this.emitProgress({
type: 'tool_proposed',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
message: `${proposal.toolName}.${proposal.action}`,
});
// Quick validation first
const quickDecision = this.guardian.quickValidate(proposal);
let decision: interfaces.IGuardianDecision;
@@ -202,6 +408,14 @@ export class DualAgentOrchestrator {
if (quickDecision) {
decision = quickDecision;
} else {
// Emit guardian evaluating event
this.emitProgress({
type: 'guardian_evaluating',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
// Full AI evaluation
decision = await this.guardian.evaluate(proposal, task);
}
@@ -209,6 +423,14 @@ export class DualAgentOrchestrator {
if (decision.decision === 'approve') {
consecutiveRejections = 0;
// Emit tool approved event
this.emitProgress({
type: 'tool_approved',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
// Execute the tool
const tool = this.tools.get(proposal.toolName);
if (!tool) {
@@ -221,12 +443,48 @@ export class DualAgentOrchestrator {
}
try {
// Emit tool executing event
this.emitProgress({
type: 'tool_executing',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
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}`;
// Emit tool completed event
this.emitProgress({
type: 'tool_completed',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
message: result.success ? 'success' : result.error,
});
// Build result message (prefer summary if provided, otherwise stringify result)
let resultMessage: string;
if (result.success) {
if (result.summary) {
// Use tool-provided summary
resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${result.summary}`;
} else {
// Stringify and potentially truncate
const resultStr = JSON.stringify(result.result, null, 2);
const maxChars = this.options.maxResultChars ?? 15000;
if (maxChars > 0 && resultStr.length > maxChars) {
// Truncate the result
const truncated = resultStr.substring(0, maxChars);
const omittedTokens = Math.round((resultStr.length - maxChars) / 4);
resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${truncated}\n\n[... output truncated, ~${omittedTokens} tokens omitted. Use more specific parameters to reduce output size.]`;
} else {
resultMessage = `TOOL RESULT (${proposal.toolName}.${proposal.action}):\n${resultStr}`;
}
}
} else {
resultMessage = `TOOL ERROR (${proposal.toolName}.${proposal.action}):\n${result.error}`;
}
this.conversationHistory.push({
role: 'system',
@@ -235,19 +493,46 @@ export class DualAgentOrchestrator {
toolResult: result,
});
driverResponse = await this.driver.continueWithMessage(resultMessage);
// Continue with appropriate method based on mode
if (useNativeTools) {
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(resultMessage, toolNameForHistory);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(resultMessage);
}
this.conversationHistory.push(driverResponse);
} catch (error) {
const errorMessage = `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`;
driverResponse = await this.driver.continueWithMessage(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
);
if (useNativeTools) {
const toolNameForHistory = `${proposal.toolName}_${proposal.action}`;
const continueResult = await this.driver.continueWithNativeTools(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`,
toolNameForHistory
);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(
`TOOL ERROR: ${errorMessage}\n\nPlease try a different approach.`
);
}
this.conversationHistory.push(driverResponse);
}
} else {
// Rejected
consecutiveRejections++;
// Emit tool rejected event
this.emitProgress({
type: 'tool_rejected',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
reason: decision.reason,
});
// Build rejection feedback
let feedback = `TOOL CALL REJECTED by Guardian:\n`;
feedback += `- Reason: ${decision.reason}\n`;
@@ -269,7 +554,14 @@ export class DualAgentOrchestrator {
guardianDecision: decision,
});
driverResponse = await this.driver.continueWithMessage(feedback);
// Continue with appropriate method based on mode
if (useNativeTools) {
const continueResult = await this.driver.continueWithNativeTools(feedback);
driverResponse = continueResult.message;
pendingNativeToolCalls = continueResult.toolCalls;
} else {
driverResponse = await this.driver.continueWithMessage(feedback);
}
this.conversationHistory.push(driverResponse);
}
}
@@ -279,8 +571,21 @@ export class DualAgentOrchestrator {
if (!completed) {
if (iterations >= this.options.maxIterations!) {
status = 'max_iterations_reached';
// Emit max iterations event
this.emitProgress({
type: 'max_iterations',
iteration: iterations,
maxIterations: this.options.maxIterations,
message: `Maximum iterations (${this.options.maxIterations}) reached`,
});
} else if (consecutiveRejections >= this.options.maxConsecutiveRejections!) {
status = 'max_rejections_reached';
// Emit max rejections event
this.emitProgress({
type: 'max_rejections',
iteration: iterations,
message: `Maximum consecutive rejections (${this.options.maxConsecutiveRejections}) reached`,
});
}
}

View File

@@ -1,5 +1,17 @@
import * as plugins from './plugins.js';
// ================================
// Task Run Options
// ================================
/**
* Options for running a task with the DualAgentOrchestrator
*/
export interface ITaskRunOptions {
/** Base64-encoded images to include with the task (for vision-capable models) */
images?: string[];
}
// ================================
// Agent Configuration Interfaces
// ================================
@@ -8,6 +20,8 @@ import * as plugins from './plugins.js';
* Configuration options for the DualAgentOrchestrator
*/
export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
/** Existing SmartAi instance to reuse (avoids creating duplicate providers) */
smartAiInstance?: plugins.smartai.SmartAi;
/** Name of the agent system */
name?: string;
/** Default AI provider for both Driver and Guardian */
@@ -24,6 +38,22 @@ export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions {
maxConsecutiveRejections?: number;
/** Enable verbose logging */
verbose?: boolean;
/** Maximum characters for tool result output before truncation (default: 15000). Set to 0 to disable. */
maxResultChars?: number;
/** Maximum history messages to pass to API (default: 20). Set to 0 for unlimited. */
maxHistoryMessages?: number;
/** Optional callback for live progress updates during execution */
onProgress?: (event: IProgressEvent) => void;
/** Prefix for log messages (e.g., "[README]", "[Commit]"). Default: empty */
logPrefix?: string;
/** Callback fired for each token during LLM generation (streaming mode) */
onToken?: (token: string, source: 'driver' | 'guardian') => void;
/**
* Enable native tool calling mode (default: false)
* When enabled, uses Ollama's native tool calling API instead of XML parsing
* This is more efficient for models that support it (e.g., GPT-OSS with Harmony format)
*/
useNativeToolCalling?: boolean;
}
// ================================
@@ -59,6 +89,18 @@ export interface IToolAction {
parameters: Record<string, unknown>;
}
/**
* Native tool call from provider (matches Ollama's tool calling format)
* Format: function name is "toolName_actionName" (e.g., "json_validate")
*/
export interface INativeToolCall {
function: {
name: string; // Format: "toolName_actionName"
arguments: Record<string, unknown>;
index?: number;
};
}
/**
* Proposed tool call from the Driver
*/
@@ -82,6 +124,8 @@ export interface IToolExecutionResult {
success: boolean;
result?: unknown;
error?: string;
/** Optional human-readable summary for history (if provided, used instead of full result) */
summary?: string;
}
/**
@@ -193,6 +237,58 @@ export interface IDualAgentRunResult {
error?: string;
}
// ================================
// Progress Event Interfaces
// ================================
/**
* Progress event types for live feedback during agent execution
*/
export type TProgressEventType =
| 'task_started'
| 'iteration_started'
| 'tool_proposed'
| 'guardian_evaluating'
| 'tool_approved'
| 'tool_rejected'
| 'tool_executing'
| 'tool_completed'
| 'task_completed'
| 'clarification_needed'
| 'max_iterations'
| 'max_rejections';
/**
* Log level for progress events
*/
export type TLogLevel = 'info' | 'warn' | 'error' | 'success';
/**
* Progress event for live feedback during agent execution
*/
export interface IProgressEvent {
/** Type of progress event */
type: TProgressEventType;
/** Current iteration number */
iteration?: number;
/** Maximum iterations configured */
maxIterations?: number;
/** Name of the tool being used */
toolName?: string;
/** Action being performed */
action?: string;
/** Reason for rejection or other explanation */
reason?: string;
/** Human-readable message about the event */
message?: string;
/** Timestamp of the event */
timestamp: Date;
/** Log level for this event (info, warn, error, success) */
logLevel: TLogLevel;
/** Pre-formatted log message ready for output */
logMessage: string;
}
// ================================
// Utility Types
// ================================

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,16 @@ import * as plugins from './plugins.js';
import * as interfaces from './smartagent.interfaces.js';
import { BaseToolWrapper } from './smartagent.tools.base.js';
/**
* Options for FilesystemTool
*/
export interface IFilesystemToolOptions {
/** Base path to scope all operations to. If set, all paths must be within this directory. */
basePath?: string;
/** Glob patterns to exclude from listings (e.g., ['.nogit/**', 'node_modules/**']) */
excludePatterns?: string[];
}
/**
* Filesystem tool for file and directory operations
* Wraps @push.rocks/smartfs
@@ -10,20 +20,66 @@ export class FilesystemTool extends BaseToolWrapper {
public name = 'filesystem';
public description = 'Read, write, list, and delete files and directories';
/** Base path to scope all operations to */
private basePath?: string;
/** Glob patterns to exclude from listings */
private excludePatterns: string[];
constructor(options?: IFilesystemToolOptions) {
super();
if (options?.basePath) {
this.basePath = plugins.path.resolve(options.basePath);
}
this.excludePatterns = options?.excludePatterns || [];
}
/**
* Check if a relative path should be excluded based on exclude patterns
*/
private isExcluded(relativePath: string): boolean {
if (this.excludePatterns.length === 0) return false;
return this.excludePatterns.some(pattern =>
plugins.minimatch(relativePath, pattern, { dot: true })
);
}
/**
* Validate that a path is within the allowed base path
* @throws Error if path is outside allowed directory
*/
private validatePath(pathArg: string): string {
const resolved = plugins.path.resolve(pathArg);
if (this.basePath) {
// Ensure the resolved path starts with the base path
if (!resolved.startsWith(this.basePath + plugins.path.sep) && resolved !== this.basePath) {
throw new Error(`Access denied: path "${pathArg}" is outside allowed directory "${this.basePath}"`);
}
}
return resolved;
}
public actions: interfaces.IToolAction[] = [
{
name: 'read',
description: 'Read the contents of a file',
description: 'Read file contents (full or specific line range)',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
path: { type: 'string', description: 'Path to the file' },
encoding: {
type: 'string',
enum: ['utf8', 'binary', 'base64'],
default: 'utf8',
description: 'File encoding',
},
startLine: {
type: 'number',
description: 'First line to read (1-indexed, inclusive). If omitted, reads from beginning.',
},
endLine: {
type: 'number',
description: 'Last line to read (1-indexed, inclusive). If omitted, reads to end.',
},
},
required: ['path'],
},
@@ -149,6 +205,55 @@ export class FilesystemTool extends BaseToolWrapper {
required: ['path'],
},
},
{
name: 'tree',
description: 'Show directory structure as a tree (no file contents)',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Root directory path' },
maxDepth: {
type: 'number',
default: 3,
description: 'Maximum depth to traverse (default: 3)',
},
filter: {
type: 'string',
description: 'Glob pattern to filter files (e.g., "*.ts")',
},
showSizes: {
type: 'boolean',
default: false,
description: 'Include file sizes in output',
},
format: {
type: 'string',
enum: ['string', 'json'],
default: 'string',
description: 'Output format: "string" for human-readable tree, "json" for structured array',
},
},
required: ['path'],
},
},
{
name: 'glob',
description: 'Find files matching a glob pattern',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.js")',
},
path: {
type: 'string',
description: 'Base path to search from (defaults to current directory)',
},
},
required: ['pattern'],
},
},
];
private smartfs!: plugins.smartfs.SmartFs;
@@ -172,25 +277,72 @@ export class FilesystemTool extends BaseToolWrapper {
try {
switch (action) {
case 'read': {
const validatedPath = this.validatePath(params.path as string);
const encoding = (params.encoding as string) || 'utf8';
const content = await this.smartfs
.file(params.path as string)
const startLine = params.startLine as number | undefined;
const endLine = params.endLine as number | undefined;
const fullContent = await this.smartfs
.file(validatedPath)
.encoding(encoding as 'utf8' | 'binary' | 'base64')
.read();
const contentStr = fullContent.toString();
const lines = contentStr.split('\n');
const totalLines = lines.length;
// Apply line range if specified
let resultContent: string;
let resultStartLine = 1;
let resultEndLine = totalLines;
if (startLine !== undefined || endLine !== undefined) {
const start = Math.max(1, startLine ?? 1);
const end = Math.min(totalLines, endLine ?? totalLines);
resultStartLine = start;
resultEndLine = end;
// Convert to 0-indexed for array slicing
const selectedLines = lines.slice(start - 1, end);
// Add line numbers to output for context
resultContent = selectedLines
.map((line, idx) => `${String(start + idx).padStart(5)}${line}`)
.join('\n');
} else {
// No range specified - return full content but warn if large
const MAX_LINES_WITHOUT_RANGE = 500;
if (totalLines > MAX_LINES_WITHOUT_RANGE) {
// Return first portion with warning
const selectedLines = lines.slice(0, MAX_LINES_WITHOUT_RANGE);
resultContent = selectedLines
.map((line, idx) => `${String(idx + 1).padStart(5)}${line}`)
.join('\n');
resultContent += `\n\n[... ${totalLines - MAX_LINES_WITHOUT_RANGE} more lines. Use startLine/endLine to read specific ranges.]`;
resultEndLine = MAX_LINES_WITHOUT_RANGE;
} else {
resultContent = contentStr;
}
}
return {
success: true,
result: {
path: params.path,
content: content.toString(),
content: resultContent,
encoding,
totalLines,
startLine: resultStartLine,
endLine: resultEndLine,
},
};
}
case 'write': {
const validatedPath = this.validatePath(params.path as string);
const encoding = (params.encoding as string) || 'utf8';
await this.smartfs
.file(params.path as string)
.file(validatedPath)
.encoding(encoding as 'utf8' | 'binary' | 'base64')
.write(params.content as string);
return {
@@ -204,7 +356,8 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'append': {
await this.smartfs.file(params.path as string).append(params.content as string);
const validatedPath = this.validatePath(params.path as string);
await this.smartfs.file(validatedPath).append(params.content as string);
return {
success: true,
result: {
@@ -215,14 +368,24 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'list': {
let dir = this.smartfs.directory(params.path as string);
const validatedPath = this.validatePath(params.path as string);
let dir = this.smartfs.directory(validatedPath);
if (params.recursive) {
dir = dir.recursive();
}
if (params.filter) {
dir = dir.filter(params.filter as string);
}
const entries = await dir.list();
let entries = await dir.list();
// Filter out excluded paths
if (this.excludePatterns.length > 0) {
entries = entries.filter(entry => {
const relativePath = plugins.path.relative(validatedPath, entry.path);
return !this.isExcluded(relativePath) && !this.isExcluded(entry.name);
});
}
return {
success: true,
result: {
@@ -234,33 +397,34 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'delete': {
const path = params.path as string;
const validatedPath = this.validatePath(params.path as string);
// Check if it's a directory or file
const exists = await this.smartfs.file(path).exists();
const exists = await this.smartfs.file(validatedPath).exists();
if (exists) {
// Try to get stats to check if it's a directory
try {
const stats = await this.smartfs.file(path).stat();
const stats = await this.smartfs.file(validatedPath).stat();
if (stats.isDirectory && params.recursive) {
await this.smartfs.directory(path).recursive().delete();
await this.smartfs.directory(validatedPath).recursive().delete();
} else {
await this.smartfs.file(path).delete();
await this.smartfs.file(validatedPath).delete();
}
} catch {
await this.smartfs.file(path).delete();
await this.smartfs.file(validatedPath).delete();
}
}
return {
success: true,
result: {
path,
path: params.path,
deleted: true,
},
};
}
case 'exists': {
const exists = await this.smartfs.file(params.path as string).exists();
const validatedPath = this.validatePath(params.path as string);
const exists = await this.smartfs.file(validatedPath).exists();
return {
success: true,
result: {
@@ -271,7 +435,8 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'stat': {
const stats = await this.smartfs.file(params.path as string).stat();
const validatedPath = this.validatePath(params.path as string);
const stats = await this.smartfs.file(validatedPath).stat();
return {
success: true,
result: {
@@ -282,7 +447,9 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'copy': {
await this.smartfs.file(params.source as string).copy(params.destination as string);
const validatedSource = this.validatePath(params.source as string);
const validatedDest = this.validatePath(params.destination as string);
await this.smartfs.file(validatedSource).copy(validatedDest);
return {
success: true,
result: {
@@ -294,7 +461,9 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'move': {
await this.smartfs.file(params.source as string).move(params.destination as string);
const validatedSource = this.validatePath(params.source as string);
const validatedDest = this.validatePath(params.destination as string);
await this.smartfs.file(validatedSource).move(validatedDest);
return {
success: true,
result: {
@@ -306,7 +475,8 @@ export class FilesystemTool extends BaseToolWrapper {
}
case 'mkdir': {
let dir = this.smartfs.directory(params.path as string);
const validatedPath = this.validatePath(params.path as string);
let dir = this.smartfs.directory(validatedPath);
if (params.recursive !== false) {
dir = dir.recursive();
}
@@ -320,6 +490,168 @@ export class FilesystemTool extends BaseToolWrapper {
};
}
case 'tree': {
const validatedPath = this.validatePath(params.path as string);
const maxDepth = (params.maxDepth as number) ?? 3;
const filter = params.filter as string | undefined;
const showSizes = (params.showSizes as boolean) ?? false;
const format = (params.format as 'string' | 'json') ?? 'string';
// Collect all entries recursively up to maxDepth
interface ITreeEntry {
path: string;
relativePath: string;
isDir: boolean;
depth: number;
size?: number;
}
const entries: ITreeEntry[] = [];
const collectEntries = async (dirPath: string, depth: number, relativePath: string) => {
if (depth > maxDepth) return;
let dir = this.smartfs.directory(dirPath);
if (filter) {
dir = dir.filter(filter);
}
const items = await dir.list();
for (const item of items) {
// item is IDirectoryEntry with name, path, isFile, isDirectory properties
const itemPath = item.path;
const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
const isDir = item.isDirectory;
// Skip excluded paths
if (this.isExcluded(itemRelPath) || this.isExcluded(item.name)) {
continue;
}
const entry: ITreeEntry = {
path: itemPath,
relativePath: itemRelPath,
isDir,
depth,
};
if (showSizes && !isDir && item.stats) {
entry.size = item.stats.size;
}
entries.push(entry);
// Recurse into directories
if (isDir && depth < maxDepth) {
await collectEntries(itemPath, depth + 1, itemRelPath);
}
}
};
await collectEntries(validatedPath, 0, '');
// Sort entries by path for consistent output
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
if (format === 'json') {
return {
success: true,
result: {
path: params.path,
entries: entries.map((e) => ({
path: e.relativePath,
isDir: e.isDir,
depth: e.depth,
...(e.size !== undefined ? { size: e.size } : {}),
})),
count: entries.length,
},
};
}
// Format as string tree
const formatSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
};
// Build tree string with proper indentation
let treeStr = `${params.path}/\n`;
const pathParts = new Map<string, number>(); // Track which paths are last in their parent
// Group by parent to determine last child
const parentChildCount = new Map<string, number>();
const parentCurrentChild = new Map<string, number>();
for (const entry of entries) {
const parentPath = entry.relativePath.includes('/')
? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
: '';
parentChildCount.set(parentPath, (parentChildCount.get(parentPath) || 0) + 1);
}
for (const entry of entries) {
const parentPath = entry.relativePath.includes('/')
? entry.relativePath.substring(0, entry.relativePath.lastIndexOf('/'))
: '';
parentCurrentChild.set(parentPath, (parentCurrentChild.get(parentPath) || 0) + 1);
const isLast = parentCurrentChild.get(parentPath) === parentChildCount.get(parentPath);
// Build prefix based on depth
let prefix = '';
const parts = entry.relativePath.split('/');
for (let i = 0; i < parts.length - 1; i++) {
prefix += '│ ';
}
prefix += isLast ? '└── ' : '├── ';
const name = parts[parts.length - 1];
const suffix = entry.isDir ? '/' : '';
const sizeStr = showSizes && entry.size !== undefined ? ` (${formatSize(entry.size)})` : '';
treeStr += `${prefix}${name}${suffix}${sizeStr}\n`;
}
return {
success: true,
result: {
path: params.path,
tree: treeStr,
count: entries.length,
},
};
}
case 'glob': {
const pattern = params.pattern as string;
const basePath = params.path ? this.validatePath(params.path as string) : (this.basePath || process.cwd());
// Use smartfs to list with filter
const dir = this.smartfs.directory(basePath).recursive().filter(pattern);
const matches = await dir.list();
// Return file paths relative to base path for readability
// Filter out excluded paths
const files = matches
.map((entry) => ({
path: entry.path,
relativePath: plugins.path.relative(basePath, entry.path),
isDirectory: entry.isDirectory,
}))
.filter((file) => !this.isExcluded(file.relativePath));
return {
success: true,
result: {
pattern,
basePath,
files,
count: files.length,
},
};
}
default:
return {
success: false,
@@ -334,10 +666,178 @@ export class FilesystemTool extends BaseToolWrapper {
}
}
public getToolExplanation(): string {
return `## Tool: filesystem
Read, write, list, and delete files and directories.
### Actions:
**read** - Read file contents (full or specific line range)
Parameters:
- path (required): Path to the file
- encoding (optional): File encoding - "utf8" (default), "binary", or "base64"
- startLine (optional): First line to read (1-indexed, inclusive)
- endLine (optional): Last line to read (1-indexed, inclusive)
Example:
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/path/to/file.txt"}</params>
</tool_call>
Example with line range:
<tool_call>
<tool>filesystem</tool>
<action>read</action>
<params>{"path": "/path/to/file.txt", "startLine": 10, "endLine": 20}</params>
</tool_call>
**write** - Write content to a file (creates or overwrites)
Parameters:
- path (required): Absolute path to the file
- content (required): Content to write
- encoding (optional): File encoding - "utf8" (default), "binary", or "base64"
Example:
<tool_call>
<tool>filesystem</tool>
<action>write</action>
<params>{"path": "/path/to/output.txt", "content": "Hello, World!"}</params>
</tool_call>
**list** - List files and directories in a path
Parameters:
- path (required): Directory path to list
- recursive (optional): List recursively (default: false)
- filter (optional): Glob pattern to filter results (e.g., "*.ts")
Example:
<tool_call>
<tool>filesystem</tool>
<action>list</action>
<params>{"path": "/path/to/dir", "recursive": true, "filter": "*.ts"}</params>
</tool_call>
**exists** - Check if a file or directory exists
Parameters:
- path (required): Path to check
Example:
<tool_call>
<tool>filesystem</tool>
<action>exists</action>
<params>{"path": "/path/to/check"}</params>
</tool_call>
**mkdir** - Create a directory
Parameters:
- path (required): Directory path to create
- recursive (optional): Create parent directories if needed (default: true)
Example:
<tool_call>
<tool>filesystem</tool>
<action>mkdir</action>
<params>{"path": "/path/to/new/dir"}</params>
</tool_call>
**delete** - Delete a file or directory
Parameters:
- path (required): Path to delete
- recursive (optional): For directories, delete recursively (default: false)
Example:
<tool_call>
<tool>filesystem</tool>
<action>delete</action>
<params>{"path": "/path/to/delete", "recursive": true}</params>
</tool_call>
**copy** - Copy a file to a new location
Parameters:
- source (required): Source file path
- destination (required): Destination file path
Example:
<tool_call>
<tool>filesystem</tool>
<action>copy</action>
<params>{"source": "/path/to/source.txt", "destination": "/path/to/dest.txt"}</params>
</tool_call>
**move** - Move a file to a new location
Parameters:
- source (required): Source file path
- destination (required): Destination file path
Example:
<tool_call>
<tool>filesystem</tool>
<action>move</action>
<params>{"source": "/path/to/old.txt", "destination": "/path/to/new.txt"}</params>
</tool_call>
**stat** - Get file or directory statistics (size, dates, etc.)
Parameters:
- path (required): Path to get stats for
Example:
<tool_call>
<tool>filesystem</tool>
<action>stat</action>
<params>{"path": "/path/to/file.txt"}</params>
</tool_call>
**append** - Append content to a file
Parameters:
- path (required): Absolute path to the file
- content (required): Content to append
Example:
<tool_call>
<tool>filesystem</tool>
<action>append</action>
<params>{"path": "/path/to/log.txt", "content": "New log entry\\n"}</params>
</tool_call>
**tree** - Show directory structure as a tree
Parameters:
- path (required): Root directory path
- maxDepth (optional): Maximum depth to traverse (default: 3)
- filter (optional): Glob pattern to filter files
- showSizes (optional): Include file sizes in output (default: false)
- format (optional): Output format - "string" (default) or "json"
Example:
<tool_call>
<tool>filesystem</tool>
<action>tree</action>
<params>{"path": "/path/to/dir", "maxDepth": 2}</params>
</tool_call>
**glob** - Find files matching a glob pattern
Parameters:
- pattern (required): Glob pattern (e.g., "**/*.ts", "src/**/*.js")
- path (optional): Base path to search from
Example:
<tool_call>
<tool>filesystem</tool>
<action>glob</action>
<params>{"pattern": "**/*.ts", "path": "/path/to/project"}</params>
</tool_call>
`;
}
public getCallSummary(action: string, params: Record<string, unknown>): string {
switch (action) {
case 'read':
return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`;
case 'read': {
const lineRange = params.startLine || params.endLine
? ` lines ${params.startLine || 1}-${params.endLine || 'end'}`
: '';
return `Read file "${params.path}"${lineRange}`;
}
case 'write': {
const content = params.content as string;
@@ -372,6 +872,12 @@ export class FilesystemTool extends BaseToolWrapper {
case 'mkdir':
return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`;
case 'tree':
return `Show tree of "${params.path}" (depth: ${params.maxDepth ?? 3}, format: ${params.format ?? 'string'})`;
case 'glob':
return `Find files matching "${params.pattern}"${params.path ? ` in "${params.path}"` : ''}`;
default:
return `Unknown action: ${action}`;
}

View File

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

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

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

View File

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