true message

This commit is contained in:
Manus
2026-03-20 17:34:20 -04:00
parent 86a1ee9062
commit c2fdfdbf72
21 changed files with 4587 additions and 254 deletions

229
server/agent-compiler.ts Normal file
View File

@@ -0,0 +1,229 @@
/**
* Agent Compiler — компилирует новых AI-агентов по техническому заданию через LLM
* Автоматически определяет: модель, роль, системный промпт, инструменты, параметры LLM
*/
import { invokeLLM } from "./_core/llm";
import { getDb } from "./db";
import { agents } from "../drizzle/schema";
import { TOOL_REGISTRY } from "./tools";
export interface CompileAgentRequest {
/** Техническое задание — описание что должен делать агент */
specification: string;
/** Имя агента (если не указано — LLM выберет) */
name?: string;
/** Провайдер LLM (ollama, openai, anthropic) */
preferredProvider?: string;
/** Предпочитаемая модель */
preferredModel?: string;
/** ID пользователя-владельца */
userId: number;
}
export interface CompiledAgentConfig {
name: string;
description: string;
role: string;
model: string;
provider: string;
temperature: number;
maxTokens: number;
topP: number;
frequencyPenalty: number;
presencePenalty: number;
systemPrompt: string;
allowedTools: string[];
allowedDomains: string[];
maxRequestsPerHour: number;
tags: string[];
reasoning: string; // Объяснение почему такие параметры
}
export interface CompileAgentResult {
success: boolean;
config?: CompiledAgentConfig;
agentId?: number;
error?: string;
}
/**
* Компилирует конфигурацию агента по ТЗ через LLM
*/
export async function compileAgentConfig(request: CompileAgentRequest): Promise<CompileAgentResult> {
// Get available tools for context
const availableTools = TOOL_REGISTRY.map(t => ({
id: t.id,
name: t.name,
description: t.description,
category: t.category,
dangerous: t.dangerous,
}));
const systemPrompt = `You are an expert AI agent architect. Your task is to analyze a technical specification (ТЗ) and generate the optimal configuration for an AI agent.
Available tools that can be assigned to the agent:
${JSON.stringify(availableTools, null, 2)}
Available providers and models:
- ollama: llama3.2, llama3.1, mistral, codellama, deepseek-coder, phi3
- openai: gpt-4o, gpt-4o-mini, gpt-3.5-turbo
- anthropic: claude-3-5-sonnet, claude-3-haiku
Guidelines for configuration:
- temperature: 0.1-0.3 for precise/analytical tasks, 0.5-0.7 for balanced, 0.8-1.0 for creative
- maxTokens: 512-1024 for simple tasks, 2048-4096 for complex, 8192 for very long outputs
- topP: 0.9-1.0 for most tasks, lower for more focused outputs
- systemPrompt: detailed, specific, includes examples if helpful
- allowedTools: only tools the agent actually needs
- allowedDomains: specific domains if web access needed, empty array if not needed
- role: one of "developer", "researcher", "analyst", "writer", "executor", "monitor", "coordinator"
Return ONLY valid JSON with this exact structure (no markdown, no extra text):
{
"name": "Agent Name",
"description": "Brief description of what this agent does",
"role": "developer|researcher|analyst|writer|executor|monitor|coordinator",
"model": "model-name",
"provider": "ollama|openai|anthropic",
"temperature": 0.7,
"maxTokens": 2048,
"topP": 1.0,
"frequencyPenalty": 0.0,
"presencePenalty": 0.0,
"systemPrompt": "Detailed system prompt for the agent...",
"allowedTools": ["tool_id1", "tool_id2"],
"allowedDomains": ["example.com"],
"maxRequestsPerHour": 100,
"tags": ["tag1", "tag2"],
"reasoning": "Explanation of why these parameters were chosen"
}`;
const userPrompt = `Technical Specification (ТЗ):
${request.specification}
${request.name ? `Preferred name: ${request.name}` : ""}
${request.preferredProvider ? `Preferred provider: ${request.preferredProvider}` : ""}
${request.preferredModel ? `Preferred model: ${request.preferredModel}` : ""}
Analyze this specification and generate the optimal agent configuration. Be specific and detailed in the systemPrompt.`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
response_format: {
type: "json_schema",
json_schema: {
name: "agent_config",
strict: true,
schema: {
type: "object",
properties: {
name: { type: "string" },
description: { type: "string" },
role: { type: "string" },
model: { type: "string" },
provider: { type: "string" },
temperature: { type: "number" },
maxTokens: { type: "integer" },
topP: { type: "number" },
frequencyPenalty: { type: "number" },
presencePenalty: { type: "number" },
systemPrompt: { type: "string" },
allowedTools: { type: "array", items: { type: "string" } },
allowedDomains: { type: "array", items: { type: "string" } },
maxRequestsPerHour: { type: "integer" },
tags: { type: "array", items: { type: "string" } },
reasoning: { type: "string" },
},
required: [
"name", "description", "role", "model", "provider",
"temperature", "maxTokens", "topP", "frequencyPenalty", "presencePenalty",
"systemPrompt", "allowedTools", "allowedDomains", "maxRequestsPerHour",
"tags", "reasoning"
],
additionalProperties: false,
},
},
},
});
const content = response.choices[0].message.content;
const config = typeof content === "string" ? JSON.parse(content) : content;
// Validate that allowedTools exist in registry
const validToolIds = TOOL_REGISTRY.map(t => t.id);
config.allowedTools = config.allowedTools.filter((id: string) => validToolIds.includes(id));
return { success: true, config };
} catch (error: any) {
return {
success: false,
error: `Failed to compile agent: ${error.message}`,
};
}
}
/**
* Деплоит скомпилированного агента в БД
*/
export async function deployCompiledAgent(
config: CompiledAgentConfig,
userId: number
): Promise<{ success: boolean; agentId?: number; error?: string }> {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
try {
const [result] = await db.insert(agents).values({
userId,
name: config.name,
description: config.description,
role: config.role,
model: config.model,
provider: config.provider,
temperature: String(config.temperature),
maxTokens: config.maxTokens,
topP: String(config.topP),
frequencyPenalty: String(config.frequencyPenalty),
presencePenalty: String(config.presencePenalty),
systemPrompt: config.systemPrompt,
allowedTools: config.allowedTools,
allowedDomains: config.allowedDomains,
maxRequestsPerHour: config.maxRequestsPerHour,
tags: config.tags,
metadata: { compiledFromSpec: true, reasoning: config.reasoning },
isActive: true,
isPublic: false,
});
return { success: true, agentId: (result as any).insertId };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Полный цикл: компиляция + деплой
*/
export async function compileAndDeployAgent(
request: CompileAgentRequest
): Promise<CompileAgentResult> {
const compileResult = await compileAgentConfig(request);
if (!compileResult.success || !compileResult.config) {
return compileResult;
}
const deployResult = await deployCompiledAgent(compileResult.config, request.userId);
if (!deployResult.success) {
return { success: false, error: deployResult.error };
}
return {
success: true,
config: compileResult.config,
agentId: deployResult.agentId,
};
}

317
server/browser-agent.ts Normal file
View File

@@ -0,0 +1,317 @@
/**
* Browser Agent — управление браузером через Puppeteer
* Поддерживает: навигация, скриншоты, клики, ввод текста, извлечение данных
*/
import puppeteer, { Browser, Page } from "puppeteer-core";
import { randomUUID } from "crypto";
import { getDb } from "./db";
import { browserSessions } from "../drizzle/schema";
import { eq } from "drizzle-orm";
import { storagePut } from "./storage";
const CHROMIUM_PATH = process.env.CHROMIUM_PATH || "/usr/bin/chromium-browser";
// In-memory session store (browser instances)
const activeSessions = new Map<string, { browser: Browser; page: Page; agentId: number }>();
export interface BrowserAction {
type: "navigate" | "click" | "type" | "extract" | "screenshot" | "scroll" | "wait" | "evaluate" | "close";
params: Record<string, any>;
}
export interface BrowserResult {
success: boolean;
sessionId: string;
screenshotUrl?: string;
data?: any;
error?: string;
currentUrl?: string;
title?: string;
executionTimeMs: number;
}
/**
* Создаёт новую браузерную сессию для агента
*/
export async function createBrowserSession(agentId: number): Promise<{ sessionId: string; error?: string }> {
const sessionId = randomUUID().replace(/-/g, "").slice(0, 16);
try {
const browser = await puppeteer.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--no-first-run",
"--no-zygote",
"--disable-gpu",
"--window-size=1280,800",
],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.setUserAgent(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);
activeSessions.set(sessionId, { browser, page, agentId });
// Persist to DB
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.insert(browserSessions).values({
sessionId,
agentId,
status: "active",
currentUrl: "about:blank",
});
return { sessionId };
} catch (error: any) {
return { sessionId: "", error: error.message };
}
}
/**
* Выполняет действие в браузерной сессии
*/
export async function executeBrowserAction(
sessionId: string,
action: BrowserAction
): Promise<BrowserResult> {
const start = Date.now();
const session = activeSessions.get(sessionId);
if (!session) {
// Try to restore from DB info
return {
success: false,
sessionId,
error: "Session not found or expired. Create a new session.",
executionTimeMs: Date.now() - start,
};
}
const { page, agentId } = session;
try {
let data: any = null;
let screenshotUrl: string | undefined;
switch (action.type) {
case "navigate": {
const url = action.params.url as string;
await page.goto(url, {
waitUntil: action.params.waitUntil || "networkidle2",
timeout: action.params.timeout || 30000,
});
break;
}
case "click": {
const selector = action.params.selector as string;
await page.waitForSelector(selector, { timeout: 10000 });
await page.click(selector);
if (action.params.waitAfter) {
await new Promise(r => setTimeout(r, action.params.waitAfter));
}
break;
}
case "type": {
const selector = action.params.selector as string;
const text = action.params.text as string;
await page.waitForSelector(selector, { timeout: 10000 });
if (action.params.clear) {
await page.click(selector, { clickCount: 3 });
}
await page.type(selector, text, { delay: action.params.delay || 0 });
break;
}
case "extract": {
const extractType = action.params.extractType || "text";
if (extractType === "text") {
data = await page.evaluate(() => document.body.innerText);
} else if (extractType === "html") {
data = await page.evaluate(() => document.documentElement.outerHTML);
} else if (extractType === "selector") {
const selector = action.params.selector as string;
data = await page.evaluate((sel) => {
const elements = document.querySelectorAll(sel);
return Array.from(elements).map(el => ({
text: (el as HTMLElement).innerText,
html: el.innerHTML,
href: (el as HTMLAnchorElement).href || null,
src: (el as HTMLImageElement).src || null,
}));
}, selector);
} else if (extractType === "links") {
data = await page.evaluate(() => {
return Array.from(document.querySelectorAll("a[href]")).map(a => ({
text: (a as HTMLAnchorElement).innerText.trim(),
href: (a as HTMLAnchorElement).href,
}));
});
} else if (extractType === "tables") {
data = await page.evaluate(() => {
return Array.from(document.querySelectorAll("table")).map(table => {
const rows = Array.from(table.querySelectorAll("tr"));
return rows.map(row =>
Array.from(row.querySelectorAll("td, th")).map(cell => (cell as HTMLElement).innerText.trim())
);
});
});
}
break;
}
case "screenshot": {
const screenshotBuffer = await page.screenshot({
type: "png",
fullPage: action.params.fullPage || false,
});
// Upload to S3
const key = `browser-sessions/${sessionId}/${Date.now()}.png`;
const uploadResult = await storagePut(key, screenshotBuffer as Buffer, "image/png");
screenshotUrl = uploadResult.url;
data = { screenshotUrl };
break;
}
case "scroll": {
const scrollY = action.params.y || 500;
const scrollX = action.params.x || 0;
await page.evaluate((x, y) => window.scrollBy(x, y), scrollX, scrollY);
break;
}
case "wait": {
const waitFor = action.params.selector
? page.waitForSelector(action.params.selector, { timeout: action.params.timeout || 10000 })
: new Promise(r => setTimeout(r, action.params.ms || 1000));
await waitFor;
break;
}
case "evaluate": {
const code = action.params.code as string;
// Safety: only allow read-only operations
data = await page.evaluate(new Function(code) as any);
break;
}
case "close": {
await session.browser.close();
activeSessions.delete(sessionId);
const closeDb = await getDb();
if (closeDb) await closeDb.update(browserSessions)
.set({ status: "closed", closedAt: new Date() })
.where(eq(browserSessions.sessionId, sessionId));
return {
success: true,
sessionId,
data: { message: "Session closed" },
executionTimeMs: Date.now() - start,
};
}
}
// Take automatic screenshot after navigation/click/type
if (["navigate", "click", "type"].includes(action.type)) {
try {
const buf = await page.screenshot({ type: "png" });
const key = `browser-sessions/${sessionId}/${Date.now()}.png`;
const uploadResult = await storagePut(key, buf as Buffer, "image/png");
screenshotUrl = uploadResult.url;
} catch {
// Screenshot is optional
}
}
const currentUrl = page.url();
const title = await page.title().catch(() => "");
// Update DB
const dbInst = await getDb();
if (dbInst) await dbInst.update(browserSessions)
.set({
currentUrl,
title,
status: "active",
screenshotUrl: screenshotUrl || undefined,
lastActionAt: new Date(),
})
.where(eq(browserSessions.sessionId, sessionId));
return {
success: true,
sessionId,
screenshotUrl,
data,
currentUrl,
title,
executionTimeMs: Date.now() - start,
};
} catch (error: any) {
// Update DB with error
const errDb = await getDb();
if (errDb) await errDb.update(browserSessions)
.set({ status: "error" })
.where(eq(browserSessions.sessionId, sessionId))
.catch(() => {});
return {
success: false,
sessionId,
error: error.message,
currentUrl: page.url(),
executionTimeMs: Date.now() - start,
};
}
}
/**
* Получает список активных сессий агента
*/
export async function getAgentSessions(agentId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(browserSessions)
.where(eq(browserSessions.agentId, agentId));
}
/**
* Получает сессию по ID
*/
export async function getSession(sessionId: string) {
const db = await getDb();
if (!db) return null;
const rows = await db.select().from(browserSessions)
.where(eq(browserSessions.sessionId, sessionId));
return rows[0] || null;
}
/**
* Закрывает все сессии агента
*/
export async function closeAllAgentSessions(agentId: number) {
for (const [sid, session] of Array.from(activeSessions.entries())) {
if (session.agentId === agentId) {
await session.browser.close().catch(() => {});
activeSessions.delete(sid);
}
}
const db = await getDb();
if (!db) return;
await db.update(browserSessions)
.set({ status: "closed", closedAt: new Date() })
.where(eq(browserSessions.agentId, agentId));
}

569
server/orchestrator.ts Normal file
View File

@@ -0,0 +1,569 @@
/**
* GoClaw Orchestrator — Main AI Agent
*
* The orchestrator is the primary interface for the user. It has access to:
* - All specialized agents (Browser, Tool Builder, Agent Compiler, custom)
* - All tools (shell, file, HTTP, Docker, browser)
* - All skills (registered capabilities)
* - System management (create/install/run components)
*
* It uses a tool-use loop: LLM decides which tools to call, executes them,
* feeds results back to LLM, and repeats until a final answer is ready.
*/
import { exec } from "child_process";
import { promisify } from "util";
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
import { existsSync } from "fs";
import { join, dirname } from "path";
import { invokeLLM } from "./_core/llm";
import { chatCompletion } from "./ollama";
import { getDb } from "./db";
import { agents, agentHistory } from "../drizzle/schema";
import { eq } from "drizzle-orm";
const execAsync = promisify(exec);
// ─── Tool Definitions for LLM ────────────────────────────────────────────────
export const ORCHESTRATOR_TOOLS = [
{
type: "function" as const,
function: {
name: "shell_exec",
description:
"Execute a shell command on the server. Use for: running scripts, installing packages, git operations, checking system status, compiling code.",
parameters: {
type: "object",
properties: {
command: { type: "string", description: "Shell command to execute" },
cwd: { type: "string", description: "Working directory (default: project root)" },
timeout: { type: "number", description: "Timeout in ms (default: 30000)" },
},
required: ["command"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "file_read",
description: "Read a file from the filesystem. Returns file content as text.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative file path" },
encoding: { type: "string", description: "File encoding (default: utf-8)" },
},
required: ["path"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "file_write",
description: "Write content to a file. Creates directories if needed.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative file path" },
content: { type: "string", description: "Content to write" },
append: { type: "boolean", description: "Append to file instead of overwrite" },
},
required: ["path", "content"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "file_list",
description: "List files in a directory.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Directory path" },
recursive: { type: "boolean", description: "List recursively" },
},
required: ["path"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "http_request",
description: "Make an HTTP request to any URL. Supports GET, POST, PUT, DELETE.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "Target URL" },
method: { type: "string", description: "HTTP method (default: GET)" },
headers: { type: "object", description: "Request headers" },
body: { type: "string", description: "Request body (for POST/PUT)" },
},
required: ["url"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "delegate_to_agent",
description:
"Delegate a task to a specialized agent. Use for: web browsing (Browser Agent), creating tools (Tool Builder), compiling agents (Agent Compiler), or any other specialized agent.",
parameters: {
type: "object",
properties: {
agentId: { type: "number", description: "Agent ID to delegate to" },
message: { type: "string", description: "Task description for the agent" },
},
required: ["agentId", "message"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "list_agents",
description: "List all available specialized agents with their capabilities.",
parameters: {
type: "object",
properties: {},
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "list_skills",
description: "List all installed skills (capabilities) available to agents.",
parameters: {
type: "object",
properties: {},
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "install_skill",
description:
"Install a new skill into the system. A skill is a reusable capability module.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Skill name (snake_case)" },
description: { type: "string", description: "What this skill does" },
code: { type: "string", description: "JavaScript implementation of the skill" },
category: { type: "string", description: "Skill category (web, data, ai, system, etc.)" },
},
required: ["name", "description", "code"],
additionalProperties: false,
},
},
},
{
type: "function" as const,
function: {
name: "docker_exec",
description: "Execute a Docker command (docker ps, docker logs, docker exec, etc.).",
parameters: {
type: "object",
properties: {
command: { type: "string", description: "Docker command (without 'docker' prefix)" },
},
required: ["command"],
additionalProperties: false,
},
},
},
];
// ─── Tool Execution ───────────────────────────────────────────────────────────
const PROJECT_ROOT = "/home/ubuntu/goclaw-control-center";
async function executeTool(
toolName: string,
args: Record<string, any>
): Promise<{ success: boolean; result?: any; error?: string }> {
try {
switch (toolName) {
case "shell_exec": {
const cwd = args.cwd || PROJECT_ROOT;
const timeout = args.timeout || 30000;
const { stdout, stderr } = await execAsync(args.command, { cwd, timeout });
return {
success: true,
result: { stdout: stdout.trim(), stderr: stderr.trim(), command: args.command },
};
}
case "file_read": {
const filePath = args.path.startsWith("/") ? args.path : join(PROJECT_ROOT, args.path);
if (!existsSync(filePath)) {
return { success: false, error: `File not found: ${filePath}` };
}
const content = await readFile(filePath, (args.encoding as BufferEncoding) || "utf-8");
const lines = (content as string).split("\n").length;
return { success: true, result: { path: filePath, content, lines } };
}
case "file_write": {
const filePath = args.path.startsWith("/") ? args.path : join(PROJECT_ROOT, args.path);
await mkdir(dirname(filePath), { recursive: true });
if (args.append) {
const existing = existsSync(filePath)
? await readFile(filePath, "utf-8")
: "";
await writeFile(filePath, existing + args.content, "utf-8");
} else {
await writeFile(filePath, args.content, "utf-8");
}
return { success: true, result: { path: filePath, written: args.content.length } };
}
case "file_list": {
const dirPath = args.path.startsWith("/") ? args.path : join(PROJECT_ROOT, args.path);
if (!existsSync(dirPath)) {
return { success: false, error: `Directory not found: ${dirPath}` };
}
const entries = await readdir(dirPath);
const details = await Promise.all(
entries.map(async (name) => {
const fullPath = join(dirPath, name);
const s = await stat(fullPath);
return { name, type: s.isDirectory() ? "dir" : "file", size: s.size };
})
);
return { success: true, result: { path: dirPath, entries: details } };
}
case "http_request": {
const method = (args.method || "GET").toUpperCase();
const response = await fetch(args.url, {
method,
headers: args.headers || {},
body: args.body || undefined,
signal: AbortSignal.timeout(15000),
});
const text = await response.text();
let body: any = text;
try {
body = JSON.parse(text);
} catch {
// keep as text
}
return {
success: true,
result: {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body,
},
};
}
case "delegate_to_agent": {
const db = await getDb();
if (!db) return { success: false, error: "DB not available" };
const [agent] = await db
.select()
.from(agents)
.where(eq(agents.id, args.agentId))
.limit(1);
if (!agent) {
return { success: false, error: `Agent ${args.agentId} not found` };
}
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [];
if (agent.systemPrompt) {
messages.push({ role: "system", content: agent.systemPrompt });
}
messages.push({ role: "user", content: args.message });
const result = await chatCompletion(agent.model, messages, {
temperature: agent.temperature ? parseFloat(agent.temperature as string) : 0.7,
max_tokens: agent.maxTokens ?? 2048,
});
const response = result.choices[0]?.message?.content ?? "";
// Save to agent history
await db.insert(agentHistory).values({
agentId: agent.id,
userMessage: args.message,
agentResponse: response,
conversationId: `orchestrator-${Date.now()}`,
status: "success",
});
return {
success: true,
result: {
agentName: agent.name,
agentRole: agent.role,
response,
model: result.model,
usage: result.usage,
},
};
}
case "list_agents": {
const db = await getDb();
if (!db) return { success: false, error: "DB not available" };
const allAgents = await db
.select({
id: agents.id,
name: agents.name,
role: agents.role,
description: agents.description,
model: agents.model,
isActive: agents.isActive,
allowedTools: agents.allowedTools,
tags: agents.tags,
})
.from(agents)
.where(eq(agents.isActive, true));
return { success: true, result: { agents: allAgents, count: allAgents.length } };
}
case "list_skills": {
const skillsPath = join(PROJECT_ROOT, "server/skills");
if (!existsSync(skillsPath)) {
return { success: true, result: { skills: [], count: 0 } };
}
const entries = await readdir(skillsPath);
const skills = await Promise.all(
entries.map(async (name) => {
const metaPath = join(skillsPath, name, "skill.json");
if (existsSync(metaPath)) {
const meta = JSON.parse(await readFile(metaPath, "utf-8") as string);
return { name, ...meta };
}
return { name, description: "No metadata" };
})
);
return { success: true, result: { skills, count: skills.length } };
}
case "install_skill": {
const skillsPath = join(PROJECT_ROOT, "server/skills", args.name);
await mkdir(skillsPath, { recursive: true });
// Write skill implementation
await writeFile(join(skillsPath, "index.js"), args.code, "utf-8");
// Write skill metadata
const meta = {
name: args.name,
description: args.description,
category: args.category || "custom",
installedAt: new Date().toISOString(),
version: "1.0.0",
};
await writeFile(join(skillsPath, "skill.json"), JSON.stringify(meta, null, 2), "utf-8");
return { success: true, result: { installed: args.name, path: skillsPath } };
}
case "docker_exec": {
const { stdout, stderr } = await execAsync(`docker ${args.command}`, {
timeout: 15000,
});
return { success: true, result: { stdout: stdout.trim(), stderr: stderr.trim() } };
}
default:
return { success: false, error: `Unknown tool: ${toolName}` };
}
} catch (err: any) {
return { success: false, error: err.message || String(err) };
}
}
// ─── Orchestrator Chat ────────────────────────────────────────────────────────
export interface OrchestratorMessage {
role: "user" | "assistant" | "system";
content: string;
}
export interface ToolCallStep {
tool: string;
args: Record<string, any>;
result: any;
success: boolean;
durationMs: number;
}
export interface OrchestratorResult {
success: boolean;
response: string;
toolCalls: ToolCallStep[];
model?: string;
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
error?: string;
}
const ORCHESTRATOR_SYSTEM_PROMPT = `You are GoClaw Orchestrator — the main AI agent managing the GoClaw distributed AI system.
You have full access to:
1. **Specialized Agents**: Browser Agent (web browsing), Tool Builder (create tools), Agent Compiler (create agents)
2. **System Tools**: shell_exec (run commands), file_read/write (manage files), http_request (web requests), docker_exec (Docker management)
3. **Skills Registry**: list_skills (see capabilities), install_skill (add new capabilities)
Your responsibilities:
- Answer user questions directly when possible
- Delegate complex web tasks to Browser Agent
- Delegate tool creation to Tool Builder agent
- Delegate agent creation to Agent Compiler
- Execute shell commands to manage the system, install packages, run scripts
- Read and write files to modify the codebase
- Monitor Docker containers and services
Decision making:
- For simple questions: answer directly without tools
- For web research: use delegate_to_agent with Browser Agent (id: 1)
- For creating tools: use delegate_to_agent with Tool Builder (id: 2)
- For creating agents: use delegate_to_agent with Agent Compiler (id: 3)
- For system tasks: use shell_exec, file_read/write
- For Docker: use docker_exec
- Always use list_agents first if you're unsure which agent to delegate to
Response style:
- Be concise and actionable
- Show what tools you used and their results
- If a task requires multiple steps, execute them in sequence
- Respond in the same language as the user
You are running on a Linux server with Node.js, Docker, and full internet access.`;
export async function orchestratorChat(
messages: OrchestratorMessage[],
model: string = "qwen2.5:7b",
maxToolIterations: number = 10
): Promise<OrchestratorResult> {
const toolCalls: ToolCallStep[] = [];
// Build conversation with system prompt
const conversation: Array<{
role: "system" | "user" | "assistant" | "tool" | "function";
content: string | any;
tool_call_id?: string;
name?: string;
}> = [
{ role: "system", content: ORCHESTRATOR_SYSTEM_PROMPT },
...messages.map((m) => ({ role: m.role, content: m.content })),
];
let iterations = 0;
let finalResponse = "";
let lastUsage: any;
let lastModel: string = model;
while (iterations < maxToolIterations) {
iterations++;
// Call LLM with tools
let llmResult: any;
try {
llmResult = await invokeLLM({
messages: conversation as any,
tools: ORCHESTRATOR_TOOLS,
tool_choice: "auto",
});
} catch (err: any) {
// Fallback: try without tools if LLM doesn't support them
try {
const fallbackResult = await chatCompletion(model, conversation as any, {
temperature: 0.7,
max_tokens: 4096,
});
finalResponse = fallbackResult.choices[0]?.message?.content ?? "";
lastUsage = fallbackResult.usage;
lastModel = fallbackResult.model;
break;
} catch (fallbackErr: any) {
return {
success: false,
response: "",
toolCalls,
error: `LLM error: ${fallbackErr.message}`,
};
}
}
const choice = llmResult.choices?.[0];
if (!choice) break;
lastUsage = llmResult.usage;
lastModel = llmResult.model || model;
const message = choice.message;
// Check if LLM wants to call tools
if (choice.finish_reason === "tool_calls" && message.tool_calls?.length > 0) {
// Add assistant message with tool calls to conversation
conversation.push({
role: "assistant",
content: message.content || "",
...message,
});
// Execute each tool call
for (const tc of message.tool_calls) {
const toolName = tc.function?.name;
let toolArgs: Record<string, any> = {};
try {
toolArgs = JSON.parse(tc.function?.arguments || "{}");
} catch {
toolArgs = {};
}
const startTime = Date.now();
const toolResult = await executeTool(toolName, toolArgs);
const durationMs = Date.now() - startTime;
toolCalls.push({
tool: toolName,
args: toolArgs,
result: toolResult.result,
success: toolResult.success,
durationMs,
});
// Add tool result to conversation
conversation.push({
role: "tool",
content: JSON.stringify(
toolResult.success
? toolResult.result
: { error: toolResult.error }
),
tool_call_id: tc.id,
name: toolName,
});
}
// Continue loop — LLM will process tool results
continue;
}
// LLM finished — extract final response
finalResponse = message.content || "";
break;
}
return {
success: true,
response: finalResponse,
toolCalls,
model: lastModel,
usage: lastUsage,
};
}

View File

@@ -265,7 +265,7 @@ export const appRouter = router({
}),
}),
/**
/**
* Tools — управление инструментами агентов
*/
tools: router({
@@ -273,13 +273,12 @@ export const appRouter = router({
const { getAllTools } = await import("./tools");
return getAllTools();
}),
execute: publicProcedure
.input(
z.object({
agentId: z.number(),
tool: z.string(),
params: z.record(z.string(), z.any()),
params: z.record(z.string(), z.unknown()),
})
)
.mutation(async ({ input }) => {
@@ -287,6 +286,216 @@ export const appRouter = router({
return executeTool(input.agentId, input.tool, input.params);
}),
}),
});
/**
* Browser Agent — управление браузерными сессиями через Puppeteer
*/
browser: router({
createSession: publicProcedure
.input(z.object({ agentId: z.number() }))
.mutation(async ({ input }) => {
const { createBrowserSession } = await import("./browser-agent");
return createBrowserSession(input.agentId);
}),
execute: publicProcedure
.input(
z.object({
sessionId: z.string(),
action: z.object({
type: z.enum(["navigate", "click", "type", "extract", "screenshot", "scroll", "wait", "evaluate", "close"]),
params: z.record(z.string(), z.unknown()),
}),
})
)
.mutation(async ({ input }) => {
const { executeBrowserAction } = await import("./browser-agent");
return executeBrowserAction(input.sessionId, input.action as any);
}),
getSessions: publicProcedure
.input(z.object({ agentId: z.number() }))
.query(async ({ input }) => {
const { getAgentSessions } = await import("./browser-agent");
return getAgentSessions(input.agentId);
}),
closeSession: publicProcedure
.input(z.object({ sessionId: z.string() }))
.mutation(async ({ input }) => {
const { executeBrowserAction } = await import("./browser-agent");
return executeBrowserAction(input.sessionId, { type: "close", params: {} });
}),
closeAllSessions: publicProcedure
.input(z.object({ agentId: z.number() }))
.mutation(async ({ input }) => {
const { closeAllAgentSessions } = await import("./browser-agent");
await closeAllAgentSessions(input.agentId);
return { success: true };
}),
}),
/**
* Tool Builder — генерация и установка новых инструментов через LLM
*/
toolBuilder: router({
generate: publicProcedure
.input(
z.object({
name: z.string().min(1),
description: z.string().min(10),
category: z.string().optional(),
exampleInput: z.string().optional(),
exampleOutput: z.string().optional(),
dangerous: z.boolean().optional(),
})
)
.mutation(async ({ input }) => {
const { generateTool } = await import("./tool-builder");
return generateTool(input);
}),
install: publicProcedure
.input(
z.object({
toolId: z.string(),
name: z.string(),
description: z.string(),
category: z.string(),
dangerous: z.boolean(),
parameters: z.record(z.string(), z.object({
type: z.string(),
description: z.string(),
required: z.boolean().optional(),
})),
implementation: z.string(),
})
)
.mutation(async ({ input }) => {
const { installTool } = await import("./tool-builder");
return installTool(input);
}),
listCustom: publicProcedure.query(async () => {
const { getCustomTools } = await import("./tool-builder");
return getCustomTools();
}),
delete: publicProcedure
.input(z.object({ toolId: z.string() }))
.mutation(async ({ input }) => {
const { deleteTool } = await import("./tool-builder");
return deleteTool(input.toolId);
}),
test: publicProcedure
.input(
z.object({
toolId: z.string(),
params: z.record(z.string(), z.unknown()),
})
)
.mutation(async ({ input }) => {
const { testTool } = await import("./tool-builder");
return testTool(input.toolId, input.params);
}),
}),
/**
* Agent Compiler — компиляция агентов по ТЗ через LLM
*/
agentCompiler: router({
compile: publicProcedure
.input(
z.object({
specification: z.string().min(20),
name: z.string().optional(),
preferredProvider: z.string().optional(),
preferredModel: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { compileAgentConfig } = await import("./agent-compiler");
return compileAgentConfig({ ...input, userId: SYSTEM_USER_ID });
}),
deploy: publicProcedure
.input(
z.object({
config: z.object({
name: z.string(),
description: z.string(),
role: z.string(),
model: z.string(),
provider: z.string(),
temperature: z.number(),
maxTokens: z.number(),
topP: z.number(),
frequencyPenalty: z.number(),
presencePenalty: z.number(),
systemPrompt: z.string(),
allowedTools: z.array(z.string()),
allowedDomains: z.array(z.string()),
maxRequestsPerHour: z.number(),
tags: z.array(z.string()),
reasoning: z.string(),
}),
})
)
.mutation(async ({ input }) => {
const { deployCompiledAgent } = await import("./agent-compiler");
return deployCompiledAgent(input.config, SYSTEM_USER_ID);
}),
compileAndDeploy: publicProcedure
.input(
z.object({
specification: z.string().min(20),
name: z.string().optional(),
preferredProvider: z.string().optional(),
preferredModel: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { compileAndDeployAgent } = await import("./agent-compiler");
return compileAndDeployAgent({ ...input, userId: SYSTEM_USER_ID });
}),
}),
/**
* Orchestrator — main AI agent with tool-use loop
*/
orchestrator: router({
chat: publicProcedure
.input(
z.object({
messages: z.array(
z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string(),
})
),
model: z.string().optional(),
maxIterations: z.number().min(1).max(20).optional(),
})
)
.mutation(async ({ input }) => {
const { orchestratorChat } = await import("./orchestrator");
return orchestratorChat(
input.messages,
input.model ?? "qwen2.5:7b",
input.maxIterations ?? 10
);
}),
tools: publicProcedure.query(async () => {
const { ORCHESTRATOR_TOOLS } = await import("./orchestrator");
return ORCHESTRATOR_TOOLS.map((t) => ({
name: t.function.name,
description: t.function.description,
parameters: t.function.parameters,
}));
}),
}),
});
export type AppRouter = typeof appRouter;

