Compare commits
87 Commits
b7e31b8716
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 82722999a8 | |||
| ad93a294fb | |||
| b677228a96 | |||
| f2c5ae43d7 | |||
| cf5ee6c0f1 | |||
| 123bcdcb58 | |||
| c8eb340afe | |||
| 414379da4f | |||
| 63015e9523 | |||
| 36b3c33dcc | |||
| 727274728f | |||
| befb480285 | |||
| 5a8a91ecf7 | |||
| 8bc6eae101 | |||
| 1f8bb58219 | |||
| 19e7c94c2e | |||
| 23943443e3 | |||
| 6f1fea85f0 | |||
| d237d3b94d | |||
| 7924d65438 | |||
| 3999e9c86d | |||
| 48e2ed852a | |||
| e5a86835e2 | |||
| 95800ad88b | |||
| f4c5a0be83 | |||
| 3b2588ad61 | |||
| 828fadf829 | |||
| 4ba1bd9eba | |||
| c09e0f50be | |||
| 1c863f0f0c | |||
| 6090e0ad2b | |||
| d16996a62a | |||
| 1a14cee3ce | |||
| 036c2f360a | |||
| 930b833055 | |||
| 4777dd957a | |||
| e88f0f1831 | |||
| 1be576a9a0 | |||
| e8303b819b | |||
| 02e0fce548 | |||
| 00a390ffab | |||
| a471b1e588 | |||
| 1541e7f9fd | |||
| 4dee0e6f69 | |||
| 56f79fd210 | |||
| 757c96b58e | |||
| 44fd370167 | |||
| b5007ce96f | |||
| 072c6e66bd | |||
| 9e51071418 | |||
| 0944aa1c2d | |||
| 34c9919444 | |||
| c1ebdc0c6f | |||
| e0d441ceae | |||
| 9133358c40 | |||
| f21f22e48f | |||
| 97ecd709a9 | |||
| 09902701b4 | |||
| 55475b80e7 | |||
| 84ec24e866 | |||
| 1a01e3f112 | |||
| db1f74997c | |||
| b469abef8f | |||
| 03d81f61be | |||
| 9b6d16b4e0 | |||
| 847096d192 | |||
| 7ee50f979a | |||
| 3870bf086c | |||
| 747b810fe1 | |||
| 3ba05b8a6a | |||
| 94598b605a | |||
| 26e03d2c9f | |||
| 6da3d95c0e | |||
| 6ae8737c1a | |||
| 92e7352d37 | |||
| ca8e33837b | |||
| fa5ead2c69 | |||
| 67a269b05d | |||
| ee3a811cc9 | |||
| 6b587d76a7 | |||
| 340be86509 | |||
| cd21519506 | |||
| 8c5b5d3a9a | |||
| f5ea0de68f | |||
| f7ce8e38a8 | |||
| 107afd85bc | |||
| 050eabfc55 |
@@ -10,7 +10,7 @@ A high-density GUI orchestrator for local LLM-driven coding sessions. Manual Slo
|
||||
**Providers**: Gemini API, Anthropic API, DeepSeek, Gemini CLI (headless), MiniMax
|
||||
**Platform**: Windows (PowerShell) — single developer, local use
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
42
conductor/archive/external_mcp_support_20260308/plan.md
Normal file
42
conductor/archive/external_mcp_support_20260308/plan.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Implementation Plan: External MCP Server Support
|
||||
|
||||
## Phase 1: Configuration & Data Modeling [checkpoint: 4ba1bd9]
|
||||
- [x] Task: Define the schema for external MCP server configuration. [1c863f0]
|
||||
- [x] Update `src/models.py` to include `MCPServerConfig` and `MCPConfiguration` classes.
|
||||
- [x] Implement logic to load `mcp_config.json` from global and project-specific paths.
|
||||
- [x] Task: Integrate configuration loading into `AppController`. [c09e0f5]
|
||||
- [x] Ensure the MCP config path is correctly resolved from `config.toml` and `manual_slop.toml`.
|
||||
- [x] Task: Write unit tests for configuration loading and validation. [c09e0f5]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' [4ba1bd9]
|
||||
|
||||
## Phase 2: MCP Client Extension [checkpoint: 828fadf]
|
||||
- [x] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`. [828fadf]
|
||||
- [x] Add support for managing multiple MCP server sessions.
|
||||
- [x] Implement the `StdioMCPClient` for local subprocess communication.
|
||||
- [x] Implement the `RemoteMCPClient` for SSE/WebSocket communication (stub).
|
||||
- [x] Task: Update Tool Discovery. [828fadf]
|
||||
- [x] Implement `list_external_tools()` to aggregate tools from all active external servers.
|
||||
- [x] Task: Update Tool Dispatch. [828fadf]
|
||||
- [x] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
|
||||
- [x] Task: Write integration tests for stdio and remote MCP client communication (using mock servers). [828fadf]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' [828fadf]
|
||||
|
||||
## Phase 3: GUI Integration & Lifecycle [checkpoint: 3b2588a]
|
||||
- [x] Task: Update the **Operations** panel in `src/gui_2.py`. [3b2588a]
|
||||
- [x] Create a new "External Tools" section.
|
||||
- [x] List discovered tools from active external servers.
|
||||
- [x] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
|
||||
- [x] Task: Implement Lifecycle Management. [3b2588a]
|
||||
- [x] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
|
||||
- [x] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
|
||||
- [x] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel. [3b2588a]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' [3b2588a]
|
||||
|
||||
## Phase 4: Agent Integration & HITL [checkpoint: f4c5a0b]
|
||||
- [x] Task: Update AI tool declarations. [f4c5a0b]
|
||||
- [x] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
|
||||
- [x] Task: Verify HITL Approval Flow. [f4c5a0b]
|
||||
- [x] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
|
||||
- [x] Verify that approved external tool results are correctly returned to the AI.
|
||||
- [x] Task: Perform a final end-to-end verification with a real external MCP server. [f4c5a0b]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' [f4c5a0b]
|
||||
@@ -3,13 +3,13 @@
|
||||
## Phase 1: Path Info Display
|
||||
Focus: Show current path resolution in GUI
|
||||
|
||||
- [ ] Task 1.1: Add path info functions to paths.py
|
||||
- [x] Task 1.1: Add path info functions to paths.py [d237d3b]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Add functions to get path resolution source (default/env/config)
|
||||
- HOW: Return tuple of (resolved_path, source)
|
||||
- SAFETY: New functions, no modifications
|
||||
|
||||
- [ ] Task 1.2: Create path display helper
|
||||
- [x] Task 1.2: Create path display helper [d237d3b]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Function to get all paths with resolution info
|
||||
- HOW: Returns dict of path_name -> (resolved, source)
|
||||
@@ -18,25 +18,25 @@ Focus: Show current path resolution in GUI
|
||||
## Phase 2: Context Hub Panel
|
||||
Focus: Add Path Configuration panel to GUI
|
||||
|
||||
- [ ] Task 2.1: Add Paths tab to Context Hub
|
||||
- [x] Task 2.1: Add Paths tab to Context Hub [d237d3b]
|
||||
- WHERE: src/gui_2.py (Context Hub section)
|
||||
- WHAT: New tab/section for path configuration
|
||||
- HOW: Add ImGui tab item, follow existing panel patterns
|
||||
- SAFETY: New panel, no modifications to existing
|
||||
|
||||
- [ ] Task 2.2: Display current paths
|
||||
- [x] Task 2.2: Display current paths [d237d3b]
|
||||
- WHERE: src/gui_2.py (new paths panel)
|
||||
- WHAT: Show resolved paths and their sources
|
||||
- HOW: Call paths.py functions, display in read-only text
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 2.3: Add path text inputs
|
||||
- [x] Task 2.3: Add path text inputs [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Editable text inputs for each path
|
||||
- HOW: ImGui input_text for conductor_dir, logs_dir, scripts_dir
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 2.4: Add browse buttons
|
||||
- [x] Task 2.4: Add browse buttons [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: File dialog buttons to browse for directories
|
||||
- HOW: Use existing file dialog patterns in gui_2.py
|
||||
@@ -45,19 +45,19 @@ Focus: Add Path Configuration panel to GUI
|
||||
## Phase 3: Persistence
|
||||
Focus: Save path changes to config.toml
|
||||
|
||||
- [ ] Task 3.1: Add config write function
|
||||
- [x] Task 3.1: Add config write function [d237d3b]
|
||||
- WHERE: src/gui_2.py or new utility
|
||||
- WHAT: Write [paths] section to config.toml
|
||||
- HOW: Read existing config, update paths section, write back
|
||||
- SAFETY: Backup before write, handle errors
|
||||
|
||||
- [ ] Task 3.2: Add Apply button
|
||||
- [x] Task 3.2: Add Apply button [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Button to save changes
|
||||
- HOW: Call config write function, show success/error message
|
||||
- SAFETY: Confirmation dialog
|
||||
|
||||
- [ ] Task 3.3: Add Reset button
|
||||
- [x] Task 3.3: Add Reset button [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Reset paths to defaults
|
||||
- HOW: Clear custom values, show confirmation
|
||||
@@ -66,13 +66,13 @@ Focus: Save path changes to config.toml
|
||||
## Phase 4: UX Polish
|
||||
Focus: Improve user experience
|
||||
|
||||
- [ ] Task 4.1: Add restart warning
|
||||
- [x] Task 4.1: Add restart warning [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Show warning that changes require restart
|
||||
- HOW: Text label after Apply
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 4.2: Add tooltips
|
||||
- [x] Task 4.2: Add tooltips [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Explain each path and resolution order
|
||||
- HOW: ImGui set_tooltip on hover
|
||||
@@ -81,7 +81,7 @@ Focus: Improve user experience
|
||||
## Phase 5: Tests
|
||||
Focus: Verify GUI path configuration
|
||||
|
||||
- [ ] Task 5.1: Test path display
|
||||
- [x] Task 5.1: Test path display [d237d3b]
|
||||
- WHERE: tests/test_gui_paths.py (new file)
|
||||
- WHAT: Verify paths panel shows correct values
|
||||
- HOW: Mock paths.py, verify display
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "opencode_config_overhaul_20260310",
|
||||
"title": "OpenCode Configuration Overhaul",
|
||||
"type": "fix",
|
||||
"status": "planned",
|
||||
"status": "completed",
|
||||
"priority": "high",
|
||||
"created": "2026-03-10",
|
||||
"depends_on": [],
|
||||
@@ -1,7 +1,6 @@
|
||||
# Implementation Plan: OpenCode Configuration Overhaul
|
||||
|
||||
## Phase 1: Core Config and Agent Temperature/Step Fixes
|
||||
Focus: Remove step limits, adjust temperatures, disable auto-compaction
|
||||
## Phase 1: Core Config and Agent Temperature/Step Fixes [checkpoint: 02abfc4]
|
||||
|
||||
- [x] Task 1.1: Update `opencode.json` - set `compaction.auto: false`, `compaction.prune: false`
|
||||
- [x] Task 1.2: Update `.opencode/agents/tier1-orchestrator.md` - remove `steps: 50`, change `temperature: 0.4` to `0.5`, add "Context Management" section
|
||||
@@ -10,15 +9,15 @@ Focus: Remove step limits, adjust temperatures, disable auto-compaction
|
||||
- [x] Task 1.5: Update `.opencode/agents/tier4-qa.md` - remove `steps: 5`, change `temperature: 0.0` to `0.2`
|
||||
- [x] Task 1.6: Update `.opencode/agents/general.md` - remove `steps: 15`, change `temperature: 0.2` to `0.3`
|
||||
- [x] Task 1.7: Update `.opencode/agents/explore.md` - remove `steps: 8`, change `temperature: 0.0` to `0.2`
|
||||
- [ ] Task 1.8: Conductor - User Manual Verification 'Phase 1: Core Config and Agent Temperature/Step Fixes' (Protocol in workflow.md)
|
||||
- [x] Task 1.8: Conductor - User Manual Verification (verified)
|
||||
|
||||
## Phase 2: MMA Tier Command Expansion
|
||||
Focus: Expand thin command wrappers into full protocol documentation
|
||||
## Phase 2: MMA Tier Command Expansion [checkpoint: 02abfc4]
|
||||
|
||||
- [x] Task 2.1: Expand `.opencode/commands/mma-tier1-orchestrator.md` - add full Surgical Methodology, limitations, context section
|
||||
- [x] Task 2.2: Expand `.opencode/commands/mma-tier2-tech-lead.md` - add TDD protocol, Pre-Delegation Checkpoint, delegation patterns
|
||||
- [x] Task 2.3: Expand `.opencode/commands/mma-tier3-worker.md` - add key constraints, task execution, blocking protocol
|
||||
- [x] Task 2.4: Expand `.opencode/commands/mma-tier4-qa.md` - add key constraints, analysis protocol, structured output format
|
||||
- [ ] Task 2.5: Conductor - User Manual Verification 'Phase 2: MMA Tier Command Expansion' (Protocol in workflow.md)
|
||||
## Phase 1: Core Config and Agent Temperature/Step Fixes [checkpoint: 02abfc4]
|
||||
- [x] Task 2.5: Conductor - User Manual Verification (verified)
|
||||
|
||||
## Phase: Review Fixes
|
||||
- [x] Task: Apply review suggestions 8c5b5d3
|
||||
@@ -3,13 +3,13 @@
|
||||
## Phase 1: Extend paths.py
|
||||
Focus: Add project-specific path resolution
|
||||
|
||||
- [ ] Task 1.1: Add project-aware conductor path functions
|
||||
- [x] Task 1.1: Add project-aware conductor path functions [48e2ed8]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Add optional project_path parameter to get_conductor_dir, get_tracks_dir, get_track_state_dir
|
||||
- HOW: If project_path provided, resolve relative to project root; otherwise use global
|
||||
- SAFETY: Maintain backward compatibility with no-arg calls
|
||||
|
||||
- [ ] Task 1.2: Add project conductor path resolution
|
||||
- [x] Task 1.2: Add project conductor path resolution [48e2ed8]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: New function `_resolve_project_conductor_dir(project_path)` that reads from project TOML
|
||||
- HOW: Load project TOML, check `[conductor].dir` key
|
||||
@@ -18,18 +18,18 @@ Focus: Add project-specific path resolution
|
||||
## Phase 2: Update project_manager.py
|
||||
Focus: Use project-specific paths for track operations
|
||||
|
||||
- [ ] Task 2.1: Update save_track_state to use project conductor dir
|
||||
- [x] Task 2.1: Update save_track_state to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 240)
|
||||
- WHAT: Pass project base_dir to paths.get_track_state_dir()
|
||||
- HOW: Get base_dir from project_path, call paths with project_path param
|
||||
- SAFETY: Maintain existing function signature compatibility
|
||||
|
||||
- [ ] Task 2.2: Update load_track_state to use project conductor dir
|
||||
- [x] Task 2.2: Update load_track_state to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 252)
|
||||
- WHAT: Load track state from project-specific directory
|
||||
- HOW: Same as above
|
||||
|
||||
- [ ] Task 2.3: Update get_all_tracks to use project conductor dir
|
||||
- [x] Task 2.3: Update get_all_tracks to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 297)
|
||||
- WHAT: List tracks from project-specific directory
|
||||
- HOW: Accept optional project_path param
|
||||
@@ -37,7 +37,7 @@ Focus: Use project-specific paths for track operations
|
||||
## Phase 3: Update app_controller.py
|
||||
Focus: Pass project path to track operations
|
||||
|
||||
- [ ] Task 3.1: Update track creation to use project conductor dir
|
||||
- [x] Task 3.1: Update track creation to use project conductor dir [3999e9c]
|
||||
- WHERE: src/app_controller.py (around line 1907, 1937)
|
||||
- WHAT: Pass active_project_path to track path functions
|
||||
- HOW: Get active_project_path, pass to paths.get_tracks_dir()
|
||||
@@ -46,13 +46,13 @@ Focus: Pass project path to track operations
|
||||
## Phase 4: Tests
|
||||
Focus: Verify project-specific behavior
|
||||
|
||||
- [ ] Task 4.1: Write test for project-specific conductor dir
|
||||
- [x] Task 4.1: Write test for project-specific conductor dir [48e2ed8]
|
||||
- WHERE: tests/test_project_paths.py (new file)
|
||||
- WHAT: Create mock project with custom conductor dir, verify tracks saved there
|
||||
- HOW: Mock project_manager, verify path resolution
|
||||
- SAFETY: New test file
|
||||
|
||||
- [ ] Task 4.2: Test backward compatibility
|
||||
- [x] Task 4.2: Test backward compatibility [3999e9c]
|
||||
- WHERE: tests/test_project_paths.py
|
||||
- WHAT: Verify global paths still work without project_path
|
||||
- HOW: Call functions without project_path, verify defaults
|
||||
@@ -33,7 +33,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
|
||||
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
|
||||
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
|
||||
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files (e.g., `conductor/tracks/<track_id>/state.toml`). This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
||||
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
||||
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
|
||||
|
||||
- **Programmable Execution State machine:** Governing the transition between "Auto-Queue" (autonomous worker spawning) and "Step Mode" (explicit manual approval for each task transition).
|
||||
@@ -45,6 +45,12 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Parallel Multi-Agent Execution:** Executes multiple AI workers in parallel using a non-blocking execution engine and a dedicated `WorkerPool`. Features configurable concurrency limits (defaulting to 4) to optimize resource usage and prevent API rate limiting.
|
||||
- **Parallel Tool Execution:** Executes independent tool calls (e.g., parallel file reads) concurrently within a single agent turn using an asynchronous execution engine, significantly reducing end-to-end latency.
|
||||
- **Automated Tier 4 QA:** Integrates real-time error interception in the shell runner, automatically forwarding technical failures to cheap sub-agents for 20-word diagnostic summaries injected back into the worker history.
|
||||
- **External MCP Server Support:** Adds support for integrating external Model Context Protocol (MCP) servers, expanding the agent's toolset with the broader MCP ecosystem.
|
||||
- **Multi-Server Lifecycle Management:** Orchestrates multiple concurrent MCP server sessions (Stdio for local subprocesses and SSE for remote servers).
|
||||
- **Flexible Configuration:** Supports global (`config.toml`) and project-specific (`manual_slop.toml`) paths for `mcp_config.json` (standard MCP configuration format).
|
||||
- **Auto-Start & Discovery:** Automatically initializes configured servers on project load and dynamically aggregates their tools into the agent's capability declarations.
|
||||
- **Dedicated Operations UI:** Features a new **External Tools** section within the Operations Hub for monitoring server status (idle, starting, running, error) and browsing discovered tool schemas. Supports **Pop-Out Panel functionality**, allowing the External Tools interface to be detached into a standalone window for optimized multi-monitor workflows.
|
||||
- **Strict HITL Safety:** All external tool calls are intercepted and require explicit human-in-the-loop approval via the standard confirmation dialog before execution.
|
||||
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
|
||||
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
|
||||
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
|
||||
@@ -54,7 +60,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
||||
- **Improved MMA Observability:** Enhances sub-agent logging by injecting precise ticket IDs and descriptive roles into communication metadata, enabling granular filtering and tracking of parallel worker activities within the Comms History.
|
||||
- **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface.
|
||||
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows.
|
||||
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config). Changes are applied immediately at runtime without requiring an application restart.
|
||||
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
|
||||
- **Structured Log Taxonomy:** Automated session-based log organization into configurable directories (defaulting to `logs/sessions/`). Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions.
|
||||
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
||||
@@ -63,7 +69,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a "Black Void" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
|
||||
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
|
||||
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
|
||||
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
|
||||
- **Headless Backend Service & Hook API:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled service. Features a comprehensive Hook API and WebSocket event streaming for remote orchestration, deep state inspection, and manual worker lifecycle management.
|
||||
- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety.
|
||||
- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. Now features full functional parity with the direct API, including accurate token estimation, safety settings, and robust system instruction handling.
|
||||
- **Context & Token Visualization:** Detailed UI panels for monitoring real-time token usage, history depth, and **visual cache awareness** (tracking specific files currently live in the provider's context cache).
|
||||
@@ -73,10 +79,14 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Scoped Inheritance:** Supports **Global** (application-wide) and **Project-Specific** presets. Project presets with the same name automatically override global counterparts, allowing for fine-tuned context tailoring.
|
||||
- **Full AI Profiles:** Presets capture not only the system prompt text but also critical model parameters like **Temperature**, **Top-P**, and **Max Output Tokens**.
|
||||
- **Preset Manager Modal:** A dedicated high-density GUI for creating, editing, and deleting presets with real-time validation and instant application to the active session.
|
||||
- **Agent Personas & Unified Profiles:** Consolidates model settings, provider routing, system prompts, tool presets, and bias profiles into named "Persona" entities.
|
||||
- **Single Configuration Entity:** Switch models, tool weights, and system prompts simultaneously using a single Persona selection.
|
||||
- **Persona Editor Modal:** A dedicated high-density GUI for creating, editing, and deleting Personas.
|
||||
- **MMA Granular Assignment:** Allows assigning specific Personas to individual agents within the 4-Tier Hierarchical MMA.
|
||||
- **Agent Tool Weighting & Bias:** Influences agent tool selection via a weighting system.
|
||||
- **Semantic Nudging:** Automatically prefixes tool and parameter descriptions with priority tags (e.g., [HIGH PRIORITY], [PREFERRED]) to bias model selection.
|
||||
- **Dynamic Tooling Strategy:** Automatically appends a Markdown "Tooling Strategy" section to system instructions based on the active preset and global bias profile.
|
||||
- **Global Bias Profiles:** Application of category-level multipliers (e.g., Execution-Focused, Discovery-Heavy) to influence agent behavior across broad toolsets.
|
||||
- **Priority Badges:** High-density, color-coded visual indicators in tool lists showing the assigned priority level of each capability.
|
||||
- **Priority Badges & Refined Layout:** High-density, color-coded visual indicators in tool lists showing the assigned priority level of each capability. Displays tool names before radio buttons with consistent spacing for improved readability.
|
||||
- **Category-Based Filtering:** Integrated category filtering in both the Active Tools panel and the Tool Preset Manager, allowing users to quickly manage large toolsets.
|
||||
- **Fine-Grained Weight Control:** Integrated sliders in the Preset Manager for adjusting individual tool weights (1-5) and parameter-level biases.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
## Web & Service Frameworks
|
||||
|
||||
- **FastAPI:** High-performance REST API framework for providing the headless backend service.
|
||||
- **websockets:** Lightweight asynchronous WebSocket server for real-time event streaming and remote orchestration.
|
||||
- **Uvicorn:** ASGI server for serving the FastAPI application.
|
||||
|
||||
## AI Integration SDKs
|
||||
@@ -29,14 +30,20 @@
|
||||
|
||||
- **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption.
|
||||
|
||||
- **src/paths.py:** Centralized module for path resolution, allowing directory paths (logs, conductor, scripts) to be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
|
||||
- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. If not specified, conductor paths default to `./conductor` relative to each project's TOML file. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Supports **Runtime Re-Resolution** via `reset_resolved()`, allowing path changes to be applied immediately without an application restart. Path configuration (logs, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
|
||||
|
||||
- **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance.
|
||||
|
||||
- **src/personas.py:** Implements `PersonaManager` for high-performance CRUD operations on unified agent personas stored in TOML format (`personas.toml`, `project_personas.toml`). Handles consolidation of model settings, prompts, and tool biases.
|
||||
|
||||
- **src/tool_bias.py:** Implements the `ToolBiasEngine` for semantic tool description nudging and dynamic tooling strategy generation.
|
||||
|
||||
- **src/tool_presets.py:** Extends `ToolPresetManager` to handle nested `Tool` models, weights, and global `BiasProfile` persistence within `tool_presets.toml`.
|
||||
|
||||
- **src/mcp_client.py (External Extension):** Implements the `ExternalMCPManager` for orchestrating third-party Model Context Protocol servers.
|
||||
- **StdioMCPServer:** Manages local MCP servers via asynchronous subprocess pipes (stdin/stdout/stderr).
|
||||
- **RemoteMCPServer (SSE):** Provides a foundation for remote MCP integration via Server-Sent Events.
|
||||
- **JSON-RPC 2.0 Engine:** Handles asynchronous message routing, request/response matching, and error handling for all external MCP communication.
|
||||
|
||||
- **tree-sitter / AST Parsing:** For deterministic AST parsing and automated generation of curated "Skeleton Views" and "Targeted Views" (extracting specific functions and their dependencies). Features an integrated AST cache with mtime-based invalidation to minimize re-parsing overhead.
|
||||
- **pydantic / dataclasses:** For defining strict state schemas (Tracks, Tickets) used in linear orchestration.
|
||||
|
||||
@@ -10,32 +10,31 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
### Architecture & Backend
|
||||
|
||||
1. [ ] **Track: External MCP Server Support**
|
||||
*Link: [./tracks/external_mcp_support_20260308/](./tracks/external_mcp_support_20260308/)*
|
||||
*Goal: Add support for external MCP servers (Local Stdio and Remote SSE/WS) with flexible configuration and lifecycle management (including auto-start on project load).*
|
||||
|
||||
2. [ ] **Track: RAG Support**
|
||||
1. [ ] **Track: RAG Support**
|
||||
*Link: [./tracks/rag_support_20260308/](./tracks/rag_support_20260308/)*
|
||||
*Goal: Add support for RAG (Retrieval-Augmented Generation) using local vector stores (Chroma/Qdrant), native vendor retrieval, and external RAG APIs. Implement indexing pipeline and retrieval UI.*
|
||||
|
||||
3. [x] **Track: Agent Tool Preference & Bias Tuning**
|
||||
2. [x] **Track: Agent Tool Preference & Bias Tuning**
|
||||
*Link: [./tracks/tool_bias_tuning_20260308/](./tracks/tool_bias_tuning_20260308/)*
|
||||
*Goal: Influence agent tool selection via a weighting system. Implement semantic nudges in tool descriptions and a dynamic "Tooling Strategy" section in the system prompt. Includes GUI badges and sliders for weight adjustment.*
|
||||
|
||||
4. [ ] **Track: Expanded Hook API & Headless Orchestration**
|
||||
3. [x] **Track: Expanded Hook API & Headless Orchestration**
|
||||
*Link: [./tracks/hook_api_expansion_20260308/](./tracks/hook_api_expansion_20260308/)*
|
||||
*Goal: Maximize internal state exposure and provide comprehensive control endpoints (worker spawn/kill, pipeline pause/resume, DAG mutation) via the Hook API. Implement WebSocket-based real-time event streaming.*
|
||||
|
||||
5. [ ] **Track: Codebase Audit and Cleanup**
|
||||
4. [ ] **Track: Codebase Audit and Cleanup**
|
||||
*Link: [./tracks/codebase_audit_20260308/](./tracks/codebase_audit_20260308/)*
|
||||
|
||||
6. [ ] **Track: Expanded Test Coverage and Stress Testing**
|
||||
5. [ ] **Track: Expanded Test Coverage and Stress Testing**
|
||||
*Link: [./tracks/test_coverage_expansion_20260309/](./tracks/test_coverage_expansion_20260309/)*
|
||||
|
||||
7. [ ] **Track: Beads Mode Integration**
|
||||
6. [ ] **Track: Beads Mode Integration**
|
||||
*Link: [./tracks/beads_mode_20260309/](./tracks/beads_mode_20260309/)*
|
||||
*Goal: Integrate Beads (git-backed graph issue tracker) as an alternative backend for MMA implementation tracks and tickets.*
|
||||
|
||||
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
|
||||
*Link: [./tracks/data_oriented_optimization_20260312/](./tracks/data_oriented_optimization_20260312/)*
|
||||
|
||||
---
|
||||
|
||||
### GUI Overhauls & Visualizations
|
||||
@@ -61,6 +60,22 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
6. [ ] **Track: Custom Shader and Window Frame Support**
|
||||
*Link: [./tracks/custom_shaders_20260309/](./tracks/custom_shaders_20260309/)*
|
||||
|
||||
7. [x] **Track: UI/UX Improvements - Presets and AI Settings**
|
||||
*Link: [./tracks/presets_ai_settings_ux_20260311/](./tracks/presets_ai_settings_ux_20260311/)*
|
||||
*Goal: Improve the layout, scaling, and control ergonomics of the Preset windows (Personas, Prompts, Tools) and AI Settings panel. Includes dual-control sliders and categorized tool management.*
|
||||
|
||||
8. [ ] **Track: Session Context Snapshots & Visibility**
|
||||
*Link: [./tracks/session_context_snapshots_20260311/](./tracks/session_context_snapshots_20260311/)*
|
||||
*Goal: Session-scoped context management, saving Context Presets, MMA assignment, and agent-focused session filtering in the UI.*
|
||||
|
||||
9. [ ] **Track: Discussion Takes & Timeline Branching**
|
||||
*Link: [./tracks/discussion_takes_branching_20260311/](./tracks/discussion_takes_branching_20260311/)*
|
||||
*Goal: Non-linear discussion timelines via tabbed "takes", message branching, and synthesis generation workflows.*
|
||||
|
||||
10. [ ] **Track: Undo/Redo History Support**
|
||||
*Link: [./tracks/undo_redo_history_20260311/](./tracks/undo_redo_history_20260311/)*
|
||||
*Goal: Robust, non-provider based undo/redo for text inputs, UI controls, discussion mutations, and context management. Includes hotkey support and a history list view.*
|
||||
|
||||
---
|
||||
|
||||
### Additional Language Support
|
||||
@@ -87,18 +102,6 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
---
|
||||
|
||||
### Path Configuration
|
||||
|
||||
1. [ ] **Track: Project-Specific Conductor Directory**
|
||||
*Link: [./tracks/project_conductor_dir_20260308/](./tracks/project_conductor_dir_20260308/)*
|
||||
*Goal: Make conductor directory per-project. Each project TOML can specify custom conductor dir for isolated track/state management.*
|
||||
|
||||
2. [ ] **Track: GUI Path Configuration in Context Hub**
|
||||
*Link: [./tracks/gui_path_config_20260308/](./tracks/gui_path_config_20260308/)*
|
||||
*Goal: Add path configuration UI to Context Hub. Allow users to view and edit configurable paths directly from the GUI.*
|
||||
|
||||
---
|
||||
|
||||
### Manual UX Controls
|
||||
|
||||
1. [x] **Track: Saved System Prompt Presets**
|
||||
@@ -117,10 +120,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
*Link: [./tracks/agent_personas_20260309/](./tracks/agent_personas_20260309/)*
|
||||
*Goal: Consolidate model settings, prompts, and tool presets into a unified "Persona" model with granular MMA assignment.*
|
||||
|
||||
5. [ ] **Track: OpenCode Configuration Overhaul**
|
||||
*Link: [./tracks/opencode_config_overhaul_20260310/](./tracks/opencode_config_overhaul_20260310/)*
|
||||
*Goal: Fix critical gaps in OpenCode agent configuration: remove step limits, disable auto-compaction, raise temperatures, expand thin MMA tier command wrappers into full protocol documentation.*
|
||||
*Goal: Fix critical gaps in OpenCode agent configuration: remove step limits, disable auto-compaction, raise temperatures, expand thin MMA tier command wrappers into full protocol documentation.*
|
||||
5. [x] **Track: OpenCode Configuration Overhaul** (Archived 2026-03-10)
|
||||
|
||||
6. [ ] **Track: Advanced Workspace Docking & Layout Profiles**
|
||||
*Link: [./tracks/workspace_profiles_20260310/](./tracks/workspace_profiles_20260310/)*
|
||||
@@ -155,6 +155,9 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
### Completed / Archived
|
||||
|
||||
- [x] **Track: External MCP Server Support** (Archived 2026-03-12)
|
||||
- [x] **Track: Project-Specific Conductor Directory** (Archived 2026-03-12)
|
||||
- [x] **Track: GUI Path Configuration in Context Hub** (Archived 2026-03-12)
|
||||
- [x] **Track: True Parallel Worker Execution (The DAG Realization)**
|
||||
- [x] **Track: Deep AST-Driven Context Pruning (RAG for Code)**
|
||||
- [x] **Track: Visual DAG & Interactive Ticket Editing**
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track data_oriented_optimization_20260312 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "data_oriented_optimization_20260312",
|
||||
"type": "chore",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-12T00:00:00Z",
|
||||
"updated_at": "2026-03-12T00:00:00Z",
|
||||
"description": "Optimization pass. I want to update the product guidlines to take into account with data-oriented appraoch the more performant way to semantically define procedrual code in python so executes almost entirely heavy operations optimally. I know there is a philosophy of 'the less python does the better' which is problably why the imgui lib is so performant because all python really does is define the ui's DAG via an imgui interface procedurally along with what state the dag may modify within its constraints of interactions the user may do. This problably can be reflected in the way the rest of the codebase is done. I want to go over the ./src and ./simulation to make sure this insight and related herustics are properly enfroced. Worst case I want to identify what code I should consider lower down to C maybe and making python bindings to if there is a significant bottleneck identified via profiling and testing that cannot be resolved otherwise."
|
||||
}
|
||||
27
conductor/tracks/data_oriented_optimization_20260312/plan.md
Normal file
27
conductor/tracks/data_oriented_optimization_20260312/plan.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Implementation Plan: Data-Oriented Python Optimization Pass
|
||||
|
||||
## Phase 1: Guidelines and Instrumentation
|
||||
- [ ] Task: Update `conductor/product-guidelines.md` with Data-Oriented Python heuristics and the "less Python does the better" philosophy.
|
||||
- [ ] Task: Review existing profiling instrumentation in `src/performance_monitor.py` or diagnostic hooks.
|
||||
- [ ] Task: Expand profiling instrumentation to capture more detailed execution times for non-GUI data structures/processes if necessary.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Guidelines and Instrumentation' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Audit and Profiling (`src/` and `simulation/`)
|
||||
- [ ] Task: Run profiling scenarios (especially utilizing simulations) to generate baseline metrics.
|
||||
- [ ] Task: Audit `src/` (e.g., `dag_engine.py`, `multi_agent_conductor.py`, `aggregate.py`) against the new guidelines, cross-referencing with profiling data to identify bottlenecks.
|
||||
- [ ] Task: Audit `simulation/` files against the new guidelines to ensure the test harness is performant and non-blocking.
|
||||
- [ ] Task: Compile a list of identified bottleneck targets to refactor.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Audit and Profiling (`src/` and `simulation/`)' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Targeted Optimization and Refactoring
|
||||
- [ ] Task: Write/update tests for the first identified bottleneck to establish a performance or structural baseline (Red Phase).
|
||||
- [ ] Task: Refactor the first identified bottleneck to align with data-oriented guidelines (Green Phase).
|
||||
- [ ] Task: Write/update tests for remaining identified bottlenecks.
|
||||
- [ ] Task: Refactor remaining identified bottlenecks.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Targeted Optimization and Refactoring' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Final Evaluation and Documentation
|
||||
- [ ] Task: Re-run all profiling scenarios to compare against the baseline metrics.
|
||||
- [ ] Task: Analyze remaining bottlenecks that did not reach performance thresholds and document them as candidates for C/C++ bindings (Last Resort).
|
||||
- [ ] Task: Generate a final summary report of the optimizations applied and the C extension evaluation.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Evaluation and Documentation' (Protocol in workflow.md)
|
||||
35
conductor/tracks/data_oriented_optimization_20260312/spec.md
Normal file
35
conductor/tracks/data_oriented_optimization_20260312/spec.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Specification: Data-Oriented Python Optimization Pass
|
||||
|
||||
## Overview
|
||||
Perform an optimization pass and audit across the codebase (`./src` and `./simulation`), aligning the implementation with the Data-Oriented Design philosophy and the "less Python does the better" heuristic. Update the `product-guidelines.md` to formally document this approach for procedural Python code.
|
||||
|
||||
## Functional Requirements
|
||||
1. **Update Product Guidelines:**
|
||||
- Formalize the heuristic that Python should act primarily as a procedural semantic definer (similar to how ImGui defines a UI DAG), delegating heavy lifting.
|
||||
- Enforce data-oriented guidelines for Python code structure, focusing on minimizing Python JIT overhead.
|
||||
2. **Codebase Audit (`./src` and `./simulation`):**
|
||||
- Review global `src/` files and simulation logic against the new guidelines.
|
||||
- Identify bottlenecks that violate these heuristics (e.g., heavy procedural state manipulation in Python).
|
||||
3. **Profiling & Instrumentation Expansion:**
|
||||
- Expand existing profiling instrumentation (e.g., `performance_monitor.py` or diagnostic hooks) if currently insufficient for identifying real structural bottlenecks.
|
||||
4. **Optimization Execution:**
|
||||
- Refactor identified bottlenecks to align with the new data-oriented Python heuristics.
|
||||
- Re-evaluate performance post-refactor.
|
||||
5. **C Extension Evaluation (Last Resort):**
|
||||
- If Python optimizations fail to meet performance thresholds, specifically identify and document routines that must be lowered to C/C++ with Python bindings. Only proceed with bindings if absolutely necessary.
|
||||
|
||||
## Non-Functional Requirements
|
||||
- Maintain existing test coverage and strict type-hinting requirements.
|
||||
- Ensure 1-space indentation and ultra-compact style rules are not violated during refactoring.
|
||||
- Ensure the main GUI rendering thread is never blocked.
|
||||
|
||||
## Acceptance Criteria
|
||||
- `product-guidelines.md` is updated with data-oriented procedural Python guidelines.
|
||||
- `src/` and `simulation/` undergo a documented profiling audit.
|
||||
- Identified bottlenecks are refactored to reduce Python overhead.
|
||||
- No regressions in automated simulation or unit tests.
|
||||
- A final report is provided detailing optimizations made and any candidates for future C extension porting.
|
||||
|
||||
## Out of Scope
|
||||
- Actually implementing C/C++ bindings in this track (this track only identifies/evaluates them as a last resort; if needed, they get a separate track).
|
||||
- Major UI visual theme changes.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track discussion_takes_branching_20260311 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "discussion_takes_branching_20260311",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-11T19:30:00Z",
|
||||
"updated_at": "2026-03-11T19:30:00Z",
|
||||
"description": "Discussion Takes & Timeline Branching: Tabbed interface for multi-timeline takes, message branching, and synthesis generation workflows."
|
||||
}
|
||||
25
conductor/tracks/discussion_takes_branching_20260311/plan.md
Normal file
25
conductor/tracks/discussion_takes_branching_20260311/plan.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Implementation Plan: Discussion Takes & Timeline Branching
|
||||
|
||||
## Phase 1: Backend Support for Timeline Branching
|
||||
- [ ] Task: Write failing tests for extending the session state model to support branching (tree-like history or parallel linear "takes" with a shared ancestor).
|
||||
- [ ] Task: Implement backend logic to branch a session history at a specific message index into a new take ID.
|
||||
- [ ] Task: Implement backend logic to promote a specific take ID into an independent, top-level session.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Timeline Branching' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: GUI Implementation for Tabbed Takes
|
||||
- [ ] Task: Write GUI tests verifying the rendering and navigation of multiple tabs for a single session.
|
||||
- [ ] Task: Implement a tabbed interface within the Discussion window to switch between different takes of the active session.
|
||||
- [ ] Task: Add a "Split/Branch from here" action to individual message entries in the discussion history.
|
||||
- [ ] Task: Add a UI button/action to promote the currently active take to a new separate session.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Implementation for Tabbed Takes' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Synthesis Workflow Formatting
|
||||
- [ ] Task: Write tests for a new text formatting utility that takes multiple history sequences and generates a compressed, diff-like text representation.
|
||||
- [ ] Task: Implement the sequence differencing and compression logic to clearly highlight variances between takes.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Synthesis Workflow Formatting' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Synthesis UI & Agent Integration
|
||||
- [ ] Task: Write GUI tests for the multi-take selection interface and synthesis action.
|
||||
- [ ] Task: Implement a UI mechanism allowing users to select multiple takes and provide a synthesis prompt.
|
||||
- [ ] Task: Implement the execution pipeline to feed the compressed differences and user prompt to an AI agent, and route the generated synthesis to a new "take" tab.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Synthesis UI & Agent Integration' (Protocol in workflow.md)
|
||||
23
conductor/tracks/discussion_takes_branching_20260311/spec.md
Normal file
23
conductor/tracks/discussion_takes_branching_20260311/spec.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Specification: Discussion Takes & Timeline Branching
|
||||
|
||||
## 1. Overview
|
||||
This track introduces non-linear discussion timelines, allowing users to create multiple "takes" (branches) from a shared point in a conversation. It includes UI for managing these parallel timelines within a single discussion window and features a specialized synthesis workflow to merge ideas from multiple takes.
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
### 2.1 Timeline Branching (Takes)
|
||||
- **Message Branching:** Add a "Split/Branch from here" action on individual discussion messages.
|
||||
- **Tabbed Interface:** Branching creates a new "take," represented visually as a new tab within the same discussion session. The new tab shares the timeline history up to the split point.
|
||||
- **Take Promotion:** Allow users to promote any specific take into an entirely new, standalone discussion session.
|
||||
|
||||
### 2.2 Take Synthesis Workflow
|
||||
- **Multi-Take Selection:** Provide a UI to select multiple takes from a shared split point for comparison and synthesis.
|
||||
- **Diff/Compressed Representation:** Develop a formatted representation (e.g., compressed diffs or parallel sequence summaries) that clearly highlights the differences between the selected takes.
|
||||
- **Synthesis Generation:** Feed the compressed representation of the differences to an AI agent along with a user prompt (e.g., "I liked aspects of both, do C with these caveats") to generate a new, synthesized take.
|
||||
|
||||
## 3. Acceptance Criteria
|
||||
- [ ] Users can split a discussion from any message to create a new "take".
|
||||
- [ ] Takes are navigable via a tabbed interface within the discussion window.
|
||||
- [ ] A take can be promoted to a standalone discussion session.
|
||||
- [ ] Multiple takes can be selected and formatted into a compressed difference view.
|
||||
- [ ] An AI agent can successfully process the compressed take view to generate a synthesized continuation.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Implementation Plan: External MCP Server Support
|
||||
|
||||
## Phase 1: Configuration & Data Modeling
|
||||
- [ ] Task: Define the schema for external MCP server configuration.
|
||||
- [ ] Update `src/models.py` to include `MCPServerConfig` and `MCPConfiguration` classes.
|
||||
- [ ] Implement logic to load `mcp_config.json` from global and project-specific paths.
|
||||
- [ ] Task: Integrate configuration loading into `AppController`.
|
||||
- [ ] Ensure the MCP config path is correctly resolved from `config.toml` and `manual_slop.toml`.
|
||||
- [ ] Task: Write unit tests for configuration loading and validation.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: MCP Client Extension
|
||||
- [ ] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`.
|
||||
- [ ] Add support for managing multiple MCP server sessions.
|
||||
- [ ] Implement the `StdioMCPClient` for local subprocess communication.
|
||||
- [ ] Implement the `RemoteMCPClient` for SSE/WebSocket communication.
|
||||
- [ ] Task: Update Tool Discovery.
|
||||
- [ ] Implement `list_external_tools()` to aggregate tools from all active external servers.
|
||||
- [ ] Task: Update Tool Dispatch.
|
||||
- [ ] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
|
||||
- [ ] Task: Write integration tests for stdio and remote MCP client communication (using mock servers).
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: GUI Integration & Lifecycle
|
||||
- [ ] Task: Update the **Operations** panel in `src/gui_2.py`.
|
||||
- [ ] Create a new "External Tools" section.
|
||||
- [ ] List discovered tools from active external servers.
|
||||
- [ ] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
|
||||
- [ ] Task: Implement Lifecycle Management.
|
||||
- [ ] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
|
||||
- [ ] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
|
||||
- [ ] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Agent Integration & HITL
|
||||
- [ ] Task: Update AI tool declarations.
|
||||
- [ ] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
|
||||
- [ ] Task: Verify HITL Approval Flow.
|
||||
- [ ] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
|
||||
- [ ] Verify that approved external tool results are correctly returned to the AI.
|
||||
- [ ] Task: Perform a final end-to-end verification with a real external MCP server.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' (Protocol in workflow.md)
|
||||
@@ -1,44 +1,44 @@
|
||||
# Implementation Plan: Expanded Hook API & Headless Orchestration
|
||||
|
||||
## Phase 1: WebSocket Infrastructure & Event Streaming
|
||||
- [ ] Task: Implement the WebSocket gateway.
|
||||
- [ ] Integrate a lightweight WebSocket library (e.g., `websockets` or `simple-websocket`).
|
||||
- [ ] Create a dedicated `WebSocketServer` class in `src/api_hooks.py` that runs on a separate port (e.g., 9000).
|
||||
- [ ] Implement a basic subscription mechanism for different event channels.
|
||||
- [ ] Task: Connect the event queue to the WebSocket stream.
|
||||
- [ ] Update `AsyncEventQueue` to broadcast events to connected WebSocket clients.
|
||||
- [ ] Add high-frequency telemetry (FPS, CPU) to the event stream.
|
||||
- [ ] Task: Write unit tests for WebSocket connection and event broadcasting.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: WebSocket Infrastructure' (Protocol in workflow.md)
|
||||
- [x] Task: Implement the WebSocket gateway.
|
||||
- [x] Integrate a lightweight WebSocket library (e.g., `websockets` or `simple-websocket`).
|
||||
- [x] Create a dedicated `WebSocketServer` class in `src/api_hooks.py` that runs on a separate port (e.g., 9000).
|
||||
- [x] Implement a basic subscription mechanism for different event channels.
|
||||
- [x] Task: Connect the event queue to the WebSocket stream.
|
||||
- [x] Update `AsyncEventQueue` to broadcast events to connected WebSocket clients.
|
||||
- [x] Add high-frequency telemetry (FPS, CPU) to the event stream.
|
||||
- [x] Task: Write unit tests for WebSocket connection and event broadcasting.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: WebSocket Infrastructure' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Expanded Read Endpoints (GET)
|
||||
- [ ] Task: Implement detailed state exposure endpoints.
|
||||
- [ ] Add `/api/mma/workers` to return the status, logs, and traces of all active sub-agents.
|
||||
- [ ] Add `/api/context/state` to expose AST cache metadata and file aggregation status.
|
||||
- [ ] Add `/api/metrics/financial` to return track-specific token usage and cost data.
|
||||
- [ ] Add `/api/system/telemetry` to expose internal thread and queue metrics.
|
||||
- [ ] Task: Enhance `/api/gui/state` to provide a truly exhaustive JSON dump of all internal managers.
|
||||
- [ ] Task: Update `api_hook_client.py` with corresponding methods for all new GET endpoints.
|
||||
- [ ] Task: Write integration tests for all new GET endpoints using `live_gui`.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Expanded Read Endpoints' (Protocol in workflow.md)
|
||||
- [x] Task: Implement detailed state exposure endpoints.
|
||||
- [x] Add `/api/mma/workers` to return the status, logs, and traces of all active sub-agents.
|
||||
- [x] Add `/api/context/state` to expose AST cache metadata and file aggregation status.
|
||||
- [x] Add `/api/metrics/financial` to return track-specific token usage and cost data.
|
||||
- [x] Add `/api/system/telemetry` to expose internal thread and queue metrics.
|
||||
- [x] Task: Enhance `/api/gui/state` to provide a truly exhaustive JSON dump of all internal managers.
|
||||
- [x] Task: Update `api_hook_client.py` with corresponding methods for all new GET endpoints.
|
||||
- [x] Task: Write integration tests for all new GET endpoints using `live_gui`.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Expanded Read Endpoints' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Comprehensive Control Endpoints (POST)
|
||||
- [ ] Task: Implement worker and pipeline control.
|
||||
- [ ] Add `/api/mma/workers/spawn` to manually initiate sub-agent execution via the API.
|
||||
- [ ] Add `/api/mma/workers/kill` to programmatically abort running workers.
|
||||
- [ ] Add `/api/mma/pipeline/pause` and `/api/mma/pipeline/resume` to control the global MMA loop.
|
||||
- [ ] Task: Implement context and DAG mutation.
|
||||
- [ ] Add `/api/context/inject` to allow programmatic context injection (files/skeletons).
|
||||
- [ ] Add `/api/mma/dag/mutate` to allow modifying task dependencies through the API.
|
||||
- [ ] Task: Update `api_hook_client.py` with corresponding methods for all new POST endpoints.
|
||||
- [ ] Task: Write integration tests for all new control endpoints using `live_gui`.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Comprehensive Control Endpoints' (Protocol in workflow.md)
|
||||
- [x] Task: Implement worker and pipeline control.
|
||||
- [x] Add `/api/mma/workers/spawn` to manually initiate sub-agent execution via the API.
|
||||
- [x] Add `/api/mma/workers/kill` to programmatically abort running workers.
|
||||
- [x] Add `/api/mma/pipeline/pause` and `/api/mma/pipeline/resume` to control the global MMA loop.
|
||||
- [x] Task: Implement context and DAG mutation.
|
||||
- [x] Add `/api/context/inject` to allow programmatic context injection (files/skeletons).
|
||||
- [x] Add `/api/mma/dag/mutate` to allow modifying task dependencies through the API.
|
||||
- [x] Task: Update `api_hook_client.py` with corresponding methods for all new POST endpoints.
|
||||
- [x] Task: Write integration tests for all new control endpoints using `live_gui`.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: Comprehensive Control Endpoints' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Headless Refinement & Verification
|
||||
- [ ] Task: Improve error reporting.
|
||||
- [ ] Refactor `HookHandler` to catch and wrap all internal exceptions in JSON error responses.
|
||||
- [ ] Task: Conduct a full headless simulation.
|
||||
- [ ] Create a specialized simulation script that replicates a full MMA track lifecycle (planning, worker spawn, DAG mutation, completion) using ONLY the Hook API.
|
||||
- [ ] Task: Final performance audit.
|
||||
- [ ] Ensure that active WebSocket clients and large state dumps do not cause GUI frame drops.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Headless Refinement & Verification' (Protocol in workflow.md)
|
||||
- [x] Task: Improve error reporting.
|
||||
- [x] Refactor `HookHandler` to catch and wrap all internal exceptions in JSON error responses.
|
||||
- [x] Task: Conduct a full headless simulation.
|
||||
- [x] Create a specialized simulation script that replicates a full MMA track lifecycle (planning, worker spawn, DAG mutation, completion) using ONLY the Hook API.
|
||||
- [x] Task: Final performance audit.
|
||||
- [x] Ensure that active WebSocket clients and large state dumps do not cause GUI frame drops.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Headless Refinement & Verification' (Protocol in workflow.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track presets_ai_settings_ux_20260311 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "presets_ai_settings_ux_20260311",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-11T14:45:00Z",
|
||||
"updated_at": "2026-03-11T14:45:00Z",
|
||||
"description": "Read through ./docs, and ./src/gui_2.py, ./src/app_controller.py. I want todo various ux improvements to the preset windows (personas, prompts, and tools) and ai settings."
|
||||
}
|
||||
33
conductor/tracks/presets_ai_settings_ux_20260311/plan.md
Normal file
33
conductor/tracks/presets_ai_settings_ux_20260311/plan.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Implementation Plan: UI/UX Improvements - Presets and AI Settings
|
||||
|
||||
This plan focuses on enhancing the layout, scaling, and control ergonomics of the Preset windows and AI Settings panel.
|
||||
|
||||
## Phase 1: Research and Layout Audit [checkpoint: db1f749]
|
||||
- [x] Task: Audit `src/gui_2.py` and `src/app_controller.py` for current window resizing and scaling logic. db1f749
|
||||
- [x] Task: Identify specific UI sections in `Personas`, `Prompts`, `Tools`, and `AI Settings` windows that require padding and width adjustments. db1f749
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Research and Layout Audit' (Protocol in workflow.md) db1f749
|
||||
|
||||
## Phase 2: Preset Windows Layout & Scaling [checkpoint: 84ec24e]
|
||||
- [x] Task: Write tests to verify window layout stability and element visibility during simulated resizes. 84ec24e
|
||||
- [x] Task: Implement improved resize/scale policies for `Personas`, `Prompts`, and `Tools` windows. 84ec24e
|
||||
- [x] Task: Apply standardized padding and adjust input box widths across these windows. 84ec24e
|
||||
- [x] Task: Implement dual-control (Slider + Input Box) for any applicable parameters in these windows. 84ec24e
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Preset Windows Layout & Scaling' (Protocol in workflow.md) 84ec24e
|
||||
|
||||
## Phase 3: AI Settings Overhaul [checkpoint: 0990270]
|
||||
- [x] Task: Write tests for AI Settings panel interactions and visual state consistency. 0990270
|
||||
- [x] Task: Refactor AI Settings panel to use visual sliders/knobs for Temperature, Top-P, and Max Tokens. 0990270
|
||||
- [x] Task: Integrate corresponding numeric input boxes for all AI setting sliders. 0990270
|
||||
- [x] Task: Improve visual clarity of preferred model entries when collapsed. 0990270
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: AI Settings Overhaul' (Protocol in workflow.md) 0990270
|
||||
|
||||
## Phase 4: Tool Management (MCP) Refinement [checkpoint: f21f22e]
|
||||
- [x] Task: Write tests for tool list rendering and category filtering. f21f22e
|
||||
- [x] Task: Update the tools section to display tool names before radio buttons with consistent spacing. f21f22e
|
||||
- [x] Task: Implement a category-based grouping/filtering system for tools (File I/O, Web, System, etc.). f21f22e
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Tool Management (MCP) Refinement' (Protocol in workflow.md) f21f22e
|
||||
|
||||
## Phase 5: Final Integration and Verification [checkpoint: e0d441c]
|
||||
- [x] Task: Perform a comprehensive UI audit across all modified windows to ensure visual consistency. e0d441c
|
||||
- [x] Task: Run all automated tests and verify no regressions in GUI performance or functionality. e0d441c
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Integration and Verification' (Protocol in workflow.md) e0d441c
|
||||
35
conductor/tracks/presets_ai_settings_ux_20260311/spec.md
Normal file
35
conductor/tracks/presets_ai_settings_ux_20260311/spec.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Specification: UI/UX Improvements - Presets and AI Settings
|
||||
|
||||
## 1. Overview
|
||||
This track aims to improve the usability and visual layout of the Preset windows (Personas, Prompts, Tools) and the AI Settings panel. Key improvements include better layout scaling, consistent input controls, and categorized tool management.
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
### 2.1 Preset Windows (Personas, Prompts, Tools)
|
||||
- **Layout Scaling:** Implement improved resize and scaling policies for sub-panels and sections within each window to ensure they adapt well to different window sizes.
|
||||
- **Section Padding:** Increase and standardize padding between UI elements for better visual separation.
|
||||
- **Input Field Width:** Adjust the width of input boxes to provide adequate space for content while maintaining a balanced layout.
|
||||
- **Dual-Control Sliders:** All sliders for model parameters (Temperature, Top-P, etc.) must have a corresponding numeric input box for direct value entry.
|
||||
|
||||
### 2.2 AI Settings Panel
|
||||
- **Visual Controls:** Implement visual sliders and knobs for key model parameters.
|
||||
- **Collapsed View Clarity:** Improve the visual representation when a preferred model entry is collapsed, ensuring key information is still visible or the transition is intuitive.
|
||||
|
||||
### 2.3 Tool Management (MCP)
|
||||
- **Layout Refinement:** In the tools section, display the tool name first, followed by radio buttons with a small, consistent gap.
|
||||
- **Categorization:** Introduce category-based filtering or grouping (e.g., File I/O, Web, System) for easier management of large toolsets.
|
||||
|
||||
## 3. Non-Functional Requirements
|
||||
- **Consistency:** UI patterns and spacing must be consistent across all modified windows.
|
||||
- **Performance:** Ensure layout recalculations and rendering remain fluid during resizing.
|
||||
|
||||
## 4. Acceptance Criteria
|
||||
- [ ] Preset windows (Personas, Prompts, Tools) have improved scaling and spacing.
|
||||
- [ ] All sliders in the modified panels have corresponding numeric input boxes.
|
||||
- [ ] Tool names are displayed before radio buttons with consistent spacing.
|
||||
- [ ] AI Settings panel features improved visual controls and collapsed states.
|
||||
- [ ] Layout remains stable and usable across various window dimensions.
|
||||
|
||||
## 5. Out of Scope
|
||||
- Major functional changes to the AI logic or tool execution.
|
||||
- Overhaul of the theme/color palette (unless required for clarity).
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track session_context_snapshots_20260311 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "session_context_snapshots_20260311",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-11T19:30:00Z",
|
||||
"updated_at": "2026-03-11T19:30:00Z",
|
||||
"description": "Session Context Snapshots & Visibility: Tying files/screenshots to active session, saving Context Presets, MMA assignment, and agent-focused session filtering."
|
||||
}
|
||||
24
conductor/tracks/session_context_snapshots_20260311/plan.md
Normal file
24
conductor/tracks/session_context_snapshots_20260311/plan.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Implementation Plan: Session Context Snapshots & Visibility
|
||||
|
||||
## Phase 1: Backend Support for Context Presets
|
||||
- [ ] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration.
|
||||
- [ ] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: GUI Integration & Persona Assignment
|
||||
- [ ] Task: Write tests for the Context Hub UI components handling preset saving and loading.
|
||||
- [ ] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets.
|
||||
- [ ] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Transparent Context Visibility
|
||||
- [ ] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state.
|
||||
- [ ] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt.
|
||||
- [ ] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Agent-Focused Session Filtering
|
||||
- [ ] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session.
|
||||
- [ ] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard.
|
||||
- [ ] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md)
|
||||
28
conductor/tracks/session_context_snapshots_20260311/spec.md
Normal file
28
conductor/tracks/session_context_snapshots_20260311/spec.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Specification: Session Context Snapshots & Visibility
|
||||
|
||||
## 1. Overview
|
||||
This track focuses on transitioning from global context management to explicit session-scoped context. It introduces transparent visibility into the exact context (system prompts, aggregated markdown, files, and screenshots) used in a session, allows saving context selections as reusable presets, and adds MMA dashboard integration for filtering session hubs by specific agents.
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
### 2.1 Context Presets & Assignment
|
||||
- **Context Snapshots:** Users can save the current selection of files and screenshots as a named "Context Preset".
|
||||
- **Preset Swapping:** Users can easily load a Context Preset into an active session.
|
||||
- **MMA Assignment:** Allow assigning specific Context Presets to individual MMA agent personas, preventing all agents from having access to all files by default.
|
||||
|
||||
### 2.2 Transparent Context Visibility
|
||||
- **No Hidden Context:** The Session Hub must expose the exact context provided to the model.
|
||||
- **Initial Payload Visibility:** The aggregated markdown content generated at the start of the discussion must be viewable in the UI.
|
||||
- **System Prompt State:** Display the fully resolved system prompt that was active for the session.
|
||||
- **Injection Timeline:** The UI must display *when* specific files or screenshots were injected or included during the progression of the discussion.
|
||||
|
||||
### 2.3 Agent-Focused Session Filtering
|
||||
- **Dashboard Integration:** Move the "Focus Agent" feature from the Operations Hub to the MMA Dashboard.
|
||||
- **Agent Context Filtering:** Add a button on any live agent's panel in the MMA Dashboard that automatically filters the other hubs (Session/Discussion) to show only information related to that specific agent's session.
|
||||
|
||||
## 3. Acceptance Criteria
|
||||
- [ ] Context selections (files/screenshots) can be saved and loaded as Presets.
|
||||
- [ ] MMA Agent Personas can be configured to use specific Context Presets.
|
||||
- [ ] The Session Hub displays the generated aggregate markdown and resolved system prompt.
|
||||
- [ ] The discussion timeline clearly shows when files/screenshots were injected.
|
||||
- [ ] The MMA Dashboard allows focusing the UI on a specific agent's session data.
|
||||
5
conductor/tracks/undo_redo_history_20260311/index.md
Normal file
5
conductor/tracks/undo_redo_history_20260311/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Track undo_redo_history_20260311 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "undo_redo_history_20260311",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-11T20:15:00Z",
|
||||
"updated_at": "2026-03-11T20:15:00Z",
|
||||
"description": "Undo/Redo history support for non-provider based user actions: text inputs, UI controls, discussion structure, and context management."
|
||||
}
|
||||
29
conductor/tracks/undo_redo_history_20260311/plan.md
Normal file
29
conductor/tracks/undo_redo_history_20260311/plan.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Implementation Plan: Undo/Redo History Support
|
||||
|
||||
This plan implements a robust undo/redo system focusing on text inputs, control states, and discussion structure.
|
||||
|
||||
## Phase 1: History Core Logic & State Management
|
||||
- [ ] Task: Design and implement a generic `HistoryManager` class to handle undo/redo stacks and state snapshots.
|
||||
- [ ] Task: Write failing tests for the `HistoryManager` core logic, including capacity limits and basic undo/redo functionality.
|
||||
- [ ] Task: Implement `HistoryManager` to pass tests, ensuring it correctly manages a fixed stack of 50-100 actions.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: History Core Logic & State Management' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Text Input & Control Undo/Redo
|
||||
- [ ] Task: Integrate `HistoryManager` with `src/gui_2.py` for system prompt and discussion entry text fields.
|
||||
- [ ] Task: Implement state snapshots for AI model parameter sliders (Temperature, Top-P) and checkboxes.
|
||||
- [ ] Task: Write simulation tests using `live_gui` to verify undo/redo for text edits and control changes.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Text Input & Control Undo/Redo' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Discussion & Context Structure Mutation
|
||||
- [ ] Task: Implement undo/redo for adding, deleting, and reordering discussion entries in `src/app_controller.py`.
|
||||
- [ ] Task: Extend the history system to track context file and screenshot additions/removals in `src/aggregate.py`.
|
||||
- [ ] Task: Write failing tests for reverting and redoing complex discussion tree mutations.
|
||||
- [ ] Task: Implement mutation tracking and restoration logic to pass tests.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Discussion & Context Structure Mutation' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: UI Features - Hotkeys & History List
|
||||
- [ ] Task: Implement global hotkey handling for `Ctrl+Z` and `Ctrl+Y` / `Ctrl+Shift+Z` in the main GUI loop.
|
||||
- [ ] Task: Create a dedicated 'History List' panel in `src/gui_2.py` showing a scrollable list of recent actions.
|
||||
- [ ] Task: Implement functionality to jump to a specific historical state via the History List.
|
||||
- [ ] Task: Write final integration tests for the full undo/redo cycle across all supported areas.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: UI Features - Hotkeys & History List' (Protocol in workflow.md)
|
||||
38
conductor/tracks/undo_redo_history_20260311/spec.md
Normal file
38
conductor/tracks/undo_redo_history_20260311/spec.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Specification: Undo/Redo History Support
|
||||
|
||||
## 1. Overview
|
||||
This track implements a robust, non-provider based Undo/Redo system within the Manual Slop GUI. It allows users to revert and redo common UI actions, focusing on text inputs, control states, and discussion structure, without impacting AI-generated content or remote state.
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
### 2.1 Supported Actions
|
||||
- **Text Inputs:** Undo/redo for system prompts, discussion entries, and any editable text boxes.
|
||||
- **UI Controls:** Revert changes to sliders (Temperature, Top-P), checkboxes, and preset selections.
|
||||
- **Discussion Structure:** Support undo/redo for deleting or inserting discussion entries.
|
||||
- **Context Management:** Undo/redo for additions and removals of context files and screenshots.
|
||||
|
||||
### 2.2 History Management
|
||||
- **Capacity:** A fixed limit of 50-100 actions in the undo stack.
|
||||
- **Scope:** History is session-specific and not persisted between application restarts.
|
||||
- **Exclusions:** Actions triggering AI vendor API requests or MMA track progression are explicitly excluded from the undo system.
|
||||
|
||||
### 2.3 User Interface
|
||||
- **Hotkeys:** Implementation of standard `Ctrl+Z` (Undo) and `Ctrl+Y` / `Ctrl+Shift+Z` (Redo) shortcuts.
|
||||
- **History List View:** A dedicated visual 'History List' panel showing recent actions, allowing users to jump back to specific points in the timeline.
|
||||
|
||||
## 3. Non-Functional Requirements
|
||||
- **Low Overhead:** The history system must have minimal impact on UI responsiveness.
|
||||
- **Thread Safety:** Ensure state snapshots and restorations are thread-safe within the GUI loop.
|
||||
|
||||
## 4. Acceptance Criteria
|
||||
- [ ] Users can undo and redo text edits in the system prompt and discussion fields.
|
||||
- [ ] UI control changes (sliders, presets) are correctly captured and restorable.
|
||||
- [ ] Discussion entry deletions/insertions can be reverted and redone.
|
||||
- [ ] Context additions/removals are tracked in the history.
|
||||
- [ ] `Ctrl+Z` and `Ctrl+Y` hotkeys work as expected.
|
||||
- [ ] The History List view accurately displays and allows jumping between states.
|
||||
|
||||
## 5. Out of Scope
|
||||
- Undo/redo for AI model generations or vendor API calls.
|
||||
- Undo/redo for MMA execution state transitions.
|
||||
- Persistent history across application restarts.
|
||||
20
config.toml
20
config.toml
@@ -2,6 +2,7 @@
|
||||
provider = "minimax"
|
||||
model = "MiniMax-M2.5"
|
||||
temperature = 0.0
|
||||
top_p = 1.0
|
||||
max_tokens = 32000
|
||||
history_trunc_limit = 900000
|
||||
active_preset = "Default"
|
||||
@@ -22,14 +23,15 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml"
|
||||
separate_message_panel = false
|
||||
separate_response_panel = false
|
||||
separate_tool_calls_panel = false
|
||||
bg_shader_enabled = true
|
||||
bg_shader_enabled = false
|
||||
crt_filter_enabled = false
|
||||
separate_task_dag = false
|
||||
separate_usage_analytics = true
|
||||
separate_usage_analytics = false
|
||||
separate_tier1 = false
|
||||
separate_tier2 = false
|
||||
separate_tier3 = false
|
||||
separate_tier4 = false
|
||||
separate_external_tools = false
|
||||
|
||||
[gui.show_windows]
|
||||
"Context Hub" = true
|
||||
@@ -54,17 +56,23 @@ Response = false
|
||||
Theme = true
|
||||
"Log Management" = true
|
||||
Diagnostics = false
|
||||
"External Tools" = false
|
||||
|
||||
[theme]
|
||||
palette = "Nord Dark"
|
||||
font_path = "C:/projects/manual_slop/assets/fonts/Inter-Regular.ttf"
|
||||
font_size = 14.0
|
||||
font_path = "fonts/Inter-Regular.ttf"
|
||||
font_size = 16.0
|
||||
scale = 1.0
|
||||
transparency = 0.5099999904632568
|
||||
child_transparency = 0.699999988079071
|
||||
transparency = 1.0
|
||||
child_transparency = 1.0
|
||||
|
||||
[mma]
|
||||
max_workers = 4
|
||||
|
||||
[headless]
|
||||
api_key = "test-secret-key"
|
||||
|
||||
[paths]
|
||||
conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor"
|
||||
logs_dir = "C:\\projects\\manual_slop\\logs"
|
||||
scripts_dir = "C:\\projects\\manual_slop\\scripts"
|
||||
|
||||
BIN
gallery/python_2026-03-11_00-37-21.png
Normal file
BIN
gallery/python_2026-03-11_00-37-21.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
@@ -54,9 +54,10 @@ Size=1111,224
|
||||
Collapsed=0
|
||||
|
||||
[Window][Tool Calls]
|
||||
Pos=694,1182
|
||||
Size=913,631
|
||||
Pos=855,1482
|
||||
Size=1014,655
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Comms History]
|
||||
ViewportPos=43,95
|
||||
@@ -73,8 +74,8 @@ Collapsed=0
|
||||
DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,1016
|
||||
Size=623,401
|
||||
Pos=0,207
|
||||
Size=499,1403
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
|
||||
@@ -84,14 +85,14 @@ Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][Diagnostics]
|
||||
Pos=2833,28
|
||||
Size=1007,2109
|
||||
Pos=2641,34
|
||||
Size=1199,2103
|
||||
Collapsed=0
|
||||
DockId=0x0000000C,2
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][Context Hub]
|
||||
Pos=0,1016
|
||||
Size=623,401
|
||||
Pos=0,207
|
||||
Size=499,1403
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
|
||||
@@ -102,26 +103,26 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1296,22
|
||||
Size=659,1395
|
||||
Pos=1172,24
|
||||
Size=703,1586
|
||||
Collapsed=0
|
||||
DockId=0x00000013,0
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=625,22
|
||||
Size=669,1395
|
||||
Pos=501,24
|
||||
Size=669,1586
|
||||
Collapsed=0
|
||||
DockId=0x00000012,0
|
||||
DockId=0x00000005,0
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,1016
|
||||
Size=623,401
|
||||
Pos=0,207
|
||||
Size=499,1403
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,22
|
||||
Size=623,992
|
||||
Pos=0,24
|
||||
Size=499,181
|
||||
Collapsed=0
|
||||
DockId=0x00000001,0
|
||||
|
||||
@@ -131,16 +132,16 @@ Size=416,325
|
||||
Collapsed=0
|
||||
|
||||
[Window][MMA Dashboard]
|
||||
Pos=1957,22
|
||||
Size=603,1395
|
||||
Pos=1877,24
|
||||
Size=1018,1586
|
||||
Collapsed=0
|
||||
DockId=0x0000000C,0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1957,22
|
||||
Size=603,1395
|
||||
Pos=1877,24
|
||||
Size=1018,1586
|
||||
Collapsed=0
|
||||
DockId=0x0000000C,1
|
||||
DockId=0x00000010,1
|
||||
|
||||
[Window][Track Proposal]
|
||||
Pos=709,326
|
||||
@@ -151,25 +152,22 @@ Collapsed=0
|
||||
Pos=2905,1238
|
||||
Size=935,899
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,0
|
||||
|
||||
[Window][Tier 2: Tech Lead]
|
||||
Pos=2905,1238
|
||||
Size=935,899
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,0
|
||||
|
||||
[Window][Tier 4: QA]
|
||||
Pos=2905,1238
|
||||
Size=935,899
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,0
|
||||
|
||||
[Window][Tier 3: Workers]
|
||||
Pos=2905,1238
|
||||
Size=935,899
|
||||
Pos=2822,1717
|
||||
Size=1018,420
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,0
|
||||
DockId=0x0000000C,0
|
||||
|
||||
[Window][Approve PowerShell Command]
|
||||
Pos=649,435
|
||||
@@ -322,28 +320,34 @@ Size=420,966
|
||||
Collapsed=0
|
||||
|
||||
[Window][Preset Manager]
|
||||
Pos=403,396
|
||||
Size=956,958
|
||||
Pos=937,444
|
||||
Size=1759,1245
|
||||
Collapsed=0
|
||||
|
||||
[Window][Task DAG]
|
||||
Pos=1700,1199
|
||||
Size=1079,662
|
||||
Pos=1398,884
|
||||
Size=967,499
|
||||
Collapsed=0
|
||||
|
||||
[Window][Usage Analytics]
|
||||
Pos=1661,426
|
||||
Size=275,375
|
||||
Pos=2822,1717
|
||||
Size=1018,420
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,0
|
||||
|
||||
[Window][Tool Preset Manager]
|
||||
Pos=192,90
|
||||
Size=1066,1324
|
||||
Pos=1301,302
|
||||
Size=1469,1267
|
||||
Collapsed=0
|
||||
|
||||
[Window][Persona Editor]
|
||||
Pos=956,447
|
||||
Size=549,447
|
||||
Pos=909,391
|
||||
Size=1886,1234
|
||||
Collapsed=0
|
||||
|
||||
[Window][Prompt Presets Manager]
|
||||
Pos=856,546
|
||||
Size=1000,800
|
||||
Collapsed=0
|
||||
|
||||
[Table][0xFB6E3870,4]
|
||||
@@ -377,11 +381,11 @@ Column 3 Width=20
|
||||
Column 4 Weight=1.0000
|
||||
|
||||
[Table][0x2A6000B6,4]
|
||||
RefScale=17
|
||||
Column 0 Width=51
|
||||
Column 1 Width=76
|
||||
RefScale=14
|
||||
Column 0 Width=42
|
||||
Column 1 Width=61
|
||||
Column 2 Weight=1.0000
|
||||
Column 3 Width=128
|
||||
Column 3 Width=105
|
||||
|
||||
[Table][0x8BCC69C7,6]
|
||||
RefScale=13
|
||||
@@ -393,18 +397,18 @@ Column 4 Weight=1.0000
|
||||
Column 5 Width=50
|
||||
|
||||
[Table][0x3751446B,4]
|
||||
RefScale=20
|
||||
Column 0 Width=60
|
||||
Column 1 Width=91
|
||||
RefScale=16
|
||||
Column 0 Width=48
|
||||
Column 1 Width=72
|
||||
Column 2 Weight=1.0000
|
||||
Column 3 Width=151
|
||||
Column 3 Width=120
|
||||
|
||||
[Table][0x2C515046,4]
|
||||
RefScale=20
|
||||
Column 0 Width=63
|
||||
RefScale=16
|
||||
Column 0 Width=48
|
||||
Column 1 Weight=1.0000
|
||||
Column 2 Width=152
|
||||
Column 3 Width=60
|
||||
Column 2 Width=119
|
||||
Column 3 Width=48
|
||||
|
||||
[Table][0xD99F45C5,4]
|
||||
Column 0 Sort=0v
|
||||
@@ -425,28 +429,57 @@ Column 1 Width=100
|
||||
Column 2 Weight=1.0000
|
||||
|
||||
[Table][0xA02D8C87,3]
|
||||
RefScale=20
|
||||
Column 0 Width=227
|
||||
Column 1 Width=150
|
||||
RefScale=24
|
||||
Column 0 Width=270
|
||||
Column 1 Width=180
|
||||
Column 2 Weight=1.0000
|
||||
|
||||
[Table][0xD0277E63,2]
|
||||
RefScale=16
|
||||
Column 0 Width=132
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Table][0x3AAF84D5,2]
|
||||
RefScale=24
|
||||
Column 0 Width=150
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Table][0x8D8494AB,2]
|
||||
RefScale=16
|
||||
Column 0 Width=132
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Table][0x2C261E6E,2]
|
||||
RefScale=16
|
||||
Column 0 Width=99
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Table][0x9CB1E6FD,2]
|
||||
RefScale=16
|
||||
Column 0 Width=187
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Docking][Data]
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,22 Size=2560,1395 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1955,1183 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,24 Size=2895,1586 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2820,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=623,858 Split=Y Selected=0x8CA2375C
|
||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,989 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,401 Selected=0x1DCB2623
|
||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1330,858 Split=X Selected=0x418C7449
|
||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=669,402 Selected=0x418C7449
|
||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=659,402 Selected=0x6F2B5B04
|
||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=499,858 Split=Y Selected=0x8CA2375C
|
||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,708 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,1403 Selected=0xF4139CA2
|
||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1374,858 Split=X Selected=0x418C7449
|
||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=669,402 Split=Y Selected=0x418C7449
|
||||
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1455 Selected=0x418C7449
|
||||
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,654 Selected=0x1D56B311
|
||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=703,402 Selected=0x6F2B5B04
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=603,1183 Split=Y Selected=0x3AEC3498
|
||||
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=1074,1208 Selected=0x2C0206CE
|
||||
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=1074,899 Selected=0x5CDB7A4B
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1018,1183 Split=Y Selected=0x3AEC3498
|
||||
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x3AEC3498
|
||||
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6
|
||||
DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9
|
||||
DockNode ID=0x0000000F Parent=0x00000011 SizeRef=281,380 Selected=0xDEB547B6
|
||||
|
||||
;;;<<<Layout_655921752_Default>>>;;;
|
||||
;;;<<<HelloImGui_Misc>>>;;;
|
||||
|
||||
@@ -2347,3 +2347,752 @@ PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['C:\\projects\\manual_slop\\tests\\mock_gemini_cli.py', '-m', 'gemini-2.5-flash-lite', '--prompt', '', '--output-format', 'stream-json']
|
||||
PROMPT:
|
||||
You are a helpful coding assistant with access to a PowerShell tool (run_powershell) and MCP tools (file access: read_file, list_directory, search_files, get_file_summary, web access: web_search, fetch_url). When calling file/directory tools, always use the 'path' parameter for the target path. When asked to create or edit files, prefer targeted edits over full rewrites. Always explain what you are doing before invoking the tool.
|
||||
|
||||
When writing or rewriting large files (especially those containing quotes, backticks, or special characters), avoid python -c with inline strings. Instead: (1) write a .py helper script to disk using a PS here-string (@'...'@ for literal content), (2) run it with `python <script>`, (3) delete the helper. For small targeted edits, use PowerShell's (Get-Content) / .Replace() / Set-Content or Add-Content directly.
|
||||
|
||||
When making function calls using tools that accept array or object parameters ensure those are structured using JSON. For example:
|
||||
When you need to verify a change, rely on the exit code and stdout/stderr from the tool — the user's context files are automatically refreshed after every tool call, so you do NOT need to re-read files that are already provided in the <context> block.
|
||||
|
||||
[USER SYSTEM PROMPT]
|
||||
|
||||
You are the Tier 1 Orchestrator (Product Manager) for the Manual Slop project.
|
||||
Your role is high-level strategic planning, architecture enforcement, and cross-module delegation.
|
||||
You operate strictly on metadata, summaries, and executive-level directives.
|
||||
NEVER request or attempt to read raw implementation code unless specifically provided in a Macro-Diff.
|
||||
Maintain a "Godot ECS Flat List format" (JSON array of objects) for structural outputs.
|
||||
|
||||
PATH: Epic Initialization (Project Planning)
|
||||
GOAL: Break down a massive feature request into discrete Implementation Tracks.
|
||||
|
||||
CONSTRAINTS:
|
||||
- IGNORE all source code, AST skeletons, and previous micro-task histories.
|
||||
- FOCUS ONLY on the Repository Map and Project Meta-State.
|
||||
|
||||
OUTPUT REQUIREMENT:
|
||||
Return a JSON array of 'Tracks'. Each track object must follow the Godot ECS Flat List format:
|
||||
[
|
||||
{
|
||||
"id": "track_unique_id",
|
||||
"type": "Track",
|
||||
"module": "target_module_name",
|
||||
"persona": "required_tech_lead_persona",
|
||||
"severity": "Low|Medium|High",
|
||||
"goal": "Descriptive goal",
|
||||
"acceptance_criteria": ["criteria_1", "criteria_2"]
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
<context>
|
||||
|
||||
</context>
|
||||
|
||||
### USER REQUEST:
|
||||
Add timestamps
|
||||
|
||||
### REPOSITORY MAP:
|
||||
|
||||
|
||||
### TRACK HISTORY:
|
||||
Track: api_hooks_verification_20260223
|
||||
Status: new
|
||||
Overview: This track focuses on integrating the existing, previously implemented API hooks (from track `test_hooks_20260223`) into the Conductor workflow. The primary goal is to automate the verification steps within the "Phase Completion Verification and Checkpointing Protocol", reducing the need for manual user intervention and enabling a more streamlined, automated development process.
|
||||
---
|
||||
Track: api_metrics_20260223
|
||||
Status: new
|
||||
Overview: This track aims to optimize token efficiency and transparency by reviewing and improving how vendor APIs (Gemini and Anthropic) handle conservative context pruning. The primary focus is on extracting, plotting, and exposing deep metrics to the GUI so developers can intuit how close they are to API limits (e.g., token caps, cache counts, history bleed).
|
||||
---
|
||||
Track: api_vendor_alignment_20260223
|
||||
Status: new
|
||||
Overview: This track involves a comprehensive audit of the "Manual Slop" codebase to ensure that the integration with Google Gemini (`google-genai`) and Anthropic Claude (`anthropic`) SDKs aligns perfectly with their latest official documentation and best practices. The goal is to identify discrepancies, performance bottlenecks, or deprecated patterns and implement the necessary fixes.
|
||||
---
|
||||
Track: architecture_boundary_hardening_20260302
|
||||
Status: new
|
||||
Overview: The `manual_slop` project sandbox provides AI meta-tooling (`mma_exec.py`, `tool_call.py`) to orchestrate its own development. When AI agents added advanced AST tools (like `set_file_slice`) to `mcp_client.py` for meta-tooling, they failed to fully integrate them into the application's GUI, config, or HITL (Human-In-The-Loop) safety models. Additionally, meta-tooling scripts are bleeding tokens and rely on non-portable hardcoded machine paths, while the internal application's state machine can deadlock.
|
||||
---
|
||||
Track: cache_analytics_20260306
|
||||
Status: planned
|
||||
Overview: Gemini cache hit/miss visualization, memory usage, TTL status display. Uses existing `ai_client.get_gemini_cache_stats()` which is implemented but has no GUI representation.
|
||||
---
|
||||
Track: codebase_migration_20260302
|
||||
Status: new
|
||||
Overview: This track focuses on restructuring the codebase to alleviate clutter by moving the main implementation files from the project root into a dedicated `src/` directory. Additionally, files that are completely unused by the current implementation will be automatically identified and removed. A new clean entry point (`sloppy.py`) will be created in the root directory.
|
||||
---
|
||||
Track: comprehensive_gui_ux_20260228
|
||||
Status: completed
|
||||
Overview: This track enhances the existing MMA orchestration GUI from its current functional-but-minimal state to a production-quality control surface. The existing implementation already has a working Track Browser, DAG tree visualizer, epic planning flow, approval dialogs, and token usage table. This track focuses on the **gaps**: dedicated tier stream panels, DAG editing, track-scoped discussions, conductor lifecycle GUI forms, cost tracking, and visual polish.
|
||||
---
|
||||
Track: conductor_workflow_improvements_20260302
|
||||
Status: new
|
||||
Overview: Recent Tier 2 track implementations have resulted in feature bleed, redundant code, unread state variables, and degradation of TDD discipline (e.g., zero-assertion tests).
|
||||
This track updates the Conductor documentation (`workflow.md`) and the Gemini skills for Tiers 2 and 3 to hard-enforce TDD, prevent hallucinated "mock" implementations, and enforce strict codebase auditing before writing code.
|
||||
---
|
||||
Track: consolidate_cruft_and_log_taxonomy_20260228
|
||||
Status: unknown
|
||||
Overview: This track focuses on cleaning up the project root by consolidating temporary and test-related files into a dedicated directory and establishing a structured taxonomy for session logs. This will improve project organization and make manual file exploration easier before a dedicated GUI log viewer is implemented.
|
||||
---
|
||||
Track: context_management_20260223
|
||||
Status: new
|
||||
Overview: This track implements UI improvements and structural changes to Manual Slop to provide explicit visualization of context memory usage and token consumption, fulfilling the "Expert systems level utility" and "Full control" product goals.
|
||||
---
|
||||
Track: context_token_viz_20260301
|
||||
Status: new
|
||||
Overview: product.md lists "Context & Memory Management" as primary use case #2: "Better visualization and management of token usage and context memory, allowing developers to optimize prompt limits manually." The backend already computes everything needed via `ai_client.get_history_bleed_stats()` (ai_client.py:1657-1796, 140 lines). This track builds the UI to expose it.
|
||||
---
|
||||
Track: cost_token_analytics_20260306
|
||||
Status: planned
|
||||
Overview: # Implementation Plan: Cost & Token Analytics Panel (cost_token_analytics_20260306)
|
||||
|
||||
> **Reference:** [Spec](./spec.md) | [Architecture Guide](../../../docs/guide_architecture.md)
|
||||
|
||||
## Phase 1: Foundat...
|
||||
---
|
||||
Track: deepseek_support_20260225
|
||||
Status: new
|
||||
Overview: Implement a new AI provider module to support the DeepSeek API within the Manual Slop application. This integration will leverage a dedicated SDK to provide access to high-performance models (DeepSeek-V3 and DeepSeek-R1) with support for streaming, tool calling, and detailed reasoning traces.
|
||||
---
|
||||
Track: deep_ast_context_pruning_20260306
|
||||
Status: planned
|
||||
Overview: Use tree_sitter to parse target file AST and inject condensed skeletons into worker prompts. Currently workers receive full file context; this track reduces token burn by injecting only relevant function/method signatures.
|
||||
---
|
||||
Track: documentation_refresh_20260224
|
||||
Status: new
|
||||
Overview: This track implements a high-density, expert-level documentation suite for the Manual Slop project. The documentation style is strictly modeled after the **pedagogical and narrative standards** of `gencpp` and `VEFontCache-Odin`. It moves beyond simple "User Guides" to provide a **"USA Graphics Company"** architectural reference: high information density, tactical technical transparency, and a narrative intent that guides a developer from high-level philosophy to low-level implementation.
|
||||
---
|
||||
Track: enhanced_context_control_20260307
|
||||
Status: planned
|
||||
Overview: Give developers granular control over how files are included in the AI context and provide visibility into the active Gemini cache state. This involves moving away from a simple list of files to a structured format with per-file flags (`auto_aggregate`, `force_full`), revamping the UI to display this state, and updating the context builders and API clients to respect and expose these details.
|
||||
---
|
||||
Track: event_driven_metrics_20260223
|
||||
Status: new
|
||||
Overview: Refactor the API metrics update mechanism to be event-driven. Currently, the UI likely polls or recalculates metrics on every frame. This track will implement a signal/event system where `ai_client.py` broadcasts updates only when significant API activities (requests, responses, tool calls, or stream chunks) occur.
|
||||
---
|
||||
Track: feature_bleed_cleanup_20260302
|
||||
Status: new
|
||||
Overview: Multiple tracks added code to `gui_2.py` without removing the old versions, leaving
|
||||
dead duplicate methods, conflicting menu bar designs, and redundant state initializations.
|
||||
This track removes confirmed dead code, resolves the two-menubar conflict, and cleans
|
||||
up the token budget layout regression — restoring a consistent, non-contradictory design state.
|
||||
---
|
||||
Track: gemini_cli_headless_20260224
|
||||
Status: new
|
||||
Overview: This track integrates the `gemini` CLI as a headless backend provider for Manual Slop. This allows users to leverage their Gemini subscription and the CLI's advanced features (e.g., specialized sub-agents like `codebase_investigator`, structured JSON streaming, and robust session management) directly within the Manual Slop GUI.
|
||||
---
|
||||
Track: gemini_cli_parity_20260225
|
||||
Status: new
|
||||
Overview: Achieve full functional and behavioral parity between the Gemini CLI integration (`gemini_cli_adapter.py`, `cli_tool_bridge.py`) and the direct Gemini API implementation (`ai_client.py`). This ensures that users leveraging the Gemini CLI as a headless backend provider experience the same level of capability, reliability, and observability as direct API users.
|
||||
---
|
||||
Track: gui2_feature_parity_20260223
|
||||
Status: new
|
||||
Overview: # Specification: GUIv2 Feature Parity
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This track aims to bring `gui_2.py` (the `imgui-bundle` based UI) to feature parity with the existing `gui.py` (the `dearpygui` based UI). This i...
|
||||
---
|
||||
Track: gui2_parity_20260224
|
||||
Status: new
|
||||
Overview: The project is transitioning from `gui.py` (Dear PyGui-based) to `gui_2.py` (ImGui Bundle-based) to leverage advanced multi-viewport and docking features not natively supported by Dear PyGui. This track focuses on achieving full visual, functional, and performance parity between the two implementations, ultimately enabling the decommissioning of the original `gui.py`.
|
||||
---
|
||||
Track: gui_decoupling_controller_20260302
|
||||
Status: new
|
||||
Overview: `gui_2.py` currently operates as a Monolithic God Object (3,500+ lines). It violates the Data-Oriented Design heuristic by owning complex business logic, orchestrator hooks, and markdown file building. This track extracts the core state machine and lifecycle into a headless `app_controller.py`, turning the GUI into a pure immediate-mode view.
|
||||
---
|
||||
Track: gui_layout_refinement_20260223
|
||||
Status: new
|
||||
Overview: This track focuses on a holistic review and reorganization of the Manual Slop GUI. The goal is to ensure that AI tunings, diagnostic features, context management, and discussion history are logically placed to support an expert-level "Multi-Viewport" workflow. We will strengthen the "Arcade Aesthetics" and "Tactile Density" values while ensuring the layout remains intuitive for power users.
|
||||
---
|
||||
Track: gui_performance_20260223
|
||||
Status: new
|
||||
Overview: This track focuses on identifying and resolving severe frametime performance issues in the Manual Slop GUI. Current observations indicate massive frametime bloat even on idle startup, with performance significantly regressing (target 60 FPS / <16.6ms) since commit `8aa70e287fbf93e669276f9757965d5a56e89b10`.
|
||||
---
|
||||
Track: gui_performance_profiling_20260307
|
||||
Status: unknown
|
||||
Overview: Implement fine-grained performance profiling within the main ImGui rendering loop (`gui_2.py`) to ensure adherence to data-oriented and immediate mode heuristics. This track will provide visual diagnostics for high-overhead UI components, allowing developers to monitor and optimize render frame times.
|
||||
---
|
||||
Track: gui_sim_extension_20260224
|
||||
Status: new
|
||||
Overview: This track aims to expand the test simulation suite by introducing comprehensive, in-breadth tests that cover all facets of the GUI interaction. The original small test simulation will be preserved as a useful baseline. The new extended tests will be structured as multiple focused, modular scripts rather than a single long-running journey, ensuring maintainability and targeted coverage.
|
||||
---
|
||||
Track: history_segregation_20260224
|
||||
Status: new
|
||||
Overview: Currently, `manual_slop.toml` stores both project configuration and the entire discussion history. This leads to redundancy and potential context bloat if the AI agent reads the raw TOML file via its tools. This track will move the discussion history to a dedicated sibling TOML file (`history.toml`) and strictly blacklist it from the AI agent's file tools to ensure it only interacts with the curated context provided in the prompt.
|
||||
---
|
||||
Track: kill_abort_workers_20260306
|
||||
Status: planned
|
||||
Overview: Add ability to kill/abort a running Tier 3 worker mid-execution. Currently workers run to completion; add cancel button with forced termination option.
|
||||
---
|
||||
Track: live_gui_testing_20260223
|
||||
Status: new
|
||||
Overview: Update the testing suite to ensure all tests (especially GUI-related and integration tests) communicate with a live running instance of `gui.py` started with the `--enable-test-hooks` argument. This ensures that tests can verify the actual application state and metrics via the built-in API hooks.
|
||||
---
|
||||
Track: live_ux_test_20260223
|
||||
Status: new
|
||||
Overview: This track implements a robust, "human-like" interaction test suite for Manual Slop. The suite will simulate a real user's workflow—from project creation to complex AI discussions and history management—using the application's API hooks. It aims to verify the "Integrated Workspace" functionality, tool execution, and history persistence without requiring manual human input, while remaining slow enough for visual audit.
|
||||
---
|
||||
Track: logging_refactor_20260226
|
||||
Status: new
|
||||
Overview: Currently, `gui_2.py` and the test suites generate a large number of log files in a flat `logs/` directory. These logs accumulate quickly, especially during incremental development and testing. This track aims to organize logs into session-based sub-directories and implement a heuristic-based pruning system to keep the log directory clean while preserving valuable sessions.
|
||||
---
|
||||
Track: manual_block_control_20260306
|
||||
Status: planned
|
||||
Overview: Allow user to manually block or unblock tickets with custom reasons. Currently blocked tickets rely solely on dependency resolution; add manual override capability.
|
||||
---
|
||||
Track: manual_skeleton_injection_20260306
|
||||
Status: planned
|
||||
Overview: Add UI controls to manually inject file skeletons into discussions. Allow user to preview skeleton content before sending to AI, with option to toggle between skeleton and full file.
|
||||
---
|
||||
Track: manual_slop_headless_20260225
|
||||
Status: new
|
||||
Overview: Transform Manual Slop into a decoupled, container-friendly backend service. This track enables the core AI orchestration and tool execution logic to run without a GUI, exposing its capabilities via a secured REST API optimized for an Unraid Docker environment.
|
||||
---
|
||||
Track: minimax_provider_20260306
|
||||
Status: unknown
|
||||
Overview: Add MiniMax as a new AI provider to Manual Slop. MiniMax provides high-performance text generation models (M2.5, M2.1, M2) with massive context windows (200k+ tokens) and competitive pricing.
|
||||
---
|
||||
Track: mma_agent_focus_ux_20260302
|
||||
Status: new
|
||||
Overview: All MMA observability panels (comms history, tool calls, discussion) display
|
||||
global/session-scoped data. When 4 tiers are running concurrently, their traffic
|
||||
is indistinguishable. This track adds a `source_tier` field to every comms and
|
||||
tool log entry at the point of emission, then adds a "Focus Agent" selector that
|
||||
filters the Operations Hub panels to show only one tier's traffic at a time.
|
||||
|
||||
**Depends on:** `feature_bleed_cleanup_20260302` (Phase 1 removes the dead comms
|
||||
panel duplicate; this track extends the live panel at gui_2.py:~3400).
|
||||
---
|
||||
Track: MMA Core Engine Implementation
|
||||
Status: planning
|
||||
Overview: # Specification: MMA Core Engine Implementation
|
||||
|
||||
## 1. Overview
|
||||
This track consolidates the implementation of the 4-Tier Hierarchical Multi-Model Architecture into the `manual_slop` codebase. The arch...
|
||||
---
|
||||
Track: MMA Dashboard Visualization Overhaul
|
||||
Status: planned
|
||||
Overview: Make the invisible backend operations visible and interactive. The current GUI is too barebones to effectively manage a multi-agent system. This track overhauls the MMA Dashboard to provide real-time insights into tracks, task dependencies, and agent streams.
|
||||
---
|
||||
Track: MMA Data Architecture & DAG Engine
|
||||
Status: planned
|
||||
Overview: Restructure how `manual_slop` stores and executes work. The current implementation relies on global state and linear execution, which does not support the complexity of multi-agent, task-based workflows. This track establishes a robust, data-oriented foundation using track-scoped state and a native Python Directed Acyclic Graph (DAG) engine.
|
||||
---
|
||||
Track: mma_formalization_20260225
|
||||
Status: new
|
||||
Overview: This track aims to formalize and automate the 4-Tier Hierarchical Multi-Model Architecture (MMA) within the Conductor framework. It introduces specialized skills for each tier and a new specialized CLI tool (`mma-exec`) to handle role-specific context gathering and "Context Amnesia" protocols.
|
||||
---
|
||||
Track: mma_implementation_20260224
|
||||
Status: new
|
||||
Overview: # Specification: 4-Tier Architecture Implementation & Conductor Self-Improvement
|
||||
|
||||
## 1. Overview
|
||||
This track encompasses two major phases. Phase 1 focuses on designing a comprehensive, step-by-step imp...
|
||||
---
|
||||
Track: mma_multiworker_viz_20260306
|
||||
Status: planned
|
||||
Overview: Split-view GUI for parallel worker streams per tier. Visualize multiple concurrent workers with individual status, output tabs, and resource usage. Enable kill/restart per worker.
|
||||
---
|
||||
Track: MMA Orchestrator Integration
|
||||
Status: in-progress
|
||||
Overview: Implement the full hierarchical orchestration loop, connecting Tier 1 (PM) strategic planning with Tier 2 (Tech Lead) tactical ticket generation. This track will enable the GUI to autonomously break down high-level user 'Epics' into actionable tracks and tickets, and manage their execution through the multi-agent system.
|
||||
---
|
||||
Track: mma_pipeline_fix_20260301
|
||||
Status: new
|
||||
Overview: The MMA pipeline has a verified code path from `run_worker_lifecycle` → `_queue_put("response", ...)` → `_process_event_queue` → `_pending_gui_tasks("handle_ai_response")` → `mma_streams[stream_id] = text`. However, the robust_live_simulation track's session compression (2026-02-28) documented that Tier 3 worker output never appears in `mma_streams` during actual GUI operation. The simulation only ever sees `'Tier 1'` in `mma_streams` keys.
|
||||
|
||||
This track diagnoses and fixes the pipeline break, then verifies end-to-end that worker output flows from `ai_client.send()` through to the GUI's `mma_streams` dict.
|
||||
---
|
||||
Track: mma_utilization_refinement_20260226
|
||||
Status: new
|
||||
Overview: Refine the Multi-Model Architecture (MMA) implementation within the Conductor framework to ensure clear role segregation, proper tool permissions, and improved observability for sub-agents.
|
||||
---
|
||||
Track: mma_verification_20260225
|
||||
Status: new
|
||||
Overview: This track aims to review and verify the implementation of the 4-Tier Hierarchical Multi-Model Architecture (MMA) within the Conductor framework. It will confirm that Conductor operates as a Tier 2 Tech Lead/Orchestrator and can successfully delegate tasks to Tier 3 (Workers) and Tier 4 (QA/Utility) sub-agents. A key part of this track is investigating whether this hierarchy should be enforced via a single centralized skill or through separate role-based sub-agent definitions.
|
||||
---
|
||||
Track: mma_verification_mock
|
||||
Status: new
|
||||
Overview: This is a mock track designed to verify the full Tier 2 -> Tier 3 -> Tier 4 delegation flow within the Conductor framework.
|
||||
---
|
||||
Track: native_orchestrator_20260306
|
||||
Status: planned
|
||||
Overview: Absorb `mma_exec.py` functionality into core application. Manual Slop natively reads/writes plan.md, manages metadata.json, and orchestrates MMA tiers in pure Python without external CLI subprocess calls.
|
||||
---
|
||||
Track: nerv_ui_theme_20260309
|
||||
Status: unknown
|
||||
Overview: This track aims to implement a new "NERV" visual theme for the manual_slop application, inspired by the aesthetic of technical/military consoles (e.g., Evangelion's NERV UI). The theme will be added as a selectable option within the application, allowing users to switch between the existing theme and the new NERV style without altering the core user experience or layout.
|
||||
---
|
||||
Track: on_demand_def_lookup_20260306
|
||||
Status: planned
|
||||
Overview: Add ability for agent to request specific class/function definitions during discussion. Parse @symbol syntax to trigger lookup and display inline in the discussion.
|
||||
---
|
||||
Track: OpenCode Configuration Overhaul
|
||||
Status: completed
|
||||
Overview: Fix critical gaps in OpenCode agent configuration that cause MMA workflow failures. Remove step limits that prematurely terminate complex tracks, disable automatic context compaction that loses critical session state, raise temperature for better problem-solving, and expand thin command wrappers into full protocol documentation.
|
||||
---
|
||||
Track: per_ticket_model_20260306
|
||||
Status: planned
|
||||
Overview: Allow user to manually select which model to use for a specific ticket, overriding the default tier model. Useful for forcing smarter model on hard tickets.
|
||||
---
|
||||
Track: pipeline_pause_resume_20260306
|
||||
Status: planned
|
||||
Overview: Add global pause/resume for entire DAG execution pipeline. Allow user to freeze all worker activity and resume later without losing state.
|
||||
---
|
||||
Track: python_style_refactor_20260227
|
||||
Status: unknown
|
||||
Overview: # Specification: AI-Optimized Python Style Refactor
|
||||
|
||||
## 1. Overview
|
||||
Refactor the Python codebase to a "Single-Space, Ultra-Compact" style specifically designed to minimize token consumption for AI age...
|
||||
---
|
||||
Track: Robust Live Simulation Verification
|
||||
Status: planned
|
||||
Overview: Establish a robust, visual simulation framework to prevent regressions in the complex GUI and asynchronous orchestration layers. This track replaces manual human verification with an automated script that clicks through the GUI and verifies the rendered state.
|
||||
---
|
||||
Track: session_insights_20260306
|
||||
Status: planned
|
||||
Overview: Token usage over time, cost projections, session summary with efficiency scores. Visualize session_logger data.
|
||||
---
|
||||
Track: simulation_hardening_20260301
|
||||
Status: new
|
||||
Overview: The `robust_live_simulation_verification` track is marked complete but its session compression documents three unresolved issues: (1) brittle mock that triggers the wrong approval popup, (2) popup state desynchronization after "Accept" clicks, (3) Tier 3 output never appearing in `mma_streams` (fixed by `mma_pipeline_fix` track). This track stabilizes the simulation framework so it reliably passes end-to-end.
|
||||
---
|
||||
Track: strict_execution_queue_completed_20260306
|
||||
Status: completed
|
||||
Overview: No overview available.
|
||||
---
|
||||
Track: strict_static_analysis_and_typing_20260302
|
||||
Status: new
|
||||
Overview: The codebase currently suffers from massive type-safety debt (512+ `mypy` errors across 64 files) and lingering `ruff` violations. This track will harden the foundation by resolving all violations, enforcing strict typing (especially in `gui_2.py` and `api_hook_client.py`), and integrating pre-commit checks. This is a prerequisite for safe AI-driven refactoring.
|
||||
---
|
||||
Track: tech_debt_and_test_cleanup_20260302
|
||||
Status: new
|
||||
Overview: Due to rapid iterative development and feature bleed across multiple Tier 2-led tracks, significant tech debt has accumulated in both the testing suite and `gui_2.py`.
|
||||
This track will clean up test fixtures, enforce test assertion integrity, and remove dead codebase remnants.
|
||||
---
|
||||
Track: test_architecture_integrity_audit_20260304
|
||||
Status: unknown
|
||||
Overview: Comprehensive audit of testing infrastructure and simulation framework to identify false positive risks, coverage gaps, and simulation fidelity issues. This analysis was triggered by a request to review how tests and simulations are setup, whether tests can report passing grades when they fail, and if simulations are rigorous enough or are just rough emulators.
|
||||
---
|
||||
Track: test_curation_20260225
|
||||
Status: new
|
||||
Overview: The current test suite for **Manual Slop** and the **Conductor** framework has grown incrementally and lacks a formal organization. This track aims to curate, categorize, and organize existing tests, specifically blacklisting Conductor-specific (MMA) tests from manual_slop's test runs. We will use a central manifest for test management and perform an exhaustive review of all tests to eliminate redundancy.
|
||||
---
|
||||
Track: test_hooks_20260223
|
||||
Status: new
|
||||
Overview: This track introduces a comprehensive suite of API hooks designed specifically for the Gemini CLI and the Conductor framework. These hooks will allow automated agents to manipulate and test the internal state of the application without requiring manual GUI interaction, enabling automated test-driven development and track progression validation.
|
||||
---
|
||||
Track: Test Integrity Audit & Intent Documentation
|
||||
Status: in_progress
|
||||
Overview: Audit and fix tests that have been "simplified" or "dumbed down" by AI agents, restoring their original verification intent through explicit documentation comments. This track addresses the growing problem of AI agents "completing" tasks by weakening test assertions rather than implementing proper functionality.
|
||||
---
|
||||
Track: test_regression_verification_20260307
|
||||
Status: unknown
|
||||
Overview: Verify that all existing tests pass with 0 regressions after recent track implementations (Kill/Abort, Block/Unblock, Pause/Resume, Per-Ticket Model Override).
|
||||
---
|
||||
Track: test_stabilization_20260302
|
||||
Status: new
|
||||
Overview: The goal of this track is to stabilize and unify the project's test suite. This involves resolving pervasive `asyncio` lifecycle errors, consolidating redundant testing paradigms (specifically manual GUI subprocesses), ensuring artifact isolation in `./tests/artifacts/`, implementing functional assertions for currently mocked-out tests, and updating documentation to reflect the finalized verification framework.
|
||||
---
|
||||
Track: ticket_queue_mgmt_20260306
|
||||
Status: planned
|
||||
Overview: Allow user to manually reorder, prioritize, or requeue tickets in the DAG. Add drag-drop reordering, priority tags, and bulk selection for execute/skip/block operations.
|
||||
---
|
||||
Track: tier4_auto_patching_20260306
|
||||
Status: planned
|
||||
Overview: Elevate Tier 4 from log summarizer to auto-patcher. When verification tests fail, Tier 4 generates a unified diff patch. GUI displays side-by-side diff; user clicks Apply Patch to resume pipeline.
|
||||
---
|
||||
Track: Tiered Context Scoping & HITL Approval
|
||||
Status: planned
|
||||
Overview: Provide the user with absolute visual control over what the AI sees at every level of the hierarchy. Currently, the system builds a single massive context blob. This track introduces context subsetting based on the target tier and implements a Human-in-the-Loop (HITL) approval gate before any Tier 3/4 worker is spawned.
|
||||
---
|
||||
Track: tool_usage_analytics_20260306
|
||||
Status: planned
|
||||
Overview: Analytics panel showing most-used tools, average execution time, and failure rates. Uses existing tool execution data from ai_client.
|
||||
---
|
||||
Track: track_progress_viz_20260306
|
||||
Status: planned
|
||||
Overview: Progress bars and percentage completion for active tracks and tickets. Better visualization of DAG execution state.
|
||||
---
|
||||
Track: true_parallel_worker_execution_20260306
|
||||
Status: planned
|
||||
Overview: Add worker pool management and configurable concurrency limits to the DAG engine. Currently workers execute in parallel per tick but with no limits or tracking; this track adds max_workers configuration, worker tracking, and proper pool management.
|
||||
---
|
||||
Track: ui_performance_20260223
|
||||
Status: new
|
||||
Overview: This track aims to resolve subpar UI performance (currently perceived below 60 FPS) by implementing a robust performance monitoring system. This system will collect high-resolution telemetry (Frame Time, Input Lag, Thread Usage) and expose it to both the user (via a Diagnostics Panel) and the AI (via API hooks). This ensures that performance degradation is caught early during development and testing.
|
||||
---
|
||||
Track: visual_dag_ticket_editing_20260306
|
||||
Status: planned
|
||||
Overview: Replace linear ticket list with interactive node graph using ImGui Bundle node editor. Users can visually drag dependency lines, split nodes, or delete tasks before execution.
|
||||
---
|
||||
Track: agent_personas_20260309
|
||||
Status: new
|
||||
Overview: Transition the application from fragmented prompt and model settings to a **Unified Persona** model. A Persona consolidates Provider, Model (or a preferred set of models), Parameters (Temp, Top-P, etc.), Prompts (Global, Project, and MMA-specific components), and links to Tool Presets into a single, versionable entity.
|
||||
---
|
||||
Track: beads_mode_20260309
|
||||
Status: new
|
||||
Overview: Introduce "Beads Mode" as a first-class, project-specific alternative to the current markdown-based implementation tracking (Native Mode). By integrating with [Beads](https://github.com/steveyegge/beads), Manual Slop will gain a distributed, git-backed graph issue tracker that allows Implementation Tracks and Tickets to be versioned alongside the codebase using Dolt.
|
||||
---
|
||||
Track: caching_optimization_20260308
|
||||
Status: new
|
||||
Overview: This track aims to verify and optimize the caching strategies across all supported AI providers (Gemini, Anthropic, DeepSeek, MiniMax, and OpenAI). The goal is to minimize token consumption and latency by ensuring that static and recurring context (system prompts, tool definitions, project documents, and conversation history) are effectively cached using each provider's specific mechanisms.
|
||||
---
|
||||
Track: codebase_audit_20260308
|
||||
Status: new
|
||||
Overview: The objective of this track is to audit the `./src` and `./simulation` directories to improve human readability and maintainability. The codebase has matured, and it is necessary to identify and address redundant code paths and state tracking, add missing docstrings to critical paths, and organize declarations/definitions within files.
|
||||
---
|
||||
Track: conductor_path_configurable_20260306
|
||||
Status: unknown
|
||||
Overview: Eliminate all hardcoded paths in the application. Make directory paths configurable via `config.toml` or environment variables, allowing the running app to use different directories from development setup. This is **Phase 0 - Critical Infrastructure** that must be completed before other Phase 3 tracks.
|
||||
---
|
||||
Track: csharp_language_support_tools_20260310
|
||||
Status: new
|
||||
Overview: Expand the Conductor's AI context-gathering and surgical editing capabilities by introducing full Tree-Sitter parsing support for C#. This brings C# to feature-parity with existing Python MCP tools, enabling deep AST-driven structural mapping, documentation extraction, and precise code modification, specifically targeting Unreal Engine build scripts, and Unity/Godot scripting.
|
||||
---
|
||||
Track: custom_shaders_20260309
|
||||
Status: new
|
||||
Overview: Implement proper custom shader support for post-process rendering and backgrounds within the Manual Slop GUI (using Dear PyGui/imgui-bundle). Additionally, investigate and implement a method to overload or replace the default OS window frame to ensure it matches the application's theme.
|
||||
---
|
||||
Track: discussion_takes_branching_20260311
|
||||
Status: new
|
||||
Overview: # Specification: Discussion Takes & Timeline Branching
|
||||
|
||||
## 1. Overview
|
||||
This track introduces non-linear discussion timelines, allowing users to create multiple "takes" (branches) from a shared point i...
|
||||
---
|
||||
Track: external_editor_integration_20260308
|
||||
Status: new
|
||||
Overview: This feature adds the ability to open files modified by AI agents in external text editors (such as VSCode or 10xNotepad) directly from the tool approval popup. This allows users to leverage their preferred editor's native diffing and editing capabilities before confirming an agent's changes.
|
||||
---
|
||||
Track: external_mcp_support_20260308
|
||||
Status: new
|
||||
Overview: This feature adds support for integrating external Model Context Protocol (MCP) servers into Manual Slop. This allows agents to utilize tools from a wide ecosystem of MCP servers (like those for databases, APIs, or specialized utilities) alongside the application's native tools.
|
||||
---
|
||||
Track: gdscript_godot_script_language_support_tools_20260310
|
||||
Status: new
|
||||
Overview: Expand the Conductor's AI context-gathering and surgical editing capabilities by introducing full Tree-Sitter parsing support for GDScript (Godot's native scripting language). This will bring GDScript to feature-parity with the existing Python MCP tools, enabling deep AST-driven structural mapping, documentation extraction, and precise code modification.
|
||||
---
|
||||
Track: Bootstrap gencpp Python Bindings Project
|
||||
Status: pending
|
||||
Overview: Create a new standalone Python project to build CFFI bindings for gencpp (C/C++ staged metaprogramming library). This will eventually provide richer C++ AST understanding than tree-sitter (full type information, operators, specifiers) but is a longer-term effort. This track bootstraps the project structure and initial bindings.
|
||||
---
|
||||
Track: GUI Path Configuration in Context Hub
|
||||
Status: pending
|
||||
Overview: Add path configuration UI to the Context Hub in the GUI. Allow users to view and edit configurable paths (conductor, logs, scripts) directly from the application without manually editing config.toml or environment variables.
|
||||
---
|
||||
Track: hook_api_expansion_20260308
|
||||
Status: new
|
||||
Overview: This track aims to transform the Manual Slop Hook API into a comprehensive control plane for headless use. It focuses on exposing all relevant internal states (worker traces, AST metadata, financial metrics, concurrency telemetry) and providing granular control over the application's lifecycle, MMA pipeline, and context management. Additionally, it introduces a WebSocket-based streaming interface for real-time event delivery.
|
||||
---
|
||||
Track: log_session_overhaul_20260308
|
||||
Status: new
|
||||
Overview: This track focuses on centralizing log management, improving the reliability and scope of session restoration, and optimizing log storage by offloading large data blobs (scripts and tool outputs) to external files. It also aims to "clean" the discussion history by moving transient system warnings to a dedicated diagnostic log.
|
||||
---
|
||||
Track: manual_ux_validation_20260302
|
||||
Status: new
|
||||
Overview: This track is an unusual, highly interactive human-in-the-loop review session. The user will act as the primary QA and Designer, manually using the GUI and observing it during slow-interval simulation runs. The goal is to aggressively iterate on the "feel" of the application: analyzing blinking animations, structural decisions (Tabs vs. Panels vs. Collapsing Headers), knob/control placements, and the efficacy of popups (including adding auto-close timers).
|
||||
---
|
||||
Track: markdown_highlighting_20260308
|
||||
Status: new
|
||||
Overview: This track introduces rich text rendering to the Manual Slop GUI by adding support for GitHub-Flavored Markdown (GFM) in message and response views. It also adds syntax highlighting for code blocks and text content when the language context can be cheaply resolved (e.g., via known metadata or file extensions).
|
||||
---
|
||||
Track: openai_integration_20260308
|
||||
Status: new
|
||||
Overview: This track introduces support for OpenAI as a first-class model provider. It involves implementing a dedicated client in `src/ai_client.py`, updating configuration models, enhancing the GUI for provider selection, and integrating OpenAI into the tiered MMA architecture.
|
||||
---
|
||||
Track: presets_ai_settings_ux_20260311
|
||||
Status: new
|
||||
Overview: # Specification: UI/UX Improvements - Presets and AI Settings
|
||||
|
||||
## 1. Overview
|
||||
This track aims to improve the usability and visual layout of the Preset windows (Personas, Prompts, Tools) and the AI Set...
|
||||
---
|
||||
Track: Project-Specific Conductor Directory
|
||||
Status: pending
|
||||
Overview: Make the conductor directory per-project instead of global. Each project TOML can specify its own `conductor_dir` path, allowing separate track/state management per project. This enables using Manual Slop with multiple independent projects without track/ticket cross-pollution.
|
||||
---
|
||||
Track: rag_support_20260308
|
||||
Status: new
|
||||
Overview: This track introduces Retrieval-Augmented Generation (RAG) capabilities to Manual Slop. It allows agents to search and retrieve relevant information from large local codebases, project documentation, or external knowledge bases, overcoming context window limitations and reducing hallucination.
|
||||
---
|
||||
Track: saved_presets_20260308
|
||||
Status: new
|
||||
Overview: This feature introduces the ability to save, manage, and switch between system prompt presets for both global (application-wide) and project-specific contexts. Presets will include not only the system prompt text but also model-specific parameters like temperature and top_p, effectively allowing for "AI Profiles."
|
||||
---
|
||||
Track: saved_tool_presets_20260308
|
||||
Status: new
|
||||
Overview: This feature adds the ability to create, save, and manage "Tool Presets" for agent roles. These presets define which tools are available to an agent and their respective "auto" vs "ask" approval levels. Tools will be organized into dynamic, TOML-defined categories (e.g., Python, General) and integrated into the global and project-specific AI settings.
|
||||
---
|
||||
Track: selectable_ui_text_20260308
|
||||
Status: new
|
||||
Overview: This track aims to address UI inconveniences by making critical text across the GUI selectable and copyable. This includes discussion history, communication logs, tool outputs, and key metrics. The goal is to provide a standard "Copy to Clipboard" capability via OS-native selection and shortcuts (Ctrl+C).
|
||||
---
|
||||
Track: session_context_snapshots_20260311
|
||||
Status: new
|
||||
Overview: # Specification: Session Context Snapshots & Visibility
|
||||
|
||||
## 1. Overview
|
||||
This track focuses on transitioning from global context management to explicit session-scoped context. It introduces transparent...
|
||||
---
|
||||
Track: test_coverage_expansion_20260309
|
||||
Status: new
|
||||
Overview: Add more unit, simulation, and integration tests to increase coverage and stress test the application. The primary focus will be on critical and complex paths rather than aggressive total coverage percentage.
|
||||
---
|
||||
Track: test_harness_hardening_20260310
|
||||
Status: new
|
||||
Overview: This track focuses on stabilizing the local development and testing environment by hardening the Hook API and its associated `live_gui` test harness. The goal is to eliminate port conflicts, ensure graceful teardowns, and standardize state serialization, laying the groundwork for a future WebSockets implementation.
|
||||
---
|
||||
Track: tool_bias_tuning_20260308
|
||||
Status: new
|
||||
Overview: This track introduces a mechanism to influence AI agent tool selection by implementing a weighting and scoring system at the orchestration layer. Since model APIs do not natively support tool priority, this feature uses semantic nudging (tags in tool descriptions) and explicit system instructions to "bias" the agent toward preferred tools and parameters.
|
||||
---
|
||||
Track: tree_sitter_lua_mcp_tools_20260310
|
||||
Status: new
|
||||
Overview: Expand the Conductor's AI context-gathering and surgical editing capabilities by introducing full Tree-Sitter parsing support for the Lua programming language. This will bring Lua to feature-parity with the existing Python MCP tools, enabling deep AST-driven structural mapping, documentation extraction, and precise code modification.
|
||||
---
|
||||
Track: Tree-Sitter C/C++ MCP Tools
|
||||
Status: pending
|
||||
Overview: Add tree-sitter-based C and C++ parsing support to the MCP client, providing skeleton and outline tools for C/C++ codebases. Tools will be prefixed `ts_c_` and `ts_cpp_` to distinguish from existing Python tools and leave namespace open for future gencpp integration.
|
||||
---
|
||||
Track: ui_theme_overhaul_20260308
|
||||
Status: new
|
||||
Overview: This track aims to modernize the application's appearance by implementing a professional, high-fidelity UI theme using `imgui-bundle`'s native styling and theming capabilities. It focuses on improving typography, visual shapes, and introducing advanced features like custom shaders, multi-viewport support, and user-managed layout presets.
|
||||
---
|
||||
Track: undo_redo_history_20260311
|
||||
Status: new
|
||||
Overview: # Specification: Undo/Redo History Support
|
||||
|
||||
## 1. Overview
|
||||
This track implements a robust, non-provider based Undo/Redo system within the Manual Slop GUI. It allows users to revert and redo common UI ...
|
||||
---
|
||||
Track: ux_sim_test_20260308
|
||||
Status: unknown
|
||||
Overview: No overview available.
|
||||
---
|
||||
Track: ux_sim_test_20260310
|
||||
Status: unknown
|
||||
Overview: No overview available.
|
||||
---
|
||||
Track: workspace_profiles_20260310
|
||||
Status: new
|
||||
Overview: Expand the existing GUI window management to support named "Workspace Profiles." This will allow users to save, manage, and instantly switch between complex multi-window, multi-viewport docking arrangements.
|
||||
---
|
||||
Track: zhipu_integration_20260308
|
||||
Status: new
|
||||
Overview: This track introduces support for Zhipu AI (z.ai) as a first-class model provider. It involves implementing a dedicated client in `src/ai_client.py` for the GLM series of models, updating configuration models, enhancing the GUI for provider selection, and integrating the provider into the tiered MMA architecture.
|
||||
---
|
||||
|
||||
Please generate the implementation tracks for this request.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
PATH: Epic Initialization — please produce tracks
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please generate the implementation tickets for this track.
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
Please read test.txt
|
||||
You are assigned to Ticket T1.
|
||||
Task Description: do something
|
||||
------------------
|
||||
--- MOCK INVOKED ---
|
||||
ARGS: ['tests/mock_gemini_cli.py']
|
||||
PROMPT:
|
||||
role: tool
|
||||
Here are the results: {"content": "done"}
|
||||
------------------
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
[personas.Default]
|
||||
system_prompt = ""
|
||||
provider = "minimax"
|
||||
tool_preset = "Default"
|
||||
bias_profile = "Balanced"
|
||||
|
||||
[[personas.Default.preferred_models]]
|
||||
model = "MiniMax-M2.5"
|
||||
preferred_models = [
|
||||
"MiniMax-M2.5",
|
||||
]
|
||||
provider = "minimax"
|
||||
temperature = 0.0
|
||||
top_p = 1.0
|
||||
max_output_tokens = 32000
|
||||
history_trunc_limit = 900000
|
||||
|
||||
[[personas.Default.preferred_models]]
|
||||
provider = "gemini_cli"
|
||||
model = "gemini-3-flash-preview"
|
||||
temperature = -1.4901161193847656e-08
|
||||
max_output_tokens = 32000
|
||||
history_trunc_limit = 900000
|
||||
top_p = 1.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[presets.Default]
|
||||
system_prompt = ""
|
||||
temperature = 0.0
|
||||
top_p = 1.0
|
||||
max_output_tokens = 32000
|
||||
|
||||
[presets.ModalPreset]
|
||||
system_prompt = "Modal Content"
|
||||
|
||||
@@ -9,5 +9,5 @@ active = "main"
|
||||
|
||||
[discussions.main]
|
||||
git_commit = ""
|
||||
last_updated = "2026-03-08T22:48:42"
|
||||
last_updated = "2026-03-12T20:34:43"
|
||||
history = []
|
||||
|
||||
@@ -34,13 +34,8 @@ def migrate():
|
||||
preset = models.Preset.from_dict(name, data)
|
||||
persona = models.Persona(
|
||||
name=name,
|
||||
provider=provider,
|
||||
model=model,
|
||||
preferred_models=[model] if model else [],
|
||||
system_prompt=preset.system_prompt,
|
||||
temperature=preset.temperature,
|
||||
top_p=preset.top_p,
|
||||
max_output_tokens=preset.max_output_tokens
|
||||
preferred_models=[{"provider": provider, "model": model}],
|
||||
system_prompt=preset.system_prompt
|
||||
)
|
||||
persona_manager.save_persona(persona, scope="global")
|
||||
print(f"Migrated global preset to persona: {name}")
|
||||
@@ -50,12 +45,13 @@ def migrate():
|
||||
if active_preset and active_preset not in persona_manager.load_all():
|
||||
persona = models.Persona(
|
||||
name=active_preset,
|
||||
provider=provider,
|
||||
model=model,
|
||||
preferred_models=[model] if model else [],
|
||||
system_prompt=ai_cfg.get("system_prompt", ""),
|
||||
temperature=ai_cfg.get("temperature"),
|
||||
max_output_tokens=ai_cfg.get("max_tokens")
|
||||
preferred_models=[{
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"temperature": ai_cfg.get("temperature"),
|
||||
"max_output_tokens": ai_cfg.get("max_tokens")
|
||||
}],
|
||||
system_prompt=ai_cfg.get("system_prompt", "")
|
||||
)
|
||||
persona_manager.save_persona(persona, scope="global")
|
||||
print(f"Created Initial Legacy persona from active_preset: {active_preset}")
|
||||
|
||||
47
scripts/mock_mcp_server.py
Normal file
47
scripts/mock_mcp_server.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import sys
|
||||
import json
|
||||
|
||||
def main():
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
try:
|
||||
req = json.loads(line)
|
||||
method = req.get("method")
|
||||
req_id = req.get("id")
|
||||
|
||||
if method == "tools/list":
|
||||
resp = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {
|
||||
"tools": [
|
||||
{"name": "echo", "description": "Echo input", "inputSchema": {"type": "object"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
elif method == "tools/call":
|
||||
name = req["params"].get("name")
|
||||
args = req["params"].get("arguments", {})
|
||||
if name == "echo":
|
||||
resp = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {
|
||||
"content": [{"type": "text", "text": f"ECHO: {args}"}]
|
||||
}
|
||||
}
|
||||
else:
|
||||
resp = {"jsonrpc": "2.0", "id": req_id, "error": {"message": "Unknown tool"}}
|
||||
else:
|
||||
resp = {"jsonrpc": "2.0", "id": req_id, "error": {"message": "Unknown method"}}
|
||||
|
||||
sys.stdout.write(json.dumps(resp) + "\n")
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Error: {e}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
252
scripts/refactor_ai_settings_2.py
Normal file
252
scripts/refactor_ai_settings_2.py
Normal file
@@ -0,0 +1,252 @@
|
||||
import sys
|
||||
|
||||
with open("src/gui_2.py", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. In _render_provider_panel, remove Fetch Models
|
||||
old_fetch = """ imgui.text("Model")
|
||||
imgui.same_line()
|
||||
if imgui.button("Fetch Models"):
|
||||
self._fetch_models(self.current_provider)
|
||||
if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)):"""
|
||||
new_fetch = """ imgui.text("Model")
|
||||
if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)):"""
|
||||
content = content.replace(old_fetch, new_fetch)
|
||||
|
||||
# 2. Extract Persona block
|
||||
# We need to find the start of 'imgui.text("Persona")' and end of 'self._editing_persona_is_new = True'
|
||||
# Let's be very careful.
|
||||
old_persona_block = """ imgui.text("Persona")
|
||||
if not hasattr(self, 'ui_active_persona'):
|
||||
self.ui_active_persona = ""
|
||||
personas = getattr(self.controller, 'personas', {})
|
||||
if imgui.begin_combo("##persona", self.ui_active_persona or "None"):
|
||||
if imgui.selectable("None", not self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = ""
|
||||
for pname in sorted(personas.keys()):
|
||||
if imgui.selectable(pname, pname == self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = pname
|
||||
if pname in personas:
|
||||
persona = personas[pname]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_provider = persona.provider or ""
|
||||
self._editing_persona_model = persona.model or ""
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_temperature = persona.temperature or 0.7
|
||||
self._editing_persona_max_tokens = persona.max_output_tokens or 4096
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
import json
|
||||
self._editing_persona_preferred_models = json.dumps(persona.preferred_models) if persona.preferred_models else "[]"
|
||||
self._editing_persona_is_new = False
|
||||
if persona.provider and persona.provider in self.controller.PROVIDERS:
|
||||
self.current_provider = persona.provider
|
||||
if persona.model:
|
||||
self.current_model = persona.model
|
||||
if persona.temperature is not None:
|
||||
ai_client.temperature = persona.temperature
|
||||
if persona.max_output_tokens:
|
||||
ai_client.max_output_tokens = persona.max_output_tokens
|
||||
if persona.system_prompt:
|
||||
ai_client.system_instruction = persona.system_prompt
|
||||
if persona.tool_preset:
|
||||
self.ui_active_tool_preset = persona.tool_preset
|
||||
ai_client.set_tool_preset(persona.tool_preset)
|
||||
if persona.bias_profile:
|
||||
self.ui_active_bias_profile = persona.bias_profile
|
||||
ai_client.set_bias_profile(persona.bias_profile)
|
||||
imgui.end_combo()
|
||||
imgui.same_line()
|
||||
if imgui.button("Manage Personas"):
|
||||
self.show_persona_editor_window = True
|
||||
if self.ui_active_persona and self.ui_active_persona in personas:
|
||||
persona = personas[self.ui_active_persona]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_provider = persona.provider or ""
|
||||
self._editing_persona_model = persona.model or ""
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_temperature = persona.temperature if persona.temperature is not None else 0.7
|
||||
self._editing_persona_max_tokens = persona.max_output_tokens if persona.max_output_tokens is not None else 4096
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
self._editing_persona_preferred_models_list = list(persona.preferred_models) if persona.preferred_models else []
|
||||
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name)
|
||||
self._editing_persona_is_new = False
|
||||
else:
|
||||
self._editing_persona_name = ""
|
||||
self._editing_persona_provider = self.current_provider
|
||||
self._editing_persona_model = self.current_model
|
||||
self._editing_persona_system_prompt = ""
|
||||
self._editing_persona_temperature = 0.7
|
||||
self._editing_persona_max_tokens = 4096
|
||||
self._editing_persona_tool_preset_id = ""
|
||||
self._editing_persona_bias_profile_id = ""
|
||||
self._editing_persona_preferred_models_list = []
|
||||
self._editing_persona_scope = "project"
|
||||
self._editing_persona_is_new = True"""
|
||||
|
||||
# We need to extract the bias profile block as well
|
||||
old_bias_block = """ imgui.text("Bias Profile")
|
||||
if imgui.begin_combo("##bias", self.ui_active_bias_profile or "None"):
|
||||
if imgui.selectable("None", not self.ui_active_bias_profile)[0]:
|
||||
self.ui_active_bias_profile = ""
|
||||
ai_client.set_bias_profile(None)
|
||||
for bname in sorted(self.bias_profiles.keys()):
|
||||
if imgui.selectable(bname, bname == self.ui_active_bias_profile)[0]:
|
||||
self.ui_active_bias_profile = bname
|
||||
ai_client.set_bias_profile(bname)
|
||||
imgui.end_combo()"""
|
||||
|
||||
# Remove them from their original spots
|
||||
content = content.replace(old_bias_block, "")
|
||||
content = content.replace(old_persona_block, "")
|
||||
|
||||
# Insert Persona block at the top of _render_provider_panel
|
||||
old_provider_start = """ def _render_provider_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel")
|
||||
imgui.text("Provider")"""
|
||||
new_provider_start = f""" def _render_provider_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel")
|
||||
{old_persona_block}
|
||||
imgui.separator()
|
||||
imgui.text("Provider")"""
|
||||
content = content.replace(old_provider_start, new_provider_start)
|
||||
|
||||
# Update _render_agent_tools_panel
|
||||
old_agent_tools_start = """ def _render_agent_tools_panel(self) -> None:
|
||||
imgui.text_colored(C_LBL, 'Active Tool Preset')"""
|
||||
new_agent_tools_start = f""" def _render_agent_tools_panel(self) -> None:
|
||||
if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open):
|
||||
imgui.text("Tool Preset")"""
|
||||
content = content.replace(old_agent_tools_start, new_agent_tools_start)
|
||||
|
||||
# Wait, if I do collapsing header, I need to indent the rest of the function.
|
||||
# Instead of indenting the whole function, I can just use the header.
|
||||
# But wait, ImGui collapsing_header doesn't require indenting, it just returns true if open.
|
||||
# So I should write it properly:
|
||||
old_agent_tools_func = """ def _render_agent_tools_panel(self) -> None:
|
||||
imgui.text_colored(C_LBL, 'Active Tool Preset')
|
||||
presets = self.controller.tool_presets
|
||||
preset_names = [""] + sorted(list(presets.keys()))
|
||||
|
||||
# Gracefully handle None or missing preset
|
||||
active = getattr(self, "ui_active_tool_preset", "")
|
||||
if active is None: active = ""
|
||||
try:
|
||||
idx = preset_names.index(active)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
|
||||
ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names)
|
||||
if ch:
|
||||
self.ui_active_tool_preset = preset_names[new_idx]
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Manage Presets##tools"):
|
||||
self.show_tool_preset_manager_window = True
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("Configure tool availability and default modes.")
|
||||
|
||||
imgui.dummy(imgui.ImVec2(0, 8))
|
||||
active_name = self.ui_active_tool_preset
|
||||
if active_name and active_name in presets:
|
||||
preset = presets[active_name]
|
||||
for cat_name, tools in preset.categories.items():
|
||||
if imgui.tree_node(cat_name):
|
||||
for tool in tools:
|
||||
if tool.weight >= 5:
|
||||
imgui.text_colored(vec4(255, 100, 100), "[HIGH]")
|
||||
imgui.same_line()
|
||||
elif tool.weight == 4:
|
||||
imgui.text_colored(vec4(255, 255, 100), "[PREF]")
|
||||
imgui.same_line()
|
||||
elif tool.weight == 2:
|
||||
imgui.text_colored(vec4(255, 150, 50), "[REJECT]")
|
||||
imgui.same_line()
|
||||
elif tool.weight <= 1:
|
||||
imgui.text_colored(vec4(180, 180, 180), "[LOW]")
|
||||
imgui.same_line()
|
||||
|
||||
imgui.text(tool.name)
|
||||
imgui.same_line(180)
|
||||
|
||||
mode = tool.approval
|
||||
if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"):
|
||||
tool.approval = "auto"
|
||||
imgui.same_line()
|
||||
if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"):
|
||||
tool.approval = "ask"
|
||||
imgui.tree_pop()"""
|
||||
|
||||
new_agent_tools_func = """ def _render_agent_tools_panel(self) -> None:
|
||||
if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open):
|
||||
imgui.text("Tool Preset")
|
||||
presets = self.controller.tool_presets
|
||||
preset_names = [""] + sorted(list(presets.keys()))
|
||||
|
||||
# Gracefully handle None or missing preset
|
||||
active = getattr(self, "ui_active_tool_preset", "")
|
||||
if active is None: active = ""
|
||||
try:
|
||||
idx = preset_names.index(active)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
|
||||
ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names)
|
||||
if ch:
|
||||
self.ui_active_tool_preset = preset_names[new_idx]
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Manage Tools##tools"):
|
||||
self.show_tool_preset_manager_window = True
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("Configure tool availability and default modes.")
|
||||
|
||||
imgui.dummy(imgui.ImVec2(0, 4))
|
||||
""" + "\n ".join(old_bias_block.split("\n")) + """
|
||||
|
||||
imgui.dummy(imgui.ImVec2(0, 8))
|
||||
active_name = self.ui_active_tool_preset
|
||||
if active_name and active_name in presets:
|
||||
preset = presets[active_name]
|
||||
for cat_name, tools in preset.categories.items():
|
||||
if imgui.tree_node(cat_name):
|
||||
for tool in tools:
|
||||
if tool.weight >= 5:
|
||||
imgui.text_colored(vec4(255, 100, 100), "[HIGH]")
|
||||
imgui.same_line()
|
||||
elif tool.weight == 4:
|
||||
imgui.text_colored(vec4(255, 255, 100), "[PREF]")
|
||||
imgui.same_line()
|
||||
elif tool.weight == 2:
|
||||
imgui.text_colored(vec4(255, 150, 50), "[REJECT]")
|
||||
imgui.same_line()
|
||||
elif tool.weight <= 1:
|
||||
imgui.text_colored(vec4(180, 180, 180), "[LOW]")
|
||||
imgui.same_line()
|
||||
|
||||
imgui.text(tool.name)
|
||||
imgui.same_line(180)
|
||||
|
||||
mode = tool.approval
|
||||
if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"):
|
||||
tool.approval = "auto"
|
||||
imgui.same_line()
|
||||
if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"):
|
||||
tool.approval = "ask"
|
||||
imgui.tree_pop()"""
|
||||
content = content.replace(old_agent_tools_func, new_agent_tools_func)
|
||||
|
||||
# Fix cache text display in Usage Analytics
|
||||
content = content.replace('self._gemini_cache_text = f"Gemini Caches: {count} ({size_bytes / 1024:.1f} KB)"', 'self._gemini_cache_text = f"Cache Usage: {count} ({size_bytes / 1024:.1f} KB)"')
|
||||
content = content.replace('imgui.text_colored(C_LBL, f"Gemini Cache: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s")', 'imgui.text_colored(C_LBL, f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s")')
|
||||
content = content.replace('imgui.text_disabled("Gemini Cache: INACTIVE")', 'imgui.text_disabled("Cache Usage: INACTIVE")')
|
||||
|
||||
# Also, user requested: "The persona should problably just mess with the project system prompt for now."
|
||||
# Currently in persona selection: `ai_client.system_instruction = persona.system_prompt`
|
||||
# Let's change that to `self.ui_project_system_prompt = persona.system_prompt` and remove ai_client direct injection
|
||||
content = content.replace('ai_client.system_instruction = persona.system_prompt', 'self.ui_project_system_prompt = persona.system_prompt')
|
||||
|
||||
with open("src/gui_2.py", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("done")
|
||||
228
scripts/refactor_ai_settings_3.py
Normal file
228
scripts/refactor_ai_settings_3.py
Normal file
@@ -0,0 +1,228 @@
|
||||
import sys
|
||||
|
||||
with open("src/gui_2.py", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Update _gui_func:
|
||||
# Extract Persona out of Provider panel. I will create a new method _render_persona_selector_panel
|
||||
old_gui_settings = """ if self.show_windows.get("AI Settings", False):
|
||||
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
self.show_windows["AI Settings"] = bool(opened)
|
||||
if exp:
|
||||
if imgui.collapsing_header("Provider & Model"):
|
||||
self._render_provider_panel()
|
||||
if imgui.collapsing_header("System Prompts"):
|
||||
self._render_system_prompts_panel()
|
||||
self._render_agent_tools_panel()
|
||||
self._render_cache_panel()
|
||||
|
||||
imgui.end()
|
||||
if self.ui_separate_usage_analytics and self.show_windows.get("Usage Analytics", False):
|
||||
exp, opened = imgui.begin("Usage Analytics", self.show_windows["Usage Analytics"])
|
||||
self.show_windows["Usage Analytics"] = bool(opened)
|
||||
if exp:
|
||||
self._render_usage_analytics_panel()
|
||||
imgui.end()"""
|
||||
|
||||
new_gui_settings = """ if self.show_windows.get("AI Settings", False):
|
||||
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
self.show_windows["AI Settings"] = bool(opened)
|
||||
if exp:
|
||||
self._render_persona_selector_panel()
|
||||
if imgui.collapsing_header("Provider & Model"):
|
||||
self._render_provider_panel()
|
||||
if imgui.collapsing_header("System Prompts"):
|
||||
self._render_system_prompts_panel()
|
||||
self._render_agent_tools_panel()
|
||||
|
||||
imgui.end()
|
||||
if self.ui_separate_usage_analytics and self.show_windows.get("Usage Analytics", False):
|
||||
exp, opened = imgui.begin("Usage Analytics", self.show_windows["Usage Analytics"])
|
||||
self.show_windows["Usage Analytics"] = bool(opened)
|
||||
if exp:
|
||||
self._render_usage_analytics_panel()
|
||||
imgui.end()"""
|
||||
|
||||
content = content.replace(old_gui_settings, new_gui_settings)
|
||||
|
||||
# Update _render_usage_analytics_panel
|
||||
old_usage = """ def _render_usage_analytics_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_usage_analytics_panel")
|
||||
self._render_token_budget_panel()
|
||||
imgui.separator()
|
||||
self._render_tool_analytics_panel()
|
||||
imgui.separator()
|
||||
self._render_session_insights_panel()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_usage_analytics_panel")"""
|
||||
|
||||
new_usage = """ def _render_usage_analytics_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_usage_analytics_panel")
|
||||
self._render_token_budget_panel()
|
||||
imgui.separator()
|
||||
self._render_cache_panel()
|
||||
imgui.separator()
|
||||
self._render_tool_analytics_panel()
|
||||
imgui.separator()
|
||||
self._render_session_insights_panel()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_usage_analytics_panel")"""
|
||||
content = content.replace(old_usage, new_usage)
|
||||
|
||||
# Remove the persona block from _render_provider_panel and put it in _render_persona_selector_panel
|
||||
old_persona_block = """ def _render_provider_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel")
|
||||
imgui.text("Persona")
|
||||
if not hasattr(self, 'ui_active_persona'):
|
||||
self.ui_active_persona = ""
|
||||
personas = getattr(self.controller, 'personas', {})
|
||||
if imgui.begin_combo("##persona", self.ui_active_persona or "None"):
|
||||
if imgui.selectable("None", not self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = ""
|
||||
for pname in sorted(personas.keys()):
|
||||
if imgui.selectable(pname, pname == self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = pname
|
||||
if pname in personas:
|
||||
persona = personas[pname]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_provider = persona.provider or ""
|
||||
self._editing_persona_model = persona.model or ""
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_temperature = persona.temperature or 0.7
|
||||
self._editing_persona_max_tokens = persona.max_output_tokens or 4096
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
import json
|
||||
self._editing_persona_preferred_models = json.dumps(persona.preferred_models) if persona.preferred_models else "[]"
|
||||
self._editing_persona_is_new = False
|
||||
if persona.provider and persona.provider in self.controller.PROVIDERS:
|
||||
self.current_provider = persona.provider
|
||||
if persona.model:
|
||||
self.current_model = persona.model
|
||||
if persona.temperature is not None:
|
||||
ai_client.temperature = persona.temperature
|
||||
if persona.max_output_tokens:
|
||||
ai_client.max_output_tokens = persona.max_output_tokens
|
||||
if persona.system_prompt:
|
||||
self.ui_project_system_prompt = persona.system_prompt
|
||||
if persona.tool_preset:
|
||||
self.ui_active_tool_preset = persona.tool_preset
|
||||
ai_client.set_tool_preset(persona.tool_preset)
|
||||
if persona.bias_profile:
|
||||
self.ui_active_bias_profile = persona.bias_profile
|
||||
ai_client.set_bias_profile(persona.bias_profile)
|
||||
imgui.end_combo()
|
||||
imgui.same_line()
|
||||
if imgui.button("Manage Personas"):
|
||||
self.show_persona_editor_window = True
|
||||
if self.ui_active_persona and self.ui_active_persona in personas:
|
||||
persona = personas[self.ui_active_persona]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_provider = persona.provider or ""
|
||||
self._editing_persona_model = persona.model or ""
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_temperature = persona.temperature if persona.temperature is not None else 0.7
|
||||
self._editing_persona_max_tokens = persona.max_output_tokens if persona.max_output_tokens is not None else 4096
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
self._editing_persona_preferred_models_list = list(persona.preferred_models) if persona.preferred_models else []
|
||||
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name)
|
||||
self._editing_persona_is_new = False
|
||||
else:
|
||||
self._editing_persona_name = ""
|
||||
self._editing_persona_provider = self.current_provider
|
||||
self._editing_persona_model = self.current_model
|
||||
self._editing_persona_system_prompt = ""
|
||||
self._editing_persona_temperature = 0.7
|
||||
self._editing_persona_max_tokens = 4096
|
||||
self._editing_persona_tool_preset_id = ""
|
||||
self._editing_persona_bias_profile_id = ""
|
||||
self._editing_persona_preferred_models_list = []
|
||||
self._editing_persona_scope = "project"
|
||||
self._editing_persona_is_new = True
|
||||
imgui.separator()
|
||||
imgui.text("Provider")"""
|
||||
|
||||
new_persona_block = """ def _render_persona_selector_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel")
|
||||
imgui.text("Persona")
|
||||
if not hasattr(self, 'ui_active_persona'):
|
||||
self.ui_active_persona = ""
|
||||
personas = getattr(self.controller, 'personas', {})
|
||||
if imgui.begin_combo("##persona", self.ui_active_persona or "None"):
|
||||
if imgui.selectable("None", not self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = ""
|
||||
for pname in sorted(personas.keys()):
|
||||
if imgui.selectable(pname, pname == self.ui_active_persona)[0]:
|
||||
self.ui_active_persona = pname
|
||||
if pname in personas:
|
||||
persona = personas[pname]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
import copy
|
||||
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
||||
self._editing_persona_is_new = False
|
||||
|
||||
# Apply persona to current state immediately
|
||||
if persona.preferred_models and len(persona.preferred_models) > 0:
|
||||
first_model = persona.preferred_models[0]
|
||||
if first_model.get("provider"):
|
||||
self.current_provider = first_model.get("provider")
|
||||
if first_model.get("model"):
|
||||
self.current_model = first_model.get("model")
|
||||
if first_model.get("temperature") is not None:
|
||||
ai_client.temperature = first_model.get("temperature")
|
||||
self.temperature = first_model.get("temperature")
|
||||
if first_model.get("max_output_tokens"):
|
||||
ai_client.max_output_tokens = first_model.get("max_output_tokens")
|
||||
self.max_tokens = first_model.get("max_output_tokens")
|
||||
if first_model.get("history_trunc_limit"):
|
||||
self.history_trunc_limit = first_model.get("history_trunc_limit")
|
||||
|
||||
if persona.system_prompt:
|
||||
self.ui_project_system_prompt = persona.system_prompt
|
||||
if persona.tool_preset:
|
||||
self.ui_active_tool_preset = persona.tool_preset
|
||||
ai_client.set_tool_preset(persona.tool_preset)
|
||||
if persona.bias_profile:
|
||||
self.ui_active_bias_profile = persona.bias_profile
|
||||
ai_client.set_bias_profile(persona.bias_profile)
|
||||
imgui.end_combo()
|
||||
imgui.same_line()
|
||||
if imgui.button("Manage Personas"):
|
||||
self.show_persona_editor_window = True
|
||||
if self.ui_active_persona and self.ui_active_persona in personas:
|
||||
persona = personas[self.ui_active_persona]
|
||||
self._editing_persona_name = persona.name
|
||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||
import copy
|
||||
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
||||
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name)
|
||||
self._editing_persona_is_new = False
|
||||
else:
|
||||
self._editing_persona_name = ""
|
||||
self._editing_persona_system_prompt = ""
|
||||
self._editing_persona_tool_preset_id = ""
|
||||
self._editing_persona_bias_profile_id = ""
|
||||
self._editing_persona_preferred_models_list = [{
|
||||
"provider": self.current_provider,
|
||||
"model": self.current_model,
|
||||
"temperature": getattr(self, "temperature", 0.7),
|
||||
"max_output_tokens": getattr(self, "max_tokens", 4096),
|
||||
"history_trunc_limit": getattr(self, "history_trunc_limit", 900000)
|
||||
}]
|
||||
self._editing_persona_scope = "project"
|
||||
self._editing_persona_is_new = True
|
||||
imgui.separator()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_persona_selector_panel")
|
||||
|
||||
def _render_provider_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel")
|
||||
imgui.text("Provider")"""
|
||||
content = content.replace(old_persona_block, new_persona_block)
|
||||
|
||||
with open("src/gui_2.py", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("done gui updates")
|
||||
@@ -363,3 +363,4 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ from src.events import EventEmitter
|
||||
_provider: str = "gemini"
|
||||
_model: str = "gemini-2.5-flash-lite"
|
||||
_temperature: float = 0.0
|
||||
_top_p: float = 1.0
|
||||
_max_tokens: int = 8192
|
||||
|
||||
_history_trunc_limit: int = 8000
|
||||
@@ -49,11 +50,12 @@ _history_trunc_limit: int = 8000
|
||||
# Global event emitter for API lifecycle events
|
||||
events: EventEmitter = EventEmitter()
|
||||
|
||||
def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000) -> None:
|
||||
global _temperature, _max_tokens, _history_trunc_limit
|
||||
def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: float = 1.0) -> None:
|
||||
global _temperature, _max_tokens, _history_trunc_limit, _top_p
|
||||
_temperature = temp
|
||||
_max_tokens = max_tok
|
||||
_history_trunc_limit = trunc_limit
|
||||
_top_p = top_p
|
||||
|
||||
def get_history_trunc_limit() -> int:
|
||||
return _history_trunc_limit
|
||||
@@ -533,7 +535,7 @@ def get_bias_profile() -> Optional[str]:
|
||||
|
||||
def _build_anthropic_tools() -> list[dict[str, Any]]:
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.MCP_TOOL_SPECS:
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
raw_tools.append({
|
||||
"name": spec["name"],
|
||||
@@ -577,7 +579,7 @@ def _get_anthropic_tools() -> list[dict[str, Any]]:
|
||||
|
||||
def _gemini_tool_declaration() -> Optional[types.Tool]:
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.MCP_TOOL_SPECS:
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
raw_tools.append({
|
||||
"name": spec["name"],
|
||||
@@ -713,10 +715,15 @@ async def _execute_single_tool_call_async(
|
||||
tool_executed = True
|
||||
|
||||
if not tool_executed:
|
||||
if name and name in mcp_client.TOOL_NAMES:
|
||||
is_native = name in mcp_client.TOOL_NAMES
|
||||
ext_tools = mcp_client.get_external_mcp_manager().get_all_tools()
|
||||
is_external = name in ext_tools
|
||||
if name and (is_native or is_external):
|
||||
_append_comms("OUT", "tool_call", {"name": name, "id": call_id, "args": args})
|
||||
if name in mcp_client.MUTATING_TOOLS and approval_mode != "auto" and pre_tool_callback:
|
||||
desc = f"# MCP MUTATING TOOL: {name}\n" + "\n".join(f"# {k}: {repr(v)}" for k, v in args.items())
|
||||
should_approve = (name in mcp_client.MUTATING_TOOLS or is_external) and approval_mode != "auto" and pre_tool_callback
|
||||
if should_approve:
|
||||
label = "MCP MUTATING" if is_native else "EXTERNAL MCP"
|
||||
desc = f"# {label} TOOL: {name}\n" + "\n".join(f"# {k}: {repr(v)}" for k, v in args.items())
|
||||
_res = await asyncio.to_thread(pre_tool_callback, desc, base_dir, qa_callback)
|
||||
out = "USER REJECTED: tool execution cancelled" if _res is None else await mcp_client.async_dispatch(name, args)
|
||||
else:
|
||||
@@ -814,7 +821,7 @@ def _build_file_diff_text(changed_items: list[dict[str, Any]]) -> str:
|
||||
|
||||
def _build_deepseek_tools() -> list[dict[str, Any]]:
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.MCP_TOOL_SPECS:
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
raw_tools.append({
|
||||
"name": spec["name"],
|
||||
@@ -939,6 +946,7 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
|
||||
system_instruction=sys_instr,
|
||||
tools=cast(Any, tools_decl),
|
||||
temperature=_temperature,
|
||||
top_p=_top_p,
|
||||
max_output_tokens=_max_tokens,
|
||||
safety_settings=[types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH)]
|
||||
)
|
||||
@@ -1010,6 +1018,7 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
|
||||
config = types.GenerateContentConfig(
|
||||
tools=[td] if td else [],
|
||||
temperature=_temperature,
|
||||
top_p=_top_p,
|
||||
max_output_tokens=_max_tokens,
|
||||
)
|
||||
|
||||
@@ -1455,6 +1464,7 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
|
||||
model=_model,
|
||||
max_tokens=_max_tokens,
|
||||
temperature=_temperature,
|
||||
top_p=_top_p,
|
||||
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
|
||||
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
|
||||
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
|
||||
@@ -1468,6 +1478,7 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
|
||||
model=_model,
|
||||
max_tokens=_max_tokens,
|
||||
temperature=_temperature,
|
||||
top_p=_top_p,
|
||||
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
|
||||
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
|
||||
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
|
||||
@@ -1696,6 +1707,7 @@ def _send_deepseek(md_content: str, user_message: str, base_dir: str,
|
||||
|
||||
if not is_reasoner:
|
||||
request_payload["temperature"] = _temperature
|
||||
request_payload["top_p"] = _top_p
|
||||
# DeepSeek max_tokens is for the output, clamp to 8192 which is their hard limit for V3/Chat
|
||||
request_payload["max_tokens"] = min(_max_tokens, 8192)
|
||||
tools = _get_deepseek_tools()
|
||||
@@ -1927,6 +1939,7 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
|
||||
request_payload["stream_options"] = {"include_usage": True}
|
||||
|
||||
request_payload["temperature"] = 1.0
|
||||
request_payload["top_p"] = _top_p
|
||||
request_payload["max_tokens"] = min(_max_tokens, 8192)
|
||||
|
||||
tools = _get_deepseek_tools()
|
||||
@@ -2400,3 +2413,4 @@ def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
||||
"current": 0,
|
||||
"percentage": 0,
|
||||
})
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class ApiHookClient:
|
||||
return None
|
||||
|
||||
def post_gui(self, payload: dict) -> dict[str, Any]:
|
||||
"""Pushes an event to the GUI's SyncEventQueue via the /api/gui endpoint."""
|
||||
"""Pushes an event to the GUI's AsyncEventQueue via the /api/gui endpoint."""
|
||||
return self._make_request('POST', '/api/gui', data=payload) or {}
|
||||
|
||||
def push_event(self, action: str, payload: dict) -> dict[str, Any]:
|
||||
@@ -186,6 +186,22 @@ class ApiHookClient:
|
||||
"""Retrieves the dedicated MMA engine status."""
|
||||
return self._make_request('GET', '/api/gui/mma_status') or {}
|
||||
|
||||
def get_mma_workers(self) -> dict[str, Any]:
|
||||
"""Retrieves status for all active MMA workers."""
|
||||
return self._make_request('GET', '/api/mma/workers') or {}
|
||||
|
||||
def get_context_state(self) -> dict[str, Any]:
|
||||
"""Retrieves the current file and screenshot context state."""
|
||||
return self._make_request('GET', '/api/context/state') or {}
|
||||
|
||||
def get_financial_metrics(self) -> dict[str, Any]:
|
||||
"""Retrieves token usage and estimated financial cost metrics."""
|
||||
return self._make_request('GET', '/api/metrics/financial') or {}
|
||||
|
||||
def get_system_telemetry(self) -> dict[str, Any]:
|
||||
"""Retrieves system-level telemetry including thread status and event queue size."""
|
||||
return self._make_request('GET', '/api/system/telemetry') or {}
|
||||
|
||||
def get_node_status(self, node_id: str) -> dict[str, Any]:
|
||||
"""Retrieves status for a specific node in the MMA DAG."""
|
||||
return self._make_request('GET', f'/api/mma/node/{node_id}') or {}
|
||||
@@ -223,3 +239,22 @@ class ApiHookClient:
|
||||
def get_patch_status(self) -> dict[str, Any]:
|
||||
"""Gets the current patch modal status."""
|
||||
return self._make_request('GET', '/api/patch/status') or {}
|
||||
|
||||
def spawn_mma_worker(self, data: dict) -> dict:
|
||||
return self._make_request('POST', '/api/mma/workers/spawn', data=data) or {}
|
||||
|
||||
def kill_mma_worker(self, worker_id: str) -> dict:
|
||||
return self._make_request('POST', '/api/mma/workers/kill', data={"worker_id": worker_id}) or {}
|
||||
|
||||
def pause_mma_pipeline(self) -> dict:
|
||||
return self._make_request('POST', '/api/mma/pipeline/pause') or {}
|
||||
|
||||
def resume_mma_pipeline(self) -> dict:
|
||||
return self._make_request('POST', '/api/mma/pipeline/resume') or {}
|
||||
|
||||
def inject_context(self, data: dict) -> dict:
|
||||
return self._make_request('POST', '/api/context/inject', data=data) or {}
|
||||
|
||||
def mutate_mma_dag(self, data: dict) -> dict:
|
||||
return self._make_request('POST', '/api/mma/dag/mutate', data=data) or {}
|
||||
|
||||
|
||||
201
src/api_hooks.py
201
src/api_hooks.py
@@ -3,10 +3,14 @@ import json
|
||||
import threading
|
||||
import uuid
|
||||
import sys
|
||||
import asyncio
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
import logging
|
||||
import websockets
|
||||
from websockets.asyncio.server import serve
|
||||
from src import session_logger
|
||||
from src import cost_tracker
|
||||
"""
|
||||
API Hooks - REST API for external automation and state inspection.
|
||||
|
||||
@@ -77,6 +81,7 @@ def _serialize_for_api(obj: Any) -> Any:
|
||||
class HookHandler(BaseHTTPRequestHandler):
|
||||
"""Handles incoming HTTP requests for the API hooks."""
|
||||
def do_GET(self) -> None:
|
||||
try:
|
||||
app = self.server.app
|
||||
session_logger.log_api_hook("GET", self.path, "")
|
||||
if self.path == "/status":
|
||||
@@ -233,9 +238,50 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send_response(504)
|
||||
self.end_headers()
|
||||
elif self.path == "/api/mma/workers":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
mma_streams = _get_app_attr(app, "mma_streams", {})
|
||||
self.wfile.write(json.dumps({"workers": _serialize_for_api(mma_streams)}).encode("utf-8"))
|
||||
elif self.path == "/api/context/state":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
files = _get_app_attr(app, "files", [])
|
||||
screenshots = _get_app_attr(app, "screenshots", [])
|
||||
self.wfile.write(json.dumps({"files": files, "screenshots": screenshots}).encode("utf-8"))
|
||||
elif self.path == "/api/metrics/financial":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
usage = _get_app_attr(app, "mma_tier_usage", {})
|
||||
metrics = {}
|
||||
for tier, data in usage.items():
|
||||
model = data.get("model", "")
|
||||
in_t = data.get("input", 0)
|
||||
out_t = data.get("output", 0)
|
||||
cost = cost_tracker.estimate_cost(model, in_t, out_t)
|
||||
metrics[tier] = {**data, "estimated_cost": cost}
|
||||
self.wfile.write(json.dumps({"financial": metrics}).encode("utf-8"))
|
||||
elif self.path == "/api/system/telemetry":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
threads = [t.name for t in threading.enumerate()]
|
||||
queue_size = 0
|
||||
if _has_app_attr(app, "_api_event_queue"):
|
||||
queue = _get_app_attr(app, "_api_event_queue")
|
||||
if queue: queue_size = len(queue)
|
||||
self.wfile.write(json.dumps({"threads": threads, "event_queue_size": queue_size}).encode("utf-8"))
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": str(e)}).encode("utf-8"))
|
||||
|
||||
def do_POST(self) -> None:
|
||||
app = self.server.app
|
||||
@@ -479,6 +525,90 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
elif self.path == "/api/mma/workers/spawn":
|
||||
def spawn_worker():
|
||||
try:
|
||||
func = _get_app_attr(app, "_spawn_worker")
|
||||
if func: func(data)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[DEBUG] Hook API spawn_worker error: {e}\n")
|
||||
sys.stderr.flush()
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": spawn_worker})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/mma/workers/kill":
|
||||
def kill_worker():
|
||||
try:
|
||||
worker_id = data.get("worker_id")
|
||||
func = _get_app_attr(app, "_kill_worker")
|
||||
if func: func(worker_id)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[DEBUG] Hook API kill_worker error: {e}\n")
|
||||
sys.stderr.flush()
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": kill_worker})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/mma/pipeline/pause":
|
||||
def pause_pipeline():
|
||||
_set_app_attr(app, "mma_step_mode", True)
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": pause_pipeline})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/mma/pipeline/resume":
|
||||
def resume_pipeline():
|
||||
_set_app_attr(app, "mma_step_mode", False)
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": resume_pipeline})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/context/inject":
|
||||
def inject_context():
|
||||
files = _get_app_attr(app, "files")
|
||||
if isinstance(files, list):
|
||||
files.extend(data.get("files", []))
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": inject_context})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/mma/dag/mutate":
|
||||
def mutate_dag():
|
||||
try:
|
||||
func = _get_app_attr(app, "_mutate_dag")
|
||||
if func: func(data)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[DEBUG] Hook API mutate_dag error: {e}\n")
|
||||
sys.stderr.flush()
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
if lock and tasks is not None:
|
||||
with lock: tasks.append({"action": "custom_callback", "callback": mutate_dag})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
@@ -498,6 +628,7 @@ class HookServer:
|
||||
self.port = port
|
||||
self.server = None
|
||||
self.thread = None
|
||||
self.websocket_server: WebSocketServer | None = None
|
||||
|
||||
def start(self) -> None:
|
||||
if self.thread and self.thread.is_alive():
|
||||
@@ -511,15 +642,85 @@ class HookServer:
|
||||
if not _has_app_attr(self.app, '_ask_responses'): _set_app_attr(self.app, '_ask_responses', {})
|
||||
if not _has_app_attr(self.app, '_api_event_queue'): _set_app_attr(self.app, '_api_event_queue', [])
|
||||
if not _has_app_attr(self.app, '_api_event_queue_lock'): _set_app_attr(self.app, '_api_event_queue_lock', threading.Lock())
|
||||
|
||||
self.websocket_server = WebSocketServer(self.app, port=self.port + 1)
|
||||
self.websocket_server.start()
|
||||
|
||||
eq = _get_app_attr(self.app, 'event_queue')
|
||||
if eq:
|
||||
eq.websocket_server = self.websocket_server
|
||||
|
||||
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
logging.info(f"Hook server started on port {self.port}")
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.websocket_server:
|
||||
self.websocket_server.stop()
|
||||
if self.server:
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
logging.info("Hook server stopped")
|
||||
|
||||
class WebSocketServer:
|
||||
"""WebSocket gateway for real-time event streaming."""
|
||||
def __init__(self, app: Any, port: int = 9000) -> None:
|
||||
self.app = app
|
||||
self.port = port
|
||||
self.clients: dict[str, set] = {"events": set(), "telemetry": set()}
|
||||
self.loop: asyncio.AbstractEventLoop | None = None
|
||||
self.thread: threading.Thread | None = None
|
||||
self.server = None
|
||||
self._stop_event: asyncio.Event | None = None
|
||||
|
||||
async def _handler(self, websocket) -> None:
|
||||
try:
|
||||
async for message in websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get("action") == "subscribe":
|
||||
channel = data.get("channel")
|
||||
if channel in self.clients:
|
||||
self.clients[channel].add(websocket)
|
||||
await websocket.send(json.dumps({"type": "subscription_confirmed", "channel": channel}))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
finally:
|
||||
for channel in self.clients:
|
||||
if websocket in self.clients[channel]:
|
||||
self.clients[channel].remove(websocket)
|
||||
|
||||
def _run_loop(self) -> None:
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self._stop_event = asyncio.Event()
|
||||
async def main():
|
||||
async with serve(self._handler, "127.0.0.1", self.port) as server:
|
||||
self.server = server
|
||||
await self._stop_event.wait()
|
||||
self.loop.run_until_complete(main())
|
||||
|
||||
def start(self) -> None:
|
||||
if self.thread and self.thread.is_alive():
|
||||
return
|
||||
self.thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.loop and self._stop_event:
|
||||
self.loop.call_soon_threadsafe(self._stop_event.set)
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def broadcast(self, channel: str, payload: dict[str, Any]) -> None:
|
||||
if not self.loop or channel not in self.clients:
|
||||
return
|
||||
message = json.dumps({"channel": channel, "payload": payload})
|
||||
for ws in list(self.clients[channel]):
|
||||
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ class GenerateRequest(BaseModel):
|
||||
prompt: str
|
||||
auto_add_history: bool = True
|
||||
temperature: float | None = None
|
||||
top_p: float | None = None
|
||||
max_tokens: int | None = None
|
||||
|
||||
class ConfirmRequest(BaseModel):
|
||||
approved: bool
|
||||
script: Optional[str] = None
|
||||
@@ -150,10 +150,11 @@ class AppController:
|
||||
self.disc_roles: List[str] = []
|
||||
self.files: List[str] = []
|
||||
self.screenshots: List[str] = []
|
||||
self.event_queue: events.SyncEventQueue = events.SyncEventQueue()
|
||||
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
self.tracks: List[Dict[str, Any]] = []
|
||||
self.active_track: Optional[models.Track] = None
|
||||
self.engine: Optional[multi_agent_conductor.ConductorEngine] = None
|
||||
self.active_tickets: List[Dict[str, Any]] = []
|
||||
self.mma_streams: Dict[str, str] = {}
|
||||
self._worker_status: Dict[str, str] = {} # stream_id -> "running" | "completed" | "failed" | "killed"
|
||||
@@ -179,7 +180,8 @@ class AppController:
|
||||
"cache_read_input_tokens": 0,
|
||||
"cache_creation_input_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"last_latency": 0.0
|
||||
"last_latency": 0.0,
|
||||
"percentage": 0.0
|
||||
}
|
||||
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
|
||||
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None},
|
||||
@@ -188,6 +190,7 @@ class AppController:
|
||||
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||
}
|
||||
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
|
||||
self._last_telemetry_time: float = 0.0
|
||||
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
||||
self._api_event_queue: List[Dict[str, Any]] = []
|
||||
# Pending dialogs state moved from App
|
||||
@@ -195,10 +198,12 @@ class AppController:
|
||||
self._pending_dialog_open: bool = False
|
||||
self._pending_actions: Dict[str, ConfirmDialog] = {}
|
||||
self._pending_ask_dialog: bool = False
|
||||
self.mcp_config: models.MCPConfiguration = models.MCPConfiguration()
|
||||
# AI settings state
|
||||
self._current_provider: str = "gemini"
|
||||
self._current_model: str = "gemini-2.5-flash-lite"
|
||||
self.temperature: float = 0.0
|
||||
self.top_p: float = 1.0
|
||||
self.max_tokens: int = 8192
|
||||
self.history_trunc_limit: int = 8000
|
||||
# UI-related state moved to controller
|
||||
@@ -281,7 +286,9 @@ class AppController:
|
||||
self._gemini_cache_text: str = ""
|
||||
self._last_stable_md: str = ''
|
||||
self._token_stats: Dict[str, Any] = {}
|
||||
self._token_stats_dirty: bool = False
|
||||
self._comms_log_dirty: bool = True
|
||||
self._tool_log_dirty: bool = True
|
||||
self._token_stats_dirty: bool = True
|
||||
self.ui_disc_truncate_pairs: int = 2
|
||||
self.ui_auto_scroll_comms: bool = True
|
||||
self.ui_auto_scroll_tool_calls: bool = True
|
||||
@@ -289,10 +296,14 @@ class AppController:
|
||||
self._track_discussion_active: bool = False
|
||||
self._tier_stream_last_len: Dict[str, int] = {}
|
||||
self.is_viewing_prior_session: bool = False
|
||||
self._current_session_usage = None
|
||||
self._current_mma_tier_usage = None
|
||||
self.prior_session_entries: List[Dict[str, Any]] = []
|
||||
self.prior_tool_calls: List[Dict[str, Any]] = []
|
||||
self.prior_disc_entries: List[Dict[str, Any]] = []
|
||||
self.prior_mma_dashboard_state: Dict[str, Any] = {}
|
||||
self.prior_mma_dashboard_state = {}
|
||||
self._current_token_history = None
|
||||
self._current_session_start_time = None
|
||||
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||
self.ui_manual_approve: bool = False
|
||||
# Injection state
|
||||
@@ -300,7 +311,9 @@ class AppController:
|
||||
self._inject_mode: str = "skeleton"
|
||||
self._inject_preview: str = ""
|
||||
self._show_inject_modal: bool = False
|
||||
self.show_preset_manager_modal: bool = False
|
||||
self.show_preset_manager_window: bool = False
|
||||
self.show_tool_preset_manager_window: bool = False
|
||||
self.show_persona_editor_window: bool = False
|
||||
self._editing_preset_name: str = ""
|
||||
self._editing_preset_content: str = ""
|
||||
self._editing_preset_temperature: float = 0.0
|
||||
@@ -340,9 +353,12 @@ class AppController:
|
||||
'global_preset_name': 'ui_global_preset_name',
|
||||
'project_preset_name': 'ui_project_preset_name',
|
||||
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||
'ui_active_bias_profile': 'ui_active_bias_profile',
|
||||
'temperature': 'temperature',
|
||||
'max_tokens': 'max_tokens',
|
||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||
'show_preset_manager_window': 'show_preset_manager_window',
|
||||
'show_tool_preset_manager_window': 'show_tool_preset_manager_window',
|
||||
'show_persona_editor_window': 'show_persona_editor_window',
|
||||
'_editing_preset_name': '_editing_preset_name',
|
||||
'_editing_preset_content': '_editing_preset_content',
|
||||
'_editing_preset_temperature': '_editing_preset_temperature',
|
||||
@@ -388,9 +404,12 @@ class AppController:
|
||||
'global_preset_name': 'ui_global_preset_name',
|
||||
'project_preset_name': 'ui_project_preset_name',
|
||||
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||
'ui_active_bias_profile': 'ui_active_bias_profile',
|
||||
'temperature': 'temperature',
|
||||
'max_tokens': 'max_tokens',
|
||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||
'show_preset_manager_window': 'show_preset_manager_window',
|
||||
'show_tool_preset_manager_window': 'show_tool_preset_manager_window',
|
||||
'show_persona_editor_window': 'show_persona_editor_window',
|
||||
'_editing_preset_name': '_editing_preset_name',
|
||||
'_editing_preset_content': '_editing_preset_content',
|
||||
'_editing_preset_temperature': '_editing_preset_temperature',
|
||||
@@ -418,6 +437,12 @@ class AppController:
|
||||
if hasattr(self, 'perf_monitor'):
|
||||
self.perf_monitor.enabled = value
|
||||
|
||||
@property
|
||||
def active_project_root(self) -> str:
|
||||
if self.active_project_path:
|
||||
return str(Path(self.active_project_path).parent)
|
||||
return self.ui_files_base_dir
|
||||
|
||||
def _update_inject_preview(self) -> None:
|
||||
"""Updates the preview content based on the selected file and injection mode."""
|
||||
if not self._inject_file_path:
|
||||
@@ -425,7 +450,7 @@ class AppController:
|
||||
return
|
||||
target_path = self._inject_file_path
|
||||
if not os.path.isabs(target_path):
|
||||
target_path = os.path.join(self.ui_files_base_dir, target_path)
|
||||
target_path = os.path.join(self.active_project_root, target_path)
|
||||
if not os.path.exists(target_path):
|
||||
self._inject_preview = ""
|
||||
return
|
||||
@@ -478,6 +503,7 @@ class AppController:
|
||||
self._predefined_callbacks: dict[str, Callable[..., Any]] = {
|
||||
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file,
|
||||
'_set_env_var': lambda k, v: os.environ.update({k: v}),
|
||||
'_set_attr': lambda k, v: setattr(self, k, v),
|
||||
'_apply_preset': self._apply_preset,
|
||||
'_cb_save_preset': self._cb_save_preset,
|
||||
'_cb_delete_preset': self._cb_delete_preset,
|
||||
@@ -511,7 +537,20 @@ class AppController:
|
||||
"payload": status
|
||||
})
|
||||
|
||||
def _trigger_gui_refresh(self):
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({'action': 'set_comms_dirty'})
|
||||
self._pending_gui_tasks.append({'action': 'set_tool_log_dirty'})
|
||||
|
||||
def _process_pending_gui_tasks(self) -> None:
|
||||
# Periodic telemetry broadcast
|
||||
now = time.time()
|
||||
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server:
|
||||
if now - self._last_telemetry_time >= 1.0:
|
||||
self._last_telemetry_time = now
|
||||
metrics = self.perf_monitor.get_metrics()
|
||||
self.event_queue.websocket_server.broadcast("telemetry", metrics)
|
||||
|
||||
if not self._pending_gui_tasks:
|
||||
return
|
||||
sys.stderr.write(f"[DEBUG] _process_pending_gui_tasks: processing {len(self._pending_gui_tasks)} tasks\n")
|
||||
@@ -529,6 +568,10 @@ class AppController:
|
||||
# ...
|
||||
if action == "refresh_api_metrics":
|
||||
self._refresh_api_metrics(task.get("payload", {}), md_content=self.last_md or None)
|
||||
elif action == 'set_comms_dirty':
|
||||
self._comms_log_dirty = True
|
||||
elif action == 'set_tool_log_dirty':
|
||||
self._tool_log_dirty = True
|
||||
elif action == "set_ai_status":
|
||||
self.ai_status = task.get("payload", "")
|
||||
sys.stderr.write(f"[DEBUG] Updated ai_status via task to: {self.ai_status}\n")
|
||||
@@ -823,12 +866,17 @@ class AppController:
|
||||
self.ui_separate_tier2 = False
|
||||
self.ui_separate_tier3 = False
|
||||
self.ui_separate_tier4 = False
|
||||
self.ui_separate_external_tools = False
|
||||
self.config = models.load_config()
|
||||
path_info = paths.get_full_path_info()
|
||||
self.ui_logs_dir = str(path_info['logs_dir']['path'])
|
||||
self.ui_scripts_dir = str(path_info['scripts_dir']['path'])
|
||||
theme.load_from_config(self.config)
|
||||
ai_cfg = self.config.get("ai", {})
|
||||
self._current_provider = ai_cfg.get("provider", "gemini")
|
||||
self._current_model = ai_cfg.get("model", "gemini-2.5-flash-lite")
|
||||
self.temperature = ai_cfg.get("temperature", 0.0)
|
||||
self.top_p = ai_cfg.get("top_p", 1.0)
|
||||
self.max_tokens = ai_cfg.get("max_tokens", 8192)
|
||||
self.history_trunc_limit = ai_cfg.get("history_trunc_limit", 8000)
|
||||
projects_cfg = self.config.get("projects", {})
|
||||
@@ -858,6 +906,7 @@ class AppController:
|
||||
self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".")
|
||||
proj_meta = self.project.get("project", {})
|
||||
self.ui_project_git_dir = proj_meta.get("git_dir", "")
|
||||
self.ui_project_conductor_dir = self.project.get('conductor', {}).get('dir', 'conductor')
|
||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
||||
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
||||
@@ -873,10 +922,24 @@ class AppController:
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
|
||||
mcp_path = self.project.get('project', {}).get('mcp_config_path') or self.config.get('ai', {}).get('mcp_config_path')
|
||||
if mcp_path:
|
||||
mcp_p = Path(mcp_path)
|
||||
if not mcp_p.is_absolute() and self.active_project_path:
|
||||
mcp_p = Path(self.active_project_path).parent / mcp_path
|
||||
if mcp_p.exists():
|
||||
self.mcp_config = models.load_mcp_config(str(mcp_p))
|
||||
else:
|
||||
self.mcp_config = models.MCPConfiguration()
|
||||
else:
|
||||
self.mcp_config = models.MCPConfiguration()
|
||||
|
||||
from src.personas import PersonaManager
|
||||
self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
self._fetch_models(self.current_provider)
|
||||
|
||||
self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET') or ai_cfg.get("active_tool_preset")
|
||||
self.ui_active_bias_profile = ai_cfg.get("active_bias_profile")
|
||||
ai_client.set_tool_preset(self.ui_active_tool_preset)
|
||||
@@ -916,7 +979,16 @@ class AppController:
|
||||
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
|
||||
label = self.project.get("project", {}).get("name", "")
|
||||
session_logger.open_session(label=label)
|
||||
session_logger.reset_session(label=label)
|
||||
# Trigger auto-start of MCP servers
|
||||
self.event_queue.put('refresh_external_mcps', None)
|
||||
|
||||
async def refresh_external_mcps(self):
|
||||
await mcp_client.get_external_mcp_manager().stop_all()
|
||||
# Start servers with auto_start=True
|
||||
for name, cfg in self.mcp_config.mcpServers.items():
|
||||
if cfg.auto_start:
|
||||
await mcp_client.get_external_mcp_manager().add_server(cfg)
|
||||
|
||||
def cb_load_prior_log(self, path: Optional[str] = None) -> None:
|
||||
root = hide_tk_root()
|
||||
@@ -929,6 +1001,12 @@ class AppController:
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not self.is_viewing_prior_session:
|
||||
self._current_session_usage = copy.deepcopy(self.session_usage)
|
||||
self._current_mma_tier_usage = copy.deepcopy(self.mma_tier_usage)
|
||||
self._current_token_history = copy.deepcopy(self._token_history)
|
||||
self._current_session_start_time = self._session_start_time
|
||||
|
||||
log_path = Path(path)
|
||||
if log_path.is_dir():
|
||||
log_file = log_path / "comms.log"
|
||||
@@ -963,6 +1041,15 @@ class AppController:
|
||||
|
||||
entries = []
|
||||
disc_entries = []
|
||||
paired_tools = {}
|
||||
final_tool_calls = []
|
||||
new_token_history = []
|
||||
new_usage = {'input_tokens': 0, 'output_tokens': 0, 'cache_read_input_tokens': 0, 'cache_creation_input_tokens': 0, 'total_tokens': 0, 'last_latency': 0.0, 'percentage': 0.0}
|
||||
new_mma_usage = copy.deepcopy(self.mma_tier_usage)
|
||||
for t in new_mma_usage:
|
||||
new_mma_usage[t]['input'] = 0
|
||||
new_mma_usage[t]['output'] = 0
|
||||
|
||||
try:
|
||||
with open(log_file, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
@@ -975,6 +1062,47 @@ class AppController:
|
||||
payload = entry.get("payload", {})
|
||||
ts = entry.get("ts", "")
|
||||
|
||||
if kind == 'tool_call':
|
||||
tid = payload.get('id') or payload.get('call_id')
|
||||
script = payload.get('script') or json.dumps(payload.get('args', {}), indent=1)
|
||||
script = _resolve_log_ref(script, session_dir)
|
||||
entry_obj = {
|
||||
'source_tier': entry.get('source_tier', 'main'),
|
||||
'script': script,
|
||||
'result': '', # Waiting for result
|
||||
'ts': ts
|
||||
}
|
||||
if tid:
|
||||
paired_tools[tid] = entry_obj
|
||||
final_tool_calls.append(entry_obj)
|
||||
elif kind == 'tool_result':
|
||||
tid = payload.get('id') or payload.get('call_id')
|
||||
output = payload.get('output', payload.get('content', ''))
|
||||
output = _resolve_log_ref(output, session_dir)
|
||||
if tid and tid in paired_tools:
|
||||
paired_tools[tid]['result'] = output
|
||||
else:
|
||||
# Fallback: if no ID, try matching last entry in final_tool_calls that has no result
|
||||
for old_call in reversed(final_tool_calls):
|
||||
if not old_call['result']:
|
||||
old_call['result'] = output
|
||||
break
|
||||
|
||||
if kind == 'response' and 'usage' in payload:
|
||||
u = payload['usage']
|
||||
for k in ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'total_tokens']:
|
||||
if k in new_usage: new_usage[k] += u.get(k, 0) or 0
|
||||
tier = entry.get('source_tier', 'main')
|
||||
if tier in new_mma_usage:
|
||||
new_mma_usage[tier]['input'] += u.get('input_tokens', 0) or 0
|
||||
new_mma_usage[tier]['output'] += u.get('output_tokens', 0) or 0
|
||||
new_token_history.append({
|
||||
'time': ts,
|
||||
'input': u.get('input_tokens', 0) or 0,
|
||||
'output': u.get('output_tokens', 0) or 0,
|
||||
'model': entry.get('model', 'unknown')
|
||||
})
|
||||
|
||||
if kind == "history_add":
|
||||
content = payload.get("content", payload.get("text", payload.get("message", "")))
|
||||
content = _resolve_log_ref(content, session_dir)
|
||||
@@ -1031,11 +1159,47 @@ class AppController:
|
||||
self._set_status(f"log load error: {e}")
|
||||
return
|
||||
|
||||
self.session_usage = new_usage
|
||||
self.mma_tier_usage = new_mma_usage
|
||||
self._token_history = new_token_history
|
||||
if new_token_history:
|
||||
try:
|
||||
import datetime
|
||||
first_ts = new_token_history[0]['time']
|
||||
dt = datetime.datetime.strptime(first_ts, '%Y-%m-%dT%H:%M:%S')
|
||||
self._session_start_time = dt.timestamp()
|
||||
except:
|
||||
self._session_start_time = time.time()
|
||||
self.prior_session_entries = entries
|
||||
self.prior_disc_entries = disc_entries
|
||||
self.prior_tool_calls = final_tool_calls
|
||||
self.is_viewing_prior_session = True
|
||||
self._trigger_gui_refresh()
|
||||
self._set_status(f"viewing prior session: {session_dir.name} ({len(entries)} entries)")
|
||||
|
||||
|
||||
def cb_exit_prior_session(self):
|
||||
self.is_viewing_prior_session = False
|
||||
if self._current_session_usage:
|
||||
self.session_usage = self._current_session_usage
|
||||
self._current_session_usage = None
|
||||
if self._current_mma_tier_usage:
|
||||
self.mma_tier_usage = self._current_mma_tier_usage
|
||||
self._current_mma_tier_usage = None
|
||||
|
||||
if self._current_token_history is not None:
|
||||
self._token_history = self._current_token_history
|
||||
self._current_token_history = None
|
||||
if self._current_session_start_time is not None:
|
||||
self._session_start_time = self._current_session_start_time
|
||||
self._current_session_start_time = None
|
||||
|
||||
self.prior_session_entries.clear()
|
||||
self.prior_disc_entries.clear()
|
||||
self.prior_tool_calls.clear()
|
||||
self._trigger_gui_refresh()
|
||||
self._set_status('idle')
|
||||
|
||||
def cb_prune_logs(self) -> None:
|
||||
"""Manually triggers the log pruning process with aggressive thresholds."""
|
||||
self._set_status("Manual prune started (Age > 0d, Size < 100KB)...")
|
||||
@@ -1230,15 +1394,19 @@ class AppController:
|
||||
"action": "ticket_completed",
|
||||
"payload": payload
|
||||
})
|
||||
elif event_name == "refresh_external_mcps":
|
||||
import asyncio
|
||||
asyncio.run(self.refresh_external_mcps())
|
||||
|
||||
def _handle_request_event(self, event: events.UserRequestEvent) -> None:
|
||||
"""Processes a UserRequestEvent by calling the AI client."""
|
||||
self._set_status('sending...')
|
||||
ai_client.set_current_tier(None) # Ensure main discussion is untagged
|
||||
# Clear response area for new turn
|
||||
self.ai_response = ""
|
||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit, self.top_p)
|
||||
ai_client.set_agent_tools(self.ui_agent_tools)
|
||||
# Force update adapter path right before send to bypass potential duplication issues
|
||||
self._update_gcli_adapter(self.ui_gemini_cli_path)
|
||||
@@ -1490,6 +1658,9 @@ class AppController:
|
||||
self._current_provider = value
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider(value, self.current_model)
|
||||
self.available_models = self.all_available_models.get(value, [])
|
||||
if not self.available_models:
|
||||
self._fetch_models(value)
|
||||
self._token_stats = {}
|
||||
self._token_stats_dirty = True
|
||||
|
||||
@@ -1618,12 +1789,13 @@ class AppController:
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Context aggregation failure: {e}")
|
||||
user_msg = req.prompt
|
||||
base_dir = self.ui_files_base_dir
|
||||
base_dir = self.active_project_root
|
||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
temp = req.temperature if req.temperature is not None else self.temperature
|
||||
top_p = req.top_p if req.top_p is not None else self.top_p
|
||||
tokens = req.max_tokens if req.max_tokens is not None else self.max_tokens
|
||||
ai_client.set_model_params(temp, tokens, self.history_trunc_limit)
|
||||
ai_client.set_model_params(temp, tokens, self.history_trunc_limit, top_p)
|
||||
ai_client.set_agent_tools(self.ui_agent_tools)
|
||||
if req.auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
@@ -1725,7 +1897,7 @@ class AppController:
|
||||
return {
|
||||
"files": [f.get("path") if isinstance(f, dict) else str(f) for f in file_items],
|
||||
"screenshots": screenshots,
|
||||
"files_base_dir": self.ui_files_base_dir,
|
||||
"files_base_dir": self.active_project_root,
|
||||
"markdown": md,
|
||||
"discussion": disc_text
|
||||
}
|
||||
@@ -1749,7 +1921,6 @@ class AppController:
|
||||
|
||||
def _cb_project_save(self) -> None:
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
self._set_status("config saved")
|
||||
@@ -1765,10 +1936,14 @@ class AppController:
|
||||
self._set_status(f"project file not found: {path}")
|
||||
return
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
try:
|
||||
self.project = project_manager.load_project(path)
|
||||
self.active_project_path = path
|
||||
new_root = Path(path).parent
|
||||
self.preset_manager = presets.PresetManager(new_root)
|
||||
self.tool_preset_manager = tool_presets.ToolPresetManager(new_root)
|
||||
from src.personas import PersonaManager
|
||||
self.persona_manager = PersonaManager(new_root)
|
||||
except Exception as e:
|
||||
self._set_status(f"failed to load project: {e}")
|
||||
return
|
||||
@@ -1802,7 +1977,7 @@ class AppController:
|
||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
|
||||
# MMA Tracks
|
||||
self.tracks = project_manager.get_all_tracks(self.ui_files_base_dir)
|
||||
self.tracks = project_manager.get_all_tracks(self.active_project_root)
|
||||
# Restore MMA state
|
||||
mma_sec = proj.get("mma", {})
|
||||
self.ui_epic_input = mma_sec.get("epic", "")
|
||||
@@ -1832,18 +2007,19 @@ class AppController:
|
||||
self.active_tickets = []
|
||||
# Load track-scoped history if track is active
|
||||
if self.active_track:
|
||||
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
|
||||
track_history = project_manager.load_track_history(self.active_track.id, self.active_project_root)
|
||||
if track_history:
|
||||
with self._disc_entries_lock:
|
||||
self.disc_entries = models.parse_history_entries(track_history, self.disc_roles)
|
||||
|
||||
self.preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||
self.preset_manager.project_root = Path(self.active_project_root)
|
||||
self.presets = self.preset_manager.load_all()
|
||||
self.tool_preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||
self.tool_preset_manager.project_root = Path(self.active_project_root)
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
|
||||
def _apply_preset(self, name: str, scope: str) -> None:
|
||||
print(f"[DEBUG] _apply_preset: name={name}, scope={scope}")
|
||||
if name == "None":
|
||||
if scope == "global":
|
||||
self.ui_global_preset_name = ""
|
||||
@@ -1852,6 +2028,7 @@ class AppController:
|
||||
return
|
||||
preset = self.presets.get(name)
|
||||
if not preset:
|
||||
print(f"[DEBUG] _apply_preset: preset {name} not found in {list(self.presets.keys())}")
|
||||
return
|
||||
if scope == "global":
|
||||
self.ui_global_system_prompt = preset.system_prompt
|
||||
@@ -1859,23 +2036,18 @@ class AppController:
|
||||
else:
|
||||
self.ui_project_system_prompt = preset.system_prompt
|
||||
self.ui_project_preset_name = name
|
||||
if preset.temperature is not None:
|
||||
self.temperature = preset.temperature
|
||||
if preset.max_output_tokens is not None:
|
||||
self.max_tokens = preset.max_output_tokens
|
||||
|
||||
def _cb_save_preset(self, name, content, temp, top_p, max_tok, scope):
|
||||
def _cb_save_preset(self, name, content, scope):
|
||||
print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}")
|
||||
if not name or not name.strip():
|
||||
raise ValueError("Preset name cannot be empty or whitespace.")
|
||||
preset = models.Preset(
|
||||
name=name,
|
||||
system_prompt=content,
|
||||
temperature=temp,
|
||||
top_p=top_p,
|
||||
max_output_tokens=max_tok
|
||||
system_prompt=content
|
||||
)
|
||||
self.preset_manager.save_preset(preset, scope)
|
||||
self.presets = self.preset_manager.load_all()
|
||||
print(f"[DEBUG] _cb_save_preset: saved {name}, total presets now {len(self.presets)}")
|
||||
|
||||
def _cb_delete_preset(self, name, scope):
|
||||
self.preset_manager.delete_preset(name, scope)
|
||||
@@ -1900,14 +2072,15 @@ class AppController:
|
||||
|
||||
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None:
|
||||
self.persona_manager.save_persona(persona, scope)
|
||||
self.personas = self.persona_manager.load_all_personas()
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
def _cb_delete_persona(self, name: str, scope: str = "project") -> None:
|
||||
self.persona_manager.delete_persona(name, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
def _cb_delete_persona(self, persona_id: str, scope: str = "project") -> None:
|
||||
self.persona_manager.delete_persona(persona_id, scope)
|
||||
self.personas = self.persona_manager.load_all_personas()
|
||||
|
||||
def _cb_load_track(self, track_id: str) -> None:
|
||||
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
||||
state = project_manager.load_track_state(track_id, self.active_project_root)
|
||||
if state:
|
||||
try:
|
||||
# Convert list[Ticket] or list[dict] to list[Ticket] for Track object
|
||||
@@ -1925,7 +2098,7 @@ class AppController:
|
||||
# Keep dicts for UI table (or convert models.Ticket objects back to dicts if needed)
|
||||
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in tickets]
|
||||
# Load track-scoped history
|
||||
history = project_manager.load_track_history(track_id, self.ui_files_base_dir)
|
||||
history = project_manager.load_track_history(track_id, self.active_project_root)
|
||||
with self._disc_entries_lock:
|
||||
if history:
|
||||
self.disc_entries = models.parse_history_entries(history, self.disc_roles)
|
||||
@@ -1940,7 +2113,8 @@ class AppController:
|
||||
def _save_active_project(self) -> None:
|
||||
if self.active_project_path:
|
||||
try:
|
||||
project_manager.save_project(self.project, self.active_project_path)
|
||||
cleaned = project_manager.clean_nones(self.project)
|
||||
project_manager.save_project(cleaned, self.active_project_path)
|
||||
except Exception as e:
|
||||
self._set_status(f"save error: {e}")
|
||||
|
||||
@@ -1967,7 +2141,7 @@ class AppController:
|
||||
def _flush_disc_entries_to_project(self) -> None:
|
||||
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
|
||||
if self.active_track and self._track_discussion_active:
|
||||
project_manager.save_track_history(self.active_track.id, history_strings, self.ui_files_base_dir)
|
||||
project_manager.save_track_history(self.active_track.id, history_strings, self.active_project_root)
|
||||
return
|
||||
disc_sec = self.project.setdefault("discussion", {})
|
||||
discussions = disc_sec.setdefault("discussions", {})
|
||||
@@ -2137,7 +2311,7 @@ class AppController:
|
||||
file_path, definition, line = res
|
||||
user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```'
|
||||
|
||||
base_dir = self.ui_files_base_dir
|
||||
base_dir = self.active_project_root
|
||||
sys.stderr.write(f"[DEBUG] _do_generate success. Prompt: {user_msg[:50]}...\n")
|
||||
sys.stderr.flush()
|
||||
# Prepare event payload
|
||||
@@ -2160,7 +2334,7 @@ class AppController:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _recalculate_session_usage(self) -> None:
|
||||
usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "total_tokens": 0, "last_latency": 0.0}
|
||||
usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "total_tokens": 0, "last_latency": 0.0, "percentage": self.session_usage.get("percentage", 0.0)}
|
||||
for entry in ai_client.get_comms_log():
|
||||
if entry.get("kind") == "response" and "usage" in entry.get("payload", {}):
|
||||
u = entry["payload"]["usage"]
|
||||
@@ -2175,6 +2349,8 @@ class AppController:
|
||||
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
|
||||
if "latency" in payload:
|
||||
self.session_usage["last_latency"] = payload["latency"]
|
||||
if "usage" in payload and "percentage" in payload["usage"]:
|
||||
self.session_usage["percentage"] = payload["usage"]["percentage"]
|
||||
self._recalculate_session_usage()
|
||||
if md_content is not None:
|
||||
stats = ai_client.get_token_stats(md_content)
|
||||
@@ -2230,6 +2406,7 @@ class AppController:
|
||||
proj["screenshots"]["paths"] = self.screenshots
|
||||
proj.setdefault("project", {})
|
||||
proj["project"]["git_dir"] = self.ui_project_git_dir
|
||||
proj.setdefault("conductor", {})["dir"] = self.ui_project_conductor_dir
|
||||
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
||||
proj["project"]["main_context"] = self.ui_project_main_context
|
||||
proj["project"]["active_preset"] = self.ui_project_preset_name
|
||||
@@ -2255,11 +2432,15 @@ class AppController:
|
||||
else:
|
||||
mma_sec["active_track"] = None
|
||||
|
||||
cleaned_proj = project_manager.clean_nones(proj)
|
||||
project_manager.save_project(cleaned_proj, self.active_project_path)
|
||||
|
||||
def _flush_to_config(self) -> None:
|
||||
self.config["ai"] = {
|
||||
"provider": self.current_provider,
|
||||
"model": self.current_model,
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"history_trunc_limit": self.history_trunc_limit,
|
||||
"active_preset": self.ui_global_preset_name,
|
||||
@@ -2274,6 +2455,7 @@ class AppController:
|
||||
"separate_message_panel": getattr(self, "ui_separate_message_panel", False),
|
||||
"separate_response_panel": getattr(self, "ui_separate_response_panel", False),
|
||||
"separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False),
|
||||
"separate_external_tools": getattr(self, "ui_separate_external_tools", False),
|
||||
"separate_task_dag": self.ui_separate_task_dag,
|
||||
"separate_usage_analytics": self.ui_separate_usage_analytics,
|
||||
"separate_tier1": self.ui_separate_tier1,
|
||||
@@ -2290,7 +2472,6 @@ class AppController:
|
||||
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
|
||||
"""Returns (full_md, output_path, file_items, stable_md, discussion_text)."""
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
track_id = self.active_track.id if self.active_track else None
|
||||
@@ -2317,7 +2498,7 @@ class AppController:
|
||||
sys.stderr.flush()
|
||||
proj = project_manager.load_project(self.active_project_path)
|
||||
flat = project_manager.flat_config(self.project)
|
||||
file_items = aggregate.build_file_items(Path(self.ui_files_base_dir), flat.get("files", {}).get("paths", []))
|
||||
file_items = aggregate.build_file_items(Path(self.active_project_root), flat.get("files", {}).get("paths", []))
|
||||
|
||||
_t1_baseline = len(ai_client.get_comms_log())
|
||||
tracks = orchestrator_pm.generate_tracks(self.ui_epic_input, flat, file_items, history_summary=history)
|
||||
@@ -2369,7 +2550,7 @@ class AppController:
|
||||
for i, file_path in enumerate(files_to_scan):
|
||||
try:
|
||||
self._set_status(f"Phase 2: Scanning files ({i+1}/{len(files_to_scan)})...")
|
||||
abs_path = Path(self.ui_files_base_dir) / file_path
|
||||
abs_path = Path(self.active_project_root) / file_path
|
||||
if abs_path.exists() and abs_path.suffix == ".py":
|
||||
with open(abs_path, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
@@ -2404,6 +2585,7 @@ class AppController:
|
||||
# Use the active track object directly to start execution
|
||||
self._set_mma_status("running")
|
||||
engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode)
|
||||
self.engine = engine
|
||||
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id)
|
||||
full_md, _, _ = aggregate.run(flat)
|
||||
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
|
||||
@@ -2470,13 +2652,14 @@ class AppController:
|
||||
# Initialize track state in the filesystem
|
||||
meta = models.Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
|
||||
state = models.TrackState(metadata=meta, discussion=[], tasks=tickets)
|
||||
project_manager.save_track_state(track_id, state, self.ui_files_base_dir)
|
||||
project_manager.save_track_state(track_id, state, self.active_project_root)
|
||||
# Add to memory and notify UI
|
||||
self.tracks.append({"id": track_id, "title": title, "status": "todo"})
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({'action': 'refresh_from_project'})
|
||||
# 4. Initialize ConductorEngine and run loop
|
||||
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode)
|
||||
self.engine = engine
|
||||
# Use current full markdown context for the track execution
|
||||
track_id_param = track.id
|
||||
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param)
|
||||
@@ -2501,8 +2684,68 @@ class AppController:
|
||||
break
|
||||
self.event_queue.put("mma_skip", {"ticket_id": ticket_id})
|
||||
|
||||
def _spawn_worker(self, ticket_id: str, data: dict = None) -> None:
|
||||
"""Manually initiates a sub-agent execution for a ticket."""
|
||||
if self.engine:
|
||||
for t in self.active_track.tickets:
|
||||
if t.id == ticket_id:
|
||||
t.status = "todo"
|
||||
t.step_mode = False
|
||||
break
|
||||
self.engine.engine.auto_queue = True
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
|
||||
|
||||
def kill_worker(self, worker_id: str) -> None:
|
||||
"""Aborts a running worker."""
|
||||
if self.engine:
|
||||
self.engine.kill_worker(worker_id)
|
||||
|
||||
def pause_mma(self) -> None:
|
||||
"""Pauses the global MMA loop."""
|
||||
self.mma_step_mode = True
|
||||
if self.engine:
|
||||
self.engine.pause()
|
||||
|
||||
def resume_mma(self) -> None:
|
||||
"""Resumes the global MMA loop."""
|
||||
self.mma_step_mode = False
|
||||
if self.engine:
|
||||
self.engine.resume()
|
||||
|
||||
def inject_context(self, data: dict) -> None:
|
||||
"""Programmatic context injection."""
|
||||
file_path = data.get("file_path")
|
||||
if file_path:
|
||||
if not os.path.isabs(file_path):
|
||||
file_path = os.path.relpath(file_path, self.active_project_root)
|
||||
existing = next((f for f in self.files if (f.path if hasattr(f, "path") else str(f)) == file_path), None)
|
||||
if not existing:
|
||||
item = models.FileItem(path=file_path)
|
||||
self.files.append(item)
|
||||
self._refresh_from_project()
|
||||
|
||||
def mutate_dag(self, data: dict) -> None:
|
||||
"""Modifies task dependencies."""
|
||||
ticket_id = data.get("ticket_id")
|
||||
depends_on = data.get("depends_on")
|
||||
if ticket_id and depends_on is not None:
|
||||
for t in self.active_tickets:
|
||||
if t.get("id") == ticket_id:
|
||||
t["depends_on"] = depends_on
|
||||
break
|
||||
if self.active_track:
|
||||
for t in self.active_track.tickets:
|
||||
if t.id == ticket_id:
|
||||
t.depends_on = depends_on
|
||||
break
|
||||
if self.engine:
|
||||
from src.dag_engine import TrackDAG, ExecutionEngine
|
||||
self.engine.dag = TrackDAG(self.active_track.tickets)
|
||||
self.engine.engine = ExecutionEngine(self.engine.dag, auto_queue=self.engine.engine.auto_queue)
|
||||
self._push_mma_state_update()
|
||||
|
||||
def _cb_run_conductor_setup(self) -> None:
|
||||
base = paths.get_conductor_dir()
|
||||
base = paths.get_conductor_dir(project_path=self.active_project_root)
|
||||
if not base.exists():
|
||||
self.ui_conductor_setup_summary = f"Error: {base}/ directory not found."
|
||||
return
|
||||
@@ -2532,7 +2775,7 @@ class AppController:
|
||||
if not name: return
|
||||
date_suffix = datetime.now().strftime("%Y%m%d")
|
||||
track_id = f"{name.lower().replace(' ', '_')}_{date_suffix}"
|
||||
track_dir = paths.get_tracks_dir() / track_id
|
||||
track_dir = paths.get_track_state_dir(track_id, project_path=self.active_project_root)
|
||||
track_dir.mkdir(parents=True, exist_ok=True)
|
||||
spec_file = track_dir / "spec.md"
|
||||
with open(spec_file, "w", encoding="utf-8") as f:
|
||||
@@ -2551,7 +2794,7 @@ class AppController:
|
||||
"progress": 0.0
|
||||
}, f, indent=1)
|
||||
# Refresh tracks from disk
|
||||
self.tracks = project_manager.get_all_tracks(self.ui_files_base_dir)
|
||||
self.tracks = project_manager.get_all_tracks(self.active_project_root)
|
||||
|
||||
def _push_mma_state_update(self) -> None:
|
||||
if not self.active_track:
|
||||
@@ -2559,7 +2802,7 @@ class AppController:
|
||||
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects)
|
||||
self.active_track.tickets = [models.Ticket.from_dict(t) for t in self.active_tickets]
|
||||
# Save the state to disk
|
||||
existing = project_manager.load_track_state(self.active_track.id, self.ui_files_base_dir)
|
||||
existing = project_manager.load_track_state(self.active_track.id, self.active_project_root)
|
||||
meta = models.Metadata(
|
||||
id=self.active_track.id,
|
||||
name=self.active_track.description,
|
||||
@@ -2572,4 +2815,5 @@ class AppController:
|
||||
discussion=existing.discussion if existing else [],
|
||||
tasks=self.active_track.tickets
|
||||
)
|
||||
project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir)
|
||||
project_manager.save_track_state(self.active_track.id, state, self.active_project_root)
|
||||
|
||||
|
||||
@@ -63,3 +63,4 @@ def get_bg():
|
||||
if _bg is None:
|
||||
_bg = BackgroundShader()
|
||||
return _bg
|
||||
|
||||
|
||||
@@ -118,3 +118,4 @@ if __name__ == "__main__":
|
||||
test_skeletons = "class NewFeature: pass"
|
||||
tickets = generate_tickets(test_brief, test_skeletons)
|
||||
print(json.dumps(tickets, indent=2))
|
||||
|
||||
|
||||
@@ -59,3 +59,4 @@ def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
||||
return input_cost + output_cost
|
||||
|
||||
return 0.0
|
||||
|
||||
|
||||
@@ -193,3 +193,4 @@ class ExecutionEngine:
|
||||
ticket = self.dag.ticket_map.get(task_id)
|
||||
if ticket:
|
||||
ticket.status = status
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
import shutil
|
||||
import os
|
||||
|
||||
@@ -9,7 +9,7 @@ between the GUI main thread and background workers:
|
||||
- Thread-safe: Callbacks execute on emitter's thread
|
||||
- Example: ai_client.py emits 'request_start' and 'response_received' events
|
||||
|
||||
2. SyncEventQueue: Producer-consumer pattern via queue.Queue
|
||||
2. AsyncEventQueue: Producer-consumer pattern via queue.Queue
|
||||
- Used for: Decoupled task submission where consumer polls at its own pace
|
||||
- Thread-safe: Built on Python's thread-safe queue.Queue
|
||||
- Example: Background workers submit tasks, main thread drains queue
|
||||
@@ -21,16 +21,16 @@ between the GUI main thread and background workers:
|
||||
Integration Points:
|
||||
- ai_client.py: EventEmitter for API lifecycle events
|
||||
- gui_2.py: Consumes events via _process_event_queue()
|
||||
- multi_agent_conductor.py: Uses SyncEventQueue for state updates
|
||||
- multi_agent_conductor.py: Uses AsyncEventQueue for state updates
|
||||
- api_hooks.py: Pushes events to _api_event_queue for external visibility
|
||||
|
||||
Thread Safety:
|
||||
- EventEmitter: NOT thread-safe for concurrent on/emit (use from single thread)
|
||||
- SyncEventQueue: FULLY thread-safe (built on queue.Queue)
|
||||
- AsyncEventQueue: FULLY thread-safe (built on queue.Queue)
|
||||
- UserRequestEvent: Immutable, safe for concurrent access
|
||||
"""
|
||||
import queue
|
||||
from typing import Callable, Any, Dict, List, Tuple
|
||||
from typing import Callable, Any, Dict, List, Tuple, Optional
|
||||
|
||||
class EventEmitter:
|
||||
"""
|
||||
@@ -70,14 +70,16 @@ class EventEmitter:
|
||||
"""Clears all registered listeners."""
|
||||
self._listeners.clear()
|
||||
|
||||
class SyncEventQueue:
|
||||
class AsyncEventQueue:
|
||||
"""
|
||||
Synchronous event queue for decoupled communication using queue.Queue.
|
||||
(Named AsyncEventQueue for architectural consistency, but is synchronous)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initializes the SyncEventQueue with an internal queue.Queue."""
|
||||
"""Initializes the AsyncEventQueue with an internal queue.Queue."""
|
||||
self._queue: queue.Queue[Tuple[str, Any]] = queue.Queue()
|
||||
self.websocket_server: Optional[Any] = None
|
||||
|
||||
def put(self, event_name: str, payload: Any = None) -> None:
|
||||
"""
|
||||
@@ -88,6 +90,8 @@ class SyncEventQueue:
|
||||
payload: Optional data associated with the event.
|
||||
"""
|
||||
self._queue.put((event_name, payload))
|
||||
if self.websocket_server:
|
||||
self.websocket_server.broadcast("events", {"event": event_name, "payload": payload})
|
||||
|
||||
def get(self) -> Tuple[str, Any]:
|
||||
"""
|
||||
@@ -98,6 +102,15 @@ class SyncEventQueue:
|
||||
"""
|
||||
return self._queue.get()
|
||||
|
||||
def empty(self) -> bool:
|
||||
"""
|
||||
Checks if the queue is empty.
|
||||
|
||||
Returns:
|
||||
True if the queue is empty, False otherwise.
|
||||
"""
|
||||
return self._queue.empty()
|
||||
|
||||
def task_done(self) -> None:
|
||||
"""Signals that a formerly enqueued task is complete."""
|
||||
self._queue.task_done()
|
||||
@@ -106,6 +119,9 @@ class SyncEventQueue:
|
||||
"""Blocks until all items in the queue have been gotten and processed."""
|
||||
self._queue.join()
|
||||
|
||||
# Alias for backward compatibility
|
||||
SyncEventQueue = AsyncEventQueue
|
||||
|
||||
class UserRequestEvent:
|
||||
"""
|
||||
Payload for a user request event.
|
||||
|
||||
@@ -376,3 +376,4 @@ def evict(path: Path) -> None:
|
||||
def list_cached() -> List[Dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -189,3 +189,4 @@ class GeminiCliAdapter:
|
||||
"""
|
||||
total_chars = len("\n".join(contents))
|
||||
return total_chars // 4
|
||||
|
||||
|
||||
1098
src/gui_2.py
1098
src/gui_2.py
File diff suppressed because it is too large
Load Diff
@@ -115,3 +115,4 @@ class LogPruner:
|
||||
sys.stderr.write(f"[LogPruner] Error removing {resolved_path}: {e}\n")
|
||||
|
||||
self.log_registry.save_registry()
|
||||
|
||||
|
||||
@@ -301,3 +301,4 @@ class LogRegistry:
|
||||
})
|
||||
return old_sessions
|
||||
|
||||
|
||||
|
||||
@@ -166,3 +166,4 @@ def render_unindented(text: str) -> None:
|
||||
|
||||
def render_code(code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
|
||||
get_renderer().render_code(code, lang, context_id, block_idx)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# mcp_client.py
|
||||
# mcp_client.py
|
||||
"""
|
||||
MCP Client - Multi-tool filesystem and network operations with sandboxing.
|
||||
|
||||
@@ -53,6 +53,8 @@ See Also:
|
||||
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
from src import models
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any, cast
|
||||
import os
|
||||
@@ -782,10 +784,10 @@ def get_tree(path: str, max_depth: int = 2) -> str:
|
||||
entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")]
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == len(entries) - 1)
|
||||
connector = "└── " if is_last else "├── "
|
||||
connector = "└── " if is_last else "├── "
|
||||
lines.append(f"{prefix}{connector}{entry.name}")
|
||||
if entry.is_dir():
|
||||
extension = " " if is_last else "│ "
|
||||
extension = " " if is_last else "│ "
|
||||
lines.extend(_build_tree(entry, current_depth + 1, prefix + extension))
|
||||
return lines
|
||||
tree_lines = [f"{p.name}/"] + _build_tree(p, 1)
|
||||
@@ -915,6 +917,126 @@ def get_ui_performance() -> str:
|
||||
return f"ERROR: Failed to retrieve UI performance: {str(e)}"
|
||||
# ------------------------------------------------------------------ tool dispatch
|
||||
|
||||
class StdioMCPServer:
|
||||
def __init__(self, config: models.MCPServerConfig):
|
||||
self.config = config
|
||||
self.name = config.name
|
||||
self.proc = None
|
||||
self.tools = {}
|
||||
self._id_counter = 0
|
||||
self._pending_requests = {}
|
||||
self.status = 'idle'
|
||||
|
||||
def _get_id(self):
|
||||
self._id_counter += 1
|
||||
return self._id_counter
|
||||
|
||||
async def start(self):
|
||||
self.status = 'starting'
|
||||
self.proc = await asyncio.create_subprocess_exec(
|
||||
self.config.command,
|
||||
*self.config.args,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
asyncio.create_task(self._read_stderr())
|
||||
await self.list_tools()
|
||||
self.status = 'running'
|
||||
|
||||
async def stop(self):
|
||||
if self.proc:
|
||||
try:
|
||||
if self.proc.stdin:
|
||||
self.proc.stdin.close()
|
||||
await self.proc.stdin.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.proc.terminate()
|
||||
await self.proc.wait()
|
||||
except Exception:
|
||||
pass
|
||||
self.proc = None
|
||||
self.status = 'idle'
|
||||
|
||||
async def _read_stderr(self):
|
||||
while self.proc and not self.proc.stdout.at_eof():
|
||||
line = await self.proc.stderr.readline()
|
||||
if line:
|
||||
print(f'[MCP:{self.name}:err] {line.decode().strip()}')
|
||||
|
||||
async def _send_request(self, method: str, params: dict = None):
|
||||
req_id = self._get_id()
|
||||
request = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': req_id,
|
||||
'method': method,
|
||||
'params': params or {}
|
||||
}
|
||||
self.proc.stdin.write(json.dumps(request).encode() + b'\n')
|
||||
await self.proc.stdin.drain()
|
||||
|
||||
# Simplistic wait for response - in real use, we'd need a read loop
|
||||
# For now, we'll read one line and hope it's ours (fragile, but for MVP)
|
||||
line = await self.proc.stdout.readline()
|
||||
if line:
|
||||
resp = json.loads(line.decode())
|
||||
return resp.get('result')
|
||||
return None
|
||||
|
||||
async def list_tools(self):
|
||||
result = await self._send_request('tools/list')
|
||||
if result and 'tools' in result:
|
||||
for t in result['tools']:
|
||||
self.tools[t['name']] = t
|
||||
return self.tools
|
||||
|
||||
async def call_tool(self, name: str, arguments: dict):
|
||||
result = await self._send_request('tools/call', {'name': name, 'arguments': arguments})
|
||||
if result and 'content' in result:
|
||||
return '\n'.join([c.get('text', '') for c in result['content'] if c.get('type') == 'text'])
|
||||
return str(result)
|
||||
|
||||
class ExternalMCPManager:
|
||||
def __init__(self):
|
||||
self.servers = {}
|
||||
|
||||
async def add_server(self, config: models.MCPServerConfig):
|
||||
if config.url:
|
||||
# RemoteMCPServer placeholder
|
||||
return
|
||||
server = StdioMCPServer(config)
|
||||
await server.start()
|
||||
self.servers[config.name] = server
|
||||
|
||||
async def stop_all(self):
|
||||
for server in self.servers.values():
|
||||
await server.stop()
|
||||
self.servers = {}
|
||||
|
||||
def get_all_tools(self) -> dict:
|
||||
all_tools = {}
|
||||
for sname, server in self.servers.items():
|
||||
for tname, tool in server.tools.items():
|
||||
all_tools[tname] = {**tool, 'server': sname, 'server_status': server.status}
|
||||
return all_tools
|
||||
|
||||
def get_servers_status(self) -> dict[str, str]:
|
||||
return {name: server.status for name, server in self.servers.items()}
|
||||
|
||||
async def async_dispatch(self, tool_name: str, tool_input: dict) -> str:
|
||||
for server in self.servers.values():
|
||||
if tool_name in server.tools:
|
||||
return await server.call_tool(tool_name, tool_input)
|
||||
return f'Error: External tool {tool_name} not found.'
|
||||
|
||||
_external_mcp_manager = ExternalMCPManager()
|
||||
|
||||
def get_external_mcp_manager() -> ExternalMCPManager:
|
||||
global _external_mcp_manager
|
||||
return _external_mcp_manager
|
||||
|
||||
TOOL_NAMES: set[str] = {"read_file", "list_directory", "search_files", "get_file_summary", "py_get_skeleton", "py_get_code_outline", "py_get_definition", "get_git_diff", "web_search", "fetch_url", "get_ui_performance", "get_file_slice", "set_file_slice", "edit_file", "py_update_definition", "py_get_signature", "py_set_signature", "py_get_class_summary", "py_get_var_declaration", "py_set_var_declaration", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree"}
|
||||
|
||||
def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
@@ -987,17 +1109,29 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
return f"ERROR: unknown MCP tool '{tool_name}'"
|
||||
|
||||
async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
"""
|
||||
Dispatch an MCP tool call by name asynchronously. Returns the result as a string.
|
||||
"""
|
||||
# Run blocking I/O bound tools in a thread to allow parallel execution via asyncio.gather
|
||||
# Check native tools
|
||||
native_names = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
if tool_name in native_names:
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
|
||||
# Check external tools
|
||||
if tool_name in get_external_mcp_manager().get_all_tools():
|
||||
return await get_external_mcp_manager().async_dispatch(tool_name, tool_input)
|
||||
|
||||
return f'ERROR: unknown MCP tool {tool_name}'
|
||||
|
||||
|
||||
|
||||
def get_tool_schemas() -> list[dict[str, Any]]:
|
||||
"""Returns the list of tool specifications for the AI."""
|
||||
return list(MCP_TOOL_SPECS)
|
||||
res = list(MCP_TOOL_SPECS)
|
||||
manager = get_external_mcp_manager()
|
||||
for tname, tinfo in manager.get_all_tools().items():
|
||||
res.append({
|
||||
'name': tname,
|
||||
'description': tinfo.get('description', ''),
|
||||
'parameters': tinfo.get('inputSchema', {'type': 'object', 'properties': {}})
|
||||
})
|
||||
return res
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ tool schema helpers
|
||||
@@ -1466,3 +1600,4 @@ MCP_TOOL_SPECS: list[dict[str, Any]] = [
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -178,3 +178,4 @@ RULES:
|
||||
|
||||
Analyze this error and generate the patch:
|
||||
"""
|
||||
|
||||
|
||||
159
src/models.py
159
src/models.py
@@ -37,6 +37,8 @@ See Also:
|
||||
- src/project_manager.py for persistence layer
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import tomllib
|
||||
import datetime
|
||||
from dataclasses import dataclass, field
|
||||
@@ -46,6 +48,13 @@ from src.paths import get_config_path
|
||||
|
||||
CONFIG_PATH = get_config_path()
|
||||
|
||||
def _clean_nones(data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
return {k: _clean_nones(v) for k, v in data.items() if v is not None}
|
||||
elif isinstance(data, list):
|
||||
return [_clean_nones(v) for v in data if v is not None]
|
||||
return data
|
||||
|
||||
def load_config() -> dict[str, Any]:
|
||||
with open(CONFIG_PATH, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
@@ -53,6 +62,7 @@ def load_config() -> dict[str, Any]:
|
||||
def save_config(config: dict[str, Any]) -> None:
|
||||
import tomli_w
|
||||
import sys
|
||||
config = _clean_nones(config)
|
||||
sys.stderr.write(f"[DEBUG] Saving config. Theme: {config.get('theme')}\n")
|
||||
sys.stderr.flush()
|
||||
with open(CONFIG_PATH, "wb") as f:
|
||||
@@ -349,30 +359,17 @@ class FileItem:
|
||||
class Preset:
|
||||
name: str
|
||||
system_prompt: str
|
||||
temperature: Optional[float] = None
|
||||
top_p: Optional[float] = None
|
||||
max_output_tokens: Optional[int] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
res = {
|
||||
return {
|
||||
"system_prompt": self.system_prompt,
|
||||
}
|
||||
if self.temperature is not None:
|
||||
res["temperature"] = self.temperature
|
||||
if self.top_p is not None:
|
||||
res["top_p"] = self.top_p
|
||||
if self.max_output_tokens is not None:
|
||||
res["max_output_tokens"] = self.max_output_tokens
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: Dict[str, Any]) -> "Preset":
|
||||
return cls(
|
||||
name=name,
|
||||
system_prompt=data.get("system_prompt", ""),
|
||||
temperature=data.get("temperature"),
|
||||
top_p=data.get("top_p"),
|
||||
max_output_tokens=data.get("max_output_tokens"),
|
||||
)
|
||||
|
||||
@dataclass
|
||||
@@ -447,32 +444,48 @@ class BiasProfile:
|
||||
@dataclass
|
||||
class Persona:
|
||||
name: str
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
preferred_models: List[str] = field(default_factory=list)
|
||||
preferred_models: List[Dict[str, Any]] = field(default_factory=list)
|
||||
system_prompt: str = ''
|
||||
temperature: Optional[float] = None
|
||||
top_p: Optional[float] = None
|
||||
max_output_tokens: Optional[int] = None
|
||||
tool_preset: Optional[str] = None
|
||||
bias_profile: Optional[str] = None
|
||||
|
||||
@property
|
||||
def provider(self) -> Optional[str]:
|
||||
if not self.preferred_models: return None
|
||||
return self.preferred_models[0].get("provider")
|
||||
|
||||
@property
|
||||
def model(self) -> Optional[str]:
|
||||
if not self.preferred_models: return None
|
||||
return self.preferred_models[0].get("model")
|
||||
|
||||
@property
|
||||
def temperature(self) -> Optional[float]:
|
||||
if not self.preferred_models: return None
|
||||
return self.preferred_models[0].get("temperature")
|
||||
|
||||
@property
|
||||
def top_p(self) -> Optional[float]:
|
||||
if not self.preferred_models: return None
|
||||
return self.preferred_models[0].get("top_p")
|
||||
|
||||
@property
|
||||
def max_output_tokens(self) -> Optional[int]:
|
||||
if not self.preferred_models: return None
|
||||
return self.preferred_models[0].get("max_output_tokens")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
res = {
|
||||
"system_prompt": self.system_prompt,
|
||||
}
|
||||
if self.provider is not None:
|
||||
res["provider"] = self.provider
|
||||
if self.model is not None:
|
||||
res["model"] = self.model
|
||||
if self.preferred_models:
|
||||
res["preferred_models"] = self.preferred_models
|
||||
if self.temperature is not None:
|
||||
res["temperature"] = self.temperature
|
||||
if self.top_p is not None:
|
||||
res["top_p"] = self.top_p
|
||||
if self.max_output_tokens is not None:
|
||||
res["max_output_tokens"] = self.max_output_tokens
|
||||
processed = []
|
||||
for m in self.preferred_models:
|
||||
if isinstance(m, str):
|
||||
processed.append({"model": m})
|
||||
else:
|
||||
processed.append(m)
|
||||
res["preferred_models"] = processed
|
||||
if self.tool_preset is not None:
|
||||
res["tool_preset"] = self.tool_preset
|
||||
if self.bias_profile is not None:
|
||||
@@ -481,15 +494,87 @@ class Persona:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: Dict[str, Any]) -> "Persona":
|
||||
raw_models = data.get("preferred_models", [])
|
||||
parsed_models = []
|
||||
for m in raw_models:
|
||||
if isinstance(m, str):
|
||||
parsed_models.append({"model": m})
|
||||
else:
|
||||
parsed_models.append(m)
|
||||
|
||||
# Migration logic: merge legacy fields if they exist
|
||||
legacy = {}
|
||||
for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]:
|
||||
if data.get(k) is not None:
|
||||
legacy[k] = data[k]
|
||||
|
||||
if legacy:
|
||||
if not parsed_models:
|
||||
parsed_models.append(legacy)
|
||||
else:
|
||||
# Merge into first item if it's missing these specific legacy fields
|
||||
for k, v in legacy.items():
|
||||
if k not in parsed_models[0] or parsed_models[0][k] is None:
|
||||
parsed_models[0][k] = v
|
||||
|
||||
return cls(
|
||||
name=name,
|
||||
provider=data.get("provider"),
|
||||
model=data.get("model"),
|
||||
preferred_models=data.get("preferred_models", []),
|
||||
preferred_models=parsed_models,
|
||||
system_prompt=data.get("system_prompt", ""),
|
||||
temperature=data.get("temperature"),
|
||||
top_p=data.get("top_p"),
|
||||
max_output_tokens=data.get("max_output_tokens"),
|
||||
tool_preset=data.get("tool_preset"),
|
||||
bias_profile=data.get("bias_profile"),
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
name: str
|
||||
command: Optional[str] = None
|
||||
args: List[str] = field(default_factory=list)
|
||||
url: Optional[str] = None
|
||||
auto_start: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
res = {'auto_start': self.auto_start}
|
||||
if self.command: res['command'] = self.command
|
||||
if self.args: res['args'] = self.args
|
||||
if self.url: res['url'] = self.url
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: Dict[str, Any]) -> 'MCPServerConfig':
|
||||
return cls(
|
||||
name=name,
|
||||
command=data.get('command'),
|
||||
args=data.get('args', []),
|
||||
url=data.get('url'),
|
||||
auto_start=data.get('auto_start', False),
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class MCPConfiguration:
|
||||
mcpServers: Dict[str, MCPServerConfig] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'mcpServers': {name: cfg.to_dict() for name, cfg in self.mcpServers.items()}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'MCPConfiguration':
|
||||
raw_servers = data.get('mcpServers', {})
|
||||
parsed_servers = {
|
||||
name: MCPServerConfig.from_dict(name, cfg)
|
||||
for name, cfg in raw_servers.items()
|
||||
}
|
||||
return cls(mcpServers=parsed_servers)
|
||||
|
||||
def load_mcp_config(path: str) -> MCPConfiguration:
|
||||
if not os.path.exists(path):
|
||||
return MCPConfiguration()
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
return MCPConfiguration.from_dict(data)
|
||||
except Exception:
|
||||
return MCPConfiguration()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Key Components:
|
||||
|
||||
Architecture Integration:
|
||||
- Uses TrackDAG and ExecutionEngine from dag_engine.py
|
||||
- Communicates with GUI via SyncEventQueue
|
||||
- Communicates with GUI via AsyncEventQueue
|
||||
- Manages tier-specific token usage via update_usage()
|
||||
|
||||
Thread Safety:
|
||||
@@ -45,7 +45,7 @@ Thread Safety:
|
||||
- Abort events enable per-ticket cancellation
|
||||
|
||||
Integration:
|
||||
- Uses SyncEventQueue for state updates to the GUI
|
||||
- Uses AsyncEventQueue for state updates to the GUI
|
||||
- Uses ai_client.send() for LLM communication
|
||||
- Uses mcp_client for tool dispatch
|
||||
|
||||
@@ -123,7 +123,7 @@ class ConductorEngine:
|
||||
Orchestrates the execution of tickets within a track.
|
||||
"""
|
||||
|
||||
def __init__(self, track: Track, event_queue: Optional[events.SyncEventQueue] = None, auto_queue: bool = False) -> None:
|
||||
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None, auto_queue: bool = False) -> None:
|
||||
self.track = track
|
||||
self.event_queue = event_queue
|
||||
self.tier_usage = {
|
||||
@@ -343,12 +343,12 @@ class ConductorEngine:
|
||||
self._push_state(active_tier="Tier 2 (Tech Lead)")
|
||||
time.sleep(1)
|
||||
|
||||
def _queue_put(event_queue: events.SyncEventQueue, event_name: str, payload) -> None:
|
||||
"""Thread-safe helper to push an event to the SyncEventQueue from a worker thread."""
|
||||
def _queue_put(event_queue: events.AsyncEventQueue, event_name: str, payload) -> None:
|
||||
"""Thread-safe helper to push an event to the AsyncEventQueue from a worker thread."""
|
||||
if event_queue is not None:
|
||||
event_queue.put(event_name, payload)
|
||||
|
||||
def confirm_execution(payload: str, event_queue: events.SyncEventQueue, ticket_id: str) -> bool:
|
||||
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
|
||||
"""
|
||||
Pushes an approval request to the GUI and waits for response.
|
||||
"""
|
||||
@@ -370,7 +370,7 @@ def confirm_execution(payload: str, event_queue: events.SyncEventQueue, ticket_i
|
||||
return approved
|
||||
return False
|
||||
|
||||
def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.SyncEventQueue, ticket_id: str) -> Tuple[bool, str, str]:
|
||||
def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> Tuple[bool, str, str]:
|
||||
"""
|
||||
Pushes a spawn approval request to the GUI and waits for response.
|
||||
Returns (approved, modified_prompt, modified_context)
|
||||
@@ -409,7 +409,7 @@ def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.S
|
||||
return approved, modified_prompt, modified_context
|
||||
return False, prompt, context_md
|
||||
|
||||
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] | None = None, event_queue: events.SyncEventQueue | None = None, engine: Optional['ConductorEngine'] = None, md_content: str = "") -> None:
|
||||
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] | None = None, event_queue: events.AsyncEventQueue | None = None, engine: Optional['ConductorEngine'] = None, md_content: str = "") -> None:
|
||||
"""
|
||||
Simulates the lifecycle of a single agent working on a ticket.
|
||||
Calls the AI client and updates the ticket status based on the response.
|
||||
@@ -614,3 +614,4 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
||||
if event_queue:
|
||||
_queue_put(event_queue, "ticket_completed", {"ticket_id": ticket.id, "timestamp": time.time()})
|
||||
return response
|
||||
|
||||
|
||||
@@ -86,3 +86,4 @@ class NativeOrchestrator:
|
||||
"""Tier 4: Generate patch for error"""
|
||||
from src import ai_client
|
||||
return ai_client.run_tier4_patch_generation(error, file_context)
|
||||
|
||||
|
||||
@@ -125,3 +125,4 @@ if __name__ == "__main__":
|
||||
history = get_track_history_summary()
|
||||
tracks = generate_tracks("Implement a basic unit test for the ai_client.py module.", flat, file_items, history_summary=history)
|
||||
print(json.dumps(tracks, indent=2))
|
||||
|
||||
|
||||
@@ -87,3 +87,4 @@ def get_outline(path: Path, code: str) -> str:
|
||||
return outliner.outline(code)
|
||||
else:
|
||||
return f"Outlining not supported for {suffix} files yet."
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, Callable, List
|
||||
from typing import Optional, Callable, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
|
||||
100
src/paths.py
100
src/paths.py
@@ -6,29 +6,28 @@ This module provides centralized path resolution for all configurable paths in t
|
||||
|
||||
Environment Variables:
|
||||
SLOP_CONFIG: Path to config.toml
|
||||
SLOP_CONDUCTOR_DIR: Path to conductor directory
|
||||
SLOP_LOGS_DIR: Path to logs directory
|
||||
SLOP_SCRIPTS_DIR: Path to generated scripts directory
|
||||
|
||||
Configuration (config.toml):
|
||||
[paths]
|
||||
conductor_dir = "conductor"
|
||||
logs_dir = "logs/sessions"
|
||||
scripts_dir = "scripts/generated"
|
||||
|
||||
Path Functions:
|
||||
get_config_path() -> Path to config.toml
|
||||
get_conductor_dir() -> Path to conductor directory
|
||||
get_conductor_dir(project_path=None) -> Path to conductor directory
|
||||
get_logs_dir() -> Path to logs/sessions
|
||||
get_scripts_dir() -> Path to scripts/generated
|
||||
get_tracks_dir() -> Path to conductor/tracks
|
||||
get_track_state_dir(track_id) -> Path to conductor/tracks/<track_id>
|
||||
get_archive_dir() -> Path to conductor/archive
|
||||
get_tracks_dir(project_path=None) -> Path to conductor/tracks
|
||||
get_track_state_dir(track_id, project_path=None) -> Path to conductor/tracks/<track_id>
|
||||
get_archive_dir(project_path=None) -> Path to conductor/archive
|
||||
|
||||
Resolution Order:
|
||||
1. Check environment variable
|
||||
2. Check config.toml [paths] section
|
||||
3. Fall back to default
|
||||
1. Check project-specific manual_slop.toml (for conductor paths)
|
||||
2. Check environment variable (for logs/scripts)
|
||||
3. Check config.toml [paths] section (for logs/scripts)
|
||||
4. Fall back to default
|
||||
|
||||
Usage:
|
||||
from src.paths import get_logs_dir, get_scripts_dir
|
||||
@@ -44,16 +43,18 @@ See Also:
|
||||
from pathlib import Path
|
||||
import os
|
||||
import tomllib
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
def get_config_path() -> Path:
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
|
||||
|
||||
def get_global_presets_path() -> Path:
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml"))
|
||||
|
||||
def get_project_presets_path(project_root: Path) -> Path:
|
||||
return project_root / "project_presets.toml"
|
||||
|
||||
@@ -72,21 +73,50 @@ def get_project_personas_path(project_root: Path) -> Path:
|
||||
return project_root / "project_personas.toml"
|
||||
|
||||
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
p = None
|
||||
if env_var in os.environ:
|
||||
return Path(os.environ[env_var])
|
||||
p = Path(os.environ[env_var])
|
||||
else:
|
||||
try:
|
||||
with open(get_config_path(), "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
if "paths" in cfg and config_key in cfg["paths"]:
|
||||
return Path(cfg["paths"][config_key])
|
||||
except FileNotFoundError:
|
||||
p = Path(cfg["paths"][config_key])
|
||||
except (FileNotFoundError, tomllib.TOMLDecodeError):
|
||||
pass
|
||||
return Path(default)
|
||||
if p is None:
|
||||
p = Path(default)
|
||||
if not p.is_absolute():
|
||||
return root_dir / p
|
||||
return p
|
||||
|
||||
def get_conductor_dir() -> Path:
|
||||
if "conductor_dir" not in _RESOLVED:
|
||||
_RESOLVED["conductor_dir"] = _resolve_path("SLOP_CONDUCTOR_DIR", "conductor_dir", "conductor")
|
||||
return _RESOLVED["conductor_dir"]
|
||||
def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
|
||||
# Look for manual_slop.toml in project_root
|
||||
toml_path = project_root / 'manual_slop.toml'
|
||||
if not toml_path.exists(): return None
|
||||
try:
|
||||
with open(toml_path, 'rb') as f:
|
||||
data = tomllib.load(f)
|
||||
# Check [conductor] dir = '...'
|
||||
c_dir = data.get('conductor', {}).get('dir')
|
||||
if c_dir:
|
||||
p = Path(c_dir)
|
||||
if not p.is_absolute(): p = project_root / p
|
||||
return p.resolve()
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def get_conductor_dir(project_path: Optional[str] = None) -> Path:
|
||||
if not project_path:
|
||||
# Fallback for legacy/tests, but we should avoid this
|
||||
return Path('conductor').resolve()
|
||||
|
||||
project_root = Path(project_path).resolve()
|
||||
p = _get_project_conductor_dir_from_toml(project_root)
|
||||
if p: return p
|
||||
|
||||
return (project_root / "conductor").resolve()
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
if "logs_dir" not in _RESOLVED:
|
||||
@@ -98,14 +128,36 @@ def get_scripts_dir() -> Path:
|
||||
_RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated")
|
||||
return _RESOLVED["scripts_dir"]
|
||||
|
||||
def get_tracks_dir() -> Path:
|
||||
return get_conductor_dir() / "tracks"
|
||||
def get_tracks_dir(project_path: Optional[str] = None) -> Path:
|
||||
return get_conductor_dir(project_path) / "tracks"
|
||||
|
||||
def get_track_state_dir(track_id: str) -> Path:
|
||||
return get_tracks_dir() / track_id
|
||||
def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Path:
|
||||
return get_tracks_dir(project_path) / track_id
|
||||
|
||||
def get_archive_dir() -> Path:
|
||||
return get_conductor_dir() / "archive"
|
||||
def get_archive_dir(project_path: Optional[str] = None) -> Path:
|
||||
return get_conductor_dir(project_path) / "archive"
|
||||
|
||||
def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, Any]:
|
||||
if env_var in os.environ:
|
||||
return {'path': Path(os.environ[env_var]).resolve(), 'source': f'env:{env_var}'}
|
||||
try:
|
||||
with open(get_config_path(), 'rb') as f:
|
||||
cfg = tomllib.load(f)
|
||||
if 'paths' in cfg and config_key in cfg['paths']:
|
||||
p = Path(cfg['paths'][config_key])
|
||||
if not p.is_absolute():
|
||||
p = (Path(__file__).resolve().parent.parent / p).resolve()
|
||||
return {'path': p, 'source': 'config.toml'}
|
||||
except: pass
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
p = (root_dir / default).resolve()
|
||||
return {'path': p, 'source': 'default'}
|
||||
|
||||
def get_full_path_info() -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'),
|
||||
'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated')
|
||||
}
|
||||
|
||||
def reset_resolved() -> None:
|
||||
"""For testing only - clear cached resolutions."""
|
||||
|
||||
@@ -232,3 +232,4 @@ class PerformanceMonitor:
|
||||
self._stop_event.set()
|
||||
if self._cpu_thread.is_alive():
|
||||
self._cpu_thread.join(timeout=2.0)
|
||||
|
||||
|
||||
@@ -47,6 +47,21 @@ class PersonaManager:
|
||||
data["personas"][persona.name] = persona.to_dict()
|
||||
self._save_file(path, data)
|
||||
|
||||
def get_persona_scope(self, name: str) -> str:
|
||||
"""Returns the scope ('global' or 'project') of a persona by name."""
|
||||
if self.project_root:
|
||||
project_path = paths.get_project_personas_path(self.project_root)
|
||||
project_data = self._load_file(project_path)
|
||||
if name in project_data.get("personas", {}):
|
||||
return "project"
|
||||
|
||||
global_path = paths.get_global_personas_path()
|
||||
global_data = self._load_file(global_path)
|
||||
if name in global_data.get("personas", {}):
|
||||
return "global"
|
||||
|
||||
return "project"
|
||||
|
||||
def delete_persona(self, name: str, scope: str = "project") -> None:
|
||||
path = self._get_path(scope)
|
||||
data = self._load_file(path)
|
||||
@@ -67,3 +82,4 @@ class PersonaManager:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
|
||||
@@ -55,19 +55,32 @@ class PresetManager:
|
||||
data["presets"][preset.name] = preset.to_dict()
|
||||
self._save_file(path, data)
|
||||
|
||||
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||
"""Deletes a preset by name from the specified scope."""
|
||||
path = self.global_path if scope == "global" else self.project_path
|
||||
if not path:
|
||||
if scope == "project":
|
||||
raise ValueError("Project scope requested but no project_root provided.")
|
||||
path = self.global_path
|
||||
def delete_preset(self, name: str, scope: str) -> None:
|
||||
if scope == "project" and self.project_root:
|
||||
path = get_project_presets_path(self.project_root)
|
||||
else:
|
||||
path = get_global_presets_path()
|
||||
|
||||
data = self._load_file(path)
|
||||
if "presets" in data and name in data["presets"]:
|
||||
if name in data.get("presets", {}):
|
||||
del data["presets"][name]
|
||||
self._save_file(path, data)
|
||||
|
||||
def get_preset_scope(self, name: str) -> str:
|
||||
"""Returns the scope ('global' or 'project') of a preset by name."""
|
||||
if self.project_root:
|
||||
project_p = get_project_presets_path(self.project_root)
|
||||
project_data = self._load_file(project_p)
|
||||
if name in project_data.get("presets", {}):
|
||||
return "project"
|
||||
|
||||
global_p = get_global_presets_path()
|
||||
global_data = self._load_file(global_p)
|
||||
if name in global_data.get("presets", {}):
|
||||
return "global"
|
||||
|
||||
return "project"
|
||||
|
||||
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {"presets": {}}
|
||||
@@ -85,7 +98,7 @@ class PresetManager:
|
||||
|
||||
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||
if path.parent.exists() and path.parent.is_file():
|
||||
raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file. The project root seems to be a file.")
|
||||
raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file.")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(tomli_w.dumps(data).encode("utf-8"))
|
||||
|
||||
@@ -196,6 +196,7 @@ def save_project(proj: dict[str, Any], path: Union[str, Path], disc_data: Option
|
||||
disc_data = proj["discussion"]
|
||||
proj = dict(proj)
|
||||
del proj["discussion"]
|
||||
proj = clean_nones(proj)
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(proj, f)
|
||||
if disc_data:
|
||||
@@ -245,7 +246,7 @@ def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Pa
|
||||
"""
|
||||
Saves a TrackState object to conductor/tracks/<track_id>/state.toml.
|
||||
"""
|
||||
track_dir = Path(base_dir) / paths.get_track_state_dir(track_id)
|
||||
track_dir = paths.get_track_state_dir(track_id, project_path=str(base_dir))
|
||||
track_dir.mkdir(parents=True, exist_ok=True)
|
||||
state_file = track_dir / "state.toml"
|
||||
data = clean_nones(state.to_dict())
|
||||
@@ -257,7 +258,7 @@ def load_track_state(track_id: str, base_dir: Union[str, Path] = ".") -> Optiona
|
||||
Loads a TrackState object from conductor/tracks/<track_id>/state.toml.
|
||||
"""
|
||||
from src.models import TrackState
|
||||
state_file = Path(base_dir) / paths.get_track_state_dir(track_id) / "state.toml"
|
||||
state_file = paths.get_track_state_dir(track_id, project_path=str(base_dir)) / 'state.toml'
|
||||
if not state_file.exists():
|
||||
return None
|
||||
with open(state_file, "rb") as f:
|
||||
@@ -302,7 +303,7 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
|
||||
Handles missing or malformed metadata.json or state.toml by falling back
|
||||
to available info or defaults.
|
||||
"""
|
||||
tracks_dir = Path(base_dir) / paths.get_tracks_dir()
|
||||
tracks_dir = paths.get_tracks_dir(project_path=str(base_dir))
|
||||
if not tracks_dir.exists():
|
||||
return []
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -391,3 +392,4 @@ def calculate_track_progress(tickets: list) -> dict:
|
||||
"blocked": blocked,
|
||||
"todo": todo
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,11 @@ def close_session() -> None:
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update auto-whitelist on close: {e}")
|
||||
|
||||
def reset_session(label: Optional[str] = None) -> None:
|
||||
"""Closes the current session and opens a new one with the given label."""
|
||||
close_session()
|
||||
open_session(label)
|
||||
|
||||
def log_api_hook(method: str, path: str, payload: str) -> None:
|
||||
"""Log an API hook invocation."""
|
||||
if _api_fh is None:
|
||||
@@ -217,3 +222,4 @@ def log_cli_call(command: str, stdin_content: Optional[str], stdout_content: Opt
|
||||
_cli_fh.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -70,3 +70,4 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
|
||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
||||
thickness=1.0
|
||||
)
|
||||
|
||||
|
||||
@@ -90,3 +90,4 @@ def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[s
|
||||
if 'process' in locals() and process:
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True)
|
||||
return f"ERROR: {e}"
|
||||
|
||||
|
||||
@@ -190,3 +190,4 @@ def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
||||
summary = item.get("summary", "")
|
||||
parts.append(f"### `{path}`\n\n{summary}")
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
|
||||
@@ -388,3 +388,4 @@ def load_from_config(config: dict[str, Any]) -> None:
|
||||
if font_path:
|
||||
apply_font(font_path, font_size)
|
||||
set_scale(scale)
|
||||
|
||||
|
||||
@@ -392,3 +392,4 @@ def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
|
||||
# Sync tweaks
|
||||
tt.tweaks.rounding = 6.0
|
||||
return tt
|
||||
|
||||
|
||||
@@ -82,3 +82,4 @@ def apply_nerv() -> None:
|
||||
style.popup_border_size = 1.0
|
||||
style.child_border_size = 1.0
|
||||
style.tab_border_size = 1.0
|
||||
|
||||
|
||||
@@ -83,3 +83,4 @@ class AlertPulsing:
|
||||
alpha = 0.05 + 0.15 * ((math.sin(time.time() * 4.0) + 1.0) / 2.0)
|
||||
color = imgui.get_color_u32((1.0, 0.0, 0.0, alpha))
|
||||
draw_list.add_rect((0.0, 0.0), (width, height), color, 0.0, 0, 10.0)
|
||||
|
||||
|
||||
@@ -63,3 +63,4 @@ class ToolBiasEngine:
|
||||
lines.append(f"- {cat}: {mult}x")
|
||||
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
@@ -108,3 +108,4 @@ class ToolPresetManager:
|
||||
if "bias_profiles" in data and name in data["bias_profiles"]:
|
||||
del data["bias_profiles"][name]
|
||||
self._write_raw(path, data)
|
||||
|
||||
|
||||
2
test_presets.toml
Normal file
2
test_presets.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[presets.ModalPreset]
|
||||
system_prompt = "Modal Content"
|
||||
@@ -200,14 +200,29 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
||||
temp_workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create minimal project files to avoid cluttering root
|
||||
(temp_workspace / "manual_slop.toml").write_text("[project]\nname = 'TestProject'\n", encoding="utf-8")
|
||||
(temp_workspace / "manual_slop.toml").write_text("[project]\nname = 'TestProject'\n\n[conductor]\ndir = 'conductor'\n", encoding="utf-8")
|
||||
(temp_workspace / "conductor" / "tracks").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create a local config.toml in temp_workspace
|
||||
config_content = {
|
||||
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
||||
'projects': {
|
||||
'paths': [str((temp_workspace / 'manual_slop.toml').absolute())],
|
||||
'active': str((temp_workspace / 'manual_slop.toml').absolute())
|
||||
},
|
||||
'paths': {
|
||||
'logs_dir': str((temp_workspace / "logs").absolute()),
|
||||
'scripts_dir': str((temp_workspace / "scripts" / "generated").absolute())
|
||||
}
|
||||
}
|
||||
import tomli_w
|
||||
with open(temp_workspace / 'config.toml', 'wb') as f:
|
||||
tomli_w.dump(config_content, f)
|
||||
|
||||
# Resolve absolute paths for shared resources
|
||||
project_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
config_file = temp_workspace / "config.toml"
|
||||
if not config_file.exists():
|
||||
config_file = project_root / "config.toml"
|
||||
|
||||
cred_file = project_root / "credentials.toml"
|
||||
mcp_file = project_root / "mcp_env.toml"
|
||||
|
||||
|
||||
58
tests/test_ai_settings_layout.py
Normal file
58
tests/test_ai_settings_layout.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
import time
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
@pytest.mark.timeout(30)
|
||||
def test_change_provider_via_hook(live_gui) -> None:
|
||||
"""Verify that we can change the current provider via the API hook."""
|
||||
client = ApiHookClient()
|
||||
if not client.wait_for_server():
|
||||
pytest.fail("Server did not become ready")
|
||||
|
||||
# Change provider to 'anthropic'
|
||||
client.set_value('current_provider', 'anthropic')
|
||||
|
||||
# Wait for state to reflect change
|
||||
success = False
|
||||
state = {}
|
||||
for _ in range(20):
|
||||
state = client.get_gui_state()
|
||||
if state.get('current_provider') == 'anthropic':
|
||||
success = True
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
assert success, f"Provider did not update. Current state: {state}"
|
||||
|
||||
@pytest.mark.timeout(30)
|
||||
def test_set_params_via_custom_callback(live_gui) -> None:
|
||||
"""Verify we can use custom_callback to set temperature and max_tokens."""
|
||||
client = ApiHookClient()
|
||||
if not client.wait_for_server():
|
||||
pytest.fail("Server did not become ready")
|
||||
|
||||
# Set temperature via custom_callback using _set_attr
|
||||
client.post_gui({
|
||||
"action": "custom_callback",
|
||||
"callback": "_set_attr",
|
||||
"args": ["temperature", 0.85]
|
||||
})
|
||||
|
||||
# Set max_tokens via custom_callback using _set_attr
|
||||
client.post_gui({
|
||||
"action": "custom_callback",
|
||||
"callback": "_set_attr",
|
||||
"args": ["max_tokens", 1024]
|
||||
})
|
||||
|
||||
# Verify via get_gui_state
|
||||
success = False
|
||||
state = {}
|
||||
for _ in range(20):
|
||||
state = client.get_gui_state()
|
||||
if state.get('temperature') == 0.85 and state.get('max_tokens') == 1024:
|
||||
success = True
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
assert success, f"Params did not update via custom_callback. Got: {state}"
|
||||
21
tests/test_api_control_endpoints.py
Normal file
21
tests/test_api_control_endpoints.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_control_endpoints_exist():
|
||||
client = ApiHookClient()
|
||||
assert hasattr(client, "spawn_mma_worker")
|
||||
assert hasattr(client, "kill_mma_worker")
|
||||
assert hasattr(client, "pause_mma_pipeline")
|
||||
assert hasattr(client, "resume_mma_pipeline")
|
||||
assert hasattr(client, "inject_context")
|
||||
assert hasattr(client, "mutate_mma_dag")
|
||||
|
||||
def test_api_hook_client_control_methods_exist():
|
||||
client = ApiHookClient()
|
||||
assert callable(getattr(client, "spawn_mma_worker", None))
|
||||
assert callable(getattr(client, "kill_mma_worker", None))
|
||||
assert callable(getattr(client, "pause_mma_pipeline", None))
|
||||
assert callable(getattr(client, "resume_mma_pipeline", None))
|
||||
assert callable(getattr(client, "inject_context", None))
|
||||
assert callable(getattr(client, "mutate_mma_dag", None))
|
||||
@@ -36,6 +36,7 @@ def test_app_processes_new_actions() -> None:
|
||||
with patch('src.models.load_config', return_value={}), \
|
||||
patch('src.performance_monitor.PerformanceMonitor'), \
|
||||
patch('src.session_logger.open_session'), \
|
||||
patch('src.session_logger.reset_session'), \
|
||||
patch('src.app_controller.AppController._prune_old_logs'), \
|
||||
patch('src.app_controller.AppController._load_active_project'):
|
||||
app = gui_2.App()
|
||||
|
||||
36
tests/test_api_read_endpoints.py
Normal file
36
tests/test_api_read_endpoints.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app():
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.mma_streams = {"W1": {"status": "running", "logs": ["started"]}}
|
||||
self.active_tickets = []
|
||||
self.files = ["file1.py", "file2.py"]
|
||||
self.mma_tier_usage = {"Tier 1": {"input": 100, "output": 50, "model": "gemini"}}
|
||||
self.event_queue = type("MockQueue", (), {"_queue": type("Q", (), {"qsize": lambda s: 5})()})()
|
||||
self._gettable_fields = {"test_field": "test_attr"}
|
||||
self.test_attr = "hello"
|
||||
self.test_hooks_enabled = True
|
||||
self._pending_gui_tasks = []
|
||||
self._pending_gui_tasks_lock = None
|
||||
self.ai_status = "idle"
|
||||
return MockApp()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_mma_workers():
|
||||
client = ApiHookClient()
|
||||
# Set up client to talk to a locally mocked server or use a live fixture if available
|
||||
# For now, just test that the methods exist on ApiHookClient
|
||||
assert hasattr(client, "get_mma_workers")
|
||||
assert hasattr(client, "get_context_state")
|
||||
assert hasattr(client, "get_financial_metrics")
|
||||
assert hasattr(client, "get_system_telemetry")
|
||||
|
||||
def test_api_hook_client_methods_exist():
|
||||
client = ApiHookClient()
|
||||
assert callable(getattr(client, "get_mma_workers", None))
|
||||
assert callable(getattr(client, "get_context_state", None))
|
||||
assert callable(getattr(client, "get_financial_metrics", None))
|
||||
assert callable(getattr(client, "get_system_telemetry", None))
|
||||
106
tests/test_app_controller_mcp.py
Normal file
106
tests/test_app_controller_mcp.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src.app_controller import AppController
|
||||
from src import models
|
||||
|
||||
@pytest.fixture
|
||||
def controller(tmp_path):
|
||||
# Setup mock config and project files
|
||||
config_path = tmp_path / "config.toml"
|
||||
project_path = tmp_path / "project.toml"
|
||||
mcp_config_path = tmp_path / "mcp_config.json"
|
||||
|
||||
config_data = {
|
||||
"ai": {
|
||||
"mcp_config_path": str(mcp_config_path)
|
||||
},
|
||||
"projects": {
|
||||
"paths": [str(project_path)],
|
||||
"active": str(project_path)
|
||||
}
|
||||
}
|
||||
|
||||
project_data = {
|
||||
"project": {
|
||||
"name": "test-project",
|
||||
"mcp_config_path": "project_mcp.json" # Relative path
|
||||
}
|
||||
}
|
||||
|
||||
mcp_data = {
|
||||
"mcpServers": {
|
||||
"global-server": {"command": "echo"}
|
||||
}
|
||||
}
|
||||
|
||||
project_mcp_data = {
|
||||
"mcpServers": {
|
||||
"project-server": {"command": "echo"}
|
||||
}
|
||||
}
|
||||
|
||||
# We can't easily use models.save_config because it uses a hardcoded path
|
||||
# But AppController.init_state calls models.load_config() which uses CONFIG_PATH
|
||||
|
||||
return AppController()
|
||||
|
||||
def test_app_controller_mcp_loading(tmp_path, monkeypatch):
|
||||
# Mock CONFIG_PATH to point to our temp config
|
||||
config_file = tmp_path / "config.toml"
|
||||
monkeypatch.setattr(models, "CONFIG_PATH", str(config_file))
|
||||
|
||||
mcp_global_file = tmp_path / "mcp_global.json"
|
||||
mcp_global_file.write_text(json.dumps({"mcpServers": {"global": {"command": "echo"}}}))
|
||||
|
||||
config_content = f"""
|
||||
[ai]
|
||||
mcp_config_path = "{mcp_global_file.as_posix()}"
|
||||
[projects]
|
||||
paths = []
|
||||
active = ""
|
||||
"""
|
||||
config_file.write_text(config_content)
|
||||
|
||||
ctrl = AppController()
|
||||
# Mock _load_active_project to not do anything for now
|
||||
monkeypatch.setattr(ctrl, "_load_active_project", lambda: None)
|
||||
ctrl.project = {}
|
||||
|
||||
ctrl.init_state()
|
||||
|
||||
assert "global" in ctrl.mcp_config.mcpServers
|
||||
assert ctrl.mcp_config.mcpServers["global"].command == "echo"
|
||||
|
||||
def test_app_controller_mcp_project_override(tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.toml"
|
||||
monkeypatch.setattr(models, "CONFIG_PATH", str(config_file))
|
||||
|
||||
project_file = tmp_path / "project.toml"
|
||||
mcp_project_file = tmp_path / "mcp_project.json"
|
||||
mcp_project_file.write_text(json.dumps({"mcpServers": {"project": {"command": "echo"}}}))
|
||||
|
||||
config_content = f"""
|
||||
[ai]
|
||||
mcp_config_path = "non-existent.json"
|
||||
[projects]
|
||||
paths = ["{project_file.as_posix()}"]
|
||||
active = "{project_file.as_posix()}"
|
||||
"""
|
||||
config_file.write_text(config_content)
|
||||
|
||||
ctrl = AppController()
|
||||
ctrl.active_project_path = str(project_file)
|
||||
ctrl.project = {
|
||||
"project": {
|
||||
"mcp_config_path": "mcp_project.json"
|
||||
}
|
||||
}
|
||||
# Mock _load_active_project to keep our manual project dict
|
||||
monkeypatch.setattr(ctrl, "_load_active_project", lambda: None)
|
||||
|
||||
ctrl.init_state()
|
||||
|
||||
assert "project" in ctrl.mcp_config.mcpServers
|
||||
assert "non-existent" not in ctrl.mcp_config.mcpServers
|
||||
@@ -47,6 +47,7 @@ class TestArchBoundaryPhase2(unittest.TestCase):
|
||||
with patch('src.models.load_config', return_value={}), \
|
||||
patch('src.performance_monitor.PerformanceMonitor'), \
|
||||
patch('src.session_logger.open_session'), \
|
||||
patch('src.session_logger.reset_session'), \
|
||||
patch('src.app_controller.AppController._prune_old_logs'), \
|
||||
patch('src.app_controller.AppController._init_ai_and_hooks'):
|
||||
controller = AppController()
|
||||
@@ -69,6 +70,7 @@ class TestArchBoundaryPhase2(unittest.TestCase):
|
||||
with patch('src.models.load_config', return_value={}), \
|
||||
patch('src.performance_monitor.PerformanceMonitor'), \
|
||||
patch('src.session_logger.open_session'), \
|
||||
patch('src.session_logger.reset_session'), \
|
||||
patch('src.app_controller.AppController._prune_old_logs'), \
|
||||
patch('src.app_controller.AppController._init_ai_and_hooks'):
|
||||
controller = AppController()
|
||||
|
||||
55
tests/test_external_mcp.py
Normal file
55
tests/test_external_mcp.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import pytest
|
||||
from src import mcp_client
|
||||
from src import models
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_mcp_real_process():
|
||||
manager = mcp_client.ExternalMCPManager()
|
||||
|
||||
# Use our mock script
|
||||
mock_script = "scripts/mock_mcp_server.py"
|
||||
config = models.MCPServerConfig(
|
||||
name="real-mock",
|
||||
command="python",
|
||||
args=[mock_script]
|
||||
)
|
||||
|
||||
await manager.add_server(config)
|
||||
|
||||
try:
|
||||
tools = manager.get_all_tools()
|
||||
assert "echo" in tools
|
||||
assert tools["echo"]["server"] == "real-mock"
|
||||
|
||||
result = await manager.async_dispatch("echo", {"hello": "world"})
|
||||
assert "ECHO: {'hello': 'world'}" in result
|
||||
finally:
|
||||
await manager.stop_all()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tool_schemas_includes_external():
|
||||
manager = mcp_client.get_external_mcp_manager()
|
||||
# Reset manager
|
||||
await manager.stop_all()
|
||||
|
||||
mock_script = "scripts/mock_mcp_server.py"
|
||||
config = models.MCPServerConfig(
|
||||
name="test-server",
|
||||
command="python",
|
||||
args=[mock_script]
|
||||
)
|
||||
|
||||
await manager.add_server(config)
|
||||
|
||||
try:
|
||||
schemas = mcp_client.get_tool_schemas()
|
||||
echo_schema = next((s for s in schemas if s["name"] == "echo"), None)
|
||||
|
||||
assert echo_schema is not None
|
||||
assert echo_schema["description"] == "Echo input"
|
||||
assert echo_schema["parameters"] == {"type": "object"}
|
||||
finally:
|
||||
await manager.stop_all()
|
||||
67
tests/test_external_mcp_e2e.py
Normal file
67
tests/test_external_mcp_e2e.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from src.app_controller import AppController
|
||||
from src import mcp_client
|
||||
from src import ai_client
|
||||
from src import models
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_mcp_e2e_refresh_and_call(tmp_path, monkeypatch):
|
||||
# 1. Setup mock config and mock server script
|
||||
config_file = tmp_path / "config.toml"
|
||||
monkeypatch.setattr(models, "CONFIG_PATH", str(config_file))
|
||||
|
||||
mock_script = Path("scripts/mock_mcp_server.py").absolute()
|
||||
|
||||
mcp_config_file = tmp_path / "mcp_config.json"
|
||||
mcp_data = {
|
||||
"mcpServers": {
|
||||
"e2e-server": {
|
||||
"command": "python",
|
||||
"args": [str(mock_script)],
|
||||
"auto_start": True
|
||||
}
|
||||
}
|
||||
}
|
||||
mcp_config_file.write_text(json.dumps(mcp_data))
|
||||
|
||||
config_content = f"""
|
||||
[ai]
|
||||
mcp_config_path = "{mcp_config_file.as_posix()}"
|
||||
[projects]
|
||||
paths = []
|
||||
active = ""
|
||||
"""
|
||||
config_file.write_text(config_content)
|
||||
|
||||
# 2. Initialize AppController
|
||||
ctrl = AppController()
|
||||
monkeypatch.setattr(ctrl, "_load_active_project", lambda: None)
|
||||
ctrl.project = {}
|
||||
|
||||
# We need to mock start_services or just manually call what we need
|
||||
ctrl.init_state()
|
||||
|
||||
# Trigger refresh event manually (since we don't have the background thread running in unit test)
|
||||
await ctrl.refresh_external_mcps()
|
||||
|
||||
# 3. Verify tools are discovered
|
||||
manager = mcp_client.get_external_mcp_manager()
|
||||
tools = manager.get_all_tools()
|
||||
assert "echo" in tools
|
||||
|
||||
# 4. Mock pre_tool_callback to auto-approve
|
||||
mock_pre_tool = lambda desc, base, qa: "Approved"
|
||||
|
||||
# 5. Call execute_single_tool_call_async (via ai_client)
|
||||
name, cid, out, orig = await ai_client._execute_single_tool_call_async(
|
||||
"echo", {"message": "hello"}, "id1", ".", mock_pre_tool, None, 0
|
||||
)
|
||||
|
||||
assert "ECHO: {'message': 'hello'}" in out
|
||||
|
||||
# Cleanup
|
||||
await manager.stop_all()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user