src / agents / dynamicTools.ts
/**
* Dynamic tool creation — Agent Zero's self-evolving capability.
*
* Tools are Python scripts saved to .agent_tools/<name>.py in the workspace.
* Each script receives JSON arguments via stdin and prints its result to stdout.
*
* All tools use the single Python interpreter configured in plugin settings.
*/
import { readFile, writeFile, unlink, mkdir } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { run } from "../sandbox";
const TOOLS_DIR_NAME = ".agent_tools";
const INDEX_FILE = "__index__.json";
function toolsDir(workspace: string): string {
return join(workspace, TOOLS_DIR_NAME);
}
interface ToolMeta {
name: string;
description: string;
argsSchema: string;
created: string;
}
interface ToolIndex {
tools: ToolMeta[];
}
async function loadIndex(workspace: string): Promise<ToolIndex> {
try {
return JSON.parse(await readFile(join(toolsDir(workspace), INDEX_FILE), "utf-8")) as ToolIndex;
} catch {
return { tools: [] };
}
}
async function saveIndex(workspace: string, index: ToolIndex): Promise<void> {
const dir = toolsDir(workspace);
await mkdir(dir, { recursive: true });
await writeFile(join(dir, INDEX_FILE), JSON.stringify(index, null, 2), "utf-8");
}
function sanitizeName(name: string): string {
return name.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 64);
}
// ── Public API ─────────────────────────────────────────────────────────────
export async function createTool(
workspace: string,
name: string,
description: string,
argsSchema: string,
pythonCode: string,
): Promise<string> {
const safe = sanitizeName(name);
if (!safe) return "Invalid tool name.";
const dir = toolsDir(workspace);
await mkdir(dir, { recursive: true });
// Wrap user code so it always reads args from stdin as JSON
const wrapped = `#!/usr/bin/env python3
"""Dynamic tool: ${safe}
${description}
"""
import json, sys
def main(args: dict):
${pythonCode.split("\n").map(l => " " + l).join("\n")}
if __name__ == "__main__":
raw = sys.stdin.read().strip()
args = json.loads(raw) if raw else {}
result = main(args)
if result is not None:
print(result if isinstance(result, str) else json.dumps(result))
`;
const scriptPath = join(dir, `${safe}.py`);
await writeFile(scriptPath, wrapped, "utf-8");
const index = await loadIndex(workspace);
const existing = index.tools.findIndex(t => t.name === safe);
const meta: ToolMeta = { name: safe, description, argsSchema, created: new Date().toISOString() };
if (existing >= 0) {
index.tools[existing] = meta;
} else {
index.tools.push(meta);
}
await saveIndex(workspace, index);
return `Created tool "${safe}" at ${scriptPath}`;
}
export async function callTool(
workspace: string,
name: string,
argsJson: string,
pythonBin: string,
timeout: number,
): Promise<string> {
const safe = sanitizeName(name);
const scriptPath = join(toolsDir(workspace), `${safe}.py`);
if (!existsSync(scriptPath)) {
const index = await loadIndex(workspace);
const names = index.tools.map(t => t.name).join(", ") || "(none)";
return `Tool "${name}" not found. Available tools: ${names}`;
}
let normalizedArgs = "{}";
if (argsJson.trim()) {
try {
normalizedArgs = JSON.stringify(JSON.parse(argsJson));
} catch {
return `Invalid JSON args: ${argsJson}`;
}
}
const r = await run(pythonBin, [scriptPath], {
stdin: normalizedArgs,
cwd: workspace,
timeout,
});
if (!r.success && !r.stdout) {
return `Tool "${name}" failed (exit ${r.code}):\n${r.stderr}`;
}
return r.stdout || r.stderr || "(no output)";
}
export async function listTools(workspace: string): Promise<string> {
const index = await loadIndex(workspace);
if (!index.tools.length) return "No dynamic tools created yet.";
return JSON.stringify(
index.tools.map(t => ({ name: t.name, description: t.description, args: t.argsSchema, created: t.created })),
null, 2
);
}
export async function deleteTool(workspace: string, name: string): Promise<string> {
const safe = sanitizeName(name);
const scriptPath = join(toolsDir(workspace), `${safe}.py`);
if (!existsSync(scriptPath)) return `Tool "${name}" not found.`;
await unlink(scriptPath);
const index = await loadIndex(workspace);
index.tools = index.tools.filter(t => t.name !== safe);
await saveIndex(workspace, index);
return `Deleted tool "${safe}".`;
}