claude-desktop/scripts/claude-bridge.js
Eddy 79b8525ede Bugfixes: Resume, Tool-Whitelist, Sub-Agent-Tree, UI-Polish
Bridge (claude-bridge.js):
- Resume-Fix: queryOptions.resume statt .sessionId (SDK-API)
- tools-Whitelist statt disallowedTools (Blacklist vererbt sich auf Sub-Agents!)
  Handlanger: Main nur Task+TodoWrite, Sub-Agents bekommen volles Tool-Set
  Experten: Main nur Task+TodoWrite+Read+Grep+Glob
  Solo: preset claude_code
- handleToolUse/handleToolResult Helper, greifen auch in assistant.content-Bloecken
  (SDK liefert tool_use/tool_result nicht als standalone events)
- Dedup via handledTools Set
- Resume-Retry-Fallback bei ungueltiger Session-ID
- Custom agents-Option entfernt (SDK spawnt Sub-Agents ohne Tools → Halluzination)
- Orchestrator-Prompt: verweist auf general-purpose (vollstaendiges Tool-Set)

Backend (claude.rs):
- claude_session_id NUR beim 1. Mal setzen (sonst verliert man History)
- Generic event emit fuer alle Bridge-Events ans Frontend
- Mode-Persistenz bei Bridge-Start (agent_mode aus DB laden)

Knowledge (knowledge.rs):
- MYSQL_HOST: 192.168.155.1 → 192.168.155.11 (MariaDB-Server)
- MYSQL_PASS: claude → 8715
- category Option<&str> Typ-Annotation fuer exec_map

Programs (programs.rs):
- xvfb_screenshot: Fallback scrot → import (ImageMagick) → ffmpeg

Voice (voice.rs):
- Part::file (existiert nicht) → Part::bytes, keine Temp-Datei

Frontend:
- events.ts: mode-changed Listener, result.text Fallback,
  addAgent({id}) fuer korrekte Parent-Child-Verknuepfung
- ChatPanel: Copy-Button, Typing-Dots in Bubble (kein Doppel-Header),
  $effect statt $:, onkeydown statt on:keydown
- AgentView: "Nur aktive" Toggle, Delegations-Badge, Tool-Count hidden bei 0,
  agentMode Import
- ProgramsPanel: Button-Styling, Error-Banner mit Copy-Button,
  selectable Text
- MonitorPanel: Filter-Dropdown Styling (Hintergrund + Hover)
- SettingsPanel: changeMode() wird beim Klick aufgerufen (nicht nur Store)
- +layout.svelte: agent_mode beim App-Start laden, Mode-Badge im Footer,
  🎓-Button fuer Schulungsfenster
- +page.svelte: Programme-Tab + Hooks-Tab

Neue Dateien:
- TEST-ROADMAP.md — Status und naechste Schritte
- .gitignore erweitert (scheduled_tasks.lock, out/, node_modules)
- vscode-extension/tsconfig.json: include nur src/, exclude node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:24:51 +02:00

741 lines
25 KiB
JavaScript

#!/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
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createInterface } from 'node:readline';
import { randomUUID } from 'node:crypto';
// Prozess am Leben halten
const keepAlive = setInterval(() => {}, 60000);
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 = '';
// ============ 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) {
process.stdout.write(JSON.stringify(msg) + '\n');
}
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: 25,
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 });
}
// Nachricht mit Context und Orchestrator kombinieren
let fullPrompt = message;
if (orchestratorPrompt) {
fullPrompt = `${orchestratorPrompt}\n\n---\n\n${message}`;
}
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: 25,
abortController: activeAbort,
};
// Session-ID für Fortsetzung — SDK erwartet `resume`, nicht `sessionId`
if (resumeSessionId) {
queryOptions.resume = resumeSessionId;
}
// Tool-Konfig je nach Modus.
// WICHTIG: disallowedTools vererbt sich auf Sub-Agents!
// Deshalb Whitelist via `tools` nutzen — die gilt nur fuer Main,
// Sub-Agents bekommen das volle Standard-Tool-Set.
queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash'];
if (effectiveMode === 'handlanger') {
queryOptions.tools = ['Task', 'TodoWrite'];
sendMonitorEvent('agent', 'Handlanger: Main nur Task+TodoWrite, Sub-Agents mit vollem Tool-Set', {
mode: effectiveMode,
mainTools: queryOptions.tools,
});
} else if (effectiveMode === 'experten') {
queryOptions.tools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob'];
sendMonitorEvent('agent', 'Experten: Main lesen+delegieren, Sub-Agents voll', {
mode: effectiveMode,
mainTools: queryOptions.tools,
});
} else {
// solo: volles Preset
queryOptions.tools = { type: 'preset', preset: 'claude_code' };
}
let conversation = query({
prompt: fullPrompt,
options: queryOptions,
});
// 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
async function* iterateWithRetry() {
try {
for await (const ev of conversation) yield ev;
} catch (err) {
// Wenn Resume-Session ungueltig → Retry ohne sessionId
if (queryOptions.sessionId) {
sendMonitorEvent('agent', 'Resume fehlgeschlagen, starte neue Session', {
reason: err.message || String(err),
oldSessionId: queryOptions.sessionId,
});
delete queryOptions.sessionId;
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 === '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;
const inputTokens = event.usage?.input_tokens || 0;
const outputTokens = event.usage?.output_tokens || 0;
const cost = event.total_cost_usd || 0;
sendEvent('result', {
text: fullText,
cost,
tokens: { input: inputTokens, output: outputTokens },
session_id: event.session_id || '',
duration_ms: durationMs,
model: usedModel,
});
// Monitor: API-Response
const tokenK = ((inputTokens + outputTokens) / 1000).toFixed(1);
sendMonitorEvent('api', `${usedModel} [${durationMs}ms] ${tokenK}k tok $${cost.toFixed(4)}`, {
model: usedModel,
inputTokens,
outputTokens,
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 {
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 'ping':
sendResponse(msg.id, { pong: true });
break;
default:
sendError(msg.id, `Unbekannter Befehl: ${msg.command}`);
}
}
// ============ Main ============
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); process.exit(0); });
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
// Bereit
sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });