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

357 lines 19.5 kB
/** * Managed Agent MCP tools — Anthropic Claude Managed Agents as a *cloud* * agent runtime alongside ruflo's local WASM-sandboxed agents (`rvagent` / * `wasm_agent_*`). See ADR-115. * * Wraps the Managed Agents REST API (beta, `anthropic-beta: * managed-agents-2026-04-01`) with plain `fetch` — no new SDK dependency. * Needs `ANTHROPIC_API_KEY` (or `CLAUDE_API_KEY`); every tool degrades * gracefully with a structured error when the key is absent so the CLI/MCP * server stays up. * * Lifecycle (mirrors `wasm_agent_*`): * managed_agent_create → agents.create + environments.create + sessions.create ↔ wasm_agent_create * managed_agent_prompt → events.send(user.message) + poll events until idle ↔ wasm_agent_prompt * managed_agent_status → GET /v1/sessions/{id} * managed_agent_events → GET /v1/sessions/{id}/events (full transcript) ↔ wasm_agent_files (artifacts/log) * managed_agent_list → GET /v1/sessions * managed_agent_terminate→ DELETE /v1/sessions/{id} (+ optionally the env) ↔ wasm_agent_terminate */ const API_BASE = process.env.ANTHROPIC_BASE_URL?.replace(/\/$/, '') || 'https://api.anthropic.com'; const BETA_HEADER = 'managed-agents-2026-04-01'; const ANTHROPIC_VERSION = '2023-06-01'; const DEFAULT_MODEL = 'claude-sonnet-4-6'; const POLL_INTERVAL_MS = 1500; const DEFAULT_MAX_WAIT_MS = 180_000; // 3 min — long enough for a real task, bounded so a tool call never hangs forever function apiKey() { return process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY || null; } function headers(key) { return { 'x-api-key': key, 'anthropic-version': ANTHROPIC_VERSION, 'anthropic-beta': BETA_HEADER, 'content-type': 'application/json', }; } const NEEDS_KEY = { error: 'managed-agent runtime needs ANTHROPIC_API_KEY (or CLAUDE_API_KEY) and Claude Managed Agents beta access. ' + 'For a local, no-key agent runtime use wasm_agent_create instead (rvagent / WASM sandbox).', }; async function maRequest(method, path, body) { const key = apiKey(); if (!key) return { ok: false, status: 0, error: NEEDS_KEY.error }; let res; try { res = await fetch(`${API_BASE}/v1${path}`, { method, headers: headers(key), body: body !== undefined ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(30_000), }); } catch (e) { return { ok: false, status: 0, error: `network error calling ${method} /v1${path}: ${e.message}` }; } const text = await res.text(); let parsed = undefined; try { parsed = text ? JSON.parse(text) : undefined; } catch { /* leave undefined */ } if (!res.ok) { const apiErr = parsed?.error?.message ?? parsed?.message ?? text.slice(0, 300); return { ok: false, status: res.status, error: `Managed Agents API ${res.status}: ${apiErr}`, body: parsed }; } return { ok: true, status: res.status, data: parsed }; } // ---- helpers -------------------------------------------------------------- function summarizeEvents(events) { let assistantText = ''; const toolUses = []; let status = 'unknown'; let stopReason = null; for (const e of events) { if (e.type === 'agent.message' && Array.isArray(e.content)) { assistantText += e.content.filter(b => b.type === 'text').map(b => b.text ?? '').join(''); } else if (e.type === 'agent.tool_use') { toolUses.push({ name: e.name ?? '?', input: e.input }); } else if (e.type === 'session.status_running') { status = 'running'; } else if (e.type === 'session.status_idle') { status = 'idle'; stopReason = e.stop_reason?.type ?? null; } else if (e.type === 'session.status_error' || e.type === 'session.status_failed') { status = 'error'; } } return { assistantText, toolUses, status, stopReason, eventCount: events.length }; } async function fetchSessionEvents(sessionId) { const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}/events`); if (!r.ok) return []; const d = r.data; return (d.data ?? d.events ?? []); } // Wait until the session is no longer "running": poll the event log for a // terminal session.status_* event. Bounded by maxWaitMs. async function waitForIdle(sessionId, maxWaitMs) { const deadline = Date.now() + Math.max(1_000, Math.min(maxWaitMs, 600_000)); let events = []; while (Date.now() < deadline) { events = await fetchSessionEvents(sessionId); const terminal = events.some(e => e.type === 'session.status_idle' || e.type === 'session.status_error' || e.type === 'session.status_failed'); // Also stop early if the session record itself reports a non-running status if (terminal) return { done: true, events }; await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); } return { done: false, events }; } function maName(prefix, given) { if (typeof given === 'string' && given.trim()) return given.trim().slice(0, 80); return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; } // --------------------------------------------------------------------------- export const managedAgentTools = [ { name: 'managed_agent_create', description: 'Spin up an Anthropic-managed cloud agent (Agent + Environment + Session) — the CLOUD counterpart of wasm_agent_create. Use when wasm_agent_create (local WASM sandbox) is wrong because the task is long-running/async (minutes-hours), needs a real cloud container with pre-installed packages + network, or persistent filesystem + transcript across turns. For a fast, free, ephemeral, offline agent use wasm_agent_create (rvagent). Needs ANTHROPIC_API_KEY + Managed Agents beta access. Returns {sessionId, agentId, environmentId}; pair with managed_agent_prompt.', category: 'agent', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Agent name (default: auto)' }, model: { type: 'string', description: 'Model id (default: claude-sonnet-4-6)' }, system: { type: 'string', description: 'System prompt' }, title: { type: 'string', description: 'Session title' }, mcpServers: { type: 'array', description: 'MCP servers to expose to the agent — each {type:"url", url, name, authorization_token?}. NOTE: the cloud agent must be able to *reach* the URL (a local `ruflo mcp start` is not reachable from Anthropic\'s cloud — deploy/tunnel it).', items: { type: 'object' }, }, skills: { type: 'array', description: 'Skills to attach to the agent', items: { type: 'object' } }, packages: { type: 'object', description: 'Environment packages: {pip?:[], npm?:[], apt?:[], cargo?:[], gem?:[], go?:[]}' }, networking: { type: 'string', enum: ['unrestricted', 'restricted', 'none'], description: 'Environment networking (default: unrestricted)' }, initScript: { type: 'string', description: 'Environment init script (bash, run at container start)' }, }, }, handler: async (input) => { if (!apiKey()) return { success: false, ...NEEDS_KEY }; // 1. Agent const agentBody = { name: maName('ruflo-managed', input.name), model: typeof input.model === 'string' && input.model ? input.model : DEFAULT_MODEL, tools: [{ type: 'agent_toolset_20260401' }], }; if (typeof input.system === 'string') agentBody.system = input.system; if (Array.isArray(input.mcpServers) && input.mcpServers.length) agentBody.mcp_servers = input.mcpServers; if (Array.isArray(input.skills) && input.skills.length) agentBody.skills = input.skills; const a = await maRequest('POST', '/agents', agentBody); if (!a.ok) return { success: false, stage: 'agent', error: a.error }; // 2. Environment const net = input.networking || 'unrestricted'; const envBody = { name: maName('ruflo-managed-env', input.name), config: { type: 'cloud', networking: { type: net } }, }; if (input.packages && typeof input.packages === 'object') envBody.config.packages = { type: 'packages', ...input.packages }; if (typeof input.initScript === 'string' && input.initScript) envBody.config.init_script = input.initScript; const e = await maRequest('POST', '/environments', envBody); if (!e.ok) return { success: false, stage: 'environment', agentId: a.data.id, error: e.error }; // 3. Session const s = await maRequest('POST', '/sessions', { agent: a.data.id, environment_id: e.data.id, title: maName('ruflo-managed session', input.title), }); if (!s.ok) return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error }; return { success: true, runtime: 'managed', sessionId: s.data.id, agentId: a.data.id, agentVersion: a.data.version, environmentId: e.data.id, status: s.data.status ?? 'idle', model: agentBody.model, note: 'Cloud agent provisioned. Send work with managed_agent_prompt({sessionId, message}); inspect with managed_agent_events; clean up with managed_agent_terminate (it bills container time + tokens until you do).', }; }, }, { name: 'managed_agent_prompt', description: 'Send a user turn to a managed cloud-agent session and wait for it to go idle, returning the assistant text + a tool-use trace — the CLOUD counterpart of wasm_agent_prompt. Use when wasm_agent_prompt (local WASM) is wrong because the work is long-running, needs the cloud container, or must persist across turns. Polls the session event log up to maxWaitMs (default 180s); for very long tasks raise maxWaitMs or follow up with managed_agent_events. Pair with managed_agent_create (for sessionId).', category: 'agent', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session id from managed_agent_create' }, message: { type: 'string', description: 'The user turn / task for the agent' }, maxWaitMs: { type: 'number', description: 'Max ms to wait for the session to go idle (default 180000, capped at 600000)' }, }, required: ['sessionId', 'message'], }, handler: async (input) => { if (!apiKey()) return { success: false, ...NEEDS_KEY }; const sessionId = String(input.sessionId ?? ''); const message = String(input.message ?? ''); if (!sessionId) return { success: false, error: 'sessionId is required' }; if (!message) return { success: false, error: 'message is required' }; const send = await maRequest('POST', `/sessions/${encodeURIComponent(sessionId)}/events`, { events: [{ type: 'user.message', content: [{ type: 'text', text: message }] }], }); if (!send.ok) return { success: false, stage: 'send', sessionId, error: send.error }; const maxWait = typeof input.maxWaitMs === 'number' && input.maxWaitMs > 0 ? input.maxWaitMs : DEFAULT_MAX_WAIT_MS; const { done, events } = await waitForIdle(sessionId, maxWait); const sum = summarizeEvents(events); return { success: true, runtime: 'managed', sessionId, finished: done, status: sum.status, stopReason: sum.stopReason, assistantText: sum.assistantText, toolUses: sum.toolUses, eventCount: sum.eventCount, note: done ? undefined : `Session still running after ${maxWait}ms — call managed_agent_events({sessionId}) to keep watching, or managed_agent_prompt again to steer.`, }; }, }, { name: 'managed_agent_status', description: 'Get the lifecycle state of a managed cloud-agent session: idle/running/error, title, last error. Use when native conversation memory is wrong because you need the cloud session\'s server-side status across turns rather than guessing. For a local WASM agent use wasm_agent_list. Pair with managed_agent_events for the full transcript.', category: 'agent', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session id' } }, required: ['sessionId'] }, handler: async (input) => { if (!apiKey()) return { ...NEEDS_KEY }; const sessionId = String(input.sessionId ?? ''); if (!sessionId) return { error: 'sessionId is required' }; const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}`); if (!r.ok) return { sessionId, error: r.error }; return { runtime: 'managed', sessionId: r.data.id, status: r.data.status, title: r.data.title, error: r.data.error ?? null, environmentId: r.data.environment_id }; }, }, { name: 'managed_agent_events', description: 'Fetch the full server-persisted event log of a managed cloud-agent session (user turns, agent thinking, tool_use, tool_result, status) — the transcript/artifact view, the CLOUD counterpart of wasm_agent_files. Use when native Read is wrong because the work happened in Anthropic\'s cloud container, not on disk. For a local WASM agent\'s filesystem use wasm_agent_files. Returns the events plus a summary (assistantText, toolUses).', category: 'agent', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session id' }, raw: { type: 'boolean', description: 'Include the full raw event objects (default: summary + compact list)' }, }, required: ['sessionId'], }, handler: async (input) => { if (!apiKey()) return { ...NEEDS_KEY }; const sessionId = String(input.sessionId ?? ''); if (!sessionId) return { error: 'sessionId is required' }; const events = await fetchSessionEvents(sessionId); const sum = summarizeEvents(events); const compact = events.map(e => { if (e.type === 'agent.message') return { type: e.type, text: (e.content ?? []).filter(b => b.type === 'text').map(b => b.text ?? '').join('').slice(0, 500) }; if (e.type === 'agent.tool_use') return { type: e.type, name: e.name, input: e.input }; if (e.type === 'agent.tool_result') return { type: e.type, content: JSON.stringify(e.content ?? {}).slice(0, 500) }; return { type: e.type, ...(e.stop_reason ? { stop_reason: e.stop_reason } : {}) }; }); return { runtime: 'managed', sessionId, status: sum.status, stopReason: sum.stopReason, assistantText: sum.assistantText, toolUses: sum.toolUses, eventCount: sum.eventCount, events: input.raw ? events : compact, }; }, }, { name: 'managed_agent_list', description: 'List managed cloud-agent sessions on this Anthropic org (id, status, title) — the CLOUD counterpart of wasm_agent_list. Use when native conversation memory is wrong because you need to see which cloud sessions exist (and which are still running / billing) across turns. For local WASM agents use wasm_agent_list. Pair with managed_agent_terminate to clean up idle sessions.', category: 'agent', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max sessions to return (default 50)' } } }, handler: async (input) => { if (!apiKey()) return { ...NEEDS_KEY, sessions: [], total: 0 }; const limit = typeof input.limit === 'number' && input.limit > 0 ? Math.min(Math.floor(input.limit), 200) : 50; const r = await maRequest('GET', `/sessions?limit=${limit}`); if (!r.ok) return { sessions: [], total: 0, error: r.error }; const list = (r.data.data ?? r.data.sessions ?? []); return { runtime: 'managed', sessions: list.map(s => ({ sessionId: s.id, status: s.status, title: s.title, environmentId: s.environment_id })), total: list.length, }; }, }, { name: 'managed_agent_terminate', description: 'Delete a managed cloud-agent session (stops billing for it) — the CLOUD counterpart of wasm_agent_terminate. Use when native nothing applies because a cloud session keeps billing container time + tokens until deleted. For a local WASM agent use wasm_agent_terminate. Optionally also deletes the session\'s environment. Always call this when done with a managed agent.', category: 'agent', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session id to delete' }, environmentId: { type: 'string', description: 'Optional: also delete this environment (the one returned by managed_agent_create)' }, }, required: ['sessionId'], }, handler: async (input) => { if (!apiKey()) return { success: false, ...NEEDS_KEY }; const sessionId = String(input.sessionId ?? ''); if (!sessionId) return { success: false, error: 'sessionId is required' }; const s = await maRequest('DELETE', `/sessions/${encodeURIComponent(sessionId)}`); const result = { runtime: 'managed', sessionId, sessionDeleted: s.ok }; if (!s.ok) result.error = s.error; if (typeof input.environmentId === 'string' && input.environmentId) { const e = await maRequest('DELETE', `/environments/${encodeURIComponent(String(input.environmentId))}`); result.environmentDeleted = e.ok; if (!e.ok) result.environmentError = e.error; } result.success = s.ok; return result; }, }, ]; //# sourceMappingURL=managed-agent-tools.js.map