From 314042a01f1c619c4b39950e44d83723f66927ac Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 14 Apr 2026 18:39:17 +0200 Subject: [PATCH] Phase 11 Basis: Multi-Agent-Modi mit Tool-Filterung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge (scripts/claude-bridge.js): - allowedTools je nach Agent-Modus erzwingt Delegation - Handlanger: nur Task + TodoWrite - Experten: Task + TodoWrite + Read + Grep + Glob - Solo/Auto: unveraendert Backend (src-tauri/src/claude.rs): - Mode-Persistenz: nach bridge-ready wird gespeicherter Modus gesetzt - Catch-all Event-Handler: leitet unbekannte Bridge-Events generisch ans Frontend weiter (subagent-started, monitor-event, mode-changed, ...) UI (routes/+layout.svelte, stores/events.ts): - Modus-Badge im Footer (Handlanger orange, Experten lila, Auto cyan) - mode-changed Event-Listener synchronisiert agentMode Store Bugfix voice.rs: - reqwest::multipart::Part::file existiert nicht → auf Part::bytes umgestellt - keine Temp-Datei mehr noetig Bugfix knowledge.rs: - Type-Annotation bei category Option<&str> fuer exec_map Inference Co-Authored-By: Claude Opus 4.6 (1M context) --- ROADMAP.md | 24 +++- scripts/claude-bridge.js | 18 +++ src-tauri/Cargo.lock | 258 ++++++++++++++++++++++++++++++++++++- src-tauri/src/claude.rs | 21 ++- src-tauri/src/knowledge.rs | 2 +- src-tauri/src/voice.rs | 18 +-- src/lib/stores/events.ts | 13 +- src/routes/+layout.svelte | 33 ++++- 8 files changed, 360 insertions(+), 27 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 178619a..2078abb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -34,6 +34,7 @@ Stand: 14.04.2026 | **Claude-DB Integration (Phase 8)** | ✅ | e6bd0de | | **Context-Management (Phase 9)** | ✅ | eb91e54 | | **Sprach-Interface (Phase 10)** | ✅ | 14.04.2026 | +| **Multi-Agent-Modi (Phase 11 — Basis)** | ✅ | 14.04.2026 | --- @@ -347,7 +348,28 @@ Benötigt `OPENAI_API_KEY` Umgebungsvariable für Whisper + TTS. --- -## Phase 11: Multi-Agent-Architektur (Context-Einsparung) +## Phase 11: Multi-Agent-Architektur (Context-Einsparung) 🚧 TEIL-ERLEDIGT + +> **Basis-Implementierung:** 14.04.2026 (Tool-Filterung + Persistenz + UI-Badge) + +### Implementiert (Basis) + +- ✅ **Bridge: Tool-Filterung je Modus** (`scripts/claude-bridge.js`) + - Handlanger: `allowedTools = ['Task', 'TodoWrite']` — erzwingt Delegation + - Experten: `allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob']` — darf sondieren, nicht schreiben + - Solo/Auto: keine Einschränkung +- ✅ **Backend: Mode-Persistenz beim Bridge-Start** (`claude.rs`) + - Gespeicherter Modus wird nach `bridge-ready` automatisch gesetzt + - `agent_mode` Setting in SQLite +- ✅ **Generische Event-Weiterleitung** (`claude.rs`) + - Unbekannte Bridge-Events werden automatisch ans Frontend emit'et + - Löst `subagent-started`, `monitor-event`, `mode-changed` etc. +- ✅ **UI: Modus-Badge im Footer** (`+layout.svelte`) + - Farbcodiert: 👷 Handlanger (orange), 🎓 Experten (lila), 🤖 Auto (cyan) +- ✅ **Frontend: mode-changed Listener** (`events.ts`) + - Badge aktualisiert sich bei Modus-Wechsel live + +### Noch offen (Ausbau) ### Die drei Agent-Modi (einstellbar!) diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index e93cf51..e62da52 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -256,6 +256,24 @@ async function sendMessage(message, requestId, model = null, contextOverride = n queryOptions.sessionId = resumeSessionId; } + // Tool-Filterung je nach Agent-Modus — erzwingt Delegation + // Handlanger: Main darf NUR delegieren (Task) und planen (TodoWrite) + // Experten: Main darf zusätzlich lesen/suchen, aber nicht schreiben + if (agentMode === 'handlanger') { + queryOptions.allowedTools = ['Task', 'TodoWrite']; + sendMonitorEvent('agent', 'Handlanger-Modus: Main darf nur Task+TodoWrite', { + mode: agentMode, + allowedTools: queryOptions.allowedTools, + }); + } else if (agentMode === 'experten') { + queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob']; + sendMonitorEvent('agent', 'Experten-Modus: Main darf lesen+delegieren', { + mode: agentMode, + allowedTools: queryOptions.allowedTools, + }); + } + // solo + auto: keine Einschränkung + const conversation = query({ prompt: fullPrompt, options: queryOptions, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d7d3e12..e09a799 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -479,8 +479,10 @@ dependencies = [ name = "claude-desktop" version = "0.1.0" dependencies = [ + "base64 0.22.1", "chrono", "mysql_async", + "reqwest 0.12.28", "rusqlite", "serde", "serde_json", @@ -526,6 +528,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -549,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", "libc", @@ -562,7 +574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -1582,6 +1594,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1715,6 +1746,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1725,6 +1757,37 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1743,9 +1806,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2326,6 +2391,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3454,6 +3529,48 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3488,6 +3605,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -3576,12 +3707,51 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3676,7 +3846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3834,6 +4004,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.18.0" @@ -4157,6 +4339,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -4210,6 +4398,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4231,7 +4440,7 @@ checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch2", @@ -4314,7 +4523,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4713,6 +4922,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4987,6 +5206,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -5005,6 +5230,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -5495,6 +5726,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -6046,6 +6288,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index d431eaf..bfadc10 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -158,6 +158,20 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) { "ready" => { println!("✅ Claude Bridge bereit"); let _ = app.emit("bridge-ready", ()); + + // Gespeicherten Agent-Modus an Bridge senden (falls vorhanden) + if let Some(db_state) = app.try_state::>>() { + let mode = { + let db = db_state.lock().unwrap(); + db.get_setting("agent_mode").ok().flatten() + }; + if let Some(mode) = mode { + if mode != "solo" { + println!("🔄 Restore Agent-Modus: {}", mode); + let _ = send_to_bridge(app, "set-mode", &mode); + } + } + } } "agent-started" | "subagent-start" => { if let Ok(agent) = serde_json::from_value::(payload.clone()) { @@ -241,8 +255,11 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) { let mut state = state.lock().unwrap(); state.agents.clear(); } - _ => { - println!("📨 Event: {} = {:?}", event, payload); + other => { + // Generische Weiterleitung aller Bridge-Events ans Frontend + // (subagent-started, subagent-stopped, monitor-event, mode-changed, + // knowledge-hint, auto-mode-chosen, etc.) + let _ = app.emit(other, &payload); } } } diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index 626a87a..48f5d4d 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -270,7 +270,7 @@ pub async fn get_tool_hints( let mut search_terms = vec![tool.clone()]; // Tool-spezifische Kategorien mappen - let category = match tool.as_str() { + let category: Option<&str> = match tool.as_str() { "Bash" => { if let Some(ref cmd) = command { // Relevante Begriffe aus Bash-Kommando extrahieren diff --git a/src-tauri/src/voice.rs b/src-tauri/src/voice.rs index 16ea46d..1366d27 100644 --- a/src-tauri/src/voice.rs +++ b/src-tauri/src/voice.rs @@ -67,21 +67,10 @@ pub async fn transcribe_audio( .map_err(|e| format!("Base64-Dekodierung fehlgeschlagen: {}", e))?; // Temporäre Datei erstellen (Whisper API braucht Datei-Upload) - let temp_dir = std::env::temp_dir(); - let temp_file = temp_dir.join(format!("whisper_audio_{}.{}", uuid::Uuid::new_v4(), format)); - - let mut file = std::fs::File::create(&temp_file) - .map_err(|e| format!("Temp-Datei erstellen fehlgeschlagen: {}", e))?; - file.write_all(&audio_bytes) - .map_err(|e| format!("Audio schreiben fehlgeschlagen: {}", e))?; - drop(file); - - // Multipart-Request an Whisper API + // Multipart-Request an Whisper API — direkt aus dem Byte-Buffer let client = reqwest::Client::new(); - let file_part = reqwest::multipart::Part::file(&temp_file) - .await - .map_err(|e| format!("Datei lesen fehlgeschlagen: {}", e))? + let file_part = reqwest::multipart::Part::bytes(audio_bytes) .file_name(format!("audio.{}", format)) .mime_str(&format!("audio/{}", format)) .map_err(|e| format!("MIME-Type fehlgeschlagen: {}", e))?; @@ -100,9 +89,6 @@ pub async fn transcribe_audio( .await .map_err(|e| format!("API-Request fehlgeschlagen: {}", e))?; - // Temp-Datei löschen - let _ = std::fs::remove_file(&temp_file); - if !response.status().is_success() { let error_text = response.text().await.unwrap_or_default(); return Err(format!("Whisper API Fehler: {}", error_text)); diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index e1cc4e7..7382599 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -23,10 +23,12 @@ import { addMonitorEvent, loadMonitorEventsFromDb, activeKnowledgeHints, + agentMode, type Message, type Agent, type MonitorEventType, - type KnowledgeHint + type KnowledgeHint, + type AgentMode } from './app'; // Event-Typen vom Backend @@ -326,6 +328,15 @@ export async function initEventListeners(): Promise { }) ); + // Agent-Modus geändert (von Bridge bestätigt) + listeners.push( + await listen<{ mode: AgentMode }>('mode-changed', (event) => { + const { mode } = event.payload; + console.log('🔄 Agent-Modus geändert:', mode); + agentMode.set(mode); + }) + ); + // Monitor-Events — für System-Monitor Panel listeners.push( await listen('monitor', (event) => { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c65672b..b1bc7f5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,7 +2,7 @@ import '../app.css'; import { onMount, onDestroy } from 'svelte'; import { invoke } from '@tauri-apps/api/core'; - import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, type DbMessage, type StickyContextInfo } from '$lib/stores'; + import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores'; import StopButton from '$lib/components/StopButton.svelte'; // Session-Typ vom Backend @@ -170,6 +170,15 @@ | {$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())} {/if} + {#if $agentMode && $agentMode !== 'solo'} + | + + {#if $agentMode === 'handlanger'}👷 Handlanger + {:else if $agentMode === 'experten'}🎓 Experten + {:else if $agentMode === 'auto'}🤖 Auto + {/if} + + {/if} @@ -284,4 +293,26 @@ font-weight: 500; cursor: help; } + + .footer-stats .mode-badge { + font-weight: 600; + cursor: help; + padding: 1px 6px; + border-radius: 3px; + } + + .footer-stats .mode-handlanger { + color: #f59e0b; + background: rgba(245, 158, 11, 0.12); + } + + .footer-stats .mode-experten { + color: #a855f7; + background: rgba(168, 85, 247, 0.12); + } + + .footer-stats .mode-auto { + color: #06b6d4; + background: rgba(6, 182, 212, 0.12); + }