BREAKING CHANGE(api): Migrate public API to ai-sdk v6 and refactor core agent architecture: replace class-based DualAgent/Driver/Guardian with a single runAgent function; introduce ts_tools factories for tools, a compactMessages compaction subpath, and truncateOutput utility; simplify ToolRegistry to return ToolSet and remove legacy BaseToolWrapper/tool classes; update package exports and dependencies and bump major version.

This commit is contained in:
2026-03-06 11:39:01 +00:00
parent 903de44644
commit f9a9c9fb48
36 changed files with 3928 additions and 6586 deletions

8
ts_tools/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export { filesystemTool } from './tool.filesystem.js';
export type { IFilesystemToolOptions } from './tool.filesystem.js';
export { shellTool } from './tool.shell.js';
export type { IShellToolOptions } from './tool.shell.js';
export { httpTool } from './tool.http.js';
export { jsonTool } from './tool.json.js';
export { truncateOutput } from './plugins.js';
export type { ITruncateResult } from './plugins.js';

30
ts_tools/plugins.ts Normal file
View File

@@ -0,0 +1,30 @@
// node native
import * as path from 'path';
import * as fs from 'fs';
export { path, fs };
// zod
import { z } from 'zod';
export { z };
// ai-sdk
import { tool } from '@push.rocks/smartai';
export { tool };
export type { ToolSet } from 'ai';
// @push.rocks scope
import * as smartfs from '@push.rocks/smartfs';
import * as smartshell from '@push.rocks/smartshell';
import * as smartrequest from '@push.rocks/smartrequest';
export { smartfs, smartshell, smartrequest };
// cross-folder import
import { truncateOutput } from '../ts/smartagent.utils.truncation.js';
export { truncateOutput };
export type { ITruncateResult } from '../ts/smartagent.utils.truncation.js';

131
ts_tools/tool.filesystem.ts Normal file
View File

@@ -0,0 +1,131 @@
import * as plugins from './plugins.js';
export interface IFilesystemToolOptions {
/** Restrict file access to this directory. Default: process.cwd() */
rootDir?: string;
}
function validatePath(filePath: string, rootDir?: string): string {
const resolved = plugins.path.resolve(filePath);
if (rootDir) {
const resolvedRoot = plugins.path.resolve(rootDir);
if (!resolved.startsWith(resolvedRoot + plugins.path.sep) && resolved !== resolvedRoot) {
throw new Error(`Access denied: "${filePath}" is outside allowed root "${rootDir}"`);
}
}
return resolved;
}
export function filesystemTool(options?: IFilesystemToolOptions): plugins.ToolSet {
const rootDir = options?.rootDir;
return {
read_file: plugins.tool({
description:
'Read file contents. Returns the full text or a specified line range.',
inputSchema: plugins.z.object({
path: plugins.z.string().describe('Absolute path to the file'),
startLine: plugins.z
.number()
.optional()
.describe('First line (1-indexed, inclusive)'),
endLine: plugins.z
.number()
.optional()
.describe('Last line (1-indexed, inclusive)'),
}),
execute: async ({
path: filePath,
startLine,
endLine,
}: {
path: string;
startLine?: number;
endLine?: number;
}) => {
const resolved = validatePath(filePath, rootDir);
const content = plugins.fs.readFileSync(resolved, 'utf-8');
if (startLine !== undefined || endLine !== undefined) {
const lines = content.split('\n');
const start = (startLine ?? 1) - 1;
const end = endLine ?? lines.length;
const sliced = lines.slice(start, end).join('\n');
return plugins.truncateOutput(sliced).content;
}
return plugins.truncateOutput(content).content;
},
}),
write_file: plugins.tool({
description:
'Write content to a file (creates parent dirs if needed, overwrites existing).',
inputSchema: plugins.z.object({
path: plugins.z.string().describe('Absolute path to the file'),
content: plugins.z.string().describe('Content to write'),
}),
execute: async ({ path: filePath, content }: { path: string; content: string }) => {
const resolved = validatePath(filePath, rootDir);
const dir = plugins.path.dirname(resolved);
plugins.fs.mkdirSync(dir, { recursive: true });
plugins.fs.writeFileSync(resolved, content, 'utf-8');
return `Written ${content.length} characters to ${filePath}`;
},
}),
list_directory: plugins.tool({
description: 'List files and directories at the given path.',
inputSchema: plugins.z.object({
path: plugins.z.string().describe('Directory path to list'),
recursive: plugins.z
.boolean()
.optional()
.describe('List recursively (default: false)'),
}),
execute: async ({
path: dirPath,
recursive,
}: {
path: string;
recursive?: boolean;
}) => {
const resolved = validatePath(dirPath, rootDir);
function listDir(dir: string, prefix: string = ''): string[] {
const entries = plugins.fs.readdirSync(dir, { withFileTypes: true });
const result: string[] = [];
for (const entry of entries) {
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
const indicator = entry.isDirectory() ? '/' : '';
result.push(`${rel}${indicator}`);
if (recursive && entry.isDirectory()) {
result.push(...listDir(plugins.path.join(dir, entry.name), rel));
}
}
return result;
}
const entries = listDir(resolved);
return plugins.truncateOutput(entries.join('\n')).content;
},
}),
delete_file: plugins.tool({
description: 'Delete a file or empty directory.',
inputSchema: plugins.z.object({
path: plugins.z.string().describe('Path to delete'),
}),
execute: async ({ path: filePath }: { path: string }) => {
const resolved = validatePath(filePath, rootDir);
const stat = plugins.fs.statSync(resolved);
if (stat.isDirectory()) {
plugins.fs.rmdirSync(resolved);
} else {
plugins.fs.unlinkSync(resolved);
}
return `Deleted ${filePath}`;
},
}),
};
}

78
ts_tools/tool.http.ts Normal file
View File

@@ -0,0 +1,78 @@
import * as plugins from './plugins.js';
export function httpTool(): plugins.ToolSet {
return {
http_get: plugins.tool({
description: 'Make an HTTP GET request and return the response.',
inputSchema: plugins.z.object({
url: plugins.z.string().describe('URL to request'),
headers: plugins.z
.record(plugins.z.string())
.optional()
.describe('Request headers'),
}),
execute: async ({
url,
headers,
}: {
url: string;
headers?: Record<string, string>;
}) => {
let req = plugins.smartrequest.default.create().url(url);
if (headers) {
req = req.headers(headers);
}
const response = await req.get();
let body: string;
try {
const json = await response.json();
body = JSON.stringify(json, null, 2);
} catch {
body = await response.text();
}
return plugins.truncateOutput(`HTTP ${response.status}\n${body}`).content;
},
}),
http_post: plugins.tool({
description: 'Make an HTTP POST request with a JSON body.',
inputSchema: plugins.z.object({
url: plugins.z.string().describe('URL to request'),
body: plugins.z
.record(plugins.z.unknown())
.optional()
.describe('JSON body to send'),
headers: plugins.z
.record(plugins.z.string())
.optional()
.describe('Request headers'),
}),
execute: async ({
url,
body,
headers,
}: {
url: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
}) => {
let req = plugins.smartrequest.default.create().url(url);
if (headers) {
req = req.headers(headers);
}
if (body) {
req = req.json(body);
}
const response = await req.post();
let responseBody: string;
try {
const json = await response.json();
responseBody = JSON.stringify(json, null, 2);
} catch {
responseBody = await response.text();
}
return plugins.truncateOutput(`HTTP ${response.status}\n${responseBody}`).content;
},
}),
};
}

53
ts_tools/tool.json.ts Normal file
View File

@@ -0,0 +1,53 @@
import * as plugins from './plugins.js';
export function jsonTool(): plugins.ToolSet {
return {
json_validate: plugins.tool({
description:
'Validate a JSON string and optionally check for required fields.',
inputSchema: plugins.z.object({
jsonString: plugins.z.string().describe('JSON string to validate'),
requiredFields: plugins.z
.array(plugins.z.string())
.optional()
.describe('Fields that must exist at the top level'),
}),
execute: async ({
jsonString,
requiredFields,
}: {
jsonString: string;
requiredFields?: string[];
}) => {
try {
const parsed = JSON.parse(jsonString);
if (requiredFields?.length) {
const missing = requiredFields.filter((f) => !(f in parsed));
if (missing.length) {
return `Invalid: missing required fields: ${missing.join(', ')}`;
}
}
const type = Array.isArray(parsed) ? 'array' : typeof parsed;
return `Valid JSON (${type})`;
} catch (e) {
return `Invalid JSON: ${(e as Error).message}`;
}
},
}),
json_transform: plugins.tool({
description: 'Parse a JSON string and return it pretty-printed.',
inputSchema: plugins.z.object({
jsonString: plugins.z.string().describe('JSON string to format'),
}),
execute: async ({ jsonString }: { jsonString: string }) => {
try {
const parsed = JSON.parse(jsonString);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return `Error parsing JSON: ${(e as Error).message}`;
}
},
}),
};
}

62
ts_tools/tool.shell.ts Normal file
View File

@@ -0,0 +1,62 @@
import * as plugins from './plugins.js';
export interface IShellToolOptions {
/** Allowed commands whitelist. If empty, all commands are allowed. */
allowedCommands?: string[];
/** Working directory for shell execution */
cwd?: string;
}
export function shellTool(options?: IShellToolOptions): plugins.ToolSet {
const smartshell = new plugins.smartshell.Smartshell({ executor: 'bash' });
return {
run_command: plugins.tool({
description:
'Execute a shell command. Provide the full command string. stdout and stderr are returned.',
inputSchema: plugins.z.object({
command: plugins.z.string().describe('The shell command to execute'),
cwd: plugins.z
.string()
.optional()
.describe('Working directory for the command'),
timeout: plugins.z
.number()
.optional()
.describe('Timeout in milliseconds'),
}),
execute: async ({
command,
cwd,
timeout,
}: {
command: string;
cwd?: string;
timeout?: number;
}) => {
// Validate against allowed commands whitelist
if (options?.allowedCommands?.length) {
const baseCommand = command.split(/\s+/)[0];
if (!options.allowedCommands.includes(baseCommand)) {
return `Command "${baseCommand}" is not in the allowed commands list: ${options.allowedCommands.join(', ')}`;
}
}
// Build full command string with cd prefix if cwd specified
const effectiveCwd = cwd ?? options?.cwd;
const fullCommand = effectiveCwd
? `cd ${JSON.stringify(effectiveCwd)} && ${command}`
: command;
const execResult = await smartshell.exec(fullCommand);
const output =
execResult.exitCode === 0
? execResult.stdout
: `Exit code: ${execResult.exitCode}\nstdout:\n${execResult.stdout}\nstderr:\n${execResult.stderr ?? ''}`;
return plugins.truncateOutput(output).content;
},
}),
};
}