Phase 11 Basis: Multi-Agent-Modi mit Tool-Filterung

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) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 18:39:17 +02:00
parent f51241efa6
commit 314042a01f
8 changed files with 360 additions and 27 deletions

View file

@ -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!)

View file

@ -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,

258
src-tauri/Cargo.lock generated
View file

@ -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"

View file

@ -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::<Arc<Mutex<crate::db::Database>>>() {
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::<AgentEvent>(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);
}
}
}

View file

@ -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

View file

@ -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));

View file

@ -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<void> {
})
);
// 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<MonitorEventPayload>('monitor', (event) => {

View file

@ -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 @@
<span class="sep">|</span>
<span class="model">{$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())}</span>
{/if}
{#if $agentMode && $agentMode !== 'solo'}
<span class="sep">|</span>
<span class="mode-badge mode-{$agentMode}" title="Agent-Modus: {$agentMode}">
{#if $agentMode === 'handlanger'}👷 Handlanger
{:else if $agentMode === 'experten'}🎓 Experten
{:else if $agentMode === 'auto'}🤖 Auto
{/if}
</span>
{/if}
</div>
</footer>
</div>
@ -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);
}
</style>