*/ use HasFactory; protected static function newFactory(): AgentSessionFactory { return AgentSessionFactory::new(); } protected $fillable = [ 'workspace_id', 'agent_api_key_id', 'agent_plan_id', 'session_id', 'agent_type', 'status', 'context_summary', 'work_log', 'artifacts', 'handoff_notes', 'final_summary', 'started_at', 'last_active_at', 'ended_at', ]; protected $casts = [ 'context_summary' => 'array', 'work_log' => 'array', 'artifacts' => 'array', 'handoff_notes' => 'array', 'started_at' => 'datetime', 'last_active_at' => 'datetime', 'ended_at' => 'datetime', ]; // Status constants public const STATUS_ACTIVE = 'active'; public const STATUS_PAUSED = 'paused'; public const STATUS_COMPLETED = 'completed'; public const STATUS_FAILED = 'failed'; // Agent types public const AGENT_OPUS = 'opus'; public const AGENT_SONNET = 'sonnet'; public const AGENT_HAIKU = 'haiku'; // Relationships public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function plan(): BelongsTo { return $this->belongsTo(AgentPlan::class, 'agent_plan_id'); } public function apiKey(): BelongsTo { return $this->belongsTo(AgentApiKey::class, 'agent_api_key_id'); } // Scopes public function scopeActive($query) { return $query->where('status', self::STATUS_ACTIVE); } public function scopeForPlan($query, AgentPlan|int $plan) { $planId = $plan instanceof AgentPlan ? $plan->id : $plan; return $query->where('agent_plan_id', $planId); } // Factory public static function start(?AgentPlan $plan = null, ?string $agentType = null, ?Workspace $workspace = null): self { $workspaceId = $workspace?->id ?? $plan?->workspace_id; return static::create([ 'workspace_id' => $workspaceId, 'agent_plan_id' => $plan?->id, 'session_id' => 'sess_'.Uuid::uuid4()->toString(), 'agent_type' => $agentType, 'status' => self::STATUS_ACTIVE, 'work_log' => [], 'artifacts' => [], 'started_at' => now(), 'last_active_at' => now(), ]); } // Status helpers public function isActive(): bool { return $this->status === self::STATUS_ACTIVE; } public function isPaused(): bool { return $this->status === self::STATUS_PAUSED; } public function isEnded(): bool { return in_array($this->status, [self::STATUS_COMPLETED, self::STATUS_FAILED]); } // Actions public function touchActivity(): self { $this->update(['last_active_at' => now()]); return $this; } public function pause(): self { $this->update(['status' => self::STATUS_PAUSED]); return $this; } public function resume(): self { $this->update([ 'status' => self::STATUS_ACTIVE, 'last_active_at' => now(), ]); return $this; } public function complete(?string $summary = null): self { $this->update([ 'status' => self::STATUS_COMPLETED, 'final_summary' => $summary, 'ended_at' => now(), ]); return $this; } public function fail(?string $reason = null): self { $this->update([ 'status' => self::STATUS_FAILED, 'final_summary' => $reason, 'ended_at' => now(), ]); return $this; } // Work log public function logAction(string $action, ?array $details = null): self { $log = $this->work_log ?? []; $log[] = [ 'action' => $action, 'details' => $details, 'timestamp' => now()->toIso8601String(), ]; $this->update([ 'work_log' => $log, 'last_active_at' => now(), ]); return $this; } /** * Add a typed work log entry. */ public function addWorkLogEntry(string $message, string $type = 'info', array $data = []): self { $log = $this->work_log ?? []; $log[] = [ 'message' => $message, 'type' => $type, // info, warning, error, success, checkpoint 'data' => $data, 'timestamp' => now()->toIso8601String(), ]; $this->update([ 'work_log' => $log, 'last_active_at' => now(), ]); return $this; } /** * End the session with a status. */ public function end(string $status, ?string $summary = null): self { $validStatuses = [self::STATUS_COMPLETED, self::STATUS_FAILED]; if (! in_array($status, $validStatuses)) { $status = self::STATUS_COMPLETED; } $this->update([ 'status' => $status, 'final_summary' => $summary, 'ended_at' => now(), ]); return $this; } public function getRecentActions(int $limit = 10): array { $log = $this->work_log ?? []; return array_slice(array_reverse($log), 0, $limit); } // Artifacts public function addArtifact(string $path, string $action = 'modified', ?array $metadata = null): self { $artifacts = $this->artifacts ?? []; $artifacts[] = [ 'path' => $path, 'action' => $action, // created, modified, deleted 'metadata' => $metadata, 'timestamp' => now()->toIso8601String(), ]; $this->update(['artifacts' => $artifacts]); return $this; } public function getArtifactsByAction(string $action): array { $artifacts = $this->artifacts ?? []; return array_filter($artifacts, fn ($a) => ($a['action'] ?? '') === $action); } // Context summary public function updateContextSummary(array $summary): self { $this->update(['context_summary' => $summary]); return $this; } public function addToContext(string $key, mixed $value): self { $context = $this->context_summary ?? []; $context[$key] = $value; $this->update(['context_summary' => $context]); return $this; } // Handoff public function prepareHandoff( string $summary, array $nextSteps = [], array $blockers = [], array $contextForNext = [] ): self { $this->update([ 'handoff_notes' => [ 'summary' => $summary, 'next_steps' => $nextSteps, 'blockers' => $blockers, 'context_for_next' => $contextForNext, ], 'status' => self::STATUS_PAUSED, ]); return $this; } public function getHandoffContext(): array { $context = [ 'session_id' => $this->session_id, 'agent_type' => $this->agent_type, 'started_at' => $this->started_at?->toIso8601String(), 'last_active_at' => $this->last_active_at?->toIso8601String(), 'context_summary' => $this->context_summary, 'recent_actions' => $this->getRecentActions(20), 'artifacts' => $this->artifacts, 'handoff_notes' => $this->handoff_notes, ]; if ($this->plan) { $context['plan'] = [ 'slug' => $this->plan->slug, 'title' => $this->plan->title, 'current_phase' => $this->plan->current_phase, 'progress' => $this->plan->getProgress(), ]; } return $context; } // Replay functionality /** * Get the replay context - reconstructs session state from work log. * * This provides the data needed to resume/replay a session by analysing * the work log entries to understand what was done and what state the * session was in. */ public function getReplayContext(): array { $workLog = $this->work_log ?? []; $artifacts = $this->artifacts ?? []; // Extract checkpoints from work log $checkpoints = array_values(array_filter( $workLog, fn ($entry) => ($entry['type'] ?? '') === 'checkpoint' )); // Get the last checkpoint if any $lastCheckpoint = ! empty($checkpoints) ? end($checkpoints) : null; // Extract decisions made during the session $decisions = array_values(array_filter( $workLog, fn ($entry) => ($entry['type'] ?? '') === 'decision' )); // Extract errors encountered $errors = array_values(array_filter( $workLog, fn ($entry) => ($entry['type'] ?? '') === 'error' )); // Build a progress summary from the work log $progressSummary = $this->buildProgressSummary($workLog); return [ 'session_id' => $this->session_id, 'status' => $this->status, 'agent_type' => $this->agent_type, 'plan' => $this->plan ? [ 'slug' => $this->plan->slug, 'title' => $this->plan->title, 'current_phase' => $this->plan->current_phase, ] : null, 'started_at' => $this->started_at?->toIso8601String(), 'last_active_at' => $this->last_active_at?->toIso8601String(), 'duration' => $this->getDurationFormatted(), // Reconstructed state 'context_summary' => $this->context_summary, 'progress_summary' => $progressSummary, 'last_checkpoint' => $lastCheckpoint, 'checkpoints' => $checkpoints, 'decisions' => $decisions, 'errors' => $errors, // Artifacts created during session 'artifacts' => $artifacts, 'artifacts_by_action' => [ 'created' => $this->getArtifactsByAction('created'), 'modified' => $this->getArtifactsByAction('modified'), 'deleted' => $this->getArtifactsByAction('deleted'), ], // Recent work for context 'recent_actions' => $this->getRecentActions(20), 'total_actions' => count($workLog), // Handoff state if available 'handoff_notes' => $this->handoff_notes, 'final_summary' => $this->final_summary, ]; } /** * Build a progress summary from work log entries. */ protected function buildProgressSummary(array $workLog): array { if (empty($workLog)) { return [ 'completed_steps' => 0, 'last_action' => null, 'summary' => 'No work recorded', ]; } $lastEntry = end($workLog); $checkpointCount = count(array_filter($workLog, fn ($e) => ($e['type'] ?? '') === 'checkpoint')); $errorCount = count(array_filter($workLog, fn ($e) => ($e['type'] ?? '') === 'error')); return [ 'completed_steps' => count($workLog), 'checkpoint_count' => $checkpointCount, 'error_count' => $errorCount, 'last_action' => $lastEntry['action'] ?? $lastEntry['message'] ?? 'Unknown', 'last_action_at' => $lastEntry['timestamp'] ?? null, 'summary' => sprintf( '%d actions recorded, %d checkpoints, %d errors', count($workLog), $checkpointCount, $errorCount ), ]; } /** * Create a new session that continues from this one (replay). * * This creates a fresh session with the context from this session, * allowing an agent to pick up where this session left off. */ public function createReplaySession(?string $agentType = null): self { $replayContext = $this->getReplayContext(); $newSession = static::create([ 'workspace_id' => $this->workspace_id, 'agent_plan_id' => $this->agent_plan_id, 'session_id' => 'ses_replay_'.now()->format('Ymd_His').'_'.substr(md5((string) $this->id), 0, 8), 'agent_type' => $agentType ?? $this->agent_type, 'status' => self::STATUS_ACTIVE, 'started_at' => now(), 'last_active_at' => now(), 'context_summary' => [ 'replayed_from' => $this->session_id, 'original_started_at' => $this->started_at?->toIso8601String(), 'original_status' => $this->status, 'inherited_context' => $this->context_summary, 'replay_checkpoint' => $replayContext['last_checkpoint'], 'original_progress' => $replayContext['progress_summary'], ], 'work_log' => [ [ 'message' => sprintf('Replayed from session %s', $this->session_id), 'type' => 'info', 'data' => [ 'original_session' => $this->session_id, 'original_actions' => $replayContext['total_actions'], 'original_checkpoints' => count($replayContext['checkpoints']), ], 'timestamp' => now()->toIso8601String(), ], ], 'artifacts' => [], 'handoff_notes' => $this->handoff_notes, ]); return $newSession; } // Duration helpers public function getDuration(): ?int { if (! $this->started_at) { return null; } $end = $this->ended_at ?? now(); return (int) $this->started_at->diffInMinutes($end); } public function getDurationFormatted(): string { $minutes = $this->getDuration(); if ($minutes === null) { return 'Unknown'; } if ($minutes < 60) { return "{$minutes}m"; } $hours = floor($minutes / 60); $mins = $minutes % 60; return "{$hours}h {$mins}m"; } // Output public function toMcpContext(): array { return [ 'session_id' => $this->session_id, 'agent_type' => $this->agent_type, 'status' => $this->status, 'workspace_id' => $this->workspace_id, 'plan_slug' => $this->plan?->slug, 'started_at' => $this->started_at?->toIso8601String(), 'last_active_at' => $this->last_active_at?->toIso8601String(), 'ended_at' => $this->ended_at?->toIso8601String(), 'duration' => $this->getDurationFormatted(), 'action_count' => count($this->work_log ?? []), 'artifact_count' => count($this->artifacts ?? []), 'context_summary' => $this->context_summary, 'handoff_notes' => $this->handoff_notes, ]; } }