diff --git a/lib/ai/agent/run_turn.js b/lib/ai/agent/run_turn.js new file mode 100644 index 0000000..40c9802 --- /dev/null +++ b/lib/ai/agent/run_turn.js @@ -0,0 +1,48 @@ +import { writeFile, unlink } from 'fs/promises'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { randomUUID } from 'crypto'; +import { fileURLToPath } from 'url'; +import { runClaudeTurn } from '../claude_cli.js'; + +// Absolute path to the MCP stdio server the claude child spawns. +const STDIO_PATH = fileURLToPath(new URL('../../mcp/companion-stdio.js', import.meta.url)); + +/** + * Shared agent turn-runner: builds the per-turn MCP config (selecting the tool + * registry + injecting agent/space/view), runs one claude turn, cleans up. + * SSE/persistence stay in the route. Returns runClaudeTurn's result. + */ +export async function runAgentTurn({ + agent, persona, registryName, toolNames, spaceId = null, view = null, + sessionId, resume = false, userText, claudeExe = 'claude', home, onEvent +}) { + const agentActor = { kind: 'agent', id: agent.id, capabilities: agent.capabilities, scopes: agent.scopes }; + const mcpConfigPath = join(tmpdir(), `void-mcp-${randomUUID()}.json`); + const mcpConfig = { + mcpServers: { + void: { + command: process.execPath, + args: [STDIO_PATH], + env: { + VOID_TOOL_REGISTRY: registryName || '', + VOID_SPACE_ID: spaceId || '', + VOID_AGENT_JSON: JSON.stringify(agentActor), + VOID_VIEW_JSON: view ? JSON.stringify(view) : '', + DATABASE_URL: process.env.DATABASE_URL || '', + OLLAMA_URL: process.env.OLLAMA_URL || '' + } + } + } + }; + await writeFile(mcpConfigPath, JSON.stringify(mcpConfig)); + try { + return await runClaudeTurn({ + sessionId, resume, systemPrompt: persona, userText, + mcpConfigPath, tools: toolNames, allowedTools: toolNames, + claudeExe, home, onEvent + }); + } finally { + unlink(mcpConfigPath).catch(() => {}); + } +} diff --git a/tests/ai/agent/run_turn.test.js b/tests/ai/agent/run_turn.test.js new file mode 100644 index 0000000..16004da --- /dev/null +++ b/tests/ai/agent/run_turn.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, vi } from 'vitest'; +import { readFile } from 'fs/promises'; + +// Capture what runAgentTurn hands to runClaudeTurn (incl. the on-disk MCP config). +const captured = {}; +vi.mock('../../../lib/ai/claude_cli.js', () => ({ + runClaudeTurn: vi.fn(async (opts) => { + captured.opts = opts; + captured.cfg = JSON.parse(await readFile(opts.mcpConfigPath, 'utf8')); + return { text: 'ok', toolTrace: [], usage: null }; + }) +})); +import { runAgentTurn } from '../../../lib/ai/agent/run_turn.js'; + +describe('runAgentTurn', () => { + it('builds the MCP config (registry + tools + agent + space) and forwards to runClaudeTurn', async () => { + const out = await runAgentTurn({ + agent: { id: 'a1', slug: 'yerin', capabilities: { read: true }, scopes: {} }, + persona: 'YERIN', registryName: 'security', + toolNames: ['mcp__void__audit_log'], spaceId: null, + sessionId: 'c1', userText: 'check', claudeExe: 'claude' + }); + expect(out.text).toBe('ok'); + expect(captured.opts.systemPrompt).toBe('YERIN'); + expect(captured.opts.tools).toEqual(['mcp__void__audit_log']); + expect(captured.opts.allowedTools).toEqual(['mcp__void__audit_log']); + const env = captured.cfg.mcpServers.void.env; + expect(env.VOID_TOOL_REGISTRY).toBe('security'); + expect(env.VOID_SPACE_ID).toBe(''); + expect(JSON.parse(env.VOID_AGENT_JSON).id).toBe('a1'); + }); +});