cd828e5267
Per spec FR3/FR4 + Phase 3.1: the MMA domain dataclasses move to their own module: - ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata, TrackState, EMPTY_TRACK_STATE - TrackMetadata is the renamed (was 'Metadata' dataclass in models.py; renamed to avoid collision with the Metadata type alias = dict[str, Any]) src/models.py: - Removed class definitions for ThinkingSegment, Ticket, Track, WorkerContext, Metadata, TrackState, EMPTY_TRACK_STATE - Added backward-compat re-exports so existing 'from src.models import Ticket' continues to work - Metadata alias kept for the dataclass name (was confusingly shadowing the type alias) TrackState's metadata field reverts to the original 'default_factory=dict' pattern (intentionally not auto-constructing TrackMetadata) to preserve the pre-existing behavior where accessing state.metadata.id on a missing state.toml throws AttributeError, which project_manager.get_all_tracks catches and falls through to metadata.json loading. This was a 'bug-on-purpose' that the test test_get_all_tracks_with_metadata_json relies on. Verification: 136 tests pass across mma_models, conductor_engine_v2, dag_engine, ticket_queue, track_state_schema, thinking_gui, manual_block, pipeline_pause, phase6_engine, parallel_execution, run_worker_lifecycle_abort, spawn_interception, persona_id, conductor_engine_abort, conductor_tech_lead, execution_engine, perf_dag, per_ticket_model, metadata_promotion_phase1, thinking_persistence, progress_viz, gui_progress, mma_ticket_actions, headless_verification, context_pruner, orchestration_logic, project_manager_tracks, track_state_persistence.
227 lines
6.8 KiB
Python
227 lines
6.8 KiB
Python
"""MMA (Multi-Model Architecture) core data structures.
|
|
|
|
Per module_taxonomy_refactor_20260627 Phase 3.1, the MMA Core (ThinkingSegment,
|
|
Ticket, Track, WorkerContext, TrackMetadata, TrackState) moved from
|
|
src/models.py to this module. The data domain is the ticket/track lifecycle
|
|
that drives the 4-Tier MMA execution.
|
|
|
|
The boundary wire schema `Metadata` (TypeAlias = dict[str, Any]) is
|
|
NOT defined here; it lives in src/type_aliases.py. This module's
|
|
TrackMetadata dataclass is the *typed* counterpart used for Track-level
|
|
metadata (id/name/status/timestamps).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional
|
|
|
|
from src.type_aliases import Metadata
|
|
|
|
|
|
@dataclass
|
|
class ThinkingSegment:
|
|
content: str
|
|
marker: str
|
|
|
|
def to_dict(self) -> Metadata:
|
|
return {"content": self.content, "marker": self.marker}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Metadata) -> "ThinkingSegment":
|
|
return cls(content=data["content"], marker=data["marker"])
|
|
|
|
|
|
@dataclass
|
|
class Ticket:
|
|
id: str
|
|
description: str
|
|
target_symbols: List[str] = field(default_factory=list)
|
|
context_requirements: List[str] = field(default_factory=list)
|
|
depends_on: List[str] = field(default_factory=list)
|
|
status: str = "todo"
|
|
assigned_to: str = "unassigned"
|
|
priority: str = "medium"
|
|
target_file: Optional[str] = None
|
|
blocked_reason: Optional[str] = None
|
|
step_mode: bool = False
|
|
retry_count: int = 0
|
|
manual_block: bool = False
|
|
model_override: Optional[str] = None
|
|
persona_id: Optional[str] = None
|
|
|
|
def mark_blocked(self, reason: str) -> None:
|
|
self.status = "blocked"
|
|
self.blocked_reason = reason
|
|
|
|
def mark_manual_block(self, reason: str) -> None:
|
|
self.status = "blocked"
|
|
self.blocked_reason = f"[MANUAL] {reason}"
|
|
self.manual_block = True
|
|
|
|
def clear_manual_block(self) -> None:
|
|
if self.manual_block:
|
|
self.status = "todo"
|
|
self.blocked_reason = None
|
|
self.manual_block = False
|
|
|
|
def mark_complete(self) -> None:
|
|
self.status = "completed"
|
|
|
|
def to_dict(self) -> Metadata:
|
|
return {
|
|
"id": self.id,
|
|
"description": self.description,
|
|
"status": self.status,
|
|
"assigned_to": self.assigned_to,
|
|
"priority": self.priority,
|
|
"target_file": self.target_file,
|
|
"target_symbols": self.target_symbols,
|
|
"context_requirements": self.context_requirements,
|
|
"depends_on": self.depends_on,
|
|
"blocked_reason": self.blocked_reason,
|
|
"step_mode": self.step_mode,
|
|
"retry_count": self.retry_count,
|
|
"manual_block": self.manual_block,
|
|
"model_override": self.model_override,
|
|
"persona_id": self.persona_id,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Metadata) -> "Ticket":
|
|
return cls(
|
|
id = data["id"],
|
|
description = data.get("description", ""),
|
|
status = data.get("status", "todo"),
|
|
assigned_to = data.get("assigned_to", "unassigned"),
|
|
priority = data.get("priority", "medium"),
|
|
target_file = data.get("target_file"),
|
|
target_symbols = data.get("target_symbols", []),
|
|
context_requirements = data.get("context_requirements", []),
|
|
depends_on = data.get("depends_on", []),
|
|
blocked_reason = data.get("blocked_reason"),
|
|
step_mode = data.get("step_mode", False),
|
|
retry_count = data.get("retry_count", 0),
|
|
manual_block = data.get("manual_block", False),
|
|
model_override = data.get("model_override"),
|
|
persona_id = data.get("persona_id"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Track:
|
|
id: str
|
|
description: str
|
|
tickets: List["Ticket"] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> Metadata:
|
|
return {
|
|
"id": self.id,
|
|
"description": self.description,
|
|
"tickets": [t.to_dict() for t in self.tickets],
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Metadata) -> "Track":
|
|
return cls(
|
|
id = data["id"],
|
|
description = data.get("description", ""),
|
|
tickets = [Ticket.from_dict(t) for t in data.get("tickets", [])],
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class WorkerContext:
|
|
ticket_id: str
|
|
model_name: str
|
|
messages: list[Metadata] = field(default_factory=list)
|
|
tool_preset: Optional[str] = None
|
|
persona_id: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class TrackMetadata:
|
|
id: str
|
|
name: str
|
|
status: Optional[str] = None
|
|
created_at: Optional[datetime.datetime] = None
|
|
updated_at: Optional[datetime.datetime] = None
|
|
|
|
def to_dict(self) -> Metadata:
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"status": self.status,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Metadata) -> "TrackMetadata":
|
|
created = data.get("created_at")
|
|
updated = data.get("updated_at")
|
|
if isinstance(created, str):
|
|
try:
|
|
created = datetime.datetime.fromisoformat(created)
|
|
except ValueError:
|
|
created = None
|
|
if isinstance(updated, str):
|
|
try:
|
|
updated = datetime.datetime.fromisoformat(updated)
|
|
except ValueError:
|
|
updated = None
|
|
return cls(
|
|
id = data["id"],
|
|
name = data.get("name", ""),
|
|
status = data.get("status"),
|
|
created_at = created,
|
|
updated_at = updated,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TrackState:
|
|
metadata: Metadata = field(default_factory=dict)
|
|
discussion: List[Metadata] = field(default_factory=list)
|
|
tasks: List["Ticket"] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> Metadata:
|
|
serialized_discussion = []
|
|
for item in self.discussion:
|
|
if isinstance(item, dict):
|
|
new_item = dict(item)
|
|
if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime):
|
|
new_item["ts"] = new_item["ts"].isoformat()
|
|
serialized_discussion.append(new_item)
|
|
else:
|
|
serialized_discussion.append(item)
|
|
return {
|
|
"metadata": self.metadata.to_dict(),
|
|
"discussion": serialized_discussion,
|
|
"tasks": [t.to_dict() for t in self.tasks],
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Metadata) -> "TrackState":
|
|
discussion = data.get("discussion", [])
|
|
parsed_discussion = []
|
|
for item in discussion:
|
|
if isinstance(item, dict):
|
|
new_item = dict(item)
|
|
ts = new_item.get("ts")
|
|
if isinstance(ts, str):
|
|
try:
|
|
new_item["ts"] = datetime.datetime.fromisoformat(ts)
|
|
except ValueError:
|
|
pass
|
|
parsed_discussion.append(new_item)
|
|
else:
|
|
parsed_discussion.append(item)
|
|
return cls(
|
|
metadata = TrackMetadata.from_dict(data["metadata"]),
|
|
discussion = parsed_discussion,
|
|
tasks = [Ticket.from_dict(t) for t in data.get("tasks", [])],
|
|
)
|
|
|
|
|
|
EMPTY_TRACK_STATE: TrackState = TrackState() |