UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

463 lines 17.6 kB
/** * RuVector Agent WASM Integration * * Wraps @ruvector/rvagent-wasm for sandboxed AI agent execution. * Provides WasmAgent lifecycle, gallery templates, RVF container building, * and MCP server bridge — all running in WASM without OS access. * * Published API (v0.1.0): WasmAgent, WasmGallery, WasmMcpServer, * WasmRvfBuilder, JsModelProvider, initSync. * * @module @claude-flow/cli/ruvector/agent-wasm */ import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; // ── WASM Module Detection & Init ───────────────────────────── let _wasmReady = false; /** * Check if @ruvector/rvagent-wasm is installed and loadable. */ export async function isAgentWasmAvailable() { try { const mod = await import('@ruvector/rvagent-wasm'); return typeof mod.WasmAgent === 'function'; } catch { return false; } } /** * Initialize the WASM module for Node.js. Safe to call multiple times. * Uses initSync with file-loaded WASM bytes (browser fetch doesn't work in Node). */ export async function initAgentWasm() { if (_wasmReady) return; try { const mod = await import('@ruvector/rvagent-wasm'); // In Node.js, load WASM bytes from disk and use initSync const require_ = createRequire(import.meta.url); const wasmPath = require_.resolve('@ruvector/rvagent-wasm/rvagent_wasm_bg.wasm'); const wasmBytes = readFileSync(wasmPath); mod.initSync(wasmBytes); _wasmReady = true; } catch (err) { throw new Error(`Failed to initialize @ruvector/rvagent-wasm: ${err}`); } } // ── Agent Registry ─────────────────────────────────────────── const agents = new Map(); let nextId = 1; function generateId() { return `wasm-agent-${nextId++}-${Date.now().toString(36)}`; } // ── Agent Lifecycle ────────────────────────────────────────── /** * Create a new sandboxed WASM agent. */ export async function createWasmAgent(config = {}) { await initAgentWasm(); const mod = await import('@ruvector/rvagent-wasm'); // #1810 — was hardcoded `anthropic:claude-sonnet-4-20250514`. Updated to // current Sonnet (4.6) so new gallery agents don't silently inherit a // year-old model. Callers can still override via `config.model`. const configJson = JSON.stringify({ model: config.model ?? 'anthropic:claude-sonnet-4-6', instructions: config.instructions ?? 'You are a helpful coding assistant.', max_turns: config.maxTurns ?? 50, }); const agent = new mod.WasmAgent(configJson); // ADR-129 P1 — wire JsModelProvider so the WASM runtime routes prompts // through the v3 provider system instead of returning the echo stub. // attachJsModelProvider is a no-op when no provider keys are set. await attachJsModelProvider(agent, config); const id = generateId(); const info = { id, state: 'idle', config, model: agent.model(), turnCount: agent.turn_count(), fileCount: agent.file_count(), isStopped: agent.is_stopped(), createdAt: new Date().toISOString(), }; agents.set(id, { agent, info }); return info; } /** * Wire a JsModelProvider to a freshly created WasmAgent so its internal * conversation loop dispatches through the v3 provider system (ADR-129 P1). * * The callback bridges the JsModelProvider JSON contract to * callAnthropicMessages, which already handles Anthropic / OpenRouter / * Ollama routing via RUFLO_PROVIDER + key-presence precedence (#2042). * * Called once at agent-creation time; the provider stays attached for the * agent's lifetime. No-op (returns false) when no provider keys are * configured so the echo-fallback path below is preserved for keyless * environments. */ async function attachJsModelProvider(agent, config) { const hasAny = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || process.env.OLLAMA_API_KEY); if (!hasAny) return false; const mod = await import('@ruvector/rvagent-wasm'); const { callAnthropicMessages, resolveAnthropicModel } = await import('../mcp-tools/agent-execute-core.js'); const model = resolveAnthropicModel(config.model); const systemPrompt = config.instructions || 'You are a helpful coding assistant running in a Ruflo WASM agent sandbox.'; const provider = new mod.JsModelProvider(async (messagesJson) => { const messages = JSON.parse(messagesJson); const lastUser = [...messages].reverse().find(m => m.role === 'user'); const prompt = lastUser?.content ?? messagesJson; const result = await callAnthropicMessages({ prompt, systemPrompt, model, maxTokens: 2048 }); if (!result.success) throw new Error(result.error ?? 'provider call failed'); return JSON.stringify({ role: 'assistant', content: result.output ?? '' }); }); agent.set_model_provider(provider); return true; } /** * Send a prompt to a WASM agent. * * ADR-129 P1: JsModelProvider is now wired at creation time so the WASM * agent's internal conversation loop (multi-turn state, turn_count, * stop conditions) runs against a real LLM. The echo-stub detection * block is kept as a fallback for keyless environments (CI, sandboxed * test runners) — behaviour is identical to the pre-P1 path when no * provider key is set. * * Billing note: every wasm_agent_prompt call with a provider key * configured makes a billable LLM call. Use a keyless environment to * get the echo stub for cost-free sandboxing. */ export async function promptWasmAgent(agentId, input) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); entry.info.state = 'running'; try { const wasmResult = await entry.agent.prompt(input); entry.info.state = 'idle'; syncAgentInfo(entry); // Detect the WASM echo stub (present when no JsModelProvider was // attached, i.e. keyless environments). const isEchoStub = typeof wasmResult === 'string' && (wasmResult === `echo: ${input}` || /^echo: /.test(wasmResult.slice(0, 12))); if (!isEchoStub) { // JsModelProvider routed through the v3 provider system — return // the real response. turn_count was already incremented by the // WASM runtime. return wasmResult; } // Echo stub path (keyless fallback — preserved from pre-P1 behaviour). if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENROUTER_API_KEY && !process.env.OLLAMA_API_KEY) { return `${wasmResult}\n[NOTE: bundled WASM agent has no LLM; set ANTHROPIC_API_KEY (or OPENROUTER_API_KEY / OLLAMA_API_KEY) to enable real responses via the v3 provider system]`; } // Key present but provider was not attached at creation time (e.g. // agent created before a key was set in the environment). Fall // through to a direct callAnthropicMessages call as a best-effort // recovery. const { callAnthropicMessages, resolveAnthropicModel } = await import('../mcp-tools/agent-execute-core.js'); const model = resolveAnthropicModel(entry.info.config.model); const systemPrompt = entry.info.config.instructions || 'You are a helpful coding assistant running in a Ruflo WASM agent sandbox.'; const result = await callAnthropicMessages({ prompt: input, systemPrompt, model, maxTokens: 2048 }); if (!result.success) { return `${wasmResult}\n[NOTE: bundled WASM agent has no LLM; provider fallback failed: ${result.error}]`; } return result.output ?? ''; } catch (err) { entry.info.state = 'error'; throw err; } } /** * Execute a tool directly on a WASM agent's sandbox. * Tool format: {tool: 'write_file', path: '...', content: '...'} (flat, snake_case). * Available tools: read_file, write_file, edit_file, write_todos, list_files. */ const VALID_WASM_TOOLS = ['read_file', 'write_file', 'edit_file', 'write_todos', 'list_files']; export async function executeWasmTool(agentId, toolCall) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); // Validate tool name to prevent WASM panics on unknown tools const toolName = toolCall.tool; if (toolName && !VALID_WASM_TOOLS.includes(toolName)) { return { success: false, output: `Unknown tool: ${toolName}. Available: ${VALID_WASM_TOOLS.join(', ')}` }; } const result = await entry.agent.execute_tool(JSON.stringify(toolCall)); syncAgentInfo(entry); return result; } function syncAgentInfo(entry) { try { entry.info.turnCount = entry.agent.turn_count(); entry.info.fileCount = entry.agent.file_count(); entry.info.isStopped = entry.agent.is_stopped(); } catch { /* best-effort */ } } /** * Get agent info. */ export function getWasmAgent(agentId) { const entry = agents.get(agentId); if (!entry) return null; syncAgentInfo(entry); return entry.info; } /** * List all active WASM agents. */ export function listWasmAgents() { return Array.from(agents.values()).map(e => { syncAgentInfo(e); return e.info; }); } /** * Terminate a WASM agent and free resources. */ export function terminateWasmAgent(agentId) { const entry = agents.get(agentId); if (!entry) return false; try { entry.agent.free(); } catch { /* already freed */ } agents.delete(agentId); return true; } /** * Get agent state (messages, turn count, etc.) */ export function getWasmAgentState(agentId) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); return entry.agent.get_state(); } /** * Get agent tools list. */ export function getWasmAgentTools(agentId) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); return entry.agent.get_tools(); } /** * Get agent todos. */ export function getWasmAgentTodos(agentId) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); return entry.agent.get_todos(); } /** * Export the full agent state as JSON (for persistence). */ export function exportWasmState(agentId) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); return JSON.stringify({ agentState: entry.agent.get_state(), tools: entry.agent.get_tools(), todos: entry.agent.get_todos(), info: entry.info, }); } // ── MCP Server Bridge ──────────────────────────────────────── /** * Create a WASM-based MCP server for an agent. * Returns a handler function for JSON-RPC requests. * * Note: WasmMcpServer may have stability issues in v0.1.0 for * certain agent configurations. Use with a fully configured agent. */ export async function createWasmMcpServer(agentId) { const entry = agents.get(agentId); if (!entry) throw new Error(`WASM agent not found: ${agentId}`); const mod = await import('@ruvector/rvagent-wasm'); const server = new mod.WasmMcpServer(entry.agent); return (jsonRpc) => server.handle_request(jsonRpc); } // ── Gallery Templates ──────────────────────────────────────── let _gallery = null; async function getGallery() { if (_gallery) return _gallery; await initAgentWasm(); const mod = await import('@ruvector/rvagent-wasm'); _gallery = new mod.WasmGallery(); return _gallery; } /** * List all available gallery templates. * Returns objects directly (Gallery.list() returns parsed objects in v0.1.0). */ export async function listGalleryTemplates() { const gallery = await getGallery(); return gallery.list(); } /** * Get gallery template count. */ export async function getGalleryCount() { const gallery = await getGallery(); return gallery.count(); } /** * Get gallery categories with counts. */ export async function getGalleryCategories() { const gallery = await getGallery(); return gallery.getCategories(); } /** * Search gallery templates by query. Returns results with relevance scores. */ export async function searchGalleryTemplates(query) { const gallery = await getGallery(); return gallery.search(query); } /** * Get a gallery template by id. * Wraps in try/catch because WasmGallery.get() panics on unknown IDs in v0.1.0. */ export async function getGalleryTemplate(id) { const gallery = await getGallery(); try { return gallery.get(id) ?? null; } catch { return null; } } /** * Create an agent from a gallery template. */ export async function createAgentFromTemplate(templateId) { const template = await getGalleryTemplate(templateId); if (!template) throw new Error(`Gallery template not found: ${templateId}`); const systemPrompt = template.prompts?.[0]?.system_prompt; return createWasmAgent({ instructions: systemPrompt ?? `You are a ${template.name}.`, model: undefined, // Use default }); } /** * Build an RVF container with prompts, tools, skills, and MCP tool descriptors. * Uses the high-level RVF builder API (addPrompt, addTool, addSkill, addMcpTools). * * ADR-129 P2: mcpTools parameter wires builder.addMcpTools() so that * composed agents can declare which of ruflo's 314 MCP tools they need. */ export async function buildRvfContainer(opts) { await initAgentWasm(); const mod = await import('@ruvector/rvagent-wasm'); const builder = new mod.WasmRvfBuilder(); for (const p of opts.prompts ?? []) { builder.addPrompt(JSON.stringify(p)); } for (const t of opts.tools ?? []) { builder.addTool(JSON.stringify(t)); } for (const s of opts.skills ?? []) { builder.addSkill(JSON.stringify(s)); } // ADR-129 P2: pass MCP tool descriptors into the RVF container so // composed agents know which tools are available via the MCP server. if (opts.mcpTools && opts.mcpTools.length > 0) { builder.addMcpTools(JSON.stringify(opts.mcpTools)); } return builder.build(); } // ── ADR-129 P3 — Additional gallery methods ────────────────────────────────── /** Load a template as raw RVF bytes. */ export async function galleryLoadRvf(id) { const gallery = await getGallery(); return gallery.loadRvf(id); } /** Apply configuration overrides to the active template. */ export async function galleryConfigure(configJson) { const gallery = await getGallery(); gallery.configure(configJson); } /** List templates filtered by category. */ export async function galleryListByCategory(category) { const gallery = await getGallery(); return gallery.listByCategory(category); } /** Add a custom template to the gallery. */ export async function galleryAddCustom(templateJson) { const gallery = await getGallery(); gallery.addCustom(templateJson); } /** Remove a custom template by ID. */ export async function galleryRemoveCustom(id) { const gallery = await getGallery(); gallery.removeCustom(id); } /** Import custom templates from JSON. Returns the count imported. */ export async function galleryImportCustom(templatesJson) { const gallery = await getGallery(); return gallery.importCustom(templatesJson); } /** Export all custom templates as JSON. */ export async function galleryExportCustom() { const gallery = await getGallery(); return gallery.exportCustom(); } /** Get the currently active template ID. */ export async function galleryGetActive() { const gallery = await getGallery(); return gallery.getActive(); } /** Get configuration overrides for the active template. */ export async function galleryGetConfig() { const gallery = await getGallery(); return gallery.getConfig(); } /** Reset a WASM agent — clears messages and turn count. */ export function resetWasmAgent(agentId) { const entry = agents.get(agentId); if (!entry) return false; try { entry.agent.reset(); syncAgentInfo(entry); } catch { /* best-effort */ } return true; } /** * Build an RVF container from a gallery template. * * ADR-129 P2: template.mcp_tools is now passed to buildRvfContainer so it * is included via builder.addMcpTools(). Previously these descriptors were * silently dropped, leaving gallery-template agents unable to declare their * intended MCP tool access. */ export async function buildRvfFromTemplate(templateId) { const template = await getGalleryTemplate(templateId); if (!template) throw new Error(`Gallery template not found: ${templateId}`); return buildRvfContainer({ prompts: template.prompts, tools: template.tools, skills: template.skills, mcpTools: template.mcp_tools, // ADR-129 P2: was silently dropped }); } //# sourceMappingURL=agent-wasm.js.map