#!/usr/bin/env python3 """ Sub-Agent Context Management - Intelligent task context propagation Features: 1. Parent task context injection into sub-agents 2. Sub-agent discovery and sibling awareness 3. 9-phase flow execution for context understanding 4. Context preservation across execution boundaries 5. Sub-agent coordination and messaging """ import json from pathlib import Path from typing import Dict, List, Optional, Any, Set, Tuple from datetime import datetime from dataclasses import dataclass, asdict, field import hashlib import uuid @dataclass class FlowPhase: """A single phase of the 9-phase unified flow""" phase_name: str # CONTEXT_PREP, RECEIVED, PREDICTING, ANALYZING, CONSENSUS_CHECK, etc. status: str # pending, in_progress, completed, failed description: str = "" output: Optional[str] = None error: Optional[str] = None started_at: Optional[str] = None completed_at: Optional[str] = None duration_seconds: Optional[float] = None @dataclass class SubAgentContext: """Context passed from parent task to sub-agents""" parent_task_id: str parent_project: str parent_description: str sub_agent_id: str created_at: str parent_context: Dict[str, Any] = field(default_factory=dict) parent_tags: List[str] = field(default_factory=list) parent_metadata: Dict[str, Any] = field(default_factory=dict) phase_progression: List[FlowPhase] = field(default_factory=list) sibling_agents: Set[str] = field(default_factory=set) coordination_messages: List[Dict[str, Any]] = field(default_factory=list) class SubAgentContextManager: """Manages sub-agent context propagation and coordination""" def __init__(self, context_dir: Optional[Path] = None): """Initialize sub-agent context manager Args: context_dir: Directory to store sub-agent context """ self.context_dir = context_dir or Path("/tmp/.luzia-sub-agents") self.context_dir.mkdir(parents=True, exist_ok=True) self.active_contexts: Dict[str, SubAgentContext] = {} self.parent_tasks: Dict[str, List[str]] = {} # parent_id -> [sub_agent_ids] self.sibling_graph: Dict[str, Set[str]] = {} # agent_id -> sibling_ids self.load_contexts() def create_sub_agent_context( self, parent_task_id: str, parent_project: str, parent_description: str, parent_context: Optional[Dict[str, Any]] = None, parent_tags: Optional[List[str]] = None, parent_metadata: Optional[Dict[str, Any]] = None, ) -> SubAgentContext: """Create context for a new sub-agent Args: parent_task_id: ID of parent task parent_project: Parent project name parent_description: Parent task description parent_context: Parent task context data parent_tags: Tags from parent task parent_metadata: Additional metadata from parent Returns: SubAgentContext for the new sub-agent """ sub_agent_id = str(uuid.uuid4()) now = datetime.utcnow().isoformat() context = SubAgentContext( parent_task_id=parent_task_id, parent_project=parent_project, parent_description=parent_description, sub_agent_id=sub_agent_id, created_at=now, parent_context=parent_context or {}, parent_tags=parent_tags or [], parent_metadata=parent_metadata or {}, ) # Initialize 9-phase progression phases = [ "CONTEXT_PREP", "RECEIVED", "PREDICTING", "ANALYZING", "CONSENSUS_CHECK", "AWAITING_APPROVAL", "STRATEGIZING", "EXECUTING", "LEARNING", ] context.phase_progression = [ FlowPhase(phase_name=phase, status="pending") for phase in phases ] # Register this sub-agent self.active_contexts[sub_agent_id] = context if parent_task_id not in self.parent_tasks: self.parent_tasks[parent_task_id] = [] self.parent_tasks[parent_task_id].append(sub_agent_id) # Discover siblings for this agent if parent_task_id in self.parent_tasks: siblings = set(self.parent_tasks[parent_task_id]) - {sub_agent_id} context.sibling_agents = siblings self.sibling_graph[sub_agent_id] = siblings # Register reverse sibling relationship for sibling_id in siblings: if sibling_id in self.sibling_graph: self.sibling_graph[sibling_id].add(sub_agent_id) else: self.sibling_graph[sibling_id] = {sub_agent_id} self.save_context(context) return context def get_sub_agent_context(self, sub_agent_id: str) -> Optional[SubAgentContext]: """Retrieve context for a sub-agent Args: sub_agent_id: ID of sub-agent Returns: SubAgentContext if found, None otherwise """ if sub_agent_id in self.active_contexts: return self.active_contexts[sub_agent_id] # Try loading from disk context_file = self.context_dir / f"{sub_agent_id}.json" if context_file.exists(): try: data = json.loads(context_file.read_text()) context = self._dict_to_context(data) self.active_contexts[sub_agent_id] = context return context except Exception as e: print(f"[Error] Failed to load context for {sub_agent_id}: {e}") return None def update_phase( self, sub_agent_id: str, phase_name: str, status: str, output: Optional[str] = None, error: Optional[str] = None, ) -> bool: """Update phase status for a sub-agent Args: sub_agent_id: ID of sub-agent phase_name: Name of phase to update status: New status (pending, in_progress, completed, failed) output: Phase output/results error: Error message if failed Returns: True if successful, False otherwise """ context = self.get_sub_agent_context(sub_agent_id) if not context: return False for phase in context.phase_progression: if phase.phase_name == phase_name: phase.status = status phase.output = output phase.error = error if status == "in_progress": phase.started_at = datetime.utcnow().isoformat() elif status in ["completed", "failed"]: phase.completed_at = datetime.utcnow().isoformat() if phase.started_at: start = datetime.fromisoformat(phase.started_at) end = datetime.fromisoformat(phase.completed_at) phase.duration_seconds = (end - start).total_seconds() break self.save_context(context) return True def get_current_phase(self, sub_agent_id: str) -> Optional[str]: """Get current active phase for a sub-agent Args: sub_agent_id: ID of sub-agent Returns: Current phase name or None """ context = self.get_sub_agent_context(sub_agent_id) if not context: return None # Return first in_progress phase, or first pending if none in progress for phase in context.phase_progression: if phase.status == "in_progress": return phase.phase_name for phase in context.phase_progression: if phase.status == "pending": return phase.phase_name return None def get_phase_progression(self, sub_agent_id: str) -> List[FlowPhase]: """Get full phase progression for a sub-agent Args: sub_agent_id: ID of sub-agent Returns: List of FlowPhase objects """ context = self.get_sub_agent_context(sub_agent_id) return context.phase_progression if context else [] def send_message_to_sibling( self, from_agent_id: str, to_agent_id: str, message_type: str, content: Dict[str, Any], ) -> bool: """Send coordination message to sibling agent Args: from_agent_id: Sending sub-agent to_agent_id: Receiving sub-agent message_type: Type of message (request, update, result, etc.) content: Message content Returns: True if sent successfully, False otherwise """ context = self.get_sub_agent_context(from_agent_id) if not context or to_agent_id not in context.sibling_agents: return False message = { "from": from_agent_id, "to": to_agent_id, "type": message_type, "content": content, "timestamp": datetime.utcnow().isoformat(), } context.coordination_messages.append(message) self.save_context(context) # Also add to recipient's message log for visibility recipient_context = self.get_sub_agent_context(to_agent_id) if recipient_context: recipient_context.coordination_messages.append(message) self.save_context(recipient_context) return True def get_sibling_agents(self, sub_agent_id: str) -> Set[str]: """Get all sibling agents for a sub-agent Args: sub_agent_id: ID of sub-agent Returns: Set of sibling agent IDs """ context = self.get_sub_agent_context(sub_agent_id) return context.sibling_agents if context else set() def get_parent_task_info( self, sub_agent_id: str ) -> Optional[Tuple[str, str, str]]: """Get parent task information for a sub-agent Args: sub_agent_id: ID of sub-agent Returns: Tuple of (parent_task_id, parent_project, parent_description) or None """ context = self.get_sub_agent_context(sub_agent_id) if context: return (context.parent_task_id, context.parent_project, context.parent_description) return None def get_sub_agents_for_parent(self, parent_task_id: str) -> List[str]: """Get all sub-agents for a parent task Args: parent_task_id: ID of parent task Returns: List of sub-agent IDs """ return self.parent_tasks.get(parent_task_id, []) def mark_sub_agent_complete( self, sub_agent_id: str, final_result: Optional[str] = None ) -> bool: """Mark sub-agent as complete after all phases Args: sub_agent_id: ID of sub-agent final_result: Final result/output Returns: True if marked successfully """ context = self.get_sub_agent_context(sub_agent_id) if not context: return False # Mark all phases as completed for phase in context.phase_progression: if phase.status in ["pending", "in_progress"]: phase.status = "completed" self.save_context(context) return True def save_context(self, context: SubAgentContext) -> None: """Save context to disk Args: context: SubAgentContext to save """ context_file = self.context_dir / f"{context.sub_agent_id}.json" try: data = { **asdict(context), "sibling_agents": list(context.sibling_agents), "phase_progression": [asdict(p) for p in context.phase_progression], } context_file.write_text(json.dumps(data, indent=2)) except Exception as e: print(f"[Error] Failed to save context for {context.sub_agent_id}: {e}") def load_contexts(self) -> None: """Load all contexts from disk""" if self.context_dir.exists(): for context_file in self.context_dir.glob("*.json"): try: data = json.loads(context_file.read_text()) context = self._dict_to_context(data) self.active_contexts[context.sub_agent_id] = context # Rebuild parent task registry parent_id = context.parent_task_id if parent_id not in self.parent_tasks: self.parent_tasks[parent_id] = [] if context.sub_agent_id not in self.parent_tasks[parent_id]: self.parent_tasks[parent_id].append(context.sub_agent_id) # Rebuild sibling graph self.sibling_graph[context.sub_agent_id] = context.sibling_agents except Exception as e: print(f"[Error] Failed to load context {context_file}: {e}") def _dict_to_context(self, data: Dict) -> SubAgentContext: """Convert dict to SubAgentContext""" phases = [ FlowPhase( phase_name=p.get("phase_name", ""), status=p.get("status", "pending"), description=p.get("description", ""), output=p.get("output"), error=p.get("error"), started_at=p.get("started_at"), completed_at=p.get("completed_at"), duration_seconds=p.get("duration_seconds"), ) for p in data.get("phase_progression", []) ] return SubAgentContext( parent_task_id=data.get("parent_task_id", ""), parent_project=data.get("parent_project", ""), parent_description=data.get("parent_description", ""), sub_agent_id=data.get("sub_agent_id", ""), created_at=data.get("created_at", ""), parent_context=data.get("parent_context", {}), parent_tags=data.get("parent_tags", []), parent_metadata=data.get("parent_metadata", {}), phase_progression=phases, sibling_agents=set(data.get("sibling_agents", [])), coordination_messages=data.get("coordination_messages", []), ) def get_context_summary(self, sub_agent_id: str) -> Optional[Dict[str, Any]]: """Get human-readable summary of sub-agent context Args: sub_agent_id: ID of sub-agent Returns: Summary dict or None """ context = self.get_sub_agent_context(sub_agent_id) if not context: return None phase_statuses = [ {"phase": p.phase_name, "status": p.status, "duration": p.duration_seconds} for p in context.phase_progression ] return { "sub_agent_id": sub_agent_id, "parent_task_id": context.parent_task_id, "parent_project": context.parent_project, "parent_description": context.parent_description, "created_at": context.created_at, "sibling_count": len(context.sibling_agents), "siblings": list(context.sibling_agents), "message_count": len(context.coordination_messages), "phase_progression": phase_statuses, "parent_context_keys": list(context.parent_context.keys()), "parent_tags": context.parent_tags, }