282
server/tool-builder.ts Normal file
View File

@@ -0,0 +1,282 @@
/**
* Tool Builder Agent — генерирует новые инструменты через LLM и добавляет их в реестр
*/
import { invokeLLM } from "./_core/llm";
import { getDb } from "./db";
import { toolDefinitions } from "../drizzle/schema";
import { eq } from "drizzle-orm";
import { TOOL_REGISTRY, type ToolDefinition } from "./tools";
export interface GenerateToolRequest {
name: string;
description: string;
category?: string;
exampleInput?: string;
exampleOutput?: string;
dangerous?: boolean;
}
export interface GeneratedToolData {
toolId: string;
name: string;
description: string;
category: string;
dangerous: boolean;
parameters: Record<string, { type: string; description: string; required?: boolean }>;
implementation: string;
warnings?: string[];
}
export interface GenerateToolResult {
success: boolean;
tool?: GeneratedToolData;
error?: string;
}
/**
* Генерирует новый инструмент через LLM на основе описания
*/
export async function generateTool(request: GenerateToolRequest): Promise<GenerateToolResult> {
const systemPrompt = `You are an expert JavaScript developer specializing in creating tool functions for AI agents.
Your task is to generate a complete, working JavaScript tool implementation.
Rules:
1. The tool must be a single async function named "execute" that accepts a params object
2. Use only built-in Node.js modules (fs, path, crypto, http, https, url) or global fetch API
3. Always handle errors gracefully and return meaningful error messages
4. The function must return a JSON-serializable result object
5. Add input validation for all required parameters
6. Keep the implementation concise but complete
Return ONLY valid JSON with this exact structure:
{
"toolId": "snake_case_id",
"name": "Human Readable Name",
"description": "What this tool does",
"category": "web|data|file|system|ai|custom",
"dangerous": false,
"parameters": {
"paramName": {
"type": "string|number|boolean|array|object",
"description": "What this parameter does",
"required": true
}
},
"implementation": "async function execute(params) { /* your code here */ return { result: 'value' }; }"
}`;
const userPrompt = `Create a tool with the following specification:
Name: ${request.name}
Description: ${request.description}
Category: ${request.category || "custom"}
${request.exampleInput ? `Example Input: ${request.exampleInput}` : ""}
${request.exampleOutput ? `Example Output: ${request.exampleOutput}` : ""}
${request.dangerous ? "Note: This tool may perform dangerous operations, add appropriate safety checks." : ""}
Generate a complete, production-ready implementation.`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
response_format: {
type: "json_schema",
json_schema: {
name: "tool_definition",
strict: true,
schema: {
type: "object",
properties: {
toolId: { type: "string" },
name: { type: "string" },
description: { type: "string" },
category: { type: "string" },
dangerous: { type: "boolean" },
parameters: { type: "object", additionalProperties: true },
implementation: { type: "string" },
},
required: ["toolId", "name", "description", "category", "dangerous", "parameters", "implementation"],
additionalProperties: false,
},
},
},
});
const content = response.choices[0].message.content;
const toolData = typeof content === "string" ? JSON.parse(content) : content;
// Validate the implementation is safe
const warnings: string[] = [];
const dangerousPatterns = ["child_process", "exec(", "spawn(", "eval(", "new Function("];
for (const pattern of dangerousPatterns) {
if (toolData.implementation.includes(pattern) && !request.dangerous) {
warnings.push(`Warning: Implementation contains potentially dangerous pattern: ${pattern}`);
}
}
return {
success: true,
tool: { ...toolData, warnings },
};
} catch (error: any) {
return {
success: false,
error: `Failed to generate tool: ${error.message}`,
};
}
}
/**
* Сохраняет инструмент в БД и регистрирует в реестре
*/
export async function installTool(
toolData: GeneratedToolData,
createdBy?: number
): Promise<{ success: boolean; toolId?: string; error?: string }> {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
try {
// Save to DB
await db.insert(toolDefinitions).values({
toolId: toolData.toolId,
name: toolData.name,
description: toolData.description,
category: toolData.category,
dangerous: toolData.dangerous,
parameters: toolData.parameters,
implementation: toolData.implementation,
isActive: true,
createdBy: createdBy || null,
}).onDuplicateKeyUpdate({
set: {
name: toolData.name,
description: toolData.description,
implementation: toolData.implementation,
parameters: toolData.parameters,
updatedAt: new Date(),
},
});
// Register in runtime TOOL_REGISTRY
registerToolInRegistry(toolData);
return { success: true, toolId: toolData.toolId };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Динамически регистрирует инструмент в TOOL_REGISTRY (массив)
*/
function registerToolInRegistry(toolData: GeneratedToolData): void {
try {
// Create the execute function from implementation string
const executeFunc = new Function("return " + toolData.implementation)() as (params: any) => Promise<any>;
const toolEntry: ToolDefinition = {
id: toolData.toolId,
name: toolData.name,
description: toolData.description,
category: toolData.category as ToolDefinition["category"],
dangerous: toolData.dangerous,
parameters: toolData.parameters,
execute: executeFunc,
};
// Remove existing entry if present, then add new one
const existingIdx = TOOL_REGISTRY.findIndex(t => t.id === toolData.toolId);
if (existingIdx >= 0) {
TOOL_REGISTRY.splice(existingIdx, 1, toolEntry);
} else {
TOOL_REGISTRY.push(toolEntry);
}
} catch (error: any) {
console.warn(`[ToolBuilder] Failed to register tool ${toolData.toolId} in runtime:`, error.message);
}
}
/**
* Загружает все кастомные инструменты из БД в реестр при старте
*/
export async function loadCustomToolsFromDb(): Promise<void> {
const db = await getDb();
if (!db) return;
try {
const tools = await db.select().from(toolDefinitions)
.where(eq(toolDefinitions.isActive, true));
for (const tool of tools) {
registerToolInRegistry({
toolId: tool.toolId,
name: tool.name,
description: tool.description,
category: tool.category,
dangerous: tool.dangerous || false,
parameters: (tool.parameters as any) || {},
implementation: tool.implementation,
});
}
console.log(`[ToolBuilder] Loaded ${tools.length} custom tools from DB`);
} catch (error: any) {
console.warn("[ToolBuilder] Failed to load custom tools:", error.message);
}
}
/**
* Получает все инструменты из БД
*/
export async function getCustomTools() {
const db = await getDb();
if (!db) return [];
return db.select().from(toolDefinitions);
}
/**
* Удаляет инструмент из БД и реестра
*/
export async function deleteTool(toolId: string): Promise<{ success: boolean; error?: string }> {
const db = await getDb();
if (!db) return { success: false, error: "Database not available" };
try {
await db.delete(toolDefinitions).where(eq(toolDefinitions.toolId, toolId));
// Remove from TOOL_REGISTRY
const idx = TOOL_REGISTRY.findIndex(t => t.id === toolId);
if (idx >= 0) TOOL_REGISTRY.splice(idx, 1);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Тестирует инструмент с заданными параметрами
*/
export async function testTool(
toolId: string,
params: Record<string, any>
): Promise<{ success: boolean; result?: any; error?: string; executionTimeMs: number }> {
const start = Date.now();
const tool = TOOL_REGISTRY.find(t => t.id === toolId);
if (!tool) {
return { success: false, error: `Tool '${toolId}' not found in registry`, executionTimeMs: 0 };
}
if (!tool.execute) {
return { success: false, error: `Tool '${toolId}' has no execute function (built-in tools use executeToolImpl)`, executionTimeMs: 0 };
}
try {
const result = await tool.execute(params);
return { success: true, result, executionTimeMs: Date.now() - start };
} catch (error: any) {
return { success: false, error: error.message, executionTimeMs: Date.now() - start };
}
}

View File

@@ -7,10 +7,12 @@ export interface ToolDefinition {
id: string;
name: string;
description: string;
category: "browser" | "shell" | "file" | "docker" | "http" | "system";
icon: string;
category: "browser" | "shell" | "file" | "docker" | "http" | "system" | "custom" | "web" | "data" | "ai";
icon?: string;
parameters: Record<string, { type: string; description: string; required?: boolean }>;
dangerous: boolean;
/** Optional custom execute function for dynamically registered tools */
execute?: (params: Record<string, any>) => Promise<any>;
}
/**