docs(conductor): Expert-level architectural documentation refresh
This commit is contained in:
@@ -1,87 +1,72 @@
|
||||
# Guide: Architecture
|
||||
# Manual Slop: Architectural Technical Reference
|
||||
|
||||
Overview of the package design, state management, and code-path layout.
|
||||
A deep-dive into the asynchronous orchestration, state synchronization, and the "Linear Execution Clutch" of the Manual Slop engine. This document is designed to move the reader from a high-level mental model to a low-level implementation understanding.
|
||||
|
||||
---
|
||||
|
||||
The purpose of this software is to alleviate the pain points of using AI as a local co-pilot by encapsulating the workflow into a resilient, strictly controlled state machine. It manages context generation, API throttling, human-in-the-loop tool execution, and session-long logging.
|
||||
## 1. Philosophy: The Decoupled State Machine
|
||||
|
||||
There are two primary state boundaries used:
|
||||
Manual Slop is built on a single, core realization: **AI reasoning is high-latency and non-deterministic, while GUI interaction must be low-latency and responsive.**
|
||||
|
||||
* The GUI State (Main Thread, Retained-Mode via Dear PyGui)
|
||||
* The AI State (Daemon Thread, stateless execution loop)
|
||||
To solve this, the engine enforces a strict decoupling between three distinct boundaries:
|
||||
|
||||
All synchronization between these boundaries is managed via lock-protected queues and events.
|
||||
* **The GUI Boundary (Main Thread):** A retained-mode loop (ImGui) that must never block. It handles visual telemetry and user "Seal of Approval" actions.
|
||||
* **The AI Boundary (Daemon Threads):** Stateless execution loops that handle the "heavy lifting" of context aggregation, LLM communication, and tool reasoning.
|
||||
* **The Orchestration Boundary (Asyncio):** A background thread that manages the flow of data between the other two, ensuring thread-safe communication without blocking the UI.
|
||||
|
||||
## Code Paths
|
||||
---
|
||||
|
||||
### Lifetime & Application Boot
|
||||
## 2. System Lifetime & Initialization
|
||||
|
||||
The application lifetime is localized within App.run in gui_legacy.py.
|
||||
The application lifecycle, managed by `App` in `gui_2.py`, follows a precise sequence to ensure the environment is ready before the first frame:
|
||||
|
||||
1. __init__ parses the global config.toml (which sets the active provider, theme, and project paths).
|
||||
2. It immediately hands off to project_manager.py to deserialize the active <project>.toml which hydrates the session's files, discussion histories, and prompts.
|
||||
3. Dear PyGui's dpg contexts are bootstrapped with docking_viewport=True, allowing individual GUI panels to exist as native OS windows.
|
||||
4. The main thread enters a blocking while dpg.is_dearpygui_running() render loop.
|
||||
5. On shutdown (clean exit), it performs a dual-flush: _flush_to_project() commits the UI state back to the <project>.toml, and _flush_to_config() commits the global state to config.toml. The viewport layout is automatically serialized to dpg_layout.ini.
|
||||
1. **Context Hydration:** The engine reads `config.toml` (global) and `<project>.toml` (local). This builds the initial "world view" of the project—what files are tracked, what the discussion history is, and which AI models are active.
|
||||
2. **Thread Bootstrapping:**
|
||||
* The `Asyncio` event loop thread is started (`_loop_thread`).
|
||||
* The `HookServer` (FastAPI) is started as a daemon to handle IPC.
|
||||
3. **UI Entry:** The main thread enters `immapp.run()`. At this point, the GUI is "alive," and the background threads are ready to receive tasks.
|
||||
4. **The Dual-Flush Shutdown:** On exit, the system commits state back to both project and global configs. This ensures that your window positions, active discussions, and even pending tool results are preserved for the next session.
|
||||
|
||||
### Context Shaping & Aggregation
|
||||
---
|
||||
|
||||
Before making a call to an AI Provider, the current state of the workspace is resolved into a dense Markdown representation.
|
||||
This occurs inside aggregate.run.
|
||||
## 3. The Task Pipeline: Producer-Consumer Synchronization
|
||||
|
||||
If using the default workflow, aggregate.py hashes through the following process:
|
||||
Because ImGui state cannot be safely modified from a background thread, Manual Slop uses a **Producer-Consumer** model for all updates.
|
||||
|
||||
1. **Glob Resolution:** Iterates through config["files"]["paths"] and unpacks any wildcards (e.g., src/**/*.rs) against the designated base_dir.
|
||||
2. **File Item Build:** `build_file_items()` reads each resolved file once, storing path, content, and `mtime`. This list is returned alongside the markdown so `ai_client.py` can use it for dynamic context refresh after tool calls without re-reading from disk.
|
||||
3. **Markdown Generation:** `build_markdown_from_items()` assembles the final `<project>_00N.md` string. By default (`summary_only=False`) it inlines full file contents. If `summary_only=True`, it delegates to `summarize.build_summary_markdown()` which uses AST-based heuristics to produce compact structural summaries instead.
|
||||
4. The Markdown file is persisted to disk (`./md_gen/` by default) for auditing. `run()` returns a 3-tuple `(markdown_str, output_path, file_items)`.
|
||||
### The Flow of an AI Request
|
||||
1. **Produce:** When you click "Gen + Send," the GUI thread produces a `UserRequestEvent` and pushes it into the `AsyncEventQueue`.
|
||||
2. **Consume:** The background `asyncio` loop pops this event and dispatches it to the `ai_client`. The GUI thread remains free to render and respond to other inputs.
|
||||
3. **Task Backlog:** When the AI responds, the background thread *cannot* update the UI text boxes directly. Instead, it appends a **Task Dictionary** to the `_pending_gui_tasks` list.
|
||||
4. **Sync:** On every frame, the GUI thread checks this list. If tasks exist, it acquires a lock, clears the list, and executes the updates (e.g., "Set AI response text," "Blink the terminal indicator").
|
||||
|
||||
### AI Communication & The Tool Loop
|
||||
---
|
||||
|
||||
The communication model is unified under ai_client.py, which normalizes the Gemini and Anthropic SDKs into a singular interface send(md_content, user_message, base_dir, file_items).
|
||||
## 4. The Execution Clutch: Human-In-The-Loop (HITL)
|
||||
|
||||
The loop is defined as follows:
|
||||
The "Execution Clutch" is our answer to the "Black Box" problem of AI. It allows you to shift from automatic execution to a manual, deterministic step-through mode.
|
||||
|
||||
1. **Prompt Injection:** The aggregated Markdown context and system prompt are injected. For Gemini, the system_instruction and tools are stored in an explicit cache via `client.caches.create()` with a 1-hour TTL; if cache creation fails (under minimum token threshold), it falls back to inline system_instruction. When context changes mid-session, the old cache is deleted and a new one is created. For Anthropic, the system prompt + context are sent as `system=` blocks with `cache_control: ephemeral` on the last chunk, and tools carry `cache_control: ephemeral` on the last tool definition.
|
||||
2. **Execution Loop:** A MAX_TOOL_ROUNDS (default 10) bounded loop begins. The tools list for Anthropic is built once per session and reused.
|
||||
3. The AI provider is polled.
|
||||
4. If the provider's stop_reason is tool_use:
|
||||
1. The loop parses the requested tool (either a read-only MCP tool or the destructive PowerShell tool).
|
||||
2. If PowerShell, it dispatches a blocking event to the Main Thread (see *On Tool Execution & Concurrency*).
|
||||
3. Once the last tool result in the batch is retrieved, the loop executes a **Dynamic Refresh** (`_reread_file_items`). Any files currently tracked by the project are pulled from disk fresh. The `file_items` variable is reassigned so subsequent tool rounds see the updated content.
|
||||
4. For Anthropic: the refreshed file contents are appended as a text block to the tool_results user message. For Gemini: the refreshed contents are appended to the last function response's output string. In both cases, the block is prefixed with `[FILES UPDATED]` / `[SYSTEM: FILES UPDATED]`.
|
||||
5. On subsequent rounds, stale file-refresh blocks from previous turns are stripped from history to prevent token accumulation. For Gemini, old tool outputs exceeding `_history_trunc_limit` characters are also truncated.
|
||||
5. Once the model outputs standard text, the loop terminates and yields the string back to the GUI callback.
|
||||
### How the "Shifting" Works
|
||||
When the AI requests a destructive action (like running a PowerShell script), the background execution thread is **suspended** using a `threading.Condition`:
|
||||
|
||||
### On Tool Execution & Concurrency
|
||||
1. **The Pause:** The thread enters a `.wait()` state. It is physically blocked.
|
||||
2. **The Modal:** A task is sent to the GUI to open a modal dialog.
|
||||
3. **The Mutation:** The user can read the script, edit it, or reject it.
|
||||
4. **The Unleash:** When the user clicks "Approve," the GUI thread updates the shared state and calls `.notify_all()`. The background thread "wakes up," executes the (potentially modified) script, and reports the result back to the AI.
|
||||
|
||||
When the AI calls a safe MCP tool (like read_file or search_files), the daemon thread immediately executes it via mcp_client.py and returns the result.
|
||||
---
|
||||
|
||||
However, when the AI requests run_powershell, the operation halts:
|
||||
## 5. Security: The MCP Allowlist
|
||||
|
||||
1. The Daemon Thread instantiates a ConfirmDialog object containing the payload and calls .wait(). This blocks the thread on a threading.Event().
|
||||
2. The ConfirmDialog instance is safely placed in a _pending_dialog_lock.
|
||||
3. The Main Thread, during its next frame cycle, pops the dialog from the lock and renders an OS-level modal window using dpg.window(modal=True).
|
||||
4. The user can inspect the script, modify it in the text box, or reject it entirely.
|
||||
5. Upon the user clicking "Approve & Run", the main thread triggers the threading.Event, unblocking the Daemon Thread.
|
||||
6. The Daemon Thread passes the script to shell_runner.py, captures stdout, stderr, and exit_code, logs it to session_logger.py, and returns it to the LLM.
|
||||
To prevent "hallucinated" file access, every filesystem tool (read, list, search) is gated by the **MCP (Model Context Protocol) Bridge**:
|
||||
|
||||
### On Context History Pruning (Anthropic)
|
||||
* **Resolution:** Every path requested by the AI is resolved to an absolute path.
|
||||
* **Checking:** It is verified against the project's `base_dir`. If the AI tries to `read_file("C:/Windows/System32/...")`, the bridge intercepts the call and returns an `ACCESS DENIED` error to the model before the OS is ever touched.
|
||||
|
||||
Because the Anthropic API requires sending the entire conversation history on every request, long sessions will inevitably hit the invalid_request_error: prompt is too long.
|
||||
---
|
||||
|
||||
To solve this, ai_client.py implements an aggressive pruning algorithm:
|
||||
|
||||
1. _strip_stale_file_refreshes: It recursively sweeps backward through the history dict and strips out large [FILES UPDATED] data blocks from old turns, preserving only the most recent snapshot.
|
||||
2. _trim_anthropic_history: If the estimated token count still exceeds _ANTHROPIC_MAX_PROMPT_TOKENS (~180,000), it slices off the oldest user/assistant message pairs from the beginning of the history array.
|
||||
3. The loop guarantees that at least the System prompt, Tool Definitions, and the final user prompt are preserved.
|
||||
|
||||
### Session Persistence
|
||||
|
||||
All I/O bound session data is recorded sequentially. session_logger.py hooks into the execution loops and records:
|
||||
|
||||
- logs/comms_<ts>.log: A JSON-L structured timeline of every raw payload sent/received.
|
||||
- logs/toolcalls_<ts>.log: A sequential markdown record detailing every AI tool invocation and its exact stdout result.
|
||||
- scripts/generated/: Every .ps1 script approved and executed by the shell runner is physically written to disk for version control transparency.
|
||||
## 6. Telemetry & Auditing
|
||||
|
||||
Every interaction in Manual Slop is designed to be auditable:
|
||||
* **JSON-L Comms Logs:** Raw API traffic is logged for debugging and token cost analysis.
|
||||
* **Generated Scripts:** Every script that passes through the "Clutch" is saved to `scripts/generated/`.
|
||||
* **Performance Monitor:** Real-time metrics (FPS, Frame Time, Input Lag) are tracked and can be queried via the Hook API to ensure the UI remains "fluid" under load.
|
||||
|
||||
Reference in New Issue
Block a user