#!/usr/bin/env node // Claude Desktop — Bridge via Claude Agent SDK // // Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion) // OAuth-Auth funktioniert automatisch (Claude Max Abo) // Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe // // Modi: // stdio (Default): node claude-bridge.js // UDS-Daemon: node claude-bridge.js --socket /tmp/claude-bridge.sock import { query } from '@anthropic-ai/claude-agent-sdk'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; import { createServer } from 'node:net'; import { writeFileSync, unlinkSync, existsSync } from 'node:fs'; // ============ IPC-Modus erkennen ============ const socketArg = process.argv.indexOf('--socket'); const SOCKET_PATH = socketArg !== -1 ? process.argv[socketArg + 1] : null; const PID_PATH = SOCKET_PATH ? SOCKET_PATH.replace('.sock', '.pid') : null; const IS_DAEMON = !!SOCKET_PATH; // Aktive UDS-Clients (bei Daemon-Modus) let udsClients = new Set(); // Prozess am Leben halten + Heartbeat alle 30s const keepAlive = setInterval(() => { sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() }); }, 30000); if (!IS_DAEMON) process.stdin.resume(); // ============ State ============ let activeAbort = null; let currentAgentId = null; let currentModel = process.env.CLAUDE_MODEL || 'opus'; // Agent-Modus (solo | handlanger | experten | auto) let agentMode = 'solo'; // Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert let stickyContext = ''; // MCP-Server Configs (vom Rust-Backend oder aus .claude.json geladen) // Format: { "name": { type: "stdio", command: "...", args: [...], env: {...} } } let mcpServerConfigs = {}; // Lokales Modell (Ollama) — für einfache Tasks ohne Cloud let ollamaAvailable = false; let ollamaEndpoint = process.env.OLLAMA_URL || 'http://localhost:11434'; let ollamaModel = process.env.OLLAMA_MODEL || 'qwen2.5-coder:7b'; // ============ Orchestrator Prompts ============ const ORCHESTRATOR_PROMPTS = { handlanger: ` Du bist der HAUPT-AGENT im HANDLANGER-MODUS. KRITISCH: Dir stehen NUR Task + TodoWrite zur Verfügung. Du kannst NICHT direkt lesen, suchen oder ausführen — du MUSST delegieren! Task-Tool mit den RICHTIGEN Sub-Agent-Typen: - "general-purpose" — Standard-Agent mit VOLLEM Tool-Zugriff (Bash, Read, Write, Grep, Glob). Nutze diesen für JEDE Aufgabe die Bash/Shell benötigt (ls, cat, find, grep auf System-Ebene). - "Explore" — read-only Agent. NUR für reine Code-/Dateisuche innerhalb des Projekts. Hat KEINEN Bash-Zugriff! Nicht für Systembefehle wie "ls /etc" verwenden. Arbeitsweise (verbindlich): 1. Wähle den RICHTIGEN subagent_type basierend auf der Aufgabe. 2. Rufe das Task-Tool auf mit EXAKTER Anweisung. 3. Prüfe im Ergebnis das "tool_uses"-Feld. Wenn tool_uses:0 → Sub-Agent hat halluziniert! In dem Fall: Neuer Task-Call mit "general-purpose" statt "Explore". 4. Verarbeite das Ergebnis und gib dem User die Zusammenfassung. Halluziniere NIEMALS Dateilisten aus dem Gedächtnis — delegiere immer real. Beispiele: - "List /etc files" → Task(subagent_type:"general-purpose", prompt:"Run 'ls -1 /etc | sort' and return the output") - "Find handleError in src/" → Task(subagent_type:"Explore", prompt:"Grep for 'handleError' in src/") - "Read /etc/hosts" → Task(subagent_type:"general-purpose", prompt:"cat /etc/hosts and return output") `, experten: ` Du bist der HAUPT-AGENT und arbeitest im EXPERTEN-MODUS. WICHTIG: Du koordinierst vier autonome Experten-Agents! Task-Tool Sub-Agent-Typen (autonome Experten): - **research**: Durchsucht Code/Docs, findet Infos. Wähle diesen für "Finde heraus…", "Wo ist…" - **implement**: Schreibt Code nach Best-Practices. Wähle diesen für "Implementiere…", "Baue…" - **test**: Schreibt und führt Tests. Wähle diesen für "Teste…" - **review**: Prüft Code auf Qualität/Sicherheit. Wähle diesen für "Prüfe…" 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 — die Experten planen selbst 4. Integriere die Zusammenfassungen, orchestriere weitere Schritte Beispiel-Delegationen: - Task(subagent_type:"research", prompt:"Finde heraus wie Authentication implementiert ist") - Task(subagent_type:"implement", prompt:"Füge OAuth2-Support hinzu mit Token-Refresh") - Task(subagent_type:"test", prompt:"Teste die neue Auth-Funktionalität") - Task(subagent_type:"review", prompt:"Prüfe die OAuth-Implementierung auf Sicherheitsprobleme") `, auto: ` Du analysierst Aufgaben und wählst den optimalen Arbeitsmodus. Entscheide basierend auf: - SOLO: Einfache, schnelle Aufgaben (Typo fix, Code erklären, einzelne Datei ändern) - HANDLANGER: Koordinations-intensive Aufgaben (viele Dateien lesen, Bug in großer Codebase) - EXPERTEN: Komplexe Features (neues System implementieren, großes Refactoring) Teile deine Wahl am Anfang mit: "[Modus: X] Begründung" `, }; // ============ Custom Sub-Agent Definitionen ============ // Werden je nach Modus an query() übergeben const HANDLANGER_AGENTS = { worker: { description: 'Führt exakte Anweisungen des Haupt-Agents aus (lesen, suchen, triviale Edits). Denkt NICHT selbst, berichtet komprimiert zurück.', // Günstiges Modell — Handlanger muss nicht planen model: 'haiku', tools: ['Read', 'Grep', 'Glob', 'Bash', 'Edit', 'Write'], prompt: `Du bist ein HANDLANGER-Agent. WICHTIG: 1. Führe GENAU aus was der Haupt-Agent verlangt — denke NICHT selbst 2. Plane keine eigene Herangehensweise 3. Berichte KOMPRIMIERT zurück (max. 500 Tokens): - Bei Read: Relevante Zeilen/Passagen, keine Volltext-Dumps - Bei Grep: Liste der Treffer mit Zeilennummern - Bei Bash: Exit-Code + wichtigste Ausgabe-Zeilen - Bei Edit/Write: Bestätigung was geändert wurde 4. Keine Erklärungen, keine Vorschläge — nur das verlangte Ergebnis`, }, }; const EXPERTEN_AGENTS = { research: { description: 'Durchsucht Code und Dokumentation autonom. Findet selbst heraus was relevant ist.', model: 'inherit', tools: ['Read', 'Grep', 'Glob', 'Bash'], prompt: `Du bist ein RESEARCH-Experte. Du bekommst eine Frage — plane selbst wie du sie beantwortest: - Wähle selbst welche Dateien/Patterns zu suchen sind - Priorisiere wichtige Infos - Berichte strukturiert: Was gefunden, wo (Pfade/Zeilen), warum relevant - Max 1000 Tokens Zusammenfassung`, }, implement: { description: 'Schreibt Code-Änderungen nach Best-Practices. Entscheidet selbst über Architektur und Details.', model: 'inherit', tools: ['Read', 'Grep', 'Glob', 'Edit', 'Write', 'Bash'], prompt: `Du bist ein IMPLEMENT-Experte. Du bekommst das WAS — entscheide selbst das WIE: - Lies relevanten Code zum Verständnis - Implementiere nach Best-Practices (Codierrichtlinien des Projekts beachten) - Berichte: welche Dateien geändert, was war der Kern, was beibehalten - Max 800 Tokens Zusammenfassung`, }, test: { description: 'Schreibt und führt Tests aus. Wählt selbst sinnvolle Testfälle.', model: 'inherit', tools: ['Read', 'Grep', 'Glob', 'Edit', 'Write', 'Bash'], prompt: `Du bist ein TEST-Experte. Du bekommst ein Feature — wähle selbst passende Testfälle: - Happy Path + sinnvolle Edge Cases - Nutze vorhandene Test-Infrastruktur - Berichte: Tests geschrieben (Anzahl), was ist abgedeckt, passed/failed - Max 500 Tokens Zusammenfassung`, }, review: { description: 'Prüft Code auf Qualität, Sicherheit und Stil. Findet selbst Probleme.', model: 'inherit', tools: ['Read', 'Grep', 'Glob', 'Bash'], prompt: `Du bist ein REVIEW-Experte. Du bekommst Code zum Prüfen — finde selbst Probleme: - Sicherheit (Injections, Secrets, Auth) - Performance (N+1, unnötige Loops) - Fehlerbehandlung (Boundary-Cases) - Stil (Konsistenz mit Projekt) - Berichte strukturiert nach Schwere (kritisch/warnung/info) - Max 800 Tokens`, }, }; // Subagent-Tracking // Map: toolUseId → { agentId, parentId, type, task, depth } const activeSubagents = new Map(); // Verfügbare Modelle const AVAILABLE_MODELS = [ { id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' }, { 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']; // 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(); // Fallback basierend auf description/prompt const desc = (input?.description || input?.prompt || '').toLowerCase(); if (desc.includes('explore') || desc.includes('search') || desc.includes('find')) return 'explore'; if (desc.includes('plan') || desc.includes('design')) return 'plan'; if (desc.includes('bash') || desc.includes('command') || desc.includes('terminal')) return 'bash'; if (desc.includes('code') || desc.includes('implement') || desc.includes('write')) return 'code'; if (desc.includes('test') || desc.includes('verify')) return 'test'; if (desc.includes('review') || desc.includes('check')) return 'review'; return 'explore'; // Default } // ============ Kommunikation mit Tauri ============ function sendToTauri(msg) { const line = JSON.stringify(msg) + '\n'; if (IS_DAEMON) { // UDS-Modus: An alle verbundenen Clients senden for (const client of udsClients) { try { client.write(line); } catch (err) { process.stderr.write(`UDS-Client Schreibfehler: ${err.message}\n`); udsClients.delete(client); } } } else { // stdio-Modus: An stdout (wie bisher) process.stdout.write(line); } } function sendEvent(event, payload = {}) { sendToTauri({ type: 'event', event, payload }); } function sendResponse(id, result) { sendToTauri({ type: 'response', id, result }); } function sendError(id, error) { sendToTauri({ type: 'response', id, error }); } // ============ Monitor-Events ============ // Sendet ein Event für den System-Monitor function sendMonitorEvent(type, summary, details = {}, options = {}) { sendEvent('monitor', { type, // 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug' summary, // Einzeiler für Kompakt-Ansicht details, // Vollständige Daten agentId: options.agentId || currentAgentId, durationMs: options.durationMs, error: options.error, }); } // AUTO-Modus: Heuristik wählt passenden Modus basierend auf Aufgabe // Rückgabe: 'solo' | 'handlanger' | 'experten' function chooseAutoMode(message) { const text = (message || '').toLowerCase(); const charCount = text.length; // Keywords die klar auf Experten-Aufgaben hinweisen (komplexe, parallelisierbare Arbeit) const expertKeywords = [ 'implementiere', 'implementier ', 'refactor', 'architektur', 'entwickle', 'erstelle feature', 'feature ', 'design', 'baue ', 'optimiere', 'migration', 'umbau', 'umstruktur', ]; // Keywords die auf Handlanger-Aufgaben hinweisen (viel koordinieren/sammeln) const handlangerKeywords = [ 'lies ', 'suche ', 'finde ', 'zeig mir ', 'untersuche', 'analysiere', 'durchsuche', 'alle dateien', 'sammle', 'liste alle', 'vergleiche', ]; // Klar triviale Aufgaben → solo 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'; // Längere Nachrichten ohne klare Keywords → handlanger (safer default) if (charCount > 300) return 'handlanger'; return 'solo'; } // Tool-Input für Logging kürzen (sensitive Daten maskieren) function summarizeToolInput(tool, input) { if (!input) return ''; // Bestimmte Tools speziell behandeln 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 || ''; } // Default: Erstes String-Feld nehmen const firstString = Object.values(input).find(v => typeof v === 'string'); if (firstString) { return firstString.length > 50 ? firstString.substring(0, 50) + '...' : firstString; } return ''; } // ============ Claude Agent SDK ============ async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) { // Modell für diese Anfrage (Parameter > State > Default) const useModel = model || currentModel; // Context für diese Anfrage (Parameter > State) const useContext = contextOverride || stickyContext; currentAgentId = randomUUID(); activeAbort = new AbortController(); const isResuming = !!resumeSessionId; sendEvent('agent-started', { id: currentAgentId, type: 'Main', task: message.substring(0, 100), model: useModel, resuming: isResuming, }); // Monitor: Agent gestartet const resumeInfo = isResuming ? ' (Fortsetzung)' : ''; sendMonitorEvent('agent', `Main Agent gestartet (${useModel})${resumeInfo}`, { agentId: currentAgentId, model: useModel, task: message.substring(0, 100), contextTokens: useContext ? Math.ceil(useContext.length / 4) : 0, resumeSessionId: resumeSessionId || null, }); // Monitor: API-Request const contextInfo = useContext ? ` +${Math.ceil(useContext.length / 4)} ctx` : ''; sendMonitorEvent('api', `→ ${useModel}${contextInfo}${resumeInfo}`, { model: useModel, promptLength: message.length, contextLength: useContext?.length || 0, maxTurns: 200, resumeSessionId: resumeSessionId || null, }); // AUTO-Modus: Effektiven Modus aus der Nachricht ableiten let effectiveMode = agentMode; if (agentMode === 'auto') { effectiveMode = chooseAutoMode(message); sendEvent('auto-mode-chosen', { chosen: effectiveMode, messageLength: message.length }); sendMonitorEvent('agent', `Auto-Modus gewählt: ${effectiveMode}`, { chosen: effectiveMode, messageLength: message.length, }); } sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel, resuming: isResuming, mode: agentMode, effectiveMode }); // Orchestrator-Prompt für nicht-Solo Modi (nutzt effektiven Modus) let orchestratorPrompt = ''; if (effectiveMode !== 'solo' && ORCHESTRATOR_PROMPTS[effectiveMode]) { orchestratorPrompt = ORCHESTRATOR_PROMPTS[effectiveMode]; sendMonitorEvent('agent', `Orchestrator-Modus: ${effectiveMode}`, { mode: effectiveMode }); } // Basis-Anweisung: Sprache + Verhalten const BASE_INSTRUCTION = `WICHTIG: Antworte IMMER auf Deutsch. Denke und formuliere deine Gedanken (Thinking) ebenfalls auf Deutsch. Du bist ein technischer Assistent für Eddy (Eduard Wisch). Fasse dich kurz und präzise.`; // Nachricht mit Context, Basis-Anweisung und Orchestrator kombinieren let fullPrompt = message; if (orchestratorPrompt) { fullPrompt = `${orchestratorPrompt}\n\n---\n\n${message}`; } fullPrompt = `${BASE_INSTRUCTION}\n\n---\n\n${fullPrompt}`; if (useContext) { fullPrompt = `${useContext}\n\n---\n\n${fullPrompt}`; } const startTime = Date.now(); let fullText = ''; let usedModel = useModel; try { // Query-Optionen zusammenstellen const queryOptions = { model: useModel, maxTurns: 200, abortController: activeAbort, }; // Session-ID für Fortsetzung — SDK erwartet `resume`, nicht `sessionId` if (resumeSessionId) { queryOptions.resume = resumeSessionId; } // MCP-Server injizieren (aus App-Config oder DB geladen) if (Object.keys(mcpServerConfigs).length > 0) { queryOptions.mcpServers = mcpServerConfigs; } // In @anthropic-ai/claude-agent-sdk 0.2.104 vererbt sich JEDE tools/disallowedTools- // Konfiguration auf Sub-Agents. Es gibt keine saubere Trennung Main vs. Sub. // Daher: Tool-Preset fuer alle Modi freischalten, Restriktion via System-Prompt. queryOptions.tools = { type: 'preset', preset: 'claude_code' }; queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash']; if (effectiveMode === 'handlanger') { sendMonitorEvent('agent', 'Handlanger: Delegation per System-Prompt durchgesetzt', { mode: effectiveMode, }); } else if (effectiveMode === 'experten') { sendMonitorEvent('agent', 'Experten: Multi-Agent via System-Prompt', { mode: effectiveMode, }); } let conversation = query({ prompt: fullPrompt, options: queryOptions, }); // Auto-Retry bei transienten Fehlern (Rate-Limit, Netzwerk, 5xx) const MAX_RETRIES = 3; const RETRY_DELAYS = [2000, 5000, 10000]; // Exponentielles Backoff function isRetryableError(err) { const msg = (err?.message || String(err)).toLowerCase(); return ( msg.includes('rate limit') || msg.includes('429') || msg.includes('overloaded') || msg.includes('529') || msg.includes('500') || msg.includes('502') || msg.includes('503') || msg.includes('network') || msg.includes('econnreset') || msg.includes('timeout') || msg.includes('etimedout') || msg.includes('fetch failed') ); } // Dedupe: Manche Tool-Events kommen sowohl in assistant-Blocks // als auch als standalone tool_use Event. Via toolUseId deduplizieren. const handledTools = new Set(); // Tool-Use handhaben — funktioniert sowohl fuer standalone tool_use Events // als auch fuer tool_use Bloecke innerhalb einer assistant-Nachricht 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 || {}; if (SUBAGENT_TOOLS.includes(toolName)) { const subagentId = randomUUID(); const subagentType = getSubagentType(toolName, toolInput); const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe'; const subagentModel = toolInput.model || useModel; const depth = 1; activeSubagents.set(toolId, { agentId: subagentId, parentId: currentAgentId, type: subagentType, task: subagentTask, depth, model: subagentModel, }); sendEvent('subagent-started', { id: subagentId, parentAgentId: currentAgentId, type: subagentType, task: subagentTask.substring(0, 100), depth, model: subagentModel, toolUseId: toolId, }); } sendEvent('tool-start', { id: toolId, tool: toolName, input: toolInput, agentId: currentAgentId, }); const toolSummary = summarizeToolInput(toolName, toolInput); sendMonitorEvent('tool', `${toolName} ${toolSummary}`, { toolId, tool: toolName, input: toolInput, }); } // Tool-Result handhaben function handleToolResult(ev) { const toolId = ev.tool_use_id || ''; if (activeSubagents.has(toolId)) { const subagent = activeSubagents.get(toolId); sendEvent('subagent-stopped', { id: subagent.agentId, parentAgentId: subagent.parentId, success: !ev.is_error, toolUseId: toolId, }); activeSubagents.delete(toolId); } sendEvent('tool-end', { id: toolId, success: !ev.is_error, agentId: currentAgentId, }); } // Hilfsfunktion: Iteration mit Fallback bei ungueltiger Session-ID. // Typischer Auslöser: "No conversation found with session ID: " // passiert wenn die in SQLite gespeicherte Claude-Session lokal fehlt // (neue Maschine, Projekt-Cache geloescht, frische Installation, ...). async function* iterateWithRetry() { try { for await (const ev of conversation) yield ev; } catch (err) { const msg = err?.message || String(err); const isStaleSession = /no conversation found with session id/i.test(msg); if (queryOptions.resume && isStaleSession) { const staleId = queryOptions.resume; // Rust-Seite informieren, damit die stale ID aus der DB geloescht // wird — sonst probieren wir beim naechsten Start wieder dasselbe. sendEvent('session-reset', { staleSessionId: staleId, reason: msg }); sendMonitorEvent('agent', 'Resume fehlgeschlagen, starte neue Session', { reason: msg, oldSessionId: staleId, }); 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': // Content-Bloecke durchgehen (Text, tool_use, thinking, ...) if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text' && block.text) { fullText += block.text; sendEvent('text', { text: block.text }); } else if (block.type === 'thinking' && block.thinking) { // Extended Thinking — als kompaktes Inline-Element (immer sichtbar) const escaped = block.thinking.replace(/&/g, '&').replace(//g, '>'); const inlineThinking = `
💭 Gedanken${escaped}
\n\n`; fullText += inlineThinking; sendEvent('text', { text: inlineThinking }); } else if (block.type === 'tool_use') { // Tool-Call von Main-Agent — manuell weiterreichen, damit // der tool_use-Case weiter unten greift 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': { // tool_result kommt vom SDK meist als Block innerhalb user-message if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'tool_result') { handleToolResult(block); } } } break; } case 'result': { // Endergebnis const durationMs = Date.now() - startTime; // Token-Zählung: input_tokens + cache_read + cache_creation = tatsächlicher Kontext 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; sendEvent('result', { text: fullText, cost, tokens: { input: contextTokens, // Gesamter Kontext (inkl. Cache) output: outputTokens, raw_input: inputTokens, // Nur neue Token (für Debug) cache_read: cacheRead, cache_creation: cacheCreation, }, session_id: event.session_id || '', duration_ms: durationMs, model: usedModel, }); // Monitor: API-Response const tokenK = ((contextTokens + outputTokens) / 1000).toFixed(1); sendMonitorEvent('api', `← ${usedModel} [${durationMs}ms] ${tokenK}k ctx $${cost.toFixed(4)}`, { model: usedModel, contextTokens, outputTokens, cacheRead, cacheCreation, cost, sessionId: event.session_id, }, { durationMs }); break; } default: // Andere Events still ignorieren break; } } } catch (err) { if (err.name === 'AbortError') { // Abgebrochen — kein Fehler sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' }); } else if (isRetryableError(err) && !activeAbort?.signal?.aborted) { // Transienter Fehler — Retry mit Backoff for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { const delay = RETRY_DELAYS[attempt] || 10000; sendMonitorEvent('agent', `Retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms: ${err.message}`, { attempt: attempt + 1, delay, error: err.message, }); sendEvent('text', { text: `\n⏳ Netzwerk-Fehler, Retry ${attempt + 1}/${MAX_RETRIES}...` }); await new Promise(r => setTimeout(r, delay)); if (activeAbort?.signal?.aborted) break; try { conversation = query({ prompt: fullPrompt, options: queryOptions }); for await (const event of conversation) { 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; sendEvent('text', { text: block.text }); } else if (block.type === 'tool_use') { handleToolUse(block); } } } break; case 'tool_use': handleToolUse(event); break; case 'tool_result': handleToolResult(event); break; case 'result': sendEvent('result', { text: fullText, cost: event.total_cost_usd || 0, tokens: { input: (event.usage?.input_tokens || 0) + (event.usage?.cache_read_input_tokens || 0), output: event.usage?.output_tokens || 0 }, session_id: event.session_id || '', duration_ms: Date.now() - startTime, model: event.message?.model || usedModel, }); break; } } // Retry erfolgreich — raus sendMonitorEvent('agent', `Retry ${attempt + 1} erfolgreich`, {}); break; } catch (retryErr) { if (retryErr.name === 'AbortError') break; if (attempt === MAX_RETRIES - 1) { sendEvent('text', { text: `\n\n**Fehler nach ${MAX_RETRIES} Versuchen:** ${retryErr.message || retryErr}` }); sendMonitorEvent('error', `Endgültig fehlgeschlagen nach ${MAX_RETRIES} Retries: ${retryErr.message}`, { name: retryErr.name, attempts: MAX_RETRIES, }); } } } } else { sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); // Monitor: Fehler sendMonitorEvent('error', `${err.message || err}`, { name: err.name, message: err.message, stack: err.stack, }, { error: err.message || String(err) }); } } finally { // Alle noch aktiven Subagents stoppen for (const [toolId, subagent] of activeSubagents) { sendEvent('subagent-stopped', { id: subagent.agentId, parentAgentId: subagent.parentId, success: false, // Vorzeitig beendet toolUseId: toolId, }); } activeSubagents.clear(); sendEvent('agent-stopped', { id: currentAgentId, code: 0 }); sendEvent('all-stopped'); currentAgentId = null; activeAbort = null; } } // ============ Befehle von Tauri ============ function handleCommand(msg) { switch (msg.command) { case 'message': if (!msg.message) { sendError(msg.id, 'Keine Nachricht angegeben'); return; } // Modell, Context und Resume-Session-ID können pro Anfrage überschrieben werden sendMessage(msg.message, msg.id, msg.model, msg.context, msg.resumeSessionId); break; case 'set-context': // Sticky Context setzen (wird bei allen folgenden Nachrichten verwendet) stickyContext = msg.context || ''; const ctxTokens = stickyContext ? Math.ceil(stickyContext.length / 4) : 0; sendResponse(msg.id, { status: 'Context gesetzt', tokens: ctxTokens }); sendMonitorEvent('hook', `Sticky Context gesetzt (~${ctxTokens} Token)`, { contextLength: stickyContext.length, estimatedTokens: ctxTokens, }); break; case 'get-context': sendResponse(msg.id, { context: stickyContext, tokens: stickyContext ? Math.ceil(stickyContext.length / 4) : 0, }); break; case 'clear-context': stickyContext = ''; sendResponse(msg.id, { status: 'Context gelöscht' }); sendMonitorEvent('hook', 'Sticky Context gelöscht', {}); break; case 'stop': if (activeAbort) { activeAbort.abort(); } sendResponse(msg.id, { status: 'gestoppt' }); break; case 'set-model': if (!msg.model) { sendError(msg.id, 'Kein Modell angegeben'); return; } const validModels = AVAILABLE_MODELS.map(m => m.id); if (!validModels.includes(msg.model)) { sendError(msg.id, `Ungültiges Modell: ${msg.model}. Verfügbar: ${validModels.join(', ')}`); return; } currentModel = msg.model; sendResponse(msg.id, { model: currentModel, status: 'Modell geändert' }); sendEvent('model-changed', { model: currentModel }); break; case 'get-models': sendResponse(msg.id, { current: currentModel, available: AVAILABLE_MODELS, }); break; case 'set-mode': // Agent-Modus setzen (solo, handlanger, experten, auto) const validModes = ['solo', 'handlanger', 'experten', 'auto']; if (!msg.mode || !validModes.includes(msg.mode)) { sendError(msg.id, `Ungültiger Modus: ${msg.mode}. Verfügbar: ${validModes.join(', ')}`); return; } agentMode = msg.mode; sendResponse(msg.id, { mode: agentMode, status: 'Modus geändert' }); sendEvent('mode-changed', { mode: agentMode }); sendMonitorEvent('agent', `Agent-Modus geändert: ${agentMode}`, { mode: agentMode }); break; case 'get-mode': sendResponse(msg.id, { mode: agentMode }); break; case 'status': sendResponse(msg.id, { model: currentModel, mode: agentMode, isProcessing: !!currentAgentId, availableModels: AVAILABLE_MODELS, }); break; case 'local-query': { // Lokale Ollama-Abfrage für einfache Tasks (Commit-Messages, Übersetzungen) if (!ollamaAvailable) { sendError(msg.id, 'Ollama nicht verfügbar'); return; } const prompt = msg.message || msg.prompt || ''; localQuery(prompt, msg.id); break; } case 'check-ollama': { // Ollama-Verfügbarkeit prüfen checkOllamaAvailability().then(status => { sendResponse(msg.id, status); }); break; } case 'set-ollama-config': { if (msg.endpoint) ollamaEndpoint = msg.endpoint; if (msg.model) ollamaModel = msg.model; checkOllamaAvailability().then(status => { sendResponse(msg.id, { ...status, endpoint: ollamaEndpoint, model: ollamaModel }); }); break; } case 'set-mcp-servers': // MCP-Server-Configs empfangen (von Rust-Backend aus DB/Config geladen) if (msg.servers && typeof msg.servers === 'object') { mcpServerConfigs = msg.servers; const names = Object.keys(mcpServerConfigs); sendResponse(msg.id, { status: 'MCP-Server gesetzt', count: names.length, servers: names }); sendMonitorEvent('mcp', `${names.length} MCP-Server konfiguriert: ${names.join(', ')}`, { servers: names, count: names.length, }); } else { sendError(msg.id, 'Ungültige MCP-Server-Konfiguration'); } break; case 'get-mcp-servers': sendResponse(msg.id, { servers: Object.keys(mcpServerConfigs), count: Object.keys(mcpServerConfigs).length, configs: mcpServerConfigs, }); break; case 'ping': sendResponse(msg.id, { pong: true }); break; default: sendError(msg.id, `Unbekannter Befehl: ${msg.command}`); } } // ============ Ollama (Lokales Modell) ============ async function checkOllamaAvailability() { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); const res = await fetch(`${ollamaEndpoint}/api/tags`, { signal: controller.signal }); clearTimeout(timeout); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const models = (data.models || []).map(m => m.name); ollamaAvailable = models.length > 0; const hasModel = models.some(m => m.startsWith(ollamaModel.split(':')[0])); sendEvent('ollama-status', { available: ollamaAvailable, models, configured: ollamaModel, hasModel }); return { available: ollamaAvailable, models, configured: ollamaModel, hasModel, endpoint: ollamaEndpoint }; } catch (err) { ollamaAvailable = false; return { available: false, models: [], error: err.message, endpoint: ollamaEndpoint }; } } async function localQuery(prompt, requestId) { if (!ollamaAvailable) { sendError(requestId, 'Ollama nicht verfügbar'); return; } sendEvent('agent-started', { id: 'local-' + requestId, type: 'Local', task: prompt.substring(0, 50), model: ollamaModel }); const startTime = Date.now(); try { const res = await fetch(`${ollamaEndpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: ollamaModel, prompt, stream: false, options: { temperature: 0.3, num_predict: 500 }, }), }); if (!res.ok) throw new Error(`Ollama HTTP ${res.status}`); const data = await res.json(); const text = data.response || ''; const durationMs = Date.now() - startTime; sendEvent('text', { text }); sendEvent('result', { text, cost: 0, tokens: { input: data.prompt_eval_count || 0, output: data.eval_count || 0 }, session_id: '', duration_ms: durationMs, model: ollamaModel, local: true, }); sendMonitorEvent('api', `← Lokal (${ollamaModel}) [${durationMs}ms]`, { model: ollamaModel, local: true, tokens: { input: data.prompt_eval_count || 0, output: data.eval_count || 0 }, }, { durationMs }); } catch (err) { sendEvent('text', { text: `**Lokaler Fehler:** ${err.message}` }); sendMonitorEvent('error', `Ollama Fehler: ${err.message}`, { model: ollamaModel }); } finally { sendEvent('agent-stopped', { id: 'local-' + requestId, code: 0 }); sendEvent('all-stopped'); } } // Ollama beim Start prüfen (non-blocking) checkOllamaAvailability().then(status => { if (status.available) { process.stderr.write(`🧠 Ollama verfügbar: ${status.models.length} Modelle (${ollamaEndpoint})\n`); } }); // ============ Main ============ function cleanupDaemon() { if (IS_DAEMON) { try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {} try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {} process.stderr.write('🔌 Daemon aufgeräumt\n'); } } if (IS_DAEMON) { // ---- UDS-Daemon-Modus ---- // Alte Socket-Datei aufräumen try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {} // PID-File schreiben writeFileSync(PID_PATH, String(process.pid)); const udsServer = createServer((client) => { process.stderr.write(`🔌 UDS-Client verbunden (${udsClients.size + 1} aktiv)\n`); udsClients.add(client); // JSON-Lines Protokoll: jede Zeile = ein Befehl let buffer = ''; client.on('data', (data) => { buffer += data.toString(); let idx; while ((idx = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx + 1); if (!line) continue; try { handleCommand(JSON.parse(line)); } catch (err) { process.stderr.write(`UDS Ungültige Eingabe: ${err.message}\n`); } } }); client.on('end', () => { udsClients.delete(client); process.stderr.write(`🔌 UDS-Client getrennt (${udsClients.size} aktiv)\n`); }); client.on('error', (err) => { udsClients.delete(client); process.stderr.write(`UDS-Client Fehler: ${err.message}\n`); }); // Neuem Client sofort den aktuellen Status senden try { client.write(JSON.stringify({ type: 'event', event: 'ready', payload: { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS, daemon: true } }) + '\n'); } catch {} }); udsServer.listen(SOCKET_PATH, () => { process.stderr.write(`🔌 Bridge-Daemon lauscht auf ${SOCKET_PATH} (PID: ${process.pid})\n`); }); udsServer.on('error', (err) => { process.stderr.write(`UDS-Server Fehler: ${err.message}\n`); cleanupDaemon(); process.exit(1); }); } else { // ---- stdio-Modus (Kompatibilität) ---- const rl = createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; try { handleCommand(JSON.parse(line)); } catch (err) { process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); } }); rl.on('close', () => { process.stderr.write('stdin geschlossen\n'); }); } process.on('SIGTERM', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); }); process.on('SIGINT', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); }); process.on('exit', () => { cleanupDaemon(); }); // Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect) if (!IS_DAEMON) { sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS }); }