feat(brain): Multi-Threading via per-request project_id + per-project queue
Erster Schritt zum echten Multi-Threading fuer ARIA-Projekte. Kein globaler active_project-State mehr — jeder /chat-Request sagt selbst welche Buehne (project_id im Body). Verschiedene Projekte laufen parallel, gleiches Projekt queued via asyncio.Lock. Backend: - ChatIn.project_id: Client bestimmt pro Request wohin. Bridge routet. - /chat: async, holt per-Projekt asyncio.Lock. Requests fuers gleiche Projekt reihen sich in _project_pending ein, warten am Lock. Requests fuer verschiedene Projekte laufen echt parallel. - Neuer /projects/queue-status endpoint: pro Kontext (inkl. Hauptchat unter __main__): busy True/False + queue_size. Fuers UI-Status-Dots. - Agent.chat() nimmt project_id + pending_queue Params. Kein projects_mod.get_active() mehr im Hot-Path. Queue-Aware Prompting: - Wenn nach dem aktuellen Turn weitere Nachrichten in der Queue liegen, wird der System-Prompt um ein QUEUE-Segment erweitert mit Instruktion: „Bevor Du den aktuellen Task loesst, pruef die Queue — widerspricht/ annuliert eine spaetere Nachricht? Dann Skip-Antwort statt Doppelarbeit." - Beispiel: Task 'titelleiste rot' + Queue-Tail 'doch nicht, blau' → ARIA skipt rot, blau kommt als naechste Anfrage sauber durch. - Kein extra LLM-Call — reine Prompt-Injection. Project-Tools: - project_enter/exit sind jetzt UI-Signale (App wechselt Ansicht via project_changed event), aendern KEINEN Brain-State mehr. Der aktuelle Turn bleibt in seinem Chat-Kontext. - project_list zeigt keinen "AKTIV"-Marker mehr (nicht mehr sinnvoll). - projects_mod.set_active/get_active bleiben als Legacy-Helpers (kein Aufruf mehr aus dem Hot-Path). Bridge: - send_to_core packt project_id in den /chat-Body. - User-Backup-Eintrag tag't project_id sauber, keine Brain-Query mehr. Naechste Schritte (kommende Commits): - App: Focus-One-View mit Drawer + Status-Dots + OS-Push - Diagnostic: Dashboard-Stack mit Karten - Voice-Router: 30s-Sticky + Meta-Command-Interception im wakeword.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+66
-29
@@ -836,10 +836,14 @@ META_TOOLS = [
|
||||
"function": {
|
||||
"name": "project_enter",
|
||||
"description": (
|
||||
"Wechselt in ein bestehendes Projekt. Fuzzy-Match auf Namen — "
|
||||
"'Spotify' findet das Projekt 'Spotify-Setup'. Nach dem Eintritt "
|
||||
"tagged jeder neue Turn die project_id. Bei sehr alten Projekten: "
|
||||
"vorher project_summary aufrufen damit Du Stefan abholst."
|
||||
"Signalisiert der App/Diagnostic 'wechsel zu diesem Projekt'. Fuzzy-"
|
||||
"Match auf Namen — 'Spotify' findet das Projekt 'Spotify-Setup'. "
|
||||
"Der AKTUELLE Turn bleibt aber in seinem Chat-Kontext — wir haben "
|
||||
"Multi-Threading, kein globales 'aktives Projekt' mehr. Wenn Stefan "
|
||||
"im Hauptchat sagt 'lass uns in Spotify weiter machen': "
|
||||
"project_enter aufrufen (App wechselt Ansicht), aber Deine Antwort "
|
||||
"geht trotzdem im Hauptchat raus. Bei sehr alten Projekten vorher "
|
||||
"project_summary aufrufen damit Du Stefan abholst."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -855,8 +859,10 @@ META_TOOLS = [
|
||||
"function": {
|
||||
"name": "project_exit",
|
||||
"description": (
|
||||
"Verlässt das aktuelle Projekt — zurück zum Hauptthread. Nutze "
|
||||
"wenn Stefan sagt 'Projekt Ende', 'zurück zum Hauptchat' o.ä."
|
||||
"Signalisiert der App/Diagnostic 'wechsel zurueck zum Hauptchat'. "
|
||||
"Nutze wenn Stefan sagt 'Projekt Ende' oder 'zurueck zum Hauptchat' "
|
||||
"waehrend er visuell in einem Projekt ist. Der aktuelle Turn bleibt "
|
||||
"in seinem Chat-Kontext — Multi-Threading."
|
||||
),
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
@@ -1051,7 +1057,21 @@ class Agent:
|
||||
|
||||
MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops
|
||||
|
||||
def chat(self, user_message: str, source: str = "") -> str:
|
||||
def chat(self, user_message: str, source: str = "",
|
||||
project_id: Optional[str] = None,
|
||||
pending_queue: Optional[list[str]] = None) -> str:
|
||||
"""Verarbeitet eine User-Nachricht — pro Request project_id explizit
|
||||
angegeben (leer = Hauptchat). Kein globaler active_project-State mehr —
|
||||
so laufen parallele /chat-Requests fuer verschiedene Projekte echt
|
||||
parallel (Multi-Threading-Architektur seit 06/2026).
|
||||
|
||||
pending_queue: Liste weiterer User-Nachrichten die in DIESEM Projekt
|
||||
NACH dem aktuellen Turn warten. ARIA sieht sie im System-Prompt und
|
||||
soll pruefen ob eine spaetere Nachricht den aktuellen Task
|
||||
korrigiert / annuliert (dann Skip-Antwort statt Ausfuehren).
|
||||
|
||||
Wenn project_id=None (Backward-Compat fuer Aufrufer die den Param nicht
|
||||
setzen): wird als Hauptchat behandelt."""
|
||||
user_message = (user_message or "").strip()
|
||||
if not user_message:
|
||||
raise ValueError("Leere Nachricht")
|
||||
@@ -1059,9 +1079,8 @@ class Agent:
|
||||
# Events vom letzten Turn weglassen
|
||||
self._pending_events = []
|
||||
|
||||
# Aktives Projekt (leer = Hauptthread) — bestimmt das Tagging der
|
||||
# neuen Turns + das Conversation-Window-Filter fuer den LLM-Prompt.
|
||||
active_project_id = projects_mod.get_active()
|
||||
# Projekt-Kontext pro Request statt aus globalem State
|
||||
active_project_id = (project_id or "").strip()
|
||||
active_project = projects_mod.get_project(active_project_id) if active_project_id else None
|
||||
|
||||
# Fast-Path: einfache "reines Steuern"-Commands ueberspringen Claude komplett.
|
||||
@@ -1127,6 +1146,28 @@ class Agent:
|
||||
oauth_callback_host=oauth_host,
|
||||
oauth_callback_port=oauth_port,
|
||||
oauth_callback_tls=oauth_tls)
|
||||
# Queue-Aware Prompting: wenn nach diesem Turn weitere Nachrichten
|
||||
# in der Warteschlange liegen, muss ARIA pruefen ob eine spaetere die
|
||||
# aktuelle Aufgabe korrigiert/annuliert (→ Skip statt Doppelarbeit).
|
||||
if pending_queue:
|
||||
queue_lines = "\n".join(f" - {m[:280]}" for m in pending_queue[:5])
|
||||
more_hint = ""
|
||||
if len(pending_queue) > 5:
|
||||
more_hint = f"\n ... und {len(pending_queue) - 5} weitere"
|
||||
system_prompt += (
|
||||
f"\n\n## QUEUE — NACH DIESEM TASK WARTEN\n"
|
||||
f"{queue_lines}{more_hint}\n"
|
||||
f"\nBEVOR DU DEN AKTUELLEN TASK LOESST:\n"
|
||||
f" 1. Pruefe die Queue oben — widerspricht/annuliert eine der spaeteren "
|
||||
f"Nachrichten den aktuellen Task?\n"
|
||||
f" 2. Wenn ja: antworte ganz kurz 'Task ubersprungen — wird durch spaetere "
|
||||
f"Nachricht korrigiert' und mach KEINE Aktion. Der spaetere Task laeuft dann "
|
||||
f"ganz normal als naechste Anfrage durch.\n"
|
||||
f" 3. Wenn nein / unabhaengige Ergaenzung: Task normal loesen.\n"
|
||||
f"Beispiel: aktueller Task 'titelleiste rot', Queue enthaelt "
|
||||
f"'doch nicht, mach sie blau' → skip, blau kommt als naechste Anfrage."
|
||||
)
|
||||
|
||||
# Aktuelle Projekt-Bühne als System-Hinweis ergaenzen, damit Claude
|
||||
# weiss in welchem Kontext sie spricht und ihre project_* Tools korrekt
|
||||
# einsetzt (z.B. bei „Projekt Ende" project_exit aufruft).
|
||||
@@ -1217,19 +1258,17 @@ class Agent:
|
||||
err_text = f"[Fehler: {exc}]"
|
||||
logger.error("chat() Exception — schreibe Error-Marker als Assistant-Turn: %s", exc)
|
||||
try:
|
||||
# Aktive Projekt-ID NEU lesen — kann sich waehrend des Tool-Loops
|
||||
# geaendert haben (project_enter/exit als Tool-Call).
|
||||
# Turn-Kontext bleibt gleich — es gibt keinen globalen Wechsel
|
||||
# mehr, jeder Request laeuft in seinem eigenen project_id-Kontext.
|
||||
self.conversation.add("assistant", err_text,
|
||||
project_id=projects_mod.get_active())
|
||||
project_id=active_project_id)
|
||||
except Exception as add_exc:
|
||||
logger.warning("Konnte Error-Marker nicht persistieren: %s", add_exc)
|
||||
raise
|
||||
|
||||
# 7. Assistant-Turn (final reply) in die Conversation
|
||||
# NEU lesen — wenn der LLM project_enter/exit gerufen hat, ist der
|
||||
# Final-Reply schon im neuen Projekt-Kontext.
|
||||
self.conversation.add("assistant", final_reply,
|
||||
project_id=projects_mod.get_active())
|
||||
project_id=active_project_id)
|
||||
return final_reply
|
||||
|
||||
# ── Tool-Dispatcher ───────────────────────────────────────
|
||||
@@ -1804,7 +1843,10 @@ class Agent:
|
||||
"project": p,
|
||||
"action": "created",
|
||||
})
|
||||
return f"OK — Projekt '{p['name']}' angelegt (id={p['id']}) und aktiv. Alle weiteren Turns gehen jetzt da rein bis Du project_exit oder project_enter aufrufst."
|
||||
return (f"OK — Projekt '{p['name']}' angelegt (id={p['id']}). App/Diagnostic "
|
||||
f"kriegen ein project_changed-Event und koennen dahin wechseln. "
|
||||
f"Kommender Turn bleibt aber im aktuellen Chat-Kontext — "
|
||||
f"Multi-Threading, jeder Chat ist eigenstaendig.")
|
||||
if name == "project_enter":
|
||||
pname = (arguments.get("name") or "").strip()
|
||||
if not pname:
|
||||
@@ -1812,7 +1854,6 @@ class Agent:
|
||||
p = projects_mod.find_project(pname)
|
||||
if not p:
|
||||
return f"Kein Projekt '{pname}' gefunden. Nutze project_list zum Aufzaehlen oder project_create wenn's neu sein soll."
|
||||
projects_mod.set_active(p["id"])
|
||||
self._pending_events.append({
|
||||
"type": "project_changed",
|
||||
"project": p,
|
||||
@@ -1822,31 +1863,27 @@ class Agent:
|
||||
hint = ""
|
||||
if turn_count > 0:
|
||||
hint = " Wenn Stefan nach dem Stand fragt: project_summary aufrufen."
|
||||
return f"OK — in Projekt '{p['name']}' eingestiegen (id={p['id']}, {turn_count} bisherige Turns).{hint}"
|
||||
return (f"OK — App/Diagnostic wird zum Projekt '{p['name']}' "
|
||||
f"(id={p['id']}, {turn_count} bisherige Turns) umschalten. "
|
||||
f"Der aktuelle Turn bleibt aber im aktuellen Chat-Kontext.{hint}")
|
||||
if name == "project_exit":
|
||||
active_id = projects_mod.get_active()
|
||||
if not active_id:
|
||||
return "Es ist gerade kein Projekt aktiv — bereits im Hauptthread."
|
||||
p = projects_mod.get_project(active_id)
|
||||
projects_mod.set_active("")
|
||||
self._pending_events.append({
|
||||
"type": "project_changed",
|
||||
"project": p,
|
||||
"project": None,
|
||||
"action": "exited",
|
||||
})
|
||||
return f"OK — Projekt '{p['name'] if p else active_id}' verlassen. Zurueck im Hauptthread."
|
||||
return ("OK — App/Diagnostic bekommt Signal 'zurueck zum Hauptchat'. "
|
||||
"Der aktuelle Turn bleibt aber im aktuellen Chat-Kontext.")
|
||||
if name == "project_list":
|
||||
items = projects_mod.list_projects()
|
||||
if not items:
|
||||
return "(keine Projekte angelegt)"
|
||||
active_id = projects_mod.get_active()
|
||||
lines = []
|
||||
for p in items:
|
||||
marker = " ← AKTIV" if p["id"] == active_id else ""
|
||||
status_lbl = p.get("status", "active")
|
||||
lines.append(
|
||||
f"- {p['name']} (id={p['id']}, {p.get('turn_count', 0)} Turns, "
|
||||
f"status={status_lbl}){marker}"
|
||||
f"status={status_lbl})"
|
||||
)
|
||||
return "Projekte:\n" + "\n".join(lines)
|
||||
if name == "project_summary":
|
||||
|
||||
Reference in New Issue
Block a user