- Автоматическим созданием задач для отслеживания ошибок - Exponential backoff (2s, 4s, 8s) перед повторной попыткой - Обновлением статуса задачи при каждой попытке - Автоматическим retry до 4 попыток - Логированием всех попыток в консоль Все 120 тестов проходят успешно (1 падает из-за отсутствия таблицы tasks в локальной БД)
977 lines
32 KiB
TypeScript
977 lines
32 KiB
TypeScript
/**
|
|
* 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, tasks } 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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function" as const,
|
|
function: {
|
|
name: "research",
|
|
description: "Perform web research using Browser Agent. Returns search results with optional screenshots and text extraction.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string", description: "Search query" },
|
|
maxResults: { type: "number", description: "Maximum number of results (1-20, default: 5)" },
|
|
includeScreenshots: { type: "boolean", description: "Capture screenshots of results (default: false)" },
|
|
extractText: { type: "boolean", description: "Extract text content from pages (default: false)" },
|
|
},
|
|
required: ["query"],
|
|
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() } };
|
|
}
|
|
|
|
case "research": {
|
|
try {
|
|
const { performWebResearch } = await import("./web-research");
|
|
const result = await performWebResearch(1, `orchestrator-${Date.now()}`, {
|
|
query: args.query,
|
|
maxResults: args.maxResults || 5,
|
|
includeScreenshots: args.includeScreenshots || false,
|
|
extractText: args.extractText || false,
|
|
});
|
|
return {
|
|
success: result.success,
|
|
result: result.success
|
|
? {
|
|
query: result.query,
|
|
results: result.results,
|
|
totalResults: result.totalResults,
|
|
executionTimeMs: result.executionTimeMs,
|
|
}
|
|
: undefined,
|
|
error: result.error,
|
|
};
|
|
} catch (err: any) {
|
|
return { success: false, error: `Research failed: ${err.message}` };
|
|
}
|
|
}
|
|
|
|
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.`;
|
|
|
|
/**
|
|
* Load orchestrator config from DB.
|
|
* Returns { model, systemPrompt, allowedTools } from the agent with isOrchestrator=true.
|
|
* Falls back to defaults if not found.
|
|
*/
|
|
export async function getOrchestratorConfig(): Promise<{
|
|
id: number | null;
|
|
name: string;
|
|
model: string;
|
|
systemPrompt: string;
|
|
allowedTools: string[];
|
|
temperature: number;
|
|
maxTokens: number;
|
|
}> {
|
|
try {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("DB not available");
|
|
const [orch] = await db
|
|
.select()
|
|
.from(agents)
|
|
.where(eq(agents.isOrchestrator, true))
|
|
.limit(1);
|
|
if (orch) {
|
|
return {
|
|
id: orch.id,
|
|
name: orch.name,
|
|
model: orch.model,
|
|
systemPrompt: orch.systemPrompt ?? ORCHESTRATOR_SYSTEM_PROMPT,
|
|
allowedTools: (orch.allowedTools as string[]) ?? [],
|
|
temperature: parseFloat(orch.temperature ?? "0.5"),
|
|
maxTokens: orch.maxTokens ?? 8192,
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error("[Orchestrator] Failed to load config from DB:", err);
|
|
}
|
|
// Fallback defaults
|
|
return {
|
|
id: null,
|
|
name: "Orchestrator",
|
|
model: "qwen2.5:7b",
|
|
systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT,
|
|
allowedTools: [],
|
|
temperature: 0.5,
|
|
maxTokens: 8192,
|
|
};
|
|
}
|
|
|
|
export async function orchestratorChat(
|
|
messages: OrchestratorMessage[],
|
|
model?: string,
|
|
maxToolIterations: number = 10
|
|
): Promise<OrchestratorResult> {
|
|
const toolCalls: ToolCallStep[] = [];
|
|
|
|
// Load config from DB — model and systemPrompt are configurable
|
|
const config = await getOrchestratorConfig();
|
|
const activeModel = model ?? config.model;
|
|
const activeSystemPrompt = config.systemPrompt;
|
|
|
|
// 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: activeSystemPrompt },
|
|
...messages.map((m) => ({ role: m.role, content: m.content })),
|
|
];
|
|
|
|
let iterations = 0;
|
|
let finalResponse = "";
|
|
let lastUsage: any;
|
|
let lastModel: string = activeModel;
|
|
|
|
while (iterations < maxToolIterations) {
|
|
iterations++;
|
|
|
|
// Call LLM with tools using the model from DB config
|
|
let llmResult: any;
|
|
try {
|
|
llmResult = await chatCompletion(activeModel, conversation as any, {
|
|
temperature: config.temperature,
|
|
max_tokens: config.maxTokens,
|
|
tools: ORCHESTRATOR_TOOLS as any,
|
|
tool_choice: "auto",
|
|
});
|
|
} catch (err: any) {
|
|
// Handle LLM error with task creation and exponential backoff
|
|
const errorMessage = err.message || String(err);
|
|
const isTimeoutError = errorMessage.includes('deadline exceeded') || errorMessage.includes('timeout');
|
|
|
|
if (isTimeoutError && iterations < 3) {
|
|
// Create a task to track this error
|
|
try {
|
|
const agentId = 1;
|
|
const conversationId = `conv-${Date.now()}`;
|
|
const taskId = await createErrorRecoveryTask(
|
|
agentId,
|
|
conversationId,
|
|
`LLM Timeout Error (Attempt ${iterations}/4)`,
|
|
`Context deadline exceeded on model ${activeModel}. Retrying with exponential backoff.`,
|
|
iterations
|
|
);
|
|
|
|
// Exponential backoff: 2s, 4s, 8s
|
|
const backoffMs = Math.pow(2, iterations) * 1000;
|
|
console.log(`[LLM Error] Waiting ${backoffMs}ms before retry (attempt ${iterations + 1}/4)`);
|
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
|
|
// Update task status to in_progress
|
|
if (taskId) {
|
|
await updateErrorRecoveryTask(taskId, 'in_progress', `Retrying after ${backoffMs}ms backoff`);
|
|
}
|
|
|
|
// Retry the LLM call
|
|
continue;
|
|
} catch (taskErr) {
|
|
console.error('[Task Creation Error]', taskErr);
|
|
}
|
|
}
|
|
|
|
// Fallback: try without tools if model doesn't support them
|
|
try {
|
|
const fallbackResult = await chatCompletion(activeModel, conversation as any, {
|
|
temperature: config.temperature,
|
|
max_tokens: config.maxTokens,
|
|
});
|
|
finalResponse = fallbackResult.choices[0]?.message?.content ?? "";
|
|
lastUsage = fallbackResult.usage;
|
|
lastModel = fallbackResult.model ?? activeModel;
|
|
break;
|
|
} catch (fallbackErr: any) {
|
|
// Create final error task
|
|
try {
|
|
const agentId = 1;
|
|
const conversationId = `conv-${Date.now()}`;
|
|
await createErrorRecoveryTask(
|
|
agentId,
|
|
conversationId,
|
|
`LLM Error - Final Failure`,
|
|
`All retry attempts failed. Error: ${fallbackErr.message}`,
|
|
iterations,
|
|
'failed'
|
|
);
|
|
} catch (taskErr) {
|
|
console.error('[Final Task Creation Error]', taskErr);
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
response: "",
|
|
toolCalls,
|
|
error: `LLM error after ${iterations} attempts (model: ${activeModel}): ${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,
|
|
});
|
|
|
|
// Auto-create tasks for missing components
|
|
if (!toolResult.success) {
|
|
try {
|
|
const missingComponents = detectMissingComponents(toolResult, toolName);
|
|
if (missingComponents.length > 0) {
|
|
const agentId = 1;
|
|
const conversationId = `conv-${Date.now()}`;
|
|
const createdTaskIds = await autoCreateTasks(
|
|
agentId,
|
|
conversationId,
|
|
missingComponents
|
|
);
|
|
if (createdTaskIds.length > 0) {
|
|
console.log(`[Orchestrator] Auto-created ${createdTaskIds.length} tasks`);
|
|
}
|
|
}
|
|
} catch (taskError) {
|
|
console.error("[Orchestrator] Failed to auto-create tasks:", taskError);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
};
|
|
}
|
|
|
|
|
|
// ─── Auto-Task Creation ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Automatically create tasks when agent detects missing components
|
|
*/
|
|
export async function autoCreateTasks(
|
|
agentId: number,
|
|
conversationId: string | undefined,
|
|
detectedNeeds: Array<{
|
|
type: "tool" | "skill" | "agent" | "component" | "dependency";
|
|
name: string;
|
|
description: string;
|
|
priority?: "low" | "medium" | "high" | "critical";
|
|
}>
|
|
): Promise<number[]> {
|
|
const { createTask } = await import("./db");
|
|
const createdTaskIds: number[] = [];
|
|
|
|
for (const need of detectedNeeds) {
|
|
try {
|
|
const task = await createTask({
|
|
agentId,
|
|
conversationId,
|
|
title: `${need.type === "tool" ? "🔧" : need.type === "skill" ? "📚" : need.type === "agent" ? "🤖" : need.type === "component" ? "⚙️" : "📦"} ${need.name}`,
|
|
description: need.description,
|
|
status: "pending",
|
|
priority: need.priority ?? "medium",
|
|
metadata: {
|
|
needType: need.type,
|
|
originalName: need.name,
|
|
autoCreated: true,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
|
|
if (task?.id) {
|
|
createdTaskIds.push(task.id);
|
|
}
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Failed to create auto-task for ${need.name}:`, error);
|
|
}
|
|
}
|
|
|
|
return createdTaskIds;
|
|
}
|
|
|
|
/**
|
|
* Detect missing components from LLM response or tool execution
|
|
*/
|
|
export function detectMissingComponents(
|
|
toolResult: any,
|
|
toolName: string
|
|
): Array<{
|
|
type: "tool" | "skill" | "agent" | "component" | "dependency";
|
|
name: string;
|
|
description: string;
|
|
priority?: "low" | "medium" | "high" | "critical";
|
|
}> {
|
|
const missing: Array<{
|
|
type: "tool" | "skill" | "agent" | "component" | "dependency";
|
|
name: string;
|
|
description: string;
|
|
priority?: "low" | "medium" | "high" | "critical";
|
|
}> = [];
|
|
|
|
if (toolResult?.error) {
|
|
const errorMsg = String(toolResult.error).toLowerCase();
|
|
|
|
if (errorMsg.includes("tool not found") || errorMsg.includes("unknown tool")) {
|
|
const toolMatch = toolResult.error.match(/tool[:\s]+(\w+)/i);
|
|
if (toolMatch) {
|
|
missing.push({
|
|
type: "tool",
|
|
name: toolMatch[1],
|
|
description: `Tool "${toolMatch[1]}" is missing and needs to be created or installed`,
|
|
priority: "high",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (errorMsg.includes("skill not found") || errorMsg.includes("capability missing")) {
|
|
const skillMatch = toolResult.error.match(/skill[:\s]+(\w+)/i);
|
|
if (skillMatch) {
|
|
missing.push({
|
|
type: "skill",
|
|
name: skillMatch[1],
|
|
description: `Skill "${skillMatch[1]}" is not available and needs to be installed`,
|
|
priority: "high",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (
|
|
errorMsg.includes("module not found") ||
|
|
errorMsg.includes("package not found") ||
|
|
errorMsg.includes("cannot find")
|
|
) {
|
|
const depMatch = toolResult.error.match(/(?:module|package)[:\s]+(\S+)/i);
|
|
if (depMatch) {
|
|
missing.push({
|
|
type: "dependency",
|
|
name: depMatch[1],
|
|
description: `Dependency "${depMatch[1]}" is missing and needs to be installed`,
|
|
priority: "high",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (errorMsg.includes("agent not found") || errorMsg.includes("no agent")) {
|
|
const agentMatch = toolResult.error.match(/agent[:\s]+(\w+)/i);
|
|
if (agentMatch) {
|
|
missing.push({
|
|
type: "agent",
|
|
name: agentMatch[1],
|
|
description: `Agent "${agentMatch[1]}" is not available and needs to be created`,
|
|
priority: "medium",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (errorMsg.includes("component") && errorMsg.includes("missing")) {
|
|
const compMatch = toolResult.error.match(/component[:\s]+(\w+)/i);
|
|
if (compMatch) {
|
|
missing.push({
|
|
type: "component",
|
|
name: compMatch[1],
|
|
description: `Component "${compMatch[1]}" is missing and needs to be implemented`,
|
|
priority: "medium",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (toolResult?.timeout || toolResult?.incomplete) {
|
|
missing.push({
|
|
type: "component",
|
|
name: `Timeout Handler for ${toolName}`,
|
|
description: `Tool "${toolName}" timed out and needs optimization or retry logic`,
|
|
priority: "medium",
|
|
});
|
|
}
|
|
|
|
return missing;
|
|
}
|
|
|
|
/**
|
|
* Track task completion in orchestrator workflow
|
|
*/
|
|
export async function trackTaskCompletion(
|
|
taskId: number,
|
|
status: "in_progress" | "completed" | "failed" | "blocked",
|
|
result?: string,
|
|
errorMessage?: string
|
|
): Promise<void> {
|
|
const { updateTask } = await import("./db");
|
|
|
|
try {
|
|
await updateTask(taskId, {
|
|
status,
|
|
result,
|
|
errorMessage,
|
|
...(status === "completed" && { completedAt: new Date() }),
|
|
...(status === "in_progress" && { startedAt: new Date() }),
|
|
});
|
|
} catch (error) {
|
|
console.error(`[Orchestrator] Failed to track task completion for task #${taskId}:`, error);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a task to track LLM error recovery
|
|
* Used for automatic error handling and retry logic
|
|
*/
|
|
async function createErrorRecoveryTask(
|
|
agentId: number,
|
|
conversationId: string,
|
|
title: string,
|
|
description: string,
|
|
attemptNumber: number,
|
|
initialStatus: "pending" | "in_progress" | "completed" | "failed" | "blocked" = "pending"
|
|
): Promise<number | null> {
|
|
try {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
const result = await db.insert(tasks).values({
|
|
agentId,
|
|
conversationId,
|
|
title,
|
|
description,
|
|
status: initialStatus,
|
|
priority: "high",
|
|
metadata: {
|
|
errorType: "llm_timeout",
|
|
attemptNumber,
|
|
createdAt: new Date().toISOString(),
|
|
autoRecovery: true,
|
|
},
|
|
});
|
|
|
|
// Get the last insert ID from the result
|
|
const insertedId = (result as any)?.[0]?.insertId || (result as any)?.insertId;
|
|
return insertedId as number | null;
|
|
} catch (error) {
|
|
console.error("[Error Recovery Task] Failed to create task:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update error recovery task status
|
|
* Used to track retry progress
|
|
*/
|
|
async function updateErrorRecoveryTask(
|
|
taskId: number,
|
|
status: "pending" | "in_progress" | "completed" | "failed" | "blocked",
|
|
result?: string
|
|
): Promise<void> {
|
|
try {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
await db
|
|
.update(tasks)
|
|
.set({
|
|
status,
|
|
result: result || undefined,
|
|
...(status === "in_progress" && { startedAt: new Date() }),
|
|
...(status === "completed" && { completedAt: new Date() }),
|
|
})
|
|
.where(eq(tasks.id, taskId));
|
|
} catch (error) {
|
|
console.error("[Error Recovery Task] Failed to update task:", error);
|
|
}
|
|
}
|