Add workflow-status telemetry view and make panel modes on-demand

Workflow-status: a new WorkflowProgressTracker singleton taps the
suppressed dynamic-workflow subagent events (those carrying
parent_tool_use_id) into per-session telemetry without altering the
existing guard; the snapshot is serialized into the new chains.workflow_info
column on FinalizeChain (ChainId-matched so a panel member of the same
session cannot clear it) and exposed via GET /api/sessions/{id}/workflow-status
(live -> persisted -> none). The header menu gains a 'Workflow-Status' entry
with a modal that live-polls every 2s while running and self-stops on close.

Panel on-demand: Fusion/MoA is now a transient per-request multiModelMode
flag (like plan/ultracode) instead of a persisted session column. Session
settings keep only the panel members + moa_layers; the mode <select> was
removed and the composer dropdown drives the mode per request.

Docs: ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md updated for both
features plus the already-shipped z.ai/GLM provider, GLM 5.2+ effort
passthrough, web-search-prime MCP and the ultracode toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-20 17:27:46 +02:00
1 parent 5b1802efee
commit b6e546f618
22 files changed
+591 -99

No files matched your search

+54 -7
View File
@@ -33,6 +33,7 @@
19. [Multi-Agent System (Preparer + Review + Finalizer)](#multi-agent-system-preparer--review--finalizer) 19. [Multi-Agent System (Preparer + Review + Finalizer)](#multi-agent-system-preparer--review--finalizer)
20. [Interaktive Rückfragen (ask_user_choice)](#interaktive-rückfragen-ask_user_choice) 20. [Interaktive Rückfragen (ask_user_choice)](#interaktive-rückfragen-ask_user_choice)
21. [Plan-Modus](#plan-modus) 21. [Plan-Modus](#plan-modus)
21b. [Ultracode & Workflow-Status](#ultracode--workflow-status)
22. [Web-Search](#web-search) 22. [Web-Search](#web-search)
23. [Nachrichten teilen (Shared Messages)](#nachrichten-teilen-shared-messages) 23. [Nachrichten teilen (Shared Messages)](#nachrichten-teilen-shared-messages)
24. [Workspace & File-Explorer](#workspace--file-explorer) 24. [Workspace & File-Explorer](#workspace--file-explorer)
@@ -164,6 +165,7 @@ gott/
│ ├── KreaService.cs # Krea Image Generation API │ ├── KreaService.cs # Krea Image Generation API
│ ├── ImageService.cs # Bild-Resize + WebP-Konvertierung (512px + 256px Thumbnail) │ ├── ImageService.cs # Bild-Resize + WebP-Konvertierung (512px + 256px Thumbnail)
│ ├── StreamingTracker.cs # Tracking aktiver Streams + CancellationTokenSource für Stop │ ├── StreamingTracker.cs # Tracking aktiver Streams + CancellationTokenSource für Stop
│ ├── WorkflowProgressTracker.cs # In-Memory-Telemetrie laufender dynamischer Workflows (Ultracode) pro Session
│ ├── WorkspaceHelper.cs # Workspace-Pfad-Auflösung (relativ → absolut) │ ├── WorkspaceHelper.cs # Workspace-Pfad-Auflösung (relativ → absolut)
│ ├── AppPaths.cs # Zentrale Pfade (WwwRoot) – Dev vs. Publish-Modus │ ├── AppPaths.cs # Zentrale Pfade (WwwRoot) – Dev vs. Publish-Modus
│ ├── KeyScannerService.cs # API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper) │ ├── KeyScannerService.cs # API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper)
@@ -180,7 +182,7 @@ gott/
│ ├── AuthEndpoint.cs # Login, Logout, Me, Autologin │ ├── AuthEndpoint.cs # Login, Logout, Me, Autologin
│ ├── ChatEndpoint.cs # POST /api/chat (SSE Streaming + CLI-Fallback) │ ├── ChatEndpoint.cs # POST /api/chat (SSE Streaming + CLI-Fallback)
│ ├── HistoryEndpoint.cs # History, Archive, Attachments, Marks, Chain-Details │ ├── HistoryEndpoint.cs # History, Archive, Attachments, Marks, Chain-Details
│ ├── SessionEndpoint.cs # CRUD + SystemPrompt, SystemPromptBlocks, PayloadPreview, Streaming, Exec, Git-Status │ ├── SessionEndpoint.cs # CRUD + SystemPrompt, SystemPromptBlocks, PayloadPreview, Streaming, WorkflowStatus, Exec, Git-Status
│ ├── AgentEndpoint.cs # CRUD + Available (inkl. public agents) │ ├── AgentEndpoint.cs # CRUD + Available (inkl. public agents)
│ ├── WorkspaceEndpoint.cs # File-Tree, File-Read/Write, Browse, Agent-Files, Raw-Download │ ├── WorkspaceEndpoint.cs # File-Tree, File-Read/Write, Browse, Agent-Files, Raw-Download
│ ├── TelegramEndpoint.cs # Bot-CRUD, Webhook, Session-Config, Registration │ ├── TelegramEndpoint.cs # Bot-CRUD, Webhook, Session-Config, Registration
@@ -241,7 +243,7 @@ gott/
│ │ ├── cards.js # Card-Builder (buildSessionCard, buildAgentCard) │ │ ├── cards.js # Card-Builder (buildSessionCard, buildAgentCard)
│ │ ├── msg-actions.js # Nachrichten-Aktionen (Delete, Mark, Share, Reply, Timestamp) │ │ ├── msg-actions.js # Nachrichten-Aktionen (Delete, Mark, Share, Reply, Timestamp)
│ │ ├── tool-ui.js # Tool-Boxes, Diffs, Todo-Liste, Visibility-Logik │ │ ├── tool-ui.js # Tool-Boxes, Diffs, Todo-Liste, Visibility-Logik
│ │ ├── modals.js # Modale Dialoge (System-Prompt, Command-Output, Krea-Image, Remote) │ │ ├── modals.js # Modale Dialoge (System-Prompt, Payload, Command-Output, Krea-Image, Remote, Workflow-Status)
│ │ ├── stream-ui.js # Stream-Bubble, Review-Streaming │ │ ├── stream-ui.js # Stream-Bubble, Review-Streaming
│ │ ├── bubbles.js # Message Bubbles (User, Assistant, Error, Warning, Choice, Review) │ │ ├── bubbles.js # Message Bubbles (User, Assistant, Error, Warning, Choice, Review)
│ │ ├── chat.js # Chat-Logic, SSE-Streaming, Plan-Mode │ │ ├── chat.js # Chat-Logic, SSE-Streaming, Plan-Mode
@@ -349,7 +351,7 @@ TelegramBot (1) ─── (*) Session (via telegram_bot_id)
| `tokens` | Auth-Tokens (token → user_id) | | `tokens` | Auth-Tokens (token → user_id) |
| `agents` | AI-Agenten (model – z.B. `claude-sonnet-4-6` oder `nvidia:deepseek-ai/deepseek-r1`, custom_commands, auto_commit, use_soul/identity/user_info/memory/auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale/offset, avatar_small, is_public) | | `agents` | AI-Agenten (model – z.B. `claude-sonnet-4-6` oder `nvidia:deepseek-ai/deepseek-r1`, custom_commands, auto_commit, use_soul/identity/user_info/memory/auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale/offset, avatar_small, is_public) |
| `sessions` | Chat-Sessions (description, image, image_small, workspace, context, custom_commands, telegram_bot_id, telegram_chat_id, avatar_scale/offset, preparer_agent_id, review_agent_id, max_review_iterations, finalizer_agent_id, multi_model_mode, panel_agent_ids, moa_layers) | | `sessions` | Chat-Sessions (description, image, image_small, workspace, context, custom_commands, telegram_bot_id, telegram_chat_id, avatar_scale/offset, preparer_agent_id, review_agent_id, max_review_iterations, finalizer_agent_id, multi_model_mode, panel_agent_ids, moa_layers) |
| `chains` | Request-Chains (model, tokens, iterations, timing, tool_choice, tool_names, stream_error_info, plan_mode, archive_id) | | `chains` | Request-Chains (model, tokens, iterations, timing, tool_choice, tool_names, stream_error_info, workflow_info, plan_mode, effort, archive_id) |
| `messages` | Einzelne Nachrichten (text, tool_use, tool_result, error, web_search_result, auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), SortKey für korrekte Reihenfolge, thought_signature (Gemini) | | `messages` | Einzelne Nachrichten (text, tool_use, tool_result, error, web_search_result, auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), SortKey für korrekte Reihenfolge, thought_signature (Gemini) |
| `attachments` | Datei-Anhänge (base64, image/document) | | `attachments` | Datei-Anhänge (base64, image/document) |
| `archives` | Archivierte Chain-Gruppen | | `archives` | Archivierte Chain-Gruppen |
@@ -380,6 +382,7 @@ TelegramBot (1) ─── (*) Session (via telegram_bot_id)
- `ToolNames` – Komma-separierte Liste der verwendeten Tools - `ToolNames` – Komma-separierte Liste der verwendeten Tools
- `ArchiveId` – Zugehöriges Archiv (wenn archiviert) - `ArchiveId` – Zugehöriges Archiv (wenn archiviert)
- `StreamErrorInfo` – Fehlerdetails bei Stream-Abbrüchen - `StreamErrorInfo` – Fehlerdetails bei Stream-Abbrüchen
- `WorkflowInfo` – JSON-Telemetrie eines dynamischen Workflows (Ultracode): Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung), aggregierte Subagent-Tokens. `null` wenn die Chain keinen Workflow gespawnt hat (siehe [Workflow-Status](#workflow-status))
- `PlanMode` – Ob die Chain im Plan-Modus erstellt wurde (informativ) - `PlanMode` – Ob die Chain im Plan-Modus erstellt wurde (informativ)
- `DurationMs` – Gesamtdauer der Chain - `DurationMs` – Gesamtdauer der Chain
@@ -459,6 +462,7 @@ RemoteConnectionManager → AuthService → DatabaseService → EnsureDefaultUse
| `KreaService` | Singleton | Krea API für Bildgenerierung, Job-Polling, Image-Download | | `KreaService` | Singleton | Krea API für Bildgenerierung, Job-Polling, Image-Download |
| `ImageService` | Singleton | Bild-Resize + WebP-Konvertierung (512px + 256px Thumbnail) für Avatare und Session-Bilder | | `ImageService` | Singleton | Bild-Resize + WebP-Konvertierung (512px + 256px Thumbnail) für Avatare und Session-Bilder |
| `StreamingTracker` | Singleton | Trackt aktive Streams + CancellationTokenSource pro Session für Stop-Button | | `StreamingTracker` | Singleton | Trackt aktive Streams + CancellationTokenSource pro Session für Stop-Button |
| `WorkflowProgressTracker` | Singleton | In-Memory-Telemetrie laufender dynamischer Workflows (Ultracode) pro Session: Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung), aggregierte Subagent-Tokens. Wird beim Chain-Finalize in `chains.workflow_info` persistiert (analog `StreamingTracker`) |
| `KeyScannerService` | Singleton | API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper), täglicher Scan | | `KeyScannerService` | Singleton | API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper), täglicher Scan |
| `GitHubEventScannerService` | Background | Echtzeit-Scanner: GitHub Public Events API (PushEvent-Diffs nach geleakten Keys), 30s Poll-Intervall | | `GitHubEventScannerService` | Background | Echtzeit-Scanner: GitHub Public Events API (PushEvent-Diffs nach geleakten Keys), 30s Poll-Intervall |
| `KeyPatterns` | Static | Zentrale Key-Pattern-Definitionen für alle Scanner (Regex, Typ-Erkennung, Display-Names) | | `KeyPatterns` | Static | Zentrale Key-Pattern-Definitionen für alle Scanner (Regex, Typ-Erkennung, Display-Names) |
@@ -478,7 +482,7 @@ Alle Endpoints folgen dem **Minimal API**-Pattern mit Extension Methods (`MapXxx
| **Auth** | `/api/auth/` | `POST login`, `POST logout`, `GET me`, `GET autologin` | | **Auth** | `/api/auth/` | `POST login`, `POST logout`, `GET me`, `GET autologin` |
| **Chat** | `/api/chat` | `POST` (SSE Streaming), `POST stop` | | **Chat** | `/api/chat` | `POST` (SSE Streaming), `POST stop` |
| **History** | `/api/history` | `GET history`, `GET poll`, `POST archive`, `GET archives`, `GET archive/{id}`, `GET attachments/{id}`, `PUT messages/{id}/mark`, `PUT messages/{id}/delete`, `GET chains/{id}` | | **History** | `/api/history` | `GET history`, `GET poll`, `POST archive`, `GET archives`, `GET archive/{id}`, `GET attachments/{id}`, `PUT messages/{id}/mark`, `PUT messages/{id}/delete`, `GET chains/{id}` |
| **Sessions** | `/api/sessions/` | CRUD + `GET system-prompt`, `GET system-prompt-blocks`, `GET payload-preview`, `GET streaming`, `POST exec`, `GET git-status` (Liste inkl. inline Git-Status + Thumbnail) | | **Sessions** | `/api/sessions/` | CRUD + `GET system-prompt`, `GET system-prompt-blocks`, `GET payload-preview`, `GET streaming`, `GET workflow-status`, `POST exec`, `GET git-status` (Liste inkl. inline Git-Status + Thumbnail) |
| **Agents** | `/api/agents/` | CRUD + `GET available` (inkl. public agents, inline Skills + Thumbnail) | | **Agents** | `/api/agents/` | CRUD + `GET available` (inkl. public agents, inline Skills + Thumbnail) |
| **Admin** | `/api/admin/` | `GET users`, `POST users`, `PUT users/{id}`, `PUT users/{id}/password`, `DELETE users/{id}`, `POST users/{id}/impersonate`, `POST users/{id}/login-link` (Admin-only) | | **Admin** | `/api/admin/` | `GET users`, `POST users`, `PUT users/{id}`, `PUT users/{id}/password`, `DELETE users/{id}`, `POST users/{id}/impersonate`, `POST users/{id}/login-link` (Admin-only) |
| **Workspace** | `/api/workspace/` | `GET tree`, `GET/PUT file`, `GET browse`, `POST mkdir`, `DELETE dir`, `POST browse/mkdir`, `GET agent/tree`, `GET/PUT agent/file`, `POST agent/mkdir`, `GET raw` | | **Workspace** | `/api/workspace/` | `GET tree`, `GET/PUT file`, `GET browse`, `POST mkdir`, `DELETE dir`, `POST browse/mkdir`, `GET agent/tree`, `GET/PUT agent/file`, `POST agent/mkdir`, `GET raw` |
@@ -641,7 +645,8 @@ Beim Server-Start werden verfügbare Modelle von den Provider-APIs geladen und i
### Routing ### Routing
`ChatService.ResolveModelId()` prüft den Model-String: `ChatService.ResolveModelId()` prüft den Model-String:
- Enthält `:` mit bekanntem Provider-Prefix → `OpenAiService.StreamChat()` - Enthält `:` mit bekanntem OpenAI-Provider-Prefix (`nvidia:`/`pollinations:`/`gemini:`/`openai:`/…) → `OpenAiService.StreamChat()`
- `glm:`-Prefix → Prefix wird gestrippt, Provider=`""` → **Claude-CLI-Pfad** mit z.ai-Env-Injection (siehe [z.ai / GLM-Provider](#zai--glm-provider-über-den-cli-pfad))
- Sonst → `AnthropicService.StreamChat()` - Sonst → `AnthropicService.StreamChat()`
--- ---
@@ -855,7 +860,7 @@ Ein **Agent** definiert die Persönlichkeit und Fähigkeiten der AI:
| `WebSearchVersion`| Web-Suche aktivieren (z.B. `web_search_20260209`) | | `WebSearchVersion`| Web-Suche aktivieren (z.B. `web_search_20260209`) |
| `AllowUserChoice` | `ask_user_choice`-Tool verfügbar (Default: true) | | `AllowUserChoice` | `ask_user_choice`-Tool verfügbar (Default: true) |
| `TaskInstruction` | Aufgabe/Anweisung – optionaler Text, wird als `## Anweisungen`-Block in System-Prompt eingefügt | | `TaskInstruction` | Aufgabe/Anweisung – optionaler Text, wird als `## Anweisungen`-Block in System-Prompt eingefügt |
| `Effort` | Reasoning-Effort-Level für Claude. `""` (=high, Default), `low`, `medium`, `high`, `xhigh` (nur Opus 4.7+), `max` (nur Opus 4.6+/Sonnet 4.6). API: als `output_config.effort` gesendet. CLI: via Env-Var `CLAUDE_CODE_EFFORT_LEVEL`. Bei OpenAI-Providern ignoriert. | | `Effort` | Reasoning-Effort-Level für Claude. `""` (=high, Default), `low`, `medium`, `high`, `xhigh` (nur Opus 4.7+), `max` (nur Opus 4.6+/Sonnet 4.6). API: als `output_config.effort` gesendet. CLI: via Env-Var `CLAUDE_CODE_EFFORT_LEVEL`. Bei OpenAI-Providern ignoriert; bei z.ai/GLM nur ab **GLM 5.2** durchgereicht (sonst entfernt). Ultracode hebt es auf `xhigh` an. |
| `IsPublic` | Andere User können den Agent in ihren Sessions verwenden (aber nicht bearbeiten) | | `IsPublic` | Andere User können den Agent in ihren Sessions verwenden (aber nicht bearbeiten) |
### Model-Auflösung ### Model-Auflösung
@@ -1489,7 +1494,7 @@ Folgende Blöcke werden **übersprungen**: Git-Commits. Der Tools-Block listet n
### Frontend ### Frontend
- **Toggle**: `#planModeBtn` (`cycleCompositeMode`) – zykliert **Aus → Plan → Fusion → MoA → Aus**. Plan-Modus transient (`_planModeActive` Compat-Var), Fusion/MoA sofort via `apiUpdateSession({multiModelMode})` persistiert. Gleiche Quelle wie der Modal-Picker in den Session-Einstellungen. - **Toggle**: `#planModeBtn` öffnet ein **Dropdown-Menü** (`_COMPOSITE_MODES`: Normal/Plan/Ultracode/Fusion/MoA). **Alle** Modi sind **transient** (pro Request, nicht persistiert, Reset beim Chat-Wechsel) und werden als Flag mitgeschickt (`planMode`/`ultracode`/`multiModelMode`). In den Session-Einstellungen werden nur noch die Panel-**Mitglieder** (`panelAgentIds`) + `moaLayers` festgelegt (persistent); der Panel-Modus selbst kommt on-demand aus dem Request.
- **Hint**: Bei aktivem Plan-Modus wird ein `<hint>` an jede User-Nachricht angehängt (Reminder: keine Dateien ändern, keine Shell-Befehle außer read-only, keine Commits) - **Hint**: Bei aktivem Plan-Modus wird ein `<hint>` an jede User-Nachricht angehängt (Reminder: keine Dateien ändern, keine Shell-Befehle außer read-only, keine Commits)
- **Chain-Meta**: Badge `📋 Plan-Modus` bei Chains die im Plan-Modus erstellt wurden - **Chain-Meta**: Badge `📋 Plan-Modus` bei Chains die im Plan-Modus erstellt wurden
- **System-Prompt-Modal**: Zeigt den angepassten System-Prompt wenn Plan-Modus aktiv - **System-Prompt-Modal**: Zeigt den angepassten System-Prompt wenn Plan-Modus aktiv
@@ -1501,6 +1506,38 @@ Wenn im Plan-Modus eine `ask_user_choice`-Rückfrage kommt und der User antworte
--- ---
## Ultracode & Workflow-Status
**Ultracode** ist ein Composite-Modus, der eine **einzelne** Anfrage als nativen **dynamischen Workflow** der Claude CLI ausführt: Claude fächert die Arbeit über mehrere gespawnte **Subagenten** parallel auf, lässt Befunde gegenprüfen und synthetisiert am Ende. Wie Plan-/Fusion-/MoA-Modus wird Ultracode **on-demand pro Request** über das Composer-Dropdown (`#planModeBtn`, `_COMPOSITE_MODES`) gewählt – nicht am Agent/an der Session persistiert.
### Aktivierung & Backend-Auswirkungen
- **API:** transientes Flag `ultracode: true` im `ChatRequest` (`ChatModels.cs`, `Ultracode`)
- **Hint:** Bei aktivem Ultracode wird an jede User-Nachricht ein `<hint>` angehängt (*"Bearbeite diese Anfrage als dynamischen Workflow – use a workflow ... Nutze dafür das Workflow-Tool"*)
- **Nur CLI-Pfad / nur Claude:** Ultracode greift ausschließlich im Claude-CLI-Pfad (`StreamChatViaCli`). Es ist **nur für Claude-Modelle** aktiv – **nicht** für z.ai/GLM (kein Workflow-Runtime) und **nicht** im Plan-Modus.
- **Effort:** Ultracode hebt das Reasoning-Effort auf `xhigh` an (bzw. `max`, wenn der Agent bereits `max` setzt) – via Env-Var `CLAUDE_CODE_EFFORT_LEVEL`
- **Allowed-Tools:** Im CLI-Aufruf (`ClaudeCliService.ChatStreamingAsync`) werden zusätzlich `Workflow` (die nativen dynamischen Workflows) und `Task` (die vom Workflow gespawnten Subagenten) zu `--allowed-tools` hinzugefügt. Ohne diese Freigabe lehnt die Permission-Engine das Workflow-Tool headless still ab (kein interaktiver Gate im `-p`-Modus).
### Workflow-Status
Da die CLI Subagent-Events (Top-Level-Messages mit gesetztem `parent_tool_use_id`) bewusst aus dem sichtbaren Stream/History ausblendet (zentraler `parent_tool_use_id`-Guard in `ClaudeCliService.RunCliStreamingAsync`), gäbe es ohne separates Tracking keine Sichtbarkeit auf den Workflow-Fortschritt. Der **Workflow-Status** macht diese Subagent-Telemetrie live und nachträglich abrufbar.
**Backend:**
- **`WorkflowProgressTracker`** (Singleton, registriert in `Program.cs`) – In-Memory-Tracking pro Session: Subagent-Anzahl (dedupliziert per `parent_tool_use_id`), Tool-Aufrufe + Aufschlüsselung (`ToolBreakdown`), aggregierte Subagent-Input-/Output-Tokens, `RecentTools` (letzte 12). Lock-leicht, defensiv – Telemetrie darf den Stream nie blockieren.
- **Tap statt Filter:** Der `parent_tool_use_id`-Guard blendet Subagent-Events weiterhin aus dem Stream aus (`continue`), greift aber für `type == "assistant"`-Turns via `TapSubagentEvent()` die Tool-Namen + Token ab und meldet sie an `RecordSubagent()`. Der existierende Guard bleibt unverändert.
- **Persistierung:** Neue Spalte `chains.workflow_info` (TEXT, via `TryAddColumn` in `DatabaseService.cs`; geschrieben in `DatabaseService.Sessions.cs` `UpdateChain`), Feld `ChainRecord.WorkflowInfo`. Beim `FinalizeChain` (`AnthropicService.cs`) wird die Telemetrie als JSON in die Chain serialisiert und der Live-Tracker freigegeben – mit **ChainId-Abgleich** (`ChainIdFor`), damit ein anderer Finalize-Pfad derselben Session (z.B. ein Panel-Member) den Workflow-Eintrag nicht fälschlich abräumt.
- **Endpoint:** `GET /api/sessions/{id}/workflow-status` (`SessionEndpoint.HandleWorkflowStatus`) liefert in dieser Reihenfolge: (1) `status: "running"` + Live-Snapshot, solange der Tracker einen Eintrag hat; (2) `status: "completed"` + persistiertes `workflow_info` der jüngsten aktiven Chain; (3) `status: "none"` mit `workflow: null`. Top-Level-Felder: `sessionId`, `streaming`, `status`, `chainId` (entfällt bei `status: "none"`) und das Telemetrie-Objekt `workflow` (`null` bei `none`).
**Felder im `workflow`-Objekt** (camelCase): `subagentCount`, `subagentToolCalls`, `subagentInputTokens`, `subagentOutputTokens`, `toolBreakdown`, `recentTools`, `startedAt`, `lastActivityAt`.
**Frontend:**
- Header-Menü-Eintrag **"Workflow-Status"** (Button `#workflowStatusBtn` in `index.html`, direkt nach "Payload anzeigen")
- API-Wrapper `apiGetWorkflowStatus(sessionId)` in `js/api.js`
- `showWorkflowStatusModal(sessionId)` in `js/modals.js` – rendert Status-Head, Statistik-Grid (Subagenten/Tool-Aufrufe/Input-/Output-Tokens), Tool-Aufschlüsselung und Zeitstempel. **Pollt alle 2 s**, solange `status === "running"`, und stoppt automatisch, sobald das Modal geschlossen wird (Overlay nicht mehr im DOM) oder der Workflow endet.
- Click-Handler in `js/pages/chat-page.js`, Styles `.workflow-status-modal` (+ `.wf-*`) in `css/chat.css`
---
## Web-Search ## Web-Search
Anthropic Claude unterstützt Web-Suche als Server-Tool. Das System integriert Web-Search mit Source-Tracking und Rendering. Anthropic Claude unterstützt Web-Suche als Server-Tool. Das System integriert Web-Search mit Source-Tracking und Rendering.
@@ -2007,6 +2044,16 @@ Der frühere harte Abbruch („Kein API-Key zugewiesen") blockte irrtümlich **j
Die MCP-Config (`AnthropicService.Cli.GenerateMcpConfig()`) sucht beim Start dynamisch nach einer Python-Installation in der Reihenfolge `python3` → `python` → `py` (das Ergebnis wird gecached). Auf Linux ist üblicherweise `python3` da, auf Windows oft nur `python` oder `py`. Wird kein Python gefunden, wird im Chat ein SSE-`warning`-Event gesendet (*"Python wurde nicht gefunden... MCP-Tools sind nicht verfügbar."*) – die normalen Tools funktionieren weiterhin. Die MCP-Config (`AnthropicService.Cli.GenerateMcpConfig()`) sucht beim Start dynamisch nach einer Python-Installation in der Reihenfolge `python3` → `python` → `py` (das Ergebnis wird gecached). Auf Linux ist üblicherweise `python3` da, auf Windows oft nur `python` oder `py`. Wird kein Python gefunden, wird im Chat ein SSE-`warning`-Event gesendet (*"Python wurde nicht gefunden... MCP-Tools sind nicht verfügbar."*) – die normalen Tools funktionieren weiterhin.
### z.ai / GLM-Provider (über den CLI-Pfad)
Ein Agent kann **z.ai/GLM-Modelle** verwenden, indem die Modell-ID mit `glm:` prefixt wird (z.B. `glm:glm-5.2`, `glm:glm-4.6`). Diese laufen über den **Anthropic-kompatiblen z.ai-Endpoint** – also über den **Claude-CLI-Pfad**, nicht über den OpenAI-Provider-Pfad.
- **Routing:** `ChatService.ResolveModelId()` strippt das `glm:`-Prefix → die rohe Modell-ID (`glm-5.2`) geht mit Provider=`""` an den CLI-Pfad. `ClaudeCliService.IsZaiModel()` erkennt sie am `glm-`-Prefix.
- **Per-Prozess Env-Injection:** Für GLM-Modelle setzt `ClaudeCliService` `ANTHROPIC_BASE_URL = https://api.z.ai/api/anthropic` und `ANTHROPIC_AUTH_TOKEN = <ZaiApiKey>` (Server-Setting `ZaiApiKey`) nur im jeweiligen Kindprozess. Ohne `ZaiApiKey` läuft der Request **nicht** über z.ai (Warnung im Log).
- **Effort (GLM 5.2+):** `CLAUDE_CODE_EFFORT_LEVEL` wird für **GLM 5.2 oder höher** durchgereicht (`IsZaiEffortModel` per Versionsvergleich `>= 5.2`, damit künftige Releases automatisch mitgenommen werden); für ältere GLM-Versionen (4.x, 5, 5.1, 5-turbo) wird die Env-Var **entfernt**. Effort bleibt damit Claude-/GLM-5.2+-only.
- **Web-Search (web-search-prime):** Wenn der Agent Web-Suche aktiviert hat **und** das Modell GLM + `ZaiApiKey` ist, werden die nativen `WebSearch`/`WebFetch`-Tools **verboten** (`--disallowed-tools`, sonst scheitert das Modell headless an der Berechtigung) und durch z.ai's **`web-search-prime`-MCP-Server** ersetzt (registriert in `GenerateMcpConfig` als `web-search-prime` → `https://api.z.ai/api/mcp/web_search_prime/mcp` mit `Authorization: Bearer <ZaiApiKey>`, freigegeben via `mcp__web-search-prime__*`). Für Claude-Modelle bleiben die nativen `WebSearch`/`WebFetch`-Tools.
- **Kein Ultracode:** Dynamische Workflows (Ultracode) sind nur für Claude-Modelle aktiv – GLM hat kein Workflow-Runtime (`Workflow`/`Task` werden für GLM nicht freigegeben).
--- ---
## Datenbank-Explorer ## Datenbank-Explorer
+18 -5
View File
@@ -89,7 +89,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id)
| `tokens` | Auth-Tokens → User | | `tokens` | Auth-Tokens → User |
| `agents` | AI-Agenten (Model – z.B. `claude-sonnet-4-6` oder `nvidia:model-id`, CustomCommands, AutoCommit, UseSoul/Identity/UserInfo/Memory/AutoContext, UseGitHubSearch, UseSerperSearch, WebSearch, AllowUserChoice, TaskInstruction, Effort, AvatarScale/Offset, AvatarSmall, IsPublic) | | `agents` | AI-Agenten (Model – z.B. `claude-sonnet-4-6` oder `nvidia:model-id`, CustomCommands, AutoCommit, UseSoul/Identity/UserInfo/Memory/AutoContext, UseGitHubSearch, UseSerperSearch, WebSearch, AllowUserChoice, TaskInstruction, Effort, AvatarScale/Offset, AvatarSmall, IsPublic) |
| `sessions` | Chat-Sessions (Description, Image, ImageSmall, Workspace, Context, CustomCommands, TelegramBotId, TelegramChatId, AvatarScale/Offset, PreparerAgentId, ReviewAgentId, MaxReviewIterations, FinalizerAgentId, MultiModelMode, PanelAgentIds, MoaLayers) | | `sessions` | Chat-Sessions (Description, Image, ImageSmall, Workspace, Context, CustomCommands, TelegramBotId, TelegramChatId, AvatarScale/Offset, PreparerAgentId, ReviewAgentId, MaxReviewIterations, FinalizerAgentId, MultiModelMode, PanelAgentIds, MoaLayers) |
| `chains` | Ein Request-Response-Zyklus (Tokens, Timing, Iterations, PlanMode, ToolChoice, ToolNames, StreamErrorInfo, ArchiveId) | | `chains` | Ein Request-Response-Zyklus (Tokens, Timing, Iterations, PlanMode, Effort, ToolChoice, ToolNames, StreamErrorInfo, ArchiveId, WorkflowInfo = JSON-Telemetrie eines dynamischen ultracode-Workflows, null = keiner) |
| `messages` | Einzelne Nachrichten (text / tool_use / tool_result / error / web_search_result / auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), thought_signature (Gemini) | | `messages` | Einzelne Nachrichten (text / tool_use / tool_result / error / web_search_result / auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), thought_signature (Gemini) |
| `attachments` | Base64-Anhänge (Bilder, Dokumente) | | `attachments` | Base64-Anhänge (Bilder, Dokumente) |
| `archives` | Archivierte Chain-Gruppen | | `archives` | Archivierte Chain-Gruppen |
@@ -114,7 +114,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id)
| Auth | `/api/auth/` | `POST login`, `POST logout`, `GET me`, `GET autologin` | | Auth | `/api/auth/` | `POST login`, `POST logout`, `GET me`, `GET autologin` |
| Chat | `/api/chat` | `POST` → SSE Stream, `POST stop` | | Chat | `/api/chat` | `POST` → SSE Stream, `POST stop` |
| History | `/api/history` | GET history, poll, archive, marks, delete | | History | `/api/history` | GET history, poll, archive, marks, delete |
| Sessions | `/api/sessions/` | CRUD + system-prompt, system-prompt-blocks, payload-preview, streaming, exec, git-status | | Sessions | `/api/sessions/` | CRUD + system-prompt, system-prompt-blocks, payload-preview, streaming, workflow-status, exec, git-status |
| Agents | `/api/agents/` | CRUD + available (inkl. public agents, inline Skills) | | Agents | `/api/agents/` | CRUD + available (inkl. public agents, inline Skills) |
| Admin | `/api/admin/` | User CRUD, impersonate, login-link (Admin-only) | | Admin | `/api/admin/` | User CRUD, impersonate, login-link (Admin-only) |
| Workspace | `/api/workspace/` | tree, file read/write, mkdir, rmdir, browse, agent-files | | Workspace | `/api/workspace/` | tree, file read/write, mkdir, rmdir, browse, agent-files |
@@ -162,6 +162,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id)
| **EmbeddingBackgroundService** | Backfill bestehender Messages + Live-Embedding neuer Messages/Memory-Files | | **EmbeddingBackgroundService** | Backfill bestehender Messages + Live-Embedding neuer Messages/Memory-Files |
| **RemoteConnectionManager** | WebSocket-basierte Remote-Verbindungen, Command-Execution auf Remote-Clients | | **RemoteConnectionManager** | WebSocket-basierte Remote-Verbindungen, Command-Execution auf Remote-Clients |
| **StreamingTracker** | Tracking aktiver Streams + CancellationTokenSource pro Session für Stop | | **StreamingTracker** | Tracking aktiver Streams + CancellationTokenSource pro Session für Stop |
| **WorkflowProgressTracker** | In-Memory-Telemetrie laufender dynamischer ultracode-Workflows pro Session (Subagent-Anzahl, Tool-Aufrufe + Aufschlüsselung, aggregierte Subagent-Tokens); LIVE während des Laufs, danach in `chains.workflow_info` persistiert (analog StreamingTracker) |
| **AppPaths** | Zentrale Pfade (WwwRoot, Dev vs. Publish-Modus) (static) | | **AppPaths** | Zentrale Pfade (WwwRoot, Dev vs. Publish-Modus) (static) |
| **KeyScannerService** | API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper), täglicher Scan | | **KeyScannerService** | API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper), täglicher Scan |
| **GitHubEventScannerService** | Echtzeit-Scanner: GitHub Public Events API (PushEvent-Diffs nach geleakten Keys), 30s Poll-Intervall | | **GitHubEventScannerService** | Echtzeit-Scanner: GitHub Public Events API (PushEvent-Diffs nach geleakten Keys), 30s Poll-Intervall |
@@ -349,7 +350,7 @@ Eine Session kann bis zu **vier Agents** haben: Worker (Pflicht), Preparer (opti
## Multi-Modell-Panel (Fusion / MoA) ## Multi-Modell-Panel (Fusion / MoA)
Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten haben (`multi_model_mode = "fusion"`/`"moa"`, `panel_agent_ids` = CSV der Agent-IDs, `moa_layers` = Proposer-Runden bei MoA, Default 2). Bei jeder Nachricht antworten alle Panel-Agenten **parallel** auf die Anfrage; ihre Antworten werden dem **Worker** als `<panel_responses>`-Block in die letzte User-Message gemergt, der daraus **eine** Gesamtantwort synthetisiert und normal streamt (OpenRouter-Fusion-/Mixture-of-Agents-Muster). Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten haben. In den Session-Einstellungen werden nur die **Mitglieder** persistiert (`panel_agent_ids` = CSV der Agent-IDs, `moa_layers` = Proposer-Runden bei MoA, Default 2). Der Panel-**Modus** (`fusion`/`moa`) wird **on-demand pro Anfrage** über den Composer-Toggle gewählt (transienter Request-Flag `multiModelMode`, nicht persistiert – wie Plan-/Ultracode-Modus). Ist er aktiv, antworten alle Panel-Agenten **parallel** auf die Anfrage; ihre Antworten werden dem **Worker** als `<panel_responses>`-Block in die letzte User-Message gemergt, der daraus **eine** Gesamtantwort synthetisiert und normal streamt (OpenRouter-Fusion-/Mixture-of-Agents-Muster).
**Fusion vs. MoA:** Fusion = 1 Proposer-Runde → Synthese. **MoA** = `moa_layers` Runden (1–5, Backstop): ab Runde 2 sieht jeder Member die Antworten der Vorrunde (`<previous_layer_responses>`, isoliert pro Member via `CloneForMember` gemergt) und verfeinert. `RunPanel(layers)` ist EINE Engine (Fusion = `layers==1`). **Alle** Runden werden persistiert (F5-sicher); JSON-Transkript pro Member enthält `layer`/`totalLayers` + Text-/Tool-Segmente (`Iteration = layer*100+index+1` macht DB-Zeilen layer-aware). **Fusion vs. MoA:** Fusion = 1 Proposer-Runde → Synthese. **MoA** = `moa_layers` Runden (1–5, Backstop): ab Runde 2 sieht jeder Member die Antworten der Vorrunde (`<previous_layer_responses>`, isoliert pro Member via `CloneForMember` gemergt) und verfeinert. `RunPanel(layers)` ist EINE Engine (Fusion = `layers==1`). **Alle** Runden werden persistiert (F5-sicher); JSON-Transkript pro Member enthält `layer`/`totalLayers` + Text-/Tool-Segmente (`Iteration = layer*100+index+1` macht DB-Zeilen layer-aware).
@@ -357,13 +358,13 @@ Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten habe
**Member streamen LIVE** (wie der Session-Agent): CLI forwardet `TextDelta`/`ToolStart`/`ToolResult`, Anthropic-API streamt pro Tool-Runde über `NonStreamingLoopCallbacks`, OpenAI = one-shot (Text am Ende). Ein `SemaphoreSlim`-Write-Lock in `SseHelper.TryWriteSse` serialisiert die SSE-Byte-Flushes paralleler Member (sonst vermischen sich die Frames). **Member streamen LIVE** (wie der Session-Agent): CLI forwardet `TextDelta`/`ToolStart`/`ToolResult`, Anthropic-API streamt pro Tool-Runde über `NonStreamingLoopCallbacks`, OpenAI = one-shot (Text am Ende). Ein `SemaphoreSlim`-Write-Lock in `SseHelper.TryWriteSse` serialisiert die SSE-Byte-Flushes paralleler Member (sonst vermischen sich die Frames).
**Subagent-Filterung:** Spawnt ein CLI-Member intern einen Subagenten (Claude-CLI-`Agent`-Tool), sendet die CLI dessen Interna als separate Top-Level-Events mit `parent_tool_use_id`. Der Parser (`ClaudeCliService.RunCliStreamingAsync`) verwirft diese **zentral am Ursprung** (Guard auf nicht-leeres `parent_tool_use_id`) → keine namenlosen Tool-Boxen, weder im Live-Stream noch in der History (beide lesen aus denselben gefilterten `CliStreamEvent`s). Die `Agent`-Tool-Box selbst (`parent_tool_use_id=null`) bleibt sichtbar; der finale `result`-Event (mit den **aggregierten** Subagent-Tokens) wird nicht gefiltert → Token-Bilanz vollständig. **Subagent-Filterung:** Spawnt ein CLI-Member intern einen Subagenten (Claude-CLI-`Agent`-Tool), sendet die CLI dessen Interna als separate Top-Level-Events mit `parent_tool_use_id`. Der Parser (`ClaudeCliService.RunCliStreamingAsync`) verwirft diese **zentral am Ursprung** (Guard auf nicht-leeres `parent_tool_use_id`) → keine namenlosen Tool-Boxen, weder im Live-Stream noch in der History (beide lesen aus denselben gefilterten `CliStreamEvent`s). Die `Agent`-Tool-Box selbst (`parent_tool_use_id=null`) bleibt sichtbar; der finale `result`-Event (mit den **aggregierten** Subagent-Tokens) wird nicht gefiltert → Token-Bilanz vollständig. Dieselben unterdrückten Subagent-Events werden zusätzlich **additiv** in den `WorkflowProgressTracker` abgegriffen (`TapSubagentEvent`, wirft nie) → speist den Workflow-Status, ohne den Guard/Stream zu verändern.
**Backend:** `ChatService.PrepareContext` baut `ChatContext.PanelAgents` (`List<SubAgentContext>`) + `MoaLayers`. `RunPanel(layers)` (AnthropicService.Panel.cs) läuft im ChatEndpoint-try VOR dem Worker-Dispatch: pro Runde Member parallel (`Task.WhenAll` + `SemaphoreSlim`), Provider-Weiche pro Member, **jede** Runde als `[Panel: Name]`-Message mit `Tag="panel"` persistiert (aus Worker-History gefiltert); JSON-Transkript = `{name, model, layer, totalLayers, segments:[Text/Tool…]}`. `NonStreamingLoopCallbacks.Persist=false` verhindert Doppel-Persistenz. Member-Tokens via `seedUsage` in Worker-`usage` gefaltet → Live-Badge **und** DB (kein separates Panel-Usage-Event). Synthese-Block in letzte User-Message gemergt. Panel-Fehler graceful. **Backend:** `ChatService.PrepareContext` baut `ChatContext.PanelAgents` (`List<SubAgentContext>`) + `MoaLayers`. `RunPanel(layers)` (AnthropicService.Panel.cs) läuft im ChatEndpoint-try VOR dem Worker-Dispatch: pro Runde Member parallel (`Task.WhenAll` + `SemaphoreSlim`), Provider-Weiche pro Member, **jede** Runde als `[Panel: Name]`-Message mit `Tag="panel"` persistiert (aus Worker-History gefiltert); JSON-Transkript = `{name, model, layer, totalLayers, segments:[Text/Tool…]}`. `NonStreamingLoopCallbacks.Persist=false` verhindert Doppel-Persistenz. Member-Tokens via `seedUsage` in Worker-`usage` gefaltet → Live-Badge **und** DB (kein separates Panel-Usage-Event). Synthese-Block in letzte User-Message gemergt. Panel-Fehler graceful.
**SSE-Events:** `panel_start` (mode/layers), `panel_layer_start`/`panel_layer_done` (MoA-Runden), `panel_member_start` (mit `layer`), `panel_member_text`/`panel_member_tool_use`/`panel_member_tool_end`/`panel_member_tool` (live), `panel_member_done` (inkl. per-Member Token-Feldern `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens`), `panel_done`. **SSE-Events:** `panel_start` (mode/layers), `panel_layer_start`/`panel_layer_done` (MoA-Runden), `panel_member_start` (mit `layer`), `panel_member_text`/`panel_member_tool_use`/`panel_member_tool_end`/`panel_member_tool` (live), `panel_member_done` (inkl. per-Member Token-Feldern `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens`), `panel_done`.
**Frontend:** `stream-ui.js` rendert pro Member eine `.panel-bubble`, gekeyt per `layer_index` (MoA: gleiche Member über mehrere Runden; aktive Runde pro Index wird bei `panel_member_start` gemerkt, da Runden sequenziell laufen). `panel_layer_start` zeigt bei >1 Runde einen `.panel-layer-divider`. **History:** `history.js _renderPanelMemberBubble` parst JSON-Transkript → Text + Tool-Boxen interleaved; bei MoA Runden-Divider beim Layer-Wechsel (Back-Compat für Alt-Format). Picker: `#multiModelMode`-Dropdown (Aus/Fusion/MoA) + `#moaLayers` + Mehrfach-Agenten-Picker (`_openMultiPickerModal`). Vereinter Composer-Toggle: **ein** Button `#planModeBtn` (chat.js `cycleCompositeMode`) schaltet zyklisch Aus→Plan→Fusion→MoA durch. Plan ist transient (Compat-Var `_planModeActive`), Fusion/MoA werden sofort via `apiUpdateSession({multiModelMode})` persistiert – gleiche Quelle wie der Picker. Modus-PATCHes laufen über eine **serialisierte Promise-Kette** (`_modePersist`, letzter Klick gewinnt); `sendMessage`/`sendMessageDirect` **awaiten** sie, bevor der Chat-Request die Session lädt → verhindert Race (z.B. MoA mit nur 1 Runde, weil das Backend noch den alten Modus liest). **Per-Member-Token-Badge:** `buildPanelMemberMeta(d)` (`stream-ui.js`, global) baut ein kompaktes `.panel-member-meta`-Badge aus den Token-Feldern – Live aus `panel_member_done`, History aus dem persistierten JSON (gleicher Helper); graceful bei fehlenden Daten (OpenAI, Fehler-Member, Alt-History → kein Badge). **Frontend:** `stream-ui.js` rendert pro Member eine `.panel-bubble`, gekeyt per `layer_index` (MoA: gleiche Member über mehrere Runden; aktive Runde pro Index wird bei `panel_member_start` gemerkt, da Runden sequenziell laufen). `panel_layer_start` zeigt bei >1 Runde einen `.panel-layer-divider`. **History:** `history.js _renderPanelMemberBubble` parst JSON-Transkript → Text + Tool-Boxen interleaved; bei MoA Runden-Divider beim Layer-Wechsel (Back-Compat für Alt-Format). Settings-Picker: **nur** die Panel-Mitglieder (`#panelAgentIds` via `_openMultiPickerModal`) + `#moaLayers` – beide immer sichtbar und **persistent**. Der Panel-**Modus** wird NICHT mehr in den Settings gewählt. Composer-Toggle: **ein** Button `#planModeBtn` (chat.js) öffnet ein **Dropdown-Menü** (`_COMPOSITE_MODES`: Normal/Plan/Ultracode/Fusion/MoA, Muster wie der Emoji-Picker; aktiver Eintrag markiert, Button trägt das Modus-Icon). **Alle** Modi sind **transient** (pro Request, nicht persistiert, beim Chat-Wechsel via `initCompositeMode` auf `off` zurückgesetzt): der gewählte Modus wird als Flag in den `/api/chat`-Payload gelegt (`planMode`/`ultracode`; Fusion/MoA als `multiModelMode`, nur diese beiden Werte). Das Backend baut das Panel daraus **on-demand** (`ChatService.PrepareContext(multiModelMode)` → `effectiveMultiModelMode`), die Mitglieder weiterhin aus der persistierten `session.PanelAgentIds`. **Per-Member-Token-Badge:** `buildPanelMemberMeta(d)` (`stream-ui.js`, global) baut ein kompaktes `.panel-member-meta`-Badge aus den Token-Feldern – Live aus `panel_member_done`, History aus dem persistierten JSON (gleicher Helper); graceful bei fehlenden Daten (OpenAI, Fehler-Member, Alt-History → kein Badge).
**Web-Search Sources:** **Web-Search Sources:**
- `web_search_result`-Messages in DB gespeichert (Sources als JSON-Array, leere werden übersprungen) - `web_search_result`-Messages in DB gespeichert (Sources als JSON-Array, leere werden übersprungen)
@@ -372,6 +373,18 @@ Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten habe
--- ---
## Workflow-Status (dynamische ultracode-Workflows)
Live-Telemetrie der Subagenten, die ein **ultracode**-Lauf über das `Workflow`/`Task`-Tool der Claude CLI spawnt (diese Subagent-Events sind im Haupt-Stream/Verlauf bewusst ausgeblendet, siehe Subagent-Filterung oben).
**Backend:** `WorkflowProgressTracker` (Singleton, lokale Instanz in `Program.cs`, weil `ClaudeCliService` befüllt und `AnthropicService` beim Finalize persistiert – dieselbe Instanz). `ClaudeCliService.TapSubagentEvent` → `RecordSubagent(...)` führt pro Session In-Memory: Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung), aggregierte Input/Output-Tokens. Beim `FinalizeChain` (`AnthropicService.cs`) wird der Stand als JSON in `chains.workflow_info` (`ChainRecord.WorkflowInfo`, Migration via `TryAddColumn` in `DatabaseService.cs`, geschrieben in `DatabaseService.Sessions.cs` `UpdateChain`) geschrieben – mit ChainId-Abgleich, damit ein Panel-Member derselben Session ihn nicht löscht.
**Endpoint:** `GET /api/sessions/{id}/workflow-status` → LIVE-Snapshot aus dem Tracker, sonst persistiertes `workflow_info` der jüngsten Chain, sonst `status: "none"`. JSON: `{ sessionId, streaming, status (running/completed/none), chainId, workflow }`; das `workflow`-Objekt trägt camelCase `subagentCount`, `subagentToolCalls`, `subagentInputTokens`, `subagentOutputTokens`, `toolBreakdown`, `recentTools`, `startedAt`, `lastActivityAt`.
**Frontend:** Header-Menü-Eintrag **„Workflow-Status"** (`#workflowStatusBtn` in `index.html`, direkt nach „Payload anzeigen"), Wrapper `apiGetWorkflowStatus` (`api.js`), `showWorkflowStatusModal` (`modals.js`, pollt alle 2 s solange `status === 'running'`, stoppt von selbst beim Schließen des Modals), Click-Handler in `pages/chat-page.js`, `.workflow-status-modal`-Styles in `css/chat.css`.
---
## Wichtige Regeln beim Entwickeln ## Wichtige Regeln beim Entwickeln
- ⚠️ **Quelldateien in `wwwroot/`** editieren, NICHT in `Build/*/publish/wwwroot/` - ⚠️ **Quelldateien in `wwwroot/`** editieren, NICHT in `Build/*/publish/wwwroot/`
+1 -1
View File
@@ -56,7 +56,7 @@ public static class ChatEndpoint
var remoteConn = remoteMgr.GetBySessionId(sessionId); var remoteConn = remoteMgr.GetBySessionId(sessionId);
var hasRemote = remoteConn != null; var hasRemote = remoteConn != null;
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote, remoteOs: remoteConn?.OsVersion); var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, multiModelMode: body.MultiModelMode, hasRemoteConnection: hasRemote, remoteOs: remoteConn?.OsVersion);
if (ctx == null) if (ctx == null)
{ {
res.StatusCode = 400; res.StatusCode = 400;
+49
View File
@@ -28,6 +28,7 @@ public static class SessionEndpoint
app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks); app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks);
app.MapGet("/api/sessions/{sessionId}/payload-preview", HandlePayloadPreview); app.MapGet("/api/sessions/{sessionId}/payload-preview", HandlePayloadPreview);
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
app.MapGet("/api/sessions/{sessionId}/workflow-status", HandleWorkflowStatus);
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
} }
@@ -430,6 +431,54 @@ public static class SessionEndpoint
await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId }); await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId });
} }
/// <summary>
/// Status eines dynamischen Workflows (ultracode) für die Session: LIVE aus dem
/// WorkflowProgressTracker, solange ein Lauf aktiv ist – sonst die zuletzt persistierte
/// Telemetrie (chains.workflow_info) der jüngsten Chain. Liefert Subagent-Anzahl,
/// Tool-Aufrufe (+ Aufschlüsselung) und aggregierte Subagent-Tokens.
/// </summary>
private static async Task HandleWorkflowStatus(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker, WorkflowProgressTracker workflowTracker)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
var streaming = streamingTracker.IsStreaming(sessionId);
// 1) Live: ein Workflow läuft gerade (Tracker hat einen Eintrag).
var live = workflowTracker.GetSnapshot(sessionId);
if (live != null)
{
await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = "running", chainId = live.ChainId, workflow = live });
return;
}
// 2) Sonst: zuletzt persistierte Workflow-Telemetrie aus den aktiven Chains der Session.
var chains = await db.GetActiveChains(sessionId); // ORDER BY id DESC
var last = chains.FirstOrDefault(c => !string.IsNullOrEmpty(c.WorkflowInfo));
if (last != null)
{
WorkflowProgress wp = null;
try { wp = JsonSerializer.Deserialize<WorkflowProgress>(last.WorkflowInfo); } catch { }
await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = "completed", chainId = last.ChainId, workflow = wp });
return;
}
// 3) Für diese Session lief nie ein Workflow.
await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = "none", workflow = (object)null });
}
private static async Task HandleExec(HttpContext ctx, DatabaseService db) private static async Task HandleExec(HttpContext ctx, DatabaseService db)
{ {
var userId = ctx.GetUserId(); var userId = ctx.GetUserId();
+1
View File
@@ -19,6 +19,7 @@ public class ChainRecord
public string ToolNames { get; set; } public string ToolNames { get; set; }
public string ArchiveId { get; set; } public string ArchiveId { get; set; }
public string StreamErrorInfo { get; set; } public string StreamErrorInfo { get; set; }
public string WorkflowInfo { get; set; } // JSON: Telemetrie eines dynamischen Workflows (ultracode), null = kein Workflow
public bool PlanMode { get; set; } public bool PlanMode { get; set; }
public string Effort { get; set; } public string Effort { get; set; }
public string CreatedAt { get; set; } public string CreatedAt { get; set; }
+9
View File
@@ -158,4 +158,13 @@ public class ChatRequest
/// </summary> /// </summary>
[JsonPropertyName("ultracode")] [JsonPropertyName("ultracode")]
public bool Ultracode { get; set; } public bool Ultracode { get; set; }
/// <summary>
/// On-demand Panel-Modus für genau diese Anfrage (transient, NICHT persistent):
/// "" / "off" / "fusion" / "moa". Wie Plan-/Ultracode-Modus pro Request gewählt.
/// Die Panel-Mitglieder kommen weiterhin aus der persistierten Session (PanelAgentIds),
/// nur der Modus (ob/wie das Panel läuft) wird hier pro Anfrage gesetzt.
/// </summary>
[JsonPropertyName("multiModelMode")]
public string MultiModelMode { get; set; } = "";
} }
+9 -2
View File
@@ -52,6 +52,12 @@ builder.Services.AddSingleton(authService);
builder.Services.AddSingleton(dbService); builder.Services.AddSingleton(dbService);
builder.Services.AddSingleton(new ImageService()); builder.Services.AddSingleton(new ImageService());
builder.Services.AddSingleton(new StreamingTracker()); builder.Services.AddSingleton(new StreamingTracker());
// Workflow-Status-Tracker (Live-Telemetrie dynamischer ultracode-Workflows). Lokale Instanz,
// weil ClaudeCliService (befüllt) und AnthropicService (persistiert beim Finalize) DIESELBE
// Instanz brauchen; zusätzlich als Singleton für den Status-Endpoint registriert.
var workflowTracker = new WorkflowProgressTracker();
builder.Services.AddSingleton(workflowTracker);
var steeringQueue = new SteeringQueue(); var steeringQueue = new SteeringQueue();
builder.Services.AddSingleton(steeringQueue); builder.Services.AddSingleton(steeringQueue);
builder.Services.AddSingleton(new RemoteConnectionManager()); builder.Services.AddSingleton(new RemoteConnectionManager());
@@ -91,7 +97,8 @@ builder.Services.AddSingleton<AnthropicService>(sp =>
serverSettings, serverSettings,
vectorSearch: vectorSearchService, vectorSearch: vectorSearchService,
embeddingBg: embeddingBgService, embeddingBg: embeddingBgService,
steeringQueue: steeringQueue); steeringQueue: steeringQueue,
workflowTracker: workflowTracker);
svc.SetChatService(chatService); svc.SetChatService(chatService);
return svc; return svc;
}); });
@@ -127,7 +134,7 @@ builder.Services.AddHostedService<GitHubEventScannerService>(sp => sp.GetRequire
// Claude CLI Service (für OAuth-basierte Chats via CLI; serverSettings für z.ai/GLM-Env) // Claude CLI Service (für OAuth-basierte Chats via CLI; serverSettings für z.ai/GLM-Env)
builder.Services.AddSingleton<ClaudeCliService>(sp => builder.Services.AddSingleton<ClaudeCliService>(sp =>
new ClaudeCliService(sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<ClaudeCliService>>(), serverSettings)); new ClaudeCliService(sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<ClaudeCliService>>(), serverSettings, workflowTracker));
var app = builder.Build(); var app = builder.Build();
+7 -4
View File
@@ -2,7 +2,7 @@
# // gott # // gott
Self-hosted AI-Coding-Agent mit **Anthropic Claude API** als Haupt-Provider (inkl. Claude CLI-Integration) sowie **OpenAI-kompatiblen Providern** (Nvidia, Pollinations, Google Gemini, OpenAI). Stellt eine Web-UI bereit, über die man in Sessions mit konfigurierbaren Agenten chatten kann. Die Agenten haben Tool-Zugriff (Dateien lesen/schreiben/bearbeiten, Shell-Befehle ausführen, Web-/Code-Suche, Todos, Memory) auf ein definiertes Workspace-Verzeichnis. Self-hosted AI-Coding-Agent mit **Anthropic Claude API** als Haupt-Provider (inkl. Claude CLI-Integration), **z.ai/GLM** (über die Claude CLI) sowie **OpenAI-kompatiblen Providern** (Nvidia, Pollinations, Google Gemini, OpenAI). Stellt eine Web-UI bereit, über die man in Sessions mit konfigurierbaren Agenten chatten kann. Die Agenten haben Tool-Zugriff (Dateien lesen/schreiben/bearbeiten, Shell-Befehle ausführen, Web-/Code-Suche, Todos, Memory) auf ein definiertes Workspace-Verzeichnis.
--- ---
@@ -14,9 +14,12 @@ Self-hosted AI-Coding-Agent mit **Anthropic Claude API** als Haupt-Provider (ink
- **Finalizer-Agent** – Optionaler Agent, der nach dem Review abschließende Arbeiten durchführt (z.B. Git-Commit, Deployment) - **Finalizer-Agent** – Optionaler Agent, der nach dem Review abschließende Arbeiten durchführt (z.B. Git-Commit, Deployment)
- **Multi-Modell-Panel (Fusion / MoA)** – Mehrere KI-Agenten beantworten dieselbe Anfrage **parallel** (read-only, live gestreamt); der Worker synthetisiert daraus **eine** Gesamtantwort (OpenRouter-Fusion-/Mixture-of-Agents-Muster). MoA unterstützt mehrere Verfeinerungs-Runden, Token-Badge pro Member - **Multi-Modell-Panel (Fusion / MoA)** – Mehrere KI-Agenten beantworten dieselbe Anfrage **parallel** (read-only, live gestreamt); der Worker synthetisiert daraus **eine** Gesamtantwort (OpenRouter-Fusion-/Mixture-of-Agents-Muster). MoA unterstützt mehrere Verfeinerungs-Runden, Token-Badge pro Member
- **Interaktive Rückfragen** – `ask_user_choice`-Tool: Agent stellt dem User klickbare Auswahlmöglichkeiten - **Interaktive Rückfragen** – `ask_user_choice`-Tool: Agent stellt dem User klickbare Auswahlmöglichkeiten
- **Plan-Modus** – Read-only-Modus zum Analysieren und Planen ohne Änderungen (Toggle im Chat) - **Modus-Menü** – Dropdown im Composer (Normal / Plan / Ultracode / Fusion / MoA); alle Modi sind transient und gelten nur für die nächste Anfrage
- **Plan-Modus** – Read-only-Modus zum Analysieren und Planen ohne Änderungen
- **Ultracode** – Composite-Modus, der eine einzelne Anfrage als nativen dynamischen Workflow ausführt (effort `xhigh`, gibt der Claude CLI die `Workflow`/`Task`-Tools frei; fächert die Arbeit über mehrere Subagenten auf). Nur Claude-Modelle (nicht GLM, nicht Plan-Modus)
- **Workflow-Status** – Live-Telemetrie laufender Ultracode-Workflows (Subagent-Anzahl, Tool-Aufrufe inkl. Aufschlüsselung, aggregierte Token); im Header-Menü pro Session abrufbar, persistiert nach Lauf-Ende
- **Web-Search** – Integrierte Web-Suche mit Quellenangaben (URLs + Titel) in Chat und geteilten Nachrichten - **Web-Search** – Integrierte Web-Suche mit Quellenangaben (URLs + Titel) in Chat und geteilten Nachrichten
- **Multi-Provider** – Anthropic Claude (Haupt-Provider, inkl. Claude CLI), Nvidia (g4f.space), Pollinations (kostenlos), Google Gemini (kostenloser Free Tier), OpenAI - **Multi-Provider** – Anthropic Claude (Haupt-Provider, inkl. Claude CLI), z.ai/GLM (Modell-IDs mit `glm:`-Prefix, laufen über die Claude CLI mit z.ai-Endpoint), Nvidia (g4f.space), Pollinations (kostenlos), Google Gemini (kostenloser Free Tier), OpenAI
- **19 Tools** – `read_file`, `write_file`, `edit_file`, `list_files`, `delete_file`, `execute`, `background_execute`, `glob`, `grep`, `todo_write`, `memory_read`, `memory_write`, `memory_list`, `memory_search`, `remote_execute`, `send_to_session`, `ask_user_choice`, `github_code_search`, `serper_web_search` - **19 Tools** – `read_file`, `write_file`, `edit_file`, `list_files`, `delete_file`, `execute`, `background_execute`, `glob`, `grep`, `todo_write`, `memory_read`, `memory_write`, `memory_list`, `memory_search`, `remote_execute`, `send_to_session`, `ask_user_choice`, `github_code_search`, `serper_web_search`
- **Streaming** – Echtzeit-Antworten via Server-Sent Events (SSE) - **Streaming** – Echtzeit-Antworten via Server-Sent Events (SSE)
- **Stop-Button** – Laufende Requests zuverlässig abbrechen (inkl. laufender Shell-Befehle) - **Stop-Button** – Laufende Requests zuverlässig abbrechen (inkl. laufender Shell-Befehle)
@@ -47,7 +50,7 @@ Self-hosted AI-Coding-Agent mit **Anthropic Claude API** als Haupt-Provider (ink
|---------|-------------| |---------|-------------|
| **Backend** | .NET 10 Minimal API, C# | | **Backend** | .NET 10 Minimal API, C# |
| **Datenbank** | SQLite + Dapper | | **Datenbank** | SQLite + Dapper |
| **AI** | Anthropic Claude API (+ Claude CLI) + OpenAI-kompatible APIs (Nvidia, Pollinations, Gemini, OpenAI) | | **AI** | Anthropic Claude API (+ Claude CLI) + z.ai/GLM (über Claude CLI) + OpenAI-kompatible APIs (Nvidia, Pollinations, Gemini, OpenAI) |
| **Embeddings** | Microsoft.ML.OnnxRuntime + all-MiniLM-L6-v2 (Sentence-Embeddings) | | **Embeddings** | Microsoft.ML.OnnxRuntime + all-MiniLM-L6-v2 (Sentence-Embeddings) |
| **Frontend** | Vanilla JS SPA (kein Framework), CSS Variables | | **Frontend** | Vanilla JS SPA (kein Framework), CSS Variables |
| **Mathe** | KaTeX (LaTeX-Rendering) | | **Mathe** | KaTeX (LaTeX-Rendering) |
+2
View File
@@ -307,6 +307,8 @@ public partial class AnthropicService
mcpConfigPath: mcpConfigPath, mcpConfigPath: mcpConfigPath,
hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl), hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl),
effort: effort, effort: effort,
sessionId: sessionId,
chainId: chainId,
cancellationToken: segCts.Token cancellationToken: segCts.Token
); );
} }
+15 -2
View File
@@ -44,11 +44,12 @@ public partial class AnthropicService
private readonly VectorSearchService _vectorSearch; private readonly VectorSearchService _vectorSearch;
private readonly EmbeddingBackgroundService _embeddingBg; private readonly EmbeddingBackgroundService _embeddingBg;
private readonly SteeringQueue _steeringQueue; private readonly SteeringQueue _steeringQueue;
private readonly WorkflowProgressTracker _workflowTracker;
private ChatService _chatService; private ChatService _chatService;
private TelegramService _telegramService; private TelegramService _telegramService;
private ClaudeCliService _cliService; private ClaudeCliService _cliService;
public AnthropicService(IHttpClientFactory httpFactory, ToolService toolService, DatabaseService db, MemoryService memoryService = null, ServerSettingsService settings = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null, SteeringQueue steeringQueue = null) public AnthropicService(IHttpClientFactory httpFactory, ToolService toolService, DatabaseService db, MemoryService memoryService = null, ServerSettingsService settings = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null, SteeringQueue steeringQueue = null, WorkflowProgressTracker workflowTracker = null)
{ {
_httpFactory = httpFactory; _httpFactory = httpFactory;
_toolService = toolService; _toolService = toolService;
@@ -58,6 +59,7 @@ public partial class AnthropicService
_vectorSearch = vectorSearch; _vectorSearch = vectorSearch;
_embeddingBg = embeddingBg; _embeddingBg = embeddingBg;
_steeringQueue = steeringQueue; _steeringQueue = steeringQueue;
_workflowTracker = workflowTracker;
} }
public void SetCliService(ClaudeCliService cliService) public void SetCliService(ClaudeCliService cliService)
@@ -320,7 +322,18 @@ public partial class AnthropicService
} }
private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null) private Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null)
=> SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw, streamErrorInfo); {
// Workflow-Telemetrie (ultracode-Subagenten) für DIESE Chain persistieren und den
// Live-Tracker freigeben. Der ChainId-Abgleich verhindert, dass ein anderer Finalize-Pfad
// derselben Session (z.B. ein Panel-Member) den Workflow-Eintrag fälschlich abräumt.
if (_workflowTracker != null && !string.IsNullOrEmpty(chain.SessionId)
&& _workflowTracker.ChainIdFor(chain.SessionId) == chain.ChainId)
{
chain.WorkflowInfo = _workflowTracker.GetJson(chain.SessionId);
_workflowTracker.End(chain.SessionId);
}
return SseHelper.FinalizeChain(_db, chain, usage, iterations, stopReason, sw, streamErrorInfo);
}
/// <summary> /// <summary>
/// Versieht das letzte Element einer Block-Liste (Tools, System-Prompt-Blöcke, etc.) mit /// Versieht das letzte Element einer Block-Liste (Tools, System-Prompt-Blöcke, etc.) mit
+9 -5
View File
@@ -84,7 +84,7 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig
/// <summary> /// <summary>
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc. /// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
/// </summary> /// </summary>
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false, string remoteOs = null) public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, string multiModelMode = null, bool hasRemoteConnection = false, string remoteOs = null)
{ {
var session = await _db.GetSession(sessionId); var session = await _db.GetSession(sessionId);
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId)) if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
@@ -167,10 +167,14 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig
var finalizerContext = await BuildSubAgentContext( var finalizerContext = await BuildSubAgentContext(
session.FinalizerAgentId, sessionId, workspace, session, user); session.FinalizerAgentId, sessionId, workspace, session, user);
// Multi-Modell-Panel (Fusion): Panel-Agenten (Proposer) bauen, falls aktiviert. // Multi-Modell-Panel (Fusion/MoA): Panel-Agenten (Proposer) bauen, falls aktiviert.
// Jeder Panel-Agent ist ein vollwertiger SubAgentContext (Modell/Provider/Key/Prompt/Effort). // Der Modus ist on-demand (transient pro Request, wie Plan-/Ultracode-Modus) → er kommt
// aus dem Request-Flag, NICHT mehr aus der Session. Die Panel-Mitglieder bleiben persistent
// (session.PanelAgentIds), nur ob/wie das Panel läuft entscheidet die einzelne Anfrage.
// Nur "fusion"/"moa" sind gültig; alles andere (leer/"off"/Müll) → kein Panel.
var effectiveMultiModelMode = (multiModelMode == "fusion" || multiModelMode == "moa") ? multiModelMode : "off";
var panelContexts = new List<SubAgentContext>(); var panelContexts = new List<SubAgentContext>();
if (!string.IsNullOrEmpty(session.MultiModelMode) && session.MultiModelMode != "off" if (effectiveMultiModelMode != "off"
&& !string.IsNullOrWhiteSpace(session.PanelAgentIds)) && !string.IsNullOrWhiteSpace(session.PanelAgentIds))
{ {
var panelIds = session.PanelAgentIds var panelIds = session.PanelAgentIds
@@ -251,7 +255,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
Preparer = preparerContext, Preparer = preparerContext,
Review = reviewContext, Review = reviewContext,
Finalizer = finalizerContext, Finalizer = finalizerContext,
MultiModelMode = session.MultiModelMode, MultiModelMode = effectiveMultiModelMode,
PanelAgents = panelContexts, PanelAgents = panelContexts,
MoaLayers = session.MoaLayers, MoaLayers = session.MoaLayers,
PlanMode = planMode PlanMode = planMode
+53 -3
View File
@@ -21,11 +21,13 @@ public class ClaudeCliService
private readonly string _cliPath; private readonly string _cliPath;
private readonly bool _isAvailable; private readonly bool _isAvailable;
private readonly ServerSettingsService _settings; private readonly ServerSettingsService _settings;
private readonly WorkflowProgressTracker _workflowTracker;
public ClaudeCliService(ILogger<ClaudeCliService> logger, ServerSettingsService settings) public ClaudeCliService(ILogger<ClaudeCliService> logger, ServerSettingsService settings, WorkflowProgressTracker workflowTracker = null)
{ {
_logger = logger; _logger = logger;
_settings = settings; _settings = settings;
_workflowTracker = workflowTracker;
_cliPath = FindCliPath(); _cliPath = FindCliPath();
_isAvailable = !string.IsNullOrEmpty(_cliPath) && File.Exists(_cliPath); _isAvailable = !string.IsNullOrEmpty(_cliPath) && File.Exists(_cliPath);
@@ -90,6 +92,44 @@ public class ClaudeCliService
return major > 5 || (major == 5 && minor >= 2); return major > 5 || (major == 5 && minor >= 2);
} }
/// <summary>
/// Greift aus einem (im Haupt-Stream unterdrückten) Subagent-Turn die Telemetrie ab:
/// die vom Subagenten aufgerufenen Tool-Namen und seine Token-Nutzung. Rein additiv für den
/// Workflow-Status – wirft nie (Telemetrie darf den Stream-Parser nicht stören).
/// </summary>
private void TapSubagentEvent(string sessionId, string chainId, string parentToolUseId, JsonElement json)
{
try
{
if (!json.TryGetProperty("message", out var msg))
return;
var toolNames = new List<string>();
if (msg.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array)
{
foreach (var item in content.EnumerateArray())
{
if (item.TryGetProperty("type", out var it) && it.GetString() == "tool_use"
&& item.TryGetProperty("name", out var nm))
{
var n = nm.GetString();
if (!string.IsNullOrEmpty(n)) toolNames.Add(n);
}
}
}
long inTok = 0, outTok = 0;
if (msg.TryGetProperty("usage", out var usage))
{
if (usage.TryGetProperty("input_tokens", out var it2) && it2.ValueKind == JsonValueKind.Number) inTok = it2.GetInt64();
if (usage.TryGetProperty("output_tokens", out var ot2) && ot2.ValueKind == JsonValueKind.Number) outTok = ot2.GetInt64();
}
_workflowTracker.RecordSubagent(sessionId, chainId, parentToolUseId, toolNames, inTok, outTok);
}
catch { /* Telemetrie darf den Stream nie stören */ }
}
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null) private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null)
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
@@ -219,6 +259,8 @@ public class ClaudeCliService
string mcpConfigPath = null, string mcpConfigPath = null,
bool hasUnityMcp = false, bool hasUnityMcp = false,
string effort = null, string effort = null,
string sessionId = null,
string chainId = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!_isAvailable) if (!_isAvailable)
@@ -354,7 +396,7 @@ public class ClaudeCliService
args.Add(mcpConfigPath); args.Add(mcpConfigPath);
} }
return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effortToUse, workspace, modelId); return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effortToUse, workspace, modelId, sessionId, chainId);
} }
private async Task<CliChatResult> RunCliStreamingAsync( private async Task<CliChatResult> RunCliStreamingAsync(
@@ -364,7 +406,9 @@ public class ClaudeCliService
CancellationToken cancellationToken, CancellationToken cancellationToken,
string effort = null, string effort = null,
string workspace = null, string workspace = null,
string modelId = null) string modelId = null,
string sessionId = null,
string chainId = null)
{ {
var result = new CliChatResult(); var result = new CliChatResult();
var fullOutput = new StringBuilder(); var fullOutput = new StringBuilder();
@@ -509,6 +553,12 @@ public class ClaudeCliService
&& parentToolUseId.ValueKind == JsonValueKind.String && parentToolUseId.ValueKind == JsonValueKind.String
&& !string.IsNullOrEmpty(parentToolUseId.GetString())) && !string.IsNullOrEmpty(parentToolUseId.GetString()))
{ {
// Subagent-Event: weiterhin aus dem Haupt-Stream AUSBLENDEN (continue) –
// hier wird nur die Telemetrie für den Workflow-Status ABGEGRIFFEN
// (Tool-Aufrufe + Subagent-Tokens), ohne den sichtbaren Verlauf zu verändern.
// Die konsolidierten Subagent-Turns kommen als type:"assistant" (mit usage).
if (_workflowTracker != null && !string.IsNullOrEmpty(sessionId) && type == "assistant")
TapSubagentEvent(sessionId, chainId, parentToolUseId.GetString(), json);
continue; continue;
} }
+1
View File
@@ -253,6 +253,7 @@ public partial class DatabaseService
iterations = @Iterations, iterations = @Iterations,
stop_reason = @StopReason, stop_reason = @StopReason,
stream_error_info = @StreamErrorInfo, stream_error_info = @StreamErrorInfo,
workflow_info = @WorkflowInfo,
duration_ms = @DurationMs duration_ms = @DurationMs
WHERE chain_id = @ChainId WHERE chain_id = @ChainId
", chain); ", chain);
+1
View File
@@ -148,6 +148,7 @@ public partial class DatabaseService
} }
catch { /* already migrated */ } catch { /* already migrated */ }
TryAddColumn(conn, "chains", "stream_error_info", "TEXT"); TryAddColumn(conn, "chains", "stream_error_info", "TEXT");
TryAddColumn(conn, "chains", "workflow_info", "TEXT");
TryAddColumn(conn, "agents", "use_soul", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_soul", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
+131
View File
@@ -0,0 +1,131 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Gott.Services;
/// <summary>
/// Sammelt Live-Telemetrie laufender dynamischer Workflows ("ultracode"): die Aktivität der
/// Subagenten, die die Claude CLI über das Workflow-/Task-Tool spawnt. Diese Subagent-Events
/// werden im Haupt-Stream bewusst unterdrückt (parent_tool_use_id-Guard in ClaudeCliService) –
/// hier werden sie abgegriffen, damit der Workflow-Fortschritt LIVE (während des Laufs) und
/// danach (persistiert in chains.workflow_info) abrufbar ist. In-Memory pro Session, analog
/// StreamingTracker. Telemetrie darf den Stream nie blockieren → alles defensiv + lock-leicht.
/// </summary>
public class WorkflowProgressTracker
{
private readonly ConcurrentDictionary<string, WorkflowProgress> _progress
= new ConcurrentDictionary<string, WorkflowProgress>();
/// <summary>
/// Verbucht ein Subagent-Event (Tool-Aufrufe + Token-Nutzung eines Subagenten).
/// Lazy: legt den Eintrag bei der ERSTEN Subagent-Aktivität einer Session an, d.h. nur
/// echte Workflow-Läufe (mit gespawnten Subagenten) erzeugen überhaupt einen Eintrag.
/// </summary>
public void RecordSubagent(string sessionId, string chainId, string parentToolUseId,
IReadOnlyList<string> toolNames, long inputTokens, long outputTokens)
{
if (string.IsNullOrEmpty(sessionId)) return;
var wp = _progress.GetOrAdd(sessionId, _ => new WorkflowProgress
{
ChainId = chainId,
StartedAt = DateTime.UtcNow
});
lock (wp.Sync)
{
// Stale-Eintrag eines früheren, nicht sauber finalisierten Laufs (gleiche Session,
// andere Chain) → atomar für die neue Chain zurücksetzen, statt fälschlich zu mischen.
if (wp.ChainId != chainId)
{
wp.ChainId = chainId;
wp.StartedAt = DateTime.UtcNow;
wp.SubagentIds.Clear();
wp.SubagentCount = 0;
wp.SubagentToolCalls = 0;
wp.SubagentInputTokens = 0;
wp.SubagentOutputTokens = 0;
wp.ToolBreakdown.Clear();
wp.RecentTools.Clear();
}
if (!string.IsNullOrEmpty(parentToolUseId) && wp.SubagentIds.Add(parentToolUseId))
wp.SubagentCount = wp.SubagentIds.Count;
if (toolNames != null)
{
foreach (var tn in toolNames)
{
if (string.IsNullOrEmpty(tn)) continue;
wp.SubagentToolCalls++;
wp.ToolBreakdown[tn] = wp.ToolBreakdown.TryGetValue(tn, out var c) ? c + 1 : 1;
wp.RecentTools.Add(tn);
if (wp.RecentTools.Count > 12) wp.RecentTools.RemoveAt(0);
}
}
wp.SubagentInputTokens += inputTokens;
wp.SubagentOutputTokens += outputTokens;
wp.LastActivityAt = DateTime.UtcNow;
}
}
/// <summary>Threadsicherer Live-Snapshot – null, wenn für die Session kein Workflow läuft.</summary>
public WorkflowProgress GetSnapshot(string sessionId)
=> _progress.TryGetValue(sessionId, out var wp) ? wp.Snapshot() : null;
/// <summary>Aktuellen Stand als JSON serialisieren (threadsicher) – null wenn kein Eintrag.</summary>
public string GetJson(string sessionId)
=> _progress.TryGetValue(sessionId, out var wp) ? wp.ToJson() : null;
/// <summary>ChainId des laufenden Workflow-Eintrags (für die Zuordnung beim Finalize).</summary>
public string ChainIdFor(string sessionId)
=> _progress.TryGetValue(sessionId, out var wp) ? wp.ChainId : null;
/// <summary>Entfernt den Eintrag (beim Chain-Finalize aufzurufen).</summary>
public void End(string sessionId) => _progress.TryRemove(sessionId, out _);
}
/// <summary>
/// Telemetrie eines dynamischen Workflows: Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung)
/// und aggregierte Subagent-Tokens. Wird live geführt und beim Chain-Finalize als JSON in
/// chains.workflow_info persistiert.
/// </summary>
public class WorkflowProgress
{
[JsonIgnore] public object Sync { get; } = new object();
// Dedup-Hilfsstruktur (nicht persistiert; SubagentCount trägt den Wert).
[JsonIgnore] public HashSet<string> SubagentIds { get; set; } = new HashSet<string>();
public string ChainId { get; set; }
public DateTime StartedAt { get; set; }
public DateTime LastActivityAt { get; set; }
public int SubagentCount { get; set; }
public int SubagentToolCalls { get; set; }
public long SubagentInputTokens { get; set; }
public long SubagentOutputTokens { get; set; }
public Dictionary<string, int> ToolBreakdown { get; set; } = new Dictionary<string, int>();
public List<string> RecentTools { get; set; } = new List<string>();
public WorkflowProgress Snapshot()
{
lock (Sync)
{
return new WorkflowProgress
{
ChainId = ChainId,
StartedAt = StartedAt,
LastActivityAt = LastActivityAt,
SubagentCount = SubagentCount,
SubagentToolCalls = SubagentToolCalls,
SubagentInputTokens = SubagentInputTokens,
SubagentOutputTokens = SubagentOutputTokens,
ToolBreakdown = new Dictionary<string, int>(ToolBreakdown),
RecentTools = new List<string>(RecentTools)
};
}
}
// Serialisiert einen entkoppelten Snapshot (Snapshot() kopiert unter Lock) – so läuft die
// Serialisierung außerhalb jeder kritischen Sektion und kann nie auf eine live mutierte
// Collection treffen.
public string ToJson() => JsonSerializer.Serialize(Snapshot());
}
+86
View File
@@ -1003,6 +1003,92 @@
border: var(--border-width) solid var(--f2-border); border: var(--border-width) solid var(--f2-border);
} }
/* Workflow-Status Modal (Ultracode) */
.workflow-status-modal {
max-width: 540px;
}
.wf-status-head {
display: flex;
align-items: center;
gap: var(--badge-spacing);
font-weight: 600;
margin-bottom: var(--box-spacing);
}
.wf-status-head.wf-running { color: var(--info); }
.wf-status-head.wf-done { color: var(--secondary); }
.wf-status-head.wf-none { color: var(--text); opacity: var(--semi-transparent); }
.wf-empty {
opacity: var(--semi-transparent);
}
.wf-stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--badge-spacing);
}
.wf-stat {
padding: var(--box-padding);
background: var(--f3-bg);
border-radius: var(--border-radius);
border: var(--border-width) solid var(--f2-border);
text-align: center;
}
.wf-stat-val {
font-family: var(--mono);
font-size: 1.3em;
color: var(--text-accent);
}
.wf-stat-lbl {
font-size: .85em;
opacity: var(--semi-transparent);
margin-top: .2em;
}
.wf-section {
margin-top: var(--box-spacing);
}
.wf-section-lbl {
font-weight: 600;
margin-bottom: var(--badge-spacing);
}
.wf-tool-list {
display: flex;
flex-direction: column;
gap: var(--border-width);
}
.wf-tool-row {
display: flex;
justify-content: space-between;
padding: .35em var(--badge-spacing);
background: var(--f3-bg);
border-radius: var(--border-radius);
}
.wf-tool-name {
font-family: var(--mono);
}
.wf-tool-count {
color: var(--primary);
font-family: var(--mono);
}
.wf-meta {
margin-top: var(--box-spacing);
font-size: .85em;
opacity: var(--semi-transparent);
}
/* Command Output Modal */ /* Command Output Modal */
.cmd-output-modal { .cmd-output-modal {
+6 -11
View File
@@ -156,21 +156,15 @@
<div class="agent-select-empty"><i class="fa fa-flag-checkered"></i> Kein Finalizer-Agent</div> <div class="agent-select-empty"><i class="fa fa-flag-checkered"></i> Kein Finalizer-Agent</div>
</div> </div>
<input type="hidden" id="finalizerAgentSelect" value=""> <input type="hidden" id="finalizerAgentSelect" value="">
<label>Multi-Modell-Panel <small>(Fusion / MoA)</small></label> <div id="panelAgentsRow">
<select id="multiModelMode"> <label>Panel-Agenten <small>(für Fusion / MoA – im Chat pro Anfrage aktivierbar)</small></label>
<option value="off">Aus</option>
<option value="fusion">Fusion – Panel antwortet parallel, Session-Agent synthetisiert</option>
<option value="moa">MoA – mehrere Proposer-Runden, dann Synthese</option>
</select>
<div id="panelAgentsRow" hidden>
<label>Panel-Agenten <small>(antworten parallel zur Anfrage)</small></label>
<div id="panelAgentsCard" class="agent-select-card"> <div id="panelAgentsCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-users"></i> Keine Panel-Agenten</div> <div class="agent-select-empty"><i class="fa fa-users"></i> Keine Panel-Agenten</div>
</div> </div>
<input type="hidden" id="panelAgentIds" value=""> <input type="hidden" id="panelAgentIds" value="">
<small class="muted">Jeder Panel-Agent kostet zusätzlich Tokens/Zeit – alle laufen pro Nachricht. Member arbeiten read-only.</small> <small class="muted">Jeder Panel-Agent kostet zusätzlich Tokens/Zeit – alle laufen pro Nachricht. Member arbeiten read-only. Fusion/MoA werden im Chat-Eingabefeld pro Anfrage gewählt.</small>
</div> </div>
<div id="moaLayersRow" hidden> <div id="moaLayersRow">
<label>MoA-Runden <small>(Proposer-Runden vor der Synthese)</small></label> <label>MoA-Runden <small>(Proposer-Runden vor der Synthese)</small></label>
<input type="number" id="moaLayers" min="1" max="5" step="1" value="2"> <input type="number" id="moaLayers" min="1" max="5" step="1" value="2">
<small class="muted">Mehr Runden = mehr Verfeinerung, aber Kosten/Zeit ≈ Runden × Agenten. Empfohlen: 2–3.</small> <small class="muted">Mehr Runden = mehr Verfeinerung, aber Kosten/Zeit ≈ Runden × Agenten. Empfohlen: 2–3.</small>
@@ -443,6 +437,7 @@
<button class="header-menu-item" id="remoteBtn"><i class="fa fa-desktop"></i> Remote-Verbindungen</button> <button class="header-menu-item" id="remoteBtn"><i class="fa fa-desktop"></i> Remote-Verbindungen</button>
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button> <button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
<button class="header-menu-item" id="payloadPreviewBtn">Payload anzeigen</button> <button class="header-menu-item" id="payloadPreviewBtn">Payload anzeigen</button>
<button class="header-menu-item" id="workflowStatusBtn">Workflow-Status</button>
<button class="header-menu-item" id="archiveBtn">Archivieren</button> <button class="header-menu-item" id="archiveBtn">Archivieren</button>
</div> </div>
</div> </div>
@@ -458,7 +453,7 @@
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" multiple hidden> <input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" multiple hidden>
<button id="fileBtn" class="icon-btn" title="Datei anhängen"><i class="fa fa-paperclip"></i></button> <button id="fileBtn" class="icon-btn" title="Datei anhängen"><i class="fa fa-paperclip"></i></button>
<button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button> <button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button>
<button id="planModeBtn" class="icon-btn" title="Modus wählen"><i class="fa fa-ban"></i></button> <button id="planModeBtn" class="icon-btn" title="Modus wählen"><i class="fa fa-user"></i></button>
<textarea id="input" placeholder="Nachricht..." rows="1"></textarea> <textarea id="input" placeholder="Nachricht..." rows="1"></textarea>
<button id="emojiBtn" class="icon-btn" title="Emoji einfügen"><i class="fa fa-smile-o"></i></button> <button id="emojiBtn" class="icon-btn" title="Emoji einfügen"><i class="fa fa-smile-o"></i></button>
<button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button> <button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button>
+7 -1
View File
@@ -60,10 +60,12 @@ function apiMe() { return _get('/api/auth/me', null); }
// ── Chat API ── // ── Chat API ──
function apiChat(content, sessionId, signal, planMode, ultracode) { function apiChat(content, sessionId, signal, planMode, ultracode, multiModelMode) {
var payload = { content: content, sessionId: sessionId }; var payload = { content: content, sessionId: sessionId };
if (planMode) payload.planMode = true; if (planMode) payload.planMode = true;
if (ultracode) payload.ultracode = true; if (ultracode) payload.ultracode = true;
// Panel-Modus ist on-demand pro Anfrage (transient): nur fusion/moa durchreichen.
if (multiModelMode === 'fusion' || multiModelMode === 'moa') payload.multiModelMode = multiModelMode;
return fetch('/api/chat', { method: 'POST', headers: _jsonHeaders, body: JSON.stringify(payload), signal: signal }); return fetch('/api/chat', { method: 'POST', headers: _jsonHeaders, body: JSON.stringify(payload), signal: signal });
} }
@@ -124,6 +126,10 @@ function apiGetPayloadPreview(sessionId, planMode) {
return _get(url); return _get(url);
} }
function apiGetWorkflowStatus(sessionId) {
return _get('/api/sessions/' + sessionId + '/workflow-status');
}
function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); } function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); }
function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); } function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); }
+19 -46
View File
@@ -223,19 +223,18 @@ function handleStreamEvent(eventType, data) {
// der Betriebsmodus direkt gewählt wird. Der Button zeigt weiterhin das Symbol des aktiven // der Betriebsmodus direkt gewählt wird. Der Button zeigt weiterhin das Symbol des aktiven
// Modus; im Menü ist der aktive Eintrag markiert. // Modus; im Menü ist der aktive Eintrag markiert.
// //
// Plan-Modus und Ultracode sind transient (pro Request, nicht in der Session persistiert) und // ALLE Modi (Plan / Ultracode / Fusion / MoA) sind transient: sie gelten nur für die nächste
// laufen ohne Panel → beim Wählen wird multiModelMode der Session auf 'off' gesetzt, damit kein // Anfrage, werden NICHT in der Session persistiert und beim Chat-Wechsel auf 'off' zurückgesetzt.
// zuvor gewähltes Fusion/MoA-Panel nebenherläuft (das Menü erlaubt direkte Sprünge, anders als // Der gewählte Modus wird pro Request als Flag mitgeschickt (planMode / ultracode / multiModelMode).
// der frühere Zyklus, der immer durch 'off' lief). Fusion/MoA werden in der Session persistiert // Die Panel-MITGLIEDER (PanelAgentIds) bleiben dagegen in den Session-Einstellungen persistent.
// und server-seitig aus ctx.MultiModelMode gelesen.
// //
// _planModeActive / _ultracodeActive bleiben als Compat-Variablen (sendMessage + modals.js lesen // _planModeActive / _ultracodeActive bleiben als Compat-Variablen (sendMessage + modals.js lesen
// sie direkt). // sie direkt); Fusion/MoA werden direkt aus _compositeMode abgeleitet.
// Metadaten je Modus – eine Quelle für Button-Render UND Menü-Aufbau: Reihenfolge im Menü, // Metadaten je Modus – eine Quelle für Button-Render UND Menü-Aufbau: Reihenfolge im Menü,
// Icon (Button + Menü), Label, Button-Aktiv-Klasse, Input-Placeholder, kurze Beschreibung. // Icon (Button + Menü), Label, Button-Aktiv-Klasse, Input-Placeholder, kurze Beschreibung.
const _COMPOSITE_MODES = [ const _COMPOSITE_MODES = [
{ mode: 'off', icon: 'fa-ban', label: 'Aus', btnClass: null, placeholder: 'Nachricht...', desc: 'Normaler Einzel-Agent' }, { mode: 'off', icon: 'fa-user', label: 'Normal', btnClass: null, placeholder: 'Nachricht...', desc: 'Normaler Einzel-Agent' },
{ mode: 'plan', icon: 'fa-map', label: 'Plan-Modus', btnClass: 'plan-mode-active', placeholder: 'Planen...', desc: 'Nur analysieren & planen (read-only)' }, { mode: 'plan', icon: 'fa-map', label: 'Plan-Modus', btnClass: 'plan-mode-active', placeholder: 'Planen...', desc: 'Nur analysieren & planen (read-only)' },
{ mode: 'ultracode', icon: 'fa-sitemap', label: 'Ultracode', btnClass: 'ultracode-active', placeholder: 'Ultracode – als Workflow...', desc: 'Dynamischer Workflow (mehrere Subagenten)' }, { mode: 'ultracode', icon: 'fa-sitemap', label: 'Ultracode', btnClass: 'ultracode-active', placeholder: 'Ultracode – als Workflow...', desc: 'Dynamischer Workflow (mehrere Subagenten)' },
{ mode: 'fusion', icon: 'fa-users', label: 'Fusion', btnClass: 'panel-mode-fusion', placeholder: 'Nachricht...', desc: 'Panel: Antworten fusionieren' }, { mode: 'fusion', icon: 'fa-users', label: 'Fusion', btnClass: 'panel-mode-fusion', placeholder: 'Nachricht...', desc: 'Panel: Antworten fusionieren' },
@@ -253,14 +252,6 @@ let _ultracodeActive = false; // true genau wenn _compositeMode === 'ultracode
let _compositeMenuEl = null; let _compositeMenuEl = null;
let _compositeMenuCloseHandler = null; let _compositeMenuCloseHandler = null;
// Serialisierte Modus-Persistenz. Schnell hintereinander gewählte Modi feuern sonst ÜBERLAPPENDE
// PATCHes, deren Commit-Reihenfolge nicht garantiert ist → ein Zwischenwert kann den letzten
// überschreiben. Und ein direkt danach gesendeter Chat-Request lädt die Session, BEVOR der Modus
// committed ist → das Backend liest den alten Modus. Die Kette garantiert (a) Reihenfolge (letzte
// Wahl gewinnt) und (b) dass sendMessage das offene PATCH abwarten kann. Sie rejected nie (Fehler
// werden hier geschluckt).
let _modePersist = Promise.resolve();
function _compositeMeta(mode) { function _compositeMeta(mode) {
for (var i = 0; i < _COMPOSITE_MODES.length; i++) { for (var i = 0; i < _COMPOSITE_MODES.length; i++) {
if (_COMPOSITE_MODES[i].mode === mode) return _COMPOSITE_MODES[i]; if (_COMPOSITE_MODES[i].mode === mode) return _COMPOSITE_MODES[i];
@@ -283,20 +274,22 @@ function _renderCompositeBtn() {
if (input) input.placeholder = meta.placeholder; if (input) input.placeholder = meta.placeholder;
} }
// Initialisiert den Button aus der geladenen Session (bei jedem Chat-Wechsel aufrufen). // Initialisiert den Button bei jedem Chat-Wechsel. ALLE Modi (Plan/Ultracode/Fusion/MoA) sind
// Plan-Modus wird immer auf 'off' zurückgesetzt (transient). // transient (pro Anfrage) → der Button startet immer auf 'off'. `hasAgents` steuert nur den Hinweis,
function initCompositeMode(panelMode, hasAgents) { // ob für Fusion/MoA überhaupt Panel-Agenten konfiguriert sind.
function initCompositeMode(hasAgents) {
_compositeHasAgents = !!hasAgents; _compositeHasAgents = !!hasAgents;
_compositeMode = (panelMode === 'fusion' || panelMode === 'moa') ? panelMode : 'off'; _compositeMode = 'off';
_planModeActive = false; _planModeActive = false;
_ultracodeActive = false; _ultracodeActive = false;
_renderCompositeBtn(); _renderCompositeBtn();
} }
// Setzt den Modus direkt (aus dem Menü gewählt). // Setzt den Modus direkt (aus dem Menü gewählt). ALLE Modi sind transient (Request-Flags) –
// Persistiert das EFFEKTIVE Panel der Session (off/fusion/moa): für plan/ultracode also 'off', // nichts wird mehr persistiert. Plan/Ultracode/Fusion/MoA wirken nur für die nächste Anfrage
// damit kein zuvor gewähltes Panel nebenherläuft. Plan/Ultracode selbst sind transient (Request-Flags). // und werden beim Chat-Wechsel auf 'off' zurückgesetzt. Die Panel-Mitglieder bleiben dagegen
async function setCompositeMode(mode) { // persistent (in den Session-Einstellungen festgelegt).
function setCompositeMode(mode) {
if (mode === _compositeMode) return; // schon aktiv → nichts tun if (mode === _compositeMode) return; // schon aktiv → nichts tun
if (_compositeMeta(mode).mode !== mode) return; // unbekannter Modus → ignorieren if (_compositeMeta(mode).mode !== mode) return; // unbekannter Modus → ignorieren
@@ -305,23 +298,9 @@ async function setCompositeMode(mode) {
_ultracodeActive = (mode === 'ultracode'); _ultracodeActive = (mode === 'ultracode');
_renderCompositeBtn(); _renderCompositeBtn();
// Effektiver Panel-Modus: nur Fusion/MoA aktivieren ein Panel, alles andere 'off'.
// Auf die serialisierte Kette legen, damit aufeinanderfolgende Wahlen in Reihenfolge committen
// (letzte gewinnt) und sendMessage das offene PATCH abwarten kann.
var panelMode = (mode === 'fusion' || mode === 'moa') ? mode : 'off';
var sid = getCurrentSessionId();
if (sid) {
_modePersist = _modePersist
.then(function () { return apiUpdateSession(sid, { multiModelMode: panelMode }); })
.catch(function (e) {
console.error('Modus speichern fehlgeschlagen:', e);
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
});
}
if (typeof showToast === 'function') { if (typeof showToast === 'function') {
if (mode === 'off') { if (mode === 'off') {
showToast('Modus: Aus', 'info'); showToast('Modus: Normal', 'info');
} else if (mode === 'plan') { } else if (mode === 'plan') {
showToast('Plan-Modus aktiv', 'info'); showToast('Plan-Modus aktiv', 'info');
} else if (mode === 'ultracode') { } else if (mode === 'ultracode') {
@@ -669,12 +648,7 @@ async function sendMessage() {
input.value = ''; input.value = '';
input.dispatchEvent(new Event('input')); input.dispatchEvent(new Event('input'));
// Ein evtl. noch offenes Modus-PATCH (Fusion/MoA/Aus aus dem Toggle) MUSS committed sein, await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive, _compositeMode));
// bevor der Chat-Request die Session lädt – sonst läuft die Chain mit dem alten Modus
// (z.B. MoA → nur 1 Runde, weil das Backend noch 'fusion' liest).
await _modePersist;
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive));
} }
// ── Steering (Einwurf in die laufende Chain) ── // ── Steering (Einwurf in die laufende Chain) ──
@@ -710,8 +684,7 @@ async function sendMessageDirect(text) {
if (isStreaming || !text) return; if (isStreaming || !text) return;
addUserBubble(text, true); addUserBubble(text, true);
scrollDownForce(); scrollDownForce();
await _modePersist; // offenes Modus-PATCH abwarten (s. sendMessage) await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive, _compositeMode));
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
} }
// ── Choice Click Handler ── // ── Choice Click Handler ──
+103
View File
@@ -96,6 +96,109 @@ async function showPayloadPreviewModal(sessionId) {
} }
} }
// ── Workflow-Status Modal (Ultracode) ──
function _wfNum(n) { return Number(n || 0).toLocaleString('de-DE'); }
function _wfTime(t) { try { return new Date(t).toLocaleTimeString('de-DE'); } catch (e) { return ''; } }
function _renderWorkflowStatus(body, data) {
var status = (data && data.status) || 'none';
var wf = data && data.workflow;
var meta = {
running: { label: 'Workflow läuft', cls: 'wf-running', icon: 'fa-spinner fa-spin' },
completed: { label: 'Workflow abgeschlossen', cls: 'wf-done', icon: 'fa-check-circle' },
none: { label: 'Kein Workflow für diese Session', cls: 'wf-none', icon: 'fa-ban' }
};
var sm = meta[status] || meta.none;
body.innerHTML = '';
var head = document.createElement('div');
head.className = 'wf-status-head ' + sm.cls;
head.innerHTML = '<i class="fa ' + sm.icon + '"></i> ' + esc(sm.label);
body.appendChild(head);
if (!wf) {
var empty = document.createElement('div');
empty.className = 'wf-empty';
empty.textContent = 'Für diese Session wurde noch kein dynamischer Workflow (Ultracode) ausgeführt.';
body.appendChild(empty);
return;
}
var stats = [
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
{ label: 'Tool-Aufrufe', value: _wfNum(wf.subagentToolCalls) },
{ label: 'Input-Tokens', value: _wfNum(wf.subagentInputTokens) },
{ label: 'Output-Tokens', value: _wfNum(wf.subagentOutputTokens) }
];
var grid = document.createElement('div');
grid.className = 'wf-stat-grid';
stats.forEach(function (s) {
var cell = document.createElement('div');
cell.className = 'wf-stat';
cell.innerHTML = '<div class="wf-stat-val">' + esc(String(s.value)) +
'</div><div class="wf-stat-lbl">' + esc(s.label) + '</div>';
grid.appendChild(cell);
});
body.appendChild(grid);
var tb = wf.toolBreakdown || {};
var keys = Object.keys(tb).sort(function (a, b) { return tb[b] - tb[a]; });
if (keys.length) {
var sec = document.createElement('div');
sec.className = 'wf-section';
sec.innerHTML = '<div class="wf-section-lbl">Tool-Aufschlüsselung</div>';
var list = document.createElement('div');
list.className = 'wf-tool-list';
keys.forEach(function (k) {
var row = document.createElement('div');
row.className = 'wf-tool-row';
row.innerHTML = '<span class="wf-tool-name">' + esc(k) +
'</span><span class="wf-tool-count">' + esc(String(tb[k])) + '</span>';
list.appendChild(row);
});
sec.appendChild(list);
body.appendChild(sec);
}
if (wf.startedAt) {
var ts = document.createElement('div');
ts.className = 'wf-meta';
ts.textContent = 'Start: ' + _wfTime(wf.startedAt) +
(wf.lastActivityAt ? ' · letzte Aktivität: ' + _wfTime(wf.lastActivityAt) : '');
body.appendChild(ts);
}
}
async function showWorkflowStatusModal(sessionId) {
const { body, overlay } = _createModal('Workflow-Status', { modal: 'workflow-status-modal' });
body.innerHTML = '<div class="sp-loading">Laden...</div>';
var poll = async function () {
try {
var data = await apiGetWorkflowStatus(sessionId);
_renderWorkflowStatus(body, data);
return data && data.status;
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Workflow-Status</div>';
return null;
}
};
var status = await poll();
// Live-Refresh, solange ein Workflow läuft – endet automatisch beim Schließen des Modals.
if (status === 'running') {
var timer = setInterval(async function () {
if (!document.body.contains(overlay)) { clearInterval(timer); return; }
var s = await poll();
if (s !== 'running') clearInterval(timer);
}, 2000);
}
}
// ── Command Output Modal ── // ── Command Output Modal ──
function showCommandOutputModal(name, command, result) { function showCommandOutputModal(name, command, result) {
+8 -1
View File
@@ -435,6 +435,13 @@ registerPage('chat', {
showPayloadPreviewModal(sessionId); showPayloadPreviewModal(sessionId);
}); });
// Workflow-Status anzeigen
const workflowStatusBtn = document.getElementById('workflowStatusBtn');
if (workflowStatusBtn) workflowStatusBtn.addEventListener('click', () => {
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
showWorkflowStatusModal(sessionId);
});
// Archivieren // Archivieren
const archiveBtn = document.getElementById('archiveBtn'); const archiveBtn = document.getElementById('archiveBtn');
if (archiveBtn) archiveBtn.addEventListener('click', () => { if (archiveBtn) archiveBtn.addEventListener('click', () => {
@@ -577,7 +584,7 @@ registerPage('chat', {
const compositeModeBtn = document.getElementById('planModeBtn'); const compositeModeBtn = document.getElementById('planModeBtn');
if (compositeModeBtn) { if (compositeModeBtn) {
const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim()); const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim());
initCompositeMode(session.multiModelMode, hasPanelAgents); initCompositeMode(hasPanelAgents);
compositeModeBtn.addEventListener('click', (e) => { compositeModeBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
toggleCompositeMenu(compositeModeBtn); toggleCompositeMenu(compositeModeBtn);
+2 -11
View File
@@ -29,11 +29,8 @@ registerPage('session', {
const maxReviewIterationsEl = document.getElementById('maxReviewIterations'); const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
const finalizerAgentSelect = document.getElementById('finalizerAgentSelect'); const finalizerAgentSelect = document.getElementById('finalizerAgentSelect');
const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard'); const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard');
const panelModeEl = document.getElementById('multiModelMode');
const panelAgentsRow = document.getElementById('panelAgentsRow');
const panelAgentsCard = document.getElementById('panelAgentsCard'); const panelAgentsCard = document.getElementById('panelAgentsCard');
const panelAgentIdsEl = document.getElementById('panelAgentIds'); const panelAgentIdsEl = document.getElementById('panelAgentIds');
const moaLayersRow = document.getElementById('moaLayersRow');
const moaLayersEl = document.getElementById('moaLayers'); const moaLayersEl = document.getElementById('moaLayers');
const imagePreview = document.getElementById('sessionImagePreview'); const imagePreview = document.getElementById('sessionImagePreview');
const saveBtn = document.getElementById('sessionSaveBtn'); const saveBtn = document.getElementById('sessionSaveBtn');
@@ -148,16 +145,11 @@ registerPage('session', {
panelAgentsCard.innerHTML = '<div>' + chips + '</div>' panelAgentsCard.innerHTML = '<div>' + chips + '</div>'
+ '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>'; + '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>';
} }
function updatePanelVisibility() { // Panel-Modus (Fusion/MoA) wird on-demand im Chat gewählt (nicht mehr hier persistiert).
panelAgentsRow.hidden = (panelModeEl.value === 'off'); // In den Settings legt man nur noch die Panel-Mitglieder + MoA-Runden fest (immer sichtbar).
if (moaLayersRow) moaLayersRow.hidden = (panelModeEl.value !== 'moa');
}
panelModeEl.value = (session && session.multiModelMode) || 'off';
panelAgentIdsEl.value = (session && session.panelAgentIds) || ''; panelAgentIdsEl.value = (session && session.panelAgentIds) || '';
if (moaLayersEl) moaLayersEl.value = (session && session.moaLayers) || 2; if (moaLayersEl) moaLayersEl.value = (session && session.moaLayers) || 2;
renderPanelInCard(); renderPanelInCard();
updatePanelVisibility();
panelModeEl.addEventListener('change', updatePanelVisibility);
panelAgentsCard.addEventListener('click', function() { panelAgentsCard.addEventListener('click', function() {
var currentIds = (panelAgentIdsEl.value || '').split(',').map(function(s){ return s.trim(); }).filter(Boolean); var currentIds = (panelAgentIdsEl.value || '').split(',').map(function(s){ return s.trim(); }).filter(Boolean);
openMultiAgentPickerModal(agents, agentSkillsMap, currentIds, function(newIds) { openMultiAgentPickerModal(agents, agentSkillsMap, currentIds, function(newIds) {
@@ -217,7 +209,6 @@ registerPage('session', {
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3, maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
finalizerAgentId: finalizerAgentSelect.value || null, finalizerAgentId: finalizerAgentSelect.value || null,
preparerAgentId: preparerAgentSelect.value || null, preparerAgentId: preparerAgentSelect.value || null,
multiModelMode: panelModeEl.value || 'off',
panelAgentIds: panelAgentIdsEl.value || null, panelAgentIds: panelAgentIdsEl.value || null,
moaLayers: moaLayersEl ? (parseInt(moaLayersEl.value, 10) || 2) : 2, moaLayers: moaLayersEl ? (parseInt(moaLayersEl.value, 10) || 2) : 2,
context: _sessionContextMde.value().trim() || null, context: _sessionContextMde.value().trim() || null,