Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions v3/@claude-flow/cli/__tests__/ruvector/agent-wasm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,22 @@ describe('agent-wasm integration', () => {
it('throws for unknown agent', async () => {
await expect(promptWasmAgent('nope', 'test')).rejects.toThrow('WASM agent not found');
});

it('sets model provider when ANTHROPIC_API_KEY is present', async () => {
const origKey = process.env.ANTHROPIC_API_KEY;
process.env.ANTHROPIC_API_KEY = 'test-key';
try {
const info = await createWasmAgent({ instructions: 'Test agent' });
// The mock's set_model_provider should have been called
const entry = (await import('../../src/ruvector/agent-wasm.js')) as any;
const agent = entry.getWasmAgent(info.id);
expect(agent).not.toBeNull();
terminateWasmAgent(info.id);
} finally {
if (origKey) process.env.ANTHROPIC_API_KEY = origKey;
else delete process.env.ANTHROPIC_API_KEY;
}
});
});

describe('tool execution', () => {
Expand Down
98 changes: 79 additions & 19 deletions v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync } from 'fs';
import { dirname, join, resolve } from 'path';
import type { MCPTool } from './types.js';
import { HeadlessWorkerExecutor, type HeadlessWorkerType, type HeadlessExecutionResult } from '../services/headless-worker-executor.js';

// Lazy-initialized headless executor singleton
let _executor: HeadlessWorkerExecutor | null = null;
function getExecutor(): HeadlessWorkerExecutor {
if (!_executor) {
_executor = new HeadlessWorkerExecutor(process.cwd());
}
return _executor;
}

// Real vector search functions - lazy loaded to avoid circular imports
let searchEntriesFn: ((options: {
Expand Down Expand Up @@ -3067,30 +3077,78 @@ export const hooksWorkerDispatch: MCPTool = {

activeWorkers.set(workerId, worker);

// Update worker progress in background
if (background) {
setTimeout(() => {
const w = activeWorkers.get(workerId);
if (w) {
w.progress = 50;
w.phase = 'processing';
}
}, 500);

setTimeout(() => {
const w = activeWorkers.get(workerId);
if (w) {
w.progress = 100;
w.phase = 'completed';
w.status = 'completed';
w.completedAt = new Date();
// Headless worker types that use Claude Code for real AI execution
const HEADLESS_TRIGGERS: Set<string> = new Set([
'audit', 'optimize', 'testgaps', 'document', 'ultralearn', 'refactor', 'deepdive', 'predict',
]);

// Check if this trigger has a real headless executor and Claude Code is available
const isHeadlessTrigger = HEADLESS_TRIGGERS.has(trigger);
const executor = isHeadlessTrigger ? getExecutor() : null;
const claudeAvailable = executor ? await executor.isAvailable() : false;

if (isHeadlessTrigger && claudeAvailable) {
// Real execution via HeadlessWorkerExecutor → spawn('claude', ['--print', ...])
const contextOverride = context !== 'default'
? { promptTemplate: `${context}\n\n` + (config as any).promptTemplate }
: undefined;

if (background) {
// Non-blocking: execute in background and update worker status when done
executor!.execute(trigger as HeadlessWorkerType, contextOverride)
.then((result: HeadlessExecutionResult) => {
const w = activeWorkers.get(workerId);
if (w) {
w.progress = 100;
w.phase = 'completed';
w.status = result.success ? 'completed' : 'failed';
w.completedAt = new Date();
(w as any).output = result.output;
(w as any).parsedOutput = result.parsedOutput;
(w as any).durationMs = result.durationMs;
(w as any).model = result.model;
}
})
.catch((err: Error) => {
const w = activeWorkers.get(workerId);
if (w) {
w.progress = 100;
w.phase = 'failed';
w.status = 'failed';
w.completedAt = new Date();
(w as any).error = err.message;
}
});
} else {
// Blocking: wait for result
try {
const result = await executor!.execute(trigger as HeadlessWorkerType, contextOverride);
worker.progress = 100;
worker.phase = 'completed';
worker.status = result.success ? 'completed' : 'failed';
worker.completedAt = new Date();
(worker as any).output = result.output;
(worker as any).parsedOutput = result.parsedOutput;
(worker as any).durationMs = result.durationMs;
} catch (err) {
worker.progress = 100;
worker.phase = 'failed';
worker.status = 'failed';
worker.completedAt = new Date();
(worker as any).error = err instanceof Error ? err.message : String(err);
}
}, 1500);
}
} else {
// Local workers (map, consolidate, benchmark, preload) or Claude Code not available
// These run locally without AI — mark as completed immediately
worker.progress = 100;
worker.phase = 'completed';
worker.status = 'completed';
worker.completedAt = new Date();

if (isHeadlessTrigger && !claudeAvailable) {
(worker as any).warning = 'Claude Code CLI not available — worker ran in stub mode. Install with: npm install -g @anthropic-ai/claude-code';
}
}

return {
Expand All @@ -3104,9 +3162,11 @@ export const hooksWorkerDispatch: MCPTool = {
estimatedDuration: config.estimatedDuration,
capabilities: config.capabilities,
},
status: background ? 'dispatched' : 'completed',
status: background ? 'dispatched' : (worker.status === 'completed' ? 'completed' : 'failed'),
background,
timestamp: new Date().toISOString(),
...(!(worker as any).output ? {} : { output: (worker as any).output }),
...(!(worker as any).warning ? {} : { warning: (worker as any).warning }),
};
},
};
Expand Down
81 changes: 81 additions & 0 deletions v3/@claude-flow/cli/src/ruvector/agent-wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,78 @@
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

// ── Anthropic API Client ─────────────────────────────────────

/**
* Create an Anthropic API model provider callback for WasmAgent.
*
* The callback receives a JSON string of messages from the WASM runtime,
* calls the Anthropic Messages API, and returns the response as JSON.
*
* Requires ANTHROPIC_API_KEY environment variable.
*/
function createAnthropicProvider(modelId: string, systemPrompt?: string): (messagesJson: string) => Promise<string> {
return async (messagesJson: string): Promise<string> => {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
return JSON.stringify({
role: 'assistant',
content: 'Error: ANTHROPIC_API_KEY environment variable is not set. Set it to enable LLM inference for WASM agents.',
});
}

// Parse the model identifier (e.g., "anthropic:claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514")
const model = modelId.replace(/^anthropic:/, '');

let messages: Array<{ role: string; content: string }>;
try {
messages = JSON.parse(messagesJson);
} catch {
messages = [{ role: 'user', content: messagesJson }];
}

// Ensure messages is an array
if (!Array.isArray(messages)) {
messages = [{ role: 'user', content: String(messages) }];
}

const body: Record<string, unknown> = {
model,
max_tokens: 4096,
messages,
};
if (systemPrompt) {
body.system = systemPrompt;
}

const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify(body),
});

if (!res.ok) {
const errText = await res.text().catch(() => 'unknown error');
return JSON.stringify({
role: 'assistant',
content: `API error (${res.status}): ${errText}`,
});
}

const data = await res.json() as { content?: Array<{ type: string; text?: string }> };
const text = data.content
?.filter((b: { type: string }) => b.type === 'text')
.map((b: { text?: string }) => b.text ?? '')
.join('') ?? '';

return JSON.stringify({ role: 'assistant', content: text });
};
}

// ── Types ────────────────────────────────────────────────────

export interface WasmAgentConfig {
Expand Down Expand Up @@ -117,6 +189,15 @@ export async function createWasmAgent(config: WasmAgentConfig = {}): Promise<Was
});

const agent = new mod.WasmAgent(configJson);

// Wire up the Anthropic API as the model provider so prompts
// go to a real LLM instead of echoing the input back.
const modelId = config.model ?? 'anthropic:claude-sonnet-4-20250514';
if (process.env.ANTHROPIC_API_KEY) {
const provider = createAnthropicProvider(modelId, config.instructions);
agent.set_model_provider(provider);
}

const id = generateId();

const info: WasmAgentInfo = {
Expand Down