From e7968a31b1ad7a46cca6e3f70e4c5d46d31c4c15 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 20 Jan 2026 01:12:03 +0000 Subject: [PATCH] feat(smartagent): add JsonValidatorTool and support passing base64-encoded images with task runs (vision-capable models); bump @push.rocks/smartai to ^0.12.0 --- changelog.md | 9 ++ package.json | 2 +- pnpm-lock.yaml | 28 +--- ts/00_commitinfo_data.ts | 2 +- ts/index.ts | 1 + ts/smartagent.classes.driveragent.ts | 13 +- ts/smartagent.classes.dualagent.ts | 11 +- ts/smartagent.interfaces.ts | 12 ++ ts/smartagent.tools.json.ts | 193 +++++++++++++++++++++++++++ 9 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 ts/smartagent.tools.json.ts diff --git a/changelog.md b/changelog.md index 119495b..5d9440c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 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 diff --git a/package.json b/package.json index 94b477f..5fd1161 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/node": "^25.0.2" }, "dependencies": { - "@push.rocks/smartai": "^0.11.1", + "@push.rocks/smartai": "^0.12.0", "@push.rocks/smartbrowser": "^2.0.8", "@push.rocks/smartdeno": "^1.2.0", "@push.rocks/smartfs": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3357c5e..0eb7341 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@push.rocks/smartai': - specifier: ^0.11.1 - version: 0.11.1(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76) + specifier: ^0.12.0 + version: 0.12.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76) '@push.rocks/smartbrowser': specifier: ^2.0.8 version: 2.0.8(typescript@5.9.3) @@ -243,10 +243,6 @@ 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==} @@ -271,9 +267,6 @@ 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==} @@ -844,8 +837,8 @@ packages: '@push.rocks/qenv@6.1.3': resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} - '@push.rocks/smartai@0.11.1': - resolution: {integrity: sha512-GPOfy4h0ItHfQabfpmzmZYiSYAtXHpGdY6IJkjyW+IN10sgMQEbCpr/2u7MkgEDJHJYQZ49BWZH7/7GdtSxwrg==} + '@push.rocks/smartai@0.12.0': + resolution: {integrity: sha512-T4HRaSSxO6TQGGXlQeswX2eYkB+gMu0FbKF9qCUri6FdRlYzmPDn19jgPrPJxyg5m3oj6TzflvfYwcBCFlWo/A==} '@push.rocks/smartarchive@4.2.4': resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} @@ -4338,8 +4331,6 @@ snapshots: '@babel/runtime@7.28.4': {} - '@babel/runtime@7.28.6': {} - '@borewit/text-codec@0.1.1': {} '@cloudflare/workers-types@4.20251202.0': {} @@ -4404,11 +4395,6 @@ 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 @@ -4696,7 +4682,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -5172,7 +5158,7 @@ snapshots: '@push.rocks/smartlog': 3.1.10 '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartai@0.11.1(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)': + '@push.rocks/smartai@0.12.0(typescript@5.9.3)(ws@8.18.3)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@3.25.76) '@mistralai/mistralai': 1.12.0 @@ -7602,7 +7588,7 @@ snapshots: json-schema-to-ts@3.1.1: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.4 ts-algebra: 2.0.0 jsonfile@6.2.0: diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c98d50c..a714aae 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartagent', - version: '1.2.7', + version: '1.3.0', description: 'an agentic framework built on top of @push.rocks/smartai' } diff --git a/ts/index.ts b/ts/index.ts index 0880afb..1bf53a7 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -16,6 +16,7 @@ export { HttpTool } from './smartagent.tools.http.js'; export { ShellTool } from './smartagent.tools.shell.js'; export { BrowserTool } from './smartagent.tools.browser.js'; export { DenoTool, type TDenoPermission } from './smartagent.tools.deno.js'; +export { JsonValidatorTool } from './smartagent.tools.json.js'; // Export all interfaces export * from './smartagent.interfaces.js'; diff --git a/ts/smartagent.classes.driveragent.ts b/ts/smartagent.classes.driveragent.ts index d8ffecd..825f82d 100644 --- a/ts/smartagent.classes.driveragent.ts +++ b/ts/smartagent.classes.driveragent.ts @@ -67,8 +67,10 @@ export class DriverAgent { /** * Initialize a new conversation for a task + * @param task The task description + * @param images Optional base64-encoded images for vision tasks */ - public async startTask(task: string): Promise { + public async startTask(task: string, images?: string[]): Promise { // Reset message history this.messageHistory = []; @@ -106,6 +108,7 @@ export class DriverAgent { systemMessage: fullSystemMessage, userMessage: userMessage, messageHistory: [], + images: images, onToken: this.onToken, }); } else { @@ -114,14 +117,16 @@ export class DriverAgent { systemMessage: fullSystemMessage, userMessage: userMessage, messageHistory: [], + images: images, }); } - // Add assistant response to history - this.messageHistory.push({ + // Add assistant response to history (store images if provided) + const historyMessage: plugins.smartai.ChatMessage = { role: 'assistant', content: response.message, - }); + }; + this.messageHistory.push(historyMessage); return { role: 'assistant', diff --git a/ts/smartagent.classes.dualagent.ts b/ts/smartagent.classes.dualagent.ts index f9da5a6..070b169 100644 --- a/ts/smartagent.classes.dualagent.ts +++ b/ts/smartagent.classes.dualagent.ts @@ -234,8 +234,10 @@ export class DualAgentOrchestrator { /** * Run a task through the dual-agent system + * @param task The task description + * @param options Optional task run options (e.g., images for vision tasks) */ - public async run(task: string): Promise { + public async run(task: string, options?: interfaces.ITaskRunOptions): Promise { if (!this.isRunning) { throw new Error('Orchestrator not started. Call start() first.'); } @@ -246,14 +248,17 @@ export class DualAgentOrchestrator { let completed = false; let finalResult: string | null = null; + // Extract images from options + const images = options?.images; + // Add initial task to history this.conversationHistory.push({ role: 'user', content: task, }); - // Start the driver with the task - let driverResponse = await this.driver.startTask(task); + // Start the driver with the task and optional images + let driverResponse = await this.driver.startTask(task, images); this.conversationHistory.push(driverResponse); // Emit task started event diff --git a/ts/smartagent.interfaces.ts b/ts/smartagent.interfaces.ts index 27d9293..1e9af0e 100644 --- a/ts/smartagent.interfaces.ts +++ b/ts/smartagent.interfaces.ts @@ -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 // ================================ diff --git a/ts/smartagent.tools.json.ts b/ts/smartagent.tools.json.ts new file mode 100644 index 0000000..496770a --- /dev/null +++ b/ts/smartagent.tools.json.ts @@ -0,0 +1,193 @@ +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 { + this.isInitialized = true; + } + + async cleanup(): Promise { + this.isInitialized = false; + } + + async execute( + action: string, + params: Record + ): Promise { + 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): 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): 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}`, + }; + } + } + + getCallSummary(action: string, params: Record): 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}`; + } + } +}