From ff24898cd4a822a5c57dc9ddc10212cd5d1ec802 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 04:03:12 +0000 Subject: [PATCH] feat(session): persist handoff notes on end Co-Authored-By: Virgil --- php/Actions/Session/EndSession.php | 12 ++++-- php/Controllers/Api/SessionController.php | 10 ++++- php/Mcp/Tools/Agent/Session/SessionEnd.php | 5 +++ php/Models/AgentSession.php | 3 +- php/Services/AgentSessionService.php | 9 ++++- php/tests/Feature/SessionControllerTest.php | 45 +++++++++++++++++++++ 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 php/tests/Feature/SessionControllerTest.php diff --git a/php/Actions/Session/EndSession.php b/php/Actions/Session/EndSession.php index d27c8ee..64b8e8f 100644 --- a/php/Actions/Session/EndSession.php +++ b/php/Actions/Session/EndSession.php @@ -16,10 +16,11 @@ use Core\Mod\Agentic\Models\AgentSession; use Core\Mod\Agentic\Services\AgentSessionService; /** - * End an agent session with a final status and optional summary. + * End an agent session with a final status, summary, and optional handoff notes. * * Usage: * $session = EndSession::run('ses_abc123', 'completed', 'All phases done'); + * $session = EndSession::run('ses_abc123', 'handed_off', 'Ready for review', ['summary' => 'Ready for review']); */ class EndSession { @@ -32,7 +33,12 @@ class EndSession /** * @throws \InvalidArgumentException */ - public function handle(string $sessionId, string $status, ?string $summary = null): AgentSession + public function handle( + string $sessionId, + string $status, + ?string $summary = null, + ?array $handoffNotes = null, + ): AgentSession { if ($sessionId === '') { throw new \InvalidArgumentException('session_id is required'); @@ -45,7 +51,7 @@ class EndSession ); } - $session = $this->sessionService->end($sessionId, $status, $summary); + $session = $this->sessionService->end($sessionId, $status, $summary, $handoffNotes); if (! $session) { throw new \InvalidArgumentException("Session not found: {$sessionId}"); diff --git a/php/Controllers/Api/SessionController.php b/php/Controllers/Api/SessionController.php index 956beff..f7c7266 100644 --- a/php/Controllers/Api/SessionController.php +++ b/php/Controllers/Api/SessionController.php @@ -122,16 +122,24 @@ class SessionController extends Controller $validated = $request->validate([ 'status' => 'required|string|in:completed,handed_off,paused,failed', 'summary' => 'nullable|string|max:10000', + 'handoff_notes' => 'nullable|array', ]); try { - $session = EndSession::run($id, $validated['status'], $validated['summary'] ?? null); + $session = EndSession::run( + $id, + $validated['status'], + $validated['summary'] ?? null, + $validated['handoff_notes'] ?? null, + ); return response()->json([ 'data' => [ 'session_id' => $session->session_id, 'status' => $session->status, 'duration' => $session->getDurationFormatted(), + 'final_summary' => $session->final_summary, + 'handoff_notes' => $session->handoff_notes, ], ]); } catch (\InvalidArgumentException $e) { diff --git a/php/Mcp/Tools/Agent/Session/SessionEnd.php b/php/Mcp/Tools/Agent/Session/SessionEnd.php index 34f57e5..ca66882 100644 --- a/php/Mcp/Tools/Agent/Session/SessionEnd.php +++ b/php/Mcp/Tools/Agent/Session/SessionEnd.php @@ -40,6 +40,10 @@ class SessionEnd extends AgentTool 'type' => 'string', 'description' => 'Final summary', ], + 'handoff_notes' => [ + 'type' => 'object', + 'description' => 'Optional handoff details for the next agent', + ], ], 'required' => ['status'], ]; @@ -57,6 +61,7 @@ class SessionEnd extends AgentTool $sessionId, $args['status'] ?? '', $args['summary'] ?? null, + $args['handoff_notes'] ?? null, ); return $this->success([ diff --git a/php/Models/AgentSession.php b/php/Models/AgentSession.php index bd657de..ca1144d 100644 --- a/php/Models/AgentSession.php +++ b/php/Models/AgentSession.php @@ -243,7 +243,7 @@ class AgentSession extends Model /** * End the session with a status. */ - public function end(string $status, ?string $summary = null): self + public function end(string $status, ?string $summary = null, ?array $handoffNotes = null): self { $validStatuses = [self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_HANDED_OFF]; @@ -254,6 +254,7 @@ class AgentSession extends Model $this->update([ 'status' => $status, 'final_summary' => $summary, + 'handoff_notes' => $handoffNotes ?? $this->handoff_notes, 'ended_at' => now(), ]); diff --git a/php/Services/AgentSessionService.php b/php/Services/AgentSessionService.php index 721e6c6..ae936f4 100644 --- a/php/Services/AgentSessionService.php +++ b/php/Services/AgentSessionService.php @@ -125,7 +125,12 @@ class AgentSessionService /** * End a session. */ - public function end(string $sessionId, string $status = AgentSession::STATUS_COMPLETED, ?string $summary = null): ?AgentSession + public function end( + string $sessionId, + string $status = AgentSession::STATUS_COMPLETED, + ?string $summary = null, + ?array $handoffNotes = null + ): ?AgentSession { $session = $this->get($sessionId); @@ -133,7 +138,7 @@ class AgentSessionService return null; } - $session->end($status, $summary); + $session->end($status, $summary, $handoffNotes); // Remove from active cache $this->clearCachedSession($session); diff --git a/php/tests/Feature/SessionControllerTest.php b/php/tests/Feature/SessionControllerTest.php new file mode 100644 index 0000000..3c5bbe8 --- /dev/null +++ b/php/tests/Feature/SessionControllerTest.php @@ -0,0 +1,45 @@ +create(); + $session = AgentSession::factory()->active()->create([ + 'workspace_id' => $workspace->id, + ]); + + $request = Request::create('/v1/sessions/'.$session->session_id.'/end', 'POST', [ + 'status' => 'handed_off', + 'summary' => 'Ready for review', + 'handoff_notes' => [ + 'summary' => 'Ready for review', + 'next_steps' => ['Run the verifier'], + 'blockers' => ['Need approval'], + 'context_for_next' => ['repo' => 'core/go-io'], + ], + ]); + $request->attributes->set('workspace', $workspace); + + $response = app(SessionController::class)->end($request, $session->session_id); + + expect($response->getStatusCode())->toBe(200); + + $payload = $response->getData(true); + + expect($payload['data']['session_id'])->toBe($session->session_id) + ->and($payload['data']['status'])->toBe(AgentSession::STATUS_HANDED_OFF) + ->and($payload['data']['final_summary'])->toBe('Ready for review') + ->and($payload['data']['handoff_notes']['summary'])->toBe('Ready for review') + ->and($payload['data']['handoff_notes']['next_steps'])->toBe(['Run the verifier']); + + $fresh = $session->fresh(); + + expect($fresh?->status)->toBe(AgentSession::STATUS_HANDED_OFF) + ->and($fresh?->final_summary)->toBe('Ready for review') + ->and($fresh?->handoff_notes['context_for_next'])->toBe(['repo' => 'core/go-io']); +});