// Claude Chat API — Bridge zum Claude Agent SDK // // Adaptiert die Logik aus scripts/claude-bridge.js fuer HTTP/WebSocket-Nutzung. // Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion) mit OAuth-Auth (Claude Max Abo). import { query } from '@anthropic-ai/claude-agent-sdk'; import { randomUUID } from 'node:crypto'; import { EventEmitter } from 'node:events'; // ============ State ============ let activeAbort = null; let currentAgentId = null; let currentModel = process.env.CLAUDE_MODEL || 'sonnet'; let agentMode = 'solo'; // solo | handlanger | experten | auto // In-Memory Sessions // Map: sessionId → { id, title, createdAt, messages: [], claudeSessionId } const sessions = new Map(); // ============ Orchestrator Prompts ============ const ORCHESTRATOR_PROMPTS = { handlanger: ` Du bist der HAUPT-AGENT im HANDLANGER-MODUS. KRITISCH: Dir stehen NUR Task + TodoWrite zur Verfuegung. Du kannst NICHT direkt lesen, suchen oder ausfuehren — du MUSST delegieren! Task-Tool mit den RICHTIGEN Sub-Agent-Typen: - "general-purpose" — Standard-Agent mit VOLLEM Tool-Zugriff (Bash, Read, Write, Grep, Glob). - "Explore" — read-only Agent. NUR fuer reine Code-/Dateisuche. Arbeitsweise: 1. Waehle den RICHTIGEN subagent_type basierend auf der Aufgabe. 2. Rufe das Task-Tool auf mit EXAKTER Anweisung. 3. Pruefe im Ergebnis das "tool_uses"-Feld. Wenn tool_uses:0 → Sub-Agent hat halluziniert! 4. Verarbeite das Ergebnis und gib dem User die Zusammenfassung. `, experten: ` Du bist der HAUPT-AGENT und arbeitest im EXPERTEN-MODUS. Task-Tool Sub-Agent-Typen (autonome Experten): - **research**: Durchsucht Code/Docs, findet Infos. - **implement**: Schreibt Code nach Best-Practices. - **test**: Schreibt und fuehrt Tests. - **review**: Prueft Code auf Qualitaet/Sicherheit. Arbeitsweise: 1. ZERLEGE die Aufgabe in Experten-Bereiche 2. DELEGIERE via Task(subagent_type: "research"|"implement"|"test"|"review", prompt: "...") 3. Formuliere das WAS, nicht das WIE 4. Integriere die Zusammenfassungen `, auto: ` Du analysierst Aufgaben und waehlst den optimalen Arbeitsmodus. Entscheide basierend auf: - SOLO: Einfache, schnelle Aufgaben - HANDLANGER: Koordinations-intensive Aufgaben - EXPERTEN: Komplexe Features `, }; // ============ Verfuegbare Modelle ============ const AVAILABLE_MODELS = [ { id: 'haiku', name: 'Claude Haiku', description: 'Schnell & guenstig' }, { id: 'sonnet', name: 'Claude Sonnet', description: 'Ausgewogen' }, { id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' }, ]; // Tools die Subagents spawnen const SUBAGENT_TOOLS = ['Task', 'Agent', 'spawn_agent', 'launch_agent']; // ============ Hilfsfunktionen ============ // AUTO-Modus: Heuristik waehlt passenden Modus function chooseAutoMode(message) { const text = (message || '').toLowerCase(); const charCount = text.length; const expertKeywords = [ 'implementiere', 'implementier ', 'refactor', 'architektur', 'entwickle', 'erstelle feature', 'feature ', 'design', 'baue ', 'optimiere', 'migration', 'umbau', 'umstruktur', ]; const handlangerKeywords = [ 'lies ', 'suche ', 'finde ', 'zeig mir ', 'untersuche', 'analysiere', 'durchsuche', 'alle dateien', 'sammle', 'liste alle', 'vergleiche', ]; if (charCount < 80) return 'solo'; if (expertKeywords.some(kw => text.includes(kw))) return 'experten'; if (handlangerKeywords.some(kw => text.includes(kw)) && charCount > 120) return 'handlanger'; if (charCount > 300) return 'handlanger'; return 'solo'; } // Subagent-Typ aus Tool-Input ermitteln function getSubagentType(toolName, input) { if (input?.subagent_type) return input.subagent_type.toLowerCase(); if (input?.agent_type) return input.agent_type.toLowerCase(); const desc = (input?.description || input?.prompt || '').toLowerCase(); if (desc.includes('explore') || desc.includes('search') || desc.includes('find')) return 'explore'; if (desc.includes('implement') || desc.includes('write') || desc.includes('code')) return 'code'; if (desc.includes('test') || desc.includes('verify')) return 'test'; if (desc.includes('review') || desc.includes('check')) return 'review'; return 'explore'; } // Tool-Input fuer Logging kuerzen function summarizeToolInput(tool, input) { if (!input) return ''; if (tool === 'Read') return input.file_path || ''; if (tool === 'Edit' || tool === 'Write') { const path = input.file_path || ''; const size = input.content ? `(${input.content.length} chars)` : ''; return `${path} ${size}`; } if (tool === 'Grep') return `"${input.pattern}" in ${input.path || '.'}`; if (tool === 'Bash') { const cmd = input.command || ''; return cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd; } if (tool === 'Task') return input.description || input.prompt || ''; const firstString = Object.values(input).find(v => typeof v === 'string'); if (firstString) return firstString.length > 50 ? firstString.substring(0, 50) + '...' : firstString; return ''; } // ============ Haupt-Funktion: Nachricht senden ============ /** * Sendet eine Nachricht an Claude und streamt Events zurueck. * Gibt einen EventEmitter zurueck der folgende Events feuert: * 'text' → { content: "..." } * 'tool_call' → { name: "...", args: {...}, id: "..." } * 'tool_result' → { id: "...", success: boolean } * 'agent_started' → { id: "...", name: "...", type: "..." } * 'agent_stopped' → { id: "...", success: boolean } * 'result' → { content: "...", cost, tokens, session_id, duration_ms, model } * 'error' → { message: "..." } * * @param {string} text - Nachricht des Users * @param {string} [model] - Modell-Override (sonst currentModel) * @param {string} [mode] - Modus-Override (sonst agentMode) * @param {string} [sessionId] - Session-ID fuer Kontext * @returns {EventEmitter} */ export function sendMessage(text, model, mode, sessionId) { const emitter = new EventEmitter(); // Asynchron starten, damit der Caller den Emitter sofort bekommt setImmediate(() => _processMessage(emitter, text, model, mode, sessionId)); return emitter; } async function _processMessage(emitter, text, model, mode, sessionId) { const useModel = model || currentModel; const useMode = mode || agentMode; currentAgentId = randomUUID(); activeAbort = new AbortController(); // Session finden oder erstellen let session = null; if (sessionId && sessions.has(sessionId)) { session = sessions.get(sessionId); } // User-Nachricht in Session speichern if (session) { session.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString(), }); } // Subagent-Tracking fuer diesen Request const activeSubagents = new Map(); const handledTools = new Set(); emitter.emit('agent_started', { id: currentAgentId, name: 'Main', type: useMode, model: useModel, }); // AUTO-Modus: effektiven Modus bestimmen let effectiveMode = useMode; if (useMode === 'auto') { effectiveMode = chooseAutoMode(text); } // Orchestrator-Prompt fuer nicht-Solo Modi let fullPrompt = text; if (effectiveMode !== 'solo' && ORCHESTRATOR_PROMPTS[effectiveMode]) { fullPrompt = `${ORCHESTRATOR_PROMPTS[effectiveMode]}\n\n---\n\n${text}`; } const startTime = Date.now(); let fullText = ''; let usedModel = useModel; try { // Query-Optionen const queryOptions = { model: useModel, maxTurns: 25, abortController: activeAbort, tools: { type: 'preset', preset: 'claude_code' }, allowedTools: ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash'], }; // Claude-Session-ID fuer Fortsetzung if (session?.claudeSessionId) { queryOptions.resume = session.claudeSessionId; } let conversation = query({ prompt: fullPrompt, options: queryOptions, }); // Tool-Use handhaben function handleToolUse(ev) { const toolId = ev.tool_use_id || ev.id || randomUUID(); if (handledTools.has(toolId)) return; handledTools.add(toolId); const toolName = ev.name || 'unknown'; const toolInput = ev.input || {}; // Subagent-Erkennung if (SUBAGENT_TOOLS.includes(toolName)) { const subagentId = randomUUID(); const subagentType = getSubagentType(toolName, toolInput); const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || ''; activeSubagents.set(toolId, { agentId: subagentId, type: subagentType, task: subagentTask, }); emitter.emit('agent_started', { id: subagentId, name: subagentType, type: subagentType, }); } emitter.emit('tool_call', { name: toolName, args: toolInput, id: toolId, summary: summarizeToolInput(toolName, toolInput), }); } // Tool-Result handhaben function handleToolResult(ev) { const toolId = ev.tool_use_id || ''; if (activeSubagents.has(toolId)) { const subagent = activeSubagents.get(toolId); emitter.emit('agent_stopped', { id: subagent.agentId, success: !ev.is_error, }); activeSubagents.delete(toolId); } emitter.emit('tool_result', { id: toolId, success: !ev.is_error, }); } // Iteration mit Fallback bei ungueltiger Session-ID async function* iterateWithRetry() { try { for await (const ev of conversation) yield ev; } catch (err) { if (queryOptions.resume) { console.log('[bridge] Resume fehlgeschlagen, starte neue Session:', err.message); delete queryOptions.resume; conversation = query({ prompt: fullPrompt, options: queryOptions }); for await (const ev of conversation) yield ev; } else { throw err; } } } for await (const event of iterateWithRetry()) { switch (event.type) { case 'assistant': if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text' && block.text) { fullText += block.text; emitter.emit('text', { content: block.text }); } else if (block.type === 'tool_use') { handleToolUse(block); } } } if (event.message?.model) { usedModel = event.message.model; } break; case 'tool_use': handleToolUse(event); break; case 'tool_result': handleToolResult(event); break; case 'user': if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'tool_result') { handleToolResult(block); } } } break; case 'result': { const durationMs = Date.now() - startTime; const usage = event.usage || {}; const inputTokens = usage.input_tokens || 0; const cacheRead = usage.cache_read_input_tokens || 0; const cacheCreation = usage.cache_creation_input_tokens || 0; const contextTokens = inputTokens + cacheRead + cacheCreation; const outputTokens = usage.output_tokens || 0; const cost = event.total_cost_usd || 0; // Claude-Session-ID speichern fuer spaetere Fortsetzung if (session && event.session_id) { session.claudeSessionId = event.session_id; } // Antwort in Session speichern if (session) { session.messages.push({ role: 'assistant', content: fullText, timestamp: new Date().toISOString(), model: usedModel, cost, }); } emitter.emit('result', { content: fullText, cost, tokens: { input: contextTokens, output: outputTokens, cache_read: cacheRead, cache_creation: cacheCreation, }, session_id: event.session_id || '', duration_ms: durationMs, model: usedModel, }); break; } default: break; } } } catch (err) { if (err.name === 'AbortError') { emitter.emit('result', { content: fullText || '(Abgebrochen)', cost: 0, tokens: { input: 0, output: 0 }, session_id: '', duration_ms: Date.now() - startTime, model: usedModel, aborted: true, }); } else { console.error('[bridge] Fehler:', err.message); emitter.emit('error', { message: err.message || String(err) }); } } finally { // Verbleibende Subagents beenden for (const [toolId, subagent] of activeSubagents) { emitter.emit('agent_stopped', { id: subagent.agentId, success: false, }); } activeSubagents.clear(); emitter.emit('agent_stopped', { id: currentAgentId, success: true }); currentAgentId = null; activeAbort = null; } } // ============ Steuerungs-Funktionen ============ /** Stoppt alle aktiven Agents */ export function stopAll() { if (activeAbort) { activeAbort.abort(); return true; } return false; } /** Aktueller Status */ export function getStatus() { return { processing: !!currentAgentId, model: currentModel, mode: agentMode, agentId: currentAgentId, uptime: Math.floor(process.uptime()), }; } /** Verfuegbare Modelle */ export function listModels() { return { current: currentModel, available: AVAILABLE_MODELS, }; } /** Modell wechseln */ export function setModel(model) { const validIds = AVAILABLE_MODELS.map(m => m.id); if (!validIds.includes(model)) { throw new Error(`Ungueltiges Modell: ${model}. Verfuegbar: ${validIds.join(', ')}`); } currentModel = model; return { model: currentModel, status: 'Modell geaendert' }; } /** Modus wechseln */ export function setMode(mode) { const validModes = ['solo', 'handlanger', 'experten', 'auto']; if (!validModes.includes(mode)) { throw new Error(`Ungueltiger Modus: ${mode}. Verfuegbar: ${validModes.join(', ')}`); } agentMode = mode; return { mode: agentMode, status: 'Modus geaendert' }; } // ============ Session-Management ============ /** Neue Session erstellen */ export function createSession(title) { const id = randomUUID(); const session = { id, title: title || `Session ${sessions.size + 1}`, createdAt: new Date().toISOString(), messages: [], claudeSessionId: null, }; sessions.set(id, session); return session; } /** Session-Liste (ohne Messages fuer Uebersicht) */ export function listSessions() { return Array.from(sessions.values()).map(s => ({ id: s.id, title: s.title, createdAt: s.createdAt, messageCount: s.messages.length, })); } /** Einzelne Session mit Messages */ export function getSession(id) { const session = sessions.get(id); if (!session) return null; return { ...session }; }