Private
Public Access
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:
+129
-129
@@ -14,7 +14,7 @@ This documentation suite provides comprehensive technical reference for the Manu
|
||||
|
||||
| Guide | Contents |
|
||||
|---|---|
|
||||
| [Architecture](guide_architecture.md) | Thread domains (GUI Main, Asyncio Worker, HookServer, Ad-hoc), cross-thread data structures (AsyncEventQueue, Guarded Lists, Condition-Variable Dialogs), event system (EventEmitter, SyncEventQueue, UserRequestEvent), application lifetime (boot sequence, shutdown sequence), task pipeline (producer-consumer synchronization), Execution Clutch (HITL mechanism with ConfirmDialog, MMAApprovalDialog, MMASpawnApprovalDialog), AI client multi-provider architecture (Gemini SDK, Anthropic, DeepSeek, Gemini CLI, MiniMax), Anthropic/Gemini caching strategies (4-breakpoint system, server-side TTL), context refresh mechanism (mtime-based file re-reading, diff injection), comms logging (JSON-L format), state machines (ai_status, HITL dialog state) |
|
||||
| [Architecture](guide_architecture.md) | Thread domains (GUI Main, Asyncio Worker, HookServer, Ad-hoc), cross-thread data structures (AsyncEventQueue, Guarded Lists, Condition-Variable Dialogs), event system (EventEmitter, SyncEventQueue, UserRequestEvent), application lifetime (boot sequence, shutdown sequence), task pipeline (producer-consumer synchronization), Execution Clutch (HITL mechanism with ConfirmDialog, MMAApprovalDialog, MMASpawnApprovalDialog), AI client multi-provider architecture (Gemini SDK, Anthropic, DeepSeek, MiniMax), Anthropic/Gemini caching strategies (4-breakpoint system, server-side TTL), context refresh mechanism (mtime-based file re-reading, diff injection), comms logging (JSON-L format), state machines (ai_status, HITL dialog state) |
|
||||
| [Meta-Boundary](guide_meta_boundary.md) | Explicit distinction between the Application's domain (Strict HITL — `gui_2.py`, `ai_client.py`, `multi_agent_conductor.py`, `dag_engine.py`) and the **Meta-Tooling** domain (the OpenCode Task tool with `.opencode/agents/*` tier prompts, `.gemini/`, `.claude/`, plus the legacy `scripts/mma_exec.py` / `scripts/claude_mma_exec.py` / `scripts/tool_call.py` / `scripts/mcp_server.py` for backward compatibility), preventing feature bleed and safety bypasses via shared bridges like `mcp_client.py`. Documents the Inter-Domain Bridges (`cli_tool_bridge.py`, `claude_tool_bridge.py`) and the `GEMINI_CLI_HOOK_CONTEXT` environment variable. **Note (2026-06-27):** the legacy `mma_exec.py` / `claude_mma_exec.py` are DEPRECATED for meta-tooling sub-agent delegation; the OpenCode Task tool is the canonical mechanism. |
|
||||
| [Tools & IPC](guide_tools.md) | MCP Bridge 3-layer security model (Allowlist Construction, Path Validation, Resolution Gate), all 45 MCP tool signatures (plus `run_powershell` from `src/shell_runner.py`, for a canonical 46 in `models.AGENT_TOOL_NAMES`) with parameters and behavior (File I/O, AST-Based, Analysis, Network, Runtime, Beads), Hook API GET/POST endpoints with request/response formats, ApiHookClient method reference (Connection Methods, State Query Methods, GUI Manipulation Methods, Polling Methods, HITL Method), `/api/ask` synchronous HITL protocol (blocking request-response over HTTP), session logging (comms.log, toolcalls.log, apihooks.log, clicalls.log, scripts/generated/*.ps1), shell runner (mcp_env.toml configuration, run_powershell function with 60s timeout, qa_callback and patch_callback integration for Tier 4 QA + auto-patch) |
|
||||
| [MMA Orchestration](guide_mma.md) | Ticket/Track/WorkerContext data structures (from `models.py`), DAG engine (TrackDAG class with cycle detection, topological sort, cascade_blocks; ExecutionEngine class with tick-based state machine), ConductorEngine execution loop (run method, _push_state for state broadcast, parse_json_tickets for ingestion), Tier 2 ticket generation (generate_tickets, topological_sort), Tier 3 worker lifecycle (run_worker_lifecycle with Context Amnesia, AST skeleton injection, HITL clutch integration via confirm_spawn and confirm_execution), Tier 4 QA integration (run_tier4_analysis, run_tier4_patch_callback), token firewalling (tier_usage tracking, model escalation), track state persistence (TrackState, save_track_state, load_track_state, get_all_tracks) |
|
||||
@@ -31,12 +31,12 @@ This documentation suite provides comprehensive technical reference for the Manu
|
||||
| [Testing](guide_testing.md) | 322 test files, 5 test categories (unit, integration, live_gui, perf, simulation), 7 conftest fixtures (`isolate_workspace`, `reset_paths`, `reset_ai_client`, `vlogger`, `kill_process_tree`, `mock_app`, `live_gui` session-scoped), Hook API testing pattern, Puppeteer pattern for MMA simulation, mock provider strategy, opt-in clean install test, opt-in docker test, coverage targets, anti-patterns (no arbitrary core mocking, artifact isolation to `tests/artifacts/`), early-render C-level crash pattern (`_ini_capture_ready` defer-not-catch for `imgui.save_ini_settings_to_memory`), live_gui authoring contract (wait-for-ready pattern over `time.sleep`, narrow test paths over kitchen-sink `render_main_interface` mocks), test-ordering sensitivity (session-scoped fixture) |
|
||||
| [Themes](guide_themes.md) | TOML-based theming system: file layout (`themes/<name>.toml` global + `project_themes.toml` per-project), schema (`syntax_palette` + `[colors]` table with `imgui.Col_` snake_case keys), 4-syntax-palette upstream limit (`imgui-bundle` ships `dark`/`light`/`mariana`/`retro_blue` only), built-in vs TOML palette dispatch, `load_themes_from_disk` / `get_syntax_palette_for_theme` / `apply_syntax_palette` public API, hot-reload behavior, color-callable convention (`C_LBL()` / `C_VAL()` for theme-aware helpers) |
|
||||
| [GUI Main](guide_gui_2.md) | `src/gui_2.py` reference: App class lifecycle, ~90 module-level render functions (UI Delegation Pattern), immgui immediate-mode rendering, Multi-Viewport docks, panel registry, command palette integration, ImGuiScope context managers, hot reload support, key bindings (Ctrl+Shift+P, Ctrl+Alt+R, Ctrl+Z/Y), `_capture_workspace_profile` defer-not-catch pattern (line 813-841, `_ini_capture_ready` flag for `imgui.save_ini_settings_to_memory`), theme color-callable pattern (e.g. `DIR_COLORS`/`KIND_COLORS` dicts store `C_VAL` not `C_VAL()` and are called at use site), `__getattr__` ui_ attrs hasattr-guard (bcdc26d0 silent-None fix), `_LazyModule` / `_FiledialogStub` lazy import proxies, `startup_profiler` + `render_warmup_status_indicator` integration, native `_detect_refresh_rate_win32` (ctypes.EnumDisplaySettingsW) |
|
||||
| [AI Client](guide_ai_client.md) | `src/ai_client.py` reference: multi-provider LLM singleton (8 providers: gemini, anthropic, gemini_cli, deepseek, minimax, qwen, grok, llama), async dispatch with `asyncio.gather`, threading.local for source tier tagging, context caching (Anthropic ephemeral + Gemini explicit), system prompt assembly, error interception for Tier 4 QA, inlined `VendorCapabilities` registry (moved from the deleted `src/vendor_capabilities.py`), `Result[str]`-returning `send()` public API |
|
||||
| [AI Client](guide_ai_client.md) | `src/ai_client.py` reference: multi-provider LLM singleton (7 providers: gemini, anthropic, deepseek, minimax, qwen, grok, llama), async dispatch with `asyncio.gather`, threading.local for source tier tagging, context caching (Anthropic ephemeral + Gemini explicit), system prompt assembly, error interception for Tier 4 QA, inlined `VendorCapabilities` registry (moved from the deleted `src/vendor_capabilities.py`), `Result[str]`-returning `send()` public API |
|
||||
| [API Hooks](guide_api_hooks.md) | `src/api_hooks.py` + `src/api_hook_client.py` reference: HookServer on `127.0.0.1:8999`, ApiHookClient Python wrapper, 8+ endpoints (`/status`, `/api/gui`, `/api/ask`, `/api/gui/mma_status`, `/api/performance`, `/api/comms`, `/api/diagnostics`), Remote Confirmation Protocol via `/api/ask` (synchronous blocking HITL), `custom_callback` action for invoking any registered App method |
|
||||
| [MCP Client](guide_mcp_client.md) | `src/mcp_client.py` reference: 45 native tools (File I/O, Python AST, C/C++ AST, Analysis, Network, Runtime, Beads), 3-layer security model (Allowlist Construction, Path Validation, Resolution Gate), `dispatch()`/`async_dispatch()` entry points, ExternalMCPManager for external MCP servers (Stdio + SSE), JSON-RPC 2.0 engine, public API, configuration |
|
||||
| [App Controller](guide_app_controller.md) | `src/app_controller.py` reference: headless orchestrator owning AppState and all subsystem managers (PresetManager, PersonaManager, ContextPresetManager, ToolPresetManager, ToolBiasEngine, RAGEngine, HistoryManager, WorkspaceManager, HookServer, HotReloader, PathManager), `_predefined_callbacks` and `_gettable_fields` registries for Hook API, SyncEventQueue bridge, preset/persona/context coordination, headless mode |
|
||||
| [MMA Engine](guide_multi_agent_conductor.md) | `src/multi_agent_conductor.py` + `src/dag_engine.py` reference: TrackDAG with cycle detection (iterative DFS) and topological sort (Kahn's variant), ExecutionEngine with Auto-Queue / Step Mode state machine, MultiAgentConductor with WorkerPool (configurable concurrency, default 4), the WorkerPool's internal `run_worker_lifecycle` subprocess template (NOT the meta-tooling `mma_exec.py` — that's deprecated; see `guide_meta_boundary.md`), parse_plan_md utility (now in `src/mma.py`), Beads mode delegation |
|
||||
| [Data Models](guide_models.md) | `src/models.py` is now a ~1.5KB legacy re-export shim (`Metadata = TrackMetadata` alias + `PROVIDERS` lazy `__getattr__`). Data models moved to per-system files per `module_taxonomy_refactor_20260627`: `src/mma.py` (TrackMetadata, Ticket, Track, WorkerContext), `src/project_files.py` (FileItem), `src/type_aliases.py` (typed boundary + per-aggregate dataclasses: Metadata, CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo, FileItemsDiff, JsonPrimitive/JsonValue), `src/mcp_tool_specs.py` (typed ToolSpec registry, 45 tools), `src/result_types.py` (Result[T], ErrorInfo, ErrorKind). `VendorCapabilities` lives in `src/ai_client.py` `#region: Vendor Capabilities`. `PROVIDERS` constant in `src/ai_client.py` (8 providers: gemini, anthropic, gemini_cli, deepseek, minimax, qwen, grok, llama). |
|
||||
| [Data Models](guide_models.md) | `src/models.py` is now a ~1.5KB legacy re-export shim (`Metadata = TrackMetadata` alias + `PROVIDERS` lazy `__getattr__`). Data models moved to per-system files per `module_taxonomy_refactor_20260627`: `src/mma.py` (TrackMetadata, Ticket, Track, WorkerContext), `src/project_files.py` (FileItem), `src/type_aliases.py` (typed boundary + per-aggregate dataclasses: Metadata, CommsLogEntry, HistoryMessage, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo, FileItemsDiff, JsonPrimitive/JsonValue), `src/mcp_tool_specs.py` (typed ToolSpec registry, 45 tools), `src/result_types.py` (Result[T], ErrorInfo, ErrorKind). `VendorCapabilities` lives in `src/ai_client.py` `#region: Vendor Capabilities`. `PROVIDERS` constant in `src/ai_client.py` (7 providers: gemini, anthropic, deepseek, minimax, qwen, grok, llama). |
|
||||
| [Discussions](guide_discussions.md) | The Discussion system: 23-operation matrix A1-A7 (per-entry) + B1-B11 (discussion-level) + C1-C5 (undo/redo), Take naming convention (`<base>_take_<n>`), branching at any entry (`project_manager.branch_discussion`), promotion to top-level (`project_manager.promote_take`), user-managed role list (`app.disc_roles`), per-role filter linked to MMA persona focus, `_disc_entries_lock` thread-safety contract, Hook API session endpoints |
|
||||
| [State Lifecycle](guide_state_lifecycle.md) | Undo/redo via `HistoryManager` + `UISnapshot` (13 captured fields, 100-snapshot capacity, debounced change detection at render frame), reset flow (`_handle_reset_session` — clears 30+ fields, replaces project, preserves `active_project_path` per the 2026-06-08 regression fix), `App.__getattr__`/`__setattr__` state delegation to Controller, 8-thread io_pool with 11 lock-protected regions (per `IO_POOL_MAX_WORKERS = 8` in `src/io_pool.py:20`; bumped 4→8 in 4a338486 on 2026-06-06), hot-reload integration |
|
||||
| [Context Aggregation](guide_context_aggregation.md) | The `aggregate.py` (518-line) pipeline: 3 aggregation strategies (`auto`/`summarize`/`full`), 7 per-file view modes (`full`/`summary`/`skeleton`/`outline`/`masked`/`custom`/`none`), full `FileItem` schema (9 fields + `__post_init__` normalizer), `ContextPreset` schema and `ContextPresetManager`, Tier 3 worker variant (`build_tier3_context` with FuzzyAnchor re-resolution and focus-file handling), `force_full`/`auto_aggregate` short-circuits, output file numbering, cache strategy (static prefix + dynamic history) |
|
||||
@@ -85,8 +85,8 @@ Controls what context is compiled and sent to the AI.
|
||||
- **Base Dir**: Root directory for path resolution and MCP tool constraints.
|
||||
- **Paths**: Explicit files or wildcard globs (`src/**/*.py`).
|
||||
- **File Flags**:
|
||||
- **Auto-Aggregate**: Include in context compilation.
|
||||
- **Force Full**: Bypass summary-only mode for this file.
|
||||
- **Auto-Aggregate**: Include in context compilation.
|
||||
- **Force Full**: Bypass summary-only mode for this file.
|
||||
- **Cache Indicator**: Green dot (●) indicates file is in provider's context cache.
|
||||
|
||||
### Discussion Hub
|
||||
@@ -101,7 +101,7 @@ Manages conversational branches to prevent context poisoning across tasks.
|
||||
|
||||
### AI Settings Panel
|
||||
|
||||
- **Provider**: Switch between API backends (Gemini, Anthropic, DeepSeek, Gemini CLI, MiniMax).
|
||||
- **Provider**: Switch between API backends (Gemini, Anthropic, DeepSeek, MiniMax).
|
||||
- **Model**: Select from available models for the current provider.
|
||||
- **Fetch Models**: Queries the active provider for the latest model list.
|
||||
- **Temperature / Max Tokens**: Generation parameters.
|
||||
@@ -324,127 +324,127 @@ EXPANDED = "${HOME}/subdir"
|
||||
|
||||
```
|
||||
manual_slop/
|
||||
├── conductor/ # Conductor system
|
||||
│ ├── tracks/ # Track directories
|
||||
│ │ └── <track_id>/ # Per-track files
|
||||
│ │ ├── spec.md
|
||||
│ │ ├── plan.md
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── state.toml
|
||||
│ ├── archive/ # Completed tracks
|
||||
│ ├── product.md # Product definition
|
||||
│ ├── product-guidelines.md
|
||||
│ ├── tech-stack.md
|
||||
│ ├── workflow.md
|
||||
│ ├── index.md
|
||||
│ └── edit_workflow.md
|
||||
├── docs/ # Deep-dive documentation (27 guides + specs/plans)
|
||||
│ ├── guide_ai_client.md # Multi-provider LLM client
|
||||
│ ├── guide_api_hooks.md # HookServer + ApiHookClient
|
||||
│ ├── guide_app_controller.md # Headless AppController
|
||||
│ ├── guide_architecture.md # Threading, event system, state machines
|
||||
│ ├── guide_beads.md # Beads/Dolt issue tracking
|
||||
│ ├── guide_command_palette.md # Command palette + 33 registered commands
|
||||
│ ├── guide_context_aggregation.md # aggregate.py pipeline (strategies + view modes)
|
||||
│ ├── guide_context_curation.md # Granular AST control + Fuzzy Anchor slices
|
||||
│ ├── guide_discussions.md # Discussion system + A1-A7 matrix
|
||||
│ ├── guide_docker_deployment.md # Docker + Gitea registry deployment
|
||||
│ ├── guide_gui_2.md # Main ImGui interface (App class, render functions)
|
||||
│ ├── guide_hot_reload.md # State-preserving module reloading
|
||||
│ ├── guide_mcp_client.md # 45 MCP tools + 3-layer security
|
||||
│ ├── guide_meta_boundary.md # Application vs Meta-Tooling split
|
||||
│ ├── guide_mma.md # 4-Tier MMA concepts
|
||||
│ ├── guide_models.md # Data model registry
|
||||
│ ├── guide_multi_agent_conductor.md # ConductorEngine + TrackDAG + WorkerPool
|
||||
│ ├── guide_nerv_theme.md # NERV Tactical Console theme
|
||||
│ ├── guide_personas.md # Unified agent profile system
|
||||
│ ├── guide_rag.md # RAG subsystem (ChromaDB + embeddings)
|
||||
│ ├── guide_shaders_and_window.md # Shader injection + custom window frame
|
||||
│ ├── guide_simulations.md # Test framework + Puppeteer pattern
|
||||
│ ├── guide_state_lifecycle.md # Undo/redo + state delegation
|
||||
│ ├── guide_testing.md # 322 test files + 7 conftest fixtures
|
||||
│ ├── guide_themes.md # Multi-theme TOML system
|
||||
│ ├── guide_tools.md # MCP tools + shell runner
|
||||
│ ├── guide_workspace_profiles.md # Workspace profile save/load
|
||||
│ ├── Readme.md
|
||||
│ ├── MMA_Support/ # Legacy MMA reference (deprecated)
|
||||
│ ├── reports/ # Phase 5 reports
|
||||
│ └── superpowers/ # Specs and plans for design work
|
||||
├── src/ # Core implementation (53 modules)
|
||||
│ ├── gui_2.py # Primary ImGui interface
|
||||
│ ├── app_controller.py # Headless controller
|
||||
│ ├── ai_client.py # Multi-provider LLM (Gemini, Anthropic, DeepSeek, MiniMax)
|
||||
│ ├── mcp_client.py # 45 MCP tools + 1 shell runner (canonical 46) with 3-layer security
|
||||
│ ├── api_hooks.py # HookServer REST API on :8999
|
||||
│ ├── api_hook_client.py # Python client for the Hook API
|
||||
│ ├── multi_agent_conductor.py # ConductorEngine
|
||||
│ ├── dag_engine.py # TrackDAG + ExecutionEngine
|
||||
│ ├── models.py # Ticket, Track, WorkerContext, etc.
|
||||
│ ├── events.py # EventEmitter, SyncEventQueue
|
||||
│ ├── project_manager.py # TOML persistence, discussion management
|
||||
│ ├── session_logger.py # JSON-L + markdown audit trails
|
||||
│ ├── rag_engine.py # RAG (ChromaDB + embedding providers)
|
||||
│ ├── beads_client.py # Beads/Dolt issue tracking client
|
||||
│ ├── hot_reloader.py # State-preserving module reloader
|
||||
│ ├── personas.py # Unified agent profile manager
|
||||
│ ├── presets.py # System prompt preset manager
|
||||
│ ├── context_presets.py # Context composition preset manager
|
||||
│ ├── tool_presets.py # Tool preset manager
|
||||
│ ├── tool_bias.py # Tool bias engine
|
||||
│ ├── command_palette.py # Command palette + fuzzy matcher
|
||||
│ ├── commands.py # 33 registered commands
|
||||
│ ├── workspace_manager.py # Workspace profile save/load
|
||||
│ ├── theme_2.py # Theme system (palette/font/etc.)
|
||||
│ ├── theme_nerv.py # NERV Tactical Console theme
|
||||
│ ├── theme_nerv_fx.py # NERV FX (scanlines, flicker, alert)
|
||||
│ ├── shell_runner.py # PowerShell execution with 60s timeout + qa_callback + patch_callback
|
||||
│ ├── file_cache.py # ASTParser (tree-sitter)
|
||||
│ ├── summarize.py # Heuristic file summaries
|
||||
│ ├── outline_tool.py # Hierarchical code outline
|
||||
│ ├── fuzzy_anchor.py # Fuzzy anchor slice algorithm
|
||||
│ ├── history.py # Undo/redo HistoryManager
|
||||
│ ├── imgui_scopes.py # ImGui context managers
|
||||
│ ├── performance_monitor.py # FPS/CPU tracking
|
||||
│ ├── log_registry.py # Session metadata
|
||||
│ ├── log_pruner.py # Automated log cleanup
|
||||
│ ├── paths.py # Centralized path resolution
|
||||
│ ├── cost_tracker.py # Token cost estimation
|
||||
│ ├── gemini_cli_adapter.py # CLI subprocess adapter
|
||||
│ ├── mma_prompts.py # Tier-specific system prompts
|
||||
│ ├── summary_cache.py # SHA256-keyed summary LRU cache
|
||||
│ ├── markdown_helper.py # Markdown rendering helpers
|
||||
│ ├── patch_modal.py # Patch approval modal
|
||||
│ ├── diff_viewer.py # Diff rendering
|
||||
│ ├── external_editor.py # External editor integration
|
||||
│ ├── orchestrator_pm.py # Orchestrator project manager
|
||||
│ ├── conductor_tech_lead.py # Tier 2 ticket generation
|
||||
│ ├── synthesis_formatter.py # Multi-take synthesis
|
||||
│ ├── thinking_parser.py # AI thinking-trace extraction
|
||||
│ └── __init__.py
|
||||
├── simulation/ # Test simulations
|
||||
│ ├── sim_base.py # BaseSimulation class
|
||||
│ ├── workflow_sim.py # WorkflowSimulator
|
||||
│ ├── user_agent.py # UserSimAgent
|
||||
│ ├── sim_context.py # ContextSimulation
|
||||
│ ├── sim_execution.py # ExecutionSimulation
|
||||
│ ├── sim_ai_settings.py # AISettingsSimulation
|
||||
│ └── sim_tools.py # ToolsSimulation
|
||||
├── tests/ # Test suite (251 files)
|
||||
│ ├── conftest.py # Fixtures (live_gui, isolate_workspace, etc.)
|
||||
│ ├── mock_gemini_cli.py # Mock provider for integration tests
|
||||
│ ├── test_*.py # Unit tests
|
||||
│ ├── *_sim.py # Integration tests using live_gui
|
||||
│ ├── test_clean_install.py # Opt-in: clones repo and verifies hooks
|
||||
│ ├── test_docker_build.py # Opt-in: builds Docker image
|
||||
│ ├── artifacts/ # Git-ignored; test outputs
|
||||
│ └── logs/ # Git-ignored; live_gui log files
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── generated/ # AI-generated scripts
|
||||
│ ├── check_test_toml_paths.py # Audit script (CI gate)
|
||||
│ ├── docker_build.sh
|
||||
│ └── docker_run.sh
|
||||
├── sloppy.py # Main entry point
|
||||
├── config.toml # Global configuration
|
||||
├── manual_slop.toml # Active project config (current)
|
||||
└── credentials.toml # API keys (gitignored)
|
||||
├── conductor/ # Conductor system
|
||||
│ ├── tracks/ # Track directories
|
||||
│ │ └── <track_id>/ # Per-track files
|
||||
│ │ ├── spec.md
|
||||
│ │ ├── plan.md
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── state.toml
|
||||
│ ├── archive/ # Completed tracks
|
||||
│ ├── product.md # Product definition
|
||||
│ ├── product-guidelines.md
|
||||
│ ├── tech-stack.md
|
||||
│ ├── workflow.md
|
||||
│ ├── index.md
|
||||
│ └── edit_workflow.md
|
||||
├── docs/ # Deep-dive documentation (27 guides + specs/plans)
|
||||
│ ├── guide_ai_client.md # Multi-provider LLM client
|
||||
│ ├── guide_api_hooks.md # HookServer + ApiHookClient
|
||||
│ ├── guide_app_controller.md # Headless AppController
|
||||
│ ├── guide_architecture.md # Threading, event system, state machines
|
||||
│ ├── guide_beads.md # Beads/Dolt issue tracking
|
||||
│ ├── guide_command_palette.md # Command palette + 33 registered commands
|
||||
│ ├── guide_context_aggregation.md # aggregate.py pipeline (strategies + view modes)
|
||||
│ ├── guide_context_curation.md # Granular AST control + Fuzzy Anchor slices
|
||||
│ ├── guide_discussions.md # Discussion system + A1-A7 matrix
|
||||
│ ├── guide_docker_deployment.md # Docker + Gitea registry deployment
|
||||
│ ├── guide_gui_2.md # Main ImGui interface (App class, render functions)
|
||||
│ ├── guide_hot_reload.md # State-preserving module reloading
|
||||
│ ├── guide_mcp_client.md # 45 MCP tools + 3-layer security
|
||||
│ ├── guide_meta_boundary.md # Application vs Meta-Tooling split
|
||||
│ ├── guide_mma.md # 4-Tier MMA concepts
|
||||
│ ├── guide_models.md # Data model registry
|
||||
│ ├── guide_multi_agent_conductor.md # ConductorEngine + TrackDAG + WorkerPool
|
||||
│ ├── guide_nerv_theme.md # NERV Tactical Console theme
|
||||
│ ├── guide_personas.md # Unified agent profile system
|
||||
│ ├── guide_rag.md # RAG subsystem (ChromaDB + embeddings)
|
||||
│ ├── guide_shaders_and_window.md # Shader injection + custom window frame
|
||||
│ ├── guide_simulations.md # Test framework + Puppeteer pattern
|
||||
│ ├── guide_state_lifecycle.md # Undo/redo + state delegation
|
||||
│ ├── guide_testing.md # 322 test files + 7 conftest fixtures
|
||||
│ ├── guide_themes.md # Multi-theme TOML system
|
||||
│ ├── guide_tools.md # MCP tools + shell runner
|
||||
│ ├── guide_workspace_profiles.md # Workspace profile save/load
|
||||
│ ├── Readme.md
|
||||
│ ├── MMA_Support/ # Legacy MMA reference (deprecated)
|
||||
│ ├── reports/ # Phase 5 reports
|
||||
│ └── superpowers/ # Specs and plans for design work
|
||||
├── src/ # Core implementation (53 modules)
|
||||
│ ├── gui_2.py # Primary ImGui interface
|
||||
│ ├── app_controller.py # Headless controller
|
||||
│ ├── ai_client.py # Multi-provider LLM (Gemini, Anthropic, DeepSeek, MiniMax)
|
||||
│ ├── mcp_client.py # 45 MCP tools + 1 shell runner (canonical 46) with 3-layer security
|
||||
│ ├── api_hooks.py # HookServer REST API on :8999
|
||||
│ ├── api_hook_client.py # Python client for the Hook API
|
||||
│ ├── multi_agent_conductor.py # ConductorEngine
|
||||
│ ├── dag_engine.py # TrackDAG + ExecutionEngine
|
||||
│ ├── models.py # Ticket, Track, WorkerContext, etc.
|
||||
│ ├── events.py # EventEmitter, SyncEventQueue
|
||||
│ ├── project_manager.py # TOML persistence, discussion management
|
||||
│ ├── session_logger.py # JSON-L + markdown audit trails
|
||||
│ ├── rag_engine.py # RAG (ChromaDB + embedding providers)
|
||||
│ ├── beads_client.py # Beads/Dolt issue tracking client
|
||||
│ ├── hot_reloader.py # State-preserving module reloader
|
||||
│ ├── personas.py # Unified agent profile manager
|
||||
│ ├── presets.py # System prompt preset manager
|
||||
│ ├── context_presets.py # Context composition preset manager
|
||||
│ ├── tool_presets.py # Tool preset manager
|
||||
│ ├── tool_bias.py # Tool bias engine
|
||||
│ ├── command_palette.py # Command palette + fuzzy matcher
|
||||
│ ├── commands.py # 33 registered commands
|
||||
│ ├── workspace_manager.py # Workspace profile save/load
|
||||
│ ├── theme_2.py # Theme system (palette/font/etc.)
|
||||
│ ├── theme_nerv.py # NERV Tactical Console theme
|
||||
│ ├── theme_nerv_fx.py # NERV FX (scanlines, flicker, alert)
|
||||
│ ├── shell_runner.py # PowerShell execution with 60s timeout + qa_callback + patch_callback
|
||||
│ ├── file_cache.py # ASTParser (tree-sitter)
|
||||
│ ├── summarize.py # Heuristic file summaries
|
||||
│ ├── outline_tool.py # Hierarchical code outline
|
||||
│ ├── fuzzy_anchor.py # Fuzzy anchor slice algorithm
|
||||
│ ├── history.py # Undo/redo HistoryManager
|
||||
│ ├── imgui_scopes.py # ImGui context managers
|
||||
│ ├── performance_monitor.py # FPS/CPU tracking
|
||||
│ ├── log_registry.py # Session metadata
|
||||
│ ├── log_pruner.py # Automated log cleanup
|
||||
│ ├── paths.py # Centralized path resolution
|
||||
│ ├── cost_tracker.py # Token cost estimation
|
||||
│ ├── gemini_cli_adapter.py # CLI subprocess adapter
|
||||
│ ├── mma_prompts.py # Tier-specific system prompts
|
||||
│ ├── summary_cache.py # SHA256-keyed summary LRU cache
|
||||
│ ├── markdown_helper.py # Markdown rendering helpers
|
||||
│ ├── patch_modal.py # Patch approval modal
|
||||
│ ├── diff_viewer.py # Diff rendering
|
||||
│ ├── external_editor.py # External editor integration
|
||||
│ ├── orchestrator_pm.py # Orchestrator project manager
|
||||
│ ├── conductor_tech_lead.py # Tier 2 ticket generation
|
||||
│ ├── synthesis_formatter.py # Multi-take synthesis
|
||||
│ ├── thinking_parser.py # AI thinking-trace extraction
|
||||
│ └── __init__.py
|
||||
├── simulation/ # Test simulations
|
||||
│ ├── sim_base.py # BaseSimulation class
|
||||
│ ├── workflow_sim.py # WorkflowSimulator
|
||||
│ ├── user_agent.py # UserSimAgent
|
||||
│ ├── sim_context.py # ContextSimulation
|
||||
│ ├── sim_execution.py # ExecutionSimulation
|
||||
│ ├── sim_ai_settings.py # AISettingsSimulation
|
||||
│ └── sim_tools.py # ToolsSimulation
|
||||
├── tests/ # Test suite (251 files)
|
||||
│ ├── conftest.py # Fixtures (live_gui, isolate_workspace, etc.)
|
||||
│ ├── mock_gemini_cli.py # Mock provider for integration tests
|
||||
│ ├── test_*.py # Unit tests
|
||||
│ ├── *_sim.py # Integration tests using live_gui
|
||||
│ ├── test_clean_install.py # Opt-in: clones repo and verifies hooks
|
||||
│ ├── test_docker_build.py # Opt-in: builds Docker image
|
||||
│ ├── artifacts/ # Git-ignored; test outputs
|
||||
│ └── logs/ # Git-ignored; live_gui log files
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── generated/ # AI-generated scripts
|
||||
│ ├── check_test_toml_paths.py # Audit script (CI gate)
|
||||
│ ├── docker_build.sh
|
||||
│ └── docker_run.sh
|
||||
├── sloppy.py # Main entry point
|
||||
├── config.toml # Global configuration
|
||||
├── manual_slop.toml # Active project config (current)
|
||||
└── credentials.toml # API keys (gitignored)
|
||||
```
|
||||
|
||||
+182
-182
@@ -6,14 +6,14 @@
|
||||
|
||||
## Overview
|
||||
|
||||
`src/ai_client.py` (~166KB) is the **unified LLM client** for 8 providers. It abstracts the differences between providers (Gemini, Anthropic, DeepSeek, MiniMax, Gemini CLI, Qwen, Grok, Llama) behind a single `send()` function.
|
||||
`src/ai_client.py` (~166KB) is the **unified LLM client** for 7 providers. It abstracts the differences between providers (Gemini, Anthropic, DeepSeek, MiniMax, Qwen, Grok, Llama) behind a single `send()` function.
|
||||
|
||||
The module is a **stateful singleton** — all provider state is held in module-level globals. There is no class wrapping; the module itself is the abstraction layer.
|
||||
|
||||
The 8 providers split into 3 API shapes:
|
||||
The 7 providers split into 3 API shapes:
|
||||
- **Native SDK**: Gemini (google-genai), Anthropic (anthropic), Qwen (DashScope)
|
||||
- **OpenAI-compatible**: MiniMax, Grok, Llama (Ollama/OpenRouter/custom), DeepSeek
|
||||
- **Subprocess**: Gemini CLI
|
||||
- **Subprocess**:
|
||||
|
||||
The OpenAI-compatible vendors all call the shared helper in `src/openai_compatible.py` (added 2026-06-06 by the `qwen_llama_grok_integration_20260606` track; see "Shared OpenAI-Compatible Helper" section below). The MiniMax provider's `_send_minimax` was refactored to use this helper (Phase 4 of the same track, 231 → 75 lines, 68% reduction).
|
||||
|
||||
@@ -21,7 +21,7 @@ The OpenAI-compatible vendors all call the shared helper in `src/openai_compatib
|
||||
|
||||
## Module-Level Imports
|
||||
|
||||
> **Important:** The provider SDKs are **NOT** imported at module level. `import google.genai`, `import anthropic`, `import openai`, `import dashscope`, and `import fastapi` are heavy (~430-955ms each on cold load) and are now obtained via `src.module_loader._require_warmed("google.genai")` and similar calls, after the `WarmupManager` has loaded them in the background. The module-level globals you see in the State section (`_gemini_client`, `_anthropic_client`, etc.) are typed as `Optional` because they're populated by `_require_warmed()` on first use, not at import time. (Updated 2026-07-02: there are 8 providers, not 5 — the original "5 SDKs" count predated the qwen/grok/llama additions.)
|
||||
> **Important:** The provider SDKs are **NOT** imported at module level. `import google.genai`, `import anthropic`, `import openai`, `import dashscope`, and `import fastapi` are heavy (~430-955ms each on cold load) and are now obtained via `src.module_loader._require_warmed("google.genai")` and similar calls, after the `WarmupManager` has loaded them in the background. The module-level globals you see in the State section (`_gemini_client`, `_anthropic_client`, etc.) are typed as `Optional` because they're populated by `_require_warmed()` on first use, not at import time. (Updated 2026-07-02: there are 7 providers, not 5 — the original "5 SDKs" count predated the qwen/grok/llama additions.)
|
||||
|
||||
This change was part of the 2026-06-06 `startup_speedup_20260606` track. Before: `import src.ai_client` took ~1800ms. After: ~161ms. The remaining cost is the bare module skeleton.
|
||||
|
||||
@@ -29,19 +29,19 @@ This change was part of the 2026-06-06 `startup_speedup_20260606` track. Before:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ai_client.send(md_content, user_message, ...) │
|
||||
│ │
|
||||
│ 1. _send_lock.acquire() — serialize all calls │
|
||||
│ 2. Read _provider / _model │
|
||||
│ ai_client.send(md_content, user_message, ...) │
|
||||
│ │
|
||||
│ 1. _send_lock.acquire() — serialize all calls │
|
||||
│ 2. Read _provider / _model │
|
||||
│ 3. Route to provider-specific _send_<provider>() │
|
||||
│ 4. Return str response │
|
||||
│ 4. Return str response │
|
||||
└─────────────────┬───────────────────────────────┘
|
||||
│ dispatches based on _provider
|
||||
▼
|
||||
┌────────┬─────────┬────────┬──────────┐
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
_gemini _anthropic _deepseek _minimax _gemini_cli
|
||||
(subprocess)
|
||||
│ dispatches based on _provider
|
||||
▼
|
||||
┌────────┬─────────┬────────┬──────────┐
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
_gemini _anthropic _deepseek _minimax _gemini_cli
|
||||
(subprocess)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -94,18 +94,18 @@ _gemini_cli_adapter: Optional[GeminiCliAdapter] = None
|
||||
|
||||
```python
|
||||
def send(
|
||||
md_content: str,
|
||||
user_message: str,
|
||||
base_dir: str = ".",
|
||||
file_items: list[dict] | None = None,
|
||||
discussion_history: str = "",
|
||||
stream: bool = False,
|
||||
pre_tool_callback: Optional[Callable] = None,
|
||||
qa_callback: Optional[Callable] = None,
|
||||
enable_tools: bool = True,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
patch_callback: Optional[Callable] = None,
|
||||
rag_engine: Optional[Any] = None,
|
||||
md_content: str,
|
||||
user_message: str,
|
||||
base_dir: str = ".",
|
||||
file_items: list[dict] | None = None,
|
||||
discussion_history: str = "",
|
||||
stream: bool = False,
|
||||
pre_tool_callback: Optional[Callable] = None,
|
||||
qa_callback: Optional[Callable] = None,
|
||||
enable_tools: bool = True,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
patch_callback: Optional[Callable] = None,
|
||||
rag_engine: Optional[Any] = None,
|
||||
) -> Result[str]:
|
||||
```
|
||||
|
||||
@@ -147,7 +147,7 @@ ai_client.set_model_params(temp=0.7, max_tok=4096, top_p=0.9, trunc_limit=4000)
|
||||
### Session Management
|
||||
|
||||
```python
|
||||
ai_client.reset_session() # Clears all provider state, history, cache
|
||||
ai_client.reset_session() # Clears all provider state, history, cache
|
||||
```
|
||||
|
||||
### Event Hooks
|
||||
@@ -171,10 +171,10 @@ ai_client.events.on("my_event", my_handler)
|
||||
### Comms Log
|
||||
|
||||
```python
|
||||
ai_client._append_comms(direction, kind, payload) # Add entry
|
||||
ai_client.get_comms_log() # Read all
|
||||
ai_client.clear_comms_log() # Clear
|
||||
ai_client.get_token_stats(md_content) # Estimate token usage
|
||||
ai_client._append_comms(direction, kind, payload) # Add entry
|
||||
ai_client.get_comms_log() # Read all
|
||||
ai_client.clear_comms_log() # Clear
|
||||
ai_client.get_token_stats(md_content) # Estimate token usage
|
||||
```
|
||||
|
||||
### Provider Error Taxonomy — Legacy (Pre-Refactor)
|
||||
@@ -187,12 +187,12 @@ ai_client.get_token_stats(md_content) # Estimate token usage
|
||||
|
||||
```python
|
||||
class ProviderError(Exception):
|
||||
kind: str # "quota" | "rate_limit" | "auth" | "balance" | "network" | "unknown"
|
||||
provider: str
|
||||
original: Exception
|
||||
kind: str # "quota" | "rate_limit" | "auth" | "balance" | "network" | "unknown"
|
||||
provider: str
|
||||
original: Exception
|
||||
|
||||
def ui_message(self) -> str:
|
||||
"""Returns a user-friendly error message."""
|
||||
def ui_message(self) -> str:
|
||||
"""Returns a user-friendly error message."""
|
||||
```
|
||||
|
||||
`ProviderError` was raised by provider-specific `_send_*` functions on failure.
|
||||
@@ -210,30 +210,30 @@ All providers follow the same high-level pattern in `_send_*`:
|
||||
|
||||
```python
|
||||
def _send_<provider>(md_content, user_message, ...):
|
||||
for round in range(MAX_TOOL_ROUNDS + 2): # up to 10 rounds
|
||||
response = provider_api_call(md_content, user_message, history, tools)
|
||||
comms_log(direction="IN", kind="response", payload=response)
|
||||
for round in range(MAX_TOOL_ROUNDS + 2): # up to 10 rounds
|
||||
response = provider_api_call(md_content, user_message, history, tools)
|
||||
comms_log(direction="IN", kind="response", payload=response)
|
||||
|
||||
if not has_function_calls(response):
|
||||
return extract_text(response)
|
||||
if not has_function_calls(response):
|
||||
return extract_text(response)
|
||||
|
||||
for call in response.function_calls:
|
||||
if pre_tool_callback and pre_tool_callback(...) is rejected:
|
||||
return rejection_message
|
||||
tool_result = dispatch(call.name, call.args, base_dir)
|
||||
append_tool_result_to_history(call, tool_result)
|
||||
for call in response.function_calls:
|
||||
if pre_tool_callback and pre_tool_callback(...) is rejected:
|
||||
return rejection_message
|
||||
tool_result = dispatch(call.name, call.args, base_dir)
|
||||
append_tool_result_to_history(call, tool_result)
|
||||
|
||||
# Context refresh: re-read all tracked files (mtime check)
|
||||
_reread_file_items(file_items)
|
||||
# Context refresh: re-read all tracked files (mtime check)
|
||||
_reread_file_items(file_items)
|
||||
|
||||
# Truncate tool outputs at _history_trunc_limit
|
||||
truncate_tool_outputs(history)
|
||||
# Truncate tool outputs at _history_trunc_limit
|
||||
truncate_tool_outputs(history)
|
||||
|
||||
# Cumulative byte check
|
||||
if cumulative_tool_bytes > 500_000:
|
||||
inject_warning()
|
||||
# Cumulative byte check
|
||||
if cumulative_tool_bytes > 500_000:
|
||||
inject_warning()
|
||||
|
||||
return final_response
|
||||
return final_response
|
||||
```
|
||||
|
||||
The constants:
|
||||
@@ -273,7 +273,7 @@ The constants:
|
||||
- **History trimming**: similar to Anthropic (drop turn pairs at threshold)
|
||||
- **History repair**: `_repair_minimax_history`
|
||||
|
||||
### Gemini CLI
|
||||
###
|
||||
|
||||
- **Subprocess adapter**: `GeminiCliAdapter` in `src/gemini_cli_adapter.py`
|
||||
- **Persistent session**: CLI maintains its own session ID
|
||||
@@ -288,9 +288,9 @@ The constants:
|
||||
|
||||
```python
|
||||
if total_in > _GEMINI_MAX_INPUT_TOKENS * 0.4:
|
||||
while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.3:
|
||||
hist.pop(0) # Assistant
|
||||
hist.pop(0) # User
|
||||
while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.3:
|
||||
hist.pop(0) # Assistant
|
||||
hist.pop(0) # User
|
||||
```
|
||||
|
||||
### Anthropic (180K limit)
|
||||
@@ -314,8 +314,8 @@ No built-in trimming (relies on the caller to keep history short).
|
||||
### Gemini Server-Side Cache
|
||||
|
||||
```python
|
||||
_gemini_cache_md_hash: Optional[str] = None # Hash of cached content
|
||||
_gemini_cache_created_at: Optional[float] = None # Monotonic time
|
||||
_gemini_cache_md_hash: Optional[str] = None # Hash of cached content
|
||||
_gemini_cache_created_at: Optional[float] = None # Monotonic time
|
||||
```
|
||||
|
||||
The cache decision is a 3-way branch on each `_send_gemini` call:
|
||||
@@ -344,8 +344,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`
|
||||
|
||||
@@ -359,19 +359,19 @@ For Tier 4: when an error occurs, `qa_callback` may be invoked to get a Tier 4 A
|
||||
|
||||
```python
|
||||
def run_tier4_analysis(stderr: str) -> str:
|
||||
"""Stateless Tier 4 QA analysis of an error message."""
|
||||
# Uses a dedicated system prompt for error triage
|
||||
# Returns analysis text (root cause, suggested fix)
|
||||
# Does NOT modify any code — analysis only
|
||||
"""Stateless Tier 4 QA analysis of an error message."""
|
||||
# Uses a dedicated system prompt for error triage
|
||||
# Returns analysis text (root cause, suggested fix)
|
||||
# Does NOT modify any code — analysis only
|
||||
```
|
||||
|
||||
For Tier 4 patch generation:
|
||||
|
||||
```python
|
||||
def run_tier4_patch_generation(error: str, file_context: str) -> str:
|
||||
"""Generate a unified diff patch from an error and file context."""
|
||||
# Returns the patch as a string
|
||||
# The caller (typically the patch modal) presents it for human review
|
||||
"""Generate a unified diff patch from an error and file context."""
|
||||
# Returns the patch as a string
|
||||
# The caller (typically the patch modal) presents it for human review
|
||||
```
|
||||
|
||||
---
|
||||
@@ -416,10 +416,10 @@ def run_tier4_patch_generation(error: str, file_context: str) -> str:
|
||||
|
||||
```python
|
||||
def test_set_provider():
|
||||
from src import ai_client
|
||||
ai_client.set_provider("anthropic", "claude-3-5-sonnet-latest")
|
||||
assert ai_client.get_provider() == "anthropic"
|
||||
ai_client.reset_session() # Cleanup
|
||||
from src import ai_client
|
||||
ai_client.set_provider("anthropic", "claude-3-5-sonnet-latest")
|
||||
assert ai_client.get_provider() == "anthropic"
|
||||
ai_client.reset_session() # Cleanup
|
||||
```
|
||||
|
||||
### Mocked Tests
|
||||
@@ -428,12 +428,12 @@ def test_set_provider():
|
||||
from unittest.mock import patch
|
||||
|
||||
def test_send_routes_to_provider(monkeypatch):
|
||||
with patch.object(ai_client, "_send_anthropic", return_value="mocked") as m:
|
||||
ai_client.set_provider("anthropic", "claude-3-5-sonnet-latest")
|
||||
result = ai_client.send("system", "user")
|
||||
assert result == "mocked"
|
||||
m.assert_called_once()
|
||||
ai_client.reset_session()
|
||||
with patch.object(ai_client, "_send_anthropic", return_value="mocked") as m:
|
||||
ai_client.set_provider("anthropic", "claude-3-5-sonnet-latest")
|
||||
result = ai_client.send("system", "user")
|
||||
assert result == "mocked"
|
||||
m.assert_called_once()
|
||||
ai_client.reset_session()
|
||||
```
|
||||
|
||||
### Integration (real API)
|
||||
@@ -451,7 +451,7 @@ canonical reference is
|
||||
### Result-Based Returns
|
||||
|
||||
All `_send_<vendor>_result()` functions (8 vendors: Gemini, Anthropic,
|
||||
DeepSeek, MiniMax, Gemini CLI, Qwen, Llama, Grok — plus the
|
||||
DeepSeek, MiniMax, Qwen, Llama, Grok — plus the
|
||||
`_send_llama_native` Ollama adapter) return `Result[str]` with `errors: list[ErrorInfo]`. SDK
|
||||
exceptions are caught at the boundary (`src/openai_compatible.py`,
|
||||
`src/qwen_adapter.py`) and converted to `ErrorInfo` dataclasses. The
|
||||
@@ -469,10 +469,10 @@ meaning — do not overload `UNKNOWN` when a new failure mode surfaces
|
||||
### Public API
|
||||
|
||||
- **`ai_client.send(...)`** — the public API. Returns
|
||||
`Result[str]` (with `errors: list[ErrorInfo]` as a side-channel field).
|
||||
Accepts 13+ parameters including 8 callbacks.
|
||||
Internally calls `_send_<vendor>()` for the active provider (the
|
||||
vendor functions return `Result[str]` directly).
|
||||
`Result[str]` (with `errors: list[ErrorInfo]` as a side-channel field).
|
||||
Accepts 13+ parameters including 8 callbacks.
|
||||
Internally calls `_send_<vendor>()` for the active provider (the
|
||||
vendor functions return `Result[str]` directly).
|
||||
|
||||
### Example
|
||||
|
||||
@@ -482,9 +482,9 @@ from src.result_types import ErrorKind
|
||||
|
||||
r = ai_client.send("system prompt", "user message")
|
||||
if not r.ok:
|
||||
for err in r.errors:
|
||||
log.error(err.ui_message())
|
||||
# err.kind is one of ErrorKind.*; err.source is "ai_client.<vendor>"
|
||||
for err in r.errors:
|
||||
log.error(err.ui_message())
|
||||
# err.kind is one of ErrorKind.*; err.source is "ai_client.<vendor>"
|
||||
# use r.data regardless (it's the zero-initialized "" on failure)
|
||||
print(r.data)
|
||||
```
|
||||
@@ -492,10 +492,10 @@ print(r.data)
|
||||
### Migration Notes for Existing Callers
|
||||
|
||||
- All production call sites and tests now use `send()`. The
|
||||
legacy `send()` function was removed in the
|
||||
`public_api_migration_and_ui_polish_20260615` track.
|
||||
legacy `send()` function was removed in the
|
||||
`public_api_migration_and_ui_polish_20260615` track.
|
||||
- Tests that mock `ai_client._send_<vendor>` should use the
|
||||
`Result(data=...)` return value pattern.
|
||||
`Result(data=...)` return value pattern.
|
||||
|
||||
### See Also (in-doc)
|
||||
|
||||
@@ -532,34 +532,34 @@ Added 2026-06-06 by the `qwen_llama_grok_integration_20260606` track. Operates o
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class NormalizedResponse:
|
||||
text: str
|
||||
tool_calls: list[dict[str, Any]]
|
||||
usage_input_tokens: int
|
||||
usage_output_tokens: int
|
||||
usage_cache_read_tokens: int
|
||||
usage_cache_creation_tokens: int
|
||||
raw_response: Any
|
||||
text: str
|
||||
tool_calls: list[dict[str, Any]]
|
||||
usage_input_tokens: int
|
||||
usage_output_tokens: int
|
||||
usage_cache_read_tokens: int
|
||||
usage_cache_creation_tokens: int
|
||||
raw_response: Any
|
||||
|
||||
@dataclass
|
||||
class OpenAICompatibleRequest:
|
||||
messages: list[dict[str, Any]]
|
||||
model: str
|
||||
temperature: float = 0.0
|
||||
top_p: float = 1.0
|
||||
max_tokens: int = 8192
|
||||
tools: Optional[list[dict[str, Any]]] = None
|
||||
tool_choice: str = "auto"
|
||||
stream: bool = False
|
||||
stream_callback: Optional[Callable[[str], None]] = None
|
||||
messages: list[dict[str, Any]]
|
||||
model: str
|
||||
temperature: float = 0.0
|
||||
top_p: float = 1.0
|
||||
max_tokens: int = 8192
|
||||
tools: Optional[list[dict[str, Any]]] = None
|
||||
tool_choice: str = "auto"
|
||||
stream: bool = False
|
||||
stream_callback: Optional[Callable[[str], None]] = None
|
||||
```
|
||||
|
||||
### The Function
|
||||
|
||||
```python
|
||||
def send_openai_compatible(
|
||||
client: Any, # openai.OpenAI client with vendor-specific base_url + auth
|
||||
request: OpenAICompatibleRequest,
|
||||
*, capabilities: "VendorCapabilities", # from src/ai_client.py #region: Vendor Capabilities
|
||||
client: Any, # openai.OpenAI client with vendor-specific base_url + auth
|
||||
request: OpenAICompatibleRequest,
|
||||
*, capabilities: "VendorCapabilities", # from src/ai_client.py #region: Vendor Capabilities
|
||||
) -> NormalizedResponse:
|
||||
```
|
||||
|
||||
@@ -577,16 +577,16 @@ The function:
|
||||
```python
|
||||
# _send_grok, _send_llama (single-shot placeholders), _send_minimax (with restored tool loop)
|
||||
def _send_grok(md_content, user_message, base_dir, file_items=None, discussion_history="", stream=False, ...):
|
||||
client = _ensure_grok_client() # openai.OpenAI(api_key=..., base_url="https://api.x.ai/v1")
|
||||
with _grok_history_lock:
|
||||
# ... build messages, append user, system + context ...
|
||||
request = OpenAICompatibleRequest(
|
||||
messages=messages, model=_model, stream=stream,
|
||||
stream_callback=stream_callback,
|
||||
)
|
||||
caps = get_capabilities("grok", _model)
|
||||
response = send_openai_compatible(client, request, capabilities=caps)
|
||||
# ... append to history, return response.text ...
|
||||
client = _ensure_grok_client() # openai.OpenAI(api_key=..., base_url="https://api.x.ai/v1")
|
||||
with _grok_history_lock:
|
||||
# ... build messages, append user, system + context ...
|
||||
request = OpenAICompatibleRequest(
|
||||
messages=messages, model=_model, stream=stream,
|
||||
stream_callback=stream_callback,
|
||||
)
|
||||
caps = get_capabilities("grok", _model)
|
||||
response = send_openai_compatible(client, request, capabilities=caps)
|
||||
# ... append to history, return response.text ...
|
||||
```
|
||||
|
||||
### Qwen Adapter (`src/qwen_adapter.py`)
|
||||
@@ -610,28 +610,28 @@ Added 2026-06-11 by the `qwen_llama_grok_followup_20260611` track. Wraps `send_o
|
||||
|
||||
```python
|
||||
def run_with_tool_loop(
|
||||
client: Any,
|
||||
request: OpenAICompatibleRequest | Callable[[int], OpenAICompatibleRequest],
|
||||
*,
|
||||
capabilities: "VendorCapabilities",
|
||||
pre_tool_callback: Optional[Callable] = None,
|
||||
qa_callback: Optional[Callable] = None,
|
||||
stream_callback: Optional[Callable[[str], None]] = None,
|
||||
patch_callback: Optional[Callable] = None,
|
||||
base_dir: str,
|
||||
vendor_name: str,
|
||||
history_lock: Optional[threading.Lock] = None,
|
||||
history: Optional[list] = None,
|
||||
trim_func: Optional[Callable] = None,
|
||||
send_func: Optional[Callable[[int], "NormalizedResponse"]] = None,
|
||||
on_pre_dispatch: Optional[Callable] = None,
|
||||
client: Any,
|
||||
request: OpenAICompatibleRequest | Callable[[int], OpenAICompatibleRequest],
|
||||
*,
|
||||
capabilities: "VendorCapabilities",
|
||||
pre_tool_callback: Optional[Callable] = None,
|
||||
qa_callback: Optional[Callable] = None,
|
||||
stream_callback: Optional[Callable[[str], None]] = None,
|
||||
patch_callback: Optional[Callable] = None,
|
||||
base_dir: str,
|
||||
vendor_name: str,
|
||||
history_lock: Optional[threading.Lock] = None,
|
||||
history: Optional[list] = None,
|
||||
trim_func: Optional[Callable] = None,
|
||||
send_func: Optional[Callable[[int], "NormalizedResponse"]] = None,
|
||||
on_pre_dispatch: Optional[Callable] = None,
|
||||
) -> str:
|
||||
```
|
||||
|
||||
**Two extensions** were added beyond the original signature:
|
||||
|
||||
1. `request` accepts a `Callable[[int], OpenAICompatibleRequest]` (per-round history rebuild). Use this when the vendor mutates history between rounds (e.g., MiniMax's per-round append).
|
||||
2. `send_func + on_pre_dispatch` allows vendored call paths (e.g., Gemini CLI's `GeminiCliAdapter`) to share the loop + dispatch without going through `send_openai_compatible`.
|
||||
2. `send_func + on_pre_dispatch` allows vendored call paths (e.g., 's `GeminiCliAdapter`) to share the loop + dispatch without going through `send_openai_compatible`.
|
||||
|
||||
**Vendors applied** (as of 2026-06-11):
|
||||
- `_send_minimax` (was inline, now uses helper)
|
||||
@@ -657,7 +657,7 @@ Added 2026-06-11. When `_llama_base_url` is `localhost` / `127.0.0.1` (Ollama de
|
||||
The dispatcher check is in `_send_llama` at the function head:
|
||||
```python
|
||||
if "localhost" in _llama_base_url or "127.0.0.1" in _llama_base_url:
|
||||
return _send_llama_native(...)
|
||||
return _send_llama_native(...)
|
||||
```
|
||||
|
||||
For OpenRouter, custom URLs, and other cloud Llama endpoints, the existing OpenAI-compat path is unchanged.
|
||||
@@ -714,11 +714,11 @@ The test in `tests/test_aggregate_caching.py` ensures the first N characters of
|
||||
|
||||
```python
|
||||
def test_aggregate_stable_to_volatile_ordering():
|
||||
ctrl = mock_app_controller()
|
||||
turn1 = aggregate.build_initial_context(ctrl, user_message="first")
|
||||
turn2 = aggregate.build_initial_context(ctrl, user_message="second")
|
||||
N = aggregate.stable_prefix_length(ctrl)
|
||||
assert turn1[:N] == turn2[:N], f"Stable prefix mismatch: {turn1[:N]!r} != {turn2[:N]!r}"
|
||||
ctrl = mock_app_controller()
|
||||
turn1 = aggregate.build_initial_context(ctrl, user_message="first")
|
||||
turn2 = aggregate.build_initial_context(ctrl, user_message="second")
|
||||
N = aggregate.stable_prefix_length(ctrl)
|
||||
assert turn1[:N] == turn2[:N], f"Stable prefix mismatch: {turn1[:N]!r} != {turn2[:N]!r}"
|
||||
```
|
||||
|
||||
**The test is the contract.** If a new layer is added in the wrong position, the test fails; the agent must move the layer to the stable position or update the test with written justification.
|
||||
@@ -729,17 +729,17 @@ def test_aggregate_stable_to_volatile_ordering():
|
||||
|
||||
```python
|
||||
def _send_anthropic(messages, *, cache_prefix_chars=None):
|
||||
if cache_prefix_chars is not None:
|
||||
content_blocks = cache_prefix_blocks(messages, cache_prefix_chars)
|
||||
else:
|
||||
content_blocks = messages
|
||||
if cache_prefix_chars is not None:
|
||||
content_blocks = cache_prefix_blocks(messages, cache_prefix_chars)
|
||||
else:
|
||||
content_blocks = messages
|
||||
|
||||
response = anthropic_client.messages.create(
|
||||
model=model,
|
||||
max_tokens=8192,
|
||||
messages=[{"role": "user", "content": content_blocks}],
|
||||
)
|
||||
return _result_with_usage(response.content, response.usage, messages)
|
||||
response = anthropic_client.messages.create(
|
||||
model=model,
|
||||
max_tokens=8192,
|
||||
messages=[{"role": "user", "content": content_blocks}],
|
||||
)
|
||||
return _result_with_usage(response.content, response.usage, messages)
|
||||
```
|
||||
|
||||
**The `cache_prefix_blocks` helper** splits the message at the given char offsets and marks each prefix with `cache_control: {"type": "ephemeral"}`. Max 3 prefix blocks (provider limit is 4 breakpoints per request).
|
||||
@@ -750,17 +750,17 @@ def _send_anthropic(messages, *, cache_prefix_chars=None):
|
||||
|
||||
```python
|
||||
def _send_gemini(messages, *, cache_ttl_seconds=3600):
|
||||
if cache_ttl_seconds > 0:
|
||||
cached_content = genai_client.caches.create(
|
||||
model=model, contents=stable_prefix_messages, ttl=f"{cache_ttl_seconds}s",
|
||||
)
|
||||
response = genai_client.models.generate_content(
|
||||
model=model, contents=volatile_messages,
|
||||
config=genai.types.GenerateContentConfig(cached_content=cached_content.name),
|
||||
)
|
||||
else:
|
||||
response = genai_client.models.generate_content(model=model, contents=messages)
|
||||
return _result_with_usage(response.text, response.usage_metadata, messages)
|
||||
if cache_ttl_seconds > 0:
|
||||
cached_content = genai_client.caches.create(
|
||||
model=model, contents=stable_prefix_messages, ttl=f"{cache_ttl_seconds}s",
|
||||
)
|
||||
response = genai_client.models.generate_content(
|
||||
model=model, contents=volatile_messages,
|
||||
config=genai.types.GenerateContentConfig(cached_content=cached_content.name),
|
||||
)
|
||||
else:
|
||||
response = genai_client.models.generate_content(model=model, contents=messages)
|
||||
return _result_with_usage(response.text, response.usage_metadata, messages)
|
||||
```
|
||||
|
||||
**The default TTL is 1 hour**; configurable per-discussion via the GUI.
|
||||
@@ -783,21 +783,21 @@ No application-side control; the provider handles caching. The GUI just shows "C
|
||||
```python
|
||||
@dataclass
|
||||
class DiscussionCacheState:
|
||||
discussion_id: str
|
||||
provider: str
|
||||
cached_at: datetime
|
||||
expires_at: Optional[datetime] # None for OpenAI implicit
|
||||
hit_count: int = 0
|
||||
tokens_cached: int = 0
|
||||
last_invalidated_at: Optional[datetime] = None
|
||||
caching_enabled: bool = True
|
||||
discussion_id: str
|
||||
provider: str
|
||||
cached_at: datetime
|
||||
expires_at: Optional[datetime] # None for OpenAI implicit
|
||||
hit_count: int = 0
|
||||
tokens_cached: int = 0
|
||||
last_invalidated_at: Optional[datetime] = None
|
||||
caching_enabled: bool = True
|
||||
```
|
||||
|
||||
**The Hook API additions:**
|
||||
|
||||
```
|
||||
GET /api/cache # list all discussion cache states
|
||||
GET /api/cache/<discussion_id> # get one
|
||||
GET /api/cache # list all discussion cache states
|
||||
GET /api/cache/<discussion_id> # get one
|
||||
POST /api/cache/<discussion_id>/invalidate
|
||||
POST /api/cache/<discussion_id>/disable
|
||||
POST /api/cache/<discussion_id>/enable
|
||||
@@ -809,15 +809,15 @@ POST /api/cache/<discussion_id>/enable
|
||||
|
||||
```python
|
||||
def _send_claude_code(message, model, *, allowed_tools=None, max_turns=1):
|
||||
options = ClaudeAgentOptions(
|
||||
model=None if not model or model == "default" else model,
|
||||
max_turns=max_turns,
|
||||
tools=list(allowed_tools) if allowed_tools else [],
|
||||
allowed_tools=list(allowed_tools) if allowed_tools else [],
|
||||
cwd=os.getcwd(),
|
||||
)
|
||||
# ... claude_agent_sdk.query(prompt=message, options=options)
|
||||
return _result_with_usage(text, usage, message)
|
||||
options = ClaudeAgentOptions(
|
||||
model=None if not model or model == "default" else model,
|
||||
max_turns=max_turns,
|
||||
tools=list(allowed_tools) if allowed_tools else [],
|
||||
allowed_tools=list(allowed_tools) if allowed_tools else [],
|
||||
cwd=os.getcwd(),
|
||||
)
|
||||
# ... claude_agent_sdk.query(prompt=message, options=options)
|
||||
return _result_with_usage(text, usage, message)
|
||||
```
|
||||
|
||||
### The cross-references
|
||||
|
||||
+288
-288
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -24,15 +24,15 @@ This is one of the most-touched modules in the project. After the nagent_review,
|
||||
|
||||
```
|
||||
aggregate.run(config, aggregation_strategy)
|
||||
├─ find_next_increment(output_dir, namespace) # next file number for output
|
||||
├─ build_file_items(base_dir, files) # read + view-mode transform
|
||||
├─ build_markdown_from_items(file_items, ...) # compose sections
|
||||
│ ├─ ## Files (or Files (Summary) or Files (Tier 3 - Focused))
|
||||
│ │ └─ _build_files_section_from_items OR summarize.build_summary_markdown
|
||||
│ ├─ ## Screenshots (if any)
|
||||
│ ├─ ## Beads Mode: Progress Track (if execution_mode == "beads")
|
||||
│ └─ ## Discussion History (if any)
|
||||
└─ output_file.write_text(markdown)
|
||||
├─ find_next_increment(output_dir, namespace) # next file number for output
|
||||
├─ build_file_items(base_dir, files) # read + view-mode transform
|
||||
├─ build_markdown_from_items(file_items, ...) # compose sections
|
||||
│ ├─ ## Files (or Files (Summary) or Files (Tier 3 - Focused))
|
||||
│ │ └─ _build_files_section_from_items OR summarize.build_summary_markdown
|
||||
│ ├─ ## Screenshots (if any)
|
||||
│ ├─ ## Beads Mode: Progress Track (if execution_mode == "beads")
|
||||
│ └─ ## Discussion History (if any)
|
||||
└─ output_file.write_text(markdown)
|
||||
```
|
||||
|
||||
The **output** is a markdown file at `{output_dir}/{namespace}_{NNN}.md` where `NNN` is a zero-padded increment. The pipeline does not *send* the markdown — that's the AI client's job. The pipeline *produces* the markdown.
|
||||
@@ -54,11 +54,11 @@ The **return value** is `(markdown: str, output_file: Path, file_items: list[dic
|
||||
**Implementation:** `aggregate.py:330-346 build_markdown_from_items`. The three-way dispatch is at lines 335-339:
|
||||
|
||||
```python
|
||||
if aggregation_strategy == "summarize": parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
|
||||
elif aggregation_strategy == "full": parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
|
||||
if aggregation_strategy == "summarize": parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
|
||||
elif aggregation_strategy == "full": parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
|
||||
else: # auto
|
||||
if summary_only: parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
|
||||
else: parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
|
||||
if summary_only: parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
|
||||
else: parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
|
||||
```
|
||||
|
||||
The `auto` strategy is the *only* one that respects `config.project.summary_only`; the other two are explicit overrides. Personas can also set `aggregation_strategy` (per `guide_personas.md`), and a persona-set strategy overrides the config-level setting.
|
||||
@@ -92,16 +92,16 @@ The `auto` strategy is the *only* one that respects `config.project.summary_only
|
||||
```python
|
||||
@dataclass
|
||||
class FileItem:
|
||||
path: str # the artifact identity (path-keyed, no inode)
|
||||
auto_aggregate: bool = True # include in auto-aggregation? (skip in build_*_from_items if False)
|
||||
force_full: bool = False # bypass view_mode; force raw content
|
||||
view_mode: str = 'full' # one of: full, summary, skeleton, outline, masked, custom, none
|
||||
selected: bool = False # for batch operations (the Context Panel multi-select)
|
||||
ast_signatures: bool = False # include only signatures (skeleton-equivalent shortcut)
|
||||
ast_definitions: bool = False # include only definitions (skeleton-equivalent shortcut)
|
||||
ast_mask: dict[str, str] # per-symbol mask: {symbol_path: 'def'|'sig'|'hide'} (from Structural File Editor)
|
||||
custom_slices: list[dict] # Fuzzy Anchor slices: {start_line, end_line, tag, comment, ...}
|
||||
injected_at: Optional[float] # timestamp of last injection
|
||||
path: str # the artifact identity (path-keyed, no inode)
|
||||
auto_aggregate: bool = True # include in auto-aggregation? (skip in build_*_from_items if False)
|
||||
force_full: bool = False # bypass view_mode; force raw content
|
||||
view_mode: str = 'full' # one of: full, summary, skeleton, outline, masked, custom, none
|
||||
selected: bool = False # for batch operations (the Context Panel multi-select)
|
||||
ast_signatures: bool = False # include only signatures (skeleton-equivalent shortcut)
|
||||
ast_definitions: bool = False # include only definitions (skeleton-equivalent shortcut)
|
||||
ast_mask: dict[str, str] # per-symbol mask: {symbol_path: 'def'|'sig'|'hide'} (from Structural File Editor)
|
||||
custom_slices: list[dict] # Fuzzy Anchor slices: {start_line, end_line, tag, comment, ...}
|
||||
injected_at: Optional[float] # timestamp of last injection
|
||||
```
|
||||
|
||||
The 9 fields are *all* serialized by `to_dict()` and *all* deserialized by `from_dict()` (with `.get(..., default)` for forward compatibility). The dataclass is round-trip-safe through TOML.
|
||||
@@ -114,13 +114,13 @@ A `custom_slices` entry is `{start_line, end_line, tag, comment, ...}` (plus Fuz
|
||||
|
||||
```python
|
||||
{
|
||||
"start_line": int, # 1-based original line
|
||||
"end_line": int, # 1-based original line (inclusive)
|
||||
"tag": str|None, # human label, defaults to None
|
||||
"comment": str|None, # human comment, defaults to None
|
||||
"content_hash": str, # SHA-256 of the slice content (for Fuzzy Anchor stability)
|
||||
"anchor_lines": [str, ...],# surrounding context for re-resolution
|
||||
# plus the original positioning metadata
|
||||
"start_line": int, # 1-based original line
|
||||
"end_line": int, # 1-based original line (inclusive)
|
||||
"tag": str|None, # human label, defaults to None
|
||||
"comment": str|None, # human comment, defaults to None
|
||||
"content_hash": str, # SHA-256 of the slice content (for Fuzzy Anchor stability)
|
||||
"anchor_lines": [str, ...],# surrounding context for re-resolution
|
||||
# plus the original positioning metadata
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,10 +144,10 @@ Multiple slices in a file are joined with `\n\n`.
|
||||
```python
|
||||
@dataclass
|
||||
class ContextPreset:
|
||||
name: str # the preset name (used as TOML key)
|
||||
files: list[ContextFileEntry] = field(default_factory=list)
|
||||
screenshots: list[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
name: str # the preset name (used as TOML key)
|
||||
files: list[ContextFileEntry] = field(default_factory=list)
|
||||
screenshots: list[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
```
|
||||
|
||||
`ContextFileEntry` is a `FileItem` (or a string path that's promoted to a `FileItem` on load). The `description` is a human-readable label for the preset list.
|
||||
@@ -170,16 +170,16 @@ class ContextPreset:
|
||||
|
||||
```python
|
||||
def build_discussion_section(history: list[Any]) -> str:
|
||||
sections = []
|
||||
for i, entry in enumerate(history, start=1):
|
||||
if isinstance(entry, dict):
|
||||
role = entry.get("role", "Unknown")
|
||||
content = entry.get("content", "").strip()
|
||||
text = f"{role}: {content}"
|
||||
else:
|
||||
text = str(entry).strip()
|
||||
sections.append(f"### Discussion Excerpt {i}\n\n{text}")
|
||||
return "\n\n---\n\n".join(sections)
|
||||
sections = []
|
||||
for i, entry in enumerate(history, start=1):
|
||||
if isinstance(entry, dict):
|
||||
role = entry.get("role", "Unknown")
|
||||
content = entry.get("content", "").strip()
|
||||
text = f"{role}: {content}"
|
||||
else:
|
||||
text = str(entry).strip()
|
||||
sections.append(f"### Discussion Excerpt {i}\n\n{text}")
|
||||
return "\n\n---\n\n".join(sections)
|
||||
```
|
||||
|
||||
The section handles *both* legacy `list[str]` (e.g. `["User: ...", "AI: ..."]`) and the new `list[dict]` shape (`[{"role": ..., "content": ...}, ...]`). The dict shape is what's persisted by `_flush_disc_entries_to_project` (per `app_controller.py:3225-3240`) and what's stored in the new format.
|
||||
@@ -231,7 +231,7 @@ For Tier 3, `force_full` is treated as a *focus flag*:
|
||||
|
||||
```python
|
||||
if is_focus or tier == 3 or force_full:
|
||||
# full content, no skeleton
|
||||
# full content, no skeleton
|
||||
```
|
||||
|
||||
So a `force_full=True` file in a Tier 3 worker context is treated as a focus file and rendered in full.
|
||||
@@ -244,8 +244,8 @@ So a `force_full=True` file in a Tier 3 worker context is treated as a focus fil
|
||||
|
||||
```python
|
||||
for item in file_items:
|
||||
if not item.get("auto_aggregate", True): continue
|
||||
# ... build section
|
||||
if not item.get("auto_aggregate", True): continue
|
||||
# ... build section
|
||||
```
|
||||
|
||||
Use case: the file is in the `files` list for the AI's *awareness* (e.g. "you can read it via `read_file`") but should not be inlined. The file's `mtime` and `view_mode` are still tracked; the file is *omitted* from the rendered markdown.
|
||||
@@ -384,7 +384,7 @@ For very large codebases (1000+ files), the bottleneck is the tree-sitter parsin
|
||||
- **FileItem schema:** `src/project_files.py:FileItem` (moved out of `src/models.py`)
|
||||
- **ContextPreset schema:** `src/context_presets.py:ContextPreset` (moved out of `src/models.py`)
|
||||
- **ContextPresetManager:** `src/context_presets.py` (30 lines)
|
||||
- **AI client consumption:** `src/ai_client.py:_send_<provider>` × 8 (gemini, anthropic, gemini_cli, deepseek, minimax, qwen, grok, llama), see `guide_ai_client.md`
|
||||
- **AI client consumption:** `src/ai_client.py:_send_<provider>` × 8 (gemini, anthropic, deepseek, minimax, qwen, grok, llama), see `guide_ai_client.md`
|
||||
- **Tier 3 worker consumption:** `src/multi_agent_conductor.py:run_worker_lifecycle`, see `guide_multi_agent_conductor.md`
|
||||
- **Per-file curation features:** `guide_context_curation.md` (Fuzzy Anchors, AST Inspector, Granular AST Control)
|
||||
- **Cache strategy:** `guide_architecture.md §"Cache Hit Strategy"`, `guide_ai_client.md §"Caching"`
|
||||
|
||||
@@ -19,12 +19,12 @@ The dataclass definitions, `DEFAULT_TOOL_CATEGORIES`, the `__getattr__` shim, an
|
||||
```python
|
||||
from src.mma import TrackMetadata
|
||||
|
||||
Metadata = TrackMetadata # legacy class name re-export
|
||||
Metadata = TrackMetadata # legacy class name re-export
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "PROVIDERS":
|
||||
from src import ai_client
|
||||
return ai_client.PROVIDERS
|
||||
from src import ai_client
|
||||
return ai_client.PROVIDERS
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
```
|
||||
|
||||
@@ -56,7 +56,7 @@ The old "one registry to look at" goal is now achieved by **per-system files**.
|
||||
|
||||
| Constant | Current location | Notes |
|
||||
|---|---|---|
|
||||
| `PROVIDERS` | `src/ai_client.py` (re-exported by `src/models.py` via lazy `__getattr__`) | `List[str]` of 8 providers: `gemini`, `anthropic`, `gemini_cli`, `deepseek`, `minimax`, `qwen`, `grok`, `llama` |
|
||||
| `PROVIDERS` | `src/ai_client.py` (re-exported by `src/models.py` via lazy `__getattr__`) | `List[str]` of 7 providers: `gemini`, `anthropic`, `gemini_cli`, `deepseek`, `minimax`, `qwen`, `grok`, `llama` |
|
||||
| `DEFAULT_TOOL_CATEGORIES` | `src/ai_client.py` | The canonical grouping of the MCP tool registry for the UI's category filter |
|
||||
| Tool names (formerly `AGENT_TOOL_NAMES`) | `src/mcp_tool_specs.py:_REGISTRY` + `mcp_tool_specs.tool_names()` | 45 tools. Re-exported as `mcp_client.TOOL_NAMES` for backward compat |
|
||||
| `DEFAULT_TIER_PERSONAS` | `src/mma_prompts.py` | MMA tier → default persona mapping |
|
||||
@@ -132,4 +132,4 @@ All v2 fields default to `False`. The dataclass is `frozen=True`; per-vendor ent
|
||||
- **`src/type_aliases.py`** — The typed boundary + per-aggregate dataclasses
|
||||
- **`src/mcp_tool_specs.py`** — The typed `ToolSpec` registry (45 tools)
|
||||
- **`src/result_types.py`** — `Result[T]`, `ErrorInfo`, `ErrorKind` for data-oriented error handling
|
||||
- **[conductor/tracks/nagent_review_20260608/report.md §6](../conductor/tracks/nagent_review_20260608/report.md)** — Deep-dive on the `FileItem` schema as Manual Slop's strongest curation dimension
|
||||
- **[conductor/tracks/nagent_review_20260608/report.md §6](../conductor/tracks/nagent_review_20260608/report.md)** — Deep-dive on the `FileItem` schema as Manual Slop's strongest curation dimension
|
||||
|
||||
+58
-58
@@ -29,13 +29,13 @@ Defined in `tests/conftest.py`, this session-scoped fixture manages the lifecycl
|
||||
```python
|
||||
@pytest.fixture(scope="session")
|
||||
def live_gui(request) -> Generator["_LiveGuiHandle", None, None]:
|
||||
process = subprocess.Popen(
|
||||
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
||||
stdout=log_file, stderr=log_file, text=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0
|
||||
)
|
||||
# ... (readiness polling + xdist coordination) ...
|
||||
yield _LiveGuiHandle(process, gui_script, workspace=temp_workspace)
|
||||
process = subprocess.Popen(
|
||||
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
||||
stdout=log_file, stderr=log_file, text=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0
|
||||
)
|
||||
# ... (readiness polling + xdist coordination) ...
|
||||
yield _LiveGuiHandle(process, gui_script, workspace=temp_workspace)
|
||||
```
|
||||
|
||||
- **`-u` flag**: Disables output buffering for real-time log capture.
|
||||
@@ -45,13 +45,13 @@ def live_gui(request) -> Generator["_LiveGuiHandle", None, None]:
|
||||
**Readiness polling:**
|
||||
|
||||
```python
|
||||
max_retries = 15 # seconds
|
||||
max_retries = 15 # seconds
|
||||
while time.time() - start_time < max_retries:
|
||||
response = requests.get("http://127.0.0.1:8999/status", timeout=0.5)
|
||||
if response.status_code == 200:
|
||||
ready = True; break
|
||||
if process.poll() is not None: break # Process died early
|
||||
time.sleep(0.5)
|
||||
response = requests.get("http://127.0.0.1:8999/status", timeout=0.5)
|
||||
if response.status_code == 200:
|
||||
ready = True; break
|
||||
if process.poll() is not None: break # Process died early
|
||||
time.sleep(0.5)
|
||||
```
|
||||
|
||||
Polls `GET /status` every 500ms for up to 15 seconds. Checks `process.poll()` each iteration to detect early crashes (avoids waiting the full timeout if the GUI exits). Pre-check: tests if port 8999 is already occupied.
|
||||
@@ -62,11 +62,11 @@ Polls `GET /status` every 500ms for up to 15 seconds. Checks `process.poll()` ea
|
||||
|
||||
```python
|
||||
finally:
|
||||
client = ApiHookClient()
|
||||
client.reset_session() # Clean GUI state before killing
|
||||
time.sleep(0.5)
|
||||
kill_process_tree(process.pid)
|
||||
log_file.close()
|
||||
client = ApiHookClient()
|
||||
client.reset_session() # Clean GUI state before killing
|
||||
time.sleep(0.5)
|
||||
kill_process_tree(process.pid)
|
||||
log_file.close()
|
||||
```
|
||||
|
||||
Sends `reset_session()` via `ApiHookClient` before killing to prevent stale state files.
|
||||
@@ -91,9 +91,9 @@ Sends `reset_session()` via `ApiHookClient` before killing to prevent stale stat
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_ai_client() -> Generator[None, None, None]:
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider("gemini", "gemini-2.5-flash-lite")
|
||||
yield
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider("gemini", "gemini-2.5-flash-lite")
|
||||
yield
|
||||
```
|
||||
|
||||
Runs automatically before every test. Resets the `ai_client` module state and defaults to a safe model, preventing state pollution between tests.
|
||||
@@ -103,9 +103,9 @@ Runs automatically before every test. Resets the `ai_client` module state and de
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_workspace(tmp_path_factory, monkeypatch) -> Generator[None, None, None]:
|
||||
# Redirects the path resolution layer to a temp directory
|
||||
# Prevents tests from writing to the user's actual project
|
||||
...
|
||||
# Redirects the path resolution layer to a temp directory
|
||||
# Prevents tests from writing to the user's actual project
|
||||
...
|
||||
```
|
||||
|
||||
This autouse fixture ensures every test runs against an isolated `tmp_path` workspace. It `monkeypatch`-es `src.paths` so that any code path resolving a project directory (e.g., `manual_slop.toml` lookup, conductor directory resolution, log directory) is redirected to a fresh temp directory per test. Without this, tests could mutate the user's actual `manual_slop.toml` or conductor tracks directory.
|
||||
@@ -117,8 +117,8 @@ This is the primary mechanism for satisfying the **Artifact Isolation** rule in
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_paths() -> Generator[None, None, None]:
|
||||
# Forces `src/paths.py` to re-resolve from environment / config on next access
|
||||
...
|
||||
# Forces `src/paths.py` to re-resolve from environment / config on next access
|
||||
...
|
||||
```
|
||||
|
||||
Pairs with `isolate_workspace` to fully reset the path subsystem. After a test that creates a project config, the next test gets a clean slate.
|
||||
@@ -147,11 +147,11 @@ Structured diagnostic logging for test telemetry:
|
||||
|
||||
```python
|
||||
class VerificationLogger:
|
||||
def __init__(self, test_name: str, script_name: str):
|
||||
self.logs_dir = Path(f"logs/test/{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
||||
def __init__(self, test_name: str, script_name: str):
|
||||
self.logs_dir = Path(f"logs/test/{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
||||
|
||||
def log_state(self, field: str, before: Any, after: Any, delta: Any = None)
|
||||
def finalize(self, description: str, status: str, result_msg: str)
|
||||
def log_state(self, field: str, before: Any, after: Any, delta: Any = None)
|
||||
def finalize(self, description: str, status: str, result_msg: str)
|
||||
```
|
||||
|
||||
Output format: fixed-width column table (`Field | Before | After | Delta`) written to `logs/test/<timestamp>/<script_name>.txt`. Dual output: file + tagged stdout lines for CI visibility.
|
||||
@@ -191,12 +191,12 @@ Enters an epic description and triggers planning. The GUI invokes the LLM (which
|
||||
|
||||
```python
|
||||
for _ in range(60):
|
||||
status = client.get_mma_status()
|
||||
if status.get('pending_mma_spawn_approval'): client.click('btn_approve_spawn')
|
||||
elif status.get('pending_mma_step_approval'): client.click('btn_approve_mma_step')
|
||||
elif status.get('pending_tool_approval'): client.click('btn_approve_tool')
|
||||
if status.get('proposed_tracks') and len(status['proposed_tracks']) > 0: break
|
||||
time.sleep(1)
|
||||
status = client.get_mma_status()
|
||||
if status.get('pending_mma_spawn_approval'): client.click('btn_approve_spawn')
|
||||
elif status.get('pending_mma_step_approval'): client.click('btn_approve_mma_step')
|
||||
elif status.get('pending_tool_approval'): client.click('btn_approve_tool')
|
||||
if status.get('proposed_tracks') and len(status['proposed_tracks']) > 0: break
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
The **approval automation** is a critical pattern repeated in every polling loop. The MMA engine has three approval gates:
|
||||
@@ -235,9 +235,9 @@ Polls until `mma_status == 'running'` or `'done'`. Continues auto-approving all
|
||||
```python
|
||||
streams = status.get('mma_streams', {})
|
||||
if any("Tier 3" in k for k in streams.keys()):
|
||||
tier3_key = [k for k in streams.keys() if "Tier 3" in k][0]
|
||||
if "SUCCESS: Mock Tier 3 worker" in streams[tier3_key]:
|
||||
streams_found = True
|
||||
tier3_key = [k for k in streams.keys() if "Tier 3" in k][0]
|
||||
if "SUCCESS: Mock Tier 3 worker" in streams[tier3_key]:
|
||||
streams_found = True
|
||||
```
|
||||
|
||||
Verifies that `mma_streams` contains a key with "Tier 3" and the value contains the exact mock output string.
|
||||
@@ -262,16 +262,16 @@ A fake Gemini CLI executable that replaces the real `gemini` binary during integ
|
||||
**Input mechanism:**
|
||||
|
||||
```python
|
||||
prompt = sys.stdin.read() # Primary: prompt via stdin
|
||||
sys.argv # Secondary: management command detection
|
||||
os.environ.get('GEMINI_CLI_HOOK_CONTEXT') # Tertiary: environment variable
|
||||
prompt = sys.stdin.read() # Primary: prompt via stdin
|
||||
sys.argv # Secondary: management command detection
|
||||
os.environ.get('GEMINI_CLI_HOOK_CONTEXT') # Tertiary: environment variable
|
||||
```
|
||||
|
||||
**Management command bypass:**
|
||||
|
||||
```python
|
||||
if len(sys.argv) > 1 and sys.argv[1] in ["mcp", "extensions", "skills", "hooks"]:
|
||||
return # Silent exit
|
||||
return # Silent exit
|
||||
```
|
||||
|
||||
**Response routing** — keyword matching on stdin content:
|
||||
@@ -390,22 +390,22 @@ The headless service uses the **Remote Confirmation Protocol** for HITL: when an
|
||||
|
||||
```python
|
||||
class ASTParser:
|
||||
def __init__(self, language: str = "python"):
|
||||
self.language = tree_sitter.Language(tree_sitter_python.language())
|
||||
self.parser = tree_sitter.Parser(self.language)
|
||||
def __init__(self, language: str = "python"):
|
||||
self.language = tree_sitter.Language(tree_sitter_python.language())
|
||||
self.parser = tree_sitter.Parser(self.language)
|
||||
|
||||
def parse(self, code: str) -> tree_sitter.Tree
|
||||
def get_skeleton(self, code: str, path: str = "") -> str
|
||||
def get_curated_view(self, code: str, path: str = "") -> str
|
||||
def get_targeted_view(self, code: str, symbols: List[str], path: str = "") -> str
|
||||
def parse(self, code: str) -> tree_sitter.Tree
|
||||
def get_skeleton(self, code: str, path: str = "") -> str
|
||||
def get_curated_view(self, code: str, path: str = "") -> str
|
||||
def get_targeted_view(self, code: str, symbols: List[str], path: str = "") -> str
|
||||
```
|
||||
|
||||
**`get_skeleton` algorithm:**
|
||||
1. Parse code to tree-sitter AST.
|
||||
2. Walk all `function_definition` nodes.
|
||||
3. For each body (`block` node):
|
||||
- If first non-comment child is a docstring: preserve docstring, replace rest with `...`.
|
||||
- Otherwise: replace entire body with `...`.
|
||||
- If first non-comment child is a docstring: preserve docstring, replace rest with `...`.
|
||||
- Otherwise: replace entire body with `...`.
|
||||
4. Apply edits in reverse byte order (maintains valid offsets).
|
||||
|
||||
**`get_curated_view` algorithm:**
|
||||
@@ -428,10 +428,10 @@ Token-efficient structural descriptions without AI calls:
|
||||
|
||||
```python
|
||||
_SUMMARISERS: dict[str, Callable] = {
|
||||
".py": _summarise_python, # imports, classes, methods, functions, constants
|
||||
".toml": _summarise_toml, # table keys + array lengths
|
||||
".md": _summarise_markdown, # h1-h3 headings
|
||||
".ini": _summarise_generic, # line count + preview
|
||||
".py": _summarise_python, # imports, classes, methods, functions, constants
|
||||
".toml": _summarise_toml, # table keys + array lengths
|
||||
".md": _summarise_markdown, # h1-h3 headings
|
||||
".ini": _summarise_generic, # line count + preview
|
||||
}
|
||||
```
|
||||
|
||||
@@ -455,8 +455,8 @@ functions: summarise_file, build_summary_markdown
|
||||
|
||||
```python
|
||||
class CodeOutliner:
|
||||
def __init__(self) -> None: ...
|
||||
def outline(self, code: str) -> str: ...
|
||||
def __init__(self) -> None: ...
|
||||
def outline(self, code: str) -> str: ...
|
||||
|
||||
def get_outline(path: Path, code: str) -> str: ...
|
||||
```
|
||||
|
||||
+66
-66
@@ -11,9 +11,9 @@ The AI's ability to interact with the filesystem is mediated by a three-layer se
|
||||
### Global State
|
||||
|
||||
```python
|
||||
_allowed_paths: set[Path] = set() # Explicit file allowlist (resolved absolutes)
|
||||
_base_dirs: set[Path] = set() # Directory roots for containment checks
|
||||
_primary_base_dir: Path | None = None # Used for resolving relative paths
|
||||
_allowed_paths: set[Path] = set() # Explicit file allowlist (resolved absolutes)
|
||||
_base_dirs: set[Path] = set() # Directory roots for containment checks
|
||||
_primary_base_dir: Path | None = None # Used for resolving relative paths
|
||||
perf_monitor_callback: Optional[Callable[[], dict[str, Any]]] = None
|
||||
```
|
||||
|
||||
@@ -61,7 +61,7 @@ The `dispatch` function (`mcp_client.py:1322`) is a flat if/elif chain mapping 4
|
||||
| Tool | Parameters | Description |
|
||||
|---|---|---|
|
||||
| `read_file` | `path` | UTF-8 file content extraction |
|
||||
| `list_directory` | `path` | Compact table: `[file/dir] name size`. Applies blacklist filter to entries. |
|
||||
| `list_directory` | `path` | Compact table: `[file/dir] name size`. Applies blacklist filter to entries. |
|
||||
| `search_files` | `path`, `pattern` | Glob pattern matching within an allowed directory. Applies blacklist filter. |
|
||||
| `get_file_slice` | `path`, `start_line`, `end_line` | Returns specific line range (1-based, inclusive) |
|
||||
| `set_file_slice` | `path`, `start_line`, `end_line`, `new_content` | Replaces a line range with new content (surgical edit) |
|
||||
@@ -166,28 +166,28 @@ See [guide_beads.md](guide_beads.md) (placeholder; written in Task 10) for the f
|
||||
**AST-based read tools** follow this pattern:
|
||||
```python
|
||||
def py_get_skeleton(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
if not p.exists(): return f"ERROR: file not found: {path}"
|
||||
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
|
||||
from file_cache import ASTParser
|
||||
code = p.read_text(encoding="utf-8")
|
||||
parser = ASTParser("python")
|
||||
return parser.get_skeleton(code)
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
if not p.exists(): return f"ERROR: file not found: {path}"
|
||||
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
|
||||
from file_cache import ASTParser
|
||||
code = p.read_text(encoding="utf-8")
|
||||
parser = ASTParser("python")
|
||||
return parser.get_skeleton(code)
|
||||
```
|
||||
|
||||
**AST-based write tools** use stdlib `ast` (not tree-sitter) to locate symbols, then delegate to `set_file_slice`:
|
||||
```python
|
||||
def py_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF)) # Strip BOM
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name) # Walks AST for matching node
|
||||
if not node: return f"ERROR: could not find definition '{name}'"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
return set_file_slice(path, start, end, new_content)
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF)) # Strip BOM
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name) # Walks AST for matching node
|
||||
if not node: return f"ERROR: could not find definition '{name}'"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
return set_file_slice(path, start, end, new_content)
|
||||
```
|
||||
|
||||
The `_get_symbol_node` helper supports dot notation (`ClassName.method_name`) by first finding the class, then searching its body for the method.
|
||||
@@ -200,19 +200,19 @@ Tools can be executed concurrently via `async_dispatch`:
|
||||
|
||||
```python
|
||||
async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
"""Dispatch an MCP tool call asynchronously."""
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
"""Dispatch an MCP tool call asynchronously."""
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
```
|
||||
|
||||
In `ai_client.py`, multiple tool calls within a single AI turn are executed in parallel:
|
||||
|
||||
```python
|
||||
async def _execute_tool_calls_concurrently(calls, base_dir, ...):
|
||||
tasks = []
|
||||
for fc in calls:
|
||||
tasks.append(_execute_single_tool_call_async(name, args, ...))
|
||||
results = await asyncio.gather(*tasks)
|
||||
return results
|
||||
tasks = []
|
||||
for fc in calls:
|
||||
tasks.append(_execute_single_tool_call_async(name, args, ...))
|
||||
results = await asyncio.gather(*tasks)
|
||||
return results
|
||||
```
|
||||
|
||||
This significantly reduces latency when the AI makes multiple independent file reads in a single turn.
|
||||
@@ -229,16 +229,16 @@ Manual Slop exposes a REST-based IPC interface on `127.0.0.1:8999` using Python'
|
||||
|
||||
```python
|
||||
class HookServerInstance(ThreadingHTTPServer):
|
||||
app: Any # Reference to main App instance
|
||||
app: Any # Reference to main App instance
|
||||
|
||||
class HookHandler(BaseHTTPRequestHandler):
|
||||
# Accesses self.server.app for all state
|
||||
# Accesses self.server.app for all state
|
||||
|
||||
class HookServer:
|
||||
app: Any
|
||||
port: int = 8999
|
||||
server: HookServerInstance | None
|
||||
thread: threading.Thread | None
|
||||
app: Any
|
||||
port: int = 8999
|
||||
server: HookServerInstance | None
|
||||
thread: threading.Thread | None
|
||||
```
|
||||
|
||||
**Start conditions**: Only starts if `app.test_hooks_enabled == True` OR current provider is `'gemini_cli'`. Otherwise `start()` silently returns.
|
||||
@@ -274,20 +274,20 @@ This ensures all state reads happen on the GUI main thread during `_process_pend
|
||||
|
||||
```python
|
||||
{
|
||||
"mma_status": str, # "idle" | "planning" | "executing" | "done"
|
||||
"ai_status": str, # "idle" | "sending..." | etc.
|
||||
"active_tier": str | None,
|
||||
"active_track": str, # Track ID or raw value
|
||||
"active_tickets": list, # Serialized ticket dicts
|
||||
"mma_step_mode": bool,
|
||||
"pending_tool_approval": bool, # _pending_ask_dialog
|
||||
"pending_mma_step_approval": bool, # _pending_mma_approval is not None
|
||||
"pending_mma_spawn_approval": bool, # _pending_mma_spawn is not None
|
||||
"pending_approval": bool, # Backward compat: step OR tool
|
||||
"pending_spawn": bool, # Alias for spawn approval
|
||||
"tracks": list,
|
||||
"proposed_tracks": list,
|
||||
"mma_streams": dict, # {stream_id: output_text}
|
||||
"mma_status": str, # "idle" | "planning" | "executing" | "done"
|
||||
"ai_status": str, # "idle" | "sending..." | etc.
|
||||
"active_tier": str | None,
|
||||
"active_track": str, # Track ID or raw value
|
||||
"active_tickets": list, # Serialized ticket dicts
|
||||
"mma_step_mode": bool,
|
||||
"pending_tool_approval": bool, # _pending_ask_dialog
|
||||
"pending_mma_step_approval": bool, # _pending_mma_approval is not None
|
||||
"pending_mma_spawn_approval": bool, # _pending_mma_spawn is not None
|
||||
"pending_approval": bool, # Backward compat: step OR tool
|
||||
"pending_spawn": bool, # Alias for spawn approval
|
||||
"tracks": list,
|
||||
"proposed_tracks": list,
|
||||
"mma_streams": dict, # {stream_id: output_text}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -295,9 +295,9 @@ This ensures all state reads happen on the GUI main thread during `_process_pend
|
||||
|
||||
```python
|
||||
{
|
||||
"thinking": bool, # ai_status in ["sending...", "running powershell..."]
|
||||
"live": bool, # ai_status in ["running powershell...", "fetching url...", ...]
|
||||
"prior": bool, # app.is_viewing_prior_session
|
||||
"thinking": bool, # ai_status in ["sending...", "running powershell..."]
|
||||
"live": bool, # ai_status in ["running powershell...", "fetching url...", ...]
|
||||
"prior": bool, # app.is_viewing_prior_session
|
||||
}
|
||||
```
|
||||
|
||||
@@ -340,7 +340,7 @@ The counterpart `/api/ask/respond`:
|
||||
|
||||
```python
|
||||
class ApiHookClient:
|
||||
def __init__(self, base_url="http://127.0.0.1:8999", max_retries=5, retry_delay=0.2)
|
||||
def __init__(self, base_url="http://127.0.0.1:8999", max_retries=5, retry_delay=0.2)
|
||||
```
|
||||
|
||||
### Connection Methods
|
||||
@@ -400,21 +400,21 @@ Tool calls are executed concurrently within a single AI turn using `asyncio.gath
|
||||
|
||||
```python
|
||||
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
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
"""
|
||||
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
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
```
|
||||
|
||||
All tools are wrapped in `asyncio.to_thread()` to prevent blocking the event loop. This enables `ai_client.py` to execute multiple tools via `asyncio.gather()`:
|
||||
|
||||
```python
|
||||
results = await asyncio.gather(
|
||||
async_dispatch("read_file", {"path": "src/module_a.py"}),
|
||||
async_dispatch("read_file", {"path": "src/module_b.py"}),
|
||||
async_dispatch("get_file_summary", {"path": "src/module_c.py"}),
|
||||
async_dispatch("read_file", {"path": "src/module_a.py"}),
|
||||
async_dispatch("read_file", {"path": "src/module_b.py"}),
|
||||
async_dispatch("get_file_summary", {"path": "src/module_c.py"}),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -453,13 +453,13 @@ Summary:
|
||||
|
||||
```
|
||||
logs/sessions/<session_id>/
|
||||
comms.log # JSON-L: every API interaction (direction, kind, payload)
|
||||
toolcalls.log # Markdown: sequential tool invocation records
|
||||
apihooks.log # API hook invocations
|
||||
clicalls.log # JSON-L: CLI subprocess details (command, stdin, stdout, stderr, latency)
|
||||
comms.log # JSON-L: every API interaction (direction, kind, payload)
|
||||
toolcalls.log # Markdown: sequential tool invocation records
|
||||
apihooks.log # API hook invocations
|
||||
clicalls.log # JSON-L: CLI subprocess details (command, stdin, stdout, stderr, latency)
|
||||
|
||||
scripts/generated/
|
||||
<ts>_<seq:04d>.ps1 # Each AI-generated PowerShell script, preserved in order
|
||||
<ts>_<seq:04d>.ps1 # Each AI-generated PowerShell script, preserved in order
|
||||
```
|
||||
|
||||
### Logging Functions
|
||||
|
||||
Reference in New Issue
Block a user