From 886461ca285bcce1bb37777b24354d72e5aa125f Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 02:19:57 +0000 Subject: [PATCH] feat(session): expose replay context on read scope Co-Authored-By: Virgil --- php/Mcp/Tools/Agent/Session/SessionReplay.php | 52 ++---------- php/tests/Feature/SessionReplayTest.php | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 php/tests/Feature/SessionReplayTest.php diff --git a/php/Mcp/Tools/Agent/Session/SessionReplay.php b/php/Mcp/Tools/Agent/Session/SessionReplay.php index fe0f46b..8ba9912 100644 --- a/php/Mcp/Tools/Agent/Session/SessionReplay.php +++ b/php/Mcp/Tools/Agent/Session/SessionReplay.php @@ -8,17 +8,16 @@ use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool; use Core\Mod\Agentic\Services\AgentSessionService; /** - * Replay a session by creating a new session with the original's context. + * Get replay context for a stored session. * - * This tool reconstructs the state from a session's work log and creates - * a new active session, allowing an agent to continue from where the - * original session left off. + * This tool reconstructs the state from a session's work log so an agent + * can resume analysis or hand off from a completed session. */ class SessionReplay extends AgentTool { protected string $category = 'session'; - protected array $scopes = ['write']; + protected array $scopes = ['read']; public function name(): string { @@ -27,7 +26,7 @@ class SessionReplay extends AgentTool public function description(): string { - return 'Replay a session - creates a new session with the original\'s reconstructed context from its work log'; + return 'Get replay context for a stored session'; } public function inputSchema(): array @@ -39,14 +38,6 @@ class SessionReplay extends AgentTool 'type' => 'string', 'description' => 'Session ID to replay from', ], - 'agent_type' => [ - 'type' => 'string', - 'description' => 'Agent type for the new session (defaults to original session\'s agent type)', - ], - 'context_only' => [ - 'type' => 'boolean', - 'description' => 'If true, only return the replay context without creating a new session', - ], ], 'required' => ['session_id'], ]; @@ -60,41 +51,16 @@ class SessionReplay extends AgentTool return $this->error($e->getMessage()); } - $agentType = $this->optional($args, 'agent_type'); - $contextOnly = $this->optional($args, 'context_only', false); - - return $this->withCircuitBreaker('agentic', function () use ($sessionId, $agentType, $contextOnly) { + return $this->withCircuitBreaker('agentic', function () use ($sessionId) { $sessionService = app(AgentSessionService::class); + $replayContext = $sessionService->getReplayContext($sessionId); - // If only context requested, return the replay context - if ($contextOnly) { - $replayContext = $sessionService->getReplayContext($sessionId); - - if (! $replayContext) { - return $this->error("Session not found: {$sessionId}"); - } - - return $this->success([ - 'replay_context' => $replayContext, - ]); - } - - // Create a new replay session - $newSession = $sessionService->replay($sessionId, $agentType); - - if (! $newSession) { + if (! $replayContext) { return $this->error("Session not found: {$sessionId}"); } return $this->success([ - 'session' => [ - 'session_id' => $newSession->session_id, - 'agent_type' => $newSession->agent_type, - 'status' => $newSession->status, - 'plan' => $newSession->plan?->slug, - ], - 'replayed_from' => $sessionId, - 'context_summary' => $newSession->context_summary, + 'replay_context' => $replayContext, ]); }, fn () => $this->error('Agentic service temporarily unavailable.', 'service_unavailable')); } diff --git a/php/tests/Feature/SessionReplayTest.php b/php/tests/Feature/SessionReplayTest.php new file mode 100644 index 0000000..fdbafcd --- /dev/null +++ b/php/tests/Feature/SessionReplayTest.php @@ -0,0 +1,85 @@ +workspace = Workspace::factory()->create(); +}); + +it('returns replay context for a stored session', function () { + $session = AgentSession::create([ + 'workspace_id' => $this->workspace->id, + 'session_id' => 'ses_test_replay', + 'agent_type' => 'opus', + 'status' => AgentSession::STATUS_FAILED, + 'context_summary' => ['goal' => 'Investigate replay'], + 'work_log' => [ + [ + 'message' => 'Reached parser step', + 'type' => 'checkpoint', + 'data' => ['step' => 2], + 'timestamp' => now()->subMinutes(20)->toIso8601String(), + ], + [ + 'message' => 'Chose retry path', + 'type' => 'decision', + 'data' => ['path' => 'retry'], + 'timestamp' => now()->subMinutes(10)->toIso8601String(), + ], + [ + 'message' => 'Vector store timeout', + 'type' => 'error', + 'data' => ['service' => 'qdrant'], + 'timestamp' => now()->subMinutes(5)->toIso8601String(), + ], + ], + 'artifacts' => [ + [ + 'path' => 'README.md', + 'action' => 'modified', + 'metadata' => ['bytes' => 128], + 'timestamp' => now()->subMinutes(8)->toIso8601String(), + ], + ], + 'started_at' => now()->subHour(), + 'last_active_at' => now()->subMinutes(5), + 'ended_at' => now()->subMinutes(1), + ]); + + $tool = new SessionReplay; + $result = $tool->handle(['session_id' => $session->session_id]); + + expect($result)->toBeArray() + ->and($result['success'])->toBeTrue() + ->and($result)->toHaveKey('replay_context'); + + $context = $result['replay_context']; + + expect($context['session_id'])->toBe($session->session_id) + ->and($context['last_checkpoint']['message'])->toBe('Reached parser step') + ->and($context['decisions'])->toHaveCount(1) + ->and($context['errors'])->toHaveCount(1) + ->and($context['progress_summary']['checkpoint_count'])->toBe(1) + ->and($context['artifacts_by_action']['modified'])->toHaveCount(1); +}); + +it('declares read scope', function () { + $tool = new SessionReplay; + + expect($tool->requiredScopes())->toBe(['read']); +}); + +it('returns an error for an unknown session', function () { + $tool = new SessionReplay; + $result = $tool->handle(['session_id' => 'missing-session']); + + expect($result)->toBeArray() + ->and($result['error'])->toBe('Session not found: missing-session'); +});