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:
1 parent
5b1802efee
commit
b6e546f618
22 files changed
+591
-99
No files matched your search
+54
-7
@@ -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
@@ -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/`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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 ──
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user