docs(guide_models): rewrite for src/models.py shim reality
The guide described models.py as a 132KB centralized registry with Provider/ModelInfo enums, Ticket/Track classes, AGENT_TOOL_NAMES, and parse_plan_md. None of that is in models.py anymore — it's a ~1.5KB re-export shim (Metadata=TrackMetadata alias + PROVIDERS lazy __getattr__). Dataclasses moved to per-system files (mma.py, project_files.py, type_aliases.py, mcp_tool_specs.py, result_types.py). VendorCapabilities moved from the deleted vendor_capabilities.py into ai_client.py #region. Rewrote the guide to reflect the current where-each-model-lives table.
This commit is contained in:
+76
-537
@@ -1,4 +1,4 @@
|
||||
# `src/models.py` — Data Models
|
||||
# `src/models.py` — Legacy Re-Export Shim
|
||||
|
||||
[Top](../Readme.md) | [Architecture](guide_architecture.md) | [MMA](guide_mma.md) | [App Controller](guide_app_controller.md)
|
||||
|
||||
@@ -6,591 +6,130 @@
|
||||
|
||||
## Overview
|
||||
|
||||
`src/models.py` (~132KB) is the **centralized data model registry**. It defines every data structure used across the app — Tickets, Tracks, Personas, Presets, Discussion entries, Context files, etc. — using `pydantic` and `dataclasses`.
|
||||
`src/models.py` is now a **~1.5KB legacy re-export shim**. It is NOT a data model registry anymore.
|
||||
|
||||
The file exists to **eliminate redundant model definitions** scattered across modules. It also serves as the single source of truth for serialization (TOML, JSON-L, Markdown).
|
||||
The dataclass definitions, `DEFAULT_TOOL_CATEGORIES`, the `__getattr__` shim, and the Pydantic proxies were moved out per:
|
||||
- `module_taxonomy_refactor_20260627` Phase 5 (reduce to Pydantic proxies)
|
||||
- `post_module_taxonomy_de_cruft_20260627` Phases 2-4 (de-cruft removals)
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **One place to look for any data structure**: If you need to know what fields a `Ticket` has, look here.
|
||||
2. **Strict types**: `pydantic` for fields with validation, `dataclasses` for internal structures.
|
||||
3. **No business logic**: Models are pure data. Methods like `to_toml()` are allowed; methods like `execute()` are not.
|
||||
4. **SDM tags**: Every model has `[C: ...]` (callers) and `[M: ...]` (mutators) tags in docstrings for AI-assisted impact analysis.
|
||||
|
||||
---
|
||||
|
||||
## Model Categories
|
||||
|
||||
The file is organized into regions:
|
||||
**Remaining content:**
|
||||
- The legacy `Metadata = TrackMetadata` alias — tests that import `from src.models import Metadata` expecting the dataclass still resolve to the same object. The `Metadata` TYPE ALIAS (from `src.type_aliases`) is the boundary wire type; legacy consumers wanting the dataclass should migrate to `from src.mma import TrackMetadata`.
|
||||
- The `PROVIDERS` lazy `__getattr__` (loads from `src.ai_client` on first access; required to break a startup-speedup circular import).
|
||||
|
||||
```python
|
||||
#region: Core Models
|
||||
#endregion: Core Models
|
||||
from src.mma import TrackMetadata
|
||||
|
||||
#region: AI Models
|
||||
#endregion: AI Models
|
||||
Metadata = TrackMetadata # legacy class name re-export
|
||||
|
||||
#region: Preset Models
|
||||
#endregion: Preset Models
|
||||
|
||||
#region: Persona Models
|
||||
#endregion: Persona Models
|
||||
|
||||
#region: Context Models
|
||||
#endregion: Context Models
|
||||
|
||||
#region: MMA Models
|
||||
#endregion: MMA Models
|
||||
|
||||
#region: UI State Models
|
||||
#endregion: UI State Models
|
||||
|
||||
#region: Logging Models
|
||||
#endregion: Logging Models
|
||||
```
|
||||
|
||||
### `Provider`, `ModelInfo` — AI Models
|
||||
|
||||
```python
|
||||
class Provider(str, Enum):
|
||||
GEMINI = "gemini"
|
||||
ANTHROPIC = "anthropic"
|
||||
DEEPSEEK = "deepseek"
|
||||
MINIMAX = "MiniMax"
|
||||
GEMINI_CLI = "gemini-cli"
|
||||
|
||||
@dataclass
|
||||
class ModelInfo:
|
||||
name: str
|
||||
provider: Provider
|
||||
context_window: int
|
||||
max_output_tokens: int
|
||||
supports_caching: bool = False
|
||||
cost_per_1k_input: float = 0.0
|
||||
cost_per_1k_output: float = 0.0
|
||||
```
|
||||
|
||||
These back the AI Settings panel and the cost tracker.
|
||||
|
||||
### `DiscussionEntry`, `Message` — Discussion History
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Message:
|
||||
role: Literal["user", "assistant", "system", "tool"]
|
||||
content: str
|
||||
timestamp: float
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class DiscussionEntry:
|
||||
entry_id: str
|
||||
messages: list[Message]
|
||||
is_take_root: bool = False # First message of a "take" (timeline branch)
|
||||
parent_take_id: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
Discussion history is a list of `DiscussionEntry` objects, each containing one or more `Message` objects. The branching structure supports "takes" (alternative timeline branches).
|
||||
|
||||
### `ContextFileEntry`, `ContextScreenshot` — Context
|
||||
|
||||
```python
|
||||
class ViewMode(str, Enum):
|
||||
FULL = "full"
|
||||
SUMMARIZE = "summarize"
|
||||
SKELETON = "skeleton"
|
||||
OUTLINE = "outline"
|
||||
NONE = "none"
|
||||
|
||||
@dataclass
|
||||
class ContextFileEntry:
|
||||
path: str # Absolute or relative to project root
|
||||
view_mode: ViewMode = ViewMode.FULL
|
||||
annotations: list[Annotation] = field(default_factory=list)
|
||||
fuzzy_slice: FuzzySlice | None = None # Optional line range
|
||||
|
||||
@dataclass
|
||||
class ContextScreenshot:
|
||||
path: str # Absolute path to image
|
||||
caption: str = ""
|
||||
```
|
||||
|
||||
Context is a composition of files + screenshots, each with optional view mode and line-range slicing.
|
||||
|
||||
### `FuzzySlice`, `Annotation` — Visual Slice Editor
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FuzzySlice:
|
||||
start_anchor: str # Fuzzy-matched string
|
||||
end_anchor: str
|
||||
start_offset: int = 0
|
||||
end_offset: int = 0
|
||||
fallback_start_line: int | None = None
|
||||
fallback_end_line: int | None = None
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
kind: Literal["tag", "comment"]
|
||||
text: str
|
||||
line_range: tuple[int, int] | None = None
|
||||
```
|
||||
|
||||
Fuzzy slices use **anchor-based matching** to survive code modifications. If `start_anchor` shifts due to edits, the slice re-anchors on the next render.
|
||||
|
||||
See **[docs/guide_context_curation.md](guide_context_curation.md)** for the full Visual Slice Editor.
|
||||
|
||||
### `Ticket`, `Track`, `WorkerContext` — MMA
|
||||
|
||||
```python
|
||||
class TicketStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
DONE = "done"
|
||||
BLOCKED = "blocked"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
class TicketPriority(str, Enum):
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
@dataclass
|
||||
class Ticket:
|
||||
ticket_id: str
|
||||
title: str
|
||||
description: str
|
||||
status: TicketStatus = TicketStatus.PENDING
|
||||
priority: TicketPriority = TicketPriority.MEDIUM
|
||||
depends_on: list[str] = field(default_factory=list)
|
||||
blocks: list[str] = field(default_factory=list)
|
||||
files_involved: list[str] = field(default_factory=list)
|
||||
persona: str | None = None
|
||||
result: dict | None = None
|
||||
error: str | None = None
|
||||
commit_sha: str | None = None
|
||||
|
||||
@dataclass
|
||||
class Track:
|
||||
track_id: str
|
||||
title: str
|
||||
description: str
|
||||
tickets: list[Ticket]
|
||||
plan_path: str
|
||||
created_at: float
|
||||
checkpoints: list[TrackCheckpoint] = field(default_factory=list)
|
||||
|
||||
@dataclass
|
||||
class TrackCheckpoint:
|
||||
sha: str
|
||||
phase: str
|
||||
timestamp: float
|
||||
note: str
|
||||
|
||||
@dataclass
|
||||
class WorkerContext:
|
||||
"""The minimal context slice given to a tier3-worker sub-agent."""
|
||||
ticket_id: str
|
||||
track_id: str
|
||||
persona: str | None
|
||||
focus_files: list[str]
|
||||
skeleton_views: dict[str, str] # path -> skeleton string
|
||||
history: list[Message] # Recent messages from the parent
|
||||
conductor_notes: str
|
||||
```
|
||||
|
||||
`WorkerContext` is the **Token Firewall** boundary: this is exactly what each Tier 3 worker sees. It includes only the focus files, their skeletons, and recent history. The parent agent's full state is never visible.
|
||||
|
||||
### `Persona`, `Preset`, `ContextPreset`, `ToolPreset` — Configuration
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Persona:
|
||||
name: str
|
||||
model: str | None = None
|
||||
system_prompt: str | None = None
|
||||
tool_weights: dict[str, int] = field(default_factory=dict) # tool_name -> 1..5
|
||||
parameter_biases: dict[str, Any] = field(default_factory=dict)
|
||||
bias_profile: str | None = None
|
||||
tier_assignments: dict[str, str] = field(default_factory=dict) # tier -> persona_name
|
||||
description: str = ""
|
||||
|
||||
@dataclass
|
||||
class Preset:
|
||||
name: str
|
||||
base_prompt: str
|
||||
user_instructions: str
|
||||
full_text: str # base_prompt + user_instructions
|
||||
temperature: float = 0.7
|
||||
top_p: float = 0.95
|
||||
max_output_tokens: int = 8192
|
||||
is_foundation: bool = False # True for the foundational base prompt
|
||||
|
||||
@dataclass
|
||||
class ContextPreset:
|
||||
name: str
|
||||
files: list[ContextFileEntry]
|
||||
screenshots: list[ContextScreenshot]
|
||||
description: str = ""
|
||||
last_validated: float = 0.0
|
||||
|
||||
@dataclass
|
||||
class ToolPreset:
|
||||
name: str
|
||||
enabled_tools: dict[str, bool] = field(default_factory=dict) # tool_name -> enabled
|
||||
weights: dict[str, int] = field(default_factory=dict) # tool_name -> 1..5
|
||||
parameter_biases: dict[str, Any] = field(default_factory=dict)
|
||||
bias_profile: str | None = None
|
||||
description: str = ""
|
||||
```
|
||||
|
||||
Personas consolidate **everything an agent needs** into a single named entity. Presets are simpler — just system prompt + parameters.
|
||||
|
||||
### `CommsLogEntry`, `LogEntry` — Logging
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CommsLogEntry:
|
||||
timestamp: float
|
||||
source: str # "main", "tier3-worker", "tier4-qa"
|
||||
role: str # "user", "assistant", "system"
|
||||
payload_type: str # "prompt", "response", "tool_call", "tool_result"
|
||||
content: str
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
ticket_id: str | None = None
|
||||
|
||||
@dataclass
|
||||
class LogEntry:
|
||||
timestamp: float
|
||||
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
|
||||
message: str
|
||||
source: str # Module or subsystem name
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
Comms logs are append-only and stored as JSON-L. They are the **primary debugging surface** for AI interactions.
|
||||
|
||||
### `UIPerformanceSnapshot`, `DiagnosticEntry` — Diagnostics
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class UIPerformanceSnapshot:
|
||||
timestamp: float
|
||||
fps: float
|
||||
frame_time_ms: float
|
||||
cpu_pct: float
|
||||
input_lag_ms: float
|
||||
|
||||
@dataclass
|
||||
class DiagnosticEntry:
|
||||
timestamp: float
|
||||
component: str # "DAG Engine", "Aggregation", "Panel:Command Palette"
|
||||
hit_count: int
|
||||
total_latency_ms: float
|
||||
peak_latency_ms: float
|
||||
min_latency_ms: float
|
||||
```
|
||||
|
||||
Diagnostics power the **Performance Diagnostics** panel (FPS, Frame Time, CPU, plus per-component hit counts and latencies).
|
||||
|
||||
### `HookRequest`, `HookResponse` — Hook API
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class HookRequest:
|
||||
action: str # "click", "set_value", "custom_callback", etc.
|
||||
item: str | None = None
|
||||
value: Any = None
|
||||
callback: str | None = None
|
||||
args: list[Any] = field(default_factory=list)
|
||||
kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class HookResponse:
|
||||
status: Literal["ok", "error", "queued", "rejected"]
|
||||
message: str = ""
|
||||
data: dict[str, Any] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
### `WorkspaceProfile` — Layouts
|
||||
|
||||
```python
|
||||
class WorkspaceProfile:
|
||||
name: str
|
||||
ini_content: str # ImGui ini settings string (from SaveIniSettingsToMemory)
|
||||
show_windows: Dict[str, bool] = field(default_factory=dict)
|
||||
panel_states: Dict[str, Any] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
> **Note:** The 2026-06-05 refactor (per `live_gui_fragility_fixes_20260605`) collapsed the previous design (which had `docking_layout: bytes` with base64, plus `theme`, `theme_fx_enabled`, `captured_at`, `description` as additional fields) into a 4-field model. `ini_content` is a plain `str` (not base64) because `ImGui.SaveIniSettingsToMemory()` returns a string and `tomli_w` rejects `bytes`. `LayoutPreset` is no longer a separate class; multi-viewport state lives in `panel_states`. See [guide_workspace_profiles.md](guide_workspace_profiles.md) for the full data model and TOML example.
|
||||
|
||||
### `RAGConfig`, `RAGChunk`, `RAGResult` — RAG
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class RAGConfig:
|
||||
enabled: bool = False
|
||||
source: Literal["chromadb", "external_mcp"] = "chromadb"
|
||||
embedding_provider: str = "gemini-embedding-001"
|
||||
chunk_size: int = 512
|
||||
chunk_overlap: int = 64
|
||||
top_k: int = 5
|
||||
external_mcp_server: str | None = None
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RAGChunk:
|
||||
id: str = ""
|
||||
document: str = ""
|
||||
path: str = ""
|
||||
score: float = 0.0
|
||||
metadata: Metadata = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class RAGResult:
|
||||
chunks: list[RAGChunk]
|
||||
query: str
|
||||
distance_threshold: float = 0.0
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "PROVIDERS":
|
||||
from src import ai_client
|
||||
return ai_client.PROVIDERS
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Constants
|
||||
## Where the data models actually live now
|
||||
|
||||
The file also defines several module-level constants used across the app:
|
||||
The old "one registry to look at" goal is now achieved by **per-system files**. Each subsystem owns its own data models in its own file:
|
||||
|
||||
```python
|
||||
# Provider routing
|
||||
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"]
|
||||
| Model(s) | Current location | Notes |
|
||||
|---|---|---|
|
||||
| `TrackMetadata` (formerly `Metadata` dataclass) | `src/mma.py` | The track/ticket data layer |
|
||||
| `Ticket`, `Track`, `WorkerContext` | `src/mma.py` | MMA domain models |
|
||||
| `VendorCapabilities`, `VendorMetric` | `src/ai_client.py` (`#region: Vendor Capabilities`) | Moved from the deleted `src/vendor_capabilities.py` |
|
||||
| `FileItem`, `FileItems` | `src/project_files.py` | Per-file curation memory |
|
||||
| `Result[T]`, `ErrorInfo`, `ErrorKind`, `NilPath` | `src/result_types.py` | Data-oriented error handling |
|
||||
| `Metadata`, `CommsLogEntry`, `HistoryMessage`, `ToolDefinition`, `ToolCall`, `CommsLogCallback`, `SessionInsights`, `DiscussionSettings`, `CustomSlice`, `MMAUsageStats`, `ProviderPayload`, `UIPanelConfig`, `PathInfo`, `JsonPrimitive`, `JsonValue`, `FileItemsDiff` | `src/type_aliases.py` | The typed boundary + per-aggregate dataclasses (see [guide_context_aggregation.md](guide_context_aggregation.md)) |
|
||||
| `ToolSpec`, `ToolParameter` | `src/mcp_tool_specs.py` | Typed tool-spec registry (replaces the legacy `MCP_TOOL_SPECS: list[dict[str, Any]]`) |
|
||||
| `MCPServerConfig`, `MCPConfiguration`, `VectorStoreConfig`, `RAGConfig` | `src/mcp_client.py` | MCP subsystem data layer |
|
||||
| `WorkspaceProfile` | `src/workspace_manager.py` | Layout profiles |
|
||||
| `Persona`, `Preset`, `ContextPreset`, `ToolPreset`, `Tool`, `BiasProfile` | `src/personas.py`, `src/presets.py`, `src/context_presets.py`, `src/tool_presets.py` | Manager-owned config models |
|
||||
| `TicketStatus`, `TicketPriority`, `TrackCheckpoint` | `src/mma.py` | MMA enums |
|
||||
|
||||
# Tool categories (for Tool Bias)
|
||||
TOOL_CATEGORIES: list[str] = [
|
||||
"File I/O",
|
||||
"Python AST",
|
||||
"C/C++ AST",
|
||||
"Analysis",
|
||||
"Network",
|
||||
"Runtime",
|
||||
"Beads",
|
||||
]
|
||||
|
||||
# MMA tier -> default persona
|
||||
DEFAULT_TIER_PERSONAS: dict[str, str] = {
|
||||
"tier1": "orchestrator",
|
||||
"tier2": "tech-lead",
|
||||
"tier3": "worker",
|
||||
"tier4": "qa",
|
||||
}
|
||||
|
||||
# AGENT_TOOL_NAMES — the canonical list of all 45 tool names
|
||||
AGENT_TOOL_NAMES: list[str] = [
|
||||
"read_file", "list_directory", "search_files", "get_file_summary",
|
||||
"get_file_slice", "set_file_slice", "edit_file",
|
||||
# ... all 45 ...
|
||||
]
|
||||
```
|
||||
|
||||
These constants eliminate the **scattered list definitions** problem — every module imports the same source of truth.
|
||||
**Rule of thumb:** if you need to know what fields a type has, read the per-system guide (e.g., [guide_context_aggregation.md](guide_context_aggregation.md) for `FileItem`, [guide_mma.md](guide_mma.md) for `Ticket`/`Track`), or read the file directly with `py_get_skeleton`.
|
||||
|
||||
---
|
||||
|
||||
## Serialization
|
||||
## Where the constants live now
|
||||
|
||||
Models use a mix of strategies:
|
||||
- **`pydantic` models**: For TOML round-trip with validation (Persona, Preset, ContextPreset, ToolPreset, WorkspaceProfile, RAGConfig).
|
||||
- **`dataclasses.asdict()`**: For JSON-L logging (CommsLogEntry, LogEntry, DiscussionEntry, Message).
|
||||
- **Custom tomli-w / tomllib**: For the modules that need precise control over TOML output ordering.
|
||||
| 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` |
|
||||
| `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 |
|
||||
| `_VENDOR_REGISTRY` | `src/ai_client.py` | The `VendorCapabilities` registry, populated via `register()` at import time |
|
||||
|
||||
Most serialization is done by the **manager classes** (PresetManager, PersonaManager, etc.) — the model itself is pure data.
|
||||
**Audit:** `scripts/audit_providers_source_of_truth.py` fails if `PROVIDERS` is declared as a literal in `src/models.py`.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
## Why the shim exists at all
|
||||
|
||||
`pydantic` validators enforce constraints:
|
||||
Backward compatibility. The refactor tracks moved the dataclasses out, but a long tail of tests and consumers still import `from src.models import Metadata` expecting the dataclass. The shim re-exports `TrackMetadata` under the legacy name so those imports keep resolving to the same object.
|
||||
|
||||
```python
|
||||
class Preset(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=64)
|
||||
temperature: float = Field(0.7, ge=0.0, le=2.0)
|
||||
top_p: float = Field(0.95, ge=0.0, le=1.0)
|
||||
max_output_tokens: int = Field(8192, ge=1, le=200000)
|
||||
|
||||
@validator('name')
|
||||
def name_must_be_safe(cls, v):
|
||||
if '/' in v or '\\' in v:
|
||||
raise ValueError("name cannot contain path separators")
|
||||
return v
|
||||
```
|
||||
|
||||
Validators run on load and on save. The managers call `.model_dump()` / `Preset.parse_obj(dict)` to round-trip.
|
||||
New code should import directly from the owning file:
|
||||
- `from src.mma import TrackMetadata` (the dataclass)
|
||||
- `from src.type_aliases import Metadata` (the boundary wire type — different thing)
|
||||
- `from src.ai_client import PROVIDERS, VendorCapabilities, get_capabilities`
|
||||
- `from src.mcp_tool_specs import tool_names, get_tool_spec`
|
||||
|
||||
---
|
||||
|
||||
## The `parse_plan_md` Function
|
||||
## `parse_plan_md`
|
||||
|
||||
A critical utility that converts a markdown plan file to `Track` and `Ticket` objects:
|
||||
|
||||
```python
|
||||
def parse_plan_md(plan_path: Path) -> list[Ticket]:
|
||||
"""Parse a plan.md file into a list of Ticket objects."""
|
||||
text = plan_path.read_text(encoding="utf-8")
|
||||
tickets = []
|
||||
current_phase = None
|
||||
for line in text.splitlines():
|
||||
line = line.rstrip()
|
||||
if not line:
|
||||
continue
|
||||
# Phase heading
|
||||
if line.startswith("# "):
|
||||
current_phase = line[2:].strip()
|
||||
continue
|
||||
# Ticket line
|
||||
m = re.match(r'^\s*-\s*\[(.)\]\s*(.+?)(?:\s*\[depends:\s*([^\]]+)\])?\s*$', line)
|
||||
if not m:
|
||||
continue
|
||||
marker, rest, deps = m.groups()
|
||||
status = {" ": "pending", "~": "running", "x": "done", "!": "blocked"}.get(marker, "pending")
|
||||
# Split rest into ticket_id and title
|
||||
id_match = re.match(r'(\S+):\s*(.+)', rest)
|
||||
if id_match:
|
||||
tid, title = id_match.groups()
|
||||
else:
|
||||
tid, title = rest, rest
|
||||
tickets.append(Ticket(
|
||||
ticket_id=tid.strip(),
|
||||
title=title.strip(),
|
||||
description="",
|
||||
status=TicketStatus(status),
|
||||
depends_on=[d.strip() for d in (deps or "").split(",") if d.strip()],
|
||||
))
|
||||
return tickets
|
||||
```
|
||||
|
||||
The DAG engine uses the returned `Ticket` objects to build the dependency graph.
|
||||
The plan-parsing utility was moved to `src/mma.py` alongside the `Track`/`Ticket` models it constructs. See [guide_mma.md](guide_mma.md) for the current signature and usage.
|
||||
|
||||
---
|
||||
|
||||
## The `AppState` Class
|
||||
## `AppState`
|
||||
|
||||
A separate large dataclass that aggregates all GUI-visible state. **Lives in `src/app_controller.py`**, not here, because it holds the controller's runtime state (not a pure data model). But it follows the same conventions (typed fields, no methods, SDM tags).
|
||||
`AppState` (the controller's runtime state aggregate) lives in `src/app_controller.py` as before. It was never in `src/models.py`; the old version of this guide was correct to note that. See [guide_app_controller.md](guide_app_controller.md).
|
||||
|
||||
---
|
||||
|
||||
## How Models Are Used
|
||||
## Adding a new model
|
||||
|
||||
### In `src/presets.py`
|
||||
1. Add the dataclass to the **owning system's file** (e.g., a new MMA model goes in `src/mma.py`; a new tool-spec shape goes in `src/mcp_tool_specs.py`). Do NOT add it to `src/models.py` — that file is a shim only.
|
||||
2. If the model is a typed boundary/aggregate shape, add it to `src/type_aliases.py` instead.
|
||||
3. Add `to_dict()` / `from_dict()` if the model is persisted.
|
||||
4. Add a docstring with `[C: ...]` (callers) and `[M: ...]` (mutators) SDM tags.
|
||||
5. Add tests in the relevant `tests/test_<system>.py`.
|
||||
6. If the model warrants a GUI surface, add a rendering helper in `src/gui_2.py` (module-level `render_<thing>(app: App)`).
|
||||
7. Update the relevant `docs/guide_<system>.md` to document the new model.
|
||||
|
||||
```python
|
||||
def save_preset(preset: Preset) -> None:
|
||||
data = preset.model_dump()
|
||||
tomli_w.dump(data, open(self.presets_path, "wb"))
|
||||
```
|
||||
|
||||
### In `src/ai_client.py`
|
||||
|
||||
```python
|
||||
def send(self, request: AIRequest) -> AIResponse:
|
||||
"""Sends a request. AIRequest is defined in models.py."""
|
||||
```
|
||||
|
||||
### In `src/multi_agent_conductor.py`
|
||||
|
||||
```python
|
||||
def load_track(self, track_id: str) -> Track:
|
||||
tickets = parse_plan_md(plan_path)
|
||||
return Track(
|
||||
track_id=track_id,
|
||||
title=...,
|
||||
tickets=tickets,
|
||||
plan_path=str(plan_path),
|
||||
created_at=time.time(),
|
||||
)
|
||||
```
|
||||
**Do NOT** add new `src/<thing>.py` files without explicit user authorization — see `AGENTS.md` "File Size and Naming Convention" HARD RULE.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
## V2 Capability Matrix
|
||||
|
||||
Models are tested for:
|
||||
- **Round-trip serialization** (`to_toml` → `from_toml` → equal)
|
||||
- **Validation** (invalid values rejected)
|
||||
- **Default values** (all fields have sensible defaults)
|
||||
- **Field types** (TypeScript-like strict checking via `pydantic`)
|
||||
`VendorCapabilities` is defined in `src/ai_client.py` under `#region: Vendor Capabilities (moved from src/vendor_capabilities.py)`. The dataclass has 12 v2 fields on top of the v1 fields:
|
||||
|
||||
Tests live in `tests/test_models.py` and module-specific test files (e.g., `tests/test_preset_manager.py` exercises the `Preset` model).
|
||||
**V1 fields:** `vision`, `tool_calling`, `caching`, `streaming`, `model_discovery`, `context_window`, `cost_tracking`
|
||||
|
||||
**V2 fields:** `local`, `reasoning`, `structured_output`, `code_execution`, `web_search`, `x_search`, `file_search`, `mcp_support`, `audio`, `video`, `grounding`, `computer_use`
|
||||
|
||||
All v2 fields default to `False`. The dataclass is `frozen=True`; per-vendor entries use `register()` at module-import time in `src/ai_client.py`. The GUI reads the matrix via `get_capabilities(vendor, model)` and adapts UI elements accordingly (see [guide_ai_client.md §V2 Capability Matrix](guide_ai_client.md#v2-capability-matrix-phase-4)).
|
||||
|
||||
**Adding a new v2 field:** the HARD RULE is that all AI-client code lives in `src/ai_client.py`. New v2 fields go in the `VendorCapabilities` dataclass in `src/ai_client.py` (the region that replaced the deleted `src/vendor_capabilities.py`) — NOT in a new `src/<v2_thing>.py` file. Update the dataclass, populate per-model in `_VENDOR_REGISTRY` via `register()`, add a small rendering helper in `src/gui_2.py` if the field warrants a UI badge.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Model
|
||||
|
||||
1. Add the model to the appropriate region block in `src/models.py`.
|
||||
2. Add validators if any fields have constraints.
|
||||
3. Add a docstring with `[C: ...]` (callers) and `[M: ...]` (mutators) SDM tags.
|
||||
4. If the model is persisted, write a `to_<format>()` / `from_<format>()` pair in the relevant manager.
|
||||
5. Add tests in `tests/test_models.py` (round-trip + validation).
|
||||
6. Update `docs/guide_models.md` (this file) to document the new model.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## PROVIDERS Constant (Location Change 2026-06-11)
|
||||
|
||||
The `PROVIDERS` list was moved from `src/models.py` to `src/ai_client.py:56` per the AGENTS.md HARD RULE (no new `src/<thing>.py` files; system code lives in the system module).
|
||||
|
||||
**Current location**: `src/ai_client.py` (import as `from src.ai_client import PROVIDERS`)
|
||||
|
||||
**Backward compat**: `src/models.py:261-264` has a PEP 562 `__getattr__` that re-exports `PROVIDERS` via lazy import. This breaks the circular dependency where `src/ai_client.py:50` imports `ToolPreset` from `src/models.py` (a top-level `from src.ai_client import PROVIDERS` in `models.py` would deadlock).
|
||||
|
||||
**Audit**: `scripts/audit_providers_source_of_truth.py` fails if `PROVIDERS` is declared as a literal in `src/models.py`.
|
||||
|
||||
The 4 internal import sites were updated in commit `6c6a4aef`:
|
||||
- `src/app_controller.py:3093`
|
||||
- `src/gui_2.py:2293, 2849, 5377`
|
||||
|
||||
---
|
||||
|
||||
## V2 Capability Matrix (Added 2026-06-11)
|
||||
|
||||
`src/vendor_capabilities.py` defines the `VendorCapabilities` dataclass (NOT in `src/models.py` — it's in its own file because it's not a "data model" but a "capability registry"). The dataclass was extended with 12 v2 fields:
|
||||
|
||||
**V1 fields** (unchanged from parent track):
|
||||
- `vision`, `tool_calling`, `caching`, `streaming`, `model_discovery`, `context_window`, `cost_tracking`
|
||||
|
||||
**V2 fields** (added in `qwen_llama_grok_followup_20260611` Phase 4):
|
||||
- `local` — backend is on-device (Ollama, etc.)
|
||||
- `reasoning` — model supports `thinking` / reasoning traces
|
||||
- `structured_output` — model supports JSON / tool-use output
|
||||
- `code_execution` — model can run code (server-side)
|
||||
- `web_search` — model can do live web search
|
||||
- `x_search` — X/Twitter search (grok-specific)
|
||||
- `file_search` — model has a file_search tool (Anthropic)
|
||||
- `mcp_support` — model supports the Model Context Protocol
|
||||
- `audio` — model accepts audio input
|
||||
- `video` — model accepts video input
|
||||
- `grounding` — model supports grounding (gemini)
|
||||
- `computer_use` — model can drive a computer (Anthropic claude-3.5+)
|
||||
|
||||
All v2 fields default to `False`. The dataclass is `frozen=True`; per-vendor entries use `register()` at module-import time. The GUI reads the matrix via `get_capabilities(vendor, model)` and adapts 9+ UI elements accordingly (see [guide_ai_client.md §V2 Capability Matrix](guide_ai_client.md#v2-capability-matrix-phase-4)).
|
||||
|
||||
**Adding a new v2 field**: The HARD RULE is that all AI-client code lives in `src/ai_client.py`. New v2 fields go in `src/vendor_capabilities.py` (existing file) — NOT in a new `src/<v2_thing>.py` file. Update the dataclass, populate per-model in the registry, add a small rendering helper in `src/gui_2.py` (e.g., `_render_v2_capability_badges` for the existing 11 v2 fields).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
- **[guide_architecture.md](guide_architecture.md)** — How models flow through the system
|
||||
- **[guide_app_controller.md](guide_app_controller.md)** — `AppState` and controller-owned models
|
||||
- **[guide_mma.md](guide_mma.md)** — `Ticket`, `Track`, `WorkerContext` usage in MMA
|
||||
- **[guide_mma.md](guide_mma.md)** — `Ticket`, `Track`, `WorkerContext`, `TrackMetadata` in `src/mma.py`
|
||||
- **[guide_ai_client.md](guide_ai_client.md)** — `VendorCapabilities`, `PROVIDERS`, `_VENDOR_REGISTRY` in `src/ai_client.py`
|
||||
- **[guide_personas.md](guide_personas.md)** — `Persona` model in detail
|
||||
- **[guide_workspace_profiles.md](guide_workspace_profiles.md)** — `WorkspaceProfile` model in detail
|
||||
- **[guide_rag.md](guide_rag.md)** — `RAGConfig`, `RAGChunk`, `RAGResult` models
|
||||
- **[guide_context_aggregation.md](guide_context_aggregation.md)** — How the `FileItem` and `ContextPreset` schemas flow through the `aggregate.py` pipeline
|
||||
- **[guide_discussions.md](guide_discussions.md)** — The entry dict shape (`{role, content, collapsed, ts, ...}`) consumed by `parse_history_entries`
|
||||
- **`src/presets.py`, `src/personas.py`, `src/context_presets.py`, `src/tool_presets.py`** — Managers that use these models
|
||||
- **`src/multi_agent_conductor.py`** — Uses `Ticket`, `Track`, `WorkerContext`
|
||||
- **[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
|
||||
- **`src/ai_client.py`** — Uses `Provider`, `ModelInfo`, `AIRequest`, `AIResponse`
|
||||
- **[guide_discussions.md](guide_discussions.md)** — The entry dict shape consumed by `parse_history_entries`
|
||||
- **`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
|
||||
Reference in New Issue
Block a user