docs: scrub gemini_cli references from 12 docs (provider count 8->7)

Cleaned: docs/guide_ai_client.md, docs/guide_architecture.md,
docs/guide_models.md, docs/guide_simulations.md,
docs/guide_context_aggregation.md, docs/guide_tools.md, docs/Readme.md,
conductor/tech-stack.md, conductor/product.md,
conductor/product-guidelines.md, conductor/workflow.md,
conductor/code_styleguides/error_handling.md.

Provider list citations updated to 7 (gemini, anthropic, deepseek,
minimax, qwen, grok, llama). guide_meta_boundary.md intentionally
retained (its gemini_cli references are the meta-tooling
GEMINI_CLI_HOOK_CONTEXT env var, NOT the provider; per spec GAP-A12).
This commit is contained in:
ed
2026-07-05 20:16:53 -04:00
parent be93c262e0
commit bd1d966c12
12 changed files with 1242 additions and 1242 deletions
+288 -288
View File
@@ -20,20 +20,20 @@ The codebase is organized into a `src/` layout to separate implementation from c
```
manual_slop/
├── conductor/ # Conductor tracks, specs, and plans
├── docs/ # Deep-dive architectural documentation
├── logs/ # Session logs, agent traces, and errors
├── scripts/ # Build, migration, and IPC bridge scripts
├── src/ # Core Python implementation
├── ai_client.py # LLM provider abstraction
├── gui_2.py # Main ImGui application
├── mcp_client.py # MCP tool implementation
└── ... # Other core modules
├── tests/ # Pytest suite and simulation fixtures
├── simulation/ # Workflow and agent simulation logic
├── sloppy.py # Primary application entry point
├── config.toml # Global application settings
└── manual_slop.toml # Project-specific configuration
├── conductor/ # Conductor tracks, specs, and plans
├── docs/ # Deep-dive architectural documentation
├── logs/ # Session logs, agent traces, and errors
├── scripts/ # Build, migration, and IPC bridge scripts
├── src/ # Core Python implementation
│ ├── ai_client.py # LLM provider abstraction
│ ├── gui_2.py # Main ImGui application
│ ├── mcp_client.py # MCP tool implementation
│ └── ... # Other core modules
├── tests/ # Pytest suite and simulation fixtures
├── simulation/ # Workflow and agent simulation logic
├── sloppy.py # Primary application entry point
├── config.toml # Global application settings
└── manual_slop.toml # Project-specific configuration
```
---
@@ -59,9 +59,9 @@ self._loop_thread.start()
# _run_event_loop:
def _run_event_loop(self) -> None:
asyncio.set_event_loop(self._loop)
self._loop.create_task(self._process_event_queue())
self._loop.run_forever()
asyncio.set_event_loop(self._loop)
self._loop.create_task(self._process_event_queue())
self._loop.run_forever()
```
The GUI thread uses `asyncio.run_coroutine_threadsafe(coro, self._loop)` to push work into this loop.
@@ -75,12 +75,12 @@ For concurrent multi-agent execution, the application uses `threading.local()` t
_local_storage = threading.local()
def get_current_tier() -> Optional[str]:
"""Returns the current tier from thread-local storage."""
return getattr(_local_storage, "current_tier", None)
"""Returns the current tier from thread-local storage."""
return getattr(_local_storage, "current_tier", None)
def set_current_tier(tier: Optional[str]) -> None:
"""Sets the current tier in thread-local storage."""
_local_storage.current_tier = tier
"""Sets the current tier in thread-local storage."""
_local_storage.current_tier = tier
```
This ensures that comms log entries and tool calls are correctly tagged with their source tier even when multiple workers execute concurrently.
@@ -96,10 +96,10 @@ All cross-thread communication uses one of three patterns:
```python
# events.py
class AsyncEventQueue:
_queue: asyncio.Queue # holds Tuple[str, Any] items
_queue: asyncio.Queue # holds Tuple[str, Any] items
async def put(self, event_name: str, payload: Any = None) -> None
async def get(self) -> Tuple[str, Any]
async def put(self, event_name: str, payload: Any = None) -> None
async def get(self) -> Tuple[str, Any]
```
The central event bus. Uses `asyncio.Queue`, so non-asyncio threads must enqueue via `asyncio.run_coroutine_threadsafe()`. Consumer is `App._process_event_queue()`, running as a long-lived coroutine on the asyncio loop.
@@ -125,8 +125,8 @@ self._pending_history_adds_lock = threading.Lock()
Additional locks:
```python
self._send_thread_lock = threading.Lock() # Guards send_thread creation
self._pending_dialog_lock = threading.Lock() # Guards _pending_dialog + _pending_actions dict
self._send_thread_lock = threading.Lock() # Guards send_thread creation
self._pending_dialog_lock = threading.Lock() # Guards _pending_dialog + _pending_actions dict
```
### Pattern C: Condition-Variable Dialogs (Bidirectional Blocking)
@@ -143,10 +143,10 @@ Three classes in `events.py` (89 lines, no external dependencies beyond `asyncio
```python
class EventEmitter:
_listeners: Dict[str, List[Callable]]
_listeners: Dict[str, List[Callable]]
def on(self, event_name: str, callback: Callable) -> None
def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None
def on(self, event_name: str, callback: Callable) -> None
def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None
```
Synchronous pub-sub. Callbacks execute in the caller's thread. Used by `ai_client.events` for lifecycle hooks (`request_start`, `response_received`, `tool_execution`). No thread safety — relies on consistent single-thread usage.
@@ -159,13 +159,13 @@ Described above in Pattern A.
```python
class UserRequestEvent:
prompt: str # User's raw input text
stable_md: str # Generated markdown context (files, screenshots)
file_items: List[Any] # File attachment items for dynamic refresh
disc_text: str # Serialized discussion history
base_dir: str # Working directory for shell commands
prompt: str # User's raw input text
stable_md: str # Generated markdown context (files, screenshots)
file_items: List[Any] # File attachment items for dynamic refresh
disc_text: str # Serialized discussion history
base_dir: str # Working directory for shell commands
def to_dict(self) -> Dict[str, Any]
def to_dict(self) -> Dict[str, Any]
```
Pure data carrier. Created on the GUI thread in `_handle_generate_send`, consumed on the asyncio thread in `_handle_request_event`.
@@ -180,8 +180,8 @@ The `App.__init__` (lines 152-296) follows this precise order:
1. **Config hydration**: Reads `config.toml` (global) and `<project>.toml` (local). Builds the initial "world view" — tracked files, discussion history, active models.
2. **Thread bootstrapping**:
- Asyncio event loop thread starts (`_loop_thread`).
- `HookServer` starts as a daemon if `test_hooks_enabled` or provider is `gemini_cli`.
- Asyncio event loop thread starts (`_loop_thread`).
- `HookServer` starts as a daemon if `test_hooks_enabled` or provider is `gemini_cli`.
3. **Callback wiring** (`_init_ai_and_hooks`): Connects `ai_client.confirm_and_run_callback`, `comms_log_callback`, `tool_log_callback` to GUI handlers.
4. **UI entry**: Main thread enters `immapp.run()`. GUI is now alive; background threads are ready.
@@ -199,10 +199,10 @@ The asyncio loop thread is a daemon — it dies with the process. `App.shutdown(
```python
def shutdown(self) -> None:
if self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
if self._loop_thread.is_alive():
self._loop_thread.join(timeout=2.0)
if self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
if self._loop_thread.is_alive():
self._loop_thread.join(timeout=2.0)
```
---
@@ -212,25 +212,25 @@ def shutdown(self) -> None:
### Request Flow
```
GUI Thread Asyncio Thread GUI Thread (next frame)
────────── ────────────── ──────────────────────
GUI Thread Asyncio Thread GUI Thread (next frame)
────────── ────────────── ──────────────────────
1. User clicks "Gen + Send"
2. _handle_generate_send():
- Compiles md context
- Creates UserRequestEvent
- Enqueues via
run_coroutine_threadsafe ──> 3. _process_event_queue():
awaits event_queue.get()
routes "user_request" to
_handle_request_event()
4. Configures ai_client
5. ai_client.send() BLOCKS
(seconds to minutes)
6. On completion, enqueues
"response" event back ──> 7. _process_pending_gui_tasks():
Drains task list under lock
Sets ai_response text
Triggers terminal blink
- Compiles md context
- Creates UserRequestEvent
- Enqueues via
run_coroutine_threadsafe ──> 3. _process_event_queue():
awaits event_queue.get()
routes "user_request" to
_handle_request_event()
4. Configures ai_client
5. ai_client.send() BLOCKS
(seconds to minutes)
6. On completion, enqueues
"response" event back ──> 7. _process_pending_gui_tasks():
Drains task list under lock
Sets ai_response text
Triggers terminal blink
```
### Event Types Routed by `_process_event_queue`
@@ -253,13 +253,13 @@ Called once per ImGui frame on the **main GUI thread**. This is the sole safe po
```python
def _process_pending_gui_tasks(self) -> None:
if not self._pending_gui_tasks:
return
with self._pending_gui_tasks_lock:
tasks = self._pending_gui_tasks[:] # Snapshot
self._pending_gui_tasks.clear() # Release lock fast
for task in tasks:
# Process each task outside the lock
if not self._pending_gui_tasks:
return
with self._pending_gui_tasks_lock:
tasks = self._pending_gui_tasks[:] # Snapshot
self._pending_gui_tasks.clear() # Release lock fast
for task in tasks:
# Process each task outside the lock
```
Acquires the lock briefly to snapshot the task list, then processes outside the lock. Minimizes lock contention with producer threads.
@@ -294,43 +294,43 @@ The "Execution Clutch" ensures every destructive AI action passes through an aud
```python
class ConfirmDialog:
_uid: str # uuid4 identifier
_script: str # The PowerShell script text (editable)
_base_dir: str # Working directory
_condition: threading.Condition # Blocking primitive
_done: bool # Signal flag
_approved: bool # User's decision
_uid: str # uuid4 identifier
_script: str # The PowerShell script text (editable)
_base_dir: str # Working directory
_condition: threading.Condition # Blocking primitive
_done: bool # Signal flag
_approved: bool # User's decision
def wait(self) -> tuple[bool, str] # Blocks until _done; returns (approved, script)
def wait(self) -> tuple[bool, str] # Blocks until _done; returns (approved, script)
```
**`MMAApprovalDialog`** — MMA tier step approval:
```python
class MMAApprovalDialog:
_ticket_id: str
_payload: str # The step payload (editable)
_condition: threading.Condition
_done: bool
_approved: bool
_ticket_id: str
_payload: str # The step payload (editable)
_condition: threading.Condition
_done: bool
_approved: bool
def wait(self) -> tuple[bool, str] # Returns (approved, payload)
def wait(self) -> tuple[bool, str] # Returns (approved, payload)
```
**`MMASpawnApprovalDialog`** — Sub-agent spawn approval:
```python
class MMASpawnApprovalDialog:
_ticket_id: str
_role: str # tier3-worker, tier4-qa, etc.
_prompt: str # Spawn prompt (editable)
_context_md: str # Context document (editable)
_condition: threading.Condition
_done: bool
_approved: bool
_abort: bool # Can abort entire track
_ticket_id: str
_role: str # tier3-worker, tier4-qa, etc.
_prompt: str # Spawn prompt (editable)
_context_md: str # Context document (editable)
_condition: threading.Condition
_done: bool
_approved: bool
_abort: bool # Can abort entire track
def wait(self) -> dict[str, Any] # Returns {approved, abort, prompt, context_md}
def wait(self) -> dict[str, Any] # Returns {approved, abort, prompt, context_md}
```
### Blocking Flow
@@ -338,27 +338,27 @@ class MMASpawnApprovalDialog:
Using `ConfirmDialog` as exemplar:
```
ASYNCIO THREAD (ai_client tool callback) GUI MAIN THREAD
───────────────────────────────────────── ───────────────
1. ai_client calls _confirm_and_run(script)
2. Creates ConfirmDialog(script, base_dir)
3. Stores dialog:
- Headless: _pending_actions[uid] = dialog
- GUI mode: _pending_dialog = dialog
4. If test_hooks_enabled:
pushes to _api_event_queue
5. dialog.wait() BLOCKS on _condition
6. Next frame: ImGui renders
_pending_dialog in modal
7. User clicks Approve/Reject
8. _handle_approve_script():
with dialog._condition:
dialog._approved = True
dialog._done = True
dialog._condition.notify_all()
9. wait() returns (True, potentially_edited_script)
10. Executes shell_runner.run_powershell()
11. Returns output to ai_client
ASYNCIO THREAD (ai_client tool callback) GUI MAIN THREAD
───────────────────────────────────────── ───────────────
1. ai_client calls _confirm_and_run(script)
2. Creates ConfirmDialog(script, base_dir)
3. Stores dialog:
- Headless: _pending_actions[uid] = dialog
- GUI mode: _pending_dialog = dialog
4. If test_hooks_enabled:
pushes to _api_event_queue
5. dialog.wait() BLOCKS on _condition
6. Next frame: ImGui renders
_pending_dialog in modal
7. User clicks Approve/Reject
8. _handle_approve_script():
with dialog._condition:
dialog._approved = True
dialog._done = True
dialog._condition.notify_all()
9. wait() returns (True, potentially_edited_script)
10. Executes shell_runner.run_powershell()
11. Returns output to ai_client
```
The `_condition.wait(timeout=0.1)` uses a 100ms polling interval inside a loop — a polling-with-condition hybrid that ensures the blocking thread wakes periodically.
@@ -373,14 +373,14 @@ The `_condition.wait(timeout=0.1)` uses a 100ms polling interval inside a loop
```python
def resolve_pending_action(self, action_id: str, approved: bool) -> bool:
with self._pending_dialog_lock:
if action_id in self._pending_actions:
dialog = self._pending_actions[action_id]
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
with self._pending_dialog_lock:
if action_id in self._pending_actions:
dialog = self._pending_actions[action_id]
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
```
**MMA approval path**:
@@ -395,14 +395,14 @@ def resolve_pending_action(self, action_id: str, approved: bool) -> bool:
### Module-Level State
```python
_provider: str = "gemini" # "gemini" | "anthropic" | "deepseek" | "gemini_cli" | "minimax"
_provider: str = "gemini" # "gemini" | "anthropic" | "deepseek" | "gemini_cli" | "minimax"
_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 # Char limit for truncating old tool outputs
_history_trunc_limit: int = 8000 # Char limit for truncating old tool outputs
_send_lock: threading.Lock # Serializes ALL send() calls across providers
_send_lock: threading.Lock # Serializes ALL send() calls across providers
```
Per-provider client objects:
@@ -410,16 +410,16 @@ Per-provider client objects:
```python
# Gemini (SDK-managed stateful chat)
_gemini_client: genai.Client | None
_gemini_chat: Any # Holds history internally
_gemini_cache: Any # Server-side CachedContent
_gemini_cache_md_hash: str | None # Hash for cache invalidation
_gemini_chat: Any # Holds history internally
_gemini_cache: Any # Server-side CachedContent
_gemini_cache_md_hash: str | None # Hash for cache invalidation
_gemini_cache_created_at: float | None # Monotonic time of cache creation
_gemini_cached_file_paths: list[str] # File paths included in the active cache
_GEMINI_CACHE_TTL: int = 3600 # 1-hour; rebuilt at 90% (3240s)
_gemini_cached_file_paths: list[str] # File paths included in the active cache
_GEMINI_CACHE_TTL: int = 3600 # 1-hour; rebuilt at 90% (3240s)
# Anthropic (client-managed history)
_anthropic_client: anthropic.Anthropic | None
_anthropic_history: list[dict] # Mutable [{role, content}, ...]
_anthropic_history: list[dict] # Mutable [{role, content}, ...]
_anthropic_history_lock: threading.Lock
# DeepSeek (raw HTTP, client-managed history)
@@ -439,27 +439,27 @@ _gemini_cli_adapter: GeminiCliAdapter | None
Safety limits:
```python
MAX_TOOL_ROUNDS: int = 10 # Max tool-call loop iterations per send()
_MAX_TOOL_OUTPUT_BYTES: int = 500_000 # 500KB cumulative tool output budget
_ANTHROPIC_CHUNK_SIZE: int = 120_000 # Max chars per system text block
_ANTHROPIC_MAX_PROMPT_TOKENS: int = 180_000 # 200k limit minus headroom
_GEMINI_MAX_INPUT_TOKENS: int = 900_000 # 1M window minus headroom
MAX_TOOL_ROUNDS: int = 10 # Max tool-call loop iterations per send()
_MAX_TOOL_OUTPUT_BYTES: int = 500_000 # 500KB cumulative tool output budget
_ANTHROPIC_CHUNK_SIZE: int = 120_000 # Max chars per system text block
_ANTHROPIC_MAX_PROMPT_TOKENS: int = 180_000 # 200k limit minus headroom
_GEMINI_MAX_INPUT_TOKENS: int = 900_000 # 1M window minus headroom
```
### The `send()` Dispatcher
```python
def send(md_content, user_message, base_dir=".", file_items=None,
discussion_history="", stream=False,
pre_tool_callback=None, qa_callback=None,
enable_tools=True, stream_callback=None, patch_callback=None,
rag_engine=None) -> str:
with _send_lock:
if _provider == "gemini": return _send_gemini(...)
elif _provider == "gemini_cli": return _send_gemini_cli(...)
elif _provider == "anthropic": return _send_anthropic(...)
elif _provider == "deepseek": return _send_deepseek(..., stream=stream)
elif _provider == "minimax": return _send_minimax(..., stream=stream)
discussion_history="", stream=False,
pre_tool_callback=None, qa_callback=None,
enable_tools=True, stream_callback=None, patch_callback=None,
rag_engine=None) -> str:
with _send_lock:
if _provider == "gemini": return _send_gemini(...)
elif _provider == "gemini_cli": return _send_gemini_cli(...)
elif _provider == "anthropic": return _send_anthropic(...)
elif _provider == "deepseek": return _send_deepseek(..., stream=stream)
elif _provider == "minimax": return _send_minimax(..., stream=stream)
```
`_send_lock` serializes all API calls — only one provider call can be in-flight at a time. All providers share the same callback signatures. Return type is always `str`.
@@ -496,11 +496,11 @@ All providers follow the same high-level loop, iterated up to `MAX_TOOL_ROUNDS +
3. Log to comms log; emit events.
4. If no function calls or max rounds exceeded: **break**.
5. For each function call:
- If `pre_tool_callback` rejects: return rejection text.
- Dispatch to `mcp_client.dispatch()` or `shell_runner.run_powershell()`.
- After the **last** call of this round: run `_reread_file_items()` for context refresh.
- Truncate tool output at `_history_trunc_limit` chars.
- Accumulate `_cumulative_tool_bytes`.
- If `pre_tool_callback` rejects: return rejection text.
- Dispatch to `mcp_client.dispatch()` or `shell_runner.run_powershell()`.
- After the **last** call of this round: run `_reread_file_items()` for context refresh.
- Truncate tool output at `_history_trunc_limit` chars.
- Accumulate `_cumulative_tool_bytes`.
6. If cumulative bytes > 500KB: inject warning.
7. Package tool results in provider-specific format; loop.
@@ -512,8 +512,8 @@ After the last tool call in each round, `_reread_file_items(file_items)` checks
2. If unchanged: pass through as-is.
3. If changed: re-read content, store `old_content` for diffing, update `mtime`.
4. Changed files are diffed via `_build_file_diff_text`:
- Files <= 200 lines: emit full content.
- Files > 200 lines with `old_content`: emit `difflib.unified_diff`.
- Files <= 200 lines: emit full content.
- Files > 200 lines with `old_content`: emit `difflib.unified_diff`.
5. Diff is appended to the last tool's output as `[SYSTEM: FILES UPDATED]\n\n{diff}`.
6. Stale `[FILES UPDATED]` blocks are stripped from older history turns by `_strip_stale_file_refreshes` to prevent context bloat.
@@ -550,27 +550,27 @@ Independent tool calls within a single round execute concurrently via `asyncio.g
```python
async def _execute_tool_calls_concurrently(
calls: list[Any],
base_dir: str,
pre_tool_callback: ...,
qa_callback: ...,
r_idx: int,
provider: str,
patch_callback: ... = None,
) -> list[tuple[str, str, str, str]]: # (tool_name, call_id, output, original_name)
...
calls: list[Any],
base_dir: str,
pre_tool_callback: ...,
qa_callback: ...,
r_idx: int,
provider: str,
patch_callback: ... = None,
) -> list[tuple[str, str, str, str]]: # (tool_name, call_id, output, original_name)
...
```
### Per-Call Worker
```python
async def _execute_single_tool_call_async(
name: str, args: dict, call_id: str, base_dir: str,
pre_tool_callback, qa_callback, r_idx: int,
tier: str | None = None,
patch_callback = None,
name: str, args: dict, call_id: str, base_dir: str,
pre_tool_callback, qa_callback, r_idx: int,
tier: str | None = None,
patch_callback = None,
) -> tuple[str, str, str, str]:
...
...
```
`tier: str | None` is propagated to the comms log and pre-tool callback so audit trails can attribute tool calls to a specific MMA tier (e.g., "Tier 3", "Tier 4"). Thread-local `_local_storage.current_tier` is the source; the parameter is the explicit pass-through.
@@ -587,11 +587,11 @@ If any individual call raises, `asyncio.gather` with `return_exceptions=True` co
```python
def send(md_content, user_message, base_dir=".", file_items=None, ...,
rag_engine: Optional[Any] = None) -> str:
if rag_engine is not None:
retrieved = rag_engine.query(user_message, top_k=5)
md_content = _inject_rag_context(md_content, retrieved)
...
rag_engine: Optional[Any] = None) -> str:
if rag_engine is not None:
retrieved = rag_engine.query(user_message, top_k=5)
md_content = _inject_rag_context(md_content, retrieved)
...
```
The RAG engine is **not** owned by `ai_client`; the caller (typically `AppController` for the main discussion flow, or `multi_agent_conductor.run_worker_lifecycle` for Tier 3 workers) is responsible for instantiating and configuring it. This keeps `ai_client` decoupled from any specific retrieval backend (ChromaDB local, external MCP RAG server, or none).
@@ -612,7 +612,7 @@ When a Tier 3 worker's test run fails, the engine can request a Tier 4 patch ins
```python
def run_tier4_patch_generation(error: str, file_context: str) -> str:
...
...
```
### Flow
@@ -637,7 +637,7 @@ Long discussions accumulate tool outputs and intermediate reasoning that bloat t
```python
def run_discussion_compression(discussion_text: str) -> str:
...
...
```
### Flow
@@ -649,7 +649,7 @@ def run_discussion_compression(discussion_text: str) -> str:
### Provider Robustness
The function tolerates case- and whitespace-variation in the provider string (`" MiniMax "` is normalized to `"minimax"`). This is important because the active provider may be set via different code paths (TOML, env var, runtime override).
The function tolerates case- and whitespace-variation in the provider string (`" MiniMax "` is normalized to `"minimax"`). This is important because the active provider may be set via different code paths (TOML, env var, runtime override).
---
@@ -661,7 +661,7 @@ For very large files, the heuristic `summarise_file` in `src/summarize.py` may b
```python
def run_subagent_summarization(file_path: str, content: str, is_code: bool, outline: str) -> str:
...
...
```
### When Invoked
@@ -688,17 +688,17 @@ Every API interaction is logged to a module-level list with real-time GUI push:
```python
def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None:
entry = {
"ts": datetime.now().strftime("%H:%M:%S"),
"direction": direction, # "OUT" (to API) or "IN" (from API)
"kind": kind, # "request" | "response" | "tool_call" | "tool_result"
"provider": _provider,
"model": _model,
"payload": payload,
}
_comms_log.append(entry)
if comms_log_callback:
comms_log_callback(entry) # Real-time push to GUI
entry = {
"ts": datetime.now().strftime("%H:%M:%S"),
"direction": direction, # "OUT" (to API) or "IN" (from API)
"kind": kind, # "request" | "response" | "tool_call" | "tool_result"
"provider": _provider,
"model": _model,
"payload": payload,
}
_comms_log.append(entry)
if comms_log_callback:
comms_log_callback(entry) # Real-time push to GUI
```
---
@@ -709,10 +709,10 @@ def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None:
```
"idle" -> "sending..." -> [AI call in progress]
-> "running powershell..." -> "powershell done, awaiting AI..."
-> "fetching url..." | "searching web..."
-> "done" | "error"
-> "idle" (on reset)
-> "running powershell..." -> "powershell done, awaiting AI..."
-> "fetching url..." | "searching web..."
-> "done" | "error"
-> "idle" (on reset)
```
### HITL Dialog State (Binary per type)
@@ -748,32 +748,32 @@ Every interaction is designed to be auditable:
```python
# Comms log entry (JSON-L)
{
"ts": "14:32:05",
"direction": "OUT",
"kind": "tool_call",
"provider": "gemini",
"model": "gemini-2.5-flash-lite",
"payload": {
"name": "run_powershell",
"id": "call_abc123",
"script": "Get-ChildItem"
},
"source_tier": "Tier 3",
"local_ts": 1709875925.123
"ts": "14:32:05",
"direction": "OUT",
"kind": "tool_call",
"provider": "gemini",
"model": "gemini-2.5-flash-lite",
"payload": {
"name": "run_powershell",
"id": "call_abc123",
"script": "Get-ChildItem"
},
"source_tier": "Tier 3",
"local_ts": 1709875925.123
}
# Performance metrics (via get_metrics())
{
"fps": 60.0,
"fps_avg": 58.5,
"last_frame_time_ms": 16.67,
"frame_time_ms_avg": 17.1,
"cpu_percent": 12.5,
"cpu_percent_avg": 15.2,
"input_lag_ms": 2.3,
"input_lag_ms_avg": 3.1,
"time_render_mma_dashboard_ms": 5.2,
"time_render_mma_dashboard_ms_avg": 4.8
"fps": 60.0,
"fps_avg": 58.5,
"last_frame_time_ms": 16.67,
"frame_time_ms_avg": 17.1,
"cpu_percent": 12.5,
"cpu_percent_avg": 15.2,
"input_lag_ms": 2.3,
"input_lag_ms_avg": 3.1,
"time_render_mma_dashboard_ms": 5.2,
"time_render_mma_dashboard_ms_avg": 4.8
}
```
@@ -787,30 +787,30 @@ The `WorkerPool` class in `multi_agent_conductor.py` manages a bounded pool of w
```python
class WorkerPool:
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
self._active: dict[str, threading.Thread] = {}
self._lock = threading.Lock()
self._semaphore = threading.Semaphore(max_workers)
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
self._active: dict[str, threading.Thread] = {}
self._lock = threading.Lock()
self._semaphore = threading.Semaphore(max_workers)
def spawn(self, ticket_id: str, target: Callable, args: tuple) -> Optional[threading.Thread]:
with self._lock:
if len(self._active) >= self.max_workers:
return None
def wrapper(*a, **kw):
try:
with self._semaphore:
target(*a, **kw)
finally:
with self._lock:
self._active.pop(ticket_id, None)
t = threading.Thread(target=wrapper, args=args, daemon=True)
with self._lock:
self._active[ticket_id] = t
t.start()
return t
def spawn(self, ticket_id: str, target: Callable, args: tuple) -> Optional[threading.Thread]:
with self._lock:
if len(self._active) >= self.max_workers:
return None
def wrapper(*a, **kw):
try:
with self._semaphore:
target(*a, **kw)
finally:
with self._lock:
self._active.pop(ticket_id, None)
t = threading.Thread(target=wrapper, args=args, daemon=True)
with self._lock:
self._active[ticket_id] = t
t.start()
return t
```
**Key behaviors**:
@@ -825,22 +825,22 @@ The `ConductorEngine` orchestrates ticket execution within a track:
```python
class ConductorEngine:
def __init__(self, track: Track, event_queue: Optional[SyncEventQueue] = None,
auto_queue: bool = False) -> None:
self.track = track
self.event_queue = event_queue
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
self.pool = WorkerPool(max_workers=4)
self._abort_events: dict[str, threading.Event] = {}
self._pause_event = threading.Event()
self._tier_usage_lock = threading.Lock()
self.tier_usage = {
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
}
def __init__(self, track: Track, event_queue: Optional[SyncEventQueue] = None,
auto_queue: bool = False) -> None:
self.track = track
self.event_queue = event_queue
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
self.pool = WorkerPool(max_workers=4)
self._abort_events: dict[str, threading.Event] = {}
self._pause_event = threading.Event()
self._tier_usage_lock = threading.Lock()
self.tier_usage = {
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
}
```
**Main execution loop** (`run` method):
@@ -864,17 +864,17 @@ self._abort_events[ticket.id] = threading.Event()
# Worker checks abort at three points:
# 1. Before major work
if abort_event.is_set():
ticket.status = "killed"
return "ABORTED"
ticket.status = "killed"
return "ABORTED"
# 2. Before tool execution (in clutch_callback)
if abort_event.is_set():
return False # Reject tool
return False # Reject tool
# 3. After blocking send() returns
if abort_event.is_set():
ticket.status = "killed"
return "ABORTED"
ticket.status = "killed"
return "ABORTED"
```
---
@@ -907,21 +907,21 @@ The `ProviderError` class provides structured error classification:
```python
class ProviderError(Exception):
def __init__(self, kind: str, provider: str, original: Exception):
self.kind = kind # "quota" | "rate_limit" | "auth" | "balance" | "network" | "unknown"
self.provider = provider
self.original = original
def ui_message(self) -> str:
labels = {
"quota": "QUOTA EXHAUSTED",
"rate_limit": "RATE LIMITED",
"auth": "AUTH / API KEY ERROR",
"balance": "BALANCE / BILLING ERROR",
"network": "NETWORK / CONNECTION ERROR",
"unknown": "API ERROR",
}
return f"[{self.provider.upper()} {labels.get(self.kind, 'API ERROR')}]\n\n{self.original}"
def __init__(self, kind: str, provider: str, original: Exception):
self.kind = kind # "quota" | "rate_limit" | "auth" | "balance" | "network" | "unknown"
self.provider = provider
self.original = original
def ui_message(self) -> str:
labels = {
"quota": "QUOTA EXHAUSTED",
"rate_limit": "RATE LIMITED",
"auth": "AUTH / API KEY ERROR",
"balance": "BALANCE / BILLING ERROR",
"network": "NETWORK / CONNECTION ERROR",
"unknown": "API ERROR",
}
return f"[{self.provider.upper()} {labels.get(self.kind, 'API ERROR')}]\n\n{self.original}"
```
### Error Recovery Patterns
@@ -944,29 +944,29 @@ class ProviderError(Exception):
**Gemini (40% threshold)**:
```python
if total_in > _GEMINI_MAX_INPUT_TOKENS * 0.4:
while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.3:
# Drop oldest message pairs
hist.pop(0) # Assistant
hist.pop(0) # User
while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.3:
# Drop oldest message pairs
hist.pop(0) # Assistant
hist.pop(0) # User
```
**Anthropic (180K limit)**:
```python
def _trim_anthropic_history(system_blocks, history):
est = _estimate_prompt_tokens(system_blocks, history)
while len(history) > 3 and est > _ANTHROPIC_MAX_PROMPT_TOKENS:
# Drop turn pairs, preserving tool_result chains
...
est = _estimate_prompt_tokens(system_blocks, history)
while len(history) > 3 and est > _ANTHROPIC_MAX_PROMPT_TOKENS:
# Drop turn pairs, preserving tool_result chains
...
```
### Tool Output Budget
```python
_MAX_TOOL_OUTPUT_BYTES: int = 500_000 # 500KB cumulative
_MAX_TOOL_OUTPUT_BYTES: int = 500_000 # 500KB cumulative
if _cumulative_tool_bytes > _MAX_TOOL_OUTPUT_BYTES:
# Inject warning, force final answer
parts.append("SYSTEM WARNING: Cumulative tool output exceeded 500KB budget.")
# Inject warning, force final answer
parts.append("SYSTEM WARNING: Cumulative tool output exceeded 500KB budget.")
```
### AST Cache (file_cache.py)
@@ -975,17 +975,17 @@ if _cumulative_tool_bytes > _MAX_TOOL_OUTPUT_BYTES:
_ast_cache: Dict[str, Tuple[float, tree_sitter.Tree]] = {}
def get_cached_tree(self, path: Optional[str], code: str) -> tree_sitter.Tree:
mtime = p.stat().st_mtime if p.exists() else 0.0
if path in _ast_cache:
cached_mtime, tree = _ast_cache[path]
if cached_mtime == mtime:
return tree
# Parse and cache with simple LRU (max 10 entries)
if len(_ast_cache) >= 10:
del _ast_cache[next(iter(_ast_cache))]
tree = self.parse(code)
_ast_cache[path] = (mtime, tree)
return tree
mtime = p.stat().st_mtime if p.exists() else 0.0
if path in _ast_cache:
cached_mtime, tree = _ast_cache[path]
if cached_mtime == mtime:
return tree
# Parse and cache with simple LRU (max 10 entries)
if len(_ast_cache) >= 10:
del _ast_cache[next(iter(_ast_cache))]
tree = self.parse(code)
_ast_cache[path] = (mtime, tree)
return tree
```
---